├── .npmrc ├── .eslintignore ├── doc └── gltf.png ├── .husky └── pre-commit ├── .gitattributes ├── specs ├── jasmine.json ├── data │ ├── 1.0 │ │ ├── box-textured-separate │ │ │ ├── cesium.png │ │ │ ├── box-textured-separate.bin │ │ │ ├── box-textured-separate-vs.glsl │ │ │ └── box-textured-separate-fs.glsl │ │ ├── box-textured-binary-separate │ │ │ ├── cesium.png │ │ │ ├── box-textured-binary-separate.glb │ │ │ ├── box-textured-binary-separate-vs.glsl │ │ │ └── box-textured-binary-separate-fs.glsl │ │ ├── box-materials-common │ │ │ ├── box-materials-common.bin │ │ │ └── box-materials-common.gltf │ │ ├── box-textured-binary │ │ │ └── box-textured-binary.glb │ │ └── box-textured-materials-common │ │ │ ├── CesiumLogoFlat.png │ │ │ ├── box-textured-materials-common.bin │ │ │ └── box-textured-materials-common.gltf │ └── 2.0 │ │ ├── box-techniques-separate │ │ ├── Image0001.png │ │ ├── CesiumTexturedBoxTest.bin │ │ ├── CesiumTexturedBoxTest0VS.glsl │ │ ├── CesiumTexturedBoxTest0FS.glsl │ │ └── box-techniques-separate.gltf │ │ ├── box-textured-separate │ │ ├── cesium logo.png │ │ ├── box-textured-separate.bin │ │ └── box-textured-separate.gltf │ │ ├── box-textured-binary-separate │ │ ├── cesium.png │ │ └── box-textured-binary-separate.glb │ │ ├── box-textured-binary │ │ └── box-textured-binary.glb │ │ ├── box-shared-image-references-separate │ │ ├── Image.png │ │ ├── box-shared-image-references-separate.bin │ │ ├── CesiumTexturedBoxTest0VS.glsl │ │ ├── CesiumTexturedBoxTest0FS.glsl │ │ └── box-shared-image-references-separate.gltf │ │ ├── extensions │ │ ├── EXT_texture_webp │ │ │ └── box-textured-separate │ │ │ │ ├── cesium logo.png │ │ │ │ ├── cesium logo.webp │ │ │ │ ├── box-textured-separate.bin │ │ │ │ ├── box-textured-separate.gltf │ │ │ │ └── box-textured-with-fallback.gltf │ │ └── EXT_meshopt_compression │ │ │ ├── meshopt-fallback │ │ │ ├── meshopt-fallback.bin │ │ │ ├── meshopt-fallback.fallback.bin │ │ │ └── meshopt-fallback.gltf │ │ │ └── meshopt-no-fallback │ │ │ ├── meshopt-no-fallback.bin │ │ │ └── meshopt-no-fallback.gltf │ │ └── triangle-without-indices │ │ └── triangle-without-indices.gltf ├── .eslintrc.json └── lib │ ├── usesExtensionSpec.js │ ├── numberOfComponentsForTypeSpec.js │ ├── addExtensionsUsedSpec.js │ ├── processGlbSpec.js │ ├── dataUriToBufferSpec.js │ ├── glbToGltfSpec.js │ ├── removeDefaultsSpec.js │ ├── addExtensionsRequiredSpec.js │ ├── addBufferSpec.js │ ├── removeExtensionsRequiredSpec.js │ ├── removeExtensionsUsedSpec.js │ ├── removePipelineExtrasSpec.js │ ├── addToArraySpec.js │ ├── getJsonBufferPaddedSpec.js │ ├── getBufferPaddedSpec.js │ ├── getComponentReaderSpec.js │ ├── addPipelineExtrasSpec.js │ ├── getAccessorByteStrideSpec.js │ ├── readAccessorPackedSpec.js │ ├── findAccessorMinMaxSpec.js │ ├── getStatisticsSpec.js │ ├── removeExtensionSpec.js │ ├── getImageExtensionSpec.js │ ├── parseGlbSpec.js │ ├── updateAccessorComponentTypeSpec.js │ ├── gltfToGlbSpec.js │ ├── moveTechniquesToExtensionSpec.js │ └── readResourcesSpec.js ├── .eslintrc.json ├── .prettierignore ├── .npmignore ├── .gitignore ├── index.js ├── lib ├── dataUriToBuffer.js ├── glbToGltf.js ├── processGlb.js ├── usesExtension.js ├── numberOfComponentsForType.js ├── addToArray.js ├── addExtensionsUsed.js ├── getBufferPadded.js ├── removeExtensionsRequired.js ├── addBuffer.js ├── removeDefaults.js ├── addExtensionsRequired.js ├── removeExtensionsUsed.js ├── getAccessorByteStride.js ├── addPipelineExtras.js ├── removePipelineExtras.js ├── getJsonBufferPadded.js ├── getImageExtension.js ├── readAccessorPacked.js ├── updateAccessorComponentTypes.js ├── removeExtension.js ├── gltfToGlb.js ├── findAccessorMinMax.js ├── parseGlb.js ├── moveTechniqueRenderStates.js ├── getComponentReader.js ├── moveTechniquesToExtension.js ├── forEachTextureInMaterial.js ├── processGltf.js ├── mergeBuffers.js ├── getStatistics.js └── readResources.js ├── .appveyor.yml ├── .github └── workflows │ └── build.yml ├── ThirdParty.json └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | coverage/** 3 | doc/** 4 | dist/** 5 | -------------------------------------------------------------------------------- /doc/gltf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/doc/gltf.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run pre-commit 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | package.json text eol=lf -------------------------------------------------------------------------------- /specs/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "specs", 3 | "spec_files": [ 4 | "**/*Spec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["cesium/node"], 3 | "rules": { 4 | "no-unused-vars": ["error", {"args": "none"}] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-separate/cesium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-separate/cesium.png -------------------------------------------------------------------------------- /specs/data/2.0/box-techniques-separate/Image0001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-techniques-separate/Image0001.png -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-separate/cesium logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-textured-separate/cesium logo.png -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-binary-separate/cesium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-binary-separate/cesium.png -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-binary-separate/cesium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-textured-binary-separate/cesium.png -------------------------------------------------------------------------------- /specs/data/1.0/box-materials-common/box-materials-common.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-materials-common/box-materials-common.bin -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-binary/box-textured-binary.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-binary/box-textured-binary.glb -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-binary/box-textured-binary.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-textured-binary/box-textured-binary.glb -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-separate/box-textured-separate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-separate/box-textured-separate.bin -------------------------------------------------------------------------------- /specs/data/2.0/box-shared-image-references-separate/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-shared-image-references-separate/Image.png -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-separate/box-textured-separate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-textured-separate/box-textured-separate.bin -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-materials-common/CesiumLogoFlat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-materials-common/CesiumLogoFlat.png -------------------------------------------------------------------------------- /specs/data/2.0/box-techniques-separate/CesiumTexturedBoxTest.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-techniques-separate/CesiumTexturedBoxTest.bin -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-binary-separate/box-textured-binary-separate.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-binary-separate/box-textured-binary-separate.glb -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-binary-separate/box-textured-binary-separate.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-textured-binary-separate/box-textured-binary-separate.glb -------------------------------------------------------------------------------- /specs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc.json", 3 | "env": { 4 | "jasmine": true 5 | }, 6 | "rules": { 7 | "no-restricted-globals": ["error", "fdescribe", "fit"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-materials-common/box-textured-materials-common.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/1.0/box-textured-materials-common/box-textured-materials-common.bin -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/cesium logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/cesium logo.png -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/cesium logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/cesium logo.webp -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # Unignore directories (to all depths) and unignore code in these directories 5 | !bin/**/ 6 | !doc/**/ 7 | !lib/**/ 8 | !specs/**/ 9 | 10 | !**/*.js 11 | !**/*.md 12 | -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.bin -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/box-textured-separate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/box-textured-separate.bin -------------------------------------------------------------------------------- /specs/data/2.0/box-shared-image-references-separate/box-shared-image-references-separate.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/box-shared-image-references-separate/box-shared-image-references-separate.bin -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-no-fallback/meshopt-no-fallback.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-no-fallback/meshopt-no-fallback.bin -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.fallback.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/gltf-pipeline/main/specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.fallback.bin -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.husky 2 | /.vscode 3 | /.idea 4 | /dist 5 | /coverage 6 | /doc/* 7 | /output 8 | /specs 9 | /test 10 | .editorconfig 11 | .eslintcache 12 | .eslintignore 13 | .eslintrc.json 14 | .gitattributes 15 | .nyc_output 16 | .npmignore 17 | .prettierignore 18 | .travis.yml 19 | .appveyor.yml 20 | gulpfile.js 21 | *.tgz 22 | !doc/gltf.png 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM 2 | node_modules 3 | npm-debug.log 4 | package-lock.json 5 | yarn.lock 6 | 7 | # WebStorm user-specific 8 | .idea/workspace.xml 9 | .idea/tasks.xml 10 | 11 | # VSCode user-specific 12 | .vscode 13 | 14 | # Generate data 15 | .eslintcache 16 | coverage 17 | dist 18 | output 19 | test 20 | *.tgz 21 | .nyc_output 22 | doc/* 23 | !doc/gltf.png 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable n/global-require*/ 2 | "use strict"; 3 | module.exports = { 4 | getStatistics: require("./lib/getStatistics"), 5 | glbToGltf: require("./lib/glbToGltf"), 6 | gltfToGlb: require("./lib/gltfToGlb"), 7 | processGlb: require("./lib/processGlb"), 8 | processGltf: require("./lib/processGltf"), 9 | removeExtension: require("./lib/removeExtension"), 10 | }; 11 | -------------------------------------------------------------------------------- /lib/dataUriToBuffer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = dataUriToBuffer; 3 | 4 | /** 5 | * Read a data uri string into a buffer. 6 | * 7 | * @param {string} dataUri The data uri. 8 | * @returns {Buffer} 9 | * 10 | * @private 11 | */ 12 | function dataUriToBuffer(dataUri) { 13 | const data = dataUri.slice(dataUri.indexOf(",") + 1); 14 | if (dataUri.indexOf("base64") >= 0) { 15 | return Buffer.from(data, "base64"); 16 | } 17 | return Buffer.from(data, "utf8"); 18 | } 19 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | # Builds are whole numbered 2 | version: '{build}' 3 | 4 | clone_depth: 50 5 | 6 | environment: 7 | matrix: 8 | - nodejs_version: "12" 9 | 10 | install: 11 | # Get the latest stable version of Node.js 12 | - ps: Install-Product node $env:nodejs_version 13 | # install modules 14 | - npm install 15 | 16 | test_script: 17 | - node --version 18 | - npm --version 19 | - npm run eslint 20 | - npm run test -- --failTaskOnError --suppressPassed 21 | 22 | # Don't actually build. 23 | build: off 24 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-separate/box-textured-separate-vs.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec3 a_position; 3 | attribute vec3 a_normal; 4 | varying vec3 v_normal; 5 | uniform mat3 u_normalMatrix; 6 | uniform mat4 u_modelViewMatrix; 7 | uniform mat4 u_projectionMatrix; 8 | attribute vec2 a_texcoord0; 9 | varying vec2 v_texcoord0; 10 | void main(void) { 11 | vec4 pos = u_modelViewMatrix * vec4(a_position,1.0); 12 | v_normal = u_normalMatrix * a_normal; 13 | v_texcoord0 = a_texcoord0; 14 | gl_Position = u_projectionMatrix * pos; 15 | } 16 | -------------------------------------------------------------------------------- /specs/data/2.0/box-techniques-separate/CesiumTexturedBoxTest0VS.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec3 a_position; 3 | attribute vec3 a_normal; 4 | varying vec3 v_normal; 5 | uniform mat3 u_normalMatrix; 6 | uniform mat4 u_modelViewMatrix; 7 | uniform mat4 u_projectionMatrix; 8 | attribute vec2 a_texcoord0; 9 | varying vec2 v_texcoord0; 10 | void main(void) { 11 | vec4 pos = u_modelViewMatrix * vec4(a_position,1.0); 12 | v_normal = u_normalMatrix * a_normal; 13 | v_texcoord0 = a_texcoord0; 14 | gl_Position = u_projectionMatrix * pos; 15 | } 16 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-binary-separate/box-textured-binary-separate-vs.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec3 a_position; 3 | attribute vec3 a_normal; 4 | varying vec3 v_normal; 5 | uniform mat3 u_normalMatrix; 6 | uniform mat4 u_modelViewMatrix; 7 | uniform mat4 u_projectionMatrix; 8 | attribute vec2 a_texcoord0; 9 | varying vec2 v_texcoord0; 10 | void main(void) { 11 | vec4 pos = u_modelViewMatrix * vec4(a_position,1.0); 12 | v_normal = u_normalMatrix * a_normal; 13 | v_texcoord0 = a_texcoord0; 14 | gl_Position = u_projectionMatrix * pos; 15 | } 16 | -------------------------------------------------------------------------------- /specs/data/2.0/box-shared-image-references-separate/CesiumTexturedBoxTest0VS.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec3 a_position; 3 | attribute vec3 a_normal; 4 | varying vec3 v_normal; 5 | uniform mat3 u_normalMatrix; 6 | uniform mat4 u_modelViewMatrix; 7 | uniform mat4 u_projectionMatrix; 8 | attribute vec2 a_texcoord0; 9 | varying vec2 v_texcoord0; 10 | void main(void) { 11 | vec4 pos = u_modelViewMatrix * vec4(a_position,1.0); 12 | v_normal = u_normalMatrix * a_normal; 13 | v_texcoord0 = a_texcoord0; 14 | gl_Position = u_projectionMatrix * pos; 15 | } 16 | -------------------------------------------------------------------------------- /specs/lib/usesExtensionSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const usesExtension = require("../../lib/usesExtension"); 3 | 4 | describe("usesExtension", () => { 5 | it("uses extension", () => { 6 | const gltf = { 7 | extensionsUsed: ["extension1", "extension2"], 8 | }; 9 | expect(usesExtension(gltf, "extension1")).toBe(true); 10 | expect(usesExtension(gltf, "extension2")).toBe(true); 11 | expect(usesExtension(gltf, "extension3")).toBe(false); 12 | 13 | const emptyGltf = {}; 14 | expect(usesExtension(emptyGltf, "extension1")).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | run-name: build 2 | on: 3 | push: 4 | concurrency: 5 | group: ${{ github.ref }} 6 | cancel-in-progress: true 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: [ 16, 20 ] 13 | name: Node ${{ matrix.node }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: npm install 20 | - run: npm run eslint 21 | - run: npm run prettier-check 22 | - run: npm run test -- --failTaskOnError --suppressPassed 23 | -------------------------------------------------------------------------------- /lib/glbToGltf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const parseGlb = require("./parseGlb"); 3 | const processGltf = require("./processGltf"); 4 | 5 | module.exports = glbToGltf; 6 | 7 | /** 8 | * Convert a glb to glTF. 9 | * 10 | * @param {Buffer} glb A buffer containing the glb contents. 11 | * @param {object} [options] The same options object as {@link processGltf} 12 | * @returns {Promise} A promise that resolves to an object containing the glTF and a dictionary containing separate resources. 13 | */ 14 | function glbToGltf(glb, options) { 15 | const gltf = parseGlb(glb); 16 | return processGltf(gltf, options); 17 | } 18 | -------------------------------------------------------------------------------- /lib/processGlb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const parseGlb = require("./parseGlb"); 3 | const gltfToGlb = require("./gltfToGlb"); 4 | 5 | module.exports = processGlb; 6 | 7 | /** 8 | * Run a glb through the gltf-pipeline. 9 | * 10 | * @param {Buffer} glb A buffer containing the glb contents. 11 | * @param {object} [options] The same options object as {@link processGltf} 12 | * @returns {Promise} A promise that resolves to an object containing the glb and a dictionary containing separate resources. 13 | */ 14 | function processGlb(glb, options) { 15 | const gltf = parseGlb(glb); 16 | return gltfToGlb(gltf, options); 17 | } 18 | -------------------------------------------------------------------------------- /lib/usesExtension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const defined = Cesium.defined; 5 | 6 | module.exports = usesExtension; 7 | 8 | /** 9 | * Checks whether the glTF uses the given extension. 10 | * 11 | * @param {object} gltf A javascript object containing a glTF asset. 12 | * @param {string} extension The name of the extension. 13 | * @returns {boolean} Whether the glTF uses the given extension. 14 | * 15 | * @private 16 | */ 17 | function usesExtension(gltf, extension) { 18 | return ( 19 | defined(gltf.extensionsUsed) && gltf.extensionsUsed.indexOf(extension) >= 0 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-separate/box-textured-separate-fs.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec3 v_normal; 3 | varying vec2 v_texcoord0; 4 | uniform sampler2D u_diffuse; 5 | uniform vec4 u_specular; 6 | uniform float u_shininess; 7 | void main(void) { 8 | vec3 normal = normalize(v_normal); 9 | vec4 color = vec4(0., 0., 0., 0.); 10 | vec4 diffuse = vec4(0., 0., 0., 1.); 11 | vec4 specular; 12 | diffuse = texture2D(u_diffuse, v_texcoord0); 13 | specular = u_specular; 14 | diffuse.xyz *= max(dot(normal,vec3(0.,0.,1.)), 0.); 15 | color.xyz += diffuse.xyz; 16 | color = vec4(color.rgb * diffuse.a, diffuse.a); 17 | gl_FragColor = color; 18 | } 19 | -------------------------------------------------------------------------------- /specs/data/2.0/box-techniques-separate/CesiumTexturedBoxTest0FS.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec3 v_normal; 3 | varying vec2 v_texcoord0; 4 | uniform sampler2D u_diffuse; 5 | uniform vec4 u_specular; 6 | uniform float u_shininess; 7 | void main(void) { 8 | vec3 normal = normalize(v_normal); 9 | vec4 color = vec4(0., 0., 0., 0.); 10 | vec4 diffuse = vec4(0., 0., 0., 1.); 11 | vec4 specular; 12 | diffuse = texture2D(u_diffuse, v_texcoord0); 13 | specular = u_specular; 14 | diffuse.xyz *= max(dot(normal,vec3(0.,0.,1.)), 0.); 15 | color.xyz += diffuse.xyz; 16 | color = vec4(color.rgb * diffuse.a, diffuse.a); 17 | gl_FragColor = color; 18 | } 19 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-binary-separate/box-textured-binary-separate-fs.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec3 v_normal; 3 | varying vec2 v_texcoord0; 4 | uniform sampler2D u_diffuse; 5 | uniform vec4 u_specular; 6 | uniform float u_shininess; 7 | void main(void) { 8 | vec3 normal = normalize(v_normal); 9 | vec4 color = vec4(0., 0., 0., 0.); 10 | vec4 diffuse = vec4(0., 0., 0., 1.); 11 | vec4 specular; 12 | diffuse = texture2D(u_diffuse, v_texcoord0); 13 | specular = u_specular; 14 | diffuse.xyz *= max(dot(normal,vec3(0.,0.,1.)), 0.); 15 | color.xyz += diffuse.xyz; 16 | color = vec4(color.rgb * diffuse.a, diffuse.a); 17 | gl_FragColor = color; 18 | } 19 | -------------------------------------------------------------------------------- /specs/data/2.0/box-shared-image-references-separate/CesiumTexturedBoxTest0FS.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | varying vec3 v_normal; 3 | varying vec2 v_texcoord0; 4 | uniform sampler2D u_diffuse; 5 | uniform vec4 u_specular; 6 | uniform float u_shininess; 7 | void main(void) { 8 | vec3 normal = normalize(v_normal); 9 | vec4 color = vec4(0., 0., 0., 0.); 10 | vec4 diffuse = vec4(0., 0., 0., 1.); 11 | vec4 specular; 12 | diffuse = texture2D(u_diffuse, v_texcoord0); 13 | specular = u_specular; 14 | diffuse.xyz *= max(dot(normal,vec3(0.,0.,1.)), 0.); 15 | color.xyz += diffuse.xyz; 16 | color = vec4(color.rgb * diffuse.a, diffuse.a); 17 | gl_FragColor = color; 18 | } 19 | -------------------------------------------------------------------------------- /specs/lib/numberOfComponentsForTypeSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const numberOfComponentsForType = require("../../lib/numberOfComponentsForType"); 3 | 4 | describe("numberOfComponentsForType", () => { 5 | it("numberOfComponentsForType", () => { 6 | expect(numberOfComponentsForType("SCALAR")).toBe(1); 7 | expect(numberOfComponentsForType("VEC2")).toBe(2); 8 | expect(numberOfComponentsForType("VEC3")).toBe(3); 9 | expect(numberOfComponentsForType("VEC4")).toBe(4); 10 | expect(numberOfComponentsForType("MAT2")).toBe(4); 11 | expect(numberOfComponentsForType("MAT3")).toBe(9); 12 | expect(numberOfComponentsForType("MAT4")).toBe(16); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /specs/lib/addExtensionsUsedSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const addExtensionsUsed = require("../../lib/addExtensionsUsed"); 3 | 4 | describe("addExtensionsUsed", () => { 5 | it("adds an extension to extensionsUsed", () => { 6 | const gltf = {}; 7 | addExtensionsUsed(gltf, "KHR_materials_pbrSpecularGlossiness"); 8 | addExtensionsUsed(gltf, "KHR_draco_mesh_compression"); 9 | addExtensionsUsed(gltf, "KHR_draco_mesh_compression"); // Test adding duplicate 10 | expect(gltf.extensionsUsed).toEqual([ 11 | "KHR_materials_pbrSpecularGlossiness", 12 | "KHR_draco_mesh_compression", 13 | ]); 14 | expect(gltf.extensionsRequired).toBeUndefined(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/numberOfComponentsForType.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = numberOfComponentsForType; 4 | 5 | /** 6 | * Utility function for retrieving the number of components in a given type. 7 | * 8 | * @param {string} type glTF type 9 | * @returns {number} The number of components in that type. 10 | * 11 | * @private 12 | */ 13 | function numberOfComponentsForType(type) { 14 | switch (type) { 15 | case "SCALAR": 16 | return 1; 17 | case "VEC2": 18 | return 2; 19 | case "VEC3": 20 | return 3; 21 | case "VEC4": 22 | case "MAT2": 23 | return 4; 24 | case "MAT3": 25 | return 9; 26 | case "MAT4": 27 | return 16; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /specs/lib/processGlbSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fsExtra = require("fs-extra"); 3 | const processGlb = require("../../lib/processGlb"); 4 | 5 | const glbPath = "specs/data/2.0/box-textured-binary/box-textured-binary.glb"; 6 | 7 | describe("processGlb", () => { 8 | it("processGlb", async () => { 9 | spyOn(console, "log"); 10 | const glb = fsExtra.readFileSync(glbPath); 11 | const options = { 12 | separate: true, 13 | stats: true, 14 | }; 15 | const results = await processGlb(glb, options); 16 | expect(Buffer.isBuffer(results.glb)).toBe(true); 17 | expect(results.separateResources).toBeDefined(); 18 | expect(console.log).toHaveBeenCalled(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /specs/lib/dataUriToBufferSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const dataUriToBuffer = require("../../lib/dataUriToBuffer"); 3 | 4 | describe("dataUriToBuffer", () => { 5 | it("converts base64 data uri to buffer", () => { 6 | const buffer = Buffer.from([103, 108, 84, 70]); 7 | const dataUri = `data:application/octet-stream;base64,${buffer.toString( 8 | "base64", 9 | )}`; 10 | expect(dataUriToBuffer(dataUri)).toEqual(buffer); 11 | }); 12 | 13 | it("converts utf8 data uri to buffer", () => { 14 | const buffer = Buffer.from([103, 108, 84, 70]); 15 | const dataUri = `data:text/plain;charset=utf-8,${buffer.toString("utf8")}`; 16 | expect(dataUriToBuffer(dataUri)).toEqual(buffer); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /specs/lib/glbToGltfSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fsExtra = require("fs-extra"); 3 | const glbToGltf = require("../../lib/glbToGltf"); 4 | 5 | const glbPath = "specs/data/2.0/box-textured-binary/box-textured-binary.glb"; 6 | 7 | describe("glbToGltf", () => { 8 | it("glbToGltf", async () => { 9 | spyOn(console, "log"); 10 | const glb = fsExtra.readFileSync(glbPath); 11 | const options = { 12 | separate: true, 13 | stats: true, 14 | }; 15 | const results = await glbToGltf(glb, options); 16 | expect(results.gltf).toBeDefined(); 17 | expect(results.separateResources).toBeDefined(); 18 | expect(results.gltf.buffers.length).toBe(1); 19 | expect(console.log).toHaveBeenCalled(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /lib/addToArray.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = addToArray; 4 | 5 | /** 6 | * Adds an element to an array and returns the element's index. 7 | * 8 | * @param {Array} array The array to add to. 9 | * @param {object} element The element to add. 10 | * @param {boolean} [checkDuplicates=false] When true, if a duplicate element is found its index is returned and element is not added to the array. 11 | * 12 | * @private 13 | */ 14 | function addToArray(array, element, checkDuplicates) { 15 | checkDuplicates = checkDuplicates ?? false; 16 | if (checkDuplicates) { 17 | const index = array.indexOf(element); 18 | if (index > -1) { 19 | return index; 20 | } 21 | } 22 | 23 | array.push(element); 24 | return array.length - 1; 25 | } 26 | -------------------------------------------------------------------------------- /lib/addExtensionsUsed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addToArray = require("./addToArray"); 4 | 5 | const defined = Cesium.defined; 6 | 7 | module.exports = addExtensionsUsed; 8 | 9 | /** 10 | * Adds an extension to gltf.extensionsUsed if it does not already exist. 11 | * Initializes extensionsUsed if it is not defined. 12 | * 13 | * @param {object} gltf A javascript object containing a glTF asset. 14 | * @param {string} extension The extension to add. 15 | * 16 | * @private 17 | */ 18 | function addExtensionsUsed(gltf, extension) { 19 | let extensionsUsed = gltf.extensionsUsed; 20 | if (!defined(extensionsUsed)) { 21 | extensionsUsed = []; 22 | gltf.extensionsUsed = extensionsUsed; 23 | } 24 | addToArray(extensionsUsed, extension, true); 25 | } 26 | -------------------------------------------------------------------------------- /specs/lib/removeDefaultsSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const removeDefaults = require("../../lib/removeDefaults"); 3 | 4 | describe("removeDefaults", () => { 5 | it("removeDefaults", () => { 6 | const gltf = { 7 | nodes: [ 8 | { 9 | matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 10 | }, 11 | { 12 | matrix: [2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], 13 | }, 14 | ], 15 | accessors: [ 16 | { 17 | bufferView: 0, 18 | normalized: false, 19 | }, 20 | ], 21 | }; 22 | removeDefaults(gltf); 23 | expect(gltf.nodes[0].matrix).toBeUndefined(); 24 | expect(gltf.nodes[1].matrix).toBeDefined(); 25 | expect(gltf.accessors[0].normalized).toBeUndefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /specs/lib/addExtensionsRequiredSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const addExtensionsRequired = require("../../lib/addExtensionsRequired"); 3 | 4 | describe("addExtensionsRequired", () => { 5 | it("adds an extension to extensionsRequired", () => { 6 | const gltf = {}; 7 | addExtensionsRequired(gltf, "KHR_materials_pbrSpecularGlossiness"); 8 | addExtensionsRequired(gltf, "KHR_draco_mesh_compression"); 9 | addExtensionsRequired(gltf, "KHR_draco_mesh_compression"); // Test adding duplicate 10 | expect(gltf.extensionsRequired).toEqual([ 11 | "KHR_materials_pbrSpecularGlossiness", 12 | "KHR_draco_mesh_compression", 13 | ]); 14 | expect(gltf.extensionsUsed).toEqual([ 15 | "KHR_materials_pbrSpecularGlossiness", 16 | "KHR_draco_mesh_compression", 17 | ]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /specs/lib/addBufferSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const addBuffer = require("../../lib/addBuffer"); 3 | 4 | describe("addBuffer", () => { 5 | it("adds buffer to gltf and returns its bufferView id", () => { 6 | const gltf = { 7 | buffers: [], 8 | bufferViews: [], 9 | }; 10 | const buffer0 = Buffer.alloc(100); 11 | const buffer1 = Buffer.alloc(200); 12 | 13 | expect(addBuffer(gltf, buffer0)).toBe(0); 14 | expect(addBuffer(gltf, buffer1)).toBe(1); 15 | expect(gltf.buffers.length).toBe(2); 16 | expect(gltf.bufferViews.length).toBe(2); 17 | expect(gltf.buffers[0].extras._pipeline.source).toEqual(buffer0); 18 | expect(gltf.buffers[1].extras._pipeline.source).toEqual(buffer1); 19 | expect(gltf.bufferViews[0].buffer).toBe(0); 20 | expect(gltf.bufferViews[1].buffer).toBe(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/getBufferPadded.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = getBufferPadded; 3 | 4 | /** 5 | * Pad the buffer to the next 8-byte boundary to ensure proper alignment for the section that follows. 6 | * glTF only requires 4-byte alignment but some extensions like EXT_structural_metadata require 8-byte 7 | * alignment for some buffer views. 8 | * 9 | * @param {Buffer} buffer The buffer. 10 | * @returns {Buffer} The padded buffer. 11 | * 12 | * @private 13 | */ 14 | function getBufferPadded(buffer) { 15 | const boundary = 8; 16 | const byteLength = buffer.length; 17 | const remainder = byteLength % boundary; 18 | if (remainder === 0) { 19 | return buffer; 20 | } 21 | const padding = remainder === 0 ? 0 : boundary - remainder; 22 | const emptyBuffer = Buffer.alloc(padding); 23 | return Buffer.concat([buffer, emptyBuffer]); 24 | } 25 | -------------------------------------------------------------------------------- /lib/removeExtensionsRequired.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const defined = Cesium.defined; 5 | 6 | module.exports = removeExtensionsRequired; 7 | 8 | /** 9 | * Removes an extension from gltf.extensionsRequired if it is present. 10 | * 11 | * @param {object} gltf A javascript object containing a glTF asset. 12 | * @param {string} extension The extension to remove. 13 | * 14 | * @private 15 | */ 16 | function removeExtensionsRequired(gltf, extension) { 17 | const extensionsRequired = gltf.extensionsRequired; 18 | if (defined(extensionsRequired)) { 19 | const index = extensionsRequired.indexOf(extension); 20 | if (index >= 0) { 21 | extensionsRequired.splice(index, 1); 22 | } 23 | if (extensionsRequired.length === 0) { 24 | delete gltf.extensionsRequired; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /specs/lib/removeExtensionsRequiredSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const removeExtensionsRequired = require("../../lib/removeExtensionsRequired"); 3 | 4 | describe("removeExtensionsRequired", () => { 5 | it("removes extension from extensionsRequired", () => { 6 | const gltf = { 7 | extensionsRequired: ["extension1", "extension2"], 8 | extensionsUsed: ["extension1", "extension2"], 9 | }; 10 | removeExtensionsRequired(gltf, "extension1"); 11 | expect(gltf.extensionsRequired).toEqual(["extension2"]); 12 | removeExtensionsRequired(gltf, "extension2"); 13 | expect(gltf.extensionsRequired).toBeUndefined(); 14 | expect(gltf.extensionsUsed).toEqual(["extension1", "extension2"]); 15 | 16 | const emptyGltf = {}; 17 | removeExtensionsRequired(gltf, "extension1"); 18 | expect(emptyGltf).toEqual({}); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /specs/lib/removeExtensionsUsedSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const removeExtensionsUsed = require("../../lib/removeExtensionsUsed"); 3 | 4 | describe("removeExtensionsUsed", () => { 5 | it("removes extension from extensionsUsed", () => { 6 | const gltf = { 7 | extensionsRequired: ["extension1", "extension2"], 8 | extensionsUsed: ["extension1", "extension2"], 9 | }; 10 | removeExtensionsUsed(gltf, "extension1"); 11 | expect(gltf.extensionsRequired).toEqual(["extension2"]); 12 | expect(gltf.extensionsUsed).toEqual(["extension2"]); 13 | 14 | removeExtensionsUsed(gltf, "extension2"); 15 | expect(gltf.extensionsRequired).toBeUndefined(); 16 | expect(gltf.extensionsUsed).toBeUndefined(); 17 | 18 | const emptyGltf = {}; 19 | removeExtensionsUsed(gltf, "extension1"); 20 | expect(emptyGltf).toEqual({}); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/addBuffer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const addToArray = require("./addToArray"); 3 | 4 | module.exports = addBuffer; 5 | 6 | /** 7 | * Adds buffer to gltf. 8 | * 9 | * @param {object} gltf A javascript object containing a glTF asset. 10 | * @param {Buffer} buffer A Buffer object which will be added to gltf.buffers. 11 | * @returns {number} The bufferView id of the newly added bufferView. 12 | * 13 | * @private 14 | */ 15 | function addBuffer(gltf, buffer) { 16 | const newBuffer = { 17 | byteLength: buffer.length, 18 | extras: { 19 | _pipeline: { 20 | source: buffer, 21 | }, 22 | }, 23 | }; 24 | const bufferId = addToArray(gltf.buffers, newBuffer); 25 | const bufferView = { 26 | buffer: bufferId, 27 | byteOffset: 0, 28 | byteLength: buffer.length, 29 | }; 30 | return addToArray(gltf.bufferViews, bufferView); 31 | } 32 | -------------------------------------------------------------------------------- /lib/removeDefaults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const ForEach = require("./ForEach"); 4 | 5 | const defined = Cesium.defined; 6 | const Matrix4 = Cesium.Matrix4; 7 | 8 | module.exports = removeDefaults; 9 | 10 | /** 11 | * Remove default values from the glTF. Not exhaustive. 12 | * 13 | * @param {object} gltf A javascript object containing a glTF asset. 14 | * @returns {object} glTF with default values removed. 15 | * 16 | * @private 17 | */ 18 | function removeDefaults(gltf) { 19 | ForEach.node(gltf, function (node) { 20 | if ( 21 | defined(node.matrix) && 22 | Matrix4.equals(Matrix4.fromArray(node.matrix), Matrix4.IDENTITY) 23 | ) { 24 | delete node.matrix; 25 | } 26 | }); 27 | ForEach.accessor(gltf, function (accessor) { 28 | if (accessor.normalized === false) { 29 | delete accessor.normalized; 30 | } 31 | }); 32 | return gltf; 33 | } 34 | -------------------------------------------------------------------------------- /lib/addExtensionsRequired.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addExtensionsUsed = require("./addExtensionsUsed"); 4 | const addToArray = require("./addToArray"); 5 | 6 | const defined = Cesium.defined; 7 | 8 | module.exports = addExtensionsRequired; 9 | 10 | /** 11 | * Adds an extension to gltf.extensionsRequired if it does not already exist. 12 | * Initializes extensionsRequired if it is not defined. 13 | * 14 | * @param {object} gltf A javascript object containing a glTF asset. 15 | * @param {string} extension The extension to add. 16 | * 17 | * @private 18 | */ 19 | function addExtensionsRequired(gltf, extension) { 20 | let extensionsRequired = gltf.extensionsRequired; 21 | if (!defined(extensionsRequired)) { 22 | extensionsRequired = []; 23 | gltf.extensionsRequired = extensionsRequired; 24 | } 25 | addToArray(extensionsRequired, extension, true); 26 | addExtensionsUsed(gltf, extension); 27 | } 28 | -------------------------------------------------------------------------------- /lib/removeExtensionsUsed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const removeExtensionsRequired = require("./removeExtensionsRequired"); 4 | 5 | const defined = Cesium.defined; 6 | 7 | module.exports = removeExtensionsUsed; 8 | 9 | /** 10 | * Removes an extension from gltf.extensionsUsed and gltf.extensionsRequired if it is present. 11 | * 12 | * @param {object} gltf A javascript object containing a glTF asset. 13 | * @param {string} extension The extension to remove. 14 | * 15 | * @private 16 | */ 17 | function removeExtensionsUsed(gltf, extension) { 18 | const extensionsUsed = gltf.extensionsUsed; 19 | if (defined(extensionsUsed)) { 20 | const index = extensionsUsed.indexOf(extension); 21 | if (index >= 0) { 22 | extensionsUsed.splice(index, 1); 23 | } 24 | removeExtensionsRequired(gltf, extension); 25 | if (extensionsUsed.length === 0) { 26 | delete gltf.extensionsUsed; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /specs/lib/removePipelineExtrasSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addPipelineExtras = require("../../lib/addPipelineExtras"); 4 | const removePipelineExtras = require("../../lib/removePipelineExtras"); 5 | 6 | const WebGLConstants = Cesium.WebGLConstants; 7 | 8 | describe("removePipelineExtras", () => { 9 | it("removes pipeline extras", () => { 10 | const gltf = { 11 | buffers: [ 12 | { 13 | byteLength: 100, 14 | }, 15 | ], 16 | extensions: { 17 | KHR_techniques_webgl: { 18 | shaders: [ 19 | { 20 | type: WebGLConstants.VERTEX_SHADER, 21 | uri: "data:,", 22 | }, 23 | ], 24 | }, 25 | }, 26 | }; 27 | const gltfWithExtrasRemoved = removePipelineExtras(addPipelineExtras(gltf)); 28 | expect(gltfWithExtrasRemoved.buffers[0].extras).toBeUndefined(); 29 | expect( 30 | gltfWithExtrasRemoved.extensions.KHR_techniques_webgl.shaders[0].extras, 31 | ).toBeUndefined(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /specs/data/2.0/triangle-without-indices/triangle-without-indices.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "scenes" : [ 3 | { 4 | "nodes" : [ 0 ] 5 | } 6 | ], 7 | 8 | "nodes" : [ 9 | { 10 | "mesh" : 0 11 | } 12 | ], 13 | 14 | "meshes" : [ 15 | { 16 | "primitives" : [ { 17 | "attributes" : { 18 | "POSITION" : 0 19 | } 20 | } ] 21 | } 22 | ], 23 | 24 | "buffers" : [ 25 | { 26 | "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA", 27 | "byteLength" : 36 28 | } 29 | ], 30 | "bufferViews" : [ 31 | { 32 | "buffer" : 0, 33 | "byteOffset" : 0, 34 | "byteLength" : 36, 35 | "target" : 34962 36 | } 37 | ], 38 | "accessors" : [ 39 | { 40 | "bufferView" : 0, 41 | "byteOffset" : 0, 42 | "componentType" : 5126, 43 | "count" : 3, 44 | "type" : "VEC3", 45 | "max" : [ 1.0, 1.0, 0.0 ], 46 | "min" : [ 0.0, 0.0, 0.0 ] 47 | } 48 | ], 49 | 50 | "asset" : { 51 | "version" : "2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/getAccessorByteStride.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const numberOfComponentsForType = require("./numberOfComponentsForType"); 4 | 5 | const ComponentDatatype = Cesium.ComponentDatatype; 6 | const defined = Cesium.defined; 7 | 8 | module.exports = getAccessorByteStride; 9 | 10 | /** 11 | * Returns the byte stride of the provided accessor. 12 | * If the byteStride is 0, it is calculated based on type and componentType 13 | * 14 | * @param {object} gltf A javascript object containing a glTF asset. 15 | * @param {object} accessor The accessor. 16 | * @returns {number} The byte stride of the accessor. 17 | * 18 | * @private 19 | */ 20 | function getAccessorByteStride(gltf, accessor) { 21 | const bufferViewId = accessor.bufferView; 22 | if (defined(bufferViewId)) { 23 | const bufferView = gltf.bufferViews[bufferViewId]; 24 | if (defined(bufferView.byteStride) && bufferView.byteStride > 0) { 25 | return bufferView.byteStride; 26 | } 27 | } 28 | return ( 29 | ComponentDatatype.getSizeInBytes(accessor.componentType) * 30 | numberOfComponentsForType(accessor.type) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/addPipelineExtras.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const ForEach = require("./ForEach"); 4 | 5 | const defined = Cesium.defined; 6 | 7 | module.exports = addPipelineExtras; 8 | 9 | /** 10 | * Adds extras._pipeline to each object that can have extras in the glTF asset. 11 | * This stage runs before updateVersion and handles both glTF 1.0 and glTF 2.0 assets. 12 | * 13 | * @param {object} gltf A javascript object containing a glTF asset. 14 | * @returns {object} The glTF asset with the added pipeline extras. 15 | * 16 | * @private 17 | */ 18 | function addPipelineExtras(gltf) { 19 | ForEach.shader(gltf, function (shader) { 20 | addExtras(shader); 21 | }); 22 | ForEach.buffer(gltf, function (buffer) { 23 | addExtras(buffer); 24 | }); 25 | ForEach.image(gltf, function (image) { 26 | addExtras(image); 27 | }); 28 | 29 | addExtras(gltf); 30 | 31 | return gltf; 32 | } 33 | 34 | function addExtras(object) { 35 | object.extras = defined(object.extras) ? object.extras : {}; 36 | object.extras._pipeline = defined(object.extras._pipeline) 37 | ? object.extras._pipeline 38 | : {}; 39 | } 40 | -------------------------------------------------------------------------------- /lib/removePipelineExtras.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const ForEach = require("./ForEach"); 4 | 5 | const defined = Cesium.defined; 6 | 7 | module.exports = removePipelineExtras; 8 | 9 | /** 10 | * Iterate through the objects within the glTF and delete their pipeline extras object. 11 | * 12 | * @param {object} gltf A javascript object containing a glTF asset. 13 | * @returns {object} glTF with no pipeline extras. 14 | * 15 | * @private 16 | */ 17 | function removePipelineExtras(gltf) { 18 | ForEach.shader(gltf, function (shader) { 19 | removeExtras(shader); 20 | }); 21 | ForEach.buffer(gltf, function (buffer) { 22 | removeExtras(buffer); 23 | }); 24 | ForEach.image(gltf, function (image) { 25 | removeExtras(image); 26 | }); 27 | 28 | removeExtras(gltf); 29 | 30 | return gltf; 31 | } 32 | 33 | function removeExtras(object) { 34 | if (!defined(object.extras)) { 35 | return; 36 | } 37 | 38 | if (defined(object.extras._pipeline)) { 39 | delete object.extras._pipeline; 40 | } 41 | 42 | if (Object.keys(object.extras).length === 0) { 43 | delete object.extras; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/getJsonBufferPadded.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = getJsonBufferPadded; 4 | 5 | /** 6 | * Convert the JSON object to a padded buffer. 7 | * 8 | * Pad the JSON with extra whitespace to fit the next 8-byte boundary. This ensures proper alignment 9 | * for the section that follows. glTF only requires 4-byte alignment but some extensions like 10 | * EXT_structural_metadata require 8-byte alignment for some buffer views. 11 | * 12 | * @param {object} json The JSON object. 13 | * @param {number} [byteOffset=0] The byte offset on which the buffer starts. 14 | * @returns {Buffer} The padded JSON buffer. 15 | * 16 | * @private 17 | */ 18 | function getJsonBufferPadded(json, byteOffset) { 19 | byteOffset = byteOffset ?? 0; 20 | let string = JSON.stringify(json); 21 | 22 | const boundary = 8; 23 | const byteLength = Buffer.byteLength(string); 24 | const remainder = (byteOffset + byteLength) % boundary; 25 | const padding = remainder === 0 ? 0 : boundary - remainder; 26 | let whitespace = ""; 27 | for (let i = 0; i < padding; ++i) { 28 | whitespace += " "; 29 | } 30 | string += whitespace; 31 | 32 | return Buffer.from(string); 33 | } 34 | -------------------------------------------------------------------------------- /specs/lib/addToArraySpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const addToArray = require("../../lib/addToArray"); 3 | 4 | describe("addToArray", () => { 5 | it("adds item to array and returns its index", () => { 6 | const gltf = { 7 | buffers: [], 8 | }; 9 | const buffer0 = { 10 | byteLength: 100, 11 | }; 12 | const buffer1 = { 13 | byteLength: 200, 14 | }; 15 | expect(addToArray(gltf.buffers, buffer0)).toBe(0); 16 | expect(addToArray(gltf.buffers, buffer1)).toBe(1); 17 | expect(gltf.buffers).toEqual([buffer0, buffer1]); 18 | }); 19 | 20 | it("returns index of duplicate element when checkDuplicates is true", () => { 21 | const gltf = { 22 | buffers: [], 23 | }; 24 | const buffer0 = { 25 | byteLength: 100, 26 | }; 27 | const buffer1 = { 28 | byteLength: 200, 29 | }; 30 | expect(addToArray(gltf.buffers, buffer0, true)).toBe(0); 31 | expect(addToArray(gltf.buffers, buffer1, true)).toBe(1); 32 | expect(addToArray(gltf.buffers, buffer0, true)).toBe(0); 33 | expect(gltf.buffers.length).toBe(2); 34 | expect(addToArray(gltf.buffers, buffer0)).toBe(2); 35 | expect(gltf.buffers.length).toBe(3); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /specs/lib/getJsonBufferPaddedSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const getJsonBufferPadded = require("../../lib/getJsonBufferPadded"); 3 | 4 | describe("getJsonBufferPadded", () => { 5 | it("get json buffer padded to 8 bytes", () => { 6 | const gltf = { 7 | asset: { 8 | version: "2.0", 9 | }, 10 | }; 11 | const string = JSON.stringify(gltf); 12 | expect(string.length).toBe(27); 13 | const bufferPadded = getJsonBufferPadded(gltf); 14 | expect(bufferPadded.length).toBe(32); 15 | expect(bufferPadded.readUInt8(27)).toBe(32); // Space 16 | expect(bufferPadded.readUInt8(28)).toBe(32); 17 | expect(bufferPadded.readUInt8(29)).toBe(32); 18 | expect(bufferPadded.readUInt8(30)).toBe(32); 19 | expect(bufferPadded.readUInt8(31)).toBe(32); 20 | }); 21 | 22 | it("get json buffer padded to 8 bytes relative to byte offset", () => { 23 | const gltf = { 24 | asset: { 25 | version: "2.0", 26 | }, 27 | }; 28 | const string = JSON.stringify(gltf); 29 | expect(string.length).toBe(27); 30 | const bufferPadded = getJsonBufferPadded(gltf, 20); 31 | expect(bufferPadded.length).toBe(28); 32 | expect(bufferPadded.readUInt8(27)).toBe(32); // Space 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /specs/lib/getBufferPaddedSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const getBufferPadded = require("../../lib/getBufferPadded"); 3 | 4 | describe("getBufferPadded", () => { 5 | it("gets buffer padded to 8 bytes", () => { 6 | let buffer = Buffer.alloc(0); 7 | let bufferPadded = getBufferPadded(buffer); 8 | expect(bufferPadded.length).toBe(0); 9 | 10 | buffer = Buffer.from([1]); 11 | bufferPadded = getBufferPadded(buffer); 12 | expect(bufferPadded.length).toBe(8); 13 | expect(bufferPadded.readUInt8(0)).toBe(1); 14 | expect(bufferPadded.readUInt8(1)).toBe(0); 15 | expect(bufferPadded.readUInt8(2)).toBe(0); 16 | expect(bufferPadded.readUInt8(3)).toBe(0); 17 | expect(bufferPadded.readUInt8(4)).toBe(0); 18 | expect(bufferPadded.readUInt8(5)).toBe(0); 19 | expect(bufferPadded.readUInt8(6)).toBe(0); 20 | expect(bufferPadded.readUInt8(7)).toBe(0); 21 | 22 | // Does not allocate a new buffer when buffer length is already aligned to 8 bytes 23 | buffer = Buffer.alloc(8); 24 | bufferPadded = getBufferPadded(buffer); 25 | expect(bufferPadded).toBe(buffer); 26 | 27 | buffer = Buffer.alloc(70); 28 | bufferPadded = getBufferPadded(buffer); 29 | expect(bufferPadded.length).toBe(72); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /ThirdParty.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "bluebird", 4 | "license": [ 5 | "MIT" 6 | ], 7 | "version": "3.7.2", 8 | "url": "https://www.npmjs.com/package/bluebird" 9 | }, 10 | { 11 | "name": "cesium", 12 | "license": [ 13 | "Apache-2.0" 14 | ], 15 | "version": "1.131.0", 16 | "url": "https://www.npmjs.com/package/cesium" 17 | }, 18 | { 19 | "name": "draco3d", 20 | "license": [ 21 | "Apache-2.0" 22 | ], 23 | "version": "1.5.7", 24 | "url": "https://www.npmjs.com/package/draco3d" 25 | }, 26 | { 27 | "name": "fs-extra", 28 | "license": [ 29 | "MIT" 30 | ], 31 | "version": "11.3.0", 32 | "url": "https://www.npmjs.com/package/fs-extra" 33 | }, 34 | { 35 | "name": "mime", 36 | "license": [ 37 | "MIT" 38 | ], 39 | "version": "3.0.0", 40 | "url": "https://www.npmjs.com/package/mime" 41 | }, 42 | { 43 | "name": "object-hash", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "version": "3.0.0", 48 | "url": "https://www.npmjs.com/package/object-hash" 49 | }, 50 | { 51 | "name": "yargs", 52 | "license": [ 53 | "MIT" 54 | ], 55 | "version": "17.7.2", 56 | "url": "https://www.npmjs.com/package/yargs" 57 | } 58 | ] -------------------------------------------------------------------------------- /lib/getImageExtension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const RuntimeError = Cesium.RuntimeError; 5 | 6 | module.exports = getImageExtension; 7 | 8 | /** 9 | * Get the image extension from a Buffer containing image data. 10 | * 11 | * @param {Buffer} data The image data. 12 | * @returns {string} The image extension. 13 | * 14 | * @private 15 | */ 16 | function getImageExtension(data) { 17 | const header = data.slice(0, 2); 18 | const webpHeaderRIFFChars = data.slice(0, 4); 19 | const webpHeaderWEBPChars = data.slice(8, 12); 20 | 21 | if (header.equals(Buffer.from([0x42, 0x4d]))) { 22 | return ".bmp"; 23 | } else if (header.equals(Buffer.from([0x47, 0x49]))) { 24 | return ".gif"; 25 | } else if (header.equals(Buffer.from([0xff, 0xd8]))) { 26 | return ".jpg"; 27 | } else if (header.equals(Buffer.from([0x89, 0x50]))) { 28 | return ".png"; 29 | } else if (header.equals(Buffer.from([0xab, 0x4b]))) { 30 | return ".ktx2"; 31 | } else if (header.equals(Buffer.from([0x73, 0x42]))) { 32 | return ".basis"; 33 | } else if ( 34 | webpHeaderRIFFChars.equals(Buffer.from([0x52, 0x49, 0x46, 0x46])) && 35 | webpHeaderWEBPChars.equals(Buffer.from([0x57, 0x45, 0x42, 0x50])) 36 | ) { 37 | // See https://developers.google.com/speed/webp/docs/riff_container#webp_file_header 38 | return ".webp"; 39 | } 40 | 41 | throw new RuntimeError("Image data does not have valid header"); 42 | } 43 | -------------------------------------------------------------------------------- /specs/lib/getComponentReaderSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const getComponentReader = require("../../lib/getComponentReader"); 4 | 5 | const ComponentDatatype = Cesium.ComponentDatatype; 6 | 7 | function testComponentReader(componentType) { 8 | const typedArray = ComponentDatatype.createTypedArray( 9 | componentType, 10 | [0, 1, 2], 11 | ); 12 | const dataView = new DataView(typedArray.buffer); 13 | const componentTypeByteLength = 14 | ComponentDatatype.getSizeInBytes(componentType); 15 | const componentReader = getComponentReader(componentType); 16 | const byteOffset = componentTypeByteLength; 17 | const numberOfComponents = 2; 18 | const result = new Array(numberOfComponents); 19 | componentReader( 20 | dataView, 21 | byteOffset, 22 | numberOfComponents, 23 | componentTypeByteLength, 24 | result, 25 | ); 26 | expect(result).toEqual([1, 2]); 27 | } 28 | 29 | describe("getComponentReader", () => { 30 | it("reads values", () => { 31 | testComponentReader(ComponentDatatype.BYTE); 32 | testComponentReader(ComponentDatatype.UNSIGNED_BYTE); 33 | testComponentReader(ComponentDatatype.SHORT); 34 | testComponentReader(ComponentDatatype.UNSIGNED_SHORT); 35 | testComponentReader(ComponentDatatype.INT); 36 | testComponentReader(ComponentDatatype.UNSIGNED_INT); 37 | testComponentReader(ComponentDatatype.FLOAT); 38 | testComponentReader(ComponentDatatype.DOUBLE); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /specs/lib/addPipelineExtrasSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addPipelineExtras = require("../../lib/addPipelineExtras"); 4 | 5 | const WebGLConstants = Cesium.WebGLConstants; 6 | 7 | describe("addPipelineExtras", () => { 8 | it("adds pipeline extras to glTF 1.0 assets", () => { 9 | const gltf = { 10 | buffers: { 11 | sampleBuffer0: { 12 | byteLength: 100, 13 | }, 14 | }, 15 | shaders: { 16 | sample0VS: { 17 | type: WebGLConstants.VERTEX_SHADER, 18 | uri: "data:,", 19 | }, 20 | }, 21 | }; 22 | const gltfWithExtras = addPipelineExtras(gltf); 23 | expect( 24 | gltfWithExtras.buffers["sampleBuffer0"].extras._pipeline, 25 | ).toBeDefined(); 26 | expect(gltfWithExtras.shaders["sample0VS"].extras._pipeline).toBeDefined(); 27 | }); 28 | 29 | it("adds pipeline extras to glTF 2.0 assets", () => { 30 | const gltf = { 31 | buffers: [ 32 | { 33 | byteLength: 100, 34 | }, 35 | ], 36 | extensions: { 37 | KHR_techniques_webgl: { 38 | shaders: [ 39 | { 40 | type: WebGLConstants.VERTEX_SHADER, 41 | uri: "data:,", 42 | }, 43 | ], 44 | }, 45 | }, 46 | extensionsRequired: ["KHR_techniques_webgl"], 47 | extensionsUsed: ["KHR_techniques_webgl"], 48 | }; 49 | const gltfWithExtras = addPipelineExtras(gltf); 50 | expect(gltfWithExtras.buffers[0].extras._pipeline).toBeDefined(); 51 | expect( 52 | gltfWithExtras.extensions.KHR_techniques_webgl.shaders[0].extras 53 | ._pipeline, 54 | ).toBeDefined(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /lib/readAccessorPacked.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const getAccessorByteStride = require("./getAccessorByteStride"); 4 | const getComponentReader = require("./getComponentReader"); 5 | const numberOfComponentsForType = require("./numberOfComponentsForType"); 6 | 7 | const ComponentDatatype = Cesium.ComponentDatatype; 8 | const defined = Cesium.defined; 9 | 10 | module.exports = readAccessorPacked; 11 | 12 | /** 13 | * Returns the accessor data in a contiguous array. 14 | * 15 | * @param {object} gltf A javascript object containing a glTF asset. 16 | * @param {object} accessor The accessor. 17 | * @returns {Array} The accessor values in a contiguous array. 18 | * 19 | * @private 20 | */ 21 | function readAccessorPacked(gltf, accessor) { 22 | const byteStride = getAccessorByteStride(gltf, accessor); 23 | const componentTypeByteLength = ComponentDatatype.getSizeInBytes( 24 | accessor.componentType, 25 | ); 26 | const numberOfComponents = numberOfComponentsForType(accessor.type); 27 | const count = accessor.count; 28 | const values = new Array(numberOfComponents * count); 29 | 30 | if (!defined(accessor.bufferView)) { 31 | return values.fill(0); 32 | } 33 | 34 | const bufferView = gltf.bufferViews[accessor.bufferView]; 35 | const source = gltf.buffers[bufferView.buffer].extras._pipeline.source; 36 | let byteOffset = 37 | accessor.byteOffset + bufferView.byteOffset + source.byteOffset; 38 | 39 | const dataView = new DataView(source.buffer); 40 | const components = new Array(numberOfComponents); 41 | const componentReader = getComponentReader(accessor.componentType); 42 | 43 | for (let i = 0; i < count; ++i) { 44 | componentReader( 45 | dataView, 46 | byteOffset, 47 | numberOfComponents, 48 | componentTypeByteLength, 49 | components, 50 | ); 51 | for (let j = 0; j < numberOfComponents; ++j) { 52 | values[i * numberOfComponents + j] = components[j]; 53 | } 54 | byteOffset += byteStride; 55 | } 56 | return values; 57 | } 58 | -------------------------------------------------------------------------------- /specs/lib/getAccessorByteStrideSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const getAccessorByteStride = require("../../lib/getAccessorByteStride"); 4 | 5 | const WebGLConstants = Cesium.WebGLConstants; 6 | 7 | describe("getAccessorByteStride", () => { 8 | it("gets accessor byte stride", () => { 9 | const gltf = { 10 | accessors: [ 11 | { 12 | componentType: WebGLConstants.FLOAT, 13 | count: 24, 14 | type: "VEC3", 15 | min: [-1.0, -1.0, -1.0], 16 | max: [1.0, 1.0, 1.0], 17 | }, 18 | { 19 | bufferView: 0, 20 | componentType: WebGLConstants.FLOAT, 21 | count: 24, 22 | type: "VEC3", 23 | min: [-1.0, -1.0, -1.0], 24 | max: [1.0, 1.0, 1.0], 25 | }, 26 | { 27 | bufferView: 1, 28 | componentType: WebGLConstants.FLOAT, 29 | count: 24, 30 | type: "VEC3", 31 | min: [-1.0, -1.0, -1.0], 32 | max: [1.0, 1.0, 1.0], 33 | }, 34 | { 35 | componentType: WebGLConstants.FLOAT, 36 | count: 24, 37 | type: "VEC2", 38 | min: [0.0, 0.0], 39 | max: [1.0, 1.0], 40 | }, 41 | { 42 | componentType: WebGLConstants.INT, 43 | count: 36, 44 | type: "SCALAR", 45 | min: [0], 46 | max: [24], 47 | }, 48 | ], 49 | bufferViews: [ 50 | { 51 | buffer: 0, 52 | byteLength: 288, 53 | byteOffset: 0, 54 | }, 55 | { 56 | buffer: 0, 57 | byteLength: 288, 58 | byteOffset: 288, 59 | byteStride: 32, 60 | }, 61 | ], 62 | }; 63 | 64 | expect(getAccessorByteStride(gltf, gltf.accessors[0])).toBe(12); 65 | expect(getAccessorByteStride(gltf, gltf.accessors[1])).toBe(12); 66 | expect(getAccessorByteStride(gltf, gltf.accessors[2])).toBe(32); 67 | expect(getAccessorByteStride(gltf, gltf.accessors[3])).toBe(8); 68 | expect(getAccessorByteStride(gltf, gltf.accessors[4])).toBe(4); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /lib/updateAccessorComponentTypes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addBuffer = require("./addBuffer"); 4 | const ForEach = require("./ForEach"); 5 | const readAccessorPacked = require("./readAccessorPacked"); 6 | 7 | const ComponentDatatype = Cesium.ComponentDatatype; 8 | const WebGLConstants = Cesium.WebGLConstants; 9 | 10 | module.exports = updateAccessorComponentTypes; 11 | 12 | /** 13 | * Update accessors referenced by JOINTS_0 and WEIGHTS_0 attributes to use correct component types. 14 | * 15 | * @param {object} gltf A javascript object containing a glTF asset. 16 | * @returns {object} The glTF asset with compressed meshes. 17 | * 18 | * @private 19 | */ 20 | function updateAccessorComponentTypes(gltf) { 21 | let componentType; 22 | ForEach.accessorWithSemantic(gltf, "JOINTS_0", function (accessorId) { 23 | const accessor = gltf.accessors[accessorId]; 24 | componentType = accessor.componentType; 25 | if (componentType === WebGLConstants.BYTE) { 26 | convertType(gltf, accessor, ComponentDatatype.UNSIGNED_BYTE); 27 | } else if ( 28 | componentType !== WebGLConstants.UNSIGNED_BYTE && 29 | componentType !== WebGLConstants.UNSIGNED_SHORT 30 | ) { 31 | convertType(gltf, accessor, ComponentDatatype.UNSIGNED_SHORT); 32 | } 33 | }); 34 | ForEach.accessorWithSemantic(gltf, "WEIGHTS_0", function (accessorId) { 35 | const accessor = gltf.accessors[accessorId]; 36 | componentType = accessor.componentType; 37 | if (componentType === WebGLConstants.BYTE) { 38 | convertType(gltf, accessor, ComponentDatatype.UNSIGNED_BYTE); 39 | } else if (componentType === WebGLConstants.SHORT) { 40 | convertType(gltf, accessor, ComponentDatatype.UNSIGNED_SHORT); 41 | } 42 | }); 43 | 44 | return gltf; 45 | } 46 | 47 | function convertType(gltf, accessor, updatedComponentType) { 48 | const typedArray = ComponentDatatype.createTypedArray( 49 | updatedComponentType, 50 | readAccessorPacked(gltf, accessor), 51 | ); 52 | const newBuffer = new Uint8Array(typedArray.buffer); 53 | accessor.bufferView = addBuffer(gltf, newBuffer); 54 | accessor.componentType = updatedComponentType; 55 | accessor.byteOffset = 0; 56 | } 57 | -------------------------------------------------------------------------------- /lib/removeExtension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const ForEach = require("./ForEach"); 4 | const removeExtensionsUsed = require("./removeExtensionsUsed"); 5 | 6 | const defined = Cesium.defined; 7 | 8 | module.exports = removeExtension; 9 | 10 | /** 11 | * Removes an extension from gltf.extensions, gltf.extensionsUsed, gltf.extensionsRequired, and any other objects in the glTF if it is present. 12 | * 13 | * @param {object} gltf A javascript object containing a glTF asset. 14 | * @param {string} extension The extension to remove. 15 | * 16 | * @returns {*} The extension data removed from gltf.extensions. 17 | */ 18 | function removeExtension(gltf, extension) { 19 | removeExtensionsUsed(gltf, extension); // Also removes from extensionsRequired 20 | 21 | if (extension === "CESIUM_RTC") { 22 | removeCesiumRTC(gltf); 23 | } 24 | 25 | return removeExtensionAndTraverse(gltf, extension); 26 | } 27 | 28 | function removeCesiumRTC(gltf) { 29 | ForEach.technique(gltf, function (technique) { 30 | ForEach.techniqueUniform(technique, function (uniform) { 31 | if (uniform.semantic === "CESIUM_RTC_MODELVIEW") { 32 | uniform.semantic = "MODELVIEW"; 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | function removeExtensionAndTraverse(object, extension) { 39 | if (Array.isArray(object)) { 40 | const length = object.length; 41 | for (let i = 0; i < length; ++i) { 42 | removeExtensionAndTraverse(object[i], extension); 43 | } 44 | } else if ( 45 | object !== null && 46 | typeof object === "object" && 47 | object.constructor === Object 48 | ) { 49 | const extensions = object.extensions; 50 | let extensionData; 51 | if (defined(extensions)) { 52 | extensionData = extensions[extension]; 53 | if (defined(extensionData)) { 54 | delete extensions[extension]; 55 | if (Object.keys(extensions).length === 0) { 56 | delete object.extensions; 57 | } 58 | } 59 | } 60 | for (const key in object) { 61 | if (Object.prototype.hasOwnProperty.call(object, key)) { 62 | removeExtensionAndTraverse(object[key], extension); 63 | } 64 | } 65 | return extensionData; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /specs/lib/readAccessorPackedSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const readAccessorPacked = require("../../lib/readAccessorPacked"); 3 | const readResources = require("../../lib/readResources"); 4 | 5 | const contiguousData = [ 6 | -1.0, 1.0, -1.0, 0.0, 0.0, 0.0, 3.0, 2.0, 1.0, -1.0, -2.0, -3.0, 7 | ]; 8 | 9 | const nan = Number.NaN; 10 | const nonContiguousData = [ 11 | -1.0, 12 | 1.0, 13 | -1.0, 14 | nan, 15 | nan, 16 | nan, 17 | 0.0, 18 | 0.0, 19 | 0.0, 20 | nan, 21 | nan, 22 | nan, 23 | 3.0, 24 | 2.0, 25 | 1.0, 26 | nan, 27 | nan, 28 | nan, 29 | -1.0, 30 | -2.0, 31 | -3.0, 32 | nan, 33 | nan, 34 | nan, 35 | ]; 36 | 37 | function createGltf(elements, byteStride) { 38 | const buffer = Buffer.from(new Float32Array(elements).buffer); 39 | const byteLength = buffer.length; 40 | const dataUri = `data:application/octet-stream;base64,${buffer.toString( 41 | "base64", 42 | )}`; 43 | const gltf = { 44 | accessors: [ 45 | { 46 | bufferView: 0, 47 | byteOffset: 0, 48 | componentType: 5126, 49 | count: 4, 50 | type: "VEC3", 51 | }, 52 | ], 53 | bufferViews: [ 54 | { 55 | buffer: 0, 56 | byteOffset: 0, 57 | byteLength: byteLength, 58 | byteStride: byteStride, 59 | }, 60 | ], 61 | buffers: [ 62 | { 63 | uri: dataUri, 64 | byteLength: byteLength, 65 | }, 66 | ], 67 | }; 68 | return readResources(gltf); 69 | } 70 | 71 | describe("readAccessorPacked", () => { 72 | it("reads contiguous accessor", async () => { 73 | const gltf = await createGltf(contiguousData, 12); 74 | expect(readAccessorPacked(gltf, gltf.accessors[0])).toEqual(contiguousData); 75 | }); 76 | 77 | it("reads non-contiguous accessor", async () => { 78 | const gltf = await createGltf(nonContiguousData, 24); 79 | expect(readAccessorPacked(gltf, gltf.accessors[0])).toEqual(contiguousData); 80 | }); 81 | 82 | it("reads accessor that does not have a buffer view", () => { 83 | const gltf = { 84 | accessors: [ 85 | { 86 | componentType: 5126, 87 | count: 4, 88 | type: "VEC3", 89 | }, 90 | ], 91 | }; 92 | const expected = new Array(12).fill(0); 93 | expect(readAccessorPacked(gltf, gltf.accessors[0])).toEqual(expected); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /specs/lib/findAccessorMinMaxSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const findAccessorMinMax = require("../../lib/findAccessorMinMax"); 3 | const readResources = require("../../lib/readResources"); 4 | 5 | const contiguousData = [ 6 | -1.0, -2.0, -3.0, 3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.5, -0.5, 0.5, 7 | ]; 8 | 9 | const nan = Number.NaN; 10 | const nonContiguousData = [ 11 | -1.0, 12 | 1.0, 13 | -1.0, 14 | nan, 15 | nan, 16 | nan, 17 | 0.0, 18 | 0.0, 19 | 0.0, 20 | nan, 21 | nan, 22 | nan, 23 | 3.0, 24 | 2.0, 25 | 1.0, 26 | nan, 27 | nan, 28 | nan, 29 | -1.0, 30 | -2.0, 31 | -3.0, 32 | nan, 33 | nan, 34 | nan, 35 | ]; 36 | 37 | function createGltf(elements, byteStride) { 38 | const buffer = Buffer.from(new Float32Array(elements).buffer); 39 | const byteLength = buffer.length; 40 | const dataUri = `data:application/octet-stream;base64,${buffer.toString( 41 | "base64", 42 | )}`; 43 | const gltf = { 44 | asset: { 45 | version: "2.0", 46 | }, 47 | accessors: [ 48 | { 49 | bufferView: 0, 50 | byteOffset: 0, 51 | componentType: 5126, 52 | count: 4, 53 | type: "VEC3", 54 | }, 55 | ], 56 | bufferViews: [ 57 | { 58 | buffer: 0, 59 | byteOffset: 0, 60 | byteLength: byteLength, 61 | byteStride: byteStride, 62 | }, 63 | ], 64 | buffers: [ 65 | { 66 | uri: dataUri, 67 | byteLength: byteLength, 68 | }, 69 | ], 70 | }; 71 | return readResources(gltf); 72 | } 73 | 74 | describe("findAccessorMinMax", () => { 75 | it("finds the min and max of an accessor", async () => { 76 | const gltf = await createGltf(contiguousData, 12); 77 | const expectedMin = [-1.0, -2.0, -3.0]; 78 | const expectedMax = [3.0, 2.0, 1.0]; 79 | const minMax = findAccessorMinMax(gltf, gltf.accessors[0]); 80 | expect(minMax.min).toEqual(expectedMin); 81 | expect(minMax.max).toEqual(expectedMax); 82 | }); 83 | 84 | it("finds the min and max in a non-contiguous accessor", async () => { 85 | const gltf = await createGltf(nonContiguousData, 24); 86 | const expectedMin = [-1.0, -2.0, -3.0]; 87 | const expectedMax = [3.0, 2.0, 1.0]; 88 | const minMax = findAccessorMinMax(gltf, gltf.accessors[0]); 89 | expect(minMax.min).toEqual(expectedMin); 90 | expect(minMax.max).toEqual(expectedMax); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gltf-pipeline", 3 | "version": "4.3.0", 4 | "description": "Content pipeline tools for optimizing glTF assets.", 5 | "license": "Apache-2.0", 6 | "contributors": [ 7 | { 8 | "name": "Richard Lee, Cesium GS, Inc., and Contributors", 9 | "url": "https://github.com/CesiumGS/gltf-pipeline/graphs/contributors" 10 | } 11 | ], 12 | "keywords": [ 13 | "glTF", 14 | "WebGL" 15 | ], 16 | "homepage": "https://github.com/CesiumGS/gltf-pipeline", 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:CesiumGS/gltf-pipeline.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/CesiumGS/gltf-pipeline/issues" 23 | }, 24 | "main": "index.js", 25 | "engines": { 26 | "node": ">=16.0.0" 27 | }, 28 | "dependencies": { 29 | "bluebird": "^3.7.2", 30 | "cesium": "^1.131.0", 31 | "draco3d": "^1.5.7", 32 | "fs-extra": "^11.3.0", 33 | "mime": "^3.0.0", 34 | "object-hash": "^3.0.0", 35 | "yargs": "^17.7.2" 36 | }, 37 | "devDependencies": { 38 | "cloc": "^2.11.0", 39 | "dependency-tree": "^10.0.9", 40 | "eslint": "^8.57.1", 41 | "eslint-config-cesium": "^10.0.2", 42 | "eslint-config-prettier": "^9.1.0", 43 | "eslint-plugin-n": "^16.6.2", 44 | "gulp": "^5.0.1", 45 | "husky": "^8.0.3", 46 | "jasmine": "^5.8.0", 47 | "jasmine-spec-reporter": "^7.0.0", 48 | "jsdoc": "^4.0.4", 49 | "lint-staged": "^15.5.2", 50 | "nyc": "^15.1.0", 51 | "prettier": "3.1.1" 52 | }, 53 | "lint-staged": { 54 | "*.(js|ts)": [ 55 | "eslint --cache --quiet --fix", 56 | "prettier --write" 57 | ], 58 | "*.!(js|ts)": "prettier --write" 59 | }, 60 | "scripts": { 61 | "prepare": "husky install", 62 | "pre-commit": "lint-staged", 63 | "jsdoc": "jsdoc ./lib -R ./README.md -d doc", 64 | "eslint-fix": "eslint \"./**/*.js\" --fix", 65 | "eslint": "eslint \"./**/*.js\" --cache --quiet", 66 | "eslint-watch": "gulp eslint-watch", 67 | "test": "gulp test", 68 | "test-watch": "gulp test-watch", 69 | "coverage": "gulp coverage", 70 | "cloc": "gulp cloc", 71 | "prettier": "prettier --write \"**/*\"", 72 | "prettier-check": "prettier --check \"**/*\"", 73 | "build-cesium": "gulp build-cesium", 74 | "generate-third-party": "gulp generate-third-party" 75 | }, 76 | "bin": { 77 | "gltf-pipeline": "./bin/gltf-pipeline.js" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/gltfToGlb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const getJsonBufferPadded = require("./getJsonBufferPadded"); 4 | const processGltf = require("./processGltf"); 5 | 6 | const clone = Cesium.clone; 7 | const defined = Cesium.defined; 8 | 9 | module.exports = gltfToGlb; 10 | 11 | /** 12 | * Convert a glTF to glb. 13 | * 14 | * @param {object} gltf A javascript object containing a glTF asset. 15 | * @param {object} [options] The same options object as {@link processGltf} 16 | * @returns {Promise} A promise that resolves to an object containing the glb and a dictionary containing separate resources. 17 | */ 18 | function gltfToGlb(gltf, options) { 19 | options = defined(options) ? clone(options) : {}; 20 | options.forceMergeBuffers = !options.separate; 21 | options.bufferStorage = { 22 | buffer: undefined, 23 | }; 24 | 25 | return processGltf(gltf, options).then(function (results) { 26 | return { 27 | glb: getGlb(results.gltf, options.bufferStorage.buffer), 28 | separateResources: results.separateResources, 29 | }; 30 | }); 31 | } 32 | 33 | function getGlb(gltf, binaryBuffer) { 34 | const jsonBuffer = getJsonBufferPadded(gltf, 20); 35 | 36 | // Compute glb length: (Global header) + (JSON chunk header) + (JSON chunk) + [(Binary chunk header) + (Binary chunk)] 37 | let glbLength = 12 + 8 + jsonBuffer.length; 38 | 39 | if (defined(binaryBuffer)) { 40 | glbLength += 8 + binaryBuffer.length; 41 | } 42 | 43 | const glb = Buffer.alloc(glbLength); 44 | 45 | // Write binary glTF header (magic, version, length) 46 | let byteOffset = 0; 47 | glb.writeUInt32LE(0x46546c67, byteOffset); 48 | byteOffset += 4; 49 | glb.writeUInt32LE(2, byteOffset); 50 | byteOffset += 4; 51 | glb.writeUInt32LE(glbLength, byteOffset); 52 | byteOffset += 4; 53 | 54 | // Write JSON Chunk header (length, type) 55 | glb.writeUInt32LE(jsonBuffer.length, byteOffset); 56 | byteOffset += 4; 57 | glb.writeUInt32LE(0x4e4f534a, byteOffset); // JSON 58 | byteOffset += 4; 59 | 60 | // Write JSON Chunk 61 | jsonBuffer.copy(glb, byteOffset); 62 | byteOffset += jsonBuffer.length; 63 | 64 | if (defined(binaryBuffer)) { 65 | // Write Binary Chunk header (length, type) 66 | glb.writeUInt32LE(binaryBuffer.length, byteOffset); 67 | byteOffset += 4; 68 | glb.writeUInt32LE(0x004e4942, byteOffset); // BIN 69 | byteOffset += 4; 70 | 71 | // Write Binary Chunk 72 | binaryBuffer.copy(glb, byteOffset); 73 | } 74 | 75 | return glb; 76 | } 77 | -------------------------------------------------------------------------------- /lib/findAccessorMinMax.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const ComponentDatatype = Cesium.ComponentDatatype; 5 | const defined = Cesium.defined; 6 | 7 | const getAccessorByteStride = require("./getAccessorByteStride"); 8 | const getComponentReader = require("./getComponentReader"); 9 | const numberOfComponentsForType = require("./numberOfComponentsForType"); 10 | 11 | module.exports = findAccessorMinMax; 12 | 13 | /** 14 | * Finds the min and max values of the accessor. 15 | * 16 | * @param {object} gltf A javascript object containing a glTF asset. 17 | * @param {object} accessor The accessor object from the glTF asset to read. 18 | * @returns {{min: Array, max: Array}} min holding the array of minimum values and max holding the array of maximum values. 19 | * 20 | * @private 21 | */ 22 | function findAccessorMinMax(gltf, accessor) { 23 | const bufferViews = gltf.bufferViews; 24 | const buffers = gltf.buffers; 25 | const bufferViewId = accessor.bufferView; 26 | const numberOfComponents = numberOfComponentsForType(accessor.type); 27 | 28 | // According to the spec, when bufferView is not defined, accessor must be initialized with zeros 29 | if (!defined(accessor.bufferView)) { 30 | return { 31 | min: new Array(numberOfComponents).fill(0.0), 32 | max: new Array(numberOfComponents).fill(0.0), 33 | }; 34 | } 35 | 36 | const min = new Array(numberOfComponents).fill(Number.POSITIVE_INFINITY); 37 | const max = new Array(numberOfComponents).fill(Number.NEGATIVE_INFINITY); 38 | 39 | const bufferView = bufferViews[bufferViewId]; 40 | const bufferId = bufferView.buffer; 41 | const buffer = buffers[bufferId]; 42 | const source = buffer.extras._pipeline.source; 43 | 44 | const count = accessor.count; 45 | const byteStride = getAccessorByteStride(gltf, accessor); 46 | let byteOffset = 47 | accessor.byteOffset + bufferView.byteOffset + source.byteOffset; 48 | const componentType = accessor.componentType; 49 | const componentTypeByteLength = 50 | ComponentDatatype.getSizeInBytes(componentType); 51 | const dataView = new DataView(source.buffer); 52 | const components = new Array(numberOfComponents); 53 | const componentReader = getComponentReader(componentType); 54 | 55 | for (let i = 0; i < count; i++) { 56 | componentReader( 57 | dataView, 58 | byteOffset, 59 | numberOfComponents, 60 | componentTypeByteLength, 61 | components, 62 | ); 63 | for (let j = 0; j < numberOfComponents; j++) { 64 | const value = components[j]; 65 | min[j] = Math.min(min[j], value); 66 | max[j] = Math.max(max[j], value); 67 | } 68 | byteOffset += byteStride; 69 | } 70 | 71 | return { 72 | min: min, 73 | max: max, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /specs/lib/getStatisticsSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const getStatistics = require("../../lib/getStatistics"); 3 | 4 | describe("getStatistics", () => { 5 | const gltf = { 6 | accessors: [ 7 | { 8 | componentType: 5123, 9 | type: "SCALAR", 10 | count: 1, 11 | }, 12 | { 13 | componentType: 5123, 14 | type: "SCALAR", 15 | count: 2, 16 | }, 17 | { 18 | componentType: 5123, 19 | type: "SCALAR", 20 | count: 6, 21 | }, 22 | { 23 | componentType: 5126, 24 | type: "VEC3", 25 | count: 6, 26 | }, 27 | ], 28 | buffers: [ 29 | { 30 | byteLength: 140, 31 | }, 32 | { 33 | byteLength: 120, 34 | uri: "buffer.bin", 35 | }, 36 | ], 37 | images: [ 38 | { 39 | uri: "image.png", 40 | }, 41 | { 42 | uri: "data:image/png;", 43 | }, 44 | { 45 | uri: "image2.png", 46 | }, 47 | ], 48 | meshes: [ 49 | { 50 | primitives: [ 51 | { 52 | indices: 0, 53 | mode: 0, // POINTS 54 | }, 55 | { 56 | indices: 1, 57 | mode: 1, // LINES 58 | }, 59 | { 60 | attributes: { 61 | POSITION: 3, 62 | }, 63 | }, 64 | ], 65 | }, 66 | { 67 | primitives: [ 68 | { 69 | indices: 2, 70 | mode: 4, // TRIANGLES 71 | }, 72 | ], 73 | }, 74 | ], 75 | materials: [{}, {}], 76 | animations: [{}, {}, {}], 77 | nodes: [ 78 | { 79 | name: "rootNode", 80 | mesh: 0, 81 | }, 82 | ], 83 | }; 84 | 85 | it("returns statistics for a gltf", () => { 86 | const statistics = getStatistics(gltf); 87 | expect(statistics.buffersByteLength).toEqual(260); 88 | expect(statistics.numberOfImages).toEqual(3); 89 | expect(statistics.numberOfExternalRequests).toEqual(3); 90 | expect(statistics.numberOfDrawCalls).toEqual(4); 91 | expect(statistics.numberOfRenderedPrimitives).toEqual(6); 92 | expect(statistics.numberOfNodes).toEqual(1); 93 | expect(statistics.numberOfMeshes).toEqual(2); 94 | expect(statistics.numberOfMaterials).toEqual(2); 95 | expect(statistics.numberOfAnimations).toEqual(3); 96 | }); 97 | 98 | it("returns draw call statistics for a gltf node", () => { 99 | const statistics = getStatistics(gltf, 0); 100 | expect(statistics.numberOfDrawCalls).toEqual(3); 101 | expect(statistics.numberOfRenderedPrimitives).toEqual(4); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /specs/data/2.0/box-textured-separate/box-textured-separate.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "COLLADA2GLTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "scenes": [ 8 | { 9 | "nodes": [ 10 | 0 11 | ] 12 | } 13 | ], 14 | "nodes": [ 15 | { 16 | "children": [ 17 | 1 18 | ], 19 | "matrix": [ 20 | 1, 21 | 0, 22 | 0, 23 | 0, 24 | 0, 25 | 0, 26 | -1, 27 | 0, 28 | 0, 29 | 1, 30 | 0, 31 | 0, 32 | 0, 33 | 0, 34 | 0, 35 | 1 36 | ] 37 | }, 38 | { 39 | "mesh": 0 40 | } 41 | ], 42 | "meshes": [ 43 | { 44 | "primitives": [ 45 | { 46 | "attributes": { 47 | "NORMAL": 1, 48 | "POSITION": 2, 49 | "TEXCOORD_0": 3 50 | }, 51 | "indices": 0, 52 | "mode": 4, 53 | "material": 0 54 | } 55 | ], 56 | "name": "Mesh" 57 | } 58 | ], 59 | "accessors": [ 60 | { 61 | "bufferView": 0, 62 | "byteOffset": 0, 63 | "componentType": 5123, 64 | "count": 36, 65 | "max": [ 66 | 23 67 | ], 68 | "min": [ 69 | 0 70 | ], 71 | "type": "SCALAR" 72 | }, 73 | { 74 | "bufferView": 1, 75 | "byteOffset": 0, 76 | "componentType": 5126, 77 | "count": 24, 78 | "max": [ 79 | 1, 80 | 1, 81 | 1 82 | ], 83 | "min": [ 84 | -1, 85 | -1, 86 | -1 87 | ], 88 | "type": "VEC3" 89 | }, 90 | { 91 | "bufferView": 1, 92 | "byteOffset": 288, 93 | "componentType": 5126, 94 | "count": 24, 95 | "max": [ 96 | 0.5, 97 | 0.5, 98 | 0.5 99 | ], 100 | "min": [ 101 | -0.5, 102 | -0.5, 103 | -0.5 104 | ], 105 | "type": "VEC3" 106 | }, 107 | { 108 | "bufferView": 2, 109 | "byteOffset": 0, 110 | "componentType": 5126, 111 | "count": 24, 112 | "max": [ 113 | 6, 114 | 1 115 | ], 116 | "min": [ 117 | 0, 118 | 0 119 | ], 120 | "type": "VEC2" 121 | } 122 | ], 123 | "materials": [ 124 | { 125 | "pbrMetallicRoughness": { 126 | "baseColorTexture": { 127 | "index": 0, 128 | "texCoord": 0 129 | }, 130 | "metallicFactor": 0, 131 | "baseColorFactor": [ 132 | 1, 133 | 1, 134 | 1, 135 | 1 136 | ], 137 | "roughnessFactor": 1 138 | }, 139 | "name": "Texture", 140 | "emissiveFactor": [ 141 | 0, 142 | 0, 143 | 0 144 | ], 145 | "alphaMode": "OPAQUE", 146 | "doubleSided": false 147 | } 148 | ], 149 | "textures": [ 150 | { 151 | "sampler": 0, 152 | "source": 0 153 | } 154 | ], 155 | "images": [ 156 | { 157 | "uri": "cesium%20logo.png" 158 | } 159 | ], 160 | "samplers": [ 161 | { 162 | "magFilter": 9729, 163 | "minFilter": 9986, 164 | "wrapS": 10497, 165 | "wrapT": 10497 166 | } 167 | ], 168 | "bufferViews": [ 169 | { 170 | "buffer": 0, 171 | "byteOffset": 768, 172 | "byteLength": 72, 173 | "target": 34963 174 | }, 175 | { 176 | "buffer": 0, 177 | "byteOffset": 0, 178 | "byteLength": 576, 179 | "byteStride": 12, 180 | "target": 34962 181 | }, 182 | { 183 | "buffer": 0, 184 | "byteOffset": 576, 185 | "byteLength": 192, 186 | "byteStride": 8, 187 | "target": 34962 188 | } 189 | ], 190 | "buffers": [ 191 | { 192 | "byteLength": 840, 193 | "uri": "box-textured-separate.bin" 194 | } 195 | ] 196 | } 197 | -------------------------------------------------------------------------------- /lib/parseGlb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const addPipelineExtras = require("./addPipelineExtras"); 4 | const removeExtensionsUsed = require("./removeExtensionsUsed"); 5 | 6 | const defined = Cesium.defined; 7 | const getMagic = Cesium.getMagic; 8 | const getStringFromTypedArray = Cesium.getStringFromTypedArray; 9 | const RuntimeError = Cesium.RuntimeError; 10 | 11 | const sizeOfUint32 = 4; 12 | 13 | module.exports = parseGlb; 14 | 15 | /** 16 | * Convert a binary glTF to glTF. 17 | * 18 | * The returned glTF has pipeline extras included. The embedded binary data is stored in gltf.buffers[0].extras._pipeline.source. 19 | * 20 | * @param {Buffer} glb The glb data to parse. 21 | * @returns {object} A javascript object containing a glTF asset with pipeline extras included. 22 | * 23 | * @private 24 | */ 25 | function parseGlb(glb) { 26 | // Check that the magic string is present 27 | const magic = getMagic(glb); 28 | if (magic !== "glTF") { 29 | throw new RuntimeError("File is not valid binary glTF"); 30 | } 31 | 32 | const header = readHeader(glb, 0, 5); 33 | const version = header[1]; 34 | if (version !== 1 && version !== 2) { 35 | throw new RuntimeError("Binary glTF version is not 1 or 2"); 36 | } 37 | 38 | if (version === 1) { 39 | return parseGlbVersion1(glb, header); 40 | } 41 | 42 | return parseGlbVersion2(glb, header); 43 | } 44 | 45 | function readHeader(glb, byteOffset, count) { 46 | const dataView = new DataView(glb.buffer); 47 | const header = new Array(count); 48 | for (let i = 0; i < count; ++i) { 49 | header[i] = dataView.getUint32( 50 | glb.byteOffset + byteOffset + i * sizeOfUint32, 51 | true, 52 | ); 53 | } 54 | return header; 55 | } 56 | 57 | function parseGlbVersion1(glb, header) { 58 | const length = header[2]; 59 | const contentLength = header[3]; 60 | const contentFormat = header[4]; 61 | 62 | // Check that the content format is 0, indicating that it is JSON 63 | if (contentFormat !== 0) { 64 | throw new RuntimeError("Binary glTF scene format is not JSON"); 65 | } 66 | 67 | const jsonStart = 20; 68 | const binaryStart = jsonStart + contentLength; 69 | 70 | const contentString = getStringFromTypedArray(glb, jsonStart, contentLength); 71 | const gltf = JSON.parse(contentString); 72 | addPipelineExtras(gltf); 73 | 74 | const binaryBuffer = glb.subarray(binaryStart, length); 75 | 76 | const buffers = gltf.buffers; 77 | if (defined(buffers) && Object.keys(buffers).length > 0) { 78 | // In some older models, the binary glTF buffer is named KHR_binary_glTF 79 | const binaryGltfBuffer = buffers.binary_glTF ?? buffers.KHR_binary_glTF; 80 | if (defined(binaryGltfBuffer)) { 81 | binaryGltfBuffer.extras._pipeline.source = binaryBuffer; 82 | delete binaryGltfBuffer.uri; 83 | } 84 | } 85 | // Remove the KHR_binary_glTF extension 86 | removeExtensionsUsed(gltf, "KHR_binary_glTF"); 87 | return gltf; 88 | } 89 | 90 | function parseGlbVersion2(glb, header) { 91 | const length = header[2]; 92 | let byteOffset = 12; 93 | let gltf; 94 | let binaryBuffer; 95 | while (byteOffset < length) { 96 | const chunkHeader = readHeader(glb, byteOffset, 2); 97 | const chunkLength = chunkHeader[0]; 98 | const chunkType = chunkHeader[1]; 99 | byteOffset += 8; 100 | const chunkBuffer = glb.subarray(byteOffset, byteOffset + chunkLength); 101 | byteOffset += chunkLength; 102 | // Load JSON chunk 103 | if (chunkType === 0x4e4f534a) { 104 | const jsonString = getStringFromTypedArray(chunkBuffer); 105 | gltf = JSON.parse(jsonString); 106 | addPipelineExtras(gltf); 107 | } 108 | // Load Binary chunk 109 | else if (chunkType === 0x004e4942) { 110 | binaryBuffer = chunkBuffer; 111 | } 112 | } 113 | if (defined(gltf) && defined(binaryBuffer)) { 114 | const buffers = gltf.buffers; 115 | if (defined(buffers) && buffers.length > 0) { 116 | const buffer = buffers[0]; 117 | buffer.extras._pipeline.source = binaryBuffer; 118 | } 119 | } 120 | return gltf; 121 | } 122 | -------------------------------------------------------------------------------- /specs/lib/removeExtensionSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const removeExtension = require("../../lib/removeExtension"); 4 | 5 | const WebGLConstants = Cesium.WebGLConstants; 6 | 7 | describe("removeExtension", () => { 8 | it("removes extension", () => { 9 | const gltf = { 10 | extensionsRequired: ["extension1", "extension2", "extension3"], 11 | extensionsUsed: ["extension1", "extension2", "extension3"], 12 | extensions: { 13 | extension1: { 14 | value: 9, 15 | }, 16 | extension2: [0, 1, 2], 17 | }, 18 | materials: [ 19 | { 20 | baseColorFactor: [1.0, 0.0, 0.0, 1.0], 21 | extensions: { 22 | extension1: { 23 | value: 10, 24 | }, 25 | }, 26 | }, 27 | { 28 | baseColorFactor: [0.0, 0.0, 1.0, 1.0], 29 | extensions: { 30 | extension1: { 31 | value: 11, 32 | }, 33 | }, 34 | }, 35 | ], 36 | cameras: [ 37 | { 38 | extensions: { 39 | extension1: { 40 | value: 9, 41 | }, 42 | extension2: [3, 4, 5], 43 | }, 44 | }, 45 | ], 46 | }; 47 | const extension1 = removeExtension(gltf, "extension1"); 48 | expect(gltf.extensionsRequired).toEqual(["extension2", "extension3"]); 49 | expect(gltf.extensionsUsed).toEqual(["extension2", "extension3"]); 50 | expect(gltf.extensions).toEqual({ 51 | extension2: [0, 1, 2], 52 | }); 53 | expect(gltf.materials[0].extensions).toBeUndefined(); 54 | expect(gltf.materials[1].extensions).toBeUndefined(); 55 | expect(gltf.cameras[0].extensions).toEqual({ 56 | extension2: [3, 4, 5], 57 | }); 58 | expect(extension1).toEqual({ 59 | value: 9, 60 | }); 61 | 62 | const extension2 = removeExtension(gltf, "extension2"); 63 | expect(gltf.extensionsRequired).toEqual(["extension3"]); 64 | expect(gltf.extensionsUsed).toEqual(["extension3"]); 65 | expect(gltf.extensions).toBeUndefined(); 66 | expect(gltf.materials[0].extensions).toBeUndefined(); 67 | expect(gltf.materials[1].extensions).toBeUndefined(); 68 | expect(gltf.cameras[0].extensions).toBeUndefined(); 69 | expect(extension2).toEqual([0, 1, 2]); 70 | 71 | const extension3 = removeExtension(gltf, "extension3"); 72 | expect(gltf.extensionsRequired).toBeUndefined(); 73 | expect(gltf.extensionsUsed).toBeUndefined(); 74 | expect(gltf.extensions).toBeUndefined(); 75 | expect(gltf.materials[0].extensions).toBeUndefined(); 76 | expect(gltf.materials[1].extensions).toBeUndefined(); 77 | expect(gltf.cameras[0].extensions).toBeUndefined(); 78 | expect(extension3).toBeUndefined(); 79 | 80 | const emptyGltf = {}; 81 | removeExtension(gltf, "extension1"); 82 | expect(emptyGltf).toEqual({}); 83 | }); 84 | 85 | it("removes CESIUM_RTC extension", () => { 86 | const gltf = { 87 | extensionsRequired: ["CESIUM_RTC", "KHR_techniques_webgl"], 88 | extensionsUsed: ["CESIUM_RTC", "KHR_techniques_webgl"], 89 | extensions: { 90 | CESIUM_RTC: { 91 | center: [1.0, 2.0, 3.0], 92 | }, 93 | KHR_techniques_webgl: { 94 | techniques: [ 95 | { 96 | uniforms: { 97 | u_modelViewMatrix: { 98 | type: WebGLConstants.FLOAT_MAT4, 99 | semantic: "CESIUM_RTC_MODELVIEW", 100 | }, 101 | }, 102 | }, 103 | ], 104 | }, 105 | }, 106 | }; 107 | const extension = removeExtension(gltf, "CESIUM_RTC"); 108 | expect(extension).toEqual({ 109 | center: [1.0, 2.0, 3.0], 110 | }); 111 | expect(gltf.extensionsRequired).toEqual(["KHR_techniques_webgl"]); 112 | expect(gltf.extensionsUsed).toEqual(["KHR_techniques_webgl"]); 113 | expect( 114 | gltf.extensions.KHR_techniques_webgl.techniques[0].uniforms 115 | .u_modelViewMatrix.semantic, 116 | ).toBe("MODELVIEW"); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /specs/lib/getImageExtensionSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { RuntimeError } = require("cesium"); 4 | 5 | const dataUriToBuffer = require("../../lib/dataUriToBuffer"); 6 | const getImageExtension = require("../../lib/getImageExtension"); 7 | 8 | const pngData = dataUriToBuffer( 9 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gQcDxwOcoRpqQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12P8//8/AwwwMSAB3BwAlm4DBdoYksUAAAAASUVORK5CYII=", 10 | ); 11 | const gifData = dataUriToBuffer( 12 | "data:image/gif;base64,R0lGODdhBAAEAIAAAP///////ywAAAAABAAEAAACBISPCQUAOw==", 13 | ); 14 | const jpgData = dataUriToBuffer( 15 | "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAEAAQDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACP/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAVSf/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9oADAMBAAIAAwAAABCf/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPxB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPxB//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxB//9k=", 16 | ); 17 | const bmpData = dataUriToBuffer( 18 | "data:image/bmp;base64,Qk1mAAAAAAAAADYAAAAoAAAABAAAAAQAAAABABgAAAAAADAAAAATCwAAEwsAAAAAAAAAAAAA////////////////////////////////////////////////////////////////", 19 | ); 20 | const ktx2Data = dataUriToBuffer( 21 | "data:image/ktx2:base64,q0tUWCAyMLsNChoKAAAAAAEAAABAAAAAQAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAaAAAACwAAACUAAAAdAAAAAgBAAAAAAAA7QEAAAAAAAD1AgAAAAAAAPgAAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAIAKACjAQIAAwMAAAAAAAAAAAAAAAA/AAAAAAAAAAAA/////xIAAABLVFhvcmllbnRhdGlvbgByZAAAAFUAAABLVFh3cml0ZXIAdG9rdHggdjQuMC5iZXRhMi41My5nZjgzZDI4NDUuZGlydHkgLyBsaWJrdHggdjQuMC5iZXRhMi41MS5nM2EzMWQ0ZGEuZGlydHkAAAAAHQBfAEcAAAAcAQAAYgAAAAAAAAAAAAAAAAAAAPgAAAAAAAAAAAAAACDAGwABAIBgclixPD5qPIA3EAEAAAJpL1qO+wKACQAAAAAAAAQCEAEAAABACMSHxYxC5qRfZDX/tDYozqTrsZwcLKFPOj8HAAjmamgUQQAA4G9md89jvYMNJJDEE0erNdUVV1pMAlpY6II/hRiwULCxywNMUggi6NVWCjb+FBYpBRcLawsfwLyBDyAEEpo8f13mE8N3+MzWN8aG0uInpReLBNfESNpLUhJjn6b0UYqK6fRJOijLPA7LtfXfpawtHOM5wslUOelTK5qmmCyvsvvBs9vv7W4xDJxWwcfD2u9chVzP234bZjCbzdBt9+rlcHEnF9Uqax51Mg7aopqvbDx89+Bm8yd0dfjyNdzXnWaTS7dwHQMKYiRu4hcUupGEncw9fArUMQahsqgFgqPLjReLeCFOmjpQB/VCqMhnZzzd2+xeHe83j+sRN/Cff4yZ8Q0Cxnpj/0d75e6dw3y0evn6/C0AwdsAARAGgie2geTzWDtGyh9KgpX8WJb+BoILSDZI+sB3AA8ABEAQhjVfWhkigV6/gQJxMCXBAADD9G9HsB/tCHQCO40d4H1eHXIIwiGiqaHGIEjX+AaACQAAAAAAEMRAAPxNflpzjZbtuGAsfCOdQtK6+1SN1+ekY6j7t75MEIfKzChq81nw7nJCXzrVkyZprvXxESlhruXkPYHimL3u9o3k57aC4TXqLc9ZhbxM8VrYjcZ4NtT2+55ZPICCeHUISHGmLmU+/LNbHx0jlpkCmnpHFAZSaHnCJ49gGUlMs7+K4PcBQ9KG618GBGDlxTULGQlp8NdIpU8tdzAFkCSPVU20irfukO/YSpScsCHeCglbH2ljLGR+7McSAoyLd9j0k06gjHZdYiPGpi0n6fX7PCd22juv/0qvP5YIoHnrGZEXMiFaw9chnA2C8POpNi0EQu/lwCZoILII", 22 | ); 23 | const basisData = dataUriToBuffer( 24 | "data:image/basis;base64,c0ITAE0AAA9wAAAAqewBAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAQBkAAAAKAAAAQCMAAAABQAAkQAAACsAAABNAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAvAAAAAEAAACPUwHABAAAAAAAAAICmAgAAAAAAEASBAATAAAAAAAAiABgAgAAAAAAABFUVVVVBQDBRAAAAAAAAPJfLQCYAAAAAAAAQAgAEwACAAAAAIgBwAQAAAAAAAACCAAA", 25 | ); 26 | const textData = dataUriToBuffer("data:text/plain;charset=utf-8,randomtext"); 27 | 28 | describe("getImageExtension", () => { 29 | it("gets image extension from buffer", () => { 30 | expect(getImageExtension(pngData)).toBe(".png"); 31 | expect(getImageExtension(gifData)).toBe(".gif"); 32 | expect(getImageExtension(jpgData)).toBe(".jpg"); 33 | expect(getImageExtension(bmpData)).toBe(".bmp"); 34 | expect(getImageExtension(ktx2Data)).toBe(".ktx2"); 35 | expect(getImageExtension(basisData)).toBe(".basis"); 36 | }); 37 | 38 | it("throws error if buffer does not contain image data", () => { 39 | let thrownError; 40 | try { 41 | getImageExtension(textData); 42 | } catch (e) { 43 | thrownError = e; 44 | } 45 | expect(thrownError).toEqual( 46 | new RuntimeError("Image data does not have valid header"), 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/box-textured-separate.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "COLLADA2GLTF", 4 | "version": "2.0" 5 | }, 6 | "extensionsUsed": [ 7 | "EXT_texture_webp" 8 | ], 9 | "extensionsRequired": [ 10 | "EXT_texture_webp" 11 | ], 12 | "scene": 0, 13 | "scenes": [ 14 | { 15 | "nodes": [ 16 | 0 17 | ] 18 | } 19 | ], 20 | "nodes": [ 21 | { 22 | "children": [ 23 | 1 24 | ], 25 | "matrix": [ 26 | 1, 27 | 0, 28 | 0, 29 | 0, 30 | 0, 31 | 0, 32 | -1, 33 | 0, 34 | 0, 35 | 1, 36 | 0, 37 | 0, 38 | 0, 39 | 0, 40 | 0, 41 | 1 42 | ] 43 | }, 44 | { 45 | "mesh": 0 46 | } 47 | ], 48 | "meshes": [ 49 | { 50 | "primitives": [ 51 | { 52 | "attributes": { 53 | "NORMAL": 1, 54 | "POSITION": 2, 55 | "TEXCOORD_0": 3 56 | }, 57 | "indices": 0, 58 | "mode": 4, 59 | "material": 0 60 | } 61 | ], 62 | "name": "Mesh" 63 | } 64 | ], 65 | "accessors": [ 66 | { 67 | "bufferView": 0, 68 | "byteOffset": 0, 69 | "componentType": 5123, 70 | "count": 36, 71 | "max": [ 72 | 23 73 | ], 74 | "min": [ 75 | 0 76 | ], 77 | "type": "SCALAR" 78 | }, 79 | { 80 | "bufferView": 1, 81 | "byteOffset": 0, 82 | "componentType": 5126, 83 | "count": 24, 84 | "max": [ 85 | 1, 86 | 1, 87 | 1 88 | ], 89 | "min": [ 90 | -1, 91 | -1, 92 | -1 93 | ], 94 | "type": "VEC3" 95 | }, 96 | { 97 | "bufferView": 1, 98 | "byteOffset": 288, 99 | "componentType": 5126, 100 | "count": 24, 101 | "max": [ 102 | 0.5, 103 | 0.5, 104 | 0.5 105 | ], 106 | "min": [ 107 | -0.5, 108 | -0.5, 109 | -0.5 110 | ], 111 | "type": "VEC3" 112 | }, 113 | { 114 | "bufferView": 2, 115 | "byteOffset": 0, 116 | "componentType": 5126, 117 | "count": 24, 118 | "max": [ 119 | 6, 120 | 1 121 | ], 122 | "min": [ 123 | 0, 124 | 0 125 | ], 126 | "type": "VEC2" 127 | } 128 | ], 129 | "materials": [ 130 | { 131 | "pbrMetallicRoughness": { 132 | "baseColorTexture": { 133 | "index": 0, 134 | "texCoord": 0 135 | }, 136 | "metallicFactor": 0, 137 | "baseColorFactor": [ 138 | 1, 139 | 1, 140 | 1, 141 | 1 142 | ], 143 | "roughnessFactor": 1 144 | }, 145 | "name": "Texture", 146 | "emissiveFactor": [ 147 | 0, 148 | 0, 149 | 0 150 | ], 151 | "alphaMode": "OPAQUE", 152 | "doubleSided": false 153 | } 154 | ], 155 | "textures": [ 156 | { 157 | "sampler": 0, 158 | "extensions": { 159 | "EXT_texture_webp": { 160 | "source": 0 161 | } 162 | } 163 | } 164 | ], 165 | "images": [ 166 | { 167 | "uri": "cesium%20logo.webp" 168 | } 169 | ], 170 | "samplers": [ 171 | { 172 | "magFilter": 9729, 173 | "minFilter": 9986, 174 | "wrapS": 10497, 175 | "wrapT": 10497 176 | } 177 | ], 178 | "bufferViews": [ 179 | { 180 | "buffer": 0, 181 | "byteOffset": 768, 182 | "byteLength": 72, 183 | "target": 34963 184 | }, 185 | { 186 | "buffer": 0, 187 | "byteOffset": 0, 188 | "byteLength": 576, 189 | "byteStride": 12, 190 | "target": 34962 191 | }, 192 | { 193 | "buffer": 0, 194 | "byteOffset": 576, 195 | "byteLength": 192, 196 | "byteStride": 8, 197 | "target": 34962 198 | } 199 | ], 200 | "buffers": [ 201 | { 202 | "byteLength": 840, 203 | "uri": "box-textured-separate.bin" 204 | } 205 | ] 206 | } 207 | -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_texture_webp/box-textured-separate/box-textured-with-fallback.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "COLLADA2GLTF", 4 | "version": "2.0" 5 | }, 6 | "extensionsUsed": [ 7 | "EXT_texture_webp" 8 | ], 9 | "scene": 0, 10 | "scenes": [ 11 | { 12 | "nodes": [ 13 | 0 14 | ] 15 | } 16 | ], 17 | "nodes": [ 18 | { 19 | "children": [ 20 | 1 21 | ], 22 | "matrix": [ 23 | 1, 24 | 0, 25 | 0, 26 | 0, 27 | 0, 28 | 0, 29 | -1, 30 | 0, 31 | 0, 32 | 1, 33 | 0, 34 | 0, 35 | 0, 36 | 0, 37 | 0, 38 | 1 39 | ] 40 | }, 41 | { 42 | "mesh": 0 43 | } 44 | ], 45 | "meshes": [ 46 | { 47 | "primitives": [ 48 | { 49 | "attributes": { 50 | "NORMAL": 1, 51 | "POSITION": 2, 52 | "TEXCOORD_0": 3 53 | }, 54 | "indices": 0, 55 | "mode": 4, 56 | "material": 0 57 | } 58 | ], 59 | "name": "Mesh" 60 | } 61 | ], 62 | "accessors": [ 63 | { 64 | "bufferView": 0, 65 | "byteOffset": 0, 66 | "componentType": 5123, 67 | "count": 36, 68 | "max": [ 69 | 23 70 | ], 71 | "min": [ 72 | 0 73 | ], 74 | "type": "SCALAR" 75 | }, 76 | { 77 | "bufferView": 1, 78 | "byteOffset": 0, 79 | "componentType": 5126, 80 | "count": 24, 81 | "max": [ 82 | 1, 83 | 1, 84 | 1 85 | ], 86 | "min": [ 87 | -1, 88 | -1, 89 | -1 90 | ], 91 | "type": "VEC3" 92 | }, 93 | { 94 | "bufferView": 1, 95 | "byteOffset": 288, 96 | "componentType": 5126, 97 | "count": 24, 98 | "max": [ 99 | 0.5, 100 | 0.5, 101 | 0.5 102 | ], 103 | "min": [ 104 | -0.5, 105 | -0.5, 106 | -0.5 107 | ], 108 | "type": "VEC3" 109 | }, 110 | { 111 | "bufferView": 2, 112 | "byteOffset": 0, 113 | "componentType": 5126, 114 | "count": 24, 115 | "max": [ 116 | 6, 117 | 1 118 | ], 119 | "min": [ 120 | 0, 121 | 0 122 | ], 123 | "type": "VEC2" 124 | } 125 | ], 126 | "materials": [ 127 | { 128 | "pbrMetallicRoughness": { 129 | "baseColorTexture": { 130 | "index": 0, 131 | "texCoord": 0 132 | }, 133 | "metallicFactor": 0, 134 | "baseColorFactor": [ 135 | 1, 136 | 1, 137 | 1, 138 | 1 139 | ], 140 | "roughnessFactor": 1 141 | }, 142 | "name": "Texture", 143 | "emissiveFactor": [ 144 | 0, 145 | 0, 146 | 0 147 | ], 148 | "alphaMode": "OPAQUE", 149 | "doubleSided": false 150 | } 151 | ], 152 | "textures": [ 153 | { 154 | "sampler": 0, 155 | "source": 1, 156 | "extensions": { 157 | "EXT_texture_webp": { 158 | "source": 0 159 | } 160 | } 161 | } 162 | ], 163 | "images": [ 164 | { 165 | "uri": "cesium%20logo.webp" 166 | }, 167 | { 168 | "uri": "cesium%20logo.png" 169 | } 170 | ], 171 | "samplers": [ 172 | { 173 | "magFilter": 9729, 174 | "minFilter": 9986, 175 | "wrapS": 10497, 176 | "wrapT": 10497 177 | } 178 | ], 179 | "bufferViews": [ 180 | { 181 | "buffer": 0, 182 | "byteOffset": 768, 183 | "byteLength": 72, 184 | "target": 34963 185 | }, 186 | { 187 | "buffer": 0, 188 | "byteOffset": 0, 189 | "byteLength": 576, 190 | "byteStride": 12, 191 | "target": 34962 192 | }, 193 | { 194 | "buffer": 0, 195 | "byteOffset": 576, 196 | "byteLength": 192, 197 | "byteStride": 8, 198 | "target": 34962 199 | } 200 | ], 201 | "buffers": [ 202 | { 203 | "byteLength": 840, 204 | "uri": "box-textured-separate.bin" 205 | } 206 | ] 207 | } 208 | -------------------------------------------------------------------------------- /lib/moveTechniqueRenderStates.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Cesium = require("cesium"); 4 | const addExtensionsUsed = require("./addExtensionsUsed"); 5 | const ForEach = require("./ForEach"); 6 | 7 | const defined = Cesium.defined; 8 | const WebGLConstants = Cesium.WebGLConstants; 9 | 10 | module.exports = moveTechniqueRenderStates; 11 | 12 | const defaultBlendEquation = [WebGLConstants.FUNC_ADD, WebGLConstants.FUNC_ADD]; 13 | 14 | const defaultBlendFactors = [ 15 | WebGLConstants.ONE, 16 | WebGLConstants.ZERO, 17 | WebGLConstants.ONE, 18 | WebGLConstants.ZERO, 19 | ]; 20 | 21 | function isStateEnabled(renderStates, state) { 22 | const enabled = renderStates.enable; 23 | if (!defined(enabled)) { 24 | return false; 25 | } 26 | 27 | return enabled.indexOf(state) > -1; 28 | } 29 | 30 | const supportedBlendFactors = [ 31 | WebGLConstants.ZERO, 32 | WebGLConstants.ONE, 33 | WebGLConstants.SRC_COLOR, 34 | WebGLConstants.ONE_MINUS_SRC_COLOR, 35 | WebGLConstants.SRC_ALPHA, 36 | WebGLConstants.ONE_MINUS_SRC_ALPHA, 37 | WebGLConstants.DST_ALPHA, 38 | WebGLConstants.ONE_MINUS_DST_ALPHA, 39 | WebGLConstants.DST_COLOR, 40 | WebGLConstants.ONE_MINUS_DST_COLOR, 41 | ]; 42 | 43 | // If any of the blend factors are not supported, return the default 44 | function getSupportedBlendFactors(value, defaultValue) { 45 | if (!defined(value)) { 46 | return defaultValue; 47 | } 48 | 49 | for (let i = 0; i < 4; i++) { 50 | if (supportedBlendFactors.indexOf(value[i]) === -1) { 51 | return defaultValue; 52 | } 53 | } 54 | 55 | return value; 56 | } 57 | 58 | /** 59 | * Move glTF 1.0 technique render states to glTF 2.0 materials properties and KHR_blend extension. 60 | * 61 | * @param {object} gltf A javascript object containing a glTF asset. 62 | * @returns {object} The updated glTF asset. 63 | * 64 | * @private 65 | */ 66 | function moveTechniqueRenderStates(gltf) { 67 | const blendingForTechnique = {}; 68 | const materialPropertiesForTechnique = {}; 69 | const techniquesLegacy = gltf.techniques; 70 | if (!defined(techniquesLegacy)) { 71 | return gltf; 72 | } 73 | 74 | ForEach.technique(gltf, function (techniqueLegacy, techniqueIndex) { 75 | const renderStates = techniqueLegacy.states; 76 | if (defined(renderStates)) { 77 | const materialProperties = (materialPropertiesForTechnique[ 78 | techniqueIndex 79 | ] = {}); 80 | 81 | // If BLEND is enabled, the material should have alpha mode BLEND 82 | if (isStateEnabled(renderStates, WebGLConstants.BLEND)) { 83 | materialProperties.alphaMode = "BLEND"; 84 | 85 | const blendFunctions = renderStates.functions; 86 | if ( 87 | defined(blendFunctions) && 88 | (defined(blendFunctions.blendEquationSeparate) || 89 | defined(blendFunctions.blendFuncSeparate)) 90 | ) { 91 | blendingForTechnique[techniqueIndex] = { 92 | blendEquation: 93 | blendFunctions.blendEquationSeparate ?? defaultBlendEquation, 94 | blendFactors: getSupportedBlendFactors( 95 | blendFunctions.blendFuncSeparate, 96 | defaultBlendFactors, 97 | ), 98 | }; 99 | } 100 | } 101 | 102 | // If CULL_FACE is not enabled, the material should be doubleSided 103 | if (!isStateEnabled(renderStates, WebGLConstants.CULL_FACE)) { 104 | materialProperties.doubleSided = true; 105 | } 106 | 107 | delete techniqueLegacy.states; 108 | } 109 | }); 110 | 111 | if (Object.keys(blendingForTechnique).length > 0) { 112 | if (!defined(gltf.extensions)) { 113 | gltf.extensions = {}; 114 | } 115 | 116 | addExtensionsUsed(gltf, "KHR_blend"); 117 | } 118 | 119 | ForEach.material(gltf, function (material) { 120 | if (defined(material.technique)) { 121 | const materialProperties = 122 | materialPropertiesForTechnique[material.technique]; 123 | ForEach.objectLegacy(materialProperties, function (value, property) { 124 | material[property] = value; 125 | }); 126 | 127 | const blending = blendingForTechnique[material.technique]; 128 | if (defined(blending)) { 129 | if (!defined(material.extensions)) { 130 | material.extensions = {}; 131 | } 132 | 133 | material.extensions.KHR_blend = blending; 134 | } 135 | } 136 | }); 137 | 138 | return gltf; 139 | } 140 | -------------------------------------------------------------------------------- /lib/getComponentReader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const ComponentDatatype = Cesium.ComponentDatatype; 5 | 6 | module.exports = getComponentReader; 7 | 8 | /** 9 | * Returns a function to read and convert data from a DataView into an array. 10 | * 11 | * @param {number} componentType Type to convert the data to. 12 | * @returns {ComponentReader} Function that reads and converts data. 13 | * 14 | * @private 15 | */ 16 | function getComponentReader(componentType) { 17 | switch (componentType) { 18 | case ComponentDatatype.BYTE: 19 | return function ( 20 | dataView, 21 | byteOffset, 22 | numberOfComponents, 23 | componentTypeByteLength, 24 | result, 25 | ) { 26 | for (let i = 0; i < numberOfComponents; ++i) { 27 | result[i] = dataView.getInt8( 28 | byteOffset + i * componentTypeByteLength, 29 | ); 30 | } 31 | }; 32 | case ComponentDatatype.UNSIGNED_BYTE: 33 | return function ( 34 | dataView, 35 | byteOffset, 36 | numberOfComponents, 37 | componentTypeByteLength, 38 | result, 39 | ) { 40 | for (let i = 0; i < numberOfComponents; ++i) { 41 | result[i] = dataView.getUint8( 42 | byteOffset + i * componentTypeByteLength, 43 | ); 44 | } 45 | }; 46 | case ComponentDatatype.SHORT: 47 | return function ( 48 | dataView, 49 | byteOffset, 50 | numberOfComponents, 51 | componentTypeByteLength, 52 | result, 53 | ) { 54 | for (let i = 0; i < numberOfComponents; ++i) { 55 | result[i] = dataView.getInt16( 56 | byteOffset + i * componentTypeByteLength, 57 | true, 58 | ); 59 | } 60 | }; 61 | case ComponentDatatype.UNSIGNED_SHORT: 62 | return function ( 63 | dataView, 64 | byteOffset, 65 | numberOfComponents, 66 | componentTypeByteLength, 67 | result, 68 | ) { 69 | for (let i = 0; i < numberOfComponents; ++i) { 70 | result[i] = dataView.getUint16( 71 | byteOffset + i * componentTypeByteLength, 72 | true, 73 | ); 74 | } 75 | }; 76 | case ComponentDatatype.INT: 77 | return function ( 78 | dataView, 79 | byteOffset, 80 | numberOfComponents, 81 | componentTypeByteLength, 82 | result, 83 | ) { 84 | for (let i = 0; i < numberOfComponents; ++i) { 85 | result[i] = dataView.getInt32( 86 | byteOffset + i * componentTypeByteLength, 87 | true, 88 | ); 89 | } 90 | }; 91 | case ComponentDatatype.UNSIGNED_INT: 92 | return function ( 93 | dataView, 94 | byteOffset, 95 | numberOfComponents, 96 | componentTypeByteLength, 97 | result, 98 | ) { 99 | for (let i = 0; i < numberOfComponents; ++i) { 100 | result[i] = dataView.getUint32( 101 | byteOffset + i * componentTypeByteLength, 102 | true, 103 | ); 104 | } 105 | }; 106 | case ComponentDatatype.FLOAT: 107 | return function ( 108 | dataView, 109 | byteOffset, 110 | numberOfComponents, 111 | componentTypeByteLength, 112 | result, 113 | ) { 114 | for (let i = 0; i < numberOfComponents; ++i) { 115 | result[i] = dataView.getFloat32( 116 | byteOffset + i * componentTypeByteLength, 117 | true, 118 | ); 119 | } 120 | }; 121 | case ComponentDatatype.DOUBLE: 122 | return function ( 123 | dataView, 124 | byteOffset, 125 | numberOfComponents, 126 | componentTypeByteLength, 127 | result, 128 | ) { 129 | for (let i = 0; i < numberOfComponents; ++i) { 130 | result[i] = dataView.getFloat64( 131 | byteOffset + i * componentTypeByteLength, 132 | true, 133 | ); 134 | } 135 | }; 136 | } 137 | } 138 | 139 | /** 140 | * A callback function that logs messages. 141 | * @callback ComponentReader 142 | * 143 | * @param {DataView} dataView The data view to read from. 144 | * @param {number} byteOffset The byte offset applied when reading from the data view. 145 | * @param {number} numberOfComponents The number of components to read. 146 | * @param {number} componentTypeByteLength The byte length of each component. 147 | * @param {number} result An array storing the components that are read. 148 | * 149 | * @private 150 | */ 151 | -------------------------------------------------------------------------------- /lib/moveTechniquesToExtension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Cesium = require("cesium"); 4 | const addExtensionsUsed = require("./addExtensionsUsed"); 5 | const addExtensionsRequired = require("./addExtensionsRequired"); 6 | const addToArray = require("./addToArray"); 7 | const ForEach = require("./ForEach"); 8 | 9 | const defined = Cesium.defined; 10 | 11 | module.exports = moveTechniquesToExtension; 12 | 13 | /** 14 | * Move glTF 1.0 material techniques to glTF 2.0 KHR_techniques_webgl extension. 15 | * 16 | * @param {object} gltf A javascript object containing a glTF asset. 17 | * @returns {object} The updated glTF asset. 18 | * 19 | * @private 20 | */ 21 | function moveTechniquesToExtension(gltf) { 22 | const techniquesLegacy = gltf.techniques; 23 | const mappedUniforms = {}; 24 | const updatedTechniqueIndices = {}; 25 | const seenPrograms = {}; 26 | if (defined(techniquesLegacy)) { 27 | const extension = { 28 | programs: [], 29 | shaders: [], 30 | techniques: [], 31 | }; 32 | 33 | // Some 1.1 models have a glExtensionsUsed property that can be transferred to program.glExtensions 34 | const glExtensions = gltf.glExtensionsUsed; 35 | delete gltf.glExtensionsUsed; 36 | 37 | ForEach.technique(gltf, function (techniqueLegacy, techniqueId) { 38 | const technique = { 39 | name: techniqueLegacy.name, 40 | program: undefined, 41 | attributes: {}, 42 | uniforms: {}, 43 | }; 44 | 45 | let parameterLegacy; 46 | ForEach.techniqueAttribute( 47 | techniqueLegacy, 48 | function (parameterName, attributeName) { 49 | parameterLegacy = techniqueLegacy.parameters[parameterName]; 50 | technique.attributes[attributeName] = { 51 | semantic: parameterLegacy.semantic, 52 | }; 53 | }, 54 | ); 55 | 56 | ForEach.techniqueUniform( 57 | techniqueLegacy, 58 | function (parameterName, uniformName) { 59 | parameterLegacy = techniqueLegacy.parameters[parameterName]; 60 | technique.uniforms[uniformName] = { 61 | count: parameterLegacy.count, 62 | node: parameterLegacy.node, 63 | type: parameterLegacy.type, 64 | semantic: parameterLegacy.semantic, 65 | value: parameterLegacy.value, 66 | }; 67 | 68 | // Store the name of the uniform to update material values. 69 | if (!defined(mappedUniforms[techniqueId])) { 70 | mappedUniforms[techniqueId] = {}; 71 | } 72 | mappedUniforms[techniqueId][parameterName] = uniformName; 73 | }, 74 | ); 75 | 76 | if (!defined(seenPrograms[techniqueLegacy.program])) { 77 | const programLegacy = gltf.programs[techniqueLegacy.program]; 78 | 79 | const program = { 80 | name: programLegacy.name, 81 | fragmentShader: undefined, 82 | vertexShader: undefined, 83 | glExtensions: glExtensions, 84 | }; 85 | 86 | const fs = gltf.shaders[programLegacy.fragmentShader]; 87 | program.fragmentShader = addToArray(extension.shaders, fs, true); 88 | 89 | const vs = gltf.shaders[programLegacy.vertexShader]; 90 | program.vertexShader = addToArray(extension.shaders, vs, true); 91 | 92 | technique.program = addToArray(extension.programs, program); 93 | seenPrograms[techniqueLegacy.program] = technique.program; 94 | } else { 95 | technique.program = seenPrograms[techniqueLegacy.program]; 96 | } 97 | 98 | // Store the index of the new technique to reference instead. 99 | updatedTechniqueIndices[techniqueId] = addToArray( 100 | extension.techniques, 101 | technique, 102 | ); 103 | }); 104 | 105 | if (extension.techniques.length > 0) { 106 | if (!defined(gltf.extensions)) { 107 | gltf.extensions = {}; 108 | } 109 | 110 | gltf.extensions.KHR_techniques_webgl = extension; 111 | addExtensionsUsed(gltf, "KHR_techniques_webgl"); 112 | addExtensionsRequired(gltf, "KHR_techniques_webgl"); 113 | } 114 | } 115 | 116 | ForEach.material(gltf, function (material) { 117 | if (defined(material.technique)) { 118 | const materialExtension = { 119 | technique: updatedTechniqueIndices[material.technique], 120 | }; 121 | 122 | ForEach.objectLegacy(material.values, function (value, parameterName) { 123 | if (!defined(materialExtension.values)) { 124 | materialExtension.values = {}; 125 | } 126 | 127 | const uniformName = mappedUniforms[material.technique][parameterName]; 128 | if (defined(uniformName)) { 129 | materialExtension.values[uniformName] = value; 130 | } 131 | }); 132 | 133 | if (!defined(material.extensions)) { 134 | material.extensions = {}; 135 | } 136 | 137 | material.extensions.KHR_techniques_webgl = materialExtension; 138 | } 139 | 140 | delete material.technique; 141 | delete material.values; 142 | }); 143 | 144 | delete gltf.techniques; 145 | delete gltf.programs; 146 | delete gltf.shaders; 147 | 148 | return gltf; 149 | } 150 | -------------------------------------------------------------------------------- /specs/data/1.0/box-materials-common/box-materials-common.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": { 3 | "accessor_21": { 4 | "bufferView": "bufferView_29", 5 | "byteOffset": 0, 6 | "byteStride": 0, 7 | "componentType": 5123, 8 | "count": 36, 9 | "type": "SCALAR" 10 | }, 11 | "accessor_23": { 12 | "bufferView": "bufferView_30", 13 | "byteOffset": 0, 14 | "byteStride": 12, 15 | "componentType": 5126, 16 | "count": 24, 17 | "max": [ 18 | 0.5, 19 | 0.5, 20 | 0.5 21 | ], 22 | "min": [ 23 | -0.5, 24 | -0.5, 25 | -0.5 26 | ], 27 | "type": "VEC3" 28 | }, 29 | "accessor_25": { 30 | "bufferView": "bufferView_30", 31 | "byteOffset": 288, 32 | "byteStride": 12, 33 | "componentType": 5126, 34 | "count": 24, 35 | "max": [ 36 | 1, 37 | 1, 38 | 1 39 | ], 40 | "min": [ 41 | -1, 42 | -1, 43 | -1 44 | ], 45 | "type": "VEC3" 46 | } 47 | }, 48 | "animations": {}, 49 | "asset": { 50 | "generator": "collada2gltf@027f74366341d569dea42e9a68b7104cc3892054", 51 | "premultipliedAlpha": true, 52 | "profile": { 53 | "api": "WebGL", 54 | "version": "1.0.2" 55 | }, 56 | "version": "1.0" 57 | }, 58 | "bufferViews": { 59 | "bufferView_29": { 60 | "buffer": "Box", 61 | "byteLength": 72, 62 | "byteOffset": 0, 63 | "target": 34963 64 | }, 65 | "bufferView_30": { 66 | "buffer": "Box", 67 | "byteLength": 576, 68 | "byteOffset": 72, 69 | "target": 34962 70 | } 71 | }, 72 | "buffers": { 73 | "Box": { 74 | "byteLength": 648, 75 | "type": "arraybuffer", 76 | "uri": "box-materials-common.bin" 77 | } 78 | }, 79 | "extensionsUsed": [ 80 | "KHR_materials_common" 81 | ], 82 | "materials": { 83 | "Effect-Red": { 84 | "extensions": { 85 | "KHR_materials_common": { 86 | "doubleSided": false, 87 | "jointCount": 0, 88 | "technique": "PHONG", 89 | "transparent": false, 90 | "values": { 91 | "diffuse": [ 92 | 0.8, 93 | 0, 94 | 0, 95 | 1 96 | ], 97 | "shininess": 256, 98 | "specular": [ 99 | 0.2, 100 | 0.2, 101 | 0.2, 102 | 1 103 | ] 104 | } 105 | } 106 | }, 107 | "name": "Red" 108 | } 109 | }, 110 | "meshes": { 111 | "Geometry-mesh002": { 112 | "name": "Mesh", 113 | "primitives": [ 114 | { 115 | "attributes": { 116 | "NORMAL": "accessor_25", 117 | "POSITION": "accessor_23" 118 | }, 119 | "indices": "accessor_21", 120 | "material": "Effect-Red", 121 | "mode": 4 122 | } 123 | ] 124 | } 125 | }, 126 | "nodes": { 127 | "Geometry-mesh002Node": { 128 | "children": [], 129 | "matrix": [ 130 | 1, 131 | 0, 132 | 0, 133 | 0, 134 | 0, 135 | 1, 136 | 0, 137 | 0, 138 | 0, 139 | 0, 140 | 1, 141 | 0, 142 | 0, 143 | 0, 144 | 0, 145 | 1 146 | ], 147 | "meshes": [ 148 | "Geometry-mesh002" 149 | ], 150 | "name": "Mesh" 151 | }, 152 | "node_1": { 153 | "children": [ 154 | "Geometry-mesh002Node" 155 | ], 156 | "matrix": [ 157 | 1, 158 | 0, 159 | 0, 160 | 0, 161 | 0, 162 | 0, 163 | -1, 164 | 0, 165 | 0, 166 | 1, 167 | 0, 168 | 0, 169 | 0, 170 | 0, 171 | 0, 172 | 1 173 | ], 174 | "name": "Y_UP_Transform" 175 | } 176 | }, 177 | "scene": "defaultScene", 178 | "scenes": { 179 | "defaultScene": { 180 | "nodes": [ 181 | "node_1" 182 | ] 183 | } 184 | }, 185 | "skins": {} 186 | } -------------------------------------------------------------------------------- /lib/forEachTextureInMaterial.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | 4 | const ForEach = require("./ForEach"); 5 | 6 | const Check = Cesium.Check; 7 | const defined = Cesium.defined; 8 | 9 | module.exports = forEachTextureInMaterial; 10 | 11 | /** 12 | * Calls the provider handler function on each texture used by the material. 13 | * Mimics the behavior of functions in gltf-pipeline ForEach. 14 | * @param {object} material The glTF material. 15 | * @param {forEachTextureInMaterial~handler} handler Function that is called for each texture in the material. 16 | * 17 | * @private 18 | */ 19 | function forEachTextureInMaterial(material, handler) { 20 | Check.typeOf.object("material", material); 21 | Check.defined("handler", handler); 22 | 23 | // Metallic roughness 24 | const pbrMetallicRoughness = material.pbrMetallicRoughness; 25 | if (defined(pbrMetallicRoughness)) { 26 | if (defined(pbrMetallicRoughness.baseColorTexture)) { 27 | const textureInfo = pbrMetallicRoughness.baseColorTexture; 28 | const value = handler(textureInfo.index, textureInfo); 29 | if (defined(value)) { 30 | return value; 31 | } 32 | } 33 | if (defined(pbrMetallicRoughness.metallicRoughnessTexture)) { 34 | const textureInfo = pbrMetallicRoughness.metallicRoughnessTexture; 35 | const value = handler(textureInfo.index, textureInfo); 36 | if (defined(value)) { 37 | return value; 38 | } 39 | } 40 | } 41 | 42 | const { extensions } = material; 43 | if (defined(extensions)) { 44 | // Spec gloss extension 45 | const pbrSpecularGlossiness = 46 | extensions.KHR_materials_pbrSpecularGlossiness; 47 | if (defined(pbrSpecularGlossiness)) { 48 | if (defined(pbrSpecularGlossiness.diffuseTexture)) { 49 | const textureInfo = pbrSpecularGlossiness.diffuseTexture; 50 | const value = handler(textureInfo.index, textureInfo); 51 | if (defined(value)) { 52 | return value; 53 | } 54 | } 55 | if (defined(pbrSpecularGlossiness.specularGlossinessTexture)) { 56 | const textureInfo = pbrSpecularGlossiness.specularGlossinessTexture; 57 | const value = handler(textureInfo.index, textureInfo); 58 | if (defined(value)) { 59 | return value; 60 | } 61 | } 62 | } 63 | 64 | // Specular extension 65 | const specular = extensions.KHR_materials_specular; 66 | if (defined(specular)) { 67 | const { specularTexture, specularColorTexture } = specular; 68 | if (defined(specularTexture)) { 69 | const value = handler(specularTexture.index, specularTexture); 70 | if (defined(value)) { 71 | return value; 72 | } 73 | } 74 | if (defined(specularColorTexture)) { 75 | const value = handler(specularColorTexture.index, specularColorTexture); 76 | if (defined(value)) { 77 | return value; 78 | } 79 | } 80 | } 81 | 82 | // Materials common extension (may be present in models converted from glTF 1.0) 83 | const materialsCommon = extensions.KHR_materials_common; 84 | if (defined(materialsCommon) && defined(materialsCommon.values)) { 85 | const { diffuse, ambient, emission, specular } = materialsCommon.values; 86 | if (defined(diffuse) && defined(diffuse.index)) { 87 | const value = handler(diffuse.index, diffuse); 88 | if (defined(value)) { 89 | return value; 90 | } 91 | } 92 | if (defined(ambient) && defined(ambient.index)) { 93 | const value = handler(ambient.index, ambient); 94 | if (defined(value)) { 95 | return value; 96 | } 97 | } 98 | if (defined(emission) && defined(emission.index)) { 99 | const value = handler(emission.index, emission); 100 | if (defined(value)) { 101 | return value; 102 | } 103 | } 104 | if (defined(specular) && defined(specular.index)) { 105 | const value = handler(specular.index, specular); 106 | if (defined(value)) { 107 | return value; 108 | } 109 | } 110 | } 111 | } 112 | 113 | // KHR_techniques_webgl extension 114 | const value = ForEach.materialValue(material, function (materialValue) { 115 | if (defined(materialValue.index)) { 116 | const value = handler(materialValue.index, materialValue); 117 | if (defined(value)) { 118 | return value; 119 | } 120 | } 121 | }); 122 | if (defined(value)) { 123 | return value; 124 | } 125 | 126 | // Top level textures 127 | if (defined(material.emissiveTexture)) { 128 | const textureInfo = material.emissiveTexture; 129 | const value = handler(textureInfo.index, textureInfo); 130 | if (defined(value)) { 131 | return value; 132 | } 133 | } 134 | 135 | if (defined(material.normalTexture)) { 136 | const textureInfo = material.normalTexture; 137 | const value = handler(textureInfo.index, textureInfo); 138 | if (defined(value)) { 139 | return value; 140 | } 141 | } 142 | 143 | if (defined(material.occlusionTexture)) { 144 | const textureInfo = material.occlusionTexture; 145 | const value = handler(textureInfo.index, textureInfo); 146 | if (defined(value)) { 147 | return value; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Function that is called for each texture in the material. If this function returns a value the for each stops and returns that value. 154 | * @callback forEachTextureInMaterial~handler 155 | * @param {number} The texture index. 156 | * @param {object} The texture info object. 157 | * 158 | * @private 159 | */ 160 | -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-no-fallback/meshopt-no-fallback.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "buffers": [ 3 | { 4 | "uri": "meshopt-no-fallback.bin", 5 | "byteLength": 468 6 | }, 7 | { 8 | "byteLength": 960 9 | } 10 | ], 11 | "asset": { 12 | "version": "2.0", 13 | "generator": "gltfpack 0.16" 14 | }, 15 | "extensionsUsed": [ 16 | "KHR_mesh_quantization", 17 | "EXT_meshopt_compression" 18 | ], 19 | "extensionsRequired": [ 20 | "KHR_mesh_quantization", 21 | "EXT_meshopt_compression" 22 | ], 23 | "bufferViews": [ 24 | { 25 | "buffer": 1, 26 | "byteOffset": 0, 27 | "byteLength": 400, 28 | "byteStride": 8, 29 | "target": 34962, 30 | "extensions": { 31 | "EXT_meshopt_compression": { 32 | "buffer": 0, 33 | "byteOffset": 0, 34 | "byteLength": 265, 35 | "byteStride": 8, 36 | "mode": "ATTRIBUTES", 37 | "count": 50 38 | } 39 | } 40 | }, 41 | { 42 | "buffer": 1, 43 | "byteOffset": 400, 44 | "byteLength": 200, 45 | "byteStride": 4, 46 | "target": 34962, 47 | "extensions": { 48 | "EXT_meshopt_compression": { 49 | "buffer": 0, 50 | "byteOffset": 268, 51 | "byteLength": 117, 52 | "byteStride": 4, 53 | "mode": "ATTRIBUTES", 54 | "filter": "OCTAHEDRAL", 55 | "count": 50 56 | } 57 | } 58 | }, 59 | { 60 | "buffer": 1, 61 | "byteOffset": 600, 62 | "byteLength": 360, 63 | "target": 34963, 64 | "extensions": { 65 | "EXT_meshopt_compression": { 66 | "buffer": 0, 67 | "byteOffset": 388, 68 | "byteLength": 78, 69 | "byteStride": 2, 70 | "mode": "TRIANGLES", 71 | "count": 180 72 | } 73 | } 74 | } 75 | ], 76 | "accessors": [ 77 | { 78 | "bufferView": 0, 79 | "byteOffset": 0, 80 | "componentType": 5123, 81 | "count": 24, 82 | "type": "VEC3", 83 | "min": [ 84 | 0, 85 | 0, 86 | 0 87 | ], 88 | "max": [ 89 | 16383, 90 | 16383, 91 | 16383 92 | ] 93 | }, 94 | { 95 | "bufferView": 1, 96 | "byteOffset": 0, 97 | "componentType": 5120, 98 | "count": 24, 99 | "type": "VEC3", 100 | "normalized": true 101 | }, 102 | { 103 | "bufferView": 2, 104 | "byteOffset": 0, 105 | "componentType": 5123, 106 | "count": 36, 107 | "type": "SCALAR" 108 | }, 109 | { 110 | "bufferView": 0, 111 | "byteOffset": 192, 112 | "componentType": 5123, 113 | "count": 26, 114 | "type": "VEC3", 115 | "min": [ 116 | 1315, 117 | 1315, 118 | 1315 119 | ], 120 | "max": [ 121 | 15068, 122 | 15068, 123 | 15068 124 | ] 125 | }, 126 | { 127 | "bufferView": 1, 128 | "byteOffset": 96, 129 | "componentType": 5120, 130 | "count": 26, 131 | "type": "VEC3", 132 | "normalized": true 133 | }, 134 | { 135 | "bufferView": 2, 136 | "byteOffset": 72, 137 | "componentType": 5123, 138 | "count": 144, 139 | "type": "SCALAR" 140 | } 141 | ], 142 | "materials": [ 143 | { 144 | "name": "Material.002", 145 | "pbrMetallicRoughness": { 146 | "baseColorFactor": [ 147 | 0.800000072, 148 | 0.00122899958, 149 | 0, 150 | 1 151 | ], 152 | "metallicFactor": 0, 153 | "roughnessFactor": 0.5 154 | }, 155 | "doubleSided": true 156 | }, 157 | { 158 | "name": "Material.001", 159 | "pbrMetallicRoughness": { 160 | "baseColorFactor": [ 161 | 0.800000012, 162 | 0.800000012, 163 | 0.800000012, 164 | 1 165 | ], 166 | "metallicFactor": 0, 167 | "roughnessFactor": 0.5 168 | }, 169 | "doubleSided": true 170 | } 171 | ], 172 | "meshes": [ 173 | { 174 | "primitives": [ 175 | { 176 | "attributes": { 177 | "POSITION": 0, 178 | "NORMAL": 1 179 | }, 180 | "mode": 4, 181 | "indices": 2, 182 | "material": 0 183 | } 184 | ] 185 | }, 186 | { 187 | "primitives": [ 188 | { 189 | "attributes": { 190 | "POSITION": 3, 191 | "NORMAL": 4 192 | }, 193 | "mode": 4, 194 | "indices": 5, 195 | "material": 1 196 | } 197 | ] 198 | } 199 | ], 200 | "nodes": [ 201 | { 202 | "mesh": 0, 203 | "translation": [ 204 | -1, 205 | -1, 206 | -1 207 | ], 208 | "scale": [ 209 | 0.000122077763, 210 | 0.000122077763, 211 | 0.000122077763 212 | ] 213 | }, 214 | { 215 | "mesh": 1, 216 | "translation": [ 217 | -1, 218 | -1, 219 | -1 220 | ], 221 | "scale": [ 222 | 0.000122077763, 223 | 0.000122077763, 224 | 0.000122077763 225 | ] 226 | }, 227 | { 228 | "name": "Cube", 229 | "children": [ 230 | 0 231 | ] 232 | }, 233 | { 234 | "name": "Cube.001", 235 | "translation": [ 236 | 0, 237 | 2.87217259, 238 | 0 239 | ], 240 | "children": [ 241 | 1 242 | ] 243 | } 244 | ], 245 | "scenes": [ 246 | { 247 | "name": "Scene", 248 | "nodes": [ 249 | 2, 250 | 3 251 | ] 252 | } 253 | ], 254 | "scene": 0 255 | } 256 | -------------------------------------------------------------------------------- /specs/lib/parseGlbSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { RuntimeError } = require("cesium"); 4 | 5 | const parseGlb = require("../../lib/parseGlb"); 6 | const removePipelineExtras = require("../../lib/removePipelineExtras"); 7 | 8 | describe("parseGlb", () => { 9 | it("throws an error with invalid magic", () => { 10 | const glb = Buffer.alloc(20); 11 | glb.write("NOPE", 0); 12 | 13 | let thrownError; 14 | try { 15 | parseGlb(glb); 16 | } catch (e) { 17 | thrownError = e; 18 | } 19 | expect(thrownError).toEqual( 20 | new RuntimeError("File is not valid binary glTF"), 21 | ); 22 | }); 23 | 24 | it("throws an error if version is not 1 or 2", () => { 25 | const glb = Buffer.alloc(20); 26 | glb.write("glTF", 0); 27 | glb.writeUInt32LE(3, 4); 28 | 29 | let thrownError; 30 | try { 31 | parseGlb(glb); 32 | } catch (e) { 33 | thrownError = e; 34 | } 35 | expect(thrownError).toEqual( 36 | new RuntimeError("Binary glTF version is not 1 or 2"), 37 | ); 38 | }); 39 | 40 | describe("1.0", () => { 41 | it("throws an error if content format is not JSON", () => { 42 | const glb = Buffer.alloc(20); 43 | glb.write("glTF", 0); 44 | glb.writeUInt32LE(1, 4); 45 | glb.writeUInt32LE(20, 8); 46 | glb.writeUInt32LE(0, 12); 47 | glb.writeUInt32LE(1, 16); 48 | 49 | let thrownError; 50 | try { 51 | parseGlb(glb); 52 | } catch (e) { 53 | thrownError = e; 54 | } 55 | expect(thrownError).toEqual( 56 | new RuntimeError("Binary glTF scene format is not JSON"), 57 | ); 58 | }); 59 | 60 | it("loads binary glTF", () => { 61 | const binaryData = Buffer.from([0, 1, 2, 3, 4, 5]); 62 | const gltf = { 63 | bufferViews: { 64 | imageBufferView: { 65 | byteLength: 0, 66 | }, 67 | shaderBufferView: { 68 | byteLength: 0, 69 | }, 70 | }, 71 | buffers: { 72 | binary_glTF: { 73 | byteLength: binaryData.length, 74 | uri: "data:,", 75 | }, 76 | }, 77 | images: { 78 | image: { 79 | extensions: { 80 | KHR_binary_glTF: { 81 | bufferView: "imageBufferView", 82 | mimeType: "image/jpg", 83 | }, 84 | }, 85 | }, 86 | }, 87 | shaders: { 88 | shader: { 89 | extensions: { 90 | KHR_binary_glTF: { 91 | bufferView: "shaderBufferView", 92 | }, 93 | }, 94 | }, 95 | }, 96 | extensionsUsed: ["KHR_binary_glTF"], 97 | }; 98 | let gltfString = JSON.stringify(gltf); 99 | while (gltfString.length % 4 !== 0) { 100 | gltfString += " "; 101 | } 102 | const glb = Buffer.alloc(20 + gltfString.length + binaryData.length); 103 | glb.write("glTF", 0); 104 | glb.writeUInt32LE(1, 4); 105 | glb.writeUInt32LE(20 + gltfString.length + binaryData.length, 8); 106 | glb.writeUInt32LE(gltfString.length, 12); 107 | glb.writeUInt32LE(0, 16); 108 | glb.write(gltfString, 20); 109 | binaryData.copy(glb, 20 + gltfString.length); 110 | 111 | const parsedGltf = parseGlb(glb); 112 | expect(parsedGltf.extensionsUsed).toBeUndefined(); 113 | const buffer = parsedGltf.buffers.binary_glTF; 114 | for (let i = 0; i < binaryData.length; i++) { 115 | expect(buffer.extras._pipeline.source[i]).toEqual(binaryData[i]); 116 | expect(buffer.uri).toBeUndefined(); 117 | } 118 | 119 | const image = parsedGltf.images.image; 120 | expect(image.extensions.KHR_binary_glTF).toBeDefined(); 121 | expect(image.extensions.KHR_binary_glTF.bufferView).toBe( 122 | "imageBufferView", 123 | ); 124 | expect(image.extensions.KHR_binary_glTF.mimeType).toBe("image/jpg"); 125 | const shader = parsedGltf.shaders.shader; 126 | expect(shader.extensions.KHR_binary_glTF).toBeDefined(); 127 | expect(shader.extensions.KHR_binary_glTF.bufferView).toBe( 128 | "shaderBufferView", 129 | ); 130 | }); 131 | }); 132 | 133 | describe("2.0", () => { 134 | it("loads binary glTF", () => { 135 | let i; 136 | const binaryData = Buffer.from([0, 1, 2, 3, 4, 5]); 137 | const gltf = { 138 | asset: { 139 | version: "2.0", 140 | }, 141 | buffers: [ 142 | { 143 | byteLength: binaryData.length, 144 | }, 145 | ], 146 | images: [ 147 | { 148 | bufferView: 0, 149 | mimeType: "image/jpg", 150 | }, 151 | ], 152 | }; 153 | let gltfString = JSON.stringify(gltf); 154 | while (gltfString.length % 4 !== 0) { 155 | gltfString += " "; 156 | } 157 | const glb = Buffer.alloc(28 + gltfString.length + binaryData.length); 158 | glb.write("glTF", 0); 159 | glb.writeUInt32LE(2, 4); 160 | glb.writeUInt32LE(12 + 8 + gltfString.length + 8 + binaryData.length, 8); 161 | glb.writeUInt32LE(gltfString.length, 12); 162 | glb.writeUInt32LE(0x4e4f534a, 16); 163 | glb.write(gltfString, 20); 164 | glb.writeUInt32LE(binaryData.length, 20 + gltfString.length); 165 | glb.writeUInt32LE(0x004e4942, 24 + gltfString.length); 166 | binaryData.copy(glb, 28 + gltfString.length); 167 | 168 | const parsedGltf = parseGlb(glb); 169 | const buffer = parsedGltf.buffers[0]; 170 | for (i = 0; i < binaryData.length; i++) { 171 | expect(buffer.extras._pipeline.source[i]).toEqual(binaryData[i]); 172 | } 173 | removePipelineExtras(parsedGltf); 174 | expect(parsedGltf).toEqual(gltf); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "buffers": [ 3 | { 4 | "uri": "meshopt-fallback.bin", 5 | "byteLength": 468 6 | }, 7 | { 8 | "uri": "meshopt-fallback.fallback.bin", 9 | "byteLength": 960, 10 | "extensions": { 11 | "EXT_meshopt_compression": { 12 | "fallback": true 13 | } 14 | } 15 | } 16 | ], 17 | "asset": { 18 | "version": "2.0", 19 | "generator": "gltfpack 0.16" 20 | }, 21 | "extensionsUsed": [ 22 | "KHR_mesh_quantization", 23 | "EXT_meshopt_compression" 24 | ], 25 | "extensionsRequired": [ 26 | "KHR_mesh_quantization" 27 | ], 28 | "bufferViews": [ 29 | { 30 | "buffer": 1, 31 | "byteOffset": 0, 32 | "byteLength": 400, 33 | "byteStride": 8, 34 | "target": 34962, 35 | "extensions": { 36 | "EXT_meshopt_compression": { 37 | "buffer": 0, 38 | "byteOffset": 0, 39 | "byteLength": 265, 40 | "byteStride": 8, 41 | "mode": "ATTRIBUTES", 42 | "count": 50 43 | } 44 | } 45 | }, 46 | { 47 | "buffer": 1, 48 | "byteOffset": 400, 49 | "byteLength": 200, 50 | "byteStride": 4, 51 | "target": 34962, 52 | "extensions": { 53 | "EXT_meshopt_compression": { 54 | "buffer": 0, 55 | "byteOffset": 268, 56 | "byteLength": 117, 57 | "byteStride": 4, 58 | "mode": "ATTRIBUTES", 59 | "filter": "OCTAHEDRAL", 60 | "count": 50 61 | } 62 | } 63 | }, 64 | { 65 | "buffer": 1, 66 | "byteOffset": 600, 67 | "byteLength": 360, 68 | "target": 34963, 69 | "extensions": { 70 | "EXT_meshopt_compression": { 71 | "buffer": 0, 72 | "byteOffset": 388, 73 | "byteLength": 78, 74 | "byteStride": 2, 75 | "mode": "TRIANGLES", 76 | "count": 180 77 | } 78 | } 79 | } 80 | ], 81 | "accessors": [ 82 | { 83 | "bufferView": 0, 84 | "byteOffset": 0, 85 | "componentType": 5123, 86 | "count": 24, 87 | "type": "VEC3", 88 | "min": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "max": [ 94 | 16383, 95 | 16383, 96 | 16383 97 | ] 98 | }, 99 | { 100 | "bufferView": 1, 101 | "byteOffset": 0, 102 | "componentType": 5120, 103 | "count": 24, 104 | "type": "VEC3", 105 | "normalized": true 106 | }, 107 | { 108 | "bufferView": 2, 109 | "byteOffset": 0, 110 | "componentType": 5123, 111 | "count": 36, 112 | "type": "SCALAR" 113 | }, 114 | { 115 | "bufferView": 0, 116 | "byteOffset": 192, 117 | "componentType": 5123, 118 | "count": 26, 119 | "type": "VEC3", 120 | "min": [ 121 | 1315, 122 | 1315, 123 | 1315 124 | ], 125 | "max": [ 126 | 15068, 127 | 15068, 128 | 15068 129 | ] 130 | }, 131 | { 132 | "bufferView": 1, 133 | "byteOffset": 96, 134 | "componentType": 5120, 135 | "count": 26, 136 | "type": "VEC3", 137 | "normalized": true 138 | }, 139 | { 140 | "bufferView": 2, 141 | "byteOffset": 72, 142 | "componentType": 5123, 143 | "count": 144, 144 | "type": "SCALAR" 145 | } 146 | ], 147 | "materials": [ 148 | { 149 | "name": "Material.002", 150 | "pbrMetallicRoughness": { 151 | "baseColorFactor": [ 152 | 0.800000072, 153 | 0.00122899958, 154 | 0, 155 | 1 156 | ], 157 | "metallicFactor": 0, 158 | "roughnessFactor": 0.5 159 | }, 160 | "doubleSided": true 161 | }, 162 | { 163 | "name": "Material.001", 164 | "pbrMetallicRoughness": { 165 | "baseColorFactor": [ 166 | 0.800000012, 167 | 0.800000012, 168 | 0.800000012, 169 | 1 170 | ], 171 | "metallicFactor": 0, 172 | "roughnessFactor": 0.5 173 | }, 174 | "doubleSided": true 175 | } 176 | ], 177 | "meshes": [ 178 | { 179 | "primitives": [ 180 | { 181 | "attributes": { 182 | "POSITION": 0, 183 | "NORMAL": 1 184 | }, 185 | "mode": 4, 186 | "indices": 2, 187 | "material": 0 188 | } 189 | ] 190 | }, 191 | { 192 | "primitives": [ 193 | { 194 | "attributes": { 195 | "POSITION": 3, 196 | "NORMAL": 4 197 | }, 198 | "mode": 4, 199 | "indices": 5, 200 | "material": 1 201 | } 202 | ] 203 | } 204 | ], 205 | "nodes": [ 206 | { 207 | "mesh": 0, 208 | "translation": [ 209 | -1, 210 | -1, 211 | -1 212 | ], 213 | "scale": [ 214 | 0.000122077763, 215 | 0.000122077763, 216 | 0.000122077763 217 | ] 218 | }, 219 | { 220 | "mesh": 1, 221 | "translation": [ 222 | -1, 223 | -1, 224 | -1 225 | ], 226 | "scale": [ 227 | 0.000122077763, 228 | 0.000122077763, 229 | 0.000122077763 230 | ] 231 | }, 232 | { 233 | "name": "Cube", 234 | "children": [ 235 | 0 236 | ] 237 | }, 238 | { 239 | "name": "Cube.001", 240 | "translation": [ 241 | 0, 242 | 2.87217259, 243 | 0 244 | ], 245 | "children": [ 246 | 1 247 | ] 248 | } 249 | ], 250 | "scenes": [ 251 | { 252 | "name": "Scene", 253 | "nodes": [ 254 | 2, 255 | 3 256 | ] 257 | } 258 | ], 259 | "scene": 0 260 | } 261 | -------------------------------------------------------------------------------- /lib/processGltf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const Promise = require("bluebird"); 4 | const addDefaults = require("./addDefaults"); 5 | const addPipelineExtras = require("./addPipelineExtras"); 6 | const getStatistics = require("./getStatistics"); 7 | const readResources = require("./readResources"); 8 | const removeDefaults = require("./removeDefaults"); 9 | const removePipelineExtras = require("./removePipelineExtras"); 10 | const removeUnusedElements = require("./removeUnusedElements"); 11 | const updateVersion = require("./updateVersion"); 12 | const writeResources = require("./writeResources"); 13 | const compressDracoMeshes = require("./compressDracoMeshes"); 14 | 15 | const clone = Cesium.clone; 16 | const defined = Cesium.defined; 17 | 18 | module.exports = processGltf; 19 | 20 | /** 21 | * Run a glTF through the gltf-pipeline. 22 | * 23 | * @param {object} gltf A javascript object containing a glTF asset. The glTF is modified in place. 24 | * @param {object} [options] An object with the following properties: 25 | * @param {string} [options.resourceDirectory] The path for reading separate resources. 26 | * @param {string} [options.name] The name of the glTF asset, for writing separate resources. 27 | * @param {boolean} [options.separate = false] Write separate buffers, shaders, and textures instead of embedding them in the glTF. 28 | * @param {boolean} [options.separateTextures = false] Write out separate textures only. 29 | * @param {boolean} [options.stats = false] Print statistics to console for input and output glTF files. 30 | * @param {object} [options.dracoOptions] Options to pass to the compressDracoMeshes stage. If undefined, stage is not run. 31 | * @param {Stage[]} [options.customStages] Custom stages to run on the glTF asset. 32 | * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. 33 | * @param {string[]} [options.baseColorTextureNames] Names of uniforms that indicate base color textures. 34 | * @param {string[]} [options.baseColorFactorNames] Names of uniforms that indicate base color factors. 35 | * 36 | * @returns {Promise} A promise that resolves to an object containing the glTF and a dictionary containing separate resources. 37 | */ 38 | function processGltf(gltf, options) { 39 | const defaults = processGltf.defaults; 40 | options = defined(options) ? clone(options) : {}; 41 | options.separateBuffers = options.separate ?? defaults.separate; 42 | options.separateShaders = options.separate ?? defaults.separate; 43 | options.separateTextures = 44 | (options.separateTextures ?? defaults.separateTextures) || options.separate; 45 | options.stats = options.stats ?? defaults.stats; 46 | options.logger = options.logger ?? getDefaultLogger(); 47 | options.separateResources = {}; 48 | options.customStages = options.customStages ?? []; 49 | 50 | const preStages = [ 51 | addPipelineExtras, 52 | readResources, 53 | updateVersion, 54 | addDefaults, 55 | ]; 56 | 57 | const postStages = [writeResources, removePipelineExtras, removeDefaults]; 58 | 59 | const pipelineStages = getStages(options); 60 | const stages = preStages.concat( 61 | options.customStages, 62 | pipelineStages, 63 | postStages, 64 | ); 65 | 66 | return Promise.each(stages, function (stage) { 67 | return stage(gltf, options); 68 | }).then(function () { 69 | printStats(gltf, options, true); 70 | return { 71 | gltf: gltf, 72 | separateResources: options.separateResources, 73 | }; 74 | }); 75 | } 76 | 77 | function printStats(gltf, options, processed) { 78 | if (options.stats) { 79 | options.logger(processed ? "Statistics after:" : "Statistics before:"); 80 | options.logger(getStatistics(gltf).toString()); 81 | } 82 | } 83 | 84 | function getStages(options) { 85 | const stages = []; 86 | if (defined(options.dracoOptions)) { 87 | stages.push(compressDracoMeshes); 88 | } 89 | if (!options.keepUnusedElements) { 90 | stages.push(function (gltf, options) { 91 | removeUnusedElements(gltf); 92 | }); 93 | } 94 | return stages; 95 | } 96 | 97 | function getDefaultLogger() { 98 | return function (message) { 99 | console.log(message); 100 | }; 101 | } 102 | 103 | /** 104 | * Default values that will be used when calling processGltf(options) unless specified in the options object. 105 | */ 106 | processGltf.defaults = { 107 | /** 108 | * Gets or sets whether to write out separate buffers, shaders, and textures instead of embedding them in the glTF 109 | * @type Boolean 110 | * @default false 111 | */ 112 | separate: false, 113 | /** 114 | * Gets or sets whether to write out separate textures only. 115 | * @type Boolean 116 | * @default false 117 | */ 118 | separateTextures: false, 119 | /** 120 | * Gets or sets whether to print statistics to console for input and output glTF files. 121 | * @type Boolean 122 | * @default false 123 | */ 124 | stats: false, 125 | /** 126 | * Keep unused 'node', 'mesh' and 'material' elements. 127 | * @type Boolean 128 | * @default false 129 | */ 130 | keepUnusedElements: false, 131 | /** 132 | * When false, materials with KHR_techniques_webgl, KHR_blend, or KHR_materials_common will be converted to PBR. 133 | * @type Boolean 134 | * @default false 135 | */ 136 | keepLegacyExtensions: false, 137 | /** 138 | * Gets or sets whether to compress the meshes using Draco. Adds the KHR_draco_mesh_compression extension. 139 | * @type Boolean 140 | * @default false 141 | */ 142 | compressDracoMeshes: false, 143 | }; 144 | 145 | /** 146 | * A callback function that logs messages. 147 | * @callback Logger 148 | * 149 | * @param {string} message The message to log. 150 | */ 151 | 152 | /** 153 | * A stage that processes a glTF asset. 154 | * @callback Stage 155 | * 156 | * @param {object} gltf The glTF asset. 157 | * @returns {Promise|Object} The glTF asset or a promise that resolves to the glTF asset. 158 | */ 159 | -------------------------------------------------------------------------------- /specs/lib/updateAccessorComponentTypeSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const readResources = require("../../lib/readResources"); 4 | const updateAccessorComponentTypes = require("../../lib/updateAccessorComponentTypes"); 5 | 6 | const WebGLConstants = Cesium.WebGLConstants; 7 | 8 | let buffer; 9 | 10 | describe("updateAccessorComponentTypes", () => { 11 | beforeAll(() => { 12 | // Note: TypedArray constructors initialize all elements to zero 13 | const byteBuffer = Buffer.from(new Int8Array(96).buffer); 14 | const floatBuffer = Buffer.from(new Float32Array(96).buffer); 15 | const unsignedShortBuffer = Buffer.from(new Uint16Array(96).buffer); 16 | const source = Buffer.concat([ 17 | byteBuffer, 18 | floatBuffer, 19 | unsignedShortBuffer, 20 | ]); 21 | const byteLength = source.length; 22 | const dataUri = `data:application/octet-stream;base64,${source.toString( 23 | "base64", 24 | )}`; 25 | buffer = { 26 | uri: dataUri, 27 | byteLength: byteLength, 28 | }; 29 | }); 30 | 31 | it("converts joints accessor types", async () => { 32 | const gltf = { 33 | meshes: [ 34 | { 35 | primitives: [ 36 | { 37 | attributes: { 38 | JOINTS_0: 0, 39 | }, 40 | }, 41 | { 42 | attributes: { 43 | JOINTS_0: 1, 44 | }, 45 | }, 46 | { 47 | attributes: { 48 | JOINTS_0: 2, 49 | }, 50 | }, 51 | ], 52 | }, 53 | ], 54 | accessors: [ 55 | { 56 | bufferView: 0, 57 | componentType: WebGLConstants.BYTE, 58 | count: 24, 59 | type: "VEC4", 60 | }, 61 | { 62 | bufferView: 1, 63 | componentType: WebGLConstants.FLOAT, 64 | byteOffset: 12, 65 | count: 24, 66 | type: "VEC4", 67 | }, 68 | { 69 | bufferView: 2, 70 | componentType: WebGLConstants.UNSIGNED_SHORT, 71 | count: 24, 72 | type: "VEC4", 73 | }, 74 | ], 75 | bufferViews: [ 76 | { 77 | buffer: 0, 78 | byteOffset: 0, 79 | byteLength: 96, 80 | }, 81 | { 82 | buffer: 0, 83 | byteOffset: 96, 84 | byteLength: 384, 85 | }, 86 | { 87 | buffer: 0, 88 | byteOffset: 480, 89 | byteLength: 192, 90 | }, 91 | ], 92 | buffers: [buffer], 93 | }; 94 | 95 | await readResources(gltf); 96 | updateAccessorComponentTypes(gltf); 97 | 98 | expect(gltf.accessors.length).toBe(3); 99 | expect(gltf.bufferViews.length).toBe(5); 100 | expect(gltf.buffers.length).toBe(3); 101 | 102 | expect(gltf.accessors[0].componentType).toBe(WebGLConstants.UNSIGNED_BYTE); 103 | expect(gltf.accessors[0].bufferView).toBe(3); 104 | expect(gltf.bufferViews[3].buffer).toBe(1); 105 | expect(gltf.bufferViews[3].byteLength).toBe(96); 106 | 107 | expect(gltf.accessors[1].componentType).toBe(WebGLConstants.UNSIGNED_SHORT); 108 | expect(gltf.accessors[1].bufferView).toBe(4); 109 | expect(gltf.accessors[1].byteOffset).toBe(0); 110 | expect(gltf.bufferViews[4].buffer).toBe(2); 111 | expect(gltf.bufferViews[4].byteLength).toBe(192); 112 | 113 | expect(gltf.accessors[2].componentType).toBe(WebGLConstants.UNSIGNED_SHORT); 114 | expect(gltf.accessors[2].bufferView).toBe(2); 115 | }); 116 | 117 | it("converts weights accessor types", async () => { 118 | const gltf = { 119 | meshes: [ 120 | { 121 | primitives: [ 122 | { 123 | attributes: { 124 | WEIGHTS_0: 0, 125 | }, 126 | }, 127 | { 128 | attributes: { 129 | WEIGHTS_0: 1, 130 | }, 131 | }, 132 | { 133 | attributes: { 134 | WEIGHTS_0: 2, 135 | }, 136 | }, 137 | ], 138 | }, 139 | ], 140 | accessors: [ 141 | { 142 | bufferView: 0, 143 | componentType: WebGLConstants.FLOAT, 144 | count: 24, 145 | type: "VEC4", 146 | }, 147 | { 148 | bufferView: 1, 149 | componentType: WebGLConstants.BYTE, 150 | byteOffset: 12, 151 | count: 24, 152 | type: "VEC4", 153 | }, 154 | { 155 | bufferView: 2, 156 | componentType: WebGLConstants.SHORT, 157 | count: 24, 158 | type: "VEC4", 159 | }, 160 | ], 161 | bufferViews: [ 162 | { 163 | buffer: 0, 164 | byteLength: 12, 165 | }, 166 | { 167 | buffer: 0, 168 | byteOffset: 12, 169 | byteLength: 12, 170 | }, 171 | { 172 | buffer: 0, 173 | byteLength: 12, 174 | }, 175 | ], 176 | buffers: [buffer], 177 | }; 178 | 179 | await readResources(gltf); 180 | updateAccessorComponentTypes(gltf); 181 | 182 | expect(gltf.accessors.length).toBe(3); 183 | expect(gltf.bufferViews.length).toBe(5); 184 | expect(gltf.buffers.length).toBe(3); 185 | 186 | expect(gltf.accessors[0].componentType).toBe(WebGLConstants.FLOAT); 187 | expect(gltf.accessors[0].bufferView).toBe(0); 188 | 189 | expect(gltf.accessors[1].componentType).toBe(WebGLConstants.UNSIGNED_BYTE); 190 | expect(gltf.accessors[1].bufferView).toBe(3); 191 | expect(gltf.accessors[1].byteOffset).toBe(0); 192 | expect(gltf.bufferViews[3].buffer).toBe(1); 193 | expect(gltf.bufferViews[3].byteLength).toBe(96); 194 | 195 | expect(gltf.accessors[2].componentType).toBe(WebGLConstants.UNSIGNED_SHORT); 196 | expect(gltf.accessors[2].bufferView).toBe(4); 197 | expect(gltf.bufferViews[4].buffer).toBe(2); 198 | expect(gltf.bufferViews[4].byteLength).toBe(192); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /specs/data/2.0/box-techniques-separate/box-techniques-separate.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 2, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 36, 8 | "type": "SCALAR", 9 | "name": "accessor_indices", 10 | "min": [ 11 | 0 12 | ], 13 | "max": [ 14 | 23 15 | ] 16 | }, 17 | { 18 | "bufferView": 0, 19 | "byteOffset": 0, 20 | "componentType": 5126, 21 | "count": 24, 22 | "max": [ 23 | 0.5, 24 | 0.5, 25 | 0.5 26 | ], 27 | "min": [ 28 | -0.5, 29 | -0.5, 30 | -0.5 31 | ], 32 | "type": "VEC3", 33 | "name": "accessor_positions" 34 | }, 35 | { 36 | "bufferView": 0, 37 | "byteOffset": 288, 38 | "componentType": 5126, 39 | "count": 24, 40 | "max": [ 41 | 1, 42 | 1, 43 | 1 44 | ], 45 | "min": [ 46 | -1, 47 | -1, 48 | -1 49 | ], 50 | "type": "VEC3", 51 | "name": "accessor_normals" 52 | }, 53 | { 54 | "bufferView": 1, 55 | "byteOffset": 0, 56 | "componentType": 5126, 57 | "count": 24, 58 | "max": [ 59 | 6, 60 | 1 61 | ], 62 | "min": [ 63 | 0, 64 | 0 65 | ], 66 | "type": "VEC2", 67 | "name": "accessor_texcoord" 68 | } 69 | ], 70 | "asset": { 71 | "generator": "collada2gltf@ceec062e3d5793f2f249f53cbd843aee382ad40b", 72 | "version": "2.0" 73 | }, 74 | "bufferViews": [ 75 | { 76 | "buffer": 0, 77 | "byteLength": 576, 78 | "byteOffset": 0, 79 | "target": 34962, 80 | "name": "bufferView_0", 81 | "byteStride": 12 82 | }, 83 | { 84 | "buffer": 0, 85 | "byteLength": 192, 86 | "byteOffset": 576, 87 | "target": 34962, 88 | "name": "bufferView_0", 89 | "byteStride": 8 90 | }, 91 | { 92 | "buffer": 0, 93 | "byteLength": 72, 94 | "byteOffset": 768, 95 | "target": 34963, 96 | "name": "bufferView_1" 97 | } 98 | ], 99 | "buffers": [ 100 | { 101 | "name": "CesiumTexturedBoxTest", 102 | "byteLength": 840, 103 | "uri": "CesiumTexturedBoxTest.bin" 104 | } 105 | ], 106 | "images": [ 107 | { 108 | "name": "Image0001", 109 | "uri": "Image0001.png" 110 | } 111 | ], 112 | "materials": [ 113 | { 114 | "name": "Texture", 115 | "extensions": { 116 | "KHR_techniques_webgl": { 117 | "technique": 0, 118 | "values": { 119 | "u_diffuse": { 120 | "index": 0, 121 | "texCoord": 0 122 | }, 123 | "u_shininess": 256, 124 | "u_specular": [ 125 | 0.2, 126 | 0.2, 127 | 0.2, 128 | 1 129 | ] 130 | } 131 | } 132 | }, 133 | "emissiveFactor": [ 134 | 0, 135 | 0, 136 | 0 137 | ], 138 | "alphaMode": "OPAQUE", 139 | "doubleSided": false 140 | } 141 | ], 142 | "meshes": [ 143 | { 144 | "name": "Mesh", 145 | "primitives": [ 146 | { 147 | "attributes": { 148 | "NORMAL": 2, 149 | "POSITION": 1, 150 | "TEXCOORD_0": 3 151 | }, 152 | "indices": 0, 153 | "material": 0, 154 | "mode": 4 155 | } 156 | ] 157 | } 158 | ], 159 | "nodes": [ 160 | { 161 | "name": "rootNode", 162 | "mesh": 0 163 | } 164 | ], 165 | "samplers": [ 166 | { 167 | "magFilter": 9729, 168 | "minFilter": 9987, 169 | "wrapS": 10497, 170 | "wrapT": 10497, 171 | "name": "sampler_0" 172 | } 173 | ], 174 | "scene": 0, 175 | "scenes": [ 176 | { 177 | "nodes": [ 178 | 0 179 | ], 180 | "name": "defaultScene" 181 | } 182 | ], 183 | "textures": [ 184 | { 185 | "sampler": 0, 186 | "source": 0, 187 | "name": "texture_Image0001" 188 | } 189 | ], 190 | "extensionsUsed": [ 191 | "KHR_techniques_webgl" 192 | ], 193 | "extensionsRequired": [ 194 | "KHR_techniques_webgl" 195 | ], 196 | "extensions": { 197 | "KHR_techniques_webgl": { 198 | "programs": [ 199 | { 200 | "name": "program_0", 201 | "fragmentShader": 0, 202 | "vertexShader": 1 203 | } 204 | ], 205 | "shaders": [ 206 | { 207 | "type": 35632, 208 | "name": "CesiumTexturedBoxTest0FS", 209 | "uri": "CesiumTexturedBoxTest0FS.glsl" 210 | }, 211 | { 212 | "type": 35633, 213 | "name": "CesiumTexturedBoxTest0VS", 214 | "uri": "CesiumTexturedBoxTest0VS.glsl" 215 | } 216 | ], 217 | "techniques": [ 218 | { 219 | "name": "technique0", 220 | "program": 0, 221 | "attributes": { 222 | "a_normal": { 223 | "semantic": "NORMAL" 224 | }, 225 | "a_position": { 226 | "semantic": "POSITION" 227 | }, 228 | "a_texcoord0": { 229 | "semantic": "TEXCOORD_0" 230 | } 231 | }, 232 | "uniforms": { 233 | "u_diffuse": { 234 | "type": 35678 235 | }, 236 | "u_modelViewMatrix": { 237 | "type": 35676, 238 | "semantic": "MODELVIEW" 239 | }, 240 | "u_normalMatrix": { 241 | "type": 35675, 242 | "semantic": "MODELVIEWINVERSETRANSPOSE" 243 | }, 244 | "u_projectionMatrix": { 245 | "type": 35676, 246 | "semantic": "PROJECTION" 247 | }, 248 | "u_shininess": { 249 | "type": 5126 250 | }, 251 | "u_specular": { 252 | "type": 35666 253 | } 254 | } 255 | } 256 | ] 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /specs/data/2.0/box-shared-image-references-separate/box-shared-image-references-separate.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 2, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 36, 8 | "type": "SCALAR", 9 | "name": "accessor_indices" 10 | }, 11 | { 12 | "bufferView": 0, 13 | "byteOffset": 0, 14 | "componentType": 5126, 15 | "count": 24, 16 | "max": [ 17 | 0.5, 18 | 0.5, 19 | 0.5 20 | ], 21 | "min": [ 22 | -0.5, 23 | -0.5, 24 | -0.5 25 | ], 26 | "type": "VEC3", 27 | "name": "accessor_positions" 28 | }, 29 | { 30 | "bufferView": 0, 31 | "byteOffset": 288, 32 | "componentType": 5126, 33 | "count": 24, 34 | "max": [ 35 | 1, 36 | 1, 37 | 1 38 | ], 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "type": "VEC3", 45 | "name": "accessor_normals" 46 | }, 47 | { 48 | "bufferView": 1, 49 | "byteOffset": 0, 50 | "componentType": 5126, 51 | "count": 24, 52 | "max": [ 53 | 6, 54 | 1 55 | ], 56 | "min": [ 57 | 0, 58 | 0 59 | ], 60 | "type": "VEC2", 61 | "name": "accessor_texcoord" 62 | } 63 | ], 64 | "asset": { 65 | "generator": "collada2gltf@ceec062e3d5793f2f249f53cbd843aee382ad40b", 66 | "version": "2.0" 67 | }, 68 | "bufferViews": [ 69 | { 70 | "buffer": 0, 71 | "byteLength": 576, 72 | "byteOffset": 0, 73 | "target": 34962, 74 | "name": "bufferView_0", 75 | "byteStride": 12 76 | }, 77 | { 78 | "buffer": 0, 79 | "byteLength": 192, 80 | "byteOffset": 576, 81 | "target": 34962, 82 | "name": "bufferView_0", 83 | "byteStride": 8 84 | }, 85 | { 86 | "buffer": 0, 87 | "byteLength": 72, 88 | "byteOffset": 768, 89 | "target": 34963, 90 | "name": "bufferView_1" 91 | } 92 | ], 93 | "buffers": [ 94 | { 95 | "name": "binary_glTF", 96 | "byteLength": 840, 97 | "uri": "box-shared-image-references-separate.bin" 98 | } 99 | ], 100 | "images": [ 101 | { 102 | "mimeType": "image/png", 103 | "uri": "Image.png" 104 | }, 105 | { 106 | "mimeType": "image/png", 107 | "uri": "Image.png" 108 | } 109 | ], 110 | "materials": [ 111 | { 112 | "name": "Texture", 113 | "extensions": { 114 | "KHR_techniques_webgl": { 115 | "technique": 0, 116 | "values": { 117 | "u_diffuse": { 118 | "index": 0, 119 | "texCoord": 0 120 | }, 121 | "u_shininess": 256, 122 | "u_specular": [ 123 | 0.2, 124 | 0.2, 125 | 0.2, 126 | 1 127 | ] 128 | } 129 | } 130 | }, 131 | "emissiveFactor": [ 132 | 0, 133 | 0, 134 | 0 135 | ], 136 | "alphaMode": "OPAQUE", 137 | "doubleSided": false 138 | } 139 | ], 140 | "meshes": [ 141 | { 142 | "name": "Mesh", 143 | "primitives": [ 144 | { 145 | "attributes": { 146 | "NORMAL": 2, 147 | "POSITION": 1, 148 | "TEXCOORD_0": 3 149 | }, 150 | "indices": 0, 151 | "material": 0, 152 | "mode": 4 153 | } 154 | ] 155 | } 156 | ], 157 | "nodes": [ 158 | { 159 | "name": "rootNode", 160 | "mesh": 0 161 | } 162 | ], 163 | "samplers": [ 164 | { 165 | "magFilter": 9729, 166 | "minFilter": 9987, 167 | "wrapS": 10497, 168 | "wrapT": 10497, 169 | "name": "sampler_0" 170 | } 171 | ], 172 | "scene": 0, 173 | "scenes": [ 174 | { 175 | "nodes": [ 176 | 0 177 | ], 178 | "name": "defaultScene" 179 | } 180 | ], 181 | "textures": [ 182 | { 183 | "sampler": 0, 184 | "source": 0, 185 | "name": "texture_Image0001" 186 | }, 187 | { 188 | "sampler": 0, 189 | "source": 1, 190 | "name": "texture_Image0002" 191 | } 192 | ], 193 | "extensionsRequired": [ 194 | "KHR_techniques_webgl" 195 | ], 196 | "extensions": { 197 | "KHR_techniques_webgl": { 198 | "programs": [ 199 | { 200 | "name": "program_0", 201 | "fragmentShader": 0, 202 | "vertexShader": 1 203 | } 204 | ], 205 | "shaders": [ 206 | { 207 | "type": 35632, 208 | "name": "CesiumTexturedBoxTest0FS", 209 | "uri": "CesiumTexturedBoxTest0FS.glsl" 210 | }, 211 | { 212 | "type": 35633, 213 | "name": "CesiumTexturedBoxTest0VS", 214 | "uri": "CesiumTexturedBoxTest0VS.glsl" 215 | } 216 | ], 217 | "techniques": [ 218 | { 219 | "name": "technique0", 220 | "program": 0, 221 | "attributes": { 222 | "a_normal": { 223 | "semantic": "NORMAL" 224 | }, 225 | "a_position": { 226 | "semantic": "POSITION" 227 | }, 228 | "a_texcoord0": { 229 | "semantic": "TEXCOORD_0" 230 | } 231 | }, 232 | "uniforms": { 233 | "u_diffuse": { 234 | "type": 35678 235 | }, 236 | "u_modelViewMatrix": { 237 | "type": 35676, 238 | "semantic": "MODELVIEW" 239 | }, 240 | "u_normalMatrix": { 241 | "type": 35675, 242 | "semantic": "MODELVIEWINVERSETRANSPOSE" 243 | }, 244 | "u_projectionMatrix": { 245 | "type": 35676, 246 | "semantic": "PROJECTION" 247 | }, 248 | "u_shininess": { 249 | "type": 5126 250 | }, 251 | "u_specular": { 252 | "type": 35666 253 | } 254 | } 255 | } 256 | ] 257 | } 258 | }, 259 | "extensionsUsed": [ 260 | "KHR_techniques_webgl" 261 | ] 262 | } 263 | -------------------------------------------------------------------------------- /lib/mergeBuffers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const FS_WRITE_MAX_LENGTH = 2147479552; // See https://github.com/nodejs/node/issues/35605 3 | const BUFFER_MAX_LENGTH = require("buffer").constants.MAX_LENGTH; 4 | const BUFFER_MAX_BYTE_LENGTH = Math.min(FS_WRITE_MAX_LENGTH, BUFFER_MAX_LENGTH); 5 | const Cesium = require("cesium"); 6 | const ForEach = require("./ForEach"); 7 | 8 | const defined = Cesium.defined; 9 | 10 | module.exports = mergeBuffers; 11 | 12 | /** 13 | * Merge all buffers. Buffers with the same extras._pipeline.mergedBufferName will be merged together. 14 | * 15 | * @param {object} gltf A javascript object containing a glTF asset. 16 | * @param {string} [defaultName] The default name of the buffer data files. 17 | * @param {boolean} [force=false] Whether to force merging all buffers. 18 | * @returns {object} The glTF asset with its buffers merged. 19 | * 20 | * @private 21 | */ 22 | function mergeBuffers(gltf, defaultName, force) { 23 | let baseBufferName = defaultName; 24 | if (!defined(baseBufferName)) { 25 | ForEach.buffer(gltf, function (buffer) { 26 | baseBufferName = baseBufferName ?? buffer.name; 27 | }); 28 | baseBufferName = baseBufferName ?? "buffer"; 29 | } 30 | 31 | let buffersByteLength = 0; 32 | const emptyBuffers = []; 33 | const emptyBufferIds = []; 34 | 35 | ForEach.buffer(gltf, function (buffer, bufferId) { 36 | const source = buffer.extras._pipeline.source; 37 | if (defined(source)) { 38 | buffersByteLength += source.length; 39 | } else { 40 | emptyBuffers.push(buffer); 41 | emptyBufferIds.push(bufferId); 42 | } 43 | 44 | const extensions = buffer.extensions ?? {}; 45 | const meshoptObject = extensions.EXT_meshopt_compression; 46 | if (defined(meshoptObject) && meshoptObject.fallback) { 47 | // Prevent empty meshopt buffer from being merged into main buffer 48 | buffer.extras._pipeline.mergedBufferName = `meshopt-fallback-${bufferId}`; 49 | } 50 | }); 51 | 52 | // Don't merge buffers if the merged buffer will exceed the Node limit. 53 | const splitBuffers = 54 | buffersByteLength > mergeBuffers._getBufferMaxByteLength(); 55 | 56 | const buffersToMerge = {}; 57 | const mergedNameCount = {}; 58 | 59 | forEachBufferViewLikeObject(gltf, function (bufferView) { 60 | const buffer = gltf.buffers[bufferView.buffer]; 61 | const source = buffer.extras._pipeline.source; 62 | if (!defined(source)) { 63 | return; 64 | } 65 | 66 | let mergedName = buffer.extras._pipeline.mergedBufferName; 67 | mergedName = defined(mergedName) 68 | ? `${baseBufferName}-${mergedName}` 69 | : baseBufferName; 70 | 71 | if (splitBuffers) { 72 | if (!defined(mergedNameCount[mergedName])) { 73 | mergedNameCount[mergedName] = 0; 74 | } 75 | mergedName += `-${mergedNameCount[mergedName]++}`; 76 | } 77 | 78 | if (force) { 79 | mergedName = baseBufferName; 80 | } 81 | 82 | if (!defined(buffersToMerge[mergedName])) { 83 | buffersToMerge[mergedName] = { 84 | buffers: [], 85 | byteLength: 0, 86 | index: Object.keys(buffersToMerge).length, 87 | }; 88 | } 89 | const buffers = buffersToMerge[mergedName].buffers; 90 | let byteLength = buffersToMerge[mergedName].byteLength; 91 | const index = buffersToMerge[mergedName].index; 92 | 93 | const sourceBufferViewData = Buffer.from( 94 | source.slice( 95 | bufferView.byteOffset, 96 | bufferView.byteOffset + bufferView.byteLength, 97 | ), 98 | ); 99 | const bufferViewPadding = allocateBufferPadding(byteLength); 100 | if (defined(bufferViewPadding)) { 101 | buffers.push(bufferViewPadding); 102 | byteLength += bufferViewPadding.byteLength; 103 | } 104 | 105 | bufferView.byteOffset = byteLength; 106 | bufferView.buffer = index; 107 | 108 | buffers.push(sourceBufferViewData); 109 | byteLength += sourceBufferViewData.byteLength; 110 | 111 | buffersToMerge[mergedName].byteLength = byteLength; 112 | }); 113 | 114 | const buffersLength = Object.keys(buffersToMerge).length; 115 | gltf.buffers = new Array(buffersLength); 116 | 117 | for (const mergedName in buffersToMerge) { 118 | if (Object.prototype.hasOwnProperty.call(buffersToMerge, mergedName)) { 119 | const buffers = buffersToMerge[mergedName].buffers; 120 | const byteLength = buffersToMerge[mergedName].byteLength; 121 | const index = buffersToMerge[mergedName].index; 122 | const bufferPadding = allocateBufferPadding(byteLength); 123 | if (defined(bufferPadding)) { 124 | buffers.push(bufferPadding); 125 | } 126 | const mergedSource = 127 | buffers.length > 1 ? Buffer.concat(buffers) : buffers[0]; 128 | gltf.buffers[index] = { 129 | name: mergedName, 130 | byteLength: mergedSource.byteLength, 131 | extras: { 132 | _pipeline: { 133 | source: mergedSource, 134 | }, 135 | }, 136 | }; 137 | } 138 | } 139 | 140 | const emptyBuffersLength = emptyBuffers.length; 141 | for (let i = 0; i < emptyBuffersLength; ++i) { 142 | const emptyBuffer = emptyBuffers[i]; 143 | const emptyBufferId = emptyBufferIds[i]; 144 | const newBufferId = gltf.buffers.length; 145 | forEachBufferViewLikeObject(gltf, function (bufferView) { 146 | if (bufferView.buffer === emptyBufferId) { 147 | bufferView.buffer = newBufferId; 148 | } 149 | }); 150 | gltf.buffers.push(emptyBuffer); 151 | } 152 | 153 | return gltf; 154 | } 155 | 156 | function forEachBufferViewLikeObject(gltf, callback) { 157 | ForEach.bufferView(gltf, function (bufferView) { 158 | callback(bufferView); 159 | 160 | const extensions = bufferView.extensions ?? {}; 161 | const meshoptObject = extensions.EXT_meshopt_compression; 162 | if (defined(meshoptObject)) { 163 | // The EXT_meshopt_compression object has buffer, byteOffset, and byteLength properties like a bufferView 164 | callback(meshoptObject); 165 | } 166 | }); 167 | } 168 | 169 | function allocateBufferPadding(byteLength) { 170 | const boundary = 8; 171 | const remainder = byteLength % boundary; 172 | const padding = remainder === 0 ? 0 : boundary - remainder; 173 | if (padding > 0) { 174 | return Buffer.alloc(padding); 175 | } 176 | return undefined; 177 | } 178 | 179 | // Exposed for testing 180 | mergeBuffers._getBufferMaxByteLength = function () { 181 | return BUFFER_MAX_BYTE_LENGTH; 182 | }; 183 | -------------------------------------------------------------------------------- /specs/lib/gltfToGlbSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fsExtra = require("fs-extra"); 3 | const path = require("path"); 4 | 5 | const gltfToGlb = require("../../lib/gltfToGlb"); 6 | const parseGlb = require("../../lib/parseGlb"); 7 | 8 | const gltfPath = 9 | "specs/data/2.0/box-textured-embedded/box-textured-embedded.gltf"; 10 | const gltfMeshoptFallbackPath = 11 | "specs/data/2.0/extensions/EXT_meshopt_compression/meshopt-fallback/meshopt-fallback.gltf"; 12 | 13 | describe("gltfToGlb", () => { 14 | it("gltfToGlb", async () => { 15 | spyOn(console, "log"); 16 | const gltf = fsExtra.readJsonSync(gltfPath); 17 | const options = { 18 | separateTextures: true, 19 | stats: true, 20 | }; 21 | const results = await gltfToGlb(gltf, options); 22 | const glb = results.glb; 23 | const separateResources = results.separateResources; 24 | expect(Buffer.isBuffer(glb)).toBe(true); 25 | expect(Object.keys(separateResources).length).toBe(1); 26 | expect(console.log).toHaveBeenCalled(); 27 | 28 | // Header + JSON header + JSON content + binary header + binary content 29 | const glbLength = glb.readUInt32LE(8); 30 | const jsonChunkLength = glb.readUInt32LE(12); 31 | const binaryChunkLength = glb.readUInt32LE(12 + 8 + jsonChunkLength); 32 | const expectedLength = 12 + 8 + jsonChunkLength + 8 + binaryChunkLength; 33 | expect(glbLength).toBe(expectedLength); 34 | expect(glb.length).toBe(expectedLength); 35 | }); 36 | 37 | it("gltfToGlb with separate resources", async () => { 38 | spyOn(console, "log"); 39 | const gltf = fsExtra.readJsonSync(gltfPath); 40 | const options = { 41 | separate: true, 42 | stats: true, 43 | }; 44 | const results = await gltfToGlb(gltf, options); 45 | const glb = results.glb; 46 | const separateResources = results.separateResources; 47 | expect(Buffer.isBuffer(glb)).toBe(true); 48 | expect(Object.keys(separateResources).length).toBe(2); 49 | expect(console.log).toHaveBeenCalled(); 50 | 51 | // Header + JSON header + JSON content. No binary header or content. 52 | const glbLength = glb.readUInt32LE(8); 53 | const jsonChunkLength = glb.readUInt32LE(12); 54 | const expectedLength = 12 + 8 + jsonChunkLength; 55 | expect(glbLength).toBe(expectedLength); 56 | expect(glb.length).toBe(expectedLength); 57 | }); 58 | 59 | it("gltfToGlb processes glTF with EXT_meshopt_compression extension.", async () => { 60 | const gltf = fsExtra.readJsonSync(gltfMeshoptFallbackPath); 61 | const options = { 62 | resourceDirectory: path.dirname(gltfMeshoptFallbackPath), 63 | }; 64 | const results = await gltfToGlb(gltf, options); 65 | expect(results.glb).toBeDefined(); 66 | 67 | const processedGltf = parseGlb(results.glb); 68 | 69 | expect(processedGltf).toBeDefined(); 70 | expect(processedGltf.buffers.length).toBe(1); 71 | 72 | const buffer0 = processedGltf.buffers[0]; 73 | 74 | const bufferView0 = processedGltf.bufferViews[0]; 75 | const bufferView1 = processedGltf.bufferViews[1]; 76 | const bufferView2 = processedGltf.bufferViews[2]; 77 | 78 | const meshoptObject0 = bufferView0.extensions.EXT_meshopt_compression; 79 | const meshoptObject1 = bufferView1.extensions.EXT_meshopt_compression; 80 | const meshoptObject2 = bufferView2.extensions.EXT_meshopt_compression; 81 | 82 | expect(buffer0.byteLength).toBe(1432); 83 | expect(buffer0.uri).not.toBeDefined(); 84 | 85 | expect(bufferView0.buffer).toBe(0); 86 | expect(bufferView0.byteOffset).toBe(0); 87 | expect(bufferView0.byteLength).toBe(400); 88 | expect(meshoptObject0.buffer).toBe(0); 89 | expect(meshoptObject0.byteOffset).toBe(400); 90 | expect(meshoptObject0.byteLength).toBe(265); 91 | 92 | expect(bufferView1.buffer).toBe(0); 93 | expect(bufferView1.byteOffset).toBe(672); 94 | expect(bufferView1.byteLength).toBe(200); 95 | expect(meshoptObject1.buffer).toBe(0); 96 | expect(meshoptObject1.byteOffset).toBe(872); 97 | expect(meshoptObject1.byteLength).toBe(117); 98 | 99 | expect(bufferView2.buffer).toBe(0); 100 | expect(bufferView2.byteOffset).toBe(992); 101 | expect(bufferView2.byteLength).toBe(360); 102 | expect(meshoptObject2.buffer).toBe(0); 103 | expect(meshoptObject2.byteOffset).toBe(1352); 104 | expect(meshoptObject2.byteLength).toBe(78); 105 | }); 106 | 107 | it("gltfToGlb processes glTF with EXT_meshopt_compression extension with separate resources.", async () => { 108 | const gltf = fsExtra.readJsonSync(gltfMeshoptFallbackPath); 109 | const options = { 110 | separate: true, 111 | resourceDirectory: path.dirname(gltfMeshoptFallbackPath), 112 | }; 113 | const results = await gltfToGlb(gltf, options); 114 | expect(results.glb).toBeDefined(); 115 | 116 | const processedGltf = parseGlb(results.glb); 117 | 118 | expect(processedGltf).toBeDefined(); 119 | expect(processedGltf.buffers.length).toBe(2); 120 | 121 | const buffer0 = processedGltf.buffers[0]; 122 | const buffer1 = processedGltf.buffers[1]; 123 | 124 | const bufferView0 = gltf.bufferViews[0]; 125 | const bufferView1 = gltf.bufferViews[1]; 126 | const bufferView2 = gltf.bufferViews[2]; 127 | 128 | const meshoptObject0 = bufferView0.extensions.EXT_meshopt_compression; 129 | const meshoptObject1 = bufferView1.extensions.EXT_meshopt_compression; 130 | const meshoptObject2 = bufferView2.extensions.EXT_meshopt_compression; 131 | 132 | expect(buffer0.byteLength).toBe(960); 133 | expect(buffer0.uri).toBe("meshopt-fallback-meshopt-fallback-1.bin"); 134 | expect(buffer1.byteLength).toBe(472); 135 | expect(buffer1.uri).toBe("meshopt-fallback.bin"); 136 | 137 | expect(bufferView0.buffer).toBe(0); 138 | expect(bufferView0.byteOffset).toBe(0); 139 | expect(bufferView0.byteLength).toBe(400); 140 | expect(meshoptObject0.buffer).toBe(1); 141 | expect(meshoptObject0.byteOffset).toBe(0); 142 | expect(meshoptObject0.byteLength).toBe(265); 143 | 144 | expect(bufferView1.buffer).toBe(0); 145 | expect(bufferView1.byteOffset).toBe(400); 146 | expect(bufferView1.byteLength).toBe(200); 147 | expect(meshoptObject1.buffer).toBe(1); 148 | expect(meshoptObject1.byteOffset).toBe(272); 149 | expect(meshoptObject1.byteLength).toBe(117); 150 | 151 | expect(bufferView2.buffer).toBe(0); 152 | expect(bufferView2.byteOffset).toBe(600); 153 | expect(bufferView2.byteLength).toBe(360); 154 | expect(meshoptObject2.buffer).toBe(1); 155 | expect(meshoptObject2.byteOffset).toBe(392); 156 | expect(meshoptObject2.byteLength).toBe(78); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /specs/lib/moveTechniquesToExtensionSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const moveTechniquesToExtension = require("../../lib/moveTechniquesToExtension"); 4 | 5 | const WebGLConstants = Cesium.WebGLConstants; 6 | describe("moveTechniquesToExtension", () => { 7 | it("moves techniques, shaders, and programs to extension", () => { 8 | const gltf = { 9 | programs: { 10 | program_0: { 11 | attributes: ["a_normal", "a_position", "a_texcoord0"], 12 | fragmentShader: "BoxTextured0FS", 13 | vertexShader: "BoxTextured0VS", 14 | }, 15 | program_1: { 16 | attributes: ["a_normal", "a_position", "a_texcoord0"], 17 | fragmentShader: "BoxTextured1FS", 18 | vertexShader: "BoxTextured0VS", 19 | }, 20 | }, 21 | shaders: { 22 | BoxTextured0FS: { 23 | type: WebGLConstants.FRAGMENT_SHADER, 24 | uri: "BoxTextured0FS.glsl", 25 | }, 26 | BoxTextured0VS: { 27 | type: WebGLConstants.VERTEX_SHADER, 28 | uri: "BoxTextured0VS.glsl", 29 | }, 30 | BoxTextured1FS: { 31 | type: WebGLConstants.FRAGMENT_SHADER, 32 | uri: "BoxTextured1FS.glsl", 33 | }, 34 | }, 35 | techniques: { 36 | technique0: { 37 | attributes: { 38 | a_normal: "normal", 39 | a_position: "position", 40 | a_texcoord0: "texcoord0", 41 | }, 42 | parameters: { 43 | diffuse: { 44 | type: WebGLConstants.SAMPLER_2D, 45 | }, 46 | modelViewMatrix: { 47 | semantic: "MODELVIEW", 48 | type: WebGLConstants.FLOAT_MAT4, 49 | }, 50 | normal: { 51 | semantic: "NORMAL", 52 | type: WebGLConstants.FLOAT_VEC3, 53 | }, 54 | normalMatrix: { 55 | semantic: "MODELVIEWINVERSETRANSPOSE", 56 | type: WebGLConstants.FLOAT_MAT3, 57 | }, 58 | position: { 59 | semantic: "POSITION", 60 | type: WebGLConstants.FLOAT_VEC3, 61 | }, 62 | projectionMatrix: { 63 | semantic: "PROJECTION", 64 | type: WebGLConstants.FLOAT_MAT4, 65 | }, 66 | shininess: { 67 | type: WebGLConstants.FLOAT, 68 | }, 69 | specular: { 70 | type: WebGLConstants.FLOAT_VEC4, 71 | }, 72 | texcoord0: { 73 | semantic: "TEXCOORD_0", 74 | type: WebGLConstants.FLOAT_VEC2, 75 | }, 76 | }, 77 | program: "program_0", 78 | states: { 79 | enable: [WebGLConstants.DEPTH_TEST, WebGLConstants.CULL_FACE], 80 | }, 81 | uniforms: { 82 | u_diffuse: "diffuse", 83 | u_modelViewMatrix: "modelViewMatrix", 84 | u_normalMatrix: "normalMatrix", 85 | u_projectionMatrix: "projectionMatrix", 86 | u_shininess: "shininess", 87 | u_specular: "specular", 88 | }, 89 | }, 90 | technique1: { 91 | program: "program_1", 92 | }, 93 | technique2: { 94 | parameters: { 95 | diffuse: { 96 | type: WebGLConstants.FLOAT_VEC4, 97 | }, 98 | }, 99 | program: "program_0", 100 | uniforms: { 101 | u_diffuse: "diffuse", 102 | }, 103 | }, 104 | }, 105 | materials: [ 106 | { 107 | name: "Texture", 108 | technique: "technique0", 109 | values: { 110 | diffuse: "texture_Image0001", 111 | shininess: 256, 112 | specular: [0.2, 0.2, 0.2, 1], 113 | }, 114 | }, 115 | { 116 | name: "Color", 117 | technique: "technique2", 118 | values: { 119 | diffuse: [0.2, 0.2, 0.2, 1], 120 | }, 121 | }, 122 | ], 123 | }; 124 | 125 | const gltfWithTechniquesWebgl = moveTechniquesToExtension(gltf); 126 | expect(gltfWithTechniquesWebgl.extensions).toBeDefined(); 127 | const techniques = gltfWithTechniquesWebgl.extensions.KHR_techniques_webgl; 128 | expect(techniques).toBeDefined(); 129 | expect(techniques.techniques.length).toBe(3); 130 | 131 | const technique = techniques.techniques[0]; 132 | const attributes = technique.attributes; 133 | expect(attributes).toBeDefined(); 134 | expect(attributes.a_position.semantic).toBe("POSITION"); 135 | 136 | const uniforms = technique.uniforms; 137 | expect(uniforms).toBeDefined(); 138 | expect(uniforms.u_modelViewMatrix.semantic).toBe("MODELVIEW"); 139 | expect(uniforms.u_modelViewMatrix.type).toBe(WebGLConstants.FLOAT_MAT4); 140 | 141 | expect(technique.program).toBe(0); 142 | expect(technique.parameters).toBeUndefined(); 143 | expect(technique.states).toBeUndefined(); 144 | 145 | expect(techniques.programs.length).toBe(2); 146 | const program = techniques.programs[technique.program]; 147 | expect(program).toBeDefined(); 148 | 149 | expect(techniques.shaders.length).toBe(3); 150 | expect(techniques.shaders[program.fragmentShader].type).toBe( 151 | WebGLConstants.FRAGMENT_SHADER, 152 | ); 153 | expect(techniques.shaders[program.vertexShader].type).toBe( 154 | WebGLConstants.VERTEX_SHADER, 155 | ); 156 | 157 | expect(gltfWithTechniquesWebgl.techniques).toBeUndefined(); 158 | expect(gltfWithTechniquesWebgl.programs).toBeUndefined(); 159 | expect(gltfWithTechniquesWebgl.shaders).toBeUndefined(); 160 | 161 | const material = gltf.materials[0]; 162 | expect(material.extensions).toBeDefined(); 163 | const materialTechniques = material.extensions.KHR_techniques_webgl; 164 | expect(materialTechniques).toBeDefined(); 165 | expect(materialTechniques.technique).toBe(0); 166 | expect(materialTechniques.values.u_shininess).toBe(256); 167 | expect(materialTechniques.values.u_diffuse).toBe("texture_Image0001"); 168 | 169 | expect(material.technique).toBeUndefined(); 170 | expect(material.values).toBeUndefined(); 171 | 172 | const technique2 = techniques.techniques[1]; 173 | const program2 = techniques.programs[technique2.program]; 174 | expect(program2.vertexShader).toBe(program.vertexShader); 175 | expect(program2.fragmentShader).not.toBe(program.fragmentShader); 176 | 177 | const technique3 = techniques.techniques[2]; 178 | expect(technique3.program).toBe(0); 179 | expect(technique3.uniforms.u_diffuse.type).toBe(WebGLConstants.FLOAT_VEC4); 180 | expect( 181 | gltf.materials[1].extensions.KHR_techniques_webgl.values.u_diffuse, 182 | ).toEqual([0.2, 0.2, 0.2, 1.0]); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /lib/getStatistics.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const ForEach = require("./ForEach"); 4 | 5 | const defined = Cesium.defined; 6 | const isDataUri = Cesium.isDataUri; 7 | const WebGLConstants = Cesium.WebGLConstants; 8 | 9 | module.exports = getStatistics; 10 | 11 | /** 12 | * Returns an object containing the statistics for the glTF asset. 13 | * 14 | * @param {object} gltf A javascript object containing a glTF asset. 15 | * @param {number} [nodeId] If defined, statistics will only process number of draw calls and rendered primitives for the specified node. 16 | * @returns {Statistics} Object containing the statistics of the glTF asset. 17 | * 18 | * @see Statistics 19 | */ 20 | function getStatistics(gltf, nodeId) { 21 | const statistics = new Statistics(); 22 | 23 | if (defined(nodeId)) { 24 | const nodeDrawStats = getDrawCallStatisticsForNode(gltf, nodeId); 25 | statistics.numberOfDrawCalls = nodeDrawStats.numberOfDrawCalls; 26 | statistics.numberOfRenderedPrimitives = 27 | nodeDrawStats.numberOfRenderedPrimitives; 28 | return statistics; 29 | } 30 | 31 | const drawStats = getDrawCallStatistics(gltf); 32 | 33 | statistics.buffersByteLength = getBuffersByteLength(gltf); 34 | statistics.numberOfImages = defined(gltf.images) ? gltf.images.length : 0; 35 | statistics.numberOfExternalRequests = getNumberOfExternalRequests(gltf); 36 | statistics.numberOfDrawCalls = drawStats.numberOfDrawCalls; 37 | statistics.numberOfRenderedPrimitives = drawStats.numberOfRenderedPrimitives; 38 | statistics.numberOfNodes = defined(gltf.nodes) ? gltf.nodes.length : 0; 39 | statistics.numberOfMeshes = defined(gltf.meshes) ? gltf.meshes.length : 0; 40 | statistics.numberOfMaterials = defined(gltf.materials) 41 | ? gltf.materials.length 42 | : 0; 43 | statistics.numberOfAnimations = defined(gltf.animations) 44 | ? gltf.animations.length 45 | : 0; 46 | 47 | return statistics; 48 | } 49 | 50 | function getBuffersByteLength(gltf) { 51 | let byteLength = 0; 52 | ForEach.buffer(gltf, function (buffer) { 53 | byteLength += buffer.byteLength; 54 | }); 55 | return byteLength; 56 | } 57 | 58 | function getNumberOfExternalRequests(gltf) { 59 | let count = 0; 60 | ForEach.buffer(gltf, function (buffer) { 61 | if (defined(buffer.uri) && !isDataUri(buffer.uri)) { 62 | count++; 63 | } 64 | }); 65 | ForEach.image(gltf, function (image) { 66 | if (defined(image.uri) && !isDataUri(image.uri)) { 67 | count++; 68 | } 69 | }); 70 | ForEach.shader(gltf, function (shader) { 71 | if (defined(shader.uri) && !isDataUri(shader.uri)) { 72 | count++; 73 | } 74 | }); 75 | return count; 76 | } 77 | 78 | function getNumberOfRenderedPrimitives(gltf, primitive) { 79 | let count = 0; 80 | if (defined(primitive.indices)) { 81 | count = gltf.accessors[primitive.indices].count; 82 | } else if (defined(primitive.attributes.POSITION)) { 83 | count = gltf.accessors[primitive.attributes.POSITION].count; 84 | } 85 | switch (primitive.mode) { 86 | case WebGLConstants.POINTS: 87 | return count; 88 | case WebGLConstants.LINES: 89 | return count / 2; 90 | case WebGLConstants.LINE_LOOP: 91 | return count; 92 | case WebGLConstants.LINE_STRIP: 93 | return Math.max(count - 1, 0); 94 | case WebGLConstants.TRIANGLES: 95 | return count / 3; 96 | case WebGLConstants.TRIANGLE_STRIP: 97 | case WebGLConstants.TRIANGLE_FAN: 98 | return Math.max(count - 2, 0); 99 | default: 100 | // TRIANGLES 101 | return count / 3; 102 | } 103 | } 104 | 105 | function getDrawCallStatisticsForNode(gltf, nodeId) { 106 | let numberOfDrawCalls = 0; 107 | let numberOfRenderedPrimitives = 0; 108 | 109 | ForEach.nodeInTree(gltf, [nodeId], function (node) { 110 | const mesh = gltf.meshes[node.mesh]; 111 | if (defined(mesh)) { 112 | ForEach.meshPrimitive(mesh, function (primitive) { 113 | numberOfDrawCalls++; 114 | numberOfRenderedPrimitives += getNumberOfRenderedPrimitives( 115 | gltf, 116 | primitive, 117 | ); 118 | }); 119 | } 120 | }); 121 | 122 | return { 123 | numberOfDrawCalls: numberOfDrawCalls, 124 | numberOfRenderedPrimitives: numberOfRenderedPrimitives, 125 | }; 126 | } 127 | 128 | function getDrawCallStatistics(gltf) { 129 | let numberOfDrawCalls = 0; 130 | let numberOfRenderedPrimitives = 0; 131 | 132 | ForEach.mesh(gltf, function (mesh) { 133 | ForEach.meshPrimitive(mesh, function (primitive) { 134 | numberOfDrawCalls++; 135 | numberOfRenderedPrimitives += getNumberOfRenderedPrimitives( 136 | gltf, 137 | primitive, 138 | ); 139 | }); 140 | }); 141 | 142 | return { 143 | numberOfDrawCalls: numberOfDrawCalls, 144 | numberOfRenderedPrimitives: numberOfRenderedPrimitives, 145 | }; 146 | } 147 | 148 | /** 149 | * Contains statistics for a glTF asset. 150 | * 151 | * @property {number} buffersByteLength The total byte length of all buffers. 152 | * @property {number} numberOfImages The number of images in the asset. 153 | * @property {number} numberOfExternalRequests The number of external requests required to fetch the asset data. 154 | * @property {number} numberOfDrawCalls The number of draw calls required to render the asset. 155 | * @property {number} numberOfRenderedPrimitives The total number of rendered primitives in the asset (e.g. triangles). 156 | * @property {number} numberOfNodes The total number of nodes in the asset. 157 | * @property {number} numberOfMeshes The total number of meshes in the asset. 158 | * @property {number} numberOfMaterials The total number of materials in the asset. 159 | * @property {number} numberOfAnimations The total number of animations in the asset. 160 | * 161 | * @constructor 162 | * 163 | * @see getStatistics 164 | */ 165 | function Statistics() { 166 | this.buffersByteLength = 0; 167 | this.numberOfImages = 0; 168 | this.numberOfExternalRequests = 0; 169 | this.numberOfDrawCalls = 0; 170 | this.numberOfRenderedPrimitives = 0; 171 | this.numberOfNodes = 0; 172 | this.numberOfMeshes = 0; 173 | this.numberOfMaterials = 0; 174 | this.numberOfAnimations = 0; 175 | } 176 | 177 | /** 178 | * Creates a string listing the statistics along with their descriptions. 179 | * 180 | * @returns {string} A string describing the statistics for the glTF asset. 181 | */ 182 | Statistics.prototype.toString = function () { 183 | return ( 184 | `Total byte length of all buffers: ${this.buffersByteLength} bytes` + 185 | `\nImages: ${this.numberOfImages}\nDraw calls: ${this.numberOfDrawCalls}\nRendered primitives (e.g., triangles): ${this.numberOfRenderedPrimitives}\nNodes: ${this.numberOfNodes}\nMeshes: ${this.numberOfMeshes}\nMaterials: ${this.numberOfMaterials}\nAnimations: ${this.numberOfAnimations}\nExternal requests (not data uris): ${this.numberOfExternalRequests}` 186 | ); 187 | }; 188 | -------------------------------------------------------------------------------- /lib/readResources.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const fsExtra = require("fs-extra"); 4 | const path = require("path"); 5 | const Promise = require("bluebird"); 6 | const { URL } = require("url"); 7 | 8 | const addPipelineExtras = require("./addPipelineExtras"); 9 | const dataUriToBuffer = require("./dataUriToBuffer"); 10 | const { fileURLToPath, pathToFileURL } = require("url"); 11 | const ForEach = require("./ForEach"); 12 | 13 | const defined = Cesium.defined; 14 | const isDataUri = Cesium.isDataUri; 15 | const RuntimeError = Cesium.RuntimeError; 16 | 17 | module.exports = readResources; 18 | 19 | /** 20 | * Read data uris, buffer views, or files referenced by the glTF into buffers. 21 | * The buffer data is placed into extras._pipeline.source for the corresponding object. 22 | * This stage runs before updateVersion and handles both glTF 1.0 and glTF 2.0 assets. 23 | * 24 | * @param {object} gltf A javascript object containing a glTF asset. 25 | * @param {object} [options] Object with the following properties: 26 | * @param {string} [options.resourceDirectory] The path to look in when reading separate files. 27 | * @returns {Promise} A promise that resolves to the glTF asset when all resources are read. 28 | * 29 | * @private 30 | */ 31 | function readResources(gltf, options) { 32 | addPipelineExtras(gltf); 33 | options = options ?? {}; 34 | 35 | // Make sure its an absolute path with a trailing separator 36 | options.resourceDirectory = defined(options.resourceDirectory) 37 | ? path.resolve(options.resourceDirectory) + path.sep 38 | : undefined; 39 | 40 | const bufferPromises = []; 41 | const resourcePromises = []; 42 | 43 | ForEach.buffer(gltf, function (buffer) { 44 | bufferPromises.push(readBuffer(gltf, buffer, options)); 45 | }); 46 | 47 | // Buffers need to be read first because images and shader may resolve to bufferViews 48 | return Promise.all(bufferPromises) 49 | .then(function () { 50 | ForEach.shader(gltf, function (shader) { 51 | resourcePromises.push(readShader(gltf, shader, options)); 52 | }); 53 | ForEach.image(gltf, function (image) { 54 | resourcePromises.push(readImage(gltf, image, options)); 55 | }); 56 | return Promise.all(resourcePromises); 57 | }) 58 | .then(function () { 59 | return gltf; 60 | }); 61 | } 62 | 63 | function readBuffer(gltf, buffer, options) { 64 | return readResource(gltf, buffer, false, options).then(function (data) { 65 | if (defined(data)) { 66 | buffer.extras._pipeline.source = data; 67 | } 68 | }); 69 | } 70 | 71 | function readImage(gltf, image, options) { 72 | return readResource(gltf, image, true, options).then(function (data) { 73 | image.extras._pipeline.source = data; 74 | }); 75 | } 76 | 77 | function readShader(gltf, shader, options) { 78 | return readResource(gltf, shader, true, options).then(function (data) { 79 | shader.extras._pipeline.source = data.toString(); 80 | }); 81 | } 82 | 83 | function readResource(gltf, object, saveResourceId, options) { 84 | const uri = object.uri; 85 | delete object.uri; // Don't hold onto the uri, its contents will be stored in extras._pipeline.source 86 | 87 | // Source already exists if the gltf was converted from a glb 88 | const source = object.extras._pipeline.source; 89 | if (defined(source)) { 90 | return Promise.resolve(Buffer.from(source)); 91 | } 92 | // Handle reading buffer view from 1.0 glb model 93 | const extensions = object.extensions; 94 | if (defined(extensions)) { 95 | const khrBinaryGltf = extensions.KHR_binary_glTF; 96 | if (defined(khrBinaryGltf)) { 97 | return Promise.resolve( 98 | readBufferView(gltf, khrBinaryGltf.bufferView, object, saveResourceId), 99 | ); 100 | } 101 | } 102 | if (defined(object.bufferView)) { 103 | return Promise.resolve( 104 | readBufferView(gltf, object.bufferView, object, saveResourceId), 105 | ); 106 | } 107 | if (!defined(uri)) { 108 | return Promise.resolve(undefined); 109 | } 110 | if (isDataUri(uri)) { 111 | return Promise.resolve(dataUriToBuffer(uri)); 112 | } 113 | return readFile(object, uri, saveResourceId, options); 114 | } 115 | 116 | function readBufferView(gltf, bufferViewId, object, saveResourceId) { 117 | if (saveResourceId) { 118 | object.extras._pipeline.resourceId = bufferViewId; 119 | } 120 | const bufferView = gltf.bufferViews[bufferViewId]; 121 | const buffer = gltf.buffers[bufferView.buffer]; 122 | const source = buffer.extras._pipeline.source; 123 | const byteOffset = bufferView.byteOffset ?? 0; 124 | return source.slice(byteOffset, byteOffset + bufferView.byteLength); 125 | } 126 | 127 | function readFile(object, uri, saveResourceId, options) { 128 | const resourceDirectory = options.resourceDirectory; 129 | const hasResourceDirectory = defined(resourceDirectory); 130 | const uriIsAbsolute = uri.startsWith("/") || uri.startsWith("file:///"); 131 | // Resolve the URL 132 | let absoluteUrl; 133 | // Since we treat this as a URI, 134 | // file:///path/to/file, //path/to/file, and /path/to/file would all be considered absolute. 135 | if (uriIsAbsolute) { 136 | if (!options.allowAbsolute && hasResourceDirectory) { 137 | return Promise.reject( 138 | new RuntimeError( 139 | "Absolute paths are not permitted; use the 'allowAbsolute' option to disable this error.", 140 | ), 141 | ); 142 | } 143 | if (!hasResourceDirectory) { 144 | console.warn( 145 | `No 'resourceDirectory' provided, so relative paths are impossible; forcing 'allowAbsolute' option to avoid breaking things.`, 146 | ); 147 | } 148 | } 149 | let localUri = uri; 150 | 151 | // new URL(`/`, undefined) throws an error because there's no protocol. Give it one so naked absolute paths 152 | // survive the next step when `resourceDirectory` is not defined. 153 | if (!resourceDirectory && localUri.startsWith("/")) { 154 | localUri = new URL(localUri, "file:///"); 155 | } 156 | 157 | try { 158 | absoluteUrl = new URL( 159 | localUri, 160 | hasResourceDirectory ? pathToFileURL(resourceDirectory) : undefined, 161 | ); 162 | } catch (error) { 163 | return Promise.reject( 164 | new RuntimeError( 165 | "glTF model references separate files but no resourceDirectory is supplied", 166 | ), 167 | ); 168 | } 169 | 170 | // Generate file paths for the resource 171 | const absolutePath = fileURLToPath(absoluteUrl); 172 | const relativePath = hasResourceDirectory 173 | ? path.relative(resourceDirectory, absolutePath) 174 | : path.basename(absolutePath); 175 | 176 | if (!defined(object.name)) { 177 | const extension = path.extname(relativePath); 178 | object.name = path.basename(relativePath, extension); 179 | } 180 | 181 | if (saveResourceId) { 182 | object.extras._pipeline.resourceId = absolutePath; 183 | } 184 | 185 | object.extras._pipeline.absolutePath = absolutePath; 186 | object.extras._pipeline.relativePath = relativePath; 187 | return fsExtra.readFile(absolutePath); 188 | } 189 | -------------------------------------------------------------------------------- /specs/data/1.0/box-textured-materials-common/box-textured-materials-common.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": { 3 | "accessor_21": { 4 | "bufferView": "bufferView_29", 5 | "byteOffset": 0, 6 | "byteStride": 0, 7 | "componentType": 5123, 8 | "count": 36, 9 | "type": "SCALAR" 10 | }, 11 | "accessor_23": { 12 | "bufferView": "bufferView_30", 13 | "byteOffset": 0, 14 | "byteStride": 12, 15 | "componentType": 5126, 16 | "count": 24, 17 | "max": [ 18 | 0.5, 19 | 0.5, 20 | 0.5 21 | ], 22 | "min": [ 23 | -0.5, 24 | -0.5, 25 | -0.5 26 | ], 27 | "type": "VEC3" 28 | }, 29 | "accessor_25": { 30 | "bufferView": "bufferView_30", 31 | "byteOffset": 288, 32 | "byteStride": 12, 33 | "componentType": 5126, 34 | "count": 24, 35 | "max": [ 36 | 1, 37 | 1, 38 | 1 39 | ], 40 | "min": [ 41 | -1, 42 | -1, 43 | -1 44 | ], 45 | "type": "VEC3" 46 | }, 47 | "accessor_27": { 48 | "bufferView": "bufferView_30", 49 | "byteOffset": 576, 50 | "byteStride": 8, 51 | "componentType": 5126, 52 | "count": 24, 53 | "max": [ 54 | 6, 55 | 1 56 | ], 57 | "min": [ 58 | 0, 59 | 0 60 | ], 61 | "type": "VEC2" 62 | } 63 | }, 64 | "animations": {}, 65 | "asset": { 66 | "generator": "collada2gltf@027f74366341d569dea42e9a68b7104cc3892054", 67 | "premultipliedAlpha": true, 68 | "profile": { 69 | "api": "WebGL", 70 | "version": "1.0.2" 71 | }, 72 | "version": "1.0" 73 | }, 74 | "bufferViews": { 75 | "bufferView_29": { 76 | "buffer": "BoxTextured", 77 | "byteLength": 72, 78 | "byteOffset": 0, 79 | "target": 34963 80 | }, 81 | "bufferView_30": { 82 | "buffer": "BoxTextured", 83 | "byteLength": 768, 84 | "byteOffset": 72, 85 | "target": 34962 86 | } 87 | }, 88 | "buffers": { 89 | "BoxTextured": { 90 | "byteLength": 840, 91 | "type": "arraybuffer", 92 | "uri": "box-textured-materials-common.bin" 93 | } 94 | }, 95 | "extensionsUsed": [ 96 | "KHR_materials_common" 97 | ], 98 | "images": { 99 | "Image0001": { 100 | "name": "Image0001", 101 | "uri": "CesiumLogoFlat.png" 102 | } 103 | }, 104 | "materials": { 105 | "Effect-Texture": { 106 | "extensions": { 107 | "KHR_materials_common": { 108 | "doubleSided": false, 109 | "jointCount": 0, 110 | "technique": "PHONG", 111 | "transparent": false, 112 | "values": { 113 | "diffuse": "texture_Image0001", 114 | "shininess": 256, 115 | "specular": [ 116 | 0.2, 117 | 0.2, 118 | 0.2, 119 | 1 120 | ] 121 | } 122 | } 123 | }, 124 | "name": "Texture" 125 | } 126 | }, 127 | "meshes": { 128 | "Geometry-mesh002": { 129 | "name": "Mesh", 130 | "primitives": [ 131 | { 132 | "attributes": { 133 | "NORMAL": "accessor_25", 134 | "POSITION": "accessor_23", 135 | "TEXCOORD_0": "accessor_27" 136 | }, 137 | "indices": "accessor_21", 138 | "material": "Effect-Texture", 139 | "mode": 4 140 | } 141 | ] 142 | } 143 | }, 144 | "nodes": { 145 | "Geometry-mesh002Node": { 146 | "children": [], 147 | "matrix": [ 148 | 1, 149 | 0, 150 | 0, 151 | 0, 152 | 0, 153 | 1, 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 1, 159 | 0, 160 | 0, 161 | 0, 162 | 0, 163 | 1 164 | ], 165 | "meshes": [ 166 | "Geometry-mesh002" 167 | ], 168 | "name": "Mesh" 169 | }, 170 | "groupLocator030Node": { 171 | "children": [ 172 | "txtrLocator026Node" 173 | ], 174 | "matrix": [ 175 | 1, 176 | 0, 177 | 0, 178 | 0, 179 | 0, 180 | 1, 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 1, 186 | 0, 187 | 0, 188 | 0, 189 | 0, 190 | 1 191 | ], 192 | "name": "Texture_Group" 193 | }, 194 | "node_3": { 195 | "children": [ 196 | "Geometry-mesh002Node", 197 | "groupLocator030Node" 198 | ], 199 | "matrix": [ 200 | 1, 201 | 0, 202 | 0, 203 | 0, 204 | 0, 205 | 0, 206 | -1, 207 | 0, 208 | 0, 209 | 1, 210 | 0, 211 | 0, 212 | 0, 213 | 0, 214 | 0, 215 | 1 216 | ], 217 | "name": "Y_UP_Transform" 218 | }, 219 | "txtrLocator026Node": { 220 | "children": [], 221 | "matrix": [ 222 | 1, 223 | 0, 224 | 0, 225 | 0, 226 | 0, 227 | 1, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 1, 233 | 0, 234 | 0, 235 | 0, 236 | 0, 237 | 1 238 | ], 239 | "name": "Cesium_Logo_Flat__Image___Texture_" 240 | } 241 | }, 242 | "samplers": { 243 | "sampler_0": { 244 | "magFilter": 9729, 245 | "minFilter": 9987, 246 | "wrapS": 10497, 247 | "wrapT": 10497 248 | } 249 | }, 250 | "scene": "defaultScene", 251 | "scenes": { 252 | "defaultScene": { 253 | "nodes": [ 254 | "node_3" 255 | ] 256 | } 257 | }, 258 | "skins": {}, 259 | "textures": { 260 | "texture_Image0001": { 261 | "format": 6408, 262 | "internalFormat": 6408, 263 | "sampler": "sampler_0", 264 | "source": "Image0001", 265 | "target": 3553, 266 | "type": 5121 267 | } 268 | } 269 | } -------------------------------------------------------------------------------- /specs/lib/readResourcesSpec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Cesium = require("cesium"); 3 | const fsExtra = require("fs-extra"); 4 | const path = require("path"); 5 | const ForEach = require("../../lib/ForEach"); 6 | const parseGlb = require("../../lib/parseGlb"); 7 | const readResources = require("../../lib/readResources"); 8 | const { pathToFileURL } = require("url"); 9 | 10 | const RuntimeError = Cesium.RuntimeError; 11 | 12 | const boxTexturedSeparate1Path = 13 | "specs/data/1.0/box-textured-separate/box-textured-separate.gltf"; 14 | const boxTexturedBinarySeparate1Path = 15 | "specs/data/1.0/box-textured-binary-separate/box-textured-binary-separate.glb"; 16 | const boxTexturedBinary1Path = 17 | "specs/data/1.0/box-textured-binary/box-textured-binary.glb"; 18 | const boxTexturedEmbedded1Path = 19 | "specs/data/1.0/box-textured-embedded/box-textured-embedded.gltf"; 20 | const boxTexturedSeparate2Path = 21 | "specs/data/2.0/box-textured-separate/box-textured-separate.gltf"; 22 | const boxTexturedBinarySeparate2Path = 23 | "specs/data/2.0/box-textured-binary-separate/box-textured-binary-separate.glb"; 24 | const boxTexturedBinary2Path = 25 | "specs/data/2.0/box-textured-binary/box-textured-binary.glb"; 26 | const boxTexturedEmbedded2Path = 27 | "specs/data/2.0/box-textured-embedded/box-textured-embedded.gltf"; 28 | 29 | function readGltf(gltfPath, binary) { 30 | if (binary) { 31 | const glb = fsExtra.readFileSync(gltfPath); 32 | return parseGlb(glb); 33 | } 34 | return fsExtra.readJsonSync(gltfPath); 35 | } 36 | 37 | function checkPaths(object, resourceDirectory) { 38 | const pipelineExtras = object.extras._pipeline; 39 | const absolutePath = pipelineExtras.absolutePath; 40 | const relativePath = pipelineExtras.relativePath; 41 | expect(path.basename(relativePath)).toBe(relativePath); 42 | expect(absolutePath).toBe(path.join(resourceDirectory, relativePath)); 43 | expect(object.name).toBe( 44 | path.basename(relativePath, path.extname(relativePath)), 45 | ); 46 | } 47 | 48 | async function readsResources(gltfPath, binary, separate) { 49 | const gltf = readGltf(gltfPath, binary); 50 | const resourceDirectory = path.resolve(path.dirname(gltfPath)); 51 | const options = { 52 | resourceDirectory: resourceDirectory, 53 | }; 54 | await readResources(gltf, options); 55 | ForEach.shader(gltf, (shader) => { 56 | const shaderText = shader.extras._pipeline.source; 57 | expect(typeof shaderText === "string").toBe(true); 58 | expect(shaderText.length).toBeGreaterThan(0); 59 | expect(shader.uri).toBeUndefined(); 60 | if (separate) { 61 | checkPaths(shader, resourceDirectory); 62 | } 63 | }); 64 | ForEach.image(gltf, (image) => { 65 | const imageSource = image.extras._pipeline.source; 66 | expect(Buffer.isBuffer(imageSource)).toBe(true); 67 | expect(image.uri).toBeUndefined(); 68 | if (separate) { 69 | checkPaths(image, resourceDirectory); 70 | } 71 | }); 72 | ForEach.buffer(gltf, (buffer) => { 73 | const bufferSource = buffer.extras._pipeline.source; 74 | expect(Buffer.isBuffer(bufferSource)).toBe(true); 75 | expect(buffer.uri).toBeUndefined(); 76 | if (separate && !binary) { 77 | checkPaths(buffer, resourceDirectory); 78 | } 79 | }); 80 | } 81 | 82 | describe("readResources", () => { 83 | it("reads separate resources from 1.0 model", async () => { 84 | await readsResources(boxTexturedSeparate1Path, false, true); 85 | }); 86 | 87 | it("reads separate resources from 1.0 glb", async () => { 88 | await readsResources(boxTexturedBinarySeparate1Path, true, true); 89 | }); 90 | 91 | it("reads embedded resources from 1.0 model", async () => { 92 | await readsResources(boxTexturedEmbedded1Path, false, false); 93 | }); 94 | 95 | it("reads resources from 1.0 glb", async () => { 96 | await readsResources(boxTexturedBinary1Path, true, false); 97 | }); 98 | 99 | it("reads separate resources from model", async () => { 100 | await readsResources(boxTexturedSeparate2Path, false, true); 101 | }); 102 | 103 | it("reads separate resources from glb", async () => { 104 | await readsResources(boxTexturedBinarySeparate2Path, true, true); 105 | }); 106 | 107 | it("reads embedded resources from model", async () => { 108 | await readsResources(boxTexturedEmbedded2Path, false, false); 109 | }); 110 | 111 | it("reads resources from glb", async () => { 112 | await readsResources(boxTexturedBinary2Path, true, false); 113 | }); 114 | 115 | it("rejects if gltf contains separate resources but no resource directory is supplied", async () => { 116 | const gltf = readGltf(boxTexturedSeparate2Path); 117 | 118 | let thrownError; 119 | try { 120 | await readResources(gltf); 121 | } catch (e) { 122 | thrownError = e; 123 | } 124 | expect(thrownError).toEqual( 125 | new RuntimeError( 126 | "glTF model references separate files but no resourceDirectory is supplied", 127 | ), 128 | ); 129 | }); 130 | 131 | it("rejects if gltf contains absolute resource paths, and there is a resourceDirectory", async () => { 132 | const gltf = readGltf(boxTexturedSeparate2Path); 133 | const localUri = gltf.buffers[0].uri; 134 | const errMsg = 135 | "Absolute paths are not permitted; use the 'allowAbsolute' option to disable this error."; 136 | 137 | // uri = file:///... 138 | gltf.buffers[0].uri = new URL( 139 | localUri, 140 | pathToFileURL(boxTexturedSeparate2Path), 141 | ).toString(); 142 | await expectAsync( 143 | readResources(gltf, { 144 | resourceDirectory: path.dirname(boxTexturedSeparate2Path), 145 | }), 146 | ).toBeRejectedWithError(RuntimeError, errMsg); 147 | }); 148 | 149 | it("reads absolute resource paths when allowAbsolute there is no resourceDirectory", async () => { 150 | const gltf = readGltf(boxTexturedSeparate2Path); 151 | const toAbs = (uri) => 152 | new URL(uri, pathToFileURL(boxTexturedSeparate2Path)) 153 | .toString() 154 | .slice("file://".length); // clip off the scheme so we have "absolute path" URIs 155 | 156 | spyOn(process.stderr, "write"); 157 | // uri = /... 158 | gltf.buffers[0].uri = toAbs(gltf.buffers[0].uri); 159 | gltf.images[0].uri = toAbs(gltf.images[0].uri); 160 | 161 | await readResources(gltf); 162 | 163 | expect(gltf.images[0]?.extras?._pipeline).toBeDefined(); 164 | expect(gltf.buffers[0]?.extras?._pipeline).toBeDefined(); 165 | // No options passed, but did an absolute path; expect a warning to have been emitted 166 | expect(process.stderr.write).toHaveBeenCalled(); 167 | process.stderr.write.calls.reset(); 168 | }); 169 | 170 | it("reads absolute resource paths when allowAbsolute is true", async () => { 171 | const gltf = readGltf(boxTexturedSeparate2Path); 172 | const toAbs = ( 173 | uri, // Generate absolute URIs 174 | ) => new URL(uri, pathToFileURL(boxTexturedSeparate2Path)).toString(); 175 | spyOn(process.stderr, "write"); 176 | // uri = file:///... 177 | gltf.buffers[0].uri = toAbs(gltf.buffers[0].uri); 178 | gltf.images[0].uri = toAbs(gltf.images[0].uri); 179 | 180 | await readResources(gltf, { 181 | allowAbsolute: true, 182 | resourceDirectory: path.dirname(boxTexturedSeparate2Path), 183 | }); 184 | 185 | expect(gltf.images[0]?.extras?._pipeline).toBeDefined(); 186 | expect(gltf.buffers[0]?.extras?._pipeline).toBeDefined(); 187 | // resourceDirectory present and allowAbsolute is true. No warnings should be emitted. 188 | expect(process.stderr.write).not.toHaveBeenCalled(); 189 | process.stderr.write.calls.reset(); 190 | }); 191 | }); 192 | --------------------------------------------------------------------------------