Merge pull request #1 from lilleyse/master

Initial code for OBJ to glTF converter
This commit is contained in:
Sean Lilley 2015-10-18 23:54:27 -04:00
commit 82698ba8e4
13 changed files with 2019 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
**/node_modules
.DS_Store
Thumbs.db

7
LICENSE.md Normal file
View File

@ -0,0 +1,7 @@
Copyright 2015 Analytical Graphics, Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -1,2 +1,25 @@
# OBJ2GLTF
Convert OBJ assets to glTF
Convert OBJ models to glTF
## Getting Started
Install [Node.js](https://nodejs.org/en/) if you don't already have it, clone this repo, and then:
```
cd OBJ2GLTF
npm install
```
Run `node ./bin/obj2gltf.js` and pass it the path to an OBJ file.
## Limitations
This tool is still in development. We plan on adding additional features like gzip compression, binary glTF export, normal generation, and a testing suite.
Pull requests are appreciated. Please use the same [Contributor License Agreement (CLA)](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/CONTRIBUTING.md) used for [Cesium](http://cesiumjs.org/).
---
Developed by the Cesium team.
<p align="center">
<a href="http://cesiumjs.org/"><img src="doc/cesium.png" /></a>
</p>

61
bin/obj2gltf.js Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env node
"use strict";
var fs = require('fs');
var path = require('path');
var argv = require('minimist')(process.argv.slice(2));
var parseObj = require('../lib/obj');
var createGltf = require('../lib/gltf');
var util = require('../lib/util');
var defined = util.defined;
var defaultValue = util.defaultValue;
// TODO : assuming all paths in obj are relative, but should consider absolute paths
// TODO : add command line flag for y-up to z-up
// TODO : support zlib
// TODO : support binary export
// TODO : generate normals if they don't exist
if (process.argv.length < 3 || defined(argv.h) || defined(argv.help)) {
console.log('Usage: ./bin/obj2gltf.js [INPUT] [OPTIONS]\n');
console.log(' -i, --input Path to obj file');
console.log(' -o, --output Directory or filename for the exported glTF file');
console.log(' -b, --binary Output binary glTF');
console.log(' -c --combine Combine glTF resources into a single file');
console.log(' -h, --help Display this help');
process.exit(0);
}
var objFile = defaultValue(argv._[0], defaultValue(argv.i, argv.input));
var outputPath = defaultValue(defaultValue(argv.o, argv.output));
var binary = defaultValue(defaultValue(argv.b, argv.binary), false);
var combine = defaultValue(defaultValue(argv.c, argv.combine), false);
if (!defined(objFile)) {
console.error('-i or --input argument is required. See --help for details.');
process.exit(1);
}
if (!defined(outputPath)) {
outputPath = path.dirname(objFile);
}
var inputPath = path.dirname(objFile);
var modelName = path.basename(objFile, '.obj');
var outputIsGltf = /.gltf$/.test(outputPath);
if (outputIsGltf) {
modelName = path.basename(outputPath, '.gltf');
outputPath = path.dirname(outputPath);
}
fs.mkdir(outputPath, function(){
console.time('Total');
console.time('Parse Obj');
parseObj(objFile, inputPath, function(data) {
console.timeEnd('Parse Obj');
console.time('Create glTF');
createGltf(data, modelName, inputPath, outputPath, binary, combine, function() {
console.timeEnd('Create glTF');
console.timeEnd('Total');
});
});
});

BIN
doc/cesium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

300
lib/WebGLConstants.js Normal file
View File

@ -0,0 +1,300 @@
"use strict";
module.exports = {
DEPTH_BUFFER_BIT : 0x00000100,
STENCIL_BUFFER_BIT : 0x00000400,
COLOR_BUFFER_BIT : 0x00004000,
POINTS : 0x0000,
LINES : 0x0001,
LINE_LOOP : 0x0002,
LINE_STRIP : 0x0003,
TRIANGLES : 0x0004,
TRIANGLE_STRIP : 0x0005,
TRIANGLE_FAN : 0x0006,
ZERO : 0,
ONE : 1,
SRC_COLOR : 0x0300,
ONE_MINUS_SRC_COLOR : 0x0301,
SRC_ALPHA : 0x0302,
ONE_MINUS_SRC_ALPHA : 0x0303,
DST_ALPHA : 0x0304,
ONE_MINUS_DST_ALPHA : 0x0305,
DST_COLOR : 0x0306,
ONE_MINUS_DST_COLOR : 0x0307,
SRC_ALPHA_SATURATE : 0x0308,
FUNC_ADD : 0x8006,
BLEND_EQUATION : 0x8009,
BLEND_EQUATION_RGB : 0x8009, /* same as BLEND_EQUATION */
BLEND_EQUATION_ALPHA : 0x883D,
FUNC_SUBTRACT : 0x800A,
FUNC_REVERSE_SUBTRACT : 0x800B,
BLEND_DST_RGB : 0x80C8,
BLEND_SRC_RGB : 0x80C9,
BLEND_DST_ALPHA : 0x80CA,
BLEND_SRC_ALPHA : 0x80CB,
CONSTANT_COLOR : 0x8001,
ONE_MINUS_CONSTANT_COLOR : 0x8002,
CONSTANT_ALPHA : 0x8003,
ONE_MINUS_CONSTANT_ALPHA : 0x8004,
BLEND_COLOR : 0x8005,
ARRAY_BUFFER : 0x8892,
ELEMENT_ARRAY_BUFFER : 0x8893,
ARRAY_BUFFER_BINDING : 0x8894,
ELEMENT_ARRAY_BUFFER_BINDING : 0x8895,
STREAM_DRAW : 0x88E0,
STATIC_DRAW : 0x88E4,
DYNAMIC_DRAW : 0x88E8,
BUFFER_SIZE : 0x8764,
BUFFER_USAGE : 0x8765,
CURRENT_VERTEX_ATTRIB : 0x8626,
FRONT : 0x0404,
BACK : 0x0405,
FRONT_AND_BACK : 0x0408,
CULL_FACE : 0x0B44,
BLEND : 0x0BE2,
DITHER : 0x0BD0,
STENCIL_TEST : 0x0B90,
DEPTH_TEST : 0x0B71,
SCISSOR_TEST : 0x0C11,
POLYGON_OFFSET_FILL : 0x8037,
SAMPLE_ALPHA_TO_COVERAGE : 0x809E,
SAMPLE_COVERAGE : 0x80A0,
NO_ERROR : 0,
INVALID_ENUM : 0x0500,
INVALID_VALUE : 0x0501,
INVALID_OPERATION : 0x0502,
OUT_OF_MEMORY : 0x0505,
CW : 0x0900,
CCW : 0x0901,
LINE_WIDTH : 0x0B21,
ALIASED_POINT_SIZE_RANGE : 0x846D,
ALIASED_LINE_WIDTH_RANGE : 0x846E,
CULL_FACE_MODE : 0x0B45,
FRONT_FACE : 0x0B46,
DEPTH_RANGE : 0x0B70,
DEPTH_WRITEMASK : 0x0B72,
DEPTH_CLEAR_VALUE : 0x0B73,
DEPTH_FUNC : 0x0B74,
STENCIL_CLEAR_VALUE : 0x0B91,
STENCIL_FUNC : 0x0B92,
STENCIL_FAIL : 0x0B94,
STENCIL_PASS_DEPTH_FAIL : 0x0B95,
STENCIL_PASS_DEPTH_PASS : 0x0B96,
STENCIL_REF : 0x0B97,
STENCIL_VALUE_MASK : 0x0B93,
STENCIL_WRITEMASK : 0x0B98,
STENCIL_BACK_FUNC : 0x8800,
STENCIL_BACK_FAIL : 0x8801,
STENCIL_BACK_PASS_DEPTH_FAIL : 0x8802,
STENCIL_BACK_PASS_DEPTH_PASS : 0x8803,
STENCIL_BACK_REF : 0x8CA3,
STENCIL_BACK_VALUE_MASK : 0x8CA4,
STENCIL_BACK_WRITEMASK : 0x8CA5,
VIEWPORT : 0x0BA2,
SCISSOR_BOX : 0x0C10,
COLOR_CLEAR_VALUE : 0x0C22,
COLOR_WRITEMASK : 0x0C23,
UNPACK_ALIGNMENT : 0x0CF5,
PACK_ALIGNMENT : 0x0D05,
MAX_TEXTURE_SIZE : 0x0D33,
MAX_VIEWPORT_DIMS : 0x0D3A,
SUBPIXEL_BITS : 0x0D50,
RED_BITS : 0x0D52,
GREEN_BITS : 0x0D53,
BLUE_BITS : 0x0D54,
ALPHA_BITS : 0x0D55,
DEPTH_BITS : 0x0D56,
STENCIL_BITS : 0x0D57,
POLYGON_OFFSET_UNITS : 0x2A00,
POLYGON_OFFSET_FACTOR : 0x8038,
TEXTURE_BINDING_2D : 0x8069,
SAMPLE_BUFFERS : 0x80A8,
SAMPLES : 0x80A9,
SAMPLE_COVERAGE_VALUE : 0x80AA,
SAMPLE_COVERAGE_INVERT : 0x80AB,
COMPRESSED_TEXTURE_FORMATS : 0x86A3,
DONT_CARE : 0x1100,
FASTEST : 0x1101,
NICEST : 0x1102,
GENERATE_MIPMAP_HINT : 0x8192,
BYTE : 0x1400,
UNSIGNED_BYTE : 0x1401,
SHORT : 0x1402,
UNSIGNED_SHORT : 0x1403,
INT : 0x1404,
UNSIGNED_INT : 0x1405,
FLOAT : 0x1406,
DEPTH_COMPONENT : 0x1902,
ALPHA : 0x1906,
RGB : 0x1907,
RGBA : 0x1908,
LUMINANCE : 0x1909,
LUMINANCE_ALPHA : 0x190A,
UNSIGNED_SHORT_4_4_4_4 : 0x8033,
UNSIGNED_SHORT_5_5_5_1 : 0x8034,
UNSIGNED_SHORT_5_6_5 : 0x8363,
FRAGMENT_SHADER : 0x8B30,
VERTEX_SHADER : 0x8B31,
MAX_VERTEX_ATTRIBS : 0x8869,
MAX_VERTEX_UNIFORM_VECTORS : 0x8DFB,
MAX_VARYING_VECTORS : 0x8DFC,
MAX_COMBINED_TEXTURE_IMAGE_UNITS : 0x8B4D,
MAX_VERTEX_TEXTURE_IMAGE_UNITS : 0x8B4C,
MAX_TEXTURE_IMAGE_UNITS : 0x8872,
MAX_FRAGMENT_UNIFORM_VECTORS : 0x8DFD,
SHADER_TYPE : 0x8B4F,
DELETE_STATUS : 0x8B80,
LINK_STATUS : 0x8B82,
VALIDATE_STATUS : 0x8B83,
ATTACHED_SHADERS : 0x8B85,
ACTIVE_UNIFORMS : 0x8B86,
ACTIVE_ATTRIBUTES : 0x8B89,
SHADING_LANGUAGE_VERSION : 0x8B8C,
CURRENT_PROGRAM : 0x8B8D,
NEVER : 0x0200,
LESS : 0x0201,
EQUAL : 0x0202,
LEQUAL : 0x0203,
GREATER : 0x0204,
NOTEQUAL : 0x0205,
GEQUAL : 0x0206,
ALWAYS : 0x0207,
KEEP : 0x1E00,
REPLACE : 0x1E01,
INCR : 0x1E02,
DECR : 0x1E03,
INVERT : 0x150A,
INCR_WRAP : 0x8507,
DECR_WRAP : 0x8508,
VENDOR : 0x1F00,
RENDERER : 0x1F01,
VERSION : 0x1F02,
NEAREST : 0x2600,
LINEAR : 0x2601,
NEAREST_MIPMAP_NEAREST : 0x2700,
LINEAR_MIPMAP_NEAREST : 0x2701,
NEAREST_MIPMAP_LINEAR : 0x2702,
LINEAR_MIPMAP_LINEAR : 0x2703,
TEXTURE_MAG_FILTER : 0x2800,
TEXTURE_MIN_FILTER : 0x2801,
TEXTURE_WRAP_S : 0x2802,
TEXTURE_WRAP_T : 0x2803,
TEXTURE_2D : 0x0DE1,
TEXTURE : 0x1702,
TEXTURE_CUBE_MAP : 0x8513,
TEXTURE_BINDING_CUBE_MAP : 0x8514,
TEXTURE_CUBE_MAP_POSITIVE_X : 0x8515,
TEXTURE_CUBE_MAP_NEGATIVE_X : 0x8516,
TEXTURE_CUBE_MAP_POSITIVE_Y : 0x8517,
TEXTURE_CUBE_MAP_NEGATIVE_Y : 0x8518,
TEXTURE_CUBE_MAP_POSITIVE_Z : 0x8519,
TEXTURE_CUBE_MAP_NEGATIVE_Z : 0x851A,
MAX_CUBE_MAP_TEXTURE_SIZE : 0x851C,
TEXTURE0 : 0x84C0,
TEXTURE1 : 0x84C1,
TEXTURE2 : 0x84C2,
TEXTURE3 : 0x84C3,
TEXTURE4 : 0x84C4,
TEXTURE5 : 0x84C5,
TEXTURE6 : 0x84C6,
TEXTURE7 : 0x84C7,
TEXTURE8 : 0x84C8,
TEXTURE9 : 0x84C9,
TEXTURE10 : 0x84CA,
TEXTURE11 : 0x84CB,
TEXTURE12 : 0x84CC,
TEXTURE13 : 0x84CD,
TEXTURE14 : 0x84CE,
TEXTURE15 : 0x84CF,
TEXTURE16 : 0x84D0,
TEXTURE17 : 0x84D1,
TEXTURE18 : 0x84D2,
TEXTURE19 : 0x84D3,
TEXTURE20 : 0x84D4,
TEXTURE21 : 0x84D5,
TEXTURE22 : 0x84D6,
TEXTURE23 : 0x84D7,
TEXTURE24 : 0x84D8,
TEXTURE25 : 0x84D9,
TEXTURE26 : 0x84DA,
TEXTURE27 : 0x84DB,
TEXTURE28 : 0x84DC,
TEXTURE29 : 0x84DD,
TEXTURE30 : 0x84DE,
TEXTURE31 : 0x84DF,
ACTIVE_TEXTURE : 0x84E0,
REPEAT : 0x2901,
CLAMP_TO_EDGE : 0x812F,
MIRRORED_REPEAT : 0x8370,
FLOAT_VEC2 : 0x8B50,
FLOAT_VEC3 : 0x8B51,
FLOAT_VEC4 : 0x8B52,
INT_VEC2 : 0x8B53,
INT_VEC3 : 0x8B54,
INT_VEC4 : 0x8B55,
BOOL : 0x8B56,
BOOL_VEC2 : 0x8B57,
BOOL_VEC3 : 0x8B58,
BOOL_VEC4 : 0x8B59,
FLOAT_MAT2 : 0x8B5A,
FLOAT_MAT3 : 0x8B5B,
FLOAT_MAT4 : 0x8B5C,
SAMPLER_2D : 0x8B5E,
SAMPLER_CUBE : 0x8B60,
VERTEX_ATTRIB_ARRAY_ENABLED : 0x8622,
VERTEX_ATTRIB_ARRAY_SIZE : 0x8623,
VERTEX_ATTRIB_ARRAY_STRIDE : 0x8624,
VERTEX_ATTRIB_ARRAY_TYPE : 0x8625,
VERTEX_ATTRIB_ARRAY_NORMALIZED : 0x886A,
VERTEX_ATTRIB_ARRAY_POINTER : 0x8645,
VERTEX_ATTRIB_ARRAY_BUFFER_BINDING : 0x889F,
IMPLEMENTATION_COLOR_READ_TYPE : 0x8B9A,
IMPLEMENTATION_COLOR_READ_FORMAT : 0x8B9B,
COMPILE_STATUS : 0x8B81,
LOW_FLOAT : 0x8DF0,
MEDIUM_FLOAT : 0x8DF1,
HIGH_FLOAT : 0x8DF2,
LOW_INT : 0x8DF3,
MEDIUM_INT : 0x8DF4,
HIGH_INT : 0x8DF5,
FRAMEBUFFER : 0x8D40,
RENDERBUFFER : 0x8D41,
RGBA4 : 0x8056,
RGB5_A1 : 0x8057,
RGB565 : 0x8D62,
DEPTH_COMPONENT16 : 0x81A5,
STENCIL_INDEX : 0x1901,
STENCIL_INDEX8 : 0x8D48,
DEPTH_STENCIL : 0x84F9,
RENDERBUFFER_WIDTH : 0x8D42,
RENDERBUFFER_HEIGHT : 0x8D43,
RENDERBUFFER_INTERNAL_FORMAT : 0x8D44,
RENDERBUFFER_RED_SIZE : 0x8D50,
RENDERBUFFER_GREEN_SIZE : 0x8D51,
RENDERBUFFER_BLUE_SIZE : 0x8D52,
RENDERBUFFER_ALPHA_SIZE : 0x8D53,
RENDERBUFFER_DEPTH_SIZE : 0x8D54,
RENDERBUFFER_STENCIL_SIZE : 0x8D55,
FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE : 0x8CD0,
FRAMEBUFFER_ATTACHMENT_OBJECT_NAME : 0x8CD1,
FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL : 0x8CD2,
FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE : 0x8CD3,
COLOR_ATTACHMENT0 : 0x8CE0,
DEPTH_ATTACHMENT : 0x8D00,
STENCIL_ATTACHMENT : 0x8D20,
DEPTH_STENCIL_ATTACHMENT : 0x821A,
NONE : 0,
FRAMEBUFFER_COMPLETE : 0x8CD5,
FRAMEBUFFER_INCOMPLETE_ATTACHMENT : 0x8CD6,
FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT : 0x8CD7,
FRAMEBUFFER_INCOMPLETE_DIMENSIONS : 0x8CD9,
FRAMEBUFFER_UNSUPPORTED : 0x8CDD,
FRAMEBUFFER_BINDING : 0x8CA6,
RENDERBUFFER_BINDING : 0x8CA7,
MAX_RENDERBUFFER_SIZE : 0x84E8,
INVALID_FRAMEBUFFER_OPERATION : 0x0506,
UNPACK_FLIP_Y_WEBGL : 0x9240,
UNPACK_PREMULTIPLY_ALPHA_WEBGL : 0x9241,
CONTEXT_LOST_WEBGL : 0x9242,
UNPACK_COLORSPACE_CONVERSION_WEBGL : 0x9243,
BROWSER_DEFAULT_WEBGL : 0x9244
};

435
lib/gltf.js Normal file
View File

@ -0,0 +1,435 @@
"use strict";
var fs = require('fs');
var fsExtra = require('fs-extra');
var path = require('path');
var async = require('async');
var zlib = require('zlib');
var util = require('./util');
var defined = util.defined;
var defaultValue = util.defaultValue;
var imageInfo = require('./image');
var WebGLConstants = require('./WebGLConstants');
var modelMaterialsCommon = require('./modelMaterialsCommon');
module.exports = createGltf;
function getImages(inputPath, outputPath, combine, materials, done) {
var images = [];
for (var name in materials) {
if (materials.hasOwnProperty(name)) {
var material = materials[name];
for (var property in material) {
if (material.hasOwnProperty(property)) {
var image = material[property];
if (typeof image === 'string') {
images.push(image);
}
}
}
}
}
var imagesInfo = {};
async.each(images, function (image, callback) {
var imagePath = path.join(inputPath, image);
var copyPath = path.join(outputPath, path.basename(image));
imageInfo(imagePath, function(info) {
var uri;
if (combine) {
uri = 'data:application/octet-stream;base64,' + info.data.toString('base64');
} else {
uri = image;
}
imagesInfo[image] = {
transparent : info.transparent,
channels : info.channels,
uri : uri
};
fsExtra.copy(imagePath, copyPath, function (err) {
if (err) {
throw err;
}
callback();
});
});
}, function (err) {
if (err) {
throw err;
}
done(imagesInfo);
});
}
function createGltf(data, modelName, inputPath, outputPath, binary, combine, done) {
var vertexCount = data.vertexCount;
var vertexArray = data.vertexArray;
var positionMin = data.positionMin;
var positionMax = data.positionMax;
var hasUVs = data.hasUVs;
var materialGroups = data.materialGroups;
var materials = data.materials;
getImages(inputPath, outputPath, combine, materials, function(images) {
var i, j, name;
var sizeOfFloat32 = 4;
var sizeOfUint32 = 4;
var sizeOfUint16 = 2;
var indexComponentType;
var indexComponentSize;
// Reserve the 65535 index for primitive restart
if (vertexCount < 65535) {
indexComponentType = WebGLConstants.UNSIGNED_SHORT;
indexComponentSize = sizeOfUint16;
} else {
indexComponentType = WebGLConstants.UNSIGNED_INT;
indexComponentSize = sizeOfUint32;
}
// Create primitives
var primitives = [];
var indexArrayLength = 0;
var indexArray;
var indexCount;
for (name in materialGroups) {
if (materialGroups.hasOwnProperty(name)) {
indexArray = materialGroups[name];
indexCount = indexArray.length;
primitives.push({
indexArray : indexArray,
indexOffset : indexArrayLength,
indexCount : indexCount,
material : name
});
indexArrayLength += indexCount;
}
}
// Create buffer to store vertex and index data
var indexArrayByteLength = indexArrayLength * indexComponentSize;
var vertexArrayLength = vertexArray.length; // In floats
var vertexArrayByteLength = vertexArrayLength * sizeOfFloat32;
var bufferByteLength = vertexArrayByteLength + indexArrayByteLength;
var buffer = new Buffer(bufferByteLength);
// Write vertex data
var byteOffset = 0;
for (i = 0; i < vertexArrayLength; ++i) {
buffer.writeFloatLE(vertexArray[i], byteOffset);
byteOffset += sizeOfFloat32;
}
// Write index data
var primitivesLength = primitives.length;
for (i = 0; i < primitivesLength; ++i) {
indexArray = primitives[i].indexArray;
indexCount = indexArray.length;
for (j = 0; j < indexCount; ++j) {
if (indexComponentSize === sizeOfUint16) {
buffer.writeUInt16LE(indexArray[j], byteOffset);
} else {
buffer.writeUInt32LE(indexArray[j], byteOffset);
}
byteOffset += indexComponentSize;
}
}
var positionByteOffset = 0;
var normalByteOffset = sizeOfFloat32 * 3;
var uvByteOffset = sizeOfFloat32 * 6;
var vertexByteStride = hasUVs ? sizeOfFloat32 * 8 : sizeOfFloat32 * 6;
var binaryRelPath = modelName + '.bin';
var binaryPath = path.join(outputPath, binaryRelPath);
var bufferId = 'buffer_' + modelName;
var bufferViewVertexId = 'bufferView_vertex';
var bufferViewIndexId = 'bufferView_index';
var accessorPositionId = 'accessor_position';
var accessorUVId = 'accessor_uv';
var accessorNormalId = 'accessor_normal';
var meshId = 'mesh_' + modelName;
var sceneId = 'scene_' + modelName;
var nodeId = 'node_' + modelName;
var samplerId = 'sampler_0';
function getAccessorIndexId(i) {
return 'accessor_index_' + i;
}
function getMaterialId(material) {
return 'material_' + material;
}
function getTextureId(image) {
if (!defined(image)) {
return undefined;
}
return 'texture_' + image.substr(0, image.lastIndexOf('.'));
}
function getImageId(image) {
return 'image_' + image.substr(0, image.lastIndexOf('.'));
}
var gltf = {
accessors : {},
asset : {},
buffers : {},
bufferViews : {},
extensionsUsed : ['KHR_materials_common'],
images : {},
materials : {},
meshes : {},
nodes : {},
samplers : {},
scene : sceneId,
scenes : {},
textures : {}
};
gltf.asset = {
"generator": "OBJ2GLTF",
"premultipliedAlpha": true,
"profile": {
"api": "WebGL",
"version": "1.0"
},
"version": 1
};
gltf.scenes[sceneId] = {
nodes : [nodeId]
};
gltf.nodes[nodeId] = {
children : [],
matrix : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
meshes : [meshId],
name : modelName
};
gltf.samplers[samplerId] = {}; // Use default values
var bufferUri;
if (combine) {
bufferUri = 'data:application/octet-stream;base64,' + buffer.toString('base64');
} else {
bufferUri = binaryRelPath;
}
gltf.buffers[bufferId] = {
byteLength : bufferByteLength,
type : 'arraybuffer',
uri : bufferUri
};
gltf.bufferViews[bufferViewVertexId] = {
buffer : bufferId,
byteLength : vertexArrayByteLength,
byteOffset : 0,
target : WebGLConstants.ARRAY_BUFFER
};
gltf.bufferViews[bufferViewIndexId] = {
buffer : bufferId,
byteLength : indexArrayByteLength,
byteOffset : vertexArrayByteLength,
target : WebGLConstants.ELEMENT_ARRAY_BUFFER
};
for (i = 0; i < primitivesLength; ++i) {
var primitive = primitives[i];
gltf.accessors[getAccessorIndexId(i)] = {
bufferView : bufferViewIndexId,
byteOffset : primitive.indexOffset * indexComponentSize,
byteStride : 0,
componentType : indexComponentType,
count : primitive.indexCount,
type : 'SCALAR'
};
}
gltf.accessors[accessorPositionId] = {
bufferView : bufferViewVertexId,
byteOffset : positionByteOffset,
byteStride : vertexByteStride,
componentType : WebGLConstants.FLOAT,
count : vertexCount,
min : positionMin,
max : positionMax,
type : 'VEC3'
};
gltf.accessors[accessorNormalId] = {
bufferView : bufferViewVertexId,
byteOffset : normalByteOffset,
byteStride : vertexByteStride,
componentType : WebGLConstants.FLOAT,
count : vertexCount,
type : 'VEC3'
};
if (hasUVs) {
gltf.accessors[accessorUVId] = {
bufferView : bufferViewVertexId,
byteOffset : uvByteOffset,
byteStride : vertexByteStride,
componentType : WebGLConstants.FLOAT,
count : vertexCount,
type : 'VEC2'
};
}
var gltfPrimitives = [];
gltf.meshes[meshId] = {
name : modelName,
primitives : gltfPrimitives
};
var gltfAttributes = {};
gltfAttributes.POSITION = accessorPositionId;
gltfAttributes.NORMAL = accessorNormalId;
if (hasUVs) {
gltfAttributes.TEXCOORD_0 = accessorUVId;
}
for (i = 0; i < primitivesLength; ++i) {
gltfPrimitives.push({
attributes : gltfAttributes,
indices : getAccessorIndexId(i),
material : getMaterialId(primitives[i].material),
primitive : WebGLConstants.TRIANGLES
});
}
function addTexture(name) {
var imageId = getImageId(name);
var textureId = getTextureId(name);
// Don't add the texture twice
if (defined(gltf.images[imageId])) {
return;
}
var image = images[name];
var format;
var channels = image.channels;
switch (channels) {
case 1:
format = WebGLConstants.ALPHA;
break;
case 2:
format = WebGLConstants.LUMINANCE_ALPHA;
break;
case 3:
format = WebGLConstants.RGB;
break;
case 4:
format = WebGLConstants.RGBA;
break;
}
gltf.images[imageId] = {
uri : image.uri
};
gltf.textures[textureId] = {
format : format,
internalFormat : format,
sampler : samplerId,
source : imageId,
target : WebGLConstants.TEXTURE_2D,
type : WebGLConstants.UNSIGNED_BYTE
};
}
for (i = 0; i < primitivesLength; ++i) {
var materialName = primitives[i].material;
var material = materials[materialName];
var materialId = getMaterialId(materialName);
// Get specular color as combination of specular color and intensity
var specularColor = material.specularColor;
var specularIntensity = material.specularIntensity;
if (defined(specularColor)) {
specularIntensity = defaultValue(specularIntensity, 1.0);
specularColor[0] *= specularIntensity;
specularColor[1] *= specularIntensity;
specularColor[2] *= specularIntensity;
} else if(defined(specularIntensity)){
specularColor = [specularIntensity, specularIntensity, specularIntensity, 1];
} else {
specularColor = [0, 0, 0, 1];
}
var specularMap = defaultValue(material.specularColorMap, material.specularIntensityMap);
var shadingTechnique = 'PHONG'; // or 'BLINN'
if (!defined(specularMap) && (specularColor[0] === 0) && (specularColor[1] === 0) && (specularColor[2] === 0)) {
shadingTechnique = 'LAMBERT';
}
if (defined(material.ambientColorMap)) {
addTexture(material.ambientColorMap);
}
if (defined(material.diffuseColorMap)) {
addTexture(material.diffuseColorMap);
}
if (defined(material.emissionColorMap)) {
addTexture(material.emissionColorMap);
}
if (defined(specularMap)) {
addTexture(specularMap);
}
var values = {
ambient : defaultValue(defaultValue(getTextureId(material.ambientColorMap), material.ambientColor), [0, 0, 0, 1]),
diffuse : defaultValue(defaultValue(getTextureId(material.diffuseColorMap), material.diffuseColor), [0, 0, 0, 1]),
emission : defaultValue(defaultValue(getTextureId(material.emissionColorMap), material.emissionColor), [0, 0, 0, 1]),
specular : defaultValue(getTextureId(specularMap), specularColor),
shininess : defaultValue(material.specularShininess, 0.0),
transparency : defaultValue(material.alpha, 1.0)
};
// If an image is transparent, set transparency to 0.99 to force alpha path
var diffuseColorMap = material.diffuseColorMap;
if (defined(diffuseColorMap) && images[diffuseColorMap].transparent) {
values.transparency = 0.99 * values.transparency;
}
gltf.materials[materialId] = {
name : materialName,
extensions : {
KHR_materials_common: {
technique: shadingTechnique,
values: values
}
}
};
}
// Generate techniques, shaders, and programs
gltf = modelMaterialsCommon(gltf);
// Save .gltf file
var gltfPath = path.join(outputPath, modelName) + '.gltf';
var gltfString = JSON.stringify(gltf, null, 4);
fs.writeFile(gltfPath, gltfString, function(error) {
if (error) {
throw error;
}
if (combine) {
done();
} else {
// Save .bin file
fs.writeFile(binaryPath, buffer, function(error) {
if (error) {
throw error;
}
done();
});
}
});
});
}

44
lib/image.js Normal file
View File

@ -0,0 +1,44 @@
"use strict";
var fs = require('fs');
var path = require('path');
module.exports = imageInfo;
function getChannels(colorType) {
switch (colorType) {
case 0: // greyscale
return 1;
case 2: // RGB
return 3;
case 4: // greyscale + alpha
return 2;
case 6: // RGB + alpha
return 4;
default:
return 3;
}
}
function imageInfo(imagePath, done) {
fs.readFile(imagePath, function(err, data) {
if (err) {
throw(err);
}
var info = {
transparent : false,
channels : 3,
data : data
};
if (path.extname(imagePath) === '.png') {
// Color type is encoded in the 25th bit of the png
var colorType = data[25];
var channels = getChannels(colorType);
info.channels = channels;
info.transparent = (channels === 4);
}
done(info);
});
}

750
lib/modelMaterialsCommon.js Normal file
View File

@ -0,0 +1,750 @@
"use strict";
var WebGLConstants = require('./WebGLConstants');
var util = require('./util');
var defined = util.defined;
var defaultValue = util.defaultValue;
module.exports = modelMaterialsCommon;
function webGLConstantToGlslType(webGLValue) {
switch(webGLValue) {
case WebGLConstants.FLOAT:
return 'float';
case WebGLConstants.FLOAT_VEC2:
return 'vec2';
case WebGLConstants.FLOAT_VEC3:
return 'vec3';
case WebGLConstants.FLOAT_VEC4:
return 'vec4';
case WebGLConstants.FLOAT_MAT2:
return 'mat2';
case WebGLConstants.FLOAT_MAT3:
return 'mat3';
case WebGLConstants.FLOAT_MAT4:
return 'mat4';
case WebGLConstants.SAMPLER_2D:
return 'sampler2D';
}
}
function generateLightParameters(gltf) {
var result = {};
var lights;
if (defined(gltf.extensions) && defined(gltf.extensions.KHR_materials_common)) {
lights = gltf.extensions.KHR_materials_common.lights;
}
if (defined(lights)) {
// Figure out which node references the light
var nodes = gltf.nodes;
for (var nodeName in nodes) {
if (nodes.hasOwnProperty(nodeName)) {
var node = nodes[nodeName];
if (defined(node.extensions) && defined(node.extensions.KHR_materials_common)) {
var nodeLightId = node.extensions.KHR_materials_common.light;
if (defined(nodeLightId) && defined(lights[nodeLightId])) {
lights[nodeLightId].node = nodeName;
}
delete node.extensions.KHR_materials_common;
}
}
}
// Add light parameters to result
var lightCount = 0;
for(var lightName in lights) {
if (lights.hasOwnProperty(lightName)) {
var light = lights[lightName];
var lightType = light.type;
if ((lightType !== 'ambient') && !defined(light.node)) {
delete lights[lightName];
continue;
}
var lightBaseName = 'light' + lightCount.toString();
light.baseName = lightBaseName;
switch(lightType) {
case 'ambient':
var ambient = light.ambient;
result[lightBaseName + 'Color'] = {
type: WebGLConstants.FLOAT_VEC3,
value: ambient.color
};
break;
case 'directional':
var directional = light.directional;
result[lightBaseName + 'Color'] =
{
type: WebGLConstants.FLOAT_VEC3,
value: directional.color
};
if (defined(light.node)) {
result[lightBaseName + 'Transform'] =
{
node: light.node,
semantic: 'MODELVIEW',
type: WebGLConstants.FLOAT_MAT4
};
}
break;
case 'point':
var point = light.point;
result[lightBaseName + 'Color'] =
{
type: WebGLConstants.FLOAT_VEC3,
value: point.color
};
if (defined(light.node)) {
result[lightBaseName + 'Transform'] =
{
node: light.node,
semantic: 'MODELVIEW',
type: WebGLConstants.FLOAT_MAT4
};
}
result[lightBaseName + 'Attenuation'] =
{
type: WebGLConstants.FLOAT_VEC3,
value: [point.constantAttenuation, point.linearAttenuation, point.quadraticAttenuation]
};
break;
case 'spot':
var spot = light.spot;
result[lightBaseName + 'Color'] =
{
type: WebGLConstants.FLOAT_VEC3,
value: spot.color
};
if (defined(light.node)) {
result[lightBaseName + 'Transform'] =
{
node: light.node,
semantic: 'MODELVIEW',
type: WebGLConstants.FLOAT_MAT4
};
result[lightBaseName + 'InverseTransform'] = {
node: light.node,
semantic: 'MODELVIEWINVERSE',
type: WebGLConstants.FLOAT_MAT4,
useInFragment: true
};
}
result[lightBaseName + 'Attenuation'] =
{
type: WebGLConstants.FLOAT_VEC3,
value: [spot.constantAttenuation, spot.linearAttenuation, spot.quadraticAttenuation]
};
result[lightBaseName + 'FallOff'] =
{
type: WebGLConstants.FLOAT_VEC2,
value: [spot.fallOffAngle, spot.fallOffExponent]
};
break;
}
++lightCount;
}
}
}
return result;
}
function getNextId(dictionary, baseName, startingCount) {
var count = defaultValue(startingCount, 0);
var nextId;
do {
nextId = baseName + (count++).toString();
} while(defined(dictionary[nextId]));
return nextId;
}
var techniqueCount = 0;
var vertexShaderCount = 0;
var fragmentShaderCount = 0;
var programCount = 0;
function generateTechnique(gltf, khrMaterialsCommon, attributes, lightParameters, jointCount) {
var techniques = gltf.techniques;
var shaders = gltf.shaders;
var programs = gltf.programs;
attributes = defaultValue(attributes, []);
var attributesCount = attributes.length;
var lightingModel = khrMaterialsCommon.technique.toUpperCase();
var lights;
if (defined(gltf.extensions) && defined(gltf.extensions.KHR_materials_common)) {
lights = gltf.extensions.KHR_materials_common.lights;
}
var hasSkinning = (jointCount > 0);
var values = khrMaterialsCommon.values;
var isDoubleSided = values.doubleSided;
delete values.doubleSided;
var vertexShader = 'precision highp float;\n';
var fragmentShader = 'precision highp float;\n';
// Generate IDs for our new objects
var techniqueId = getNextId(techniques, 'technique', techniqueCount);
var vertexShaderId = getNextId(shaders, 'vertexShader', vertexShaderCount);
var fragmentShaderId = getNextId(shaders, 'fragmentShader', fragmentShaderCount);
var programId = getNextId(programs, 'program', programCount);
// Add techniques
var lowerCase;
var techniqueAttributes = {};
for (var i=0;i<attributesCount;++i) {
lowerCase = attributes[i].toLowerCase();
techniqueAttributes['a_' + lowerCase] = lowerCase;
}
var techniqueParameters = {
// Add matrices
modelViewMatrix: {
semantic: 'MODELVIEW',
type: WebGLConstants.FLOAT_MAT4
},
normalMatrix: {
semantic: 'MODELVIEWINVERSETRANSPOSE',
type: WebGLConstants.FLOAT_MAT3
},
projectionMatrix: {
semantic: 'PROJECTION',
type: WebGLConstants.FLOAT_MAT4
}
};
if (hasSkinning) {
techniqueParameters.jointMatrix = {
count: jointCount,
semantic: 'JOINTMATRIX',
type: WebGLConstants.FLOAT_MAT4
};
}
// Add material parameters
var hasAlpha = false;
var typeValue;
for(var name in values) {
if (values.hasOwnProperty(name)) {
var value = values[name];
var type = typeof value;
typeValue = -1;
switch (type) {
case 'string':
typeValue = WebGLConstants.SAMPLER_2D;
break;
case 'number':
typeValue = WebGLConstants.FLOAT;
if (!hasAlpha && (name === 'transparency')) {
hasAlpha = (value !== 1.0);
}
break;
default:
if (Array.isArray(value)) {
// 35664 (vec2), 35665 (vec3), 35666 (vec4)
typeValue = 35662 + value.length;
if (!hasAlpha && (typeValue === WebGLConstants.FLOAT_VEC4) && (name === 'diffuse')) {
hasAlpha = (value[3] !== 1.0);
}
}
break;
}
if (typeValue > 0) {
lowerCase = name.toLowerCase();
techniqueParameters[lowerCase] = {
type: typeValue
};
}
}
}
// Copy light parameters into technique parameters
if (defined(lightParameters)) {
for (var lightParamName in lightParameters) {
if (lightParameters.hasOwnProperty(lightParamName)) {
techniqueParameters[lightParamName] = lightParameters[lightParamName];
}
}
}
// Generate uniforms object before attributes are added
var techniqueUniforms = {};
for (var paramName in techniqueParameters) {
if (techniqueParameters.hasOwnProperty(paramName)) {
var param = techniqueParameters[paramName];
techniqueUniforms['u_' + paramName] = paramName;
var arraySize = defined(param.count) ? '['+param.count+']' : '';
if (((param.type !== WebGLConstants.FLOAT_MAT3) && (param.type !== WebGLConstants.FLOAT_MAT4)) ||
param.useInFragment) {
fragmentShader += 'uniform ' + webGLConstantToGlslType(param.type) + ' u_' + paramName + arraySize + ';\n';
delete param.useInFragment;
}
else {
vertexShader += 'uniform ' + webGLConstantToGlslType(param.type) + ' u_' + paramName + arraySize + ';\n';
}
}
}
// Add attributes with semantics
var vertexShaderMain = '';
if (hasSkinning) {
vertexShaderMain += ' mat4 skinMat = a_weight.x * u_jointMatrix[int(a_joint.x)];\n';
vertexShaderMain += ' skinMat += a_weight.y * u_jointMatrix[int(a_joint.y)];\n';
vertexShaderMain += ' skinMat += a_weight.z * u_jointMatrix[int(a_joint.z)];\n';
vertexShaderMain += ' skinMat += a_weight.w * u_jointMatrix[int(a_joint.w)];\n';
}
// TODO: Handle multiple texture coordinates for now we just use 1
var v_texcoord;
for (i=0;i<attributesCount;++i) {
var attribute = attributes[i];
typeValue = -1;
if (attribute === 'POSITION') {
typeValue = WebGLConstants.FLOAT_VEC3;
vertexShader += 'attribute vec3 a_position;\n';
vertexShader += 'varying vec3 v_positionEC;\n';
if (hasSkinning) {
vertexShaderMain += ' vec4 pos = u_modelViewMatrix * skinMat * vec4(a_position,1.0);\n';
}
else {
vertexShaderMain += ' vec4 pos = u_modelViewMatrix * vec4(a_position,1.0);\n';
}
vertexShaderMain += ' v_positionEC = pos.xyz;\n';
vertexShaderMain += ' gl_Position = u_projectionMatrix * pos;\n';
fragmentShader += 'varying vec3 v_positionEC;\n';
}
else if (attribute === 'NORMAL') {
typeValue = WebGLConstants.FLOAT_VEC3;
vertexShader += 'attribute vec3 a_normal;\n';
vertexShader += 'varying vec3 v_normal;\n';
if (hasSkinning) {
vertexShaderMain += ' v_normal = u_normalMatrix * mat3(skinMat) * a_normal;\n';
}
else {
vertexShaderMain += ' v_normal = u_normalMatrix * a_normal;\n';
}
fragmentShader += 'varying vec3 v_normal;\n';
}
else if (attribute.indexOf('TEXCOORD') === 0) {
typeValue = WebGLConstants.FLOAT_VEC2;
lowerCase = attribute.toLowerCase();
var a_texcoord = 'a_' + lowerCase;
v_texcoord = 'v_' + lowerCase;
vertexShader += 'attribute vec2 ' + a_texcoord + ';\n';
vertexShader += 'varying vec2 ' + v_texcoord + ';\n';
vertexShaderMain += ' ' + v_texcoord + ' = ' + a_texcoord + ';\n';
fragmentShader += 'varying vec2 ' + v_texcoord + ';\n';
}
else if (attribute === 'JOINT') {
typeValue = WebGLConstants.FLOAT_VEC4;
vertexShader += 'attribute vec4 a_joint;\n';
}
else if (attribute === 'WEIGHT') {
typeValue = WebGLConstants.FLOAT_VEC4;
vertexShader += 'attribute vec4 a_weight;\n';
}
if (typeValue > 0) {
lowerCase = attribute.toLowerCase();
techniqueParameters[lowerCase] = {
semantic: attribute,
type: typeValue
};
}
}
var hasNormals = defined(techniqueParameters.normal);
var hasSpecular = hasNormals && ((lightingModel === 'BLINN') || (lightingModel === 'PHONG')) &&
defined(techniqueParameters.specular) && defined(techniqueParameters.shininess);
// Generate lighting code blocks
var hasNonAmbientLights = false;
var hasAmbientLights = false;
var fragmentLightingBlock = '';
for (var lightName in lights) {
if (lights.hasOwnProperty(lightName)) {
var light = lights[lightName];
var lightType = light.type.toLowerCase();
var lightBaseName = light.baseName;
fragmentLightingBlock += ' {\n';
var lightColorName = 'u_' + lightBaseName + 'Color';
var varyingName;
if(lightType === 'ambient') {
hasAmbientLights = true;
fragmentLightingBlock += ' ambientLight += ' + lightColorName + ';\n';
}
else if (hasNormals && (lightingModel !== 'CONSTANT')) {
hasNonAmbientLights = true;
varyingName = 'v_' + lightBaseName + 'Direction';
vertexShader += 'varying vec3 ' + varyingName + ';\n';
fragmentShader += 'varying vec3 ' + varyingName + ';\n';
if (lightType === 'directional') {
vertexShaderMain += ' ' + varyingName + ' = mat3(u_' + lightBaseName + 'Transform) * vec3(0.,0.,1.);\n';
fragmentLightingBlock += ' float attenuation = 1.0;\n';
}
else {
vertexShaderMain += ' ' + varyingName + ' = u_' + lightBaseName + 'Transform[3].xyz - pos.xyz;\n';
fragmentLightingBlock += ' float range = length(' + varyingName + ');\n';
fragmentLightingBlock += ' float attenuation = 1.0 / (u_' + lightBaseName + 'Attenuation.x + ';
fragmentLightingBlock += '(u_' + lightBaseName + 'Attenuation.y * range) + ';
fragmentLightingBlock += '(u_' + lightBaseName + 'Attenuation.z * range * range));\n';
}
fragmentLightingBlock += ' vec3 l = normalize(' + varyingName + ');\n';
if (lightType === 'spot') {
fragmentLightingBlock += ' vec4 spotPosition = u_' + lightBaseName + 'InverseTransform * vec4(v_positionEC, 1.0);\n';
fragmentLightingBlock += ' float cosAngle = dot(vec3(0.0,0.0,-1.0), normalize(spotPosition.xyz));\n';
fragmentLightingBlock += ' if (cosAngle < cos(u_' + lightBaseName + 'FallOff.x * 0.5))\n';
fragmentLightingBlock += ' {\n';
fragmentLightingBlock += ' attenuation *= max(0.0, pow(cosAngle, u_' + lightBaseName + 'FallOff.y));\n';
}
fragmentLightingBlock += ' diffuseLight += ' + lightColorName + '* max(dot(normal,l), 0.) * attenuation;\n';
if (hasSpecular) {
if (lightingModel === 'BLINN') {
fragmentLightingBlock += ' vec3 h = normalize(l + viewDir);\n';
fragmentLightingBlock += ' float specularIntensity = max(0., pow(max(dot(normal, h), 0.), u_shininess)) * attenuation;\n';
}
else { // PHONG
fragmentLightingBlock += ' vec3 reflectDir = reflect(-l, normal);\n';
fragmentLightingBlock += ' float specularIntensity = max(0., pow(max(dot(reflectDir, viewDir), 0.), u_shininess)) * attenuation;\n';
}
fragmentLightingBlock += ' specularLight += ' + lightColorName + ' * specularIntensity;\n';
}
if (lightType === 'spot') {
fragmentLightingBlock += ' }\n';
}
}
fragmentLightingBlock += ' }\n';
}
}
if (!hasAmbientLights) {
// Add an ambient light if we don't have one
fragmentLightingBlock += ' ambientLight += vec3(0.1, 0.1, 0.1);\n';
}
if (!hasNonAmbientLights) {
fragmentLightingBlock += ' vec3 l = normalize(czm_sunDirectionEC);\n';
fragmentLightingBlock += ' diffuseLight += vec3(1.0, 1.0, 1.0) * max(dot(normal,l), 0.);\n';
if (hasSpecular) {
if (lightingModel === 'BLINN') {
fragmentLightingBlock += ' vec3 h = normalize(l + viewDir);\n';
fragmentLightingBlock += ' float specularIntensity = max(0., pow(max(dot(normal, h), 0.), u_shininess));\n';
}
else { // PHONG
fragmentLightingBlock += ' vec3 reflectDir = reflect(-l, normal);\n';
fragmentLightingBlock += ' float specularIntensity = max(0., pow(max(dot(reflectDir, viewDir), 0.), u_shininess));\n';
}
fragmentLightingBlock += ' specularLight += vec3(1.0, 1.0, 1.0) * specularIntensity;\n';
}
}
vertexShader += 'void main(void) {\n';
vertexShader += vertexShaderMain;
vertexShader += '}\n';
fragmentShader += 'void main(void) {\n';
var colorCreationBlock = ' vec3 color = vec3(0.0, 0.0, 0.0);\n';
if (hasNormals) {
fragmentShader += ' vec3 normal = normalize(v_normal);\n';
if (isDoubleSided) {
fragmentShader += ' if (gl_FrontFacing == false)\n';
fragmentShader += ' {\n';
fragmentShader += ' normal = -normal;\n';
fragmentShader += ' }\n';
}
}
var finalColorComputation;
if (lightingModel !== 'CONSTANT') {
if (defined(techniqueParameters.diffuse)) {
if (techniqueParameters.diffuse.type === WebGLConstants.SAMPLER_2D) {
fragmentShader += ' vec4 diffuse = texture2D(u_diffuse, ' + v_texcoord + ');\n';
}
else {
fragmentShader += ' vec4 diffuse = u_diffuse;\n';
}
fragmentShader += ' vec3 diffuseLight = vec3(0.0, 0.0, 0.0);\n';
colorCreationBlock += ' color += diffuse.rgb * diffuseLight;\n';
}
if (hasSpecular) {
if (techniqueParameters.specular.type === WebGLConstants.SAMPLER_2D) {
fragmentShader += ' vec3 specular = texture2D(u_specular, ' + v_texcoord + ');\n';
}
else {
fragmentShader += ' vec3 specular = u_specular.rgb;\n';
}
fragmentShader += ' vec3 specularLight = vec3(0.0, 0.0, 0.0);\n';
colorCreationBlock += ' color += specular * specularLight;\n';
}
if (defined(techniqueParameters.transparency)) {
finalColorComputation = ' gl_FragColor = vec4(color * diffuse.a, diffuse.a * u_transparency);\n';
}
else {
finalColorComputation = ' gl_FragColor = vec4(color * diffuse.a, diffuse.a);\n';
}
}
else {
if (defined(techniqueParameters.transparency)) {
finalColorComputation = ' gl_FragColor = vec4(color, u_transparency);\n';
}
else {
finalColorComputation = ' gl_FragColor = vec4(color, 1.0);\n';
}
}
if (defined(techniqueParameters.emission)) {
if (techniqueParameters.emission.type === WebGLConstants.SAMPLER_2D) {
fragmentShader += ' vec3 emission = texture2D(u_emission, ' + v_texcoord + ');\n';
}
else {
fragmentShader += ' vec3 emission = u_emission.rgb;\n';
}
colorCreationBlock += ' color += emission;\n';
}
if (defined(techniqueParameters.ambient)) {
if (techniqueParameters.ambient.type === WebGLConstants.SAMPLER_2D) {
fragmentShader += ' vec3 ambient = texture2D(u_ambient, ' + v_texcoord + ');\n';
}
else {
fragmentShader += ' vec3 ambient = u_ambient.rgb;\n';
}
}
else {
fragmentShader += ' vec3 ambient = diffuse.rgb;\n';
}
fragmentShader += ' vec3 viewDir = -normalize(v_positionEC);\n';
fragmentShader += ' vec3 ambientLight = vec3(0.0, 0.0, 0.0);\n';
colorCreationBlock += ' color += ambient * ambientLight;\n';
// Add in light computations
fragmentShader += fragmentLightingBlock;
fragmentShader += colorCreationBlock;
fragmentShader += finalColorComputation;
fragmentShader += '}\n';
// TODO: Handle texture transparency
var techniqueStates;
if (hasAlpha) {
techniqueStates = {
enable: [
WebGLConstants.DEPTH_TEST,
WebGLConstants.BLEND
],
depthMask: false,
functions: {
"blendEquationSeparate": [
WebGLConstants.FUNC_ADD,
WebGLConstants.FUNC_ADD
],
"blendFuncSeparate": [
WebGLConstants.ONE,
WebGLConstants.ONE_MINUS_SRC_ALPHA,
WebGLConstants.ONE,
WebGLConstants.ONE_MINUS_SRC_ALPHA
]
}
};
}
else {
techniqueStates = {
enable: [
WebGLConstants.CULL_FACE,
WebGLConstants.DEPTH_TEST
]
};
}
techniques[techniqueId] = {
attributes: techniqueAttributes,
parameters: techniqueParameters,
program: programId,
states: techniqueStates,
uniforms: techniqueUniforms
};
// Add shaders
shaders[vertexShaderId] = {
type: WebGLConstants.VERTEX_SHADER,
uri: '',
extras: {
source: vertexShader
}
};
shaders[fragmentShaderId] = {
type: WebGLConstants.FRAGMENT_SHADER,
uri: '',
extras: {
source: fragmentShader
}
};
// Add program
var programAttributes = [];
for (i=0;i<attributesCount;++i) {
programAttributes.push('a_' + attributes[i].toLowerCase());
}
programs[programId] = {
attributes: programAttributes,
fragmentShader: fragmentShaderId,
vertexShader: vertexShaderId
};
return techniqueId;
}
function getTechniqueKey(khrMaterialsCommon) {
var techniqueKey = '';
techniqueKey += 'technique:' + khrMaterialsCommon.technique + ';';
var values = khrMaterialsCommon.values;
var keys = Object.keys(values).sort();
var keysCount = keys.length;
for (var i=0;i<keysCount;++i) {
var name = keys[i];
if (values.hasOwnProperty(name)) {
var value = values[name];
techniqueKey += name + ':';
var type = typeof value;
switch (type) {
case 'string':
techniqueKey += 'texture';
break;
case 'number':
techniqueKey += 'float';
break;
default:
if (Array.isArray(value)) {
techniqueKey += 'vec' + value.length.toString();
}
break;
}
techniqueKey += ';';
}
}
}
function getPrimitiveInfo(materialId, gltf) {
var meshes = gltf.meshes;
for (var name in meshes) {
if (meshes.hasOwnProperty(name)) {
var mesh = meshes[name];
var primitives = mesh.primitives;
var primitivesCount = primitives.length;
for (var i=0;i<primitivesCount;++i) {
var primitive = primitives[i];
if (primitive.material === materialId) {
var jointCount = 0;
if (defined(primitive.attributes.JOINT)) {
var nodes = gltf.nodes;
for (var nodeName in nodes) {
if (nodes.hasOwnProperty(nodeName)) {
var node = nodes[nodeName];
// We have skinning for this node and it contains this mesh
if (defined(node.skeletons) && defined(node.meshes) &&
(node.meshes.indexOf(name) !== -1)) {
jointCount = node.skeletons.length;
}
}
}
}
return {
attributes : Object.keys(primitive.attributes),
jointCount : jointCount
};
}
}
}
}
}
/**
* Modifies gltf in place.
*
* @private
*/
function modelMaterialsCommon(gltf) {
if (!defined(gltf)) {
return undefined;
}
var hasExtension = false;
var extensionsUsed = gltf.extensionsUsed;
if (defined(extensionsUsed)) {
var extensionsUsedCount = extensionsUsed.length;
for(var i=0;i<extensionsUsedCount;++i) {
if (extensionsUsed[i] === 'KHR_materials_common') {
hasExtension = true;
extensionsUsed.splice(i, 1);
break;
}
}
}
if (hasExtension) {
if (!defined(gltf.programs)) {
gltf.programs = {};
}
if (!defined(gltf.shaders)) {
gltf.shaders = {};
}
if (!defined(gltf.techniques)) {
gltf.techniques = {};
}
var lightParameters = generateLightParameters(gltf);
var techniques = {};
var materials = gltf.materials;
var meshes = gltf.meshes;
for (var name in materials) {
if (materials.hasOwnProperty(name)) {
var material = materials[name];
if (defined(material.extensions) && defined(material.extensions.KHR_materials_common)) {
var khrMaterialsCommon = material.extensions.KHR_materials_common;
var techniqueKey = getTechniqueKey(khrMaterialsCommon);
var technique = techniques[techniqueKey];
if (!defined(technique)) {
var primitiveInfo = getPrimitiveInfo(name, gltf);
technique = generateTechnique(gltf, khrMaterialsCommon, primitiveInfo.attributes,
lightParameters, primitiveInfo.jointCount);
}
// Take advantage of the fact that we generate techniques that use the
// same parameter names as the extension values.
material.values = khrMaterialsCommon.values;
material.technique = technique;
delete material.extensions.KHR_materials_common;
}
}
}
if (defined(gltf.extensions)) {
delete gltf.extensions.KHR_materials_common;
}
//var json = JSON.stringify(gltf, null, 4);
//var a = document.createElement('a');
//a.setAttribute('href', 'data:text;base64,' + btoa(json));
//a.setAttribute('target', '_blank');
//a.setAttribute('download', 'model.json');
//a.click();
}
return gltf;
};

118
lib/mtl.js Normal file
View File

@ -0,0 +1,118 @@
"use strict";
var fs = require('fs');
module.exports = {
getDefault : getDefault,
parse : parse
};
function createMaterial() {
return {
ambientColor : undefined, // Ka
emissionColor : undefined, // Ke
diffuseColor : undefined, // Kd
specularColor : undefined, // Ks
specularShininess : undefined, // Ns
specularIntensity : undefined, // Ni
alpha : undefined, // d
ambientColorMap : undefined, // map_Ka
emissionColorMap : undefined, // map_Ke
diffuseColorMap : undefined, // map_Kd
specularColorMap : undefined, // map_Ks
specularShininessMap : undefined, // map_Ns
specularIntensityMap : undefined, // map_Ni
normalMap : undefined, // map_Bump
alphaMap : undefined // map_d
};
}
function getDefault() {
var material = createMaterial();
material.diffuseColor = [0.5, 0.5, 0.5, 1.0];
return material;
}
function parse(mtlPath, done) {
fs.readFile(mtlPath, 'utf-8', function (err, contents) {
if (err) {
console.log('Could not read material file at ' + mtlPath + '. Using default material instead.');
done({});
return;
}
var materials = {};
var material;
var values;
var value;
var lines = contents.split('\n');
var length = lines.length;
for (var i = 0; i < length; ++i) {
var line = lines[i].trim();
if (/^newmtl /i.test(line)) {
var name = line.substring(7).trim();
material = createMaterial();
materials[name] = material;
} else if (/^Ka /i.test(line)) {
values = line.substring(3).trim().split(' ');
material.ambientColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0
];
} else if (/^Ke /i.test(line)) {
values = line.substring(3).trim().split(' ');
material.emissionColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0
];
} else if (/^Kd /i.test(line)) {
values = line.substring(3).trim().split(' ');
material.diffuseColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0
];
} else if (/^Ks /i.test(line)) {
values = line.substring(3).trim().split(' ');
material.specularColor = [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
1.0
];
} else if (/^Ns /i.test(line)) {
value = line.substring(3).trim();
material.specularShininess = parseFloat(value);
} else if (/^Ni /i.test(line)) {
value = line.substring(3).trim();
material.specularIntensity = parseFloat(value);
} else if (/^d /i.test(line)) {
value = line.substring(2).trim();
material.alpha = parseFloat(value);
} else if (/^map_Ka /i.test(line)) {
material.ambientColorMap = line.substring(7).trim();
} else if (/^map_Ke /i.test(line)) {
material.emissionColorMap = line.substring(7).trim();
} else if (/^map_Kd /i.test(line)) {
material.diffuseColorMap = line.substring(7).trim();
} else if (/^map_Ks /i.test(line)) {
material.specularColorMap = line.substring(7).trim();
} else if (/^map_Ns /i.test(line)) {
material.specularShininessMap = line.substring(7).trim();
} else if (/^map_Ni /i.test(line)) {
material.specularIntensityMap = line.substring(7).trim();
} else if (/^map_Bump /i.test(line)) {
material.normalMap = line.substring(9).trim();
} else if (/^map_d /i.test(line)) {
material.alphaMap = line.substring(6).trim();
}
}
done(materials);
});
}

