From e4dcbc6987fc51b36445ecebf9cf3a0802493799 Mon Sep 17 00:00:00 2001 From: Squareys Date: Tue, 19 Mar 2019 18:49:40 +0100 Subject: [PATCH 1/7] Implement removal of unused nodes, materials and meshes Includes adaption of existing code to keep backwards-compatible behaviour, mainly because the tests often expect unused materials, meshes and nodes to be kept. Signed-off-by: Squareys --- lib/compressDracoMeshes.js | 2 +- lib/removeUnusedElements.js | 154 ++++++++++++++++++++++++++++++++++-- lib/splitPrimitives.js | 2 +- lib/updateVersion.js | 2 +- lib/writeResources.js | 2 +- 5 files changed, 152 insertions(+), 10 deletions(-) diff --git a/lib/compressDracoMeshes.js b/lib/compressDracoMeshes.js index 61b398a5..7c5faa46 100644 --- a/lib/compressDracoMeshes.js +++ b/lib/compressDracoMeshes.js @@ -226,7 +226,7 @@ function compressDracoMeshes(gltf, options) { } else { addExtensionsRequired(gltf, 'KHR_draco_mesh_compression'); } - removeUnusedElements(gltf); + removeUnusedElements(gltf, ['accessor', 'bufferView', 'buffer']); if (uncompressedFallback) { assignMergedBufferNames(gltf); diff --git a/lib/removeUnusedElements.js b/lib/removeUnusedElements.js index cc6a4e66..40389729 100644 --- a/lib/removeUnusedElements.js +++ b/lib/removeUnusedElements.js @@ -12,20 +12,28 @@ module.exports = removeUnusedElements; * This function currently only works for accessors, buffers, and bufferViews. * * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Array} elementTypes Element types to be removed. Needs to be a subset of + * ['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer'], other items + * will be ignored. * * @private */ -function removeUnusedElements(gltf) { - removeUnusedElementsByType(gltf, 'accessor'); - removeUnusedElementsByType(gltf, 'bufferView'); - removeUnusedElementsByType(gltf, 'buffer'); +function removeUnusedElements(gltf, elementTypes=['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer']) { + ['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer'].forEach(function(type) { + if(elementTypes.includes(type)) { + removeUnusedElementsByType(gltf, type); + } + }); return gltf; } const TypeToGltfElementName = { accessor: 'accessors', buffer: 'buffers', - bufferView: 'bufferViews' + bufferView: 'bufferViews', + node: 'nodes', + material: 'materials', + mesh: 'meshes' }; function removeUnusedElementsByType(gltf, type) { @@ -38,7 +46,7 @@ function removeUnusedElementsByType(gltf, type) { const length = arrayOfObjects.length; for (let i = 0; i < length; ++i) { - if (!usedIds[i]) { + if(!usedIds[i]) { Remove[type](gltf, i - removed); removed++; } @@ -158,6 +166,62 @@ Remove.bufferView = function(gltf, bufferViewId) { } }; +Remove.mesh = function(gltf, meshId) { + const meshes = gltf.meshes; + meshes.splice(meshId, 1); + + ForEach.node(gltf, function(n) { + if(defined(n.mesh)) { + if(n.mesh > meshId) { + --n.mesh; + } else if(n.mesh === meshId) { + /* Remove reference to deleted mesh */ + delete n.mesh; + } + } + }); +}; + +Remove.node = function(gltf, nodeId) { + const nodes = gltf.nodes; + + /* Remove this node from parent */ + const parentId = findParentNode(gltf, nodeId); + if(parentId >= 0) { + nodes[parentId].children = nodes[parentId].children.filter(x => x !== nodeId); + } + + nodes.splice(nodeId, 1); + + /* Shift all node references */ + nodes.forEach(other => { + if(defined(other.children)) { + other.children = other.children.map(x => x > nodeId ? x - 1 : x); + } + }); + ForEach.scene(gltf, (s, i) => { + s.nodes = s.nodes + .filter(x => x !== nodeId) /* Remove */ + .map(x => x > nodeId ? x - 1 : x); /* Shift indices */ + }); +}; + +Remove.material = function(gltf, materialId) { + const materials = gltf.materials; + materials.splice(materialId, 1); + + /* Shift other material ids */ + ForEach.mesh(gltf, (m, j) => { + m.primitives.forEach(p => { + if(p.material > materialId) { + --p.material; + } + }); + }); + + return gltf; +}; + /** * Contains functions for getting a list of element ids in use by the glTF asset. * @constructor @@ -260,3 +324,81 @@ getListOfElementsIdsInUse.bufferView = function(gltf) { return usedBufferViewIds; }; + +getListOfElementsIdsInUse.mesh = function(gltf) { + const usedMeshIds = {}; + ForEach.mesh(gltf, (mesh, i) => { + if(!defined(mesh.primitives) || mesh.primitives.length === 0) { + usedMeshIds[i] = false; + } + }); + + ForEach.node(gltf, (node, i) => { + if(!defined(node.mesh)) { + return; + } + /* Mesh marked as empty in previous step? */ + const meshIsEmpty = defined(usedMeshIds[node.mesh]); + + if(!meshIsEmpty) { + usedMeshIds[node.mesh] = true; + } + }); + + return usedMeshIds; +}; + +/* Find parent node id of the given node, or -1 if no parent */ +function findParentNode(gltf, node) { + return gltf.nodes.findIndex(n => (n.children || []).includes(node)); +} + +/* Check if node is empty. It is considered empty if neither referencing + * mesh, camera, extensions and has no children */ +function nodeIsEmpty(gltf, node) { + if(defined(node.mesh) || defined(node.camera) || defined(node.skin) + || defined(node.weights) || defined(node.extras) + || (defined(node.extensions) && node.extensions.length !== 0)) { + return false; + } + + /* Empty if no children or children are all empty nodes */ + return !defined(node.children) + || node.children.filter(n => !nodeIsEmpty(gltf, gltf.nodes[n])).length === 0; +} + +getListOfElementsIdsInUse.node = function(gltf) { + const usedNodeIds = {}; + ForEach.node(gltf, (node, nodeId) => { + if(!nodeIsEmpty(gltf, node)) { + usedNodeIds[nodeId] = true; + } + }); + + ForEach.animation(gltf, (anim, animId) => { + if(!defined(anim.channels)) { + return; + } + + anim.channels.forEach(c => { + if(defined(c.target) && defined(c.target.node)) { + /* Keep all nodes that are being targeted + * by an animation */ + usedNodeIds[c.target.node] = true; + } + }); + }); + + return usedNodeIds; +}; + +getListOfElementsIdsInUse.material = function(gltf) { + const usedMaterialIds = {}; + gltf.meshes.forEach((m, i) => { + m.primitives.forEach((p, i) => { + usedMaterialIds[p.material] = true; + }); + }); + + return usedMaterialIds; +}; diff --git a/lib/splitPrimitives.js b/lib/splitPrimitives.js index b68ed29b..676c38da 100644 --- a/lib/splitPrimitives.js +++ b/lib/splitPrimitives.js @@ -90,7 +90,7 @@ function splitPrimitives(gltf) { } } } - removeUnusedElements(gltf); + removeUnusedElements(gltf, ['accessor', 'bufferView', 'buffer']); } return gltf; diff --git a/lib/updateVersion.js b/lib/updateVersion.js index e2372308..37089570 100644 --- a/lib/updateVersion.js +++ b/lib/updateVersion.js @@ -814,7 +814,7 @@ function moveByteStrideToBufferView(gltf) { } // Remove unused buffer views - removeUnusedElements(gltf); + removeUnusedElements(gltf, ['accessor', 'bufferView', 'buffer']); } function requirePositionAccessorMinMax(gltf) { diff --git a/lib/writeResources.js b/lib/writeResources.js index edbc5887..f7ea215a 100644 --- a/lib/writeResources.js +++ b/lib/writeResources.js @@ -55,7 +55,7 @@ function writeResources(gltf, options) { }); // Buffers need to be written last because images and shaders may write to new buffers - removeUnusedElements(gltf); + removeUnusedElements(gltf, ['acessor', 'bufferView', 'buffer']); mergeBuffers(gltf, options.name); ForEach.buffer(gltf, function(buffer, bufferId) { From 39386da71668587a09969d6b93ec1f023e4dbb4b Mon Sep 17 00:00:00 2001 From: Squareys Date: Tue, 19 Mar 2019 18:49:59 +0100 Subject: [PATCH 2/7] Extend removeUnusedElementsSpec with nodes, materials and meshes Signed-off-by: Squareys --- specs/lib/removeUnusedElementsSpec.js | 132 ++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 16 deletions(-) diff --git a/specs/lib/removeUnusedElementsSpec.js b/specs/lib/removeUnusedElementsSpec.js index 0b99fcd8..a84a0c26 100644 --- a/specs/lib/removeUnusedElementsSpec.js +++ b/specs/lib/removeUnusedElementsSpec.js @@ -8,9 +8,38 @@ const WebGLConstants = Cesium.WebGLConstants; const gltf = { nodes: [ { + name: 'skin', skin: 0, mesh: 0, translation: [0.0, 0.0, 0.0] + }, + { + name: 'used', + mesh: 0, + children: [2, 5] + }, + { + name: 'unused' + }, + { + name: 'nodeWithEmptyMesh', + mesh: 1 + }, + { + name: 'unusedParent', + children: [2] + }, + { + name: 'camera', + camera: 0 + }, + { + name: 'light', + extensions : { + KHR_lights_punctual : { + light : 0 + } + } } ], buffers: [ @@ -207,6 +236,7 @@ const gltf = { ], meshes: [ { + name: 'mesh0', primitives: [ { attributes: { @@ -225,9 +255,20 @@ const gltf = { } ], indices: 7, - mode: WebGLConstants.TRIANGLES + mode: WebGLConstants.TRIANGLES, + material: 1 } ] + }, + { + name: 'mesh1', + primitives: [] + } + ], + cameras: [ + { + name: 'cam', + type: 'perspective' } ], skins: [ @@ -268,39 +309,98 @@ const gltf = { bufferView: 11, mimeType: 'image/png' } + ], + extensions: { + KHR_lights_punctual : { + lights: [ + { + name: 'sun', + type: 'directional' + } + ] + } + }, + materials: [ + { + name: 'unused' + }, + { + name: 'used' + } + ], + scenes: [ + { + nodes: [2, 3] + } ] }; describe('removeUnusedElements', () => { - it('removes unused accessors, bufferViews, and buffers', () => { - delete gltf.animations; - delete gltf.skins; - gltf.meshes[0].primitives[0].targets.splice(0, 1); - gltf.images.splice(1, 2); - removeUnusedElements(gltf); + delete gltf.animations; + delete gltf.skins; + gltf.meshes[0].primitives[0].targets.splice(0, 1); + gltf.images.splice(1, 2); + removeUnusedElements(gltf); - const remainingAccessorNames = ['positions', 'normals', 'texcoords', 'positions-target1', 'normals-target1', 'indices']; - const remainingAcessorBufferViewIds = [0, 1, 2, 3, 4, 5]; - const remainingBufferViewNames = ['positions', 'normals', 'texcoords', 'positions-target1', 'normals-target1', 'indices', 'image0']; - const remainingBufferViewBufferIds = [0, 0, 0, 0, 0, 0, 1]; - const remainingBufferNames = ['mesh', 'image01']; + const remainingAccessorNames = ['positions', 'normals', 'texcoords', 'positions-target1', 'normals-target1', 'indices']; + const remainingAcessorBufferViewIds = [0, 1, 2, 3, 4, 5]; + const remainingBufferViewNames = ['positions', 'normals', 'texcoords', 'positions-target1', 'normals-target1', 'indices', 'image0']; + const remainingBufferViewBufferIds = [0, 0, 0, 0, 0, 0, 1]; + const remaining = { + nodes: ['skin', 'used', 'camera', 'light'], + cameras: ['cam'], + meshes: ['mesh0'], + buffers: ['mesh', 'image01'], + lights: ['sun'], + materials: ['used'] + }; + + it('correctly removes/keeps accessors', () => { expect(gltf.accessors.length).toBe(remainingAccessorNames.length); - expect(gltf.bufferViews.length).toBe(remainingBufferViewNames.length); - expect(gltf.buffers.length).toBe(remainingBufferNames.length); ForEach.accessor(gltf, (accessor, index) => { expect(accessor.name).toBe(remainingAccessorNames[index]); expect(accessor.bufferView).toBe(remainingAcessorBufferViewIds[index]); }); + }); + + it('correctly removes/keeps bufferViews', () => { + expect(gltf.bufferViews.length).toBe(remainingBufferViewNames.length); ForEach.bufferView(gltf, (bufferView, index) => { expect(bufferView.name).toBe(remainingBufferViewNames[index]); expect(bufferView.buffer).toBe(remainingBufferViewBufferIds[index]); }); + }); + + ['materials', 'nodes', 'cameras', 'meshes', 'buffers'].forEach(k => { + it('correctly removes/keeps ' + k, () => { + expect(Object.keys(gltf)).toContain(k); + expect(gltf[k].length).toBe(remaining[k].length); + + /* Check that at least the remaining elements are present */ + ForEach.topLevel(gltf, k, (element, index) => { + expect(remaining[k]).toContain(element.name); + }); + + /* Check that all the elements should actually remain */ + remaining[k].forEach((name, index) => { + expect(gltf[k].map(x => x.name)).toContain(name); + }); + }); + }); + + it('correctly removes/keeps lights', () => { + expect(Object.keys(gltf)).toContain('extensions'); + expect(Object.keys(gltf.extensions)).toContain('KHR_lights_punctual'); + expect(Object.keys(gltf.extensions.KHR_lights_punctual)).toContain('lights'); + + expect(gltf.extensions.KHR_lights_punctual.lights.length) + .toBe(remaining.lights.length); - ForEach.buffer(gltf, (buffer, index) => { - expect(buffer.name).toBe(remainingBufferNames[index]); + gltf.extensions.KHR_lights_punctual.lights.forEach((element, index) => { + expect(remaining['lights']).toContain(element.name); }); }); }); From 28b94768bca112703f84fed5100ba032281ad0c0 Mon Sep 17 00:00:00 2001 From: Squareys Date: Tue, 19 Mar 2019 18:54:37 +0100 Subject: [PATCH 3/7] Add gltf-pipeline "--keepUnusedElements" CLI argument Signed-off-by: Squareys --- CHANGES.md | 5 +++++ README.md | 1 + bin/gltf-pipeline.js | 5 +++++ lib/processGltf.js | 10 ++++++++++ 4 files changed, 21 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 57660a7b..70f6ef4b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change Log ========== +### HEAD + +* Added removal of unused materials, nodes and meshes. [#465](https://github.com/AnalyticalGraphicsInc/gltf-pipeline/pull/465) +* Added `keepUnusedElements` flag to keep unused materials, nodes and meshes. [#465](https://github.com/AnalyticalGraphicsInc/gltf-pipeline/pull/465) + ### 2.1.3 - 2019-03-21 * Fixed a crash when saving separate resources that would exceed the Node buffer size limit. [#468](https://github.com/AnalyticalGraphicsInc/gltf-pipeline/pull/468) diff --git a/README.md b/README.md index 3ea9c246..122731a6 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ processGltf(gltf, options) |`--separate`, `-s`|Write separate buffers, shaders, and textures instead of embedding them in the glTF.|No, default `false`| |`--separateTextures`, `-t`|Write out separate textures only.|No, default `false`| |`--stats`|Print statistics to console for output glTF file.|No, default `false`| +|`--keepUnusedElements`|Keep unused materials, nodes and meshes.|No, default `false`| |`--draco.compressMeshes`, `-d`|Compress the meshes using Draco. Adds the KHR_draco_mesh_compression extension.|No, default `false`| |`--draco.compressionLevel`|Draco compression level [0-10], most is 10, least is 0. A value of 0 will apply sequential encoding and preserve face order.|No, default `7`| |`--draco.quantizePositionBits`|Quantization bits for position attribute when using Draco compression.|No, default `14`| diff --git a/bin/gltf-pipeline.js b/bin/gltf-pipeline.js index 93d6efdf..53191749 100644 --- a/bin/gltf-pipeline.js +++ b/bin/gltf-pipeline.js @@ -69,6 +69,11 @@ const argv = yargs type: 'boolean', default: defaults.stats }, + keepUnusedElements: { + describe: 'Keep unused materials, nodes and meshes.', + type: 'boolean', + default: false + }, 'draco.compressMeshes': { alias: 'd', describe: 'Compress the meshes using Draco. Adds the KHR_draco_mesh_compression extension.', diff --git a/lib/processGltf.js b/lib/processGltf.js index 08f057ba..c3cf36fc 100644 --- a/lib/processGltf.js +++ b/lib/processGltf.js @@ -7,6 +7,7 @@ const getStatistics = require('./getStatistics'); const readResources = require('./readResources'); const removeDefaults = require('./removeDefaults'); const removePipelineExtras = require('./removePipelineExtras'); +const removeUnusedElements = require('./removeUnusedElements'); const updateVersion = require('./updateVersion'); const writeResources = require('./writeResources'); const compressDracoMeshes = require('./compressDracoMeshes'); @@ -82,6 +83,9 @@ function getStages(options) { if (defined(options.dracoOptions)) { stages.push(compressDracoMeshes); } + if (!defined(options.keepUnusedElements)) { + stages.push((gltf, options) => removeUnusedElements(gltf)); + } return stages; } @@ -113,6 +117,12 @@ processGltf.defaults = { * @default false */ stats: false, + /** + * Keep unused 'node', 'mesh' and 'material' elements. + * @type Boolean + * @default false + */ + keepUnusedElements: false, /** * Gets or sets whether to compress the meshes using Draco. Adds the KHR_draco_mesh_compression extension. * @type Boolean From 58752f7331f089e170620f30c5907756078a4541 Mon Sep 17 00:00:00 2001 From: Squareys Date: Mon, 15 Apr 2019 10:10:08 +0200 Subject: [PATCH 4/7] fixup! Implement removal of unused nodes, materials and meshes Signed-off-by: Squareys --- lib/removeUnusedElements.js | 131 +++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 47 deletions(-) diff --git a/lib/removeUnusedElements.js b/lib/removeUnusedElements.js index 40389729..d936e3d4 100644 --- a/lib/removeUnusedElements.js +++ b/lib/removeUnusedElements.js @@ -18,9 +18,12 @@ module.exports = removeUnusedElements; * * @private */ -function removeUnusedElements(gltf, elementTypes=['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer']) { +function removeUnusedElements(gltf, elementTypes) { + if(elementTypes === undefined) { + elementTypes = ['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer']; + } ['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer'].forEach(function(type) { - if(elementTypes.includes(type)) { + if (elementTypes.includes(type)) { removeUnusedElementsByType(gltf, type); } }); @@ -46,7 +49,7 @@ function removeUnusedElementsByType(gltf, type) { const length = arrayOfObjects.length; for (let i = 0; i < length; ++i) { - if(!usedIds[i]) { + if (!usedIds[i]) { Remove[type](gltf, i - removed); removed++; } @@ -171,11 +174,11 @@ Remove.mesh = function(gltf, meshId) { meshes.splice(meshId, 1); ForEach.node(gltf, function(n) { - if(defined(n.mesh)) { - if(n.mesh > meshId) { + if (defined(n.mesh)) { + if (n.mesh > meshId) { --n.mesh; - } else if(n.mesh === meshId) { - /* Remove reference to deleted mesh */ + } else if (n.mesh === meshId) { + // Remove reference to deleted mesh delete n.mesh; } } @@ -184,25 +187,39 @@ Remove.mesh = function(gltf, meshId) { Remove.node = function(gltf, nodeId) { const nodes = gltf.nodes; - - /* Remove this node from parent */ - const parentId = findParentNode(gltf, nodeId); - if(parentId >= 0) { - nodes[parentId].children = nodes[parentId].children.filter(x => x !== nodeId); - } - nodes.splice(nodeId, 1); - /* Shift all node references */ - nodes.forEach(other => { - if(defined(other.children)) { - other.children = other.children.map(x => x > nodeId ? x - 1 : x); + // Shift all node references + ForEach.skin(gltf, function(s) { + if (s.skeleton) { + s.skeleton -= s.skeleton > nodeId ? 1 : 0; + } + + s.joints -= s.joints.map(function(x) { x > nodeId ? x - 1 : x }); + }); + ForEach.animation(gltf, function(animation) { + if(animation.target && animation.target.node) { + animation.target.node -= animation.target.node > nodeId ? 1 : 0; } }); - ForEach.scene(gltf, (s, i) => { + ForEach.technique(gltf, function(technique) { + ForEach.techniqueUniform(technique, function(uniform) { + if (defined(uniform.node)) { + uniform.node -= uniform.node > nodeId ? 1 : 0; + } + }); + }); + ForEach.node(gltf, function(n) { + if (!n.children) return; + + n.children = n.children + .filter(function(x) { x !== nodeId }) // Remove + .map(function(x) { x > nodeId ? x - 1 : x }); // Shift indices + }); + ForEach.scene(gltf, function(s, i) { s.nodes = s.nodes - .filter(x => x !== nodeId) /* Remove */ - .map(x => x > nodeId ? x - 1 : x); /* Shift indices */ + .filter(function(x) { x !== nodeId }) // Remove + .map(function(x) { x > nodeId ? x - 1 : x }); // Shift indices }); }; @@ -210,13 +227,11 @@ Remove.material = function(gltf, materialId) { const materials = gltf.materials; materials.splice(materialId, 1); - /* Shift other material ids */ - ForEach.mesh(gltf, (m, j) => { - m.primitives.forEach(p => { - if(p.material > materialId) { - --p.material; - } - }); + // Shift other material ids + ForEach.meshPrimitive(gltf, function(p) { + if (p.material > materialId) { + --p.material; + } }); return gltf; @@ -327,20 +342,20 @@ getListOfElementsIdsInUse.bufferView = function(gltf) { getListOfElementsIdsInUse.mesh = function(gltf) { const usedMeshIds = {}; - ForEach.mesh(gltf, (mesh, i) => { - if(!defined(mesh.primitives) || mesh.primitives.length === 0) { + ForEach.mesh(gltf, function(mesh, i) { + if (!defined(mesh.primitives) || mesh.primitives.length === 0) { usedMeshIds[i] = false; } }); - ForEach.node(gltf, (node, i) => { - if(!defined(node.mesh)) { + ForEach.node(gltf, function(node, i) { + if (!defined(node.mesh)) { return; } - /* Mesh marked as empty in previous step? */ + // Mesh marked as empty in previous step? const meshIsEmpty = defined(usedMeshIds[node.mesh]); - if(!meshIsEmpty) { + if (!meshIsEmpty) { usedMeshIds[node.mesh] = true; } }); @@ -348,40 +363,61 @@ getListOfElementsIdsInUse.mesh = function(gltf) { return usedMeshIds; }; -/* Find parent node id of the given node, or -1 if no parent */ +// Find parent node id of the given node, or -1 if no parent function findParentNode(gltf, node) { - return gltf.nodes.findIndex(n => (n.children || []).includes(node)); + return gltf.nodes.findIndex(function(n) { (n.children || []).includes(node) }); } /* Check if node is empty. It is considered empty if neither referencing * mesh, camera, extensions and has no children */ function nodeIsEmpty(gltf, node) { - if(defined(node.mesh) || defined(node.camera) || defined(node.skin) + if (defined(node.mesh) || defined(node.camera) || defined(node.skin) || defined(node.weights) || defined(node.extras) || (defined(node.extensions) && node.extensions.length !== 0)) { return false; } - /* Empty if no children or children are all empty nodes */ + // Empty if no children or children are all empty nodes return !defined(node.children) - || node.children.filter(n => !nodeIsEmpty(gltf, gltf.nodes[n])).length === 0; + || node.children.filter(function(n) { !nodeIsEmpty(gltf, gltf.nodes[n]) }).length === 0; } getListOfElementsIdsInUse.node = function(gltf) { const usedNodeIds = {}; - ForEach.node(gltf, (node, nodeId) => { - if(!nodeIsEmpty(gltf, node)) { + ForEach.node(gltf, function(node, nodeId) { + if (!nodeIsEmpty(gltf, node)) { usedNodeIds[nodeId] = true; } }); + ForEach.skin(gltf, function(s) { + if (s.skeleton) { + usedNodeIds[s.skeleton] = true; + } + + ForEach.joint(s, function(j) { + usedNodeIds[j] = true; + }); + }); + ForEach.animation(gltf, function(animation) { + if(animation.target && animation.target.node) { + usedNodeIds[animation.target.node] = true; + } + }); + ForEach.technique(gltf, function(technique) { + ForEach.techniqueUniform(technique, function(uniform) { + if (defined(uniform.node)) { + usedNodeIds[uniform.node] = true; + } + }); + }); - ForEach.animation(gltf, (anim, animId) => { - if(!defined(anim.channels)) { + ForEach.animation(gltf, function(anim, animId) { + if (!defined(anim.channels)) { return; } - anim.channels.forEach(c => { - if(defined(c.target) && defined(c.target.node)) { + anim.channels.forEach(function(c) { + if (defined(c.target) && defined(c.target.node)) { /* Keep all nodes that are being targeted * by an animation */ usedNodeIds[c.target.node] = true; @@ -394,8 +430,9 @@ getListOfElementsIdsInUse.node = function(gltf) { getListOfElementsIdsInUse.material = function(gltf) { const usedMaterialIds = {}; - gltf.meshes.forEach((m, i) => { - m.primitives.forEach((p, i) => { + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(p) { usedMaterialIds[p.material] = true; }); }); From 0762b7b4a633929aec4a7c3ca9eb6bbea9dfefc3 Mon Sep 17 00:00:00 2001 From: Squareys Date: Mon, 15 Apr 2019 10:10:34 +0200 Subject: [PATCH 5/7] fixup! Add gltf-pipeline "--keepUnusedElements" CLI argument Signed-off-by: Squareys --- bin/gltf-pipeline.js | 2 +- lib/processGltf.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/gltf-pipeline.js b/bin/gltf-pipeline.js index 53191749..f4b5ed51 100644 --- a/bin/gltf-pipeline.js +++ b/bin/gltf-pipeline.js @@ -72,7 +72,7 @@ const argv = yargs keepUnusedElements: { describe: 'Keep unused materials, nodes and meshes.', type: 'boolean', - default: false + default: defaults.keepUnusedElements }, 'draco.compressMeshes': { alias: 'd', diff --git a/lib/processGltf.js b/lib/processGltf.js index c3cf36fc..10d316fa 100644 --- a/lib/processGltf.js +++ b/lib/processGltf.js @@ -83,7 +83,7 @@ function getStages(options) { if (defined(options.dracoOptions)) { stages.push(compressDracoMeshes); } - if (!defined(options.keepUnusedElements)) { + if (!options.keepUnusedElements) { stages.push((gltf, options) => removeUnusedElements(gltf)); } return stages; From e66fa774457f8bc4aa8c7f7da66722d3f22ef2e0 Mon Sep 17 00:00:00 2001 From: Squareys Date: Mon, 15 Apr 2019 10:16:48 +0200 Subject: [PATCH 6/7] fixup! Implement removal of unused nodes, materials and meshes Signed-off-by: Squareys --- lib/removeUnusedElements.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/removeUnusedElements.js b/lib/removeUnusedElements.js index d936e3d4..e764b3f0 100644 --- a/lib/removeUnusedElements.js +++ b/lib/removeUnusedElements.js @@ -195,7 +195,9 @@ Remove.node = function(gltf, nodeId) { s.skeleton -= s.skeleton > nodeId ? 1 : 0; } - s.joints -= s.joints.map(function(x) { x > nodeId ? x - 1 : x }); + s.joints -= s.joints.map(function(x) { + return x > nodeId ? x - 1 : x; + }); }); ForEach.animation(gltf, function(animation) { if(animation.target && animation.target.node) { @@ -210,16 +212,26 @@ Remove.node = function(gltf, nodeId) { }); }); ForEach.node(gltf, function(n) { - if (!n.children) return; + if (!n.children) { + return; + } n.children = n.children - .filter(function(x) { x !== nodeId }) // Remove - .map(function(x) { x > nodeId ? x - 1 : x }); // Shift indices + .filter(function(x) { + return x !== nodeId; // Remove + }) + .map(function(x) { + return x > nodeId ? x - 1 : x; // Shift indices + }); }); ForEach.scene(gltf, function(s, i) { s.nodes = s.nodes - .filter(function(x) { x !== nodeId }) // Remove - .map(function(x) { x > nodeId ? x - 1 : x }); // Shift indices + .filter(function(x) { + return x !== nodeId; // Remove + }) + .map(function(x) { + return x > nodeId ? x - 1 : x; // Shift indices + }); }); }; @@ -363,11 +375,6 @@ getListOfElementsIdsInUse.mesh = function(gltf) { return usedMeshIds; }; -// Find parent node id of the given node, or -1 if no parent -function findParentNode(gltf, node) { - return gltf.nodes.findIndex(function(n) { (n.children || []).includes(node) }); -} - /* Check if node is empty. It is considered empty if neither referencing * mesh, camera, extensions and has no children */ function nodeIsEmpty(gltf, node) { @@ -379,7 +386,9 @@ function nodeIsEmpty(gltf, node) { // Empty if no children or children are all empty nodes return !defined(node.children) - || node.children.filter(function(n) { !nodeIsEmpty(gltf, gltf.nodes[n]) }).length === 0; + || node.children.filter(function(n) { + return !nodeIsEmpty(gltf, gltf.nodes[n]); + }).length === 0; } getListOfElementsIdsInUse.node = function(gltf) { From 145640eb6ff41b67143e623f52fb378ffeb63654 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Sat, 8 Jun 2019 12:20:11 -0400 Subject: [PATCH 7/7] Cleanup --- lib/ForEach.js | 15 +++ lib/processGltf.js | 4 +- lib/removeUnusedElements.js | 137 +++++++++++--------------- specs/lib/removeUnusedElementsSpec.js | 8 +- 4 files changed, 81 insertions(+), 83 deletions(-) diff --git a/lib/ForEach.js b/lib/ForEach.js index 93a64482..c2953b3a 100644 --- a/lib/ForEach.js +++ b/lib/ForEach.js @@ -348,6 +348,21 @@ ForEach.skin = function(gltf, handler) { return ForEach.topLevel(gltf, 'skins', handler); }; +ForEach.skinJoint = function(skin, handler) { + const joints = skin.joints; + if (defined(joints)) { + const jointsLength = joints.length; + for (let i = 0; i < jointsLength; i++) { + const joint = joints[i]; + const value = handler(joint); + + if (defined(value)) { + return value; + } + } + } +}; + ForEach.techniqueAttribute = function(technique, handler) { const attributes = technique.attributes; for (const attributeName in attributes) { diff --git a/lib/processGltf.js b/lib/processGltf.js index 10d316fa..bed45c07 100644 --- a/lib/processGltf.js +++ b/lib/processGltf.js @@ -84,7 +84,9 @@ function getStages(options) { stages.push(compressDracoMeshes); } if (!options.keepUnusedElements) { - stages.push((gltf, options) => removeUnusedElements(gltf)); + stages.push(function(gltf, options) { + removeUnusedElements(gltf); + }); } return stages; } diff --git a/lib/removeUnusedElements.js b/lib/removeUnusedElements.js index e764b3f0..b3c991ce 100644 --- a/lib/removeUnusedElements.js +++ b/lib/removeUnusedElements.js @@ -3,27 +3,25 @@ const Cesium = require('cesium'); const ForEach = require('./ForEach'); const hasExtension = require('./hasExtension'); +const defaultValue = Cesium.defaultValue; const defined = Cesium.defined; module.exports = removeUnusedElements; +const allElementTypes = ['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer']; + /** * Removes unused elements from gltf. - * This function currently only works for accessors, buffers, and bufferViews. * * @param {Object} gltf A javascript object containing a glTF asset. - * @param {Array} elementTypes Element types to be removed. Needs to be a subset of - * ['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer'], other items - * will be ignored. + * @param {String[]} [elementTypes=['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer']] Element types to be removed. Needs to be a subset of ['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer'], other items will be ignored. * * @private */ function removeUnusedElements(gltf, elementTypes) { - if(elementTypes === undefined) { - elementTypes = ['node', 'mesh', 'material', 'accessor', 'bufferView', 'buffer']; - } - ['mesh', 'node', 'material', 'accessor', 'bufferView', 'buffer'].forEach(function(type) { - if (elementTypes.includes(type)) { + elementTypes = defaultValue(elementTypes, allElementTypes); + allElementTypes.forEach(function(type) { + if (elementTypes.indexOf(type) > -1) { removeUnusedElementsByType(gltf, type); } }); @@ -156,8 +154,8 @@ Remove.bufferView = function(gltf, bufferViewId) { }); if (hasExtension(gltf, 'KHR_draco_mesh_compression')) { - ForEach.mesh(gltf, function (mesh) { - ForEach.meshPrimitive(mesh, function (primitive) { + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { if (defined(primitive.extensions) && defined(primitive.extensions.KHR_draco_mesh_compression)) { if (primitive.extensions.KHR_draco_mesh_compression.bufferView > bufferViewId) { @@ -173,13 +171,13 @@ Remove.mesh = function(gltf, meshId) { const meshes = gltf.meshes; meshes.splice(meshId, 1); - ForEach.node(gltf, function(n) { - if (defined(n.mesh)) { - if (n.mesh > meshId) { - --n.mesh; - } else if (n.mesh === meshId) { + ForEach.node(gltf, function(node) { + if (defined(node.mesh)) { + if (node.mesh > meshId) { + node.mesh--; + } else if (node.mesh === meshId) { // Remove reference to deleted mesh - delete n.mesh; + delete node.mesh; } } }); @@ -190,33 +188,35 @@ Remove.node = function(gltf, nodeId) { nodes.splice(nodeId, 1); // Shift all node references - ForEach.skin(gltf, function(s) { - if (s.skeleton) { - s.skeleton -= s.skeleton > nodeId ? 1 : 0; + ForEach.skin(gltf, function(skin) { + if (defined(skin.skeleton) && skin.skeleton > nodeId) { + skin.skeleton--; } - s.joints -= s.joints.map(function(x) { + skin.joints = skin.joints.map(function(x) { return x > nodeId ? x - 1 : x; }); }); ForEach.animation(gltf, function(animation) { - if(animation.target && animation.target.node) { - animation.target.node -= animation.target.node > nodeId ? 1 : 0; - } + ForEach.animationChannel(animation, function(channel) { + if (defined(channel.target) && defined(channel.target.node) && (channel.target.node > nodeId)) { + channel.target.node--; + } + }); }); ForEach.technique(gltf, function(technique) { ForEach.techniqueUniform(technique, function(uniform) { - if (defined(uniform.node)) { - uniform.node -= uniform.node > nodeId ? 1 : 0; + if (defined(uniform.node) && uniform.node > nodeId) { + uniform.node--; } }); }); - ForEach.node(gltf, function(n) { - if (!n.children) { + ForEach.node(gltf, function(node) { + if (!defined(node.children)) { return; } - n.children = n.children + node.children = node.children .filter(function(x) { return x !== nodeId; // Remove }) @@ -224,8 +224,8 @@ Remove.node = function(gltf, nodeId) { return x > nodeId ? x - 1 : x; // Shift indices }); }); - ForEach.scene(gltf, function(s, i) { - s.nodes = s.nodes + ForEach.scene(gltf, function(scene) { + scene.nodes = scene.nodes .filter(function(x) { return x !== nodeId; // Remove }) @@ -240,13 +240,13 @@ Remove.material = function(gltf, materialId) { materials.splice(materialId, 1); // Shift other material ids - ForEach.meshPrimitive(gltf, function(p) { - if (p.material > materialId) { - --p.material; - } + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.material) && primitive.material > materialId) { + primitive.material--; + } + }); }); - - return gltf; }; /** @@ -354,29 +354,20 @@ getListOfElementsIdsInUse.bufferView = function(gltf) { getListOfElementsIdsInUse.mesh = function(gltf) { const usedMeshIds = {}; - ForEach.mesh(gltf, function(mesh, i) { - if (!defined(mesh.primitives) || mesh.primitives.length === 0) { - usedMeshIds[i] = false; - } - }); - - ForEach.node(gltf, function(node, i) { - if (!defined(node.mesh)) { - return; - } - // Mesh marked as empty in previous step? - const meshIsEmpty = defined(usedMeshIds[node.mesh]); - - if (!meshIsEmpty) { - usedMeshIds[node.mesh] = true; + ForEach.node(gltf, function(node) { + if (defined(node.mesh && defined(gltf.meshes))) { + const mesh = gltf.meshes[node.mesh]; + if (defined(mesh) && defined(mesh.primitives) && (mesh.primitives.length > 0)) { + usedMeshIds[node.mesh] = true; + } } }); return usedMeshIds; }; -/* Check if node is empty. It is considered empty if neither referencing - * mesh, camera, extensions and has no children */ +// Check if node is empty. It is considered empty if neither referencing +// mesh, camera, extensions and has no children function nodeIsEmpty(gltf, node) { if (defined(node.mesh) || defined(node.camera) || defined(node.skin) || defined(node.weights) || defined(node.extras) @@ -398,19 +389,21 @@ getListOfElementsIdsInUse.node = function(gltf) { usedNodeIds[nodeId] = true; } }); - ForEach.skin(gltf, function(s) { - if (s.skeleton) { - usedNodeIds[s.skeleton] = true; + ForEach.skin(gltf, function(skin) { + if (defined(skin.skeleton)) { + usedNodeIds[skin.skeleton] = true; } - ForEach.joint(s, function(j) { - usedNodeIds[j] = true; + ForEach.skinJoint(skin, function(joint) { + usedNodeIds[joint] = true; }); }); ForEach.animation(gltf, function(animation) { - if(animation.target && animation.target.node) { - usedNodeIds[animation.target.node] = true; - } + ForEach.animationChannel(animation, function(channel) { + if (defined(channel.target) && defined(channel.target.node)) { + usedNodeIds[channel.target.node] = true; + } + }); }); ForEach.technique(gltf, function(technique) { ForEach.techniqueUniform(technique, function(uniform) { @@ -420,20 +413,6 @@ getListOfElementsIdsInUse.node = function(gltf) { }); }); - ForEach.animation(gltf, function(anim, animId) { - if (!defined(anim.channels)) { - return; - } - - anim.channels.forEach(function(c) { - if (defined(c.target) && defined(c.target.node)) { - /* Keep all nodes that are being targeted - * by an animation */ - usedNodeIds[c.target.node] = true; - } - }); - }); - return usedNodeIds; }; @@ -441,8 +420,10 @@ getListOfElementsIdsInUse.material = function(gltf) { const usedMaterialIds = {}; ForEach.mesh(gltf, function(mesh) { - ForEach.meshPrimitive(mesh, function(p) { - usedMaterialIds[p.material] = true; + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.material)) { + usedMaterialIds[primitive.material] = true; + } }); }); diff --git a/specs/lib/removeUnusedElementsSpec.js b/specs/lib/removeUnusedElementsSpec.js index a84a0c26..4111e4eb 100644 --- a/specs/lib/removeUnusedElementsSpec.js +++ b/specs/lib/removeUnusedElementsSpec.js @@ -379,13 +379,13 @@ describe('removeUnusedElements', () => { expect(Object.keys(gltf)).toContain(k); expect(gltf[k].length).toBe(remaining[k].length); - /* Check that at least the remaining elements are present */ - ForEach.topLevel(gltf, k, (element, index) => { + // Check that at least the remaining elements are present + ForEach.topLevel(gltf, k, (element) => { expect(remaining[k]).toContain(element.name); }); - /* Check that all the elements should actually remain */ - remaining[k].forEach((name, index) => { + // Check that all the elements should actually remain + remaining[k].forEach((name) => { expect(gltf[k].map(x => x.name)).toContain(name); }); });