@downdraft/engine 0.1.0 → 0.1.2-alpha.5

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