@downdraft/engine 0.0.0 → 0.1.1

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.
Files changed (908) hide show
  1. package/README.md +33 -0
  2. package/app/src/index.ts +10 -0
  3. package/app/src/renderer/bootstrap.ts +271 -0
  4. package/app/src/renderer/downdraft-base.css +93 -0
  5. package/app/src/renderer/feature-log.ts +200 -0
  6. package/app/src/renderer/game-module.ts +923 -0
  7. package/app/src/renderer/hooks.ts +88 -0
  8. package/app/src/renderer/hot-reload.ts +178 -0
  9. package/app/src/renderer/import-cache.ts +88 -0
  10. package/app/src/renderer/index.ts +206 -0
  11. package/app/src/renderer/mcp-harness.ts +144 -0
  12. package/app/src/renderer/save-store-factory.ts +239 -0
  13. package/app/src/renderer/sim-bridge.ts +183 -0
  14. package/app/src/renderer/standard-automation-tools.ts +771 -0
  15. package/app/src/shared/feature-log.ts +71 -0
  16. package/app/src/shared/gpu-info.ts +82 -0
  17. package/app/src/shared/import-cache-store.ts +142 -0
  18. package/app/src/shared/index.ts +6 -0
  19. package/app/src/shared/mcp-discovery.ts +47 -0
  20. package/app/src/shared/mcp-proxy.ts +82 -0
  21. package/app/src/shared/types.ts +324 -0
  22. package/asset-bake/src/bake-audio.ts +119 -0
  23. package/asset-bake/src/bake-gltf.ts +172 -0
  24. package/asset-bake/src/bake.ts +78 -0
  25. package/asset-bake/src/cache.ts +188 -0
  26. package/asset-bake/src/config.ts +186 -0
  27. package/asset-bake/src/index.ts +36 -0
  28. package/core/src/animation/animation-event.ts +41 -0
  29. package/core/src/animation/bone-mask.ts +122 -0
  30. package/core/src/animation/clip.ts +252 -0
  31. package/core/src/animation/index.ts +22 -0
  32. package/core/src/animation/mixamo.ts +113 -0
  33. package/core/src/animation/morph-target.ts +69 -0
  34. package/core/src/animation/player.ts +697 -0
  35. package/core/src/animation/retarget.ts +158 -0
  36. package/core/src/animation/skeleton-animator.ts +1399 -0
  37. package/core/src/animation/skeleton.ts +214 -0
  38. package/core/src/animation/state-machine.ts +280 -0
  39. package/core/src/assets/blob-store.ts +66 -0
  40. package/core/src/assets/cache.ts +186 -0
  41. package/core/src/assets/cubemap-converter.ts +155 -0
  42. package/core/src/assets/environment-manager.ts +121 -0
  43. package/core/src/assets/import-cache.ts +65 -0
  44. package/core/src/assets/import-settings.ts +72 -0
  45. package/core/src/assets/importer.ts +108 -0
  46. package/core/src/assets/index.ts +74 -0
  47. package/core/src/assets/irradiance-generator.ts +198 -0
  48. package/core/src/assets/loader-audio.ts +86 -0
  49. package/core/src/assets/loader-cubemap.ts +211 -0
  50. package/core/src/assets/loader-dds.ts +236 -0
  51. package/core/src/assets/loader-hdr.ts +474 -0
  52. package/core/src/assets/loader-mesh.ts +143 -0
  53. package/core/src/assets/loader-shader.ts +73 -0
  54. package/core/src/assets/loader-texture.ts +349 -0
  55. package/core/src/assets/lod.ts +198 -0
  56. package/core/src/assets/manager.ts +480 -0
  57. package/core/src/assets/manifest.ts +81 -0
  58. package/core/src/assets/material-bridge.ts +186 -0
  59. package/core/src/assets/model-importer.ts +72 -0
  60. package/core/src/assets/model-normalizer.ts +248 -0
  61. package/core/src/assets/model-to-gpu.ts +70 -0
  62. package/core/src/assets/model-to-mesh.ts +237 -0
  63. package/core/src/assets/prefilter-generator.ts +250 -0
  64. package/core/src/audio/engine.ts +177 -0
  65. package/core/src/audio/interface.ts +130 -0
  66. package/core/src/audio/listener.ts +32 -0
  67. package/core/src/audio/mixer.ts +141 -0
  68. package/core/src/audio/registry.ts +52 -0
  69. package/core/src/audio/sab.ts +87 -0
  70. package/core/src/audio/source.ts +89 -0
  71. package/core/src/change-detection/atomics.ts +1 -0
  72. package/core/src/change-detection/tracker.ts +47 -0
  73. package/core/src/debug-draw/lines.ts +64 -0
  74. package/core/src/debug-draw/points.ts +17 -0
  75. package/core/src/debug-draw/queue.ts +124 -0
  76. package/core/src/debug-draw/text.ts +25 -0
  77. package/core/src/ecs/archetype.ts +323 -0
  78. package/core/src/ecs/component.ts +188 -0
  79. package/core/src/ecs/entity.ts +25 -0
  80. package/core/src/ecs/events.ts +93 -0
  81. package/core/src/ecs/hierarchy.ts +124 -0
  82. package/core/src/ecs/hmr-swap-registry.ts +22 -0
  83. package/core/src/ecs/index.ts +25 -0
  84. package/core/src/ecs/query.ts +141 -0
  85. package/core/src/ecs/resource.ts +8 -0
  86. package/core/src/ecs/schedule.ts +181 -0
  87. package/core/src/ecs/system-params.ts +99 -0
  88. package/core/src/ecs/system.ts +42 -0
  89. package/core/src/ecs/world.ts +348 -0
  90. package/core/src/index.ts +352 -0
  91. package/core/src/input/context.ts +73 -0
  92. package/core/src/input/dom-handler.ts +380 -0
  93. package/core/src/input/local-player-manager.ts +218 -0
  94. package/core/src/input/mapping.ts +40 -0
  95. package/core/src/input/multi-sab-bridge.ts +276 -0
  96. package/core/src/input/multi-state.ts +54 -0
  97. package/core/src/input/sab-bridge.ts +71 -0
  98. package/core/src/input/state.ts +132 -0
  99. package/core/src/input/ui-router.ts +87 -0
  100. package/core/src/library/host.ts +317 -0
  101. package/core/src/library/index.ts +16 -0
  102. package/core/src/library/library.ts +292 -0
  103. package/core/src/material/compiler.ts +80 -0
  104. package/core/src/material/graph-bridge.ts +172 -0
  105. package/core/src/material/index.ts +9 -0
  106. package/core/src/material/library.ts +442 -0
  107. package/core/src/material/material.ts +156 -0
  108. package/core/src/material/variants.ts +61 -0
  109. package/core/src/math/mat4.ts +202 -0
  110. package/core/src/math/perlin-noise-2d.ts +77 -0
  111. package/core/src/math/perlin-noise-3d.ts +110 -0
  112. package/core/src/math/quat.ts +33 -0
  113. package/core/src/math/rng.ts +29 -0
  114. package/core/src/math/trig.ts +47 -0
  115. package/core/src/math/value-noise.ts +67 -0
  116. package/core/src/mesh/builder.ts +177 -0
  117. package/core/src/mesh/csg.ts +312 -0
  118. package/core/src/mesh/greased-line.ts +145 -0
  119. package/core/src/mesh/mesh.ts +17 -0
  120. package/core/src/mesh/parametric.ts +501 -0
  121. package/core/src/mesh/skinning.ts +67 -0
  122. package/core/src/mesh/vertex-layout.ts +91 -0
  123. package/core/src/module/cross-thread.ts +163 -0
  124. package/core/src/module/diagnostics.ts +123 -0
  125. package/core/src/module/host.ts +451 -0
  126. package/core/src/module/index.ts +22 -0
  127. package/core/src/module/module.ts +109 -0
  128. package/core/src/module/registry.ts +94 -0
  129. package/core/src/module/renderer-module.ts +247 -0
  130. package/core/src/module/ts-loader.ts +26 -0
  131. package/core/src/particles/compute-pass.ts +378 -0
  132. package/core/src/particles/emitter.ts +156 -0
  133. package/core/src/particles/index.ts +11 -0
  134. package/core/src/particles/particle-data.ts +47 -0
  135. package/core/src/particles/render-pass.ts +365 -0
  136. package/core/src/particles/simulator.ts +264 -0
  137. package/core/src/particles/system.ts +193 -0
  138. package/core/src/physics/body.ts +59 -0
  139. package/core/src/physics/ccd-heuristic.ts +55 -0
  140. package/core/src/physics/character.ts +282 -0
  141. package/core/src/physics/collider.ts +55 -0
  142. package/core/src/physics/collision-system.ts +91 -0
  143. package/core/src/physics/events.ts +133 -0
  144. package/core/src/physics/index.ts +34 -0
  145. package/core/src/physics/interface.ts +473 -0
  146. package/core/src/physics/interpolation-buffer.ts +192 -0
  147. package/core/src/physics/lifecycle.ts +87 -0
  148. package/core/src/physics/load-shedder.ts +164 -0
  149. package/core/src/physics/physics-accumulator.ts +101 -0
  150. package/core/src/physics/physics-system.ts +240 -0
  151. package/core/src/physics/ragdoll-presets.ts +272 -0
  152. package/core/src/physics/ragdoll-system.ts +220 -0
  153. package/core/src/physics/ragdoll.ts +182 -0
  154. package/core/src/physics/raycast.ts +143 -0
  155. package/core/src/physics/realm-manager.ts +499 -0
  156. package/core/src/physics/realm-worker-pool.ts +191 -0
  157. package/core/src/physics/realm-worker.ts +102 -0
  158. package/core/src/physics/realm.ts +189 -0
  159. package/core/src/physics/registry.ts +52 -0
  160. package/core/src/physics/safety.ts +247 -0
  161. package/core/src/physics/snapshot-manager.ts +141 -0
  162. package/core/src/physics/worker-protocol.ts +30 -0
  163. package/core/src/platform/bun-preload.ts +169 -0
  164. package/core/src/platform/css-stub.ts +4 -0
  165. package/core/src/platform/glob-polyfill.ts +124 -0
  166. package/core/src/platform/hidpi.ts +57 -0
  167. package/core/src/platform/render-surface.ts +77 -0
  168. package/core/src/platform/rpc.ts +98 -0
  169. package/core/src/platform/runtime.ts +132 -0
  170. package/core/src/plugin/context.ts +364 -0
  171. package/core/src/plugin/diagnostics.ts +61 -0
  172. package/core/src/plugin/discovery.ts +186 -0
  173. package/core/src/plugin/extension-loaders.ts +172 -0
  174. package/core/src/plugin/host.ts +659 -0
  175. package/core/src/plugin/index.ts +106 -0
  176. package/core/src/plugin/loader-asset.ts +124 -0
  177. package/core/src/plugin/loader-quickjs.ts +84 -0
  178. package/core/src/plugin/loader-wasm.ts +411 -0
  179. package/core/src/plugin/loader-worker.ts +261 -0
  180. package/core/src/plugin/manifest.ts +653 -0
  181. package/core/src/plugin/material-registry.ts +65 -0
  182. package/core/src/plugin/mcp-tools.ts +165 -0
  183. package/core/src/plugin/permissions.ts +175 -0
  184. package/core/src/plugin/quickjs-bridge.ts +294 -0
  185. package/core/src/plugin/registry.ts +107 -0
  186. package/core/src/plugin/sandbox-shim.ts +86 -0
  187. package/core/src/plugin/sandbox-worker.ts +215 -0
  188. package/core/src/plugin/wasm-abi.ts +152 -0
  189. package/core/src/plugin/wasm-worker.ts +229 -0
  190. package/core/src/plugin/workshop.ts +169 -0
  191. package/core/src/profiling/event-loop.ts +180 -0
  192. package/core/src/profiling/index.ts +69 -0
  193. package/core/src/profiling/iops/idb-patch.ts +242 -0
  194. package/core/src/profiling/iops/opfs-patch.ts +276 -0
  195. package/core/src/profiling/profiling-sab.ts +839 -0
  196. package/core/src/profiling/task-latency.ts +134 -0
  197. package/core/src/profiling/thread-metrics.ts +126 -0
  198. package/core/src/profiling/trace-event-writer.ts +205 -0
  199. package/core/src/profiling/warnings.ts +423 -0
  200. package/core/src/profiling/worker-prelude.ts +376 -0
  201. package/core/src/render/bind-group.ts +38 -0
  202. package/core/src/render/bindless/bindless-struct.ts +42 -0
  203. package/core/src/render/bindless/bindless.wgsl.ts +167 -0
  204. package/core/src/render/bindless/frame-bindings.ts +176 -0
  205. package/core/src/render/bindless/index.ts +10 -0
  206. package/core/src/render/bindless/material-manager.ts +257 -0
  207. package/core/src/render/bindless/texture-registry.ts +609 -0
  208. package/core/src/render/buffer.ts +82 -0
  209. package/core/src/render/camera-2d.ts +217 -0
  210. package/core/src/render/camera-controller.ts +440 -0
  211. package/core/src/render/camera.ts +409 -0
  212. package/core/src/render/canvas-resize-watcher.ts +46 -0
  213. package/core/src/render/compute-kernel.ts +172 -0
  214. package/core/src/render/constants.ts +2 -0
  215. package/core/src/render/device.ts +111 -0
  216. package/core/src/render/frame-graph.ts +768 -0
  217. package/core/src/render/frustum.ts +185 -0
  218. package/core/src/render/fullscreen-vs.ts +25 -0
  219. package/core/src/render/g-buffer.ts +186 -0
  220. package/core/src/render/game-renderer.ts +1452 -0
  221. package/core/src/render/gpu-driven/cull-pass.ts +394 -0
  222. package/core/src/render/gpu-driven/hzb.ts +215 -0
  223. package/core/src/render/gpu-driven/index.ts +7 -0
  224. package/core/src/render/gpu-driven/indirect-draw-pass.ts +324 -0
  225. package/core/src/render/gpu-driven/instance-buffer.ts +178 -0
  226. package/core/src/render/gpu-driven/mesh-table.ts +175 -0
  227. package/core/src/render/gpu-driven/passes/hzb-build-pass.ts +47 -0
  228. package/core/src/render/gpu-utils.ts +96 -0
  229. package/core/src/render/hdr.ts +88 -0
  230. package/core/src/render/hot-reload.ts +295 -0
  231. package/core/src/render/ibl-bind-group.ts +266 -0
  232. package/core/src/render/ibl.ts +180 -0
  233. package/core/src/render/index.ts +163 -0
  234. package/core/src/render/input-manager.ts +259 -0
  235. package/core/src/render/lighting/area-light.ts +196 -0
  236. package/core/src/render/lighting/cluster-grid.ts +288 -0
  237. package/core/src/render/lighting/cluster-types.ts +193 -0
  238. package/core/src/render/lighting/ies-parser.ts +162 -0
  239. package/core/src/render/lighting.ts +454 -0
  240. package/core/src/render/material-types/depth.wgsl +44 -0
  241. package/core/src/render/material-types/line.wgsl +50 -0
  242. package/core/src/render/material-types/matcap.wgsl +51 -0
  243. package/core/src/render/material-types/normal.wgsl +41 -0
  244. package/core/src/render/material-types/physical.wgsl +152 -0
  245. package/core/src/render/material-types/sprite.wgsl +46 -0
  246. package/core/src/render/material-types/sss.wgsl +79 -0
  247. package/core/src/render/material-types/toon.wgsl +79 -0
  248. package/core/src/render/passes/README.md +121 -0
  249. package/core/src/render/passes/atmosphere.ts +152 -0
  250. package/core/src/render/passes/cluster-lighting-pass.ts +476 -0
  251. package/core/src/render/passes/csm.ts +408 -0
  252. package/core/src/render/passes/cubemap-capture.ts +114 -0
  253. package/core/src/render/passes/debug-viz.ts +278 -0
  254. package/core/src/render/passes/debug.ts +538 -0
  255. package/core/src/render/passes/decal-mesh.ts +111 -0
  256. package/core/src/render/passes/decal-pass.ts +377 -0
  257. package/core/src/render/passes/deferred-lighting.ts +434 -0
  258. package/core/src/render/passes/depth-prepass.ts +163 -0
  259. package/core/src/render/passes/dome-360.ts +83 -0
  260. package/core/src/render/passes/fluid-render.ts +137 -0
  261. package/core/src/render/passes/gi-types.ts +118 -0
  262. package/core/src/render/passes/graph-compute.ts +300 -0
  263. package/core/src/render/passes/greased-line-pass.ts +232 -0
  264. package/core/src/render/passes/morph-target.ts +40 -0
  265. package/core/src/render/passes/opaque.ts +1028 -0
  266. package/core/src/render/passes/point-light-shadow.ts +266 -0
  267. package/core/src/render/passes/procedural-texture.ts +185 -0
  268. package/core/src/render/passes/reflection-probe.ts +140 -0
  269. package/core/src/render/passes/rsm-pass.ts +248 -0
  270. package/core/src/render/passes/sdf-text.ts +155 -0
  271. package/core/src/render/passes/shadow-map.ts +163 -0
  272. package/core/src/render/passes/shadow.ts +220 -0
  273. package/core/src/render/passes/skinning-vs.ts +121 -0
  274. package/core/src/render/passes/skinning.ts +326 -0
  275. package/core/src/render/passes/sky-dome.ts +404 -0
  276. package/core/src/render/passes/skybox.ts +162 -0
  277. package/core/src/render/passes/spot-light-shadow.ts +273 -0
  278. package/core/src/render/passes/terrain.ts +326 -0
  279. package/core/src/render/passes/transparent.ts +353 -0
  280. package/core/src/render/passes/underwater-fog.ts +185 -0
  281. package/core/src/render/passes/video-texture.ts +79 -0
  282. package/core/src/render/passes/volumetric-pass.ts +312 -0
  283. package/core/src/render/passes/volumetric-types.ts +171 -0
  284. package/core/src/render/passes/water.ts +753 -0
  285. package/core/src/render/pbr.ts +296 -0
  286. package/core/src/render/pipeline.ts +52 -0
  287. package/core/src/render/render-pass.ts +30 -0
  288. package/core/src/render/renderer-input-bus.ts +220 -0
  289. package/core/src/render/renderer-module-host.ts +623 -0
  290. package/core/src/render/resource-tracker.ts +67 -0
  291. package/core/src/render/sab-canvas-overlay.ts +122 -0
  292. package/core/src/render/shader-validator.ts +200 -0
  293. package/core/src/render/shaders/debug/debug-line.wgsl +27 -0
  294. package/core/src/render/shaders/debug/debug-point.wgsl +30 -0
  295. package/core/src/render/shaders/debug/debug-text.wgsl +42 -0
  296. package/core/src/render/shaders/debug-viz/aabb.wgsl +20 -0
  297. package/core/src/render/shaders/debug-viz/depth.wgsl +29 -0
  298. package/core/src/render/shaders/debug-viz/lod.wgsl +38 -0
  299. package/core/src/render/shaders/debug-viz/normals.wgsl +30 -0
  300. package/core/src/render/shaders/debug-viz/overdraw.wgsl +21 -0
  301. package/core/src/render/shaders/debug-viz/tangents.wgsl +29 -0
  302. package/core/src/render/shaders/debug-viz/wireframe.wgsl +33 -0
  303. package/core/src/render/shaders/shadow-chunks.ts +135 -0
  304. package/core/src/render/skybox.ts +146 -0
  305. package/core/src/render/splitscreen.ts +85 -0
  306. package/core/src/render/surface.ts +78 -0
  307. package/core/src/render/tonemap.ts +180 -0
  308. package/core/src/render/tracked-render-pass.ts +158 -0
  309. package/core/src/render/viewport.ts +109 -0
  310. package/core/src/render/wgsl-hmr.ts +70 -0
  311. package/core/src/sab/core-input-channel.ts +23 -0
  312. package/core/src/sab/define.ts +239 -0
  313. package/core/src/sab/errors.ts +76 -0
  314. package/core/src/sab/game-input.ts +343 -0
  315. package/core/src/sab/gamepad-devices.ts +259 -0
  316. package/core/src/sab/grid-buffer.ts +117 -0
  317. package/core/src/sab/input.ts +55 -0
  318. package/core/src/sab/layout.ts +219 -0
  319. package/core/src/sab/multi-input-channel.ts +50 -0
  320. package/core/src/sab/record-buffer.ts +155 -0
  321. package/core/src/sab/sab-polyfill.ts +56 -0
  322. package/core/src/sab/sim-channel.ts +453 -0
  323. package/core/src/sab/slot-buffer.ts +190 -0
  324. package/core/src/sab/types.ts +158 -0
  325. package/core/src/safety/bounds.ts +76 -0
  326. package/core/src/safety/json.ts +80 -0
  327. package/core/src/safety/path.ts +167 -0
  328. package/core/src/save/binary-format.ts +117 -0
  329. package/core/src/save/game-save-system.ts +161 -0
  330. package/core/src/save/grid-save-system.ts +158 -0
  331. package/core/src/save/migrate.ts +44 -0
  332. package/core/src/save/migration-registry.ts +94 -0
  333. package/core/src/save/persist-types.ts +208 -0
  334. package/core/src/save/schema.ts +46 -0
  335. package/core/src/save/serializer.ts +180 -0
  336. package/core/src/scene/builder.ts +65 -0
  337. package/core/src/scene/camera.ts +142 -0
  338. package/core/src/scene/checkpoint.ts +109 -0
  339. package/core/src/scene/game-loop.ts +198 -0
  340. package/core/src/scene/index.ts +24 -0
  341. package/core/src/scene/layer.ts +6 -0
  342. package/core/src/scene/prefab.ts +145 -0
  343. package/core/src/scene/scene-manager.ts +157 -0
  344. package/core/src/scene/scene.ts +192 -0
  345. package/core/src/scene/sky.ts +256 -0
  346. package/core/src/scene/spatial-grid.ts +176 -0
  347. package/core/src/scene/streaming.ts +304 -0
  348. package/core/src/scene/vegetation.ts +163 -0
  349. package/core/src/scene/world.ts +89 -0
  350. package/core/src/scripting/binding.ts +129 -0
  351. package/core/src/scripting/hot-reload.ts +144 -0
  352. package/core/src/scripting/script.ts +99 -0
  353. package/core/src/sim/hot-reload-pipeline.ts +49 -0
  354. package/core/src/sim/sim-state-helper.ts +47 -0
  355. package/core/src/sim/transient-state-registry.ts +77 -0
  356. package/core/src/sim/types.ts +113 -0
  357. package/core/src/sim/worker-loop.ts +222 -0
  358. package/core/src/stores/game-store.ts +158 -0
  359. package/core/src/stores/hot-reload-store.ts +44 -0
  360. package/core/src/telemetry/collector.ts +410 -0
  361. package/core/src/telemetry/gc-controller.ts +453 -0
  362. package/core/src/telemetry/gc-profiler.ts +126 -0
  363. package/core/src/telemetry/gc-tracker.ts +69 -0
  364. package/core/src/telemetry/gpu-profiler.ts +703 -0
  365. package/core/src/telemetry/gpu-resource-tracker.ts +285 -0
  366. package/core/src/telemetry/gpu-timer-pool.ts +234 -0
  367. package/core/src/telemetry/gpu-timer.ts +123 -0
  368. package/core/src/telemetry/gpu-timestamp-gate.ts +21 -0
  369. package/core/src/telemetry/reporter.ts +62 -0
  370. package/core/src/types/engine-types.ts +129 -0
  371. package/core/src/ui-font-scale.ts +112 -0
  372. package/core/src/util/broad-phase-grid.ts +82 -0
  373. package/core/src/util/feature-log.ts +220 -0
  374. package/core/src/util/index.ts +17 -0
  375. package/core/src/util/logger.ts +604 -0
  376. package/core/src/webgpu-destroy-augmentation.d.ts +25 -0
  377. package/core/src/worker/base-worker-host.ts +183 -0
  378. package/core/src/worker/buffer-sync.ts +550 -0
  379. package/core/src/worker/crash-recovery.ts +201 -0
  380. package/core/src/worker/entity-sim-worker-host.ts +190 -0
  381. package/core/src/worker/instrumented-worker-host.ts +171 -0
  382. package/core/src/worker/rpc.ts +253 -0
  383. package/core/src/worker/sim-worker-base.ts +913 -0
  384. package/core/src/worker/sim-worker-host.ts +358 -0
  385. package/core/src/worker/task-pool.ts +256 -0
  386. package/core/src/worker/task-worker.ts +83 -0
  387. package/libraries/asset-browser/src/index.ts +11 -0
  388. package/libraries/asset-browser/src/software-thumbnails.ts +949 -0
  389. package/libraries/audio-kira/README.md +17 -0
  390. package/libraries/audio-kira/native/Cargo.toml +14 -0
  391. package/libraries/audio-kira/native/src/engine.rs +27 -0
  392. package/libraries/audio-kira/native/src/lib.rs +185 -0
  393. package/libraries/audio-kira/native/src/mixer.rs +11 -0
  394. package/libraries/audio-kira/native/src/source.rs +80 -0
  395. package/libraries/audio-kira/src/backend.ts +469 -0
  396. package/libraries/audio-kira/src/index.ts +8 -0
  397. package/libraries/audio-kira/src/library.ts +82 -0
  398. package/libraries/blitz-ui/native/Cargo.toml +33 -0
  399. package/libraries/blitz-ui/native/assets/DejaVuSans.woff2 +0 -0
  400. package/libraries/blitz-ui/native/src/bridge.rs +100 -0
  401. package/libraries/blitz-ui/native/src/fonts.rs +20 -0
  402. package/libraries/blitz-ui/native/src/input.rs +80 -0
  403. package/libraries/blitz-ui/native/src/lib.rs +96 -0
  404. package/libraries/blitz-ui/native/src/shell.rs +301 -0
  405. package/libraries/blitz-ui/native-osr/Cargo.toml +24 -0
  406. package/libraries/blitz-ui/native-osr/src/events.rs +237 -0
  407. package/libraries/blitz-ui/native-osr/src/lib.rs +846 -0
  408. package/libraries/blitz-ui/native-osr/src/mutate.rs +385 -0
  409. package/libraries/blitz-ui/native-osr/src/resources.rs +45 -0
  410. package/libraries/blitz-ui/native-osr-spike.ts +70 -0
  411. package/libraries/blitz-ui/src/index.ts +15 -0
  412. package/libraries/blitz-ui/src/native-module.ts +221 -0
  413. package/libraries/blitz-ui/src/native-osr-ffi.ts +423 -0
  414. package/libraries/blitz-ui/src/router.ts +101 -0
  415. package/libraries/blitz-ui/src/shaders/ui-blit.wgsl.ts +35 -0
  416. package/libraries/blitz-ui/src/ui-blit-pass.ts +116 -0
  417. package/libraries/character/README.md +25 -0
  418. package/libraries/character/src/character-animator.ts +374 -0
  419. package/libraries/character/src/index.ts +24 -0
  420. package/libraries/character/src/loader.ts +375 -0
  421. package/libraries/character/src/locomotion.ts +124 -0
  422. package/libraries/character/src/preview.ts +287 -0
  423. package/libraries/devtools/README.md +51 -0
  424. package/libraries/devtools/native/Cargo.toml +12 -0
  425. package/libraries/devtools/native/src/lib.rs +1646 -0
  426. package/libraries/devtools/native/src/panels/console.rs +338 -0
  427. package/libraries/devtools/native/src/panels/dom_tree.rs +47 -0
  428. package/libraries/devtools/native/src/panels/generic.rs +317 -0
  429. package/libraries/devtools/native/src/panels/gpu.rs +107 -0
  430. package/libraries/devtools/native/src/panels/input.rs +88 -0
  431. package/libraries/devtools/native/src/panels/mod.rs +154 -0
  432. package/libraries/devtools/native/src/panels/perf_metrics.rs +91 -0
  433. package/libraries/devtools/native/src/panels/perf_recorder.rs +158 -0
  434. package/libraries/devtools/native/src/panels/scene.rs +72 -0
  435. package/libraries/devtools/native/src/state.rs +394 -0
  436. package/libraries/devtools/src/cdp-bridge.ts +349 -0
  437. package/libraries/devtools/src/collectors.ts +346 -0
  438. package/libraries/devtools/src/egui-ffi.ts +804 -0
  439. package/libraries/devtools/src/egui-renderer.ts +407 -0
  440. package/libraries/devtools/src/host.ts +363 -0
  441. package/libraries/devtools/src/index.ts +40 -0
  442. package/libraries/devtools/src/mirror.ts +794 -0
  443. package/libraries/devtools/src/native-providers.ts +744 -0
  444. package/libraries/devtools/src/verify.ts +167 -0
  445. package/libraries/devtools/src/web/browser.ts +102 -0
  446. package/libraries/devtools/src/web/host.ts +212 -0
  447. package/libraries/devtools/src/web/mirror.ts +430 -0
  448. package/libraries/devtools/src/web/server.ts +198 -0
  449. package/libraries/entities/README.md +17 -0
  450. package/libraries/entities/src/index.ts +4 -0
  451. package/libraries/entities/src/library.ts +64 -0
  452. package/libraries/entities/src/model-renderer.ts +1345 -0
  453. package/libraries/entities/src/shaders/model.wgsl +375 -0
  454. package/libraries/gamepad/src/feed-input.ts +83 -0
  455. package/libraries/gamepad/src/filters.ts +49 -0
  456. package/libraries/gamepad/src/glyphs.ts +93 -0
  457. package/libraries/gamepad/src/hub.ts +206 -0
  458. package/libraries/gamepad/src/index.ts +22 -0
  459. package/libraries/gamepad/src/library.ts +68 -0
  460. package/libraries/gamepad/src/source-sab.ts +70 -0
  461. package/libraries/gamepad/src/sysfs-enrich.ts +138 -0
  462. package/libraries/gaussian-splats/README.md +25 -0
  463. package/libraries/gaussian-splats/src/gpu-sort.ts +595 -0
  464. package/libraries/gaussian-splats/src/index.ts +31 -0
  465. package/libraries/gaussian-splats/src/library.ts +60 -0
  466. package/libraries/gaussian-splats/src/parser.ts +382 -0
  467. package/libraries/gaussian-splats/src/renderer.ts +427 -0
  468. package/libraries/gaussian-splats/src/sh-eval.ts +211 -0
  469. package/libraries/gaussian-splats/src/sorter.ts +110 -0
  470. package/libraries/gaussian-splats/src/tile-raster.ts +473 -0
  471. package/libraries/gpu-kernels/README.md +154 -0
  472. package/libraries/gpu-kernels/src/index.ts +11 -0
  473. package/libraries/gpu-kernels/src/kernel.ts +696 -0
  474. package/libraries/gpu-kernels/src/transpile.ts +958 -0
  475. package/libraries/html-ui-kit/src/behaviors.ts +250 -0
  476. package/libraries/html-ui-kit/src/components.ts +309 -0
  477. package/libraries/html-ui-kit/src/index.ts +30 -0
  478. package/libraries/html-ui-kit/src/nav/nav-controller.ts +238 -0
  479. package/libraries/html-ui-kit/src/nav/nav-input.ts +145 -0
  480. package/libraries/html-ui-kit/src/nav/osk.ts +153 -0
  481. package/libraries/html-ui-kit/src/nav/router.ts +167 -0
  482. package/libraries/html-ui-kit/src/nav/vcursor.ts +203 -0
  483. package/libraries/html-ui-kit/src/ten-foot.ts +182 -0
  484. package/libraries/html-ui-kit/src/theme.ts +238 -0
  485. package/libraries/lighting/README.md +20 -0
  486. package/libraries/lighting/src/index.ts +5 -0
  487. package/libraries/lighting/src/library.ts +66 -0
  488. package/libraries/lighting/src/light-system.ts +452 -0
  489. package/libraries/lighting/src/lighting-system.ts +68 -0
  490. package/libraries/lighting/src/shaders/light-debug.wgsl +30 -0
  491. package/libraries/marching-cubes/README.md +25 -0
  492. package/libraries/marching-cubes/src/chunked-field.ts +177 -0
  493. package/libraries/marching-cubes/src/deformation.ts +63 -0
  494. package/libraries/marching-cubes/src/generator.ts +407 -0
  495. package/libraries/marching-cubes/src/index.ts +32 -0
  496. package/libraries/marching-cubes/src/library.ts +80 -0
  497. package/libraries/marching-cubes/src/lod.ts +122 -0
  498. package/libraries/marching-cubes/src/marching-tetrahedra.ts +252 -0
  499. package/libraries/marching-cubes/src/sab.ts +45 -0
  500. package/libraries/marching-cubes/src/streaming-config.ts +48 -0
  501. package/libraries/marching-cubes/src/streaming-manager.ts +686 -0
  502. package/libraries/marching-cubes/src/tables.ts +323 -0
  503. package/libraries/marching-cubes/src/types.ts +18 -0
  504. package/libraries/models/README.md +25 -0
  505. package/libraries/models/src/bake-node-transforms.ts +171 -0
  506. package/libraries/models/src/codecs/basisu-codec.ts +180 -0
  507. package/libraries/models/src/codecs/draco-codec.ts +372 -0
  508. package/libraries/models/src/codecs/draco3dgltf.d.ts +40 -0
  509. package/libraries/models/src/codecs/index.ts +22 -0
  510. package/libraries/models/src/codecs/meshopt-codec.ts +91 -0
  511. package/libraries/models/src/codecs/registry.ts +221 -0
  512. package/libraries/models/src/dae.ts +753 -0
  513. package/libraries/models/src/default-part-selection.ts +56 -0
  514. package/libraries/models/src/dxf.ts +200 -0
  515. package/libraries/models/src/fbx/index.ts +457 -0
  516. package/libraries/models/src/fbx/interpreter/animation.ts +431 -0
  517. package/libraries/models/src/fbx/interpreter/blend-shapes.ts +323 -0
  518. package/libraries/models/src/fbx/interpreter/connections.ts +166 -0
  519. package/libraries/models/src/fbx/interpreter/diagnostics.ts +65 -0
  520. package/libraries/models/src/fbx/interpreter/geometry.ts +535 -0
  521. package/libraries/models/src/fbx/interpreter/global-settings.ts +92 -0
  522. package/libraries/models/src/fbx/interpreter/materials.ts +265 -0
  523. package/libraries/models/src/fbx/interpreter/nodes.ts +197 -0
  524. package/libraries/models/src/fbx/interpreter/skeleton.ts +476 -0
  525. package/libraries/models/src/fbx/parsers/binary-parser.ts +244 -0
  526. package/libraries/models/src/fbx/types.ts +119 -0
  527. package/libraries/models/src/gltf-extensions.ts +196 -0
  528. package/libraries/models/src/gltf.ts +935 -0
  529. package/libraries/models/src/index.ts +87 -0
  530. package/libraries/models/src/library.ts +75 -0
  531. package/libraries/models/src/loader.ts +265 -0
  532. package/libraries/models/src/material-adapter.ts +108 -0
  533. package/libraries/models/src/mini-xml.ts +174 -0
  534. package/libraries/models/src/normalize.ts +216 -0
  535. package/libraries/models/src/obj.ts +264 -0
  536. package/libraries/models/src/off.ts +95 -0
  537. package/libraries/models/src/ply.ts +348 -0
  538. package/libraries/models/src/pose-sampler.ts +219 -0
  539. package/libraries/models/src/sidecar/blender-extras.ts +37 -0
  540. package/libraries/models/src/sidecar/ddmeta.ts +114 -0
  541. package/libraries/models/src/sidecar/godot-import.ts +86 -0
  542. package/libraries/models/src/sidecar/resolver.ts +145 -0
  543. package/libraries/models/src/sidecar/types.ts +11 -0
  544. package/libraries/models/src/sidecar/unity-meta.ts +61 -0
  545. package/libraries/models/src/skeleton-synthesis.ts +132 -0
  546. package/libraries/models/src/stl.ts +149 -0
  547. package/libraries/models/src/threeds.ts +363 -0
  548. package/libraries/models/src/threemf.ts +201 -0
  549. package/libraries/models/src/types.ts +188 -0
  550. package/libraries/models/src/vtk.ts +218 -0
  551. package/libraries/models/test-fixtures/fbx/LICENSE-notice.txt +24 -0
  552. package/libraries/models/test-fixtures/fbx/README.md +46 -0
  553. package/libraries/models/test-fixtures/fbx/m01_cube_phong.fbx +0 -0
  554. package/libraries/models/test-fixtures/fbx/m02_geo_ngons.fbx +0 -0
  555. package/libraries/models/test-fixtures/fbx/m03_normals.fbx +0 -0
  556. package/libraries/models/test-fixtures/fbx/m04_material_properties.fbx +0 -0
  557. package/libraries/models/test-fixtures/fbx/m05_external_diffuse.png +0 -0
  558. package/libraries/models/test-fixtures/fbx/m05_textures.fbx +0 -0
  559. package/libraries/models/test-fixtures/fbx/m06_uv_transform.fbx +0 -0
  560. package/libraries/models/test-fixtures/fbx/m07_multimaterial.fbx +0 -0
  561. package/libraries/models/test-fixtures/fbx/m08_transforms.fbx +0 -0
  562. package/libraries/models/test-fixtures/fbx/m09_skinning.fbx +0 -0
  563. package/libraries/models/test-fixtures/fbx/m10_morph.fbx +0 -0
  564. package/libraries/models/test-fixtures/fbx/m11_node_anim.fbx +0 -0
  565. package/libraries/models/test-fixtures/fbx/m12_skeletal_anim.fbx +0 -0
  566. package/libraries/models/test-fixtures/fbx/m13_morph_anim.fbx +0 -0
  567. package/libraries/models/test-fixtures/fbx/m14_multiclip.fbx +0 -0
  568. package/libraries/models/test-fixtures/fbx/m15_camera_lights.fbx +0 -0
  569. package/libraries/models/test-fixtures/fbx/m16_axis_yup.fbx +0 -0
  570. package/libraries/models/test-fixtures/fbx/m16_axis_zup.fbx +0 -0
  571. package/libraries/models/test-fixtures/fbx/m16_units_254.fbx +0 -0
  572. package/libraries/navmesh/README.md +23 -0
  573. package/libraries/navmesh/src/crowd-system.ts +240 -0
  574. package/libraries/navmesh/src/debug-viz.ts +74 -0
  575. package/libraries/navmesh/src/index.ts +15 -0
  576. package/libraries/navmesh/src/library.ts +64 -0
  577. package/libraries/navmesh/src/navmesh-generator.ts +407 -0
  578. package/libraries/navmesh/src/navmesh.ts +159 -0
  579. package/libraries/navmesh/src/pathfinder.ts +252 -0
  580. package/libraries/navmesh/src/types.ts +63 -0
  581. package/libraries/networking/README.md +25 -0
  582. package/libraries/networking/src/authority.ts +121 -0
  583. package/libraries/networking/src/connection.ts +139 -0
  584. package/libraries/networking/src/discovery.ts +373 -0
  585. package/libraries/networking/src/enhanced-replication.ts +355 -0
  586. package/libraries/networking/src/epic-adapter.ts +129 -0
  587. package/libraries/networking/src/index.ts +45 -0
  588. package/libraries/networking/src/library.ts +69 -0
  589. package/libraries/networking/src/lobby.ts +108 -0
  590. package/libraries/networking/src/platform-adapter.ts +163 -0
  591. package/libraries/networking/src/rcs-adapter.ts +127 -0
  592. package/libraries/networking/src/remote-input.ts +188 -0
  593. package/libraries/networking/src/replication.ts +327 -0
  594. package/libraries/networking/src/rpc.ts +188 -0
  595. package/libraries/networking/src/session.ts +173 -0
  596. package/libraries/networking/src/transport.ts +201 -0
  597. package/libraries/networking/src/webrtc.ts +323 -0
  598. package/libraries/pathfinding-2d/README.md +15 -0
  599. package/libraries/pathfinding-2d/src/astar-grid.ts +312 -0
  600. package/libraries/pathfinding-2d/src/index.ts +11 -0
  601. package/libraries/persistence/README.md +25 -0
  602. package/libraries/persistence/src/autosave-manager.ts +112 -0
  603. package/libraries/persistence/src/binary-record-store.ts +244 -0
  604. package/libraries/persistence/src/browser.ts +51 -0
  605. package/libraries/persistence/src/file-save-store.ts +553 -0
  606. package/libraries/persistence/src/hash-utils.ts +59 -0
  607. package/libraries/persistence/src/index.ts +57 -0
  608. package/libraries/persistence/src/library.ts +74 -0
  609. package/libraries/persistence/src/mock-opfs.ts +141 -0
  610. package/libraries/persistence/src/node-fs-directory-handle.ts +161 -0
  611. package/libraries/persistence/src/opfs-save-store.ts +745 -0
  612. package/libraries/persistence/src/save-worker-proxy.ts +127 -0
  613. package/libraries/persistence/src/save-worker.ts +99 -0
  614. package/libraries/persistence/src/xxh3-ts.d.ts +10 -0
  615. package/libraries/physics-native/README.md +21 -0
  616. package/libraries/physics-native/native/Cargo.toml +11 -0
  617. package/libraries/physics-native/native/src/body.rs +403 -0
  618. package/libraries/physics-native/native/src/character.rs +226 -0
  619. package/libraries/physics-native/native/src/collider.rs +299 -0
  620. package/libraries/physics-native/native/src/joint.rs +81 -0
  621. package/libraries/physics-native/native/src/lib.rs +830 -0
  622. package/libraries/physics-native/native/src/query.rs +261 -0
  623. package/libraries/physics-native/native/src/realm.rs +33 -0
  624. package/libraries/physics-native/native/src/step.rs +54 -0
  625. package/libraries/physics-native/native/src/world.rs +148 -0
  626. package/libraries/physics-native/src/backend.ts +1046 -0
  627. package/libraries/physics-native/src/broadphase.ts +101 -0
  628. package/libraries/physics-native/src/ffi-spike-worker.ts +71 -0
  629. package/libraries/physics-native/src/index.ts +11 -0
  630. package/libraries/physics-native/src/library.ts +79 -0
  631. package/libraries/physics-native/src/narrowphase-shapes.ts +420 -0
  632. package/libraries/physics-native/src/narrowphase.ts +123 -0
  633. package/libraries/physics-native/src/solver.ts +141 -0
  634. package/libraries/physics-native/src/types.ts +76 -0
  635. package/libraries/physics-rapier/README.md +21 -0
  636. package/libraries/physics-rapier/bench/ffi-bench.ts +79 -0
  637. package/libraries/physics-rapier/bench/wasm-baseline.ts +90 -0
  638. package/libraries/physics-rapier/src/api.ts +452 -0
  639. package/libraries/physics-rapier/src/auto-backend.ts +59 -0
  640. package/libraries/physics-rapier/src/backend.ts +1207 -0
  641. package/libraries/physics-rapier/src/bulk-ops.ts +68 -0
  642. package/libraries/physics-rapier/src/ffi-backend.ts +21 -0
  643. package/libraries/physics-rapier/src/ffi-lib.ts +597 -0
  644. package/libraries/physics-rapier/src/index.ts +12 -0
  645. package/libraries/physics-rapier/src/library.ts +107 -0
  646. package/libraries/physics-rapier/src/rapier-backend.ts +1052 -0
  647. package/libraries/physics-rapier/src/realm-worker-script.ts +237 -0
  648. package/libraries/postfx/README.md +17 -0
  649. package/libraries/postfx/src/index.ts +19 -0
  650. package/libraries/postfx/src/library.ts +337 -0
  651. package/libraries/postfx/src/post-process-stack.ts +1752 -0
  652. package/libraries/postfx/src/shaders/post-process/afterimage.wgsl +14 -0
  653. package/libraries/postfx/src/shaders/post-process/ascii.wgsl +24 -0
  654. package/libraries/postfx/src/shaders/post-process/blit.wgsl +12 -0
  655. package/libraries/postfx/src/shaders/post-process/bloom-blur.wgsl +21 -0
  656. package/libraries/postfx/src/shaders/post-process/bloom-bright.wgsl +15 -0
  657. package/libraries/postfx/src/shaders/post-process/bloom-composite.wgsl +14 -0
  658. package/libraries/postfx/src/shaders/post-process/bloom-downsample.wgsl +60 -0
  659. package/libraries/postfx/src/shaders/post-process/bloom-soft.wgsl +46 -0
  660. package/libraries/postfx/src/shaders/post-process/bloom-upsample.wgsl +51 -0
  661. package/libraries/postfx/src/shaders/post-process/channel-mixer.wgsl +36 -0
  662. package/libraries/postfx/src/shaders/post-process/chromatic-aberration.wgsl +37 -0
  663. package/libraries/postfx/src/shaders/post-process/dithering.wgsl +64 -0
  664. package/libraries/postfx/src/shaders/post-process/dof.wgsl +131 -0
  665. package/libraries/postfx/src/shaders/post-process/edges.wgsl +45 -0
  666. package/libraries/postfx/src/shaders/post-process/fullscreen-vs.wgsl +15 -0
  667. package/libraries/postfx/src/shaders/post-process/fxaa.wgsl +102 -0
  668. package/libraries/postfx/src/shaders/post-process/gaussian-blur.wgsl +37 -0
  669. package/libraries/postfx/src/shaders/post-process/glow-blur.wgsl +33 -0
  670. package/libraries/postfx/src/shaders/post-process/glow-composite.wgsl +25 -0
  671. package/libraries/postfx/src/shaders/post-process/grain.wgsl +37 -0
  672. package/libraries/postfx/src/shaders/post-process/halftone.wgsl +80 -0
  673. package/libraries/postfx/src/shaders/post-process/highlight-blur.wgsl +33 -0
  674. package/libraries/postfx/src/shaders/post-process/highlight-composite.wgsl +32 -0
  675. package/libraries/postfx/src/shaders/post-process/lens-distortion.wgsl +43 -0
  676. package/libraries/postfx/src/shaders/post-process/lens-flare.wgsl +68 -0
  677. package/libraries/postfx/src/shaders/post-process/lut.wgsl +30 -0
  678. package/libraries/postfx/src/shaders/post-process/motion-blur.wgsl +55 -0
  679. package/libraries/postfx/src/shaders/post-process/occluder-chunk.wgsl +28 -0
  680. package/libraries/postfx/src/shaders/post-process/outline.wgsl +51 -0
  681. package/libraries/postfx/src/shaders/post-process/pixelation.wgsl +47 -0
  682. package/libraries/postfx/src/shaders/post-process/sharpen.wgsl +32 -0
  683. package/libraries/postfx/src/shaders/post-process/sobel.wgsl +26 -0
  684. package/libraries/postfx/src/shaders/post-process/split-tone.wgsl +36 -0
  685. package/libraries/postfx/src/shaders/post-process/ssao-blur.wgsl +38 -0
  686. package/libraries/postfx/src/shaders/post-process/ssao-composite.wgsl +25 -0
  687. package/libraries/postfx/src/shaders/post-process/ssao.wgsl +147 -0
  688. package/libraries/postfx/src/shaders/post-process/ssr.wgsl +155 -0
  689. package/libraries/postfx/src/shaders/post-process/taa.wgsl +98 -0
  690. package/libraries/postfx/src/shaders/post-process/tonemap.wgsl +40 -0
  691. package/libraries/postfx/src/shaders/post-process/watercolor.wgsl +82 -0
  692. package/libraries/postfx/src/shaders/post-process/white-balance.wgsl +41 -0
  693. package/libraries/recast/README.md +21 -0
  694. package/libraries/recast/src/backend.ts +150 -0
  695. package/libraries/recast/src/crowd-system.ts +352 -0
  696. package/libraries/recast/src/index.ts +12 -0
  697. package/libraries/recast/src/library.ts +91 -0
  698. package/libraries/recast/src/types.ts +86 -0
  699. package/libraries/sand/README.md +25 -0
  700. package/libraries/sand/src/cell.ts +43 -0
  701. package/libraries/sand/src/fields.ts +29 -0
  702. package/libraries/sand/src/fluid-grid.ts +261 -0
  703. package/libraries/sand/src/grid-dims.ts +74 -0
  704. package/libraries/sand/src/grid-sim-buffer.ts +271 -0
  705. package/libraries/sand/src/index.ts +81 -0
  706. package/libraries/sand/src/library.ts +92 -0
  707. package/libraries/sand/src/materials.ts +492 -0
  708. package/libraries/sand/src/palette.ts +93 -0
  709. package/libraries/sand/src/particles.ts +185 -0
  710. package/libraries/sand/src/render/sand-grid-pass.ts +430 -0
  711. package/libraries/sand/src/rng.ts +51 -0
  712. package/libraries/sand/src/rules/builtin-rules.ts +132 -0
  713. package/libraries/sand/src/rules/rule-compiler.ts +162 -0
  714. package/libraries/sand/src/rules/rule-engine.ts +231 -0
  715. package/libraries/sand/src/rules/rule-types.ts +87 -0
  716. package/libraries/sand/src/sand-step-pool.ts +484 -0
  717. package/libraries/sand/src/sand-step-worker.ts +97 -0
  718. package/libraries/sand/src/sand-world.ts +3227 -0
  719. package/libraries/stickman/README.md +25 -0
  720. package/libraries/stickman/src/index.ts +25 -0
  721. package/libraries/stickman/src/library.ts +61 -0
  722. package/libraries/stickman/src/proportions.ts +45 -0
  723. package/libraries/stickman/src/shader.ts +117 -0
  724. package/libraries/stickman/src/skeleton.ts +197 -0
  725. package/libraries/stickman/src/stickman-pass.ts +255 -0
  726. package/libraries/stickman/src/thick-line.ts +123 -0
  727. package/libraries/surface-nets/README.md +25 -0
  728. package/libraries/surface-nets/src/chunked-field.ts +14 -0
  729. package/libraries/surface-nets/src/deformation.ts +45 -0
  730. package/libraries/surface-nets/src/index.ts +36 -0
  731. package/libraries/surface-nets/src/library.ts +59 -0
  732. package/libraries/surface-nets/src/lod.ts +7 -0
  733. package/libraries/surface-nets/src/sab.ts +4 -0
  734. package/libraries/surface-nets/src/streaming-config.ts +5 -0
  735. package/libraries/surface-nets/src/streaming-manager.ts +15 -0
  736. package/libraries/surface-nets/src/surface-nets.ts +334 -0
  737. package/libraries/surface-nets/src/types.ts +23 -0
  738. package/libraries/water/README.md +25 -0
  739. package/libraries/water/src/buoyancy.ts +103 -0
  740. package/libraries/water/src/index.ts +43 -0
  741. package/libraries/water/src/library.ts +85 -0
  742. package/libraries/water/src/shore-damping.ts +93 -0
  743. package/libraries/water/src/water-buffer.ts +89 -0
  744. package/libraries/water/src/water-chunks.ts +13 -0
  745. package/libraries/water/src/water-physics.ts +376 -0
  746. package/libraries/water/src/water-renderer.ts +809 -0
  747. package/libraries/water/src/water-sab.ts +161 -0
  748. package/libraries/water/src/wave-sources.ts +84 -0
  749. package/libraries/weather/README.md +20 -0
  750. package/libraries/weather/src/index.ts +9 -0
  751. package/libraries/weather/src/library.ts +72 -0
  752. package/libraries/weather/src/types.ts +64 -0
  753. package/libraries/weather/src/weather-blend.ts +112 -0
  754. package/libraries/weather/src/weather-system.ts +271 -0
  755. package/libraries/weatherfx/README.md +24 -0
  756. package/libraries/weatherfx/src/cloud-provider.ts +31 -0
  757. package/libraries/weatherfx/src/cloud-system.ts +372 -0
  758. package/libraries/weatherfx/src/index.ts +9 -0
  759. package/libraries/weatherfx/src/library.ts +58 -0
  760. package/libraries/weatherfx/src/particle-system.ts +434 -0
  761. package/libraries/weatherfx/src/shaders/cloud.wgsl +111 -0
  762. package/libraries/weatherfx/src/shaders/particle-compute.wgsl +187 -0
  763. package/libraries/weatherfx/src/shaders/particle-render.wgsl +95 -0
  764. package/lint-plugin.mjs +41 -0
  765. package/mcp/src/client.ts +703 -0
  766. package/mcp/src/engine-context.ts +186 -0
  767. package/mcp/src/http-transport.ts +471 -0
  768. package/mcp/src/index.ts +13 -0
  769. package/mcp/src/prompts/add-entity.ts +42 -0
  770. package/mcp/src/prompts/create-scene.ts +44 -0
  771. package/mcp/src/prompts/debug-frame.ts +39 -0
  772. package/mcp/src/prompts/index.ts +12 -0
  773. package/mcp/src/resources/asset-list.ts +32 -0
  774. package/mcp/src/resources/checkpoint-list.ts +33 -0
  775. package/mcp/src/resources/entity-state.ts +55 -0
  776. package/mcp/src/resources/gpu-info.ts +103 -0
  777. package/mcp/src/resources/index.ts +57 -0
  778. package/mcp/src/resources/performance.ts +37 -0
  779. package/mcp/src/resources/scene-tree.ts +43 -0
  780. package/mcp/src/server.ts +375 -0
  781. package/mcp/src/tools/animation.ts +118 -0
  782. package/mcp/src/tools/asset.ts +161 -0
  783. package/mcp/src/tools/audio.ts +96 -0
  784. package/mcp/src/tools/build.ts +143 -0
  785. package/mcp/src/tools/camera.ts +167 -0
  786. package/mcp/src/tools/checkpoint.ts +100 -0
  787. package/mcp/src/tools/component.ts +199 -0
  788. package/mcp/src/tools/debug.ts +300 -0
  789. package/mcp/src/tools/entity.ts +361 -0
  790. package/mcp/src/tools/inspect.ts +210 -0
  791. package/mcp/src/tools/lighting.ts +263 -0
  792. package/mcp/src/tools/material.ts +243 -0
  793. package/mcp/src/tools/mesh.ts +239 -0
  794. package/mcp/src/tools/physics.ts +266 -0
  795. package/mcp/src/tools/scene.ts +153 -0
  796. package/mcp/src/tools/script.ts +122 -0
  797. package/mcp/src/types.ts +92 -0
  798. package/mcp/src/undo-redo.ts +77 -0
  799. package/modules/camera-controls/README.md +20 -0
  800. package/modules/camera-controls/src/index.ts +161 -0
  801. package/modules/controller-ui/src/index.ts +164 -0
  802. package/modules/devtools/README.md +25 -0
  803. package/modules/devtools/src/api.ts +461 -0
  804. package/modules/devtools/src/asset-utils.ts +105 -0
  805. package/modules/devtools/src/bind-debug-store.ts +48 -0
  806. package/modules/devtools/src/data-bridge.ts +286 -0
  807. package/modules/devtools/src/debug-overlay.ts +238 -0
  808. package/modules/devtools/src/debug-raycast.ts +328 -0
  809. package/modules/devtools/src/debug-store.ts +92 -0
  810. package/modules/devtools/src/debug-view-descriptors.ts +59 -0
  811. package/modules/devtools/src/doctor-panel.ts +182 -0
  812. package/modules/devtools/src/gizmo-module.ts +136 -0
  813. package/modules/devtools/src/graph-model.ts +509 -0
  814. package/modules/devtools/src/grid-renderer.ts +157 -0
  815. package/modules/devtools/src/height-ruler-renderer.ts +216 -0
  816. package/modules/devtools/src/index.ts +81 -0
  817. package/modules/devtools/src/init.ts +261 -0
  818. package/modules/devtools/src/label-overlay.ts +154 -0
  819. package/modules/devtools/src/material-stats-panel.ts +188 -0
  820. package/modules/devtools/src/profiling-bridge.ts +377 -0
  821. package/modules/devtools/src/profiling-hooks.ts +98 -0
  822. package/modules/devtools/src/renderer-adapter.ts +76 -0
  823. package/modules/devtools/src/scene-inspector.ts +637 -0
  824. package/modules/devtools/src/scene-store.ts +320 -0
  825. package/modules/devtools/src/scene-sync.ts +116 -0
  826. package/modules/devtools/src/shaders/transform-gizmo.wgsl +38 -0
  827. package/modules/devtools/src/sim-stats-panel.ts +272 -0
  828. package/modules/devtools/src/sim-stats-provider.ts +105 -0
  829. package/modules/devtools/src/skeleton-renderer.ts +351 -0
  830. package/modules/devtools/src/transform-gizmo.ts +928 -0
  831. package/modules/devtools/src/types.ts +290 -0
  832. package/modules/devtools/src/worker-expose.ts +67 -0
  833. package/modules/devtools/src/worker-sync.ts +137 -0
  834. package/modules/editor/src/commands/component.ts +156 -0
  835. package/modules/editor/src/commands/entity.ts +287 -0
  836. package/modules/editor/src/commands/hierarchy.ts +83 -0
  837. package/modules/editor/src/commands/index.ts +61 -0
  838. package/modules/editor/src/commands/prefab.ts +163 -0
  839. package/modules/editor/src/commands/registry.ts +331 -0
  840. package/modules/editor/src/commands/scene-io.ts +211 -0
  841. package/modules/editor/src/commands/selection.ts +73 -0
  842. package/modules/editor/src/commands/transform.ts +91 -0
  843. package/modules/editor/src/ddscene.ts +267 -0
  844. package/modules/editor/src/document.ts +123 -0
  845. package/modules/editor/src/editor-context.ts +172 -0
  846. package/modules/editor/src/index.ts +41 -0
  847. package/modules/editor/src/mcp.ts +103 -0
  848. package/modules/editor/src/module.ts +148 -0
  849. package/modules/editor/src/selection.ts +70 -0
  850. package/modules/editor/src/shell.ts +456 -0
  851. package/modules/editor/src/viewport.ts +199 -0
  852. package/modules/html-ui/src/composite.ts +135 -0
  853. package/modules/html-ui/src/create-html-ui.ts +188 -0
  854. package/modules/html-ui/src/doc-backend.ts +343 -0
  855. package/modules/html-ui/src/host.ts +646 -0
  856. package/modules/html-ui/src/index.ts +13 -0
  857. package/modules/html-ui/src/jsx-dev-runtime.ts +12 -0
  858. package/modules/html-ui/src/jsx-runtime.ts +126 -0
  859. package/modules/html-ui/src/protocol.ts +88 -0
  860. package/modules/html-ui/src/ui-worker.ts +16 -0
  861. package/modules/mcp/README.md +18 -0
  862. package/modules/mcp/src/index.ts +27 -0
  863. package/modules/movement-2d/README.md +19 -0
  864. package/modules/movement-2d/src/grid-character.ts +455 -0
  865. package/modules/movement-2d/src/index.ts +45 -0
  866. package/modules/movement-3d/README.md +21 -0
  867. package/modules/movement-3d/src/character-motor.ts +230 -0
  868. package/modules/movement-3d/src/index.ts +46 -0
  869. package/modules/native-osr/src/index.ts +22 -0
  870. package/modules/native-osr/src/input-router.ts +401 -0
  871. package/modules/native-osr/src/native-osr-manager.ts +247 -0
  872. package/modules/native-osr/src/native-texture-source.ts +86 -0
  873. package/modules/native-osr/src/types.ts +112 -0
  874. package/modules/native-osr/src/world-space-ui-pass.ts +351 -0
  875. package/modules/sailing/README.md +18 -0
  876. package/modules/sailing/src/index.ts +51 -0
  877. package/modules/terrain/README.md +18 -0
  878. package/modules/terrain/src/index.ts +78 -0
  879. package/modules/vitals/README.md +17 -0
  880. package/modules/vitals/src/index.ts +43 -0
  881. package/modules/vitals/src/vitals.ts +265 -0
  882. package/modules/xr/README.md +25 -0
  883. package/modules/xr/src/camera-rig.ts +146 -0
  884. package/modules/xr/src/debug.ts +59 -0
  885. package/modules/xr/src/frame-loop.ts +197 -0
  886. package/modules/xr/src/index.ts +16 -0
  887. package/modules/xr/src/input.ts +208 -0
  888. package/modules/xr/src/layer.ts +140 -0
  889. package/modules/xr/src/session.ts +156 -0
  890. package/modules/xr/src/sim-module.ts +23 -0
  891. package/modules/xr/src/types.ts +32 -0
  892. package/modules/xr/src/webxr.d.ts +182 -0
  893. package/modules/xr/src/xr-module.ts +179 -0
  894. package/package.json +232 -3
  895. package/shader-graph/src/chunks.ts +349 -0
  896. package/shader-graph/src/compiler.ts +732 -0
  897. package/shader-graph/src/compute-compiler.ts +453 -0
  898. package/shader-graph/src/compute-graph.ts +112 -0
  899. package/shader-graph/src/compute-profiles.ts +54 -0
  900. package/shader-graph/src/graph.ts +52 -0
  901. package/shader-graph/src/index.ts +53 -0
  902. package/shader-graph/src/profiles.ts +407 -0
  903. package/shader-graph/src/validator.ts +48 -0
  904. package/shader-graph/src/wgsl-binding-validator.ts +143 -0
  905. package/shader-graph/src/wgsl-struct-validator.ts +245 -0
  906. package/shader-graph/src/wgsl-struct.ts +397 -0
  907. package/test/src/index.ts +7 -0
  908. package/test/src/vision-test.ts +369 -0
