@flighthq/scene3d-wgpu 0.2.1-next.840.857425c

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 (263) hide show
  1. package/dist/anisotropyPbrWgpuMeshMaterialRenderer.d.ts +4 -0
  2. package/dist/anisotropyPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  3. package/dist/anisotropyPbrWgpuMeshMaterialRenderer.js +52 -0
  4. package/dist/anisotropyPbrWgpuMeshMaterialRenderer.js.map +1 -0
  5. package/dist/blinnPhongWgpuMeshMaterialRenderer.d.ts +4 -0
  6. package/dist/blinnPhongWgpuMeshMaterialRenderer.d.ts.map +1 -0
  7. package/dist/blinnPhongWgpuMeshMaterialRenderer.js +66 -0
  8. package/dist/blinnPhongWgpuMeshMaterialRenderer.js.map +1 -0
  9. package/dist/clearcoatPbrWgpuMeshMaterialRenderer.d.ts +4 -0
  10. package/dist/clearcoatPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  11. package/dist/clearcoatPbrWgpuMeshMaterialRenderer.js +52 -0
  12. package/dist/clearcoatPbrWgpuMeshMaterialRenderer.js.map +1 -0
  13. package/dist/customShaderWgpuMeshMaterialRenderer.d.ts +7 -0
  14. package/dist/customShaderWgpuMeshMaterialRenderer.d.ts.map +1 -0
  15. package/dist/customShaderWgpuMeshMaterialRenderer.js +225 -0
  16. package/dist/customShaderWgpuMeshMaterialRenderer.js.map +1 -0
  17. package/dist/depthWgpuMeshMaterialRenderer.d.ts +4 -0
  18. package/dist/depthWgpuMeshMaterialRenderer.d.ts.map +1 -0
  19. package/dist/depthWgpuMeshMaterialRenderer.js +42 -0
  20. package/dist/depthWgpuMeshMaterialRenderer.js.map +1 -0
  21. package/dist/drawWgpuScene3D.d.ts +4 -0
  22. package/dist/drawWgpuScene3D.d.ts.map +1 -0
  23. package/dist/drawWgpuScene3D.js +171 -0
  24. package/dist/drawWgpuScene3D.js.map +1 -0
  25. package/dist/emissiveWgpuMeshMaterialRenderer.d.ts +4 -0
  26. package/dist/emissiveWgpuMeshMaterialRenderer.d.ts.map +1 -0
  27. package/dist/emissiveWgpuMeshMaterialRenderer.js +51 -0
  28. package/dist/emissiveWgpuMeshMaterialRenderer.js.map +1 -0
  29. package/dist/enableWgpuScene3DCustomShaderGuards.d.ts +5 -0
  30. package/dist/enableWgpuScene3DCustomShaderGuards.d.ts.map +1 -0
  31. package/dist/enableWgpuScene3DCustomShaderGuards.js +56 -0
  32. package/dist/enableWgpuScene3DCustomShaderGuards.js.map +1 -0
  33. package/dist/enableWgpuScene3DForwardLightSelectionGuards.d.ts +4 -0
  34. package/dist/enableWgpuScene3DForwardLightSelectionGuards.d.ts.map +1 -0
  35. package/dist/enableWgpuScene3DForwardLightSelectionGuards.js +17 -0
  36. package/dist/enableWgpuScene3DForwardLightSelectionGuards.js.map +1 -0
  37. package/dist/explainWgpuScene3DForwardLightSelection.d.ts +3 -0
  38. package/dist/explainWgpuScene3DForwardLightSelection.d.ts.map +1 -0
  39. package/dist/explainWgpuScene3DForwardLightSelection.js +18 -0
  40. package/dist/explainWgpuScene3DForwardLightSelection.js.map +1 -0
  41. package/dist/index.d.ts +51 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +51 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/iridescencePbrWgpuMeshMaterialRenderer.d.ts +4 -0
  46. package/dist/iridescencePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  47. package/dist/iridescencePbrWgpuMeshMaterialRenderer.js +60 -0
  48. package/dist/iridescencePbrWgpuMeshMaterialRenderer.js.map +1 -0
  49. package/dist/lambertWgpuMeshMaterialRenderer.d.ts +4 -0
  50. package/dist/lambertWgpuMeshMaterialRenderer.d.ts.map +1 -0
  51. package/dist/lambertWgpuMeshMaterialRenderer.js +61 -0
  52. package/dist/lambertWgpuMeshMaterialRenderer.js.map +1 -0
  53. package/dist/matcapWgpuMeshMaterialRenderer.d.ts +4 -0
  54. package/dist/matcapWgpuMeshMaterialRenderer.d.ts.map +1 -0
  55. package/dist/matcapWgpuMeshMaterialRenderer.js +54 -0
  56. package/dist/matcapWgpuMeshMaterialRenderer.js.map +1 -0
  57. package/dist/normalWgpuMeshMaterialRenderer.d.ts +4 -0
  58. package/dist/normalWgpuMeshMaterialRenderer.d.ts.map +1 -0
  59. package/dist/normalWgpuMeshMaterialRenderer.js +47 -0
  60. package/dist/normalWgpuMeshMaterialRenderer.js.map +1 -0
  61. package/dist/phongWgpuMeshMaterialRenderer.d.ts +4 -0
  62. package/dist/phongWgpuMeshMaterialRenderer.d.ts.map +1 -0
  63. package/dist/phongWgpuMeshMaterialRenderer.js +63 -0
  64. package/dist/phongWgpuMeshMaterialRenderer.js.map +1 -0
  65. package/dist/prepareWgpuScene3DForwardLights.d.ts +3 -0
  66. package/dist/prepareWgpuScene3DForwardLights.d.ts.map +1 -0
  67. package/dist/prepareWgpuScene3DForwardLights.js +110 -0
  68. package/dist/prepareWgpuScene3DForwardLights.js.map +1 -0
  69. package/dist/registerStandardPbrWgpuMaterial.d.ts +3 -0
  70. package/dist/registerStandardPbrWgpuMaterial.d.ts.map +1 -0
  71. package/dist/registerStandardPbrWgpuMaterial.js +11 -0
  72. package/dist/registerStandardPbrWgpuMaterial.js.map +1 -0
  73. package/dist/shadedWgpuMeshMaterialRenderer.d.ts +11 -0
  74. package/dist/shadedWgpuMeshMaterialRenderer.d.ts.map +1 -0
  75. package/dist/shadedWgpuMeshMaterialRenderer.js +47 -0
  76. package/dist/shadedWgpuMeshMaterialRenderer.js.map +1 -0
  77. package/dist/sheenPbrWgpuMeshMaterialRenderer.d.ts +4 -0
  78. package/dist/sheenPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  79. package/dist/sheenPbrWgpuMeshMaterialRenderer.js +58 -0
  80. package/dist/sheenPbrWgpuMeshMaterialRenderer.js.map +1 -0
  81. package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.d.ts +4 -0
  82. package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  83. package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.js +126 -0
  84. package/dist/specularGlossinessPbrWgpuMeshMaterialRenderer.js.map +1 -0
  85. package/dist/specularPbrWgpuMeshMaterialRenderer.d.ts +4 -0
  86. package/dist/specularPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  87. package/dist/specularPbrWgpuMeshMaterialRenderer.js +65 -0
  88. package/dist/specularPbrWgpuMeshMaterialRenderer.js.map +1 -0
  89. package/dist/standardPbrWgpuMeshMaterialRenderer.d.ts +9 -0
  90. package/dist/standardPbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  91. package/dist/standardPbrWgpuMeshMaterialRenderer.js +174 -0
  92. package/dist/standardPbrWgpuMeshMaterialRenderer.js.map +1 -0
  93. package/dist/subsurfacePbrWgpuMeshMaterialRenderer.d.ts +4 -0
  94. package/dist/subsurfacePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  95. package/dist/subsurfacePbrWgpuMeshMaterialRenderer.js +68 -0
  96. package/dist/subsurfacePbrWgpuMeshMaterialRenderer.js.map +1 -0
  97. package/dist/toonWgpuMeshMaterialRenderer.d.ts +4 -0
  98. package/dist/toonWgpuMeshMaterialRenderer.d.ts.map +1 -0
  99. package/dist/toonWgpuMeshMaterialRenderer.js +71 -0
  100. package/dist/toonWgpuMeshMaterialRenderer.js.map +1 -0
  101. package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.d.ts +4 -0
  102. package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.d.ts.map +1 -0
  103. package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.js +74 -0
  104. package/dist/transmissionVolumePbrWgpuMeshMaterialRenderer.js.map +1 -0
  105. package/dist/unlitWgpuMeshMaterialRenderer.d.ts +4 -0
  106. package/dist/unlitWgpuMeshMaterialRenderer.d.ts.map +1 -0
  107. package/dist/unlitWgpuMeshMaterialRenderer.js +57 -0
  108. package/dist/unlitWgpuMeshMaterialRenderer.js.map +1 -0
  109. package/dist/vertexColorWgpuMeshMaterialRenderer.d.ts +4 -0
  110. package/dist/vertexColorWgpuMeshMaterialRenderer.d.ts.map +1 -0
  111. package/dist/vertexColorWgpuMeshMaterialRenderer.js +55 -0
  112. package/dist/vertexColorWgpuMeshMaterialRenderer.js.map +1 -0
  113. package/dist/wgpuClassicPrelude.d.ts +8 -0
  114. package/dist/wgpuClassicPrelude.d.ts.map +1 -0
  115. package/dist/wgpuClassicPrelude.js +319 -0
  116. package/dist/wgpuClassicPrelude.js.map +1 -0
  117. package/dist/wgpuCustomMaterialAbi.d.ts +3 -0
  118. package/dist/wgpuCustomMaterialAbi.d.ts.map +1 -0
  119. package/dist/wgpuCustomMaterialAbi.js +5 -0
  120. package/dist/wgpuCustomMaterialAbi.js.map +1 -0
  121. package/dist/wgpuDebugPrelude.d.ts +7 -0
  122. package/dist/wgpuDebugPrelude.d.ts.map +1 -0
  123. package/dist/wgpuDebugPrelude.js +133 -0
  124. package/dist/wgpuDebugPrelude.js.map +1 -0
  125. package/dist/wgpuEnvironmentCube.d.ts +4 -0
  126. package/dist/wgpuEnvironmentCube.d.ts.map +1 -0
  127. package/dist/wgpuEnvironmentCube.js +66 -0
  128. package/dist/wgpuEnvironmentCube.js.map +1 -0
  129. package/dist/wgpuEnvironmentIblBake.d.ts +4 -0
  130. package/dist/wgpuEnvironmentIblBake.d.ts.map +1 -0
  131. package/dist/wgpuEnvironmentIblBake.js +458 -0
  132. package/dist/wgpuEnvironmentIblBake.js.map +1 -0
  133. package/dist/wgpuEnvironmentSkybox.d.ts +3 -0
  134. package/dist/wgpuEnvironmentSkybox.d.ts.map +1 -0
  135. package/dist/wgpuEnvironmentSkybox.js +159 -0
  136. package/dist/wgpuEnvironmentSkybox.js.map +1 -0
  137. package/dist/wgpuMatcapPrelude.d.ts +7 -0
  138. package/dist/wgpuMatcapPrelude.d.ts.map +1 -0
  139. package/dist/wgpuMatcapPrelude.js +134 -0
  140. package/dist/wgpuMatcapPrelude.js.map +1 -0
  141. package/dist/wgpuMeshMaterialRegistry.d.ts +5 -0
  142. package/dist/wgpuMeshMaterialRegistry.d.ts.map +1 -0
  143. package/dist/wgpuMeshMaterialRegistry.js +29 -0
  144. package/dist/wgpuMeshMaterialRegistry.js.map +1 -0
  145. package/dist/wgpuMeshPipeline.d.ts +43 -0
  146. package/dist/wgpuMeshPipeline.d.ts.map +1 -0
  147. package/dist/wgpuMeshPipeline.js +1007 -0
  148. package/dist/wgpuMeshPipeline.js.map +1 -0
  149. package/dist/wgpuMeshUpload.d.ts +3 -0
  150. package/dist/wgpuMeshUpload.d.ts.map +1 -0
  151. package/dist/wgpuMeshUpload.js +58 -0
  152. package/dist/wgpuMeshUpload.js.map +1 -0
  153. package/dist/wgpuParticleEmitter3D.d.ts +4 -0
  154. package/dist/wgpuParticleEmitter3D.d.ts.map +1 -0
  155. package/dist/wgpuParticleEmitter3D.js +403 -0
  156. package/dist/wgpuParticleEmitter3D.js.map +1 -0
  157. package/dist/wgpuPbrPipelineCache.d.ts +4 -0
  158. package/dist/wgpuPbrPipelineCache.d.ts.map +1 -0
  159. package/dist/wgpuPbrPipelineCache.js +52 -0
  160. package/dist/wgpuPbrPipelineCache.js.map +1 -0
  161. package/dist/wgpuPbrPrelude.d.ts +7 -0
  162. package/dist/wgpuPbrPrelude.d.ts.map +1 -0
  163. package/dist/wgpuPbrPrelude.js +551 -0
  164. package/dist/wgpuPbrPrelude.js.map +1 -0
  165. package/dist/wgpuScene3DRuntime.d.ts +5 -0
  166. package/dist/wgpuScene3DRuntime.d.ts.map +1 -0
  167. package/dist/wgpuScene3DRuntime.js +81 -0
  168. package/dist/wgpuScene3DRuntime.js.map +1 -0
  169. package/dist/wgpuScene3DTestHelper.d.ts +14 -0
  170. package/dist/wgpuScene3DTestHelper.d.ts.map +1 -0
  171. package/dist/wgpuScene3DTestHelper.js +121 -0
  172. package/dist/wgpuScene3DTestHelper.js.map +1 -0
  173. package/dist/wgpuScene3DTime.d.ts +4 -0
  174. package/dist/wgpuScene3DTime.d.ts.map +1 -0
  175. package/dist/wgpuScene3DTime.js +11 -0
  176. package/dist/wgpuScene3DTime.js.map +1 -0
  177. package/dist/wgpuShadedModifierSnippet.d.ts +4 -0
  178. package/dist/wgpuShadedModifierSnippet.d.ts.map +1 -0
  179. package/dist/wgpuShadedModifierSnippet.js +18 -0
  180. package/dist/wgpuShadedModifierSnippet.js.map +1 -0
  181. package/dist/wgpuShadedPrelude.d.ts +16 -0
  182. package/dist/wgpuShadedPrelude.d.ts.map +1 -0
  183. package/dist/wgpuShadedPrelude.js +625 -0
  184. package/dist/wgpuShadedPrelude.js.map +1 -0
  185. package/dist/wgpuShadowMap.d.ts +4 -0
  186. package/dist/wgpuShadowMap.d.ts.map +1 -0
  187. package/dist/wgpuShadowMap.js +155 -0
  188. package/dist/wgpuShadowMap.js.map +1 -0
  189. package/dist/wgpuSkinPalette.d.ts +7 -0
  190. package/dist/wgpuSkinPalette.d.ts.map +1 -0
  191. package/dist/wgpuSkinPalette.js +173 -0
  192. package/dist/wgpuSkinPalette.js.map +1 -0
  193. package/dist/wgpuToonPrelude.d.ts +8 -0
  194. package/dist/wgpuToonPrelude.d.ts.map +1 -0
  195. package/dist/wgpuToonPrelude.js +192 -0
  196. package/dist/wgpuToonPrelude.js.map +1 -0
  197. package/dist/wgpuUnlitPrelude.d.ts +9 -0
  198. package/dist/wgpuUnlitPrelude.d.ts.map +1 -0
  199. package/dist/wgpuUnlitPrelude.js +142 -0
  200. package/dist/wgpuUnlitPrelude.js.map +1 -0
  201. package/dist/wgpuWireframePrelude.d.ts +6 -0
  202. package/dist/wgpuWireframePrelude.d.ts.map +1 -0
  203. package/dist/wgpuWireframePrelude.js +77 -0
  204. package/dist/wgpuWireframePrelude.js.map +1 -0
  205. package/dist/wgpuWireframeUpload.d.ts +3 -0
  206. package/dist/wgpuWireframeUpload.d.ts.map +1 -0
  207. package/dist/wgpuWireframeUpload.js +68 -0
  208. package/dist/wgpuWireframeUpload.js.map +1 -0
  209. package/dist/wireframeWgpuMeshMaterialRenderer.d.ts +4 -0
  210. package/dist/wireframeWgpuMeshMaterialRenderer.d.ts.map +1 -0
  211. package/dist/wireframeWgpuMeshMaterialRenderer.js +69 -0
  212. package/dist/wireframeWgpuMeshMaterialRenderer.js.map +1 -0
  213. package/package.json +57 -0
  214. package/src/anisotropyPbrWgpuMeshMaterialRenderer.test.ts +90 -0
  215. package/src/blinnPhongWgpuMeshMaterialRenderer.test.ts +82 -0
  216. package/src/clearcoatPbrWgpuMeshMaterialRenderer.test.ts +90 -0
  217. package/src/customShaderWgpuMeshMaterialRenderer.test.ts +207 -0
  218. package/src/depthWgpuMeshMaterialRenderer.test.ts +69 -0
  219. package/src/drawWgpuScene3D.test.ts +247 -0
  220. package/src/emissiveWgpuMeshMaterialRenderer.test.ts +64 -0
  221. package/src/enableWgpuScene3DCustomShaderGuards.test.ts +73 -0
  222. package/src/enableWgpuScene3DForwardLightSelectionGuards.test.ts +39 -0
  223. package/src/explainWgpuScene3DForwardLightSelection.test.ts +15 -0
  224. package/src/iridescencePbrWgpuMeshMaterialRenderer.test.ts +90 -0
  225. package/src/lambertWgpuMeshMaterialRenderer.test.ts +77 -0
  226. package/src/matcapWgpuMeshMaterialRenderer.test.ts +69 -0
  227. package/src/normalWgpuMeshMaterialRenderer.test.ts +69 -0
  228. package/src/phongWgpuMeshMaterialRenderer.test.ts +79 -0
  229. package/src/prepareWgpuScene3DForwardLights.test.ts +84 -0
  230. package/src/registerStandardPbrWgpuMaterial.test.ts +15 -0
  231. package/src/shadedWgpuMeshMaterialRenderer.test.ts +63 -0
  232. package/src/sheenPbrWgpuMeshMaterialRenderer.test.ts +87 -0
  233. package/src/specularGlossinessPbrWgpuMeshMaterialRenderer.test.ts +100 -0
  234. package/src/specularPbrWgpuMeshMaterialRenderer.test.ts +90 -0
  235. package/src/standardPbrWgpuMeshMaterialRenderer.test.ts +172 -0
  236. package/src/subsurfacePbrWgpuMeshMaterialRenderer.test.ts +90 -0
  237. package/src/toonWgpuMeshMaterialRenderer.test.ts +89 -0
  238. package/src/transmissionVolumePbrWgpuMeshMaterialRenderer.test.ts +97 -0
  239. package/src/unlitWgpuMeshMaterialRenderer.test.ts +94 -0
  240. package/src/vertexColorWgpuMeshMaterialRenderer.test.ts +62 -0
  241. package/src/wgpuClassicPrelude.test.ts +176 -0
  242. package/src/wgpuDebugPrelude.test.ts +79 -0
  243. package/src/wgpuEnvironmentCube.test.ts +86 -0
  244. package/src/wgpuEnvironmentIblBake.test.ts +79 -0
  245. package/src/wgpuEnvironmentSkybox.test.ts +56 -0
  246. package/src/wgpuMatcapPrelude.test.ts +70 -0
  247. package/src/wgpuMeshMaterialRegistry.test.ts +55 -0
  248. package/src/wgpuMeshPipeline.test.ts +844 -0
  249. package/src/wgpuMeshUpload.test.ts +177 -0
  250. package/src/wgpuParticleEmitter3D.test.ts +200 -0
  251. package/src/wgpuPbrPipelineCache.test.ts +67 -0
  252. package/src/wgpuPbrPrelude.test.ts +162 -0
  253. package/src/wgpuScene3DRuntime.test.ts +35 -0
  254. package/src/wgpuScene3DTime.test.ts +20 -0
  255. package/src/wgpuShadedModifierSnippet.test.ts +33 -0
  256. package/src/wgpuShadedPrelude.test.ts +394 -0
  257. package/src/wgpuShadowMap.test.ts +169 -0
  258. package/src/wgpuSkinPalette.test.ts +88 -0
  259. package/src/wgpuToonPrelude.test.ts +93 -0
  260. package/src/wgpuUnlitPrelude.test.ts +102 -0
  261. package/src/wgpuWireframePrelude.test.ts +54 -0
  262. package/src/wgpuWireframeUpload.test.ts +36 -0
  263. package/src/wireframeWgpuMeshMaterialRenderer.test.ts +69 -0
