@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,33 @@
1
+ import type { WgpuModifierSnippet } from '@flighthq/types';
2
+ import { ModifierSlot } from '@flighthq/types';
3
+
4
+ import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
5
+ import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
6
+ import { registerWgpuModifierSnippet, resolveWgpuModifierSnippet } from './wgpuShadedModifierSnippet';
7
+
8
+ function makeSnippet(overrides?: Partial<WgpuModifierSnippet>): WgpuModifierSnippet {
9
+ return {
10
+ contribution: () => ({ source: '// noop' }),
11
+ kind: 'acme.Test',
12
+ slot: ModifierSlot.Effect,
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe('registerWgpuModifierSnippet', () => {
18
+ it('lazily allocates a state-scoped registry and is last-write-wins', () => {
19
+ const { state } = makeWgpuScene3DState();
20
+ expect(getWgpuScene3DRuntime(state).modifierSnippetRegistry).toBeNull();
21
+ registerWgpuModifierSnippet(state, makeSnippet());
22
+ const override = makeSnippet({ contribution: () => ({ source: '// override' }) });
23
+ registerWgpuModifierSnippet(state, override);
24
+ expect(resolveWgpuModifierSnippet(state, 'acme.Test')).toBe(override);
25
+ });
26
+ });
27
+
28
+ describe('resolveWgpuModifierSnippet', () => {
29
+ it('returns null for an unregistered kind', () => {
30
+ const { state } = makeWgpuScene3DState();
31
+ expect(resolveWgpuModifierSnippet(state, 'acme.Missing')).toBeNull();
32
+ });
33
+ });
@@ -0,0 +1,394 @@
1
+ import {
2
+ createAnimatedNormalModifier,
3
+ createDissolveModifier,
4
+ createEmissiveModifier,
5
+ createEnvReflectModifier,
6
+ createFogModifier,
7
+ createRimModifier,
8
+ createShadedMaterial,
9
+ createToonModifier,
10
+ createVertexDisplaceModifier,
11
+ } from '@flighthq/shading';
12
+ import { createTexture } from '@flighthq/texture';
13
+ import type { ImageResource, Modifier, Texture, WgpuModifierSnippet } from '@flighthq/types';
14
+ import { FogModifierMode, ModifierSlot, VertexDisplaceModifierSource } from '@flighthq/types';
15
+
16
+ import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
17
+ import { makeWgpuScene3DState, makeWgpuSkinningAdapter } from './wgpuScene3DTestHelper';
18
+ import { registerWgpuModifierSnippet, resolveWgpuModifierSnippet } from './wgpuShadedModifierSnippet';
19
+ import {
20
+ animatedNormalWgpuModifierSnippet,
21
+ bindWgpuShadedSurface,
22
+ buildWgpuShadedCacheKey,
23
+ dissolveWgpuModifierSnippet,
24
+ emissiveWgpuModifierSnippet,
25
+ ensureWgpuShadedPipeline,
26
+ envReflectWgpuModifierSnippet,
27
+ fogWgpuModifierSnippet,
28
+ getWgpuShadedModuleSource,
29
+ registerBuiltInWgpuModifierSnippets,
30
+ rimWgpuModifierSnippet,
31
+ toonWgpuModifierSnippet,
32
+ vertexDisplaceWgpuModifierSnippet,
33
+ } from './wgpuShadedPrelude';
34
+
35
+ describe('bindWgpuShadedSurface', () => {
36
+ it('uploads the base and modifier uniform block and binds group resources', () => {
37
+ const { fake, state } = makeWgpuScene3DState();
38
+ registerBuiltInWgpuModifierSnippets(state);
39
+ const material = createShadedMaterial({ modifiers: [createRimModifier({ color: 0xffffffff })] });
40
+ const pipeline = ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
41
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
42
+ expect(fake.calls.some((call) => call.name === 'createBindGroup')).toBe(true);
43
+ expect(fake.calls.some((call) => call.name === 'writeBuffer')).toBe(true);
44
+ });
45
+ });
46
+
47
+ describe('buildWgpuShadedCacheKey', () => {
48
+ it('canonicalizes cross-slot order and captures modifier structural variants', () => {
49
+ const rim = createRimModifier({ color: 0xffffffff });
50
+ const emissive = createEmissiveModifier({ color: 0xffffffff, strength: 1 });
51
+ const a = createShadedMaterial({ modifiers: [rim, emissive] });
52
+ const b = createShadedMaterial({ modifiers: [emissive, rim] });
53
+ expect(buildWgpuShadedCacheKey(a)).toBe(buildWgpuShadedCacheKey(b));
54
+
55
+ const masked = createShadedMaterial({
56
+ modifiers: [createEmissiveModifier({ color: 0xffffffff, mask: createTexture(), strength: 1 })],
57
+ });
58
+ expect(buildWgpuShadedCacheKey(masked)).not.toBe(buildWgpuShadedCacheKey(a));
59
+ });
60
+ });
61
+
62
+ describe('ensureWgpuShadedPipeline', () => {
63
+ it('caches shaded opaque and blended variants separately', () => {
64
+ const { state } = makeWgpuScene3DState();
65
+ registerBuiltInWgpuModifierSnippets(state);
66
+ const material = createShadedMaterial({ modifiers: [createRimModifier({ color: 0xffffffff })] });
67
+ ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
68
+ getWgpuScene3DRuntime(state).activeBlendedRun = true;
69
+ ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
70
+ const keys = [...getWgpuScene3DRuntime(state).pipelineCache.keys()];
71
+ expect(keys.some((key) => key.startsWith('shaded:') && key.endsWith('|opaque|rigid'))).toBe(true);
72
+ expect(keys.some((key) => key.startsWith('shaded:') && key.endsWith('|blend|rigid'))).toBe(true);
73
+ });
74
+
75
+ it('reserves alpha-map binding 5 and starts modifier textures at binding 6', () => {
76
+ const { fake, state } = makeWgpuScene3DState();
77
+ registerBuiltInWgpuModifierSnippets(state);
78
+ const mask = createTexture();
79
+ const material = createShadedMaterial({
80
+ modifiers: [createEmissiveModifier({ color: 0xffffffff, mask, strength: 1 })],
81
+ });
82
+ ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
83
+ const layout = fake.calls
84
+ .filter((call) => call.name === 'createBindGroupLayout')
85
+ .map((call) => call.args[0] as { entries: { binding: number }[] })
86
+ .find(({ entries }) => entries.some(({ binding }) => binding === 6));
87
+ expect(layout?.entries.map(({ binding }) => binding)).toEqual([0, 1, 2, 3, 4, 5, 6]);
88
+ });
89
+
90
+ it('recompiles after a last-write-wins snippet replacement with the same define signature', () => {
91
+ const { fake, state } = makeWgpuScene3DState();
92
+ const modifier = { kind: 'acme.Replace', slot: ModifierSlot.Effect };
93
+ registerWgpuModifierSnippet(state, {
94
+ contribution: () => ({ source: '// compiler-marker-A' }),
95
+ kind: modifier.kind,
96
+ slot: modifier.slot,
97
+ });
98
+ const material = createShadedMaterial({ modifiers: [modifier] });
99
+ ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
100
+ const before = fake.calls.filter((call) => call.name === 'createShaderModule').length;
101
+
102
+ registerWgpuModifierSnippet(state, {
103
+ contribution: () => ({ source: '// compiler-marker-B' }),
104
+ kind: modifier.kind,
105
+ slot: modifier.slot,
106
+ });
107
+ ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
108
+ const modules = fake.calls.filter((call) => call.name === 'createShaderModule');
109
+
110
+ expect(modules).toHaveLength(before + 1);
111
+ expect((modules.at(-1)!.args[0] as { code: string }).code).toContain('compiler-marker-B');
112
+ });
113
+ });
114
+
115
+ describe('getWgpuShadedModuleSource', () => {
116
+ it('threads the HAS_SKIN palette variant through the composed classic base', () => {
117
+ const material = createShadedMaterial();
118
+ expect(getWgpuShadedModuleSource(material)).not.toContain('jointTexture');
119
+ expect(getWgpuShadedModuleSource(material, undefined, true, makeWgpuSkinningAdapter())).toContain(
120
+ 'textureLoad(jointTexture',
121
+ );
122
+ });
123
+
124
+ it('composes all built-in fragment modifier families into one shader', () => {
125
+ const { state } = makeWgpuScene3DState();
126
+ registerBuiltInWgpuModifierSnippets(state);
127
+ const texture = createTexture();
128
+ const material = createShadedMaterial({
129
+ modifiers: [
130
+ createAnimatedNormalModifier({ map: texture, scroll: { x: 0.1, y: 0.2 } }),
131
+ createEmissiveModifier({ color: 0xff0000ff, mask: texture, strength: 2 }),
132
+ createRimModifier({ color: 0x00ffffff }),
133
+ createDissolveModifier({ threshold: 0.2 }),
134
+ createEnvReflectModifier(),
135
+ createFogModifier({ color: 0xffffffff, mode: FogModifierMode.Exponential2 }),
136
+ createToonModifier({ steps: 3 }),
137
+ ],
138
+ });
139
+ const source = getWgpuShadedModuleSource(material, getWgpuScene3DRuntime(state).modifierSnippetRegistry!);
140
+ expect(source).toContain('animatedNormal');
141
+ expect(source).toContain('emissiveTerm');
142
+ expect(source).toContain('rimFactor');
143
+ expect(source).toContain('shadedValueNoise');
144
+ expect(source).toContain('iblPrefiltered');
145
+ expect(source).toContain('exp(-pow(');
146
+ expect(source).toContain('toonQuant');
147
+ expect(source).toContain('@group(2) @binding(6) var modifierTexture6');
148
+ });
149
+
150
+ it('injects vertex displacement before the world transform', () => {
151
+ const { state } = makeWgpuScene3DState();
152
+ registerBuiltInWgpuModifierSnippets(state);
153
+ const material = createShadedMaterial({
154
+ modifiers: [
155
+ createVertexDisplaceModifier({
156
+ amplitude: 0.25,
157
+ source: VertexDisplaceModifierSource.Sine,
158
+ }),
159
+ ],
160
+ });
161
+ const source = getWgpuShadedModuleSource(material, getWgpuScene3DRuntime(state).modifierSnippetRegistry!);
162
+ expect(source.indexOf('localPosition = vec4f(localPosition.xyz')).toBeLessThan(
163
+ source.indexOf('let world = draw.world * localPosition'),
164
+ );
165
+ });
166
+
167
+ it('composes a registered vendor kind and treats an unregistered kind as an explicit miss', () => {
168
+ const { state } = makeWgpuScene3DState();
169
+ const vendor: WgpuModifierSnippet = {
170
+ contribution: () => ({ source: 'radiance = radiance + vec3f(0.125);\\n' }),
171
+ kind: 'acme.Glow',
172
+ slot: ModifierSlot.Effect,
173
+ };
174
+ registerWgpuModifierSnippet(state, vendor);
175
+ const registered = { kind: 'acme.Glow', slot: ModifierSlot.Effect } as Modifier;
176
+ const missing = { kind: 'acme.Missing', slot: ModifierSlot.Effect } as Modifier;
177
+ const material = createShadedMaterial({ modifiers: [registered, missing] });
178
+ const registry = getWgpuScene3DRuntime(state).modifierSnippetRegistry!;
179
+ const source = getWgpuShadedModuleSource(material, registry);
180
+ expect(source).toContain('radiance = radiance + vec3f(0.125)');
181
+ expect(buildWgpuShadedCacheKey(material, registry)).toContain('acme.Missing');
182
+ expect(resolveWgpuModifierSnippet(state, 'acme.Glow')).toBe(vendor);
183
+ expect(resolveWgpuModifierSnippet(state, 'acme.Missing')).toBeNull();
184
+ });
185
+
186
+ it('routes every open modifier slot and delimits vendor contributions', () => {
187
+ const { state } = makeWgpuScene3DState();
188
+ const slots = [
189
+ ModifierSlot.Normal,
190
+ ModifierSlot.Diffuse,
191
+ ModifierSlot.Specular,
192
+ ModifierSlot.Emissive,
193
+ ModifierSlot.Effect,
194
+ ModifierSlot.Vertex,
195
+ ];
196
+ const statements = [
197
+ 'normal = normal;',
198
+ 'diffuse = diffuse;',
199
+ 'specularColor = specularColor;',
200
+ 'emissive = emissive;',
201
+ 'radiance = radiance;',
202
+ 'localPosition = localPosition;',
203
+ ];
204
+ const modifiers: Modifier[] = [];
205
+ for (let i = 0; i < slots.length; i++) {
206
+ const kind = `acme.Slot${i}`;
207
+ const marker = `slot_marker_${i}`;
208
+ registerWgpuModifierSnippet(state, {
209
+ contribution: () => ({ source: `// ${marker}\n${statements[i]}` }),
210
+ kind,
211
+ slot: slots[i],
212
+ });
213
+ modifiers.push({ kind, slot: slots[i] });
214
+ }
215
+ const registry = getWgpuScene3DRuntime(state).modifierSnippetRegistry!;
216
+ const source = getWgpuShadedModuleSource(createShadedMaterial({ modifiers }), registry);
217
+ for (let i = 0; i < slots.length; i++) expect(source).toContain(`slot_marker_${i}`);
218
+ expect(source).toMatch(/\/\/ slot_marker_1\s+diffuse = diffuse;\s+if \(ALPHA_MASK/);
219
+ expect(source.indexOf('slot_marker_2')).toBeLessThan(source.indexOf('var radiance'));
220
+ });
221
+
222
+ it('deduplicates repeated helper declarations and separates arbitrary declaration blocks', () => {
223
+ const { state } = makeWgpuScene3DState();
224
+ registerBuiltInWgpuModifierSnippets(state);
225
+ registerWgpuModifierSnippet(state, {
226
+ contribution: () => ({ declarations: 'fn vendorA() {}', source: '' }),
227
+ kind: 'acme.DeclarationA',
228
+ slot: ModifierSlot.Effect,
229
+ });
230
+ registerWgpuModifierSnippet(state, {
231
+ contribution: () => ({ declarations: 'fn vendorB() {}', source: '' }),
232
+ kind: 'acme.DeclarationB',
233
+ slot: ModifierSlot.Effect,
234
+ });
235
+ const material = createShadedMaterial({
236
+ modifiers: [
237
+ createDissolveModifier({ threshold: 0.1 }),
238
+ createDissolveModifier({ threshold: 0.2 }),
239
+ { kind: 'acme.DeclarationA', slot: ModifierSlot.Effect },
240
+ { kind: 'acme.DeclarationB', slot: ModifierSlot.Effect },
241
+ ],
242
+ });
243
+ const source = getWgpuShadedModuleSource(material, getWgpuScene3DRuntime(state).modifierSnippetRegistry!);
244
+ expect(source.match(/fn shadedValueNoise/g)).toHaveLength(1);
245
+ expect(source).toContain('fn vendorA() {}\nfn vendorB() {}');
246
+ });
247
+ });
248
+
249
+ describe('registerBuiltInWgpuModifierSnippets', () => {
250
+ it('installs the built-in compiler records explicitly', () => {
251
+ const { state } = makeWgpuScene3DState();
252
+ registerBuiltInWgpuModifierSnippets(state);
253
+ expect(resolveWgpuModifierSnippet(state, 'RimModifier')).not.toBeNull();
254
+ expect([
255
+ animatedNormalWgpuModifierSnippet.kind,
256
+ dissolveWgpuModifierSnippet.kind,
257
+ emissiveWgpuModifierSnippet.kind,
258
+ envReflectWgpuModifierSnippet.kind,
259
+ fogWgpuModifierSnippet.kind,
260
+ rimWgpuModifierSnippet.kind,
261
+ toonWgpuModifierSnippet.kind,
262
+ vertexDisplaceWgpuModifierSnippet.kind,
263
+ ]).toEqual([
264
+ 'AnimatedNormalModifier',
265
+ 'DissolveModifier',
266
+ 'EmissiveModifier',
267
+ 'EnvReflectModifier',
268
+ 'FogModifier',
269
+ 'RimModifier',
270
+ 'ToonModifier',
271
+ 'VertexDisplaceModifier',
272
+ ]);
273
+ });
274
+ });
275
+
276
+ describe('shaded binding cache', () => {
277
+ it('owns GPU bindings per render state when a material is shared across devices', () => {
278
+ const first = makeWgpuScene3DState();
279
+ const second = makeWgpuScene3DState();
280
+ registerBuiltInWgpuModifierSnippets(first.state);
281
+ registerBuiltInWgpuModifierSnippets(second.state);
282
+ const material = createShadedMaterial();
283
+ const firstPipeline = ensureWgpuShadedPipeline(first.state, material, 'bgra8unorm');
284
+ const secondPipeline = ensureWgpuShadedPipeline(second.state, material, 'bgra8unorm');
285
+
286
+ const firstGroup = bindWgpuShadedSurface(first.state, firstPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
287
+ const firstBufferCount = first.fake.calls.filter((call) => call.name === 'createBuffer').length;
288
+ const firstGroupCount = first.fake.calls.filter((call) => call.name === 'createBindGroup').length;
289
+ const secondGroup = bindWgpuShadedSurface(second.state, secondPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
290
+ const firstAgain = bindWgpuShadedSurface(first.state, firstPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
291
+
292
+ expect(firstGroup).not.toBe(secondGroup);
293
+ expect(firstAgain).toBe(firstGroup);
294
+ expect(first.fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(firstBufferCount);
295
+ expect(first.fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(firstGroupCount);
296
+ });
297
+
298
+ it('reuses its uniform allocation while rebuilding for texture identity and readiness changes', () => {
299
+ const { fake, state } = makeWgpuScene3DState();
300
+ registerBuiltInWgpuModifierSnippets(state);
301
+ const first = createTexture({ image: { source: {} } as ImageResource });
302
+ const material = createShadedMaterial({ diffuseMap: first });
303
+ const pipeline = ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
304
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
305
+ const buffers = fake.calls.filter((call) => call.name === 'createBuffer').length;
306
+ const groups = fake.calls.filter((call) => call.name === 'createBindGroup').length;
307
+
308
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
309
+ expect(fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(buffers);
310
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(groups);
311
+
312
+ material.diffuseMap = createTexture({ image: { source: {} } as ImageResource });
313
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
314
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup').length).toBeGreaterThan(groups);
315
+ expect(fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(buffers);
316
+
317
+ const modifierTexture = createTexture({ image: makeImageResource() });
318
+ const emissive = createEmissiveModifier({ color: 0xffffffff, mask: modifierTexture, strength: 1 });
319
+ material.modifiers = [emissive];
320
+ const modifierPipeline = ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
321
+ bindWgpuShadedSurface(state, modifierPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
322
+ const beforeModifierSwap = fake.calls.filter((call) => call.name === 'createBindGroup').length;
323
+ emissive.mask = createTexture({ image: makeImageResource() });
324
+ bindWgpuShadedSurface(state, modifierPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
325
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup').length).toBeGreaterThan(beforeModifierSwap);
326
+
327
+ emissive.mask = createTexture();
328
+ bindWgpuShadedSurface(state, modifierPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
329
+ const beforeReady = fake.calls.filter((call) => call.name === 'createBindGroup').length;
330
+ const readyingTexture = emissive.mask;
331
+ expect(readyingTexture).toBeDefined();
332
+ readyingTexture!.image = { source: {} } as ImageResource;
333
+ bindWgpuShadedSurface(state, modifierPipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
334
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup').length).toBeGreaterThan(beforeReady);
335
+ });
336
+
337
+ it('rebinds when a ready image swaps or its version changes', () => {
338
+ const { fake, state } = makeWgpuScene3DState();
339
+ registerBuiltInWgpuModifierSnippets(state);
340
+ const firstImage = makeImageResource();
341
+ const texture = createTexture({ image: firstImage });
342
+ const material = createShadedMaterial({ diffuseMap: texture });
343
+ const pipeline = ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
344
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
345
+
346
+ const beforeSwap = fake.calls.filter((call) => call.name === 'createBindGroup').length;
347
+ texture.image = makeImageResource();
348
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
349
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup').length).toBeGreaterThan(beforeSwap);
350
+
351
+ const beforeVersion = fake.calls.filter((call) => call.name === 'createBindGroup').length;
352
+ texture.image.version++;
353
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
354
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup').length).toBeGreaterThan(beforeVersion);
355
+ });
356
+
357
+ it('reuses the compiled plan and GPU resource arrays on an unchanged draw', () => {
358
+ const { fake, state } = makeWgpuScene3DState();
359
+ let contributions = 0;
360
+ const modifier = { kind: 'acme.Stable', slot: ModifierSlot.Effect };
361
+ registerWgpuModifierSnippet(state, {
362
+ contribution: () => {
363
+ contributions++;
364
+ return { source: 'radiance = radiance;' };
365
+ },
366
+ kind: modifier.kind,
367
+ slot: modifier.slot,
368
+ });
369
+ const material = createShadedMaterial({ modifiers: [modifier] });
370
+ const pipeline = ensureWgpuShadedPipeline(state, material, 'bgra8unorm');
371
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
372
+ const buffers = fake.calls.filter((call) => call.name === 'createBuffer').length;
373
+ const groups = fake.calls.filter((call) => call.name === 'createBindGroup').length;
374
+ const compiled = contributions;
375
+
376
+ bindWgpuShadedSurface(state, pipeline, material, [1, 1, 1, 1], [1, 1, 1, 1]);
377
+ expect(contributions).toBe(compiled);
378
+ expect(fake.calls.filter((call) => call.name === 'createBuffer')).toHaveLength(buffers);
379
+ expect(fake.calls.filter((call) => call.name === 'createBindGroup')).toHaveLength(groups);
380
+ });
381
+ });
382
+
383
+ function makeImageResource(): ImageResource {
384
+ return {
385
+ alphaType: 'straight',
386
+ compressed: null,
387
+ data: null,
388
+ format: 'rgba8unorm',
389
+ height: 1,
390
+ source: {} as CanvasImageSource,
391
+ version: 0,
392
+ width: 1,
393
+ } as ImageResource;
394
+ }
@@ -0,0 +1,169 @@
1
+ import { configureDirectionalShadowCamera3D, createCamera3D } from '@flighthq/camera';
2
+ import { createAabb, createVector3 } from '@flighthq/geometry';
3
+ import { createAmbientLight, createDirectionalLight } from '@flighthq/lighting';
4
+ import { createStandardPbrMaterial } from '@flighthq/materials';
5
+ import { createBoxMeshGeometry } from '@flighthq/mesh';
6
+ import { addNodeChild } from '@flighthq/node';
7
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
8
+ import { createMesh, createNode3D, Node3DKind } from '@flighthq/scene3d';
9
+ import type { Camera3D, Scene3DLightsLike, Node3D } from '@flighthq/types';
10
+
11
+ import { drawWgpuScene3D } from './drawWgpuScene3D';
12
+ import { registerStandardPbrWgpuMaterial } from './registerStandardPbrWgpuMaterial';
13
+ import { buildWgpuPbrStandardDefineKey } from './standardPbrWgpuMeshMaterialRenderer';
14
+ import { getWgpuClassicModuleSourceForKey } from './wgpuClassicPrelude';
15
+ import { getWgpuPbrModuleSourceForKey } from './wgpuPbrPrelude';
16
+ import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
17
+ import { makeWgpuScene3DState } from './wgpuScene3DTestHelper';
18
+ import { destroyWgpuScene3DShadow, drawWgpuScene3DShadowMap } from './wgpuShadowMap';
19
+
20
+ const LIGHTS: Scene3DLightsLike = {
21
+ ambient: createAmbientLight({ color: 0xffffffff, intensity: 0.2 }),
22
+ directional: createDirectionalLight({ color: 0xffffffff, direction: createVector3(0, -1, -1), intensity: 1 }),
23
+ };
24
+
25
+ function makeShadowCamera(): Camera3D {
26
+ // The projection is overwritten by configureDirectionalShadowCamera3D (to orthographic); start perspective.
27
+ const camera = createCamera3D({
28
+ far: 10,
29
+ near: 0.1,
30
+ projection: { aspect: 1, fovY: Math.PI / 3, kind: 'perspective' },
31
+ });
32
+ configureDirectionalShadowCamera3D(camera, { x: 0, y: -1, z: -1 }, createAabb(-1, -1, -1, 1, 1, 1));
33
+ return camera;
34
+ }
35
+
36
+ function makeShadowScene3D(): Node3D {
37
+ const scene = createNode3D(Node3DKind);
38
+ addNodeChild(scene, createMesh(createBoxMeshGeometry(), []));
39
+ return scene;
40
+ }
41
+
42
+ describe('destroyWgpuScene3DShadow', () => {
43
+ it('destroys the shadow depth texture and clears the slot', () => {
44
+ const { state } = makeWgpuScene3DState();
45
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
46
+
47
+ const runtime = getWgpuScene3DRuntime(state);
48
+ expect(runtime.shadow).not.toBeNull();
49
+ let destroyed = false;
50
+ runtime.shadow!.depthTexture.destroy = () => {
51
+ destroyed = true;
52
+ };
53
+
54
+ destroyWgpuScene3DShadow(state);
55
+ expect(destroyed).toBe(true);
56
+ expect(runtime.shadow).toBeNull();
57
+ expect(runtime.shadowDepthPipeline).toBeNull();
58
+ });
59
+
60
+ it('is a no-op when no shadow was drawn', () => {
61
+ const { state } = makeWgpuScene3DState();
62
+ expect(() => destroyWgpuScene3DShadow(state)).not.toThrow();
63
+ });
64
+ });
65
+
66
+ describe('drawWgpuScene3DShadowMap', () => {
67
+ it('creates a sampleable depth32float shadow map and stores it on the runtime', () => {
68
+ const { fake, state } = makeWgpuScene3DState();
69
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
70
+
71
+ const depthCreate = fake.calls.find(
72
+ (c) => c.name === 'createTexture' && (c.args[0] as GPUTextureDescriptor).format === 'depth32float',
73
+ );
74
+ expect(depthCreate).toBeDefined();
75
+
76
+ const runtime = getWgpuScene3DRuntime(state);
77
+ expect(runtime.shadow).not.toBeNull();
78
+ expect(runtime.shadow!.matrix.m.some((v) => v !== 0)).toBe(true);
79
+ });
80
+
81
+ it('opens a depth-only pass and renders each caster mesh depth', () => {
82
+ const { fake, state } = makeWgpuScene3DState();
83
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
84
+
85
+ expect(fake.calls.some((c) => c.name === 'beginRenderPass')).toBe(true);
86
+ expect(fake.calls.some((c) => c.name === 'setPipeline')).toBe(true);
87
+ expect(fake.calls.some((c) => c.name === 'drawIndexed')).toBe(true);
88
+ expect(fake.calls.some((c) => c.name === 'end')).toBe(true);
89
+ });
90
+
91
+ it('compiles a vertex-only depth module with the GL->WebGPU depth remap', () => {
92
+ const { fake, state } = makeWgpuScene3DState();
93
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
94
+
95
+ const shaderCall = fake.calls.find(
96
+ (c) => c.name === 'createShaderModule' && String((c.args[0] as { code: string }).code).includes('draw.world'),
97
+ );
98
+ expect(shaderCall).toBeDefined();
99
+ const code = (shaderCall!.args[0] as { code: string }).code;
100
+ expect(code).toContain('clip.z = (clip.z + clip.w) * 0.5');
101
+ expect(code).not.toContain('fs_main');
102
+ });
103
+
104
+ it('reuses the shadow depth texture across frames', () => {
105
+ const { fake, state } = makeWgpuScene3DState();
106
+ const scene = makeShadowScene3D();
107
+ const camera = makeShadowCamera();
108
+ drawWgpuScene3DShadowMap(state, scene, camera);
109
+ drawWgpuScene3DShadowMap(state, scene, camera);
110
+ const depthCreates = fake.calls.filter(
111
+ (c) => c.name === 'createTexture' && (c.args[0] as GPUTextureDescriptor).format === 'depth32float',
112
+ ).length;
113
+ expect(depthCreates).toBe(1);
114
+ });
115
+
116
+ it('is a no-op when no command encoder is active', () => {
117
+ const { fake, state } = makeWgpuScene3DState();
118
+ getWgpuRenderStateRuntime(state).commandEncoder = null;
119
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
120
+ expect(fake.calls.some((c) => c.name === 'beginRenderPass')).toBe(false);
121
+ expect(getWgpuScene3DRuntime(state).shadow).toBeNull();
122
+ });
123
+
124
+ it('binds a group(3) shadow group on the lit PBR draw that follows', () => {
125
+ const { fake, state } = makeWgpuScene3DState();
126
+ registerStandardPbrWgpuMaterial(state);
127
+ drawWgpuScene3DShadowMap(state, makeShadowScene3D(), makeShadowCamera());
128
+
129
+ const scene = createNode3D(Node3DKind);
130
+ addNodeChild(scene, createMesh(createBoxMeshGeometry(), [createStandardPbrMaterial()]));
131
+ const camera = createCamera3D({
132
+ far: 100,
133
+ near: 0.1,
134
+ projection: { aspect: 1, fovY: Math.PI / 3, kind: 'perspective' },
135
+ });
136
+ drawWgpuScene3D(state, scene, camera, LIGHTS);
137
+
138
+ expect(fake.calls.some((c) => c.name === 'setBindGroup' && c.args[0] === 3)).toBe(true);
139
+ });
140
+ });
141
+
142
+ // The lit WGSL string surface: both PBR and classic declare the group(3) shadow bindings and apply the
143
+ // PCF comparison to their directional terms, mirroring scene-gl.
144
+ describe('wgpuPbrPrelude shadow sampling', () => {
145
+ it('declares the group(3) shadow bindings and PCF comparison', () => {
146
+ const code = getWgpuPbrModuleSourceForKey(buildWgpuPbrStandardDefineKey(null, null));
147
+ expect(code).toContain('@group(3) @binding(0) var<uniform> shadow');
148
+ expect(code).toContain('var shadowMap : texture_depth_2d');
149
+ expect(code).toContain('var shadowSampler : sampler_comparison');
150
+ expect(code).toContain('fn sampleDirectionalShadow');
151
+ expect(code).toContain('textureSampleCompareLevel(shadowMap, shadowSampler');
152
+ expect(code).toContain('direct * sampleDirectionalShadow(in.worldPosition)');
153
+ });
154
+
155
+ it('classic prelude samples the shadow map on the directional term (mirrors scene-gl classic)', () => {
156
+ const code = getWgpuClassicModuleSourceForKey({
157
+ alphaMaskEnabled: false,
158
+ doubleSided: false,
159
+ hasAlphaMap: false,
160
+ hasDiffuseMap: false,
161
+ hasNormalMap: false,
162
+ hasSpecularMap: false,
163
+ lightingModel: 'lambert',
164
+ });
165
+ expect(code).toContain('@group(3) @binding(1) var shadowMap : texture_depth_2d');
166
+ expect(code).toContain('fn sampleDirectionalShadow');
167
+ expect(code).toContain('direct * sampleDirectionalShadow(in.worldPosition)');
168
+ });
169
+ });