@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,1007 @@
1
+ import { getCamera3DViewProjectionMatrix4 } from '@flighthq/camera';
2
+ import { createMatrix3, createMatrix4, getMatrix4Position, inverseMatrix4 } from '@flighthq/geometry';
3
+ import { hasImageResourcePixels } from '@flighthq/image';
4
+ import { bindWgpuImageResourceTexture, getWgpuRenderStateRuntime, getWgpuSampler } from '@flighthq/render-wgpu';
5
+ import { getTextureUvMatrix, hasTextureUvTransform } from '@flighthq/texture';
6
+ import { MAX_FORWARD_LIGHTS, SCENE_LIGHT_HEMISPHERE_OFFSET, SCENE_LIGHT_HEMISPHERE_STRIDE, SCENE_LIGHT_POINT_OFFSET, SCENE_LIGHT_POINT_STRIDE, SCENE_LIGHT_SPOT_OFFSET, SCENE_LIGHT_SPOT_STRIDE, } from '@flighthq/types';
7
+ import { ensureWgpuMeshUpload } from './wgpuMeshUpload';
8
+ import { getWgpuScene3DRuntime } from './wgpuScene3DRuntime';
9
+ // Sets the family's pipeline active for the bind→draw handoff, binds it, and binds the shared Frame
10
+ // bind group at group(0). A family's bind() calls this after selecting its pipeline + writing the
11
+ // Frame uniform; draw() reads scene.activeMeshPipeline back. Mirrors scene-gl's beginGlMeshDraw.
12
+ export function beginWgpuMeshDraw(state, pipeline) {
13
+ const stateRuntime = getWgpuRenderStateRuntime(state);
14
+ const pass = stateRuntime.renderPass;
15
+ if (pass === null)
16
+ return;
17
+ const scene = getWgpuScene3DRuntime(state);
18
+ scene.activeMeshPipeline = pipeline;
19
+ pass.setPipeline(pipeline.pipeline);
20
+ pass.setBindGroup(0, scene.frameBindGroup);
21
+ // Lit families that PCF-sample the directional shadow map carry a group(3) shadow layout; bind the
22
+ // shared shadow-sample group (the real depth map when drawWgpuScene3DShadowMap ran this frame, else a
23
+ // 1x1 dummy gated off by the shadow uniform). Non-lit families have no group(3) and skip this.
24
+ if (pipeline.hasPbrSampleGroup) {
25
+ pass.setBindGroup(3, ensureWgpuPbrSampleBindGroup(state));
26
+ }
27
+ else if (pipeline.hasShadowGroup) {
28
+ pass.setBindGroup(3, ensureWgpuShadowSampleBindGroup(state));
29
+ }
30
+ // Legacy IBL-only layouts are still supported for callers that provide one directly.
31
+ if (pipeline.hasIblGroup) {
32
+ pass.setBindGroup(4, ensureWgpuIblSampleBindGroup(state));
33
+ }
34
+ }
35
+ // Builds a material bind group in the shared classic/PBR entry layout: the uniform buffer at binding 0,
36
+ // the sampler at binding 1, then each resolved map view at binding 2 + i. Both the classic and standard-
37
+ // PBR binders share this shape, so the same builder + the same rebuild check (below) cover both.
38
+ export function buildWgpuMaterialBindGroup(state, layout, buffer, sampler, views) {
39
+ const entries = [
40
+ { binding: 0, resource: { buffer } },
41
+ { binding: 1, resource: sampler },
42
+ ];
43
+ for (let i = 0; i < views.length; i++)
44
+ entries.push({ binding: 2 + i, resource: views[i] });
45
+ return state.device.createBindGroup({ layout, entries });
46
+ }
47
+ // Per-map material bind group: uniform at binding 0, N samplers at 1..N, then N matching texture
48
+ // views at N+1..2N. Keeping the two equal-length arrays parallel makes each WGSL map select its own
49
+ // Texture.sampler while preserving a fixed layout for absent maps via placeholder views/default samplers.
50
+ export function buildWgpuPerMapMaterialBindGroup(state, layout, buffer, samplers, views) {
51
+ const count = views.length;
52
+ const entries = [{ binding: 0, resource: { buffer } }];
53
+ for (let i = 0; i < count; i++)
54
+ entries.push({ binding: 1 + i, resource: samplers[i] });
55
+ for (let i = 0; i < count; i++)
56
+ entries.push({ binding: 1 + count + i, resource: views[i] });
57
+ return state.device.createBindGroup({ layout, entries });
58
+ }
59
+ // Builds a render pipeline for a family: compiles its WGSL module, and lays out [shared Frame, shared
60
+ // Draw, family Material] over the canonical 48-byte PBR vertex. Depth-stencil is depth24plus-stencil8,
61
+ // compare 'less', depth-write on (the scene pass owns depth; stencil inert); culling is back-face
62
+ // unless doubleSided. The family passes its own materialBindGroupLayout + entry points (default
63
+ // vs_main/fs_main). A lit family that PCF-samples the directional shadow map passes the shared
64
+ // group(3) `shadowBindGroupLayout` (ensureWgpuShadowSampleLayout), which extends the pipeline layout to
65
+ // [Frame, Draw, Material, Shadow] and flags the pipeline so beginWgpuMeshDraw binds group(3). A custom
66
+ // family may instead pass `extraBindGroupLayout`; it occupies group(3), but the family binds it itself.
67
+ export function createWgpuMeshPipeline(state, options) {
68
+ const device = state.device;
69
+ const layouts = ensureWgpuScene3DLayouts(state);
70
+ const skinning = getWgpuScene3DRuntime(state).skinningAdapter;
71
+ const bindGroupLayouts = [
72
+ layouts.frameBindGroupLayout,
73
+ options.skinned && skinning !== null ? skinning.getDrawLayout(state) : layouts.drawBindGroupLayout,
74
+ options.materialBindGroupLayout,
75
+ ];
76
+ if (options.extraBindGroupLayout !== undefined) {
77
+ bindGroupLayouts.push(options.extraBindGroupLayout);
78
+ }
79
+ if (options.extraBindGroupLayout === undefined && options.pbrSampleBindGroupLayout !== undefined) {
80
+ bindGroupLayouts.push(options.pbrSampleBindGroupLayout);
81
+ }
82
+ else if (options.extraBindGroupLayout === undefined) {
83
+ // Group order is positional: shadow (group 3) then IBL (group 4). Prefer pbrSampleBindGroupLayout
84
+ // for new PBR pipelines so they fit WebGPU's minimum maxBindGroups=4.
85
+ if (options.shadowBindGroupLayout !== undefined)
86
+ bindGroupLayouts.push(options.shadowBindGroupLayout);
87
+ if (options.iblBindGroupLayout !== undefined)
88
+ bindGroupLayouts.push(options.iblBindGroupLayout);
89
+ }
90
+ const layout = device.createPipelineLayout({ bindGroupLayouts });
91
+ const pipeline = device.createRenderPipeline({
92
+ layout,
93
+ vertex: {
94
+ module: options.module,
95
+ entryPoint: 'vs_main',
96
+ buffers: options.skinned && skinning !== null ? skinning.vertexBufferLayouts : VERTEX_BUFFER_LAYOUTS,
97
+ },
98
+ fragment: {
99
+ module: options.module,
100
+ entryPoint: 'fs_main',
101
+ targets: [
102
+ {
103
+ format: options.format,
104
+ blend: options.blended
105
+ ? {
106
+ color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
107
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
108
+ }
109
+ : undefined,
110
+ },
111
+ ],
112
+ },
113
+ primitive: {
114
+ topology: options.topology ?? 'triangle-list',
115
+ frontFace: 'ccw',
116
+ cullMode: options.doubleSided ? 'none' : 'back',
117
+ },
118
+ depthStencil: { format: DEPTH_STENCIL_FORMAT, depthWriteEnabled: !options.blended, depthCompare: 'less' },
119
+ });
120
+ return {
121
+ hasIblGroup: options.iblBindGroupLayout !== undefined,
122
+ hasPbrSampleGroup: options.pbrSampleBindGroupLayout !== undefined,
123
+ hasShadowGroup: options.shadowBindGroupLayout !== undefined,
124
+ materialBindGroupLayout: options.materialBindGroupLayout,
125
+ pipeline,
126
+ skinned: options.skinned === true,
127
+ };
128
+ }
129
+ // The shared per-draw tail for every mesh-material family: ring-allocates + writes the Draw uniform
130
+ // (world + normal matrix) for the proxy, lazily uploads the geometry's GPU buffers (cached by
131
+ // geometry.version), binds the dynamic-offset Draw group at group(1) + the vertex/index buffers, and
132
+ // issues the indexed draw over the proxy's subset. A family's draw() reads scene.activeMeshPipeline (set
133
+ // by beginWgpuMeshDraw) before calling this. Mirrors scene-gl's drawGlMeshSubset.
134
+ export function drawWgpuMeshSubset(state, proxy, geometry) {
135
+ const stateRuntime = getWgpuRenderStateRuntime(state);
136
+ const pass = stateRuntime.renderPass;
137
+ const scene = getWgpuScene3DRuntime(state);
138
+ if (pass === null || scene.activeMeshPipeline === null)
139
+ return;
140
+ const subset = proxy.subset;
141
+ if (subset.indexCount === 0)
142
+ return;
143
+ const upload = ensureWgpuMeshUpload(state, geometry, scene.activeMeshPipeline.skinned);
144
+ if (upload === null || upload.indexBuffer === null)
145
+ return;
146
+ const drawBindGroup = writeWgpuDrawUniform(state, proxy);
147
+ const activePipeline = scene.activeMeshPipeline;
148
+ const jointMatrices = proxy.jointMatrices ?? null;
149
+ const skinning = scene.skinningAdapter;
150
+ const boundDrawGroup = activePipeline.skinned && jointMatrices !== null && skinning !== null
151
+ ? skinning.getDrawBindGroup(state, jointMatrices)
152
+ : drawBindGroup;
153
+ _dynamicOffsets[0] = scene.pendingDrawOffset;
154
+ pass.setBindGroup(1, boundDrawGroup, _dynamicOffsets);
155
+ pass.setVertexBuffer(0, upload.vertexBuffer);
156
+ pass.setIndexBuffer(upload.indexBuffer, upload.indexFormat);
157
+ pass.drawIndexed(subset.indexCount, 1, subset.indexOffset, 0, 0);
158
+ }
159
+ // Resolves the shared Frame bind group, creating it from the shared Frame layout + Frame buffer on
160
+ // first use. Every family pipeline declares the same group(0) layout, so this one bind group is valid
161
+ // for all of them.
162
+ export function ensureWgpuFrameBindGroup(state) {
163
+ const scene = getWgpuScene3DRuntime(state);
164
+ if (scene.frameBuffer === null) {
165
+ scene.frameBuffer = state.device.createBuffer({
166
+ size: FRAME_UNIFORM_BYTES,
167
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
168
+ });
169
+ }
170
+ if (scene.frameBindGroup === null) {
171
+ scene.frameBindGroup = state.device.createBindGroup({
172
+ layout: ensureWgpuScene3DLayouts(state).frameBindGroupLayout,
173
+ entries: [{ binding: 0, resource: { buffer: scene.frameBuffer } }],
174
+ });
175
+ }
176
+ return scene.frameBindGroup;
177
+ }
178
+ // Resolves the shared IBL sample bind group a caller binds when its pipeline carries the
179
+ // legacy standalone IBL layout — the WGSL counterpart of scene-gl's IBL texture-unit + u_ibl* uniform binds in
180
+ // bindGlMeshLightBlock. Lazily creates the IBL uniform buffer (enabled/intensity/maxMip), a filtering
181
+ // sampler, and 1x1 dummy cube + 2D-LUT textures for the no-IBL case, then rewrites the uniform every call
182
+ // (from scene.ibl) and rebuilds the bind group only when the bound irradiance view changes (present ↔
183
+ // absent). A scene with no baked environment still renders: the dummy views are bound and the shader's
184
+ // `enabled < 0.5` gate keeps them unsampled — mirroring GL's u_iblEnabled = 0 placeholder path.
185
+ export function ensureWgpuIblSampleBindGroup(state) {
186
+ const scene = getWgpuScene3DRuntime(state);
187
+ const device = state.device;
188
+ if (scene.iblUniformBuffer === null) {
189
+ scene.iblUniformBuffer = device.createBuffer({
190
+ size: IBL_SAMPLE_UNIFORM_BYTES,
191
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
192
+ });
193
+ }
194
+ if (scene.iblSampler === null) {
195
+ // Trilinear so the prefiltered specular cube's roughness mip chain (textureSampleLevel) filters.
196
+ scene.iblSampler = device.createSampler({
197
+ magFilter: 'linear',
198
+ minFilter: 'linear',
199
+ mipmapFilter: 'linear',
200
+ });
201
+ }
202
+ if (scene.iblDummyCubeView === null) {
203
+ // 1x1 cube + 1x1 2D bound when no IBL is baked this frame; never sampled (the enabled flag gates them
204
+ // off), they only satisfy the texture_cube / texture_2d slots so the draw is valid.
205
+ scene.iblDummyCubeTexture = device.createTexture({
206
+ size: [1, 1, 6],
207
+ format: IBL_DUMMY_FORMAT,
208
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
209
+ });
210
+ scene.iblDummyCubeView = scene.iblDummyCubeTexture.createView({ dimension: 'cube' });
211
+ scene.iblDummyLutTexture = device.createTexture({
212
+ size: [1, 1, 1],
213
+ format: IBL_DUMMY_FORMAT,
214
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
215
+ });
216
+ scene.iblDummyLutView = scene.iblDummyLutTexture.createView();
217
+ }
218
+ const ibl = scene.ibl;
219
+ const u = _iblSampleScratch;
220
+ if (ibl !== null) {
221
+ u[0] = 1; // enabled
222
+ u[1] = ibl.intensity;
223
+ u[2] = ibl.prefilteredMipCount - 1; // maxMip (roughness 1.0)
224
+ }
225
+ else {
226
+ u[0] = 0;
227
+ u[1] = 1;
228
+ u[2] = 0;
229
+ }
230
+ u[3] = 0;
231
+ device.queue.writeBuffer(scene.iblUniformBuffer, 0, u.buffer, 0, IBL_SAMPLE_UNIFORM_BYTES);
232
+ const irradianceView = ibl !== null ? ibl.irradianceCubeView : scene.iblDummyCubeView;
233
+ const prefilteredView = ibl !== null ? ibl.prefilteredCubeView : scene.iblDummyCubeView;
234
+ const brdfView = ibl !== null ? ibl.brdfLutView : scene.iblDummyLutView;
235
+ if (scene.iblSampleBindGroup === null || scene.iblSampleCubeView !== irradianceView) {
236
+ scene.iblSampleBindGroup = device.createBindGroup({
237
+ layout: ensureWgpuIblSampleLayout(state),
238
+ entries: [
239
+ { binding: 0, resource: { buffer: scene.iblUniformBuffer } },
240
+ { binding: 1, resource: irradianceView },
241
+ { binding: 2, resource: prefilteredView },
242
+ { binding: 3, resource: brdfView },
243
+ { binding: 4, resource: scene.iblSampler },
244
+ ],
245
+ });
246
+ scene.iblSampleCubeView = irradianceView;
247
+ }
248
+ return scene.iblSampleBindGroup;
249
+ }
250
+ // Resolves the shared standalone IBL sample bind-group layout (uniform enabled/intensity/maxMip, a diffuse
251
+ // irradiance cube, a prefiltered specular cube, a 2D BRDF LUT, and a filtering sampler), created once per
252
+ // state. Lit PBR pipelines pass this to createWgpuMeshPipeline; the shared bind group built by
253
+ // ensureWgpuIblSampleBindGroup targets it, so one IBL bind group serves every lit PBR pipeline variant.
254
+ export function ensureWgpuIblSampleLayout(state) {
255
+ const scene = getWgpuScene3DRuntime(state);
256
+ if (scene.iblSampleLayout === null) {
257
+ scene.iblSampleLayout = state.device.createBindGroupLayout({
258
+ entries: [
259
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
260
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
261
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
262
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
263
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
264
+ ],
265
+ });
266
+ }
267
+ return scene.iblSampleLayout;
268
+ }
269
+ // Fetches (or on first use creates) the per-material binding cached under `key`, rebuilding its bind
270
+ // group ONLY when the primary sampler or a resolved map view differs from the cached set. The hot path
271
+ // (an unchanged material re-bound every frame) allocates nothing: the caller fills a REUSED `views`
272
+ // scratch, this compares it against the binding's owned view array in place, and returns the cached
273
+ // bind group untouched. On a create OR a rebuild — both change events, not steady state —
274
+ // buildWgpuMaterialBindGroup (re)allocates the GPUBindGroupEntry array; the binding's own view-array
275
+ // copy is allocated only on create (or a view-count change), and a rebuild reuses the buffer and
276
+ // overwrites the owned view array in place. This is the shared no-hidden-allocation cache both the
277
+ // classic and standard-PBR binders route through. The caller still writes the uniform buffer + stashes
278
+ // the uv transform after this returns.
279
+ export function ensureWgpuMaterialBinding(state, key, layout, uniformByteSize, sampler, views) {
280
+ const scene = getWgpuScene3DRuntime(state);
281
+ let binding = scene.materialBindGroups.get(key);
282
+ if (binding === undefined) {
283
+ const buffer = state.device.createBuffer({
284
+ size: uniformByteSize,
285
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
286
+ });
287
+ binding = {
288
+ bindGroup: buildWgpuMaterialBindGroup(state, layout, buffer, sampler, views),
289
+ buffer,
290
+ sampler,
291
+ views: views.slice(),
292
+ };
293
+ scene.materialBindGroups.set(key, binding);
294
+ }
295
+ else if (isWgpuMaterialBindGroupRebuildNeeded(binding, sampler, views)) {
296
+ binding.bindGroup = buildWgpuMaterialBindGroup(state, layout, binding.buffer, sampler, views);
297
+ binding.sampler = sampler;
298
+ // Overwrite the binding-owned view array in place — never alias the reused caller scratch, which the
299
+ // next material's bind will overwrite.
300
+ const cached = binding.views;
301
+ if (cached === undefined || cached.length !== views.length) {
302
+ binding.views = views.slice();
303
+ }
304
+ else {
305
+ for (let i = 0; i < views.length; i++)
306
+ cached[i] = views[i];
307
+ }
308
+ }
309
+ return binding;
310
+ }
311
+ // Resolves the combined PBR sample bind group. WebGPU's required maxBindGroups minimum is 4, so PBR
312
+ // cannot afford separate shadow group(3) and IBL group(4). This layout packs both into group(3).
313
+ export function ensureWgpuPbrSampleBindGroup(state) {
314
+ const scene = getWgpuScene3DRuntime(state);
315
+ const device = state.device;
316
+ if (scene.shadowUniformBuffer === null) {
317
+ scene.shadowUniformBuffer = device.createBuffer({
318
+ size: SHADOW_SAMPLE_UNIFORM_BYTES,
319
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
320
+ });
321
+ }
322
+ if (scene.shadowComparisonSampler === null) {
323
+ scene.shadowComparisonSampler = device.createSampler({ compare: 'less-equal' });
324
+ }
325
+ if (scene.shadowDummyView === null) {
326
+ scene.shadowDummyTexture = device.createTexture({
327
+ size: [1, 1, 1],
328
+ format: SHADOW_DEPTH_FORMAT,
329
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
330
+ });
331
+ scene.shadowDummyView = scene.shadowDummyTexture.createView();
332
+ }
333
+ if (scene.iblUniformBuffer === null) {
334
+ scene.iblUniformBuffer = device.createBuffer({
335
+ size: IBL_SAMPLE_UNIFORM_BYTES,
336
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
337
+ });
338
+ }
339
+ if (scene.iblSampler === null) {
340
+ scene.iblSampler = device.createSampler({
341
+ magFilter: 'linear',
342
+ minFilter: 'linear',
343
+ mipmapFilter: 'linear',
344
+ });
345
+ }
346
+ if (scene.iblDummyCubeView === null) {
347
+ scene.iblDummyCubeTexture = device.createTexture({
348
+ size: [1, 1, 6],
349
+ format: IBL_DUMMY_FORMAT,
350
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
351
+ });
352
+ scene.iblDummyCubeView = scene.iblDummyCubeTexture.createView({ dimension: 'cube' });
353
+ scene.iblDummyLutTexture = device.createTexture({
354
+ size: [1, 1, 1],
355
+ format: IBL_DUMMY_FORMAT,
356
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
357
+ });
358
+ scene.iblDummyLutView = scene.iblDummyLutTexture.createView();
359
+ }
360
+ const shadow = scene.shadow;
361
+ const s = _shadowSampleScratch;
362
+ if (shadow !== null) {
363
+ const m = shadow.matrix.m;
364
+ for (let i = 0; i < 16; i++)
365
+ s[i] = m[i];
366
+ s[16] = 1;
367
+ }
368
+ else {
369
+ for (let i = 0; i < 16; i++)
370
+ s[i] = 0;
371
+ s[0] = 1;
372
+ s[5] = 1;
373
+ s[10] = 1;
374
+ s[15] = 1;
375
+ s[16] = 0;
376
+ }
377
+ s[17] = 0;
378
+ s[18] = 0;
379
+ s[19] = 0;
380
+ device.queue.writeBuffer(scene.shadowUniformBuffer, 0, s.buffer, 0, SHADOW_SAMPLE_UNIFORM_BYTES);
381
+ const ibl = scene.ibl;
382
+ const u = _iblSampleScratch;
383
+ if (ibl !== null) {
384
+ u[0] = 1;
385
+ u[1] = ibl.intensity;
386
+ u[2] = ibl.prefilteredMipCount - 1;
387
+ }
388
+ else {
389
+ u[0] = 0;
390
+ u[1] = 1;
391
+ u[2] = 0;
392
+ }
393
+ u[3] = 0;
394
+ device.queue.writeBuffer(scene.iblUniformBuffer, 0, u.buffer, 0, IBL_SAMPLE_UNIFORM_BYTES);
395
+ const shadowView = shadow !== null ? shadow.depthView : scene.shadowDummyView;
396
+ const irradianceView = ibl !== null ? ibl.irradianceCubeView : scene.iblDummyCubeView;
397
+ const prefilteredView = ibl !== null ? ibl.prefilteredCubeView : scene.iblDummyCubeView;
398
+ const brdfView = ibl !== null ? ibl.brdfLutView : scene.iblDummyLutView;
399
+ if (scene.pbrSampleBindGroup === null ||
400
+ scene.pbrSampleShadowView !== shadowView ||
401
+ scene.pbrSampleIblCubeView !== irradianceView) {
402
+ scene.pbrSampleBindGroup = device.createBindGroup({
403
+ layout: ensureWgpuPbrSampleLayout(state),
404
+ entries: [
405
+ { binding: 0, resource: { buffer: scene.shadowUniformBuffer } },
406
+ { binding: 1, resource: shadowView },
407
+ { binding: 2, resource: scene.shadowComparisonSampler },
408
+ { binding: 3, resource: { buffer: scene.iblUniformBuffer } },
409
+ { binding: 4, resource: irradianceView },
410
+ { binding: 5, resource: prefilteredView },
411
+ { binding: 6, resource: brdfView },
412
+ { binding: 7, resource: scene.iblSampler },
413
+ ],
414
+ });
415
+ scene.pbrSampleShadowView = shadowView;
416
+ scene.pbrSampleIblCubeView = irradianceView;
417
+ }
418
+ return scene.pbrSampleBindGroup;
419
+ }
420
+ export function ensureWgpuPbrSampleLayout(state) {
421
+ const scene = getWgpuScene3DRuntime(state);
422
+ if (scene.pbrSampleLayout === null) {
423
+ scene.pbrSampleLayout = state.device.createBindGroupLayout({
424
+ entries: [
425
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
426
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
427
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
428
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
429
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
430
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float', viewDimension: 'cube' } },
431
+ { binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
432
+ { binding: 7, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
433
+ ],
434
+ });
435
+ }
436
+ return scene.pbrSampleLayout;
437
+ }
438
+ export function ensureWgpuPerMapMaterialBinding(state, key, layout, uniformByteSize, samplers, views) {
439
+ const scene = getWgpuScene3DRuntime(state);
440
+ let binding = scene.materialBindGroups.get(key);
441
+ if (binding === undefined) {
442
+ const buffer = state.device.createBuffer({
443
+ size: uniformByteSize,
444
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
445
+ });
446
+ binding = {
447
+ bindGroup: buildWgpuPerMapMaterialBindGroup(state, layout, buffer, samplers, views),
448
+ buffer,
449
+ samplers: samplers.slice(),
450
+ views: views.slice(),
451
+ };
452
+ scene.materialBindGroups.set(key, binding);
453
+ }
454
+ else if (wgpuPerMapMaterialBindGroupNeedsRebuild(binding, samplers, views)) {
455
+ binding.bindGroup = buildWgpuPerMapMaterialBindGroup(state, layout, binding.buffer, samplers, views);
456
+ overwriteIdentityCache(binding, 'samplers', samplers);
457
+ overwriteIdentityCache(binding, 'views', views);
458
+ }
459
+ return binding;
460
+ }
461
+ // The one-time opaque-white 1x1 RGBA texture view bound to a family's map slots in the untextured
462
+ // path, so a material bind-group layout that declares texture slots can be satisfied without uploading
463
+ // real maps. Shared across families (cached on the scene runtime).
464
+ export function ensureWgpuPlaceholderTextureView(state) {
465
+ const scene = getWgpuScene3DRuntime(state);
466
+ let view = scene.placeholderView;
467
+ if (view === null) {
468
+ const texture = state.device.createTexture({
469
+ size: [1, 1, 1],
470
+ format: 'rgba8unorm',
471
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
472
+ });
473
+ state.device.queue.writeTexture({ texture }, WHITE_PIXEL, { bytesPerRow: 4 }, [1, 1, 1]);
474
+ view = texture.createView();
475
+ scene.placeholderView = view;
476
+ }
477
+ return view;
478
+ }
479
+ // Resolves the shared group(0) Frame + group(1) Draw bind-group layouts, creating them once per state.
480
+ // group(0) is a single uniform visible to both stages (camera + lights); group(1) is a dynamic-offset
481
+ // uniform visible to the vertex scene2d (per-draw world + normal matrix).
482
+ export function ensureWgpuScene3DLayouts(state) {
483
+ const scene = getWgpuScene3DRuntime(state);
484
+ if (scene.frameBindGroupLayout === null || scene.drawBindGroupLayout === null) {
485
+ const device = state.device;
486
+ scene.frameBindGroupLayout = device.createBindGroupLayout({
487
+ entries: [
488
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
489
+ ],
490
+ });
491
+ scene.drawBindGroupLayout = device.createBindGroupLayout({
492
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform', hasDynamicOffset: true } }],
493
+ });
494
+ }
495
+ return { drawBindGroupLayout: scene.drawBindGroupLayout, frameBindGroupLayout: scene.frameBindGroupLayout };
496
+ }
497
+ // Resolves a compiled pipeline for a string cache key, compiling it via the factory on first use and
498
+ // caching it on the scene runtime's per-state pipelineCache. Every family routes its pipeline through
499
+ // this one cache; the key is namespaced by family + define key + color format (for example
500
+ // `unlit:bgra8unorm|-c-`), so families and feature/format variants compile at most once and never
501
+ // collide. Mirrors scene-gl's ensureGlScene3DProgram.
502
+ export function ensureWgpuScene3DPipeline(state, key, compile) {
503
+ const runtime = getWgpuScene3DRuntime(state);
504
+ const blended = runtime.activeBlendedRun;
505
+ const skinned = runtime.activeSkinnedRun;
506
+ const variantKey = `${key}|${blended ? 'blend' : 'opaque'}|${skinned ? 'skin' : 'rigid'}`;
507
+ let pipeline = runtime.pipelineCache.get(variantKey);
508
+ if (pipeline === undefined) {
509
+ pipeline = compile(blended, skinned);
510
+ runtime.pipelineCache.set(variantKey, pipeline);
511
+ }
512
+ return pipeline;
513
+ }
514
+ // Resolves the shared group(3) shadow-sample bind group a lit family binds when its pipeline carries the
515
+ // shadow layout — the WGSL counterpart of scene-gl's shadow texture-unit + u_shadow* uniform binds in
516
+ // bindGlMeshLightBlock. Lazily creates the shadow uniform buffer (light matrix + enabled flag), the
517
+ // comparison sampler ('less-equal', matching the GL PCF's `current <= closest`), and a 1x1 dummy depth
518
+ // texture for the no-shadow case, then rewrites the uniform every call (matrix + enabled from
519
+ // scene.shadow) and rebuilds the bind group only when the bound depth view changes (present ↔ absent).
520
+ // A shadow-less scene still renders: the dummy view is bound and the shader's `enabled < 0.5` early-out
521
+ // keeps it unsampled — mirroring GL's u_shadowEnabled = 0 path.
522
+ export function ensureWgpuShadowSampleBindGroup(state) {
523
+ const scene = getWgpuScene3DRuntime(state);
524
+ const device = state.device;
525
+ if (scene.shadowUniformBuffer === null) {
526
+ scene.shadowUniformBuffer = device.createBuffer({
527
+ size: SHADOW_SAMPLE_UNIFORM_BYTES,
528
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
529
+ });
530
+ }
531
+ if (scene.shadowComparisonSampler === null) {
532
+ scene.shadowComparisonSampler = device.createSampler({ compare: 'less-equal' });
533
+ }
534
+ if (scene.shadowDummyView === null) {
535
+ // A 1x1 sampleable depth texture bound when no shadow map exists this frame; never actually sampled
536
+ // (the enabled flag gates it off), it only satisfies the group(3) texture_depth_2d slot.
537
+ scene.shadowDummyTexture = device.createTexture({
538
+ size: [1, 1, 1],
539
+ format: SHADOW_DEPTH_FORMAT,
540
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
541
+ });
542
+ scene.shadowDummyView = scene.shadowDummyTexture.createView();
543
+ }
544
+ const shadow = scene.shadow;
545
+ const s = _shadowSampleScratch;
546
+ if (shadow !== null) {
547
+ const m = shadow.matrix.m;
548
+ for (let i = 0; i < 16; i++)
549
+ s[i] = m[i];
550
+ s[16] = 1; // enabled
551
+ }
552
+ else {
553
+ for (let i = 0; i < 16; i++)
554
+ s[i] = 0;
555
+ s[0] = 1;
556
+ s[5] = 1;
557
+ s[10] = 1;
558
+ s[15] = 1; // identity
559
+ s[16] = 0; // disabled
560
+ }
561
+ s[17] = 0;
562
+ s[18] = 0;
563
+ s[19] = 0;
564
+ device.queue.writeBuffer(scene.shadowUniformBuffer, 0, s.buffer, 0, SHADOW_SAMPLE_UNIFORM_BYTES);
565
+ const view = shadow !== null ? shadow.depthView : scene.shadowDummyView;
566
+ if (scene.shadowSampleBindGroup === null || scene.shadowSampleView !== view) {
567
+ scene.shadowSampleBindGroup = device.createBindGroup({
568
+ layout: ensureWgpuShadowSampleLayout(state),
569
+ entries: [
570
+ { binding: 0, resource: { buffer: scene.shadowUniformBuffer } },
571
+ { binding: 1, resource: view },
572
+ { binding: 2, resource: scene.shadowComparisonSampler },
573
+ ],
574
+ });
575
+ scene.shadowSampleView = view;
576
+ }
577
+ return scene.shadowSampleBindGroup;
578
+ }
579
+ // Resolves the shared group(3) shadow-sample bind-group layout (uniform light matrix + enabled flag, a
580
+ // depth texture, and a comparison sampler), created once per state. Lit pipelines pass this to
581
+ // createWgpuMeshPipeline; the shared bind group built by ensureWgpuShadowSampleBindGroup targets it, so
582
+ // one shadow bind group serves every lit family's pipeline (pbr today).
583
+ export function ensureWgpuShadowSampleLayout(state) {
584
+ const scene = getWgpuScene3DRuntime(state);
585
+ if (scene.shadowSampleLayout === null) {
586
+ scene.shadowSampleLayout = state.device.createBindGroupLayout({
587
+ entries: [
588
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
589
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
590
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
591
+ ],
592
+ });
593
+ }
594
+ return scene.shadowSampleLayout;
595
+ }
596
+ // Selects the ONE GPUSampler a material bind group uses, from its PRIMARY map's full sampler descriptor
597
+ // (the diffuse map for classic, the base-color map for PBR): wrap (a tiling repeat/mirror-repeat map
598
+ // gets the matching cached sampler so setTextureUvScale tiles), min/mag filter, a mip filter when the
599
+ // map requests a mip chain (paired with the mip chain generateWgpuMipmaps builds on upload), and
600
+ // anisotropy. A null/absent primary map falls back to the shared clamp sampler.
601
+ //
602
+ // SHARED-PRIMARY-SAMPLER CONTRACT (a deliberate wgpu↔GL divergence): the group(2) material layout carries
603
+ // a single sampler at binding 1 that the WGSL samples EVERY map (diffuse/specular/normal/alpha; base-
604
+ // color/metallic-roughness/normal/occlusion/emissive/alpha) through. So a non-primary map's per-Texture
605
+ // sampler is NOT honored on wgpu — only the primary map's. scene-gl, by contrast, binds each map with
606
+ // its own `map.sampler` (bindGlImageResourceTexture). This keeps the wgpu bind group to one sampler; a
607
+ // per-map-sampler wgpu path (one sampler binding per map) is a chartered later change, not a bug. The
608
+ // invalidation cache tracks this one sampler accordingly (see isWgpuMaterialBindGroupRebuildNeeded).
609
+ // Because a GPUSampler is immutable and baked into the cached bind group, this reads the descriptor at
610
+ // bind-group creation — the same lifetime as the resolved texture views.
611
+ export function getWgpuMaterialSampler(state, texture) {
612
+ if (texture === null)
613
+ return getWgpuRenderStateRuntime(state).linearSampler;
614
+ const sampler = texture.sampler;
615
+ const filter = sampler.magFilter.startsWith('nearest') ? 'nearest' : 'linear';
616
+ const useMips = sampler.mipmaps && sampler.minFilter !== 'linear' && sampler.minFilter !== 'nearest';
617
+ const mipmapFilter = useMips
618
+ ? sampler.minFilter.endsWith('nearest')
619
+ ? 'nearest'
620
+ : 'linear'
621
+ : undefined;
622
+ return getWgpuSampler(state, filter, sampler.wrapU, sampler.wrapV, mipmapFilter, sampler.anisotropy);
623
+ }
624
+ // Produces the distinct HAS_SKIN vertex variant. The palette stays in group(1), beside Draw, so lit
625
+ // pipelines still fit WebGPU's minimum four bind groups. Rigid source carries neither skin attributes
626
+ // nor the palette binding.
627
+ export function getWgpuMeshPreludeWgsl(skinned, skinning = null) {
628
+ return skinned && skinning !== null ? skinning.extendMeshPrelude(WGPU_MESH_PRELUDE_WGSL) : WGPU_MESH_PRELUDE_WGSL;
629
+ }
630
+ // Whether a cached material binding must rebuild its GPUBindGroup because a freshly-resolved view or the
631
+ // primary sampler no longer matches what it was built from. `resolveWgpuMaterialTextureView` is the
632
+ // invalidation seam: a map swap, an unready→ready transition, a ready→ready image replacement, or an
633
+ // ImageResource version bump each yield a different view identity, so identity comparison alone catches
634
+ // every live material-map mutation with no parallel epoch bookkeeping. `sampler` is the shared primary
635
+ // sampler (getWgpuMaterialSampler of the diffuse/base-color map), so a change to the primary map's
636
+ // sampler trips a rebuild; a non-primary map's per-Texture sampler is not bound and does not (the
637
+ // shared-primary-sampler contract). A binding with no cached views always rebuilds. Allocation-free —
638
+ // callers pass a reused scratch as `views`.
639
+ export function isWgpuMaterialBindGroupRebuildNeeded(binding, sampler, views) {
640
+ if (binding.sampler !== sampler)
641
+ return true;
642
+ const cached = binding.views;
643
+ if (cached === undefined || cached.length !== views.length)
644
+ return true;
645
+ for (let i = 0; i < views.length; i++) {
646
+ if (cached[i] !== views[i])
647
+ return true;
648
+ }
649
+ return false;
650
+ }
651
+ // True when a material map texture is present AND carries GPU-uploadable pixels — an element or a data-only
652
+ // generated Surface. Families call this to decide the `has*Map` define flag — the textured pipeline variant
653
+ // compiles only when the map can actually be sampled, so an empty texture renders the untextured path.
654
+ // Mirrors the `map !== null && map.image !== null && hasImageResourcePixels(map.image)` guard in the GL renderers.
655
+ export function isWgpuTextureReady(texture) {
656
+ return texture !== null && texture.image !== null && hasImageResourcePixels(texture.image);
657
+ }
658
+ // Resolves the GPUTextureView a family binds into a material map slot: the real uploaded view when the
659
+ // texture carries pixels (cached per state by render-wgpu's resource texture cache), otherwise the shared
660
+ // 1x1 opaque-white placeholder so the bind-group layout's texture slot is always satisfied. The single
661
+ // texture-resolution seam every scene-wgpu family routes its maps through — the WGSL mirror of scene-gl's
662
+ // `bindGlImageResourceTexture(state, map.image)` / unbound-attribute fallback.
663
+ export function resolveWgpuMaterialTextureView(state, texture) {
664
+ if (texture !== null && texture.image !== null && hasImageResourcePixels(texture.image)) {
665
+ // Request a mip chain when the map's sampler asks for mipmaps, so getWgpuMaterialSampler's mip
666
+ // filter has levels to sample; the shared placeholder and 2D path stay single-level.
667
+ return bindWgpuImageResourceTexture(state, texture.image, texture.sampler.mipmaps).view;
668
+ }
669
+ return ensureWgpuPlaceholderTextureView(state);
670
+ }
671
+ // Stashes a material's primary-texture uv transform for the next writeWgpuDrawUniform to fold into the
672
+ // shared Draw uniform. A family's bind() calls this with its base/diffuse map; writeWgpuDrawUniform
673
+ // consumes and resets the stash so a following draw whose family does not stash gets the untiled uv.
674
+ // @flighthq/texture composes the KHR transform column-major, the layout WGSL reads, matching the CPU
675
+ // transformTextureUv reference. A null / identity / unbound texture leaves the stash at identity (the
676
+ // vs_main multiply then reproduces the raw uv).
677
+ export function stashWgpuUvTransform(state, texture) {
678
+ const out = getWgpuScene3DRuntime(state).pendingUvTransform;
679
+ if (texture === null || ('image' in texture && texture.image === null) || !hasTextureUvTransform(texture)) {
680
+ resetWgpuUvTransformStash(out);
681
+ return;
682
+ }
683
+ getTextureUvMatrix(scratchUvMatrix, texture);
684
+ const m = scratchUvMatrix.m;
685
+ for (let i = 0; i < 9; i++)
686
+ out[i] = m[i];
687
+ }
688
+ export function wgpuPerMapMaterialBindGroupNeedsRebuild(binding, samplers, views) {
689
+ if (samplers.length !== views.length)
690
+ return true;
691
+ const cachedSamplers = binding.samplers;
692
+ const cachedViews = binding.views;
693
+ if (cachedSamplers === undefined ||
694
+ cachedViews === undefined ||
695
+ cachedSamplers.length !== samplers.length ||
696
+ cachedViews.length !== views.length) {
697
+ return true;
698
+ }
699
+ for (let i = 0; i < views.length; i++) {
700
+ if (cachedSamplers[i] !== samplers[i] || cachedViews[i] !== views[i])
701
+ return true;
702
+ }
703
+ return false;
704
+ }
705
+ function overwriteIdentityCache(binding, key, values) {
706
+ const cached = binding[key];
707
+ if (cached === undefined || cached.length !== values.length) {
708
+ binding[key] = values.slice();
709
+ return;
710
+ }
711
+ for (let i = 0; i < values.length; i++)
712
+ cached[i] = values[i];
713
+ }
714
+ // Allocates a draw slot from the render-state's uniform ring buffer, writes the Draw uniform (world
715
+ // mat4x4f + normal mat3x3f padded to std140) into it, records the slot's byte offset on the scene
716
+ // runtime (the draw path passes it as the bind group's dynamic offset), and returns the shared
717
+ // dynamic-offset Draw bind group. Reusing the render-state ring keeps each subset draw to one ring
718
+ // slot, not a fresh buffer; submitWgpuRenderPass uploads the used ring region before submit. Mirrors
719
+ // the per-draw model/normal upload in scene-gl's drawGlMeshSubset.
720
+ export function writeWgpuDrawUniform(state, proxy) {
721
+ const scene = getWgpuScene3DRuntime(state);
722
+ const stateRuntime = getWgpuRenderStateRuntime(state);
723
+ if (scene.drawBindGroup === null) {
724
+ scene.drawBindGroup = state.device.createBindGroup({
725
+ layout: ensureWgpuScene3DLayouts(state).drawBindGroupLayout,
726
+ entries: [{ binding: 0, resource: { buffer: stateRuntime.uniformBuffer, size: DRAW_UNIFORM_BYTES } }],
727
+ });
728
+ }
729
+ const offset = stateRuntime.uniformOffset;
730
+ const floatOffset = offset / 4;
731
+ const u = stateRuntime.uniformData;
732
+ const world = proxy.worldMatrix.m;
733
+ for (let i = 0; i < 16; i++)
734
+ u[floatOffset + i] = world[i];
735
+ // mat3x3f normal matrix: three vec3 columns each padded to vec4 (std140) → floats 16..27.
736
+ const n = proxy.normalMatrix.m;
737
+ u[floatOffset + 16] = n[0];
738
+ u[floatOffset + 17] = n[1];
739
+ u[floatOffset + 18] = n[2];
740
+ u[floatOffset + 19] = 0;
741
+ u[floatOffset + 20] = n[3];
742
+ u[floatOffset + 21] = n[4];
743
+ u[floatOffset + 22] = n[5];
744
+ u[floatOffset + 23] = 0;
745
+ u[floatOffset + 24] = n[6];
746
+ u[floatOffset + 25] = n[7];
747
+ u[floatOffset + 26] = n[8];
748
+ u[floatOffset + 27] = 0;
749
+ // mat3x3f uv transform: three vec3 columns each padded to vec4 (std140) → floats 28..39. The stash
750
+ // (set by a family's bind() via stashWgpuUvTransform) is already column-major and PERSISTS across
751
+ // draws — read, never reset here. drawWgpuScene3D binds once per material then draws many meshes, so the
752
+ // transform must survive every draw under one bind, mirroring the persistent GL u_uvTransform uniform.
753
+ // Every family's bind stashes authoritatively (its map, or identity for non-texturing families), so
754
+ // switching materials always re-establishes the correct value and no stale transform leaks forward.
755
+ const uv = scene.pendingUvTransform;
756
+ u[floatOffset + 28] = uv[0];
757
+ u[floatOffset + 29] = uv[1];
758
+ u[floatOffset + 30] = uv[2];
759
+ u[floatOffset + 31] = 0;
760
+ u[floatOffset + 32] = uv[3];
761
+ u[floatOffset + 33] = uv[4];
762
+ u[floatOffset + 34] = uv[5];
763
+ u[floatOffset + 35] = 0;
764
+ u[floatOffset + 36] = uv[6];
765
+ u[floatOffset + 37] = uv[7];
766
+ u[floatOffset + 38] = uv[8];
767
+ u[floatOffset + 39] = 0;
768
+ u[floatOffset + 40] = proxy.alpha ?? 1;
769
+ u[floatOffset + 41] = 0;
770
+ u[floatOffset + 42] = 0;
771
+ u[floatOffset + 43] = 0;
772
+ scene.pendingDrawOffset = offset;
773
+ stateRuntime.uniformOffset += stateRuntime.uniformStride;
774
+ return scene.drawBindGroup;
775
+ }
776
+ // The shared WGSL prelude every family module prepends after its const-flag block: the Frame + Draw
777
+ // uniform structs and their group(0)/group(1) bindings, the VertexOutput, the vs_main entry, and the
778
+ // srgbToLinear helper. A family appends its own group(2) Material block + fs_main. Keeping the Frame/
779
+ // Draw structs here keeps them in lockstep with writeWgpuFrameUniform / writeWgpuDrawUniform. Mirrors
780
+ // scene-gl's shared vertex body + GL_MESH_LIGHT_BLOCK_GLSL.
781
+ export const WGPU_MESH_PRELUDE_WGSL = /* wgsl */ `
782
+ const PI : f32 = 3.14159265359;
783
+ const MAX_FORWARD_LIGHTS : u32 = 4u;
784
+
785
+ struct Frame {
786
+ viewProjection : mat4x4f,
787
+ cameraPosition : vec4f,
788
+ lightDirection : vec4f, // xyz = directional light travel direction; w = directionalCount
789
+ directionalRadiance : vec4f, // rgb = linear premultiplied radiance
790
+ ambientRadiance : vec4f, // rgb = linear premultiplied radiance; w = ambientCount
791
+ view : mat4x4f, // camera view matrix; rotates world normals into view space (matcap)
792
+ // Punctual light arrays — layout mirrors Scene3DLightBlock.data (packScene3DLightBlock).
793
+ // point[i] = pointLights[i*2+0]={pos.xyz,range}, [i*2+1]={radiance.rgb,invSqrRange}
794
+ // spot[i] = spotLights[i*4+0..1] as point, [i*4+2]={dir.xyz,_}, [i*4+3]={cosInner,cosOuter,_,_}
795
+ // hemisphere[i] = hemisphereLights[i*3+0]={sky.rgb,_}, [i*3+1]={ground.rgb,_}, [i*3+2]={up.xyz,_}
796
+ pointLights : array<vec4f, 8>, // MAX_FORWARD_LIGHTS * 2
797
+ spotLights : array<vec4f, 16>, // MAX_FORWARD_LIGHTS * 4
798
+ hemisphereLights : array<vec4f, 12>, // MAX_FORWARD_LIGHTS * 3
799
+ punctualCounts : vec4f, // x = pointCount, y = spotCount, z = hemisphereCount
800
+ };
801
+
802
+ struct Draw {
803
+ world : mat4x4f,
804
+ normalMatrix : mat3x3f,
805
+ uvTransform : mat3x3f, // KHR_texture_transform of the material's primary map (identity when unused)
806
+ params : vec4f, // x = resolved object alpha
807
+ };
808
+
809
+ @group(0) @binding(0) var<uniform> frame : Frame;
810
+ @group(1) @binding(0) var<uniform> draw : Draw;
811
+
812
+ struct VertexOutput {
813
+ @builtin(position) clipPosition : vec4f,
814
+ @location(0) worldPosition : vec3f,
815
+ @location(1) worldNormal : vec3f,
816
+ @location(2) worldTangent : vec4f,
817
+ @location(3) uv : vec2f,
818
+ @location(4) @interpolate(flat) objectAlpha : f32,
819
+ };
820
+
821
+ @vertex fn vs_main(
822
+ @location(0) position : vec3f,
823
+ @location(1) normal : vec3f,
824
+ @location(2) tangent : vec4f,
825
+ @location(3) uv : vec2f,
826
+ ) -> VertexOutput {
827
+ var out : VertexOutput;
828
+ var localPosition = vec4f(position, 1.0);
829
+ var localNormal = normal;
830
+ var localTangent = tangent.xyz;
831
+ let world = draw.world * localPosition;
832
+ out.worldPosition = world.xyz;
833
+ out.clipPosition = frame.viewProjection * world;
834
+ out.worldNormal = draw.normalMatrix * localNormal;
835
+ out.worldTangent = vec4f(draw.normalMatrix * localTangent, tangent.w);
836
+ // Apply the material's KHR_texture_transform to the uv. draw.uvTransform is identity for an untiled
837
+ // material (writeWgpuDrawUniform's default), so this is a no-op there — applied unconditionally rather
838
+ // than behind a pipeline const because this vs_main is shared by every family (classic/unlit/toon/
839
+ // matcap/debug/wireframe) and a const would have to thread through all of them; a per-vertex mat3
840
+ // multiply is negligible. The scene-gl mirror gates the equivalent branch via its #ifdef variant.
841
+ out.uv = (draw.uvTransform * vec3f(uv, 1.0)).xy;
842
+ out.objectAlpha = draw.params.x;
843
+ return out;
844
+ }
845
+
846
+ // sRgb albedo texels are gamma-encoded; decode to linear before lighting.
847
+ fn srgbToLinear(c : vec3f) -> vec3f {
848
+ let lo = c / 12.92;
849
+ let hi = pow((c + vec3f(0.055)) / 1.055, vec3f(2.4));
850
+ return select(lo, hi, c > vec3f(0.04045));
851
+ }
852
+ `;
853
+ // Writes the per-frame Frame uniform (camera view-projection + world position + the packed light
854
+ // block) into the scene runtime's Frame buffer and ensures the Frame bind group exists. The light
855
+ // block layout matches Scene3DLightBlock.data: directional { direction.xyz @0, radiance.rgb @4 } then
856
+ // ambient { radiance.rgb @8 }; the presence counts go into the lightDirection.w / ambientRadiance.w
857
+ // lanes the shader branches on. Camera3D world position is the translation of the inverse view matrix.
858
+ // Punctual light arrays (point/spot/hemisphere) follow the camera view matrix, mirroring the packed
859
+ // layout from Scene3DLightBlock.data; a final vec4f carries the three punctual counts. Shared by every
860
+ // family — lighting-independent families simply ignore the light lanes.
861
+ export function writeWgpuFrameUniform(state, camera, lights) {
862
+ const scene = getWgpuScene3DRuntime(state);
863
+ let binding = scene.frameBindings.get(lights);
864
+ if (binding === undefined) {
865
+ const buffer = state.device.createBuffer({
866
+ size: FRAME_UNIFORM_BYTES,
867
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
868
+ });
869
+ const bindGroup = state.device.createBindGroup({
870
+ layout: ensureWgpuScene3DLayouts(state).frameBindGroupLayout,
871
+ entries: [{ binding: 0, resource: { buffer } }],
872
+ });
873
+ binding = { bindGroup, buffer };
874
+ scene.frameBindings.set(lights, binding);
875
+ }
876
+ // beginWgpuMeshDraw binds these current aliases immediately after this write. Distinct light blocks
877
+ // retain distinct buffers until command submission, so later binds cannot rewrite earlier draws.
878
+ scene.frameBuffer = binding.buffer;
879
+ scene.frameBindGroup = binding.bindGroup;
880
+ const f = _frameScratch;
881
+ const aspect = camera.projection.kind === 'perspective' ? camera.projection.aspect : 1;
882
+ getCamera3DViewProjectionMatrix4(scratchViewProjection, camera, aspect !== 0 ? aspect : 1);
883
+ // Camera projection matrices use OpenGL's [-1, 1] NDC-Z convention. WebGPU clips Z to [0, 1],
884
+ // so left-multiply the view-projection by the depth correction z' = 0.5z + 0.5w. In column-major
885
+ // storage this replaces the Z row while preserving X, Y, and W. Shadow-map WGSL applies its own
886
+ // equivalent correction and deliberately does not consume this frame uniform.
887
+ const sourceVp = scratchViewProjection.m;
888
+ const webGpuVp = scratchWebGpuViewProjection.m;
889
+ webGpuVp.set(sourceVp);
890
+ webGpuVp[2] = 0.5 * (sourceVp[2] + sourceVp[3]);
891
+ webGpuVp[6] = 0.5 * (sourceVp[6] + sourceVp[7]);
892
+ webGpuVp[10] = 0.5 * (sourceVp[10] + sourceVp[11]);
893
+ webGpuVp[14] = 0.5 * (sourceVp[14] + sourceVp[15]);
894
+ const vp = webGpuVp;
895
+ for (let i = 0; i < 16; i++)
896
+ f[i] = vp[i];
897
+ inverseMatrix4(scratchInverseView, camera.view);
898
+ getMatrix4Position(scratchCameraPosition, scratchInverseView);
899
+ f[16] = scratchCameraPosition.x;
900
+ f[17] = scratchCameraPosition.y;
901
+ f[18] = scratchCameraPosition.z;
902
+ f[19] = 0;
903
+ const data = lights.data;
904
+ f[20] = data[0];
905
+ f[21] = data[1];
906
+ f[22] = data[2];
907
+ f[23] = lights.directionalCount;
908
+ f[24] = data[4];
909
+ f[25] = data[5];
910
+ f[26] = data[6];
911
+ f[27] = 0;
912
+ f[28] = data[8];
913
+ f[29] = data[9];
914
+ f[30] = data[10];
915
+ f[31] = lights.ambientCount;
916
+ // Camera3D view matrix (floats 32..47): used by matcap to rotate the world-space normal into view
917
+ // space. Lighting-independent families ignore these lanes; the cost is one extra mat4 per frame.
918
+ const view = camera.view.m;
919
+ for (let i = 0; i < 16; i++)
920
+ f[32 + i] = view[i];
921
+ // Punctual light arrays (floats 48..) — the point/spot/hemisphere slices from Scene3DLightBlock.data
922
+ // (identical packed layout), followed by a counts vec4f. Families that shade punctual lights read
923
+ // these; others simply ignore the trailing data.
924
+ const pointFloats = SCENE_LIGHT_POINT_STRIDE * MAX_FORWARD_LIGHTS;
925
+ for (let i = 0; i < pointFloats; i++)
926
+ f[FRAME_POINT_OFFSET + i] = data[SCENE_LIGHT_POINT_OFFSET + i];
927
+ const spotFloats = SCENE_LIGHT_SPOT_STRIDE * MAX_FORWARD_LIGHTS;
928
+ for (let i = 0; i < spotFloats; i++)
929
+ f[FRAME_SPOT_OFFSET + i] = data[SCENE_LIGHT_SPOT_OFFSET + i];
930
+ const hemisphereFloats = SCENE_LIGHT_HEMISPHERE_STRIDE * MAX_FORWARD_LIGHTS;
931
+ for (let i = 0; i < hemisphereFloats; i++)
932
+ f[FRAME_HEMISPHERE_OFFSET + i] = data[SCENE_LIGHT_HEMISPHERE_OFFSET + i];
933
+ f[FRAME_COUNTS_OFFSET] = lights.pointCount;
934
+ f[FRAME_COUNTS_OFFSET + 1] = lights.spotCount;
935
+ f[FRAME_COUNTS_OFFSET + 2] = lights.hemisphereCount;
936
+ f[FRAME_COUNTS_OFFSET + 3] = 0;
937
+ state.device.queue.writeBuffer(binding.buffer, 0, f.buffer, 0, FRAME_UNIFORM_BYTES);
938
+ }
939
+ // Frame uniform float offsets for the punctual light arrays — the byte offset within the Frame buffer
940
+ // where each punctual array begins, used by writeWgpuFrameUniform to copy the packed data from
941
+ // Scene3DLightBlock.data into the right Frame buffer position. All offsets in FLOATS (multiply by 4
942
+ // for bytes). The head block (viewProjection + cameraPosition + directional + ambient + view) is 48
943
+ // floats, followed by point → spot → hemisphere → counts.
944
+ const FRAME_POINT_OFFSET = 48;
945
+ const FRAME_SPOT_OFFSET = FRAME_POINT_OFFSET + SCENE_LIGHT_POINT_STRIDE * MAX_FORWARD_LIGHTS;
946
+ const FRAME_HEMISPHERE_OFFSET = FRAME_SPOT_OFFSET + SCENE_LIGHT_SPOT_STRIDE * MAX_FORWARD_LIGHTS;
947
+ const FRAME_COUNTS_OFFSET = FRAME_HEMISPHERE_OFFSET + SCENE_LIGHT_HEMISPHERE_STRIDE * MAX_FORWARD_LIGHTS;
948
+ // Frame uniform: mat4x4f viewProjection (64) + vec4f cameraPosition (16) + vec4f lightDirection (16)
949
+ // + vec4f directionalRadiance (16) + vec4f ambientRadiance (16) + mat4x4f view (64) + point lights
950
+ // (8 * 16 = 128) + spot lights (16 * 16 = 256) + hemisphere lights (12 * 16 = 192) + counts vec4f
951
+ // (16) = 784 bytes / 196 floats.
952
+ const FRAME_UNIFORM_BYTES = (FRAME_COUNTS_OFFSET + 4) * 4;
953
+ // Draw uniform: mat4x4f world (64) + mat3x3f normalMatrix as 3 padded vec4 (48) + mat3x3f uvTransform as
954
+ // 3 padded vec4 (48) + vec4f params (16, x = resolved object alpha) = 176; the ring buffer rounds the
955
+ // per-slot stride up to the device's minUniformBufferOffsetAlignment.
956
+ const DRAW_UNIFORM_BYTES = 176;
957
+ // Writes the column-major identity mat3 into a uv-transform stash buffer, the untiled default.
958
+ function resetWgpuUvTransformStash(out) {
959
+ out[0] = 1;
960
+ out[1] = 0;
961
+ out[2] = 0;
962
+ out[3] = 0;
963
+ out[4] = 1;
964
+ out[5] = 0;
965
+ out[6] = 0;
966
+ out[7] = 0;
967
+ out[8] = 1;
968
+ }
969
+ // Row-major uv matrix composed per stash, transposed into the column-major pendingUvTransform buffer.
970
+ const scratchUvMatrix = createMatrix3();
971
+ // The depth-stencil format the scene pass uses, matching render-wgpu's main-canvas / effect-target
972
+ // depth attachment.
973
+ const DEPTH_STENCIL_FORMAT = 'depth24plus-stencil8';
974
+ // The sampleable depth format the directional shadow map (and its 1x1 no-shadow dummy) use. depth32float
975
+ // is bindable as a texture_depth_2d for the lit PCF comparison; drawWgpuScene3DShadowMap renders into it.
976
+ export const SHADOW_DEPTH_FORMAT = 'depth32float';
977
+ // Shadow-sample uniform: mat4x4f light matrix (64) + vec4f params (16, x = enabled) = 80 bytes / 20 floats.
978
+ const SHADOW_SAMPLE_UNIFORM_BYTES = 80;
979
+ // IBL-sample uniform: vec4f params (16, x = enabled, y = intensity, z = maxMip) = 16 bytes / 4 floats.
980
+ const IBL_SAMPLE_UNIFORM_BYTES = 16;
981
+ // The 1x1 no-IBL dummy cube + LUT format. A plain filterable 8-bit format satisfies the IBL `float`
982
+ // texture slots (the dummies are never sampled — the IBL uniform's enabled flag gates them off).
983
+ const IBL_DUMMY_FORMAT = 'rgba8unorm';
984
+ // Opaque-white 1x1 RGBA pixel for the shared placeholder map texture (untextured path).
985
+ const WHITE_PIXEL = new Uint8Array([255, 255, 255, 255]);
986
+ // The canonical interleaved 48-byte PBR vertex: position(float32x3) @0, normal(float32x3) @12,
987
+ // tangent(float32x4) @24, uv0(float32x2) @40. Matches the @location slots in the WGSL vs_main.
988
+ const VERTEX_BUFFER_LAYOUTS = [
989
+ {
990
+ arrayStride: 48,
991
+ attributes: [
992
+ { shaderLocation: 0, offset: 0, format: 'float32x3' },
993
+ { shaderLocation: 1, offset: 12, format: 'float32x3' },
994
+ { shaderLocation: 2, offset: 24, format: 'float32x4' },
995
+ { shaderLocation: 3, offset: 40, format: 'float32x2' },
996
+ ],
997
+ },
998
+ ];
999
+ const scratchViewProjection = createMatrix4();
1000
+ const scratchWebGpuViewProjection = createMatrix4();
1001
+ const scratchInverseView = createMatrix4();
1002
+ const scratchCameraPosition = { x: 0, y: 0, z: 0 };
1003
+ const _frameScratch = new Float32Array(FRAME_UNIFORM_BYTES / 4);
1004
+ const _dynamicOffsets = new Uint32Array(1);
1005
+ const _shadowSampleScratch = new Float32Array(SHADOW_SAMPLE_UNIFORM_BYTES / 4);
1006
+ const _iblSampleScratch = new Float32Array(IBL_SAMPLE_UNIFORM_BYTES / 4);
1007
+ //# sourceMappingURL=wgpuMeshPipeline.js.map