@@ -0,0 +1,844 @@
1
+ import { createCamera3D } from '@flighthq/camera';
2
+ import { createMatrix3, createMatrix4 } from '@flighthq/geometry';
3
+ import { createStandardPbrMaterial } from '@flighthq/materials';
4
+ import { createBoxMeshGeometry } from '@flighthq/mesh';
5
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
6
+ import { createTexture, setTextureUvOffset, setTextureUvScale } from '@flighthq/texture';
7
+ import type {
8
+ Camera3D,
9
+ ImageResource,
10
+ Scene3DLightBlock,
11
+ Scene3DRenderProxy,
12
+ Texture,
13
+ WgpuMaterialBinding,
14
+ } from '@flighthq/types';
15
+ import { SCENE_LIGHT_BLOCK_FLOATS } from '@flighthq/types';
16
+
17
+ import {
18
+ beginWgpuMeshDraw,
19
+ buildWgpuMaterialBindGroup,
20
+ buildWgpuPerMapMaterialBindGroup,
21
+ createWgpuMeshPipeline,
22
+ drawWgpuMeshSubset,
23
+ ensureWgpuMaterialBinding,
24
+ ensureWgpuPerMapMaterialBinding,
25
+ ensureWgpuFrameBindGroup,
26
+ ensureWgpuIblSampleBindGroup,
27
+ ensureWgpuIblSampleLayout,
28
+ ensureWgpuPbrSampleBindGroup,
29
+ ensureWgpuPbrSampleLayout,
30
+ ensureWgpuPlaceholderTextureView,
31
+ ensureWgpuScene3DLayouts,
32
+ ensureWgpuScene3DPipeline,
33
+ ensureWgpuShadowSampleBindGroup,
34
+ ensureWgpuShadowSampleLayout,
35
+ getWgpuMaterialSampler,
36
+ getWgpuMeshPreludeWgsl,
37
+ isWgpuTextureReady,
38
+ resolveWgpuMaterialTextureView,
39
+ stashWgpuUvTransform,
40
+ isWgpuMaterialBindGroupRebuildNeeded,
41
+ wgpuPerMapMaterialBindGroupNeedsRebuild,
42
+ WGPU_MESH_PRELUDE_WGSL,
43
+ writeWgpuDrawUniform,
44
+ writeWgpuFrameUniform,
45
+ } from './wgpuMeshPipeline';
46
+ import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
47
+ import { makeWgpuScene3DState, makeWgpuSkinningAdapter } from './wgpuScene3DTestHelper';
48
+
49
+ function makeCamera(): Camera3D {
50
+ return createCamera3D({ far: 100, near: 0.1, projection: { aspect: 1, fovY: Math.PI / 3, kind: 'perspective' } });
51
+ }
52
+
53
+ function makeLights(): Scene3DLightBlock {
54
+ const data = new Float32Array(SCENE_LIGHT_BLOCK_FLOATS);
55
+ data[1] = -1;
56
+ data[4] = 1;
57
+ data[5] = 1;
58
+ data[6] = 1;
59
+ data[8] = 0.1;
60
+ return { ambientCount: 1, data, directionalCount: 1, hemisphereCount: 0, pointCount: 0, spotCount: 0, version: 1 };
61
+ }
62
+
63
+ function makeProxy(): Scene3DRenderProxy {
64
+ const geometry = createBoxMeshGeometry();
65
+ return {
66
+ material: createStandardPbrMaterial(),
67
+ normalMatrix: createMatrix3(),
68
+ subset: geometry.subsets[0],
69
+ worldMatrix: createMatrix4(),
70
+ };
71
+ }
72
+
73
+ function makePipeline(state: ReturnType<typeof makeWgpuScene3DState>['state']) {
74
+ const module = state.device.createShaderModule({ code: '' });
75
+ const materialBindGroupLayout = state.device.createBindGroupLayout({ entries: [] });
76
+ return createWgpuMeshPipeline(state, { doubleSided: false, format: 'bgra8unorm', materialBindGroupLayout, module });
77
+ }
78
+
79
+ function makeShadowPipeline(state: ReturnType<typeof makeWgpuScene3DState>['state']) {
80
+ const module = state.device.createShaderModule({ code: '' });
81
+ const materialBindGroupLayout = state.device.createBindGroupLayout({ entries: [] });
82
+ return createWgpuMeshPipeline(state, {
83
+ doubleSided: false,
84
+ format: 'bgra8unorm',
85
+ materialBindGroupLayout,
86
+ module,
87
+ shadowBindGroupLayout: ensureWgpuShadowSampleLayout(state),
88
+ });
89
+ }
90
+
91
+ function makePbrSamplePipeline(state: ReturnType<typeof makeWgpuScene3DState>['state']) {
92
+ const module = state.device.createShaderModule({ code: '' });
93
+ const materialBindGroupLayout = state.device.createBindGroupLayout({ entries: [] });
94
+ return createWgpuMeshPipeline(state, {
95
+ doubleSided: false,
96
+ format: 'bgra8unorm',
97
+ materialBindGroupLayout,
98
+ module,
99
+ pbrSampleBindGroupLayout: ensureWgpuPbrSampleLayout(state),
100
+ });
101
+ }
102
+
103
+ describe('beginWgpuMeshDraw', () => {
104
+ it('stores the active pipeline, sets it, and binds the frame group', () => {
105
+ const { fake, state } = makeWgpuScene3DState();
106
+ ensureWgpuFrameBindGroup(state);
107
+ const pipeline = makePipeline(state);
108
+ beginWgpuMeshDraw(state, pipeline);
109
+ expect(getWgpuScene3DRuntime(state).activeMeshPipeline).toBe(pipeline);
110
+ expect(fake.calls.some((c) => c.name === 'setPipeline')).toBe(true);
111
+ expect(fake.calls.some((c) => c.name === 'setBindGroup' && c.args[0] === 0)).toBe(true);
112
+ });
113
+
114
+ it('does not bind group(3) for a pipeline without a shadow layout', () => {
115
+ const { fake, state } = makeWgpuScene3DState();
116
+ ensureWgpuFrameBindGroup(state);
117
+ beginWgpuMeshDraw(state, makePipeline(state));
118
+ expect(fake.calls.some((c) => c.name === 'setBindGroup' && c.args[0] === 3)).toBe(false);
119
+ });
120
+
121
+ it('binds the shared shadow group at group(3) for a shadow pipeline', () => {
122
+ const { fake, state } = makeWgpuScene3DState();
123
+ ensureWgpuFrameBindGroup(state);
124
+ beginWgpuMeshDraw(state, makeShadowPipeline(state));
125
+ expect(fake.calls.some((c) => c.name === 'setBindGroup' && c.args[0] === 3)).toBe(true);
126
+ });
127
+
128
+ it('binds the combined PBR sample group at group(3)', () => {
129
+ const { fake, state } = makeWgpuScene3DState();
130
+ ensureWgpuFrameBindGroup(state);
131
+ beginWgpuMeshDraw(state, makePbrSamplePipeline(state));
132
+ expect(fake.calls.some((c) => c.name === 'setBindGroup' && c.args[0] === 3)).toBe(true);
133
+ });
134
+ });
135
+
136
+ describe('buildWgpuMaterialBindGroup', () => {
137
+ it('emits the uniform buffer at 0, the sampler at 1, and each map view at 2 + i', () => {
138
+ const { fake, state } = makeWgpuScene3DState();
139
+ const layout = {} as GPUBindGroupLayout;
140
+ const buffer = {} as GPUBuffer;
141
+ const sampler = {} as GPUSampler;
142
+ const view0 = {} as GPUTextureView;
143
+ const view1 = {} as GPUTextureView;
144
+
145
+ buildWgpuMaterialBindGroup(state, layout, buffer, sampler, [view0, view1]);
146
+
147
+ const call = fake.calls.find((c) => c.name === 'createBindGroup');
148
+ const entries = (call!.args[0] as { entries: GPUBindGroupEntry[] }).entries;
149
+ expect(entries.map((e) => e.binding)).toEqual([0, 1, 2, 3]);
150
+ expect((entries[0].resource as { buffer: GPUBuffer }).buffer).toBe(buffer);
151
+ expect(entries[1].resource).toBe(sampler);
152
+ expect(entries[2].resource).toBe(view0);
153
+ expect(entries[3].resource).toBe(view1);
154
+ });
155
+ });
156
+
157
+ describe('buildWgpuPerMapMaterialBindGroup', () => {
158
+ it('emits parallel samplers before their matching map views', () => {
159
+ const { fake, state } = makeWgpuScene3DState();
160
+ const layout = {} as GPUBindGroupLayout;
161
+ const buffer = {} as GPUBuffer;
162
+ const sampler0 = {} as GPUSampler;
163
+ const sampler1 = {} as GPUSampler;
164
+ const view0 = {} as GPUTextureView;
165
+ const view1 = {} as GPUTextureView;
166
+
167
+ buildWgpuPerMapMaterialBindGroup(state, layout, buffer, [sampler0, sampler1], [view0, view1]);
168
+
169
+ const call = fake.calls.find((c) => c.name === 'createBindGroup');
170
+ const entries = (call!.args[0] as { entries: GPUBindGroupEntry[] }).entries;
171
+ expect(entries.map((entry) => entry.binding)).toEqual([0, 1, 2, 3, 4]);
172
+ expect((entries[0].resource as { buffer: GPUBuffer }).buffer).toBe(buffer);
173
+ expect(entries.slice(1).map((entry) => entry.resource)).toEqual([sampler0, sampler1, view0, view1]);
174
+ });
175
+ });
176
+
177
+ describe('createWgpuMeshPipeline', () => {
178
+ it('builds a pipeline over the shared frame + draw layouts', () => {
179
+ const { fake, state } = makeWgpuScene3DState();
180
+ const pipeline = makePipeline(state);
181
+ expect(pipeline.pipeline).toBeDefined();
182
+ expect(pipeline.materialBindGroupLayout).toBeDefined();
183
+ expect(pipeline.hasShadowGroup).toBe(false);
184
+ const layoutCall = fake.calls.find((c) => c.name === 'createPipelineLayout');
185
+ expect((layoutCall!.args[0] as { bindGroupLayouts: unknown[] }).bindGroupLayouts.length).toBe(3);
186
+ });
187
+
188
+ it('appends the shadow layout as group(3) when given a shadow bind-group layout', () => {
189
+ const { fake, state } = makeWgpuScene3DState();
190
+ const pipeline = makeShadowPipeline(state);
191
+ expect(pipeline.hasShadowGroup).toBe(true);
192
+ const layoutCall = fake.calls.filter((c) => c.name === 'createPipelineLayout').at(-1);
193
+ expect((layoutCall!.args[0] as { bindGroupLayouts: unknown[] }).bindGroupLayouts.length).toBe(4);
194
+ });
195
+
196
+ it('uses one group(3) sample layout for PBR shadow and IBL resources', () => {
197
+ const { fake, state } = makeWgpuScene3DState();
198
+ const pipeline = makePbrSamplePipeline(state);
199
+ expect(pipeline.hasPbrSampleGroup).toBe(true);
200
+ expect(pipeline.hasShadowGroup).toBe(false);
201
+ expect(pipeline.hasIblGroup).toBe(false);
202
+ const layoutCall = fake.calls.filter((c) => c.name === 'createPipelineLayout').at(-1);
203
+ expect((layoutCall!.args[0] as { bindGroupLayouts: unknown[] }).bindGroupLayouts.length).toBe(4);
204
+ });
205
+
206
+ it('uses src-alpha blending and disables depth writes for a blended variant', () => {
207
+ const { fake, state } = makeWgpuScene3DState();
208
+ const module = state.device.createShaderModule({ code: '' });
209
+ const materialBindGroupLayout = state.device.createBindGroupLayout({ entries: [] });
210
+ createWgpuMeshPipeline(state, {
211
+ blended: true,
212
+ doubleSided: false,
213
+ format: 'bgra8unorm',
214
+ materialBindGroupLayout,
215
+ module,
216
+ });
217
+
218
+ const call = fake.calls.filter((c) => c.name === 'createRenderPipeline').at(-1);
219
+ const descriptor = call!.args[0] as GPURenderPipelineDescriptor;
220
+ expect(Array.from(descriptor.fragment!.targets)[0]!.blend).toEqual({
221
+ alpha: { dstFactor: 'one-minus-src-alpha', operation: 'add', srcFactor: 'one' },
222
+ color: { dstFactor: 'one-minus-src-alpha', operation: 'add', srcFactor: 'src-alpha' },
223
+ });
224
+ expect(descriptor.depthStencil!.depthWriteEnabled).toBe(false);
225
+ });
226
+
227
+ it('keeps blending disabled and depth writes enabled for an opaque variant', () => {
228
+ const { fake, state } = makeWgpuScene3DState();
229
+ makePipeline(state);
230
+ const call = fake.calls.filter((c) => c.name === 'createRenderPipeline').at(-1);
231
+ const descriptor = call!.args[0] as GPURenderPipelineDescriptor;
232
+ expect(Array.from(descriptor.fragment!.targets)[0]!.blend).toBeUndefined();
233
+ expect(descriptor.depthStencil!.depthWriteEnabled).toBe(true);
234
+ });
235
+ });
236
+
237
+ describe('drawWgpuMeshSubset', () => {
238
+ it('issues an indexed draw over the subset after a pipeline is active', () => {
239
+ const { fake, state } = makeWgpuScene3DState();
240
+ ensureWgpuFrameBindGroup(state);
241
+ beginWgpuMeshDraw(state, makePipeline(state));
242
+ const proxy = makeProxy();
243
+ drawWgpuMeshSubset(state, proxy, createBoxMeshGeometry());
244
+ const draw = fake.calls.find((c) => c.name === 'drawIndexed');
245
+ expect(draw).toBeDefined();
246
+ expect(draw!.args[0]).toBe(proxy.subset.indexCount);
247
+ });
248
+
249
+ it('is a no-op when no pipeline is active', () => {
250
+ const { fake, state } = makeWgpuScene3DState();
251
+ drawWgpuMeshSubset(state, makeProxy(), createBoxMeshGeometry());
252
+ expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(false);
253
+ });
254
+ });
255
+
256
+ describe('ensureWgpuFrameBindGroup', () => {
257
+ it('creates the frame buffer + bind group once and reuses them', () => {
258
+ const { fake, state } = makeWgpuScene3DState();
259
+ const a = ensureWgpuFrameBindGroup(state);
260
+ const buffers = fake.calls.filter((c) => c.name === 'createBuffer').length;
261
+ const b = ensureWgpuFrameBindGroup(state);
262
+ expect(a).toBe(b);
263
+ expect(fake.calls.filter((c) => c.name === 'createBuffer').length).toBe(buffers);
264
+ });
265
+ });
266
+
267
+ describe('ensureWgpuIblSampleBindGroup', () => {
268
+ it('writes the disabled IBL uniform and reuses the bind group when no IBL changes', () => {
269
+ const { fake, state } = makeWgpuScene3DState();
270
+ const a = ensureWgpuIblSampleBindGroup(state);
271
+ const made = fake.calls.filter((c) => c.name === 'createBindGroup').length;
272
+ const b = ensureWgpuIblSampleBindGroup(state);
273
+ expect(a).toBe(b);
274
+ // No new bind group on the second call (dummy views unchanged); the uniform is rewritten each call.
275
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(made);
276
+ expect(fake.calls.some((c) => c.name === 'writeBuffer')).toBe(true);
277
+ });
278
+
279
+ it('rebuilds the bind group when a baked IBL set becomes present', () => {
280
+ const { fake, state } = makeWgpuScene3DState();
281
+ ensureWgpuIblSampleBindGroup(state);
282
+ const before = fake.calls.filter((c) => c.name === 'createBindGroup').length;
283
+ // Simulate bakeWgpuEnvironmentIbl having stored a baked set this frame.
284
+ getWgpuScene3DRuntime(state).ibl = {
285
+ brdfLut: {} as GPUTexture,
286
+ brdfLutView: {} as GPUTextureView,
287
+ intensity: 1,
288
+ irradianceCube: {} as GPUTexture,
289
+ irradianceCubeView: {} as GPUTextureView,
290
+ prefilteredCube: {} as GPUTexture,
291
+ prefilteredCubeView: {} as GPUTextureView,
292
+ prefilteredMipCount: 5,
293
+ };
294
+ ensureWgpuIblSampleBindGroup(state);
295
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(before + 1);
296
+ });
297
+ });
298
+
299
+ describe('ensureWgpuIblSampleLayout', () => {
300
+ it('creates the IBL-sample layout once per state', () => {
301
+ const { fake, state } = makeWgpuScene3DState();
302
+ const a = ensureWgpuIblSampleLayout(state);
303
+ const made = fake.calls.filter((c) => c.name === 'createBindGroupLayout').length;
304
+ const b = ensureWgpuIblSampleLayout(state);
305
+ expect(a).toBe(b);
306
+ expect(fake.calls.filter((c) => c.name === 'createBindGroupLayout').length).toBe(made);
307
+ });
308
+ });
309
+
310
+ describe('ensureWgpuMaterialBinding', () => {
311
+ const layout = {} as GPUBindGroupLayout;
312
+ const sampler = {} as GPUSampler;
313
+ const view0 = {} as GPUTextureView;
314
+ const view1 = {} as GPUTextureView;
315
+
316
+ it('creates the buffer + bind group once, owns a copy of the scratch, and steady-state binds build nothing', () => {
317
+ const { fake, state } = makeWgpuScene3DState();
318
+ const key = {};
319
+ const scratch = [view0, view1];
320
+ const binding = ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
321
+
322
+ expect(fake.calls.filter((c) => c.name === 'createBuffer').length).toBe(1);
323
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(1);
324
+ // The binding owns a COPY of the scratch — a later material's bind reusing the scratch must not
325
+ // corrupt this binding's cached views.
326
+ expect(binding.views).not.toBe(scratch);
327
+ expect(binding.views).toEqual([view0, view1]);
328
+
329
+ // Steady state: same key + same sampler + same view identities re-bound → no new GPU objects built
330
+ // (the hot path allocates/builds nothing; the caller's uniform write is separate).
331
+ ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
332
+ ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
333
+ expect(fake.calls.filter((c) => c.name === 'createBuffer').length).toBe(1);
334
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(1);
335
+ });
336
+
337
+ it('rebuilds the bind group in place (reusing the buffer) when a resolved view changes', () => {
338
+ const { fake, state } = makeWgpuScene3DState();
339
+ const key = {};
340
+ const scratch = [view0, view1];
341
+ ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
342
+ // A live map swap resolves to a new view identity in the reused scratch.
343
+ scratch[0] = {} as GPUTextureView;
344
+ ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
345
+
346
+ expect(fake.calls.filter((c) => c.name === 'createBuffer').length).toBe(1); // buffer reused
347
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(2); // bind group rebuilt
348
+ });
349
+
350
+ it('rebuilds when the primary sampler changes', () => {
351
+ const { fake, state } = makeWgpuScene3DState();
352
+ const key = {};
353
+ const scratch = [view0, view1];
354
+ ensureWgpuMaterialBinding(state, key, layout, 48, sampler, scratch);
355
+ ensureWgpuMaterialBinding(state, key, layout, 48, {} as GPUSampler, scratch);
356
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(2);
357
+ });
358
+ });
359
+
360
+ describe('ensureWgpuPbrSampleBindGroup', () => {
361
+ it('packs shadow and IBL sample bindings into one cached group', () => {
362
+ const { fake, state } = makeWgpuScene3DState();
363
+ const a = ensureWgpuPbrSampleBindGroup(state);
364
+ const bindGroupCall = fake.calls.filter((c) => c.name === 'createBindGroup').at(-1);
365
+ const made = fake.calls.filter((c) => c.name === 'createBindGroup').length;
366
+ const b = ensureWgpuPbrSampleBindGroup(state);
367
+
368
+ expect(a).toBe(b);
369
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(made);
370
+ expect((bindGroupCall!.args[0] as { entries: unknown[] }).entries.length).toBe(8);
371
+ expect(fake.calls.filter((c) => c.name === 'writeBuffer').length).toBeGreaterThanOrEqual(2);
372
+ });
373
+
374
+ it('rebuilds the group when a shadow map becomes present', () => {
375
+ const { fake, state } = makeWgpuScene3DState();
376
+ ensureWgpuPbrSampleBindGroup(state);
377
+ const before = fake.calls.filter((c) => c.name === 'createBindGroup').length;
378
+
379
+ getWgpuScene3DRuntime(state).shadow = {
380
+ depthTexture: {} as GPUTexture,
381
+ depthView: {} as GPUTextureView,
382
+ matrix: createMatrix4(),
383
+ };
384
+
385
+ ensureWgpuPbrSampleBindGroup(state);
386
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(before + 1);
387
+ });
388
+ });
389
+
390
+ describe('ensureWgpuPbrSampleLayout', () => {
391
+ it('creates the combined PBR sample layout once per state', () => {
392
+ const { fake, state } = makeWgpuScene3DState();
393
+ const a = ensureWgpuPbrSampleLayout(state);
394
+ const layoutCall = fake.calls.filter((c) => c.name === 'createBindGroupLayout').at(-1);
395
+ const made = fake.calls.filter((c) => c.name === 'createBindGroupLayout').length;
396
+ const b = ensureWgpuPbrSampleLayout(state);
397
+
398
+ expect(a).toBe(b);
399
+ expect(fake.calls.filter((c) => c.name === 'createBindGroupLayout').length).toBe(made);
400
+ expect((layoutCall!.args[0] as { entries: unknown[] }).entries.length).toBe(8);
401
+ });
402
+ });
403
+
404
+ describe('ensureWgpuPerMapMaterialBinding', () => {
405
+ const layout = {} as GPUBindGroupLayout;
406
+ const sampler0 = {} as GPUSampler;
407
+ const sampler1 = {} as GPUSampler;
408
+ const view0 = {} as GPUTextureView;
409
+ const view1 = {} as GPUTextureView;
410
+
411
+ it('owns the parallel scratch copies and builds nothing on steady-state rebind', () => {
412
+ const { fake, state } = makeWgpuScene3DState();
413
+ const key = {};
414
+ const samplers = [sampler0, sampler1];
415
+ const views = [view0, view1];
416
+ const binding = ensureWgpuPerMapMaterialBinding(state, key, layout, 48, samplers, views);
417
+
418
+ expect(binding.samplers).not.toBe(samplers);
419
+ expect(binding.views).not.toBe(views);
420
+ expect(binding.samplers).toEqual(samplers);
421
+ expect(binding.views).toEqual(views);
422
+ ensureWgpuPerMapMaterialBinding(state, key, layout, 48, samplers, views);
423
+ expect(fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(1);
424
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(1);
425
+ });
426
+
427
+ it('rebuilds only the bind group when a non-primary sampler changes', () => {
428
+ const { fake, state } = makeWgpuScene3DState();
429
+ const key = {};
430
+ const samplers = [sampler0, sampler1];
431
+ const views = [view0, view1];
432
+ const binding = ensureWgpuPerMapMaterialBinding(state, key, layout, 48, samplers, views);
433
+ const replacement = {} as GPUSampler;
434
+ samplers[1] = replacement;
435
+
436
+ const rebuilt = ensureWgpuPerMapMaterialBinding(state, key, layout, 48, samplers, views);
437
+
438
+ expect(rebuilt).toBe(binding);
439
+ expect(rebuilt.samplers).toEqual([sampler0, replacement]);
440
+ expect(fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(1);
441
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(2);
442
+ });
443
+ });
444
+
445
+ describe('ensureWgpuPlaceholderTextureView', () => {
446
+ it('creates the 1x1 white texture once and reuses the view', () => {
447
+ const { fake, state } = makeWgpuScene3DState();
448
+ const a = ensureWgpuPlaceholderTextureView(state);
449
+ const textures = fake.calls.filter((c) => c.name === 'createTexture').length;
450
+ const b = ensureWgpuPlaceholderTextureView(state);
451
+ expect(a).toBe(b);
452
+ expect(fake.calls.filter((c) => c.name === 'createTexture').length).toBe(textures);
453
+ });
454
+ });
455
+
456
+ describe('ensureWgpuScene3DLayouts', () => {
457
+ it('creates the frame + draw layouts once per state', () => {
458
+ const { fake, state } = makeWgpuScene3DState();
459
+ ensureWgpuScene3DLayouts(state);
460
+ const made = fake.calls.filter((c) => c.name === 'createBindGroupLayout').length;
461
+ ensureWgpuScene3DLayouts(state);
462
+ expect(fake.calls.filter((c) => c.name === 'createBindGroupLayout').length).toBe(made);
463
+ });
464
+ });
465
+
466
+ describe('ensureWgpuScene3DPipeline', () => {
467
+ it('compiles a key once and returns the cached pipeline', () => {
468
+ const { state } = makeWgpuScene3DState();
469
+ let compiles = 0;
470
+ const compile = () => {
471
+ compiles++;
472
+ return makePipeline(state);
473
+ };
474
+ const a = ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
475
+ const b = ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
476
+ expect(a).toBe(b);
477
+ expect(compiles).toBe(1);
478
+ });
479
+
480
+ it('caches separate opaque and blended variants of the same family key', () => {
481
+ const { state } = makeWgpuScene3DState();
482
+ const variants: boolean[] = [];
483
+ const compile = (blended: boolean) => {
484
+ variants.push(blended);
485
+ return makePipeline(state);
486
+ };
487
+
488
+ ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
489
+ getWgpuScene3DRuntime(state).activeBlendedRun = true;
490
+ ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
491
+ ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
492
+
493
+ expect(variants).toEqual([false, true]);
494
+ expect(Array.from(getWgpuScene3DRuntime(state).pipelineCache.keys())).toEqual([
495
+ 'fam:bgra8unorm|-|opaque|rigid',
496
+ 'fam:bgra8unorm|-|blend|rigid',
497
+ ]);
498
+ });
499
+
500
+ it('caches rigid and skinned variants independently', () => {
501
+ const { state } = makeWgpuScene3DState();
502
+ const variants: boolean[] = [];
503
+ const compile = (_blended: boolean, skinned: boolean) => {
504
+ variants.push(skinned);
505
+ return makePipeline(state);
506
+ };
507
+
508
+ ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
509
+ getWgpuScene3DRuntime(state).activeSkinnedRun = true;
510
+ ensureWgpuScene3DPipeline(state, 'fam:bgra8unorm|-', compile);
511
+
512
+ expect(variants).toEqual([false, true]);
513
+ expect(Array.from(getWgpuScene3DRuntime(state).pipelineCache.keys())).toEqual([
514
+ 'fam:bgra8unorm|-|opaque|rigid',
515
+ 'fam:bgra8unorm|-|opaque|skin',
516
+ ]);
517
+ });
518
+ });
519
+
520
+ describe('ensureWgpuShadowSampleBindGroup', () => {
521
+ it('writes the disabled shadow uniform and reuses the bind group when no shadow changes', () => {
522
+ const { fake, state } = makeWgpuScene3DState();
523
+ const a = ensureWgpuShadowSampleBindGroup(state);
524
+ const made = fake.calls.filter((c) => c.name === 'createBindGroup').length;
525
+ const b = ensureWgpuShadowSampleBindGroup(state);
526
+ expect(a).toBe(b);
527
+ // No new bind group on the second call (dummy view unchanged); the uniform is rewritten each call.
528
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(made);
529
+ expect(fake.calls.some((c) => c.name === 'writeBuffer')).toBe(true);
530
+ });
531
+
532
+ it('rebuilds the bind group when a shadow map becomes present', () => {
533
+ const { fake, state } = makeWgpuScene3DState();
534
+ ensureWgpuShadowSampleBindGroup(state);
535
+ const before = fake.calls.filter((c) => c.name === 'createBindGroup').length;
536
+ // Simulate drawWgpuScene3DShadowMap having stored a shadow this frame.
537
+ getWgpuScene3DRuntime(state).shadow = {
538
+ depthTexture: {} as GPUTexture,
539
+ depthView: {} as GPUTextureView,
540
+ matrix: createMatrix4(),
541
+ };
542
+ ensureWgpuShadowSampleBindGroup(state);
543
+ expect(fake.calls.filter((c) => c.name === 'createBindGroup').length).toBe(before + 1);
544
+ });
545
+ });
546
+
547
+ describe('ensureWgpuShadowSampleLayout', () => {
548
+ it('creates the shadow-sample layout once per state', () => {
549
+ const { fake, state } = makeWgpuScene3DState();
550
+ const a = ensureWgpuShadowSampleLayout(state);
551
+ const made = fake.calls.filter((c) => c.name === 'createBindGroupLayout').length;
552
+ const b = ensureWgpuShadowSampleLayout(state);
553
+ expect(a).toBe(b);
554
+ expect(fake.calls.filter((c) => c.name === 'createBindGroupLayout').length).toBe(made);
555
+ });
556
+ });
557
+
558
+ describe('getWgpuMaterialSampler', () => {
559
+ it('returns the shared clamp sampler for a null map', () => {
560
+ const { state } = makeWgpuScene3DState();
561
+ expect(getWgpuMaterialSampler(state, null)).toBe(getWgpuRenderStateRuntime(state).linearSampler);
562
+ });
563
+
564
+ it('derives a wrap- and mip-specific sampler for a tiling map', () => {
565
+ const { state } = makeWgpuScene3DState();
566
+ const texture = createTexture({ image: {} as ImageResource });
567
+ texture.sampler.wrapU = 'repeat';
568
+ texture.sampler.wrapV = 'repeat';
569
+
570
+ const sampler = getWgpuMaterialSampler(state, texture);
571
+
572
+ // A non-clamp (tiling) map goes through the sampler cache, not the shared clamp sampler, and the
573
+ // derivation is deterministic (same texture → same cached sampler).
574
+ expect(sampler).not.toBe(getWgpuRenderStateRuntime(state).linearSampler);
575
+ expect(getWgpuMaterialSampler(state, texture)).toBe(sampler);
576
+ });
577
+
578
+ it('drops the mip filter when the map disables mipmaps (distinct packed key from the mipmapped default)', () => {
579
+ const { state } = makeWgpuScene3DState();
580
+ const noMip = createTexture({ image: {} as ImageResource });
581
+ noMip.sampler.mipmaps = false;
582
+ const withMip = createTexture({ image: {} as ImageResource }); // default: trilinear (mipmaps on)
583
+
584
+ getWgpuMaterialSampler(state, noMip);
585
+ getWgpuMaterialSampler(state, withMip);
586
+ // The two mip configs pack into two distinct numeric keys, so the cache holds both.
587
+ expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(2);
588
+ });
589
+
590
+ it('carries the map anisotropy into the derived sampler (distinct packed key from the non-anisotropic one)', () => {
591
+ const { state } = makeWgpuScene3DState();
592
+ const aniso = createTexture({ image: {} as ImageResource });
593
+ aniso.sampler.anisotropy = 8;
594
+ const noAniso = createTexture({ image: {} as ImageResource });
595
+
596
+ getWgpuMaterialSampler(state, aniso);
597
+ getWgpuMaterialSampler(state, noAniso);
598
+ expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(2);
599
+ });
600
+ });
601
+
602
+ describe('getWgpuMeshPreludeWgsl', () => {
603
+ it('adds the palette binding, attributes, and textureLoad skin matrix only to the skinned variant', () => {
604
+ const rigid = getWgpuMeshPreludeWgsl(false);
605
+ const skinned = getWgpuMeshPreludeWgsl(true, makeWgpuSkinningAdapter());
606
+
607
+ expect(rigid).not.toContain('jointTexture');
608
+ expect(rigid).not.toContain('joints0');
609
+ expect(skinned).toContain('@group(1) @binding(1) var jointTexture');
610
+ expect(skinned).toContain('@location(4) joints0 : vec4f');
611
+ expect(skinned).toContain('@location(5) weights0 : vec4f');
612
+ expect(skinned).toContain('textureLoad(jointTexture');
613
+ expect(skinned).toContain('localPosition = skin * localPosition');
614
+ });
615
+ });
616
+
617
+ describe('isWgpuMaterialBindGroupRebuildNeeded', () => {
618
+ const sampler = {} as GPUSampler;
619
+ const view0 = {} as GPUTextureView;
620
+ const view1 = {} as GPUTextureView;
621
+ const cached: WgpuMaterialBinding = {
622
+ bindGroup: {} as GPUBindGroup,
623
+ buffer: {} as GPUBuffer,
624
+ sampler,
625
+ views: [view0, view1],
626
+ };
627
+
628
+ it('is false when the sampler and every resolved view match the cache', () => {
629
+ expect(isWgpuMaterialBindGroupRebuildNeeded(cached, sampler, [view0, view1])).toBe(false);
630
+ });
631
+
632
+ it('rebuilds when the primary sampler identity changes (a primary-map sampler edit)', () => {
633
+ // Only the ONE primary-map sampler participates (shared-primary-sampler contract); a non-primary
634
+ // map's per-Texture sampler is never bound and cannot trip this.
635
+ expect(isWgpuMaterialBindGroupRebuildNeeded(cached, {} as GPUSampler, [view0, view1])).toBe(true);
636
+ });
637
+
638
+ it('rebuilds when any resolved view identity changes (swap / unready->ready / ready->ready / version++)', () => {
639
+ // resolveWgpuMaterialTextureView is the invalidation seam: a texture swap, an unready->ready
640
+ // transition, a ready->ready image replacement, or an ImageResource version bump each yield a new
641
+ // view identity, so this single identity check covers all four.
642
+ expect(isWgpuMaterialBindGroupRebuildNeeded(cached, sampler, [{} as GPUTextureView, view1])).toBe(true);
643
+ });
644
+
645
+ it('rebuilds when the resolved view count changes or the cache has no views yet', () => {
646
+ expect(isWgpuMaterialBindGroupRebuildNeeded(cached, sampler, [view0])).toBe(true);
647
+ const noViews: WgpuMaterialBinding = { bindGroup: {} as GPUBindGroup, buffer: {} as GPUBuffer };
648
+ expect(isWgpuMaterialBindGroupRebuildNeeded(noViews, sampler, [view0])).toBe(true);
649
+ });
650
+ });
651
+
652
+ describe('isWgpuTextureReady', () => {
653
+ it('is true when the texture carries pixels (element or data), false otherwise', () => {
654
+ expect(isWgpuTextureReady(null)).toBe(false);
655
+ expect(isWgpuTextureReady({ image: null } as unknown as Texture)).toBe(false);
656
+ expect(isWgpuTextureReady({ image: { source: null, data: null, compressed: null } } as unknown as Texture)).toBe(
657
+ false,
658
+ );
659
+ expect(isWgpuTextureReady({ image: { source: {} } } as unknown as Texture)).toBe(true);
660
+ expect(isWgpuTextureReady({ image: { source: null, data: new Uint8ClampedArray(4) } } as unknown as Texture)).toBe(
661
+ true,
662
+ );
663
+ });
664
+ });
665
+
666
+ describe('resolveWgpuMaterialTextureView', () => {
667
+ it('returns the shared placeholder view for a map without an image source', () => {
668
+ const { state } = makeWgpuScene3DState();
669
+ const placeholder = ensureWgpuPlaceholderTextureView(state);
670
+ expect(resolveWgpuMaterialTextureView(state, null)).toBe(placeholder);
671
+ expect(resolveWgpuMaterialTextureView(state, { image: null } as unknown as Texture)).toBe(placeholder);
672
+ });
673
+ });
674
+
675
+ describe('stashWgpuUvTransform', () => {
676
+ it('stores the column-major transform for a bound non-identity texture', () => {
677
+ const { state } = makeWgpuScene3DState();
678
+ const texture = createTexture({ image: {} as ImageResource });
679
+ setTextureUvScale(texture, 2, 3);
680
+ setTextureUvOffset(texture, 0.5, 0.25);
681
+
682
+ stashWgpuUvTransform(state, texture);
683
+
684
+ // Column-major: col0 = scaled U axis, col1 = scaled V axis, col2 = translation.
685
+ const stash = Array.from(getWgpuScene3DRuntime(state).pendingUvTransform).map((n) => n + 0);
686
+ expect(stash).toEqual([2, 0, 0, 0, 3, 0, 0.5, 0.25, 1]);
687
+ });
688
+
689
+ it('resets to identity for a null texture', () => {
690
+ const { state } = makeWgpuScene3DState();
691
+ const texture = createTexture({ image: {} as ImageResource });
692
+ setTextureUvScale(texture, 4, 4);
693
+ stashWgpuUvTransform(state, texture);
694
+
695
+ stashWgpuUvTransform(state, null);
696
+
697
+ expect(Array.from(getWgpuScene3DRuntime(state).pendingUvTransform)).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1]);
698
+ });
699
+
700
+ it('resets to identity for an identity-transform texture', () => {
701
+ const { state } = makeWgpuScene3DState();
702
+
703
+ stashWgpuUvTransform(state, createTexture({ image: {} as ImageResource }));
704
+
705
+ expect(Array.from(getWgpuScene3DRuntime(state).pendingUvTransform)).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1]);
706
+ });
707
+ });
708
+
709
+ describe('WGPU_MESH_PRELUDE_WGSL', () => {
710
+ it('declares the shared Frame + Draw structs and the vertex entry', () => {
711
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('struct Frame');
712
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('struct Draw');
713
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('fn vs_main');
714
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('srgbToLinear');
715
+ });
716
+
717
+ it('applies the uv transform in the shared vertex scene2d', () => {
718
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('uvTransform : mat3x3f');
719
+ expect(WGPU_MESH_PRELUDE_WGSL).toContain('draw.uvTransform * vec3f(uv, 1.0)');
720
+ });
721
+ });
722
+
723
+ describe('wgpuPerMapMaterialBindGroupNeedsRebuild', () => {
724
+ const sampler0 = {} as GPUSampler;
725
+ const sampler1 = {} as GPUSampler;
726
+ const view0 = {} as GPUTextureView;
727
+ const view1 = {} as GPUTextureView;
728
+ const cached: WgpuMaterialBinding = {
729
+ bindGroup: {} as GPUBindGroup,
730
+ buffer: {} as GPUBuffer,
731
+ samplers: [sampler0, sampler1],
732
+ views: [view0, view1],
733
+ };
734
+
735
+ it('keeps an unchanged parallel sampler/view cache', () => {
736
+ expect(wgpuPerMapMaterialBindGroupNeedsRebuild(cached, [sampler0, sampler1], [view0, view1])).toBe(false);
737
+ });
738
+
739
+ it('rebuilds when a non-primary sampler changes', () => {
740
+ expect(wgpuPerMapMaterialBindGroupNeedsRebuild(cached, [sampler0, {} as GPUSampler], [view0, view1])).toBe(true);
741
+ });
742
+
743
+ it('rebuilds for a view change or mismatched array shape', () => {
744
+ expect(wgpuPerMapMaterialBindGroupNeedsRebuild(cached, [sampler0, sampler1], [view0, {} as GPUTextureView])).toBe(
745
+ true,
746
+ );
747
+ expect(wgpuPerMapMaterialBindGroupNeedsRebuild(cached, [sampler0], [view0, view1])).toBe(true);
748
+ });
749
+ });
750
+
751
+ describe('writeWgpuDrawUniform', () => {
752
+ it('writes the draw uniform and returns the dynamic-offset bind group', () => {
753
+ const { state } = makeWgpuScene3DState();
754
+ const group = writeWgpuDrawUniform(state, makeProxy());
755
+ expect(group).toBeDefined();
756
+ expect(getWgpuScene3DRuntime(state).pendingDrawOffset).toBe(0);
757
+ });
758
+
759
+ it('folds the stashed uv transform into the draw uniform', () => {
760
+ const { state } = makeWgpuScene3DState();
761
+ const texture = createTexture({ image: {} as ImageResource });
762
+ setTextureUvScale(texture, 2, 3);
763
+ stashWgpuUvTransform(state, texture);
764
+
765
+ writeWgpuDrawUniform(state, makeProxy());
766
+
767
+ // The uvTransform occupies floats 28..39 (3 padded vec4) after world (0..15) + normalMatrix (16..27).
768
+ const u = getWgpuRenderStateRuntime(state).uniformData;
769
+ expect([u[28], u[29], u[30]].map((n) => n + 0)).toEqual([2, 0, 0]);
770
+ expect([u[32], u[33], u[34]].map((n) => n + 0)).toEqual([0, 3, 0]);
771
+ });
772
+
773
+ it('persists the stash across draws so a material shared by many meshes tiles every one', () => {
774
+ // Regression: drawWgpuScene3D binds once per material then draws many meshes. The stash must survive
775
+ // each writeWgpuDrawUniform (not be consumed), or only the first mesh under a bind would tile.
776
+ const { state } = makeWgpuScene3DState();
777
+ const texture = createTexture({ image: {} as ImageResource });
778
+ setTextureUvScale(texture, 2, 3);
779
+ stashWgpuUvTransform(state, texture);
780
+
781
+ writeWgpuDrawUniform(state, makeProxy());
782
+ // Not consumed — the stash still holds the transform for the next mesh under the same bind.
783
+ expect(Array.from(getWgpuScene3DRuntime(state).pendingUvTransform).map((n) => n + 0)).toEqual([
784
+ 2, 0, 0, 0, 3, 0, 0, 0, 1,
785
+ ]);
786
+
787
+ // The second draw (no re-stash) folds the same transform into its own ring slot.
788
+ const secondBase = getWgpuRenderStateRuntime(state).uniformOffset / 4;
789
+ writeWgpuDrawUniform(state, makeProxy());
790
+ const u = getWgpuRenderStateRuntime(state).uniformData;
791
+ // col0.x = scaleX at +28, col1.y = scaleY at +33.
792
+ expect([u[secondBase + 28], u[secondBase + 33]].map((n) => n + 0)).toEqual([2, 3]);
793
+ });
794
+
795
+ it('writes the resolved object alpha into the draw params', () => {
796
+ const { state } = makeWgpuScene3DState();
797
+ const proxy = makeProxy();
798
+ proxy.alpha = 0.375;
799
+ writeWgpuDrawUniform(state, proxy);
800
+ expect(getWgpuRenderStateRuntime(state).uniformData[40]).toBeCloseTo(0.375);
801
+ });
802
+ });
803
+
804
+ describe('writeWgpuFrameUniform', () => {
805
+ it('writes the frame uniform buffer', () => {
806
+ const { fake, state } = makeWgpuScene3DState();
807
+ writeWgpuFrameUniform(state, makeCamera(), makeLights());
808
+ expect(fake.calls.some((c) => c.name === 'writeBuffer')).toBe(true);
809
+ });
810
+
811
+ it('uses distinct cached buffers for distinct light blocks so queued draws retain their data', () => {
812
+ const { state } = makeWgpuScene3DState();
813
+ const camera = makeCamera();
814
+ const firstLights = makeLights();
815
+ const secondLights = makeLights();
816
+ writeWgpuFrameUniform(state, camera, firstLights);
817
+ const firstBuffer = getWgpuScene3DRuntime(state).frameBuffer;
818
+ writeWgpuFrameUniform(state, camera, secondLights);
819
+ const secondBuffer = getWgpuScene3DRuntime(state).frameBuffer;
820
+ expect(secondBuffer).not.toBe(firstBuffer);
821
+ writeWgpuFrameUniform(state, camera, firstLights);
822
+ expect(getWgpuScene3DRuntime(state).frameBuffer).toBe(firstBuffer);
823
+ });
824
+
825
+ it('remaps OpenGL clip-space Z into WebGPU NDC depth', () => {
826
+ const { fake, state } = makeWgpuScene3DState();
827
+ const camera = createCamera3D({
828
+ far: 11,
829
+ near: 1,
830
+ projection: { halfHeight: 1, halfWidth: 1, kind: 'orthographic' },
831
+ });
832
+ writeWgpuFrameUniform(state, camera, makeLights());
833
+
834
+ const write = fake.calls.find((c) => c.name === 'writeBuffer');
835
+ const frame = new Float32Array(write!.args[2] as ArrayBuffer, 0, 16);
836
+ // With identity view, z=-near maps to NDC 0 and z=-far maps to NDC 1.
837
+ const nearClipZ = frame[10] * -camera.near + frame[14];
838
+ const nearClipW = frame[11] * -camera.near + frame[15];
839
+ const farClipZ = frame[10] * -camera.far + frame[14];
840
+ const farClipW = frame[11] * -camera.far + frame[15];
841
+ expect(nearClipZ / nearClipW).toBeCloseTo(0);
842
+ expect(farClipZ / farClipW).toBeCloseTo(1);
843
+ });
844
+ });