diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31dde5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/node_modules +.DS_Store +Thumbs.db diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6cb7cfd --- /dev/null +++ b/LICENSE.md @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 30775bd..18570c2 100644 --- a/README.md +++ b/README.md @@ -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. +

+ +

diff --git a/bin/obj2gltf.js b/bin/obj2gltf.js new file mode 100644 index 0000000..27347e7 --- /dev/null +++ b/bin/obj2gltf.js @@ -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'); + }); + }); +}); diff --git a/doc/cesium.png b/doc/cesium.png new file mode 100644 index 0000000..32db616 Binary files /dev/null and b/doc/cesium.png differ diff --git a/lib/WebGLConstants.js b/lib/WebGLConstants.js new file mode 100644 index 0000000..80098a2 --- /dev/null +++ b/lib/WebGLConstants.js @@ -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 +}; diff --git a/lib/gltf.js b/lib/gltf.js new file mode 100644 index 0000000..aeae108 --- /dev/null +++ b/lib/gltf.js @@ -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(); + }); + } + }); + }); +} diff --git a/lib/image.js b/lib/image.js new file mode 100644 index 0000000..d435b83 --- /dev/null +++ b/lib/image.js @@ -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); + }); +} diff --git a/lib/modelMaterialsCommon.js b/lib/modelMaterialsCommon.js new file mode 100644 index 0000000..d1b78bd --- /dev/null +++ b/lib/modelMaterialsCommon.js @@ -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 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 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