diff --git a/CHANGES.md b/CHANGES.md index 218b30bc..cb2f4c09 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Change Log ========== +### Next Release +* `modelMaterialsCommon` renamed to `processModelMaterialsCommon`. +* Added `generateModelMaterialsCommon` and command line `kmc` flags for generating models with the `KHR_materials_common` extension. + ### 0.1.0-alpha6 - 2016-11-18 * Fixed `combinePrimitives` stage and re-added it to the pipeline. [#108](https://github.com/AnalyticalGraphicsInc/gltf-pipeline/issues/108) diff --git a/README.md b/README.md index 74d6d624..13fb29b8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ node ./bin/gltf-pipeline.js -i ./specs/data/boxTexturedUnoptimized/CesiumTexture |`--removeNormals`, `-r`|Strips off existing normals, allowing them to be regenerated.|No, default `false`| |`--faceNormals`, `-f`|If normals are missing, they should be generated using the face normal.|No, default `false`| |`--cesium`, `-c`|Optimize the glTF for Cesium by using the sun as a default light source.|No, default `false`| +|`--kmc.enable`|Materials should be expressed using the KHR_materials_common extension. If other `kmc` flags are enabled, this is implicitly true.|No, default `false`| +|`--kmc.doubleSided`|Declares whether backface culling should be disabled.|No, default `false`| +|`--kmc.technique`|The lighting model to use.|No, default `PHONG`| |`--ao.enable`|Bake ambient occlusion (to vertex data by default). If other `ao` flags are enabled, this is implicitly true. Advanced settings in `lib/bakeAmbientOcclusion.js`|No, default `false`| |`--ao.toTexture`|Bake AO to existing diffuse textures instead of to vertices. Does not modify shaders.|No, default `false`| |`--ao.groundPlane`|Simulate a groundplane at the lowest point of the model when baking AO.|No, default `false`| diff --git a/lib/Pipeline.js b/lib/Pipeline.js index 8a3fe5ea..6d16e79d 100644 --- a/lib/Pipeline.js +++ b/lib/Pipeline.js @@ -15,6 +15,7 @@ var compressTextureCoordinates = require('./compressTextureCoordinates'); var combinePrimitives = require('./combinePrimitives'); var convertDagToTree = require('./convertDagToTree'); var encodeImages = require('./encodeImages'); +var generateModelMaterialsCommon = require('./generateModelMaterialsCommon'); var generateNormals = require('./generateNormals'); var loadGltfUris = require('./loadGltfUris'); var mergeDuplicateVertices = require('./mergeDuplicateVertices'); @@ -132,6 +133,13 @@ Pipeline.processJSONWithExtras = function(gltfWithExtras, options) { quantizeAttributes(gltfWithExtras, quantizedOptions); } return encodeImages(gltfWithExtras); + }) + .then(function() { + var kmcOptions = options.kmcOptions; + if (defined(kmcOptions) && kmcOptions.enable) { + generateModelMaterialsCommon(gltfWithExtras, options.kmcOptions); + } + return gltfWithExtras; }); }; diff --git a/lib/addDefaults.js b/lib/addDefaults.js index 33bf6626..cb3706e2 100644 --- a/lib/addDefaults.js +++ b/lib/addDefaults.js @@ -9,7 +9,7 @@ var WebGLConstants = Cesium.WebGLConstants; var addExtensionsUsed = require('./addExtensionsUsed'); var findAccessorMinMax = require('./findAccessorMinMax'); -var modelMaterialsCommon = require('./modelMaterialsCommon'); +var processModelMaterialsCommon = require('./processModelMaterialsCommon'); module.exports = addDefaults; @@ -552,7 +552,7 @@ function addDefaults(gltf, options) { skinDefaults(gltf); textureDefaults(gltf); - modelMaterialsCommon(gltf, options); + processModelMaterialsCommon(gltf, options); return gltf; } diff --git a/lib/cesiumGeometryToGltfPrimitive.js b/lib/cesiumGeometryToGltfPrimitive.js index 3b43ae2f..008b63dd 100644 --- a/lib/cesiumGeometryToGltfPrimitive.js +++ b/lib/cesiumGeometryToGltfPrimitive.js @@ -75,6 +75,6 @@ function cesiumGeometryToGltfPrimitive(gltf, primitive, geometry) { var indicesId = primitive.indices; var indicesAccessor = gltf.accessors[indicesId]; writeAccessor(gltf, indicesAccessor, geometry.indices); - mergeBuffers(gltf, 'buffer_0'); + mergeBuffers(gltf); uninterleaveAndPackBuffers(gltf); } diff --git a/lib/generateModelMaterialsCommon.js b/lib/generateModelMaterialsCommon.js new file mode 100644 index 00000000..1542f67b --- /dev/null +++ b/lib/generateModelMaterialsCommon.js @@ -0,0 +1,160 @@ +'use strict'; +var Cesium = require('cesium'); +var addExtensionsUsed = require('./addExtensionsUsed'); + +var defaultValue = Cesium.defaultValue; +var defined = Cesium.defined; + +module.exports = generateModelMaterialsCommon; + +/** + * Converts materials to use the KHR_materials_common extension and deletes + * techniques, programs and shaders. + * + * The glTF asset must be initialized for the pipeline. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} [kmcOptions] KHR_materials_common options for material generation. + * @returns {Object} The glTF asset using the KHR_materials_common extension. + * + * @see addPipelineExtras + * @see loadGltfUris + */ +function generateModelMaterialsCommon(gltf, kmcOptions) { + kmcOptions = defaultValue(kmcOptions, {}); + kmcOptions.doubleSided = defaultValue(kmcOptions.doubleSided, false); + kmcOptions.technique = defaultValue(kmcOptions.technique, 'PHONG'); + addExtensionsUsed(gltf, 'KHR_materials_common'); + var materialsCommon; + var materials = gltf.materials; + var nodes = gltf.nodes; + var techniques = gltf.techniques; + var jointCountForMaterialId = getJointCountForMaterials(gltf); + + var gltfExtensions = gltf.extensions; + if (!defined(gltfExtensions)) { + gltfExtensions = {}; + gltf.extensions = gltfExtensions; + } + materialsCommon = {}; + gltfExtensions.KHR_materials_common = materialsCommon; + var lights = {}; + materialsCommon.lights = lights; + + for (var materialId in materials) { + if (materials.hasOwnProperty(materialId)) { + var material = materials[materialId]; + var technique = techniques[material.technique]; + var techniqueParameters = technique.parameters; + if (defined(techniqueParameters.ambient) && !defined(lights.defaultAmbient)) { + lights.defaultAmbient = { + ambient: { + color: [1, 1, 1] + }, + name: 'defaultAmbient', + type: 'ambient' + }; + } + for (var parameterId in techniqueParameters) { + if (techniqueParameters.hasOwnProperty(parameterId)) { + if (parameterId.indexOf('light') === 0 && parameterId.indexOf('Transform') >= 0) { + var lightId = parameterId.substring(0, parameterId.indexOf('Transform')); + var lightTransform = techniqueParameters[parameterId]; + var lightNodeId = lightTransform.node; + var lightNode = nodes[lightNodeId]; + var nodeExtensions = lightNode.extensions; + if (!defined(nodeExtensions)) { + nodeExtensions = {}; + lightNode.extensions = nodeExtensions; + } + materialsCommon = {}; + nodeExtensions.KHR_materials_common = materialsCommon; + materialsCommon.light = lightId; + var lightColor = techniqueParameters[lightId + 'Color']; + var light = { + name : lightId, + type : 'directional', + directional : { + color : defaultValue(lightColor.value, [1, 1, 1]) + } + }; + lights[lightId] = light; + } + } + } + + delete material.technique; + var extensions = material.extensions; + if (!defined(extensions)) { + extensions = {}; + material.extensions = extensions; + } + materialsCommon = {}; + extensions.KHR_materials_common = materialsCommon; + for (var kmcOption in kmcOptions) { + if (kmcOptions.hasOwnProperty(kmcOption) && kmcOption !== 'enable') { + materialsCommon[kmcOption] = kmcOptions[kmcOption]; + } + } + var jointCount = jointCountForMaterialId[materialId]; + if (defined(jointCount)) { + materialsCommon.jointCount = jointCount; + } + materialsCommon.values = material.values; + delete material.values; + } + } + delete gltf.techniques; + delete gltf.programs; + delete gltf.shaders; + return gltf; +} + +function getJointCountForMaterials(gltf) { + var accessors = gltf.accessors; + var meshes = gltf.meshes; + var nodes = gltf.nodes; + var skins = gltf.skins; + var jointCountForMaterialId = {}; + + var nodesForSkinId = {}; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + var node = nodes[nodeId]; + if (defined(node.skin)) { + if (!defined(nodesForSkinId[node.skin])) { + nodesForSkinId[node.skin] = []; + } + nodesForSkinId[node.skin].push(node); + } + } + } + + for (var skinId in skins) { + if (skins.hasOwnProperty(skinId)) { + var skin = skins[skinId]; + var jointCount = 1; + if (defined(skin.inverseBindMatrices)) { + jointCount = accessors[skin.inverseBindMatrices].count; + } + var skinnedNodes = nodesForSkinId[skinId]; + var skinnedNodesLength = skinnedNodes.length; + for (var i = 0; i < skinnedNodesLength; i++) { + var skinnedNode = skinnedNodes[i]; + var nodeMeshes = skinnedNode.meshes; + var nodeMeshesLength = nodeMeshes.length; + for (var j = 0; j < nodeMeshesLength; j++) { + var meshId = nodeMeshes[j]; + var mesh = meshes[meshId]; + var primitives = mesh.primitives; + var primitivesLength = primitives.length; + for (var k = 0; k < primitivesLength; k++) { + var primitive = primitives[k]; + jointCountForMaterialId[primitive.material] = jointCount; + } + } + } + } + } + return jointCountForMaterialId; +} diff --git a/lib/generateNormals.js b/lib/generateNormals.js index 46ef3921..ed29217f 100644 --- a/lib/generateNormals.js +++ b/lib/generateNormals.js @@ -16,7 +16,7 @@ var gltfPrimitiveToCesiumGeometry = require('./gltfPrimitiveToCesiumGeometry'); var getPrimitiveAttributeSemantics = require('./getPrimitiveAttributeSemantics'); var getUniqueId = require('./getUniqueId'); var mergeBuffers = require('./mergeBuffers'); -var modelMaterialsCommon = require('./modelMaterialsCommon'); +var processModelMaterialsCommon = require('./processModelMaterialsCommon'); var packArray = require('./packArray'); var readAccessor = require('./readAccessor'); var techniqueParameterForSemantic = require('./techniqueParameterForSemantic'); @@ -65,7 +65,7 @@ function generateNormals(gltf, options) { removeBufferViews(gltf); removeBuffers(gltf); mergeBuffers(gltf); - modelMaterialsCommon(gltf, options); + processModelMaterialsCommon(gltf, options); return gltf; } diff --git a/lib/mergeBuffers.js b/lib/mergeBuffers.js index b658f1d0..d2a4f9b8 100755 --- a/lib/mergeBuffers.js +++ b/lib/mergeBuffers.js @@ -19,9 +19,6 @@ module.exports = mergeBuffers; * @see loadGltfUris */ function mergeBuffers(gltf, bufferId) { - if (!defined(bufferId)) { - bufferId = getUniqueId(gltf, 'buffer'); - } var buffers = gltf.buffers; var bufferViews = gltf.bufferViews; var bufferViewsForBuffers = getBufferViewsForBuffers(gltf); @@ -32,6 +29,9 @@ function mergeBuffers(gltf, bufferId) { if (buffers.hasOwnProperty(gltfBufferId)) { //Add the buffer to the merged source var buffer = buffers[gltfBufferId]; + if (!defined(bufferId)) { + bufferId = gltfBufferId; + } var bufferViewIds = bufferViewsForBuffers[gltfBufferId]; for (var bufferViewId in bufferViewIds) { if (bufferViewIds.hasOwnProperty(bufferViewId)) { @@ -44,6 +44,10 @@ function mergeBuffers(gltf, bufferId) { } } + if (!defined(bufferId)) { + bufferId = getUniqueId(gltf, 'buffer'); + } + //Replace existing buffer with new merged buffer gltf.buffers = {}; gltf.buffers[bufferId] = { diff --git a/lib/parseArguments.js b/lib/parseArguments.js index 61afb899..c1f6dd97 100644 --- a/lib/parseArguments.js +++ b/lib/parseArguments.js @@ -79,20 +79,42 @@ function parseArguments(args) { describe: 'Optimize the glTF for Cesium by using the sun as a default light source.', type: 'boolean' }, + 'kmc.enable': { + default: false, + describe: 'Materials should be expressed using the KHR_materials_common extension. If other `kmc` flags are enabled, this is implictly true.', + group: 'Options: KHR_materials_common', + type: 'boolean' + }, + 'kmc.doubleSided': { + default: false, + describe: 'Declares whether backface culling should be disabled.', + group: 'Options: KHR_materials_common', + type: 'boolean' + }, + 'kmc.technique': { + choices: ['CONSTANT', 'BLINN', 'PHONG', 'LAMBERT'], + default: 'PHONG', + describe: 'The lighting model to use.', + group: 'Options: KHR_materials_common', + type: 'string' + }, 'ao.enable': { default: false, describe: 'Bake ambient occlusion (to vertex data by default). If other `ao` flags are enabled, this is implicitly true.', - group: 'Options: Ambient Occlusion' + group: 'Options: Ambient Occlusion', + type: 'boolean' }, 'ao.toTexture': { default: false, describe: 'Bake AO to existing diffuse textures instead of vertices. Does not modify shaders.', - group: 'Options: Ambient Occlusion' + group: 'Options: Ambient Occlusion', + type: 'boolean' }, 'ao.groundPlane': { default: false, describe: 'Simulate a ground plane at the lowest point of the model when baking AO.', - group: 'Options: Ambient Occlusion' + group: 'Options: Ambient Occlusion', + type: 'boolean' }, 'ao.ambientShadowContribution': { default: 0.5, @@ -111,13 +133,16 @@ function parseArguments(args) { } }).parse(args); - // If any raw ao parameters were specified, ao is enabled + // If any raw ao or kmc parameters were specified, they are enabled var nargs = process.argv.length; for (var i = 0; i < nargs; i++) { var arg = process.argv[i]; if (arg.indexOf('ao') >= 0) { argv.ao.enable = true; } + if (arg.indexOf('kmc') >= 0) { + argv.kmc.enable = true; + } } var gltfPath = defaultValue(argv.i, argv._[0]); @@ -151,6 +176,7 @@ function parseArguments(args) { encodeNormals: argv.n, faceNormals: argv.f, inputPath: gltfPath, + kmcOptions: argv.kmc, removeNormals: argv.r, optimizeForCesium: argv.cesium, outputPath: outputPath, diff --git a/lib/modelMaterialsCommon.js b/lib/processModelMaterialsCommon.js similarity index 100% rename from lib/modelMaterialsCommon.js rename to lib/processModelMaterialsCommon.js diff --git a/specs/lib/generateModelMaterialsCommonSpec.js b/specs/lib/generateModelMaterialsCommonSpec.js new file mode 100644 index 00000000..84100ffb --- /dev/null +++ b/specs/lib/generateModelMaterialsCommonSpec.js @@ -0,0 +1,128 @@ +'use strict'; +var generateModelMaterialsCommon = require('../../lib/generateModelMaterialsCommon'); + +describe('generateModelMaterialsCommon', function() { + it('removes techniques, programs, and shaders', function() { + var gltf = { + techniques : {}, + programs : {}, + shaders : {} + }; + generateModelMaterialsCommon(gltf); + expect(gltf.extensionsUsed).toEqual(['KHR_materials_common']); + expect(gltf.techniques).not.toBeDefined(); + expect(gltf.programs).not.toBeDefined(); + expect(gltf.shaders).not.toBeDefined(); + }); + + it('generates a KHR_materials_common material with values', function() { + var gltf = { + materials : { + material : { + values : { + ambient : [0, 0, 0, 1], + otherAttribute : true + }, + technique: 'technique' + } + }, + techniques : { + technique : { + parameters : {} + } + } + }; + generateModelMaterialsCommon(gltf, { + technique : 'PHONG', + doubleSided : true, + someAttribute : true + }); + expect(gltf.extensionsUsed).toEqual(['KHR_materials_common']); + var material = gltf.materials.material; + var materialsCommon = material.extensions.KHR_materials_common; + expect(materialsCommon.doubleSided).toBe(true); + expect(materialsCommon.technique).toBe('PHONG'); + expect(materialsCommon.someAttribute).toBe(true); + var values = materialsCommon.values; + expect(values.ambient).toEqual([0, 0, 0, 1]); + expect(values.otherAttribute).toBe(true); + }); + + it('generates lights from a technique', function() { + var gltf = { + materials : { + material : { + technique : 'technique' + } + }, + nodes : { + lightNode : {} + }, + techniques : { + technique : { + parameters : { + ambient : {}, + light0Color : { + value : [1, 1, 0.5] + }, + light0Transform : { + node : 'lightNode' + } + } + } + } + }; + generateModelMaterialsCommon(gltf); + expect(gltf.nodes.lightNode.extensions.KHR_materials_common.light).toBe('light0'); + var lights = gltf.extensions.KHR_materials_common.lights; + expect(lights.defaultAmbient.ambient.color).toEqual([1, 1, 1]); + expect(lights.light0.directional.color).toEqual([1, 1, 0.5]); + expect(gltf.techniques).not.toBeDefined(); + }); + + it('declares jointCount for skinned nodes', function() { + var gltf = { + accessors : { + accessor : { + count : 4 + } + }, + materials : { + material : { + technique : 'technique' + } + }, + meshes : { + mesh : { + primitives : [ + { + material : 'material' + } + ] + } + }, + nodes : { + skinnedNode : { + meshes : [ + 'mesh' + ], + skin : 'skin' + } + }, + skins : { + skin : { + inverseBindMatrices : 'accessor' + } + }, + techniques : { + technique : { + parameters : {} + } + } + }; + generateModelMaterialsCommon(gltf); + var material = gltf.materials.material; + var materialsCommon = material.extensions.KHR_materials_common; + expect(materialsCommon.jointCount).toBe(4); + }); +}); \ No newline at end of file