@thunderclap-engine/engine 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (566) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/LICENSE +21 -0
  3. package/README.md +333 -2
  4. package/dist/analytics/analytics.d.ts +80 -0
  5. package/dist/analytics/analytics.js +129 -0
  6. package/dist/analytics/index.d.ts +1 -0
  7. package/dist/analytics/index.js +1 -0
  8. package/dist/anim/aim.d.ts +43 -0
  9. package/dist/anim/aim.js +157 -0
  10. package/dist/anim/clip.d.ts +98 -0
  11. package/dist/anim/clip.js +282 -0
  12. package/dist/anim/compress.d.ts +65 -0
  13. package/dist/anim/compress.js +339 -0
  14. package/dist/anim/foot-ik.d.ts +74 -0
  15. package/dist/anim/foot-ik.js +241 -0
  16. package/dist/anim/graph.d.ts +153 -0
  17. package/dist/anim/graph.js +532 -0
  18. package/dist/anim/ik.d.ts +105 -0
  19. package/dist/anim/ik.js +435 -0
  20. package/dist/anim/index.d.ts +36 -0
  21. package/dist/anim/index.js +21 -0
  22. package/dist/anim/match.d.ts +66 -0
  23. package/dist/anim/match.js +153 -0
  24. package/dist/anim/mixer.d.ts +113 -0
  25. package/dist/anim/mixer.js +314 -0
  26. package/dist/anim/morph.d.ts +41 -0
  27. package/dist/anim/morph.js +94 -0
  28. package/dist/anim/pose.d.ts +74 -0
  29. package/dist/anim/pose.js +254 -0
  30. package/dist/anim/quat.d.ts +44 -0
  31. package/dist/anim/quat.js +187 -0
  32. package/dist/anim/retarget.d.ts +97 -0
  33. package/dist/anim/retarget.js +366 -0
  34. package/dist/anim/root-motion.d.ts +44 -0
  35. package/dist/anim/root-motion.js +224 -0
  36. package/dist/anim/skin-pose.d.ts +39 -0
  37. package/dist/anim/skin-pose.js +53 -0
  38. package/dist/anim/socket.d.ts +47 -0
  39. package/dist/anim/socket.js +84 -0
  40. package/dist/anim/spring-chain.d.ts +73 -0
  41. package/dist/anim/spring-chain.js +294 -0
  42. package/dist/assets/atlas.d.ts +90 -0
  43. package/dist/assets/atlas.js +212 -0
  44. package/dist/assets/baked.d.ts +33 -0
  45. package/dist/assets/baked.js +38 -0
  46. package/dist/assets/bundle.d.ts +88 -0
  47. package/dist/assets/bundle.js +464 -0
  48. package/dist/assets/cache.d.ts +56 -0
  49. package/dist/assets/cache.js +123 -0
  50. package/dist/assets/gltf-load.d.ts +252 -0
  51. package/dist/assets/gltf-load.js +661 -0
  52. package/dist/assets/gltf-skin.d.ts +36 -0
  53. package/dist/assets/gltf-skin.js +109 -0
  54. package/dist/assets/gltf.d.ts +706 -0
  55. package/dist/assets/gltf.js +1376 -0
  56. package/dist/assets/index.d.ts +20 -0
  57. package/dist/assets/index.js +10 -0
  58. package/dist/assets/ktx2.d.ts +71 -0
  59. package/dist/assets/ktx2.js +213 -0
  60. package/dist/assets/manifest.d.ts +79 -0
  61. package/dist/assets/manifest.js +7 -0
  62. package/dist/assets/meshopt.d.ts +23 -0
  63. package/dist/assets/meshopt.js +403 -0
  64. package/dist/audio/audio.d.ts +733 -0
  65. package/dist/audio/audio.js +2013 -0
  66. package/dist/audio/dsp.d.ts +61 -0
  67. package/dist/audio/dsp.js +159 -0
  68. package/dist/audio/duck.d.ts +44 -0
  69. package/dist/audio/duck.js +101 -0
  70. package/dist/audio/envelope.d.ts +53 -0
  71. package/dist/audio/envelope.js +106 -0
  72. package/dist/audio/index.d.ts +13 -0
  73. package/dist/audio/index.js +14 -0
  74. package/dist/audio/music.d.ts +113 -0
  75. package/dist/audio/music.js +548 -0
  76. package/dist/audio/occlusion.d.ts +55 -0
  77. package/dist/audio/occlusion.js +61 -0
  78. package/dist/audio/persist.d.ts +23 -0
  79. package/dist/audio/persist.js +27 -0
  80. package/dist/audio/recipes.d.ts +55 -0
  81. package/dist/audio/recipes.js +227 -0
  82. package/dist/audio/send.d.ts +41 -0
  83. package/dist/audio/send.js +118 -0
  84. package/dist/audio/sprites.d.ts +59 -0
  85. package/dist/audio/sprites.js +43 -0
  86. package/dist/audio/synth.d.ts +103 -0
  87. package/dist/audio/synth.js +197 -0
  88. package/dist/audio/variations.d.ts +25 -0
  89. package/dist/audio/variations.js +55 -0
  90. package/dist/audio/zones.d.ts +100 -0
  91. package/dist/audio/zones.js +167 -0
  92. package/dist/core/detached-surface.d.ts +44 -0
  93. package/dist/core/detached-surface.js +104 -0
  94. package/dist/core/events.d.ts +24 -0
  95. package/dist/core/events.js +43 -0
  96. package/dist/core/game.d.ts +409 -0
  97. package/dist/core/game.js +530 -0
  98. package/dist/core/hash.d.ts +24 -0
  99. package/dist/core/hash.js +49 -0
  100. package/dist/core/index.d.ts +17 -0
  101. package/dist/core/index.js +9 -0
  102. package/dist/core/loop.d.ts +113 -0
  103. package/dist/core/loop.js +185 -0
  104. package/dist/core/math.d.ts +58 -0
  105. package/dist/core/math.js +210 -0
  106. package/dist/core/noise.d.ts +40 -0
  107. package/dist/core/noise.js +247 -0
  108. package/dist/core/pool.d.ts +42 -0
  109. package/dist/core/pool.js +85 -0
  110. package/dist/core/random.d.ts +61 -0
  111. package/dist/core/random.js +199 -0
  112. package/dist/core/scene.d.ts +79 -0
  113. package/dist/core/scene.js +195 -0
  114. package/dist/core/timer.d.ts +66 -0
  115. package/dist/core/timer.js +286 -0
  116. package/dist/fx/billboard-wgsl.d.ts +29 -0
  117. package/dist/fx/billboard-wgsl.js +158 -0
  118. package/dist/fx/billboard.d.ts +249 -0
  119. package/dist/fx/billboard.js +702 -0
  120. package/dist/fx/debug-draw.d.ts +127 -0
  121. package/dist/fx/debug-draw.js +523 -0
  122. package/dist/fx/debug-wgsl.d.ts +7 -0
  123. package/dist/fx/debug-wgsl.js +32 -0
  124. package/dist/fx/decals-wgsl.d.ts +15 -0
  125. package/dist/fx/decals-wgsl.js +173 -0
  126. package/dist/fx/decals.d.ts +216 -0
  127. package/dist/fx/decals.js +607 -0
  128. package/dist/fx/effect.d.ts +129 -0
  129. package/dist/fx/effect.js +349 -0
  130. package/dist/fx/effects/hit-spark.json +48 -0
  131. package/dist/fx/effects/index.d.ts +8 -0
  132. package/dist/fx/effects/index.js +9 -0
  133. package/dist/fx/effects/magic-aura.json +47 -0
  134. package/dist/fx/effects/smoke-puff.json +34 -0
  135. package/dist/fx/emit-surface.d.ts +40 -0
  136. package/dist/fx/emit-surface.js +124 -0
  137. package/dist/fx/flicker.d.ts +30 -0
  138. package/dist/fx/flicker.js +42 -0
  139. package/dist/fx/index.d.ts +31 -0
  140. package/dist/fx/index.js +20 -0
  141. package/dist/fx/particles-wgsl.d.ts +57 -0
  142. package/dist/fx/particles-wgsl.js +828 -0
  143. package/dist/fx/particles.d.ts +566 -0
  144. package/dist/fx/particles.js +1533 -0
  145. package/dist/fx/random.d.ts +15 -0
  146. package/dist/fx/random.js +20 -0
  147. package/dist/fx/sky-wgsl.d.ts +23 -0
  148. package/dist/fx/sky-wgsl.js +127 -0
  149. package/dist/fx/sky.d.ts +96 -0
  150. package/dist/fx/sky.js +175 -0
  151. package/dist/fx/text-atlas.d.ts +143 -0
  152. package/dist/fx/text-atlas.js +327 -0
  153. package/dist/fx/trail-wgsl.d.ts +11 -0
  154. package/dist/fx/trail-wgsl.js +42 -0
  155. package/dist/fx/trail.d.ts +194 -0
  156. package/dist/fx/trail.js +555 -0
  157. package/dist/fx/water-wgsl.d.ts +34 -0
  158. package/dist/fx/water-wgsl.js +197 -0
  159. package/dist/fx/water.d.ts +176 -0
  160. package/dist/fx/water.js +371 -0
  161. package/dist/geom/collide/bvh.d.ts +151 -0
  162. package/dist/geom/collide/bvh.js +493 -0
  163. package/dist/geom/collide/debug.d.ts +14 -0
  164. package/dist/geom/collide/debug.js +74 -0
  165. package/dist/geom/collide/grid.d.ts +26 -0
  166. package/dist/geom/collide/grid.js +91 -0
  167. package/dist/geom/collide/index.d.ts +8 -0
  168. package/dist/geom/collide/index.js +8 -0
  169. package/dist/geom/collide/queries.d.ts +70 -0
  170. package/dist/geom/collide/queries.js +583 -0
  171. package/dist/geom/collide/serialize.d.ts +14 -0
  172. package/dist/geom/collide/serialize.js +145 -0
  173. package/dist/geom/collide/shapes.d.ts +161 -0
  174. package/dist/geom/collide/shapes.js +686 -0
  175. package/dist/geom/collide/slide.d.ts +66 -0
  176. package/dist/geom/collide/slide.js +375 -0
  177. package/dist/geom/collide/tile-slide.d.ts +88 -0
  178. package/dist/geom/collide/tile-slide.js +255 -0
  179. package/dist/geom/geometry.d.ts +95 -0
  180. package/dist/geom/geometry.js +322 -0
  181. package/dist/geom/heightfield.d.ts +72 -0
  182. package/dist/geom/heightfield.js +295 -0
  183. package/dist/geom/hull.d.ts +27 -0
  184. package/dist/geom/hull.js +279 -0
  185. package/dist/geom/index.d.ts +35 -0
  186. package/dist/geom/index.js +23 -0
  187. package/dist/geom/iso.d.ts +74 -0
  188. package/dist/geom/iso.js +89 -0
  189. package/dist/geom/lod.d.ts +29 -0
  190. package/dist/geom/lod.js +27 -0
  191. package/dist/geom/ops.d.ts +47 -0
  192. package/dist/geom/ops.js +285 -0
  193. package/dist/geom/primitives.d.ts +97 -0
  194. package/dist/geom/primitives.js +359 -0
  195. package/dist/geom/shapes.d.ts +43 -0
  196. package/dist/geom/shapes.js +88 -0
  197. package/dist/geom/simplify.d.ts +38 -0
  198. package/dist/geom/simplify.js +519 -0
  199. package/dist/geom/split.d.ts +37 -0
  200. package/dist/geom/split.js +413 -0
  201. package/dist/geom/sweep.d.ts +83 -0
  202. package/dist/geom/sweep.js +534 -0
  203. package/dist/geom/tangents.d.ts +16 -0
  204. package/dist/geom/tangents.js +124 -0
  205. package/dist/geom/triangulate.d.ts +19 -0
  206. package/dist/geom/triangulate.js +147 -0
  207. package/dist/host/default.d.ts +10 -0
  208. package/dist/host/default.js +10 -0
  209. package/dist/host/index.d.ts +4 -0
  210. package/dist/host/index.js +3 -0
  211. package/dist/host/types.d.ts +144 -0
  212. package/dist/host/types.js +1 -0
  213. package/dist/host/web-links.d.ts +38 -0
  214. package/dist/host/web-links.js +110 -0
  215. package/dist/host/web.d.ts +30 -0
  216. package/dist/host/web.js +216 -0
  217. package/dist/host/xr.d.ts +24 -0
  218. package/dist/host/xr.js +59 -0
  219. package/dist/i18n/i18n.d.ts +59 -0
  220. package/dist/i18n/i18n.js +241 -0
  221. package/dist/i18n/index.d.ts +3 -0
  222. package/dist/i18n/index.js +3 -0
  223. package/dist/i18n/message.d.ts +72 -0
  224. package/dist/i18n/message.js +369 -0
  225. package/dist/i18n/negotiate.d.ts +17 -0
  226. package/dist/i18n/negotiate.js +58 -0
  227. package/dist/index.d.ts +7 -0
  228. package/dist/index.js +1 -0
  229. package/dist/input/bindings.d.ts +31 -0
  230. package/dist/input/bindings.js +141 -0
  231. package/dist/input/clock.d.ts +44 -0
  232. package/dist/input/clock.js +49 -0
  233. package/dist/input/follow.d.ts +133 -0
  234. package/dist/input/follow.js +218 -0
  235. package/dist/input/gestures.d.ts +281 -0
  236. package/dist/input/gestures.js +662 -0
  237. package/dist/input/glyphs.d.ts +35 -0
  238. package/dist/input/glyphs.js +216 -0
  239. package/dist/input/gyro.d.ts +85 -0
  240. package/dist/input/gyro.js +140 -0
  241. package/dist/input/index.d.ts +19 -0
  242. package/dist/input/index.js +16 -0
  243. package/dist/input/input.d.ts +543 -0
  244. package/dist/input/input.js +1492 -0
  245. package/dist/input/look.d.ts +327 -0
  246. package/dist/input/look.js +681 -0
  247. package/dist/input/magnify.d.ts +177 -0
  248. package/dist/input/magnify.js +131 -0
  249. package/dist/input/pointer-dispatch.d.ts +138 -0
  250. package/dist/input/pointer-dispatch.js +180 -0
  251. package/dist/input/pointer-router.d.ts +204 -0
  252. package/dist/input/pointer-router.js +533 -0
  253. package/dist/input/pointer-sources.d.ts +164 -0
  254. package/dist/input/pointer-sources.js +306 -0
  255. package/dist/input/rays.d.ts +138 -0
  256. package/dist/input/rays.js +615 -0
  257. package/dist/input/record.d.ts +209 -0
  258. package/dist/input/record.js +601 -0
  259. package/dist/input/touch-zones.d.ts +147 -0
  260. package/dist/input/touch-zones.js +300 -0
  261. package/dist/input/types.d.ts +158 -0
  262. package/dist/input/types.js +1 -0
  263. package/dist/input/xr-source.d.ts +53 -0
  264. package/dist/input/xr-source.js +164 -0
  265. package/dist/nav/astar.d.ts +45 -0
  266. package/dist/nav/astar.js +182 -0
  267. package/dist/nav/corridor.d.ts +40 -0
  268. package/dist/nav/corridor.js +95 -0
  269. package/dist/nav/crowd.d.ts +94 -0
  270. package/dist/nav/crowd.js +624 -0
  271. package/dist/nav/flow.d.ts +61 -0
  272. package/dist/nav/flow.js +193 -0
  273. package/dist/nav/grid.d.ts +108 -0
  274. package/dist/nav/grid.js +210 -0
  275. package/dist/nav/heap.d.ts +22 -0
  276. package/dist/nav/heap.js +92 -0
  277. package/dist/nav/index.d.ts +37 -0
  278. package/dist/nav/index.js +19 -0
  279. package/dist/nav/mesh/areas.d.ts +44 -0
  280. package/dist/nav/mesh/areas.js +98 -0
  281. package/dist/nav/mesh/bake-options.d.ts +33 -0
  282. package/dist/nav/mesh/bake-options.js +76 -0
  283. package/dist/nav/mesh/contour.d.ts +22 -0
  284. package/dist/nav/mesh/contour.js +323 -0
  285. package/dist/nav/mesh/flow3.d.ts +35 -0
  286. package/dist/nav/mesh/flow3.js +229 -0
  287. package/dist/nav/mesh/links.d.ts +38 -0
  288. package/dist/nav/mesh/links.js +72 -0
  289. package/dist/nav/mesh/mesh-path.d.ts +152 -0
  290. package/dist/nav/mesh/mesh-path.js +643 -0
  291. package/dist/nav/mesh/mesh-reach.d.ts +78 -0
  292. package/dist/nav/mesh/mesh-reach.js +450 -0
  293. package/dist/nav/mesh/navmesh.d.ts +117 -0
  294. package/dist/nav/mesh/navmesh.js +647 -0
  295. package/dist/nav/mesh/polymesh.d.ts +24 -0
  296. package/dist/nav/mesh/polymesh.js +384 -0
  297. package/dist/nav/mesh/regions.d.ts +12 -0
  298. package/dist/nav/mesh/regions.js +367 -0
  299. package/dist/nav/mesh/serialize.d.ts +13 -0
  300. package/dist/nav/mesh/serialize.js +197 -0
  301. package/dist/nav/mesh/voxelize.d.ts +111 -0
  302. package/dist/nav/mesh/voxelize.js +552 -0
  303. package/dist/nav/obstacles.d.ts +75 -0
  304. package/dist/nav/obstacles.js +294 -0
  305. package/dist/nav/path.d.ts +59 -0
  306. package/dist/nav/path.js +177 -0
  307. package/dist/nav/query-queue.d.ts +46 -0
  308. package/dist/nav/query-queue.js +308 -0
  309. package/dist/nav/reach.d.ts +53 -0
  310. package/dist/nav/reach.js +148 -0
  311. package/dist/nav/steer.d.ts +36 -0
  312. package/dist/nav/steer.js +78 -0
  313. package/dist/platform/boot.d.ts +28 -0
  314. package/dist/platform/boot.js +43 -0
  315. package/dist/platform/device-mode.d.ts +40 -0
  316. package/dist/platform/device-mode.js +104 -0
  317. package/dist/platform/device.d.ts +31 -0
  318. package/dist/platform/device.js +91 -0
  319. package/dist/platform/environment.d.ts +42 -0
  320. package/dist/platform/environment.js +82 -0
  321. package/dist/platform/gpu.d.ts +40 -0
  322. package/dist/platform/gpu.js +57 -0
  323. package/dist/platform/index.d.ts +18 -0
  324. package/dist/platform/index.js +10 -0
  325. package/dist/platform/letterbox.d.ts +43 -0
  326. package/dist/platform/letterbox.js +57 -0
  327. package/dist/platform/links.d.ts +20 -0
  328. package/dist/platform/links.js +43 -0
  329. package/dist/platform/screens.d.ts +36 -0
  330. package/dist/platform/screens.js +138 -0
  331. package/dist/platform/surface.d.ts +72 -0
  332. package/dist/platform/surface.js +157 -0
  333. package/dist/platform/xr-fake.d.ts +168 -0
  334. package/dist/platform/xr-fake.js +434 -0
  335. package/dist/platform/xr-surface.d.ts +120 -0
  336. package/dist/platform/xr-surface.js +214 -0
  337. package/dist/platform/xr-types.d.ts +262 -0
  338. package/dist/post/ao.d.ts +12 -0
  339. package/dist/post/ao.js +136 -0
  340. package/dist/post/blend.d.ts +27 -0
  341. package/dist/post/blend.js +129 -0
  342. package/dist/post/depth-pass.d.ts +43 -0
  343. package/dist/post/depth-pass.js +118 -0
  344. package/dist/post/dof.d.ts +1 -0
  345. package/dist/post/dof.js +66 -0
  346. package/dist/post/exposure.d.ts +80 -0
  347. package/dist/post/exposure.js +322 -0
  348. package/dist/post/index.d.ts +12 -0
  349. package/dist/post/index.js +11 -0
  350. package/dist/post/lens-dirt.d.ts +13 -0
  351. package/dist/post/lens-dirt.js +91 -0
  352. package/dist/post/lut.d.ts +19 -0
  353. package/dist/post/lut.js +50 -0
  354. package/dist/post/motion-blur.d.ts +9 -0
  355. package/dist/post/motion-blur.js +78 -0
  356. package/dist/post/post-stack.d.ts +302 -0
  357. package/dist/post/post-stack.js +1137 -0
  358. package/dist/post/settings.d.ts +485 -0
  359. package/dist/post/settings.js +416 -0
  360. package/dist/post/shaders.d.ts +25 -0
  361. package/dist/post/shaders.js +410 -0
  362. package/dist/post/ssr.d.ts +16 -0
  363. package/dist/post/ssr.js +155 -0
  364. package/dist/post/taa.d.ts +74 -0
  365. package/dist/post/taa.js +235 -0
  366. package/dist/post/white-balance.d.ts +25 -0
  367. package/dist/post/white-balance.js +98 -0
  368. package/dist/render/bindings.d.ts +98 -0
  369. package/dist/render/bindings.js +281 -0
  370. package/dist/render/blend.d.ts +13 -0
  371. package/dist/render/blend.js +26 -0
  372. package/dist/render/camera-2d.d.ts +72 -0
  373. package/dist/render/camera-2d.js +92 -0
  374. package/dist/render/camera.d.ts +302 -0
  375. package/dist/render/camera.js +671 -0
  376. package/dist/render/color.d.ts +16 -0
  377. package/dist/render/color.js +41 -0
  378. package/dist/render/compressed.d.ts +104 -0
  379. package/dist/render/compressed.js +248 -0
  380. package/dist/render/diagnostics.d.ts +15 -0
  381. package/dist/render/diagnostics.js +43 -0
  382. package/dist/render/dynamic-resolution.d.ts +70 -0
  383. package/dist/render/dynamic-resolution.js +126 -0
  384. package/dist/render/fake-gpu.d.ts +204 -0
  385. package/dist/render/fake-gpu.js +426 -0
  386. package/dist/render/flipbook.d.ts +56 -0
  387. package/dist/render/flipbook.js +87 -0
  388. package/dist/render/frame.d.ts +278 -0
  389. package/dist/render/frame.js +174 -0
  390. package/dist/render/fullscreen.d.ts +54 -0
  391. package/dist/render/fullscreen.js +91 -0
  392. package/dist/render/gpu-timer.d.ts +73 -0
  393. package/dist/render/gpu-timer.js +186 -0
  394. package/dist/render/index.d.ts +53 -0
  395. package/dist/render/index.js +29 -0
  396. package/dist/render/instance-list.d.ts +185 -0
  397. package/dist/render/instance-list.js +547 -0
  398. package/dist/render/lights.d.ts +181 -0
  399. package/dist/render/lights.js +233 -0
  400. package/dist/render/lod.d.ts +54 -0
  401. package/dist/render/lod.js +52 -0
  402. package/dist/render/material.d.ts +165 -0
  403. package/dist/render/material.js +292 -0
  404. package/dist/render/mesh.d.ts +284 -0
  405. package/dist/render/mesh.js +472 -0
  406. package/dist/render/mipmap.d.ts +16 -0
  407. package/dist/render/mipmap.js +121 -0
  408. package/dist/render/parallax.d.ts +50 -0
  409. package/dist/render/parallax.js +47 -0
  410. package/dist/render/pbr-wgsl.d.ts +31 -0
  411. package/dist/render/pbr-wgsl.js +119 -0
  412. package/dist/render/ping-pong.d.ts +42 -0
  413. package/dist/render/ping-pong.js +74 -0
  414. package/dist/render/renderer.d.ts +741 -0
  415. package/dist/render/renderer.js +2486 -0
  416. package/dist/render/scene-pipelines.d.ts +44 -0
  417. package/dist/render/scene-pipelines.js +92 -0
  418. package/dist/render/scene-target.d.ts +170 -0
  419. package/dist/render/scene-target.js +450 -0
  420. package/dist/render/shadow.d.ts +74 -0
  421. package/dist/render/shadow.js +208 -0
  422. package/dist/render/skin.d.ts +112 -0
  423. package/dist/render/skin.js +240 -0
  424. package/dist/render/snapshot.d.ts +36 -0
  425. package/dist/render/snapshot.js +34 -0
  426. package/dist/render/standard.d.ts +162 -0
  427. package/dist/render/standard.js +263 -0
  428. package/dist/render/texture.d.ts +142 -0
  429. package/dist/render/texture.js +228 -0
  430. package/dist/render/tile-layer.d.ts +110 -0
  431. package/dist/render/tile-layer.js +304 -0
  432. package/dist/render/transform.d.ts +115 -0
  433. package/dist/render/transform.js +318 -0
  434. package/dist/render/wgsl.d.ts +131 -0
  435. package/dist/render/wgsl.js +528 -0
  436. package/dist/save/codec.d.ts +49 -0
  437. package/dist/save/codec.js +169 -0
  438. package/dist/save/index.d.ts +8 -0
  439. package/dist/save/index.js +8 -0
  440. package/dist/save/journal.d.ts +24 -0
  441. package/dist/save/journal.js +102 -0
  442. package/dist/save/migrate.d.ts +15 -0
  443. package/dist/save/migrate.js +37 -0
  444. package/dist/save/schema.d.ts +72 -0
  445. package/dist/save/schema.js +154 -0
  446. package/dist/save/settings.d.ts +28 -0
  447. package/dist/save/settings.js +59 -0
  448. package/dist/save/slots.d.ts +56 -0
  449. package/dist/save/slots.js +200 -0
  450. package/dist/save/storage.d.ts +35 -0
  451. package/dist/save/storage.js +62 -0
  452. package/dist/save/store.d.ts +114 -0
  453. package/dist/save/store.js +269 -0
  454. package/dist/testing/fake-host.d.ts +103 -0
  455. package/dist/testing/fake-host.js +163 -0
  456. package/dist/testing/fixed-clock.d.ts +25 -0
  457. package/dist/testing/fixed-clock.js +30 -0
  458. package/dist/testing/index.d.ts +4 -0
  459. package/dist/testing/index.js +5 -0
  460. package/dist/testing/test-game.d.ts +76 -0
  461. package/dist/testing/test-game.js +92 -0
  462. package/dist/tween/ease.d.ts +77 -0
  463. package/dist/tween/ease.js +192 -0
  464. package/dist/tween/index.d.ts +8 -0
  465. package/dist/tween/index.js +7 -0
  466. package/dist/tween/timeline.d.ts +78 -0
  467. package/dist/tween/timeline.js +269 -0
  468. package/dist/tween/tween.d.ts +168 -0
  469. package/dist/tween/tween.js +320 -0
  470. package/dist/ui/anchor.d.ts +206 -0
  471. package/dist/ui/anchor.js +307 -0
  472. package/dist/ui/caption.d.ts +122 -0
  473. package/dist/ui/caption.js +286 -0
  474. package/dist/ui/cursor.d.ts +105 -0
  475. package/dist/ui/cursor.js +261 -0
  476. package/dist/ui/dom.d.ts +30 -0
  477. package/dist/ui/dom.js +92 -0
  478. package/dist/ui/floats.d.ts +86 -0
  479. package/dist/ui/floats.js +260 -0
  480. package/dist/ui/hud.d.ts +198 -0
  481. package/dist/ui/hud.js +431 -0
  482. package/dist/ui/index.d.ts +37 -0
  483. package/dist/ui/index.js +34 -0
  484. package/dist/ui/inventory-card.d.ts +52 -0
  485. package/dist/ui/inventory-card.js +173 -0
  486. package/dist/ui/inventory-context.d.ts +29 -0
  487. package/dist/ui/inventory-context.js +125 -0
  488. package/dist/ui/inventory-drag.d.ts +63 -0
  489. package/dist/ui/inventory-drag.js +394 -0
  490. package/dist/ui/inventory-grid.d.ts +66 -0
  491. package/dist/ui/inventory-grid.js +625 -0
  492. package/dist/ui/inventory-radial.d.ts +39 -0
  493. package/dist/ui/inventory-radial.js +168 -0
  494. package/dist/ui/inventory.d.ts +128 -0
  495. package/dist/ui/inventory.js +154 -0
  496. package/dist/ui/layout.d.ts +356 -0
  497. package/dist/ui/layout.js +856 -0
  498. package/dist/ui/link.d.ts +64 -0
  499. package/dist/ui/link.js +155 -0
  500. package/dist/ui/loading.d.ts +40 -0
  501. package/dist/ui/loading.js +157 -0
  502. package/dist/ui/loupe.d.ts +74 -0
  503. package/dist/ui/loupe.js +164 -0
  504. package/dist/ui/menu-nav.d.ts +33 -0
  505. package/dist/ui/menu-nav.js +28 -0
  506. package/dist/ui/minimap-canvas.d.ts +145 -0
  507. package/dist/ui/minimap-canvas.js +460 -0
  508. package/dist/ui/notify.d.ts +40 -0
  509. package/dist/ui/notify.js +141 -0
  510. package/dist/ui/orb.d.ts +53 -0
  511. package/dist/ui/orb.js +304 -0
  512. package/dist/ui/overlay.d.ts +213 -0
  513. package/dist/ui/overlay.js +394 -0
  514. package/dist/ui/padnav.d.ts +53 -0
  515. package/dist/ui/padnav.js +146 -0
  516. package/dist/ui/perf.d.ts +130 -0
  517. package/dist/ui/perf.js +329 -0
  518. package/dist/ui/rotate-card.d.ts +54 -0
  519. package/dist/ui/rotate-card.js +97 -0
  520. package/dist/ui/stack.d.ts +128 -0
  521. package/dist/ui/stack.js +312 -0
  522. package/dist/ui/styles.d.ts +13 -0
  523. package/dist/ui/styles.js +200 -0
  524. package/dist/ui/text-scale.d.ts +67 -0
  525. package/dist/ui/text-scale.js +102 -0
  526. package/dist/ui/theme.d.ts +70 -0
  527. package/dist/ui/theme.js +98 -0
  528. package/dist/ui/toast.d.ts +23 -0
  529. package/dist/ui/toast.js +46 -0
  530. package/dist/ui/tooltip.d.ts +89 -0
  531. package/dist/ui/tooltip.js +333 -0
  532. package/dist/ui/widgets-2.d.ts +134 -0
  533. package/dist/ui/widgets-2.js +399 -0
  534. package/dist/ui/widgets-3.d.ts +91 -0
  535. package/dist/ui/widgets-3.js +212 -0
  536. package/dist/ui/widgets.d.ts +34 -0
  537. package/dist/ui/widgets.js +50 -0
  538. package/dist/ui/world-panel/index.d.ts +5 -0
  539. package/dist/ui/world-panel/index.js +5 -0
  540. package/dist/ui/world-panel/layout.d.ts +38 -0
  541. package/dist/ui/world-panel/layout.js +68 -0
  542. package/dist/ui/world-panel/paint.d.ts +96 -0
  543. package/dist/ui/world-panel/paint.js +136 -0
  544. package/dist/ui/world-panel/panel.d.ts +235 -0
  545. package/dist/ui/world-panel/panel.js +762 -0
  546. package/dist/ui/world-panel/wgsl.d.ts +10 -0
  547. package/dist/ui/world-panel/wgsl.js +52 -0
  548. package/dist/ui/world-panel/widgets.d.ts +207 -0
  549. package/dist/ui/world-panel/widgets.js +569 -0
  550. package/dist/voxel/chunks.d.ts +77 -0
  551. package/dist/voxel/chunks.js +149 -0
  552. package/dist/voxel/grid.d.ts +84 -0
  553. package/dist/voxel/grid.js +223 -0
  554. package/dist/voxel/index.d.ts +22 -0
  555. package/dist/voxel/index.js +7 -0
  556. package/dist/voxel/mesher-worker.d.ts +14 -0
  557. package/dist/voxel/mesher-worker.js +47 -0
  558. package/dist/voxel/mesher.d.ts +73 -0
  559. package/dist/voxel/mesher.js +306 -0
  560. package/dist/voxel/ray.d.ts +32 -0
  561. package/dist/voxel/ray.js +73 -0
  562. package/dist/voxel/wgsl.d.ts +16 -0
  563. package/dist/voxel/wgsl.js +28 -0
  564. package/dist/voxel/worker-client.d.ts +79 -0
  565. package/dist/voxel/worker-client.js +122 -0
  566. package/package.json +177 -3
