@thunderclap-engine/engine 0.0.0-stage → 1.1.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/CHANGELOG.md +390 -0
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/analytics/analytics.d.ts +80 -0
- package/dist/analytics/analytics.js +129 -0
- package/dist/analytics/index.d.ts +1 -0
- package/dist/analytics/index.js +1 -0
- package/dist/anim/aim.d.ts +43 -0
- package/dist/anim/aim.js +157 -0
- package/dist/anim/clip.d.ts +98 -0
- package/dist/anim/clip.js +282 -0
- package/dist/anim/compress.d.ts +65 -0
- package/dist/anim/compress.js +339 -0
- package/dist/anim/foot-ik.d.ts +74 -0
- package/dist/anim/foot-ik.js +241 -0
- package/dist/anim/graph.d.ts +153 -0
- package/dist/anim/graph.js +532 -0
- package/dist/anim/ik.d.ts +105 -0
- package/dist/anim/ik.js +435 -0
- package/dist/anim/index.d.ts +36 -0
- package/dist/anim/index.js +21 -0
- package/dist/anim/match.d.ts +66 -0
- package/dist/anim/match.js +153 -0
- package/dist/anim/mixer.d.ts +113 -0
- package/dist/anim/mixer.js +314 -0
- package/dist/anim/morph.d.ts +41 -0
- package/dist/anim/morph.js +94 -0
- package/dist/anim/pose.d.ts +74 -0
- package/dist/anim/pose.js +254 -0
- package/dist/anim/quat.d.ts +44 -0
- package/dist/anim/quat.js +187 -0
- package/dist/anim/retarget.d.ts +97 -0
- package/dist/anim/retarget.js +366 -0
- package/dist/anim/root-motion.d.ts +44 -0
- package/dist/anim/root-motion.js +224 -0
- package/dist/anim/skin-pose.d.ts +39 -0
- package/dist/anim/skin-pose.js +53 -0
- package/dist/anim/socket.d.ts +47 -0
- package/dist/anim/socket.js +84 -0
- package/dist/anim/spring-chain.d.ts +73 -0
- package/dist/anim/spring-chain.js +294 -0
- package/dist/assets/atlas.d.ts +90 -0
- package/dist/assets/atlas.js +212 -0
- package/dist/assets/baked.d.ts +33 -0
- package/dist/assets/baked.js +38 -0
- package/dist/assets/bundle.d.ts +88 -0
- package/dist/assets/bundle.js +464 -0
- package/dist/assets/cache.d.ts +56 -0
- package/dist/assets/cache.js +123 -0
- package/dist/assets/gltf-load.d.ts +252 -0
- package/dist/assets/gltf-load.js +661 -0
- package/dist/assets/gltf-skin.d.ts +36 -0
- package/dist/assets/gltf-skin.js +109 -0
- package/dist/assets/gltf.d.ts +706 -0
- package/dist/assets/gltf.js +1376 -0
- package/dist/assets/index.d.ts +20 -0
- package/dist/assets/index.js +10 -0
- package/dist/assets/ktx2.d.ts +71 -0
- package/dist/assets/ktx2.js +213 -0
- package/dist/assets/manifest.d.ts +79 -0
- package/dist/assets/manifest.js +7 -0
- package/dist/assets/meshopt.d.ts +23 -0
- package/dist/assets/meshopt.js +403 -0
- package/dist/audio/audio.d.ts +733 -0
- package/dist/audio/audio.js +2013 -0
- package/dist/audio/dsp.d.ts +61 -0
- package/dist/audio/dsp.js +159 -0
- package/dist/audio/duck.d.ts +44 -0
- package/dist/audio/duck.js +101 -0
- package/dist/audio/envelope.d.ts +53 -0
- package/dist/audio/envelope.js +106 -0
- package/dist/audio/index.d.ts +13 -0
- package/dist/audio/index.js +14 -0
- package/dist/audio/music.d.ts +113 -0
- package/dist/audio/music.js +548 -0
- package/dist/audio/occlusion.d.ts +55 -0
- package/dist/audio/occlusion.js +61 -0
- package/dist/audio/persist.d.ts +23 -0
- package/dist/audio/persist.js +27 -0
- package/dist/audio/recipes.d.ts +55 -0
- package/dist/audio/recipes.js +227 -0
- package/dist/audio/send.d.ts +41 -0
- package/dist/audio/send.js +118 -0
- package/dist/audio/sprites.d.ts +59 -0
- package/dist/audio/sprites.js +43 -0
- package/dist/audio/synth.d.ts +103 -0
- package/dist/audio/synth.js +197 -0
- package/dist/audio/variations.d.ts +25 -0
- package/dist/audio/variations.js +55 -0
- package/dist/audio/zones.d.ts +100 -0
- package/dist/audio/zones.js +167 -0
- package/dist/core/detached-surface.d.ts +44 -0
- package/dist/core/detached-surface.js +104 -0
- package/dist/core/events.d.ts +24 -0
- package/dist/core/events.js +43 -0
- package/dist/core/game.d.ts +409 -0
- package/dist/core/game.js +530 -0
- package/dist/core/hash.d.ts +24 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +9 -0
- package/dist/core/loop.d.ts +113 -0
- package/dist/core/loop.js +185 -0
- package/dist/core/math.d.ts +58 -0
- package/dist/core/math.js +210 -0
- package/dist/core/noise.d.ts +40 -0
- package/dist/core/noise.js +247 -0
- package/dist/core/pool.d.ts +42 -0
- package/dist/core/pool.js +85 -0
- package/dist/core/random.d.ts +61 -0
- package/dist/core/random.js +199 -0
- package/dist/core/scene.d.ts +79 -0
- package/dist/core/scene.js +195 -0
- package/dist/core/timer.d.ts +66 -0
- package/dist/core/timer.js +286 -0
- package/dist/fx/billboard-wgsl.d.ts +29 -0
- package/dist/fx/billboard-wgsl.js +158 -0
- package/dist/fx/billboard.d.ts +249 -0
- package/dist/fx/billboard.js +702 -0
- package/dist/fx/debug-draw.d.ts +127 -0
- package/dist/fx/debug-draw.js +523 -0
- package/dist/fx/debug-wgsl.d.ts +7 -0
- package/dist/fx/debug-wgsl.js +32 -0
- package/dist/fx/decals-wgsl.d.ts +15 -0
- package/dist/fx/decals-wgsl.js +173 -0
- package/dist/fx/decals.d.ts +216 -0
- package/dist/fx/decals.js +607 -0
- package/dist/fx/effect.d.ts +129 -0
- package/dist/fx/effect.js +349 -0
- package/dist/fx/effects/hit-spark.json +48 -0
- package/dist/fx/effects/index.d.ts +8 -0
- package/dist/fx/effects/index.js +9 -0
- package/dist/fx/effects/magic-aura.json +47 -0
- package/dist/fx/effects/smoke-puff.json +34 -0
- package/dist/fx/emit-surface.d.ts +40 -0
- package/dist/fx/emit-surface.js +124 -0
- package/dist/fx/flicker.d.ts +30 -0
- package/dist/fx/flicker.js +42 -0
- package/dist/fx/index.d.ts +31 -0
- package/dist/fx/index.js +20 -0
- package/dist/fx/particles-wgsl.d.ts +57 -0
- package/dist/fx/particles-wgsl.js +828 -0
- package/dist/fx/particles.d.ts +566 -0
- package/dist/fx/particles.js +1533 -0
- package/dist/fx/random.d.ts +15 -0
- package/dist/fx/random.js +20 -0
- package/dist/fx/sky-wgsl.d.ts +23 -0
- package/dist/fx/sky-wgsl.js +127 -0
- package/dist/fx/sky.d.ts +96 -0
- package/dist/fx/sky.js +175 -0
- package/dist/fx/text-atlas.d.ts +143 -0
- package/dist/fx/text-atlas.js +327 -0
- package/dist/fx/trail-wgsl.d.ts +11 -0
- package/dist/fx/trail-wgsl.js +42 -0
- package/dist/fx/trail.d.ts +194 -0
- package/dist/fx/trail.js +555 -0
- package/dist/fx/water-wgsl.d.ts +34 -0
- package/dist/fx/water-wgsl.js +197 -0
- package/dist/fx/water.d.ts +176 -0
- package/dist/fx/water.js +371 -0
- package/dist/geom/collide/bvh.d.ts +151 -0
- package/dist/geom/collide/bvh.js +493 -0
- package/dist/geom/collide/debug.d.ts +14 -0
- package/dist/geom/collide/debug.js +74 -0
- package/dist/geom/collide/grid.d.ts +26 -0
- package/dist/geom/collide/grid.js +91 -0
- package/dist/geom/collide/index.d.ts +8 -0
- package/dist/geom/collide/index.js +8 -0
- package/dist/geom/collide/queries.d.ts +70 -0
- package/dist/geom/collide/queries.js +583 -0
- package/dist/geom/collide/serialize.d.ts +14 -0
- package/dist/geom/collide/serialize.js +145 -0
- package/dist/geom/collide/shapes.d.ts +161 -0
- package/dist/geom/collide/shapes.js +686 -0
- package/dist/geom/collide/slide.d.ts +66 -0
- package/dist/geom/collide/slide.js +375 -0
- package/dist/geom/collide/tile-slide.d.ts +88 -0
- package/dist/geom/collide/tile-slide.js +255 -0
- package/dist/geom/geometry.d.ts +95 -0
- package/dist/geom/geometry.js +322 -0
- package/dist/geom/heightfield.d.ts +72 -0
- package/dist/geom/heightfield.js +295 -0
- package/dist/geom/hull.d.ts +27 -0
- package/dist/geom/hull.js +279 -0
- package/dist/geom/index.d.ts +35 -0
- package/dist/geom/index.js +23 -0
- package/dist/geom/iso.d.ts +74 -0
- package/dist/geom/iso.js +89 -0
- package/dist/geom/lod.d.ts +29 -0
- package/dist/geom/lod.js +27 -0
- package/dist/geom/ops.d.ts +47 -0
- package/dist/geom/ops.js +285 -0
- package/dist/geom/primitives.d.ts +97 -0
- package/dist/geom/primitives.js +359 -0
- package/dist/geom/shapes.d.ts +43 -0
- package/dist/geom/shapes.js +88 -0
- package/dist/geom/simplify.d.ts +38 -0
- package/dist/geom/simplify.js +519 -0
- package/dist/geom/split.d.ts +37 -0
- package/dist/geom/split.js +413 -0
- package/dist/geom/sweep.d.ts +83 -0
- package/dist/geom/sweep.js +534 -0
- package/dist/geom/tangents.d.ts +16 -0
- package/dist/geom/tangents.js +124 -0
- package/dist/geom/triangulate.d.ts +19 -0
- package/dist/geom/triangulate.js +147 -0
- package/dist/host/default.d.ts +10 -0
- package/dist/host/default.js +10 -0
- package/dist/host/index.d.ts +4 -0
- package/dist/host/index.js +3 -0
- package/dist/host/types.d.ts +144 -0
- package/dist/host/types.js +1 -0
- package/dist/host/web-links.d.ts +38 -0
- package/dist/host/web-links.js +110 -0
- package/dist/host/web.d.ts +30 -0
- package/dist/host/web.js +216 -0
- package/dist/host/xr.d.ts +24 -0
- package/dist/host/xr.js +59 -0
- package/dist/i18n/i18n.d.ts +59 -0
- package/dist/i18n/i18n.js +241 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/message.d.ts +72 -0
- package/dist/i18n/message.js +369 -0
- package/dist/i18n/negotiate.d.ts +17 -0
- package/dist/i18n/negotiate.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -0
- package/dist/input/bindings.d.ts +31 -0
- package/dist/input/bindings.js +141 -0
- package/dist/input/clock.d.ts +44 -0
- package/dist/input/clock.js +49 -0
- package/dist/input/follow.d.ts +133 -0
- package/dist/input/follow.js +218 -0
- package/dist/input/gestures.d.ts +281 -0
- package/dist/input/gestures.js +662 -0
- package/dist/input/glyphs.d.ts +35 -0
- package/dist/input/glyphs.js +216 -0
- package/dist/input/gyro.d.ts +85 -0
- package/dist/input/gyro.js +140 -0
- package/dist/input/index.d.ts +19 -0
- package/dist/input/index.js +16 -0
- package/dist/input/input.d.ts +543 -0
- package/dist/input/input.js +1492 -0
- package/dist/input/look.d.ts +327 -0
- package/dist/input/look.js +681 -0
- package/dist/input/magnify.d.ts +177 -0
- package/dist/input/magnify.js +131 -0
- package/dist/input/pointer-dispatch.d.ts +138 -0
- package/dist/input/pointer-dispatch.js +180 -0
- package/dist/input/pointer-router.d.ts +204 -0
- package/dist/input/pointer-router.js +533 -0
- package/dist/input/pointer-sources.d.ts +164 -0
- package/dist/input/pointer-sources.js +306 -0
- package/dist/input/rays.d.ts +138 -0
- package/dist/input/rays.js +615 -0
- package/dist/input/record.d.ts +209 -0
- package/dist/input/record.js +601 -0
- package/dist/input/touch-zones.d.ts +147 -0
- package/dist/input/touch-zones.js +300 -0
- package/dist/input/types.d.ts +158 -0
- package/dist/input/types.js +1 -0
- package/dist/input/xr-source.d.ts +53 -0
- package/dist/input/xr-source.js +164 -0
- package/dist/nav/astar.d.ts +45 -0
- package/dist/nav/astar.js +182 -0
- package/dist/nav/corridor.d.ts +40 -0
- package/dist/nav/corridor.js +95 -0
- package/dist/nav/crowd.d.ts +94 -0
- package/dist/nav/crowd.js +624 -0
- package/dist/nav/flow.d.ts +61 -0
- package/dist/nav/flow.js +193 -0
- package/dist/nav/grid.d.ts +108 -0
- package/dist/nav/grid.js +210 -0
- package/dist/nav/heap.d.ts +22 -0
- package/dist/nav/heap.js +92 -0
- package/dist/nav/index.d.ts +37 -0
- package/dist/nav/index.js +19 -0
- package/dist/nav/mesh/areas.d.ts +44 -0
- package/dist/nav/mesh/areas.js +98 -0
- package/dist/nav/mesh/bake-options.d.ts +33 -0
- package/dist/nav/mesh/bake-options.js +76 -0
- package/dist/nav/mesh/contour.d.ts +22 -0
- package/dist/nav/mesh/contour.js +323 -0
- package/dist/nav/mesh/flow3.d.ts +35 -0
- package/dist/nav/mesh/flow3.js +229 -0
- package/dist/nav/mesh/links.d.ts +38 -0
- package/dist/nav/mesh/links.js +72 -0
- package/dist/nav/mesh/mesh-path.d.ts +152 -0
- package/dist/nav/mesh/mesh-path.js +643 -0
- package/dist/nav/mesh/mesh-reach.d.ts +78 -0
- package/dist/nav/mesh/mesh-reach.js +450 -0
- package/dist/nav/mesh/navmesh.d.ts +117 -0
- package/dist/nav/mesh/navmesh.js +647 -0
- package/dist/nav/mesh/polymesh.d.ts +24 -0
- package/dist/nav/mesh/polymesh.js +384 -0
- package/dist/nav/mesh/regions.d.ts +12 -0
- package/dist/nav/mesh/regions.js +367 -0
- package/dist/nav/mesh/serialize.d.ts +13 -0
- package/dist/nav/mesh/serialize.js +197 -0
- package/dist/nav/mesh/voxelize.d.ts +111 -0
- package/dist/nav/mesh/voxelize.js +552 -0
- package/dist/nav/obstacles.d.ts +75 -0
- package/dist/nav/obstacles.js +294 -0
- package/dist/nav/path.d.ts +59 -0
- package/dist/nav/path.js +177 -0
- package/dist/nav/query-queue.d.ts +46 -0
- package/dist/nav/query-queue.js +308 -0
- package/dist/nav/reach.d.ts +53 -0
- package/dist/nav/reach.js +148 -0
- package/dist/nav/steer.d.ts +36 -0
- package/dist/nav/steer.js +78 -0
- package/dist/platform/boot.d.ts +28 -0
- package/dist/platform/boot.js +43 -0
- package/dist/platform/device-mode.d.ts +40 -0
- package/dist/platform/device-mode.js +104 -0
- package/dist/platform/device.d.ts +31 -0
- package/dist/platform/device.js +91 -0
- package/dist/platform/environment.d.ts +42 -0
- package/dist/platform/environment.js +82 -0
- package/dist/platform/gpu.d.ts +40 -0
- package/dist/platform/gpu.js +57 -0
- package/dist/platform/index.d.ts +18 -0
- package/dist/platform/index.js +10 -0
- package/dist/platform/letterbox.d.ts +43 -0
- package/dist/platform/letterbox.js +57 -0
- package/dist/platform/links.d.ts +20 -0
- package/dist/platform/links.js +43 -0
- package/dist/platform/screens.d.ts +36 -0
- package/dist/platform/screens.js +138 -0
- package/dist/platform/surface.d.ts +72 -0
- package/dist/platform/surface.js +157 -0
- package/dist/platform/xr-fake.d.ts +168 -0
- package/dist/platform/xr-fake.js +434 -0
- package/dist/platform/xr-surface.d.ts +120 -0
- package/dist/platform/xr-surface.js +214 -0
- package/dist/platform/xr-types.d.ts +262 -0
- package/dist/post/ao.d.ts +12 -0
- package/dist/post/ao.js +136 -0
- package/dist/post/blend.d.ts +27 -0
- package/dist/post/blend.js +129 -0
- package/dist/post/depth-pass.d.ts +43 -0
- package/dist/post/depth-pass.js +118 -0
- package/dist/post/dof.d.ts +1 -0
- package/dist/post/dof.js +66 -0
- package/dist/post/exposure.d.ts +80 -0
- package/dist/post/exposure.js +322 -0
- package/dist/post/index.d.ts +12 -0
- package/dist/post/index.js +11 -0
- package/dist/post/lens-dirt.d.ts +13 -0
- package/dist/post/lens-dirt.js +91 -0
- package/dist/post/lut.d.ts +19 -0
- package/dist/post/lut.js +50 -0
- package/dist/post/motion-blur.d.ts +9 -0
- package/dist/post/motion-blur.js +78 -0
- package/dist/post/post-stack.d.ts +302 -0
- package/dist/post/post-stack.js +1137 -0
- package/dist/post/settings.d.ts +485 -0
- package/dist/post/settings.js +416 -0
- package/dist/post/shaders.d.ts +25 -0
- package/dist/post/shaders.js +410 -0
- package/dist/post/ssr.d.ts +16 -0
- package/dist/post/ssr.js +155 -0
- package/dist/post/taa.d.ts +74 -0
- package/dist/post/taa.js +235 -0
- package/dist/post/white-balance.d.ts +25 -0
- package/dist/post/white-balance.js +98 -0
- package/dist/render/bindings.d.ts +98 -0
- package/dist/render/bindings.js +281 -0
- package/dist/render/blend.d.ts +13 -0
- package/dist/render/blend.js +26 -0
- package/dist/render/camera-2d.d.ts +72 -0
- package/dist/render/camera-2d.js +92 -0
- package/dist/render/camera.d.ts +302 -0
- package/dist/render/camera.js +671 -0
- package/dist/render/color.d.ts +16 -0
- package/dist/render/color.js +41 -0
- package/dist/render/compressed.d.ts +104 -0
- package/dist/render/compressed.js +248 -0
- package/dist/render/diagnostics.d.ts +15 -0
- package/dist/render/diagnostics.js +43 -0
- package/dist/render/dynamic-resolution.d.ts +70 -0
- package/dist/render/dynamic-resolution.js +126 -0
- package/dist/render/fake-gpu.d.ts +204 -0
- package/dist/render/fake-gpu.js +426 -0
- package/dist/render/flipbook.d.ts +56 -0
- package/dist/render/flipbook.js +87 -0
- package/dist/render/frame.d.ts +278 -0
- package/dist/render/frame.js +174 -0
- package/dist/render/fullscreen.d.ts +54 -0
- package/dist/render/fullscreen.js +91 -0
- package/dist/render/gpu-timer.d.ts +73 -0
- package/dist/render/gpu-timer.js +186 -0
- package/dist/render/index.d.ts +53 -0
- package/dist/render/index.js +29 -0
- package/dist/render/instance-list.d.ts +185 -0
- package/dist/render/instance-list.js +547 -0
- package/dist/render/lights.d.ts +181 -0
- package/dist/render/lights.js +233 -0
- package/dist/render/lod.d.ts +54 -0
- package/dist/render/lod.js +52 -0
- package/dist/render/material.d.ts +165 -0
- package/dist/render/material.js +292 -0
- package/dist/render/mesh.d.ts +284 -0
- package/dist/render/mesh.js +472 -0
- package/dist/render/mipmap.d.ts +16 -0
- package/dist/render/mipmap.js +121 -0
- package/dist/render/parallax.d.ts +50 -0
- package/dist/render/parallax.js +47 -0
- package/dist/render/pbr-wgsl.d.ts +31 -0
- package/dist/render/pbr-wgsl.js +119 -0
- package/dist/render/ping-pong.d.ts +42 -0
- package/dist/render/ping-pong.js +74 -0
- package/dist/render/renderer.d.ts +741 -0
- package/dist/render/renderer.js +2486 -0
- package/dist/render/scene-pipelines.d.ts +44 -0
- package/dist/render/scene-pipelines.js +92 -0
- package/dist/render/scene-target.d.ts +170 -0
- package/dist/render/scene-target.js +450 -0
- package/dist/render/shadow.d.ts +74 -0
- package/dist/render/shadow.js +208 -0
- package/dist/render/skin.d.ts +112 -0
- package/dist/render/skin.js +240 -0
- package/dist/render/snapshot.d.ts +36 -0
- package/dist/render/snapshot.js +34 -0
- package/dist/render/standard.d.ts +162 -0
- package/dist/render/standard.js +263 -0
- package/dist/render/texture.d.ts +142 -0
- package/dist/render/texture.js +228 -0
- package/dist/render/tile-layer.d.ts +110 -0
- package/dist/render/tile-layer.js +304 -0
- package/dist/render/transform.d.ts +115 -0
- package/dist/render/transform.js +318 -0
- package/dist/render/wgsl.d.ts +131 -0
- package/dist/render/wgsl.js +528 -0
- package/dist/save/codec.d.ts +49 -0
- package/dist/save/codec.js +169 -0
- package/dist/save/index.d.ts +8 -0
- package/dist/save/index.js +8 -0
- package/dist/save/journal.d.ts +24 -0
- package/dist/save/journal.js +102 -0
- package/dist/save/migrate.d.ts +15 -0
- package/dist/save/migrate.js +37 -0
- package/dist/save/schema.d.ts +72 -0
- package/dist/save/schema.js +154 -0
- package/dist/save/settings.d.ts +28 -0
- package/dist/save/settings.js +59 -0
- package/dist/save/slots.d.ts +56 -0
- package/dist/save/slots.js +200 -0
- package/dist/save/storage.d.ts +35 -0
- package/dist/save/storage.js +62 -0
- package/dist/save/store.d.ts +114 -0
- package/dist/save/store.js +269 -0
- package/dist/testing/fake-host.d.ts +103 -0
- package/dist/testing/fake-host.js +163 -0
- package/dist/testing/fixed-clock.d.ts +25 -0
- package/dist/testing/fixed-clock.js +30 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-game.d.ts +76 -0
- package/dist/testing/test-game.js +92 -0
- package/dist/tween/ease.d.ts +77 -0
- package/dist/tween/ease.js +192 -0
- package/dist/tween/index.d.ts +8 -0
- package/dist/tween/index.js +7 -0
- package/dist/tween/timeline.d.ts +78 -0
- package/dist/tween/timeline.js +269 -0
- package/dist/tween/tween.d.ts +168 -0
- package/dist/tween/tween.js +320 -0
- package/dist/ui/anchor.d.ts +206 -0
- package/dist/ui/anchor.js +307 -0
- package/dist/ui/caption.d.ts +122 -0
- package/dist/ui/caption.js +286 -0
- package/dist/ui/cursor.d.ts +105 -0
- package/dist/ui/cursor.js +261 -0
- package/dist/ui/dom.d.ts +30 -0
- package/dist/ui/dom.js +92 -0
- package/dist/ui/floats.d.ts +86 -0
- package/dist/ui/floats.js +260 -0
- package/dist/ui/hud.d.ts +198 -0
- package/dist/ui/hud.js +431 -0
- package/dist/ui/index.d.ts +37 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/inventory-card.d.ts +52 -0
- package/dist/ui/inventory-card.js +173 -0
- package/dist/ui/inventory-context.d.ts +29 -0
- package/dist/ui/inventory-context.js +125 -0
- package/dist/ui/inventory-drag.d.ts +63 -0
- package/dist/ui/inventory-drag.js +394 -0
- package/dist/ui/inventory-grid.d.ts +66 -0
- package/dist/ui/inventory-grid.js +625 -0
- package/dist/ui/inventory-radial.d.ts +39 -0
- package/dist/ui/inventory-radial.js +168 -0
- package/dist/ui/inventory.d.ts +128 -0
- package/dist/ui/inventory.js +154 -0
- package/dist/ui/layout.d.ts +356 -0
- package/dist/ui/layout.js +856 -0
- package/dist/ui/link.d.ts +64 -0
- package/dist/ui/link.js +155 -0
- package/dist/ui/loading.d.ts +40 -0
- package/dist/ui/loading.js +157 -0
- package/dist/ui/loupe.d.ts +74 -0
- package/dist/ui/loupe.js +164 -0
- package/dist/ui/menu-nav.d.ts +33 -0
- package/dist/ui/menu-nav.js +28 -0
- package/dist/ui/minimap-canvas.d.ts +145 -0
- package/dist/ui/minimap-canvas.js +460 -0
- package/dist/ui/notify.d.ts +40 -0
- package/dist/ui/notify.js +141 -0
- package/dist/ui/orb.d.ts +53 -0
- package/dist/ui/orb.js +304 -0
- package/dist/ui/overlay.d.ts +213 -0
- package/dist/ui/overlay.js +394 -0
- package/dist/ui/padnav.d.ts +53 -0
- package/dist/ui/padnav.js +146 -0
- package/dist/ui/perf.d.ts +130 -0
- package/dist/ui/perf.js +329 -0
- package/dist/ui/rotate-card.d.ts +54 -0
- package/dist/ui/rotate-card.js +97 -0
- package/dist/ui/stack.d.ts +128 -0
- package/dist/ui/stack.js +312 -0
- package/dist/ui/styles.d.ts +13 -0
- package/dist/ui/styles.js +200 -0
- package/dist/ui/text-scale.d.ts +67 -0
- package/dist/ui/text-scale.js +102 -0
- package/dist/ui/theme.d.ts +70 -0
- package/dist/ui/theme.js +98 -0
- package/dist/ui/toast.d.ts +23 -0
- package/dist/ui/toast.js +46 -0
- package/dist/ui/tooltip.d.ts +89 -0
- package/dist/ui/tooltip.js +333 -0
- package/dist/ui/widgets-2.d.ts +134 -0
- package/dist/ui/widgets-2.js +399 -0
- package/dist/ui/widgets-3.d.ts +91 -0
- package/dist/ui/widgets-3.js +212 -0
- package/dist/ui/widgets.d.ts +34 -0
- package/dist/ui/widgets.js +50 -0
- package/dist/ui/world-panel/index.d.ts +5 -0
- package/dist/ui/world-panel/index.js +5 -0
- package/dist/ui/world-panel/layout.d.ts +38 -0
- package/dist/ui/world-panel/layout.js +68 -0
- package/dist/ui/world-panel/paint.d.ts +96 -0
- package/dist/ui/world-panel/paint.js +136 -0
- package/dist/ui/world-panel/panel.d.ts +235 -0
- package/dist/ui/world-panel/panel.js +762 -0
- package/dist/ui/world-panel/wgsl.d.ts +10 -0
- package/dist/ui/world-panel/wgsl.js +52 -0
- package/dist/ui/world-panel/widgets.d.ts +207 -0
- package/dist/ui/world-panel/widgets.js +569 -0
- package/dist/voxel/chunks.d.ts +77 -0
- package/dist/voxel/chunks.js +149 -0
- package/dist/voxel/grid.d.ts +84 -0
- package/dist/voxel/grid.js +223 -0
- package/dist/voxel/index.d.ts +22 -0
- package/dist/voxel/index.js +7 -0
- package/dist/voxel/mesher-worker.d.ts +14 -0
- package/dist/voxel/mesher-worker.js +47 -0
- package/dist/voxel/mesher.d.ts +73 -0
- package/dist/voxel/mesher.js +306 -0
- package/dist/voxel/ray.d.ts +32 -0
- package/dist/voxel/ray.js +73 -0
- package/dist/voxel/wgsl.d.ts +16 -0
- package/dist/voxel/wgsl.js +28 -0
- package/dist/voxel/worker-client.d.ts +79 -0
- package/dist/voxel/worker-client.js +122 -0
- package/package.json +177 -3
|
@@ -0,0 +1,1376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glTF 2.0 parser: bytes in, an engine-free `GltfDocument` out. Pure data, no DOM, no GPU, no
|
|
3
|
+
* fetch (the game fetches the file and any external resource; VISION rule: the engine never
|
|
4
|
+
* makes a network call). `gltf-load.ts` turns the document into meshes, textures and transforms.
|
|
5
|
+
*
|
|
6
|
+
* Handles the GLB container, `.gltf` JSON (as text bytes or a parsed object), `data:` URIs in
|
|
7
|
+
* buffers and images, and every accessor component type (float, normalised and unnormalised
|
|
8
|
+
* integers, interleaved strides). Static geometry only: `triangles` primitives, metallic-roughness
|
|
9
|
+
* materials, node hierarchies, plus skins (joints, inverse bind matrices, `JOINTS_0`/`WEIGHTS_0`)
|
|
10
|
+
* and animations (every sampler interpolation, cubic-spline tangents kept) and morph targets
|
|
11
|
+
* (`POSITION` and `NORMAL` deltas per target, sparse or dense, plus the mesh's default
|
|
12
|
+
* `weights`). Sparse accessors outside morph targets, other primitive modes and required
|
|
13
|
+
* extensions throw a `GltfError`; `extensionsUsed` entries the parser does not read are listed
|
|
14
|
+
* in `warnings`. Materials carry every metallic-roughness slot (`TextureRef` per map, with
|
|
15
|
+
* `KHR_texture_transform` and `KHR_materials_emissive_strength` applied) and textures their
|
|
16
|
+
* sampler; primitives also read `TANGENT` and `TEXCOORD_1`. `gltf-skin.ts` turns skins into the
|
|
17
|
+
* animation module's `Skeleton` shape and skinned primitives into the skinned vertex layout.
|
|
18
|
+
*
|
|
19
|
+
* Matrices follow the engine's `Transform` convention: 3×4 row-major, `local = T · R · S`,
|
|
20
|
+
* `world = parent.world · local`.
|
|
21
|
+
*/
|
|
22
|
+
import { decodeMeshopt } from './meshopt.js';
|
|
23
|
+
export class GltfError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(`gltf: ${message}`);
|
|
26
|
+
this.name = 'GltfError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const GLB_MAGIC = 0x46546c67; // 'glTF'
|
|
30
|
+
const CHUNK_JSON = 0x4e4f534a;
|
|
31
|
+
const CHUNK_BIN = 0x004e4942;
|
|
32
|
+
const MODE_TRIANGLES = 4;
|
|
33
|
+
const COMPONENTS = {
|
|
34
|
+
SCALAR: 1,
|
|
35
|
+
VEC2: 2,
|
|
36
|
+
VEC3: 3,
|
|
37
|
+
VEC4: 4,
|
|
38
|
+
MAT2: 4,
|
|
39
|
+
MAT3: 9,
|
|
40
|
+
MAT4: 16,
|
|
41
|
+
};
|
|
42
|
+
const COMPONENT_BYTES = {
|
|
43
|
+
5120: 1,
|
|
44
|
+
5121: 1,
|
|
45
|
+
5122: 2,
|
|
46
|
+
5123: 2,
|
|
47
|
+
5125: 4,
|
|
48
|
+
5126: 4,
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Parse a GLB (`ArrayBuffer`), the UTF-8 text of a `.gltf` (`ArrayBuffer`), or a parsed
|
|
52
|
+
* `.gltf` with its buffers. Throws `GltfError` on anything this step does not support.
|
|
53
|
+
*/
|
|
54
|
+
export function parseGltf(source) {
|
|
55
|
+
const { json, buffers } = source instanceof ArrayBuffer ? readContainer(source) : source;
|
|
56
|
+
return parseJson(json, buffers ?? []);
|
|
57
|
+
}
|
|
58
|
+
/** Split a GLB into its JSON and BIN chunks; bytes that are not a GLB are parsed as `.gltf` text. */
|
|
59
|
+
function readContainer(bytes) {
|
|
60
|
+
const view = new DataView(bytes);
|
|
61
|
+
if (bytes.byteLength < 12 || view.getUint32(0, true) !== GLB_MAGIC) {
|
|
62
|
+
let text;
|
|
63
|
+
try {
|
|
64
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new GltfError('not a GLB (bad magic) and not UTF-8 JSON');
|
|
68
|
+
}
|
|
69
|
+
return { json: parseJsonText(text) };
|
|
70
|
+
}
|
|
71
|
+
const version = view.getUint32(4, true);
|
|
72
|
+
if (version !== 2)
|
|
73
|
+
throw new GltfError(`GLB version ${version}, expected 2`);
|
|
74
|
+
const length = Math.min(view.getUint32(8, true), bytes.byteLength);
|
|
75
|
+
let json = null;
|
|
76
|
+
let bin = null;
|
|
77
|
+
let offset = 12;
|
|
78
|
+
while (offset + 8 <= length) {
|
|
79
|
+
const chunkLength = view.getUint32(offset, true);
|
|
80
|
+
const chunkType = view.getUint32(offset + 4, true);
|
|
81
|
+
const start = offset + 8;
|
|
82
|
+
if (start + chunkLength > length)
|
|
83
|
+
throw new GltfError('GLB chunk runs past the file');
|
|
84
|
+
if (chunkType === CHUNK_JSON) {
|
|
85
|
+
json = parseJsonText(new TextDecoder().decode(new Uint8Array(bytes, start, chunkLength)));
|
|
86
|
+
}
|
|
87
|
+
else if (chunkType === CHUNK_BIN && bin === null) {
|
|
88
|
+
bin = bytes.slice(start, start + chunkLength);
|
|
89
|
+
}
|
|
90
|
+
offset = start + chunkLength;
|
|
91
|
+
}
|
|
92
|
+
if (!json)
|
|
93
|
+
throw new GltfError('GLB has no JSON chunk');
|
|
94
|
+
return { json, buffers: bin ? [bin] : [] };
|
|
95
|
+
}
|
|
96
|
+
function parseJsonText(text) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(text);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
throw new GltfError(`invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Extensions the parser reads, so neither `extensionsRequired` nor `warnings` mention them. */
|
|
105
|
+
const HANDLED_EXTENSIONS = new Set([
|
|
106
|
+
'KHR_texture_transform',
|
|
107
|
+
'KHR_materials_emissive_strength',
|
|
108
|
+
'KHR_mesh_quantization',
|
|
109
|
+
'EXT_meshopt_compression',
|
|
110
|
+
'KHR_lights_punctual',
|
|
111
|
+
'KHR_materials_variants',
|
|
112
|
+
]);
|
|
113
|
+
/** Extensions parsed into the document but not rendered: listed in `unsupportedExtensions`. */
|
|
114
|
+
const PARSED_EXTENSIONS = new Set([
|
|
115
|
+
'KHR_materials_clearcoat',
|
|
116
|
+
'KHR_materials_sheen',
|
|
117
|
+
'KHR_materials_transmission',
|
|
118
|
+
'KHR_materials_volume',
|
|
119
|
+
'KHR_materials_ior',
|
|
120
|
+
'KHR_materials_specular',
|
|
121
|
+
'KHR_texture_basisu',
|
|
122
|
+
]);
|
|
123
|
+
/** `minFilter` values whose base filter is nearest (9728, 9984 nearest-mipmap-nearest, 9986 nearest-mipmap-linear). */
|
|
124
|
+
const MIN_NEAREST = new Set([9728, 9984, 9986]);
|
|
125
|
+
function mipmapFilterOf(minFilter) {
|
|
126
|
+
switch (minFilter) {
|
|
127
|
+
case 9984: // NEAREST_MIPMAP_NEAREST
|
|
128
|
+
case 9985: // LINEAR_MIPMAP_NEAREST
|
|
129
|
+
return 'nearest';
|
|
130
|
+
case 9986: // NEAREST_MIPMAP_LINEAR
|
|
131
|
+
case 9987: // LINEAR_MIPMAP_LINEAR
|
|
132
|
+
case undefined: // the spec leaves it to the implementation: trilinear is the engine default
|
|
133
|
+
return 'linear';
|
|
134
|
+
default:
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function wrapOf(wrap) {
|
|
139
|
+
if (wrap === 33071)
|
|
140
|
+
return 'clamp-to-edge';
|
|
141
|
+
if (wrap === 33648)
|
|
142
|
+
return 'mirror-repeat';
|
|
143
|
+
return 'repeat';
|
|
144
|
+
}
|
|
145
|
+
function textureRef(info, textureCount, where) {
|
|
146
|
+
if (!info)
|
|
147
|
+
return null;
|
|
148
|
+
if (!Number.isInteger(info.index) || info.index < 0 || info.index >= textureCount) {
|
|
149
|
+
throw new GltfError(`${where}: texture ${info.index} does not exist`);
|
|
150
|
+
}
|
|
151
|
+
const x = info.extensions?.KHR_texture_transform;
|
|
152
|
+
let transform = null;
|
|
153
|
+
if (x) {
|
|
154
|
+
transform = {
|
|
155
|
+
offset: [x.offset?.[0] ?? 0, x.offset?.[1] ?? 0],
|
|
156
|
+
rotation: x.rotation ?? 0,
|
|
157
|
+
scale: [x.scale?.[0] ?? 1, x.scale?.[1] ?? 1],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { index: info.index, texCoord: x?.texCoord ?? info.texCoord ?? 0, transform };
|
|
161
|
+
}
|
|
162
|
+
function parseMaterial(m, i, textureCount) {
|
|
163
|
+
const where = `material ${i}`;
|
|
164
|
+
const pbr = m.pbrMetallicRoughness;
|
|
165
|
+
const f = pbr?.baseColorFactor;
|
|
166
|
+
const e = m.emissiveFactor;
|
|
167
|
+
const strength = m.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
168
|
+
const baseColor = textureRef(pbr?.baseColorTexture, textureCount, `${where} baseColorTexture`);
|
|
169
|
+
return {
|
|
170
|
+
name: m.name ?? null,
|
|
171
|
+
baseColorFactor: f ? [f[0] ?? 1, f[1] ?? 1, f[2] ?? 1, f[3] ?? 1] : [1, 1, 1, 1],
|
|
172
|
+
baseColor,
|
|
173
|
+
metallicFactor: pbr?.metallicFactor ?? 1,
|
|
174
|
+
roughnessFactor: pbr?.roughnessFactor ?? 1,
|
|
175
|
+
metallicRoughness: textureRef(pbr?.metallicRoughnessTexture, textureCount, `${where} metallicRoughnessTexture`),
|
|
176
|
+
normal: textureRef(m.normalTexture, textureCount, `${where} normalTexture`),
|
|
177
|
+
normalScale: m.normalTexture?.scale ?? 1,
|
|
178
|
+
occlusion: textureRef(m.occlusionTexture, textureCount, `${where} occlusionTexture`),
|
|
179
|
+
occlusionStrength: m.occlusionTexture?.strength ?? 1,
|
|
180
|
+
emissive: textureRef(m.emissiveTexture, textureCount, `${where} emissiveTexture`),
|
|
181
|
+
emissiveFactor: [(e?.[0] ?? 0) * strength, (e?.[1] ?? 0) * strength, (e?.[2] ?? 0) * strength],
|
|
182
|
+
doubleSided: m.doubleSided ?? false,
|
|
183
|
+
alphaMode: m.alphaMode ?? 'OPAQUE',
|
|
184
|
+
alphaCutoff: m.alphaCutoff ?? 0.5,
|
|
185
|
+
extensions: parseMaterialExtensions(m, where, textureCount),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function vec3Of(v, fallback) {
|
|
189
|
+
return [v?.[0] ?? fallback, v?.[1] ?? fallback, v?.[2] ?? fallback];
|
|
190
|
+
}
|
|
191
|
+
function parseMaterialExtensions(m, where, textureCount) {
|
|
192
|
+
const x = m.extensions;
|
|
193
|
+
const ref = (info, slot) => textureRef(info, textureCount, `${where} ${slot}`);
|
|
194
|
+
const cc = x?.KHR_materials_clearcoat;
|
|
195
|
+
const sh = x?.KHR_materials_sheen;
|
|
196
|
+
const tr = x?.KHR_materials_transmission;
|
|
197
|
+
const vo = x?.KHR_materials_volume;
|
|
198
|
+
const io = x?.KHR_materials_ior;
|
|
199
|
+
const sp = x?.KHR_materials_specular;
|
|
200
|
+
return {
|
|
201
|
+
clearcoat: cc
|
|
202
|
+
? {
|
|
203
|
+
clearcoatFactor: cc.clearcoatFactor ?? 0,
|
|
204
|
+
clearcoatTexture: ref(cc.clearcoatTexture, 'clearcoatTexture'),
|
|
205
|
+
clearcoatRoughnessFactor: cc.clearcoatRoughnessFactor ?? 0,
|
|
206
|
+
clearcoatRoughnessTexture: ref(cc.clearcoatRoughnessTexture, 'clearcoatRoughnessTexture'),
|
|
207
|
+
clearcoatNormalTexture: ref(cc.clearcoatNormalTexture, 'clearcoatNormalTexture'),
|
|
208
|
+
}
|
|
209
|
+
: null,
|
|
210
|
+
sheen: sh
|
|
211
|
+
? {
|
|
212
|
+
sheenColorFactor: vec3Of(sh.sheenColorFactor, 0),
|
|
213
|
+
sheenColorTexture: ref(sh.sheenColorTexture, 'sheenColorTexture'),
|
|
214
|
+
sheenRoughnessFactor: sh.sheenRoughnessFactor ?? 0,
|
|
215
|
+
sheenRoughnessTexture: ref(sh.sheenRoughnessTexture, 'sheenRoughnessTexture'),
|
|
216
|
+
}
|
|
217
|
+
: null,
|
|
218
|
+
transmission: tr
|
|
219
|
+
? {
|
|
220
|
+
transmissionFactor: tr.transmissionFactor ?? 0,
|
|
221
|
+
transmissionTexture: ref(tr.transmissionTexture, 'transmissionTexture'),
|
|
222
|
+
}
|
|
223
|
+
: null,
|
|
224
|
+
volume: vo
|
|
225
|
+
? {
|
|
226
|
+
thicknessFactor: vo.thicknessFactor ?? 0,
|
|
227
|
+
thicknessTexture: ref(vo.thicknessTexture, 'thicknessTexture'),
|
|
228
|
+
attenuationDistance: vo.attenuationDistance ?? Infinity,
|
|
229
|
+
attenuationColor: vec3Of(vo.attenuationColor, 1),
|
|
230
|
+
}
|
|
231
|
+
: null,
|
|
232
|
+
ior: io ? { ior: io.ior ?? 1.5 } : null,
|
|
233
|
+
specular: sp
|
|
234
|
+
? {
|
|
235
|
+
specularFactor: sp.specularFactor ?? 1,
|
|
236
|
+
specularTexture: ref(sp.specularTexture, 'specularTexture'),
|
|
237
|
+
specularColorFactor: vec3Of(sp.specularColorFactor, 1),
|
|
238
|
+
specularColorTexture: ref(sp.specularColorTexture, 'specularColorTexture'),
|
|
239
|
+
}
|
|
240
|
+
: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Apply a `KHR_texture_transform` to one uv (`T · R · S · uv`): `out[0], out[1]` ← the
|
|
245
|
+
* transformed `u, v`. Returns `out`.
|
|
246
|
+
*/
|
|
247
|
+
export function transformUv(t, u, v, out = [0, 0]) {
|
|
248
|
+
const c = Math.cos(t.rotation);
|
|
249
|
+
const s = Math.sin(t.rotation);
|
|
250
|
+
const su = u * t.scale[0];
|
|
251
|
+
const sv = v * t.scale[1];
|
|
252
|
+
out[0] = c * su - s * sv + t.offset[0];
|
|
253
|
+
out[1] = s * su + c * sv + t.offset[1];
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* `EXT_meshopt_compression`: decode every compressed buffer view into its own buffer (appended
|
|
258
|
+
* to `buffers`) and return a shallow copy of `json` whose views point at the decoded bytes. A
|
|
259
|
+
* view without the extension, or a file without any, is read as written (`json` returned as is).
|
|
260
|
+
*/
|
|
261
|
+
function decodeMeshoptViews(json, buffers) {
|
|
262
|
+
const views = json.bufferViews ?? [];
|
|
263
|
+
if (!views.some((v) => v.extensions?.EXT_meshopt_compression))
|
|
264
|
+
return json;
|
|
265
|
+
const out = views.map((bv, i) => {
|
|
266
|
+
const x = bv.extensions?.EXT_meshopt_compression;
|
|
267
|
+
if (!x)
|
|
268
|
+
return bv;
|
|
269
|
+
const where = `bufferView ${i} (EXT_meshopt_compression)`;
|
|
270
|
+
const src = buffers[x.buffer];
|
|
271
|
+
if (!src)
|
|
272
|
+
throw new GltfError(`${where}: buffer ${x.buffer} does not exist`);
|
|
273
|
+
const off = x.byteOffset ?? 0;
|
|
274
|
+
if (off + x.byteLength > src.byteLength) {
|
|
275
|
+
throw new GltfError(`${where}: stream runs past buffer ${x.buffer}`);
|
|
276
|
+
}
|
|
277
|
+
const target = new Uint8Array(x.count * x.byteStride);
|
|
278
|
+
try {
|
|
279
|
+
decodeMeshopt(target, x.count, x.byteStride, new Uint8Array(src, off, x.byteLength), x.mode, x.filter ?? 'NONE');
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
throw new GltfError(`${where}: ${e instanceof Error ? e.message : String(e)}`);
|
|
283
|
+
}
|
|
284
|
+
buffers.push(target.buffer);
|
|
285
|
+
const view = {
|
|
286
|
+
buffer: buffers.length - 1,
|
|
287
|
+
byteOffset: 0,
|
|
288
|
+
byteLength: Math.min(bv.byteLength, target.byteLength),
|
|
289
|
+
};
|
|
290
|
+
if (bv.byteStride !== undefined)
|
|
291
|
+
view.byteStride = bv.byteStride;
|
|
292
|
+
return view;
|
|
293
|
+
});
|
|
294
|
+
return { ...json, bufferViews: out };
|
|
295
|
+
}
|
|
296
|
+
function parseJson(json, given) {
|
|
297
|
+
const version = json.asset?.version ?? '';
|
|
298
|
+
if (!version.startsWith('2.'))
|
|
299
|
+
throw new GltfError(`asset.version "${version}", expected 2.x`);
|
|
300
|
+
const required = (json.extensionsRequired ?? []).filter((e) => !HANDLED_EXTENSIONS.has(e) && !PARSED_EXTENSIONS.has(e));
|
|
301
|
+
if (required.length > 0) {
|
|
302
|
+
throw new GltfError(`required extensions not supported: ${required.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
const warnings = [];
|
|
305
|
+
const unsupportedExtensions = [];
|
|
306
|
+
for (const ext of new Set([...(json.extensionsUsed ?? []), ...(json.extensionsRequired ?? [])])) {
|
|
307
|
+
if (PARSED_EXTENSIONS.has(ext))
|
|
308
|
+
unsupportedExtensions.push(ext);
|
|
309
|
+
else if (!HANDLED_EXTENSIONS.has(ext))
|
|
310
|
+
warnings.push(`extension ignored: ${ext}`);
|
|
311
|
+
}
|
|
312
|
+
const externalUris = [];
|
|
313
|
+
const buffers = (json.buffers ?? []).map((b, i) => {
|
|
314
|
+
if (b.uri !== undefined && isDataUri(b.uri))
|
|
315
|
+
return decodeDataUri(b.uri).bytes.buffer;
|
|
316
|
+
const bytes = given[i];
|
|
317
|
+
if (!bytes && b.extensions?.EXT_meshopt_compression?.fallback) {
|
|
318
|
+
// A meshopt fallback buffer with no data: only compressed views point at it.
|
|
319
|
+
return new ArrayBuffer(0);
|
|
320
|
+
}
|
|
321
|
+
if (!bytes) {
|
|
322
|
+
throw new GltfError(b.uri === undefined
|
|
323
|
+
? `buffer ${i} has no uri and no GLB BIN chunk`
|
|
324
|
+
: `buffer ${i} needs external resource "${b.uri}" (pass it in buffers[${i}])`);
|
|
325
|
+
}
|
|
326
|
+
if (b.uri !== undefined)
|
|
327
|
+
externalUris.push(b.uri);
|
|
328
|
+
return bytes;
|
|
329
|
+
});
|
|
330
|
+
const ctx = { json: decodeMeshoptViews(json, buffers), buffers };
|
|
331
|
+
const images = (json.images ?? []).map((img, i) => {
|
|
332
|
+
const name = img.name ?? null;
|
|
333
|
+
if (img.bufferView !== undefined) {
|
|
334
|
+
const view = bufferViewBytes(ctx, img.bufferView, `image ${i}`);
|
|
335
|
+
return { name, bytes: view, mimeType: img.mimeType ?? null, uri: null };
|
|
336
|
+
}
|
|
337
|
+
if (img.uri === undefined)
|
|
338
|
+
throw new GltfError(`image ${i} has neither uri nor bufferView`);
|
|
339
|
+
if (isDataUri(img.uri)) {
|
|
340
|
+
const d = decodeDataUri(img.uri);
|
|
341
|
+
return { name, bytes: d.bytes, mimeType: img.mimeType ?? d.mimeType, uri: null };
|
|
342
|
+
}
|
|
343
|
+
externalUris.push(img.uri);
|
|
344
|
+
return { name, bytes: null, mimeType: img.mimeType ?? mimeFromUri(img.uri), uri: img.uri };
|
|
345
|
+
});
|
|
346
|
+
const textures = (json.textures ?? []).map((t) => {
|
|
347
|
+
const s = t.sampler !== undefined ? json.samplers?.[t.sampler] : undefined;
|
|
348
|
+
return {
|
|
349
|
+
name: t.name ?? null,
|
|
350
|
+
source: t.source ?? null,
|
|
351
|
+
basisuSource: t.extensions?.KHR_texture_basisu?.source ?? null,
|
|
352
|
+
wrapS: s?.wrapS ?? 10497,
|
|
353
|
+
wrapT: s?.wrapT ?? 10497,
|
|
354
|
+
magFilter: s?.magFilter ?? null,
|
|
355
|
+
sampler: {
|
|
356
|
+
magFilter: s?.magFilter === 9728 ? 'nearest' : 'linear',
|
|
357
|
+
minFilter: MIN_NEAREST.has(s?.minFilter ?? 0) ? 'nearest' : 'linear',
|
|
358
|
+
mipmapFilter: mipmapFilterOf(s?.minFilter),
|
|
359
|
+
wrapS: wrapOf(s?.wrapS),
|
|
360
|
+
wrapT: wrapOf(s?.wrapT),
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
const materials = (json.materials ?? []).map((m, i) => parseMaterial(m, i, textures.length));
|
|
365
|
+
const meshes = (json.meshes ?? []).map((m, mi) => parseMesh(ctx, m, mi));
|
|
366
|
+
const lightDefs = json.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
367
|
+
const nodes = parseNodes(json.nodes ?? [], meshes.length, (json.skins ?? []).length, (json.cameras ?? []).length, lightDefs.length);
|
|
368
|
+
const skins = (json.skins ?? []).map((s, i) => parseSkin(ctx, s, i, nodes.length));
|
|
369
|
+
const animations = (json.animations ?? []).map((a, i) => parseAnimation(ctx, a, i, nodes.length, warnings));
|
|
370
|
+
const roots = sceneRoots(json, nodes);
|
|
371
|
+
const sceneNodes = [];
|
|
372
|
+
const visit = (i) => {
|
|
373
|
+
sceneNodes.push(i);
|
|
374
|
+
for (const c of nodes[i].children)
|
|
375
|
+
visit(c);
|
|
376
|
+
};
|
|
377
|
+
for (const r of roots)
|
|
378
|
+
visit(r);
|
|
379
|
+
const variants = (json.extensions?.KHR_materials_variants?.variants ?? []).map((v, i) => v.name ?? `variant${i}`);
|
|
380
|
+
meshes.forEach((m, mi) => {
|
|
381
|
+
m.primitives.forEach((p, pi) => {
|
|
382
|
+
for (const map of p.variants) {
|
|
383
|
+
for (const v of map.variants) {
|
|
384
|
+
if (!Number.isInteger(v) || v < 0 || v >= variants.length) {
|
|
385
|
+
throw new GltfError(`mesh ${mi} primitive ${pi}: variant ${v} does not exist`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
version,
|
|
393
|
+
scene: roots,
|
|
394
|
+
sceneNodes,
|
|
395
|
+
nodes,
|
|
396
|
+
meshes,
|
|
397
|
+
materials,
|
|
398
|
+
textures,
|
|
399
|
+
images,
|
|
400
|
+
skins,
|
|
401
|
+
animations,
|
|
402
|
+
externalUris,
|
|
403
|
+
warnings,
|
|
404
|
+
unsupportedExtensions,
|
|
405
|
+
lights: placeLights(lightDefs, nodes, sceneNodes),
|
|
406
|
+
cameras: placeCameras(json.cameras ?? [], nodes, sceneNodes),
|
|
407
|
+
variants,
|
|
408
|
+
bounds: sceneBounds(nodes, meshes, sceneNodes),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** The node's world -Z, normalised (the direction glTF lights shine and cameras look). */
|
|
412
|
+
function forwardOf(w) {
|
|
413
|
+
const x = -w[2];
|
|
414
|
+
const y = -w[6];
|
|
415
|
+
const z = -w[10];
|
|
416
|
+
const l = Math.hypot(x, y, z) || 1;
|
|
417
|
+
// `+ 0` turns the -0 of an axis-aligned node into 0.
|
|
418
|
+
return [x / l + 0, y / l + 0, z / l + 0];
|
|
419
|
+
}
|
|
420
|
+
function placeLights(defs, nodes, sceneNodes) {
|
|
421
|
+
const out = [];
|
|
422
|
+
for (const ni of sceneNodes) {
|
|
423
|
+
const n = nodes[ni];
|
|
424
|
+
if (n.light === null)
|
|
425
|
+
continue;
|
|
426
|
+
const d = defs[n.light];
|
|
427
|
+
const where = `light ${n.light}`;
|
|
428
|
+
if (d.type !== 'directional' && d.type !== 'point' && d.type !== 'spot') {
|
|
429
|
+
throw new GltfError(`${where}: type "${String(d.type)}" unknown`);
|
|
430
|
+
}
|
|
431
|
+
const light = {
|
|
432
|
+
name: d.name ?? null,
|
|
433
|
+
node: ni,
|
|
434
|
+
light: n.light,
|
|
435
|
+
type: d.type,
|
|
436
|
+
color: vec3Of(d.color, 1),
|
|
437
|
+
intensity: d.intensity ?? 1,
|
|
438
|
+
position: [n.world[3], n.world[7], n.world[11]],
|
|
439
|
+
direction: forwardOf(n.world),
|
|
440
|
+
world: n.world,
|
|
441
|
+
};
|
|
442
|
+
if (d.type !== 'directional' && d.range !== undefined) {
|
|
443
|
+
if (!(d.range > 0))
|
|
444
|
+
throw new GltfError(`${where}: range ${d.range} must be > 0`);
|
|
445
|
+
light.range = d.range;
|
|
446
|
+
}
|
|
447
|
+
if (d.type === 'spot') {
|
|
448
|
+
const inner = d.spot?.innerConeAngle ?? 0;
|
|
449
|
+
const outer = d.spot?.outerConeAngle ?? Math.PI / 4;
|
|
450
|
+
if (!(inner >= 0 && inner < outer && outer <= Math.PI / 2)) {
|
|
451
|
+
throw new GltfError(`${where}: cone angles ${inner}, ${outer} need 0 <= inner < outer <= π/2`);
|
|
452
|
+
}
|
|
453
|
+
light.innerConeAngle = inner;
|
|
454
|
+
light.outerConeAngle = outer;
|
|
455
|
+
}
|
|
456
|
+
out.push(light);
|
|
457
|
+
}
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
function placeCameras(defs, nodes, sceneNodes) {
|
|
461
|
+
const out = [];
|
|
462
|
+
for (const ni of sceneNodes) {
|
|
463
|
+
const n = nodes[ni];
|
|
464
|
+
if (n.camera === null)
|
|
465
|
+
continue;
|
|
466
|
+
const d = defs[n.camera];
|
|
467
|
+
const where = `camera ${n.camera}`;
|
|
468
|
+
const c = {
|
|
469
|
+
name: d.name ?? null,
|
|
470
|
+
node: ni,
|
|
471
|
+
camera: n.camera,
|
|
472
|
+
type: d.type,
|
|
473
|
+
yfov: 0,
|
|
474
|
+
aspectRatio: null,
|
|
475
|
+
znear: 0,
|
|
476
|
+
zfar: null,
|
|
477
|
+
xmag: 0,
|
|
478
|
+
ymag: 0,
|
|
479
|
+
position: [n.world[3], n.world[7], n.world[11]],
|
|
480
|
+
world: n.world,
|
|
481
|
+
view: viewOf(n.world),
|
|
482
|
+
};
|
|
483
|
+
if (d.type === 'perspective') {
|
|
484
|
+
const p = d.perspective;
|
|
485
|
+
if (!p)
|
|
486
|
+
throw new GltfError(`${where}: perspective camera without "perspective"`);
|
|
487
|
+
c.yfov = p.yfov;
|
|
488
|
+
c.aspectRatio = p.aspectRatio ?? null;
|
|
489
|
+
c.znear = p.znear;
|
|
490
|
+
c.zfar = p.zfar ?? null;
|
|
491
|
+
}
|
|
492
|
+
else if (d.type === 'orthographic') {
|
|
493
|
+
const o = d.orthographic;
|
|
494
|
+
if (!o)
|
|
495
|
+
throw new GltfError(`${where}: orthographic camera without "orthographic"`);
|
|
496
|
+
c.xmag = o.xmag;
|
|
497
|
+
c.ymag = o.ymag;
|
|
498
|
+
c.znear = o.znear;
|
|
499
|
+
c.zfar = o.zfar;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
throw new GltfError(`${where}: type "${String(d.type)}" unknown`);
|
|
503
|
+
}
|
|
504
|
+
out.push(c);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
/** The inverse of a 3×4 row-major world matrix with its scale removed, as a column-major 4×4. */
|
|
509
|
+
function viewOf(w) {
|
|
510
|
+
const v = new Float32Array(16);
|
|
511
|
+
// Column j of the world rotation is the node's local axis j; its length is that axis' scale.
|
|
512
|
+
const len = [0, 1, 2].map((j) => Math.hypot(w[j], w[4 + j], w[8 + j]) || 1);
|
|
513
|
+
const t = [w[3], w[7], w[11]];
|
|
514
|
+
for (let r = 0; r < 3; r++) {
|
|
515
|
+
// View rotation row r = world rotation column r, normalised; column-major index c * 4 + r.
|
|
516
|
+
for (let c = 0; c < 3; c++)
|
|
517
|
+
v[c * 4 + r] = w[c * 4 + r] / len[r];
|
|
518
|
+
v[12 + r] = -(v[r] * t[0] + v[4 + r] * t[1] + v[8 + r] * t[2]);
|
|
519
|
+
}
|
|
520
|
+
v[15] = 1;
|
|
521
|
+
return v;
|
|
522
|
+
}
|
|
523
|
+
function sceneRoots(json, nodes) {
|
|
524
|
+
const scenes = json.scenes ?? [];
|
|
525
|
+
if (scenes.length > 0) {
|
|
526
|
+
const i = json.scene ?? 0;
|
|
527
|
+
const s = scenes[i];
|
|
528
|
+
if (!s)
|
|
529
|
+
throw new GltfError(`scene ${i} does not exist`);
|
|
530
|
+
for (const n of s.nodes ?? []) {
|
|
531
|
+
if (!nodes[n])
|
|
532
|
+
throw new GltfError(`scene ${i} names node ${n}, which does not exist`);
|
|
533
|
+
}
|
|
534
|
+
return [...(s.nodes ?? [])];
|
|
535
|
+
}
|
|
536
|
+
return nodes.flatMap((n, i) => (n.parent === null ? [i] : []));
|
|
537
|
+
}
|
|
538
|
+
// ---------------------------------------------------------------- nodes and matrices
|
|
539
|
+
function parseNodes(src, meshCount, skinCount, cameraCount, lightCount) {
|
|
540
|
+
const nodes = src.map((n, i) => {
|
|
541
|
+
if (n.camera !== undefined && !(n.camera >= 0 && n.camera < cameraCount)) {
|
|
542
|
+
throw new GltfError(`node ${i} names camera ${n.camera}, which does not exist`);
|
|
543
|
+
}
|
|
544
|
+
const light = n.extensions?.KHR_lights_punctual?.light;
|
|
545
|
+
if (light !== undefined && !(light >= 0 && light < lightCount)) {
|
|
546
|
+
throw new GltfError(`node ${i} names light ${light}, which does not exist`);
|
|
547
|
+
}
|
|
548
|
+
if (n.mesh !== undefined && (n.mesh < 0 || n.mesh >= meshCount)) {
|
|
549
|
+
throw new GltfError(`node ${i} names mesh ${n.mesh}, which does not exist`);
|
|
550
|
+
}
|
|
551
|
+
if (n.skin !== undefined && (n.skin < 0 || n.skin >= skinCount)) {
|
|
552
|
+
throw new GltfError(`node ${i} names skin ${n.skin}, which does not exist`);
|
|
553
|
+
}
|
|
554
|
+
const node = {
|
|
555
|
+
name: n.name ?? null,
|
|
556
|
+
parent: null,
|
|
557
|
+
children: [...(n.children ?? [])],
|
|
558
|
+
mesh: n.mesh ?? null,
|
|
559
|
+
skin: n.skin ?? null,
|
|
560
|
+
camera: n.camera ?? null,
|
|
561
|
+
light: light ?? null,
|
|
562
|
+
translation: [0, 0, 0],
|
|
563
|
+
rotation: [0, 0, 0, 1],
|
|
564
|
+
scale: [1, 1, 1],
|
|
565
|
+
local: new Float32Array(12),
|
|
566
|
+
world: new Float32Array(12),
|
|
567
|
+
};
|
|
568
|
+
if (n.matrix) {
|
|
569
|
+
if (n.matrix.length !== 16)
|
|
570
|
+
throw new GltfError(`node ${i} matrix has ${n.matrix.length} values`);
|
|
571
|
+
columnMajorToRows(n.matrix, node.local);
|
|
572
|
+
decompose(node.local, node.translation, node.rotation, node.scale);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
if (n.translation)
|
|
576
|
+
node.translation = [n.translation[0], n.translation[1], n.translation[2]];
|
|
577
|
+
if (n.rotation) {
|
|
578
|
+
node.rotation = [n.rotation[0], n.rotation[1], n.rotation[2], n.rotation[3]];
|
|
579
|
+
}
|
|
580
|
+
if (n.scale)
|
|
581
|
+
node.scale = [n.scale[0], n.scale[1], n.scale[2]];
|
|
582
|
+
composeTrs(node.translation, node.rotation, node.scale, node.local);
|
|
583
|
+
}
|
|
584
|
+
return node;
|
|
585
|
+
});
|
|
586
|
+
nodes.forEach((n, i) => {
|
|
587
|
+
for (const c of n.children) {
|
|
588
|
+
const child = nodes[c];
|
|
589
|
+
if (!child)
|
|
590
|
+
throw new GltfError(`node ${i} names child ${c}, which does not exist`);
|
|
591
|
+
if (child.parent !== null)
|
|
592
|
+
throw new GltfError(`node ${c} has two parents`);
|
|
593
|
+
child.parent = i;
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
// World matrices, parents first; a cycle would never reach a root, so detect it by depth.
|
|
597
|
+
const done = new Uint8Array(nodes.length);
|
|
598
|
+
const resolve = (i, depth) => {
|
|
599
|
+
if (done[i])
|
|
600
|
+
return;
|
|
601
|
+
if (depth > nodes.length)
|
|
602
|
+
throw new GltfError('node hierarchy has a cycle');
|
|
603
|
+
const n = nodes[i];
|
|
604
|
+
if (n.parent === null) {
|
|
605
|
+
n.world.set(n.local);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
resolve(n.parent, depth + 1);
|
|
609
|
+
multiply3x4(n.world, nodes[n.parent].world, n.local);
|
|
610
|
+
}
|
|
611
|
+
done[i] = 1;
|
|
612
|
+
};
|
|
613
|
+
for (let i = 0; i < nodes.length; i++)
|
|
614
|
+
resolve(i, 0);
|
|
615
|
+
return nodes;
|
|
616
|
+
}
|
|
617
|
+
/** glTF column-major 4×4 at `m[from..]` → the engine's 3×4 row-major at `out[to..]` (last row dropped). */
|
|
618
|
+
function columnMajorToRows(m, out, from = 0, to = 0) {
|
|
619
|
+
for (let r = 0; r < 3; r++) {
|
|
620
|
+
for (let c = 0; c < 4; c++)
|
|
621
|
+
out[to + r * 4 + c] = m[from + c * 4 + r];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// ---------------------------------------------------------------- skins and animations
|
|
625
|
+
function parseSkin(ctx, s, i, nodeCount) {
|
|
626
|
+
const where = `skin ${i}`;
|
|
627
|
+
if (!Array.isArray(s.joints) || s.joints.length === 0) {
|
|
628
|
+
throw new GltfError(`${where} has no joints`);
|
|
629
|
+
}
|
|
630
|
+
for (const j of s.joints) {
|
|
631
|
+
if (!Number.isInteger(j) || j < 0 || j >= nodeCount) {
|
|
632
|
+
throw new GltfError(`${where} names joint node ${j}, which does not exist`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (s.skeleton !== undefined && (s.skeleton < 0 || s.skeleton >= nodeCount)) {
|
|
636
|
+
throw new GltfError(`${where} names skeleton node ${s.skeleton}, which does not exist`);
|
|
637
|
+
}
|
|
638
|
+
const n = s.joints.length;
|
|
639
|
+
const inverseBind = new Float32Array(n * 12);
|
|
640
|
+
if (s.inverseBindMatrices !== undefined) {
|
|
641
|
+
const acc = accessor(ctx, s.inverseBindMatrices, where);
|
|
642
|
+
if (acc.type !== 'MAT4') {
|
|
643
|
+
throw new GltfError(`${where}: inverseBindMatrices is ${acc.type}, expected MAT4`);
|
|
644
|
+
}
|
|
645
|
+
if (acc.count !== n) {
|
|
646
|
+
throw new GltfError(`${where}: ${acc.count} inverse bind matrices for ${n} joints`);
|
|
647
|
+
}
|
|
648
|
+
const m = readFloats(ctx, acc, 16, where);
|
|
649
|
+
for (let j = 0; j < n; j++)
|
|
650
|
+
columnMajorToRows(m, inverseBind, j * 16, j * 12);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
for (let j = 0; j < n; j++) {
|
|
654
|
+
inverseBind[j * 12] = 1;
|
|
655
|
+
inverseBind[j * 12 + 5] = 1;
|
|
656
|
+
inverseBind[j * 12 + 10] = 1;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return { name: s.name ?? null, joints: [...s.joints], inverseBind, skeleton: s.skeleton ?? null };
|
|
660
|
+
}
|
|
661
|
+
/** Components per keyframe value; null for `weights` (one per morph target, any count). */
|
|
662
|
+
const PATH_COMPONENTS = {
|
|
663
|
+
translation: 3,
|
|
664
|
+
rotation: 4,
|
|
665
|
+
scale: 3,
|
|
666
|
+
weights: null,
|
|
667
|
+
};
|
|
668
|
+
function parseAnimation(ctx, a, i, nodeCount, warnings) {
|
|
669
|
+
const where = `animation ${i}`;
|
|
670
|
+
const channels = [];
|
|
671
|
+
let duration = 0;
|
|
672
|
+
(a.channels ?? []).forEach((c, ci) => {
|
|
673
|
+
const here = `${where} channel ${ci}`;
|
|
674
|
+
const target = c.target;
|
|
675
|
+
if (!target)
|
|
676
|
+
throw new GltfError(`${here} has no target`);
|
|
677
|
+
if (target.node === undefined) {
|
|
678
|
+
warnings.push(`${here} ignored: no target node`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (target.node < 0 || target.node >= nodeCount) {
|
|
682
|
+
throw new GltfError(`${here} targets node ${target.node}, which does not exist`);
|
|
683
|
+
}
|
|
684
|
+
const path = target.path;
|
|
685
|
+
if (!(path in PATH_COMPONENTS))
|
|
686
|
+
throw new GltfError(`${here}: path "${target.path}" unknown`);
|
|
687
|
+
const sampler = (a.samplers ?? [])[c.sampler];
|
|
688
|
+
if (!sampler)
|
|
689
|
+
throw new GltfError(`${here}: sampler ${c.sampler} does not exist`);
|
|
690
|
+
const interpolation = sampler.interpolation ?? 'LINEAR';
|
|
691
|
+
if (interpolation !== 'LINEAR' && interpolation !== 'STEP' && interpolation !== 'CUBICSPLINE') {
|
|
692
|
+
throw new GltfError(`${here}: interpolation "${interpolation}" unknown`);
|
|
693
|
+
}
|
|
694
|
+
const inAcc = accessor(ctx, sampler.input, here);
|
|
695
|
+
if (inAcc.type !== 'SCALAR') {
|
|
696
|
+
throw new GltfError(`${here}: input is ${inAcc.type}, expected SCALAR`);
|
|
697
|
+
}
|
|
698
|
+
const times = readFloats(ctx, inAcc, 1, here);
|
|
699
|
+
for (let k = 1; k < times.length; k++) {
|
|
700
|
+
if (!(times[k] >= times[k - 1])) {
|
|
701
|
+
throw new GltfError(`${here}: keyframe times are not ascending`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const outAcc = accessor(ctx, sampler.output, here);
|
|
705
|
+
const have = COMPONENTS[outAcc.type];
|
|
706
|
+
const want = PATH_COMPONENTS[path];
|
|
707
|
+
if (want !== null && have !== want) {
|
|
708
|
+
throw new GltfError(`${here}: ${path} output is ${outAcc.type}, expected ${want} components`);
|
|
709
|
+
}
|
|
710
|
+
const perKey = interpolation === 'CUBICSPLINE' ? 3 : 1;
|
|
711
|
+
const n = times.length;
|
|
712
|
+
// `weights` holds one value per morph target per key; the TRS paths exactly one.
|
|
713
|
+
const wrong = want === null
|
|
714
|
+
? outAcc.count === 0 || outAcc.count % (n * perKey) !== 0
|
|
715
|
+
: outAcc.count !== n * perKey;
|
|
716
|
+
if (n > 0 && wrong) {
|
|
717
|
+
const hint = perKey === 3 ? ' (×3 for CUBICSPLINE)' : '';
|
|
718
|
+
throw new GltfError(`${here}: ${outAcc.count} output values for ${n} keyframes${hint}`);
|
|
719
|
+
}
|
|
720
|
+
const values = readFloats(ctx, outAcc, have, here);
|
|
721
|
+
if (n > 0)
|
|
722
|
+
duration = Math.max(duration, times[n - 1]);
|
|
723
|
+
channels.push({ node: target.node, path, interpolation, times, values });
|
|
724
|
+
});
|
|
725
|
+
return { name: a.name ?? `animation${i}`, duration, channels };
|
|
726
|
+
}
|
|
727
|
+
/** `T · R · S` as 3×4 row-major (the same formula as `Transform.updateLocal`). */
|
|
728
|
+
export function composeTrs(t, q, s, out) {
|
|
729
|
+
const [x, y, z, w] = q;
|
|
730
|
+
const xx = x * x;
|
|
731
|
+
const yy = y * y;
|
|
732
|
+
const zz = z * z;
|
|
733
|
+
const xy = x * y;
|
|
734
|
+
const xz = x * z;
|
|
735
|
+
const yz = y * z;
|
|
736
|
+
const wx = w * x;
|
|
737
|
+
const wy = w * y;
|
|
738
|
+
const wz = w * z;
|
|
739
|
+
out[0] = (1 - 2 * (yy + zz)) * s[0];
|
|
740
|
+
out[1] = 2 * (xy - wz) * s[1];
|
|
741
|
+
out[2] = 2 * (xz + wy) * s[2];
|
|
742
|
+
out[3] = t[0];
|
|
743
|
+
out[4] = 2 * (xy + wz) * s[0];
|
|
744
|
+
out[5] = (1 - 2 * (xx + zz)) * s[1];
|
|
745
|
+
out[6] = 2 * (yz - wx) * s[2];
|
|
746
|
+
out[7] = t[1];
|
|
747
|
+
out[8] = 2 * (xz - wy) * s[0];
|
|
748
|
+
out[9] = 2 * (yz + wx) * s[1];
|
|
749
|
+
out[10] = (1 - 2 * (xx + yy)) * s[2];
|
|
750
|
+
out[11] = t[2];
|
|
751
|
+
}
|
|
752
|
+
/** TRS from a 3×4 affine matrix (glTF matrices are TRS by spec; shear is not preserved). */
|
|
753
|
+
function decompose(m, t, q, s) {
|
|
754
|
+
t[0] = m[3];
|
|
755
|
+
t[1] = m[7];
|
|
756
|
+
t[2] = m[11];
|
|
757
|
+
// Columns are the scaled basis vectors.
|
|
758
|
+
s[0] = Math.hypot(m[0], m[4], m[8]);
|
|
759
|
+
s[1] = Math.hypot(m[1], m[5], m[9]);
|
|
760
|
+
s[2] = Math.hypot(m[2], m[6], m[10]);
|
|
761
|
+
// A negative determinant flips one axis.
|
|
762
|
+
const det = m[0] * (m[5] * m[10] - m[6] * m[9]) -
|
|
763
|
+
m[1] * (m[4] * m[10] - m[6] * m[8]) +
|
|
764
|
+
m[2] * (m[4] * m[9] - m[5] * m[8]);
|
|
765
|
+
if (det < 0)
|
|
766
|
+
s[0] = -s[0];
|
|
767
|
+
const sx = s[0] || 1;
|
|
768
|
+
const sy = s[1] || 1;
|
|
769
|
+
const sz = s[2] || 1;
|
|
770
|
+
const r00 = m[0] / sx;
|
|
771
|
+
const r01 = m[1] / sy;
|
|
772
|
+
const r02 = m[2] / sz;
|
|
773
|
+
const r10 = m[4] / sx;
|
|
774
|
+
const r11 = m[5] / sy;
|
|
775
|
+
const r12 = m[6] / sz;
|
|
776
|
+
const r20 = m[8] / sx;
|
|
777
|
+
const r21 = m[9] / sy;
|
|
778
|
+
const r22 = m[10] / sz;
|
|
779
|
+
const trace = r00 + r11 + r22;
|
|
780
|
+
if (trace > 0) {
|
|
781
|
+
const k = Math.sqrt(trace + 1) * 2;
|
|
782
|
+
q[3] = k / 4;
|
|
783
|
+
q[0] = (r21 - r12) / k;
|
|
784
|
+
q[1] = (r02 - r20) / k;
|
|
785
|
+
q[2] = (r10 - r01) / k;
|
|
786
|
+
}
|
|
787
|
+
else if (r00 > r11 && r00 > r22) {
|
|
788
|
+
const k = Math.sqrt(1 + r00 - r11 - r22) * 2;
|
|
789
|
+
q[3] = (r21 - r12) / k;
|
|
790
|
+
q[0] = k / 4;
|
|
791
|
+
q[1] = (r01 + r10) / k;
|
|
792
|
+
q[2] = (r02 + r20) / k;
|
|
793
|
+
}
|
|
794
|
+
else if (r11 > r22) {
|
|
795
|
+
const k = Math.sqrt(1 + r11 - r00 - r22) * 2;
|
|
796
|
+
q[3] = (r02 - r20) / k;
|
|
797
|
+
q[0] = (r01 + r10) / k;
|
|
798
|
+
q[1] = k / 4;
|
|
799
|
+
q[2] = (r12 + r21) / k;
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
const k = Math.sqrt(1 + r22 - r00 - r11) * 2;
|
|
803
|
+
q[3] = (r10 - r01) / k;
|
|
804
|
+
q[0] = (r02 + r20) / k;
|
|
805
|
+
q[1] = (r12 + r21) / k;
|
|
806
|
+
q[2] = k / 4;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/** `out = a · b` for 3×4 affine matrices (implicit last row `0 0 0 1`). */
|
|
810
|
+
function multiply3x4(out, a, b) {
|
|
811
|
+
for (let r = 0; r < 3; r++) {
|
|
812
|
+
const a0 = a[r * 4];
|
|
813
|
+
const a1 = a[r * 4 + 1];
|
|
814
|
+
const a2 = a[r * 4 + 2];
|
|
815
|
+
const a3 = a[r * 4 + 3];
|
|
816
|
+
out[r * 4] = a0 * b[0] + a1 * b[4] + a2 * b[8];
|
|
817
|
+
out[r * 4 + 1] = a0 * b[1] + a1 * b[5] + a2 * b[9];
|
|
818
|
+
out[r * 4 + 2] = a0 * b[2] + a1 * b[6] + a2 * b[10];
|
|
819
|
+
out[r * 4 + 3] = a0 * b[3] + a1 * b[7] + a2 * b[11] + a3;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function sceneBounds(nodes, meshes, sceneNodes) {
|
|
823
|
+
const min = [Infinity, Infinity, Infinity];
|
|
824
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
825
|
+
let any = false;
|
|
826
|
+
for (const ni of sceneNodes) {
|
|
827
|
+
const n = nodes[ni];
|
|
828
|
+
if (n.mesh === null)
|
|
829
|
+
continue;
|
|
830
|
+
const m = n.world;
|
|
831
|
+
for (const p of meshes[n.mesh].primitives) {
|
|
832
|
+
const b = p.bounds;
|
|
833
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
834
|
+
const x = corner & 1 ? b.max[0] : b.min[0];
|
|
835
|
+
const y = corner & 2 ? b.max[1] : b.min[1];
|
|
836
|
+
const z = corner & 4 ? b.max[2] : b.min[2];
|
|
837
|
+
for (let r = 0; r < 3; r++) {
|
|
838
|
+
const v = m[r * 4] * x + m[r * 4 + 1] * y + m[r * 4 + 2] * z + m[r * 4 + 3];
|
|
839
|
+
if (v < min[r])
|
|
840
|
+
min[r] = v;
|
|
841
|
+
if (v > max[r])
|
|
842
|
+
max[r] = v;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
any = true;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (!any)
|
|
849
|
+
return { min: [0, 0, 0], max: [0, 0, 0], center: [0, 0, 0], radius: 0 };
|
|
850
|
+
return boundsOf(min, max);
|
|
851
|
+
}
|
|
852
|
+
function boundsOf(min, max) {
|
|
853
|
+
const center = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
|
|
854
|
+
const radius = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2;
|
|
855
|
+
return { min, max, center, radius };
|
|
856
|
+
}
|
|
857
|
+
// ---------------------------------------------------------------- primitives and accessors
|
|
858
|
+
function parseMesh(ctx, m, mi) {
|
|
859
|
+
const where = `mesh ${mi}`;
|
|
860
|
+
const primitives = m.primitives.map((p, pi) => parsePrimitive(ctx, p, `${where} primitive ${pi}`));
|
|
861
|
+
const targetCount = primitives[0]?.targets.length ?? 0;
|
|
862
|
+
for (let pi = 1; pi < primitives.length; pi++) {
|
|
863
|
+
const n = primitives[pi].targets.length;
|
|
864
|
+
if (n !== targetCount) {
|
|
865
|
+
throw new GltfError(`${where}: primitive ${pi} has ${n} morph targets, primitive 0 has ${targetCount}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
let weights = null;
|
|
869
|
+
if (m.weights !== undefined) {
|
|
870
|
+
if (m.weights.length !== targetCount) {
|
|
871
|
+
throw new GltfError(`${where}: ${m.weights.length} default weights for ${targetCount} morph targets`);
|
|
872
|
+
}
|
|
873
|
+
weights = new Float32Array(m.weights);
|
|
874
|
+
}
|
|
875
|
+
return { name: m.name ?? null, primitives, weights };
|
|
876
|
+
}
|
|
877
|
+
/** The target attributes this parser reads; others are listed in `ignoredTargetAttributes`. */
|
|
878
|
+
const MORPH_ATTRIBUTES = new Set(['POSITION', 'NORMAL']);
|
|
879
|
+
/**
|
|
880
|
+
* `primitive.targets`: `POSITION` and `NORMAL` deltas per target, each `vertexCount` vec3s.
|
|
881
|
+
* Target accessors may be sparse (the usual encoding for deltas): the dense data (or zeros,
|
|
882
|
+
* without a bufferView) is overlaid with `sparse.count` values at `sparse.indices`.
|
|
883
|
+
*/
|
|
884
|
+
function readTargets(ctx, targets, vertexCount, where, ignored) {
|
|
885
|
+
if (!targets)
|
|
886
|
+
return [];
|
|
887
|
+
return targets.map((t, ti) => {
|
|
888
|
+
const here = `${where} target ${ti}`;
|
|
889
|
+
for (const name of Object.keys(t)) {
|
|
890
|
+
if (!MORPH_ATTRIBUTES.has(name) && !ignored.includes(name))
|
|
891
|
+
ignored.push(name);
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
position: readTargetDeltas(ctx, t.POSITION, vertexCount, `${here} POSITION`),
|
|
895
|
+
normal: readTargetDeltas(ctx, t.NORMAL, vertexCount, `${here} NORMAL`),
|
|
896
|
+
};
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function readTargetDeltas(ctx, index, vertexCount, where) {
|
|
900
|
+
if (index === undefined)
|
|
901
|
+
return null;
|
|
902
|
+
const acc = accessor(ctx, index, where, true);
|
|
903
|
+
if (acc.type !== 'VEC3')
|
|
904
|
+
throw new GltfError(`${where}: ${acc.type}, expected VEC3`);
|
|
905
|
+
if (acc.count !== vertexCount) {
|
|
906
|
+
throw new GltfError(`${where}: ${acc.count} values for ${vertexCount} vertices`);
|
|
907
|
+
}
|
|
908
|
+
return readFloats(ctx, acc, 3, where);
|
|
909
|
+
}
|
|
910
|
+
function parsePrimitive(ctx, p, where) {
|
|
911
|
+
const mode = p.mode ?? MODE_TRIANGLES;
|
|
912
|
+
if (mode !== MODE_TRIANGLES) {
|
|
913
|
+
throw new GltfError(`${where}: mode ${mode} not supported (only 4, triangles)`);
|
|
914
|
+
}
|
|
915
|
+
const posIndex = p.attributes.POSITION;
|
|
916
|
+
if (posIndex === undefined)
|
|
917
|
+
throw new GltfError(`${where}: no POSITION attribute`);
|
|
918
|
+
const posAcc = accessor(ctx, posIndex, where);
|
|
919
|
+
if (posAcc.type !== 'VEC3')
|
|
920
|
+
throw new GltfError(`${where}: POSITION is ${posAcc.type}, expected VEC3`);
|
|
921
|
+
const vertexCount = posAcc.count;
|
|
922
|
+
let position = readFloats(ctx, posAcc, 3, where);
|
|
923
|
+
let indices;
|
|
924
|
+
if (p.indices !== undefined) {
|
|
925
|
+
const acc = accessor(ctx, p.indices, where);
|
|
926
|
+
if (acc.type !== 'SCALAR')
|
|
927
|
+
throw new GltfError(`${where}: indices are ${acc.type}, expected SCALAR`);
|
|
928
|
+
indices = readIndices(ctx, acc, where);
|
|
929
|
+
for (let i = 0; i < indices.length; i++) {
|
|
930
|
+
if (indices[i] >= vertexCount) {
|
|
931
|
+
throw new GltfError(`${where}: index ${indices[i]} out of range (${vertexCount} vertices)`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
indices = vertexCount > 0xffff ? new Uint32Array(vertexCount) : new Uint16Array(vertexCount);
|
|
937
|
+
for (let i = 0; i < vertexCount; i++)
|
|
938
|
+
indices[i] = i;
|
|
939
|
+
}
|
|
940
|
+
if (indices.length % 3 !== 0) {
|
|
941
|
+
throw new GltfError(`${where}: ${indices.length} indices is not a whole number of triangles`);
|
|
942
|
+
}
|
|
943
|
+
let uv0 = optionalFloats(ctx, p.attributes.TEXCOORD_0, vertexCount, 2, `${where} TEXCOORD_0`);
|
|
944
|
+
let uv1 = optionalFloats(ctx, p.attributes.TEXCOORD_1, vertexCount, 2, `${where} TEXCOORD_1`);
|
|
945
|
+
let color0 = optionalFloats(ctx, p.attributes.COLOR_0, vertexCount, 4, `${where} COLOR_0`, 1);
|
|
946
|
+
if (!uv0)
|
|
947
|
+
uv0 = new Float32Array(vertexCount * 2);
|
|
948
|
+
if (!color0)
|
|
949
|
+
color0 = new Float32Array(vertexCount * 4).fill(1);
|
|
950
|
+
let tangent = readTangents(ctx, p.attributes.TANGENT, vertexCount, `${where} TANGENT`);
|
|
951
|
+
let joints = readJoints(ctx, p.attributes.JOINTS_0, vertexCount, `${where} JOINTS_0`);
|
|
952
|
+
let weights = optionalFloats(ctx, p.attributes.WEIGHTS_0, vertexCount, 4, `${where} WEIGHTS_0`);
|
|
953
|
+
if (joints && !weights)
|
|
954
|
+
throw new GltfError(`${where}: JOINTS_0 without WEIGHTS_0`);
|
|
955
|
+
if (weights && !joints)
|
|
956
|
+
throw new GltfError(`${where}: WEIGHTS_0 without JOINTS_0`);
|
|
957
|
+
const ignoredTargetAttributes = [];
|
|
958
|
+
let targets = readTargets(ctx, p.targets, vertexCount, where, ignoredTargetAttributes);
|
|
959
|
+
let normal = optionalFloats(ctx, p.attributes.NORMAL, vertexCount, 3, `${where} NORMAL`);
|
|
960
|
+
// KHR_mesh_quantization: a normalised byte or short normal is only near unit length.
|
|
961
|
+
if (normal && ctx.json.accessors?.[p.attributes.NORMAL]?.componentType !== 5126) {
|
|
962
|
+
renormalize(normal, 3);
|
|
963
|
+
}
|
|
964
|
+
const hadNormals = normal !== null;
|
|
965
|
+
let count = vertexCount;
|
|
966
|
+
if (!normal) {
|
|
967
|
+
// Flat normals need one normal per face: unweld so each triangle owns its vertices.
|
|
968
|
+
const n = indices.length;
|
|
969
|
+
const pos = new Float32Array(n * 3);
|
|
970
|
+
const uv = new Float32Array(n * 2);
|
|
971
|
+
const col = new Float32Array(n * 4);
|
|
972
|
+
const jnt = joints
|
|
973
|
+
? joints instanceof Uint8Array
|
|
974
|
+
? new Uint8Array(n * 4)
|
|
975
|
+
: new Uint16Array(n * 4)
|
|
976
|
+
: null;
|
|
977
|
+
const wgt = weights ? new Float32Array(n * 4) : null;
|
|
978
|
+
const uvb = uv1 ? new Float32Array(n * 2) : null;
|
|
979
|
+
const tan = tangent ? new Float32Array(n * 4) : null;
|
|
980
|
+
normal = new Float32Array(n * 3);
|
|
981
|
+
for (let i = 0; i < n; i++) {
|
|
982
|
+
const src = indices[i];
|
|
983
|
+
pos.set(position.subarray(src * 3, src * 3 + 3), i * 3);
|
|
984
|
+
uv.set(uv0.subarray(src * 2, src * 2 + 2), i * 2);
|
|
985
|
+
col.set(color0.subarray(src * 4, src * 4 + 4), i * 4);
|
|
986
|
+
if (jnt && joints)
|
|
987
|
+
jnt.set(joints.subarray(src * 4, src * 4 + 4), i * 4);
|
|
988
|
+
if (wgt && weights)
|
|
989
|
+
wgt.set(weights.subarray(src * 4, src * 4 + 4), i * 4);
|
|
990
|
+
if (uvb && uv1)
|
|
991
|
+
uvb.set(uv1.subarray(src * 2, src * 2 + 2), i * 2);
|
|
992
|
+
if (tan && tangent)
|
|
993
|
+
tan.set(tangent.subarray(src * 4, src * 4 + 4), i * 4);
|
|
994
|
+
}
|
|
995
|
+
joints = jnt;
|
|
996
|
+
weights = wgt;
|
|
997
|
+
uv1 = uvb;
|
|
998
|
+
tangent = tan;
|
|
999
|
+
targets = targets.map((t) => ({
|
|
1000
|
+
position: t.position ? unweld3(t.position, indices) : null,
|
|
1001
|
+
normal: t.normal ? unweld3(t.normal, indices) : null,
|
|
1002
|
+
}));
|
|
1003
|
+
for (let t = 0; t < n; t += 3) {
|
|
1004
|
+
const ax = pos[t * 3];
|
|
1005
|
+
const ay = pos[t * 3 + 1];
|
|
1006
|
+
const az = pos[t * 3 + 2];
|
|
1007
|
+
const ux = pos[t * 3 + 3] - ax;
|
|
1008
|
+
const uy = pos[t * 3 + 4] - ay;
|
|
1009
|
+
const uz = pos[t * 3 + 5] - az;
|
|
1010
|
+
const vx = pos[t * 3 + 6] - ax;
|
|
1011
|
+
const vy = pos[t * 3 + 7] - ay;
|
|
1012
|
+
const vz = pos[t * 3 + 8] - az;
|
|
1013
|
+
let nx = uy * vz - uz * vy;
|
|
1014
|
+
let ny = uz * vx - ux * vz;
|
|
1015
|
+
let nz = ux * vy - uy * vx;
|
|
1016
|
+
const len = Math.hypot(nx, ny, nz);
|
|
1017
|
+
if (len > 0) {
|
|
1018
|
+
nx /= len;
|
|
1019
|
+
ny /= len;
|
|
1020
|
+
nz /= len;
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
ny = 1;
|
|
1024
|
+
}
|
|
1025
|
+
for (let k = 0; k < 3; k++) {
|
|
1026
|
+
normal[(t + k) * 3] = nx;
|
|
1027
|
+
normal[(t + k) * 3 + 1] = ny;
|
|
1028
|
+
normal[(t + k) * 3 + 2] = nz;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
position = pos;
|
|
1032
|
+
uv0 = uv;
|
|
1033
|
+
color0 = col;
|
|
1034
|
+
count = n;
|
|
1035
|
+
indices = n > 0xffff ? new Uint32Array(n) : new Uint16Array(n);
|
|
1036
|
+
for (let i = 0; i < n; i++)
|
|
1037
|
+
indices[i] = i;
|
|
1038
|
+
}
|
|
1039
|
+
let bounds;
|
|
1040
|
+
// A normalized (KHR_mesh_quantization) accessor's min / max are raw integers: measure instead.
|
|
1041
|
+
if (!posAcc.normalized &&
|
|
1042
|
+
posAcc.min &&
|
|
1043
|
+
posAcc.max &&
|
|
1044
|
+
posAcc.min.length >= 3 &&
|
|
1045
|
+
posAcc.max.length >= 3) {
|
|
1046
|
+
bounds = boundsOf([posAcc.min[0], posAcc.min[1], posAcc.min[2]], [posAcc.max[0], posAcc.max[1], posAcc.max[2]]);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
const min = [Infinity, Infinity, Infinity];
|
|
1050
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
1051
|
+
for (let i = 0; i < position.length; i += 3) {
|
|
1052
|
+
for (let k = 0; k < 3; k++) {
|
|
1053
|
+
const v = position[i + k];
|
|
1054
|
+
if (v < min[k])
|
|
1055
|
+
min[k] = v;
|
|
1056
|
+
if (v > max[k])
|
|
1057
|
+
max[k] = v;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
bounds = position.length ? boundsOf(min, max) : boundsOf([0, 0, 0], [0, 0, 0]);
|
|
1061
|
+
}
|
|
1062
|
+
if (p.material !== undefined && !(ctx.json.materials ?? [])[p.material]) {
|
|
1063
|
+
throw new GltfError(`${where}: material ${p.material} does not exist`);
|
|
1064
|
+
}
|
|
1065
|
+
const variants = [];
|
|
1066
|
+
for (const m of p.extensions?.KHR_materials_variants?.mappings ?? []) {
|
|
1067
|
+
if (!(ctx.json.materials ?? [])[m.material]) {
|
|
1068
|
+
throw new GltfError(`${where}: variant mapping names material ${m.material}, which does not exist`);
|
|
1069
|
+
}
|
|
1070
|
+
variants.push({ material: m.material, variants: [...(m.variants ?? [])] });
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
vertexCount: count,
|
|
1074
|
+
position,
|
|
1075
|
+
normal,
|
|
1076
|
+
uv0,
|
|
1077
|
+
uv1,
|
|
1078
|
+
tangent,
|
|
1079
|
+
color0,
|
|
1080
|
+
indices,
|
|
1081
|
+
material: p.material ?? null,
|
|
1082
|
+
bounds,
|
|
1083
|
+
hadNormals,
|
|
1084
|
+
joints,
|
|
1085
|
+
weights,
|
|
1086
|
+
targets,
|
|
1087
|
+
ignoredTargetAttributes,
|
|
1088
|
+
variants,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
/** `TANGENT`: VEC4 floats, w the handedness (anything but -1 reads as +1). */
|
|
1092
|
+
function readTangents(ctx, index, vertexCount, where) {
|
|
1093
|
+
if (index === undefined)
|
|
1094
|
+
return null;
|
|
1095
|
+
const acc = accessor(ctx, index, where);
|
|
1096
|
+
if (acc.type !== 'VEC4')
|
|
1097
|
+
throw new GltfError(`${where}: ${acc.type}, expected VEC4`);
|
|
1098
|
+
if (acc.count !== vertexCount) {
|
|
1099
|
+
throw new GltfError(`${where}: ${acc.count} values for ${vertexCount} vertices`);
|
|
1100
|
+
}
|
|
1101
|
+
const t = readFloats(ctx, acc, 4, where);
|
|
1102
|
+
for (let i = 3; i < t.length; i += 4)
|
|
1103
|
+
t[i] = t[i] < 0 ? -1 : 1;
|
|
1104
|
+
if (acc.componentType !== 5126)
|
|
1105
|
+
renormalize(t, 4);
|
|
1106
|
+
return t;
|
|
1107
|
+
}
|
|
1108
|
+
/** Scale the xyz of each `stride`-float element to unit length (quantized normals, tangents). */
|
|
1109
|
+
function renormalize(v, stride) {
|
|
1110
|
+
for (let i = 0; i + 2 < v.length; i += stride) {
|
|
1111
|
+
const l = Math.hypot(v[i], v[i + 1], v[i + 2]);
|
|
1112
|
+
if (l > 0) {
|
|
1113
|
+
v[i] = v[i] / l;
|
|
1114
|
+
v[i + 1] = v[i + 1] / l;
|
|
1115
|
+
v[i + 2] = v[i + 2] / l;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/** One vec3 per index, in index order (the unwelding that flat normals need). */
|
|
1120
|
+
function unweld3(src, indices) {
|
|
1121
|
+
const out = new Float32Array(indices.length * 3);
|
|
1122
|
+
for (let i = 0; i < indices.length; i++) {
|
|
1123
|
+
const s = indices[i] * 3;
|
|
1124
|
+
out[i * 3] = src[s];
|
|
1125
|
+
out[i * 3 + 1] = src[s + 1];
|
|
1126
|
+
out[i * 3 + 2] = src[s + 2];
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
1130
|
+
/** `JOINTS_0`: any unsigned integer type, 4 per vertex → bytes when every index fits, else shorts. */
|
|
1131
|
+
function readJoints(ctx, index, vertexCount, where) {
|
|
1132
|
+
if (index === undefined)
|
|
1133
|
+
return null;
|
|
1134
|
+
const acc = accessor(ctx, index, where);
|
|
1135
|
+
if (acc.type !== 'VEC4')
|
|
1136
|
+
throw new GltfError(`${where}: ${acc.type}, expected VEC4`);
|
|
1137
|
+
if (acc.count !== vertexCount) {
|
|
1138
|
+
throw new GltfError(`${where}: ${acc.count} values for ${vertexCount} vertices`);
|
|
1139
|
+
}
|
|
1140
|
+
const t = acc.componentType;
|
|
1141
|
+
if (t !== 5121 && t !== 5123 && t !== 5125) {
|
|
1142
|
+
throw new GltfError(`${where}: componentType ${t} must be unsigned byte, short or int`);
|
|
1143
|
+
}
|
|
1144
|
+
const wide = new Uint32Array(vertexCount * 4);
|
|
1145
|
+
const r = readerFor(ctx, acc, where);
|
|
1146
|
+
let max = 0;
|
|
1147
|
+
if (r) {
|
|
1148
|
+
const cb = COMPONENT_BYTES[t];
|
|
1149
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
1150
|
+
const at = r.base + i * r.stride;
|
|
1151
|
+
for (let k = 0; k < 4; k++) {
|
|
1152
|
+
const v = readComponent(r.view, at + k * cb, t, false);
|
|
1153
|
+
wide[i * 4 + k] = v;
|
|
1154
|
+
if (v > max)
|
|
1155
|
+
max = v;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (max > 0xffff)
|
|
1160
|
+
throw new GltfError(`${where}: joint index ${max} exceeds 65535`);
|
|
1161
|
+
return max <= 0xff ? Uint8Array.from(wide) : Uint16Array.from(wide);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* The accessor at `index`, checked. Sparse accessors are honoured only where `allowSparse` says
|
|
1165
|
+
* so (morph target attributes, read through `readFloats`); elsewhere they throw.
|
|
1166
|
+
*/
|
|
1167
|
+
function accessor(ctx, index, where, allowSparse = false) {
|
|
1168
|
+
const acc = ctx.json.accessors?.[index];
|
|
1169
|
+
if (!acc)
|
|
1170
|
+
throw new GltfError(`${where}: accessor ${index} does not exist`);
|
|
1171
|
+
if (acc.sparse && !allowSparse) {
|
|
1172
|
+
throw new GltfError(`${where}: accessor ${index} is sparse (supported for morph target attributes only)`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!(acc.componentType in COMPONENT_BYTES)) {
|
|
1175
|
+
throw new GltfError(`${where}: accessor ${index} componentType ${acc.componentType} unknown`);
|
|
1176
|
+
}
|
|
1177
|
+
if (!(acc.type in COMPONENTS))
|
|
1178
|
+
throw new GltfError(`${where}: accessor ${index} type ${acc.type} unknown`);
|
|
1179
|
+
return acc;
|
|
1180
|
+
}
|
|
1181
|
+
/** An attribute that may be absent; `fill` pads a narrower accessor (a vec3 colour's alpha). */
|
|
1182
|
+
function optionalFloats(ctx, index, vertexCount, components, where, fill = 0) {
|
|
1183
|
+
if (index === undefined)
|
|
1184
|
+
return null;
|
|
1185
|
+
const acc = accessor(ctx, index, where);
|
|
1186
|
+
if (acc.count !== vertexCount) {
|
|
1187
|
+
throw new GltfError(`${where}: ${acc.count} values for ${vertexCount} vertices`);
|
|
1188
|
+
}
|
|
1189
|
+
const have = COMPONENTS[acc.type];
|
|
1190
|
+
if (have > components)
|
|
1191
|
+
throw new GltfError(`${where}: ${acc.type} has more than ${components} components`);
|
|
1192
|
+
const data = readFloats(ctx, acc, have, where);
|
|
1193
|
+
if (have === components)
|
|
1194
|
+
return data;
|
|
1195
|
+
const out = new Float32Array(vertexCount * components).fill(fill);
|
|
1196
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
1197
|
+
for (let k = 0; k < have; k++)
|
|
1198
|
+
out[i * components + k] = data[i * have + k];
|
|
1199
|
+
}
|
|
1200
|
+
return out;
|
|
1201
|
+
}
|
|
1202
|
+
function readerFor(ctx, acc, where) {
|
|
1203
|
+
const components = COMPONENTS[acc.type];
|
|
1204
|
+
const componentBytes = COMPONENT_BYTES[acc.componentType];
|
|
1205
|
+
const elementBytes = components * componentBytes;
|
|
1206
|
+
if (acc.bufferView === undefined)
|
|
1207
|
+
return null;
|
|
1208
|
+
const bv = ctx.json.bufferViews?.[acc.bufferView];
|
|
1209
|
+
if (!bv)
|
|
1210
|
+
throw new GltfError(`${where}: bufferView ${acc.bufferView} does not exist`);
|
|
1211
|
+
const buffer = ctx.buffers[bv.buffer];
|
|
1212
|
+
if (!buffer)
|
|
1213
|
+
throw new GltfError(`${where}: buffer ${bv.buffer} does not exist`);
|
|
1214
|
+
const stride = bv.byteStride ?? elementBytes;
|
|
1215
|
+
if (stride < elementBytes)
|
|
1216
|
+
throw new GltfError(`${where}: byteStride ${stride} < element size ${elementBytes}`);
|
|
1217
|
+
const base = (bv.byteOffset ?? 0) + (acc.byteOffset ?? 0);
|
|
1218
|
+
const needed = acc.count === 0 ? 0 : (acc.count - 1) * stride + elementBytes;
|
|
1219
|
+
if ((acc.byteOffset ?? 0) + needed > bv.byteLength) {
|
|
1220
|
+
throw new GltfError(`${where}: accessor runs past its bufferView`);
|
|
1221
|
+
}
|
|
1222
|
+
if ((bv.byteOffset ?? 0) + bv.byteLength > buffer.byteLength) {
|
|
1223
|
+
throw new GltfError(`${where}: bufferView ${acc.bufferView} runs past buffer ${bv.buffer}`);
|
|
1224
|
+
}
|
|
1225
|
+
return { view: new DataView(buffer), base, stride, elementBytes };
|
|
1226
|
+
}
|
|
1227
|
+
function readComponent(view, at, type, normalized) {
|
|
1228
|
+
switch (type) {
|
|
1229
|
+
case 5126:
|
|
1230
|
+
return view.getFloat32(at, true);
|
|
1231
|
+
case 5120: {
|
|
1232
|
+
const v = view.getInt8(at);
|
|
1233
|
+
return normalized ? Math.max(v / 127, -1) : v;
|
|
1234
|
+
}
|
|
1235
|
+
case 5121: {
|
|
1236
|
+
const v = view.getUint8(at);
|
|
1237
|
+
return normalized ? v / 255 : v;
|
|
1238
|
+
}
|
|
1239
|
+
case 5122: {
|
|
1240
|
+
const v = view.getInt16(at, true);
|
|
1241
|
+
return normalized ? Math.max(v / 32767, -1) : v;
|
|
1242
|
+
}
|
|
1243
|
+
case 5123: {
|
|
1244
|
+
const v = view.getUint16(at, true);
|
|
1245
|
+
return normalized ? v / 65535 : v;
|
|
1246
|
+
}
|
|
1247
|
+
case 5125:
|
|
1248
|
+
return view.getUint32(at, true);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/** Accessor → floats, `components` per element (an accessor without a bufferView reads zeros). */
|
|
1252
|
+
function readFloats(ctx, acc, components, where) {
|
|
1253
|
+
const out = new Float32Array(acc.count * components);
|
|
1254
|
+
const r = readerFor(ctx, acc, where);
|
|
1255
|
+
const cb = COMPONENT_BYTES[acc.componentType];
|
|
1256
|
+
const normalized = acc.normalized ?? false;
|
|
1257
|
+
if (r) {
|
|
1258
|
+
for (let i = 0; i < acc.count; i++) {
|
|
1259
|
+
const at = r.base + i * r.stride;
|
|
1260
|
+
for (let k = 0; k < components; k++) {
|
|
1261
|
+
out[i * components + k] = readComponent(r.view, at + k * cb, acc.componentType, normalized);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (acc.sparse)
|
|
1266
|
+
applySparse(ctx, acc, acc.sparse, components, out, where);
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
/** Overlay a sparse accessor's `count` values at its `indices` onto `out` (dense data or zeros). */
|
|
1270
|
+
function applySparse(ctx, acc, sp, components, out, where) {
|
|
1271
|
+
if (!Number.isInteger(sp.count) || sp.count < 0 || sp.count > acc.count) {
|
|
1272
|
+
throw new GltfError(`${where}: sparse count ${sp.count} for ${acc.count} elements`);
|
|
1273
|
+
}
|
|
1274
|
+
if (!sp.indices || !sp.values)
|
|
1275
|
+
throw new GltfError(`${where}: sparse without indices/values`);
|
|
1276
|
+
const it = sp.indices.componentType;
|
|
1277
|
+
if (it !== 5121 && it !== 5123 && it !== 5125) {
|
|
1278
|
+
throw new GltfError(`${where}: sparse index componentType ${it} must be unsigned`);
|
|
1279
|
+
}
|
|
1280
|
+
const ib = COMPONENT_BYTES[it];
|
|
1281
|
+
const idx = sparseView(ctx, sp.indices.bufferView, sp.indices.byteOffset, sp.count * ib, where);
|
|
1282
|
+
const cb = COMPONENT_BYTES[acc.componentType];
|
|
1283
|
+
const bytes = sp.count * components * cb;
|
|
1284
|
+
const vals = sparseView(ctx, sp.values.bufferView, sp.values.byteOffset, bytes, where);
|
|
1285
|
+
const normalized = acc.normalized ?? false;
|
|
1286
|
+
for (let i = 0; i < sp.count; i++) {
|
|
1287
|
+
const e = readComponent(idx.view, idx.base + i * ib, it, false);
|
|
1288
|
+
if (e >= acc.count)
|
|
1289
|
+
throw new GltfError(`${where}: sparse index ${e} out of range`);
|
|
1290
|
+
for (let k = 0; k < components; k++) {
|
|
1291
|
+
const at = vals.base + (i * components + k) * cb;
|
|
1292
|
+
out[e * components + k] = readComponent(vals.view, at, acc.componentType, normalized);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/** A tightly packed sparse `indices` or `values` block: its view and base byte offset. */
|
|
1297
|
+
function sparseView(ctx, bufferView, byteOffset, bytes, where) {
|
|
1298
|
+
const bv = ctx.json.bufferViews?.[bufferView];
|
|
1299
|
+
if (!bv)
|
|
1300
|
+
throw new GltfError(`${where}: bufferView ${bufferView} does not exist`);
|
|
1301
|
+
const buffer = ctx.buffers[bv.buffer];
|
|
1302
|
+
if (!buffer)
|
|
1303
|
+
throw new GltfError(`${where}: buffer ${bv.buffer} does not exist`);
|
|
1304
|
+
const off = byteOffset ?? 0;
|
|
1305
|
+
if (off + bytes > bv.byteLength) {
|
|
1306
|
+
throw new GltfError(`${where}: sparse data runs past its bufferView`);
|
|
1307
|
+
}
|
|
1308
|
+
if ((bv.byteOffset ?? 0) + bv.byteLength > buffer.byteLength) {
|
|
1309
|
+
throw new GltfError(`${where}: bufferView ${bufferView} runs past buffer ${bv.buffer}`);
|
|
1310
|
+
}
|
|
1311
|
+
return { view: new DataView(buffer), base: (bv.byteOffset ?? 0) + off };
|
|
1312
|
+
}
|
|
1313
|
+
function readIndices(ctx, acc, where) {
|
|
1314
|
+
const t = acc.componentType;
|
|
1315
|
+
if (t !== 5121 && t !== 5123 && t !== 5125) {
|
|
1316
|
+
throw new GltfError(`${where}: index componentType ${t} must be unsigned byte, short or int`);
|
|
1317
|
+
}
|
|
1318
|
+
const out = t === 5125 ? new Uint32Array(acc.count) : new Uint16Array(acc.count);
|
|
1319
|
+
const r = readerFor(ctx, acc, where);
|
|
1320
|
+
if (!r)
|
|
1321
|
+
return out;
|
|
1322
|
+
for (let i = 0; i < acc.count; i++) {
|
|
1323
|
+
out[i] = readComponent(r.view, r.base + i * r.stride, t, false);
|
|
1324
|
+
}
|
|
1325
|
+
return out;
|
|
1326
|
+
}
|
|
1327
|
+
function bufferViewBytes(ctx, index, where) {
|
|
1328
|
+
const bv = ctx.json.bufferViews?.[index];
|
|
1329
|
+
if (!bv)
|
|
1330
|
+
throw new GltfError(`${where}: bufferView ${index} does not exist`);
|
|
1331
|
+
const buffer = ctx.buffers[bv.buffer];
|
|
1332
|
+
if (!buffer)
|
|
1333
|
+
throw new GltfError(`${where}: buffer ${bv.buffer} does not exist`);
|
|
1334
|
+
const offset = bv.byteOffset ?? 0;
|
|
1335
|
+
if (offset + bv.byteLength > buffer.byteLength) {
|
|
1336
|
+
throw new GltfError(`${where}: bufferView ${index} runs past buffer ${bv.buffer}`);
|
|
1337
|
+
}
|
|
1338
|
+
return new Uint8Array(buffer, offset, bv.byteLength);
|
|
1339
|
+
}
|
|
1340
|
+
// ---------------------------------------------------------------- URIs
|
|
1341
|
+
function isDataUri(uri) {
|
|
1342
|
+
return uri.startsWith('data:');
|
|
1343
|
+
}
|
|
1344
|
+
/** Decode a `data:[<mime>][;base64],<payload>` URI. */
|
|
1345
|
+
export function decodeDataUri(uri) {
|
|
1346
|
+
const comma = uri.indexOf(',');
|
|
1347
|
+
if (!uri.startsWith('data:') || comma < 0)
|
|
1348
|
+
throw new GltfError(`malformed data URI`);
|
|
1349
|
+
const meta = uri.slice(5, comma);
|
|
1350
|
+
const payload = uri.slice(comma + 1);
|
|
1351
|
+
const parts = meta.split(';');
|
|
1352
|
+
const mimeType = parts[0] || null;
|
|
1353
|
+
const isBase64 = parts.includes('base64');
|
|
1354
|
+
if (isBase64) {
|
|
1355
|
+
const bin = atob(payload);
|
|
1356
|
+
const bytes = new Uint8Array(bin.length);
|
|
1357
|
+
for (let i = 0; i < bin.length; i++)
|
|
1358
|
+
bytes[i] = bin.charCodeAt(i);
|
|
1359
|
+
return { bytes, mimeType };
|
|
1360
|
+
}
|
|
1361
|
+
const text = decodeURIComponent(payload);
|
|
1362
|
+
const bytes = new Uint8Array(text.length);
|
|
1363
|
+
for (let i = 0; i < text.length; i++)
|
|
1364
|
+
bytes[i] = text.charCodeAt(i) & 0xff;
|
|
1365
|
+
return { bytes, mimeType };
|
|
1366
|
+
}
|
|
1367
|
+
function mimeFromUri(uri) {
|
|
1368
|
+
const path = uri.split(/[?#]/)[0].toLowerCase();
|
|
1369
|
+
if (path.endsWith('.png'))
|
|
1370
|
+
return 'image/png';
|
|
1371
|
+
if (path.endsWith('.jpg') || path.endsWith('.jpeg'))
|
|
1372
|
+
return 'image/jpeg';
|
|
1373
|
+
if (path.endsWith('.webp'))
|
|
1374
|
+
return 'image/webp';
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|