From c8c4d5674480c0fa577333e20e21b2e2b66c9113 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 16 Oct 2015 17:32:23 -0400 Subject: [PATCH] Convert obj to gltf --- .gitignore | 12 + bin/obj2gltf.js | 61 +++ lib/WebGLConstants.js | 300 +++++++++++++++ lib/gltf.js | 435 +++++++++++++++++++++ lib/image.js | 44 +++ lib/modelMaterialsCommon.js | 750 ++++++++++++++++++++++++++++++++++++ lib/mtl.js | 118 ++++++ lib/obj.js | 251 ++++++++++++ lib/util.js | 16 + package.json | 10 + 10 files changed, 1997 insertions(+) create mode 100644 .gitignore create mode 100644 bin/obj2gltf.js create mode 100644 lib/WebGLConstants.js create mode 100644 lib/gltf.js create mode 100644 lib/image.js create mode 100644 lib/modelMaterialsCommon.js create mode 100644 lib/mtl.js create mode 100644 lib/obj.js create mode 100644 lib/util.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f431c7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.sdf +*.opensdf +*.suo +*.csproj.user +*.vcxproj.user +*.log +x64 +**/node_modules + +# WebStorm user-specific +.idea/ +data/ \ No newline at end of file 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/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