@@ -0,0 +1,2486 @@
1
+ import { ALL_LAYERS, CAMERA_BLOCK_FLOATS, Camera, FRUSTUM_FLOATS } from './camera.js';
2
+ import { rendererCaps } from './compressed.js';
3
+ import { installGpuErrorLogging } from './diagnostics.js';
4
+ import { Frame, INSTANCE_PARAM_FLOATS, writeInstanceParams, } from './frame.js';
5
+ import { FullscreenPass } from './fullscreen.js';
6
+ import { GpuTimer, supportsTimestamps } from './gpu-timer.js';
7
+ import { InstanceList } from './instance-list.js';
8
+ import { LIGHTS_BLOCK_FLOATS, packInstanceFlags } from './lights.js';
9
+ import { DEFAULT_LOD_HYSTERESIS, lodSizePx, pickLodLevel, } from './lod.js';
10
+ import { DEPTH_FORMAT, INSTANCE_FLOATS, INSTANCE_STRIDE, Material, SPRITE_WGSL, sceneColorTargets, } from './material.js';
11
+ import { Mesh, quadGeometry, VERTEX_LAYOUT_2D } from './mesh.js';
12
+ import { PingPong } from './ping-pong.js';
13
+ import { SCENE_FORMAT, SceneTarget, } from './scene-target.js';
14
+ import { DEFAULT_SHADOW_SIZE, SHADOW_FORMAT, ShadowMap } from './shadow.js';
15
+ import { bytesPerTexel, createSamplers, defaultTextures, loadTexture, Texture, } from './texture.js';
16
+ import { MATRIX3X4_FLOATS, writeMatrix2d } from './transform.js';
17
+ /** `InstanceParams` of a batch without `params`: the defaults. */
18
+ const NO_PARAMS = Object.freeze({});
19
+ /** Smallest scale `setResolution` accepts (a quarter of the output in each direction). */
20
+ export const MIN_RESOLUTION_SCALE = 0.25;
21
+ /**
22
+ * Effective scene-target size for an output size and resolution options. Pure (unit-tested).
23
+ * Sizes are rounded to even numbers so the half-res bloom/ray targets stay pixel aligned.
24
+ */
25
+ export function sceneSize(outW, outH, opts) {
26
+ const w = Math.max(1, Math.floor(outW));
27
+ const h = Math.max(1, Math.floor(outH));
28
+ let k = Math.min(1, Math.max(MIN_RESOLUTION_SCALE, opts.scale));
29
+ const px = w * h * k * k;
30
+ if (px > opts.maxPixels)
31
+ k *= Math.sqrt(opts.maxPixels / px);
32
+ if (k >= 0.999)
33
+ return { width: w, height: h, scale: 1 };
34
+ const even = (x) => Math.max(2, 2 * Math.round(x / 2));
35
+ const width = Math.min(w, even(w * k));
36
+ const height = Math.min(h, even(h * k));
37
+ return { width, height, scale: width / w };
38
+ }
39
+ /** Bytes per instance of the previous-matrix lane (a 3×4 matrix). */
40
+ const LANE_STRIDE = 48;
41
+ /** What `resolve`'s built-in present clears to when it covers the whole output. */
42
+ /** `sceneTargets` cache key for a pipeline without blending. */
43
+ const NO_BLEND = Object.freeze({
44
+ color: Object.freeze({}),
45
+ alpha: Object.freeze({}),
46
+ });
47
+ const PRESENT_CLEAR = { r: 0, g: 0, b: 0, a: 1 };
48
+ /** Globals block (binding 0) floats: logicalSize, targetSize, time, dt, pad. */
49
+ const GLOBALS_FLOATS = 8;
50
+ /** The camera block (binding 1) starts at this float offset (256 bytes: uniform alignment). */
51
+ const CAMERA_OFFSET_FLOATS = 64;
52
+ /** The lighting block (binding 2) starts at this float offset (768 bytes, after the camera). */
53
+ const LIGHTS_OFFSET_FLOATS = CAMERA_OFFSET_FLOATS + CAMERA_BLOCK_FLOATS;
54
+ /** One upload covers all three blocks. */
55
+ const ENGINE_UNIFORM_FLOATS = LIGHTS_OFFSET_FLOATS + LIGHTS_BLOCK_FLOATS;
56
+ /**
57
+ * WebGPU renderer: owns the HDR scene targets, instanced batch, materials and the hand-off to
58
+ * the post stack. One per canvas.
59
+ *
60
+ * ```ts
61
+ * const r = new Renderer(gpu);
62
+ * r.resize(w, h);
63
+ * const f = r.beginFrame(time, dt);
64
+ * f.sprite(myMaterial, { x: 960, y: 540, w: 200, h: 200 });
65
+ * r.endFrame(); // = r.render(f, r.camera) into r.target, then post + present
66
+ * ```
67
+ *
68
+ * A frame is `beginFrame` → record → `render(frame, camera, target?, viewport?)` any number of
69
+ * times → `endFrame`. Each `render` runs the recorded draw lists through one camera (its layer
70
+ * mask, sort and depth) into a `SceneTarget` (default `renderer.target`, what the surface
71
+ * shows); `endFrame` renders through `renderer.camera` when nothing called `render`, then runs
72
+ * the post stack (or the built-in present) from `renderer.target` to the swapchain and submits.
73
+ */
74
+ export class Renderer {
75
+ device;
76
+ context;
77
+ /** Swapchain format. */
78
+ format;
79
+ /**
80
+ * Formats of the scene pass's two base colour targets (HDR colour, HDR emissive). A target
81
+ * with aux attachments adds more after them: build a pipeline's target list with
82
+ * `sceneTargets`, not from this.
83
+ */
84
+ sceneFormats = [SCENE_FORMAT, SCENE_FORMAT];
85
+ logicalWidth;
86
+ logicalHeight;
87
+ /**
88
+ * The camera the scene pass draws through, uploaded to the camera block each frame.
89
+ * Default `Camera.ortho2d(logicalWidth, logicalHeight)`; its `x`, `y`, `zoom` and
90
+ * `rotation` pan the 2D view. Assign another `Camera` to swap presets.
91
+ */
92
+ camera;
93
+ samplers;
94
+ /**
95
+ * What the device can do beyond core WebGPU, read from its features at construction
96
+ * (ws/render-5): `caps.compressed.bc` / `.etc2` / `.astc` are the block-compression families
97
+ * `uploadKtx2` can upload. A device gets them only when created with them
98
+ * (`compressionFeatures(adapter)` in `requiredFeatures`).
99
+ */
100
+ caps;
101
+ globalsLayout;
102
+ /**
103
+ * The engine uniforms of the frame's first view (`@group(0)`: globals, camera and lights
104
+ * blocks, the sun's shadow map and its sampler). Replaced when the shadow map is (re)created.
105
+ */
106
+ globalsBindGroup;
107
+ quad;
108
+ /** Built-in sprite material bound to the default white texture (solid colour quads via tint). */
109
+ defaultSprite;
110
+ stats = {
111
+ drawCalls: 0,
112
+ triangles: 0,
113
+ instances: 0,
114
+ drawables: 0,
115
+ batches: 0,
116
+ culled: 0,
117
+ shadowDraws: 0,
118
+ passes: 0,
119
+ lodCounts: [],
120
+ };
121
+ /**
122
+ * The band around each LOD threshold an instance must cross before it changes level, as a
123
+ * fraction of the threshold (default 0.1): see `pickLodLevel`. 0 turns it off.
124
+ */
125
+ lodHysteresis = DEFAULT_LOD_HYSTERESIS;
126
+ /** See `FrameTiming`. Only updated while `timingEvery > 0`. */
127
+ timing = {
128
+ gpuMs: 0,
129
+ source: 'queue',
130
+ passes: {},
131
+ cpuMs: 0,
132
+ samples: 0,
133
+ ready: () => this.timingReady(),
134
+ };
135
+ /** Resolvers of `timing.ready()` promises waiting on the first GPU sample. */
136
+ readyWaiters = [];
137
+ /**
138
+ * Pass timer when the device has `timestamp-query`, else null. Passes encoded during
139
+ * `endFrame` (the post processor's included) label themselves with `gpuTimer.pass(label)`;
140
+ * it returns undefined except on sampled frames.
141
+ */
142
+ gpuTimer;
143
+ /**
144
+ * Sample GPU time every N frames (0 = off). One sample is in flight at a time; each costs one
145
+ * `onSubmittedWorkDone` promise.
146
+ */
147
+ timingEvery = 0;
148
+ /** Final stage. Defaults to a plain tonemap + sRGB present; set to a `PostStack`. */
149
+ post = null;
150
+ /** Scene-target size (internal resolution). The post stack reads these. */
151
+ width = 1;
152
+ height = 1;
153
+ /** Output (canvas backbuffer) size, as passed to `resize`. */
154
+ outputWidth = 1;
155
+ outputHeight = 1;
156
+ /**
157
+ * The default `SceneTarget`, at the scene resolution: what `render` draws into unless given
158
+ * another, what the post stack reads and what the surface shows.
159
+ */
160
+ target;
161
+ views = [];
162
+ viewCount = 0;
163
+ /** The sun's shadow map and its pipelines (see `shadow.ts`). */
164
+ shadow;
165
+ /** Engine uniform slots for the shadow passes of a frame (the sun camera in the camera block). */
166
+ shadowSlots = [];
167
+ shadowCount = 0;
168
+ shadowRect = { x: 0, y: 0, width: 1, height: 1 };
169
+ globalsData = new Float32Array(ENGINE_UNIFORM_FLOATS);
170
+ globalsU32 = new Uint32Array(this.globalsData.buffer);
171
+ viewRect = { x: 0, y: 0, width: 1, height: 1 };
172
+ /** Targets rendered into so far this frame, and whether their depth was cleared. */
173
+ usedTargets = [];
174
+ usedDepth = [];
175
+ /** The last depth camera into each used target (ws/post-depth: `PostInputs.projection`). */
176
+ usedCamera = [];
177
+ /** `sceneTargets` lists per blend state object (`NO_BLEND` for none; weak, so a caller's
178
+ * fresh blend object is not kept), indexed by aux key. */
179
+ sceneTargetLists = new WeakMap();
180
+ /** The target `buildDraws` is building for (its aux set keys the pipelines). */
181
+ drawTarget = null;
182
+ postProjection = { near: 0, far: 0, proj: new Float32Array(16) };
183
+ /** `PostInputs.view` storage, one per inputs object (`postInputs`, `resolveInputs`). */
184
+ postView = newPostView();
185
+ resolveView = newPostView();
186
+ usedCount = 0;
187
+ instanceData;
188
+ instanceBuffer;
189
+ /** Byte offset in `instanceBuffer` where the next view's instances go. */
190
+ instanceBase = 0;
191
+ /** Instance buffers outgrown this frame, destroyed after submit (passes still reference them). */
192
+ retired = [];
193
+ /**
194
+ * The shared previous-matrix lane (ws/render-7): 12 floats per instance, indexed like the
195
+ * view's upload, for batches with `prevMatrices` drawn into a velocity target by an aux
196
+ * material. Made on first use; `laneBase` is the current view's byte offset in it.
197
+ */
198
+ laneData = new Float32Array(0);
199
+ laneBuffer = null;
200
+ laneBase = 0;
201
+ /** The previous matrices `copyBatch` / `copyBatchLod` copy into `laneData`, else null. */
202
+ laneSrc = null;
203
+ /** Batch records with `prev` this frame, and whether the current view drew one with the lane. */
204
+ laneRecs = 0;
205
+ laneUsed = false;
206
+ instanceCount = 0;
207
+ /** Instances in this frame's batches (`pushBatch`), excluding lists; they take the sort path. */
208
+ batchInstances = 0;
209
+ /** Batch records this frame (lists included). */
210
+ batchRecs = 0;
211
+ /** Records with a LOD chain this frame. */
212
+ lodRecs = 0;
213
+ /** The camera `pickLods` is picking for. */
214
+ lodCam = null;
215
+ /** The LOD level memory of each camera (view) that drew a LOD record. */
216
+ lodMemory = new WeakMap();
217
+ /** Per camera: each single draw's Transform history for the velocity lane (ws/render-8). */
218
+ motionMemory = new WeakMap();
219
+ /** Per-instance level scratch of `copyBatchLod` (-1: culled). */
220
+ lodScratch = new Int8Array(256);
221
+ /** `copyBatchLod`'s buckets: first instance, count and write cursor per level. */
222
+ bucketFirst = new Int32Array(8);
223
+ bucketCount = new Int32Array(8);
224
+ bucketAt = new Int32Array(8);
225
+ /** `setJitter`'s hook, the frames it has run and this frame's offset. */
226
+ jitterFn = null;
227
+ jitterFrame = 0;
228
+ jitterX = 0;
229
+ jitterY = 0;
230
+ /** Cameras the hook has jittered (cleared when it is removed). */
231
+ jitterCams = [];
232
+ /** Batch or list draws of the current view's scene pass, and instances `copyBatch` dropped. */
233
+ sceneBatches = 0;
234
+ batchCulled = 0;
235
+ /** Instances of the lists the current view's scene pass draws (not in the upload). */
236
+ sceneListInstances = 0;
237
+ /** This view's scene commands (`buildDraws`). */
238
+ scene = { cmds: [], count: 0 };
239
+ /** This view's shadow-pass commands (`buildShadowDraws`): opaque casters inside the sun box. */
240
+ shadowCmds = { cmds: [], count: 0 };
241
+ recs = [];
242
+ recCount = 0;
243
+ /** True while every draw this frame had `order` >= the previous draw's. */
244
+ ordered = true;
245
+ lastOrder = Number.NEGATIVE_INFINITY;
246
+ sortIdx = new Uint32Array(256);
247
+ sortTmp = new Uint32Array(256);
248
+ sortDepth = false;
249
+ /** Record indices of the shadow list, sorted by mesh then material (`sortShadow`). */
250
+ shadowIdx = new Uint32Array(256);
251
+ shadowTmp = new Uint32Array(256);
252
+ /** True while `sortDraws` orders the shadow list (mesh, material, index) instead of the scene. */
253
+ sortShadow = false;
254
+ /** Stable numeric keys for meshes and materials, for the shadow sort; grows per new object. */
255
+ keys = new WeakMap();
256
+ nextKey = 0;
257
+ /** Instance data in draw order, used when sorting or layer culling reorders draws. */
258
+ sortedData = new Float32Array(0);
259
+ /** The array `endFrame` uploads: `instanceData` (submission order) or `sortedData`. */
260
+ uploadData;
261
+ /** Instances `render` uploads for this view: the scene's, then the shadow list's copies. */
262
+ uploadCount = 0;
263
+ /** Instances the scene pass draws this view (layer- and frustum-culled draws excluded). */
264
+ sceneCount = 0;
265
+ /** Draws the last `buildDraws` dropped by the frustum test. */
266
+ culledCount = 0;
267
+ prePasses = [];
268
+ prepared = new Set();
269
+ /**
270
+ * Where the last `buildDraws` would split the scene commands for a `readsDepth` drawable:
271
+ * the index of the first blended command at or above the lowest hooked order; -1 when no
272
+ * visible drawable asked (or the camera has no depth test).
273
+ */
274
+ splitAt = -1;
275
+ /** Drawables whose `afterOpaque` ran for the current view (once each). */
276
+ hooked = new Set();
277
+ /** The `ViewInfo` handed to `afterOpaque`, reused across views. */
278
+ viewInfo = {
279
+ camera: undefined,
280
+ target: undefined,
281
+ depthCopy: undefined,
282
+ depthView: undefined,
283
+ viewport: { x: 0, y: 0, width: 0, height: 0 },
284
+ };
285
+ /** Per-draw uniform sets with blocks staged this frame. */
286
+ drawUniformSets = [];
287
+ dynamicOffsets = [0];
288
+ frame;
289
+ present;
290
+ presentGroup = null;
291
+ postInputs;
292
+ /** `resolve` (ws/xr-post): inputs for an explicit target and the present group cached for it. */
293
+ resolveInputs;
294
+ resolveTarget = null;
295
+ resolveColor = null;
296
+ resolveGroup = null;
297
+ /** The frame's command encoder, opened by the first `render`, submitted by `endFrame`. */
298
+ encoder = null;
299
+ sampled = false;
300
+ resizeListeners = new Set();
301
+ resolution = {
302
+ scale: 1,
303
+ maxPixels: Number.POSITIVE_INFINITY,
304
+ };
305
+ inFrame = false;
306
+ destroyed = false;
307
+ frameStart = 0;
308
+ timingFrame = 0;
309
+ timingPending = false;
310
+ timingSubmit = 0;
311
+ timingListeners = new Set();
312
+ onTimingDone = () => {
313
+ this.timingPending = false;
314
+ if (this.destroyed)
315
+ return;
316
+ this.emitGpuTime(performance.now() - this.timingSubmit);
317
+ };
318
+ onTimerResult = () => {
319
+ if (this.destroyed || !this.gpuTimer)
320
+ return;
321
+ const r = this.gpuTimer.result;
322
+ if (r.totalMs > 0)
323
+ this.emitGpuTime(r.totalMs);
324
+ };
325
+ constructor(gpu, opts = {}) {
326
+ this.device = gpu.device;
327
+ this.context = gpu.context;
328
+ this.format = gpu.format;
329
+ this.logicalWidth = opts.logicalWidth ?? 1920;
330
+ this.logicalHeight = opts.logicalHeight ?? 1080;
331
+ this.camera = Camera.ortho2d(this.logicalWidth, this.logicalHeight);
332
+ const device = this.device;
333
+ installGpuErrorLogging(device);
334
+ this.caps = rendererCaps(device);
335
+ this.gpuTimer = supportsTimestamps(device) ? new GpuTimer(device) : null;
336
+ if (this.gpuTimer) {
337
+ this.timing.source = 'timestamp';
338
+ this.timing.passes = this.gpuTimer.result.passes;
339
+ this.gpuTimer.onResult(this.onTimerResult);
340
+ }
341
+ this.samplers = createSamplers(device);
342
+ this.globalsLayout = device.createBindGroupLayout({
343
+ label: 'engine-globals-layout',
344
+ entries: [
345
+ {
346
+ binding: 0,
347
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
348
+ buffer: {},
349
+ },
350
+ {
351
+ binding: 1,
352
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
353
+ buffer: {},
354
+ },
355
+ {
356
+ binding: 2,
357
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
358
+ buffer: {},
359
+ },
360
+ {
361
+ binding: 3,
362
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
363
+ texture: { sampleType: 'depth' },
364
+ },
365
+ {
366
+ binding: 4,
367
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
368
+ sampler: { type: 'comparison' },
369
+ },
370
+ ],
371
+ });
372
+ this.shadow = new ShadowMap(device, this.globalsLayout);
373
+ this.globalsBindGroup = this.viewSlot(0).group;
374
+ const cap = opts.initialInstances ?? 4096;
375
+ this.instanceData = new Float32Array(cap * INSTANCE_FLOATS);
376
+ this.uploadData = this.instanceData;
377
+ this.instanceBuffer = this.makeInstanceBuffer(cap);
378
+ this.quad = new Mesh(device, { label: 'quad', ...quadGeometry() });
379
+ this.defaultSprite = this.createSpriteMaterial({
380
+ texture: defaultTextures(device).white,
381
+ label: 'sprite-default',
382
+ });
383
+ this.present = this.createFullscreenPass({
384
+ label: 'present-basic',
385
+ targets: [this.format],
386
+ textures: 2,
387
+ wgsl: /* wgsl */ `
388
+ @group(0) @binding(1) var samp: sampler;
389
+ @group(0) @binding(2) var colorTex: texture_2d<f32>;
390
+ @group(0) @binding(3) var emissiveTex: texture_2d<f32>;
391
+ @fragment fn fs_main(in: FullscreenIn) -> @location(0) vec4f {
392
+ let c = textureSample(colorTex, samp, in.uv).rgb + textureSample(emissiveTex, samp, in.uv).rgb;
393
+ let t = c / (1.0 + c);
394
+ return vec4f(pow(t, vec3f(1.0 / 2.2)), 1.0);
395
+ }`,
396
+ });
397
+ this.frame = new Frame(this, this.logicalWidth, this.logicalHeight);
398
+ this.target = new SceneTarget(device, { width: 1, height: 1, aux: opts.aux });
399
+ this.postInputs = {
400
+ color: this.target.color,
401
+ emissive: this.target.emissive,
402
+ time: 0,
403
+ dt: 0,
404
+ depth: null,
405
+ projection: null,
406
+ view: null,
407
+ roughMetal: null,
408
+ velocity: null,
409
+ };
410
+ this.resolveInputs = { ...this.postInputs };
411
+ this.resize(1, 1);
412
+ }
413
+ /** The default target's HDR colour texture (`target.color`). */
414
+ get sceneColor() {
415
+ return this.target.color;
416
+ }
417
+ /** The default target's HDR emissive texture (`target.emissive`). */
418
+ get sceneEmissive() {
419
+ return this.target.emissive;
420
+ }
421
+ /**
422
+ * Set the output size (canvas backbuffer pixels). The HDR scene targets follow it, scaled
423
+ * by `setResolution`. Cheap no-op when nothing changes.
424
+ */
425
+ resize(width, height) {
426
+ this.outputWidth = Math.max(1, Math.floor(width));
427
+ this.outputHeight = Math.max(1, Math.floor(height));
428
+ this.applyResolution();
429
+ }
430
+ /**
431
+ * Render the scene below output resolution: `scale` (0.25..1, linear) and/or a `maxPixels`
432
+ * budget. The final pass upscales to the canvas. Only the given fields change. Changing it
433
+ * reallocates the scene and post targets, so do it rarely (a resolution controller should
434
+ * step, not track every frame).
435
+ */
436
+ setResolution(opts) {
437
+ if (opts.scale !== undefined)
438
+ this.resolution.scale = opts.scale;
439
+ if (opts.maxPixels !== undefined)
440
+ this.resolution.maxPixels = opts.maxPixels;
441
+ this.applyResolution();
442
+ }
443
+ /** Current resolution settings (scale as requested, before the pixel budget). */
444
+ get resolutionOptions() {
445
+ return this.resolution;
446
+ }
447
+ /** Effective linear scale of the scene targets vs the output (after the pixel budget). */
448
+ get resolutionScale() {
449
+ return this.width / this.outputWidth;
450
+ }
451
+ applyResolution() {
452
+ const { width: w, height: h } = sceneSize(this.outputWidth, this.outputHeight, this.resolution);
453
+ const changed = this.target.resize(w, h);
454
+ if (!changed && this.presentGroup)
455
+ return;
456
+ this.width = w;
457
+ this.height = h;
458
+ this.presentGroup = this.present.createBindGroup([
459
+ this.target.color.view,
460
+ this.target.emissive.view,
461
+ ]);
462
+ this.postInputs.color = this.target.color;
463
+ this.postInputs.emissive = this.target.emissive;
464
+ this.postInputs.roughMetal = this.target.roughMetal;
465
+ this.postInputs.velocity = this.target.velocity;
466
+ this.post?.resize(w, h);
467
+ for (const fn of this.resizeListeners)
468
+ fn(w, h);
469
+ }
470
+ /** Called with every GPU time sample (see `timingEvery`). Returns unsubscribe. */
471
+ onGpuTime(fn) {
472
+ this.timingListeners.add(fn);
473
+ return () => this.timingListeners.delete(fn);
474
+ }
475
+ /** Called after the scene targets change size, with the scene size. Returns unsubscribe. */
476
+ onResize(fn) {
477
+ this.resizeListeners.add(fn);
478
+ return () => this.resizeListeners.delete(fn);
479
+ }
480
+ /**
481
+ * The colour target list (`GPUFragmentState.targets`) of a pipeline that draws into the
482
+ * scene pass of a target with aux key `aux` (`SceneTarget.auxKey`; default
483
+ * `renderer.target`'s, ws/render-10): colour and emissive (`sceneFormats`) with `blend`,
484
+ * then each aux attachment the target has (rough-metal, velocity) with `writeMask: 0`. Every
485
+ * pipeline drawing into the scene pass takes its targets from here (materials build theirs
486
+ * the same way), so its count matches the pass's whichever aux attachments are on. A
487
+ * `Drawable` that may draw into more than one target keys its pipelines on
488
+ * `frame.auxKey` (set per view, like `frame.depthFormat`) and passes it here. The list is
489
+ * built once per (`blend`, `aux`) and the same array returned after; do not mutate it.
490
+ */
491
+ sceneTargets(blend, aux = this.target.auxKey) {
492
+ const key = blend ?? NO_BLEND;
493
+ let lists = this.sceneTargetLists.get(key);
494
+ if (!lists) {
495
+ lists = [undefined, undefined, undefined, undefined];
496
+ this.sceneTargetLists.set(key, lists);
497
+ }
498
+ const a = aux & 3;
499
+ let t = lists[a];
500
+ if (!t) {
501
+ t = sceneColorTargets(this.sceneFormats, a, blend, false);
502
+ lists[a] = t;
503
+ }
504
+ return t;
505
+ }
506
+ // ---------------------------------------------------------------- resources
507
+ createMaterial(desc) {
508
+ return new Material(this.device, desc, this.globalsLayout, this.sceneFormats, this.samplers[desc.sampler ?? 'linear-clamp'], defaultTextures(this.device).white.view);
509
+ }
510
+ /**
511
+ * The built-in textured sprite material (`texture × vertex colour × tint`; emissive is the
512
+ * colour × `emissive`): `createSpriteMaterial({ texture, label, blend, sampler })`, the same
513
+ * option-object shape as `createMaterial`. `label` defaults to `'sprite'`, `blend` to
514
+ * `'alpha'` and `sampler` to `'linear-clamp'`.
515
+ */
516
+ createSpriteMaterial(opts) {
517
+ const m = this.createMaterial({
518
+ label: opts.label ?? 'sprite',
519
+ wgsl: SPRITE_WGSL,
520
+ textures: 1,
521
+ blend: opts.blend,
522
+ sampler: opts.sampler,
523
+ });
524
+ m.setTexture(0, opts.texture);
525
+ return m;
526
+ }
527
+ createMesh(desc) {
528
+ return new Mesh(this.device, desc);
529
+ }
530
+ /**
531
+ * Upload a LOD chain (ws/api-render): one mesh per `geometries` entry, labelled
532
+ * `<label>-lod<i>`, returned with the thresholds and the geometries as a `MeshLod` that
533
+ * `frame.mesh` takes in place of a mesh. geom's `createLodChain(g, ratios)` builds the
534
+ * geometries (level 0 is `g`, so `ratios.length + 1` levels and `ratios.length` thresholds).
535
+ * Throws on an empty chain or more thresholds than levels.
536
+ */
537
+ createMeshLod(desc) {
538
+ const n = desc.geometries.length;
539
+ if (n === 0)
540
+ throw new Error('createMeshLod: the chain has no geometries');
541
+ if (desc.thresholdsPx.length > n) {
542
+ throw new Error(`createMeshLod: ${desc.thresholdsPx.length} thresholds for ${n} levels (at most one per level)`);
543
+ }
544
+ const label = desc.label ?? 'lod';
545
+ const meshes = desc.geometries.map((g, i) => this.createMesh({ ...g, label: `${label}-lod${i}` }));
546
+ return { meshes, thresholdsPx: [...desc.thresholdsPx], geometries: desc.geometries };
547
+ }
548
+ createTexture(desc) {
549
+ return new Texture(this.device, desc);
550
+ }
551
+ /**
552
+ * A scene target of its own size for `render` to draw a view into offscreen (a portal, a
553
+ * minimap, a picture-in-picture camera). Its `color`/`emissive` are sampleable textures;
554
+ * it is not resized with the surface. The caller destroys it.
555
+ */
556
+ createSceneTarget(desc) {
557
+ return new SceneTarget(this.device, desc);
558
+ }
559
+ /** Offscreen render target (sampleable). Default format is the HDR scene format. */
560
+ createRenderTarget(desc) {
561
+ return new Texture(this.device, {
562
+ format: SCENE_FORMAT,
563
+ usage: GPUTextureUsage.RENDER_ATTACHMENT |
564
+ GPUTextureUsage.TEXTURE_BINDING |
565
+ GPUTextureUsage.COPY_SRC |
566
+ GPUTextureUsage.COPY_DST,
567
+ ...desc,
568
+ });
569
+ }
570
+ createFullscreenPass(desc) {
571
+ return new FullscreenPass(this.device, desc, this.samplers[desc.sampler ?? 'linear-clamp'], defaultTextures(this.device).white.view);
572
+ }
573
+ /**
574
+ * Upload an image/canvas/ImageData. By default it is an sRGB colour texture (sampled as
575
+ * linear). Pass `{ linear: true }` for data (masks, normals, lookup tables): the bytes are
576
+ * stored as `rgba8unorm` and sampled exactly as written, with no sRGB decode. `{ mips: true }`
577
+ * allocates the full mip chain and fills it on the GPU (`generateMipmaps`), the same as
578
+ * `loadTexture`; the default is one level. The options object may replace the label
579
+ * (`textureFromSource(src, { mips: true })`), or follow it.
580
+ */
581
+ textureFromSource(source, label = 'image', opts = {}) {
582
+ if (typeof label !== 'string') {
583
+ opts = label;
584
+ label = opts.label ?? 'image';
585
+ }
586
+ return loadTexture(this.device, source, {
587
+ label,
588
+ format: opts.linear ? 'rgba8unorm' : 'rgba8unorm-srgb',
589
+ mips: opts.mips ?? false,
590
+ });
591
+ }
592
+ /**
593
+ * Upload raw texels. Default format `rgba8unorm` (linear data, no sRGB decode); any format
594
+ * from `bytesPerTexel` works, e.g. `r8unorm` masks or `rgba32float` lookup tables. `data`
595
+ * holds `width × height` tightly packed texels.
596
+ */
597
+ textureFromPixels(desc) {
598
+ const tex = this.createTexture({
599
+ width: desc.width,
600
+ height: desc.height,
601
+ format: desc.format ?? 'rgba8unorm',
602
+ label: desc.label ?? 'pixels',
603
+ usage: desc.usage,
604
+ });
605
+ this.writePixels(tex, desc.data);
606
+ return tex;
607
+ }
608
+ /** Overwrite a whole texture with tightly packed texels (e.g. a CPU-updated data texture). */
609
+ writePixels(tex, data) {
610
+ const bpt = bytesPerTexel(tex.format);
611
+ const need = tex.width * tex.height * bpt;
612
+ if (data.byteLength < need) {
613
+ throw new Error(`writePixels: ${data.byteLength} bytes, ${tex.width}×${tex.height} needs ${need}`);
614
+ }
615
+ this.device.queue.writeTexture({ texture: tex.texture }, data, { bytesPerRow: tex.width * bpt, rowsPerImage: tex.height }, [tex.width, tex.height]);
616
+ }
617
+ /**
618
+ * Two render targets for feedback passes (see `PingPong`). Default format is the HDR
619
+ * scene format.
620
+ */
621
+ createPingPong(desc) {
622
+ return new PingPong(this.device, {
623
+ format: SCENE_FORMAT,
624
+ usage: GPUTextureUsage.RENDER_ATTACHMENT |
625
+ GPUTextureUsage.TEXTURE_BINDING |
626
+ GPUTextureUsage.COPY_SRC |
627
+ GPUTextureUsage.COPY_DST,
628
+ ...desc,
629
+ });
630
+ }
631
+ /**
632
+ * Generate a texture procedurally on the GPU. `wgsl` must define
633
+ * `fn shade(uv: vec2f) -> vec4f` returning **linear** colour (uv 0..1, top-left origin).
634
+ * Engine noise helpers (`engine_fbm` …) are available. Optional uniforms at
635
+ * `@group(0) @binding(0)`.
636
+ */
637
+ textureFromShader(opts) {
638
+ const format = opts.format ?? 'rgba8unorm-srgb';
639
+ const label = opts.label ?? 'procedural';
640
+ const tex = this.createTexture({ width: opts.width, height: opts.height, format, label });
641
+ const pass = this.createFullscreenPass({
642
+ label: `gen:${label}`,
643
+ targets: [format],
644
+ uniformBytes: opts.uniforms?.byteLength ?? 0,
645
+ wgsl: `${opts.wgsl}
646
+ @fragment fn fs_main(in: FullscreenIn) -> @location(0) vec4f { return shade(in.uv); }`,
647
+ });
648
+ if (opts.uniforms)
649
+ pass.setUniforms(opts.uniforms);
650
+ const enc = this.device.createCommandEncoder({ label: `gen:${label}` });
651
+ pass.encode(enc, tex.view);
652
+ this.device.queue.submit([enc.finish()]);
653
+ pass.destroy();
654
+ return tex;
655
+ }
656
+ // ---------------------------------------------------------------- frame
657
+ /**
658
+ * The renderer's jitter hook (ws/render-6): `fn(frame)` runs once per `beginFrame` (`frame`
659
+ * counts from 0 since the hook was set) and its `[x, y]` (target pixels, +x right, +y down)
660
+ * goes to every camera `render` draws this frame, XR eyes included, through
661
+ * `camera.setJitter`, with the jitter target set per view as when the post processor
662
+ * `wantsJitter`. The renderer keeps each camera's history (`recordHistory`), so
663
+ * `previousViewProj` and `previousView` stay unjittered. `null` removes the hook and clears
664
+ * the jitter of every camera it reached. Without a hook `Camera.setJitter` works as before.
665
+ * Return a reused tuple to keep the frame allocation-free.
666
+ */
667
+ setJitter(fn) {
668
+ if (fn === this.jitterFn)
669
+ return;
670
+ this.jitterFn = fn;
671
+ this.jitterFrame = 0;
672
+ this.jitterX = 0;
673
+ this.jitterY = 0;
674
+ if (fn)
675
+ return;
676
+ for (const c of this.jitterCams)
677
+ c.clearJitter();
678
+ this.jitterCams.length = 0;
679
+ }
680
+ /**
681
+ * The jitter hook installed by `setJitter`, or null when none is (ws/render-8). Read-only:
682
+ * compare it by identity, so a post stack can check whether the renderer still holds the
683
+ * hook it installed (`renderer.jitter === myHook`) before clearing it, and leave another
684
+ * owner's hook alone.
685
+ */
686
+ get jitter() {
687
+ return this.jitterFn;
688
+ }
689
+ /**
690
+ * The LOD level instance `index` of a LOD batch or list (`frame.meshes` / `frame.list` on a
691
+ * `MeshLod`) drew at for `camera` (default `this.camera`) in the last frame that drew it,
692
+ * or -1 when it has none (ws/render-7). For probes and debug overlays: it reads the view's
693
+ * level memory, which a keyed `InstanceList` carries by key across flushes.
694
+ */
695
+ lodLevelOf(batch, index, camera = this.camera) {
696
+ const slots = this.lodMemory.get(camera)?.indexed.get(batch);
697
+ if (!slots || index < 0 || index >= slots.levels.length)
698
+ return -1;
699
+ return slots.levels[index];
700
+ }
701
+ /** Start recording a frame. `time`/`dt` in seconds are exposed to shaders via `globals`. */
702
+ beginFrame(time, dt) {
703
+ if (this.inFrame)
704
+ throw new Error('beginFrame called twice without endFrame');
705
+ this.inFrame = true;
706
+ if (this.timingEvery > 0)
707
+ this.frameStart = performance.now();
708
+ this.instanceCount = 0;
709
+ this.batchInstances = 0;
710
+ this.batchRecs = 0;
711
+ this.lodRecs = 0;
712
+ this.laneRecs = 0;
713
+ this.scene.count = 0;
714
+ this.shadowCmds.count = 0;
715
+ this.recCount = 0;
716
+ this.ordered = true;
717
+ this.lastOrder = Number.NEGATIVE_INFINITY;
718
+ this.prePasses.length = 0;
719
+ this.prepared.clear();
720
+ for (const b of this.drawUniformSets)
721
+ b.discardDrawUniforms();
722
+ this.drawUniformSets.length = 0;
723
+ this.stats.drawCalls = 0;
724
+ this.stats.triangles = 0;
725
+ this.stats.instances = 0;
726
+ this.stats.drawables = 0;
727
+ this.stats.batches = 0;
728
+ this.stats.culled = 0;
729
+ this.stats.shadowDraws = 0;
730
+ this.stats.passes = 0;
731
+ const lc = this.stats.lodCounts;
732
+ for (let i = 0; i < lc.length; i++)
733
+ lc[i] = 0;
734
+ const jf = this.jitterFn;
735
+ if (jf) {
736
+ const j = jf(this.jitterFrame++);
737
+ this.jitterX = j[0];
738
+ this.jitterY = j[1];
739
+ }
740
+ const f = this.frame;
741
+ f.time = time;
742
+ f.dt = dt;
743
+ return f;
744
+ }
745
+ /**
746
+ * Run the draw lists recorded in `frame` through `camera` into `target` (default: the
747
+ * renderer's own, which the surface shows). Call it any number of times between
748
+ * `beginFrame` and `endFrame`; `endFrame` calls it with `renderer.camera` when nothing did.
749
+ *
750
+ * Each call is one scene pass: the camera's layer mask culls, its depth flags pick the sort
751
+ * and the depth attachment, and its `screen` rect is the viewport. A `screen` in logical
752
+ * units (the `ortho2d` default is the whole design space) maps onto the target in
753
+ * proportion; one inside the unit square (the perspective default `0,0,1,1`) is a fraction
754
+ * of it. The first view into a target this frame clears it (`frame.clearColor`); later ones
755
+ * draw over it, so a UI camera over a world camera or two half-screen cameras just work.
756
+ * The first call opens the frame's command encoder and runs the pre-passes and drawable
757
+ * `prepare` hooks (once per frame). No allocation in steady state.
758
+ */
759
+ render(frame, camera, target = this.target) {
760
+ if (!this.inFrame)
761
+ throw new Error('render outside beginFrame/endFrame');
762
+ if (frame !== this.frame)
763
+ throw new Error("render: frame is not this renderer's");
764
+ if (this.destroyed)
765
+ return;
766
+ const device = this.device;
767
+ const timer = this.gpuTimer;
768
+ let enc = this.encoder;
769
+ if (!enc) {
770
+ enc = this.encoder = device.createCommandEncoder({ label: 'frame' });
771
+ this.sampled = this.timingEvery > 0 && this.wantSample();
772
+ if (timer && this.sampled && timer.begin())
773
+ timer.mark(enc);
774
+ for (const fn of this.prePasses)
775
+ fn(enc);
776
+ for (const b of this.drawUniformSets)
777
+ b.flushDrawUniforms();
778
+ this.drawUniformSets.length = 0;
779
+ }
780
+ // Viewport: camera.screen in target pixels (see the doc comment).
781
+ const vp = this.viewRect;
782
+ const sc = camera.screen;
783
+ const tw = target.width;
784
+ const th = target.height;
785
+ if (sc.x >= 0 && sc.y >= 0 && sc.x + sc.width <= 1 && sc.y + sc.height <= 1) {
786
+ vp.x = sc.x * tw;
787
+ vp.y = sc.y * th;
788
+ vp.width = sc.width * tw;
789
+ vp.height = sc.height * th;
790
+ }
791
+ else {
792
+ const kx = tw / frame.logicalWidth;
793
+ const ky = th / frame.logicalHeight;
794
+ vp.x = sc.x * kx;
795
+ vp.y = sc.y * ky;
796
+ vp.width = sc.width * kx;
797
+ vp.height = sc.height * ky;
798
+ }
799
+ const full = vp.x === 0 && vp.y === 0 && vp.width === tw && vp.height === th;
800
+ // History first, before anything reads the camera: last frame's matrices are what the
801
+ // camera held at its previous view. Jitter applies only when the post stack asks for it.
802
+ camera.recordHistory();
803
+ if (this.jitterFn) {
804
+ camera.setJitter(this.jitterX, this.jitterY);
805
+ if (!this.jitterCams.includes(camera))
806
+ this.jitterCams.push(camera);
807
+ }
808
+ if (this.jitterFn || this.post?.wantsJitter)
809
+ camera.setJitterTarget(vp.width, vp.height);
810
+ else
811
+ camera.setJitterTarget(0, 0);
812
+ // The sun box is placed before the draw lists are built: the shadow list culls against it.
813
+ const shadowCfg = frame.lights.sun.shadow;
814
+ if (shadowCfg) {
815
+ const size = shadowCfg.size ?? DEFAULT_SHADOW_SIZE;
816
+ this.shadow.place(camera.position, frame.lights.sun.direction, shadowCfg, size);
817
+ }
818
+ const depth = this.buildDraws(camera, target);
819
+ this.stats.culled += this.culledCount;
820
+ const scene = this.scene;
821
+ for (let i = 0; i < scene.count; i++) {
822
+ const d = scene.cmds[i].drawable;
823
+ if (d?.prepare && !this.prepared.has(d)) {
824
+ this.prepared.add(d);
825
+ d.prepare(enc, frame);
826
+ }
827
+ }
828
+ // Instances of this view go after the previous views' in the shared buffer.
829
+ const base = this.instanceBase;
830
+ const uploadCount = this.uploadCount;
831
+ if (uploadCount > 0) {
832
+ const needed = uploadCount * INSTANCE_STRIDE;
833
+ if (base + needed > this.instanceBuffer.size) {
834
+ this.retired.push(this.instanceBuffer);
835
+ let cap = this.instanceData.length / INSTANCE_FLOATS;
836
+ while (cap * INSTANCE_STRIDE < base + needed)
837
+ cap *= 2;
838
+ this.instanceBuffer = this.makeInstanceBuffer(cap);
839
+ }
840
+ device.queue.writeBuffer(this.instanceBuffer, base, this.uploadData.buffer, 0, needed);
841
+ this.instanceBase = base + needed;
842
+ }
843
+ if (this.laneUsed)
844
+ this.uploadLane(base);
845
+ const shadow = this.encodeShadow(enc, frame, camera, base);
846
+ const slot = this.viewSlot(this.viewCount);
847
+ this.writeView(frame, camera, target, slot, shadow);
848
+ this.viewCount++;
849
+ // First view into this target clears it; later ones draw over.
850
+ let used = -1;
851
+ for (let i = 0; i < this.usedCount; i++) {
852
+ if (this.usedTargets[i] === target) {
853
+ used = i;
854
+ break;
855
+ }
856
+ }
857
+ const first = used < 0;
858
+ if (first) {
859
+ used = this.usedCount++;
860
+ this.usedTargets[used] = target;
861
+ this.usedDepth[used] = false;
862
+ }
863
+ target.colorAtt.clearValue = frame.clearColor;
864
+ target.colorAtt.loadOp = first ? 'clear' : 'load';
865
+ target.emissiveAtt.loadOp = first ? 'clear' : 'load';
866
+ target.setAuxLoadOp(first ? 'clear' : 'load');
867
+ this.usedCamera[used] = camera;
868
+ const desc = target.passDesc;
869
+ desc.timestampWrites = timer?.pass('scene');
870
+ if (depth) {
871
+ const att = target.depthAttachment();
872
+ att.depthLoadOp = this.usedDepth[used] ? 'load' : 'clear';
873
+ this.usedDepth[used] = true;
874
+ desc.depthStencilAttachment = att;
875
+ }
876
+ else {
877
+ desc.depthStencilAttachment = undefined;
878
+ }
879
+ // A view with a `readsDepth` drawable splits here: opaque draws, a copy of the depth
880
+ // attachment, the drawables' `afterOpaque`, then the blended draws loading everything.
881
+ const split = depth && this.splitAt >= 0 && target.canCopyDepth ? this.splitAt : -1;
882
+ const pass = enc.beginRenderPass(desc);
883
+ this.setViewRect(pass, vp, full);
884
+ pass.setBindGroup(0, slot.group);
885
+ this.encodeDraws(pass, frame, slot, base, 0, split >= 0 ? split : scene.count);
886
+ pass.end();
887
+ this.stats.passes++;
888
+ if (split >= 0) {
889
+ const copy = target.copyDepth(enc);
890
+ const info = this.viewInfo;
891
+ info.camera = camera;
892
+ info.target = target;
893
+ info.depthCopy = copy.texture;
894
+ info.depthView = copy.view;
895
+ info.viewport.x = vp.x;
896
+ info.viewport.y = vp.y;
897
+ info.viewport.width = vp.width;
898
+ info.viewport.height = vp.height;
899
+ const hooked = this.hooked;
900
+ hooked.clear();
901
+ for (let i = split; i < scene.count; i++) {
902
+ const d = scene.cmds[i].drawable;
903
+ if (d?.readsDepth && d.afterOpaque && !hooked.has(d)) {
904
+ hooked.add(d);
905
+ d.afterOpaque(enc, info);
906
+ }
907
+ }
908
+ const blendDesc = target.blendPassDesc;
909
+ blendDesc.timestampWrites = timer?.pass('scene-blend');
910
+ const blend = enc.beginRenderPass(blendDesc);
911
+ this.setViewRect(blend, vp, full);
912
+ blend.setBindGroup(0, slot.group);
913
+ this.encodeDraws(blend, frame, slot, base, split, scene.count);
914
+ blend.end();
915
+ this.stats.passes++;
916
+ }
917
+ this.stats.instances += this.sceneCount + this.sceneListInstances;
918
+ this.stats.batches += this.sceneBatches;
919
+ }
920
+ /** Viewport and scissor of a view that does not cover its target. */
921
+ setViewRect(pass, vp, full) {
922
+ if (full)
923
+ return;
924
+ pass.setViewport(vp.x, vp.y, vp.width, vp.height, 0, 1);
925
+ pass.setScissorRect(Math.floor(vp.x), Math.floor(vp.y), Math.ceil(vp.width), Math.ceil(vp.height));
926
+ }
927
+ /**
928
+ * Encode scene commands `[from, to)` into `pass` (group 0 already bound to `slot.group`),
929
+ * counting draw calls and drawables into `stats`.
930
+ */
931
+ encodeDraws(pass, frame, slot, base, from, to) {
932
+ const scene = this.scene;
933
+ let curPipeline = null;
934
+ let curMaterial = null;
935
+ let curOffset = -1;
936
+ let curMesh = null;
937
+ let curBuffer = null;
938
+ let curPrev = null;
939
+ let drawCalls = 0;
940
+ let triangles = 0;
941
+ let drawables = 0;
942
+ for (let i = from; i < to; i++) {
943
+ const c = scene.cmds[i];
944
+ if (c.drawable) {
945
+ c.drawable.draw(pass, frame);
946
+ drawables++;
947
+ curPipeline = null;
948
+ curMaterial = null;
949
+ curOffset = -1;
950
+ curMesh = null;
951
+ curBuffer = null;
952
+ curPrev = null;
953
+ pass.setBindGroup(0, slot.group);
954
+ continue;
955
+ }
956
+ const m = c.material;
957
+ const mesh = c.mesh;
958
+ if (c.pipeline !== curPipeline) {
959
+ pass.setPipeline(c.pipeline);
960
+ curPipeline = c.pipeline;
961
+ }
962
+ if (c.lane) {
963
+ const lane = this.laneBuffer;
964
+ if (lane !== curPrev) {
965
+ pass.setVertexBuffer(2, lane, this.laneBase);
966
+ curPrev = lane;
967
+ }
968
+ }
969
+ else if (c.prevBuffer && c.prevBuffer !== curPrev) {
970
+ pass.setVertexBuffer(2, c.prevBuffer, 0);
971
+ curPrev = c.prevBuffer;
972
+ }
973
+ if (m !== curMaterial || c.offset !== curOffset) {
974
+ if (c.offset >= 0) {
975
+ this.dynamicOffsets[0] = c.offset;
976
+ pass.setBindGroup(1, m.bindings.bindGroup, this.dynamicOffsets);
977
+ }
978
+ else if (m.bindings) {
979
+ pass.setBindGroup(1, m.bindings.bindGroup);
980
+ }
981
+ curMaterial = m;
982
+ curOffset = c.offset;
983
+ }
984
+ if (mesh !== curMesh || c.buffer !== curBuffer) {
985
+ pass.setVertexBuffer(0, mesh.vertexBuffer);
986
+ if (c.buffer)
987
+ pass.setVertexBuffer(1, c.buffer, 0);
988
+ else
989
+ pass.setVertexBuffer(1, this.instanceBuffer, base);
990
+ if (mesh.indexBuffer)
991
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
992
+ curMesh = mesh;
993
+ curBuffer = c.buffer;
994
+ }
995
+ if (mesh.indexBuffer) {
996
+ // An indexed mesh with no indices (e.g. a streamed chunk that is empty) draws nothing.
997
+ const indices = c.indexCount >= 0 ? c.indexCount : mesh.indexCount;
998
+ if (indices === 0)
999
+ continue;
1000
+ pass.drawIndexed(indices, c.count, c.firstIndex, 0, c.first);
1001
+ triangles += ((indices / 3) | 0) * c.count;
1002
+ }
1003
+ else {
1004
+ pass.draw(mesh.vertexCount, c.count, 0, c.first);
1005
+ triangles += ((mesh.vertexCount / 3) | 0) * c.count;
1006
+ }
1007
+ drawCalls++;
1008
+ }
1009
+ this.stats.drawCalls += drawCalls;
1010
+ this.stats.triangles += triangles;
1011
+ this.stats.drawables += drawables;
1012
+ }
1013
+ /**
1014
+ * Run the post stack (or the built-in present) from `target` into `out` now, inside the frame
1015
+ * (ws/xr-post): what `endFrame` does from `renderer.target` to the swapchain, for any target
1016
+ * and any output. `viewport` confines the result to that part of `out` (texture px) and loads
1017
+ * the rest, so two eyes resolve into one XR layer texture one after the other. The target is
1018
+ * consumed: the next `render` into it this frame clears it again, so one HDR target serves
1019
+ * every eye in turn. Needs a `render` before it this frame (the encoder is open then).
1020
+ */
1021
+ resolve(target, out, viewport) {
1022
+ if (!this.inFrame)
1023
+ throw new Error('resolve outside beginFrame/endFrame');
1024
+ if (this.destroyed)
1025
+ return;
1026
+ const enc = this.encoder;
1027
+ if (!enc)
1028
+ throw new Error('resolve before any render this frame');
1029
+ if (this.post) {
1030
+ const inputs = this.resolveInputs;
1031
+ inputs.color = target.color;
1032
+ inputs.emissive = target.emissive;
1033
+ inputs.time = this.frame.time;
1034
+ inputs.dt = this.frame.dt;
1035
+ this.fillDepthInputs(inputs, target);
1036
+ this.post.encode(enc, inputs, out, viewport);
1037
+ }
1038
+ else {
1039
+ if (!this.resolveGroup ||
1040
+ this.resolveTarget !== target ||
1041
+ this.resolveColor !== target.color.view) {
1042
+ this.resolveTarget = target;
1043
+ this.resolveColor = target.color.view;
1044
+ this.resolveGroup = this.present.createBindGroup([target.color.view, target.emissive.view]);
1045
+ }
1046
+ this.present.encode(enc, out, {
1047
+ bindGroup: this.resolveGroup,
1048
+ clear: viewport ? null : PRESENT_CLEAR,
1049
+ viewport,
1050
+ });
1051
+ }
1052
+ // Consumed: forget the target so its next view this frame clears colour and depth.
1053
+ for (let i = 0; i < this.usedCount; i++) {
1054
+ if (this.usedTargets[i] === target) {
1055
+ const last = --this.usedCount;
1056
+ this.usedTargets[i] = this.usedTargets[last];
1057
+ this.usedDepth[i] = this.usedDepth[last];
1058
+ this.usedCamera[i] = this.usedCamera[last];
1059
+ break;
1060
+ }
1061
+ }
1062
+ }
1063
+ /**
1064
+ * `PostInputs.depth` / `projection` (ws/post-depth): the target's depth and the last depth
1065
+ * camera's projection when one rendered into it this frame, else `null`. No allocation.
1066
+ */
1067
+ fillDepthInputs(inputs, target) {
1068
+ inputs.depth = null;
1069
+ inputs.projection = null;
1070
+ inputs.view = null;
1071
+ inputs.roughMetal = target.roughMetal;
1072
+ inputs.velocity = target.velocity;
1073
+ for (let i = 0; i < this.usedCount; i++) {
1074
+ if (this.usedTargets[i] !== target)
1075
+ continue;
1076
+ const cam = this.usedCamera[i];
1077
+ const view = inputs === this.postInputs ? this.postView : this.resolveView;
1078
+ view.viewProj.set(cam.viewProjection);
1079
+ view.invViewProj.set(cam.invViewProj);
1080
+ view.previousViewProj.set(cam.previousViewProj);
1081
+ view.previousView.set(cam.previousView);
1082
+ cam.getJitter(view.jitter);
1083
+ inputs.view = view;
1084
+ if (!this.usedDepth[i])
1085
+ return;
1086
+ const depth = target.depth;
1087
+ if (!depth)
1088
+ return;
1089
+ const p = this.postProjection;
1090
+ p.proj.set(this.usedCamera[i].projection);
1091
+ // near = p14 / p10 for both kinds; far = p14 / (p10 + 1) for perspective (∞ when
1092
+ // p10 = -1), (p14 - 1) / p10 for an orthographic matrix (p11 = 0).
1093
+ const p10 = p.proj[10];
1094
+ const p14 = p.proj[14];
1095
+ p.near = p14 / p10;
1096
+ if (p.proj[11] === 0) {
1097
+ p.far = (p14 - 1) / p10;
1098
+ }
1099
+ else {
1100
+ const k = p10 + 1;
1101
+ p.far = Math.abs(k) < 1e-7 ? Number.POSITIVE_INFINITY : p14 / k;
1102
+ }
1103
+ inputs.depth = depth;
1104
+ inputs.projection = p;
1105
+ return;
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Finish the frame: `render(frame, renderer.camera)` when nothing rendered yet, then the
1110
+ * post stack (or the built-in present) from `renderer.target` to the swapchain, and submit.
1111
+ * `{ present: false }` skips the post stack and the swapchain (no `getCurrentTexture`): the
1112
+ * frame's views went to their own targets, as an XR session's eyes do (ws/xr), resolved into
1113
+ * the layer with `resolve` (ws/xr-post).
1114
+ */
1115
+ endFrame(opts) {
1116
+ if (!this.inFrame)
1117
+ throw new Error('endFrame without beginFrame');
1118
+ if (this.destroyed) {
1119
+ this.inFrame = false;
1120
+ return;
1121
+ }
1122
+ if (!this.encoder)
1123
+ this.render(this.frame, this.camera);
1124
+ this.inFrame = false;
1125
+ const enc = this.encoder;
1126
+ this.encoder = null;
1127
+ const timer = this.gpuTimer;
1128
+ const sampled = this.sampled;
1129
+ if (opts?.present !== false) {
1130
+ const out = this.context.getCurrentTexture().createView();
1131
+ if (this.post) {
1132
+ const inputs = this.postInputs;
1133
+ inputs.time = this.frame.time;
1134
+ inputs.dt = this.frame.dt;
1135
+ this.fillDepthInputs(inputs, this.target);
1136
+ this.post.encode(enc, inputs, out);
1137
+ }
1138
+ else {
1139
+ this.present.encode(enc, out, {
1140
+ bindGroup: this.presentGroup,
1141
+ clear: { r: 0, g: 0, b: 0, a: 1 },
1142
+ });
1143
+ }
1144
+ }
1145
+ timer?.resolve(enc);
1146
+ this.device.queue.submit([enc.finish()]);
1147
+ this.viewCount = 0;
1148
+ this.shadowCount = 0;
1149
+ this.usedCount = 0;
1150
+ this.instanceBase = 0;
1151
+ this.shadow.flush();
1152
+ if (this.retired.length > 0) {
1153
+ for (const b of this.retired)
1154
+ b.destroy();
1155
+ this.retired.length = 0;
1156
+ }
1157
+ if (this.timingEvery > 0) {
1158
+ const now = performance.now();
1159
+ this.timing.cpuMs = now - this.frameStart;
1160
+ if (timer)
1161
+ timer.afterSubmit();
1162
+ else if (sampled) {
1163
+ this.timingPending = true;
1164
+ this.timingSubmit = now;
1165
+ this.device.queue.onSubmittedWorkDone().then(this.onTimingDone, this.onTimingDone);
1166
+ }
1167
+ }
1168
+ }
1169
+ /** Every `timingEvery` frames, when no sample is in flight. */
1170
+ wantSample() {
1171
+ if (this.gpuTimer ? this.gpuTimer.busy : this.timingPending)
1172
+ return false;
1173
+ if (++this.timingFrame < this.timingEvery)
1174
+ return false;
1175
+ this.timingFrame = 0;
1176
+ return true;
1177
+ }
1178
+ emitGpuTime(ms) {
1179
+ this.timing.gpuMs = ms;
1180
+ this.timing.samples++;
1181
+ for (const fn of this.timingListeners)
1182
+ fn(ms);
1183
+ this.resolveReady();
1184
+ }
1185
+ /** `timing.ready()`: a settled promise once a sample exists, else one kept until the first. */
1186
+ timingReady() {
1187
+ // Unsupported: no timer and no `onSubmittedWorkDone` (a stub device); nothing ever samples.
1188
+ const unsupported = !this.gpuTimer && typeof this.device.queue.onSubmittedWorkDone !== 'function';
1189
+ if (this.timing.samples > 0 || this.destroyed || unsupported)
1190
+ return Promise.resolve();
1191
+ return new Promise((resolve) => {
1192
+ this.readyWaiters.push(resolve);
1193
+ });
1194
+ }
1195
+ resolveReady() {
1196
+ if (this.readyWaiters.length === 0)
1197
+ return;
1198
+ for (const fn of this.readyWaiters)
1199
+ fn();
1200
+ this.readyWaiters.length = 0;
1201
+ }
1202
+ destroy() {
1203
+ this.destroyed = true;
1204
+ this.resolveReady();
1205
+ this.gpuTimer?.destroy();
1206
+ this.target.destroy();
1207
+ this.instanceBuffer.destroy();
1208
+ this.laneBuffer?.destroy();
1209
+ this.laneBuffer = null;
1210
+ for (const b of this.retired)
1211
+ b.destroy();
1212
+ this.retired.length = 0;
1213
+ for (const v of this.views)
1214
+ v.buffer.destroy();
1215
+ this.views.length = 0;
1216
+ for (const v of this.shadowSlots)
1217
+ v.buffer.destroy();
1218
+ this.shadowSlots.length = 0;
1219
+ this.shadow.destroy();
1220
+ this.quad.destroy();
1221
+ // `white` belongs to the device's `defaultTextures` registry and outlives the renderer.
1222
+ }
1223
+ // ---------------------------------------------------------------- FrameSink
1224
+ pushInstance(material, mesh, tr, p, bounds, lod = null) {
1225
+ let offset = -1;
1226
+ const b = material.bindings;
1227
+ if (b?.perDraw) {
1228
+ if (p.uniforms) {
1229
+ if (b.drawCount === 0)
1230
+ this.drawUniformSets.push(b);
1231
+ offset = b.pushDrawUniforms(p.uniforms);
1232
+ }
1233
+ else {
1234
+ offset = 0;
1235
+ }
1236
+ }
1237
+ else if (p.uniforms) {
1238
+ throw new Error(`material ${material.label}: DrawParams.uniforms needs perDrawUniforms`);
1239
+ }
1240
+ const idx = this.instanceCount++;
1241
+ if (this.instanceCount * INSTANCE_FLOATS > this.instanceData.length)
1242
+ this.growInstances();
1243
+ const d = this.instanceData;
1244
+ let o = idx * INSTANCE_FLOATS;
1245
+ let sx;
1246
+ let sy;
1247
+ if (tr) {
1248
+ d.set(tr.world, o);
1249
+ sx = tr.scale.x;
1250
+ sy = tr.scale.y;
1251
+ }
1252
+ else {
1253
+ const dp = p;
1254
+ sx = dp.w ?? 1;
1255
+ sy = dp.h ?? 1;
1256
+ writeMatrix2d(d, o, dp.x, dp.y, sx, sy, dp.rotation ?? 0);
1257
+ }
1258
+ o += 12;
1259
+ const uv = p.uv;
1260
+ d[o++] = uv ? uv[0] : 0;
1261
+ d[o++] = uv ? uv[1] : 0;
1262
+ d[o++] = uv ? uv[2] : 1;
1263
+ d[o++] = uv ? uv[3] : 1;
1264
+ const t = p.tint;
1265
+ d[o++] = t ? t[0] : 1;
1266
+ d[o++] = t ? t[1] : 1;
1267
+ d[o++] = t ? t[2] : 1;
1268
+ d[o++] = t ? (t[3] ?? 1) : 1;
1269
+ const q = p.params;
1270
+ d[o++] = q ? q[0] : 0;
1271
+ d[o++] = q ? (q[1] ?? 0) : 0;
1272
+ d[o++] = q ? (q[2] ?? 0) : 0;
1273
+ d[o++] = q ? (q[3] ?? 0) : 0;
1274
+ d[o++] = p.emissive ?? 0;
1275
+ d[o++] = packInstanceFlags(p.receiveShadow !== false, p.layers ?? 1); // flags: bit 0 = no shadow, bits 1.. = layers
1276
+ d[o++] = sx;
1277
+ d[o] = sy;
1278
+ const r = this.nextRec(p.order ?? 0, p.layers ?? 1);
1279
+ r.material = material;
1280
+ r.mesh = mesh;
1281
+ r.drawable = null;
1282
+ r.offset = offset;
1283
+ r.inst = idx;
1284
+ if (tr) {
1285
+ r.xf = tr;
1286
+ // Only an aux material can write object motion: the lane is built for those alone.
1287
+ if (material.aux)
1288
+ this.laneRecs++;
1289
+ }
1290
+ if (bounds && bounds.radius !== Number.POSITIVE_INFINITY) {
1291
+ // The sphere through the 3×4 instance matrix: centre by the matrix, radius by the largest
1292
+ // axis scale (column length), which bounds any rotation and non-uniform scale.
1293
+ const m = idx * INSTANCE_FLOATS;
1294
+ const c = bounds.center;
1295
+ const cx = c[0];
1296
+ const cy = c[1];
1297
+ const cz = c[2];
1298
+ r.bx = d[m] * cx + d[m + 1] * cy + d[m + 2] * cz + d[m + 3];
1299
+ r.by = d[m + 4] * cx + d[m + 5] * cy + d[m + 6] * cz + d[m + 7];
1300
+ r.bz = d[m + 8] * cx + d[m + 9] * cy + d[m + 10] * cz + d[m + 11];
1301
+ const s0 = d[m] * d[m] + d[m + 4] * d[m + 4] + d[m + 8] * d[m + 8];
1302
+ const s1 = d[m + 1] * d[m + 1] + d[m + 5] * d[m + 5] + d[m + 9] * d[m + 9];
1303
+ const s2 = d[m + 2] * d[m + 2] + d[m + 6] * d[m + 6] + d[m + 10] * d[m + 10];
1304
+ r.br = bounds.radius * Math.sqrt(Math.max(s0, s1, s2));
1305
+ }
1306
+ else {
1307
+ r.br = -1;
1308
+ }
1309
+ r.castShadow = p.castShadow ?? mesh.layout !== VERTEX_LAYOUT_2D;
1310
+ const range = p.range;
1311
+ if (range) {
1312
+ r.rangeFirst = range.first;
1313
+ r.rangeCount = range.count;
1314
+ }
1315
+ if (lod) {
1316
+ r.lod = lod;
1317
+ r.lodKey = tr ?? p;
1318
+ this.lodRecs++;
1319
+ }
1320
+ }
1321
+ pushBatch(material, mesh, batch, bounds, list, lod = null) {
1322
+ if (material.bindings?.perDraw) {
1323
+ throw new Error(`material ${material.label}: a batch cannot use perDrawUniforms`);
1324
+ }
1325
+ if (list)
1326
+ list.flush(this.device.queue);
1327
+ // A LOD list is copied into the upload bucketed per level, like a batch.
1328
+ if (!list || lod)
1329
+ this.batchInstances += batch.count;
1330
+ this.batchRecs++;
1331
+ const r = this.nextRec(batch.order ?? 0, batch.layerBits ?? 1);
1332
+ r.material = material;
1333
+ r.mesh = mesh;
1334
+ r.drawable = null;
1335
+ r.offset = -1;
1336
+ r.inst = -1;
1337
+ r.batch = batch;
1338
+ r.list = lod ? null : list;
1339
+ if (lod) {
1340
+ r.lod = lod;
1341
+ this.lodRecs++;
1342
+ }
1343
+ // A copied batch (not a list drawn from its own buffer) brings its previous matrices.
1344
+ const prev = !r.list ? (batch.prevMatrices ?? null) : null;
1345
+ if (prev) {
1346
+ if (prev.length < batch.count * MATRIX3X4_FLOATS) {
1347
+ throw new Error(`frame.meshes: prevMatrices holds ${prev.length} floats for ${batch.count} instances`);
1348
+ }
1349
+ r.prev = prev;
1350
+ this.laneRecs++;
1351
+ }
1352
+ const bb = batch.bounds;
1353
+ if (bb) {
1354
+ r.bx = bb.center[0];
1355
+ r.by = bb.center[1];
1356
+ r.bz = bb.center[2];
1357
+ r.br = bb.radius;
1358
+ r.mb = null;
1359
+ }
1360
+ else {
1361
+ r.br = -1;
1362
+ r.mb = bounds && bounds.radius !== Number.POSITIVE_INFINITY ? bounds : null;
1363
+ }
1364
+ // A LOD list is copied per instance anyway, so its instances are culled one by one too.
1365
+ if (lod && list) {
1366
+ const lb = lod.meshes[0].bounds;
1367
+ r.mb = lb.radius !== Number.POSITIVE_INFINITY ? lb : null;
1368
+ }
1369
+ r.castShadow = batch.castShadow ?? mesh.layout !== VERTEX_LAYOUT_2D;
1370
+ }
1371
+ /**
1372
+ * A retained `InstanceList` of up to `capacity` instances of `mesh` with `material`, drawn
1373
+ * with `frame.list(list)`. Owns a GPU buffer; `list.destroy()` releases it. A `MeshLod`
1374
+ * (ws/render-6) makes a list whose instances pick their level per view.
1375
+ */
1376
+ createInstanceList(mesh, material, capacity, opts) {
1377
+ if (material.bindings?.perDraw) {
1378
+ throw new Error(`material ${material.label}: an InstanceList cannot use perDrawUniforms`);
1379
+ }
1380
+ return new InstanceList(this.device, mesh, material, capacity, opts);
1381
+ }
1382
+ pushDrawable(d, order, layers) {
1383
+ const r = this.nextRec(order, layers);
1384
+ r.material = null;
1385
+ r.mesh = null;
1386
+ r.drawable = d;
1387
+ r.offset = -1;
1388
+ r.inst = -1;
1389
+ r.br = -1;
1390
+ r.castShadow = false;
1391
+ }
1392
+ pushPrePass(fn) {
1393
+ this.prePasses.push(fn);
1394
+ }
1395
+ // ---------------------------------------------------------------- draw list
1396
+ nextRec(order, layers) {
1397
+ if (order < this.lastOrder)
1398
+ this.ordered = false;
1399
+ this.lastOrder = order;
1400
+ if (this.recCount === this.recs.length) {
1401
+ this.recs.push({
1402
+ material: null,
1403
+ mesh: null,
1404
+ drawable: null,
1405
+ offset: -1,
1406
+ inst: -1,
1407
+ order: 0,
1408
+ layers: 1,
1409
+ group: 0,
1410
+ dist: 0,
1411
+ bx: 0,
1412
+ by: 0,
1413
+ bz: 0,
1414
+ br: -1,
1415
+ castShadow: false,
1416
+ mk: 0,
1417
+ tk: 0,
1418
+ batch: null,
1419
+ list: null,
1420
+ mb: null,
1421
+ rangeFirst: 0,
1422
+ rangeCount: -1,
1423
+ lod: null,
1424
+ lodKey: null,
1425
+ lodLevel: -1,
1426
+ lodLevels: null,
1427
+ lk: -1,
1428
+ lt: -1,
1429
+ prev: null,
1430
+ lane: false,
1431
+ xf: null,
1432
+ });
1433
+ }
1434
+ const r = this.recs[this.recCount++];
1435
+ r.prev = null;
1436
+ r.lane = false;
1437
+ r.xf = null;
1438
+ r.lod = null;
1439
+ r.lodKey = null;
1440
+ r.lodLevel = -1;
1441
+ r.lodLevels = null;
1442
+ r.lk = -1;
1443
+ r.lt = -1;
1444
+ r.batch = null;
1445
+ r.list = null;
1446
+ r.mb = null;
1447
+ r.rangeFirst = 0;
1448
+ r.rangeCount = -1;
1449
+ r.order = order;
1450
+ r.layers = layers;
1451
+ return r;
1452
+ }
1453
+ /**
1454
+ * Turn this frame's draw records into batched commands. Draws whose `layers` miss the
1455
+ * camera's mask are dropped; with `camera.cull`, so are mesh draws whose world bounding
1456
+ * sphere lies outside any plane of `camera.frustum` (counted in `culledCount`). Without
1457
+ * depth or culling, draws sort by `order` then submission index; when every `order` is
1458
+ * already non-decreasing (the default) nothing moves and the instance data uploads as
1459
+ * recorded. With `camera.depthTest`, the keys are `order`, then opaque before blended, then
1460
+ * depth (opaque front-to-back, blended back-to-front), then submission index. Sets
1461
+ * `uploadData` / `uploadCount`; returns whether depth is on. With `frame.lights.sun.shadow`
1462
+ * set, `buildShadowDraws` then appends the shadow list's instances to the same upload.
1463
+ */
1464
+ buildDraws(cam, target) {
1465
+ this.drawTarget = target;
1466
+ const camLayers = cam.layers >>> 0;
1467
+ const depth = cam.depthTest;
1468
+ const depthWrite = cam.depthWrite;
1469
+ const cull = cam.cull;
1470
+ const shadowCfg = this.frame.lights.sun.shadow;
1471
+ this.frame.depthFormat = depth ? DEPTH_FORMAT : null;
1472
+ this.frame.auxKey = target.auxKey;
1473
+ const recs = this.recs;
1474
+ const n = this.recCount;
1475
+ this.scene.count = 0;
1476
+ this.shadowCmds.count = 0;
1477
+ this.culledCount = 0;
1478
+ this.splitAt = -1;
1479
+ let visible = n;
1480
+ if (camLayers !== ALL_LAYERS) {
1481
+ visible = 0;
1482
+ for (let i = 0; i < n; i++)
1483
+ if ((recs[i].layers & camLayers) !== 0)
1484
+ visible++;
1485
+ }
1486
+ this.sceneBatches = 0;
1487
+ this.sceneListInstances = 0;
1488
+ const lods = this.lodRecs > 0;
1489
+ if (lods)
1490
+ this.pickLods(cam);
1491
+ if (!depth &&
1492
+ !cull &&
1493
+ !lods &&
1494
+ this.ordered &&
1495
+ visible === n &&
1496
+ !shadowCfg &&
1497
+ this.batchRecs === 0 &&
1498
+ this.laneRecs === 0) {
1499
+ for (let i = 0; i < n; i++) {
1500
+ const r = recs[i];
1501
+ this.batch(this.scene, r, r.drawable ? null : this.scenePipeline(r, 0), r.inst);
1502
+ }
1503
+ this.uploadData = this.instanceData;
1504
+ this.uploadCount = this.sceneCount = this.instanceCount;
1505
+ return false;
1506
+ }
1507
+ if (this.sortIdx.length < n) {
1508
+ this.sortIdx = new Uint32Array(n * 2);
1509
+ this.sortTmp = new Uint32Array(n * 2);
1510
+ }
1511
+ const idx = this.sortIdx;
1512
+ const src = this.instanceData;
1513
+ // Clip-space z of the instance origin (row 2 of view_proj, before the w divide): what the
1514
+ // depth buffer compares, up to a monotonic map, for any projection (ortho2d looks down +z,
1515
+ // perspective down -z).
1516
+ const v = depth ? cam.viewProjection : null;
1517
+ const planes = cull ? cam.frustum : null;
1518
+ let culled = 0;
1519
+ let k = 0;
1520
+ // The lowest `order` of a visible drawable that reads depth: the view splits before the
1521
+ // first blended draw of that order (ws/render-hooks).
1522
+ let hookOrder = Number.POSITIVE_INFINITY;
1523
+ for (let i = 0; i < n; i++) {
1524
+ const r = recs[i];
1525
+ if ((r.layers & camLayers) === 0)
1526
+ continue;
1527
+ if (planes && r.br >= 0 && outside(planes, r.bx, r.by, r.bz, r.br)) {
1528
+ // A batch with bounds is culled as one: all its instances count.
1529
+ culled += r.batch ? r.batch.count : 1;
1530
+ continue;
1531
+ }
1532
+ idx[k++] = i;
1533
+ if (r.lod && !r.batch)
1534
+ this.countLod(r.lodLevel, 1);
1535
+ if (!depth)
1536
+ continue;
1537
+ if (r.drawable) {
1538
+ // No position: sorts as the nearest blended draw of its order (drawn after the rest).
1539
+ r.group = 1;
1540
+ r.dist = Number.NEGATIVE_INFINITY;
1541
+ if (r.drawable.readsDepth && r.order < hookOrder)
1542
+ hookOrder = r.order;
1543
+ continue;
1544
+ }
1545
+ r.group = r.material.blend === 'opaque' ? 0 : 1;
1546
+ let x;
1547
+ let y;
1548
+ let z;
1549
+ if (r.batch) {
1550
+ // A batch sorts by its sphere's centre, or its first instance's origin.
1551
+ if (r.br >= 0) {
1552
+ x = r.bx;
1553
+ y = r.by;
1554
+ z = r.bz;
1555
+ }
1556
+ else {
1557
+ const m = r.batch.matrices;
1558
+ x = m[3];
1559
+ y = m[7];
1560
+ z = m[11];
1561
+ }
1562
+ }
1563
+ else {
1564
+ const o = r.inst * INSTANCE_FLOATS;
1565
+ x = src[o + 3];
1566
+ y = src[o + 7];
1567
+ z = src[o + 11];
1568
+ }
1569
+ r.dist = v[2] * x + v[6] * y + v[10] * z + v[14];
1570
+ }
1571
+ this.culledCount = culled;
1572
+ this.sortDepth = depth;
1573
+ this.sortShadow = false;
1574
+ if (depth || !this.ordered || lods)
1575
+ this.sortDraws(this.sortIdx, this.sortTmp, k);
1576
+ // The shadow list's instances follow the scene's in the same upload: room for both.
1577
+ const total = (this.instanceCount + this.batchInstances) * INSTANCE_FLOATS;
1578
+ const need = shadowCfg ? total * 2 : total;
1579
+ if (this.sortedData.length < need) {
1580
+ this.sortedData = new Float32Array(Math.max(need, this.sortedData.length * 2));
1581
+ }
1582
+ const dst = this.sortedData;
1583
+ // The shared previous-matrix lane: only into a velocity target, for batches that bring one.
1584
+ const laneOn = this.laneRecs > 0 && (target.auxKey & 2) !== 0;
1585
+ this.laneUsed = false;
1586
+ if (laneOn) {
1587
+ const laneNeed = (this.instanceCount + this.batchInstances) * MATRIX3X4_FLOATS;
1588
+ if (this.laneData.length < laneNeed) {
1589
+ this.laneData = new Float32Array(Math.max(laneNeed, this.laneData.length * 2));
1590
+ }
1591
+ }
1592
+ // Every view of a camera advances its motion memory once single draws could use it, so a
1593
+ // view into a target without velocity breaks the history like a skipped frame.
1594
+ const motionMem = this.laneRecs > 0 ? this.motionView(cam) : null;
1595
+ const motion = laneOn ? motionMem : null;
1596
+ let inst = 0;
1597
+ let batches = 0;
1598
+ let listInst = 0;
1599
+ for (let j = 0; j < k; j++) {
1600
+ const r = recs[idx[j]];
1601
+ if (this.splitAt < 0 && r.group === 1 && r.order >= hookOrder) {
1602
+ this.splitAt = this.scene.count;
1603
+ }
1604
+ if (r.drawable) {
1605
+ this.batch(this.scene, r, null, 0);
1606
+ continue;
1607
+ }
1608
+ const mode = !depth ? 0 : r.group === 0 && depthWrite ? 2 : 1;
1609
+ if (r.batch) {
1610
+ r.lane = laneOn && r.prev !== null && r.material.aux;
1611
+ this.laneSrc = r.lane ? r.prev : null;
1612
+ if (r.lane)
1613
+ this.laneUsed = true;
1614
+ const list = r.list;
1615
+ if (list) {
1616
+ batches++;
1617
+ listInst += list.count;
1618
+ this.batch(this.scene, r, this.scenePipeline(r, mode), 0, list.count, list.buffer);
1619
+ continue;
1620
+ }
1621
+ if (r.lod) {
1622
+ // One command per non-empty level, the instances bucketed by level (ws/render-6).
1623
+ const written = this.copyBatchLod(r, dst, inst, planes);
1624
+ culled += this.batchCulled;
1625
+ if (written === 0)
1626
+ continue;
1627
+ batches++;
1628
+ const meshes = r.lod.meshes;
1629
+ for (let l = 0; l < meshes.length; l++) {
1630
+ const c = this.bucketCount[l];
1631
+ if (c === 0)
1632
+ continue;
1633
+ r.mesh = meshes[l];
1634
+ this.batch(this.scene, r, this.scenePipeline(r, mode), this.bucketFirst[l], c);
1635
+ this.countLod(l, c);
1636
+ }
1637
+ r.mesh = meshes[0];
1638
+ inst += written;
1639
+ continue;
1640
+ }
1641
+ const written = this.copyBatch(r, dst, inst, planes);
1642
+ culled += this.batchCulled;
1643
+ if (written === 0)
1644
+ continue;
1645
+ batches++;
1646
+ this.batch(this.scene, r, this.scenePipeline(r, mode), inst, written);
1647
+ inst += written;
1648
+ continue;
1649
+ }
1650
+ const from = r.inst * INSTANCE_FLOATS;
1651
+ const to = inst * INSTANCE_FLOATS;
1652
+ for (let f = 0; f < INSTANCE_FLOATS; f++)
1653
+ dst[to + f] = src[from + f];
1654
+ // A single draw's previous matrix: its Transform's, kept per view (ws/render-8).
1655
+ r.lane = motion !== null && r.xf !== null && r.material.aux;
1656
+ if (r.lane) {
1657
+ this.writeMotion(motion, r.xf, src, from, inst);
1658
+ this.laneUsed = true;
1659
+ }
1660
+ this.batch(this.scene, r, this.scenePipeline(r, mode), inst);
1661
+ inst++;
1662
+ }
1663
+ this.laneSrc = null;
1664
+ this.culledCount = culled;
1665
+ this.sceneBatches = batches;
1666
+ this.sceneListInstances = listInst;
1667
+ this.uploadData = dst;
1668
+ this.sceneCount = inst;
1669
+ this.uploadCount = shadowCfg ? this.buildShadowDraws(camLayers, dst, inst) : inst;
1670
+ return depth;
1671
+ }
1672
+ /** `cam`'s motion memory, advanced to a new view (one per `render` with lane records). */
1673
+ motionView(cam) {
1674
+ let m = this.motionMemory.get(cam);
1675
+ if (!m) {
1676
+ m = { view: 0, slots: new WeakMap() };
1677
+ this.motionMemory.set(cam, m);
1678
+ }
1679
+ m.view++;
1680
+ return m;
1681
+ }
1682
+ /**
1683
+ * Write the previous world matrix of a single draw's `tr` into lane slot `slot`: the matrix
1684
+ * `tr` held at this view's last frame, or the current one (zero motion) when the view did
1685
+ * not draw it then (first seen, or culled or skipped last frame). The current matrix is
1686
+ * `src[from..from+12]`, as recorded. A transform drawn twice with different matrices in one
1687
+ * view is a scratch transform: it reports no motion of its own from then on. Allocates only
1688
+ * on a transform's first draw in a view.
1689
+ */
1690
+ writeMotion(memory, tr, src, from, slot) {
1691
+ const now = memory.view;
1692
+ let s = memory.slots.get(tr);
1693
+ if (!s) {
1694
+ s = {
1695
+ prev: new Float32Array(MATRIX3X4_FLOATS),
1696
+ cur: new Float32Array(MATRIX3X4_FLOATS),
1697
+ view: -1,
1698
+ shared: false,
1699
+ };
1700
+ memory.slots.set(tr, s);
1701
+ }
1702
+ const prev = s.prev;
1703
+ const cur = s.cur;
1704
+ if (s.view === now) {
1705
+ // Drawn again in this view: the same matrix keeps its history; a different one is a
1706
+ // scratch transform.
1707
+ if (!s.shared) {
1708
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++) {
1709
+ if (cur[f] !== src[from + f]) {
1710
+ s.shared = true;
1711
+ break;
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+ else {
1717
+ const continuous = s.view === now - 1;
1718
+ if (continuous)
1719
+ prev.set(cur);
1720
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
1721
+ cur[f] = src[from + f];
1722
+ if (!continuous)
1723
+ prev.set(cur);
1724
+ s.view = now;
1725
+ }
1726
+ const lane = this.laneData;
1727
+ const lo = slot * MATRIX3X4_FLOATS;
1728
+ const hist = s.shared ? src : prev;
1729
+ const ho = s.shared ? from : 0;
1730
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
1731
+ lane[lo + f] = hist[ho + f];
1732
+ }
1733
+ /**
1734
+ * Pick this view's level of every LOD record (`frame.mesh` with a `MeshLod`): the projected
1735
+ * height of its world sphere through `cam` on the view's viewport (`lodSizePx`) times
1736
+ * `cam.lodBias`, against the chain's thresholds with `lodHysteresis` around the level its key
1737
+ * drew last (`pickLodLevel`). Sets the record's `mesh` (so the scene and shadow lists draw the
1738
+ * level) and, for opaque materials, the sort keys that group a level's instances into one
1739
+ * draw. No allocation.
1740
+ */
1741
+ pickLods(cam) {
1742
+ const recs = this.recs;
1743
+ const n = this.recCount;
1744
+ const proj = cam.projection;
1745
+ const eye = cam.position;
1746
+ const ex = eye[0];
1747
+ const ey = eye[1];
1748
+ const ez = eye[2];
1749
+ const vpH = this.viewRect.height;
1750
+ const bias = cam.lodBias;
1751
+ const h = this.lodHysteresis;
1752
+ const camLayers = cam.layers >>> 0;
1753
+ this.lodCam = cam;
1754
+ let memory = this.lodMemory.get(cam);
1755
+ if (!memory) {
1756
+ memory = { keyed: new WeakMap(), indexed: new WeakMap() };
1757
+ this.lodMemory.set(cam, memory);
1758
+ }
1759
+ const levels = memory.keyed;
1760
+ for (let i = 0; i < n; i++) {
1761
+ const r = recs[i];
1762
+ const lod = r.lod;
1763
+ if (!lod)
1764
+ continue;
1765
+ if (r.batch) {
1766
+ if ((r.layers & camLayers) !== 0)
1767
+ this.pickBatchLods(r, lod, memory, ex, ey, ez, vpH);
1768
+ continue;
1769
+ }
1770
+ const meshes = lod.meshes;
1771
+ let level = 0;
1772
+ if (meshes.length > 1) {
1773
+ const dist = Math.hypot(r.bx - ex, r.by - ey, r.bz - ez);
1774
+ const size = r.br >= 0 ? lodSizePx(r.br, dist, proj, vpH) * bias : Number.POSITIVE_INFINITY;
1775
+ const key = r.lodKey;
1776
+ const prev = levels.get(key) ?? -1;
1777
+ level = pickLodLevel(size, lod.thresholdsPx, meshes.length, prev, h);
1778
+ if (level !== prev)
1779
+ levels.set(key, level);
1780
+ }
1781
+ const mesh = meshes[level];
1782
+ r.mesh = mesh;
1783
+ r.lodLevel = level;
1784
+ if (r.material.blend === 'opaque') {
1785
+ r.lk = this.keyOf(mesh);
1786
+ r.lt = this.keyOf(r.material);
1787
+ }
1788
+ else {
1789
+ r.lk = -1;
1790
+ r.lt = -1;
1791
+ }
1792
+ }
1793
+ }
1794
+ /**
1795
+ * `pickLods` for a batch or list record: every instance's level for this view, from the
1796
+ * sphere of `meshes[0].bounds` through its matrix (as `pushInstance` places it), with the
1797
+ * hysteresis held per instance index in the view's memory for the batch. The levels land in
1798
+ * `r.lodLevels` (that memory), read by the scene and the shadow copy alike, so a shadow pass
1799
+ * draws the level the view picked. Grows the memory only when the batch does.
1800
+ */
1801
+ pickBatchLods(r, lod, memory, ex, ey, ez, vpH) {
1802
+ const b = r.batch;
1803
+ const n = b.count;
1804
+ let slots = memory.indexed.get(b);
1805
+ if (!slots) {
1806
+ const size = Math.max(n, 16);
1807
+ slots = { levels: new Int8Array(size).fill(-1), spare: new Int8Array(size), flush: -1 };
1808
+ memory.indexed.set(b, slots);
1809
+ }
1810
+ else if (slots.levels.length < n) {
1811
+ const next = new Int8Array(Math.max(n, slots.levels.length * 2)).fill(-1);
1812
+ next.set(slots.levels);
1813
+ slots.levels = next;
1814
+ slots.spare = new Int8Array(next.length);
1815
+ }
1816
+ // A keyed list (ws/render-7): carry each level from the index its key held at the last
1817
+ // flush, so a removal or reorder moves no other instance's level.
1818
+ if (b instanceof InstanceList && b.keyed) {
1819
+ const now = b.flushCount;
1820
+ if (slots.flush !== now) {
1821
+ const pi = b.previousIndex;
1822
+ const old = slots.levels;
1823
+ const spare = slots.spare;
1824
+ const remap = slots.flush === now - 1;
1825
+ for (let i = 0; i < n; i++) {
1826
+ const j = remap ? pi[i] : -1;
1827
+ spare[i] = j >= 0 ? old[j] : -1;
1828
+ }
1829
+ slots.levels = spare;
1830
+ slots.spare = old;
1831
+ }
1832
+ slots.flush = now;
1833
+ }
1834
+ const mem = slots.levels;
1835
+ r.lodLevels = mem;
1836
+ const meshes = lod.meshes;
1837
+ const count = meshes.length;
1838
+ const mb = meshes[0].bounds;
1839
+ if (count <= 1 || !(mb.radius < Number.POSITIVE_INFINITY)) {
1840
+ mem.fill(0, 0, n);
1841
+ return;
1842
+ }
1843
+ const cam = this.lodCam;
1844
+ const proj = cam.projection;
1845
+ const bias = cam.lodBias;
1846
+ const h = this.lodHysteresis;
1847
+ const th = lod.thresholdsPx;
1848
+ const m = b.matrices;
1849
+ const cx = mb.center[0];
1850
+ const cy = mb.center[1];
1851
+ const cz = mb.center[2];
1852
+ for (let i = 0; i < n; i++) {
1853
+ const mo = i * MATRIX3X4_FLOATS;
1854
+ const x = m[mo] * cx + m[mo + 1] * cy + m[mo + 2] * cz + m[mo + 3];
1855
+ const y = m[mo + 4] * cx + m[mo + 5] * cy + m[mo + 6] * cz + m[mo + 7];
1856
+ const z = m[mo + 8] * cx + m[mo + 9] * cy + m[mo + 10] * cz + m[mo + 11];
1857
+ const s0 = m[mo] * m[mo] + m[mo + 4] * m[mo + 4] + m[mo + 8] * m[mo + 8];
1858
+ const s1 = m[mo + 1] * m[mo + 1] + m[mo + 5] * m[mo + 5] + m[mo + 9] * m[mo + 9];
1859
+ const s2 = m[mo + 2] * m[mo + 2] + m[mo + 6] * m[mo + 6] + m[mo + 10] * m[mo + 10];
1860
+ const radius = mb.radius * Math.sqrt(Math.max(s0, s1, s2));
1861
+ const dist = Math.hypot(x - ex, y - ey, z - ez);
1862
+ const size = lodSizePx(radius, dist, proj, vpH) * bias;
1863
+ mem[i] = pickLodLevel(size, th, count, mem[i], h);
1864
+ }
1865
+ }
1866
+ /** Count `n` scene instances of LOD level `level` in `stats.lodCounts`. */
1867
+ countLod(level, n) {
1868
+ const lc = this.stats.lodCounts;
1869
+ while (lc.length <= level)
1870
+ lc.push(0);
1871
+ lc[level] += n;
1872
+ }
1873
+ /**
1874
+ * Copy the instances of batch record `r` into `dst` from instance `inst` on, in the instance
1875
+ * format (matrices, then `params` or the defaults with the matrix columns' lengths as size).
1876
+ * With `planes` and a batch without bounds (`r.mb` set), each instance is tested the way
1877
+ * `pushInstance` places a mesh sphere and the ones outside are skipped, counted in
1878
+ * `batchCulled`. Returns the instances written. No allocation.
1879
+ */
1880
+ copyBatch(r, dst, inst, planes) {
1881
+ const b = r.batch;
1882
+ const n = b.count;
1883
+ const m = b.matrices;
1884
+ const p = b.params ?? null;
1885
+ const mb = planes ? r.mb : null;
1886
+ const cx = mb ? mb.center[0] : 0;
1887
+ const cy = mb ? mb.center[1] : 0;
1888
+ const cz = mb ? mb.center[2] : 0;
1889
+ const cr = mb ? mb.radius : 0;
1890
+ const lane = this.laneSrc;
1891
+ const laneDst = this.laneData;
1892
+ let written = 0;
1893
+ let culled = 0;
1894
+ for (let i = 0; i < n; i++) {
1895
+ const mo = i * MATRIX3X4_FLOATS;
1896
+ if (mb) {
1897
+ const x = m[mo] * cx + m[mo + 1] * cy + m[mo + 2] * cz + m[mo + 3];
1898
+ const y = m[mo + 4] * cx + m[mo + 5] * cy + m[mo + 6] * cz + m[mo + 7];
1899
+ const z = m[mo + 8] * cx + m[mo + 9] * cy + m[mo + 10] * cz + m[mo + 11];
1900
+ const s0 = m[mo] * m[mo] + m[mo + 4] * m[mo + 4] + m[mo + 8] * m[mo + 8];
1901
+ const s1 = m[mo + 1] * m[mo + 1] + m[mo + 5] * m[mo + 5] + m[mo + 9] * m[mo + 9];
1902
+ const s2 = m[mo + 2] * m[mo + 2] + m[mo + 6] * m[mo + 6] + m[mo + 10] * m[mo + 10];
1903
+ if (outside(planes, x, y, z, cr * Math.sqrt(Math.max(s0, s1, s2)))) {
1904
+ culled++;
1905
+ continue;
1906
+ }
1907
+ }
1908
+ const to = (inst + written) * INSTANCE_FLOATS;
1909
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
1910
+ dst[to + f] = m[mo + f];
1911
+ if (lane) {
1912
+ const lo = (inst + written) * MATRIX3X4_FLOATS;
1913
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
1914
+ laneDst[lo + f] = lane[mo + f];
1915
+ }
1916
+ if (p) {
1917
+ const po = i * INSTANCE_PARAM_FLOATS;
1918
+ for (let f = 0; f < INSTANCE_PARAM_FLOATS; f++) {
1919
+ dst[to + MATRIX3X4_FLOATS + f] = p[po + f];
1920
+ }
1921
+ }
1922
+ else {
1923
+ const sx = Math.hypot(m[mo], m[mo + 4], m[mo + 8]);
1924
+ const sy = Math.hypot(m[mo + 1], m[mo + 5], m[mo + 9]);
1925
+ writeInstanceParams(dst, to + MATRIX3X4_FLOATS, NO_PARAMS, sx, sy);
1926
+ }
1927
+ written++;
1928
+ }
1929
+ this.batchCulled = culled;
1930
+ return written;
1931
+ }
1932
+ /**
1933
+ * `copyBatch` for a LOD batch or list: the instances that pass the same per-instance test,
1934
+ * written bucketed by the level `pickLods` chose for this view (`r.lodLevels`), level 0
1935
+ * first. Sets `bucketFirst` / `bucketCount` per level and `batchCulled`; returns the
1936
+ * instances written. Grows its scratch only when a batch outgrows it.
1937
+ */
1938
+ copyBatchLod(r, dst, inst, planes) {
1939
+ const b = r.batch;
1940
+ const n = b.count;
1941
+ const m = b.matrices;
1942
+ const p = b.params ?? null;
1943
+ const levels = r.lodLevels;
1944
+ const count = r.lod.meshes.length;
1945
+ if (this.bucketCount.length < count) {
1946
+ const c = Math.max(count, this.bucketCount.length * 2);
1947
+ this.bucketFirst = new Int32Array(c);
1948
+ this.bucketCount = new Int32Array(c);
1949
+ this.bucketAt = new Int32Array(c);
1950
+ }
1951
+ if (this.lodScratch.length < n) {
1952
+ this.lodScratch = new Int8Array(Math.max(n, this.lodScratch.length * 2));
1953
+ }
1954
+ const lv = this.lodScratch;
1955
+ const bc = this.bucketCount;
1956
+ const bf = this.bucketFirst;
1957
+ const at = this.bucketAt;
1958
+ bc.fill(0, 0, count);
1959
+ const mb = planes ? r.mb : null;
1960
+ const cx = mb ? mb.center[0] : 0;
1961
+ const cy = mb ? mb.center[1] : 0;
1962
+ const cz = mb ? mb.center[2] : 0;
1963
+ const cr = mb ? mb.radius : 0;
1964
+ let culled = 0;
1965
+ for (let i = 0; i < n; i++) {
1966
+ if (mb) {
1967
+ const mo = i * MATRIX3X4_FLOATS;
1968
+ const x = m[mo] * cx + m[mo + 1] * cy + m[mo + 2] * cz + m[mo + 3];
1969
+ const y = m[mo + 4] * cx + m[mo + 5] * cy + m[mo + 6] * cz + m[mo + 7];
1970
+ const z = m[mo + 8] * cx + m[mo + 9] * cy + m[mo + 10] * cz + m[mo + 11];
1971
+ const s0 = m[mo] * m[mo] + m[mo + 4] * m[mo + 4] + m[mo + 8] * m[mo + 8];
1972
+ const s1 = m[mo + 1] * m[mo + 1] + m[mo + 5] * m[mo + 5] + m[mo + 9] * m[mo + 9];
1973
+ const s2 = m[mo + 2] * m[mo + 2] + m[mo + 6] * m[mo + 6] + m[mo + 10] * m[mo + 10];
1974
+ if (outside(planes, x, y, z, cr * Math.sqrt(Math.max(s0, s1, s2)))) {
1975
+ lv[i] = -1;
1976
+ culled++;
1977
+ continue;
1978
+ }
1979
+ }
1980
+ const l = levels ? Math.min(Math.max(levels[i], 0), count - 1) : 0;
1981
+ lv[i] = l;
1982
+ bc[l]++;
1983
+ }
1984
+ const lane = this.laneSrc;
1985
+ const laneDst = this.laneData;
1986
+ let next = inst;
1987
+ for (let l = 0; l < count; l++) {
1988
+ bf[l] = next;
1989
+ at[l] = next;
1990
+ next += bc[l];
1991
+ }
1992
+ for (let i = 0; i < n; i++) {
1993
+ const l = lv[i];
1994
+ if (l < 0)
1995
+ continue;
1996
+ const mo = i * MATRIX3X4_FLOATS;
1997
+ const slot = at[l]++;
1998
+ const to = slot * INSTANCE_FLOATS;
1999
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
2000
+ dst[to + f] = m[mo + f];
2001
+ if (lane) {
2002
+ const lo = slot * MATRIX3X4_FLOATS;
2003
+ for (let f = 0; f < MATRIX3X4_FLOATS; f++)
2004
+ laneDst[lo + f] = lane[mo + f];
2005
+ }
2006
+ if (p) {
2007
+ const po = i * INSTANCE_PARAM_FLOATS;
2008
+ for (let f = 0; f < INSTANCE_PARAM_FLOATS; f++) {
2009
+ dst[to + MATRIX3X4_FLOATS + f] = p[po + f];
2010
+ }
2011
+ }
2012
+ else {
2013
+ const sx = Math.hypot(m[mo], m[mo + 4], m[mo + 8]);
2014
+ const sy = Math.hypot(m[mo + 1], m[mo + 5], m[mo + 9]);
2015
+ writeInstanceParams(dst, to + MATRIX3X4_FLOATS, NO_PARAMS, sx, sy);
2016
+ }
2017
+ }
2018
+ this.batchCulled = culled;
2019
+ return next - inst;
2020
+ }
2021
+ /** The shadow pipeline of a caster record (its current `mesh`). */
2022
+ shadowPipeline(r) {
2023
+ const layout = r.mesh.layout;
2024
+ const material = r.material;
2025
+ // A material with a shadow fragment (alpha mask, dissolve) discards in the shadow pass too;
2026
+ // every other caster takes the depth-only pipeline.
2027
+ return material.shadowFragment
2028
+ ? material.shadowPipelineFor(layout, SHADOW_FORMAT)
2029
+ : this.shadow.pipelineFor(layout, layout.skinned ? material.bindings?.layout : undefined);
2030
+ }
2031
+ /**
2032
+ * Build this view's shadow-pass commands: every opaque mesh draw with `castShadow` on the
2033
+ * camera's layers whose bounding sphere touches the sun's box (`ShadowMap.sunCamera.frustum`,
2034
+ * placed by `render` before the lists are built), whether or not the view's own frustum sees
2035
+ * it. The casters are sorted by mesh, then material, then submission index, their instance
2036
+ * data copied into `dst` from `inst` on (after the scene's), and batched into `shadowCmds`
2037
+ * with the scene's keys, so one `(mesh, material)` is one instanced draw. Returns the new
2038
+ * instance count of the upload. No allocation in steady state.
2039
+ */
2040
+ buildShadowDraws(camLayers, dst, inst) {
2041
+ const recs = this.recs;
2042
+ const n = this.recCount;
2043
+ if (this.shadowIdx.length < n) {
2044
+ this.shadowIdx = new Uint32Array(n * 2);
2045
+ this.shadowTmp = new Uint32Array(n * 2);
2046
+ }
2047
+ const idx = this.shadowIdx;
2048
+ const planes = this.shadow.sunCamera.frustum;
2049
+ let k = 0;
2050
+ for (let i = 0; i < n; i++) {
2051
+ const r = recs[i];
2052
+ if (!r.castShadow || r.drawable || r.material.blend !== 'opaque')
2053
+ continue;
2054
+ if ((r.layers & camLayers) === 0)
2055
+ continue;
2056
+ if (r.br >= 0 && outside(planes, r.bx, r.by, r.bz, r.br))
2057
+ continue;
2058
+ r.mk = this.keyOf(r.mesh);
2059
+ r.tk = this.keyOf(r.material);
2060
+ idx[k++] = i;
2061
+ }
2062
+ if (k === 0)
2063
+ return inst;
2064
+ this.sortShadow = true;
2065
+ this.sortDraws(idx, this.shadowTmp, k);
2066
+ const src = this.instanceData;
2067
+ for (let j = 0; j < k; j++) {
2068
+ const r = recs[idx[j]];
2069
+ if (r.batch && r.lod) {
2070
+ // The levels the view picked, bucketed: one shadow draw per non-empty level.
2071
+ const written = this.copyBatchLod(r, dst, inst, planes);
2072
+ if (written === 0)
2073
+ continue;
2074
+ const meshes = r.lod.meshes;
2075
+ for (let l = 0; l < meshes.length; l++) {
2076
+ const c = this.bucketCount[l];
2077
+ if (c === 0)
2078
+ continue;
2079
+ r.mesh = meshes[l];
2080
+ this.batch(this.shadowCmds, r, this.shadowPipeline(r), this.bucketFirst[l], c);
2081
+ }
2082
+ r.mesh = meshes[0];
2083
+ inst += written;
2084
+ continue;
2085
+ }
2086
+ const pipeline = this.shadowPipeline(r);
2087
+ if (r.batch) {
2088
+ const list = r.list;
2089
+ if (list) {
2090
+ this.batch(this.shadowCmds, r, pipeline, 0, list.count, list.buffer);
2091
+ continue;
2092
+ }
2093
+ // Per-instance casters are tested against the sun box, not the view.
2094
+ const written = this.copyBatch(r, dst, inst, planes);
2095
+ if (written === 0)
2096
+ continue;
2097
+ this.batch(this.shadowCmds, r, pipeline, inst, written);
2098
+ inst += written;
2099
+ continue;
2100
+ }
2101
+ const from = r.inst * INSTANCE_FLOATS;
2102
+ const to = inst * INSTANCE_FLOATS;
2103
+ for (let f = 0; f < INSTANCE_FLOATS; f++)
2104
+ dst[to + f] = src[from + f];
2105
+ this.batch(this.shadowCmds, r, pipeline, inst);
2106
+ inst++;
2107
+ }
2108
+ return inst;
2109
+ }
2110
+ /** A stable small integer for a mesh or material (assigned on first sight). */
2111
+ keyOf(o) {
2112
+ let k = this.keys.get(o);
2113
+ if (k === undefined) {
2114
+ k = this.nextKey++;
2115
+ this.keys.set(o, k);
2116
+ }
2117
+ return k;
2118
+ }
2119
+ /** The scene pipeline of a mesh record for `depth` into the view's target. */
2120
+ scenePipeline(r, depth) {
2121
+ return r.material.pipelineFor(r.mesh.layout, depth, this.drawTarget, r.lane || (r.list !== null && r.list.prevBuffer !== null));
2122
+ }
2123
+ /**
2124
+ * Append a record to `list` as a command drawn with `pipeline`, merging into the previous
2125
+ * one when it continues it (same pipeline, material, mesh, offset and shadow flag, and its
2126
+ * instances follow on). Drawables pass `pipeline` null.
2127
+ */
2128
+ batch(list, r, pipeline, first, count = 1, buffer = null) {
2129
+ if (r.drawable) {
2130
+ const c = this.nextCmd(list);
2131
+ c.material = null;
2132
+ c.mesh = null;
2133
+ c.drawable = r.drawable;
2134
+ c.pipeline = null;
2135
+ c.first = 0;
2136
+ c.count = 0;
2137
+ c.offset = -1;
2138
+ c.castShadow = false;
2139
+ c.buffer = null;
2140
+ c.prevBuffer = null;
2141
+ c.lane = false;
2142
+ c.firstIndex = 0;
2143
+ c.indexCount = -1;
2144
+ return;
2145
+ }
2146
+ const material = r.material;
2147
+ const mesh = r.mesh;
2148
+ const last = list.count > 0 ? list.cmds[list.count - 1] : null;
2149
+ if (last &&
2150
+ !last.drawable &&
2151
+ last.pipeline === pipeline &&
2152
+ last.material === material &&
2153
+ last.mesh === mesh &&
2154
+ last.offset === r.offset &&
2155
+ last.castShadow === r.castShadow &&
2156
+ last.buffer === null &&
2157
+ buffer === null &&
2158
+ last.firstIndex === r.rangeFirst &&
2159
+ last.indexCount === r.rangeCount &&
2160
+ last.first + last.count === first) {
2161
+ last.count += count;
2162
+ return;
2163
+ }
2164
+ const c = this.nextCmd(list);
2165
+ c.material = material;
2166
+ c.mesh = mesh;
2167
+ c.drawable = null;
2168
+ c.pipeline = pipeline;
2169
+ c.first = first;
2170
+ c.count = count;
2171
+ c.offset = r.offset;
2172
+ c.castShadow = r.castShadow;
2173
+ c.buffer = buffer;
2174
+ // The lane is read only by a pipeline built for it (an aux material into a velocity target).
2175
+ c.prevBuffer = list === this.scene && r.list ? r.list.prevBuffer : null;
2176
+ c.lane = list === this.scene && r.lane;
2177
+ c.firstIndex = r.rangeFirst;
2178
+ c.indexCount = r.rangeCount;
2179
+ }
2180
+ /**
2181
+ * True when record `a` draws before record `b` (a total order: ties go by index). For the
2182
+ * scene list: `order`, then (with depth) group and depth; for the shadow list
2183
+ * (`sortShadow`): mesh key, then material key.
2184
+ */
2185
+ drawsBefore(a, b) {
2186
+ const ra = this.recs[a];
2187
+ const rb = this.recs[b];
2188
+ if (this.sortShadow) {
2189
+ if (ra.mk !== rb.mk)
2190
+ return ra.mk < rb.mk;
2191
+ if (ra.tk !== rb.tk)
2192
+ return ra.tk < rb.tk;
2193
+ return a < b;
2194
+ }
2195
+ if (ra.order !== rb.order)
2196
+ return ra.order < rb.order;
2197
+ if (this.sortDepth && ra.group !== rb.group)
2198
+ return ra.group < rb.group;
2199
+ // Opaque LOD draws group per (level mesh, material) so each level is one instanced call;
2200
+ // every other draw has -1 here, so a frame without LOD sorts as before.
2201
+ if (ra.lk !== rb.lk)
2202
+ return ra.lk < rb.lk;
2203
+ if (ra.lt !== rb.lt)
2204
+ return ra.lt < rb.lt;
2205
+ if (this.sortDepth && ra.dist !== rb.dist) {
2206
+ return ra.group === 0 ? ra.dist < rb.dist : ra.dist > rb.dist;
2207
+ }
2208
+ return a < b;
2209
+ }
2210
+ /** Bottom-up merge sort of `idx[0..n)` with `drawsBefore`, `tmp` as scratch; no allocation. */
2211
+ sortDraws(idx, tmp, n) {
2212
+ let src = idx;
2213
+ let dst = tmp;
2214
+ for (let width = 1; width < n; width *= 2) {
2215
+ for (let lo = 0; lo < n; lo += 2 * width) {
2216
+ const mid = Math.min(lo + width, n);
2217
+ const hi = Math.min(lo + 2 * width, n);
2218
+ let i = lo;
2219
+ let j = mid;
2220
+ let k = lo;
2221
+ while (i < mid && j < hi) {
2222
+ dst[k++] = this.drawsBefore(src[j], src[i]) ? src[j++] : src[i++];
2223
+ }
2224
+ while (i < mid)
2225
+ dst[k++] = src[i++];
2226
+ while (j < hi)
2227
+ dst[k++] = src[j++];
2228
+ }
2229
+ const t = src;
2230
+ src = dst;
2231
+ dst = t;
2232
+ }
2233
+ if (src !== idx) {
2234
+ for (let i = 0; i < n; i++)
2235
+ idx[i] = src[i];
2236
+ }
2237
+ }
2238
+ // ---------------------------------------------------------------- internals
2239
+ nextCmd(list) {
2240
+ if (list.count === list.cmds.length) {
2241
+ list.cmds.push({
2242
+ material: null,
2243
+ mesh: null,
2244
+ drawable: null,
2245
+ pipeline: null,
2246
+ first: 0,
2247
+ count: 0,
2248
+ offset: -1,
2249
+ castShadow: false,
2250
+ buffer: null,
2251
+ prevBuffer: null,
2252
+ lane: false,
2253
+ firstIndex: 0,
2254
+ indexCount: -1,
2255
+ });
2256
+ }
2257
+ return list.cmds[list.count++];
2258
+ }
2259
+ growInstances() {
2260
+ const next = new Float32Array(this.instanceData.length * 2);
2261
+ next.set(this.instanceData);
2262
+ this.instanceData = next;
2263
+ }
2264
+ /**
2265
+ * Upload this view's scene instances of the shared previous-matrix lane at the byte offset
2266
+ * matching `base` in the instance buffer (48 bytes per instance where it has 112), growing
2267
+ * the lane buffer when the frame outgrows it (the old one retires after submit).
2268
+ */
2269
+ uploadLane(base) {
2270
+ const first = base / INSTANCE_STRIDE;
2271
+ const laneBase = first * LANE_STRIDE;
2272
+ const bytes = this.sceneCount * LANE_STRIDE;
2273
+ let buf = this.laneBuffer;
2274
+ if (!buf || laneBase + bytes > buf.size) {
2275
+ if (buf)
2276
+ this.retired.push(buf);
2277
+ let cap = Math.max(256, buf ? buf.size / LANE_STRIDE : 0);
2278
+ while (cap * LANE_STRIDE < laneBase + bytes)
2279
+ cap *= 2;
2280
+ buf = this.laneBuffer = this.device.createBuffer({
2281
+ label: 'instance-lane',
2282
+ size: cap * LANE_STRIDE,
2283
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2284
+ });
2285
+ }
2286
+ this.laneBase = laneBase;
2287
+ if (bytes > 0)
2288
+ this.device.queue.writeBuffer(buf, laneBase, this.laneData.buffer, 0, bytes);
2289
+ }
2290
+ makeInstanceBuffer(count) {
2291
+ return this.device.createBuffer({
2292
+ label: 'instances',
2293
+ size: count * INSTANCE_STRIDE,
2294
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
2295
+ });
2296
+ }
2297
+ /**
2298
+ * The engine uniform slot for view `i` of a frame, made on first use (slot 0 at startup).
2299
+ * Its bind group carries the sun's shadow map at binding 3 (the 1×1 placeholder until a
2300
+ * shadow pass has rendered; `encodeShadow` rebuilds the groups when the map changes).
2301
+ */
2302
+ viewSlot(i) {
2303
+ let slot = this.views[i];
2304
+ if (slot)
2305
+ return slot;
2306
+ const buffer = this.device.createBuffer({
2307
+ label: `engine-globals${i === 0 ? '' : `-${i}`}`,
2308
+ size: ENGINE_UNIFORM_FLOATS * 4,
2309
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2310
+ });
2311
+ slot = { buffer, group: this.globalsGroup(buffer, i, false) };
2312
+ this.views[i] = slot;
2313
+ return slot;
2314
+ }
2315
+ /** The uniform slot for shadow pass `i` of a frame; binds the placeholder, not the map. */
2316
+ shadowSlot(i) {
2317
+ let slot = this.shadowSlots[i];
2318
+ if (slot)
2319
+ return slot;
2320
+ const buffer = this.device.createBuffer({
2321
+ label: `engine-shadow-globals-${i}`,
2322
+ size: ENGINE_UNIFORM_FLOATS * 4,
2323
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
2324
+ });
2325
+ slot = { buffer, group: this.globalsGroup(buffer, i, true) };
2326
+ this.shadowSlots[i] = slot;
2327
+ return slot;
2328
+ }
2329
+ /** A `globalsLayout` bind group over `buffer`, with the shadow map (or its placeholder) at 3. */
2330
+ globalsGroup(buffer, i, placeholder) {
2331
+ const label = placeholder
2332
+ ? `engine-shadow-globals-group-${i}`
2333
+ : `engine-globals-group${i === 0 ? '' : `-${i}`}`;
2334
+ return this.device.createBindGroup({
2335
+ label,
2336
+ layout: this.globalsLayout,
2337
+ entries: [
2338
+ { binding: 0, resource: { buffer, size: GLOBALS_FLOATS * 4 } },
2339
+ {
2340
+ binding: 1,
2341
+ resource: { buffer, offset: CAMERA_OFFSET_FLOATS * 4, size: CAMERA_BLOCK_FLOATS * 4 },
2342
+ },
2343
+ {
2344
+ binding: 2,
2345
+ resource: { buffer, offset: LIGHTS_OFFSET_FLOATS * 4, size: LIGHTS_BLOCK_FLOATS * 4 },
2346
+ },
2347
+ { binding: 3, resource: placeholder ? this.shadow.placeholder : this.shadow.view },
2348
+ { binding: 4, resource: this.shadow.sampler },
2349
+ ],
2350
+ });
2351
+ }
2352
+ /**
2353
+ * When `frame.lights.sun.shadow` is set and this view has opaque shadow casters inside the
2354
+ * sun's box, encode one depth-only pass into the sun's shadow map (through
2355
+ * `ShadowMap.sunCamera`, an ortho box centred on `camera.position`, placed by `render`) over
2356
+ * `shadowCmds` (built by `buildShadowDraws`: casters the view frustum culled included), before
2357
+ * its scene pass. Returns what `writeView` puts in the lights block, or null when no shadow
2358
+ * was rendered.
2359
+ */
2360
+ encodeShadow(enc, frame, camera, base) {
2361
+ const cfg = frame.lights.sun.shadow;
2362
+ if (!cfg)
2363
+ return null;
2364
+ const cmds = this.shadowCmds.cmds;
2365
+ const n = this.shadowCmds.count;
2366
+ if (n === 0)
2367
+ return null;
2368
+ const shadow = this.shadow;
2369
+ const size = cfg.size ?? DEFAULT_SHADOW_SIZE;
2370
+ if (shadow.ensure(size)) {
2371
+ // The map is new: every view's bind group must point at it.
2372
+ for (let i = 0; i < this.views.length; i++) {
2373
+ const v = this.views[i];
2374
+ v.group = this.globalsGroup(v.buffer, i, false);
2375
+ }
2376
+ this.globalsBindGroup = this.views[0].group;
2377
+ }
2378
+ const slot = this.shadowSlot(this.shadowCount++);
2379
+ const g = this.globalsData;
2380
+ g[0] = this.logicalWidth;
2381
+ g[1] = this.logicalHeight;
2382
+ g[2] = size;
2383
+ g[3] = size;
2384
+ g[4] = frame.time;
2385
+ g[5] = frame.dt;
2386
+ const rect = this.shadowRect;
2387
+ rect.width = size;
2388
+ rect.height = size;
2389
+ shadow.sunCamera.writeBlock(g, this.globalsU32, CAMERA_OFFSET_FLOATS, rect, camera.viewIndex);
2390
+ frame.lights.writeBlock(g, this.globalsU32, LIGHTS_OFFSET_FLOATS, null);
2391
+ this.device.queue.writeBuffer(slot.buffer, 0, g);
2392
+ const desc = shadow.passDesc;
2393
+ desc.timestampWrites = this.gpuTimer?.pass('shadow');
2394
+ const pass = enc.beginRenderPass(desc);
2395
+ pass.setBindGroup(0, slot.group);
2396
+ let curPipeline = null;
2397
+ let curMesh = null;
2398
+ let curBuffer = null;
2399
+ let curMaterial = null;
2400
+ let curOffset = -1;
2401
+ let drawn = 0;
2402
+ for (let i = 0; i < n; i++) {
2403
+ const c = cmds[i];
2404
+ const mesh = c.mesh;
2405
+ const pipeline = c.pipeline;
2406
+ if (pipeline !== curPipeline) {
2407
+ pass.setPipeline(pipeline);
2408
+ curPipeline = pipeline;
2409
+ }
2410
+ // A skinned caster's pipeline has the material group (its joint palette) as group 1, as
2411
+ // does a material's own shadow pipeline (`shadowFragmentEntry`).
2412
+ const m = c.material;
2413
+ if ((mesh.layout.skinned || m.shadowFragment) &&
2414
+ m.bindings &&
2415
+ (m !== curMaterial || c.offset !== curOffset)) {
2416
+ if (c.offset >= 0) {
2417
+ this.dynamicOffsets[0] = c.offset;
2418
+ pass.setBindGroup(1, m.bindings.bindGroup, this.dynamicOffsets);
2419
+ }
2420
+ else {
2421
+ pass.setBindGroup(1, m.bindings.bindGroup);
2422
+ }
2423
+ curMaterial = m;
2424
+ curOffset = c.offset;
2425
+ }
2426
+ if (mesh !== curMesh || c.buffer !== curBuffer) {
2427
+ pass.setVertexBuffer(0, mesh.vertexBuffer);
2428
+ if (c.buffer)
2429
+ pass.setVertexBuffer(1, c.buffer, 0);
2430
+ else
2431
+ pass.setVertexBuffer(1, this.instanceBuffer, base);
2432
+ if (mesh.indexBuffer)
2433
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
2434
+ curMesh = mesh;
2435
+ curBuffer = c.buffer;
2436
+ }
2437
+ if (mesh.indexBuffer) {
2438
+ const indices = c.indexCount >= 0 ? c.indexCount : mesh.indexCount;
2439
+ if (indices === 0)
2440
+ continue;
2441
+ pass.drawIndexed(indices, c.count, c.firstIndex, 0, c.first);
2442
+ }
2443
+ else {
2444
+ pass.draw(mesh.vertexCount, c.count, 0, c.first);
2445
+ }
2446
+ drawn += c.count;
2447
+ }
2448
+ pass.end();
2449
+ this.stats.shadowDraws += drawn;
2450
+ return shadow.uniforms;
2451
+ }
2452
+ /**
2453
+ * Upload the globals, camera and lighting blocks of one view (`viewRect` holds its viewport;
2454
+ * `view_index` is `camera.viewIndex`; `shadow` is this view's shadow pass, if any).
2455
+ */
2456
+ writeView(frame, camera, target, slot, shadow) {
2457
+ const g = this.globalsData;
2458
+ g[0] = this.logicalWidth;
2459
+ g[1] = this.logicalHeight;
2460
+ g[2] = target.width;
2461
+ g[3] = target.height;
2462
+ g[4] = frame.time;
2463
+ g[5] = frame.dt;
2464
+ camera.writeBlock(g, this.globalsU32, CAMERA_OFFSET_FLOATS, this.viewRect);
2465
+ frame.lights.writeBlock(g, this.globalsU32, LIGHTS_OFFSET_FLOATS, shadow);
2466
+ this.device.queue.writeBuffer(slot.buffer, 0, g);
2467
+ }
2468
+ }
2469
+ function newPostView() {
2470
+ return {
2471
+ viewProj: new Float32Array(16),
2472
+ invViewProj: new Float32Array(16),
2473
+ previousViewProj: new Float32Array(16),
2474
+ previousView: new Float32Array(16),
2475
+ jitter: [0, 0],
2476
+ };
2477
+ }
2478
+ /** Is the sphere `(x, y, z, r)` wholly outside one of the six `planes` (`Camera.frustum`)? */
2479
+ function outside(planes, x, y, z, r) {
2480
+ for (let i = 0; i < FRUSTUM_FLOATS; i += 4) {
2481
+ if (planes[i] * x + planes[i + 1] * y + planes[i + 2] * z + planes[i + 3] < -r) {
2482
+ return true;
2483
+ }
2484
+ }
2485
+ return false;
2486
+ }