├── .editorconfig ├── .github ├── .c8rc ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── codecov.yml ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .vercelignore ├── .yarnrc.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── benchmarks ├── benchmark.ts ├── constants.ts ├── report.ts ├── results │ └── apple-m1-pro.csv ├── tasks │ ├── clone.bench.ts │ ├── create.bench.ts │ ├── dequantize.bench.ts │ ├── dispose.bench.ts │ ├── flatten.bench.ts │ ├── index.ts │ ├── join.bench.ts │ ├── quantize.bench.ts │ └── weld.bench.ts └── utils.ts ├── biome.json ├── lerna.json ├── package.json ├── packages ├── cli │ ├── LICENSE.md │ ├── README.md │ ├── bin │ │ └── cli.js │ ├── package.json │ ├── src │ │ ├── cli.ts │ │ ├── config.ts │ │ ├── inspect.ts │ │ ├── program.ts │ │ ├── session.ts │ │ ├── transforms │ │ │ ├── index.ts │ │ │ ├── ktxdecompress.ts │ │ │ ├── ktxfix.ts │ │ │ ├── merge.ts │ │ │ ├── toktx.ts │ │ │ └── xmp.ts │ │ ├── types │ │ │ └── global.d.ts │ │ ├── util.ts │ │ └── validate.ts │ ├── test │ │ ├── cli.test.ts │ │ ├── in │ │ │ ├── ACMEBox.bin │ │ │ ├── ACMEBox.gltf │ │ │ ├── acme-gltf-transform.config.js │ │ │ ├── chr_knight.glb │ │ │ └── test.ktx2 │ │ ├── ktxdecompress.test.ts │ │ ├── ktxfix.test.ts │ │ ├── toktx.test.ts │ │ ├── tsconfig.json │ │ └── util.test.ts │ └── tsconfig.json ├── core │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── document.ts │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── io │ │ │ ├── deno-io.ts │ │ │ ├── index.ts │ │ │ ├── node-io.ts │ │ │ ├── platform-io.ts │ │ │ ├── reader-context.ts │ │ │ ├── reader.ts │ │ │ ├── web-io.ts │ │ │ ├── writer-context.ts │ │ │ └── writer.ts │ │ ├── json-document.ts │ │ ├── properties │ │ │ ├── accessor.ts │ │ │ ├── animation-channel.ts │ │ │ ├── animation-sampler.ts │ │ │ ├── animation.ts │ │ │ ├── buffer.ts │ │ │ ├── camera.ts │ │ │ ├── extensible-property.ts │ │ │ ├── extension-property.ts │ │ │ ├── index.ts │ │ │ ├── material.ts │ │ │ ├── mesh.ts │ │ │ ├── node.ts │ │ │ ├── primitive-target.ts │ │ │ ├── primitive.ts │ │ │ ├── property.ts │ │ │ ├── root.ts │ │ │ ├── scene.ts │ │ │ ├── skin.ts │ │ │ ├── texture-info.ts │ │ │ └── texture.ts │ │ ├── types │ │ │ └── gltf.ts │ │ └── utils │ │ │ ├── buffer-utils.ts │ │ │ ├── color-utils.ts │ │ │ ├── file-utils.ts │ │ │ ├── get-bounds.ts │ │ │ ├── http-utils.ts │ │ │ ├── image-utils.ts │ │ │ ├── index.ts │ │ │ ├── is-plain-object.ts │ │ │ ├── logger.ts │ │ │ ├── math-utils.ts │ │ │ ├── property-utils.ts │ │ │ └── uuid.ts │ ├── test │ │ ├── document.test.ts │ │ ├── extension.test.ts │ │ ├── in │ │ │ ├── BoxTextured_glTF-pbrSpecularGlossiness │ │ │ │ ├── BoxTextured.gltf │ │ │ │ ├── BoxTextured0.bin │ │ │ │ └── CesiumLogoFlat.png │ │ │ ├── BoxTextured_glTF │ │ │ │ ├── BoxTextured.gltf │ │ │ │ ├── BoxTextured0.bin │ │ │ │ └── CesiumLogoFlat.png │ │ │ ├── BoxVertexColors.glb │ │ │ ├── Box_glTF-Binary │ │ │ │ └── Box.glb │ │ │ ├── Box_glTF-Embedded │ │ │ │ └── Box.gltf │ │ │ ├── Box_glTF │ │ │ │ ├── Box.gltf │ │ │ │ └── Box0.bin │ │ │ ├── EncodingTest │ │ │ │ ├── Unicode ❤♻ Binary.bin │ │ │ │ ├── Unicode ❤♻ Test.gltf │ │ │ │ └── Unicode ❤♻ Texture.png │ │ │ ├── test.jpg │ │ │ └── test.png │ │ ├── io │ │ │ ├── node-io.test.ts │ │ │ ├── platform-io.test.ts │ │ │ └── web-io.test.ts │ │ ├── out │ │ │ └── .gitkeep │ │ ├── properties │ │ │ ├── accessor.test.ts │ │ │ ├── animation.test.ts │ │ │ ├── buffer.test.ts │ │ │ ├── camera.test.ts │ │ │ ├── material.test.ts │ │ │ ├── mesh.test.ts │ │ │ ├── node.test.ts │ │ │ ├── primitive-target.test.ts │ │ │ ├── property.test.ts │ │ │ ├── root.test.ts │ │ │ ├── scene.test.ts │ │ │ ├── skin.test.ts │ │ │ └── texture.test.ts │ │ ├── tsconfig.json │ │ └── utils │ │ │ ├── buffer-utils.test.ts │ │ │ ├── color-utils.test.ts │ │ │ ├── file-utils.test.ts │ │ │ ├── image-utils.test.ts │ │ │ ├── logger.test.ts │ │ │ ├── math-utils.test.ts │ │ │ └── uuid.test.ts │ └── tsconfig.json ├── docs │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ ├── components │ │ │ │ ├── commercial-use.svelte │ │ │ │ └── license.svelte │ │ │ ├── pages │ │ │ │ ├── cli-configuration.md │ │ │ │ ├── cli.md │ │ │ │ ├── concepts.md │ │ │ │ ├── credits.md │ │ │ │ ├── extensions.md │ │ │ │ └── functions.md │ │ │ └── server │ │ │ │ └── model │ │ │ │ └── index.ts │ │ └── routes │ │ │ ├── +layout.server.ts │ │ │ ├── +layout.svelte │ │ │ ├── +page.md │ │ │ ├── +page.server.ts │ │ │ ├── [slug] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ │ └── modules │ │ │ └── [module] │ │ │ └── [kind] │ │ │ └── [slug] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ ├── static │ │ ├── main.css │ │ └── media │ │ │ ├── concepts.png │ │ │ ├── extensions │ │ │ ├── khr-material-pbr-specular-glossiness.png │ │ │ ├── khr-materials-anisotropy.jpg │ │ │ ├── khr-materials-clearcoat.png │ │ │ ├── khr-materials-diffuse-transmission.png │ │ │ ├── khr-materials-dispersion.jpg │ │ │ ├── khr-materials-emissive-strength.jpg │ │ │ ├── khr-materials-iridescence.png │ │ │ ├── khr-materials-sheen.png │ │ │ ├── khr-materials-transmission.png │ │ │ ├── khr-materials-unlit.png │ │ │ ├── khr-materials-variants.jpg │ │ │ └── khr-materials-volume.png │ │ │ ├── functions │ │ │ └── palette.png │ │ │ ├── hero.jpg │ │ │ └── kicker.jpg │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vercel.json │ └── vite.config.ts ├── extensions │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── ext-mesh-gpu-instancing │ │ │ ├── index.ts │ │ │ ├── instanced-mesh.ts │ │ │ └── mesh-gpu-instancing.ts │ │ ├── ext-meshopt-compression │ │ │ ├── constants.ts │ │ │ ├── decoder.ts │ │ │ ├── encoder.ts │ │ │ ├── index.ts │ │ │ └── meshopt-compression.ts │ │ ├── ext-texture-avif │ │ │ ├── index.ts │ │ │ └── texture-avif.ts │ │ ├── ext-texture-webp │ │ │ ├── index.ts │ │ │ └── texture-webp.ts │ │ ├── index.ts │ │ ├── khr-draco-mesh-compression │ │ │ ├── decoder.ts │ │ │ ├── draco-mesh-compression.ts │ │ │ ├── encoder.ts │ │ │ └── index.ts │ │ ├── khr-lights-punctual │ │ │ ├── index.ts │ │ │ ├── light.ts │ │ │ └── lights-punctual.ts │ │ ├── khr-materials-anisotropy │ │ │ ├── anisotropy.ts │ │ │ ├── index.ts │ │ │ └── materials-anisotropy.ts │ │ ├── khr-materials-clearcoat │ │ │ ├── clearcoat.ts │ │ │ ├── index.ts │ │ │ └── materials-clearcoat.ts │ │ ├── khr-materials-diffuse-transmission │ │ │ ├── diffuse-transmission.ts │ │ │ ├── index.ts │ │ │ └── materials-diffuse-transmission.ts │ │ ├── khr-materials-dispersion │ │ │ ├── dispersion.ts │ │ │ ├── index.ts │ │ │ └── materials-dispersion.ts │ │ ├── khr-materials-emissive-strength │ │ │ ├── emissive-strength.ts │ │ │ ├── index.ts │ │ │ └── materials-emissive-strength.ts │ │ ├── khr-materials-ior │ │ │ ├── index.ts │ │ │ ├── ior.ts │ │ │ └── materials-ior.ts │ │ ├── khr-materials-iridescence │ │ │ ├── index.ts │ │ │ ├── iridescence.ts │ │ │ └── materials-iridescence.ts │ │ ├── khr-materials-pbr-specular-glossiness │ │ │ ├── index.ts │ │ │ ├── materials-pbr-specular-glossiness.ts │ │ │ └── pbr-specular-glossiness.ts │ │ ├── khr-materials-sheen │ │ │ ├── index.ts │ │ │ ├── materials-sheen.ts │ │ │ └── sheen.ts │ │ ├── khr-materials-specular │ │ │ ├── index.ts │ │ │ ├── materials-specular.ts │ │ │ └── specular.ts │ │ ├── khr-materials-transmission │ │ │ ├── index.ts │ │ │ ├── materials-transmission.ts │ │ │ └── transmission.ts │ │ ├── khr-materials-unlit │ │ │ ├── index.ts │ │ │ ├── materials-unlit.ts │ │ │ └── unlit.ts │ │ ├── khr-materials-variants │ │ │ ├── index.ts │ │ │ ├── mapping-list.ts │ │ │ ├── mapping.ts │ │ │ ├── materials-variants.ts │ │ │ └── variant.ts │ │ ├── khr-materials-volume │ │ │ ├── index.ts │ │ │ ├── materials-volume.ts │ │ │ └── volume.ts │ │ ├── khr-mesh-quantization │ │ │ ├── index.ts │ │ │ └── mesh-quantization.ts │ │ ├── khr-texture-basisu │ │ │ ├── index.ts │ │ │ └── texture-basisu.ts │ │ ├── khr-texture-transform │ │ │ ├── index.ts │ │ │ ├── texture-transform.ts │ │ │ └── transform.ts │ │ └── khr-xmp-json-ld │ │ │ ├── index.ts │ │ │ ├── packet.ts │ │ │ └── xmp.ts │ ├── test │ │ ├── draco-mesh-compression.test.ts │ │ ├── in │ │ │ ├── 0.bin │ │ │ ├── BoxDraco.gltf │ │ │ ├── BoxMeshopt.bin │ │ │ ├── BoxMeshopt.glb │ │ │ ├── BoxMeshopt.gltf │ │ │ ├── DracoSparseMesh.bin │ │ │ ├── DracoSparseMesh.gltf │ │ │ ├── test-lossless.webp │ │ │ ├── test-lossy.webp │ │ │ ├── test.avif │ │ │ └── test.ktx2 │ │ ├── lights-punctual.test.ts │ │ ├── materials-anisotropy.test.ts │ │ ├── materials-clearcoat.test.ts │ │ ├── materials-diffuse-transmission.test.ts │ │ ├── materials-dispersion.test.ts │ │ ├── materials-emissive-strength.test.ts │ │ ├── materials-ior.test.ts │ │ ├── materials-iridescence.test.ts │ │ ├── materials-pbr-specular-glossiness.test.ts │ │ ├── materials-sheen.test.ts │ │ ├── materials-specular.test.ts │ │ ├── materials-transmission.test.ts │ │ ├── materials-unlit.test.ts │ │ ├── materials-variants.test.ts │ │ ├── materials-volume.test.ts │ │ ├── mesh-gpu-instancing.test.ts │ │ ├── mesh-quantization.test.ts │ │ ├── meshopt-compression.test.ts │ │ ├── texture-avif.test.ts │ │ ├── texture-basisu.test.ts │ │ ├── texture-transform.test.ts │ │ ├── texture-webp.test.ts │ │ ├── tsconfig.json │ │ └── xmp.test.ts │ └── tsconfig.json ├── functions │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── center.ts │ │ ├── clean-primitive.ts │ │ ├── clear-node-parent.ts │ │ ├── clear-node-transform.ts │ │ ├── compact-primitive.ts │ │ ├── convert-primitive-mode.ts │ │ ├── dedup.ts │ │ ├── dequantize.ts │ │ ├── document-utils.ts │ │ ├── draco.ts │ │ ├── flatten.ts │ │ ├── get-bounds.ts │ │ ├── get-texture-color-space.ts │ │ ├── get-vertex-count.ts │ │ ├── hash-table.ts │ │ ├── index.ts │ │ ├── inspect.ts │ │ ├── instance.ts │ │ ├── join-primitives.ts │ │ ├── join.ts │ │ ├── list-node-scenes.ts │ │ ├── list-texture-channels.ts │ │ ├── list-texture-info.ts │ │ ├── list-texture-slots.ts │ │ ├── meshopt.ts │ │ ├── metal-rough.ts │ │ ├── normals.ts │ │ ├── palette.ts │ │ ├── partition.ts │ │ ├── prune.ts │ │ ├── quantize.ts │ │ ├── reorder.ts │ │ ├── resample.ts │ │ ├── sequence.ts │ │ ├── simplify.ts │ │ ├── sort-primitive-weights.ts │ │ ├── sparse.ts │ │ ├── tangents.ts │ │ ├── texture-compress.ts │ │ ├── transform-mesh.ts │ │ ├── transform-primitive.ts │ │ ├── types │ │ │ └── .gitkeep │ │ ├── uninstance.ts │ │ ├── unlit.ts │ │ ├── unpartition.ts │ │ ├── unweld.ts │ │ ├── utils.ts │ │ ├── vertex-color-space.ts │ │ └── weld.ts │ ├── test │ │ ├── center.test.ts │ │ ├── clear-node-parent.test.ts │ │ ├── clear-node-transform.test.ts │ │ ├── convert-primitive-to-mode.test.ts │ │ ├── dedup.test.ts │ │ ├── dequantize.test.ts │ │ ├── document-utils.test.ts │ │ ├── draco.test.ts │ │ ├── flatten.test.ts │ │ ├── get-bounds.test.ts │ │ ├── get-vertex-count.test.ts │ │ ├── in │ │ │ ├── DenseSphere.glb │ │ │ ├── Mesh_PrimitiveMode_01_to_06.json │ │ │ ├── ShapeCollection.glb │ │ │ ├── TwoCubes.glb │ │ │ ├── many-cubes.bin │ │ │ ├── many-cubes.gltf │ │ │ ├── pattern-half.png │ │ │ └── pattern.png │ │ ├── inspect.test.ts │ │ ├── instance.test.ts │ │ ├── join-primitives.test.ts │ │ ├── join.test.ts │ │ ├── list-node-scenes.test.ts │ │ ├── list-texture-channels.test.ts │ │ ├── list-texture-info.test.ts │ │ ├── list-texture-slots.test.ts │ │ ├── meshopt.test.ts │ │ ├── metal-rough.test.ts │ │ ├── normals.test.ts │ │ ├── palette.test.ts │ │ ├── partition.test.ts │ │ ├── prune.test.ts │ │ ├── quantize.test.ts │ │ ├── reorder.test.ts │ │ ├── resample.test.ts │ │ ├── sequence.test.ts │ │ ├── simplify.test.ts │ │ ├── sort-primitive-weights.test.ts │ │ ├── sparse.test.ts │ │ ├── tangents.test.ts │ │ ├── texture-compress.test.ts │ │ ├── transform-mesh.test.ts │ │ ├── tsconfig.json │ │ ├── uninstance.test.ts │ │ ├── unlit.test.ts │ │ ├── unpartition.test.ts │ │ ├── unweld.test.ts │ │ ├── utils.test.ts │ │ ├── vertex-color-space.test.ts │ │ └── weld.test.ts │ └── tsconfig.json ├── global.d.ts ├── test-utils │ ├── LICENSE.md │ ├── package.json │ ├── src │ │ ├── create-basic-primitive.ts │ │ ├── create-torus-primitive.ts │ │ └── index.ts │ └── tsconfig.json └── view │ ├── CONTRIBUTING.md │ ├── LICENSE.md │ ├── README.md │ ├── assets │ ├── DamagedHelmet.glb │ ├── royal_esplanade_1k.hdr │ ├── view_architecture.json │ ├── view_architecture.png │ └── view_architecture.tldr │ ├── examples │ ├── 1-model.html │ ├── 1-model.ts │ ├── 2-material.html │ ├── 2-material.ts │ ├── 3-diff.html │ ├── 3-diff.ts │ ├── dropzone.ts │ ├── index.html │ ├── material-pane.ts │ ├── stats-pane.ts │ ├── style.css │ ├── util.ts │ └── vite.config.js │ ├── package.json │ ├── src │ ├── DocumentView.ts │ ├── DocumentViewImpl.ts │ ├── ImageProvider.ts │ ├── constants.ts │ ├── index.ts │ ├── observers │ │ ├── RefListObserver.ts │ │ ├── RefMapObserver.ts │ │ ├── RefObserver.ts │ │ └── index.ts │ ├── pools │ │ ├── MaterialPool.ts │ │ ├── Pool.ts │ │ ├── SingleUserPool.ts │ │ ├── TexturePool.ts │ │ └── index.ts │ ├── subjects │ │ ├── AccessorSubject.ts │ │ ├── ExtensionSubject.ts │ │ ├── InstancedMeshSubject.ts │ │ ├── LightSubject.ts │ │ ├── MaterialSubject.ts │ │ ├── MeshSubject.ts │ │ ├── NodeSubject.ts │ │ ├── PrimitiveSubject.ts │ │ ├── SceneSubject.ts │ │ ├── SkinSubject.ts │ │ ├── Subject.ts │ │ ├── TextureSubject.ts │ │ └── index.ts │ └── utils │ │ ├── Observable.ts │ │ └── index.ts │ ├── test │ ├── DocumentView.test.ts │ ├── InstancedMeshSubject.test.ts │ ├── LightSubject.test.ts │ ├── MaterialSubject.test.ts │ ├── MeshSubject.test.ts │ ├── NodeSubject.test.ts │ ├── PrimitiveSubject.test.ts │ ├── SceneSubject.test.ts │ ├── SkinSubject.test.ts │ ├── TextureSubject.test.ts │ └── tsconfig.json │ └── tsconfig.json ├── scripts ├── README.md ├── check-release.ts ├── deno.json ├── deno.lock ├── deno_test.ts ├── index.html ├── out │ └── .gitkeep ├── roundtrip.ts └── validate.cjs ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [{*.ts,*.js}] 9 | indent_size = 4 10 | indent_style = tab 11 | quote_type = single 12 | 13 | [{*.json,*.md,*.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "packages/*/vendor", 4 | "packages/*/test", 5 | "packages/test-utils/**/*", 6 | "packages/core/src/constants.ts", 7 | "packages/cli/src/cli.ts", 8 | "packages/cli/src/transforms/toktx.ts", 9 | "packages/cli/src/transforms/xmp.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [donmccurdy] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Versions:** 24 | - Version: [e.g. v0.11] 25 | - Environment: [e.g. Browser, Node.js] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Help and support 4 | url: https://github.com/donmccurdy/glTF-Transform/discussions 5 | about: Please use GitHub Discussions if you have questions or need help. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.codecov.com/docs/codecov-yaml#default-yaml 2 | codecov: 3 | require_ci_to_pass: yes 4 | 5 | comment: false 6 | 7 | coverage: 8 | precision: 2 9 | round: down 10 | range: "50...95" 11 | status: 12 | patch: off 13 | project: 14 | default: 15 | target: 90% 16 | threshold: 1% 17 | 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: yes 22 | loop: yes 23 | method: no 24 | macro: no 25 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>donmccurdy/renovate-config"], 3 | "packageRules": [ 4 | { 5 | "description": "Requires updates to tweakpane-plugin-thumbnail-list", 6 | "matchPackageNames": ["tweakpane", "@tweakpane/core"], 7 | "enabled": false 8 | }, 9 | { 10 | "description": "https://github.com/lovell/sharp/issues/4095", 11 | "matchPackageNames": ["sharp"], 12 | "enabled": false 13 | }, 14 | { 15 | "description": "Requires updates to greendoc", 16 | "matchPackageNames": ["ts-morph"], 17 | "enabled": false 18 | }, 19 | { 20 | "description": "Requires Node.js v22+ from v2.0.1, and v2.0.0 is broken, see https://github.com/mattcg/language-tags/commit/efbe8c81e58fe847aa1ec922ee735edbea6c2545", 21 | "matchPackageNames": ["language-tags", "@types/language-tags"], 22 | "enabled": false 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yarn 2 | .pnp.* 3 | .yarn/* 4 | !.yarn/patches 5 | !.yarn/plugins 6 | !.yarn/releases 7 | !.yarn/sdks 8 | !.yarn/versions 9 | node_modules 10 | 11 | # debug 12 | *.log 13 | 14 | # editor 15 | .vscode 16 | 17 | # test data 18 | **/out/* 19 | !**/out/.gitkeep 20 | 21 | # build artifacts 22 | packages/**/dist/* 23 | packages/**/.rts2* 24 | 25 | # documentation 26 | .vercel 27 | docs/dist/** 28 | 29 | # coverage 30 | coverage 31 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | packages/core 2 | packages/extensions 3 | packages/functions 4 | packages/cli 5 | packages/test-utils 6 | node_modules 7 | test 8 | .svelte-kit 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/benchmark.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from 'tinybench'; 2 | import { tasks } from './tasks/index.js'; 3 | import { printReport, readReport, updateReport, writeReport } from './report.js'; 4 | import { VERSION } from '@gltf-transform/core'; 5 | 6 | /** 7 | * DEVELOPER NOTES: 8 | * 9 | * Started out with benchmark.js, but quickly hit some frustrating issues. 10 | * Async is difficult. Setup functions are serialized and can't access scope. 11 | * Options on the Suite appear to do nothing. Switched to tinybench. 12 | */ 13 | 14 | const argv = process.argv; 15 | const parseFlag = (flag: string, value: string): string => { 16 | if (!value || value.startsWith('-')) { 17 | throw new Error(`Usage: ${flag} `); 18 | } 19 | return value; 20 | }; 21 | const flags = { 22 | filter: argv.includes('--filter') ? parseFlag('--filter', argv[argv.indexOf('--filter') + 1]) : false, 23 | past: argv.includes('--past'), 24 | table: argv.includes('--table'), 25 | report: argv.includes('--report') ? parseFlag('--report', argv[argv.indexOf('--report') + 1]) : false, 26 | reportVersion: argv.includes('--report-version'), 27 | print: argv.includes('--print'), 28 | }; 29 | 30 | /****************************************************************************** 31 | * CREATE BENCHMARK SUITE 32 | */ 33 | 34 | const bench = new Bench({ time: 1000 }); 35 | for (const [title, fn, options] of tasks) { 36 | if (!flags.filter || title.startsWith(flags.filter as string)) { 37 | bench.add(title, fn, options); 38 | } 39 | } 40 | 41 | /****************************************************************************** 42 | * EXECUTE 43 | */ 44 | 45 | const version = flags.reportVersion ? VERSION : flags.report || 'dev'; 46 | const report = await readReport(); 47 | 48 | if (flags.past === false) { 49 | await bench.run(); 50 | await updateReport(report, bench, version as string); 51 | } 52 | 53 | /****************************************************************************** 54 | * REPORT 55 | */ 56 | 57 | if (flags.table && flags.past === false) { 58 | console.table(bench.table()); 59 | } else if (flags.table) { 60 | console.warn('Skipping table, bench did not run'); 61 | } 62 | 63 | if (flags.print) { 64 | await printReport(report); 65 | } 66 | 67 | if (flags.report || flags.reportVersion) { 68 | await writeReport(report); 69 | } 70 | -------------------------------------------------------------------------------- /benchmarks/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Size { 2 | SM = 32, 3 | MD = 64, 4 | LG = 128, 5 | } 6 | 7 | export interface TaskOptions { 8 | beforeAll?: () => void; 9 | beforeEach?: () => void; 10 | afterAll?: () => void; 11 | afterEach?: () => void; 12 | } 13 | export type Task = [string, () => void, TaskOptions]; 14 | -------------------------------------------------------------------------------- /benchmarks/results/apple-m1-pro.csv: -------------------------------------------------------------------------------- 1 | version,clone,create,dequantize,dispose,join,quantize,weld,flatten 2 | v3.10.1,12.6604,29.8105,663.0726,0.5123,127.446,95.8734,575.7039, 3 | v4.0.0-alpha.12,12.5776,29.095,9.2248,0.4989,33.6132,100.5125,64.7211,578.4956 4 | v4.0.0-alpha.13,12.6597,29.4154,9.206,0.4779,36.136,107.7871,70.5743,574.4995 5 | v4.0.0-alpha.14,12.0619,31.4205,9.362,0.4374,39.452,107.4877,100.6954,334.1944 6 | v4.0.0-alpha.15,12.8537,31.2225,9.5291,0.4582,33.7321,107.2782,72.3632,340.503 7 | v4.0.0-alpha.16,11.9447,32.1367,9.3621,0.5009,18.6755,93.1424,69.9355,380.0857 8 | v4.0.0-alpha.17,12.6231,31.5785,9.7358,0.4902,18.6976,94.8338,69.6019,347.3177 9 | v4.0.0-alpha.18,11.932,31.4591,9.2686,0.4709,18.5291,94.2212,69.3379,336.9486 10 | v4.0.0-alpha.19,11.9581,31.1612,9.3829,0.5068,19.1629,100.7591,70.5272,362.7554 11 | v4.0.0,11.8195,31.7387,9.4672,0.4611,18.6351,94.6259,71.7457,409.8502 12 | v4.0.1,12.1877,32.2043,9.3158,0.463,18.6923,93.9946,69.4927,340.4948 13 | v4.0.2,12.6758,33.1228,9.6622,0.4888,24.7399,97.4306,74.2208,343.6087 14 | v4.0.3,13.2641,32.1008,10.2885,0.5158,19.4867,93.6037,73.1397,366.1845 15 | v4.0.4,11.7087,31.8374,9.5598,0.4813,19.3532,95.0073,73.1497,364.7338 16 | v4.0.5,12.415,32.8697,9.6256,0.4934,18.9091,96.2481,70.3749,354.646 17 | v4.0.6,12.1584,31.5368,9.5556,0.4598,19.4351,96.2734,69.3757,344.8655 18 | v4.0.7,12.2474,31.6123,9.3566,0.4994,18.2556,93.2856,69.8443,424.0822 19 | v4.0.8,12.2151,34.3063,10.3714,0.4745,19.278,94.8912,80.1642,346.7352 20 | v4.0.9,11.6376,31.0928,9.4095,0.4837,18.2683,92.6743,74.8708,342.8251 21 | v4.1.0,10.1354,25.6024,9.452,0.2546,17.5893,93.1082,67.2979,295.0716 22 | v4.1.1,11.7221,35.7541,10.833,0.3323,17.6417,100.332,69.2065,333.2662 23 | v4.1.2,11.6863,33.8882,9.8055,0.3239,17.7402,102.6906,74.8895,334.0207 24 | v4.1.3,11.1851,31.8596,9.6498,0.3134,16.6054,97.0211,63.5831,294.6802 25 | v4.1.4,11.8264,33.6632,10.6777,0.3218,17.2593,98.0173,64.8605,307.0761 -------------------------------------------------------------------------------- /benchmarks/tasks/clone.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { cloneDocument } from '@gltf-transform/functions'; 3 | import { Size, Task } from '../constants'; 4 | import { createLargeDocument } from '../utils'; 5 | 6 | let _document: Document; 7 | 8 | export const tasks: Task[] = [ 9 | ['clone', () => cloneDocument(_document), { beforeAll: () => void (_document = createLargeDocument(Size.SM)) }], 10 | ]; 11 | -------------------------------------------------------------------------------- /benchmarks/tasks/create.bench.ts: -------------------------------------------------------------------------------- 1 | import { Size, Task } from '../constants'; 2 | import { createLargeDocument } from '../utils'; 3 | 4 | const createMD: Task = ['create', () => createLargeDocument(Size.MD), {}]; 5 | 6 | export const tasks = [createMD]; 7 | -------------------------------------------------------------------------------- /benchmarks/tasks/dequantize.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { dequantize, quantize } from '@gltf-transform/functions'; 3 | import { createTorusKnotPrimitive } from '@gltf-transform/test-utils'; 4 | import { Task } from '../constants'; 5 | import { LOGGER } from '../utils'; 6 | 7 | let _document: Document; 8 | 9 | export const tasks: Task[] = [ 10 | [ 11 | 'dequantize', 12 | async () => { 13 | await _document.transform(dequantize()); 14 | }, 15 | { 16 | beforeEach: async () => { 17 | // ~250,000 vertices / prim 18 | _document = new Document().setLogger(LOGGER); 19 | const prim = createTorusKnotPrimitive(_document, { radialSegments: 512, tubularSegments: 512 }); 20 | const mesh = _document.createMesh().addPrimitive(prim); 21 | const node = _document.createNode().setMesh(mesh); 22 | _document.createScene().addChild(node); 23 | await _document.transform(quantize()); 24 | }, 25 | }, 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /benchmarks/tasks/dispose.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { Size, Task } from '../constants'; 3 | import { createLargeDocument } from '../utils'; 4 | 5 | let _document: Document; 6 | 7 | export const tasks: Task[] = [ 8 | [ 9 | 'dispose', 10 | () => { 11 | const nodes = _document.getRoot().listNodes(); 12 | for (let i = 0, il = Math.min(nodes.length, 100); i < il; i++) { 13 | nodes[i].dispose(); 14 | } 15 | }, 16 | { 17 | beforeEach: () => { 18 | _document = createLargeDocument(Size.MD); 19 | }, 20 | }, 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /benchmarks/tasks/flatten.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { flatten } from '@gltf-transform/functions'; 3 | import { Size, Task } from '../constants'; 4 | import { createLargeDocument } from '../utils'; 5 | 6 | let _document: Document; 7 | 8 | export const tasks: Task[] = [ 9 | [ 10 | 'flatten', 11 | async () => { 12 | await _document.transform(flatten()); 13 | }, 14 | { 15 | beforeEach: () => { 16 | _document = createLargeDocument(Size.LG); 17 | }, 18 | }, 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /benchmarks/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '../constants.js'; 2 | import { tasks as createTasks } from './clone.bench.js'; 3 | import { tasks as cloneTasks } from './create.bench.js'; 4 | import { tasks as dequantizeTasks } from './dequantize.bench.js'; 5 | import { tasks as disposeTasks } from './dispose.bench.js'; 6 | import { tasks as flattenTasks } from './flatten.bench.js'; 7 | import { tasks as joinTasks } from './join.bench.js'; 8 | import { tasks as quantizeTasks } from './quantize.bench.js'; 9 | import { tasks as weldTasks } from './weld.bench.js'; 10 | 11 | export const tasks: Task[] = [ 12 | ...createTasks, 13 | ...cloneTasks, 14 | ...dequantizeTasks, 15 | ...disposeTasks, 16 | ...flattenTasks, 17 | ...joinTasks, 18 | ...quantizeTasks, 19 | ...weldTasks, 20 | ]; 21 | -------------------------------------------------------------------------------- /benchmarks/tasks/join.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { join } from '@gltf-transform/functions'; 3 | import { createTorusKnotPrimitive } from '@gltf-transform/test-utils'; 4 | import { Task } from '../constants'; 5 | import { LOGGER } from '../utils'; 6 | 7 | let _document: Document; 8 | 9 | export const tasks: Task[] = [ 10 | [ 11 | 'join', 12 | async () => { 13 | await _document.transform(join()); 14 | }, 15 | { beforeEach: () => void (_document = createDocument(10, 64, 64)) }, // ~4000 vertices / prim 16 | ], 17 | ]; 18 | 19 | function createDocument(primCount: number, radialSegments: number, tubularSegments: number): Document { 20 | const document = new Document().setLogger(LOGGER); 21 | 22 | const scene = document.createScene(); 23 | for (let i = 0; i < primCount; i++) { 24 | const prim = createTorusKnotPrimitive(document, { radialSegments, tubularSegments }); 25 | const mesh = document.createMesh().addPrimitive(prim); 26 | const node = document.createNode().setMesh(mesh); 27 | scene.addChild(node); 28 | } 29 | 30 | return document; 31 | } 32 | -------------------------------------------------------------------------------- /benchmarks/tasks/quantize.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { quantize } from '@gltf-transform/functions'; 3 | import { createTorusKnotPrimitive } from '@gltf-transform/test-utils'; 4 | import { Task } from '../constants'; 5 | import { LOGGER } from '../utils'; 6 | 7 | let _document: Document; 8 | 9 | export const tasks: Task[] = [ 10 | [ 11 | 'quantize', 12 | async () => { 13 | await _document.transform(quantize()); 14 | }, 15 | { 16 | beforeEach: () => { 17 | // ~250,000 vertices / prim 18 | _document = new Document().setLogger(LOGGER); 19 | const prim = createTorusKnotPrimitive(_document, { radialSegments: 512, tubularSegments: 512 }); 20 | const mesh = _document.createMesh().addPrimitive(prim); 21 | const node = _document.createNode().setMesh(mesh); 22 | _document.createScene().addChild(node); 23 | }, 24 | }, 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /benchmarks/tasks/weld.bench.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@gltf-transform/core'; 2 | import { weld } from '@gltf-transform/functions'; 3 | import { createTorusKnotPrimitive } from '@gltf-transform/test-utils'; 4 | import { Task } from '../constants'; 5 | import { LOGGER } from '../utils'; 6 | 7 | let _document: Document; 8 | 9 | export const tasks: Task[] = [ 10 | [ 11 | 'weld', 12 | async () => { 13 | await _document.transform(weld()); 14 | }, 15 | { beforeEach: () => void (_document = createTorusKnotDocument(512, 512)) }, // ~250,000 vertices 16 | ], 17 | ]; 18 | 19 | function createTorusKnotDocument(radialSegments: number, tubularSegments: number): Document { 20 | const document = new Document().setLogger(LOGGER); 21 | const prim = createTorusKnotPrimitive(document, { radialSegments, tubularSegments }); 22 | const mesh = document.createMesh().addPrimitive(prim); 23 | const node = document.createNode().setMesh(mesh); 24 | document.createScene().addChild(node); 25 | return document; 26 | } 27 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatter": { 3 | "indentStyle": "tab", 4 | "indentWidth": 4, 5 | "lineWidth": 120 6 | }, 7 | "linter": { 8 | "rules": { 9 | "suspicious": { 10 | "noApproximativeNumericConstant": "off", 11 | "noAssignInExpressions": "off", 12 | "noEmptyInterface": "off", 13 | "noFallthroughSwitchClause": "off", 14 | "noGlobalIsFinite": "off", 15 | "noImplicitAnyLet": "off" 16 | }, 17 | "style": { 18 | "noNonNullAssertion": "off", 19 | "noParameterAssign": "off", 20 | "noUnusedTemplateLiteral": "off", 21 | "noUselessElse": "off", 22 | "useEnumInitializers": "off", 23 | "useExponentiationOperator": "off", 24 | "useImportType": "off", 25 | "useNodejsImportProtocol": "off", 26 | "useNumberNamespace": "off", 27 | "useSingleVarDeclarator": "off", 28 | "useTemplate": "off" 29 | }, 30 | "complexity": { 31 | "noForEach": "off", 32 | "noStaticOnlyClass": "off", 33 | "noThisInStatic": "off", 34 | "noUselessSwitchCase": "off", 35 | "useArrowFunction": "off", 36 | "useLiteralKeys": "off", 37 | "useOptionalChain": "off" 38 | }, 39 | "performance": { 40 | "noDelete": "off" 41 | } 42 | } 43 | }, 44 | "organizeImports": { 45 | "enabled": false 46 | }, 47 | "javascript": { 48 | "formatter": { 49 | "quoteStyle": "single" 50 | } 51 | }, 52 | "json": { 53 | "formatter": { 54 | "enabled": false 55 | } 56 | }, 57 | "files": { 58 | "ignore": ["coverage/**", "packages/*/dist/**", "**/build", "**/.svelte-kit"] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.1.4", 3 | "npmClient": "yarn", 4 | "packages": [ 5 | "packages/*" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/cli/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/cli/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program, programReady } from '../dist/cli.esm.js'; 4 | 5 | program.disableGlobalOption('--silent'); 6 | programReady.then(() => program.run()); 7 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/cli", 3 | "version": "4.1.4", 4 | "repository": "github:donmccurdy/glTF-Transform", 5 | "homepage": "https://gltf-transform.dev/cli.html", 6 | "description": "CLI interface to glTF Transform", 7 | "author": "Don McCurdy ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/donmccurdy", 10 | "type": "module", 11 | "source": "./src/cli.ts", 12 | "types": "./dist/cli.d.ts", 13 | "exports": { 14 | "types": "./dist/cli.d.ts", 15 | "default": "./dist/cli.esm.js" 16 | }, 17 | "browserslist": [ 18 | "node >= 18" 19 | ], 20 | "engines": { 21 | "node": ">=18" 22 | }, 23 | "scripts": { 24 | "build": "microbundle --format esm --target node --no-compress", 25 | "build:watch": "microbundle watch --format esm --target node --no-compress" 26 | }, 27 | "bin": { 28 | "gltf-transform": "./bin/cli.js" 29 | }, 30 | "dependencies": { 31 | "@donmccurdy/caporal": "~0.0.10", 32 | "@gltf-transform/core": "^4.1.4", 33 | "@gltf-transform/extensions": "^4.1.4", 34 | "@gltf-transform/functions": "^4.1.4", 35 | "@types/language-tags": "~1.0.4", 36 | "@types/micromatch": "~4.0.9", 37 | "@types/node-fetch": "~2.6.12", 38 | "@types/prompts": "^2.4.9", 39 | "@types/spdx-correct": "~3.1.3", 40 | "@types/tmp": "~0.2.6", 41 | "cli-table3": "~0.6.5", 42 | "command-exists": "~1.2.9", 43 | "csv-stringify": "~6.5.2", 44 | "draco3dgltf": "~1.5.7", 45 | "gltf-validator": "~2.0.0-dev.3.10", 46 | "keyframe-resample": "~0.1.0", 47 | "ktx-parse": "^1.0.0", 48 | "language-tags": "<2.0.0", 49 | "listr2": "~8.2.5", 50 | "meshoptimizer": "~0.22.0", 51 | "micromatch": "~4.0.8", 52 | "mikktspace": "~1.1.1", 53 | "node-fetch": "~3.3.2", 54 | "node-gzip": "~1.1.2", 55 | "p-limit": "~6.2.0", 56 | "prompts": "^2.4.2", 57 | "sharp": "~0.33.5", 58 | "spdx-correct": "~3.2.0", 59 | "strip-ansi": "~7.1.0", 60 | "tmp": "~0.2.3" 61 | }, 62 | "files": [ 63 | "bin/", 64 | "dist/", 65 | "src/", 66 | "README.md", 67 | "LICENSE.md", 68 | "package-lock.json" 69 | ], 70 | "gitHead": "895b70777fda68aa321358d17dab2105e8564d08" 71 | } 72 | -------------------------------------------------------------------------------- /packages/cli/src/transforms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ktxdecompress.js'; 2 | export * from './ktxfix.js'; 3 | export * from './merge.js'; 4 | export * from './toktx.js'; 5 | export * from './xmp.js'; 6 | -------------------------------------------------------------------------------- /packages/cli/src/transforms/ktxfix.ts: -------------------------------------------------------------------------------- 1 | import { KHR_DF_PRIMARIES_BT709, KHR_DF_PRIMARIES_UNSPECIFIED, read, write } from 'ktx-parse'; 2 | import type { Document, Transform } from '@gltf-transform/core'; 3 | import { getTextureColorSpace, listTextureSlots } from '@gltf-transform/functions'; 4 | 5 | const NAME = 'ktxfix'; 6 | 7 | export function ktxfix(): Transform { 8 | return async (doc: Document): Promise => { 9 | const logger = doc.getLogger(); 10 | 11 | let numChanged = 0; 12 | 13 | for (const texture of doc.getRoot().listTextures()) { 14 | if (texture.getMimeType() !== 'image/ktx2') continue; 15 | 16 | const image = texture.getImage(); 17 | if (!image) continue; 18 | 19 | const ktx = read(image); 20 | const dfd = ktx.dataFormatDescriptor[0]; 21 | const slots = listTextureSlots(texture); 22 | 23 | // Don't make changes if we have no information. 24 | if (slots.length === 0) continue; 25 | 26 | const colorSpace = getTextureColorSpace(texture); 27 | const colorPrimaries = colorSpace === 'srgb' ? KHR_DF_PRIMARIES_BT709 : KHR_DF_PRIMARIES_UNSPECIFIED; 28 | const name = texture.getURI() || texture.getName(); 29 | 30 | let changed = false; 31 | 32 | // See: https://github.com/donmccurdy/glTF-Transform/issues/218 33 | if (dfd.colorPrimaries !== colorPrimaries) { 34 | dfd.colorPrimaries = colorPrimaries; 35 | logger.info(`${NAME}: Set colorPrimaries=${colorPrimaries} for texture "${name}"`); 36 | changed = true; 37 | } 38 | 39 | if (changed) { 40 | texture.setImage(write(ktx)); 41 | numChanged++; 42 | } 43 | } 44 | 45 | logger.info(`${NAME}: Found and repaired issues in ${numChanged} textures`); 46 | logger.debug(`${NAME}: Complete.`); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // See: https://github.com/KhronosGroup/glTF-Validator/issues/114 2 | declare module 'gltf-validator'; 3 | -------------------------------------------------------------------------------- /packages/cli/test/in/ACMEBox.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/cli/test/in/ACMEBox.bin -------------------------------------------------------------------------------- /packages/cli/test/in/acme-gltf-transform.config.js: -------------------------------------------------------------------------------- 1 | import { Extension } from '@gltf-transform/core'; 2 | 3 | class GizmoExtension extends Extension { 4 | static EXTENSION_NAME = 'ACME_gizmo'; 5 | extensionName = 'ACME_gizmo'; 6 | write(_context) { 7 | return this; 8 | } 9 | read(_context) { 10 | return this; 11 | } 12 | } 13 | 14 | export default { extensions: [GizmoExtension] }; 15 | -------------------------------------------------------------------------------- /packages/cli/test/in/chr_knight.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/cli/test/in/chr_knight.glb -------------------------------------------------------------------------------- /packages/cli/test/in/test.ktx2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/cli/test/in/test.ktx2 -------------------------------------------------------------------------------- /packages/cli/test/ktxfix.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path, { dirname } from 'path'; 3 | import { KHR_DF_PRIMARIES_BT709, KHR_DF_PRIMARIES_UNSPECIFIED, read } from 'ktx-parse'; 4 | import test from 'ava'; 5 | import { Document, Texture } from '@gltf-transform/core'; 6 | import { ktxfix } from '@gltf-transform/cli'; 7 | import { logger } from '@gltf-transform/test-utils'; 8 | import { fileURLToPath } from 'url'; 9 | 10 | const __dirname = dirname(fileURLToPath(import.meta.url)); 11 | 12 | test('repair', async (t) => { 13 | const document = new Document().setLogger(logger); 14 | const material = document.createMaterial(); 15 | const texture = document 16 | .createTexture() 17 | .setMimeType('image/ktx2') 18 | .setImage(fs.readFileSync(path.join(__dirname, 'in', 'test.ktx2'))); 19 | 20 | t.is(getColorPrimaries(texture), KHR_DF_PRIMARIES_BT709, 'initial - sRGB'); 21 | 22 | await document.transform(ktxfix()); 23 | 24 | t.is(getColorPrimaries(texture), KHR_DF_PRIMARIES_BT709, 'unused - no change'); 25 | 26 | material.setOcclusionTexture(texture); 27 | await document.transform(ktxfix()); 28 | 29 | t.is(getColorPrimaries(texture), KHR_DF_PRIMARIES_UNSPECIFIED, 'occlusion - unspecified'); 30 | 31 | texture.detach(); 32 | await document.transform(ktxfix()); 33 | 34 | t.is(getColorPrimaries(texture), KHR_DF_PRIMARIES_UNSPECIFIED, 'unused - no change'); 35 | 36 | material.setBaseColorTexture(texture); 37 | await document.transform(ktxfix()); 38 | 39 | t.is(getColorPrimaries(texture), KHR_DF_PRIMARIES_BT709, 'base color - sRGB'); 40 | }); 41 | 42 | function getColorPrimaries(texture: Texture): number { 43 | const image = texture.getImage()!; 44 | const ktx = read(image); 45 | return ktx.dataFormatDescriptor[0].colorPrimaries; 46 | } 47 | -------------------------------------------------------------------------------- /packages/cli/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "strict": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/test/util.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { formatBytes, formatHeader, formatParagraph } from '@gltf-transform/cli'; 3 | 4 | const HEADER = ` 5 | HELLO 6 | ────────────────────────────────────────────`; 7 | 8 | const TEXT = 9 | 'Chupa chups biscuit ice cream wafer. Chocolate bar lollipop marshmallow powder. Sesame snaps sweet roll icing macaroon croissant jujubes pastry apple pie chocolate cake. Liquorice jelly-o pie jujubes fruitcake chocolate bar jelly-o tart. Marshmallow icing tart tootsie roll brownie dragée.'; 10 | 11 | const PARAGRAPH = ` 12 | Chupa chups biscuit ice cream wafer. Chocolate bar lollipop marshmallow powder. 13 | Sesame snaps sweet roll icing macaroon croissant jujubes pastry apple pie 14 | chocolate cake. Liquorice jelly-o pie jujubes fruitcake chocolate bar jelly-o 15 | tart. Marshmallow icing tart tootsie roll brownie dragée.`.trim(); 16 | 17 | test('formatBytes', (t) => { 18 | t.is(formatBytes(1000), '1 KB', 'formatBytes'); 19 | }); 20 | 21 | test('formatHeader', (t) => { 22 | t.is(formatHeader('Hello'), HEADER, 'formatHeader'); 23 | }); 24 | 25 | test('formatParagraph', (t) => { 26 | t.is(formatParagraph(TEXT), PARAGRAPH, 'formatParagraph'); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "strict": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @gltf-transform/core 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/@gltf-transform/core.svg)](https://www.npmjs.com/package/@gltf-transform/core) 4 | [![Minzipped size](https://badgen.net/bundlephobia/minzip/@gltf-transform/core)](https://bundlephobia.com/result?p=@gltf-transform/core) 5 | [![License](https://img.shields.io/npm/l/@gltf-transform/core.svg)](https://github.com/donmccurdy/glTF-Transform/blob/main/LICENSE.md) 6 | 7 | Part of the glTF Transform project. 8 | 9 | - GitHub: https://github.com/donmccurdy/glTF-Transform 10 | - Documentation: https://gltf-transform.dev/ 11 | 12 | ## Credits 13 | 14 | See [*Credits*](https://gltf-transform.dev/credits). 15 | 16 |

Commercial Use

17 | 18 |

19 | Using glTF Transform for a personal project? That's great! Sponsorship is neither expected nor required. Feel 20 | free to share screenshots if you've made something you're excited about — I enjoy seeing those! 21 |

22 | 23 |

24 | Using glTF Transform in for-profit work? That's wonderful! Your support is important to keep glTF Transform 25 | maintained, independent, and open source under MIT License. Please consider a 26 | subscription 27 | or 28 | GitHub sponsorship. 29 |

30 | 31 |

32 | 33 | Learn more in the 34 | glTF Transform Pro FAQs. 36 |

37 | 38 | ## License 39 | 40 | Copyright 2023, MIT License. 41 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/core", 3 | "version": "4.1.4", 4 | "repository": "github:donmccurdy/glTF-Transform", 5 | "homepage": "https://gltf-transform.dev/", 6 | "description": "glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.", 7 | "author": "Don McCurdy ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/donmccurdy", 10 | "type": "module", 11 | "sideEffects": false, 12 | "exports": { 13 | "types": "./dist/index.d.ts", 14 | "require": "./dist/index.cjs", 15 | "default": "./dist/index.modern.js" 16 | }, 17 | "types": "./dist/index.d.ts", 18 | "main": "./dist/index.cjs", 19 | "module": "./dist/index.modern.js", 20 | "source": "./src/index.ts", 21 | "browserslist": [ 22 | "defaults", 23 | "not IE 11", 24 | "node >= 14" 25 | ], 26 | "scripts": { 27 | "build": "microbundle --format modern,cjs --no-compress --define PACKAGE_VERSION=$npm_package_version", 28 | "build:watch": "microbundle watch --format modern,cjs --no-compress --define PACKAGE_VERSION=$npm_package_version" 29 | }, 30 | "keywords": [ 31 | "gltf", 32 | "3d", 33 | "model", 34 | "webgl", 35 | "threejs" 36 | ], 37 | "files": [ 38 | "dist/", 39 | "src/", 40 | "README.md", 41 | "LICENSE.md", 42 | "package-lock.json" 43 | ], 44 | "browser": { 45 | "fs": false, 46 | "path": false 47 | }, 48 | "dependencies": { 49 | "property-graph": "^3.0.0" 50 | }, 51 | "mangle": { 52 | "regex": "^_" 53 | }, 54 | "gitHead": "895b70777fda68aa321358d17dab2105e8564d08" 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Document, Transform, TransformContext } from './document.js'; 2 | export { JSONDocument } from './json-document.js'; 3 | export { Extension } from './extension.js'; 4 | export { 5 | Accessor, 6 | Animation, 7 | AnimationChannel, 8 | AnimationSampler, 9 | Buffer, 10 | Camera, 11 | ExtensionProperty, 12 | ExtensibleProperty, 13 | Property, 14 | IProperty, 15 | Material, 16 | Mesh, 17 | Node, 18 | Primitive, 19 | PrimitiveTarget, 20 | Root, 21 | Scene, 22 | Skin, 23 | Texture, 24 | TextureInfo, 25 | PropertyResolver, 26 | COPY_IDENTITY, 27 | } from './properties/index.js'; 28 | export { Graph, GraphEdge, Ref, RefList, RefSet, RefMap } from 'property-graph'; 29 | export { DenoIO, PlatformIO, NodeIO, WebIO, ReaderContext, WriterContext } from './io/index.js'; 30 | export { 31 | BufferUtils, 32 | HTTPUtils, 33 | ColorUtils, 34 | FileUtils, 35 | ImageUtils, 36 | ImageUtilsFormat, 37 | ILogger, 38 | Logger, 39 | MathUtils, 40 | Verbosity, 41 | getBounds, 42 | uuid, 43 | } from './utils/index.js'; 44 | export { 45 | TypedArray, 46 | TypedArrayConstructor, 47 | ComponentTypeToTypedArray, 48 | PropertyType, 49 | Format, 50 | Nullable, 51 | TextureChannel, 52 | VertexLayout, 53 | vec2, 54 | vec3, 55 | vec4, 56 | mat3, 57 | mat4, 58 | bbox, 59 | GLB_BUFFER, 60 | VERSION, 61 | } from './constants.js'; 62 | export { GLTF } from './types/gltf.js'; 63 | -------------------------------------------------------------------------------- /packages/core/src/io/deno-io.ts: -------------------------------------------------------------------------------- 1 | import { PlatformIO } from './platform-io.js'; 2 | 3 | interface Path { 4 | resolve(base: string, path: string): string; 5 | dirname(uri: string): string; 6 | } 7 | 8 | /** 9 | * *I/O service for [Deno](https://deno.land/).* 10 | * 11 | * The most common use of the I/O service is to read/write a {@link Document} with a given path. 12 | * Methods are also available for converting in-memory representations of raw glTF files, both 13 | * binary (*Uint8Array*) and JSON ({@link JSONDocument}). 14 | * 15 | * _*NOTICE:* Support for the Deno environment is currently experimental. See 16 | * [glTF-Transform#457](https://github.com/donmccurdy/glTF-Transform/issues/457)._ 17 | * 18 | * Usage: 19 | * 20 | * ```typescript 21 | * import { DenoIO } from 'https://esm.sh/@gltf-transform/core'; 22 | * import * as path from 'https://deno.land/std/path/mod.ts'; 23 | * 24 | * const io = new DenoIO(path); 25 | * 26 | * // Read. 27 | * let document; 28 | * document = io.read('model.glb'); // → Document 29 | * document = io.readBinary(glb); // Uint8Array → Document 30 | * 31 | * // Write. 32 | * const glb = io.writeBinary(document); // Document → Uint8Array 33 | * ``` 34 | * 35 | * @category I/O 36 | */ 37 | export class DenoIO extends PlatformIO { 38 | private _path: Path; 39 | 40 | constructor(path: unknown) { 41 | super(); 42 | this._path = path as Path; 43 | } 44 | 45 | protected async readURI(uri: string, type: 'view'): Promise; 46 | protected async readURI(uri: string, type: 'text'): Promise; 47 | protected async readURI(uri: string, type: 'view' | 'text'): Promise { 48 | switch (type) { 49 | case 'view': 50 | return Deno.readFile(uri); 51 | case 'text': 52 | return Deno.readTextFile(uri); 53 | } 54 | } 55 | 56 | protected resolve(base: string, path: string): string { 57 | // https://github.com/KhronosGroup/glTF/issues/1449 58 | // https://stackoverflow.com/a/27278490/1314762 59 | return this._path.resolve(base, decodeURIComponent(path)); 60 | } 61 | 62 | protected dirname(uri: string): string { 63 | return this._path.dirname(uri); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/io/index.ts: -------------------------------------------------------------------------------- 1 | export { NodeIO } from './node-io.js'; 2 | export { DenoIO } from './deno-io.js'; 3 | export { PlatformIO } from './platform-io.js'; 4 | export { WebIO } from './web-io.js'; 5 | export { ReaderOptions } from './reader.js'; 6 | export { WriterOptions } from './writer.js'; 7 | export { ReaderContext } from './reader-context.js'; 8 | export { WriterContext } from './writer-context.js'; 9 | -------------------------------------------------------------------------------- /packages/core/src/io/web-io.ts: -------------------------------------------------------------------------------- 1 | import { PlatformIO } from './platform-io.js'; 2 | import { HTTPUtils } from '../utils/index.js'; 3 | 4 | /** 5 | * *I/O service for Web.* 6 | * 7 | * The most common use of the I/O service is to read/write a {@link Document} with a given path. 8 | * Methods are also available for converting in-memory representations of raw glTF files, both 9 | * binary (*Uint8Array*) and JSON ({@link JSONDocument}). 10 | * 11 | * Usage: 12 | * 13 | * ```typescript 14 | * import { WebIO } from '@gltf-transform/core'; 15 | * 16 | * const io = new WebIO({credentials: 'include'}); 17 | * 18 | * // Read. 19 | * let document; 20 | * document = await io.read('model.glb'); // → Document 21 | * document = await io.readBinary(glb); // Uint8Array → Document 22 | * 23 | * // Write. 24 | * const glb = await io.writeBinary(document); // Document → Uint8Array 25 | * ``` 26 | * 27 | * @category I/O 28 | */ 29 | export class WebIO extends PlatformIO { 30 | private readonly _fetchConfig: RequestInit; 31 | 32 | /** 33 | * Constructs a new WebIO service. Instances are reusable. 34 | * @param fetchConfig Configuration object for Fetch API. 35 | */ 36 | constructor(fetchConfig = HTTPUtils.DEFAULT_INIT) { 37 | super(); 38 | this._fetchConfig = fetchConfig; 39 | } 40 | 41 | protected async readURI(uri: string, type: 'view'): Promise; 42 | protected async readURI(uri: string, type: 'text'): Promise; 43 | protected async readURI(uri: string, type: 'view' | 'text'): Promise { 44 | const response = await fetch(uri, this._fetchConfig); 45 | switch (type) { 46 | case 'view': 47 | return new Uint8Array(await response.arrayBuffer()); 48 | case 'text': 49 | return response.text(); 50 | } 51 | } 52 | 53 | protected resolve(base: string, path: string): string { 54 | return HTTPUtils.resolve(base, path); 55 | } 56 | 57 | protected dirname(uri: string): string { 58 | return HTTPUtils.dirname(uri); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/core/src/json-document.ts: -------------------------------------------------------------------------------- 1 | import type { GLTF } from './types/gltf.js'; 2 | 3 | /** 4 | * *Raw glTF asset, with its JSON and binary resources.* 5 | * 6 | * A JSONDocument is a plain object containing the raw JSON of a glTF file, and any binary or image 7 | * resources referenced by that file. When modifying the file, it should generally be first 8 | * converted to the more useful {@link Document} wrapper. 9 | * 10 | * When loading glTF data that is in memory, or which the I/O utilities cannot otherwise access, 11 | * you might assemble the JSONDocument yourself, then convert it to a Document with 12 | * {@link PlatformIO.readJSON}(jsonDocument). 13 | * 14 | * Usage: 15 | * 16 | * ```ts 17 | * import fs from 'fs/promises'; 18 | * 19 | * const jsonDocument = { 20 | * // glTF JSON schema. 21 | * json: { 22 | * asset: {version: '2.0'}, 23 | * images: [{uri: 'image1.png'}, {uri: 'image2.png'}] 24 | * }, 25 | * 26 | * // URI → Uint8Array mapping. 27 | * resources: { 28 | * 'image1.png': await fs.readFile('image1.png'), 29 | * 'image2.png': await fs.readFile('image2.png'), 30 | * } 31 | * }; 32 | * 33 | * const document = await new NodeIO().readJSON(jsonDocument); 34 | * ``` 35 | * 36 | * @category Documents 37 | */ 38 | export interface JSONDocument { 39 | json: GLTF.IGLTF; 40 | resources: { [s: string]: Uint8Array }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/properties/extensible-property.ts: -------------------------------------------------------------------------------- 1 | import { RefMap } from 'property-graph'; 2 | import type { Nullable } from '../constants.js'; 3 | import type { ExtensionProperty } from './extension-property.js'; 4 | import { Property, IProperty } from './property.js'; 5 | 6 | export interface IExtensibleProperty extends IProperty { 7 | extensions: RefMap; 8 | } 9 | 10 | /** 11 | * *A {@link Property} that can have {@link ExtensionProperty} instances attached.* 12 | * 13 | * Most properties are extensible. See the {@link Extension} documentation for information about 14 | * how to use extensions. 15 | * 16 | * @category Properties 17 | */ 18 | export abstract class ExtensibleProperty extends Property { 19 | protected getDefaults(): Nullable { 20 | return Object.assign(super.getDefaults(), { extensions: new RefMap() }); 21 | } 22 | 23 | /** Returns an {@link ExtensionProperty} attached to this Property, if any. */ 24 | public getExtension(name: string): Prop | null { 25 | return (this as ExtensibleProperty).getRefMap('extensions', name) as Prop; 26 | } 27 | 28 | /** 29 | * Attaches the given {@link ExtensionProperty} to this Property. For a given extension, only 30 | * one ExtensionProperty may be attached to any one Property at a time. 31 | */ 32 | public setExtension(name: string, extensionProperty: Prop | null): this { 33 | if (extensionProperty) extensionProperty._validateParent(this as ExtensibleProperty); 34 | return (this as ExtensibleProperty).setRefMap('extensions', name, extensionProperty) as this; 35 | } 36 | 37 | /** Lists all {@link ExtensionProperty} instances attached to this Property. */ 38 | public listExtensions(): ExtensionProperty[] { 39 | return (this as ExtensibleProperty).listRefMapValues('extensions'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/properties/extension-property.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensibleProperty } from './extensible-property.js'; 2 | import { Property, IProperty } from './property.js'; 3 | 4 | /** 5 | * *Base class for all {@link Property} types that can be attached by an {@link Extension}.* 6 | * 7 | * After an {@link Extension} is attached to a glTF {@link Document}, the Extension may be used to 8 | * construct ExtensionProperty instances, to be referenced throughout the document as prescribed by 9 | * the Extension. For example, the `KHR_materials_clearcoat` Extension defines a `Clearcoat` 10 | * ExtensionProperty, which is referenced by {@link Material} Properties in the Document, and may 11 | * contain references to {@link Texture} properties of its own. 12 | * 13 | * For more information on available extensions and their usage, see [Extensions](/extensions). 14 | * 15 | * Reference: 16 | * - [glTF → Extensions](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#specifying-extensions) 17 | * 18 | * @category Properties 19 | */ 20 | export abstract class ExtensionProperty extends Property { 21 | public static EXTENSION_NAME: string; 22 | public abstract readonly extensionName: string; 23 | 24 | /** List of supported {@link Property} types. */ 25 | public abstract readonly parentTypes: string[]; 26 | 27 | /** @hidden */ 28 | public _validateParent(parent: ExtensibleProperty): void { 29 | if (!this.parentTypes.includes(parent.propertyType)) { 30 | throw new Error(`Parent "${parent.propertyType}" invalid for child "${this.propertyType}".`); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/properties/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accessor.js'; 2 | export * from './animation.js'; 3 | export * from './animation-channel.js'; 4 | export * from './animation-sampler.js'; 5 | export * from './buffer.js'; 6 | export * from './camera.js'; 7 | export * from './extension-property.js'; 8 | export * from './extensible-property.js'; 9 | export * from './property.js'; 10 | export * from './material.js'; 11 | export * from './mesh.js'; 12 | export * from './node.js'; 13 | export * from './primitive.js'; 14 | export * from './primitive-target.js'; 15 | export * from './root.js'; 16 | export * from './scene.js'; 17 | export * from './skin.js'; 18 | export * from './texture.js'; 19 | export * from './texture-info.js'; 20 | -------------------------------------------------------------------------------- /packages/core/src/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | import { ImageUtils } from './image-utils.js'; 2 | 3 | /** 4 | * *Utility class for working with file systems and URI paths.* 5 | * 6 | * @category Utilities 7 | */ 8 | export class FileUtils { 9 | /** 10 | * Extracts the basename from a file path, e.g. "folder/model.glb" -> "model". 11 | * See: {@link HTTPUtils.basename} 12 | */ 13 | static basename(uri: string): string { 14 | const fileName = uri.split(/[\\/]/).pop()!; 15 | return fileName.substring(0, fileName.lastIndexOf('.')); 16 | } 17 | 18 | /** 19 | * Extracts the extension from a file path, e.g. "folder/model.glb" -> "glb". 20 | * See: {@link HTTPUtils.extension} 21 | */ 22 | static extension(uri: string): string { 23 | if (uri.startsWith('data:image/')) { 24 | const mimeType = uri.match(/data:(image\/\w+)/)![1]; 25 | return ImageUtils.mimeTypeToExtension(mimeType); 26 | } else if (uri.startsWith('data:model/gltf+json')) { 27 | return 'gltf'; 28 | } else if (uri.startsWith('data:model/gltf-binary')) { 29 | return 'glb'; 30 | } else if (uri.startsWith('data:application/')) { 31 | return 'bin'; 32 | } 33 | return uri.split(/[\\/]/).pop()!.split(/[.]/).pop()!; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/utils/http-utils.ts: -------------------------------------------------------------------------------- 1 | import { FileUtils } from './file-utils.js'; 2 | 3 | // Need a placeholder domain to construct a URL from a relative path. We only 4 | // access `url.pathname`, so the domain doesn't matter. 5 | const NULL_DOMAIN = 'https://null.example'; 6 | 7 | /** 8 | * *Utility class for working with URLs.* 9 | * 10 | * @category Utilities 11 | */ 12 | export class HTTPUtils { 13 | static readonly DEFAULT_INIT: RequestInit = {}; 14 | static readonly PROTOCOL_REGEXP = /^[a-zA-Z]+:\/\//; 15 | 16 | static dirname(path: string): string { 17 | const index = path.lastIndexOf('/'); 18 | if (index === -1) return './'; 19 | return path.substring(0, index + 1); 20 | } 21 | 22 | /** 23 | * Extracts the basename from a URL, e.g. "folder/model.glb" -> "model". 24 | * See: {@link FileUtils.basename} 25 | */ 26 | static basename(uri: string): string { 27 | return FileUtils.basename(new URL(uri, NULL_DOMAIN).pathname); 28 | } 29 | 30 | /** 31 | * Extracts the extension from a URL, e.g. "folder/model.glb" -> "glb". 32 | * See: {@link FileUtils.extension} 33 | */ 34 | static extension(uri: string): string { 35 | return FileUtils.extension(new URL(uri, NULL_DOMAIN).pathname); 36 | } 37 | 38 | static resolve(base: string, path: string) { 39 | if (!this.isRelativePath(path)) return path; 40 | 41 | const stack = base.split('/'); 42 | const parts = path.split('/'); 43 | stack.pop(); 44 | for (let i = 0; i < parts.length; i++) { 45 | if (parts[i] === '.') continue; 46 | if (parts[i] === '..') { 47 | stack.pop(); 48 | } else { 49 | stack.push(parts[i]); 50 | } 51 | } 52 | return stack.join('/'); 53 | } 54 | 55 | /** 56 | * Returns true for URLs containing a protocol, and false for both 57 | * absolute and relative paths. 58 | */ 59 | static isAbsoluteURL(path: string) { 60 | return this.PROTOCOL_REGEXP.test(path); 61 | } 62 | 63 | /** 64 | * Returns true for paths that are declared relative to some unknown base 65 | * path. For example, "foo/bar/" is relative both "/foo/bar/" is not. 66 | */ 67 | static isRelativePath(path: string): boolean { 68 | return !/^(?:[a-zA-Z]+:)?\//.test(path); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-bounds.js'; 2 | export * from './buffer-utils.js'; 3 | export * from './color-utils.js'; 4 | export * from './file-utils.js'; 5 | export * from './image-utils.js'; 6 | export * from './is-plain-object.js'; 7 | export * from './logger.js'; 8 | export * from './math-utils.js'; 9 | export * from './property-utils.js'; 10 | export * from './uuid.js'; 11 | export * from './http-utils.js'; 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/is-plain-object.ts: -------------------------------------------------------------------------------- 1 | // Reference: https://github.com/jonschlinkert/is-plain-object 2 | 3 | function isObject(o: unknown): o is object { 4 | return Object.prototype.toString.call(o) === '[object Object]'; 5 | } 6 | 7 | export function isPlainObject(o: unknown): o is object { 8 | if (isObject(o) === false) return false; 9 | 10 | // If has modified constructor 11 | const ctor = o.constructor; 12 | if (ctor === undefined) return true; 13 | 14 | // If has modified prototype 15 | const prot = ctor.prototype; 16 | if (isObject(prot) === false) return false; 17 | 18 | // If constructor does not have an Object-specific method 19 | if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) { 20 | return false; 21 | } 22 | 23 | // Most likely a plain Object 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** Logger verbosity thresholds. */ 2 | export enum Verbosity { 3 | /** No events are logged. */ 4 | SILENT = 4, 5 | 6 | /** Only error events are logged. */ 7 | ERROR = 3, 8 | 9 | /** Only error and warn events are logged. */ 10 | WARN = 2, 11 | 12 | /** Only error, warn, and info events are logged. (DEFAULT) */ 13 | INFO = 1, 14 | 15 | /** All events are logged. */ 16 | DEBUG = 0, 17 | } 18 | 19 | export interface ILogger { 20 | debug(text: string): void; 21 | info(text: string): void; 22 | warn(text: string): void; 23 | error(text: string): void; 24 | } 25 | 26 | /** 27 | * *Logger utility class.* 28 | * 29 | * @category Utilities 30 | */ 31 | export class Logger implements ILogger { 32 | /** Logger verbosity thresholds. */ 33 | static Verbosity = Verbosity; 34 | 35 | /** Default logger instance. */ 36 | public static DEFAULT_INSTANCE = new Logger(Logger.Verbosity.INFO); 37 | 38 | /** Constructs a new Logger instance. */ 39 | constructor(private readonly verbosity: number) {} 40 | 41 | /** Logs an event at level {@link Logger.Verbosity.DEBUG}. */ 42 | debug(text: string): void { 43 | if (this.verbosity <= Logger.Verbosity.DEBUG) { 44 | console.debug(text); 45 | } 46 | } 47 | 48 | /** Logs an event at level {@link Logger.Verbosity.INFO}. */ 49 | info(text: string): void { 50 | if (this.verbosity <= Logger.Verbosity.INFO) { 51 | console.info(text); 52 | } 53 | } 54 | 55 | /** Logs an event at level {@link Logger.Verbosity.WARN}. */ 56 | warn(text: string): void { 57 | if (this.verbosity <= Logger.Verbosity.WARN) { 58 | console.warn(text); 59 | } 60 | } 61 | 62 | /** Logs an event at level {@link Logger.Verbosity.ERROR}. */ 63 | error(text: string): void { 64 | if (this.verbosity <= Logger.Verbosity.ERROR) { 65 | console.error(text); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/core/src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | const ALPHABET = '23456789abdegjkmnpqrvwxyzABDEGJKMNPQRVWXYZ'; 2 | const UNIQUE_RETRIES = 999; 3 | const ID_LENGTH = 6; 4 | 5 | const previousIDs = new Set(); 6 | 7 | const generateOne = function (): string { 8 | let rtn = ''; 9 | for (let i = 0; i < ID_LENGTH; i++) { 10 | rtn += ALPHABET.charAt(Math.floor(Math.random() * ALPHABET.length)); 11 | } 12 | return rtn; 13 | }; 14 | 15 | /** 16 | * Short ID generator. 17 | * 18 | * Generated IDs are short, easy to type, and unique for the duration of the program's execution. 19 | * Uniqueness across multiple program executions, or on other devices, is not guaranteed. Based on 20 | * [Short ID Generation in JavaScript](https://tomspencer.dev/blog/2014/11/16/short-id-generation-in-javascript/), 21 | * with alterations. 22 | * 23 | * @category Utilities 24 | * @hidden 25 | */ 26 | export const uuid = function (): string { 27 | for (let retries = 0; retries < UNIQUE_RETRIES; retries++) { 28 | const id = generateOne(); 29 | if (!previousIDs.has(id)) { 30 | previousIDs.add(id); 31 | return id; 32 | } 33 | } 34 | return ''; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/test/document.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | 4 | test('transform', async (t) => { 5 | const document = new Document(); 6 | 7 | await document.transform( 8 | (c) => c.createTexture(''), 9 | (c) => c.createBuffer(''), 10 | ); 11 | 12 | t.is(document.getRoot().listTextures().length, 1, 'transform 1'); 13 | t.is(document.getRoot().listBuffers().length, 1, 'transform 2'); 14 | }); 15 | 16 | test('defaults', (t) => { 17 | // offering to the code coverage gods. 18 | const document = new Document(); 19 | 20 | document.createAccessor('test'); 21 | document.createAnimation('test'); 22 | document.createAnimationChannel('test'); 23 | document.createAnimationSampler('test'); 24 | document.createBuffer('test'); 25 | document.createCamera('test'); 26 | document.createMesh('test'); 27 | document.createNode('test'); 28 | document.createPrimitive(); 29 | document.createPrimitiveTarget('test'); 30 | document.createScene('test'); 31 | document.createSkin('test'); 32 | 33 | t.truthy(true); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/core/test/in/BoxTextured_glTF-pbrSpecularGlossiness/BoxTextured0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/BoxTextured_glTF-pbrSpecularGlossiness/BoxTextured0.bin -------------------------------------------------------------------------------- /packages/core/test/in/BoxTextured_glTF-pbrSpecularGlossiness/CesiumLogoFlat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/BoxTextured_glTF-pbrSpecularGlossiness/CesiumLogoFlat.png -------------------------------------------------------------------------------- /packages/core/test/in/BoxTextured_glTF/BoxTextured0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/BoxTextured_glTF/BoxTextured0.bin -------------------------------------------------------------------------------- /packages/core/test/in/BoxTextured_glTF/CesiumLogoFlat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/BoxTextured_glTF/CesiumLogoFlat.png -------------------------------------------------------------------------------- /packages/core/test/in/BoxVertexColors.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/BoxVertexColors.glb -------------------------------------------------------------------------------- /packages/core/test/in/Box_glTF-Binary/Box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/Box_glTF-Binary/Box.glb -------------------------------------------------------------------------------- /packages/core/test/in/Box_glTF/Box0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/Box_glTF/Box0.bin -------------------------------------------------------------------------------- /packages/core/test/in/EncodingTest/Unicode ❤♻ Binary.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/EncodingTest/Unicode ❤♻ Binary.bin -------------------------------------------------------------------------------- /packages/core/test/in/EncodingTest/Unicode ❤♻ Texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/EncodingTest/Unicode ❤♻ Texture.png -------------------------------------------------------------------------------- /packages/core/test/in/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/test.jpg -------------------------------------------------------------------------------- /packages/core/test/in/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/in/test.png -------------------------------------------------------------------------------- /packages/core/test/out/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/core/test/out/.gitkeep -------------------------------------------------------------------------------- /packages/core/test/properties/buffer.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { createPlatformIO } from '@gltf-transform/test-utils'; 4 | 5 | test('basic', async (t) => { 6 | const doc = new Document(); 7 | const buffer1 = doc.createBuffer().setURI('mybuffer.bin'); 8 | const buffer2 = doc.createBuffer().setURI(''); 9 | const buffer3 = doc.createBuffer(); 10 | doc.createBuffer().setURI('empty.bin'); 11 | 12 | // Empty buffers aren't written. 13 | doc.createAccessor() 14 | .setArray(new Uint8Array([1, 2, 3])) 15 | .setBuffer(buffer1); 16 | doc.createAccessor() 17 | .setArray(new Uint8Array([1, 2, 3])) 18 | .setBuffer(buffer2); 19 | doc.createAccessor() 20 | .setArray(new Uint8Array([1, 2, 3])) 21 | .setBuffer(buffer3); 22 | 23 | const io = await createPlatformIO(); 24 | const jsonDoc = await io.writeJSON(doc, { basename: 'basename' }); 25 | 26 | t.true('mybuffer.bin' in jsonDoc.resources, 'explicitly named buffer'); 27 | t.true('basename_1.bin' in jsonDoc.resources, 'implicitly named buffer #1'); 28 | t.true('basename_2.bin' in jsonDoc.resources, 'implicitly named buffer #2'); 29 | t.false('empty.bin' in jsonDoc.resources, 'empty buffer skipped'); 30 | }); 31 | 32 | test('copy', (t) => { 33 | const document = new Document(); 34 | const buffer1 = document.createBuffer('MyBuffer').setURI('mybuffer.bin'); 35 | const buffer2 = document.createBuffer().copy(buffer1); 36 | 37 | t.is(buffer1.getName(), buffer2.getName(), 'copy name'); 38 | t.is(buffer1.getURI(), buffer2.getURI(), 'copy URI'); 39 | }); 40 | 41 | test('extras', async (t) => { 42 | const io = await createPlatformIO(); 43 | const document = new Document(); 44 | const buffer = document.createBuffer('A').setExtras({ foo: 1, bar: 2 }); 45 | document 46 | .createAccessor() 47 | .setArray(new Uint8Array([1, 2, 3])) 48 | .setBuffer(buffer); 49 | 50 | const document2 = await io.readJSON(await io.writeJSON(document, { basename: 'test' })); 51 | 52 | t.deepEqual(document.getRoot().listBuffers()[0].getExtras(), { foo: 1, bar: 2 }, 'stores extras'); 53 | t.deepEqual(document2.getRoot().listBuffers()[0].getExtras(), { foo: 1, bar: 2 }, 'roundtrips extras'); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/core/test/properties/primitive-target.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document, Property } from '@gltf-transform/core'; 3 | 4 | const toType = (p: Property): string => p.propertyType; 5 | 6 | test('basic', (t) => { 7 | const doc = new Document(); 8 | const prim1 = doc.createPrimitiveTarget(); 9 | const acc1 = doc.createAccessor('acc1'); 10 | prim1.setAttribute('POSITION', acc1); 11 | const prim2 = prim1.clone(); 12 | 13 | t.is(prim1.getAttribute('POSITION'), acc1, 'sets POSITION'); 14 | t.is(prim2.getAttribute('POSITION'), acc1, 'sets POSITION'); 15 | t.deepEqual(acc1.listParents().map(toType), ['Root', 'PrimitiveTarget', 'PrimitiveTarget'], 'links POSITION'); 16 | 17 | prim1.setAttribute('POSITION', null); 18 | t.is(prim1.getAttribute('POSITION'), null, 'unsets POSITION'); 19 | t.deepEqual(acc1.listParents().map(toType), ['Root', 'PrimitiveTarget'], 'unlinks POSITION'); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/core/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "strict": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/test/utils/buffer-utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { BufferUtils } from '@gltf-transform/core'; 3 | 4 | const IS_NODEJS = typeof window === 'undefined'; 5 | 6 | const HELLO_WORLD = 'data:application/octet-stream;base64,aGVsbG8gd29ybGQ='; 7 | 8 | test('web', (t) => { 9 | if (IS_NODEJS) return t.pass(); 10 | t.is( 11 | BufferUtils.decodeText(BufferUtils.createBufferFromDataURI(HELLO_WORLD)), 12 | 'hello world', 13 | 'createBufferFromDataURI', 14 | ); 15 | t.is(BufferUtils.decodeText(BufferUtils.encodeText('hey')), 'hey', 'encode/decode'); 16 | }); 17 | 18 | test('node.js', (t) => { 19 | if (!IS_NODEJS) return t.pass(); 20 | t.is( 21 | BufferUtils.decodeText(BufferUtils.createBufferFromDataURI(HELLO_WORLD)), 22 | 'hello world', 23 | 'createBufferFromDataURI', 24 | ); 25 | t.is(BufferUtils.decodeText(BufferUtils.encodeText('hey')), 'hey', 'encode/decode'); 26 | 27 | const buffer = new Uint8Array([1, 2]); 28 | t.is(BufferUtils.equals(buffer, buffer), true, 'equals strict'); 29 | t.is(BufferUtils.equals(buffer, new Uint8Array([1])), false, 'equals by length'); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/core/test/utils/color-utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { ColorUtils } from '@gltf-transform/core'; 3 | 4 | test('basic', (t) => { 5 | t.deepEqual(ColorUtils.hexToFactor(0xff0000, []), [1, 0, 0], 'hexToFactor'); 6 | t.deepEqual(ColorUtils.factorToHex([1, 0, 0]), 16646144, 'factorToHex'); 7 | 8 | const linear = ColorUtils.convertSRGBToLinear([0.5, 0.5, 0.5], []); 9 | t.is(linear[0].toFixed(4), '0.2140', 'convertSRGBToLinear[0]'); 10 | t.is(linear[1].toFixed(4), '0.2140', 'convertSRGBToLinear[1]'); 11 | t.is(linear[2].toFixed(4), '0.2140', 'convertSRGBToLinear[2]'); 12 | 13 | const srgb = ColorUtils.convertLinearToSRGB([0.5, 0.5, 0.5], []); 14 | t.is(srgb[0].toFixed(4), '0.7354', 'convertLinearToSRGB[0]'); 15 | t.is(srgb[1].toFixed(4), '0.7354', 'convertLinearToSRGB[1]'); 16 | t.is(srgb[2].toFixed(4), '0.7354', 'convertLinearToSRGB[2]'); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/core/test/utils/file-utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { FileUtils } from '@gltf-transform/core'; 3 | 4 | test('basename', (t) => { 5 | t.is(FileUtils.basename('http://foo.com/path/to/index.html'), 'index', 'URI'); 6 | t.is(FileUtils.basename('http://foo.com/path/to/index.test.suffix.html'), 'index.test.suffix', 'URI'); 7 | }); 8 | 9 | test('extension', (t) => { 10 | t.is(FileUtils.extension('http://foo.com/path/to/index.html'), 'html', 'URI'); 11 | t.is(FileUtils.extension('data:image/png;base64,iVBORw0K'), 'png', 'PNG data URI'); 12 | t.is(FileUtils.extension('data:image/jpeg;base64,iVBORw0K'), 'jpg', 'JPEG data URI'); 13 | t.is(FileUtils.extension('data:application/octet-stream;base64,iVBORw0K'), 'bin', 'binary data URI'); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core/test/utils/logger.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Logger } from '@gltf-transform/core'; 3 | 4 | test('basic', (t) => { 5 | const { debug, info, warn, error } = console; 6 | 7 | const calls = { debug: 0, info: 0, warn: 0, error: 0 }; 8 | Object.assign(console, { 9 | debug: () => calls.debug++, 10 | info: () => calls.info++, 11 | warn: () => calls.warn++, 12 | error: () => calls.error++, 13 | }); 14 | 15 | let logger = new Logger(Logger.Verbosity.SILENT); 16 | logger.debug('debug'); 17 | logger.info('info'); 18 | logger.warn('warn'); 19 | logger.error('error'); 20 | t.is(calls.debug, 0, 'no debug when silenced'); 21 | t.is(calls.info, 0, 'no info when silenced'); 22 | t.is(calls.warn, 0, 'no warn when silenced'); 23 | t.is(calls.error, 0, 'no error when silenced'); 24 | 25 | logger = new Logger(Logger.Verbosity.DEBUG); 26 | logger.debug('debug'); 27 | logger.info('info'); 28 | logger.warn('warn'); 29 | logger.error('error'); 30 | t.is(calls.debug, 1, 'debug when not silenced'); 31 | t.is(calls.info, 1, 'info when not silenced'); 32 | t.is(calls.warn, 1, 'warn when not silenced'); 33 | t.is(calls.error, 1, 'error when not silenced'); 34 | 35 | Object.assign(console, { debug, info, warn, error }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/test/utils/math-utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { MathUtils } from '@gltf-transform/core'; 3 | 4 | test('identity', (t) => { 5 | t.is(MathUtils.identity(25), 25, 'identity'); 6 | }); 7 | 8 | test('clamp', (t) => { 9 | t.is(MathUtils.clamp(0.5, 0, 1), 0.5, 'clamp(0.5, 0, 1) === 0.5'); 10 | t.is(MathUtils.clamp(-0.1, 0, 1), 0, 'clamp(-0.1, 0, 1) === 0.0'); 11 | t.is(MathUtils.clamp(1, 0, 1), 1, 'clamp(1, 0, 1) === 1'); 12 | t.is(MathUtils.clamp(Infinity, 0, 1), 1, 'clamp(Infinity, 0, 1) === 1'); 13 | }); 14 | 15 | test('decodeNormalizedInt', (t) => { 16 | t.is(MathUtils.decodeNormalizedInt(25, 5126), 25, 'float'); 17 | t.is(MathUtils.decodeNormalizedInt(13107, 5123), 0.2, 'ushort'); 18 | t.is(MathUtils.decodeNormalizedInt(51, 5121), 0.2, 'ubyte'); 19 | t.is(MathUtils.decodeNormalizedInt(1000, 5122).toFixed(4), '0.0305', 'short'); 20 | t.is(MathUtils.decodeNormalizedInt(3, 5120).toFixed(4), '0.0236', 'byte'); 21 | }); 22 | 23 | test('encodeNormalizedInt', (t) => { 24 | t.is(MathUtils.encodeNormalizedInt(25, 5126), 25, 'float'); 25 | t.is(MathUtils.encodeNormalizedInt(0.2, 5123), 13107, 'ushort'); 26 | t.is(MathUtils.encodeNormalizedInt(0.2, 5121), 51, 'ubyte'); 27 | t.is(MathUtils.encodeNormalizedInt(-0.5, 5121), 0, 'ubyte out of bounds'); 28 | t.is(MathUtils.encodeNormalizedInt(0.03053, 5122), 1000, 'short'); 29 | t.is(MathUtils.encodeNormalizedInt(0.0236, 5120), 3, 'byte'); 30 | t.is(MathUtils.encodeNormalizedInt(1.5, 5120), 127, 'byte out of bounds'); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/core/test/utils/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { uuid } from '@gltf-transform/core'; 3 | 4 | test('basic', (t) => { 5 | const set = new Set(); 6 | for (let i = 0; i < 1000; i++) { 7 | set.add(uuid()); 8 | } 9 | t.is(set.size, 1000, 'generates 1000 unique IDs'); 10 | }); 11 | 12 | test('conflict', (t) => { 13 | const { random } = Math; 14 | 15 | // Number of elements must match ID length. 16 | const values = [ 17 | 0.12, 0.22, 0.32, 0.42, 0.52, 0.62, 0.11, 0.21, 0.31, 0.41, 0.51, 0.61, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.1, 0.2, 18 | 0.3, 0.4, 0.5, 0.6, 19 | ]; 20 | Math.random = (): number => values.pop(); 21 | 22 | const set = new Set(); 23 | for (let i = 0; i < 3; i++) { 24 | set.add(uuid()); 25 | } 26 | t.is(set.size, 3, 'generates 3 unique IDs'); 27 | 28 | Math.random = random; 29 | }); 30 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "strict": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /packages/docs/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # @gltf-transform/functions 2 | 3 | Generates website and API documentation for glTF Transform. Built with [SvelteKit](https://kit.svelte.dev/) and [greendoc](https://github.com/donmccurdy/greendoc). 4 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/docs", 3 | "version": "4.1.4", 4 | "private": true, 5 | "scripts": { 6 | "build": "vite build", 7 | "build:watch": "vite dev", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 11 | }, 12 | "devDependencies": { 13 | "@gltf-transform/core": "^4.1.4", 14 | "@gltf-transform/extensions": "^4.1.4", 15 | "@gltf-transform/functions": "^4.1.4", 16 | "@greendoc/parse": "^0.4.1", 17 | "@greendoc/svelte": "^0.4.1", 18 | "@sveltejs/adapter-static": "^3.0.8", 19 | "@sveltejs/kit": "^2.17.1", 20 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 21 | "@types/he": "^1.2.3", 22 | "he": "^1.2.0", 23 | "highlight.js": "^11.11.1", 24 | "mdsvex": "^0.12.3", 25 | "svelte": "^5.19.9", 26 | "svelte-check": "^4.1.4", 27 | "ts-morph": "^22.0.0", 28 | "tslib": "^2.8.1", 29 | "vite": "^6.1.0" 30 | }, 31 | "type": "module" 32 | } 33 | -------------------------------------------------------------------------------- /packages/docs/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /packages/docs/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | %sveltekit.head% 13 | 14 | 15 | 16 |
%sveltekit.body%
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/docs/src/lib/components/commercial-use.svelte: -------------------------------------------------------------------------------- 1 |

Commercial Use

2 | 3 |

4 | Using glTF Transform for a personal project? That's great! Sponsorship is neither expected nor required. Feel 5 | free to share screenshots if you've made something you're excited about — I enjoy seeing those! 6 |

7 | 8 |

9 | Using glTF Transform in for-profit work? That's wonderful! Your support is important to keep glTF Transform 10 | maintained, independent, and open source under MIT License. Please consider a 11 | subscription 12 | or 13 | GitHub sponsorship. 14 |

15 | 16 |

17 | 18 | Learn more in the 19 | glTF Transform Pro FAQs. 21 |

22 | -------------------------------------------------------------------------------- /packages/docs/src/lib/components/license.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |

License

6 | 7 |

Copyright {year}, MIT License.

8 | -------------------------------------------------------------------------------- /packages/docs/src/lib/pages/credits.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Credits | glTF Transform 3 | snippet: The glTF Transform project is developed and maintained by Don McCurdy. Thanks to the individuals and companies who have supported the project through GitHub Sponsors or by… 4 | --- 5 | 6 | 10 | 11 | # Credits 12 | 13 | The glTF Transform project is developed and maintained by [Don McCurdy](https://github.com/donmccurdy). Thanks to the individuals and companies who have supported the project through [glTF Transform Pro](https://gltf-transform.dev/pro), [GitHub Sponsors](https://github.com/sponsors/donmccurdy/), or by [contributing to the codebase](https://github.com/donmccurdy/glTF-Transform/graphs/contributors). Additional thanks to the following organizations: 14 | 15 | - [Khronos Group](https://www.khronos.org/), for sponsoring development of [XMP metadata](/modules/extensions/classes/KHRXMP) 16 | - [Muse](https://www.muse.place/), for sponsoring development of [palette textures](/modules/functions/functions/palette) 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/docs/src/lib/pages/functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Functions | glTF Transform 3 | snippet: Common glTF modifications, written using the core API. Most of these functions are Transforms, applying a modification to the Document… 4 | --- 5 | 6 | # Functions 7 | 8 | Common operations on glTF data are implemented by the `@gltf-transform/functions` module, and are organized in two categories: _Transforms_ and _Functions_. 9 | 10 | Installation: 11 | 12 | ```bash 13 | npm install --save @gltf-transform/functions 14 | ``` 15 | 16 | ## Transforms 17 | 18 | _Transforms_ apply a modification to the [Document](/modules/core/classes/Document), and are applied with the 19 | [Document.transform](/modules/core/classes/Document#transform) method. glTF Transform includes many expressive transforms already, and 20 | others can be implemented easily using the same APIs. 21 | 22 | ```typescript 23 | import { NodeIO } from '@gltf-transform/core'; 24 | import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions'; 25 | import { weld, quantize, dedup } from '@gltf-transform/functions'; 26 | 27 | const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS); 28 | const document = await io.read('input.glb'); 29 | 30 | await document.transform( 31 | weld(), 32 | quantize(), 33 | dedup(), 34 | 35 | // Custom transform. 36 | backfaceCulling({cull: true}), 37 | ); 38 | 39 | // Custom transform: enable/disable backface culling. 40 | function backfaceCulling(options) { 41 | return (document) => { 42 | for (const material of document.getRoot().listMaterials()) { 43 | material.setDoubleSided(!options.cull); 44 | } 45 | }; 46 | } 47 | 48 | await io.write('output.glb', document); 49 | ``` 50 | 51 | For a complete list of available transforms, see the navigation sidebar. 52 | 53 | ## Functions 54 | 55 | Other functions, like [getBounds](/modules/functions/functions/getBounds) or [compressTexture](/modules/functions/functions/compressTexture), are utility functions for general-purpose use. When making changes narrowly to a specific Texture or Material, these offer more targeted alternatives Transforms affecting the entire Document. 56 | 57 | For a complete list of available functions, see the navigation sidebar. 58 | -------------------------------------------------------------------------------- /packages/docs/src/lib/server/model/index.ts: -------------------------------------------------------------------------------- 1 | import { Encoder, GD, Parser, createPrefixSort } from '@greendoc/parse'; 2 | import { resolve, dirname } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { Project } from 'ts-morph'; 5 | import he from 'he'; 6 | 7 | const ROOT_DELTA = '../../../../../../'; 8 | const ROOT_FILE_PATH = resolve(dirname(fileURLToPath(import.meta.url)), ROOT_DELTA); 9 | const ROOT_WEB_PATH = new URL(ROOT_DELTA, import.meta.url).pathname.replace(/\/$/, ''); 10 | 11 | const corePath = resolve(ROOT_FILE_PATH, `packages/core/src/index.ts`); 12 | const extensionsPath = resolve(ROOT_FILE_PATH, `packages/extensions/src/index.ts`); 13 | const functionsPath = resolve(ROOT_FILE_PATH, `packages/functions/src/index.ts`); 14 | 15 | const project = new Project({ 16 | compilerOptions: { 17 | paths: { 18 | '@gltf-transform/core': [corePath], 19 | '@gltf-transform/extensions': [extensionsPath], 20 | '@gltf-transform/functions': [functionsPath], 21 | }, 22 | }, 23 | }); 24 | 25 | export const parser = new Parser(project) 26 | .addModule({ name: '@gltf-transform/core', slug: 'core', entry: corePath }) 27 | .addModule({ name: '@gltf-transform/extensions', slug: 'extensions', entry: extensionsPath }) 28 | .addModule({ name: '@gltf-transform/functions', slug: 'functions', entry: functionsPath }) 29 | .setRootPath(ROOT_WEB_PATH) 30 | .setBaseURL('https://github.com/donmccurdy/glTF-Transform/tree/main') 31 | .init(); 32 | 33 | export const encoder = new Encoder(parser).setSort(createPrefixSort()); 34 | 35 | export function getMetadata(item: GD.ApiItem): { 36 | title: string; 37 | snippet: string; 38 | } { 39 | return { 40 | title: item.name + ' | glTF Transform', 41 | snippet: item.comment ? getSnippet(item.comment) : '', 42 | }; 43 | } 44 | 45 | export function getSnippet(html: string): string { 46 | const text = he.decode(html.replace(/(<([^>]+)>)/gi, '')); 47 | const words = text.split(/\s+/); 48 | if (words.length < 30) return text; 49 | return words.slice(0, 30).join(' ') + '…'; 50 | } 51 | -------------------------------------------------------------------------------- /packages/docs/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | 3 | export const load: PageServerLoad = async () => ({ 4 | metadata: { 5 | title: 'glTF Transform', 6 | snippet: 'glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.', 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/docs/src/routes/[slug]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { render } from 'svelte/server'; 2 | import { error } from '@sveltejs/kit'; 3 | 4 | export const load = async ({ params }) => { 5 | try { 6 | const { default: Page } = await import(`../../lib/pages/${params.slug}.md`); 7 | return render(Page); 8 | } catch { 9 | error(404, 'Not found'); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/docs/src/routes/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {@html data.html} 7 |
8 | -------------------------------------------------------------------------------- /packages/docs/src/routes/modules/[module]/[kind]/[slug]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | import { parser, encoder, getMetadata } from '$lib/server/model'; 4 | import type { GD } from '@greendoc/parse'; 5 | 6 | export const load: PageServerLoad<{ export: GD.ApiItem }> = async ({ params }) => { 7 | const slug = params.slug.replace(/\.html$/, ''); 8 | const item = parser.getItemBySlug(slug); 9 | const encodedItem = encoder.encodeItem(item); 10 | if (item && encodedItem) { 11 | return { 12 | metadata: getMetadata(encodedItem), 13 | export: encodedItem, 14 | }; 15 | } 16 | error(404, 'Not found'); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/docs/src/routes/modules/[module]/[kind]/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |

{data.export.name}

10 |
11 | 12 | {#if data.export.kind === 'Class'} 13 | 14 | {:else if data.export.kind === 'Interface'} 15 | 16 | {:else if data.export.kind === 'Enum'} 17 | 18 | {:else if data.export.kind === 'Function'} 19 | 20 | {:else} 21 |

Error: Unknown kind, "${data.export.kind}"

22 | {/if} 23 | -------------------------------------------------------------------------------- /packages/docs/static/media/concepts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/concepts.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-material-pbr-specular-glossiness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-material-pbr-specular-glossiness.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-anisotropy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-anisotropy.jpg -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-clearcoat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-clearcoat.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-diffuse-transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-diffuse-transmission.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-dispersion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-dispersion.jpg -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-emissive-strength.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-emissive-strength.jpg -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-iridescence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-iridescence.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-sheen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-sheen.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-transmission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-transmission.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-unlit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-unlit.png -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-variants.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-variants.jpg -------------------------------------------------------------------------------- /packages/docs/static/media/extensions/khr-materials-volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/extensions/khr-materials-volume.png -------------------------------------------------------------------------------- /packages/docs/static/media/functions/palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/functions/palette.png -------------------------------------------------------------------------------- /packages/docs/static/media/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/hero.jpg -------------------------------------------------------------------------------- /packages/docs/static/media/kicker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/docs/static/media/kicker.jpg -------------------------------------------------------------------------------- /packages/docs/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { mdsvex } from 'mdsvex'; 4 | import hljs from 'highlight.js'; 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | extensions: ['.svelte', '.md'], 9 | preprocess: [ 10 | vitePreprocess(), 11 | mdsvex({ 12 | extensions: ['.md'], 13 | highlight: { 14 | highlighter: function (code, lang) { 15 | const language = hljs.getLanguage(lang) ? lang : 'plaintext'; 16 | const html = hljs.highlight(code, { language }).value; 17 | return `
{@html \`${html}\`}
`; 18 | } 19 | } 20 | }) 21 | ], 22 | kit: { adapter: adapter() } 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": false, 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /packages/docs/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true, 3 | "redirects": [ 4 | { 5 | "source": "/pro", 6 | "destination": "https://store.donmccurdy.com/l/gltf-transform-pro", 7 | "permanent": false 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | server: { port: 3000 } 7 | }); 8 | -------------------------------------------------------------------------------- /packages/extensions/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/extensions/README.md: -------------------------------------------------------------------------------- 1 | # @gltf-transform/extensions 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/@gltf-transform/extensions.svg)](https://www.npmjs.com/package/@gltf-transform/extensions) 4 | [![Minzipped size](https://badgen.net/bundlephobia/minzip/@gltf-transform/extensions)](https://bundlephobia.com/result?p=@gltf-transform/extensions) 5 | [![License](https://img.shields.io/npm/l/@gltf-transform/core.svg)](https://github.com/donmccurdy/glTF-Transform/blob/main/LICENSE.md) 6 | 7 | Part of the glTF Transform project. 8 | 9 | - GitHub: https://github.com/donmccurdy/glTF-Transform 10 | - Project Documentation: https://gltf-transform.dev/ 11 | - Package Documentation: https://gltf-transform.dev/extensions 12 | 13 | ## Credits 14 | 15 | See [*Credits*](https://gltf-transform.dev/credits). 16 | 17 |

Commercial Use

18 | 19 |

20 | Using glTF Transform for a personal project? That's great! Sponsorship is neither expected nor required. Feel 21 | free to share screenshots if you've made something you're excited about — I enjoy seeing those! 22 |

23 | 24 |

25 | Using glTF Transform in for-profit work? That's wonderful! Your support is important to keep glTF Transform 26 | maintained, independent, and open source under MIT License. Please consider a 27 | subscription 28 | or 29 | GitHub sponsorship. 30 |

31 | 32 |

33 | 34 | Learn more in the 35 | glTF Transform Pro FAQs. 37 |

38 | 39 | ## License 40 | 41 | Copyright 2024, MIT License. 42 | -------------------------------------------------------------------------------- /packages/extensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/extensions", 3 | "version": "4.1.4", 4 | "repository": "github:donmccurdy/glTF-Transform", 5 | "homepage": "https://gltf-transform.dev/extensions.html", 6 | "description": "Adds extension support to @gltf-transform/core", 7 | "author": "Don McCurdy ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/donmccurdy", 10 | "type": "module", 11 | "sideEffects": false, 12 | "exports": { 13 | "types": "./dist/index.d.ts", 14 | "require": "./dist/index.cjs", 15 | "default": "./dist/index.modern.js" 16 | }, 17 | "types": "./dist/index.d.ts", 18 | "main": "./dist/index.cjs", 19 | "module": "./dist/index.modern.js", 20 | "source": "./src/index.ts", 21 | "browserslist": [ 22 | "defaults", 23 | "not IE 11", 24 | "node >= 14" 25 | ], 26 | "scripts": { 27 | "build": "microbundle --format modern,cjs --no-compress", 28 | "build:watch": "microbundle watch --format modern,cjs --no-compress" 29 | }, 30 | "keywords": [ 31 | "gltf", 32 | "3d", 33 | "model", 34 | "webgl", 35 | "threejs" 36 | ], 37 | "dependencies": { 38 | "@gltf-transform/core": "^4.1.4", 39 | "ktx-parse": "^1.0.0" 40 | }, 41 | "files": [ 42 | "dist/", 43 | "src/", 44 | "README.md", 45 | "LICENSE.md" 46 | ], 47 | "gitHead": "895b70777fda68aa321358d17dab2105e8564d08" 48 | } 49 | -------------------------------------------------------------------------------- /packages/extensions/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXT_MESH_GPU_INSTANCING = 'EXT_mesh_gpu_instancing'; 2 | export const EXT_MESHOPT_COMPRESSION = 'EXT_meshopt_compression'; 3 | export const EXT_TEXTURE_WEBP = 'EXT_texture_webp'; 4 | export const EXT_TEXTURE_AVIF = 'EXT_texture_avif'; 5 | export const KHR_DRACO_MESH_COMPRESSION = 'KHR_draco_mesh_compression'; 6 | export const KHR_LIGHTS_PUNCTUAL = 'KHR_lights_punctual'; 7 | export const KHR_MATERIALS_ANISOTROPY = 'KHR_materials_anisotropy'; 8 | export const KHR_MATERIALS_CLEARCOAT = 'KHR_materials_clearcoat'; 9 | export const KHR_MATERIALS_DIFFUSE_TRANSMISSION = 'KHR_materials_diffuse_transmission'; 10 | export const KHR_MATERIALS_DISPERSION = 'KHR_materials_dispersion'; 11 | export const KHR_MATERIALS_EMISSIVE_STRENGTH = 'KHR_materials_emissive_strength'; 12 | export const KHR_MATERIALS_IOR = 'KHR_materials_ior'; 13 | export const KHR_MATERIALS_IRIDESCENCE = 'KHR_materials_iridescence'; 14 | export const KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS = 'KHR_materials_pbrSpecularGlossiness'; 15 | export const KHR_MATERIALS_SHEEN = 'KHR_materials_sheen'; 16 | export const KHR_MATERIALS_SPECULAR = 'KHR_materials_specular'; 17 | export const KHR_MATERIALS_TRANSMISSION = 'KHR_materials_transmission'; 18 | export const KHR_MATERIALS_UNLIT = 'KHR_materials_unlit'; 19 | export const KHR_MATERIALS_VOLUME = 'KHR_materials_volume'; 20 | export const KHR_MATERIALS_VARIANTS = 'KHR_materials_variants'; 21 | export const KHR_MESH_QUANTIZATION = 'KHR_mesh_quantization'; 22 | export const KHR_TEXTURE_BASISU = 'KHR_texture_basisu'; 23 | export const KHR_TEXTURE_TRANSFORM = 'KHR_texture_transform'; 24 | export const KHR_XMP_JSON_LD = 'KHR_xmp_json_ld'; 25 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-mesh-gpu-instancing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mesh-gpu-instancing.js'; 2 | export * from './instanced-mesh.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-meshopt-compression/constants.ts: -------------------------------------------------------------------------------- 1 | import type { GLTF, TypedArray } from '@gltf-transform/core'; 2 | 3 | export enum EncoderMethod { 4 | QUANTIZE = 'quantize', 5 | FILTER = 'filter', 6 | } 7 | 8 | export interface MeshoptBufferExtension { 9 | fallback?: boolean; 10 | } 11 | 12 | export enum MeshoptMode { 13 | ATTRIBUTES = 'ATTRIBUTES', 14 | TRIANGLES = 'TRIANGLES', 15 | INDICES = 'INDICES', 16 | } 17 | 18 | export enum MeshoptFilter { 19 | /** No filter — quantize only. */ 20 | NONE = 'NONE', 21 | /** Four 8- or 16-bit normalized values. */ 22 | OCTAHEDRAL = 'OCTAHEDRAL', 23 | /** Four 16-bit normalized values. */ 24 | QUATERNION = 'QUATERNION', 25 | /** K single-precision floating point values. */ 26 | EXPONENTIAL = 'EXPONENTIAL', 27 | } 28 | 29 | export interface MeshoptBufferViewExtension { 30 | buffer: number; 31 | byteOffset: number; 32 | byteLength: number; 33 | byteStride: number; 34 | count: number; 35 | mode: MeshoptMode; 36 | filter?: MeshoptFilter; 37 | } 38 | 39 | /** 40 | * When using filters, the accessor definition written to the file will not necessarily have the 41 | * same properties as the input accessor. For example, octahedral encoding requires int8 or int16 42 | * output, so float32 input must be ignored. 43 | */ 44 | export interface PreparedAccessor { 45 | array: TypedArray; 46 | byteStride: number; 47 | normalized: boolean; 48 | componentType: GLTF.AccessorComponentType; 49 | min?: number[]; 50 | max?: number[]; 51 | } 52 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-meshopt-compression/decoder.ts: -------------------------------------------------------------------------------- 1 | import { EXT_MESHOPT_COMPRESSION } from '../constants.js'; 2 | import type { GLTF } from '@gltf-transform/core'; 3 | import type { MeshoptBufferExtension } from './constants.js'; 4 | 5 | /** 6 | * Returns true for a fallback buffer, else false. 7 | * 8 | * - All references to the fallback buffer must come from bufferViews that 9 | * have a EXT_meshopt_compression extension specified. 10 | * - No references to the fallback buffer may come from 11 | * EXT_meshopt_compression extension JSON. 12 | */ 13 | export function isFallbackBuffer(bufferDef: GLTF.IBuffer): boolean { 14 | if (!bufferDef.extensions || !bufferDef.extensions[EXT_MESHOPT_COMPRESSION]) return false; 15 | const fallbackDef = bufferDef.extensions[EXT_MESHOPT_COMPRESSION] as MeshoptBufferExtension; 16 | return !!fallbackDef.fallback; 17 | } 18 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-meshopt-compression/index.ts: -------------------------------------------------------------------------------- 1 | export * from './meshopt-compression.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-texture-avif/index.ts: -------------------------------------------------------------------------------- 1 | export * from './texture-avif.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/ext-texture-webp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './texture-webp.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-draco-mesh-compression/index.ts: -------------------------------------------------------------------------------- 1 | export * from './draco-mesh-compression.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-lights-punctual/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lights-punctual.js'; 2 | export * from './light.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-anisotropy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-anisotropy.js'; 2 | export * from './anisotropy.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-clearcoat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-clearcoat.js'; 2 | export * from './clearcoat.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-diffuse-transmission/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-diffuse-transmission.js'; 2 | export * from './diffuse-transmission.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-dispersion/dispersion.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Nullable, PropertyType } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_DISPERSION } from '../constants.js'; 3 | 4 | interface IDispersion extends IProperty { 5 | dispersion: number; 6 | } 7 | 8 | /** 9 | * Defines dispersion for a PBR {@link Material}. See {@link KHRMaterialsDispersion}. 10 | */ 11 | export class Dispersion extends ExtensionProperty { 12 | public static EXTENSION_NAME = KHR_MATERIALS_DISPERSION; 13 | public declare extensionName: typeof KHR_MATERIALS_DISPERSION; 14 | public declare propertyType: 'Dispersion'; 15 | public declare parentTypes: [PropertyType.MATERIAL]; 16 | 17 | protected init(): void { 18 | this.extensionName = KHR_MATERIALS_DISPERSION; 19 | this.propertyType = 'Dispersion'; 20 | this.parentTypes = [PropertyType.MATERIAL]; 21 | } 22 | 23 | protected getDefaults(): Nullable { 24 | return Object.assign(super.getDefaults() as IProperty, { dispersion: 0 }); 25 | } 26 | 27 | /********************************************************************************************** 28 | * Dispersion. 29 | */ 30 | 31 | /** Dispersion. */ 32 | public getDispersion(): number { 33 | return this.get('dispersion'); 34 | } 35 | 36 | /** Dispersion. */ 37 | public setDispersion(dispersion: number): this { 38 | return this.set('dispersion', dispersion); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-dispersion/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-dispersion.js'; 2 | export * from './dispersion.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-emissive-strength/emissive-strength.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Nullable, PropertyType } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_EMISSIVE_STRENGTH } from '../constants.js'; 3 | 4 | interface IEmissiveStrength extends IProperty { 5 | emissiveStrength: number; 6 | } 7 | 8 | /** 9 | * Defines emissive strength for a PBR {@link Material}, allowing high-dynamic-range 10 | * (HDR) emissive materials. See {@link KHRMaterialsEmissiveStrength}. 11 | */ 12 | export class EmissiveStrength extends ExtensionProperty { 13 | public static EXTENSION_NAME = KHR_MATERIALS_EMISSIVE_STRENGTH; 14 | public declare extensionName: typeof KHR_MATERIALS_EMISSIVE_STRENGTH; 15 | public declare propertyType: 'EmissiveStrength'; 16 | public declare parentTypes: [PropertyType.MATERIAL]; 17 | 18 | protected init(): void { 19 | this.extensionName = KHR_MATERIALS_EMISSIVE_STRENGTH; 20 | this.propertyType = 'EmissiveStrength'; 21 | this.parentTypes = [PropertyType.MATERIAL]; 22 | } 23 | 24 | protected getDefaults(): Nullable { 25 | return Object.assign(super.getDefaults() as IProperty, { emissiveStrength: 1.0 }); 26 | } 27 | 28 | /********************************************************************************************** 29 | * EmissiveStrength. 30 | */ 31 | 32 | /** EmissiveStrength. */ 33 | public getEmissiveStrength(): number { 34 | return this.get('emissiveStrength'); 35 | } 36 | 37 | /** EmissiveStrength. */ 38 | public setEmissiveStrength(strength: number): this { 39 | return this.set('emissiveStrength', strength); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-emissive-strength/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-emissive-strength.js'; 2 | export * from './emissive-strength.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-ior/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-ior.js'; 2 | export * from './ior.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-ior/ior.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Nullable, PropertyType } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_IOR } from '../constants.js'; 3 | 4 | interface IIOR extends IProperty { 5 | ior: number; 6 | } 7 | 8 | /** 9 | * Defines index of refraction for a PBR {@link Material}. See {@link KHRMaterialsIOR}. 10 | */ 11 | export class IOR extends ExtensionProperty { 12 | public static EXTENSION_NAME = KHR_MATERIALS_IOR; 13 | public declare extensionName: typeof KHR_MATERIALS_IOR; 14 | public declare propertyType: 'IOR'; 15 | public declare parentTypes: [PropertyType.MATERIAL]; 16 | 17 | protected init(): void { 18 | this.extensionName = KHR_MATERIALS_IOR; 19 | this.propertyType = 'IOR'; 20 | this.parentTypes = [PropertyType.MATERIAL]; 21 | } 22 | 23 | protected getDefaults(): Nullable { 24 | return Object.assign(super.getDefaults() as IProperty, { ior: 1.5 }); 25 | } 26 | 27 | /********************************************************************************************** 28 | * IOR. 29 | */ 30 | 31 | /** IOR. */ 32 | public getIOR(): number { 33 | return this.get('ior'); 34 | } 35 | 36 | /** IOR. */ 37 | public setIOR(ior: number): this { 38 | return this.set('ior', ior); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-iridescence/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-iridescence.js'; 2 | export * from './iridescence.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-pbr-specular-glossiness/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-pbr-specular-glossiness.js'; 2 | export * from './pbr-specular-glossiness.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-sheen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-sheen.js'; 2 | export * from './sheen.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-specular/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-specular.js'; 2 | export * from './specular.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-transmission/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-transmission.js'; 2 | export * from './transmission.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-unlit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-unlit.js'; 2 | export * from './unlit.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-unlit/unlit.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty } from '@gltf-transform/core'; 2 | import { PropertyType } from '@gltf-transform/core'; 3 | import { KHR_MATERIALS_UNLIT } from '../constants.js'; 4 | 5 | /** 6 | * Converts a PBR {@link Material} to an unlit shading model. See {@link KHRMaterialsUnlit}. 7 | */ 8 | export class Unlit extends ExtensionProperty { 9 | public static EXTENSION_NAME = KHR_MATERIALS_UNLIT; 10 | public declare extensionName: typeof KHR_MATERIALS_UNLIT; 11 | public declare propertyType: 'Unlit'; 12 | public declare parentTypes: [PropertyType.MATERIAL]; 13 | 14 | protected init(): void { 15 | this.extensionName = KHR_MATERIALS_UNLIT; 16 | this.propertyType = 'Unlit'; 17 | this.parentTypes = [PropertyType.MATERIAL]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-variants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-variants.js'; 2 | export * from './variant.js'; 3 | export * from './mapping.js'; 4 | export * from './mapping-list.js'; 5 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-variants/mapping-list.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Nullable, PropertyType, RefSet } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_VARIANTS } from '../constants.js'; 3 | import type { Mapping } from './mapping.js'; 4 | 5 | interface IMappingList extends IProperty { 6 | mappings: RefSet; 7 | } 8 | 9 | /** 10 | * List of material variant {@link Mapping}s. See {@link KHRMaterialsVariants}. 11 | */ 12 | export class MappingList extends ExtensionProperty { 13 | public static EXTENSION_NAME = KHR_MATERIALS_VARIANTS; 14 | public declare extensionName: typeof KHR_MATERIALS_VARIANTS; 15 | public declare propertyType: 'MappingList'; 16 | public declare parentTypes: [PropertyType.PRIMITIVE]; 17 | 18 | protected init(): void { 19 | this.extensionName = KHR_MATERIALS_VARIANTS; 20 | this.propertyType = 'MappingList'; 21 | this.parentTypes = [PropertyType.PRIMITIVE]; 22 | } 23 | 24 | protected getDefaults(): Nullable { 25 | return Object.assign(super.getDefaults() as IProperty, { mappings: new RefSet() }); 26 | } 27 | 28 | /** Adds a {@link Mapping} to this mapping. */ 29 | public addMapping(mapping: Mapping): this { 30 | return this.addRef('mappings', mapping); 31 | } 32 | 33 | /** Removes a {@link Mapping} from the list for this {@link Primitive}. */ 34 | public removeMapping(mapping: Mapping): this { 35 | return this.removeRef('mappings', mapping); 36 | } 37 | 38 | /** Lists {@link Mapping}s in this {@link Primitive}. */ 39 | public listMappings(): Mapping[] { 40 | return this.listRefs('mappings'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-variants/mapping.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Material, Nullable, RefSet } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_VARIANTS } from '../constants.js'; 3 | import type { Variant } from './variant.js'; 4 | 5 | interface IMapping extends IProperty { 6 | material: Material; 7 | variants: RefSet; 8 | } 9 | 10 | /** 11 | * Maps {@link Variant}s to {@link Material}s. See {@link KHRMaterialsVariants}. 12 | */ 13 | export class Mapping extends ExtensionProperty { 14 | public static EXTENSION_NAME = KHR_MATERIALS_VARIANTS; 15 | public declare extensionName: typeof KHR_MATERIALS_VARIANTS; 16 | public declare propertyType: 'Mapping'; 17 | public declare parentTypes: ['MappingList']; 18 | 19 | protected init(): void { 20 | this.extensionName = KHR_MATERIALS_VARIANTS; 21 | this.propertyType = 'Mapping'; 22 | this.parentTypes = ['MappingList']; 23 | } 24 | 25 | protected getDefaults(): Nullable { 26 | return Object.assign(super.getDefaults() as IProperty, { material: null, variants: new RefSet() }); 27 | } 28 | 29 | /** The {@link Material} designated for this {@link Primitive}, under the given variants. */ 30 | public getMaterial(): Material | null { 31 | return this.getRef('material'); 32 | } 33 | 34 | /** The {@link Material} designated for this {@link Primitive}, under the given variants. */ 35 | public setMaterial(material: Material | null): this { 36 | return this.setRef('material', material); 37 | } 38 | 39 | /** Adds a {@link Variant} to this mapping. */ 40 | public addVariant(variant: Variant): this { 41 | return this.addRef('variants', variant); 42 | } 43 | 44 | /** Removes a {@link Variant} from this mapping. */ 45 | public removeVariant(variant: Variant): this { 46 | return this.removeRef('variants', variant); 47 | } 48 | 49 | /** Lists {@link Variant}s in this mapping. */ 50 | public listVariants(): Variant[] { 51 | return this.listRefs('variants'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-variants/variant.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty } from '@gltf-transform/core'; 2 | import { KHR_MATERIALS_VARIANTS } from '../constants.js'; 3 | 4 | /** 5 | * Defines a variant of a {@link Material}. See {@link KHRMaterialsVariants}. 6 | */ 7 | export class Variant extends ExtensionProperty { 8 | public static EXTENSION_NAME = KHR_MATERIALS_VARIANTS; 9 | public declare extensionName: typeof KHR_MATERIALS_VARIANTS; 10 | public declare propertyType: 'Variant'; 11 | public declare parentTypes: ['MappingList']; 12 | 13 | protected init(): void { 14 | this.extensionName = KHR_MATERIALS_VARIANTS; 15 | this.propertyType = 'Variant'; 16 | this.parentTypes = ['MappingList']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-materials-volume/index.ts: -------------------------------------------------------------------------------- 1 | export * from './materials-volume.js'; 2 | export * from './volume.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-mesh-quantization/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mesh-quantization.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-texture-basisu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './texture-basisu.js'; 2 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-texture-transform/index.ts: -------------------------------------------------------------------------------- 1 | export * from './texture-transform.js'; 2 | export * from './transform.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-texture-transform/transform.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionProperty, IProperty, Nullable, vec2 } from '@gltf-transform/core'; 2 | import { PropertyType } from '@gltf-transform/core'; 3 | import { KHR_TEXTURE_TRANSFORM } from '../constants.js'; 4 | 5 | interface ITransform extends IProperty { 6 | offset: vec2; 7 | rotation: number; 8 | scale: vec2; 9 | texCoord: number | null; // null → do not override TextureInfo. 10 | } 11 | 12 | /** 13 | * Defines UV transform for a {@link TextureInfo}. See {@link KHRTextureTransform}. 14 | */ 15 | export class Transform extends ExtensionProperty { 16 | public static EXTENSION_NAME = KHR_TEXTURE_TRANSFORM; 17 | public declare extensionName: typeof KHR_TEXTURE_TRANSFORM; 18 | public declare propertyType: 'Transform'; 19 | public declare parentTypes: [PropertyType.TEXTURE_INFO]; 20 | 21 | protected init(): void { 22 | this.extensionName = KHR_TEXTURE_TRANSFORM; 23 | this.propertyType = 'Transform'; 24 | this.parentTypes = [PropertyType.TEXTURE_INFO]; 25 | } 26 | 27 | protected getDefaults(): Nullable { 28 | return Object.assign(super.getDefaults() as IProperty, { 29 | offset: [0.0, 0.0] as vec2, 30 | rotation: 0, 31 | scale: [1.0, 1.0] as vec2, 32 | texCoord: null, 33 | }); 34 | } 35 | 36 | public getOffset(): vec2 { 37 | return this.get('offset'); 38 | } 39 | public setOffset(offset: vec2): this { 40 | return this.set('offset', offset); 41 | } 42 | 43 | public getRotation(): number { 44 | return this.get('rotation'); 45 | } 46 | public setRotation(rotation: number): this { 47 | return this.set('rotation', rotation); 48 | } 49 | 50 | public getScale(): vec2 { 51 | return this.get('scale'); 52 | } 53 | public setScale(scale: vec2): this { 54 | return this.set('scale', scale); 55 | } 56 | 57 | public getTexCoord(): number | null { 58 | return this.get('texCoord'); 59 | } 60 | public setTexCoord(texCoord: number | null): this { 61 | return this.set('texCoord', texCoord); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/extensions/src/khr-xmp-json-ld/index.ts: -------------------------------------------------------------------------------- 1 | export * from './packet.js'; 2 | export * from './xmp.js'; 3 | -------------------------------------------------------------------------------- /packages/extensions/test/in/0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/0.bin -------------------------------------------------------------------------------- /packages/extensions/test/in/BoxMeshopt.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/BoxMeshopt.bin -------------------------------------------------------------------------------- /packages/extensions/test/in/BoxMeshopt.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/BoxMeshopt.glb -------------------------------------------------------------------------------- /packages/extensions/test/in/DracoSparseMesh.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/DracoSparseMesh.bin -------------------------------------------------------------------------------- /packages/extensions/test/in/test-lossless.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/test-lossless.webp -------------------------------------------------------------------------------- /packages/extensions/test/in/test-lossy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/test-lossy.webp -------------------------------------------------------------------------------- /packages/extensions/test/in/test.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/test.avif -------------------------------------------------------------------------------- /packages/extensions/test/in/test.ktx2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/extensions/test/in/test.ktx2 -------------------------------------------------------------------------------- /packages/extensions/test/materials-ior.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document, NodeIO } from '@gltf-transform/core'; 3 | import { IOR, KHRMaterialsIOR } from '@gltf-transform/extensions'; 4 | import { cloneDocument } from '@gltf-transform/functions'; 5 | 6 | const WRITER_OPTIONS = { basename: 'extensionTest' }; 7 | 8 | test('basic', async (t) => { 9 | const doc = new Document(); 10 | const iorExtension = doc.createExtension(KHRMaterialsIOR); 11 | const ior = iorExtension.createIOR().setIOR(1.2); 12 | 13 | const mat = doc 14 | .createMaterial('MyMaterial') 15 | .setBaseColorFactor([1.0, 0.5, 0.5, 1.0]) 16 | .setExtension('KHR_materials_ior', ior); 17 | 18 | t.is(mat.getExtension('KHR_materials_ior'), ior, 'ior is attached'); 19 | 20 | const jsonDoc = await new NodeIO().registerExtensions([KHRMaterialsIOR]).writeJSON(doc, WRITER_OPTIONS); 21 | const materialDef = jsonDoc.json.materials[0]; 22 | 23 | t.deepEqual(materialDef.pbrMetallicRoughness.baseColorFactor, [1.0, 0.5, 0.5, 1.0], 'writes base color'); 24 | t.deepEqual(materialDef.extensions, { KHR_materials_ior: { ior: 1.2 } }, 'writes ior extension'); 25 | t.deepEqual(jsonDoc.json.extensionsUsed, [KHRMaterialsIOR.EXTENSION_NAME], 'writes extensionsUsed'); 26 | 27 | iorExtension.dispose(); 28 | t.is(mat.getExtension('KHR_materials_ior'), null, 'ior is detached'); 29 | 30 | const roundtripDoc = await new NodeIO().registerExtensions([KHRMaterialsIOR]).readJSON(jsonDoc); 31 | const roundtripMat = roundtripDoc.getRoot().listMaterials().pop(); 32 | 33 | t.is(roundtripMat.getExtension('KHR_materials_ior').getIOR(), 1.2, 'reads ior'); 34 | }); 35 | 36 | test('copy', (t) => { 37 | const doc = new Document(); 38 | const iorExtension = doc.createExtension(KHRMaterialsIOR); 39 | const ior = iorExtension.createIOR().setIOR(1.2); 40 | doc.createMaterial().setExtension('KHR_materials_ior', ior); 41 | 42 | const doc2 = cloneDocument(doc); 43 | const ior2 = doc2.getRoot().listMaterials()[0].getExtension('KHR_materials_ior'); 44 | t.is(doc2.getRoot().listExtensionsUsed().length, 1, 'copy KHRMaterialsIOR'); 45 | t.truthy(ior2, 'copy IOR'); 46 | t.is(ior2.getIOR(), 1.2, 'copy ior'); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/extensions/test/materials-unlit.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document, NodeIO } from '@gltf-transform/core'; 3 | import { KHRMaterialsUnlit } from '@gltf-transform/extensions'; 4 | import { cloneDocument } from '@gltf-transform/functions'; 5 | 6 | const WRITER_OPTIONS = { basename: 'extensionTest' }; 7 | 8 | test('basic', async (t) => { 9 | const doc = new Document(); 10 | const unlitExtension = doc.createExtension(KHRMaterialsUnlit); 11 | const unlit = unlitExtension.createUnlit(); 12 | 13 | const mat = doc 14 | .createMaterial('MyUnlitMaterial') 15 | .setBaseColorFactor([1.0, 0.5, 0.5, 1.0]) 16 | .setRoughnessFactor(1.0) 17 | .setMetallicFactor(0.0) 18 | .setExtension('KHR_materials_unlit', unlit); 19 | 20 | t.is(mat.getExtension('KHR_materials_unlit'), unlit, 'unlit is attached'); 21 | 22 | const jsonDoc = await new NodeIO().registerExtensions([KHRMaterialsUnlit]).writeJSON(doc, WRITER_OPTIONS); 23 | const materialDef = jsonDoc.json.materials[0]; 24 | 25 | t.deepEqual(materialDef.pbrMetallicRoughness.baseColorFactor, [1.0, 0.5, 0.5, 1.0], 'writes base color'); 26 | t.deepEqual(materialDef.extensions, { KHR_materials_unlit: {} }, 'writes unlit extension'); 27 | t.deepEqual(jsonDoc.json.extensionsUsed, [KHRMaterialsUnlit.EXTENSION_NAME], 'writes extensionsUsed'); 28 | 29 | const rtDoc = await new NodeIO().registerExtensions([KHRMaterialsUnlit]).readJSON(jsonDoc); 30 | const rtMat = rtDoc.getRoot().listMaterials()[0]; 31 | t.truthy(rtMat.getExtension('KHR_materials_unlit'), 'unlit is round tripped'); 32 | 33 | unlitExtension.dispose(); 34 | 35 | t.is(mat.getExtension('KHR_materials_unlit'), null, 'unlit is detached'); 36 | }); 37 | 38 | test('copy', (t) => { 39 | const doc = new Document(); 40 | const unlitExtension = doc.createExtension(KHRMaterialsUnlit); 41 | doc.createMaterial().setExtension('KHR_materials_unlit', unlitExtension.createUnlit()); 42 | 43 | const doc2 = cloneDocument(doc); 44 | t.is(doc2.getRoot().listExtensionsUsed().length, 1, 'copy KHRMaterialsUnlit'); 45 | t.truthy(doc2.getRoot().listMaterials()[0].getExtension('KHR_materials_unlit'), 'copy Unlit'); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/extensions/test/mesh-quantization.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document, JSONDocument, NodeIO } from '@gltf-transform/core'; 3 | import { KHRMeshQuantization } from '@gltf-transform/extensions'; 4 | import { cloneDocument } from '@gltf-transform/functions'; 5 | 6 | const WRITER_OPTIONS = { basename: 'extensionTest' }; 7 | 8 | test('basic', async (t) => { 9 | const doc = new Document(); 10 | const quantizationExtension = doc.createExtension(KHRMeshQuantization); 11 | let jsonDoc: JSONDocument; 12 | 13 | jsonDoc = await new NodeIO().registerExtensions([KHRMeshQuantization]).writeJSON(doc, WRITER_OPTIONS); 14 | t.deepEqual(jsonDoc.json.extensionsUsed, [KHRMeshQuantization.EXTENSION_NAME], 'writes extensionsUsed'); 15 | 16 | quantizationExtension.dispose(); 17 | 18 | jsonDoc = await new NodeIO().writeJSON(doc, WRITER_OPTIONS); 19 | t.is(jsonDoc.json.extensionsUsed, undefined, 'clears extensionsUsed'); 20 | }); 21 | 22 | test('copy', (t) => { 23 | const doc = new Document(); 24 | doc.createExtension(KHRMeshQuantization); 25 | 26 | t.is(cloneDocument(doc).getRoot().listExtensionsUsed().length, 1, 'copy KHRMeshQuantization'); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/extensions/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "strict": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/extensions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "strict": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/functions/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/functions/README.md: -------------------------------------------------------------------------------- 1 | # @gltf-transform/functions 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/@gltf-transform/functions.svg)](https://www.npmjs.com/package/@gltf-transform/functions) 4 | [![Minzipped size](https://badgen.net/bundlephobia/minzip/@gltf-transform/functions)](https://bundlephobia.com/result?p=@gltf-transform/functions) 5 | [![License](https://img.shields.io/npm/l/@gltf-transform/core.svg)](https://github.com/donmccurdy/glTF-Transform/blob/main/LICENSE.md) 6 | 7 | Part of the glTF Transform project. 8 | 9 | - GitHub: https://github.com/donmccurdy/glTF-Transform 10 | - Project Documentation: https://gltf-transform.dev 11 | - Package Documentation: https://gltf-transform.dev/functions 12 | 13 | ## Credits 14 | 15 | See [*Credits*](https://gltf-transform.dev/credits). 16 | 17 |

Commercial Use

18 | 19 |

20 | Using glTF Transform for a personal project? That's great! Sponsorship is neither expected nor required. Feel 21 | free to share screenshots if you've made something you're excited about — I enjoy seeing those! 22 |

23 | 24 |

25 | Using glTF Transform in for-profit work? That's wonderful! Your support is important to keep glTF Transform 26 | maintained, independent, and open source under MIT License. Please consider a 27 | subscription 28 | or 29 | GitHub sponsorship. 30 |

31 | 32 |

33 | 34 | Learn more in the 35 | glTF Transform Pro FAQs. 37 |

38 | 39 | ## License 40 | 41 | Copyright 2024, MIT License. 42 | -------------------------------------------------------------------------------- /packages/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/functions", 3 | "version": "4.1.4", 4 | "repository": "github:donmccurdy/glTF-Transform", 5 | "homepage": "https://gltf-transform.dev/functions.html", 6 | "description": "Functions for common glTF modifications, written using the core API", 7 | "author": "Don McCurdy ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/donmccurdy", 10 | "type": "module", 11 | "sideEffects": false, 12 | "exports": { 13 | "types": "./dist/index.d.ts", 14 | "require": "./dist/functions.cjs", 15 | "default": "./dist/functions.modern.js" 16 | }, 17 | "types": "./dist/index.d.ts", 18 | "main": "./dist/functions.cjs", 19 | "module": "./dist/functions.modern.js", 20 | "source": "./src/index.ts", 21 | "browserslist": [ 22 | "defaults", 23 | "not IE 11", 24 | "node >= 14" 25 | ], 26 | "scripts": { 27 | "build": "microbundle --format modern,cjs --no-compress", 28 | "build:watch": "microbundle watch --format modern,cjs --no-compress" 29 | }, 30 | "keywords": [ 31 | "gltf", 32 | "3d", 33 | "model", 34 | "webgl", 35 | "threejs" 36 | ], 37 | "dependencies": { 38 | "@gltf-transform/core": "^4.1.4", 39 | "@gltf-transform/extensions": "^4.1.4", 40 | "ktx-parse": "^1.0.0", 41 | "ndarray": "^1.0.19", 42 | "ndarray-lanczos": "^0.3.0", 43 | "ndarray-pixels": "^4.1.0" 44 | }, 45 | "files": [ 46 | "dist/", 47 | "src/", 48 | "README.md", 49 | "LICENSE.md", 50 | "package.json", 51 | "package-lock.json" 52 | ], 53 | "publishConfig": { 54 | "access": "public" 55 | }, 56 | "gitHead": "895b70777fda68aa321358d17dab2105e8564d08" 57 | } 58 | -------------------------------------------------------------------------------- /packages/functions/src/clean-primitive.ts: -------------------------------------------------------------------------------- 1 | import { ComponentTypeToTypedArray, Primitive } from '@gltf-transform/core'; 2 | 3 | /** 4 | * Removes degenerate triangles from the {@link Primitive}. Any triangle containing fewer than 5 | * three different vertex indices is considered degenerate. This method does not merge/weld 6 | * different vertices containing identical data — use {@link weld} first for that purpose. 7 | * 8 | * @internal 9 | */ 10 | export function cleanPrimitive(prim: Primitive): void { 11 | // TODO(feat): Clean degenerate primitives of other topologies. 12 | const indices = prim.getIndices(); 13 | if (!indices || prim.getMode() !== Primitive.Mode.TRIANGLES) return; 14 | 15 | // TODO(perf): untyped array allocation 16 | const srcIndicesArray = indices.getArray()!; 17 | const dstIndicesArray = []; 18 | let maxIndex = -Infinity; 19 | 20 | for (let i = 0, il = srcIndicesArray.length; i < il; i += 3) { 21 | const a = srcIndicesArray[i]; 22 | const b = srcIndicesArray[i + 1]; 23 | const c = srcIndicesArray[i + 2]; 24 | 25 | if (a === b || a === c || b === c) continue; 26 | 27 | dstIndicesArray.push(a, b, c); 28 | maxIndex = Math.max(maxIndex, a, b, c); 29 | } 30 | 31 | const TypedArray = ComponentTypeToTypedArray[indices.getComponentType()]; 32 | indices.setArray(new TypedArray(dstIndicesArray)); 33 | } 34 | -------------------------------------------------------------------------------- /packages/functions/src/clear-node-parent.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from '@gltf-transform/core'; 2 | import { listNodeScenes } from './list-node-scenes.js'; 3 | 4 | /** 5 | * Clears the parent of the given {@link Node}, leaving it attached 6 | * directly to its {@link Scene}. Inherited transforms will be applied 7 | * to the Node. This operation changes the Node's local transform, 8 | * but leaves its world transform unchanged. 9 | * 10 | * Example: 11 | * 12 | * ```typescript 13 | * import { clearNodeParent } from '@gltf-transform/functions'; 14 | * 15 | * scene.traverse((node) => { ... }); // Scene → … → Node 16 | * 17 | * clearNodeParent(node); 18 | * 19 | * scene.traverse((node) => { ... }); // Scene → Node 20 | * ``` 21 | * 22 | * To clear _all_ transforms of a Node, first clear its inherited transforms with 23 | * {@link clearNodeParent}, then clear the local transform with {@link clearNodeTransform}. 24 | */ 25 | export function clearNodeParent(node: Node): Node { 26 | const scenes = listNodeScenes(node); 27 | const parent = node.getParentNode(); 28 | 29 | if (!parent) return node; 30 | 31 | // Apply inherited transforms to local matrix. Skinned meshes are not affected 32 | // by the node parent's transform, and can be ignored. Updates to IBMs and TRS 33 | // animations are out of scope in this context. 34 | node.setMatrix(node.getWorldMatrix()); 35 | 36 | // Add to Scene roots. 37 | parent.removeChild(node); 38 | for (const scene of scenes) scene.addChild(node); 39 | 40 | return node; 41 | } 42 | -------------------------------------------------------------------------------- /packages/functions/src/clear-node-transform.ts: -------------------------------------------------------------------------------- 1 | import { mat4, MathUtils, Node } from '@gltf-transform/core'; 2 | import { multiply as multiplyMat4 } from 'gl-matrix/mat4'; 3 | import { transformMesh } from './transform-mesh.js'; 4 | 5 | // biome-ignore format: Readability. 6 | const IDENTITY: mat4 = [ 7 | 1, 0, 0, 0, 8 | 0, 1, 0, 0, 9 | 0, 0, 1, 0, 10 | 0, 0, 0, 1 11 | ]; 12 | 13 | /** 14 | * Clears local transform of the {@link Node}, applying the transform to children and meshes. 15 | * 16 | * - Applies transform to children 17 | * - Applies transform to {@link Mesh mesh} 18 | * - Resets {@link Light lights}, {@link Camera cameras}, and other attachments to the origin 19 | * 20 | * Example: 21 | * 22 | * ```typescript 23 | * import { clearNodeTransform } from '@gltf-transform/functions'; 24 | * 25 | * node.getTranslation(); // → [ 5, 0, 0 ] 26 | * node.getMesh(); // → vertex data centered at origin 27 | * 28 | * clearNodeTransform(node); 29 | * 30 | * node.getTranslation(); // → [ 0, 0, 0 ] 31 | * node.getMesh(); // → vertex data centered at [ 5, 0, 0 ] 32 | * ``` 33 | * 34 | * To clear _all_ transforms of a Node, first clear its inherited transforms with 35 | * {@link clearNodeParent}, then clear the local transform with {@link clearNodeTransform}. 36 | */ 37 | export function clearNodeTransform(node: Node): Node { 38 | const mesh = node.getMesh(); 39 | const localMatrix = node.getMatrix(); 40 | 41 | if (mesh && !MathUtils.eq(localMatrix, IDENTITY)) { 42 | transformMesh(mesh, localMatrix); 43 | } 44 | 45 | for (const child of node.listChildren()) { 46 | const matrix = child.getMatrix(); 47 | multiplyMat4(matrix, matrix, localMatrix); 48 | child.setMatrix(matrix); 49 | } 50 | 51 | return node.setMatrix(IDENTITY); 52 | } 53 | -------------------------------------------------------------------------------- /packages/functions/src/get-bounds.ts: -------------------------------------------------------------------------------- 1 | import { Scene, Node, getBounds as _getBounds, bbox } from '@gltf-transform/core'; 2 | 3 | /** 4 | * Computes bounding box (AABB) in world space for the given {@link Node} or {@link Scene}. 5 | * 6 | * Example: 7 | * 8 | * ```ts 9 | * import { getBounds } from '@gltf-transform/functions'; 10 | * 11 | * const {min, max} = getBounds(scene); 12 | * ``` 13 | */ 14 | export function getBounds(node: Node | Scene): bbox { 15 | return _getBounds(node); 16 | } 17 | -------------------------------------------------------------------------------- /packages/functions/src/get-texture-color-space.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from '@gltf-transform/core'; 2 | 3 | const SRGB_PATTERN = /color|emissive|diffuse/i; 4 | 5 | /** 6 | * Returns the color space (if any) implied by the {@link Material} slots to 7 | * which a texture is assigned, or null for non-color textures. If the texture 8 | * is not connected to any {@link Material}, this function will also return 9 | * null — any metadata in the image file will be ignored. 10 | * 11 | * Under current glTF specifications, only 'srgb' and non-color (null) textures 12 | * are used. 13 | * 14 | * Example: 15 | * 16 | * ```typescript 17 | * import { getTextureColorSpace } from '@gltf-transform/functions'; 18 | * 19 | * const baseColorTexture = material.getBaseColorTexture(); 20 | * const normalTexture = material.getNormalTexture(); 21 | * 22 | * getTextureColorSpace(baseColorTexture); // → 'srgb' 23 | * getTextureColorSpace(normalTexture); // → null 24 | * ``` 25 | */ 26 | export function getTextureColorSpace(texture: Texture): string | null { 27 | const graph = texture.getGraph(); 28 | const edges = graph.listParentEdges(texture); 29 | const isSRGB = edges.some((edge) => { 30 | return edge.getAttributes().isColor || SRGB_PATTERN.test(edge.getName()); 31 | }); 32 | return isSRGB ? 'srgb' : null; 33 | } 34 | -------------------------------------------------------------------------------- /packages/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './center.js'; 2 | export * from './clear-node-parent.js'; 3 | export * from './clear-node-transform.js'; 4 | export * from './compact-primitive.js'; 5 | export * from './convert-primitive-mode.js'; 6 | export * from './dedup.js'; 7 | export { dequantize, dequantizePrimitive, DequantizeOptions } from './dequantize.js'; 8 | export { 9 | cloneDocument, 10 | mergeDocuments, 11 | copyToDocument, 12 | moveToDocument, 13 | createDefaultPropertyResolver, 14 | } from './document-utils.js'; 15 | export * from './draco.js'; 16 | export * from './flatten.js'; 17 | export * from './get-bounds.js'; 18 | export * from './get-vertex-count.js'; 19 | export * from './get-texture-color-space.js'; 20 | export * from './inspect.js'; 21 | export * from './instance.js'; 22 | export * from './join-primitives.js'; 23 | export * from './join.js'; 24 | export * from './list-node-scenes.js'; 25 | export * from './list-texture-channels.js'; 26 | export * from './list-texture-info.js'; 27 | export * from './list-texture-slots.js'; 28 | export * from './meshopt.js'; 29 | export * from './metal-rough.js'; 30 | export * from './normals.js'; 31 | export * from './palette.js'; 32 | export * from './partition.js'; 33 | export * from './prune.js'; 34 | export * from './quantize.js'; 35 | export * from './resample.js'; 36 | export * from './reorder.js'; 37 | export * from './sequence.js'; 38 | export * from './simplify.js'; 39 | export * from './sort-primitive-weights.js'; 40 | export * from './sparse.js'; 41 | export * from './texture-compress.js'; 42 | export * from './tangents.js'; 43 | export * from './transform-mesh.js'; 44 | export * from './transform-primitive.js'; 45 | export * from './uninstance.js'; 46 | export * from './unlit.js'; 47 | export * from './unpartition.js'; 48 | export { 49 | assignDefaults, 50 | getGLPrimitiveCount, 51 | isTransformPending, 52 | createTransform, 53 | fitWithin, 54 | fitPowerOfTwo, 55 | } from './utils.js'; 56 | export * from './unweld.js'; 57 | export * from './vertex-color-space.js'; 58 | export * from './weld.js'; 59 | -------------------------------------------------------------------------------- /packages/functions/src/list-node-scenes.ts: -------------------------------------------------------------------------------- 1 | import { Node, Scene } from '@gltf-transform/core'; 2 | 3 | /** 4 | * Finds the parent {@link Scene Scenes} associated with the given {@link Node}. 5 | * In most cases a Node is associated with only one Scene, but it is possible 6 | * for a Node to be located in two or more Scenes, or none at all. 7 | * 8 | * Example: 9 | * 10 | * ```typescript 11 | * import { listNodeScenes } from '@gltf-transform/functions'; 12 | * 13 | * const node = document.getRoot().listNodes() 14 | * .find((node) => node.getName() === 'MyNode'); 15 | * 16 | * const scenes = listNodeScenes(node); 17 | * ``` 18 | */ 19 | export function listNodeScenes(node: Node): Scene[] { 20 | const visited = new Set(); 21 | 22 | let child = node; 23 | let parent: Node | null; 24 | 25 | while ((parent = child.getParentNode() as Node | null)) { 26 | if (visited.has(parent)) { 27 | throw new Error('Circular dependency in scene graph.'); 28 | } 29 | visited.add(parent); 30 | child = parent; 31 | } 32 | 33 | return child.listParents().filter((parent) => parent instanceof Scene) as Scene[]; 34 | } 35 | -------------------------------------------------------------------------------- /packages/functions/src/list-texture-slots.ts: -------------------------------------------------------------------------------- 1 | import { Document, Texture } from '@gltf-transform/core'; 2 | 3 | /** 4 | * Returns names of all texture slots using the given texture. 5 | * 6 | * Example: 7 | * 8 | * ```js 9 | * const slots = listTextureSlots(texture); 10 | * // → ['occlusionTexture', 'metallicRoughnessTexture'] 11 | * ``` 12 | */ 13 | export function listTextureSlots(texture: Texture): string[] { 14 | const document = Document.fromGraph(texture.getGraph())!; 15 | const root = document.getRoot(); 16 | const slots = texture 17 | .getGraph() 18 | .listParentEdges(texture) 19 | .filter((edge) => edge.getParent() !== root) 20 | .map((edge) => edge.getName()); 21 | return Array.from(new Set(slots)); 22 | } 23 | -------------------------------------------------------------------------------- /packages/functions/src/types/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/src/types/.gitkeep -------------------------------------------------------------------------------- /packages/functions/src/unlit.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Transform } from '@gltf-transform/core'; 2 | import { KHRMaterialsUnlit } from '@gltf-transform/extensions'; 3 | 4 | /** 5 | * @category Transforms 6 | */ 7 | export function unlit(): Transform { 8 | return (doc: Document): void => { 9 | const unlitExtension = doc.createExtension(KHRMaterialsUnlit) as KHRMaterialsUnlit; 10 | const unlit = unlitExtension.createUnlit(); 11 | doc.getRoot() 12 | .listMaterials() 13 | .forEach((material) => { 14 | material.setExtension('KHR_materials_unlit', unlit); 15 | }); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/functions/src/unpartition.ts: -------------------------------------------------------------------------------- 1 | import type { Document, Transform } from '@gltf-transform/core'; 2 | import { createTransform } from './utils.js'; 3 | 4 | const NAME = 'unpartition'; 5 | 6 | export interface UnpartitionOptions {} 7 | const UNPARTITION_DEFAULTS: Required = {}; 8 | 9 | /** 10 | * Removes partitions from the binary payload of a glTF file, so that the asset 11 | * contains at most one (1) `.bin` {@link Buffer}. This process reverses the 12 | * changes from a {@link partition} transform. 13 | * 14 | * Example: 15 | * 16 | * ```ts 17 | * document.getRoot().listBuffers(); // → [Buffer, Buffer, ...] 18 | * 19 | * await document.transform(unpartition()); 20 | * 21 | * document.getRoot().listBuffers(); // → [Buffer] 22 | * ``` 23 | * 24 | * @category Transforms 25 | */ 26 | export function unpartition(_options: UnpartitionOptions = UNPARTITION_DEFAULTS): Transform { 27 | return createTransform(NAME, async (document: Document): Promise => { 28 | const logger = document.getLogger(); 29 | 30 | const buffer = document.getRoot().listBuffers()[0]; 31 | document 32 | .getRoot() 33 | .listAccessors() 34 | .forEach((a) => a.setBuffer(buffer)); 35 | document 36 | .getRoot() 37 | .listBuffers() 38 | .forEach((b, index) => (index > 0 ? b.dispose() : null)); 39 | 40 | logger.debug(`${NAME}: Complete.`); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /packages/functions/test/center.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Accessor, Document, getBounds } from '@gltf-transform/core'; 3 | import { center } from '@gltf-transform/functions'; 4 | 5 | test('basic', async (t) => { 6 | const doc = new Document(); 7 | const position = doc 8 | .createAccessor() 9 | .setArray(new Float32Array([0, 0, 0, 1, 1, 1])) 10 | .setType(Accessor.Type.VEC3); 11 | const prim = doc.createPrimitive().setAttribute('POSITION', position); 12 | const mesh = doc.createMesh().addPrimitive(prim); 13 | const node = doc.createNode().setMesh(mesh).setTranslation([100, 100, 100]).setScale([5, 5, 5]); 14 | const scene = doc.createScene().addChild(node); 15 | 16 | await doc.transform(center({ pivot: 'center' })); 17 | 18 | t.deepEqual( 19 | getBounds(scene), 20 | { 21 | min: [-2.5, -2.5, -2.5], 22 | max: [2.5, 2.5, 2.5], 23 | }, 24 | 'center', 25 | ); 26 | 27 | await doc.transform(center({ pivot: 'above' })); 28 | 29 | t.deepEqual( 30 | getBounds(scene), 31 | { 32 | min: [-2.5, -5.0, -2.5], 33 | max: [2.5, 0.0, 2.5], 34 | }, 35 | 'above', 36 | ); 37 | 38 | await doc.transform(center({ pivot: 'below' })); 39 | 40 | t.deepEqual( 41 | getBounds(scene), 42 | { 43 | min: [-2.5, 0.0, -2.5], 44 | max: [2.5, 5.0, 2.5], 45 | }, 46 | 'below', 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/functions/test/clear-node-parent.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { clearNodeParent } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document().setLogger(logger); 8 | const nodeA = document.createNode('A').setTranslation([2, 0, 0]); 9 | const nodeB = document.createNode('B').setScale([4, 4, 4]).addChild(nodeA); 10 | const nodeC = document.createNode('C').addChild(nodeB); 11 | const scene = document.createScene().addChild(nodeC); 12 | 13 | t.truthy(nodeA.getParentNode() === nodeB, 'B → A (before)'); 14 | t.truthy(nodeB.getParentNode() === nodeC, 'C → B (before)'); 15 | t.truthy(nodeC.getParentNode() === null, 'Scene → C (before)'); 16 | t.deepEqual(scene.listChildren(), [nodeC], 'Scene → C (before)'); 17 | 18 | clearNodeParent(nodeA); 19 | 20 | t.truthy(nodeA.getParentNode() === null, 'Scene → A (after)'); 21 | t.truthy(nodeB.getParentNode() === nodeC, 'C → B (after)'); 22 | t.truthy(nodeC.getParentNode() === null, 'Scene → C (after)'); 23 | t.deepEqual(scene.listChildren(), [nodeC, nodeA], 'Scene → [C, A] (after)'); 24 | 25 | t.deepEqual(nodeA.getTranslation(), [8, 0, 0], 'A.translation'); 26 | t.deepEqual(nodeA.getScale(), [4, 4, 4], 'A.scale'); 27 | t.deepEqual(nodeB.getScale(), [4, 4, 4], 'B.scale'); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/functions/test/clear-node-transform.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { clearNodeTransform } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document().setLogger(logger); 8 | 9 | const camera = document.createCamera(); 10 | 11 | const position = document 12 | .createAccessor() 13 | .setType('VEC3') 14 | .setArray(new Float32Array([1, 0, 1])); 15 | const prim = document.createPrimitive().setAttribute('POSITION', position); 16 | const mesh = document.createMesh().addPrimitive(prim); 17 | 18 | const childNode = document.createNode('B'); 19 | 20 | const parentNode = document 21 | .createNode('A') 22 | .setTranslation([2, 0, 0]) 23 | .setScale([4, 4, 4]) 24 | .addChild(childNode) 25 | .setMesh(mesh) 26 | .setCamera(camera); 27 | 28 | clearNodeTransform(parentNode); 29 | 30 | t.deepEqual(parentNode.getTranslation(), [0, 0, 0], 'parent.translation'); 31 | t.deepEqual(parentNode.getRotation(), [0, 0, 0, 1], 'parent.rotation'); 32 | t.deepEqual(parentNode.getScale(), [1, 1, 1], 'parent.scale'); 33 | 34 | t.deepEqual(childNode.getTranslation(), [2, 0, 0], 'child.children[0].translation'); 35 | t.deepEqual(childNode.getRotation(), [0, 0, 0, 1], 'child.children[0].rotation'); 36 | t.deepEqual(childNode.getScale(), [4, 4, 4], 'child.children[0].scale'); 37 | 38 | t.truthy(parentNode.getCamera(), 'parent.camera'); 39 | t.deepEqual(prim.getAttribute('POSITION')!.getElement(0, []), [6, 0, 4], 'parent.mesh'); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/functions/test/draco.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { draco } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document().setLogger(logger); 8 | await document.transform(draco({ method: 'edgebreaker' })); 9 | await document.transform(draco({ method: 'sequential' })); 10 | const dracoExtension = document.getRoot().listExtensionsUsed()[0]; 11 | t.is(dracoExtension.extensionName, 'KHR_draco_mesh_compression', 'adds extension'); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/functions/test/in/DenseSphere.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/DenseSphere.glb -------------------------------------------------------------------------------- /packages/functions/test/in/Mesh_PrimitiveMode_01_to_06.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mode": 1, 4 | "attributes": { 5 | "POSITION": [ 6 | 0.5, -0.5, 0, -0.5, -0.5, 0, -0.5, -0.5, 0, -0.5, 0.5, 0, -0.5, 0.5, 0, 0.5, 0.30000001192092896, 0, 0.5, 7 | 0.30000001192092896, 0, 0.5, -0.5, 0 8 | ] 9 | }, 10 | "indices": [0, 1, 2, 3, 4, 5, 6, 7] 11 | }, 12 | { 13 | "mode": 2, 14 | "attributes": { 15 | "POSITION": [0.5, -0.5, 0, 0.5, 0.30000001192092896, 0, -0.5, 0.5, 0, -0.5, -0.5, 0] 16 | }, 17 | "indices": [0, 1, 2, 3] 18 | }, 19 | { 20 | "mode": 3, 21 | "attributes": { 22 | "POSITION": [0.5, -0.5, 0, 0.5, 0.30000001192092896, 0, -0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0] 23 | }, 24 | "indices": [0, 1, 2, 3, 4] 25 | }, 26 | { 27 | "mode": 5, 28 | "attributes": { 29 | "POSITION": [0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, -0.5, 0, -0.5, 0.5, 0] 30 | }, 31 | "indices": [0, 1, 2, 3] 32 | }, 33 | { 34 | "mode": 6, 35 | "attributes": { 36 | "POSITION": [0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0, -0.5, -0.5, 0] 37 | }, 38 | "indices": [0, 1, 2, 3] 39 | }, 40 | { 41 | "mode": 4, 42 | "attributes": { 43 | "POSITION": [-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0] 44 | }, 45 | "indices": [0, 1, 2, 3, 4, 5] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /packages/functions/test/in/ShapeCollection.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/ShapeCollection.glb -------------------------------------------------------------------------------- /packages/functions/test/in/TwoCubes.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/TwoCubes.glb -------------------------------------------------------------------------------- /packages/functions/test/in/many-cubes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/many-cubes.bin -------------------------------------------------------------------------------- /packages/functions/test/in/pattern-half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/pattern-half.png -------------------------------------------------------------------------------- /packages/functions/test/in/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/functions/test/in/pattern.png -------------------------------------------------------------------------------- /packages/functions/test/inspect.test.ts: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path'; 2 | import test from 'ava'; 3 | import { NodeIO } from '@gltf-transform/core'; 4 | import { inspect } from '@gltf-transform/functions'; 5 | import { logger } from '@gltf-transform/test-utils'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | test('basic', async (t) => { 11 | const io = new NodeIO(); 12 | const doc = await io.read(path.join(__dirname, 'in/TwoCubes.glb')); 13 | doc.setLogger(logger); 14 | 15 | doc.createAnimation('TestAnim'); 16 | doc.createTexture('TestTex').setImage(new Uint8Array(10)).setMimeType('image/fake'); 17 | 18 | const report = inspect(doc); 19 | 20 | t.truthy(report, 'report'); 21 | t.is(report.scenes.properties.length, 1, 'report.scenes'); 22 | t.is(report.meshes.properties.length, 2, 'report.meshes'); 23 | t.is(report.materials.properties.length, 2, 'report.materials'); 24 | t.is(report.animations.properties.length, 1, 'report.animations'); 25 | t.is(report.textures.properties.length, 1, 'report.textures'); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/functions/test/list-node-scenes.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { listNodeScenes } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document().setLogger(logger); 8 | const nodeA = document.createNode('A').setTranslation([2, 0, 0]); 9 | const nodeB = document.createNode('B').setScale([4, 4, 4]).addChild(nodeA); 10 | const nodeC = document.createNode('C').addChild(nodeB); 11 | const sceneA = document.createScene().addChild(nodeC); 12 | const sceneB = document.createScene().addChild(nodeC); 13 | 14 | t.deepEqual(listNodeScenes(nodeA), [sceneA, sceneB], 'A → Scene'); 15 | t.deepEqual(listNodeScenes(nodeB), [sceneA, sceneB], 'B → Scene'); 16 | t.deepEqual(listNodeScenes(nodeC), [sceneA, sceneB], 'C → Scene'); 17 | 18 | sceneA.removeChild(nodeC); 19 | 20 | t.deepEqual(listNodeScenes(nodeA), [sceneB], 'A → null'); 21 | t.deepEqual(listNodeScenes(nodeB), [sceneB], 'B → null'); 22 | t.deepEqual(listNodeScenes(nodeC), [sceneB], 'C → null'); 23 | 24 | sceneB.removeChild(nodeC); 25 | 26 | t.deepEqual(listNodeScenes(nodeA), [], 'A → null'); 27 | t.deepEqual(listNodeScenes(nodeB), [], 'B → null'); 28 | t.deepEqual(listNodeScenes(nodeC), [], 'C → null'); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/functions/test/list-texture-channels.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document, TextureChannel } from '@gltf-transform/core'; 3 | import { listTextureChannels, getTextureChannelMask } from '@gltf-transform/functions'; 4 | import { KHRMaterialsSheen } from '@gltf-transform/extensions'; 5 | 6 | const { R, G, B, A } = TextureChannel; 7 | 8 | test('listTextureChannels', (t) => { 9 | const document = new Document(); 10 | const textureA = document.createTexture(); 11 | const textureB = document.createTexture(); 12 | const sheenExtension = document.createExtension(KHRMaterialsSheen); 13 | const sheen = sheenExtension.createSheen().setSheenRoughnessTexture(textureB); 14 | const material = document 15 | .createMaterial() 16 | .setAlphaMode('BLEND') 17 | .setBaseColorTexture(textureA) 18 | .setExtension('KHR_materials_sheen', sheen); 19 | 20 | t.deepEqual(listTextureChannels(textureA), [R, G, B, A], 'baseColorTexture RGBA'); 21 | t.deepEqual(listTextureChannels(textureB), [A], 'sheenColorTexture A'); 22 | 23 | material.setAlphaMode('OPAQUE'); 24 | t.deepEqual(listTextureChannels(textureA), [R, G, B], 'baseColorTexture RGB'); 25 | 26 | sheen.setSheenColorTexture(textureB); 27 | t.deepEqual(listTextureChannels(textureB), [R, G, B, A], 'sheenColorTexture RGBA'); 28 | }); 29 | 30 | test('getTextureChannelMask', (t) => { 31 | const document = new Document(); 32 | const textureA = document.createTexture(); 33 | const textureB = document.createTexture(); 34 | const sheenExtension = document.createExtension(KHRMaterialsSheen); 35 | const sheen = sheenExtension.createSheen().setSheenRoughnessTexture(textureB); 36 | const material = document 37 | .createMaterial() 38 | .setAlphaMode('BLEND') 39 | .setBaseColorTexture(textureA) 40 | .setExtension('KHR_materials_sheen', sheen); 41 | 42 | t.is(getTextureChannelMask(textureA), R | G | B | A, 'baseColorTexture RGBA'); 43 | t.is(getTextureChannelMask(textureB), A, 'sheenColorTexture A'); 44 | 45 | material.setAlphaMode('OPAQUE'); 46 | t.is(getTextureChannelMask(textureA), R | G | B, 'baseColorTexture RGB'); 47 | 48 | sheen.setSheenColorTexture(textureB); 49 | t.is(getTextureChannelMask(textureB), R | G | B | A, 'sheenColorTexture RGBA'); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/functions/test/list-texture-slots.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { listTextureSlots } from '@gltf-transform/functions'; 4 | import { KHRMaterialsSheen } from '@gltf-transform/extensions'; 5 | 6 | test('basic', (t) => { 7 | const document = new Document(); 8 | const textureA = document.createTexture(); 9 | const textureB = document.createTexture(); 10 | const sheenExtension = document.createExtension(KHRMaterialsSheen); 11 | const sheen = sheenExtension.createSheen().setSheenColorTexture(textureB); 12 | document.createMaterial().setBaseColorTexture(textureA).setExtension('KHR_materials_sheen', sheen); 13 | t.deepEqual(listTextureSlots(textureA), ['baseColorTexture'], 'baseColorTexture'); 14 | t.deepEqual(listTextureSlots(textureB), ['sheenColorTexture'], 'sheenColorTexture'); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/functions/test/meshopt.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { meshopt } from '@gltf-transform/functions'; 4 | import { createTorusKnotPrimitive, logger } from '@gltf-transform/test-utils'; 5 | import { MeshoptEncoder } from 'meshoptimizer'; 6 | 7 | test('basic', async (t) => { 8 | const document = new Document().setLogger(logger); 9 | document.createMesh().addPrimitive(createTorusKnotPrimitive(document, { tubularSegments: 6 })); 10 | 11 | await document.transform(meshopt({ encoder: MeshoptEncoder })); 12 | 13 | t.true(hasMeshopt(document), 'adds extension'); 14 | }); 15 | 16 | test('noop', async (t) => { 17 | const document = new Document().setLogger(logger); 18 | await document.transform(meshopt({ encoder: MeshoptEncoder })); 19 | 20 | t.false(hasMeshopt(document), 'skips extension if no accessors found'); 21 | }); 22 | 23 | const hasMeshopt = (document: Document): boolean => 24 | document 25 | .getRoot() 26 | .listExtensionsUsed() 27 | .some((ext) => ext.extensionName === 'EXT_meshopt_compression'); 28 | -------------------------------------------------------------------------------- /packages/functions/test/normals.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { normals } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const doc = new Document().setLogger(logger); 8 | const indicesArray = new Uint16Array([0, 1, 2]); 9 | const positionArray = new Float32Array([0, 0, 0, 0, 0, 1, 1, 0, 0]); 10 | const normalArray = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]); 11 | const resultArray = new Float32Array([0, 1, 0, 0, 1, 0, 0, 1, 0]); 12 | const indices = doc.createAccessor().setType('SCALAR').setArray(indicesArray); 13 | const position = doc.createAccessor().setType('VEC3').setArray(positionArray); 14 | const normal = doc.createAccessor().setType('VEC3').setArray(normalArray); 15 | const prim = doc 16 | .createPrimitive() 17 | .setIndices(indices) 18 | .setAttribute('POSITION', position) 19 | .setAttribute('NORMAL', normal); 20 | doc.createMesh().addPrimitive(prim); 21 | 22 | await doc.transform(normals({ overwrite: false })); 23 | 24 | t.deepEqual(prim.getAttribute('NORMAL').getArray(), normalArray, 'skips normals'); 25 | 26 | await doc.transform(normals({ overwrite: true })); 27 | 28 | t.deepEqual(prim.getAttribute('NORMAL').getArray(), resultArray, 'overwrites normals'); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/functions/test/sequence.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { sequence } from '@gltf-transform/functions'; 4 | 5 | test('basic', async (t) => { 6 | const doc = new Document(); 7 | const root = doc.getRoot(); 8 | const scene = doc.createScene(); 9 | 10 | for (let i = 0; i < 4; i++) { 11 | scene.addChild(doc.createNode(`Step.00${i + 1}`)); 12 | } 13 | 14 | await doc.transform(sequence({ fps: 1, pattern: /^Step\.\d{3}$/ })); 15 | 16 | const anim = root.listAnimations().pop(); 17 | 18 | t.truthy(anim, 'creates animation'); 19 | t.deepEqual( 20 | anim.listChannels().map((channel) => channel.getTargetNode().getName()), 21 | ['Step.001', 'Step.002', 'Step.003', 'Step.004'], 22 | 'creates one channel per node', 23 | ); 24 | t.is(anim.listChannels()[0].getTargetPath(), 'scale', 'channels target scale'); 25 | t.is(anim.listSamplers().length, 4, 'creates one sampler per node'); 26 | t.deepEqual(anim.listSamplers()[0].getInput().getArray(), new Float32Array([0, 1]), 'input #1'); 27 | t.deepEqual(anim.listSamplers()[0].getOutput().getArray(), new Float32Array([1, 1, 1, 0, 0, 0]), 'output #1'); 28 | t.deepEqual(anim.listSamplers()[1].getInput().getArray(), new Float32Array([0, 1, 2]), 'input #2'); 29 | t.deepEqual( 30 | anim.listSamplers()[1].getOutput().getArray(), 31 | new Float32Array([0, 0, 0, 1, 1, 1, 0, 0, 0]), 32 | 'output #2', 33 | ); 34 | t.deepEqual(anim.listSamplers()[2].getInput().getArray(), new Float32Array([1, 2, 3]), 'input #3'); 35 | t.deepEqual( 36 | anim.listSamplers()[2].getOutput().getArray(), 37 | new Float32Array([0, 0, 0, 1, 1, 1, 0, 0, 0]), 38 | 'output #3', 39 | ); 40 | t.deepEqual(anim.listSamplers()[3].getInput().getArray(), new Float32Array([2, 3]), 'input #4'); 41 | t.deepEqual(anim.listSamplers()[3].getOutput().getArray(), new Float32Array([0, 0, 0, 1, 1, 1]), 'output #4'); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/functions/test/sparse.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { sparse } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document().setLogger(logger); 8 | const denseAccessor = document.createAccessor().setArray(new Float32Array([1, 2, 3, 4, 5, 6, 7, 8])); 9 | const sparseAccessor = document.createAccessor().setArray(new Float32Array([0, 0, 0, 0, 1, 0, 0, 0])); 10 | await document.transform(sparse()); 11 | t.is(denseAccessor.getSparse(), false, 'denseAccessor.sparse = false'); 12 | t.is(sparseAccessor.getSparse(), true, 'sparseAccessor.sparse = true'); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/functions/test/tangents.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { tangents } from '@gltf-transform/functions'; 4 | 5 | test('basic', async (t) => { 6 | const doc = new Document(); 7 | const positionArray = new Float32Array([1, 1, 1]); 8 | const normalArray = new Uint16Array([0, 1, 0]); 9 | const texcoordArray = new Float32Array([10, 5]); 10 | const resultArray = new Float32Array([-1, -1, -1, -1]); 11 | const position = doc.createAccessor().setType('VEC3').setArray(positionArray); 12 | const normal = doc.createAccessor().setType('VEC3').setArray(normalArray); 13 | const texcoord0 = doc 14 | .createAccessor() 15 | .setType('VEC2') 16 | .setArray(new Float32Array([0, 0])); 17 | const texcoord1 = doc.createAccessor().setType('VEC2').setArray(texcoordArray); 18 | const normalTexture = doc.createTexture(); 19 | const material = doc.createMaterial().setNormalTexture(normalTexture); 20 | material.getNormalTextureInfo().setTexCoord(1); 21 | const prim = doc 22 | .createPrimitive() 23 | .setMaterial(material) 24 | .setAttribute('POSITION', position) 25 | .setAttribute('NORMAL', normal) 26 | .setAttribute('TEXCOORD_0', texcoord0) 27 | .setAttribute('TEXCOORD_1', texcoord1); 28 | doc.createMesh().addPrimitive(prim); 29 | 30 | let a, b, c; 31 | await doc.transform( 32 | tangents({ 33 | generateTangents: (_a, _b, _c) => { 34 | a = _a; 35 | b = _b; 36 | c = _c; 37 | return resultArray; 38 | }, 39 | }), 40 | ); 41 | 42 | t.deepEqual(Array.from(a), Array.from(positionArray), 'provides position'); 43 | t.deepEqual(Array.from(b), Array.from(normalArray), 'provides normal'); 44 | t.deepEqual(Array.from(c), Array.from(texcoordArray), 'provides texcoord'); 45 | t.is(prim.getAttribute('TANGENT').getArray(), resultArray, 'sets tangent'); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/functions/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "strict": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/functions/test/unlit.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { unlit } from '@gltf-transform/functions'; 4 | 5 | test('basic', async (t) => { 6 | const document = new Document(); 7 | document.createMaterial(); 8 | await document.transform(unlit()); 9 | const unlitExtension = document.getRoot().listExtensionsUsed()[0]; 10 | t.is(unlitExtension.extensionName, 'KHR_materials_unlit', 'adds extension'); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/functions/test/unpartition.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Document } from '@gltf-transform/core'; 3 | import { unpartition } from '@gltf-transform/functions'; 4 | import { logger } from '@gltf-transform/test-utils'; 5 | 6 | test('basic', async (t) => { 7 | const document = new Document(); 8 | const root = document.getRoot(); 9 | const bufferA = document.createBuffer(); 10 | const bufferB = document.createBuffer(); 11 | const bufferC = document.createBuffer(); 12 | const accessorA = document.createAccessor().setBuffer(bufferA); 13 | const accessorB = document.createAccessor().setBuffer(bufferB); 14 | const accessorC = document.createAccessor().setBuffer(bufferC); 15 | 16 | document.setLogger(logger); 17 | 18 | await document.transform(unpartition()); 19 | 20 | t.is(root.listBuffers().length, 1, 'buffers.length === 1'); 21 | t.falsy(bufferA.isDisposed(), 'buffersA live'); 22 | t.truthy(bufferB.isDisposed(), 'buffersB disposed'); 23 | t.truthy(bufferC.isDisposed(), 'buffersC disposed'); 24 | t.is(accessorA.getBuffer(), bufferA, 'accessorA → bufferA'); 25 | t.is(accessorB.getBuffer(), bufferA, 'accessorA → bufferA'); 26 | t.is(accessorC.getBuffer(), bufferA, 'accessorA → bufferA'); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/functions/test/unweld.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Accessor, Document, Primitive } from '@gltf-transform/core'; 3 | import { unweld } from '@gltf-transform/functions'; 4 | 5 | test('basic', async (t) => { 6 | const doc = new Document(); 7 | const positionArray = new Float32Array([0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 1, 0, 0, -1, 0, 1, 0, 0, -1, 0, 0]); 8 | const position = doc.createAccessor().setType(Accessor.Type.VEC3).setArray(positionArray); 9 | const indices1 = doc.createAccessor().setArray(new Uint32Array([0, 3, 5, 0, 3, 6])); 10 | const indices2 = doc.createAccessor().setArray(new Uint32Array([0, 3, 5, 1, 4, 5])); 11 | const prim1 = doc 12 | .createPrimitive() 13 | .setIndices(indices1) 14 | .setAttribute('POSITION', position) 15 | .setMode(Primitive.Mode.TRIANGLES); 16 | const prim2 = doc 17 | .createPrimitive() 18 | .setIndices(indices2) 19 | .setAttribute('POSITION', position) 20 | .setMode(Primitive.Mode.TRIANGLES); 21 | const prim3 = doc.createPrimitive().setAttribute('POSITION', position).setMode(Primitive.Mode.TRIANGLE_FAN); 22 | doc.createMesh().addPrimitive(prim1).addPrimitive(prim2).addPrimitive(prim3); 23 | 24 | await doc.transform(unweld()); 25 | 26 | t.is(prim1.getIndices(), null, 'no index on prim1'); 27 | t.is(prim2.getIndices(), null, 'no index on prim2'); 28 | t.is(prim3.getIndices(), null, 'no index on prim3'); 29 | 30 | t.deepEqual( 31 | prim1.getAttribute('POSITION').getArray(), 32 | new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, -1, 0, 0]), 33 | 'subset of vertices in prim1', 34 | ); 35 | t.deepEqual( 36 | prim2.getAttribute('POSITION').getArray(), 37 | new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, -1, 0, 1, 0, 0]), 38 | 'subset of vertices in prim2', 39 | ); 40 | t.deepEqual(prim3.getAttribute('POSITION').getArray(), positionArray, 'original vertices in prim3'); 41 | t.is(doc.getRoot().listAccessors().length, 3, 'keeps only needed accessors'); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/functions/test/vertex-color-space.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { Accessor, Document } from '@gltf-transform/core'; 3 | import { vertexColorSpace } from '@gltf-transform/functions'; 4 | 5 | test('basic', (t) => { 6 | const input = [0.25882352941176473, 0.5215686274509804, 0.9568627450980393]; // sRGB 7 | const expected = [0.054480276435339814, 0.23455058215026167, 0.9046611743890203]; // linear 8 | 9 | const doc = new Document(); 10 | const mesh = doc.createMesh('test-mesh'); 11 | 12 | const primitive1 = doc.createPrimitive(); 13 | const primitive2 = doc.createPrimitive(); 14 | mesh.addPrimitive(primitive1); 15 | mesh.addPrimitive(primitive2); 16 | 17 | const accessor1 = doc.createAccessor('#1'); 18 | const accessor2 = doc.createAccessor('#2'); 19 | accessor1.setType(Accessor.Type.VEC3).setArray(new Float32Array([...input, ...input])); 20 | accessor2.setType(Accessor.Type.VEC4).setArray(new Float32Array([...input, 0.5])); 21 | 22 | primitive1.setAttribute('COLOR_0', accessor1).setAttribute('COLOR_1', accessor2); 23 | primitive2.setAttribute('COLOR_0', accessor1); 24 | 25 | vertexColorSpace({ inputColorSpace: 'srgb' })(doc); 26 | 27 | let actual; 28 | 29 | actual = primitive1.getAttribute('COLOR_0').getArray(); 30 | t.is(actual[0].toFixed(3), expected[0].toFixed(3), 'prim1.color1[0].r'); 31 | t.is(actual[1].toFixed(3), expected[1].toFixed(3), 'prim1.color1[0].g'); 32 | t.is(actual[2].toFixed(3), expected[2].toFixed(3), 'prim1.color1[0].b'); 33 | t.is(actual[3].toFixed(3), expected[0].toFixed(3), 'prim1.color1[1].r'); 34 | t.is(actual[4].toFixed(3), expected[1].toFixed(3), 'prim1.color1[1].g'); 35 | t.is(actual[5].toFixed(3), expected[2].toFixed(3), 'prim1.color1[1].b'); 36 | 37 | actual = primitive1.getAttribute('COLOR_1').getArray(); 38 | t.is(actual[0].toFixed(3), expected[0].toFixed(3), 'prim1.color2[0].r'); 39 | t.is(actual[1].toFixed(3), expected[1].toFixed(3), 'prim1.color2[0].g'); 40 | t.is(actual[2].toFixed(3), expected[2].toFixed(3), 'prim1.color2[0].b'); 41 | t.is(actual[3].toFixed(3), '0.500', 'prim1.color2[0].a'); 42 | 43 | t.deepEqual(primitive1.getAttribute('COLOR_0'), primitive2.getAttribute('COLOR_0'), 'shared COLOR_0 accessor'); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "strict": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global internal type definitions. 3 | * 4 | * Definitions provided here cannot be used in public APIs, as they aren't 5 | * bundled with the published packages. declaring an interface that depends on 6 | * them will yield, "Property 'X' of exported interface has or is using private 7 | * name 'Y'.ts(4033)". 8 | */ 9 | 10 | /** GL Matrix */ 11 | 12 | // See: https://github.com/toji/gl-matrix/issues/423 13 | 14 | declare module 'gl-matrix/vec4' { 15 | import { vec4 } from 'gl-matrix'; 16 | export = vec4; 17 | } 18 | 19 | declare module 'gl-matrix/vec3' { 20 | import { vec3 } from 'gl-matrix'; 21 | export = vec3; 22 | } 23 | 24 | declare module 'gl-matrix/vec2' { 25 | import { vec2 } from 'gl-matrix'; 26 | export = vec2; 27 | } 28 | 29 | declare module 'gl-matrix/mat4' { 30 | import { mat4 } from 'gl-matrix'; 31 | export = mat4; 32 | } 33 | 34 | declare module 'gl-matrix/mat3' { 35 | import { mat3 } from 'gl-matrix'; 36 | export = mat3; 37 | } 38 | 39 | declare module 'gl-matrix/quat' { 40 | import { quat } from 'gl-matrix'; 41 | export = quat; 42 | } 43 | 44 | declare module 'gl-matrix/quat2' { 45 | import { quat2 } from 'gl-matrix'; 46 | export = quat2; 47 | } 48 | 49 | /** Deno */ 50 | 51 | declare const Deno: { 52 | readFile: (path: string) => Promise; 53 | readTextFile: (path: string) => Promise; 54 | }; 55 | -------------------------------------------------------------------------------- /packages/test-utils/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Don McCurdy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@gltf-transform/test-utils", 4 | "version": "4.1.4", 5 | "type": "module", 6 | "sideEffects": false, 7 | "source": "./src/index.ts", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | "types": "./dist/index.d.ts", 11 | "default": "./dist/test-utils.modern.js" 12 | }, 13 | "scripts": { 14 | "build": "microbundle --format modern --no-compress", 15 | "build:watch": "microbundle watch --format modern --no-compress" 16 | }, 17 | "dependencies": { 18 | "@gltf-transform/core": "^4.1.4", 19 | "@gltf-transform/extensions": "^4.1.4", 20 | "@gltf-transform/functions": "^4.1.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/test-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import { PlatformIO, WebIO, NodeIO, Logger, bbox, vec3 as _vec3 } from '@gltf-transform/core'; 2 | 3 | export enum Environment { 4 | WEB, 5 | DENO, 6 | NODE, 7 | } 8 | 9 | export const environment = (typeof window !== 'undefined' ? Environment.WEB : Environment.NODE) as Environment; 10 | 11 | export const logger = new Logger(Logger.Verbosity.SILENT); 12 | 13 | export const createPlatformIO = async (): Promise => { 14 | switch (environment) { 15 | case Environment.WEB: 16 | return new WebIO().setLogger(logger); 17 | case Environment.NODE: 18 | return new NodeIO().setLogger(logger); 19 | } 20 | }; 21 | 22 | export function resolve(path: string, base: string): string { 23 | return new URL(path, base).pathname; 24 | } 25 | 26 | /** Creates a rounding function for given decimal precision. */ 27 | export function round(decimals = 4): (v: number) => number { 28 | const f = Math.pow(10, decimals); 29 | return (v: number) => { 30 | v = Math.round(v * f) / f; 31 | v = Object.is(v, -0) ? 0 : v; 32 | return v; 33 | }; 34 | } 35 | 36 | /** Rounds a 3D bounding box to given decimal precision. */ 37 | export function roundBbox(bbox: bbox, decimals = 4): bbox { 38 | return { 39 | min: bbox.min.map(round(decimals)) as _vec3, 40 | max: bbox.max.map(round(decimals)) as _vec3, 41 | }; 42 | } 43 | 44 | // bundle and re-export these, because the tests can't import them directly. 45 | // https://github.com/toji/gl-matrix/issues/444 46 | import * as mat4 from 'gl-matrix/mat4'; 47 | import * as mat3 from 'gl-matrix/mat3'; 48 | import * as quat from 'gl-matrix/quat'; 49 | import * as vec4 from 'gl-matrix/vec4'; 50 | import * as vec3 from 'gl-matrix/vec3'; 51 | import * as vec2 from 'gl-matrix/vec2'; 52 | export { mat4, mat3, quat, vec4, vec3, vec2 }; 53 | 54 | export * from './create-basic-primitive.js'; 55 | export * from './create-torus-primitive.js'; 56 | -------------------------------------------------------------------------------- /packages/test-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/view/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Quickstart 4 | 5 | ```shell 6 | # Install dependencies. 7 | yarn 8 | 9 | # Build source, watch for changes. 10 | yarn watch 11 | 12 | # Build source, watch for changes, and run examples. 13 | yarn dev 14 | 15 | # Run tests. 16 | yarn test 17 | ``` 18 | 19 | ## Concepts 20 | 21 | The project is designed around a few common object types, loosely based on reactive programming patterns. 22 | 23 | - **Subject:** Each glTF definition (e.g. `Material`) is bound to a single Subject (e.g. `MaterialSubject`). 24 | The Subject is responsible for receiving change events published by the definition, generating a 25 | derived three.js object (e.g. `THREE.Material`), and publishing the new value to all Observers. More 26 | precisely, this is a [*BehaviorSubject*](https://reactivex.io/documentation/subject.html), which holds 27 | a single current value at any given time. 28 | - **Observer:** An Observer is subscribed to the values published by a particular Subject, and 29 | passes those events along to a parent — usually another Subject. For example, a MaterialSubject 30 | subscribes to updates from a TextureSubject using an Observer. Observers are parameterized: 31 | for example, a single Texture may be used by many Materials, with different offset/scale/encoding 32 | parameters in each. The TextureSubject treats each of these Observers as a different "output", and 33 | uses the parameters associated with the Observer to publish the appropriate value. This library 34 | implements three observer types: `RefObserver`, `RefListObserver`, and `RefMapObserver`; the latter 35 | two are collections of `RefObservers` used to collate events from multiple Subjects (e.g. lists of Nodes). 36 | - **Pool:** As Subjects publish many variations of the same values to Observers, it's important to 37 | allocate those variations efficiently, reuse instances where possible, and clean up unused 38 | instances. That bookkeeping is assigned to Pools (not a Reactive concept). 39 | 40 | The diagram below shows a subset of the data flow sequence connecting Texture, Material, Primitive, and Node 41 | data types. 42 | 43 | ![View architecture, showing Subject and Observer event sequence](./assets/view_architecture.png) 44 | -------------------------------------------------------------------------------- /packages/view/LICENSE.md: -------------------------------------------------------------------------------- 1 | # Blue Oak Model License 2 | 3 | Version 1.0.0 4 | 5 | ## Purpose 6 | 7 | This license gives everyone as much permission to work with 8 | this software as possible, while protecting contributors 9 | from liability. 10 | 11 | ## Acceptance 12 | 13 | In order to receive this license, you must agree to its 14 | rules. The rules of this license are both obligations 15 | under that agreement and conditions to your license. 16 | You must not do anything with this software that triggers 17 | a rule that you cannot or will not follow. 18 | 19 | ## Copyright 20 | 21 | Each contributor licenses you to do everything with this 22 | software that would otherwise infringe that contributor's 23 | copyright in it. 24 | 25 | ## Notices 26 | 27 | You must ensure that everyone who gets a copy of 28 | any part of this software from you, with or without 29 | changes, also gets the text of this license or a link to 30 | . 31 | 32 | ## Excuse 33 | 34 | If anyone notifies you in writing that you have not 35 | complied with [Notices](#notices), you can keep your 36 | license by taking all practical steps to comply within 30 37 | days after the notice. If you do not do so, your license 38 | ends immediately. 39 | 40 | ## Patent 41 | 42 | Each contributor licenses you to do everything with this 43 | software that would otherwise infringe any patent claims 44 | they can license or become able to license. 45 | 46 | ## Reliability 47 | 48 | No contributor can revoke this license. 49 | 50 | ## No Liability 51 | 52 | ***As far as the law allows, this software comes as is, 53 | without any warranty or condition, and no contributor 54 | will be liable to anyone for any damages related to this 55 | software or this license, under any kind of legal claim.*** 56 | -------------------------------------------------------------------------------- /packages/view/assets/DamagedHelmet.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/view/assets/DamagedHelmet.glb -------------------------------------------------------------------------------- /packages/view/assets/royal_esplanade_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/view/assets/royal_esplanade_1k.hdr -------------------------------------------------------------------------------- /packages/view/assets/view_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/packages/view/assets/view_architecture.png -------------------------------------------------------------------------------- /packages/view/examples/1-model.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @gltf-transform/view : examples : 1-model.html 5 | 6 | 7 | 8 |
9 | ⏮   back 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/view/examples/2-material.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @gltf-transform/view : examples : 2-material.html 5 | 6 | 7 | 8 |
9 | ⏮   back 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/view/examples/3-diff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @gltf-transform/view : examples : 3-diff.html 5 | 6 | 7 | 8 |
9 | ⏮   back 10 |
11 |

Select .GLTF or .GLB file.

12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/view/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @gltf-transform/view : examples : index.html 5 | 16 | 17 | 18 |

@gltf-transform/view : examples

19 | 24 | 25 | -------------------------------------------------------------------------------- /packages/view/examples/stats-pane.ts: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer } from 'three'; 2 | import { Pane } from 'tweakpane'; 3 | 4 | export function createStatsPane(renderer: WebGLRenderer, pane: Pane) { 5 | const stats = {info: ''}; 6 | const monitorFolder = pane.addFolder({index: 0, title: 'Monitor'}) 7 | monitorFolder.addMonitor(stats, 'info', {bufferSize: 1, multiline: true, lineCount: 3}); 8 | 9 | 10 | 11 | return () => { 12 | const info = renderer.info; 13 | stats.info = ` 14 | programs ${info.programs.length} 15 | geometries ${info.memory.geometries} 16 | textures ${info.memory.textures} 17 | `.trim(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/view/examples/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --tp-plugin-thumbnail-list-thumb-size: 64px; 3 | --tp-plugin-thumbnail-list-width: 325px; 4 | --tp-plugin-thumbnail-list-height: 300px; 5 | } 6 | 7 | html, body { 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | .back { 13 | position: fixed; 14 | top: 1em; 15 | left: 1em; 16 | z-index: 1; 17 | color: #FFFFFF; 18 | text-decoration: none; 19 | opacity: 0.75; 20 | } 21 | 22 | .back:hover { 23 | opacity: 1; 24 | } 25 | 26 | /* Dropzone */ 27 | 28 | .dropzone-placeholder { 29 | width: 400px; 30 | height: 200px; 31 | position: absolute; 32 | top: calc(50% - 100px); 33 | left: calc(50% - 200px); 34 | background: rgba(255, 255, 255, 0.75); 35 | border-radius: 8px; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | padding: 3em; 40 | box-sizing: border-box; 41 | } 42 | 43 | .dropzone-placeholder p { 44 | margin-top: 0; 45 | } 46 | 47 | /* Tweakpane */ 48 | 49 | .tp-dfwv { 50 | width: 350px !important; 51 | } 52 | 53 | .tp-colv_t .tp-txtv { 54 | position: relative; 55 | } 56 | 57 | .tp-colv_t .tp-txtv::after { 58 | content: 'sRGB'; 59 | position: absolute; 60 | right: 0.5em; 61 | top: 0.3em; 62 | color: #888; 63 | font-size: 1em; 64 | } 65 | -------------------------------------------------------------------------------- /packages/view/examples/util.ts: -------------------------------------------------------------------------------- 1 | import { WebIO } from '@gltf-transform/core'; 2 | import { PMREMGenerator, REVISION, Texture, WebGLRenderer } from 'three'; 3 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; 4 | import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader'; 5 | import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'; 6 | import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer'; 7 | import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; 8 | 9 | const TRANSCODER_PATH = `https://unpkg.com/three@0.${REVISION}.x/examples/jsm/libs/basis/`; 10 | 11 | // await MeshoptDecoder.ready; 12 | // await MeshoptEncoder.ready; 13 | 14 | let _ktx2Loader: KTX2Loader; 15 | export function createKTX2Loader() { 16 | if (_ktx2Loader) return _ktx2Loader; 17 | const renderer = new WebGLRenderer(); 18 | const loader = new KTX2Loader() 19 | .detectSupport(renderer) 20 | .setTranscoderPath(TRANSCODER_PATH); 21 | renderer.dispose(); 22 | return (_ktx2Loader = loader); 23 | } 24 | 25 | let _gltfLoader: GLTFLoader; 26 | export function createGLTFLoader() { 27 | if (_gltfLoader) return _gltfLoader; 28 | const loader = new GLTFLoader() 29 | .setMeshoptDecoder(MeshoptDecoder) 30 | .setKTX2Loader(createKTX2Loader()); 31 | return (_gltfLoader = loader); 32 | } 33 | 34 | let _io: WebIO; 35 | export function createIO() { 36 | if (_io) return _io; 37 | const io = new WebIO() 38 | .registerExtensions(ALL_EXTENSIONS) 39 | .registerDependencies({ 40 | 'meshopt.encoder': MeshoptEncoder, 41 | 'meshopt.decoder': MeshoptDecoder, 42 | }); 43 | return (_io = io); 44 | } 45 | 46 | export function createEnvironment(renderer: WebGLRenderer): Promise { 47 | const pmremGenerator = new PMREMGenerator(renderer); 48 | pmremGenerator.compileEquirectangularShader(); 49 | 50 | return new Promise((resolve, reject) => { 51 | new RGBELoader() 52 | .load( './royal_esplanade_1k.hdr', ( texture ) => { 53 | const envMap = pmremGenerator.fromEquirectangular( texture ).texture; 54 | texture.dispose(); 55 | pmremGenerator.dispose(); 56 | resolve(envMap); 57 | }, undefined, reject ); 58 | }) as Promise; 59 | } 60 | -------------------------------------------------------------------------------- /packages/view/examples/vite.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: 'examples', 3 | publicDir: '../assets', 4 | resolve: { alias: { '@gltf-transform/view': '../' } }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gltf-transform/view", 3 | "version": "4.1.4", 4 | "repository": "github:donmccurdy/glTF-Transform-View", 5 | "homepage": "https://gltf-transform.dev/", 6 | "description": "Syncs a glTF-Transform Document with a three.js scene graph", 7 | "author": "Don McCurdy ", 8 | "license": "BlueOak-1.0.0", 9 | "funding": "https://github.com/sponsors/donmccurdy", 10 | "type": "module", 11 | "sideEffects": false, 12 | "exports": { 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/view.modern.js" 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "module": "./dist/view.modern.js", 18 | "source": "./src/index.ts", 19 | "browserslist": [ 20 | "last 2 and_chr versions", 21 | "last 2 chrome versions", 22 | "last 2 opera versions", 23 | "last 2 ios_saf versions", 24 | "last 2 safari versions", 25 | "last 2 firefox versions" 26 | ], 27 | "scripts": { 28 | "build": "microbundle --no-compress --format modern", 29 | "build:watch": "microbundle watch --no-compress --format modern", 30 | "dev": "vite -c examples/vite.config.js" 31 | }, 32 | "files": [ 33 | "dist/", 34 | "src/", 35 | "README.md", 36 | "LICENSE.md" 37 | ], 38 | "peerDependencies": { 39 | "@gltf-transform/core": ">=4.0.0", 40 | "@gltf-transform/extensions": ">=4.0.0", 41 | "@types/three": ">=0.155.0", 42 | "three": ">=0.155.0" 43 | }, 44 | "gitHead": "895b70777fda68aa321358d17dab2105e8564d08" 45 | } 46 | -------------------------------------------------------------------------------- /packages/view/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | DirectionalLight, 4 | Line, 5 | LineLoop, 6 | LineSegments, 7 | Material, 8 | Mesh, 9 | PointLight, 10 | Points, 11 | SkinnedMesh, 12 | SpotLight, 13 | } from 'three'; 14 | 15 | export type Subscription = () => void; 16 | 17 | export type MeshLike = 18 | | Mesh 19 | | SkinnedMesh 20 | | Points 21 | | Line 22 | | LineSegments 23 | | LineLoop; 24 | 25 | export type LightLike = PointLight | SpotLight | DirectionalLight; 26 | 27 | export interface THREEObject { 28 | name: string; 29 | type: string; 30 | } 31 | -------------------------------------------------------------------------------- /packages/view/src/index.ts: -------------------------------------------------------------------------------- 1 | export { DocumentView } from './DocumentView.js'; 2 | export { DefaultImageProvider as ImageProvider, NullImageProvider } from './ImageProvider.js'; 3 | -------------------------------------------------------------------------------- /packages/view/src/observers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RefObserver.js'; 2 | export * from './RefListObserver.js'; 3 | export * from './RefMapObserver.js'; 4 | -------------------------------------------------------------------------------- /packages/view/src/pools/SingleUserPool.ts: -------------------------------------------------------------------------------- 1 | import { Property as PropertyDef, Mesh as MeshDef, Node as NodeDef, uuid } from '@gltf-transform/core'; 2 | import { Object3D } from 'three'; 3 | import { LightLike } from '../constants.js'; 4 | import { Pool } from './Pool.js'; 5 | 6 | export interface SingleUserParams { 7 | id: string; 8 | } 9 | 10 | /** @internal */ 11 | export class SingleUserPool extends Pool { 12 | private static _parentIDs = new WeakMap(); 13 | 14 | /** Generates a unique Object3D for every parent. */ 15 | static createParams(property: MeshDef | NodeDef): SingleUserParams { 16 | const id = this._parentIDs.get(property) || uuid(); 17 | this._parentIDs.set(property, id); 18 | return { id }; 19 | } 20 | 21 | requestVariant(base: T, params: SingleUserParams): T { 22 | return this._request(this._createVariant(base, params)); 23 | } 24 | 25 | protected _createVariant(srcObject: T, _params: SingleUserParams): T { 26 | // With a deep clone of a NodeDef or MeshDef value, we're cloning 27 | // any PrimitiveDef values (e.g. Mesh, Lines, Points) within it. 28 | // Record the new outputs. 29 | const dstObject = srcObject.clone(); 30 | parallelTraverse(srcObject, dstObject, (base, variant) => { 31 | if (base === srcObject) return; // Skip root; recorded elsewhere. 32 | if ((srcObject as unknown as LightLike).isLight) return; // Skip light target. 33 | this.documentView.recordOutputVariant(base, variant); 34 | }); 35 | return dstObject; 36 | } 37 | 38 | protected _updateVariant(_srcObject: T, _dstObject: T): T { 39 | throw new Error('Not implemented'); 40 | } 41 | } 42 | 43 | function parallelTraverse(a: Object3D, b: Object3D, callback: (a: Object3D, b: Object3D) => void) { 44 | callback(a, b); 45 | for (let i = 0; i < a.children.length; i++) { 46 | parallelTraverse(a.children[i], b.children[i], callback); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/view/src/pools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Pool.js'; 2 | export * from './TexturePool.js'; 3 | export * from './MaterialPool.js'; 4 | export * from './SingleUserPool.js'; 5 | -------------------------------------------------------------------------------- /packages/view/src/subjects/AccessorSubject.ts: -------------------------------------------------------------------------------- 1 | import { BufferAttribute } from 'three'; 2 | import { Accessor as AccessorDef } from '@gltf-transform/core'; 3 | import type { DocumentViewImpl } from '../DocumentViewImpl.js'; 4 | import { Subject } from './Subject.js'; 5 | import { ValuePool } from '../pools/index.js'; 6 | 7 | /** @internal */ 8 | export class AccessorSubject extends Subject { 9 | constructor(documentView: DocumentViewImpl, def: AccessorDef) { 10 | super( 11 | documentView, 12 | def, 13 | AccessorSubject.createValue(def, documentView.accessorPool), 14 | documentView.accessorPool, 15 | ); 16 | } 17 | 18 | private static createValue(def: AccessorDef, pool: ValuePool) { 19 | return pool.requestBase(new BufferAttribute(def.getArray()!, def.getElementSize(), def.getNormalized())); 20 | } 21 | 22 | update() { 23 | const def = this.def; 24 | const value = this.value; 25 | 26 | if ( 27 | def.getArray() !== value.array || 28 | def.getElementSize() !== value.itemSize || 29 | def.getNormalized() !== value.normalized 30 | ) { 31 | this.pool.releaseBase(value); 32 | this.value = AccessorSubject.createValue(def, this.pool); 33 | } else { 34 | value.needsUpdate = true; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/view/src/subjects/ExtensionSubject.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionProperty as ExtensionPropertyDef } from '@gltf-transform/core'; 2 | import type { DocumentViewImpl } from '../DocumentViewImpl.js'; 3 | import { Subject } from './Subject.js'; 4 | 5 | /** @internal */ 6 | export class ExtensionSubject extends Subject { 7 | constructor(documentView: DocumentViewImpl, def: ExtensionPropertyDef) { 8 | super(documentView, def, def, documentView.extensionPool); 9 | } 10 | update() {} 11 | } 12 | -------------------------------------------------------------------------------- /packages/view/src/subjects/MeshSubject.ts: -------------------------------------------------------------------------------- 1 | import { Group } from 'three'; 2 | import { Mesh as MeshDef, Primitive as PrimitiveDef } from '@gltf-transform/core'; 3 | import type { DocumentViewSubjectAPI } from '../DocumentViewImpl.js'; 4 | import { Subject } from './Subject.js'; 5 | import { RefListObserver } from '../observers/index.js'; 6 | import { MeshLike } from '../constants.js'; 7 | import { SingleUserParams, SingleUserPool } from '../pools/index.js'; 8 | 9 | /** @internal */ 10 | export class MeshSubject extends Subject { 11 | protected primitives = new RefListObserver( 12 | 'primitives', 13 | this._documentView, 14 | ).setParamsFn(() => SingleUserPool.createParams(this.def)); 15 | 16 | constructor(documentView: DocumentViewSubjectAPI, def: MeshDef) { 17 | super(documentView, def, documentView.meshPool.requestBase(new Group()), documentView.meshPool); 18 | 19 | this.primitives.subscribe((nextPrims, prevPrims) => { 20 | if (prevPrims.length) this.value.remove(...prevPrims); 21 | if (nextPrims.length) this.value.add(...nextPrims); 22 | this.publishAll(); 23 | }); 24 | } 25 | 26 | update() { 27 | const def = this.def; 28 | const value = this.value; 29 | 30 | if (def.getName() !== value.name) { 31 | value.name = def.getName(); 32 | } 33 | 34 | this.primitives.update(def.listPrimitives()); 35 | } 36 | 37 | dispose() { 38 | this.primitives.dispose(); 39 | super.dispose(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/view/src/subjects/SceneSubject.ts: -------------------------------------------------------------------------------- 1 | import { Group, Object3D } from 'three'; 2 | import type { Node as NodeDef, Scene as SceneDef } from '@gltf-transform/core'; 3 | import type { DocumentViewSubjectAPI } from '../DocumentViewImpl.js'; 4 | import { Subject } from './Subject.js'; 5 | import { RefListObserver } from '../observers/index.js'; 6 | 7 | /** @internal */ 8 | export class SceneSubject extends Subject { 9 | protected children = new RefListObserver('children', this._documentView); 10 | 11 | constructor(documentView: DocumentViewSubjectAPI, def: SceneDef) { 12 | super(documentView, def, documentView.scenePool.requestBase(new Group()), documentView.scenePool); 13 | this.children.subscribe((nextChildren, prevChildren) => { 14 | if (prevChildren.length) this.value.remove(...prevChildren); 15 | if (nextChildren.length) this.value.add(...nextChildren); 16 | this.publishAll(); 17 | }); 18 | } 19 | 20 | update() { 21 | const def = this.def; 22 | const target = this.value; 23 | 24 | if (def.getName() !== target.name) { 25 | target.name = def.getName(); 26 | } 27 | 28 | this.children.update(def.listChildren()); 29 | } 30 | 31 | dispose() { 32 | this.children.dispose(); 33 | super.dispose(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/view/src/subjects/TextureSubject.ts: -------------------------------------------------------------------------------- 1 | import { Texture } from 'three'; 2 | import { Texture as TextureDef } from '@gltf-transform/core'; 3 | import type { DocumentViewSubjectAPI } from '../DocumentViewImpl.js'; 4 | import { Subject } from './Subject.js'; 5 | 6 | /** @internal */ 7 | export class TextureSubject extends Subject { 8 | private _image: ArrayBuffer | null = null; 9 | 10 | constructor(documentView: DocumentViewSubjectAPI, def: TextureDef) { 11 | super(documentView, def, documentView.imageProvider.loadingTexture, documentView.texturePool); 12 | } 13 | 14 | update() { 15 | const def = this.def; 16 | const value = this.value; 17 | 18 | if (def.getName() !== value.name) { 19 | value.name = def.getName(); 20 | } 21 | 22 | const image = def.getImage() as ArrayBuffer; 23 | if (image !== this._image) { 24 | this._image = image; 25 | if (this.value !== this._documentView.imageProvider.loadingTexture) { 26 | this.pool.releaseBase(this.value); 27 | } 28 | this._documentView.imageProvider.getTexture(def).then((texture: Texture) => { 29 | this.value = this.pool.requestBase(texture); 30 | this.publishAll(); // TODO(perf): Might be wasting cycles here. 31 | }); 32 | } 33 | } 34 | 35 | dispose() { 36 | super.dispose(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/view/src/subjects/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Subject.js'; 2 | export * from './AccessorSubject.js'; 3 | export * from './ExtensionSubject.js'; 4 | export * from './InstancedMeshSubject.js'; 5 | export * from './LightSubject.js'; 6 | export * from './MaterialSubject.js'; 7 | export * from './MeshSubject.js'; 8 | export * from './NodeSubject.js'; 9 | export * from './PrimitiveSubject.js'; 10 | export * from './SceneSubject.js'; 11 | export * from './SkinSubject.js'; 12 | export * from './TextureSubject.js'; 13 | -------------------------------------------------------------------------------- /packages/view/src/utils/Observable.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from '../constants.js'; 2 | 3 | export class Observable { 4 | public value: Value; 5 | private _subscriber: ((next: Value, prev: Value) => void) | null = null; 6 | 7 | constructor(value: Value) { 8 | this.value = value; 9 | } 10 | 11 | public subscribe(subscriber: (next: Value, prev: Value) => void): Subscription { 12 | if (this._subscriber) { 13 | throw new Error('Observable: Limit one subscriber per Observable.'); 14 | } 15 | 16 | this._subscriber = subscriber; 17 | return () => (this._subscriber = null); 18 | } 19 | 20 | public next(value: Value) { 21 | const prevValue = this.value; 22 | this.value = value; 23 | if (this._subscriber) { 24 | this._subscriber(this.value, prevValue); 25 | } 26 | } 27 | 28 | public dispose() { 29 | this._subscriber = null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/view/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { MeshStandardMaterial } from 'three'; 2 | 3 | export * from './Observable.js'; 4 | 5 | export function eq(a: number[], b: number[]): boolean { 6 | if (a.length !== b.length) return false; 7 | for (let i = 0; i < a.length; i++) { 8 | if (a[i] !== b[i]) return false; 9 | } 10 | return true; 11 | } 12 | 13 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 14 | export const DEFAULT_MATERIAL = new MeshStandardMaterial({ 15 | name: '__DefaultMaterial', 16 | color: 0xffffff, 17 | roughness: 1.0, 18 | metalness: 1.0, 19 | }); 20 | 21 | export function semanticToAttributeName(semantic: string): string { 22 | switch (semantic) { 23 | case 'POSITION': 24 | return 'position'; 25 | case 'NORMAL': 26 | return 'normal'; 27 | case 'TANGENT': 28 | return 'tangent'; 29 | case 'COLOR_0': 30 | return 'color'; 31 | case 'JOINTS_0': 32 | return 'skinIndex'; 33 | case 'WEIGHTS_0': 34 | return 'skinWeight'; 35 | case 'TEXCOORD_0': 36 | return 'uv'; 37 | case 'TEXCOORD_1': 38 | return 'uv1'; 39 | case 'TEXCOORD_2': 40 | return 'uv2'; 41 | case 'TEXCOORD_3': 42 | return 'uv3'; 43 | default: 44 | return '_' + semantic.toLowerCase(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/view/test/InstancedMeshSubject.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { JSDOM } from 'jsdom'; 3 | import { Document } from '@gltf-transform/core'; 4 | import { EXTMeshGPUInstancing } from '@gltf-transform/extensions'; 5 | import { DocumentView, NullImageProvider } from '@gltf-transform/view'; 6 | import { Group, InstancedMesh, Object3D } from 'three'; 7 | 8 | global.document = new JSDOM().window.document; 9 | const imageProvider = new NullImageProvider(); 10 | 11 | test('InstancedMeshSubject', async (t) => { 12 | const document = new Document(); 13 | const batchExt = document.createExtension(EXTMeshGPUInstancing); 14 | const batchTranslation = document 15 | .createAccessor() 16 | .setType('VEC3') 17 | .setArray(new Float32Array([0, 0, 0, 10, 0, 0, 20, 0, 0])); 18 | const batchDef = batchExt.createInstancedMesh().setAttribute('TRANSLATION', batchTranslation); 19 | const position = document 20 | .createAccessor() 21 | .setType('VEC3') 22 | .setArray(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0])); 23 | const primDef = document.createPrimitive().setAttribute('POSITION', position); 24 | const meshDef = document.createMesh().addPrimitive(primDef); 25 | const nodeDef = document.createNode().setMesh(meshDef).setExtension('EXT_mesh_gpu_instancing', batchDef); 26 | 27 | const documentView = new DocumentView(document, { imageProvider }); 28 | const node = documentView.view(nodeDef); 29 | const group = node.children[0] as Group; 30 | const mesh = group.children[0] as InstancedMesh; 31 | 32 | t.deepEqual(node.children.map(toType), ['Group'], 'node.children → [Group]'); 33 | t.deepEqual(group.children.map(toType), ['Mesh'], 'group.children → [Mesh]'); 34 | t.is(mesh.isInstancedMesh, true, 'isInstancedMesh → true'); 35 | t.is(mesh.count, 3, 'count → 3'); 36 | }); 37 | 38 | function toType(object: Object3D): string { 39 | return object.type; 40 | } 41 | -------------------------------------------------------------------------------- /packages/view/test/MeshSubject.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { JSDOM } from 'jsdom'; 3 | import { Document } from '@gltf-transform/core'; 4 | import { DocumentView, NullImageProvider } from '@gltf-transform/view'; 5 | 6 | global.document = new JSDOM().window.document; 7 | const imageProvider = new NullImageProvider(); 8 | 9 | test('MeshSubject', async (t) => { 10 | const document = new Document(); 11 | const position = document 12 | .createAccessor() 13 | .setType('VEC3') 14 | .setArray(new Float32Array([0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0])); 15 | const primDef = document.createPrimitive().setAttribute('POSITION', position); 16 | const meshDef = document.createMesh().setName('MyMesh').addPrimitive(primDef); 17 | 18 | const documentView = new DocumentView(document, { imageProvider }); 19 | const mesh = documentView.view(meshDef); 20 | 21 | t.is(mesh.name, 'MyMesh', 'mesh → name'); 22 | 23 | meshDef.setName('MyMeshRenamed'); 24 | t.is(mesh.name, 'MyMeshRenamed', 'mesh → name (2)'); 25 | 26 | t.is(mesh.children[0].type, 'Mesh', 'mesh → prim (initial)'); 27 | 28 | meshDef.removePrimitive(primDef); 29 | t.is(mesh.children.length, 0, 'mesh → prim (remove)'); 30 | 31 | meshDef.addPrimitive(primDef); 32 | t.is(mesh.children.length, 1, 'mesh → prim (add)'); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/view/test/PrimitiveSubject.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { JSDOM } from 'jsdom'; 3 | import { Document, Primitive as PrimitiveDef } from '@gltf-transform/core'; 4 | import { DocumentView, NullImageProvider } from '@gltf-transform/view'; 5 | 6 | global.document = new JSDOM().window.document; 7 | const imageProvider = new NullImageProvider(); 8 | 9 | test('PrimitiveSubject', async (t) => { 10 | const document = new Document(); 11 | const position = document 12 | .createAccessor() 13 | .setType('VEC3') 14 | .setArray(new Float32Array([0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0])); 15 | const materialDef = document.createMaterial('MyMaterial'); 16 | const primDef = document.createPrimitive().setAttribute('POSITION', position).setMaterial(materialDef); 17 | 18 | const documentView = new DocumentView(document, { imageProvider }); 19 | let prim = documentView.view(primDef); 20 | const geometry = prim.geometry; 21 | 22 | const disposed = new Set(); 23 | geometry.addEventListener('dispose', () => disposed.add(geometry)); 24 | 25 | t.is(prim.type, 'Mesh', 'Mesh'); 26 | 27 | primDef.setMode(PrimitiveDef.Mode.POINTS); 28 | prim = documentView.view(primDef); 29 | 30 | t.is(prim.type, 'Points', 'Points'); 31 | 32 | primDef.setMode(PrimitiveDef.Mode.LINES); 33 | prim = documentView.view(primDef); 34 | 35 | t.is(prim.type, 'LineSegments', 'LineSegments'); 36 | 37 | primDef.setMode(PrimitiveDef.Mode.LINE_LOOP); 38 | prim = documentView.view(primDef); 39 | 40 | t.is(prim.type, 'LineLoop', 'LineLoop'); 41 | 42 | primDef.setMode(PrimitiveDef.Mode.LINE_STRIP); 43 | prim = documentView.view(primDef); 44 | 45 | t.is(prim.type, 'Line', 'Line'); 46 | 47 | t.is(prim.material.name, 'MyMaterial', 'prim.material → material'); 48 | 49 | primDef.setMaterial(null); 50 | 51 | t.is(prim.material.name, '__DefaultMaterial', 'prim.material → null'); 52 | 53 | t.is(disposed.size, 0, 'preserve geometry'); 54 | 55 | primDef.dispose(); 56 | 57 | t.is(disposed.size, 1, 'dispose geometry'); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/view/test/SceneSubject.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { JSDOM } from 'jsdom'; 3 | import { Document, Node } from '@gltf-transform/core'; 4 | import { DocumentView, NullImageProvider } from '@gltf-transform/view'; 5 | 6 | global.document = new JSDOM().window.document; 7 | const imageProvider = new NullImageProvider(); 8 | 9 | test('SceneBinding', async (t) => { 10 | const document = new Document(); 11 | let nodeDef: Node; 12 | const sceneDef = document 13 | .createScene('MyScene') 14 | .addChild(document.createNode('Node1')) 15 | .addChild((nodeDef = document.createNode('Node2'))) 16 | .addChild(document.createNode('Node3')); 17 | nodeDef.addChild(document.createNode('Node4')); 18 | 19 | const documentView = new DocumentView(document, { imageProvider }); 20 | const scene = documentView.view(sceneDef); 21 | 22 | t.is(scene.name, 'MyScene', 'scene → name'); 23 | sceneDef.setName('MySceneRenamed'); 24 | t.is(scene.name, 'MySceneRenamed', 'scene → name (renamed)'); 25 | t.is(scene.children.length, 3, 'scene → children → 3'); 26 | 27 | t.is(scene.children[1].children[0].name, 'Node4', 'scene → ... → grandchild'); 28 | nodeDef.listChildren()[0].dispose(); 29 | t.is(scene.children[1].children.length, 0, 'scene → ... → grandchild (dispose)'); 30 | 31 | nodeDef.dispose(); 32 | t.is(scene.children.length, 2, 'scene → children → 2'); 33 | sceneDef.removeChild(sceneDef.listChildren()[0]); 34 | t.is(scene.children.length, 1, 'scene → children → 1'); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/view/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["./**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/**/*", "../global.d.ts"], 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "strict": true, 7 | "noImplicitAny": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | ## Roundtrip tests 4 | 5 | Integration tests, completing a read/write roundtrip on the [KhronosGroup/glTF-Sample-Models](https://github.com/KhronosGroup/glTF-Sample-Models) repository. Test constants assume that the glTF-Transform and glTF-Sample-Models repositories are siblings under a common parent folder. After generating roundtrip models, serve the `test/` folder locally and confirm that the before/after examples match. 6 | 7 | ``` 8 | # Run. 9 | node scripts/roundtrip.cjs 10 | 11 | # Verify. 12 | npx serve scripts 13 | ``` 14 | 15 | For unit tests on individual packages, see `packages/*/test`. 16 | -------------------------------------------------------------------------------- /scripts/check-release.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import semver from 'semver'; 3 | 4 | const version = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim(); 5 | 6 | if (!semver.valid(version)) { 7 | console.error(`🚨 Invalid version, "${version}".`); 8 | process.exit(1); // 'general error' 9 | } else if (semver.prerelease(version) !== null) { 10 | console.warn(`⚠️ Not a stable release.`); 11 | process.exit(126); // 'cannot execute' 12 | } else { 13 | process.exit(0); // 'ok' 14 | } 15 | -------------------------------------------------------------------------------- /scripts/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@gltf-transform/core": "../packages/core/dist/index.modern.js", 4 | "@gltf-transform/extensions": "../packages/extensions/dist/index.modern.js", 5 | "@gltf-transform/functions": "../packages/functions/dist/index.modern.js", 6 | "property-graph": "npm:property-graph", 7 | "ktx-parse": "npm:ktx-parse" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "npm:@types/node": "npm:@types/node@18.14.6", 6 | "npm:ktx-parse": "npm:ktx-parse@0.7.0", 7 | "npm:property-graph": "npm:property-graph@2.0.0" 8 | }, 9 | "npm": { 10 | "@types/node@18.14.6": { 11 | "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", 12 | "dependencies": {} 13 | }, 14 | "ktx-parse@0.7.0": { 15 | "integrity": "sha512-naezun/2iiWrantwoRI9mw6E4iN41ggYzJSR9XAZzf6+rv+2Tb1yYN8VJhGsA0uptBexE0m4GDh+iiQhYpW+Qw==", 16 | "dependencies": {} 17 | }, 18 | "property-graph@2.0.0": { 19 | "integrity": "sha512-peDswWfLn7Lx+iPIxefhEUbLC7cs1KbfyKqOl5C5TF5F6urnyAORxzQ7JY21yjRzH7CpLa6+d1G+BemBr/lKyw==", 20 | "dependencies": {} 21 | } 22 | } 23 | }, 24 | "remote": { 25 | "https://deno.land/std@0.178.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", 26 | "https://deno.land/std@0.178.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", 27 | "https://deno.land/std@0.178.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", 28 | "https://deno.land/std@0.178.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab" 29 | }, 30 | "workspace": { 31 | "dependencies": [ 32 | "npm:ktx-parse", 33 | "npm:property-graph" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/deno_test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertEquals } from 'https://deno.land/std@0.178.0/testing/asserts.ts'; 2 | import { DenoIO } from '@gltf-transform/core'; 3 | import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; 4 | import path from 'node:path'; 5 | 6 | Deno.test('@gltf-transform/core::deno | read', async () => { 7 | const io = new DenoIO(path).registerExtensions(ALL_EXTENSIONS); 8 | const document = await io.read('packages/core/test/in/BoxVertexColors.glb'); 9 | assert(document, 'reads document'); 10 | assertEquals(document.getRoot().listScenes().length, 1, 'reads 1 scene'); 11 | }); 12 | -------------------------------------------------------------------------------- /scripts/out/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donmccurdy/glTF-Transform/9a702244917e3ff74239bc494ff662fa907ba8bb/scripts/out/.gitkeep -------------------------------------------------------------------------------- /scripts/validate.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const glob = require('glob'); 6 | const validator = require('gltf-validator'); 7 | 8 | const verbose = process.argv.indexOf('--verbose') > 0; 9 | 10 | const messages = { 11 | numAssets: 0, 12 | numFailed: 0, 13 | numErrors: 0, 14 | numWarnings: 0, 15 | numInfos: 0, 16 | numHints: 0, 17 | failed: [], 18 | reports: [], 19 | }; 20 | 21 | const pending = []; 22 | 23 | const validateURI = (uri) => { 24 | const asset = fs.readFileSync(uri); 25 | const dir = path.dirname(uri); 26 | const promise = validator 27 | .validateBytes(new Uint8Array(asset), { 28 | uri, 29 | externalResourceFunction: (uri) => 30 | new Promise((resolve) => { 31 | uri = path.resolve(dir, decodeURIComponent(uri)); 32 | const buffer = fs.readFileSync(uri); 33 | resolve(new Uint8Array(buffer)); 34 | }), 35 | }) 36 | .then((report) => { 37 | messages.numAssets++; 38 | messages.numErrors += report.issues.numErrors; 39 | messages.numWarnings += report.issues.numWarnings; 40 | messages.numInfos += report.issues.numInfos; 41 | messages.numHints += report.issues.numHints; 42 | if (verbose) { 43 | messages.reports.push(report); 44 | } 45 | if (report.issues.numErrors || report.issues.numWarnings) { 46 | messages.failed.push(uri); 47 | } 48 | }) 49 | .catch((error) => { 50 | messages.numFailed++; 51 | failed.push([uri, error]); 52 | }); 53 | pending.push(promise); 54 | }; 55 | 56 | glob.sync(path.join(__dirname, '../packages/*/test/out/**/*.glb')).forEach(validateURI); 57 | glob.sync(path.join(__dirname, '../packages/*/test/out/**/*.gltf')).forEach(validateURI); 58 | 59 | Promise.all(pending) 60 | .catch(() => true) 61 | .then(() => { 62 | console.log(JSON.stringify(messages, null, 2)); 63 | if (messages.numFailed || messages.numErrors || messages.numWarnings) { 64 | process.exit(2); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./packages", 4 | "paths": { 5 | "@gltf-transform/core": ["./packages/core/"], 6 | "@gltf-transform/extensions": ["./packages/extensions/"], 7 | "@gltf-transform/functions": ["./packages/functions/"], 8 | "@gltf-transform/cli": ["./packages/cli/"], 9 | "@gltf-transform/test-utils": ["./packages/test-utils/"], 10 | "@gltf-transform/view": ["./packages/view/"] 11 | }, 12 | "esModuleInterop": true, 13 | "moduleResolution": "nodenext", 14 | "lib": ["es2020", "dom"], 15 | "target": "es2020", 16 | "module": "nodenext", 17 | "declaration": true, 18 | "stripInternal": true, 19 | 20 | // Components of "strict" mode. 21 | "alwaysStrict": true, 22 | "strictFunctionTypes": true, 23 | "strictBindCallApply": true, 24 | "noImplicitThis": true 25 | 26 | // Certain rules (noImplicitAny, strictNullChecks, strictPropertyInitialization) 27 | // are tightend by sub-package tsconfig. Global tsconfig affects unit tests, and 28 | // I consider those rules too strict for unit tests. 29 | }, 30 | "include": [ 31 | "packages/*/src/**/*.ts", 32 | // Included to allow TypeDoc to find `pages.ts` content. 33 | "docs/*.ts" 34 | ] 35 | } 36 | --------------------------------------------------------------------------------