@developerz.ai/ts23d 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/bug-hunter.md +21 -0
- package/.claude/agents/docs-author.md +13 -0
- package/.claude/agents/dx-auditor.md +14 -0
- package/.claude/agents/primitive-author.md +22 -0
- package/.claude/agents/recipe-author.md +36 -0
- package/.claude/agents/visual-reviewer.md +23 -0
- package/.claude/commands/feature.md +28 -0
- package/.claude/commands/new-recipe.md +19 -0
- package/.claude/commands/release.md +14 -0
- package/.claude/commands/verify.md +9 -0
- package/.claude/skills/author-recipe/SKILL.md +93 -0
- package/CHANGELOG.md +90 -0
- package/CLAUDE.md +85 -0
- package/LICENSE +21 -0
- package/README.md +385 -2
- package/dist/chunk-6b05mnmx.js +19 -0
- package/dist/chunk-6b05mnmx.js.map +11 -0
- package/dist/chunk-7rtfjh9f.js +45 -0
- package/dist/chunk-7rtfjh9f.js.map +10 -0
- package/dist/chunk-8j4c28jt.js +4085 -0
- package/dist/chunk-8j4c28jt.js.map +46 -0
- package/dist/chunk-d1fv6hrm.js +2532 -0
- package/dist/chunk-d1fv6hrm.js.map +43 -0
- package/dist/chunk-dbq4qf52.js +902 -0
- package/dist/chunk-dbq4qf52.js.map +25 -0
- package/dist/chunk-drna2rny.js +836 -0
- package/dist/chunk-drna2rny.js.map +17 -0
- package/dist/chunk-dxvypr7y.js +89 -0
- package/dist/chunk-dxvypr7y.js.map +11 -0
- package/dist/chunk-f6yme003.js +1589 -0
- package/dist/chunk-f6yme003.js.map +30 -0
- package/dist/chunk-hg7pf34j.js +465 -0
- package/dist/chunk-hg7pf34j.js.map +19 -0
- package/dist/chunk-jq4e17tj.js +1031 -0
- package/dist/chunk-jq4e17tj.js.map +21 -0
- package/dist/chunk-y63c6zcv.js +2668 -0
- package/dist/chunk-y63c6zcv.js.map +53 -0
- package/dist/cli/args.d.ts +30 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +348 -0
- package/dist/cli/bin.js.map +15 -0
- package/dist/cli/load.d.ts +2 -0
- package/dist/cli/manifest.d.ts +53 -0
- package/dist/cli/render-cmds.d.ts +12 -0
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/texture-cache.d.ts +4 -0
- package/dist/core/asset.d.ts +10 -0
- package/dist/core/definition.d.ts +155 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/family.d.ts +23 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +69 -0
- package/dist/core/index.js.map +9 -0
- package/dist/core/mesh.d.ts +35 -0
- package/dist/core/part.d.ts +114 -0
- package/dist/core/png.d.ts +1 -0
- package/dist/core/rng.d.ts +12 -0
- package/dist/core/transform.d.ts +20 -0
- package/dist/core/vec.d.ts +11 -0
- package/dist/core/world.d.ts +16 -0
- package/dist/export/bin-writer.d.ts +16 -0
- package/dist/export/container.d.ts +3 -0
- package/dist/export/credits.d.ts +4 -0
- package/dist/export/glb-options.d.ts +48 -0
- package/dist/export/glb.d.ts +8 -0
- package/dist/export/hierarchy.d.ts +23 -0
- package/dist/export/index.d.ts +5 -0
- package/dist/export/index.js +40 -0
- package/dist/export/index.js.map +9 -0
- package/dist/export/lights.d.ts +8 -0
- package/dist/export/lod-coverage.d.ts +12 -0
- package/dist/export/materials.d.ts +23 -0
- package/dist/export/merge-pivots.d.ts +6 -0
- package/dist/export/metadata.d.ts +21 -0
- package/dist/export/motion.d.ts +9 -0
- package/dist/export/optimize-parts.d.ts +26 -0
- package/dist/export/primitive.d.ts +3 -0
- package/dist/export/tangents.d.ts +2 -0
- package/dist/export/textures.d.ts +22 -0
- package/dist/export/version.d.ts +2 -0
- package/dist/grammar/index.d.ts +2 -0
- package/dist/grammar/index.js +16 -0
- package/dist/grammar/index.js.map +9 -0
- package/dist/grammar/split.d.ts +21 -0
- package/dist/grammar/whyte.d.ts +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +457 -0
- package/dist/index.js.map +9 -0
- package/dist/kinds/building.d.ts +44 -0
- package/dist/kinds/faces.d.ts +27 -0
- package/dist/kinds/head-armour.d.ts +5 -0
- package/dist/kinds/head-beard.d.ts +6 -0
- package/dist/kinds/head-bust.d.ts +5 -0
- package/dist/kinds/head-extras.d.ts +15 -0
- package/dist/kinds/head-face.d.ts +8 -0
- package/dist/kinds/head-garb.d.ts +4 -0
- package/dist/kinds/head-geom.d.ts +7 -0
- package/dist/kinds/head-hair.d.ts +4 -0
- package/dist/kinds/head-layout.d.ts +29 -0
- package/dist/kinds/head-mesh.d.ts +31 -0
- package/dist/kinds/head-mouth.d.ts +4 -0
- package/dist/kinds/head-params.d.ts +116 -0
- package/dist/kinds/head-presets.d.ts +19 -0
- package/dist/kinds/head-rim.d.ts +10 -0
- package/dist/kinds/head-tone.d.ts +10 -0
- package/dist/kinds/head-torso.d.ts +35 -0
- package/dist/kinds/head.d.ts +39 -0
- package/dist/kinds/hull-lines.d.ts +65 -0
- package/dist/kinds/hull-skin.d.ts +6 -0
- package/dist/kinds/hull.d.ts +14 -0
- package/dist/kinds/index.d.ts +16 -0
- package/dist/kinds/index.js +78 -0
- package/dist/kinds/index.js.map +9 -0
- package/dist/kinds/mass.d.ts +15 -0
- package/dist/kinds/openings.d.ts +30 -0
- package/dist/kinds/planned.d.ts +6 -0
- package/dist/kinds/roof-chimney.d.ts +16 -0
- package/dist/kinds/roof-shape.d.ts +64 -0
- package/dist/kinds/roof.d.ts +31 -0
- package/dist/kinds/vegetation-fronds.d.ts +15 -0
- package/dist/kinds/vegetation-impostor.d.ts +9 -0
- package/dist/kinds/vegetation-leaves.d.ts +15 -0
- package/dist/kinds/vegetation-mesh.d.ts +21 -0
- package/dist/kinds/vegetation-params.d.ts +90 -0
- package/dist/kinds/vegetation-presets.d.ts +20 -0
- package/dist/kinds/vegetation-skeleton.d.ts +20 -0
- package/dist/kinds/vegetation.d.ts +42 -0
- package/dist/kinds/vehicle.d.ts +42 -0
- package/dist/material/cc0.d.ts +4 -0
- package/dist/material/define.d.ts +34 -0
- package/dist/material/generator.d.ts +4 -0
- package/dist/material/index.d.ts +9 -0
- package/dist/material/index.js +70 -0
- package/dist/material/index.js.map +9 -0
- package/dist/material/library.d.ts +7 -0
- package/dist/material/material-def.d.ts +30 -0
- package/dist/material/paint/bond.d.ts +3 -0
- package/dist/material/paint/canvas.d.ts +39 -0
- package/dist/material/paint/cell.d.ts +4 -0
- package/dist/material/paint/cells-round.d.ts +5 -0
- package/dist/material/paint/cells.d.ts +7 -0
- package/dist/material/paint/compose.d.ts +5 -0
- package/dist/material/paint/concrete.d.ts +3 -0
- package/dist/material/paint/courses.d.ts +31 -0
- package/dist/material/paint/decode-png.d.ts +6 -0
- package/dist/material/paint/default-sheet.d.ts +4 -0
- package/dist/material/paint/fabric.d.ts +4 -0
- package/dist/material/paint/foliage-broad.d.ts +5 -0
- package/dist/material/paint/foliage-common.d.ts +35 -0
- package/dist/material/paint/foliage-compound.d.ts +4 -0
- package/dist/material/paint/foliage-conifer.d.ts +5 -0
- package/dist/material/paint/foliage-raster.d.ts +26 -0
- package/dist/material/paint/foliage.d.ts +3 -0
- package/dist/material/paint/image-strip.d.ts +4 -0
- package/dist/material/paint/kit.d.ts +5 -0
- package/dist/material/paint/masks.d.ts +3 -0
- package/dist/material/paint/metal.d.ts +3 -0
- package/dist/material/paint/noise.d.ts +9 -0
- package/dist/material/paint/normal.d.ts +8 -0
- package/dist/material/paint/orm.d.ts +8 -0
- package/dist/material/paint/planks.d.ts +3 -0
- package/dist/material/paint/plates.d.ts +3 -0
- package/dist/material/paint/shapes.d.ts +8 -0
- package/dist/material/paint/slate.d.ts +3 -0
- package/dist/material/paint/stamp.d.ts +3 -0
- package/dist/material/paint/strip.d.ts +3 -0
- package/dist/material/paint/surface.d.ts +5 -0
- package/dist/material/paint/types.d.ts +65 -0
- package/dist/material/paint/validate.d.ts +3 -0
- package/dist/material/palette.d.ts +14 -0
- package/dist/material/shader-godot.d.ts +2 -0
- package/dist/material/shader-three.d.ts +2 -0
- package/dist/material/shader.d.ts +46 -0
- package/dist/material/slots.d.ts +5 -0
- package/dist/material/texture-source.d.ts +34 -0
- package/dist/material/trim-sheet.d.ts +38 -0
- package/dist/neural/generator.d.ts +25 -0
- package/dist/neural/images.d.ts +17 -0
- package/dist/neural/index.d.ts +2 -0
- package/dist/neural/index.js +14 -0
- package/dist/neural/index.js.map +9 -0
- package/dist/passes/bake-ao.d.ts +17 -0
- package/dist/passes/grime.d.ts +15 -0
- package/dist/passes/index.d.ts +6 -0
- package/dist/passes/index.js +22 -0
- package/dist/passes/index.js.map +9 -0
- package/dist/passes/livery.d.ts +19 -0
- package/dist/passes/optimize-cache.d.ts +15 -0
- package/dist/passes/optimize-weld.d.ts +15 -0
- package/dist/passes/optimize.d.ts +28 -0
- package/dist/passes/ray-grid.d.ts +23 -0
- package/dist/passes/weighted-normals.d.ts +8 -0
- package/dist/primitives/beam.d.ts +17 -0
- package/dist/primitives/bevel-box.d.ts +13 -0
- package/dist/primitives/box.d.ts +16 -0
- package/dist/primitives/cell.d.ts +20 -0
- package/dist/primitives/check.d.ts +12 -0
- package/dist/primitives/closed.d.ts +11 -0
- package/dist/primitives/erosion.d.ts +22 -0
- package/dist/primitives/font-5x7.d.ts +3 -0
- package/dist/primitives/heightfield.d.ts +16 -0
- package/dist/primitives/index.d.ts +23 -0
- package/dist/primitives/index.js +81 -0
- package/dist/primitives/index.js.map +9 -0
- package/dist/primitives/lathe-facets.d.ts +8 -0
- package/dist/primitives/lathe.d.ts +26 -0
- package/dist/primitives/lattice.d.ts +30 -0
- package/dist/primitives/loft.d.ts +15 -0
- package/dist/primitives/mc-table.d.ts +2 -0
- package/dist/primitives/member.d.ts +19 -0
- package/dist/primitives/noise.d.ts +34 -0
- package/dist/primitives/planar.d.ts +10 -0
- package/dist/primitives/prism.d.ts +13 -0
- package/dist/primitives/sculpt-edit.d.ts +56 -0
- package/dist/primitives/sculpt-tree.d.ts +23 -0
- package/dist/primitives/sculpt.d.ts +27 -0
- package/dist/primitives/sdf-grid.d.ts +17 -0
- package/dist/primitives/sdf-ops.d.ts +37 -0
- package/dist/primitives/sdf.d.ts +21 -0
- package/dist/primitives/strip-uv.d.ts +11 -0
- package/dist/primitives/surface-nets.d.ts +7 -0
- package/dist/primitives/sweep.d.ts +19 -0
- package/dist/primitives/terrain-color.d.ts +30 -0
- package/dist/primitives/terrain-grid.d.ts +36 -0
- package/dist/primitives/text-layout.d.ts +22 -0
- package/dist/primitives/text.d.ts +27 -0
- package/dist/primitives/tiled.d.ts +22 -0
- package/dist/primitives/transport.d.ts +17 -0
- package/dist/primitives/triangulate.d.ts +2 -0
- package/dist/primitives/value-noise.d.ts +10 -0
- package/dist/qa/checks.d.ts +11 -0
- package/dist/qa/clash.d.ts +2 -0
- package/dist/qa/floating.d.ts +5 -0
- package/dist/qa/gates.d.ts +12 -0
- package/dist/qa/geometry.d.ts +21 -0
- package/dist/qa/index.d.ts +12 -0
- package/dist/qa/index.js +78 -0
- package/dist/qa/index.js.map +9 -0
- package/dist/qa/inside.d.ts +12 -0
- package/dist/qa/inspect.d.ts +18 -0
- package/dist/qa/lods.d.ts +12 -0
- package/dist/qa/measures.d.ts +4 -0
- package/dist/qa/occlusion.d.ts +3 -0
- package/dist/qa/sanity.d.ts +7 -0
- package/dist/qa/types.d.ts +12 -0
- package/dist/render/beauty.d.ts +40 -0
- package/dist/render/camera.d.ts +33 -0
- package/dist/render/depth-map.d.ts +38 -0
- package/dist/render/font.d.ts +1 -0
- package/dist/render/ground.d.ts +13 -0
- package/dist/render/index.d.ts +10 -0
- package/dist/render/index.js +47 -0
- package/dist/render/index.js.map +9 -0
- package/dist/render/lighting.d.ts +27 -0
- package/dist/render/lod-strip.d.ts +5 -0
- package/dist/render/metrics.d.ts +37 -0
- package/dist/render/options.d.ts +28 -0
- package/dist/render/photo-match.d.ts +19 -0
- package/dist/render/raster.d.ts +25 -0
- package/dist/render/resolve.d.ts +11 -0
- package/dist/render/review-prompt.d.ts +2 -0
- package/dist/render/review-warnings.d.ts +34 -0
- package/dist/render/review.d.ts +24 -0
- package/dist/render/scene.d.ts +18 -0
- package/dist/render/shade.d.ts +22 -0
- package/dist/render/sheet.d.ts +15 -0
- package/dist/render/texture.d.ts +15 -0
- package/dist/render/thumbnail.d.ts +8 -0
- package/dist/render/vis-buffer.d.ts +18 -0
- package/dist/testing/assert.d.ts +21 -0
- package/dist/testing/compiled.d.ts +15 -0
- package/dist/testing/hash.d.ts +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +13 -0
- package/dist/testing/snapshot.d.ts +13 -0
- package/docs/README.md +56 -0
- package/docs/architecture/README.md +68 -0
- package/docs/architecture/adding-a-kind.md +44 -0
- package/docs/architecture/adding-a-primitive.md +49 -0
- package/docs/architecture/errors.md +47 -0
- package/docs/architecture/glb-production.md +214 -0
- package/docs/architecture/gltf-capabilities.md +117 -0
- package/docs/architecture/materials.md +151 -0
- package/docs/decisions/001-recipe-compiler-over-ai-meshes.md +27 -0
- package/docs/decisions/002-typescript-not-json.md +22 -0
- package/docs/decisions/003-shared-trim-sheet.md +21 -0
- package/docs/decisions/004-z-up-metres.md +21 -0
- package/docs/decisions/005-zero-runtime-deps.md +21 -0
- package/docs/decisions/006-single-package-subpath-exports.md +20 -0
- package/docs/decisions/007-cpu-contact-sheets.md +21 -0
- package/docs/decisions/008-surface-materials.md +21 -0
- package/docs/decisions/009-gltf-front-axis.md +21 -0
- package/docs/decisions/README.md +15 -0
- package/docs/idea/01-vision.md +117 -0
- package/docs/idea/02-dsl.md +343 -0
- package/docs/idea/03-ir.md +97 -0
- package/docs/idea/04-materials.md +114 -0
- package/docs/idea/05-agent-loop.md +141 -0
- package/docs/idea/06-qa.md +101 -0
- package/docs/idea/07-what-fits.md +45 -0
- package/docs/idea/08-neural-adapters.md +78 -0
- package/docs/idea/09-kinds.md +105 -0
- package/docs/idea/10-sculpting.md +166 -0
- package/docs/idea/11-materials-authoring.md +132 -0
- package/docs/idea/12-blender-bridge.md +136 -0
- package/docs/idea/13-asset-pipeline-research.md +89 -0
- package/docs/landscape.md +49 -0
- package/docs/plans/roadmap.md +139 -0
- package/examples/build-with-bun.ts +41 -0
- package/llms.txt +47 -0
- package/package.json +100 -4
- package/recipes/README.md +38 -0
- package/recipes/asteroid/README.md +46 -0
- package/recipes/asteroid/asteroid.ts +336 -0
- package/recipes/boulders/README.md +28 -0
- package/recipes/boulders/boulders.ts +117 -0
- package/recipes/boxcar/README.md +53 -0
- package/recipes/boxcar/boxcar.ts +1000 -0
- package/recipes/cargo_container_scifi/README.md +52 -0
- package/recipes/cargo_container_scifi/cargo_container_scifi.ts +461 -0
- package/recipes/dropship/README.md +84 -0
- package/recipes/dropship/dropship.ts +1144 -0
- package/recipes/eiffel_tower/README.md +110 -0
- package/recipes/eiffel_tower/eiffel_tower.ts +781 -0
- package/recipes/goblin_head/README.md +75 -0
- package/recipes/goblin_head/goblin_head.ts +38 -0
- package/recipes/human_head/README.md +68 -0
- package/recipes/human_head/human_head.ts +38 -0
- package/recipes/loco_440/README.md +32 -0
- package/recipes/loco_440/loco_440.ts +1404 -0
- package/recipes/material_swatches/README.md +21 -0
- package/recipes/material_swatches/material_swatches.ts +189 -0
- package/recipes/mountain_range/README.md +45 -0
- package/recipes/mountain_range/mountain_range.ts +128 -0
- package/recipes/oak/README.md +30 -0
- package/recipes/oak/oak.ts +19 -0
- package/recipes/orc_head/README.md +78 -0
- package/recipes/orc_head/orc_head.ts +41 -0
- package/recipes/palm/README.md +30 -0
- package/recipes/palm/palm.ts +19 -0
- package/recipes/pine/README.md +29 -0
- package/recipes/pine/pine.ts +19 -0
- package/recipes/signal_gantry/README.md +36 -0
- package/recipes/signal_gantry/signal_gantry.ts +522 -0
- package/recipes/station_halt/README.md +39 -0
- package/recipes/station_halt/station_halt.ts +722 -0
- package/recipes/t34/README.md +44 -0
- package/recipes/t34/t34.ts +1068 -0
- package/recipes/tiger_1/README.md +75 -0
- package/recipes/tiger_1/tiger_1.ts +1370 -0
- package/recipes/water_tower/README.md +40 -0
- package/recipes/water_tower/water_tower.ts +542 -0
- package/src/cli/args.ts +125 -0
- package/src/cli/bin.ts +7 -0
- package/src/cli/load.ts +17 -0
- package/src/cli/manifest.ts +95 -0
- package/src/cli/render-cmds.ts +66 -0
- package/src/cli/run.ts +192 -0
- package/src/cli/texture-cache.ts +24 -0
- package/src/core/asset.ts +129 -0
- package/src/core/definition.ts +156 -0
- package/src/core/errors.ts +50 -0
- package/src/core/family.ts +50 -0
- package/src/core/index.ts +66 -0
- package/src/core/mesh.ts +114 -0
- package/src/core/part.ts +130 -0
- package/src/core/png.ts +49 -0
- package/src/core/rng.ts +49 -0
- package/src/core/transform.ts +90 -0
- package/src/core/vec.ts +26 -0
- package/src/core/world.ts +80 -0
- package/src/export/bin-writer.ts +79 -0
- package/src/export/container.ts +35 -0
- package/src/export/credits.ts +25 -0
- package/src/export/glb-options.ts +53 -0
- package/src/export/glb.ts +187 -0
- package/src/export/hierarchy.ts +122 -0
- package/src/export/index.ts +34 -0
- package/src/export/lights.ts +49 -0
- package/src/export/lod-coverage.ts +71 -0
- package/src/export/materials.ts +126 -0
- package/src/export/merge-pivots.ts +146 -0
- package/src/export/metadata.ts +62 -0
- package/src/export/motion.ts +70 -0
- package/src/export/optimize-parts.ts +80 -0
- package/src/export/primitive.ts +35 -0
- package/src/export/tangents.ts +75 -0
- package/src/export/textures.ts +88 -0
- package/src/export/version.ts +6 -0
- package/src/grammar/index.ts +4 -0
- package/src/grammar/split.ts +87 -0
- package/src/grammar/whyte.ts +20 -0
- package/src/index.ts +14 -0
- package/src/kinds/building.ts +153 -0
- package/src/kinds/faces.ts +138 -0
- package/src/kinds/head-armour.ts +181 -0
- package/src/kinds/head-beard.ts +133 -0
- package/src/kinds/head-bust.ts +184 -0
- package/src/kinds/head-extras.ts +150 -0
- package/src/kinds/head-face.ts +242 -0
- package/src/kinds/head-garb.ts +181 -0
- package/src/kinds/head-geom.ts +31 -0
- package/src/kinds/head-hair.ts +193 -0
- package/src/kinds/head-layout.ts +63 -0
- package/src/kinds/head-mesh.ts +195 -0
- package/src/kinds/head-mouth.ts +111 -0
- package/src/kinds/head-params.ts +162 -0
- package/src/kinds/head-presets.ts +155 -0
- package/src/kinds/head-rim.ts +128 -0
- package/src/kinds/head-tone.ts +52 -0
- package/src/kinds/head-torso.ts +230 -0
- package/src/kinds/head.ts +176 -0
- package/src/kinds/hull-lines.ts +160 -0
- package/src/kinds/hull-skin.ts +83 -0
- package/src/kinds/hull.ts +209 -0
- package/src/kinds/index.ts +63 -0
- package/src/kinds/mass.ts +110 -0
- package/src/kinds/openings.ts +141 -0
- package/src/kinds/planned.ts +12 -0
- package/src/kinds/roof-chimney.ts +100 -0
- package/src/kinds/roof-shape.ts +230 -0
- package/src/kinds/roof.ts +250 -0
- package/src/kinds/vegetation-fronds.ts +126 -0
- package/src/kinds/vegetation-impostor.ts +118 -0
- package/src/kinds/vegetation-leaves.ts +168 -0
- package/src/kinds/vegetation-mesh.ts +174 -0
- package/src/kinds/vegetation-params.ts +147 -0
- package/src/kinds/vegetation-presets.ts +184 -0
- package/src/kinds/vegetation-skeleton.ts +168 -0
- package/src/kinds/vegetation.ts +199 -0
- package/src/kinds/vehicle.ts +144 -0
- package/src/material/cc0.ts +41 -0
- package/src/material/define.ts +130 -0
- package/src/material/generator.ts +63 -0
- package/src/material/index.ts +60 -0
- package/src/material/library.ts +196 -0
- package/src/material/material-def.ts +84 -0
- package/src/material/paint/bond.ts +60 -0
- package/src/material/paint/canvas.ts +101 -0
- package/src/material/paint/cell.ts +39 -0
- package/src/material/paint/cells-round.ts +119 -0
- package/src/material/paint/cells.ts +111 -0
- package/src/material/paint/compose.ts +53 -0
- package/src/material/paint/concrete.ts +62 -0
- package/src/material/paint/courses.ts +85 -0
- package/src/material/paint/decode-png.ts +106 -0
- package/src/material/paint/default-sheet.ts +73 -0
- package/src/material/paint/fabric.ts +63 -0
- package/src/material/paint/foliage-broad.ts +126 -0
- package/src/material/paint/foliage-common.ts +93 -0
- package/src/material/paint/foliage-compound.ts +135 -0
- package/src/material/paint/foliage-conifer.ts +143 -0
- package/src/material/paint/foliage-raster.ts +196 -0
- package/src/material/paint/foliage.ts +23 -0
- package/src/material/paint/image-strip.ts +88 -0
- package/src/material/paint/kit.ts +37 -0
- package/src/material/paint/masks.ts +46 -0
- package/src/material/paint/metal.ts +109 -0
- package/src/material/paint/noise.ts +52 -0
- package/src/material/paint/normal.ts +52 -0
- package/src/material/paint/orm.ts +108 -0
- package/src/material/paint/planks.ts +101 -0
- package/src/material/paint/plates.ts +72 -0
- package/src/material/paint/shapes.ts +28 -0
- package/src/material/paint/slate.ts +52 -0
- package/src/material/paint/stamp.ts +40 -0
- package/src/material/paint/strip.ts +48 -0
- package/src/material/paint/surface.ts +60 -0
- package/src/material/paint/types.ts +94 -0
- package/src/material/paint/validate.ts +74 -0
- package/src/material/palette.ts +46 -0
- package/src/material/shader-godot.ts +77 -0
- package/src/material/shader-three.ts +103 -0
- package/src/material/shader.ts +100 -0
- package/src/material/slots.ts +18 -0
- package/src/material/texture-source.ts +86 -0
- package/src/material/trim-sheet.ts +166 -0
- package/src/neural/generator.ts +37 -0
- package/src/neural/images.ts +27 -0
- package/src/neural/index.ts +10 -0
- package/src/passes/bake-ao.ts +183 -0
- package/src/passes/grime.ts +37 -0
- package/src/passes/index.ts +8 -0
- package/src/passes/livery.ts +66 -0
- package/src/passes/optimize-cache.ts +175 -0
- package/src/passes/optimize-weld.ts +147 -0
- package/src/passes/optimize.ts +85 -0
- package/src/passes/ray-grid.ts +237 -0
- package/src/passes/weighted-normals.ts +125 -0
- package/src/primitives/beam.ts +60 -0
- package/src/primitives/bevel-box.ts +140 -0
- package/src/primitives/box.ts +90 -0
- package/src/primitives/cell.ts +115 -0
- package/src/primitives/check.ts +49 -0
- package/src/primitives/closed.ts +74 -0
- package/src/primitives/erosion.ts +183 -0
- package/src/primitives/font-5x7.ts +117 -0
- package/src/primitives/heightfield.ts +101 -0
- package/src/primitives/index.ts +56 -0
- package/src/primitives/lathe-facets.ts +45 -0
- package/src/primitives/lathe.ts +174 -0
- package/src/primitives/lattice.ts +131 -0
- package/src/primitives/loft.ts +174 -0
- package/src/primitives/mc-table.ts +41 -0
- package/src/primitives/member.ts +130 -0
- package/src/primitives/noise.ts +157 -0
- package/src/primitives/planar.ts +55 -0
- package/src/primitives/prism.ts +85 -0
- package/src/primitives/sculpt-edit.ts +212 -0
- package/src/primitives/sculpt-tree.ts +226 -0
- package/src/primitives/sculpt.ts +80 -0
- package/src/primitives/sdf-grid.ts +131 -0
- package/src/primitives/sdf-ops.ts +145 -0
- package/src/primitives/sdf.ts +83 -0
- package/src/primitives/strip-uv.ts +26 -0
- package/src/primitives/surface-nets.ts +161 -0
- package/src/primitives/sweep.ts +162 -0
- package/src/primitives/terrain-color.ts +85 -0
- package/src/primitives/terrain-grid.ts +137 -0
- package/src/primitives/text-layout.ts +85 -0
- package/src/primitives/text.ts +197 -0
- package/src/primitives/tiled.ts +93 -0
- package/src/primitives/transport.ts +85 -0
- package/src/primitives/triangulate.ts +42 -0
- package/src/primitives/value-noise.ts +62 -0
- package/src/qa/checks.ts +110 -0
- package/src/qa/clash.ts +177 -0
- package/src/qa/floating.ts +63 -0
- package/src/qa/gates.ts +126 -0
- package/src/qa/geometry.ts +187 -0
- package/src/qa/index.ts +28 -0
- package/src/qa/inside.ts +114 -0
- package/src/qa/inspect.ts +107 -0
- package/src/qa/lods.ts +97 -0
- package/src/qa/measures.ts +67 -0
- package/src/qa/occlusion.ts +166 -0
- package/src/qa/sanity.ts +137 -0
- package/src/qa/types.ts +17 -0
- package/src/render/beauty.ts +115 -0
- package/src/render/camera.ts +142 -0
- package/src/render/depth-map.ts +191 -0
- package/src/render/font.ts +76 -0
- package/src/render/ground.ts +87 -0
- package/src/render/index.ts +34 -0
- package/src/render/lighting.ts +121 -0
- package/src/render/lod-strip.ts +34 -0
- package/src/render/metrics.ts +147 -0
- package/src/render/options.ts +81 -0
- package/src/render/photo-match.ts +28 -0
- package/src/render/raster.ts +143 -0
- package/src/render/resolve.ts +109 -0
- package/src/render/review-prompt.ts +59 -0
- package/src/render/review-warnings.ts +92 -0
- package/src/render/review.ts +103 -0
- package/src/render/scene.ts +119 -0
- package/src/render/shade.ts +204 -0
- package/src/render/sheet.ts +88 -0
- package/src/render/texture.ts +97 -0
- package/src/render/thumbnail.ts +75 -0
- package/src/render/vis-buffer.ts +119 -0
- package/src/testing/assert.ts +115 -0
- package/src/testing/compiled.ts +31 -0
- package/src/testing/hash.ts +14 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/snapshot.ts +37 -0
- package/src/types/gltf-validator.d.ts +13 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Production GLB
|
|
2
|
+
|
|
3
|
+
**A ts23d GLB is production-ready when it passes glTF-Validator with zero errors and warnings, faces glTF +Z, uses only core glTF 2.0 by default, and keeps engine-bound names stable across LODs. Compression, instancing and draft physics are opt-in profiles, never the default.** Godot 4 refuses any file whose `extensionsRequired` lists something it does not implement, and in 2026-10 that includes `KHR_mesh_quantization`, `EXT_meshopt_compression` and `KHR_draco_mesh_compression` ([Godot `_parse_gltf_extensions` and `get_supported_gltf_extensions_hashset`](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp)). Those extensions must be required, so a "drop into any engine" default cannot use them ([KHR_mesh_quantization §Overview](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization), [KHR_draco_mesh_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression)).
|
|
4
|
+
|
|
5
|
+
As of 2026-10. The exporter here is `src/export/glb.ts` (`toGlb`), `bin-writer.ts` (`BinWriter`), `motion.ts` (`motionChannel`) and `merge-pivots.ts` (`mergeToPivots`). These were read on 2026-10-25, while another agent was adding emitter `extras` to `glb.ts`. The spec is cited as [glTF 2.0 Specification.adoc](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc) by section anchor, written **[spec §anchor]**. The rendered copy is at [registry.khronos.org](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html).
|
|
6
|
+
|
|
7
|
+
**Status updated 2026-10-25 after the exporter work (G1, G2, G4, G5, G10, surfaces, `rest` clip). Sections (b) and (d) keep the original findings, with an update block on top.**
|
|
8
|
+
|
|
9
|
+
Status legend: **done** · **partial** · **missing** · **n/a (not yet)**. *Verified by*: `code` means read in the exporter, `val` means glTF-Validator 2.0.0-dev.3.10 output, and `insp` means the byte-level inspection script run on the tiger_1 GLBs (section b).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## (a) Production checklist
|
|
14
|
+
|
|
15
|
+
### Must
|
|
16
|
+
|
|
17
|
+
| # | Rule | Source | Status | Verified by |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| M1 | 0 validator errors and 0 warnings | [glTF-Validator ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: 0 / 0 on all 6 tiger GLBs; glTF-Validator now runs in the test suite (`src/export/validator.test.ts`, devDependency) and caught a sealed-buffer bug, since fixed | val |
|
|
20
|
+
| M2 | `POSITION` accessors carry `min`/`max` that match the data exactly (`ACCESSOR_MIN_MISMATCH`/`MAX_MISMATCH` are errors) | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes): "Accessors for the `POSITION` semantic MUST have their bounds… defined" | **done**: `BinWriter.accessor(…, bounds=true)` computes bounds from the same `Float32Array` | code, val |
|
|
21
|
+
| M3 | Animation sampler `input` has `min`/`max` and strictly increasing times | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) | **done**: `motionChannel` passes `bounds=true` | code, val |
|
|
22
|
+
| M4 | Vertex attribute offsets are 4-byte aligned; buffer views ≥ accessor extent; no `byteStride` on non-vertex views | [spec §data-alignment](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#data-alignment) | **done**: `BinWriter.push` pads every view to 4 bytes and views are tightly packed | code, val |
|
|
23
|
+
| M5 | GLB: JSON chunk padded with `0x20`, BIN chunk padded with `0x00`, lengths multiple of 4, header length equals file size | [spec §glb-file-format-specification](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#glb-file-format-specification) | **done**: `toGlb` tail and `BinWriter.bytes` | code, val |
|
|
24
|
+
| M6 | `bufferView.target` set for vertex (34962) and index (34963) data; unset for animation data | [ISSUES.md `BUFFER_VIEW_TARGET_MISSING`](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done** | code, val (no hint) |
|
|
25
|
+
| M7 | Node hierarchy is disjoint strict trees: one parent at most, no cycles | [spec §nodes-and-hierarchy](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#nodes-and-hierarchy) | **done**: single root, parents resolved by name; `T3D_PART_UNKNOWN` rejects forward references at authoring | code, val |
|
|
26
|
+
| M8 | Normals unit length; rotation quaternions normalised (`ACCESSOR_VECTOR3_NON_UNIT`, `ROTATION_NON_UNIT`) | [ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: 0 non-unit normals out of 37,749 | val, insp |
|
|
27
|
+
| M9 | Indices use u8/u16/u32 and never contain the type's max value (primitive restart) | [spec §meshes-topology](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-topology) | **done**: u16 when ≤ 65,535 vertices, else u32 (G4) | code, val |
|
|
28
|
+
| M10 | Y-up, metres, **front faces +Z, left faces +X** | [spec §coordinate-system-and-units](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#coordinate-system-and-units): "the front side of a glTF asset faces +Z, the left side of a glTF asset faces +X" | **done** (G1): root quaternion `[-0.5,-0.5,-0.5,0.5]` maps authoring +X front → glTF +Z, +Z up → +Y, +Y left → +X, checked numerically. Was `[-√½,0,0,√½]` (assets faced glTF +X, the left side). [ADR 009](../decisions/009-gltf-front-axis.md) | code, insp |
|
|
29
|
+
| M11 | `COLOR_0` is linear and in [0,1]; it multiplies baseColor | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes), [spec §metallic-roughness-material](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#metallic-roughness-material): "encoded with the linear transfer function" | **done**: `src/material/palette.ts` converts sRGB hex to linear; measured range 0.009–1.0 | code, insp |
|
|
30
|
+
| M12 | Every node that engine code binds to (pivots) has a stable, unique name **in every LOD** | Engines bind by node path (glTFast/Godot/Unreal all keep node names) | **partial**: names are unique and stable at LOD0/LOD1 (24 nodes). LOD2 has only `tiger_1, body, turret, gun`, so the 18 wheel/sprocket/idler pivots disappear and game code that drives `wheel_l_0` breaks at LOD2 (G3) | insp |
|
|
31
|
+
| M13 | No accidental engine name hints: no node or mesh name ends in a Godot suffix (`-col`, `_col`, `$col`, `-colonly`, `-convcol`, `-convcolonly`, `-noimp`, `-occ`, `-occonly`, `-navmesh`, `-vehicle`, `-wheel`, `-rigid`), and no clip name starts or ends with `loop`/`cycle` unless looping is intended. Suffix matching is case-insensitive and accepts `-`, `$` or `_` | [Godot: node type customization](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html) | **missing as a gate**. No current recipe violates it (repo grep), but a part named `spare_wheel` would import into Godot as a `VehicleWheel3D` (G6) | code (grep) |
|
|
32
|
+
| M14 | A looping clip's last key equals its first key | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) clamps outside the input range; it does not define looping, so any seam is the asset's fault | **done** (G2): clip length 4 s, each motion rounded to whole cycles (`cyclesOf`), spins get ≥ 12 keys per turn, so the last key equals the first. Was a fixed 2 s clip that popped 216° on `rps 0.8` | code, insp |
|
|
33
|
+
|
|
34
|
+
### Should
|
|
35
|
+
|
|
36
|
+
| # | Rule | Source | Status | Verified by |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| S1 | Smallest index type that fits: u16 when the primitive has at most 65,535 vertices (index 65,535 is the restart value, so the highest usable index is 65,534), u8 is legal but slow on some GPUs | [spec §meshes-topology](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-topology) | **done** (G4): u16 indices whenever the primitive has ≤ 65,535 vertices | code, insp |
|
|
39
|
+
| S2 | `COLOR_0` as normalised `unorm16` VEC4 (8 B/v) instead of float VEC4 (16 B/v). This is core glTF, needs no extension, and every loader supports it | [spec §meshes-attributes](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-attributes): COLOR_n is float32, unorm8 or unorm16 | **done** (G5): COLOR_0 is normalised u16 RGBA (8 B/v). Was float VEC4, 27 % of LOD0 | code, insp |
|
|
40
|
+
| S3 | No unused attributes (`UNUSED_OBJECT`) | [ISSUES.md](https://github.com/KhronosGroup/glTF-Validator/blob/main/ISSUES.md) | **done**: the painted trim sheet is bound (embedded or external), so `TEXCOORD_0` is used. Infos remain only with `--sheet none` | val |
|
|
41
|
+
| S4 | Reuse one `mesh` index across nodes with identical local geometry; engines then share one mesh asset | [spec §meshes-overview](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-overview) (nodes reference meshes by index) | **missing** (G9) | insp |
|
|
42
|
+
| S5 | `asset.copyright`, plus provenance in `asset.extras` (recipe, ts23d version, seed, content hash) | [spec §asset](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#asset): "metadata MAY be stored in… `generator` or `copyright`" | **partial**: `generator: "@developerz.ai/ts23d"` has no version; there is no copyright and no provenance | code |
|
|
43
|
+
| S6 | Clip names follow engine conventions: Godot loops a clip only if its name starts or ends with `loop`/`cycle` | [Godot node type customization](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html); [`gltf_document.cpp` `begins_with("loop")`](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp) | **missing** (G7): clips are `rest` and `run`; Godot imports `run` non-looping | code |
|
|
44
|
+
| S7 | Default to no clip, or a harmless idle; viewers autoplay the first clip | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) (runtime behaviour undefined); Babylon `animationStartMode = FIRST` ([source](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/loaders/src/glTF/glTFFileLoader.pure.ts)); model-viewer `autoplay` plays the first ([docs](https://modelviewer.dev/docs/index.html)) | **done**: `toGlb` exports no clip unless `animations: true`. When it does (and `t3d build` does by default), the first clip is `rest` (static, two keys), so viewers open paused and `run` plays on request | code |
|
|
45
|
+
| S8 | One sampler `input` accessor shared by channels with the same keyframe times | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) | **done** (G10): time accessors shared per key count | code |
|
|
46
|
+
| S9 | Root transform: rotation-only on one named root; no scale | [Godot import configuration: Apply Root Scale](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/import_configuration.html) | **done** (rotation only; corrected quaternion, see M10) | code |
|
|
47
|
+
| S10 | `TANGENT` only when a normal map is bound. Without one, omit it (the spec says it "SHOULD NOT" be present without normals). With one, either ship MikkTSpace tangents or accept the `MESH_PRIMITIVE_GENERATED_TANGENT_SPACE` warning | [spec §meshes-usage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-usage): tangents not specified means "calculated using default MikkTSpace algorithms" | **done**: `TANGENT` is emitted only when a sheet (with its normal map) is bound | code |
|
|
48
|
+
| S11 | Collision proxies available in a form each engine imports (section 6) | Per-engine (section c) | **missing** | code |
|
|
49
|
+
| S12 | LODs consumable without hand work: one file with MSFT_lod plus `MSFT_screencoverage`, and/or `_LODn` names, plus the per-LOD files | [MSFT_lod](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod) | **partial**: three separate files `tiger_1.lod{0,1,2}.glb` with `extras.lod`; no switch distances | code |
|
|
50
|
+
|
|
51
|
+
### Nice
|
|
52
|
+
|
|
53
|
+
| # | Rule | Source | Status |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| N1 | `--profile web`: `KHR_mesh_quantization` + `EXT_meshopt_compression` (or `KHR_meshopt_compression`, Release Candidate) for three.js/Babylon/glTFast | [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression) (Complete, Ratified); [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_meshopt_compression) | missing |
|
|
56
|
+
| N2 | `EXT_mesh_gpu_instancing` for bolts, rivets and track links in a web/Unity profile | [EXT_mesh_gpu_instancing](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing): "files that use the extension should specify it in `extensionsRequired`" | missing |
|
|
57
|
+
| N3 | KTX2 (`KHR_texture_basisu`) trim sheet: UASTC for normal and ORM, ETC1S for base colour | [KTX Artist Guide](https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXArtistGuide.md) | n/a (no textures yet) |
|
|
58
|
+
| N4 | `KHR_materials_emissive_strength` for lit windows and headlights | [spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_emissive_strength) (Ratified) | missing |
|
|
59
|
+
| N5 | `KHR_xmp_json_ld` with `dc:creator`, `dc:rights`, `xmpRights:UsageTerms` | [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) (Ratified) | missing |
|
|
60
|
+
| N6 | `KHR_physics_rigid_bodies` + `KHR_implicit_shapes` (in `extensionsUsed` only, never required) | Draft PRs [#2424](https://github.com/KhronosGroup/glTF/pull/2424), [#2370](https://github.com/KhronosGroup/glTF/pull/2370), both open | missing (wait) |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## (b) Validator results: tiger_1
|
|
65
|
+
|
|
66
|
+
**Update 2026-10-25, after G1/G2/G4/G5/G10:** tiger_1 is now 88,176 / 16,304 / 648 triangles (the recipe was reworked). LOD0 dropped from 13.0 MB to 11.0 MB with 0 validator errors. Build plus QA at 3 LODs, contact sheet included, takes about 4.7 s. The table below is the original run on the older, smaller recipe.
|
|
67
|
+
|
|
68
|
+
**Verdict: VALID. 0 errors, 0 warnings, 0 hints on all six files. The only infos are `UNUSED_OBJECT` on `TEXCOORD_0` (one per primitive), because no texture is bound.** The real problems (sideways front, u32 indices, fat colours, loop seam, LOD2 losing pivots) are outside the validator's scope and were found by the inspection script.
|
|
69
|
+
|
|
70
|
+
The build was `bun run t3d build recipes/tiger_1/tiger_1.ts --out …/glbcheck`, then again with `--animations`. Both print `QA FAIL` (clashes and floating parts in the recipe, not the exporter) and both still write all three GLBs. The validator was npm `gltf-validator@2.0.0-dev.3.10` (`validateBytes`, `maxIssues: 0`), run from the scratchpad.
|
|
71
|
+
|
|
72
|
+
| File | Bytes | Err | Warn | Info | Hint | Draw calls | Verts | Tris | Anim |
|
|
73
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
74
|
+
| `tiger_1.lod0.glb` | 2,209,336 | 0 | 0 | 23 | 0 | 23 | 37,749 | 31,148 | 0 |
|
|
75
|
+
| `tiger_1.lod1.glb` | 426,376 | 0 | 0 | 23 | 0 | 23 | 6,592 | 7,212 | 0 |
|
|
76
|
+
| `tiger_1.lod2.glb` | 36,012 | 0 | 0 | 3 | 0 | 3 | 584 | 384 | 0 |
|
|
77
|
+
| `--animations` lod0 | 2,226,572 | 0 | 0 | 23 | 0 | 23 | 37,749 | 31,148 | 1 (`run`, 20 rotation channels, LINEAR, 0–2 s) |
|
|
78
|
+
| `--animations` lod1 | 443,576 | 0 | 0 | 23 | 0 | 23 | 6,592 | 7,212 | 1 |
|
|
79
|
+
| `--animations` lod2 | 36,012 | 0 | 0 | 3 | 0 | 3 | 584 | 384 | 0 (no motion pivots left) |
|
|
80
|
+
|
|
81
|
+
All infos have the same form:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "code": "UNUSED_OBJECT", "severity": 2, "pointer": "/meshes/N/primitives/0/attributes/TEXCOORD_0" }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Findings from the inspection script, which the validator does not check:
|
|
88
|
+
|
|
89
|
+
| Check | LOD0 result | Rule |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| u32 indices on primitives with fewer than 65k verts | 23/23 | S1 |
|
|
92
|
+
| Byte split: POSITION / TEXCOORD_0 / COLOR_0 / indices (NORMAL ≈ POSITION) | 453 K / 302 K / **604 K** / 374 K | S1, S2 |
|
|
93
|
+
| Degenerate triangles | 0 | (`ACCESSOR_INDEX_TRIANGLE_DEGENERATE`) |
|
|
94
|
+
| Non-unit normals (more than 5e-4 off) | 0 | M8 |
|
|
95
|
+
| COLOR_0 range | 0.0092–1.0, alpha always 1 | M11, S2 |
|
|
96
|
+
| Root rotation | `[-0.7071, 0, 0, 0.7071]`: authoring +X goes to glTF +X (left) | M10 |
|
|
97
|
+
| Node names, LOD0 / LOD2 | 24 / 4 (all wheels gone at LOD2) | M12 |
|
|
98
|
+
| Spin loop seam (`rps 0.8`) | key 0 `[0,0,0,1]`, key 24 `[0,-0.951,0,0.309]` | M14 |
|
|
99
|
+
| Animation time accessors | 20 identical | S8 |
|
|
100
|
+
| `extensionsUsed` | none (core-only, which is good for Godot) | — |
|
|
101
|
+
|
|
102
|
+
Re-run: write the two `.mjs` scripts below into a scratch directory with `npm i gltf-validator`, then run `node validate.mjs out/*.glb`. `gltf-validator` is now a devDependency used by `src/export/validator.test.ts`; ADR 005 restricts runtime dependencies only.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## (c) Per-engine import table
|
|
107
|
+
|
|
108
|
+
"glTF" below means the engine's default glTF importer, as of 2026-10.
|
|
109
|
+
|
|
110
|
+
| | **Unity (glTFast 6.x)** | **Unreal 5 (Interchange glTF)** | **Godot 4** | **three.js (GLTFLoader)** | **Babylon.js** |
|
|
111
|
+
|---|---|---|---|---|---|
|
|
112
|
+
| Importer | glTFast is Unity's glTF package. Unity has no built-in glTF importer ([glTFast manual](https://docs.unity3d.com/Packages/com.unity.cloud.gltfast@6.14/manual/index.html)) | Interchange is the default for glTF static meshes ([format support](https://dev.epicgames.com/documentation/en-us/unreal-engine/gltf-file-format-support-in-unreal-engine)) | Built-in `GLTFDocument` | `examples/jsm` loader | `@babylonjs/loaders` |
|
|
113
|
+
| Naming | Node names become GameObject names. Mesh name becomes the asset name | Mesh name becomes the Static Mesh asset name. Interchange has renamed collision meshes in the past ([UE-248981](https://issues.unrealengine.com/issue/UE-248981), fixed May 2025) | Node names are kept. Suffix hints are stripped and acted on (`-col`, `-noimp`, …), with `-`, `$` or `_` and case-insensitive ([docs](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/node_type_customization.html)) | `Object3D.name` = node name (sanitised) | `TransformNode.name` = node name |
|
|
114
|
+
| Collision | None imported. Add colliders in-engine | Prefixes `UBX_` box, `UCP_` capsule, `USP_` sphere, `UCX_` convex, matched to the unprefixed mesh name (`UCX_body`, `UCX_body_01`) ([Interchange Generic Mesh Pipeline](https://api.unrealengine.com/INT/PythonAPI/class/InterchangeGenericMeshPipeline.html), [FBX pipeline](https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-static-mesh-pipeline-in-unreal-engine)). **glTF path unverified:** test-import a fixture | `-col` adds a trimesh body, `-convcol` a convex one; `-colonly` and `-convcolonly` replace the mesh with a collider; `-rigid` makes a RigidBody3D. `OMI_physics_body`/`OMI_physics_shape` are the import extensions it declares. `KHR_physics_rigid_bodies` / `KHR_collision_shapes` appear only in its JSON-pointer mapping so far ([physics ext source](https://github.com/godotengine/godot/blob/master/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp)) | None | None (no physics import) |
|
|
115
|
+
| LOD | MSFT_lod is listed "not investigated", so only LOD0 loads ([features](https://github.com/Unity-Technologies/com.unity.cloud.gltfast/blob/main/Packages/com.unity.cloud.gltfast/Documentation~/features.md)). Unity's `_LOD0` naming applies to the model importer (FBX), not glTFast ([manual](https://docs.unity3d.com/Manual/importing-lod-meshes.html)) | No glTF LOD import found. Import LOD1/2 files into LOD slots, or auto-generate them | Ignores MSFT_lod (not in the supported set). Generates its own LODs on import ("Generate LODs", [import configuration](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/import_configuration.html)). Use [visibility ranges](https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html) for hand LODs | MSFT_lod not supported ([GLTFLoader docs](https://threejs.org/docs/pages/GLTFLoader.html)). Build `THREE.LOD` from the per-LOD files | **MSFT_lod supported** with progressive loading and `maxLODsToLoad` ([source](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.pure.ts)) |
|
|
116
|
+
| Animation | Legacy Animation ✅, Mecanim partial (clips not assigned), Playables ⛔ ([features](https://github.com/Unity-Technologies/com.unity.cloud.gltfast/blob/main/Packages/com.unity.cloud.gltfast/Documentation~/features.md)) | Imports node animation as Level Sequence or anim assets (verify on fixture) | AnimationPlayer. A clip loops only if its name starts or ends with `loop`/`cycle` ([source](https://github.com/godotengine/godot/blob/master/modules/gltf/gltf_document.cpp)) | `gltf.animations` holds `AnimationClip`s; no autoplay | Autoplays the **first** clip by default (`animationStartMode = FIRST`) |
|
|
117
|
+
| Quantization / meshopt / Draco | ✅ / ✅ (needs package) / ✅ (needs package) | Exporter supports quantization; import support for the three is undocumented | ⛔ / ⛔ / ⛔: the file **fails to import** if they are required | ✅ / ✅ (`setMeshoptDecoder`; `KHR_meshopt_compression` also built in) / ✅ (`setDRACOLoader`) | ✅ / ✅ / ✅ |
|
|
118
|
+
| KTX2 basisu | ✅ (KTX package) | Undocumented | ✅ (`gltf_document_extension_texture_ktx`) | ✅ (`setKTX2Loader`) | ✅ |
|
|
119
|
+
| GPU instancing | ✅ (no custom `_ID` attrs) | Undocumented | ⛔ (not in supported set) | ✅ (`InstancedMesh`) | ✅ |
|
|
120
|
+
| Emissive strength | ⛔ (blank in features table) | Undocumented | ✅ | ✅ | ✅ |
|
|
121
|
+
| `extras` | Kept in the glTFast API; not on components | Interchange metadata (verify) | `GLTFNode` metadata (opt-in) | `userData` | `metadata` (`ExtrasAsMetadata`) |
|
|
122
|
+
|
|
123
|
+
**Consequence:** the default GLB uses core glTF only. A `web` profile adds compression and instancing. An engine profile adds collision naming. Where this table says Unreal is "undocumented" or "verify", import a test file before relying on the behaviour.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Topics 3–7: rules
|
|
128
|
+
|
|
129
|
+
### 3. Optimisation extensions: when to use
|
|
130
|
+
|
|
131
|
+
| Extension | Use when | Don't use when | Registry status |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `KHR_mesh_quantization` | Web/Unity/Babylon profile. Positions as int16 with a node scale/offset dequantisation, normals as int8 normalised (stride 4) | Default export (Godot refuses it because it must be required) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization)) |
|
|
134
|
+
| `EXT_meshopt_compression` | Web profile. Pair with quantization. Decoding is fast, it compresses animation (quaternion filter 2) too, and a `fallback` buffer can keep it optional | Godot targets (unless a fallback buffer is shipped, which doubles the size) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression)). `KHR_meshopt_compression` is the RC successor with a v1 bitstream and colour filter ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_meshopt_compression)) |
|
|
135
|
+
| `KHR_draco_mesh_compression` | Only when a target lacks meshopt. Geometry only, slower decode, must be required without a fallback | Prefer meshopt for everything we ship ([meshoptimizer gltfpack](https://github.com/zeux/meshoptimizer/blob/master/gltf/README.md)) | Ratified |
|
|
136
|
+
| `KHR_texture_basisu` | When the trim sheet ships. UASTC for normal and ORM, ETC1S for base colour ([KTX Artist Guide](https://github.com/KhronosGroup/3D-Formats-Guidelines/blob/main/KTXArtistGuide.md)). Keep a PNG `source` fallback so it stays optional | Required without a fallback in the default profile | Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu)) |
|
|
137
|
+
| `KHR_texture_transform` | Only if strips get tiled by material instead of UVs | Our trim sheet bakes the UV rect into `TEXCOORD_0`, so it is not needed | Ratified |
|
|
138
|
+
| `EXT_mesh_gpu_instancing` | Bolts, rivets, track links, fence posts: ≥ ~16 copies of one sub-mesh in a web/Unity profile | Default (no fallback, so it should be required; Godot lacks it) | Complete, Ratified ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing)) |
|
|
139
|
+
| `MSFT_lod` + `extras.MSFT_screencoverage` | Ship a single-file LOD set: extension on the LOD0 root node, `ids` → LOD1/LOD2 roots, coverage like `[0.25, 0.08, 0.01]`. It can live in `extensionsUsed` only, because non-supporting loaders load the highest LOD | — (safe additive; costs about 20 % more bytes) | Vendor ([spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod)) |
|
|
140
|
+
|
|
141
|
+
### 4. Materials
|
|
142
|
+
|
|
143
|
+
| Rule | Source | Us |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| ORM packing: one linear texture, R = occlusion (`occlusionTexture`), G = roughness, B = metallic (`metallicRoughnessTexture`), both pointing at the same texture | [spec §additional-textures-occlusion](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#additional-textures-occlusion) note: "could point to the same texture that packs all three" | Planned ORM in ADR 003. Bind both slots to one texture index |
|
|
146
|
+
| baseColor and emissive textures are sRGB; ORM and normal are linear | [spec §metallic-roughness-material](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#metallic-roughness-material) | Painter must write the albedo as sRGB and the ORM as linear |
|
|
147
|
+
| `COLOR_0` is linear and multiplies baseColor (texture × factor × COLOR_0) | same | done |
|
|
148
|
+
| Foliage cards: `alphaMode: "MASK"`, `alphaCutoff` (default 0.5), `doubleSided: true` | [spec §alpha-coverage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#alpha-coverage), [§double-sided](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#double-sided) | **done**: `surface: 'cutout'` ([ADR 008](../decisions/008-surface-materials.md)) |
|
|
149
|
+
| Tangents: omit them and let the importer run MikkTSpace (the spec default) unless a baked normal map needs bit-exact tangents. Our tiling trim normals don't | [spec §meshes-usage](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#meshes-usage) | correct today |
|
|
150
|
+
| Lit windows: `emissiveFactor` up to 1 plus `KHR_materials_emissive_strength`. Unity glTFast ignores the extension and falls back to strength 1 | [spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_emissive_strength) | missing |
|
|
151
|
+
|
|
152
|
+
### 5. Animation
|
|
153
|
+
|
|
154
|
+
| Rule | Source |
|
|
155
|
+
|---|---|
|
|
156
|
+
| glTF defines no autoplay, loop or order. Loaders choose: Babylon and model-viewer play the first clip; three and Godot don't autoplay | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) |
|
|
157
|
+
| LINEAR on `rotation` is slerp; keep consecutive keys under 180° apart (we use 15° steps or finer) | same, [§interpolation-slerp](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#interpolation-slerp) |
|
|
158
|
+
| Loops: last key = first key, and the clip length is an integer number of periods for every channel | consequence of clamping (same section) |
|
|
159
|
+
| One clip per behaviour, named `<verb>_loop` (Godot loop flag); keep pivot-driving clips out of the first slot | Godot source above |
|
|
160
|
+
| Each (node, path) appears at most once per animation | [spec §animations](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#animations) |
|
|
161
|
+
|
|
162
|
+
### 6. Collision and physics
|
|
163
|
+
|
|
164
|
+
| Approach | Engines | Ship how |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| Godot suffix nodes `<pivot>-colonly` / `-convcolonly` | Godot | Engine profile `godot`; the collider node is a child of the pivot |
|
|
167
|
+
| Unreal prefix meshes `UCX_<mesh>_NN`, `UBX_`, `USP_`, `UCP_` | Unreal (FBX documented; glTF via Interchange unverified) | Engine profile `unreal`; the collision mesh is a sibling, named after the render mesh |
|
|
168
|
+
| `extras.ts23d.collision` on a hidden child node (box/convex hull data) | Unity, three, Babylon (scripts read it) | Default: no visible mesh. Shapes go in extras so nothing renders by accident |
|
|
169
|
+
| `KHR_physics_rigid_bodies` + `KHR_implicit_shapes` | None import it yet (Godot imports the OMI equivalents) | Draft PRs [#2424](https://github.com/KhronosGroup/glTF/pull/2424) and [#2370](https://github.com/KhronosGroup/glTF/pull/2370), open as of 2026-10. Emit in `extensionsUsed` only, after ratification |
|
|
170
|
+
|
|
171
|
+
A collider mesh in a generic GLB renders as visible geometry in Unity, three and Babylon. So collision geometry is never in the default visual file. It goes in extras, an engine profile, or a sibling `*.collision.glb`.
|
|
172
|
+
|
|
173
|
+
### 7. Metadata
|
|
174
|
+
|
|
175
|
+
| Field | Rule | Source |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `asset.generator` | `"@developerz.ai/ts23d <version>"` | [spec §asset](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#asset) |
|
|
178
|
+
| `asset.copyright` | From the recipe (`license`/`author`), else `"ts23d recipe <name>, MIT"` per repo license | same |
|
|
179
|
+
| `asset.extras.ts23d` | `{ recipe, version, seed, lod, lodCount, units:"m", up:"+Y", front:"+Z", sourceUp:"+Z", sourceFront:"+X", contentHash }` | [spec §specifying-extensions](https://github.com/KhronosGroup/glTF/blob/main/specification/2.0/Specification.adoc#specifying-extensions) and the `extras` property (any JSON) |
|
|
180
|
+
| LOD switch distances | `extras.MSFT_screencoverage` on the MSFT_lod node, plus `extras.ts23d.lodScreenCoverage` in each per-LOD file | [MSFT_lod](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod) |
|
|
181
|
+
| Rights | `KHR_xmp_json_ld` packet with `dc:creator`, `dc:rights`, `xmpRights:UsageTerms` (in `extensionsUsed` only) | [KHR_xmp_json_ld](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) |
|
|
182
|
+
|
|
183
|
+
Today the exporter writes `extras` at the glTF **root** (`kind, lod, units, up`), not in `asset.extras`. Root `extras` are legal, but many importers surface only node and `asset` extras.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## (d) Prioritised gap list
|
|
188
|
+
|
|
189
|
+
`ZERO-DEP WIN` means pure gain: core glTF only, no new dependency, valid in every engine, byte-deterministic. Every test lives next to the file it tests and runs in `bun run verify`.
|
|
190
|
+
|
|
191
|
+
| P | Gap | Exact change | Test that proves it | Tag |
|
|
192
|
+
|---|---|---|---|---|
|
|
193
|
+
| **G1** | Asset faces glTF +X (glTF's left) instead of +Z (M10) | `src/export/glb.ts` `toGlb`: root `rotation` changes from `[-Math.SQRT1_2,0,0,Math.SQRT1_2]` to `[-0.5,-0.5,-0.5,0.5]`, which maps authoring +X front to +Z, +Y left to +X and +Z up to +Y (checked numerically). Amend ADR 004's "one −90° X rotation" line, and set `extras.up/front`. Breaking for anyone who has already placed assets: CHANGELOG | `glb.test.ts`: rotate `[1,0,0]` by the root quaternion and expect `[0,0,1]`; rotate `[0,1,0]` and expect `[1,0,0]` | **done** · ZERO-DEP WIN (breaking) |
|
|
194
|
+
| **G2** | Loop seam: spin ends at 1.6 rev (M14) | `src/export/motion.ts` `motionChannel`: replace the fixed `LOOP_SECONDS` with a clip length `D` passed from `toGlb`. Per channel, round cycles to `n = max(1, round(rate·D))` and sample `max(24, 8n)` keys so steps stay under 45°. Record the true rate in `extras.ts23d.motion`. `toGlb` picks `D` = the smallest of 2, 4 or 8 s that keeps every channel's rounding error under 10 % | `motion.test.ts` (new): for spin `rps 0.8`, the first and last output quaternions are equal up to sign (`|dot| > 0.9999`); same for sway and bob | **done** · ZERO-DEP WIN |
|
|
195
|
+
| **G3** | Pivots vanish at LOD2 (M12) | `src/cli/run.ts`: collect the LOD0 pivot list (names, parent, `at`) and pass it as `GlbOptions.pivots`. `src/export/glb.ts` `toGlb` emits an empty node for each pivot missing from this LOD, under its LOD0 parent | `glb.test.ts`: an asset whose LOD2 build omits `wheel_0` still has node `wheel_0` in the LOD2 GLB, and its node-name set equals LOD0's | ZERO-DEP WIN |
|
|
196
|
+
| **G4** | u32 indices everywhere (S1) | `src/export/bin-writer.ts` `BinWriter.accessor`: accept `Uint16Array` and set `componentType: 5123`. `toGlb` converts to `Uint16Array` when `positions.length/3 <= 65535`. `push` already 4-aligns the following view | `glb.test.ts`: the box crate's index accessor has `componentType === 5123`; a 70k-vertex mesh keeps 5125; scratch run of glTF-Validator stays 0/0 | **done** · ZERO-DEP WIN (−187 KB on tiger LOD0) |
|
|
197
|
+
| **G5** | COLOR_0 float VEC4 is 27 % of the file (S2) | `bin-writer.ts`: `accessor` takes `Uint16Array` + `normalized: true` (componentType 5123, add `normalized`). `glb.ts` `toGlb` writes the colour as `Math.round(c·65535)` into VEC4 unorm16 (8 B/v, aligned) | `glb.test.ts`: the COLOR_0 accessor has `componentType 5123`, `normalized: true`, and the decoded value is within 1/65535 of the source | **done** · ZERO-DEP WIN (−302 KB on tiger LOD0) |
|
|
198
|
+
| **G6** | No guard against Godot name hints (M13) | New gate `src/qa/` `engineNames` (tier 3; QA already sees all part names): fail on `/[-_$](col|colonly|convcol|convcolonly|noimp|occ|occonly|navmesh|vehicle|wheel|rigid)$/i`, with `fix: rename to <name>_part or opt in via exportHint`. Exporter-side mirror is optional | `qa/*.test.ts`: a part named `spare_wheel` makes `runQa` report `engine-names`; `wheel_l_0` passes | ZERO-DEP WIN |
|
|
199
|
+
| **G7** | Clip named `run`: Godot won't loop it, and it is the first clip that viewers autoplay (S6) | `src/export/glb.ts` `toGlb`: name the clip `motion_loop` and set `extras.ts23d.loop: true` | `glb.test.ts`: `animations[0].name` matches `/loop$/` | ZERO-DEP WIN |
|
|
200
|
+
| **G8** | No provenance or copyright (S5, section 7) | `glb.ts` `toGlb`: `asset.generator` with the package version (import from `package.json` at build or a `VERSION` const in core). Add `asset.copyright` and `asset.extras.ts23d = {recipe, version, seed, lod, lodCount, up, front, units}`. Move root `extras` into it, keeping the old root `extras` one minor release for compatibility | `glb.test.ts`: `asset.copyright` is a non-empty string; `asset.extras.ts23d.front === '+Z'`; determinism test still passes | ZERO-DEP WIN |
|
|
201
|
+
| **G9** | Identical geometry not shared (S4) | `src/export/glb.ts` `toGlb`: hash each merged mesh (positions, normals, uvs, colors, indices bytes) and reuse the `meshes[]` index on a hit. Today only 4 wheel pairs match, because grime noise is seeded per part. Follow-up: let `kinds/vehicle.ts` seed grime per wheel class so all 16 road wheels share a few meshes | `glb.test.ts`: two `ctx.use` copies of the same sub-asset yield two nodes with the same `mesh` index | ZERO-DEP WIN |
|
|
202
|
+
| **G10** | 20 duplicate animation time accessors (S8) | `motion.ts` `motionChannel`: take an `inputs: Map<string, number>` cache keyed by `(D, keyCount)` from `toGlb` | `glb.test.ts`: crate with two spinning parts has `new Set(samplers.map(s=>s.input)).size === 1` | **done** · ZERO-DEP WIN |
|
|
203
|
+
| G11 | LODs are three files and engines get no switch distances (S12) | `src/cli/run.ts` + new `toGlbLodSet(compiled[])` in `src/export/glb.ts`. One GLB with LOD0 root `extensions.MSFT_lod.ids` → LOD1/LOD2 roots (named `<asset>_LOD1`, `_LOD2`, excluded from `scenes[0].nodes`) and `extras.MSFT_screencoverage` from a new `budget.screenCoverage` (default `[0.25, 0.08, 0.01]`). `extensionsUsed: ["MSFT_lod"]`, never required. Keep the per-LOD files | `glb.test.ts`: `extensionsUsed` has `MSFT_lod`; `extensionsRequired` is undefined; the `ids` length is 2; validator 0/0. Load it in Babylon in the apps gallery smoke test | ZERO-DEP WIN |
|
|
204
|
+
| G12 | No collision export (S11) | `src/core/asset.ts`: `ctx.collider(name, {shape:'box'|'convex', of?: part})`. `src/export/glb.ts` gains `GlbOptions.engine: 'generic'|'godot'|'unreal'`. Generic writes `extras.ts23d.collision` on an empty child; godot writes a `<pivot>-convcolonly` mesh node; unreal writes a `UCX_<mesh>_01` sibling mesh. CLI `--engine` | `glb.test.ts` per profile: node and mesh names match the convention, and the generic profile has no extra mesh. Manual: import a fixture into Godot 4 and UE 5 and check that a collider appears | ZERO-DEP (needs an API addition) |
|
|
205
|
+
| G13 | Single opaque material blocks foliage cutouts and lit windows (section 4) | `src/material`: add part flags `surface: 'cutout'|'emissive'`. `glb.ts` `toGlb` writes up to three materials: `ts23d_trim`; `ts23d_trim_cutout` (`alphaMode:"MASK"`, `alphaCutoff:0.5`, `doubleSided:true`); `ts23d_trim_emissive` (`emissiveFactor`, `KHR_materials_emissive_strength` in `extensionsUsed` only). `mergeToPivots` splits primitives per material. Needs an ADR 003 amendment ("exactly one material") | `glb.test.ts`: a cutout part lands in a primitive whose material has `alphaMode MASK`; with no flagged parts the GLB still has exactly 1 material | **partial**: cutout, glass, translucent shipped as `surface`; emissive missing · ZERO-DEP (ADR change) |
|
|
206
|
+
| G14 | Web profile size (N1, N2) | New optional subpath `src/export/web/` (ADR 005 "behind an optional subpath"): quantize positions (int16 plus a node dequant transform), normals (int8, stride 4) and UVs (unorm16); meshopt encode (own port of the codec or the `meshoptimizer` npm package as an optional peer); `EXT_mesh_gpu_instancing` for repeated sub-parts. All of these go in `extensionsRequired`, so never for Godot | Snapshot: `--profile web` tiger LOD0 is under 40 % of default bytes; validator 0/0; three.js gallery loads it with `setMeshoptDecoder` | **runtime dep (optional)** |
|
|
207
|
+
| G15 | Textures (N3) | With the M2 painter: PNG baseColor (sRGB) + ORM + normal, plus an optional KTX2 profile (UASTC for ORM/normal, ETC1S for albedo) with a PNG fallback `source` | Validator 0/0 with `hasTextures: true` and no `UNUSED_OBJECT` on TEXCOORD_0 | **done** (PNG, embedded or external; KTX2 profile still missing) · dep for KTX2 (optional) |
|
|
208
|
+
| G16 | Physics extensions (N6) | Wait for #2424/#2370 to ratify. Then emit from the G12 collider data in `extensionsUsed` only | Godot import test | wait |
|
|
209
|
+
|
|
210
|
+
**Combined size effect of G4 + G5 on tiger LOD0:** 2,209 KB drops to about 1,720 KB (−22 %) with no extension, no dependency and no engine risk. G9 and G10 add a little more.
|
|
211
|
+
|
|
212
|
+
### Inspection scripts (for reproducibility; not in the repo)
|
|
213
|
+
|
|
214
|
+
`validate.mjs`: `validateBytes(bytes, { maxIssues: 0 })` from `gltf-validator@2.0.0-dev.3.10`, printing `numErrors/numWarnings/numInfos/numHints`, `info`, and issue counts by code. `inspect.mjs` parses the GLB and reports index component type against vertex count, per-attribute byte totals, normal length, COLOR_0 range, duplicate geometry signatures, root rotation, node names, and animation sampler input sharing and first/last keys.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# What a GLB can carry, and what ts23d writes
|
|
2
|
+
|
|
3
|
+
**glTF 2.0 / GLB stays the output format.** It is open and royalty-free, one compact binary file, and
|
|
4
|
+
first-class in Godot, three.js and Babylon, imported by Unreal (Interchange) and Unity (glTFast). FBX is
|
|
5
|
+
proprietary (Autodesk SDK) and inconsistent across versions; USD/USDZ is built for film-style scene
|
|
6
|
+
composition and Apple AR (a later USDZ export is possible); OBJ/STL carry no hierarchy, motion or
|
|
7
|
+
materials. This page maps the whole glTF surface onto the DSL: shipped, next, later, never.
|
|
8
|
+
|
|
9
|
+
Status legend: **shipped** = on `main` with a test · **next** = the current roadmap slice ·
|
|
10
|
+
**later** = wanted, unscheduled · **no** = out of scope, with the reason.
|
|
11
|
+
|
|
12
|
+
## Core glTF 2.0
|
|
13
|
+
|
|
14
|
+
| Capability | glTF | ts23d | Status |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| Named node hierarchy, pivots | `nodes` | `ctx.part` / `ctx.group`, parent + pivot | shipped |
|
|
17
|
+
| Static batching | one mesh per pivot | `mergeToPivots` | shipped |
|
|
18
|
+
| Vertex colour albedo | `COLOR_0` (u16 normalised) | every primitive's `color` | shipped |
|
|
19
|
+
| PBR metal/rough + normal + ORM | `pbrMetallicRoughness`, `normalTexture`, `occlusionTexture` | procedural trim sheet | shipped |
|
|
20
|
+
| Alpha modes | `OPAQUE` / `MASK` / `BLEND` | surfaces opaque / cutout / glass / translucent | shipped |
|
|
21
|
+
| Node animation (TRS) | `animations` | `spin`, `slide`, `sway` → `rest` + `run` clips | shipped |
|
|
22
|
+
| Sockets, emitters | `extras` on empty nodes | `ctx.socket`, `ctx.emitter` | shipped |
|
|
23
|
+
| Skins (bones) | `skins`, `JOINTS_0` / `WEIGHTS_0`, `inverseBindMatrices` | `ctx.bone` + skinned sculpt (below) | later |
|
|
24
|
+
| Morph targets | `targets` + `weights` | face shapes: blink, jaw open, snarl | later |
|
|
25
|
+
| Cameras | `cameras` | preview / hero camera per asset | later |
|
|
26
|
+
|
|
27
|
+
### Skins: how ts23d will rig without auto-rigging
|
|
28
|
+
Heat-diffusion auto-rigging (Pinocchio, Baran & Popović 2007; Blender "bone heat"; Maya heat map) guesses
|
|
29
|
+
which bone owns which vertex, and fails on messy meshes. ts23d knows. A sculpted character is an ordered
|
|
30
|
+
list of SDF edits (Dreams' model: Evans, SIGGRAPH 2015). Tag each edit with its bone, and the per-vertex
|
|
31
|
+
smooth-min blend weights that already mix the edits' colours *are* the skin weights. Keep the strongest
|
|
32
|
+
4 per vertex (`JOINTS_0`/`WEIGHTS_0`; Unreal's Interchange importer truncates to 4) and renormalise.
|
|
33
|
+
`skin.joints` order must match `inverseBindMatrices`; that ordering is the classic broken-skin bug, so
|
|
34
|
+
a test pins it. Rigid parts (`ctx.part`) stay node-animated: a tank has no skin.
|
|
35
|
+
|
|
36
|
+
## Ratified extensions worth writing
|
|
37
|
+
|
|
38
|
+
| Extension | Use in ts23d | Status |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `KHR_materials_emissive_strength` | lit windows, lamps, sci-fi panel glow | shipped |
|
|
41
|
+
| `KHR_lights_punctual` | headlamps, street lamps, cockpit lights as light nodes | shipped |
|
|
42
|
+
| `KHR_materials_variants` | liveries / camo schemes as switchable sets in one file | later: variants swap materials, and ts23d colour is vertex colour, so a livery needs a per-asset baked albedo first |
|
|
43
|
+
| `KHR_xmp_json_ld` | licence, author, generator version, `sources` inside the file | shipped |
|
|
44
|
+
| `KHR_materials_clearcoat` | glossy vehicle paint over the base | later |
|
|
45
|
+
| `KHR_materials_transmission` + `ior` + `volume` | real glass (bottles, cockpit canopies) | later |
|
|
46
|
+
| `KHR_materials_sheen` | cloth, banners, upholstery | later |
|
|
47
|
+
| `KHR_texture_transform` | strip offset / scale without re-baking UVs | later |
|
|
48
|
+
| `EXT_mesh_gpu_instancing` | rivets, bolts, scatter trees: one mesh, N transforms | later |
|
|
49
|
+
| `KHR_mesh_quantization` + `EXT_meshopt_compression` | `web` profile (smaller files) | later (opt-in: Godot rejects *required* extensions) |
|
|
50
|
+
| `MSFT_lod` (vendor, complete) | three LODs in one file with screen-coverage hints: `<asset>.glb`, `toLodGlb` | shipped (below) |
|
|
51
|
+
| `KHR_animation_pointer` | animate material / light values (blinking lamp, heat glow) | later |
|
|
52
|
+
| `KHR_texture_basisu` | KTX2 GPU-compressed trim sheet | later |
|
|
53
|
+
| `KHR_draco_mesh_compression` | — | no: meshopt is faster to decode and preferred |
|
|
54
|
+
| `KHR_gaussian_splatting` | — | no: we generate meshes, not captures |
|
|
55
|
+
| `KHR_interactivity` | — | no: behaviour is engine code, out of scope |
|
|
56
|
+
|
|
57
|
+
## LODs in one file: `MSFT_lod`
|
|
58
|
+
|
|
59
|
+
`t3d build` writes `lod0/1/2.glb` **and** `<asset>.glb` holding all three (`--lods split|single|both`,
|
|
60
|
+
default `both`). Default is `both` because separate files import into every engine's LOD tooling
|
|
61
|
+
today, while the single file serves MSFT_lod-aware loaders and one-file pipelines; disk is the only cost.
|
|
62
|
+
|
|
63
|
+
Layout (per the [spec](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/MSFT_lod)):
|
|
64
|
+
- LOD0's root node is the only scene root. It carries `extensions.MSFT_lod.ids` = the LOD1, LOD2 root
|
|
65
|
+
nodes (lower quality in order) and `extras.MSFT_screencoverage` = one value per LOD: the screen-height
|
|
66
|
+
fraction where that LOD hands over to the next, the last one being the cull (`[0.43, 0.086, 0.01]`
|
|
67
|
+
for tiger_1: LOD0 above 43 %, LOD1 to 8.6 %, LOD2 to 1 %, then nothing). Same convention as Unity
|
|
68
|
+
LODGroup's screen-relative transition height.
|
|
69
|
+
- LOD1/LOD2 roots (`<asset>_LOD1`, `<asset>_LOD2`) are parentless and **not** in `scenes[0].nodes`: the
|
|
70
|
+
spec says a client without the extension loads the highest LOD and ignores the rest, which only
|
|
71
|
+
holds if the alternates are outside the scene graph. Part names inside each LOD are unchanged
|
|
72
|
+
(`turret`, `wheel_l_0`), so game code binds the same names at any LOD.
|
|
73
|
+
- `MSFT_lod` is in `extensionsUsed`, never `extensionsRequired` (Godot refuses unknown required
|
|
74
|
+
extensions). Materials, the trim sheet, lights and identical motion samplers are written once; the
|
|
75
|
+
`rest` and `run` clips hold one channel per LOD node.
|
|
76
|
+
- Thresholds: `lodCoverage: [a, b, cull]` on `defineAsset`, else derived: a LOD with r × the
|
|
77
|
+
triangles reads equally dense at √r × the screen size (a ∈ [0.1, 0.6], b ∈ [0.02, a/2]); cull 1 %
|
|
78
|
+
for props, 0.2 % for anything ≥ 10 m so landmarks stay on the skyline.
|
|
79
|
+
|
|
80
|
+
| Loader | Reads MSFT_lod? | What a single file shows | Verified how |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| Babylon.js | yes, node + material level (progressive: loads low LODs first, ends on LOD0) | all LODs, switching | Babylon docs / spec "Known Implementations" |
|
|
83
|
+
| three.js `GLTFLoader` | no (community plugin `three-gltf-extensions`, in progress) | LOD0 only; the mixer warns "No target node found" for the alternates' tracks (harmless) | loaded tiger_1.glb with three 0.186: 23 meshes, same bounds as lod0.glb |
|
|
84
|
+
| Godot 4.5+ | no (not in `GLTFDocument` supported list) | LOD0 only (scene roots only, `gltf/naming_version` 2 default) | Godot master `gltf_document.cpp` |
|
|
85
|
+
| Godot 4.0–4.4 (or naming_version < 2) | no | **all three overlapping**: treats every parentless node as a root. Use `lod0.glb` there | same source, `_compute_node_heights` |
|
|
86
|
+
| Unity glTFast | "not investigated yet" in its docs | expected LOD0 only (instantiates the scene) — unverified | glTFast 6.0 features page |
|
|
87
|
+
| Unreal Interchange | not documented | unverified; use `lod0/1/2.glb` and Unreal's LOD import | — |
|
|
88
|
+
| Khronos glTF-Validator | info `UNSUPPORTED_EXTENSION` | 0 errors, 0 warnings (test `src/export/lod-glb.test.ts`) | test |
|
|
89
|
+
| ArcGIS Pro | yes, node level on the scene's first node | all LODs | Esri docs |
|
|
90
|
+
|
|
91
|
+
Engines that build LOD groups from names (Unity's FBX `_LOD0/_LOD1` convention) don't apply it to glTF
|
|
92
|
+
imports, so the split files plus each engine's own LOD importer remain the portable path.
|
|
93
|
+
|
|
94
|
+
## Physics (drafts: write behind a flag, plus engine conventions)
|
|
95
|
+
|
|
96
|
+
`KHR_physics_rigid_bodies` and `KHR_collision_shapes` (formerly `KHR_implicit_shapes`) are **Khronos
|
|
97
|
+
review drafts**, with importers in Blender, Godot and Babylon. ts23d will write:
|
|
98
|
+
- **Collision shapes** per part: box / sphere / capsule / cylinder fitted from the part's own geometry,
|
|
99
|
+
or a convex hull. Also as engine-convention nodes for importers without the extension:
|
|
100
|
+
`UCX_<name>` (Unreal) and `<name>-col` / `-colonly` (Godot).
|
|
101
|
+
- **Mass and centre of mass** from real volume × material density (steel 7850 kg/m³, oak 750,
|
|
102
|
+
brick 1900). A 57 t Tiger I is a QA check, not a guess: `real.mass` + tolerance.
|
|
103
|
+
- **Physics material**: friction / restitution per surface kind.
|
|
104
|
+
Status: later, behind `--physics`.
|
|
105
|
+
|
|
106
|
+
## Everything else: `extras`
|
|
107
|
+
Any JSON fits in `extras` on any object. ts23d uses `extras.ts23d` for sockets, emitters, the
|
|
108
|
+
generator version and provenance. Engines read it on import (Godot `meta`, Unity glTFast extras
|
|
109
|
+
callbacks, three.js `userData`).
|
|
110
|
+
|
|
111
|
+
## Sources
|
|
112
|
+
- Khronos extension registry: https://github.com/KhronosGroup/glTF/blob/main/extensions/README.md
|
|
113
|
+
- Skins tutorial: https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_020_Skins.html
|
|
114
|
+
- Unreal Interchange 4-influence truncation: https://forums.unrealengine.com/t/interchange-gltf-importer-truncates-skeletal-mesh-bone-influences-to-4-per-vertex/2745391
|
|
115
|
+
- glTF physics drafts: https://github.com/eoineoineoin/glTF_Physics
|
|
116
|
+
- Automatic rigging: Baran & Popović, "Automatic Rigging and Animation of 3D Characters", SIGGRAPH 2007
|
|
117
|
+
- Dreams SDF sculpting: Alex Evans, SIGGRAPH 2015, https://advances.realtimerendering.com/s2015/AlexEvans_SIGGRAPH-2015-sml.pdf
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Materials: one trim sheet, many physically distinct surfaces
|
|
2
|
+
|
|
3
|
+
Every asset is still **one material, one draw call**: a shared atlas with albedo, normal and ORM
|
|
4
|
+
(occlusion, roughness, metalness). A surface looks like oak, rusty iron or chrome because of what
|
|
5
|
+
its **strip** holds. Vertex colour multiplies the albedo: it tints a classic strip and sets the paint
|
|
6
|
+
colour of a painted strip.
|
|
7
|
+
|
|
8
|
+
## Layout (v2): 2048² atlas
|
|
9
|
+
|
|
10
|
+
| V range | Contents |
|
|
11
|
+
|---|---|
|
|
12
|
+
| 0 – 0.25 | 8 **classic** strips: `plain brick stone slate iron wood glass concrete`. Greyscale, tinted by vertex colour, unchanged from v1. |
|
|
13
|
+
| 0.25 – 0.75 | 16 **library** strips with their own colour (`MATERIAL_LIBRARY`, below) |
|
|
14
|
+
| 0.75 – 0.875 | 4 **custom** rows for project materials (`defineMaterial`) |
|
|
15
|
+
| 0.875 – 1 | 128 px cells: decals (row 0), foliage cards (from slot 8) |
|
|
16
|
+
|
|
17
|
+
Each strip row is 1/32 of the sheet: 64 px at 2048², the same V resolution v1 strips had at 1024².
|
|
18
|
+
The U density doubles, and cells keep their 128 px. That is why the house sheet moved to 2048².
|
|
19
|
+
|
|
20
|
+
**Cost.** Painting takes about 4 s at 2048² (about 1 s at 1024²). The three PNGs total about 3.6 MB at 2048² (v1 at 1024² was about 1 MB).
|
|
21
|
+
GPU memory is 16 MB per map plus mips. The site's trim-sheet set is shared by every gallery asset.
|
|
22
|
+
`--sheet-size 1024` still works (32 px strip rows). The CLI default is 1024 until `src/cli/args.ts`
|
|
23
|
+
moves it to 2048.
|
|
24
|
+
|
|
25
|
+
**Compatibility.** UVs come from `stripRect()`/`cellRect()` at compile time, so geometry and triangle
|
|
26
|
+
counts are unchanged and every recipe recompiles onto the new layout. Classic strips paint byte for
|
|
27
|
+
byte as before; only their atlas position moved.
|
|
28
|
+
|
|
29
|
+
## The library and its numbers
|
|
30
|
+
|
|
31
|
+
Metal colours are the measured reflectance (F0) from [physicallybased.info](https://physicallybased.info)
|
|
32
|
+
(API: `api.physicallybased.info/materials`, which cites refractiveindex.info). Each value is
|
|
33
|
+
converted to sRGB here. Dielectric albedos stay inside the 30–240 sRGB range from the
|
|
34
|
+
[PBR guide](https://www.adobe.com/learn/substance-3d-designer/web/the-pbr-guide-part-2). Roughness
|
|
35
|
+
ranges come from the [Roblox material reference](https://create.roblox.com/docs/art/modeling/material-reference),
|
|
36
|
+
which uses glTF metal/rough conventions.
|
|
37
|
+
|
|
38
|
+
| Strip | Kind | Albedo (sRGB) / 2nd | Rough | Metal | Basis |
|
|
39
|
+
|---|---|---|---|---|---|
|
|
40
|
+
| `oak` | planks + rings + ray flecks | `#a27b52` / latewood `#6a482b` | 0.68 | 0 | worn wood 0.7–0.9, glossy 0.2–0.3 (Roblox) |
|
|
41
|
+
| `pine` | planks + strong rings, knots | `#d2b183` / `#9b6a3c` | 0.72 | 0 | same |
|
|
42
|
+
| `wood_weathered` | planks + checks (eroded grain, splits) | `#9b968c` / `#4f463c` | 0.86 | 0 | worn wood upper range |
|
|
43
|
+
| `wood_painted` | planks + paint film + chips | paint `#ededed` (×vertex colour) / bare wood `#a08c74` | wood 0.84, film 0.62 | 0 | painted surfaces are dielectric |
|
|
44
|
+
| `iron_rust` | metal + oxide + streaks | iron `#8e8a85` / rust `#764323` | 0.55 → rust ≈ 0.9 | 0.9 → 0 in rust | Iron F0 0.53 linear; Rust (0.181, 0.057, 0.017) linear, non-metal (physicallybased) |
|
|
45
|
+
| `iron_cast` | metal, sand-cast pits, oxide | `#6c6966` / `#55372a` | 0.68 | 0.75 | darker weathered iron skin |
|
|
46
|
+
| `steel_painted` | metal + paint film + chips | paint `#eeeeee` (×vertex colour) / rusty steel `#5a3b2a` | film 0.42 | 0 film, 1 under | painted metal 0.3 (Roblox) |
|
|
47
|
+
| `steel_galvanised` | metal + zinc spangle | `#d5dadc` / white rust `#9a9c98` | 0.42 | 1 | Zinc (0.808, 0.844, 0.865) linear |
|
|
48
|
+
| `brass` | metal, light brush, tarnish | `#f5e5ae` / `#6b5a3a` | 0.28 | 1 | Brass (0.910, 0.778, 0.423) linear; pure metals 0.2–0.3 |
|
|
49
|
+
| `copper` | metal + verdigris | `#f2b89c` / `#6fa58f` | 0.32 | 1 | Copper (0.932, 0.623, 0.522) linear, nudged warmer to match photographs |
|
|
50
|
+
| `aluminium` | metal, brushed along U | `#e6e8e9` | 0.34 | 1 | Aluminium (0.916, 0.923, 0.924) linear, brushed is a little darker |
|
|
51
|
+
| `chrome` | metal, near mirror | `#d3d9dc` | 0.07 | 1 | Chromium (0.654, 0.685, 0.701) linear |
|
|
52
|
+
| `rubber` | noise | `#2c2c2c` | 0.78 | 0 | Tire 0.023 linear, roughness 0.7 (physicallybased); rubber 0.5–0.6 (Roblox) |
|
|
53
|
+
| `canvas` | weave + folds + stains | `#cbbea2` / `#8a7c62` | 0.88 | 0 | cotton/canvas 0.8–0.9 |
|
|
54
|
+
| `leather` | pebble grain + creases + wear | `#7c5438` / `#3e281a` | 0.58 | 0 | worn leather 0.5–0.7 |
|
|
55
|
+
| `plastic` | quiet noise | `#ececec` (×vertex colour) | 0.35 | 0 | matte plastic 0.4, glossy 0.1 |
|
|
56
|
+
|
|
57
|
+
Unverified: the wood albedos are chosen within the dielectric range and checked against photographs
|
|
58
|
+
by eye. They are not spectrometer data, because no published wood-species albedo table was found.
|
|
59
|
+
Emissive panels have no strip: glTF emission is a separate per-material slot, not ORM. Glowing parts
|
|
60
|
+
use `glow` rules in export.
|
|
61
|
+
|
|
62
|
+
**How to colour library strips.** Use `NATURAL` (white) as the vertex colour on library strips, so
|
|
63
|
+
the measured albedo reads true. On painted strips (`wood_painted`, `steel_painted`, `plastic`) the
|
|
64
|
+
vertex colour is the paint colour. Chips multiply the bare surface by it too, so they read as
|
|
65
|
+
darker, rougher wear.
|
|
66
|
+
|
|
67
|
+
## Painters (src/material/paint/)
|
|
68
|
+
|
|
69
|
+
Every painter writes float layers in real metres: `height`, `tone`, `rough`, `metal`, `mix` (blend
|
|
70
|
+
from albedo to albedo2) and `occl`. `compose` then derives the normal (Sobel of height) and the
|
|
71
|
+
cavity AO, and packs the result. These painters are new or extended in v2:
|
|
72
|
+
|
|
73
|
+
- `planks`: `rings` (latewood colour at a frequency the strip's V texels can hold), `rays`, `checks`, `paint`/`chips`/`paintRough`.
|
|
74
|
+
- `metal`: `brush`, `cast`, `spangle`, `oxide`, `paint`/`chips`, `streaks`.
|
|
75
|
+
- `weave`, `leather` (fabric.ts).
|
|
76
|
+
|
|
77
|
+
All painters are seeded and tileable along U, so the output is deterministic.
|
|
78
|
+
|
|
79
|
+
## Project materials: `defineMaterial`
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { defineMaterial, NATURAL, box } from '@developerz.ai/ts23d';
|
|
83
|
+
|
|
84
|
+
export const teak = defineMaterial({
|
|
85
|
+
id: 'teak_deck',
|
|
86
|
+
kind: 'planks', // start from any painter kind
|
|
87
|
+
scaleMetres: 0.6,
|
|
88
|
+
roughness: 0.6,
|
|
89
|
+
albedo: '#8a5a32', // sRGB hex or linear RGB
|
|
90
|
+
albedo2: '#5a3a1e', // latewood
|
|
91
|
+
params: { width: 0.12, rings: 0.5, knots: 0.5 },
|
|
92
|
+
// optional: your own layer on top, deterministic via kit.noise / kit.hash
|
|
93
|
+
paint: (s, kit) => kit.each((i, xm, ym) => {
|
|
94
|
+
s.tone[i] *= 1 - 0.1 * kit.smooth(0.4, 0.8, kit.noise(xm, ym, 0.5, 0.05, 1));
|
|
95
|
+
}),
|
|
96
|
+
sources: ['https://…'],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
box({ size: [4, 2, 0.05], strip: teak, color: NATURAL });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- It validates first: bad input throws `T3D_MATERIAL_INVALID`, an unknown id `T3D_MATERIAL_UNKNOWN`, a
|
|
103
|
+
built-in or differently redefined id `T3D_MATERIAL_CONFLICT`, and a fifth custom row
|
|
104
|
+
`T3D_SHEET_FULL`. A painter that writes NaN or ∞ fails the paint with `T3D_MATERIAL_INVALID`.
|
|
105
|
+
- Rows are given out in registration order. Define materials in one shared module that every recipe
|
|
106
|
+
imports, so the same program always produces the same bytes. Redefining an id with the identical
|
|
107
|
+
definition is a no-op.
|
|
108
|
+
- `DEFAULT_SHEET` paints defined materials automatically. To restyle a built-in for one project,
|
|
109
|
+
override its recipe: `paintTrimSheet({ ...DEFAULT_SHEET, strips: { ...DEFAULT_SHEET.strips, oak: {...} } })`.
|
|
110
|
+
- `materialOf(id)` returns the recipe, origin (classic/library/defined), description and sources.
|
|
111
|
+
`resetMaterials()` is for tests and long-lived workers.
|
|
112
|
+
|
|
113
|
+
## CC0 image strips (opt-in)
|
|
114
|
+
|
|
115
|
+
A strip can come from a photo-scanned CC0 PBR set instead of a painter:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
defineMaterial({ id: 'ph_wood_planks_grey', roughness: 0.85, texture: {
|
|
119
|
+
source: 'polyhaven', id: 'wood_planks_grey', license: 'CC0-1.0',
|
|
120
|
+
page: 'https://polyhaven.com/a/wood_planks_grey', author: 'Rob Tuytel', metres: [1.5, 1.5],
|
|
121
|
+
maps: { albedo: { url: '…_diff_1k.png', sha256: '…' }, normal: { … nor_gl … }, arm: { … } },
|
|
122
|
+
} });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`CC0_WOOD_PLANKS_GREY` ships as a working example. It is not registered by default.
|
|
126
|
+
|
|
127
|
+
- **Offline compile.** `bun run textures [module…]` downloads every declared map once into
|
|
128
|
+
`node_modules/.cache/ts23d/textures/<sha256>.png` (gitignored). It checks each file's sha256 and
|
|
129
|
+
refuses a mismatch. `provideTextures(await loadTextureCache())` (from `scripts/textures.ts`) hands
|
|
130
|
+
the bytes to the painter. The library never touches the network. If a map is missing, painting
|
|
131
|
+
throws `T3D_TEXTURE_MISSING`, and the error names the command to run.
|
|
132
|
+
- **Painting.** PNGs are 8- or 16-bit (JPEG is not decoded). They are box-filtered onto the strip at
|
|
133
|
+
true scale, with whole repeats along U so the strip still tiles. V is flipped so the image's top is
|
|
134
|
+
world up. The OpenGL normal's green channel is flipped to the sheet's convention. `arm` fills
|
|
135
|
+
occlusion, roughness and metalness.
|
|
136
|
+
- **Credits.** Only CC0 licences are accepted. When an asset samples an image strip, the GLB's XMP
|
|
137
|
+
packet gets a `dc:contributor` line with the set, author, licence and page. Assets without image
|
|
138
|
+
strips are byte-identical to before.
|
|
139
|
+
- **Next (planned).** ambientCG ships its sets as zips, and zip extraction is not built yet, so
|
|
140
|
+
ambientCG sets work only when pointed at a PNG URL or a local file (`source: 'file'`). The CLI
|
|
141
|
+
does not load the cache yet (see the roadmap request).
|
|
142
|
+
|
|
143
|
+
## Seeing it: the beauty renderer
|
|
144
|
+
|
|
145
|
+
`src/render/shade.ts` reads roughness and metalness per texel from the ORM. Specular uses a
|
|
146
|
+
normalised Blinn–Phong sun lobe plus a **studio environment reflection** (`envRadiance` in
|
|
147
|
+
`lighting.ts`). The environment has a bright cyclorama horizon, a cooler zenith, a dark band where
|
|
148
|
+
the floor meets the wall, and the lit floor. Roughness blurs it toward its mean, so chrome shows the
|
|
149
|
+
horizon line, brass looks gold, and oak stays matte. Fresnel uses Schlick with a roughness-limited
|
|
150
|
+
grazing term. Glass keeps its own path. The site viewer gets the same ORM through three.js
|
|
151
|
+
`MeshStandardMaterial`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 001 — Assets are compiled from recipes, not generated by AI mesh models
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The pipeline this project is inspired by started on AI mesh generation (Meshy plus an image model) and froze it. Measured pains:
|
|
9
|
+
- Credits per attempt; iteration multiplies cost.
|
|
10
|
+
- No reproducibility: same prompt, different mesh. Nothing to diff, nothing to regenerate in CI.
|
|
11
|
+
- One fused mesh: no named parts, no pivots, so nothing animates without manual work.
|
|
12
|
+
- A texture per asset: a draw call per asset, inconsistent texel density.
|
|
13
|
+
- Every mesh needed repair: scale, pivots, holes, topology.
|
|
14
|
+
It replaced generation with a recipe compiler, and those problems went away for hard-surface assets.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
The core path is a deterministic compiler: recipe source → mesh IR → GLB. AI enters as the **author** of the recipe (a coding agent), not as the mesh generator. Mesh-generating models are an optional, cached adapter for what recipes do badly ([idea/08](../idea/08-neural-adapters.md)).
|
|
18
|
+
|
|
19
|
+
## Alternatives considered
|
|
20
|
+
- **AI mesh generation as the core** — why not: the five pains above; outputs aren't reproducible or partitioned into named parts.
|
|
21
|
+
- **Procedural modelling in a DCC (Blender geometry nodes, Houdini)** — why not: a heavy external runtime, node graphs agents edit poorly, and no plain-text diff.
|
|
22
|
+
- **Kitbashing from asset libraries** — why not: licence per asset, mismatched styles and texel density.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
- Easier: free iteration, reproducible builds, named animatable parts, one material, CI gates.
|
|
26
|
+
- Harder: organic and sculpted forms (people, animals) are a weak spot. Answered by the planned `creature` kind and `/neural` adapter.
|
|
27
|
+
- Revisit when: an open-weight model outputs part-segmented, reproducible, low-poly meshes with consistent UVs on CPU.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 002 — Recipes are TypeScript modules, not JSON
|
|
2
|
+
|
|
3
|
+
Status: accepted
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
The predecessor pipeline used JSON recipes. They worked, then grew: repeat syntax for windows, `$ref` for shared wheels, variant tables for streets, an expression mini-language for derived sizes. Each was a worse copy of a programming-language feature, with no type checker, no editor support and a schema the agent had to learn. The authors are coding agents, and TypeScript is what they write most fluently.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
An asset is a TypeScript module exporting `defineAsset({ …, build(ctx) })`. Loops, functions, imports and types are the composition model. String mini-languages (split grammar, Whyte notation) stay as small, parsed helpers called *from* TS, where the domain notation is better than code.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **JSON / YAML recipes** — why not: every growth step re-invents loops, references and expressions, untyped.
|
|
15
|
+
- **A custom DSL with its own parser** — why not: an agent has never seen it; we'd own a language, tooling and error messages.
|
|
16
|
+
- **Python** — why not: a second runtime next to a TS/Bun toolchain and a browser gallery; recipes must also run in the browser.
|
|
17
|
+
- **Lua / sandboxed scripting** — why not: weaker typing and agent fluency, an extra interpreter.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
- Easier: composition (`ctx.use`), shared helpers, typed options with docs on hover, the same module runs in the CLI, tests and the browser gallery.
|
|
21
|
+
- Harder: a recipe is code, so it can do anything code can, including breaking determinism. Mitigated by the determinism rules and a sealed `Math.random` in tests. Running untrusted recipes means running untrusted code ([SECURITY.md](../../SECURITY.md)).
|
|
22
|
+
- Revisit when: recipes must be authored by non-programmers in a GUI. Then emit TS from the GUI; don't add a data format.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 003 — Every asset shares one trim-sheet material
|
|
2
|
+
|
|
3
|
+
Status: superseded by 008
|
|
4
|
+
Date: 2026-10-25
|
|
5
|
+
Supersedes: —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
Per-asset textures meant a material and a draw call per asset, texel density that varied by asset size, and a new texture for every recolour. Game scenes with hundreds of props pay for that in draw calls and memory.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
One 1024² trim sheet (albedo + normal + ORM) holds tiling strips and fixed decal cells. Every primitive maps UVs into it by world size (metres per rect). Hue comes from linear vertex colour multiplied over the greyscale sheet. Every GLB carries exactly one material, `ts23d_trim`. As of 2026-10 the material has no texture bound; the painter is M2.
|
|
12
|
+
|
|
13
|
+
## Alternatives considered
|
|
14
|
+
- **Per-asset unwrapped textures** — why not: a draw call per asset, uneven texel density, needs a painter per asset.
|
|
15
|
+
- **Flat colour only, no textures** — why not: caps visual quality; no brick, grain or rivet detail.
|
|
16
|
+
- **Texture arrays / bindless per asset** — why not: engine-specific, defeats "drop the GLB into any engine".
|
|
17
|
+
|
|
18
|
+
## Consequences
|
|
19
|
+
- Easier: a whole scene batches into one material; palette swaps are recipe edits; equal texel density everywhere.
|
|
20
|
+
- Harder: unique details (a specific logo, a painted mural) must fit a cell; a hero asset can't have its own unwrap.
|
|
21
|
+
- Revisit when: a target engine needs per-asset PBR for hero assets. Then add a second, opt-in path; keep the trim sheet the default.
|