@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,71 @@
|
|
|
1
|
+
// View-distance hints for the single-file GLB: MSFT_lod (vendor extension, node level) on the
|
|
2
|
+
// LOD0 root lists the LOD1/LOD2 roots, and `extras.MSFT_screencoverage` holds, per LOD, the
|
|
3
|
+
// screen-height fraction below which the next LOD (or nothing) takes over — Unity LODGroup's
|
|
4
|
+
// "screen relative transition height". Spec: KhronosGroup/glTF extensions/2.0/Vendor/MSFT_lod.
|
|
5
|
+
//
|
|
6
|
+
// Default thresholds match screen-space triangle density: a LOD with r × the triangles looks as
|
|
7
|
+
// dense at √r × the size on screen (triangles per pixel ∝ tris / coverage²). Landmarks (≥ 10 m)
|
|
8
|
+
// cull later than props, so a building stays on the skyline.
|
|
9
|
+
|
|
10
|
+
import type { CompiledAsset } from '../core/index.ts';
|
|
11
|
+
import { Ts23dError, worldMeshes } from '../core/index.ts';
|
|
12
|
+
|
|
13
|
+
export const MSFT_LOD = 'MSFT_lod';
|
|
14
|
+
export const SCREEN_COVERAGE = 'MSFT_screencoverage';
|
|
15
|
+
|
|
16
|
+
export type LodCoverage = readonly [number, number, number];
|
|
17
|
+
|
|
18
|
+
const tris = (a: CompiledAsset): number =>
|
|
19
|
+
a.parts.reduce((s, p) => s + (p.mesh ? p.mesh.indices.length / 3 : 0), 0);
|
|
20
|
+
|
|
21
|
+
/** Largest extent in metres: `real` when declared, else the LOD0 world bounds. */
|
|
22
|
+
function extent(a: CompiledAsset): number {
|
|
23
|
+
if (a.real) return Math.max(a.real.length, a.real.width, a.real.height);
|
|
24
|
+
const lo = [Infinity, Infinity, Infinity];
|
|
25
|
+
const hi = [-Infinity, -Infinity, -Infinity];
|
|
26
|
+
for (const mesh of worldMeshes(a.parts).values()) {
|
|
27
|
+
const p = mesh.positions;
|
|
28
|
+
for (let i = 0; i < p.length; i++) {
|
|
29
|
+
const k = i % 3;
|
|
30
|
+
const v = p[i] ?? 0;
|
|
31
|
+
lo[k] = Math.min(lo[k] ?? 0, v);
|
|
32
|
+
hi[k] = Math.max(hi[k] ?? 0, v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return Math.max(0, ...hi.map((h, k) => h - (lo[k] ?? 0)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const clamp = (v: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, v));
|
|
39
|
+
const round = (v: number): number => Math.round(v * 1000) / 1000;
|
|
40
|
+
|
|
41
|
+
/** Default [LOD0→1, LOD1→2, cull] for an asset from its compiled LODs (any subset, LOD0 first). */
|
|
42
|
+
export function defaultCoverage(lods: readonly CompiledAsset[]): LodCoverage {
|
|
43
|
+
const byLod = new Map<number, number>(lods.map((a) => [a.lod, tris(a)]));
|
|
44
|
+
const top = lods[0];
|
|
45
|
+
const t0 = Math.max(1, top ? tris(top) : 1);
|
|
46
|
+
const ratio = (lod: number): number => (byLod.get(lod) ?? t0) / t0;
|
|
47
|
+
const c1 = round(clamp(Math.sqrt(ratio(1)), 0.1, 0.6));
|
|
48
|
+
const c2 = round(clamp(Math.sqrt(ratio(2)), 0.02, c1 / 2));
|
|
49
|
+
const cull = top && extent(top) >= 10 ? 0.002 : 0.01;
|
|
50
|
+
return [c1, c2, Math.min(cull, round(c2 / 2))];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function checkCoverage(c: readonly number[], asset: string): void {
|
|
54
|
+
const [a = -1, b = -1, cull = -1] = c;
|
|
55
|
+
if (c.length !== 3 || !(a < 1 && a > b && b > cull && cull >= 0))
|
|
56
|
+
throw new Ts23dError('T3D_RECIPE_INVALID', {
|
|
57
|
+
cause: `lodCoverage ${JSON.stringify(c)} on '${asset}' is not strictly descending in [0, 1)`,
|
|
58
|
+
fix: 'give [LOD0→LOD1, LOD1→LOD2, cull] screen-height fractions, e.g. [0.5, 0.2, 0.01]; 0 = never cull',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* MSFT_screencoverage for the LODs actually written (e.g. [0, 2]): entry j is the coverage where
|
|
64
|
+
* LOD j hands over — to the next included LOD's switch point, or to nothing (cull) for the last.
|
|
65
|
+
*/
|
|
66
|
+
export function coverageFor(levels: readonly number[], c: LodCoverage): number[] {
|
|
67
|
+
return levels.map((_, j) => {
|
|
68
|
+
const next = levels[j + 1];
|
|
69
|
+
return next === undefined ? c[2] : (c[next - 1] ?? c[2]);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// The (at most) four shared materials, one per Surface in use. All sample the same trim sheet and
|
|
2
|
+
// take tint from COLOR_0, so batching holds; only the shading model differs. Glass/translucent use
|
|
3
|
+
// KHR_materials_transmission + _volume + _ior (three, Babylon, glTFast); engines without them fall
|
|
4
|
+
// back to alpha BLEND via baseColorFactor.a. With a painted sheet bound (textures.ts), every
|
|
5
|
+
// material samples it: baseColor (sRGB, alpha = cutout mask), normal, and ORM in both the
|
|
6
|
+
// occlusion and metallicRoughness slots. Opaque/cutout factors go to 1 so the ORM texture rules;
|
|
7
|
+
// glass/translucent keep theirs as multipliers (clear stays clear whatever strip it maps).
|
|
8
|
+
|
|
9
|
+
import type { Surface } from '../core/index.ts';
|
|
10
|
+
import type { Json } from './bin-writer.ts';
|
|
11
|
+
|
|
12
|
+
const MATERIALS: Readonly<Record<Surface, Json>> = {
|
|
13
|
+
opaque: {
|
|
14
|
+
name: 'ts23d_trim',
|
|
15
|
+
pbrMetallicRoughness: {
|
|
16
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
17
|
+
metallicFactor: 0,
|
|
18
|
+
roughnessFactor: 0.8,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
cutout: {
|
|
22
|
+
name: 'ts23d_trim_cutout',
|
|
23
|
+
alphaMode: 'MASK',
|
|
24
|
+
alphaCutoff: 0.5,
|
|
25
|
+
doubleSided: true,
|
|
26
|
+
pbrMetallicRoughness: {
|
|
27
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
28
|
+
metallicFactor: 0,
|
|
29
|
+
roughnessFactor: 0.9,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
glass: {
|
|
33
|
+
name: 'ts23d_glass',
|
|
34
|
+
alphaMode: 'BLEND',
|
|
35
|
+
pbrMetallicRoughness: {
|
|
36
|
+
baseColorFactor: [1, 1, 1, 0.25],
|
|
37
|
+
metallicFactor: 0,
|
|
38
|
+
roughnessFactor: 0.05,
|
|
39
|
+
},
|
|
40
|
+
extensions: {
|
|
41
|
+
KHR_materials_transmission: { transmissionFactor: 1 },
|
|
42
|
+
KHR_materials_volume: { thicknessFactor: 0.01 },
|
|
43
|
+
KHR_materials_ior: { ior: 1.5 },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
translucent: {
|
|
47
|
+
name: 'ts23d_translucent',
|
|
48
|
+
alphaMode: 'BLEND',
|
|
49
|
+
pbrMetallicRoughness: {
|
|
50
|
+
baseColorFactor: [1, 1, 1, 0.6],
|
|
51
|
+
metallicFactor: 0,
|
|
52
|
+
roughnessFactor: 0.35,
|
|
53
|
+
},
|
|
54
|
+
extensions: {
|
|
55
|
+
KHR_materials_transmission: { transmissionFactor: 0.6 },
|
|
56
|
+
KHR_materials_volume: { thicknessFactor: 0.02 },
|
|
57
|
+
KHR_materials_ior: { ior: 1.45 },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const ORDER: readonly Surface[] = ['opaque', 'cutout', 'glass', 'translucent'];
|
|
63
|
+
|
|
64
|
+
/** The same material sampling the sheet (texture indices fixed by textures.ts). */
|
|
65
|
+
function textured(m: Json, surface: Surface): Json {
|
|
66
|
+
const pbr = m['pbrMetallicRoughness'] as Json;
|
|
67
|
+
const own = surface === 'opaque' || surface === 'cutout';
|
|
68
|
+
return {
|
|
69
|
+
...m,
|
|
70
|
+
pbrMetallicRoughness: {
|
|
71
|
+
...pbr,
|
|
72
|
+
baseColorTexture: { index: 0 },
|
|
73
|
+
metallicRoughnessTexture: { index: 2 },
|
|
74
|
+
...(own ? { metallicFactor: 1, roughnessFactor: 1 } : {}),
|
|
75
|
+
},
|
|
76
|
+
normalTexture: { index: 1 },
|
|
77
|
+
occlusionTexture: { index: 2 },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Materials for the surfaces present, plus each surface's index and the extensions used. */
|
|
82
|
+
export function materialsFor(
|
|
83
|
+
used: ReadonlySet<Surface>,
|
|
84
|
+
withSheet = false,
|
|
85
|
+
): {
|
|
86
|
+
materials: Json[];
|
|
87
|
+
index: Map<Surface, number>;
|
|
88
|
+
extensionsUsed: string[];
|
|
89
|
+
} {
|
|
90
|
+
const present = ORDER.filter((s) => s === 'opaque' || used.has(s));
|
|
91
|
+
const index = new Map(present.map((s, i) => [s, i] as const));
|
|
92
|
+
const ext = new Set<string>();
|
|
93
|
+
for (const s of present)
|
|
94
|
+
for (const e of Object.keys((MATERIALS[s]['extensions'] as Json | undefined) ?? {})) ext.add(e);
|
|
95
|
+
const materials = present.map((s) => (withSheet ? textured(MATERIALS[s], s) : MATERIALS[s]));
|
|
96
|
+
return { materials, index, extensionsUsed: [...ext].sort() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const EMISSIVE_STRENGTH = 'KHR_materials_emissive_strength';
|
|
100
|
+
|
|
101
|
+
/** A lit surface: lamp lenses, windows at night, panel glow. */
|
|
102
|
+
export interface GlowRule {
|
|
103
|
+
/** Part names or globs: 'headlamp_*', 'window_*'. */
|
|
104
|
+
readonly parts: readonly string[];
|
|
105
|
+
/** Linear RGB 0..1 (use `hex('#ffd9a0')` from /material). Default warm white. */
|
|
106
|
+
readonly color?: readonly [number, number, number];
|
|
107
|
+
/** Brightness multiplier (KHR_materials_emissive_strength, Ratified); 1 = plain glTF. */
|
|
108
|
+
readonly strength?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The opaque trim material plus emission. emissiveFactor caps at 1 in core glTF; anything
|
|
113
|
+
* brighter rides KHR_materials_emissive_strength (engines without it see the clamped glow).
|
|
114
|
+
*/
|
|
115
|
+
export function glowMaterial(rule: GlowRule, i: number, withSheet: boolean): Json {
|
|
116
|
+
const base = withSheet ? textured(MATERIALS.opaque, 'opaque') : MATERIALS.opaque;
|
|
117
|
+
const strength = rule.strength ?? 4;
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
name: `ts23d_glow_${i}`,
|
|
121
|
+
emissiveFactor: [...(rule.color ?? [1, 0.85, 0.6])],
|
|
122
|
+
...(strength !== 1
|
|
123
|
+
? { extensions: { [EMISSIVE_STRENGTH]: { emissiveStrength: strength } } }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Batching for engines: every part that is NOT a pivot is merged into its nearest pivot ancestor,
|
|
2
|
+
// so draw calls = pivots with geometry, not parts. A pivot is anything the engine may move: a
|
|
3
|
+
// group (`ctx.group`, a named handle like `turret`, `gun`, `door_l`) or a part with `motion`.
|
|
4
|
+
// Static top-level geometry merges into one `body` part. Names the engine binds to survive.
|
|
5
|
+
// Non-opaque surfaces split into `<pivot>_<surface>` children (one draw call per surface), and
|
|
6
|
+
// glowing parts (GlbOptions.glow) into `<pivot>_glow_<i>` children with their emissive material.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type CompiledAsset,
|
|
10
|
+
type Mesh,
|
|
11
|
+
merge,
|
|
12
|
+
type Part,
|
|
13
|
+
rotateZ,
|
|
14
|
+
type Surface,
|
|
15
|
+
toWorld,
|
|
16
|
+
type WorldTransform,
|
|
17
|
+
worldTransforms,
|
|
18
|
+
} from '../core/index.ts';
|
|
19
|
+
|
|
20
|
+
const isPivot = (p: Part): boolean => p.mesh === null || p.motion !== null;
|
|
21
|
+
|
|
22
|
+
/** Merged layer parts that glow → index of the glow rule (read by the GLB writer). */
|
|
23
|
+
export const glowLayer = new WeakMap<Part, number>();
|
|
24
|
+
|
|
25
|
+
/** `*`/`?` glob → anchored RegExp (export cannot import /qa: same tier). */
|
|
26
|
+
export const globRe = (pattern: string): RegExp =>
|
|
27
|
+
new RegExp(
|
|
28
|
+
`^${pattern
|
|
29
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
30
|
+
.replaceAll('*', '.*')
|
|
31
|
+
.replaceAll('?', '.')}$`,
|
|
32
|
+
);
|
|
33
|
+
const SURFACES: readonly Surface[] = ['cutout', 'glass', 'translucent'];
|
|
34
|
+
|
|
35
|
+
/** Re-express a part's mesh in its pivot's local space. */
|
|
36
|
+
function intoPivot(mesh: Mesh, part: WorldTransform, pivot: WorldTransform): Mesh {
|
|
37
|
+
const positions = new Float32Array(mesh.positions.length);
|
|
38
|
+
const normals = new Float32Array(mesh.normals.length);
|
|
39
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
40
|
+
const w = toWorld(part, [
|
|
41
|
+
mesh.positions[i] ?? 0,
|
|
42
|
+
mesh.positions[i + 1] ?? 0,
|
|
43
|
+
mesh.positions[i + 2] ?? 0,
|
|
44
|
+
]);
|
|
45
|
+
positions.set(
|
|
46
|
+
rotateZ([w[0] - pivot.origin[0], w[1] - pivot.origin[1], w[2] - pivot.origin[2]], -pivot.yaw),
|
|
47
|
+
i,
|
|
48
|
+
);
|
|
49
|
+
normals.set(
|
|
50
|
+
rotateZ(
|
|
51
|
+
rotateZ(
|
|
52
|
+
[mesh.normals[i] ?? 0, mesh.normals[i + 1] ?? 0, mesh.normals[i + 2] ?? 0],
|
|
53
|
+
part.yaw,
|
|
54
|
+
),
|
|
55
|
+
-pivot.yaw,
|
|
56
|
+
),
|
|
57
|
+
i,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return { ...mesh, positions, normals };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function mergeToPivots(
|
|
64
|
+
asset: CompiledAsset,
|
|
65
|
+
glow: readonly (readonly string[])[] = [],
|
|
66
|
+
): CompiledAsset {
|
|
67
|
+
const glowRes = glow.map((globs) => globs.map(globRe));
|
|
68
|
+
const glowOf = (name: string): number => glowRes.findIndex((rs) => rs.some((r) => r.test(name)));
|
|
69
|
+
const byName = new Map(asset.parts.map((p) => [p.name, p]));
|
|
70
|
+
const xf = worldTransforms(asset.parts);
|
|
71
|
+
const ROOT = 'body';
|
|
72
|
+
const identity: WorldTransform = { origin: [0, 0, 0], yaw: 0 };
|
|
73
|
+
const pivotOf = (p: Part): string => {
|
|
74
|
+
let cur = p.parent ? byName.get(p.parent) : undefined;
|
|
75
|
+
while (cur && !isPivot(cur)) cur = cur.parent ? byName.get(cur.parent) : undefined;
|
|
76
|
+
return cur?.name ?? ROOT;
|
|
77
|
+
};
|
|
78
|
+
// Buckets keyed by pivot + surface: opaque merges into the pivot's own mesh; each other
|
|
79
|
+
// surface in use becomes one child `<pivot>_<surface>` so materials never mix in a primitive.
|
|
80
|
+
const extra = new Map<string, Mesh[]>();
|
|
81
|
+
const bucket = (pivot: string, surface: Surface | undefined, mesh: Mesh, g = -1): void => {
|
|
82
|
+
const k = `${pivot}|${g >= 0 ? `glow_${g}` : (surface ?? 'opaque')}`;
|
|
83
|
+
extra.set(k, [...(extra.get(k) ?? []), mesh]);
|
|
84
|
+
};
|
|
85
|
+
for (const p of asset.parts) {
|
|
86
|
+
if (!p.mesh) continue;
|
|
87
|
+
const g = glowOf(p.name);
|
|
88
|
+
if (isPivot(p)) {
|
|
89
|
+
bucket(p.name, p.surface, p.mesh, g);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const pivot = pivotOf(p);
|
|
93
|
+
const t = xf.get(p.name) ?? identity;
|
|
94
|
+
const pt = pivot === ROOT ? identity : (xf.get(pivot) ?? identity);
|
|
95
|
+
bucket(pivot, p.surface, intoPivot(p.mesh, t, pt), g);
|
|
96
|
+
}
|
|
97
|
+
const parts: Part[] = [];
|
|
98
|
+
const layers = (pivot: string, parent: string | null): void => {
|
|
99
|
+
for (const surface of SURFACES) {
|
|
100
|
+
const meshes = extra.get(`${pivot}|${surface}`);
|
|
101
|
+
if (!meshes) continue;
|
|
102
|
+
parts.push({
|
|
103
|
+
name: `${pivot}_${surface}`,
|
|
104
|
+
mesh: merge(meshes),
|
|
105
|
+
at: [0, 0, 0],
|
|
106
|
+
parent,
|
|
107
|
+
yaw: 0,
|
|
108
|
+
motion: null,
|
|
109
|
+
surface,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
glow.forEach((_, g) => {
|
|
113
|
+
const meshes = extra.get(`${pivot}|glow_${g}`);
|
|
114
|
+
if (!meshes) return;
|
|
115
|
+
const part: Part = {
|
|
116
|
+
name: `${pivot}_glow_${g}`,
|
|
117
|
+
mesh: merge(meshes),
|
|
118
|
+
at: [0, 0, 0],
|
|
119
|
+
parent,
|
|
120
|
+
yaw: 0,
|
|
121
|
+
motion: null,
|
|
122
|
+
};
|
|
123
|
+
glowLayer.set(part, g);
|
|
124
|
+
parts.push(part);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
const body = extra.get(`${ROOT}|opaque`);
|
|
128
|
+
if (body)
|
|
129
|
+
parts.push({
|
|
130
|
+
name: ROOT,
|
|
131
|
+
mesh: merge(body),
|
|
132
|
+
at: [0, 0, 0],
|
|
133
|
+
parent: null,
|
|
134
|
+
yaw: 0,
|
|
135
|
+
motion: null,
|
|
136
|
+
});
|
|
137
|
+
layers(ROOT, null);
|
|
138
|
+
for (const p of asset.parts) {
|
|
139
|
+
if (!isPivot(p)) continue;
|
|
140
|
+
const own = extra.get(`${p.name}|opaque`);
|
|
141
|
+
const { surface: _drop, ...rest } = p;
|
|
142
|
+
parts.push({ ...rest, mesh: own ? merge(own) : null });
|
|
143
|
+
layers(p.name, p.name);
|
|
144
|
+
}
|
|
145
|
+
return { ...asset, parts };
|
|
146
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Provenance inside the file: `asset.generator` with the version, `asset.copyright`, and an XMP
|
|
2
|
+
// packet (KHR_xmp_json_ld, Ratified) with title, description, creators, rights, licence and the
|
|
3
|
+
// recipe's sources — what Sketchfab, Blender and asset stores read. No dates: bytes stay
|
|
4
|
+
// deterministic.
|
|
5
|
+
|
|
6
|
+
import type { Json } from './bin-writer.ts';
|
|
7
|
+
import { GENERATOR } from './version.ts';
|
|
8
|
+
|
|
9
|
+
export interface GlbMeta {
|
|
10
|
+
/** Default: the asset name. */
|
|
11
|
+
readonly title?: string;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
readonly creators?: readonly string[];
|
|
14
|
+
/** Goes to `asset.copyright` and `dc:rights`, e.g. "© 2026 Jane Doe". */
|
|
15
|
+
readonly copyright?: string;
|
|
16
|
+
/** SPDX id or licence URL → `xmpRights:UsageTerms`, e.g. "CC-BY-4.0". */
|
|
17
|
+
readonly license?: string;
|
|
18
|
+
/** Where the numbers came from (the recipe's `sources`) → `dc:relation`. */
|
|
19
|
+
readonly sources?: readonly string[];
|
|
20
|
+
/** Third-party inputs (e.g. CC0 texture sets) → `dc:contributor`. Filled by export. */
|
|
21
|
+
readonly credits?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const XMP = 'KHR_xmp_json_ld';
|
|
25
|
+
const alt = (value: string): Json => ({
|
|
26
|
+
'@type': 'rdf:Alt',
|
|
27
|
+
'rdf:_1': { '@language': 'x-default', '@value': value },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** `asset` block, plus root `extensions` and the extension name when an XMP packet is written. */
|
|
31
|
+
export function metadata(
|
|
32
|
+
name: string,
|
|
33
|
+
meta: GlbMeta | undefined,
|
|
34
|
+
): { asset: Json; extensions?: Json; used: string[] } {
|
|
35
|
+
const asset: Json = {
|
|
36
|
+
version: '2.0',
|
|
37
|
+
generator: GENERATOR,
|
|
38
|
+
...(meta?.copyright ? { copyright: meta.copyright } : {}),
|
|
39
|
+
};
|
|
40
|
+
if (!meta) return { asset, used: [] };
|
|
41
|
+
const packet: Json = {
|
|
42
|
+
'@context': {
|
|
43
|
+
dc: 'http://purl.org/dc/elements/1.1/',
|
|
44
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
45
|
+
xmp: 'http://ns.adobe.com/xap/1.0/',
|
|
46
|
+
xmpRights: 'http://ns.adobe.com/xap/1.0/rights/',
|
|
47
|
+
},
|
|
48
|
+
'dc:title': alt(meta.title ?? name),
|
|
49
|
+
...(meta.description ? { 'dc:description': alt(meta.description) } : {}),
|
|
50
|
+
...(meta.creators?.length ? { 'dc:creator': { '@list': [...meta.creators] } } : {}),
|
|
51
|
+
...(meta.copyright ? { 'dc:rights': alt(meta.copyright) } : {}),
|
|
52
|
+
...(meta.license ? { 'xmpRights:UsageTerms': alt(meta.license) } : {}),
|
|
53
|
+
...(meta.sources?.length ? { 'dc:relation': { '@set': [...meta.sources] } } : {}),
|
|
54
|
+
...(meta.credits?.length ? { 'dc:contributor': { '@set': [...meta.credits] } } : {}),
|
|
55
|
+
'xmp:CreatorTool': GENERATOR,
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
asset: { ...asset, extensions: { [XMP]: { packet: 0 } } },
|
|
59
|
+
extensions: { [XMP]: { packets: [packet] } },
|
|
60
|
+
used: [XMP],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Bakes a part's Motion into one glTF sampler + channel of the looping `run` clip.
|
|
2
|
+
// Seamless by construction: every motion is rounded to a WHOLE number of cycles over the clip,
|
|
3
|
+
// so the last key equals the first (a 0.8 rps wheel over 4 s = 3 turns, not 3.2 — no jump at
|
|
4
|
+
// the loop point). Spins get ≥ 12 keys per turn so LINEAR quaternion interpolation never takes
|
|
5
|
+
// the short way round. Time arrays are shared between channels of equal key count.
|
|
6
|
+
|
|
7
|
+
import type { Motion } from '../core/index.ts';
|
|
8
|
+
import { quatFromAxisAngle } from '../core/index.ts';
|
|
9
|
+
import type { BinWriter, Json } from './bin-writer.ts';
|
|
10
|
+
|
|
11
|
+
export const LOOP_SECONDS = 4;
|
|
12
|
+
|
|
13
|
+
/** Whole cycles of `rate` (Hz or rev/s) over the clip, at least one. */
|
|
14
|
+
export const cyclesOf = (rate: number): number =>
|
|
15
|
+
Math.max(1, Math.round(Math.abs(rate) * LOOP_SECONDS)) * Math.sign(rate || 1);
|
|
16
|
+
|
|
17
|
+
const timeAccessors = new WeakMap<BinWriter, Map<number, number>>();
|
|
18
|
+
function times(bin: BinWriter, keys: number): { input: number; t: Float32Array } {
|
|
19
|
+
const t = new Float32Array(keys + 1).map((_, i) => (i / keys) * LOOP_SECONDS);
|
|
20
|
+
const byBin = timeAccessors.get(bin) ?? new Map<number, number>();
|
|
21
|
+
timeAccessors.set(bin, byBin);
|
|
22
|
+
let input = byBin.get(keys);
|
|
23
|
+
if (input === undefined) {
|
|
24
|
+
input = bin.accessor(t, 'SCALAR', undefined, true);
|
|
25
|
+
byBin.set(keys, input);
|
|
26
|
+
}
|
|
27
|
+
return { input, t };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function motionChannel(
|
|
31
|
+
bin: BinWriter,
|
|
32
|
+
motion: Motion,
|
|
33
|
+
node: number,
|
|
34
|
+
rest: readonly number[],
|
|
35
|
+
): { sampler: Json; channel: Json } {
|
|
36
|
+
const rate = motion.kind === 'spin' ? motion.rps : motion.hz;
|
|
37
|
+
const cycles = cyclesOf(rate);
|
|
38
|
+
const keys = Math.max(24, Math.abs(cycles) * (motion.kind === 'spin' ? 12 : 16));
|
|
39
|
+
const { input, t } = times(bin, keys);
|
|
40
|
+
const phase = (time: number): number => (time / LOOP_SECONDS) * cycles * Math.PI * 2;
|
|
41
|
+
// Two-sided: −1..1 around rest. One-way: 0..1, starting and ending at rest (seamless loop).
|
|
42
|
+
const wave = (time: number): number =>
|
|
43
|
+
motion.kind !== 'spin' && motion.oneWay
|
|
44
|
+
? (1 - Math.cos(phase(time))) / 2
|
|
45
|
+
: Math.sin(phase(time));
|
|
46
|
+
if (motion.kind === 'slide') {
|
|
47
|
+
const values = new Float32Array((keys + 1) * 3);
|
|
48
|
+
t.forEach((time, i) => {
|
|
49
|
+
const s = wave(time);
|
|
50
|
+
values.set(
|
|
51
|
+
[0, 1, 2].map((k) => (rest[k] ?? 0) + (motion.offset[k] ?? 0) * s),
|
|
52
|
+
i * 3,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
sampler: { input, output: bin.accessor(values, 'VEC3'), interpolation: 'LINEAR' },
|
|
57
|
+
channel: { target: { node, path: 'translation' } },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const values = new Float32Array((keys + 1) * 4);
|
|
61
|
+
const peak = motion.kind === 'sway' ? (motion.degrees * Math.PI) / 180 : 0;
|
|
62
|
+
t.forEach((time, i) => {
|
|
63
|
+
const angle = motion.kind === 'spin' ? phase(time) : peak * wave(time);
|
|
64
|
+
values.set(quatFromAxisAngle(motion.axis, angle), i * 4);
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
sampler: { input, output: bin.accessor(values, 'VEC4'), interpolation: 'LINEAR' },
|
|
68
|
+
channel: { target: { node, path: 'rotation' } },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Runs the mesh optimiser (passes/optimize: weld → vertex cache → vertex fetch) over every part
|
|
2
|
+
// headed for the GLB, and sums what it did into one report the CLI and manifest can print.
|
|
3
|
+
|
|
4
|
+
import type { CompiledAsset } from '../core/index.ts';
|
|
5
|
+
import { type OptimizeOptions, type OptimizeReport, optimize } from '../passes/index.ts';
|
|
6
|
+
|
|
7
|
+
export interface PartOptimizeReport extends OptimizeReport {
|
|
8
|
+
readonly part: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Totals over all parts; ACMR/ATVR are triangle-weighted means. */
|
|
12
|
+
export interface ExportReport {
|
|
13
|
+
readonly optimized: boolean;
|
|
14
|
+
readonly verticesBefore: number;
|
|
15
|
+
readonly verticesAfter: number;
|
|
16
|
+
readonly trianglesBefore: number;
|
|
17
|
+
readonly trianglesAfter: number;
|
|
18
|
+
readonly degenerate: number;
|
|
19
|
+
readonly duplicate: number;
|
|
20
|
+
readonly acmrBefore: number;
|
|
21
|
+
readonly acmr: number;
|
|
22
|
+
readonly atvrBefore: number;
|
|
23
|
+
readonly atvr: number;
|
|
24
|
+
readonly parts: readonly PartOptimizeReport[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function summarize(parts: readonly PartOptimizeReport[], optimized: boolean): ExportReport {
|
|
28
|
+
const sum = (f: (r: PartOptimizeReport) => number): number => parts.reduce((s, r) => s + f(r), 0);
|
|
29
|
+
const tb = sum((r) => r.trianglesBefore);
|
|
30
|
+
const ta = sum((r) => r.trianglesAfter);
|
|
31
|
+
const mean = (f: (r: PartOptimizeReport) => number, by: number, w: 'b' | 'a'): number =>
|
|
32
|
+
by ? sum((r) => f(r) * (w === 'b' ? r.trianglesBefore : r.trianglesAfter)) / by : 0;
|
|
33
|
+
return {
|
|
34
|
+
optimized,
|
|
35
|
+
verticesBefore: sum((r) => r.verticesBefore),
|
|
36
|
+
verticesAfter: sum((r) => r.verticesAfter),
|
|
37
|
+
trianglesBefore: tb,
|
|
38
|
+
trianglesAfter: ta,
|
|
39
|
+
degenerate: sum((r) => r.degenerate),
|
|
40
|
+
duplicate: sum((r) => r.duplicate),
|
|
41
|
+
acmrBefore: mean((r) => r.acmrBefore, tb, 'b'),
|
|
42
|
+
acmr: mean((r) => r.acmr, ta, 'a'),
|
|
43
|
+
atvrBefore: mean((r) => r.atvrBefore, tb, 'b'),
|
|
44
|
+
atvr: mean((r) => r.atvr, ta, 'a'),
|
|
45
|
+
parts,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Optimise every part mesh. `false` passes meshes through and reports them as-is. */
|
|
50
|
+
export function optimizeParts(
|
|
51
|
+
asset: CompiledAsset,
|
|
52
|
+
o: OptimizeOptions | false,
|
|
53
|
+
): { asset: CompiledAsset; report: ExportReport } {
|
|
54
|
+
const reports: PartOptimizeReport[] = [];
|
|
55
|
+
const parts = asset.parts.map((p) => {
|
|
56
|
+
if (!p.mesh || p.mesh.indices.length === 0) return p;
|
|
57
|
+
if (o === false) {
|
|
58
|
+
const v = p.mesh.positions.length / 3;
|
|
59
|
+
const t = p.mesh.indices.length / 3;
|
|
60
|
+
reports.push({
|
|
61
|
+
part: p.name,
|
|
62
|
+
verticesBefore: v,
|
|
63
|
+
verticesAfter: v,
|
|
64
|
+
trianglesBefore: t,
|
|
65
|
+
trianglesAfter: t,
|
|
66
|
+
degenerate: 0,
|
|
67
|
+
duplicate: 0,
|
|
68
|
+
acmrBefore: 0,
|
|
69
|
+
acmr: 0,
|
|
70
|
+
atvrBefore: 0,
|
|
71
|
+
atvr: 0,
|
|
72
|
+
});
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
const { mesh, report } = optimize(p.mesh, o);
|
|
76
|
+
reports.push({ part: p.name, ...report });
|
|
77
|
+
return { ...p, mesh };
|
|
78
|
+
});
|
|
79
|
+
return { asset: { ...asset, parts }, report: summarize(reports, o !== false) };
|
|
80
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// One mesh → one glTF primitive: POSITION (with bounds), NORMAL, TEXCOORD_0, COLOR_0 as unorm16
|
|
2
|
+
// RGBA, TANGENT only when a normal map is bound, and the smallest index type that fits.
|
|
3
|
+
|
|
4
|
+
import type { Mesh } from '../core/index.ts';
|
|
5
|
+
import { ARRAY_BUFFER, type BinWriter, ELEMENT_ARRAY_BUFFER, type Json } from './bin-writer.ts';
|
|
6
|
+
import { tangentsOf } from './tangents.ts';
|
|
7
|
+
|
|
8
|
+
export function writePrimitive(bin: BinWriter, m: Mesh, material: number, tangents: boolean): Json {
|
|
9
|
+
// COLOR_0 as normalised unsigned short RGBA: 8 bytes/vertex instead of 16, no banding in
|
|
10
|
+
// darks (8-bit would). VEC4 because vertex attributes must be 4-byte aligned.
|
|
11
|
+
const colors = new Uint16Array((m.colors.length / 3) * 4);
|
|
12
|
+
const q = (v: number): number => Math.round(Math.min(1, Math.max(0, v)) * 65535);
|
|
13
|
+
for (let i = 0; i < m.colors.length / 3; i++)
|
|
14
|
+
colors.set(
|
|
15
|
+
[q(m.colors[i * 3] ?? 1), q(m.colors[i * 3 + 1] ?? 1), q(m.colors[i * 3 + 2] ?? 1), 65535],
|
|
16
|
+
i * 4,
|
|
17
|
+
);
|
|
18
|
+
return {
|
|
19
|
+
attributes: {
|
|
20
|
+
POSITION: bin.accessor(m.positions, 'VEC3', ARRAY_BUFFER, true),
|
|
21
|
+
NORMAL: bin.accessor(m.normals, 'VEC3', ARRAY_BUFFER),
|
|
22
|
+
TEXCOORD_0: bin.accessor(m.uvs, 'VEC2', ARRAY_BUFFER),
|
|
23
|
+
COLOR_0: bin.accessor(colors, 'VEC4', ARRAY_BUFFER, false, true),
|
|
24
|
+
// A bound normal map needs a tangent space (validator S10 / G15).
|
|
25
|
+
...(tangents ? { TANGENT: bin.accessor(tangentsOf(m), 'VEC4', ARRAY_BUFFER) } : {}),
|
|
26
|
+
},
|
|
27
|
+
// 16-bit indices whenever the primitive allows: half the bytes, every engine reads them.
|
|
28
|
+
indices: bin.accessor(
|
|
29
|
+
m.positions.length / 3 <= 65535 ? Uint16Array.from(m.indices) : m.indices,
|
|
30
|
+
'SCALAR',
|
|
31
|
+
ELEMENT_ARRAY_BUFFER,
|
|
32
|
+
),
|
|
33
|
+
material,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Per-vertex tangents (glTF TANGENT, VEC4) for normal-mapped meshes: Lengyel's UV-derivative
|
|
2
|
+
// method, Gram-Schmidt against the vertex normal, w = handedness. The bitangent cross(N, T) · w
|
|
3
|
+
// points toward DECREASING glTF v (the "green up" convention the painter writes). Identical to
|
|
4
|
+
// MikkTSpace on flat-shaded quads; close, not bit-equal, on smooth curved surfaces.
|
|
5
|
+
|
|
6
|
+
import type { Mesh } from '../core/index.ts';
|
|
7
|
+
|
|
8
|
+
export function tangentsOf(m: Mesh): Float32Array {
|
|
9
|
+
const n = m.positions.length / 3;
|
|
10
|
+
const tan = new Float32Array(n * 3);
|
|
11
|
+
const bit = new Float32Array(n * 3);
|
|
12
|
+
const P = m.positions;
|
|
13
|
+
const U = m.uvs;
|
|
14
|
+
for (let t = 0; t < m.indices.length; t += 3) {
|
|
15
|
+
const a = m.indices[t] ?? 0;
|
|
16
|
+
const b = m.indices[t + 1] ?? 0;
|
|
17
|
+
const c = m.indices[t + 2] ?? 0;
|
|
18
|
+
const e1x = (P[b * 3] ?? 0) - (P[a * 3] ?? 0);
|
|
19
|
+
const e1y = (P[b * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
|
|
20
|
+
const e1z = (P[b * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
|
|
21
|
+
const e2x = (P[c * 3] ?? 0) - (P[a * 3] ?? 0);
|
|
22
|
+
const e2y = (P[c * 3 + 1] ?? 0) - (P[a * 3 + 1] ?? 0);
|
|
23
|
+
const e2z = (P[c * 3 + 2] ?? 0) - (P[a * 3 + 2] ?? 0);
|
|
24
|
+
const du1 = (U[b * 2] ?? 0) - (U[a * 2] ?? 0);
|
|
25
|
+
const dv1 = -((U[b * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
|
|
26
|
+
const du2 = (U[c * 2] ?? 0) - (U[a * 2] ?? 0);
|
|
27
|
+
const dv2 = -((U[c * 2 + 1] ?? 0) - (U[a * 2 + 1] ?? 0));
|
|
28
|
+
const det = du1 * dv2 - du2 * dv1;
|
|
29
|
+
if (Math.abs(det) < 1e-12) continue;
|
|
30
|
+
const r = 1 / det;
|
|
31
|
+
const tx = (e1x * dv2 - e2x * dv1) * r;
|
|
32
|
+
const ty = (e1y * dv2 - e2y * dv1) * r;
|
|
33
|
+
const tz = (e1z * dv2 - e2z * dv1) * r;
|
|
34
|
+
const bx = (e2x * du1 - e1x * du2) * r;
|
|
35
|
+
const by = (e2y * du1 - e1y * du2) * r;
|
|
36
|
+
const bz = (e2z * du1 - e1z * du2) * r;
|
|
37
|
+
for (const v of [a, b, c]) {
|
|
38
|
+
tan[v * 3] = (tan[v * 3] ?? 0) + tx;
|
|
39
|
+
tan[v * 3 + 1] = (tan[v * 3 + 1] ?? 0) + ty;
|
|
40
|
+
tan[v * 3 + 2] = (tan[v * 3 + 2] ?? 0) + tz;
|
|
41
|
+
bit[v * 3] = (bit[v * 3] ?? 0) + bx;
|
|
42
|
+
bit[v * 3 + 1] = (bit[v * 3 + 1] ?? 0) + by;
|
|
43
|
+
bit[v * 3 + 2] = (bit[v * 3 + 2] ?? 0) + bz;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const out = new Float32Array(n * 4);
|
|
47
|
+
for (let v = 0; v < n; v++) {
|
|
48
|
+
const nx = m.normals[v * 3] ?? 0;
|
|
49
|
+
const ny = m.normals[v * 3 + 1] ?? 0;
|
|
50
|
+
const nz = m.normals[v * 3 + 2] ?? 1;
|
|
51
|
+
let tx = tan[v * 3] ?? 0;
|
|
52
|
+
let ty = tan[v * 3 + 1] ?? 0;
|
|
53
|
+
let tz = tan[v * 3 + 2] ?? 0;
|
|
54
|
+
const d = nx * tx + ny * ty + nz * tz;
|
|
55
|
+
tx -= nx * d;
|
|
56
|
+
ty -= ny * d;
|
|
57
|
+
tz -= nz * d;
|
|
58
|
+
let len = Math.hypot(tx, ty, tz);
|
|
59
|
+
if (len < 1e-8) {
|
|
60
|
+
// No usable UV gradient: any unit vector perpendicular to the normal.
|
|
61
|
+
[tx, ty, tz] = Math.abs(nx) < 0.9 ? [0, -nz, ny] : [nz, 0, -nx];
|
|
62
|
+
len = Math.hypot(tx, ty, tz) || 1;
|
|
63
|
+
}
|
|
64
|
+
tx /= len;
|
|
65
|
+
ty /= len;
|
|
66
|
+
tz /= len;
|
|
67
|
+
// Handedness: does cross(N, T) agree with the accumulated bitangent?
|
|
68
|
+
const cx = ny * tz - nz * ty;
|
|
69
|
+
const cy = nz * tx - nx * tz;
|
|
70
|
+
const cz = nx * ty - ny * tx;
|
|
71
|
+
const w = cx * (bit[v * 3] ?? 0) + cy * (bit[v * 3 + 1] ?? 0) + cz * (bit[v * 3 + 2] ?? 0);
|
|
72
|
+
out.set([tx, ty, tz, w < 0 ? -1 : 1], v * 4);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|