@@ -0,0 +1,1345 @@
1
+ // ============================================================================
2
+ // Model Renderer — renders imported 3D models (FBX/GLTF/OBJ) via WebGPU
3
+ //
4
+ // Bindless binding model: per-draw material data lives in a shared material
5
+ // SSBO (@group(3)), indexed by a `materialIndex` written into the per-draw
6
+ // uniform struct (@group(0)). Textures are registered into the
7
+ // BindlessTextureRegistry's texture_2d_array buckets. The bindless bind group
8
+ // (@group(3)) is set once per frame by the caller (via setBindlessBindGroup)
9
+ // — no per-draw bind-group creation or texture bind-group churn.
10
+ // ============================================================================
11
+
12
+ import {
13
+ calculateViewProj,
14
+ createLogger, createValidatedShaderModule, DEPTH_FORMAT,
15
+ MSAA_SAMPLE_COUNT,
16
+ type BindlessMaterialManager,
17
+ type BindlessTextureRegistry,
18
+ type CameraState,
19
+ type MaterialParams
20
+ } from "@downdraft/engine";
21
+ import type { MaterialData, MeshData } from "@downdraft/engine/libraries/models";
22
+ import MODEL_WGSL from "./shaders/model.wgsl?raw" with { type: "text" };
23
+
24
+ const log = createLogger();
25
+
26
+
27
+ interface ModelGPUResources {
28
+ vertexBuffer: GPUBuffer;
29
+ indexBuffer: GPUBuffer;
30
+ indexCount: number;
31
+ indexFormat: GPUIndexFormat;
32
+ uniformOffset: number;
33
+ materialIndex: number;
34
+ // Skinning (optional): second vertex buffer with joints+weights, and a flag
35
+ // to select the skinned pipeline + skin-matrix bind group.
36
+ skinVertexBuffer?: GPUBuffer;
37
+ skinned: boolean;
38
+ }
39
+
40
+ export interface ModelRendererBindlessDeps {
41
+ registry: BindlessTextureRegistry;
42
+ materialManager: BindlessMaterialManager;
43
+ /** Bind group layout for @group(3) bindless resources. */
44
+ bindGroupLayout: GPUBindGroupLayout;
45
+ }
46
+
47
+ export class ModelRenderer {
48
+ private device: GPUDevice;
49
+ private format: GPUTextureFormat;
50
+ private pipeline: GPURenderPipeline | null = null;
51
+ /** Inverted-hull outline pipeline (front-face culled, vertex-extruded). */
52
+ private maskPipeline: GPURenderPipeline | null = null;
53
+ /** Depth-only pipeline for shadow map rendering (no color targets). */
54
+ private depthPipeline: GPURenderPipeline | null = null;
55
+ private bindGroupLayout: GPUBindGroupLayout | null = null;
56
+ private uniformBuffer: GPUBuffer | null = null;
57
+ private bindGroup: GPUBindGroup | null = null;
58
+ private perSlotBindGroups: GPUBindGroup[] = [];
59
+
60
+ // 4096 slots × 256 bytes = 1 MiB uniform buffer. Character FBX models can
61
+ // have hundreds of meshes (e.g. Aisha has 374 geometry nodes → 374 uniform
62
+ // slots). The old limit of 256 was exhausted by a single character model,
63
+ // leaving zero slots for prop models — props then uploaded 0 meshes but
64
+ // still had their nodeId written to the SAB, making them invisible (skipped
65
+ // by builtin rendering, early-returned by model rendering) while their
66
+ // physics bodies still provided collision.
67
+ private static readonly MAX_MODELS = 4096;
68
+ private static readonly UNIFORM_SIZE = 256; // 64 floats, padded to 256
69
+ /** Initial bone buffer capacity. Grows dynamically when larger skeletons are loaded. */
70
+ private static readonly INITIAL_BONE_CAPACITY = 256;
71
+
72
+ private modelResources: Map<string, ModelGPUResources[]> = new Map();
73
+ // Per-model instance uniform base offsets. Key = nodeId, value = array of
74
+ // base offsets (in bytes) for each instance. Instance 0 uses the original
75
+ // offsets from modelResources; instances 1+ use allocated offsets from here.
76
+ private modelInstanceOffsets: Map<string, number[]> = new Map();
77
+ private viewProjCache: Float32Array | null = null;
78
+ private cameraPosCache: [number, number, number] = [0, 0, 0];
79
+ // Light state for the fragment shader (defaults match the old hardcoded values).
80
+ // lightDir is in world space (does not need to be normalized — the shader normalizes).
81
+ private lightDirCache: [number, number, number] = [0.5, 0.8, 0.3];
82
+ private lightAmbientCache = 0.5;
83
+ private lightIntensityCache = 0.5;
84
+ private nextUniformOffset = 0;
85
+ private reusableUniforms = new Float32Array(64);
86
+ private reusableUniformsU32 = new Uint32Array(this.reusableUniforms.buffer);
87
+ private textureLoadVersion = new Map<string, number>();
88
+
89
+ // Skinning: a shared storage buffer of bone matrices (MAX_BONES * mat4),
90
+ // updated per-frame via updateSkinMatrices. Bound at @group(1) for skinned
91
+ // draws only.
92
+ private skinMatrixBuffer: GPUBuffer | null = null;
93
+ private skinBindGroupLayout: GPUBindGroupLayout | null = null;
94
+ private skinBindGroup: GPUBindGroup | null = null;
95
+ private skinnedPipeline: GPURenderPipeline | null = null;
96
+ private skinBindGroupSetThisFrame = false;
97
+ /** Current bone capacity (number of mat4s the skin buffer can hold). Grows on demand. */
98
+ private boneCapacity = ModelRenderer.INITIAL_BONE_CAPACITY;
99
+
100
+ // Bindless deps
101
+ private bindless: ModelRendererBindlessDeps | null = null;
102
+ private bindlessLayout: GPUBindGroupLayout | null = null;
103
+ private bindlessBindGroup: GPUBindGroup | null = null;
104
+ private bindlessBindGroupSetThisFrame = false;
105
+
106
+ // Frame-global lighting bind group (group 2). Provides sun color, hemisphere
107
+ // ambient, and point lights. Defaults to a neutral white-sun / no-point-light
108
+ // bind group so existing consumers see no visual change. Games with colored
109
+ // lighting call setFrameLightingBindGroup() each frame to override.
110
+ private frameLightingLayout: GPUBindGroupLayout | null = null;
111
+ private defaultFrameLightingBuffer: GPUBuffer | null = null;
112
+ private defaultFrameLightingBg: GPUBindGroup | null = null;
113
+ private frameLightingBg: GPUBindGroup | null = null;
114
+ private frameLightingBgSetThisFrame = false;
115
+ /** materialIndex for the default white material (albedo = default white layer). */
116
+ private defaultMaterialIndex = 0;
117
+ /** Composite key `${nodeId}:${matIdx}` → bindless materialIndex, so reupload reuses the same slot. */
118
+ private meshMaterialIndex = new Map<string, number>();
119
+ /** Composite key `${nodeId}:${matIdx}` → texture sourceId (used for async update + cleanup). */
120
+ private meshTextureSourceId = new Map<string, string>();
121
+ private meshNormalTextureSourceId = new Map<string, string>();
122
+ /** Track the current albedo handle per materialKey (for normal texture updates). */
123
+ private meshAlbedoHandle = new Map<string, number>();
124
+ /** When true, newly registered mesh textures get a full mip chain.
125
+ * Toggle takes effect on the next uploadModel/reuploadModel (textures are
126
+ * re-registered into a mipmapped bucket). */
127
+ private mipmapsEnabled = false;
128
+
129
+ constructor(device: GPUDevice, format: GPUTextureFormat) {
130
+ this.device = device;
131
+ this.format = format;
132
+ }
133
+
134
+ /** Provide the bindless texture registry + material manager. Required before init. */
135
+ setBindlessDeps(deps: ModelRendererBindlessDeps): void {
136
+ this.bindless = deps;
137
+ this.bindlessLayout = deps.bindGroupLayout;
138
+ // Register a default white material (all texture slots → default white handle).
139
+ this.defaultMaterialIndex = deps.materialManager.registerMaterial({
140
+ baseColor: [1, 1, 1, 1],
141
+ roughness: 1,
142
+ metallic: 0,
143
+ emissiveIntensity: 0,
144
+ albedoTexHandle: deps.registry.defaultWhiteHandle,
145
+ normalTexHandle: deps.registry.defaultNormalHandle,
146
+ metallicRoughnessTexHandle: deps.registry.defaultWhiteHandle,
147
+ aoTexHandle: deps.registry.defaultWhiteHandle,
148
+ emissiveTexHandle: deps.registry.defaultWhiteHandle,
149
+ });
150
+ }
151
+
152
+ /** Set the shared bindless bind group (@group(3)) for the frame. */
153
+ setBindlessBindGroup(bg: GPUBindGroup | null): void {
154
+ this.bindlessBindGroup = bg;
155
+ this.bindlessBindGroupSetThisFrame = false;
156
+ }
157
+
158
+ /**
159
+ * Set the frame-global lighting bind group (group 2). Provides sun color,
160
+ * hemisphere ambient, and point lights via a uniform buffer matching the
161
+ * FrameLighting WGSL struct. Call once per frame before render(). If never
162
+ * called, a default neutral bind group is used (white sun, no point lights).
163
+ */
164
+ setFrameLightingBindGroup(bg: GPUBindGroup | null): void {
165
+ this.frameLightingBg = bg;
166
+ this.frameLightingBgSetThisFrame = false;
167
+ }
168
+
169
+ /** Get the frame-lighting bind group layout (for creating compatible bind groups). */
170
+ getFrameLightingLayout(): GPUBindGroupLayout | null {
171
+ return this.frameLightingLayout;
172
+ }
173
+
174
+ async init(): Promise<void> {
175
+ this.uniformBuffer = this.device.createBuffer({
176
+ size: ModelRenderer.MAX_MODELS * ModelRenderer.UNIFORM_SIZE,
177
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
178
+ });
179
+
180
+ // Explicit bind group layout for group(0) — per-draw uniform with
181
+ // hasDynamicOffset so we can index into the uniform buffer per instance.
182
+ this.bindGroupLayout = this.device.createBindGroupLayout({
183
+ entries: [
184
+ {
185
+ binding: 0,
186
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
187
+ buffer: { type: "uniform", hasDynamicOffset: true },
188
+ },
189
+ ],
190
+ });
191
+
192
+ this.bindGroup = this.device.createBindGroup({
193
+ layout: this.bindGroupLayout,
194
+ entries: [
195
+ { binding: 0, resource: { buffer: this.uniformBuffer, size: ModelRenderer.UNIFORM_SIZE } },
196
+ ],
197
+ });
198
+ // Per-slot bind groups with the uniform offset baked into the resource.
199
+ // The layout declares hasDynamicOffset, so bind calls must pass [0].
200
+ this.perSlotBindGroups = [];
201
+ for (let i = 0; i < ModelRenderer.MAX_MODELS; i++) {
202
+ this.perSlotBindGroups.push(this.device.createBindGroup({
203
+ layout: this.bindGroupLayout,
204
+ entries: [{ binding: 0, resource: { buffer: this.uniformBuffer, offset: i * ModelRenderer.UNIFORM_SIZE, size: ModelRenderer.UNIFORM_SIZE } }],
205
+ }));
206
+ }
207
+
208
+ // Frame-lighting bind group layout (group 2): a single uniform buffer
209
+ // providing sun color, hemisphere ambient, and point lights. A default
210
+ // neutral bind group is created so existing consumers see no visual change.
211
+ this.frameLightingLayout = this.device.createBindGroupLayout({
212
+ label: "model-frame-lighting-layout",
213
+ entries: [
214
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
215
+ ],
216
+ });
217
+ // Default frame-lighting UBO: white sun, neutral hemisphere, no point lights.
218
+ // Layout: sunDir(3)+ambientIntensity(1), sunColor(3)+pointLightCount(1),
219
+ // skyAmbient(3)+pad(1), groundAmbient(3)+pad(1), pointLights(16 vec4s)
220
+ const defaultLightingData = new Float32Array(80);
221
+ // sunDir must be non-zero: fs_main does normalize(sunDir), and a zero
222
+ // vector produces NaN that turns the entire fragment output black.
223
+ defaultLightingData[0] = 0.4; defaultLightingData[1] = 0.8; defaultLightingData[2] = 0.3; // sunDir
224
+ defaultLightingData[3] = 1.0; // ambientIntensity
225
+ defaultLightingData[4] = 1.0; defaultLightingData[5] = 1.0; defaultLightingData[6] = 1.0; // sunColor
226
+ defaultLightingData[8] = 1.0; defaultLightingData[9] = 1.0; defaultLightingData[10] = 1.0; // skyAmbient
227
+ defaultLightingData[12] = 1.0; defaultLightingData[13] = 1.0; defaultLightingData[14] = 1.0; // groundAmbient
228
+ this.defaultFrameLightingBuffer = this.device.createBuffer({
229
+ label: "model-default-frame-lighting",
230
+ size: defaultLightingData.byteLength,
231
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
232
+ });
233
+ this.device.queue.writeBuffer(this.defaultFrameLightingBuffer, 0, defaultLightingData as Float32Array<ArrayBuffer>);
234
+ this.defaultFrameLightingBg = this.device.createBindGroup({
235
+ label: "model-default-frame-lighting-bg",
236
+ layout: this.frameLightingLayout,
237
+ entries: [{ binding: 0, resource: { buffer: this.defaultFrameLightingBuffer } }],
238
+ });
239
+
240
+ const shaderModule = createValidatedShaderModule(this.device, { code: MODEL_WGSL, label: "ModelRenderer" });
241
+ // Explicit pipeline layout: group(0) = per-draw uniform (dynamic offset),
242
+ // group(2) = frame-lighting UBO, group(3) = bindless materials SSBO +
243
+ // texture arrays. Group 1 is unused by the non-skinned pipeline (empty layout).
244
+ const emptyLayout = this.device.createBindGroupLayout({ entries: [] });
245
+ const pipelineLayout = this.device.createPipelineLayout({
246
+ bindGroupLayouts: [this.bindGroupLayout, emptyLayout, this.frameLightingLayout, this.bindlessLayout],
247
+ });
248
+ this.pipeline = this.device.createRenderPipeline({
249
+ layout: pipelineLayout,
250
+ vertex: {
251
+ module: shaderModule,
252
+ entryPoint: "vs_main",
253
+ buffers: [
254
+ {
255
+ arrayStride: 44, // pos3 + normal3 + uv2 + color3 = 11 floats
256
+ attributes: [
257
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
258
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
259
+ { shaderLocation: 2, offset: 24, format: "float32x2" },
260
+ { shaderLocation: 3, offset: 32, format: "float32x3" },
261
+ ],
262
+ },
263
+ ],
264
+ },
265
+ fragment: {
266
+ module: shaderModule,
267
+ entryPoint: "fs_main",
268
+ targets: [{ format: this.format }],
269
+ },
270
+ primitive: { topology: "triangle-list" },
271
+ multisample: { count: MSAA_SAMPLE_COUNT },
272
+ depthStencil: {
273
+ format: DEPTH_FORMAT,
274
+ depthWriteEnabled: true,
275
+ depthCompare: "less",
276
+ },
277
+ });
278
+
279
+ // ── Skinning resources ──
280
+ // Shared storage buffer for bone matrices. Starts at INITIAL_BONE_CAPACITY
281
+ // and grows on demand when larger skeletons are loaded.
282
+ this.skinMatrixBuffer = this.device.createBuffer({
283
+ size: this.boneCapacity * 64, // mat4x4 = 64 bytes
284
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
285
+ });
286
+
287
+ // Bind group layout for @group(1): a single read-only storage buffer.
288
+ this.skinBindGroupLayout = this.device.createBindGroupLayout({
289
+ entries: [
290
+ {
291
+ binding: 0,
292
+ visibility: GPUShaderStage.VERTEX,
293
+ buffer: { type: "read-only-storage" },
294
+ },
295
+ ],
296
+ });
297
+
298
+ this.skinBindGroup = this.device.createBindGroup({
299
+ layout: this.skinBindGroupLayout,
300
+ entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
301
+ });
302
+
303
+ // Skinned pipeline: same pipeline layout (group 1 is now the skin layout
304
+ // instead of an empty layout, group 2 is the frame-lighting layout), with
305
+ // a second vertex buffer for joints + weights and the vs_skinned entry point.
306
+ const skinnedPipelineLayout = this.device.createPipelineLayout({
307
+ bindGroupLayouts: [this.bindGroupLayout, this.skinBindGroupLayout, this.frameLightingLayout, this.bindlessLayout],
308
+ });
309
+ this.skinnedPipeline = this.device.createRenderPipeline({
310
+ layout: skinnedPipelineLayout,
311
+ vertex: {
312
+ module: shaderModule,
313
+ entryPoint: "vs_skinned",
314
+ buffers: [
315
+ {
316
+ arrayStride: 44, // pos3 + normal3 + uv2 + color3 = 11 floats
317
+ attributes: [
318
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
319
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
320
+ { shaderLocation: 2, offset: 24, format: "float32x2" },
321
+ { shaderLocation: 3, offset: 32, format: "float32x3" },
322
+ ],
323
+ },
324
+ {
325
+ // joints (4 x uint8 packed as uint32x4 = 16 bytes) + weights (4 x float32 = 16 bytes) = 32 bytes
326
+ arrayStride: 32,
327
+ attributes: [
328
+ { shaderLocation: 4, offset: 0, format: "uint32x4" },
329
+ { shaderLocation: 5, offset: 16, format: "float32x4" },
330
+ ],
331
+ },
332
+ ],
333
+ },
334
+ fragment: {
335
+ module: shaderModule,
336
+ entryPoint: "fs_main",
337
+ targets: [{ format: this.format }],
338
+ },
339
+ primitive: { topology: "triangle-list" },
340
+ multisample: { count: MSAA_SAMPLE_COUNT },
341
+ depthStencil: {
342
+ format: DEPTH_FORMAT,
343
+ depthWriteEnabled: true,
344
+ depthCompare: "less",
345
+ },
346
+ });
347
+
348
+ // ── Mask pipeline for post-process outline ──
349
+ // Renders the model as solid white to a mask texture. Uses the same
350
+ // vertex shader as the normal render (vs_main) with back-face culling
351
+ // and depth testing (read-only) so the mask respects occlusion.
352
+ this.maskPipeline = this.device.createRenderPipeline({
353
+ layout: pipelineLayout,
354
+ vertex: {
355
+ module: shaderModule,
356
+ entryPoint: "vs_main",
357
+ buffers: [
358
+ {
359
+ arrayStride: 44,
360
+ attributes: [
361
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
362
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
363
+ { shaderLocation: 2, offset: 24, format: "float32x2" },
364
+ { shaderLocation: 3, offset: 32, format: "float32x3" },
365
+ ],
366
+ },
367
+ ],
368
+ },
369
+ fragment: {
370
+ module: shaderModule,
371
+ entryPoint: "fs_mask",
372
+ targets: [{ format: "rgba8unorm" }],
373
+ },
374
+ primitive: { topology: "triangle-list", cullMode: "back" },
375
+ depthStencil: {
376
+ format: DEPTH_FORMAT,
377
+ depthWriteEnabled: false,
378
+ depthCompare: "less-equal",
379
+ },
380
+ });
381
+
382
+ // ── Depth-only pipeline for shadow map rendering ──
383
+ // Reuses vs_main (same vertex transform) but has no fragment shader /
384
+ // color targets — only writes depth. Uses a minimal pipeline layout with
385
+ // just group(0) (the per-draw uniform with dynamic offset) so the caller
386
+ // doesn't need to bind lighting/bindless groups for the shadow pass.
387
+ const depthPipelineLayout = this.device.createPipelineLayout({
388
+ bindGroupLayouts: [this.bindGroupLayout],
389
+ });
390
+ this.depthPipeline = this.device.createRenderPipeline({
391
+ label: "model-depth-only",
392
+ layout: depthPipelineLayout,
393
+ vertex: {
394
+ module: shaderModule,
395
+ entryPoint: "vs_main",
396
+ buffers: [
397
+ {
398
+ arrayStride: 44,
399
+ attributes: [
400
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
401
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
402
+ { shaderLocation: 2, offset: 24, format: "float32x2" },
403
+ { shaderLocation: 3, offset: 32, format: "float32x3" },
404
+ ],
405
+ },
406
+ ],
407
+ },
408
+ primitive: { topology: "triangle-list", cullMode: "back" },
409
+ depthStencil: {
410
+ format: DEPTH_FORMAT,
411
+ depthWriteEnabled: true,
412
+ depthCompare: "less",
413
+ },
414
+ });
415
+ }
416
+
417
+ /**
418
+ * Upload a model's meshes to GPU buffers and allocate uniform slots.
419
+ * Returns the number of meshes actually uploaded — this may be less than
420
+ * `meshes.length` (or 0) if the uniform buffer is full. Callers should
421
+ * check the return value and fall back appropriately when 0 meshes are
422
+ * uploaded (e.g. render as a builtin cube instead of leaving the entity
423
+ * invisible).
424
+ */
425
+ uploadModel(nodeId: string, meshes: MeshData[], materials?: MaterialData[], modelBaseUrl?: string): number {
426
+ this.removeModel(nodeId);
427
+
428
+ const resources: ModelGPUResources[] = [];
429
+ let uniformOffset = this.nextUniformOffset;
430
+
431
+ for (let i = 0; i < meshes.length && uniformOffset < ModelRenderer.MAX_MODELS; i++) {
432
+ const mesh = meshes[i];
433
+ // Degenerate mesh (partial parse failure, empty geometry): nothing to
434
+ // draw, and a 0-byte index buffer trips wgpu-native's setIndexBuffer
435
+ // validation — a Rust panic that aborts the whole process.
436
+ if (mesh.indexCount === 0 || mesh.vertexCount === 0 || mesh.indices.byteLength === 0) continue;
437
+ const vertexCount = mesh.vertexCount;
438
+ const stride = 11; // pos3 + normal3 + uv2 + color3
439
+ const interleaved = new Float32Array(vertexCount * stride);
440
+
441
+ for (let v = 0; v < vertexCount; v++) {
442
+ interleaved[v * stride] = mesh.vertices[v * 6];
443
+ interleaved[v * stride + 1] = mesh.vertices[v * 6 + 1];
444
+ interleaved[v * stride + 2] = mesh.vertices[v * 6 + 2];
445
+ interleaved[v * stride + 3] = mesh.vertices[v * 6 + 3];
446
+ interleaved[v * stride + 4] = mesh.vertices[v * 6 + 4];
447
+ interleaved[v * stride + 5] = mesh.vertices[v * 6 + 5];
448
+ interleaved[v * stride + 6] = mesh.uvs ? mesh.uvs[v * 2] : 0;
449
+ interleaved[v * stride + 7] = mesh.uvs ? mesh.uvs[v * 2 + 1] : 0;
450
+ interleaved[v * stride + 8] = mesh.colors ? mesh.colors[v * 3] : 1;
451
+ interleaved[v * stride + 9] = mesh.colors ? mesh.colors[v * 3 + 1] : 1;
452
+ interleaved[v * stride + 10] = mesh.colors ? mesh.colors[v * 3 + 2] : 1;
453
+ }
454
+
455
+ const vertexBuffer = this.device.createBuffer({
456
+ size: interleaved.byteLength,
457
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
458
+ });
459
+ this.device.queue.writeBuffer(vertexBuffer, 0, interleaved as Float32Array<ArrayBuffer>);
460
+
461
+ const indexFormat: GPUIndexFormat =
462
+ mesh.indices instanceof Uint32Array ? "uint32" : "uint16";
463
+ const indexByteLength = mesh.indices.byteLength;
464
+ const paddedIndexSize = Math.ceil(indexByteLength / 4) * 4;
465
+ const indexBuffer = this.device.createBuffer({
466
+ size: paddedIndexSize,
467
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
468
+ });
469
+ if (paddedIndexSize === indexByteLength) {
470
+ this.device.queue.writeBuffer(indexBuffer, 0, mesh.indices as (Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer>));
471
+ } else {
472
+ const padded = new Uint8Array(paddedIndexSize);
473
+ padded.set(new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, indexByteLength));
474
+ this.device.queue.writeBuffer(indexBuffer, 0, padded);
475
+ }
476
+
477
+ // Determine the bindless material index for this mesh based on its
478
+ // materialIndex field (index into ModelData.materials). Each unique
479
+ // (nodeId, matIdx) gets its own bindless material slot so multi-material
480
+ // models render each sub-mesh with its own texture.
481
+ const matIdx = mesh.materialIndex ?? 0;
482
+ const materialKey = `${nodeId}:${matIdx}`;
483
+ let bindlessMatIndex = this.defaultMaterialIndex;
484
+ if (this.bindless) {
485
+ const existing = this.meshMaterialIndex.get(materialKey);
486
+ if (existing !== undefined) {
487
+ bindlessMatIndex = existing;
488
+ } else {
489
+ const mat = materials?.[matIdx];
490
+ bindlessMatIndex = this.bindless.materialManager.registerMaterial({
491
+ baseColor: mat?.baseColor ?? [1, 1, 1, 1],
492
+ roughness: mat?.roughness ?? 1,
493
+ metallic: mat?.metallic ?? 0,
494
+ emissiveIntensity: 0,
495
+ albedoTexHandle: this.bindless.registry.defaultWhiteHandle,
496
+ normalTexHandle: this.bindless.registry.defaultNormalHandle,
497
+ metallicRoughnessTexHandle: this.bindless.registry.defaultWhiteHandle,
498
+ aoTexHandle: this.bindless.registry.defaultWhiteHandle,
499
+ emissiveTexHandle: this.bindless.registry.defaultWhiteHandle,
500
+ });
501
+ this.meshMaterialIndex.set(materialKey, bindlessMatIndex);
502
+
503
+ // Async load this material's texture (embedded data or external URI)
504
+ if (mat?.textureData && mat.textureData.byteLength > 0) {
505
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
506
+ this.textureLoadVersion.set(materialKey, version);
507
+ this.loadMeshTexture(materialKey, mat.textureData, version);
508
+ } else if (mat?.textureUri && modelBaseUrl) {
509
+ // Resolve relative texture URI against the model's base URL.
510
+ const resolvedUri = modelBaseUrl + mat.textureUri;
511
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
512
+ this.textureLoadVersion.set(materialKey, version);
513
+ this.loadMeshTextureFromUri(materialKey, resolvedUri, version);
514
+ }
515
+ // Async load the normal texture if present.
516
+ if ((mat?.normalTextureData && mat.normalTextureData.byteLength > 0) || mat?.normalTextureUri) {
517
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
518
+ this.loadMeshNormalTexture(materialKey, mat.normalTextureData ?? null, mat?.normalTextureUri, version);
519
+ }
520
+ }
521
+ }
522
+
523
+ // Build the skin vertex buffer (joints + weights) if this mesh is skinned.
524
+ const skinVertexBuffer = this.buildSkinVertexBuffer(mesh);
525
+
526
+ resources.push({
527
+ vertexBuffer,
528
+ indexBuffer,
529
+ indexCount: mesh.indexCount,
530
+ indexFormat,
531
+ uniformOffset: uniformOffset * ModelRenderer.UNIFORM_SIZE,
532
+ materialIndex: bindlessMatIndex,
533
+ skinVertexBuffer,
534
+ skinned: skinVertexBuffer !== undefined,
535
+ });
536
+ uniformOffset++;
537
+ }
538
+
539
+ this.nextUniformOffset = uniformOffset;
540
+ this.modelResources.set(nodeId, resources);
541
+
542
+ if (resources.length < meshes.length) {
543
+ log.warn(
544
+ "ModelRenderer",
545
+ `Uniform buffer full: uploaded ${resources.length}/${meshes.length} meshes for "${nodeId}" ` +
546
+ `(${uniformOffset}/${ModelRenderer.MAX_MODELS} slots used). ` +
547
+ `Increase MAX_MODELS to avoid invisible models.`,
548
+ );
549
+ }
550
+
551
+ return resources.length;
552
+ }
553
+
554
+ /**
555
+ * Allocate an additional uniform slot for an existing model, sharing its
556
+ * vertex/index buffers. Returns the instance index (1-based, since instance
557
+ * 0 uses the original offsets), or -1 if the model doesn't exist or the
558
+ * uniform buffer is full.
559
+ *
560
+ * Use with render()'s instanceIndex parameter to draw multiple instances of
561
+ * the same model at different positions in a single frame. Without this,
562
+ * multiple render() calls with the same nodeId overwrite each other's
563
+ * uniforms (device.queue.writeBuffer is a queue-level op, so all writes
564
+ * execute before the render pass — only the last write is visible).
565
+ */
566
+ allocateInstance(nodeId: string): number {
567
+ const resources = this.modelResources.get(nodeId);
568
+ if (!resources || resources.length === 0) return -1;
569
+
570
+ const meshCount = resources.length;
571
+ if (this.nextUniformOffset + meshCount > ModelRenderer.MAX_MODELS) return -1;
572
+
573
+ // Allocate `meshCount` contiguous uniform slots for this instance.
574
+ const baseOffset = this.nextUniformOffset * ModelRenderer.UNIFORM_SIZE;
575
+ this.nextUniformOffset += meshCount;
576
+
577
+ const instances = this.modelInstanceOffsets.get(nodeId) ?? [];
578
+ instances.push(baseOffset);
579
+ this.modelInstanceOffsets.set(nodeId, instances);
580
+ return instances.length; // 1-based instance index
581
+ }
582
+
583
+ /**
584
+ * Free all instance uniform slots for a model (except instance 0 which is
585
+ * tied to the model upload). Call when an instance is no longer needed.
586
+ */
587
+ freeInstances(nodeId: string): void {
588
+ const instances = this.modelInstanceOffsets.get(nodeId);
589
+ if (instances && instances.length > 0) {
590
+ // Reclaim uniform slots (simple: just reset nextUniformOffset if these
591
+ // were the last allocated. A production implementation would use a free
592
+ // list, but instance lifetimes are typically long-lived.)
593
+ this.nextUniformOffset = Math.max(0, this.nextUniformOffset - instances.length * this.modelResources.get(nodeId)!.length);
594
+ instances.length = 0;
595
+ }
596
+ this.modelInstanceOffsets.delete(nodeId);
597
+ }
598
+
599
+ /** Get the number of allocated instances for a model (0 = only the original). */
600
+ getInstanceCount(nodeId: string): number {
601
+ return this.modelInstanceOffsets.get(nodeId)?.length ?? 0;
602
+ }
603
+
604
+ /**
605
+ * Build the per-mesh skin vertex buffer: joints as uint32x4 (4 bone indices,
606
+ * upcast from uint8/uint16/uint32 source) followed by weights as float32x4.
607
+ * 32 bytes/vertex. Returns undefined when the mesh has no skinning data.
608
+ */
609
+ private buildSkinVertexBuffer(mesh: MeshData): GPUBuffer | undefined {
610
+ if (!mesh.joints || !mesh.weights || mesh.joints.length < mesh.vertexCount * 4) return undefined;
611
+ const vertexCount = mesh.vertexCount;
612
+ // 8 slots of 4 bytes per vertex: [j0,j1,j2,j3, w0,w1,w2,w3] = 32 bytes.
613
+ const data = new Float32Array(vertexCount * 8);
614
+ const dataU32 = new Uint32Array(data.buffer);
615
+ for (let v = 0; v < vertexCount; v++) {
616
+ const src = v * 4;
617
+ const dst = v * 8;
618
+ dataU32[dst] = mesh.joints[src];
619
+ dataU32[dst + 1] = mesh.joints[src + 1];
620
+ dataU32[dst + 2] = mesh.joints[src + 2];
621
+ dataU32[dst + 3] = mesh.joints[src + 3];
622
+ data[dst + 4] = mesh.weights[src];
623
+ data[dst + 5] = mesh.weights[src + 1];
624
+ data[dst + 6] = mesh.weights[src + 2];
625
+ data[dst + 7] = mesh.weights[src + 3];
626
+ }
627
+ const buffer = this.device.createBuffer({
628
+ size: data.byteLength,
629
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
630
+ });
631
+ this.device.queue.writeBuffer(buffer, 0, data as Float32Array<ArrayBuffer>);
632
+ return buffer;
633
+ }
634
+
635
+ private async loadMeshTexture(materialKey: string, textureData: ArrayBuffer, version: number): Promise<void> {
636
+ if (!this.bindless) return;
637
+ try {
638
+ const blob = new Blob([textureData]);
639
+ const imageBitmap = await createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });
640
+
641
+ if (this.textureLoadVersion.get(materialKey) !== version) {
642
+ imageBitmap.close();
643
+ return;
644
+ }
645
+
646
+ // Register the bitmap into the bindless texture registry. The sourceId
647
+ // is the materialKey so reuploadModel can reuse/update it.
648
+ const sourceId = `model:${materialKey}`;
649
+ const existing = this.bindless.registry.getRegistration(sourceId);
650
+ let handle: number;
651
+ if (existing) {
652
+ // Update in place (same dimensions expected).
653
+ this.bindless.registry.updateFromImageBitmap(sourceId, imageBitmap);
654
+ handle = existing.handle;
655
+ } else {
656
+ // Atlas textures must not be mipmapped — adjacent atlas regions bleed
657
+ // into each other during mip downsample, causing distance-based color shifts.
658
+ // Albedo/color textures are sRGB-encoded (e.g. Kenney colormap.png) — use
659
+ // the srgb format so the GPU decodes to linear on sample. Treating them as
660
+ // linear (rgba8unorm) washes out midtones since the pipeline renders in
661
+ // linear HDR and re-encodes to sRGB at the tonemap/output stage.
662
+ const reg = this.bindless.registry.registerFromImageBitmap(sourceId, imageBitmap, "rgba8unorm-srgb", 1, this.mipmapsEnabled);
663
+ handle = reg.handle;
664
+ }
665
+ this.meshTextureSourceId.set(materialKey, sourceId);
666
+ this.meshAlbedoHandle.set(materialKey, handle);
667
+
668
+ // Update the material to point at the real albedo texture.
669
+ const materialIndex = this.meshMaterialIndex.get(materialKey);
670
+ if (materialIndex !== undefined) {
671
+ const matParams: MaterialParams = {
672
+ baseColor: [1, 1, 1, 1],
673
+ roughness: 1,
674
+ metallic: 0,
675
+ emissiveIntensity: 0,
676
+ albedoTexHandle: handle,
677
+ normalTexHandle: this.bindless.registry.defaultNormalHandle,
678
+ metallicRoughnessTexHandle: this.bindless.registry.defaultWhiteHandle,
679
+ aoTexHandle: this.bindless.registry.defaultWhiteHandle,
680
+ emissiveTexHandle: this.bindless.registry.defaultWhiteHandle,
681
+ };
682
+ this.bindless.materialManager.updateMaterial(materialIndex, matParams);
683
+ }
684
+
685
+ log.info("ModelRenderer", `Texture ready for ${materialKey}: ${imageBitmap.width}x${imageBitmap.height} (bindless)`);
686
+ imageBitmap.close();
687
+ } catch (e) {
688
+ log.error("ModelRenderer", `Failed to load texture for ${materialKey}: ${e}`);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Async-load a base color texture from an external URI (for GLBs that
694
+ * reference textures by relative path instead of embedding them).
695
+ */
696
+ private async loadMeshTextureFromUri(materialKey: string, textureUri: string, version: number): Promise<void> {
697
+ if (!this.bindless) return;
698
+ try {
699
+ const resp = await fetch(textureUri);
700
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
701
+ const blob = await resp.blob();
702
+ const imageBitmap = await createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });
703
+
704
+ if (this.textureLoadVersion.get(materialKey) !== version) {
705
+ imageBitmap.close();
706
+ return;
707
+ }
708
+
709
+ const sourceId = `model:${materialKey}`;
710
+ const existing = this.bindless.registry.getRegistration(sourceId);
711
+ let handle: number;
712
+ if (existing) {
713
+ this.bindless.registry.updateFromImageBitmap(sourceId, imageBitmap);
714
+ handle = existing.handle;
715
+ } else {
716
+ // sRGB-encoded albedo (see loadMeshTexture for rationale).
717
+ const reg = this.bindless.registry.registerFromImageBitmap(sourceId, imageBitmap, "rgba8unorm-srgb", 1, this.mipmapsEnabled);
718
+ handle = reg.handle;
719
+ }
720
+ this.meshTextureSourceId.set(materialKey, sourceId);
721
+ this.meshAlbedoHandle.set(materialKey, handle);
722
+
723
+ const materialIndex = this.meshMaterialIndex.get(materialKey);
724
+ if (materialIndex !== undefined) {
725
+ const matParams: MaterialParams = {
726
+ baseColor: [1, 1, 1, 1],
727
+ roughness: 1,
728
+ metallic: 0,
729
+ emissiveIntensity: 0,
730
+ albedoTexHandle: handle,
731
+ normalTexHandle: this.bindless.registry.defaultNormalHandle,
732
+ metallicRoughnessTexHandle: this.bindless.registry.defaultWhiteHandle,
733
+ aoTexHandle: this.bindless.registry.defaultWhiteHandle,
734
+ emissiveTexHandle: this.bindless.registry.defaultWhiteHandle,
735
+ };
736
+ this.bindless.materialManager.updateMaterial(materialIndex, matParams);
737
+ }
738
+
739
+ imageBitmap.close();
740
+ } catch (e) {
741
+ log.error("ModelRenderer", `Failed to load texture from URI for ${materialKey} (${textureUri}): ${e}`);
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Async-load a normal texture for a material and update the bindless material
747
+ * to point at it. Normal textures are loaded from embedded bufferView data
748
+ * (normalTextureData) or fetched from normalTextureUri.
749
+ */
750
+ private async loadMeshNormalTexture(materialKey: string, textureData: ArrayBuffer | null, textureUri: string | undefined, version: number): Promise<void> {
751
+ if (!this.bindless) return;
752
+ try {
753
+ let imageBitmap: ImageBitmap;
754
+ if (textureData && textureData.byteLength > 0) {
755
+ const blob = new Blob([textureData]);
756
+ imageBitmap = await createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });
757
+ } else if (textureUri) {
758
+ const resp = await fetch(textureUri);
759
+ const blob = await resp.blob();
760
+ imageBitmap = await createImageBitmap(blob, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' });
761
+ } else {
762
+ return;
763
+ }
764
+
765
+ if (this.textureLoadVersion.get(materialKey) !== version) {
766
+ imageBitmap.close();
767
+ return;
768
+ }
769
+
770
+ const sourceId = `model-normal:${materialKey}`;
771
+ const existing = this.bindless.registry.getRegistration(sourceId);
772
+ let handle: number;
773
+ if (existing) {
774
+ this.bindless.registry.updateFromImageBitmap(sourceId, imageBitmap);
775
+ handle = existing.handle;
776
+ } else {
777
+ const reg = this.bindless.registry.registerFromImageBitmap(sourceId, imageBitmap, "rgba8unorm", 1, this.mipmapsEnabled);
778
+ handle = reg.handle;
779
+ }
780
+ this.meshNormalTextureSourceId.set(materialKey, sourceId);
781
+
782
+ // Update the material's normalTexHandle. Preserve existing albedo handle.
783
+ const materialIndex = this.meshMaterialIndex.get(materialKey);
784
+ if (materialIndex !== undefined) {
785
+ const albedoHandle = this.meshAlbedoHandle.get(materialKey) ?? this.bindless.registry.defaultWhiteHandle;
786
+ const matParams: MaterialParams = {
787
+ baseColor: [1, 1, 1, 1],
788
+ roughness: 1,
789
+ metallic: 0,
790
+ emissiveIntensity: 0,
791
+ albedoTexHandle: albedoHandle,
792
+ normalTexHandle: handle,
793
+ metallicRoughnessTexHandle: this.bindless.registry.defaultWhiteHandle,
794
+ aoTexHandle: this.bindless.registry.defaultWhiteHandle,
795
+ emissiveTexHandle: this.bindless.registry.defaultWhiteHandle,
796
+ };
797
+ this.bindless.materialManager.updateMaterial(materialIndex, matParams);
798
+ }
799
+
800
+ log.info("ModelRenderer", `Normal texture ready for ${materialKey}: ${imageBitmap.width}x${imageBitmap.height}`);
801
+ imageBitmap.close();
802
+ } catch (e) {
803
+ log.error("ModelRenderer", `Failed to load normal texture for ${materialKey}: ${e}`);
804
+ }
805
+ }
806
+
807
+ removeModel(nodeId: string): void {
808
+ const resources = this.modelResources.get(nodeId);
809
+ if (resources) {
810
+ for (let i = 0; i < resources.length; i++) {
811
+ resources[i].vertexBuffer.destroy();
812
+ resources[i].indexBuffer.destroy();
813
+ resources[i].skinVertexBuffer?.destroy();
814
+ }
815
+ this.nextUniformOffset = Math.max(0, this.nextUniformOffset - resources.length);
816
+ this.modelResources.delete(nodeId);
817
+ }
818
+ // Unregister all per-mesh textures + materials from the bindless managers.
819
+ // Clean up any materialKey that starts with `${nodeId}:`.
820
+ const prefix = `${nodeId}:`;
821
+ for (const [materialKey, matIdx] of this.meshMaterialIndex.entries()) {
822
+ if (materialKey.startsWith(prefix)) {
823
+ const sourceId = this.meshTextureSourceId.get(materialKey);
824
+ if (sourceId && this.bindless) {
825
+ this.bindless.registry.unregister(sourceId);
826
+ this.meshTextureSourceId.delete(materialKey);
827
+ }
828
+ if (this.bindless && matIdx !== this.defaultMaterialIndex) {
829
+ this.bindless.materialManager.unregisterMaterial(matIdx);
830
+ }
831
+ this.meshMaterialIndex.delete(materialKey);
832
+ this.textureLoadVersion.delete(materialKey);
833
+ }
834
+ }
835
+ }
836
+
837
+ reuploadModel(nodeId: string, meshes: MeshData[], materials?: MaterialData[], modelBaseUrl?: string): number {
838
+ // Preserve existing per-mesh texture registrations to avoid flickering on
839
+ // part selection changes. Per-mesh material indices are reused.
840
+ const oldResources = this.modelResources.get(nodeId);
841
+ if (oldResources) {
842
+ for (let i = 0; i < oldResources.length; i++) {
843
+ oldResources[i].vertexBuffer.destroy();
844
+ oldResources[i].indexBuffer.destroy();
845
+ oldResources[i].skinVertexBuffer?.destroy();
846
+ }
847
+ this.nextUniformOffset = Math.max(0, this.nextUniformOffset - oldResources.length);
848
+ this.modelResources.delete(nodeId);
849
+ }
850
+
851
+ const newResources: ModelGPUResources[] = [];
852
+ let uniformOffset = this.nextUniformOffset;
853
+
854
+ for (let i = 0; i < meshes.length && uniformOffset < ModelRenderer.MAX_MODELS; i++) {
855
+ const mesh = meshes[i];
856
+ if (mesh.indexCount === 0 || mesh.vertexCount === 0 || mesh.indices.byteLength === 0) continue;
857
+ const vertexCount = mesh.vertexCount;
858
+ const stride = 11;
859
+ const interleaved = new Float32Array(vertexCount * stride);
860
+
861
+ for (let v = 0; v < vertexCount; v++) {
862
+ interleaved[v * stride] = mesh.vertices[v * 6];
863
+ interleaved[v * stride + 1] = mesh.vertices[v * 6 + 1];
864
+ interleaved[v * stride + 2] = mesh.vertices[v * 6 + 2];
865
+ interleaved[v * stride + 3] = mesh.vertices[v * 6 + 3];
866
+ interleaved[v * stride + 4] = mesh.vertices[v * 6 + 4];
867
+ interleaved[v * stride + 5] = mesh.vertices[v * 6 + 5];
868
+ interleaved[v * stride + 6] = mesh.uvs ? mesh.uvs[v * 2] : 0;
869
+ interleaved[v * stride + 7] = mesh.uvs ? mesh.uvs[v * 2 + 1] : 0;
870
+ interleaved[v * stride + 8] = mesh.colors ? mesh.colors[v * 3] : 1;
871
+ interleaved[v * stride + 9] = mesh.colors ? mesh.colors[v * 3 + 1] : 1;
872
+ interleaved[v * stride + 10] = mesh.colors ? mesh.colors[v * 3 + 2] : 1;
873
+ }
874
+
875
+ const vertexBuffer = this.device.createBuffer({
876
+ size: interleaved.byteLength,
877
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
878
+ });
879
+ this.device.queue.writeBuffer(vertexBuffer, 0, interleaved as Float32Array<ArrayBuffer>);
880
+
881
+ const indexFormat: GPUIndexFormat =
882
+ mesh.indices instanceof Uint32Array ? "uint32" : "uint16";
883
+ const indexByteLength = mesh.indices.byteLength;
884
+ const paddedIndexSize = Math.ceil(indexByteLength / 4) * 4;
885
+ const indexBuffer = this.device.createBuffer({
886
+ size: paddedIndexSize,
887
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
888
+ });
889
+ if (paddedIndexSize === indexByteLength) {
890
+ this.device.queue.writeBuffer(indexBuffer, 0, mesh.indices as (Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer>));
891
+ } else {
892
+ const padded = new Uint8Array(paddedIndexSize);
893
+ padded.set(new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, indexByteLength));
894
+ this.device.queue.writeBuffer(indexBuffer, 0, padded);
895
+ }
896
+
897
+ // Per-mesh material: reuse existing bindless material if already registered,
898
+ // otherwise register a new one and start async texture load.
899
+ const matIdx = mesh.materialIndex ?? 0;
900
+ const materialKey = `${nodeId}:${matIdx}`;
901
+ let bindlessMatIndex = this.defaultMaterialIndex;
902
+ if (this.bindless) {
903
+ const existing = this.meshMaterialIndex.get(materialKey);
904
+ if (existing !== undefined) {
905
+ bindlessMatIndex = existing;
906
+ } else {
907
+ const mat = materials?.[matIdx];
908
+ bindlessMatIndex = this.bindless.materialManager.registerMaterial({
909
+ baseColor: mat?.baseColor ?? [1, 1, 1, 1],
910
+ roughness: mat?.roughness ?? 1,
911
+ metallic: mat?.metallic ?? 0,
912
+ emissiveIntensity: 0,
913
+ albedoTexHandle: this.bindless.registry.defaultWhiteHandle,
914
+ normalTexHandle: this.bindless.registry.defaultNormalHandle,
915
+ metallicRoughnessTexHandle: this.bindless.registry.defaultWhiteHandle,
916
+ aoTexHandle: this.bindless.registry.defaultWhiteHandle,
917
+ emissiveTexHandle: this.bindless.registry.defaultWhiteHandle,
918
+ });
919
+ this.meshMaterialIndex.set(materialKey, bindlessMatIndex);
920
+
921
+ // Start async texture load if this material has texture data and no
922
+ // existing registration (first time seeing this material).
923
+ if (mat?.textureData && mat.textureData.byteLength > 0 && !this.meshTextureSourceId.has(materialKey)) {
924
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
925
+ this.textureLoadVersion.set(materialKey, version);
926
+ this.loadMeshTexture(materialKey, mat.textureData, version);
927
+ } else if (mat?.textureUri && modelBaseUrl && !this.meshTextureSourceId.has(materialKey)) {
928
+ const resolvedUri = modelBaseUrl + mat.textureUri;
929
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
930
+ this.textureLoadVersion.set(materialKey, version);
931
+ this.loadMeshTextureFromUri(materialKey, resolvedUri, version);
932
+ }
933
+ // Start async normal texture load if present and not yet loaded.
934
+ if (((mat?.normalTextureData && mat.normalTextureData.byteLength > 0) || mat?.normalTextureUri) && !this.meshNormalTextureSourceId.has(materialKey)) {
935
+ const version = (this.textureLoadVersion.get(materialKey) ?? 0) + 1;
936
+ this.loadMeshNormalTexture(materialKey, mat.normalTextureData ?? null, mat?.normalTextureUri, version);
937
+ }
938
+ }
939
+ }
940
+
941
+ const skinVertexBuffer = this.buildSkinVertexBuffer(mesh);
942
+
943
+ newResources.push({
944
+ vertexBuffer,
945
+ indexBuffer,
946
+ indexCount: mesh.indexCount,
947
+ indexFormat,
948
+ uniformOffset: uniformOffset * ModelRenderer.UNIFORM_SIZE,
949
+ materialIndex: bindlessMatIndex,
950
+ skinVertexBuffer,
951
+ skinned: skinVertexBuffer !== undefined,
952
+ });
953
+ uniformOffset++;
954
+ }
955
+
956
+ this.nextUniformOffset = uniformOffset;
957
+ this.modelResources.set(nodeId, newResources);
958
+ return newResources.length;
959
+ }
960
+
961
+ beginFrame(camera: CameraState): void {
962
+ this.viewProjCache = calculateViewProj(camera);
963
+ this.cameraPosCache = [camera.position[0], camera.position[1], camera.position[2]];
964
+ this.bindlessBindGroupSetThisFrame = false;
965
+ this.skinBindGroupSetThisFrame = false;
966
+ this.frameLightingBgSetThisFrame = false;
967
+ }
968
+
969
+ /**
970
+ * The effective frame-lighting bind group for group(2): the caller-provided
971
+ * one if set, otherwise the default neutral one created in init(). The model
972
+ * pipeline's group(2) layout is the explicit frameLightingLayout, so a
973
+ * compatible bind group MUST be bound before every draw — otherwise a stale
974
+ * group(2) from a prior pipeline (e.g. an auto-layout procedural pipeline)
975
+ * triggers a WebGPU bind-group-layout incompatibility error.
976
+ */
977
+ private get effectiveFrameLightingBg(): GPUBindGroup | null {
978
+ return this.frameLightingBg ?? this.defaultFrameLightingBg;
979
+ }
980
+
981
+ /**
982
+ * Set the directional light state for the fragment shader.
983
+ * Call once per frame before render(). All values are in world space.
984
+ *
985
+ * @param dir Light direction (the direction the light travels). The shader
986
+ * normalizes this. For a sun at the top of the sky in a Y-down world, use
987
+ * a negative Y (e.g. [0.5, -0.8, 0.3]).
988
+ * @param ambient Ambient light level (0-1). Combined with directional.
989
+ * @param intensity Directional light intensity (0-1). Final lighting = ambient + ndotl * intensity.
990
+ */
991
+ setLightState(dir: [number, number, number], ambient: number, intensity: number): void {
992
+ this.lightDirCache = dir;
993
+ this.lightAmbientCache = ambient;
994
+ this.lightIntensityCache = intensity;
995
+ }
996
+
997
+ /**
998
+ * Enable/disable mip-chain generation for mesh textures. Takes effect on
999
+ * the next uploadModel/reuploadModel — existing registrations are not
1000
+ * re-bucketed. Callers should re-upload (removeModel + uploadModel) to
1001
+ * apply the change to already-loaded models.
1002
+ */
1003
+ setMipmapsEnabled(enabled: boolean): void {
1004
+ this.mipmapsEnabled = enabled;
1005
+ }
1006
+
1007
+ getMipmapsEnabled(): boolean {
1008
+ return this.mipmapsEnabled;
1009
+ }
1010
+
1011
+ /**
1012
+ * Upload bone skin matrices for the current frame. The `matrices` buffer is
1013
+ * a flat Float32Array of boneCount * 16 floats (column-major mat4s). It is
1014
+ * copied into the shared skin matrix storage buffer. If the buffer is too
1015
+ * small for the current skeleton, it is recreated (grown) automatically.
1016
+ * Call once per frame before render() for skinned models.
1017
+ */
1018
+ updateSkinMatrices(matrices: Float32Array): void {
1019
+ if (!this.skinMatrixBuffer) return;
1020
+
1021
+ const requiredBones = matrices.length / 16;
1022
+ if (requiredBones > this.boneCapacity) {
1023
+ // Grow the skin matrix buffer to fit the larger skeleton.
1024
+ // Round up to the next power of 2 to reduce reallocations.
1025
+ const newCapacity = Math.max(requiredBones, this.boneCapacity * 2);
1026
+ this.skinMatrixBuffer.destroy();
1027
+ this.skinMatrixBuffer = this.device.createBuffer({
1028
+ size: newCapacity * 64,
1029
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
1030
+ });
1031
+ this.boneCapacity = newCapacity;
1032
+ // Recreate the bind group with the new buffer
1033
+ if (this.skinBindGroupLayout) {
1034
+ this.skinBindGroup = this.device.createBindGroup({
1035
+ layout: this.skinBindGroupLayout,
1036
+ entries: [{ binding: 0, resource: { buffer: this.skinMatrixBuffer } }],
1037
+ });
1038
+ }
1039
+ this.skinBindGroupSetThisFrame = false; // force re-bind
1040
+ }
1041
+
1042
+ this.device.queue.writeBuffer(
1043
+ this.skinMatrixBuffer,
1044
+ 0,
1045
+ matrices as Float32Array<ArrayBuffer>,
1046
+ );
1047
+ }
1048
+
1049
+ render(
1050
+ passEncoder: GPURenderPassEncoder,
1051
+ nodeId: string,
1052
+ position: [number, number, number],
1053
+ rotation: [number, number, number, number],
1054
+ scale: [number, number, number],
1055
+ instanceIndex: number = 0,
1056
+ /** Per-draw highlight: 0 = normal shading, 1 = ghost hologram (cyan),
1057
+ * 2 = hover outline (bright rim). Values >= 3 encode per-prop material
1058
+ * overrides: `3 + shaderMode + texMode * 4` where shaderMode is
1059
+ * 0=Standard 1=Toon 2=Hologram 3=Outline and texMode is
1060
+ * 0=Default 1=Wireframe 2=Checker. Defaults to 0 (normal). */
1061
+ highlight: number = 0,
1062
+ ): void {
1063
+ if (!this.pipeline || !this.bindGroup || !this.uniformBuffer || !this.viewProjCache) return;
1064
+
1065
+ const resources = this.modelResources.get(nodeId);
1066
+ if (!resources || resources.length === 0) return;
1067
+
1068
+ // For instance 0, use the original uniform offsets stored in resources.
1069
+ // For instance 1+, compute offsets from the allocated instance base.
1070
+ let instanceBaseOffset = 0; // in bytes, added to each mesh's uniformOffset
1071
+ if (instanceIndex > 0) {
1072
+ const instances = this.modelInstanceOffsets.get(nodeId);
1073
+ if (!instances || instanceIndex > instances.length) return;
1074
+ instanceBaseOffset = instances[instanceIndex - 1];
1075
+ // instanceBaseOffset is the absolute byte offset for mesh 0 of this
1076
+ // instance. Mesh r's offset = instanceBaseOffset + r * UNIFORM_SIZE.
1077
+ // But res.uniformOffset is already mesh 0's original offset + r * UNIFORM_SIZE.
1078
+ // So the per-mesh offset for this instance = instanceBaseOffset + r * UNIFORM_SIZE.
1079
+ // We compute it as: instanceBaseOffset - resources[0].uniformOffset + res.uniformOffset
1080
+ // = instanceBaseOffset + (res.uniformOffset - resources[0].uniformOffset)
1081
+ // = instanceBaseOffset + r * UNIFORM_SIZE (since original offsets are contiguous).
1082
+ }
1083
+
1084
+ // Set the bindless material bind group once per frame (group 3).
1085
+ if (this.bindlessBindGroup && !this.bindlessBindGroupSetThisFrame) {
1086
+ passEncoder.setBindGroup(3, this.bindlessBindGroup);
1087
+ this.bindlessBindGroupSetThisFrame = true;
1088
+ }
1089
+ // Set the frame-lighting bind group once per frame (group 2). Must be
1090
+ // bound explicitly — the model pipeline's group(2) uses an explicit layout
1091
+ // that is incompatible with auto-layout bind groups left over from other
1092
+ // pipelines drawn earlier in the same pass.
1093
+ const frameLightBg = this.effectiveFrameLightingBg;
1094
+ if (frameLightBg && !this.frameLightingBgSetThisFrame) {
1095
+ passEncoder.setBindGroup(2, frameLightBg);
1096
+ this.frameLightingBgSetThisFrame = true;
1097
+ }
1098
+ // Set the skin-matrix bind group once per frame (group 1) when any skinned
1099
+ // mesh is drawn. The skin matrix buffer is updated per-frame by the caller
1100
+ // via updateSkinMatrices().
1101
+ const hasSkinned = resources.some((r) => r.skinned);
1102
+ if (hasSkinned && this.skinBindGroup && !this.skinBindGroupSetThisFrame) {
1103
+ passEncoder.setBindGroup(1, this.skinBindGroup);
1104
+ this.skinBindGroupSetThisFrame = true;
1105
+ }
1106
+
1107
+ const mesh0Offset = resources[0].uniformOffset;
1108
+ for (let r = 0; r < resources.length; r++) {
1109
+ const res = resources[r];
1110
+ // Compute the uniform offset for this instance + mesh.
1111
+ const uniformOffset = instanceIndex === 0
1112
+ ? res.uniformOffset
1113
+ : instanceBaseOffset + (res.uniformOffset - mesh0Offset);
1114
+ const uniforms = this.reusableUniforms;
1115
+ for (let i = 0; i < 16; i++) uniforms[i] = this.viewProjCache[i];
1116
+ uniforms[16] = this.cameraPosCache[0];
1117
+ uniforms[17] = this.cameraPosCache[1];
1118
+ uniforms[18] = this.cameraPosCache[2];
1119
+ uniforms[19] = performance.now() / 1000;
1120
+ uniforms[20] = position[0];
1121
+ uniforms[21] = position[1];
1122
+ uniforms[22] = position[2];
1123
+ // uniforms[23] = padding (vec3<f32> alignment in WGSL uniform layout)
1124
+ uniforms[24] = scale[0];
1125
+ uniforms[25] = scale[1];
1126
+ uniforms[26] = scale[2];
1127
+ // uniforms[27] = padding (vec4<f32> alignment in WGSL uniform layout)
1128
+ uniforms[28] = rotation[0];
1129
+ uniforms[29] = rotation[1];
1130
+ uniforms[30] = rotation[2];
1131
+ uniforms[31] = rotation[3];
1132
+ // materialIndex (u32) at float slot 32 — write via Uint32Array view so
1133
+ // the shader reads the correct u32 bit pattern (not a float reinterpretation).
1134
+ this.reusableUniformsU32[32] = res.materialIndex;
1135
+ // Light state at float slots 33-35 (the former _pad3/_pad4/_pad5 slots):
1136
+ // 33: lightDir.x, 34: lightDir.y, 35: lightDir.z
1137
+ // 36: ambient, 37: intensity
1138
+ uniforms[33] = this.lightDirCache[0];
1139
+ uniforms[34] = this.lightDirCache[1];
1140
+ uniforms[35] = this.lightDirCache[2];
1141
+ uniforms[36] = this.lightAmbientCache;
1142
+ uniforms[37] = this.lightIntensityCache;
1143
+ // highlight flag (float slot 38 — the former _pad6). Written as a float
1144
+ // so the WGSL `highlight: f32` field reads the correct bit pattern.
1145
+ uniforms[38] = highlight;
1146
+ // Outline params (slots 39-42) — unused by fs_main but written so the
1147
+ // same uniform buffer works for the outline pipeline (vs_outline /
1148
+ // fs_outline). Defaults: 0 width, black color.
1149
+ uniforms[39] = 0.0;
1150
+ uniforms[40] = 0.0; uniforms[41] = 0.0; uniforms[42] = 0.0;
1151
+
1152
+ this.device.queue.writeBuffer(
1153
+ this.uniformBuffer,
1154
+ uniformOffset,
1155
+ uniforms as Float32Array<ArrayBuffer>,
1156
+ );
1157
+
1158
+ // Select the skinned pipeline + skin vertex buffer for skinned meshes,
1159
+ // otherwise the standard non-skinned pipeline.
1160
+ const slotIdx = uniformOffset / ModelRenderer.UNIFORM_SIZE;
1161
+ const mBg = this.perSlotBindGroups[slotIdx] ?? this.bindGroup;
1162
+ if (res.skinned && this.skinnedPipeline && res.skinVertexBuffer) {
1163
+ passEncoder.setPipeline(this.skinnedPipeline);
1164
+ if (this.perSlotBindGroups.length > 0) passEncoder.setBindGroup(0, mBg, [0]);
1165
+ else passEncoder.setBindGroup(0, mBg!, [uniformOffset]);
1166
+ passEncoder.setVertexBuffer(0, res.vertexBuffer);
1167
+ passEncoder.setVertexBuffer(1, res.skinVertexBuffer);
1168
+ passEncoder.setIndexBuffer(res.indexBuffer, res.indexFormat);
1169
+ passEncoder.drawIndexed(res.indexCount);
1170
+ } else {
1171
+ passEncoder.setPipeline(this.pipeline);
1172
+ if (this.perSlotBindGroups.length > 0) passEncoder.setBindGroup(0, mBg, [0]);
1173
+ else passEncoder.setBindGroup(0, mBg!, [uniformOffset]);
1174
+ passEncoder.setVertexBuffer(0, res.vertexBuffer);
1175
+ passEncoder.setIndexBuffer(res.indexBuffer, res.indexFormat);
1176
+ passEncoder.drawIndexed(res.indexCount);
1177
+ }
1178
+ }
1179
+ }
1180
+
1181
+ /**
1182
+ * Render the model as solid white to a mask texture for the post-process
1183
+ * outline. Uses the same vertex shader as the normal render (vs_main)
1184
+ * with a simple white fragment shader (fs_mask). Depth-tested against
1185
+ * the scene depth (read-only) so the mask respects occlusion.
1186
+ */
1187
+ renderMask(
1188
+ passEncoder: GPURenderPassEncoder,
1189
+ nodeId: string,
1190
+ position: [number, number, number],
1191
+ rotation: [number, number, number, number],
1192
+ scale: [number, number, number],
1193
+ ): void {
1194
+ if (!this.maskPipeline || !this.bindGroup || !this.uniformBuffer || !this.viewProjCache) return;
1195
+
1196
+ const resources = this.modelResources.get(nodeId);
1197
+ if (!resources || resources.length === 0) return;
1198
+
1199
+ if (this.bindlessBindGroup && !this.bindlessBindGroupSetThisFrame) {
1200
+ passEncoder.setBindGroup(3, this.bindlessBindGroup);
1201
+ this.bindlessBindGroupSetThisFrame = true;
1202
+ }
1203
+ // renderMask() runs in a separate render pass from render(), so the
1204
+ // per-frame "set this frame" flags are unreliable (they may already be
1205
+ // true from the scene pass). Bind group 2 (frame lighting) and group 3
1206
+ // (bindless) explicitly here — both are non-empty in the mask pipeline's
1207
+ // layout and must be re-bound for this pass.
1208
+ const maskFrameLightBg = this.effectiveFrameLightingBg;
1209
+ if (maskFrameLightBg) passEncoder.setBindGroup(2, maskFrameLightBg);
1210
+ if (this.bindlessBindGroup) passEncoder.setBindGroup(3, this.bindlessBindGroup);
1211
+
1212
+ for (let r = 0; r < resources.length; r++) {
1213
+ const res = resources[r];
1214
+ const uniformOffset = res.uniformOffset;
1215
+ const uniforms = this.reusableUniforms;
1216
+ for (let i = 0; i < 16; i++) uniforms[i] = this.viewProjCache[i];
1217
+ uniforms[16] = this.cameraPosCache[0];
1218
+ uniforms[17] = this.cameraPosCache[1];
1219
+ uniforms[18] = this.cameraPosCache[2];
1220
+ uniforms[19] = performance.now() / 1000;
1221
+ uniforms[20] = position[0];
1222
+ uniforms[21] = position[1];
1223
+ uniforms[22] = position[2];
1224
+ uniforms[24] = scale[0];
1225
+ uniforms[25] = scale[1];
1226
+ uniforms[26] = scale[2];
1227
+ uniforms[28] = rotation[0];
1228
+ uniforms[29] = rotation[1];
1229
+ uniforms[30] = rotation[2];
1230
+ uniforms[31] = rotation[3];
1231
+ this.reusableUniformsU32[32] = res.materialIndex;
1232
+ // Remaining fields not read by fs_mask.
1233
+ this.device.queue.writeBuffer(
1234
+ this.uniformBuffer,
1235
+ uniformOffset,
1236
+ uniforms as Float32Array<ArrayBuffer>,
1237
+ );
1238
+
1239
+ passEncoder.setPipeline(this.maskPipeline);
1240
+ passEncoder.setBindGroup(0, this.bindGroup, [uniformOffset]);
1241
+ passEncoder.setVertexBuffer(0, res.vertexBuffer);
1242
+ passEncoder.setIndexBuffer(res.indexBuffer, res.indexFormat);
1243
+ passEncoder.drawIndexed(res.indexCount);
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Render the model depth-only into a shadow map from an arbitrary
1249
+ * view-projection (the light's VP). Uses the depthPipeline (no color
1250
+ * targets, depthWriteEnabled). Only group(0) (per-draw uniform) is bound
1251
+ * — the depthPipeline's layout has no lighting/bindless groups.
1252
+ *
1253
+ * @param viewProj The light's view-projection matrix (16 floats).
1254
+ */
1255
+ renderDepth(
1256
+ passEncoder: GPURenderPassEncoder,
1257
+ viewProj: Float32Array,
1258
+ nodeId: string,
1259
+ position: [number, number, number],
1260
+ rotation: [number, number, number, number],
1261
+ scale: [number, number, number],
1262
+ ): void {
1263
+ if (!this.depthPipeline || !this.bindGroup || !this.uniformBuffer) return;
1264
+
1265
+ const resources = this.modelResources.get(nodeId);
1266
+ if (!resources || resources.length === 0) return;
1267
+
1268
+ const uniforms = this.reusableUniforms;
1269
+ for (let r = 0; r < resources.length; r++) {
1270
+ const res = resources[r];
1271
+ const uniformOffset = res.uniformOffset;
1272
+ // viewProj (light VP) replaces the camera viewProj for shadow rendering.
1273
+ for (let i = 0; i < 16; i++) uniforms[i] = viewProj[i];
1274
+ // cameraPos + time not read by the vertex shader, but zero for safety.
1275
+ uniforms[16] = 0; uniforms[17] = 0; uniforms[18] = 0; uniforms[19] = 0;
1276
+ uniforms[20] = position[0];
1277
+ uniforms[21] = position[1];
1278
+ uniforms[22] = position[2];
1279
+ uniforms[24] = scale[0];
1280
+ uniforms[25] = scale[1];
1281
+ uniforms[26] = scale[2];
1282
+ uniforms[28] = rotation[0];
1283
+ uniforms[29] = rotation[1];
1284
+ uniforms[30] = rotation[2];
1285
+ uniforms[31] = rotation[3];
1286
+ this.reusableUniformsU32[32] = res.materialIndex;
1287
+
1288
+ this.device.queue.writeBuffer(
1289
+ this.uniformBuffer,
1290
+ uniformOffset,
1291
+ uniforms as Float32Array<ArrayBuffer>,
1292
+ );
1293
+
1294
+ passEncoder.setPipeline(this.depthPipeline);
1295
+ passEncoder.setBindGroup(0, this.bindGroup, [uniformOffset]);
1296
+ passEncoder.setVertexBuffer(0, res.vertexBuffer);
1297
+ passEncoder.setIndexBuffer(res.indexBuffer, res.indexFormat);
1298
+ passEncoder.drawIndexed(res.indexCount);
1299
+ }
1300
+ }
1301
+
1302
+ hasModel(nodeId: string): boolean {
1303
+ return this.modelResources.has(nodeId);
1304
+ }
1305
+
1306
+ /**
1307
+ * Overwrite a mesh's vertex buffer with new interleaved data (stride 11:
1308
+ * pos3 + normal3 + uv2 + color3). Used for CPU-side mesh deformation —
1309
+ * the caller owns the vertex layout and must supply vertexCount*11 floats.
1310
+ */
1311
+ updateVertexBuffer(nodeId: string, meshIndex: number, interleaved: Float32Array): void {
1312
+ const res = this.modelResources.get(nodeId)?.[meshIndex];
1313
+ if (!res) return;
1314
+ this.device.queue.writeBuffer(res.vertexBuffer, 0, interleaved as Float32Array<ArrayBuffer>);
1315
+ }
1316
+
1317
+ /**
1318
+ * Overwrite a mesh's index buffer with a compacted triangle list and update
1319
+ * the draw count. Used for CPU-side mesh deformation that deletes triangles
1320
+ * (e.g. punched-through holes). `indices`/`indexCount` describe the live
1321
+ * prefix of the caller's index array.
1322
+ */
1323
+ updateIndexBuffer(nodeId: string, meshIndex: number, indices: Uint16Array | Uint32Array, indexCount: number): void {
1324
+ const res = this.modelResources.get(nodeId)?.[meshIndex];
1325
+ if (!res) return;
1326
+ const live = indices.subarray(0, indexCount);
1327
+ const byteLen = live.byteLength;
1328
+ if (byteLen % 4 === 0) {
1329
+ this.device.queue.writeBuffer(res.indexBuffer, 0, live as (Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer>));
1330
+ } else {
1331
+ const padded = new Uint8Array(Math.ceil(byteLen / 4) * 4);
1332
+ padded.set(new Uint8Array(live.buffer, live.byteOffset, byteLen));
1333
+ this.device.queue.writeBuffer(res.indexBuffer, 0, padded);
1334
+ }
1335
+ res.indexCount = indexCount;
1336
+ }
1337
+
1338
+ destroy(): void {
1339
+ const ids = Array.from(this.modelResources.keys());
1340
+ for (let i = 0; i < ids.length; i++) {
1341
+ this.removeModel(ids[i]);
1342
+ }
1343
+ this.skinMatrixBuffer?.destroy();
1344
+ }
1345
+ }