251
lib/obj.js Normal file
View File

@ -0,0 +1,251 @@
"use strict";
var fs = require('fs');
var path = require('path');
var Material = require('./mtl');
var util = require('./util');
var defined = util.defined;
var defaultValue = util.defaultValue;
module.exports = parseObj;
// Obj regex patterns are from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js)
function getMaterials(contents, inputPath, done) {
var hasMaterialGroups = /^usemtl/gm.test(contents);
if (!hasMaterialGroups) {
done({});
return;
}
var mtllibMatches = contents.match(/^mtllib.*/gm);
if (mtllibMatches === null) {
done({});
} else {
var mtlFile = mtllibMatches[0].substring(7).trim();
var mtlPath = path.join(inputPath, mtlFile);
Material.parse(mtlPath, function (materials) {
done(materials);
});
}
}
function parseObj(objFile, inputPath, done) {
fs.readFile(objFile, 'utf-8', function (err, contents) {
if (err) {
throw err;
}
getMaterials(contents, inputPath, function (materials) {
// A vertex in a face is specified by indexes into each of the attribute arrays,
// but these indexes may be different. This maps the separate indexes to a single index.
var vertexCache = {};
var vertexCount = 0;
var vertexArray = [];
var positions = [];
var normals = [];
var uvs = [];
var positionMin = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
var positionMax = [Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE];
var hasPositions = /^v\s/gm.test(contents);
var hasNormals = /^vn/gm.test(contents);
var hasUVs = /^vt/gm.test(contents);
if (!hasPositions) {
console.log('Could not process obj file, no positions.')
}
// Map material to index array
var materialGroups = {};
var matIndexArray;
// Switch to the material-specific index array, or create it if it doesn't exist
function useMaterial(material) {
if (!defined(materials[material])) {
useDefaultMaterial();
} else {
matIndexArray = materialGroups[material];
if (!defined(matIndexArray)) {
matIndexArray = [];
materialGroups[material] = matIndexArray;
}
}
}
function useDefaultMaterial() {
var defaultMaterial = 'czmDefaultMat';
if (!defined(materials[defaultMaterial])) {
materials[defaultMaterial] = Material.getDefault();
}
useMaterial(defaultMaterial);
}
var materialsLength = Object.keys(materials).length;
if (materialsLength === 0) {
useDefaultMaterial();
}
// Sometimes the obj will have objects with different vertex formats
// e.g. one object has uvs, the other does not.
// In this case, use default values.
function createVertex(p, u, n) {
// Positions
var pi = (parseInt(p) - 1) * 3;
var px = positions[pi + 0];
var py = positions[pi + 1];
var pz = positions[pi + 2];
positionMin[0] = Math.min(px, positionMin[0]);
positionMin[1] = Math.min(py, positionMin[1]);
positionMin[2] = Math.min(pz, positionMin[2]);
positionMax[0] = Math.max(px, positionMax[0]);
positionMax[1] = Math.max(py, positionMax[1]);
positionMax[2] = Math.max(pz, positionMax[2]);
vertexArray.push(px, py, pz);
// Normals
var ni = (parseInt(n) - 1) * 3;
var nx = defaultValue(normals[ni + 0], 0.0);
var ny = defaultValue(normals[ni + 1], 0.0);
var nz = defaultValue(normals[ni + 2], 0.0);
vertexArray.push(nx, ny, nz);
// UVs
if (hasUVs) {
var ui = (parseInt(u) - 1) * 2;
var ux = defaultValue(uvs[ui + 0], 0.0);
var uy = defaultValue(uvs[ui + 1], 0.0);
vertexArray.push(ux, uy);
}
}
function addVertex(v, p, u, n) {
var index = vertexCache[v];
if (!defined(index)) {
index = vertexCount++;
vertexCache[v] = index;
createVertex(p, u, n);
}
return index;
}
function addFace(v1, p1, u1, n1, v2, p2, u2, n2, v3, p3, u3, n3, v4, p4, u4, n4) {
var index1 = addVertex(v1, p1, u1, n1);
var index2 = addVertex(v2, p2, u2, n2);
var index3 = addVertex(v3, p3, u3, n3);
matIndexArray.push(index1);
matIndexArray.push(index2);
matIndexArray.push(index3);
// Triangulate if the face is a quad
if (defined(v4)) {
var index4 = addVertex(v4, p4, u4, n4);
matIndexArray.push(index1);
matIndexArray.push(index3);
matIndexArray.push(index4);
}
}
// v float float float
var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
// vn float float float
var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
// vt float float
var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/;
// f vertex vertex vertex ...
var facePattern1 = /f( +-?\d+)( +-?\d+)( +-?\d+)( +-?\d+)?/;
// f vertex/uv vertex/uv vertex/uv ...
var facePattern2 = /f( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+))?/;
// f vertex/uv/normal vertex/uv/normal vertex/uv/normal ...
var facePattern3 = /f( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))( +(-?\d+)\/(-?\d+)\/(-?\d+))?/;
// f vertex//normal vertex//normal vertex//normal ...
var facePattern4 = /f( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))( +(-?\d+)\/\/(-?\d+))?/;
var lines = contents.split('\n');
var length = lines.length;
for (var i = 0; i < length; ++i) {
var line = lines[i].trim();
var result;
if ((line.length === 0) || (line.charAt(0) === '#')) {
continue;
} else if ((result = vertexPattern.exec(line)) !== null) {
positions.push(
parseFloat(result[1]),
parseFloat(result[2]),
parseFloat(result[3])
);
} else if ((result = normalPattern.exec(line) ) !== null) {
// Normalize
var nx = parseFloat(result[1]);
var ny = parseFloat(result[2]);
var nz = parseFloat(result[3]);
var magnitude = Math.sqrt(nx * nx + ny * ny + nz * nz);
nx /= magnitude;
ny /= magnitude;
nz /= magnitude;
normals.push(
nx,
ny,
nz
);
} else if ((result = uvPattern.exec(line)) !== null) {
uvs.push(
parseFloat(result[1]),
parseFloat(result[2])
);
} else if ((result = facePattern1.exec(line)) !== null) {
addFace(
result[1], result[1], 0, 0,
result[2], result[2], 0, 0,
result[3], result[3], 0, 0,
result[4], result[4], 0, 0
);
} else if ((result = facePattern2.exec(line)) !== null) {
addFace(
result[1], result[2], result[3], 0,
result[4], result[5], result[6], 0,
result[7], result[8], result[9], 0,
result[10], result[11], result[12], 0
);
} else if ((result = facePattern3.exec(line)) !== null) {
addFace(
result[1], result[2], result[3], result[4],
result[5], result[6], result[7], result[8],
result[9], result[10], result[11], result[12],
result[13], result[14], result[15], result[16]
);
} else if ((result = facePattern4.exec(line)) !== null) {
addFace(
result[1], result[2], 0, result[3],
result[4], result[5], 0, result[6],
result[7], result[8], 0, result[9],
result[10], result[11], 0, result[12]
);
} else if (/^usemtl /.test(line)) {
var materialName = line.substring(7).trim();
useMaterial(materialName);
}
}
done({
vertexCount: vertexCount,
vertexArray: vertexArray,
positionMin : positionMin,
positionMax : positionMax,
hasUVs: hasUVs,
materialGroups: materialGroups,
materials: materials
});
});
});
}

16
lib/util.js Normal file
View File

@ -0,0 +1,16 @@
"use strict";
module.exports = {
defined : defined,
defaultValue : defaultValue
};
function defined(value) {
return value !== undefined;
}
function defaultValue(a, b) {
if (a !== undefined) {
return a;
}
return b;
}

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "obj2gltf",
"version": "1.0.0",
"dependencies": {
"async": "^1.4.2",
"fs-extra": "^0.24.0",
"minimist": "^1.2.0",
"path": "^0.12.7"
}
}