@driftengine/core 4.8.2 → 4.8.4

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 (259) hide show
  1. package/CHANGELOG.json +127 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +11 -4
  5. package/dist/index.js +22 -1
  6. package/dist/math/scalar.js +1 -1
  7. package/dist/render/anisotropicLobe.d.ts +7 -0
  8. package/dist/render/anisotropicLobe.js +33 -0
  9. package/dist/render/backend/api.d.ts +41 -4
  10. package/dist/render/backend/materialChanges.d.ts +7 -2
  11. package/dist/render/backend/materialChanges.js +8 -3
  12. package/dist/render/backend/select.js +8 -0
  13. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  14. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  15. package/dist/render/backend/webgl2/renderer.d.ts +159 -3
  16. package/dist/render/backend/webgl2/renderer.js +662 -80
  17. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  18. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  19. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  20. package/dist/render/backend/webgpu/buffers.js +98 -49
  21. package/dist/render/backend/webgpu/causticsPass.js +2 -1
  22. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  23. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  24. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  25. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  26. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  27. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  28. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  29. package/dist/render/backend/webgpu/computeHost.js +5 -0
  30. package/dist/render/backend/webgpu/depthPass.d.ts +29 -9
  31. package/dist/render/backend/webgpu/depthPass.js +43 -12
  32. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  33. package/dist/render/backend/webgpu/filmPass.js +2 -1
  34. package/dist/render/backend/webgpu/flatPass.d.ts +43 -4
  35. package/dist/render/backend/webgpu/flatPass.js +111 -13
  36. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  37. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  38. package/dist/render/backend/webgpu/linePass.js +2 -1
  39. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  40. package/dist/render/backend/webgpu/motionPass.js +106 -9
  41. package/dist/render/backend/webgpu/panelPass.js +2 -1
  42. package/dist/render/backend/webgpu/particlePass.js +2 -1
  43. package/dist/render/backend/webgpu/plumePass.js +2 -1
  44. package/dist/render/backend/webgpu/postPass.d.ts +15 -3
  45. package/dist/render/backend/webgpu/postPass.js +25 -8
  46. package/dist/render/backend/webgpu/renderer.d.ts +123 -6
  47. package/dist/render/backend/webgpu/renderer.js +534 -58
  48. package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
  49. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  50. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  51. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  52. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  55. package/dist/render/backend/webgpu/textPass.js +2 -1
  56. package/dist/render/backend/webgpu/waterPass.js +2 -1
  57. package/dist/render/backend/webgpu/windStreakPass.js +2 -1
  58. package/dist/render/boltBatch.js +2 -1
  59. package/dist/render/causticsRenderer.js +2 -1
  60. package/dist/render/clothBindingData.d.ts +41 -0
  61. package/dist/render/clothBindingData.js +72 -0
  62. package/dist/render/clothPlacement.d.ts +25 -0
  63. package/dist/render/clothPlacement.js +85 -0
  64. package/dist/render/compressedSource.d.ts +66 -0
  65. package/dist/render/compressedSource.js +138 -0
  66. package/dist/render/cutoutCaster.d.ts +5 -1
  67. package/dist/render/cutoutCaster.js +6 -0
  68. package/dist/render/cutoutDither.d.ts +51 -0
  69. package/dist/render/cutoutDither.js +90 -0
  70. package/dist/render/decalPass.js +2 -1
  71. package/dist/render/eyeRefraction.d.ts +35 -0
  72. package/dist/render/eyeRefraction.js +40 -0
  73. package/dist/render/glCompressed.d.ts +27 -0
  74. package/dist/render/glCompressed.js +53 -0
  75. package/dist/render/globalMediumPass.js +3 -2
  76. package/dist/render/hairLobes.d.ts +70 -0
  77. package/dist/render/hairLobes.js +120 -0
  78. package/dist/render/lightBudget.d.ts +22 -0
  79. package/dist/render/lightBudget.js +22 -0
  80. package/dist/render/lineBatch.js +5 -1
  81. package/dist/render/mesh.d.ts +7 -0
  82. package/dist/render/mesh.js +24 -0
  83. package/dist/render/oitPass.js +2 -1
  84. package/dist/render/particleBatch.js +3 -2
  85. package/dist/render/particlePool.d.ts +6 -2
  86. package/dist/render/plumeRenderer.js +3 -2
  87. package/dist/render/recon/motionVectors.d.ts +4 -2
  88. package/dist/render/recon/motionVectors.js +6 -3
  89. package/dist/render/renderQuality.d.ts +29 -2
  90. package/dist/render/renderQuality.js +5 -0
  91. package/dist/render/rendererHarness.js +4 -0
  92. package/dist/render/sceneCoverage.d.ts +54 -0
  93. package/dist/render/sceneCoverage.js +72 -0
  94. package/dist/render/sceneTarget.d.ts +12 -0
  95. package/dist/render/sceneTarget.js +14 -0
  96. package/dist/render/sdfTextRenderer.js +2 -1
  97. package/dist/render/shaders/clothBinding.d.ts +22 -0
  98. package/dist/render/shaders/clothBinding.js +80 -0
  99. package/dist/render/shaders/depth.js +45 -8
  100. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  101. package/dist/render/shaders/flat/anisotropic.js +68 -0
  102. package/dist/render/shaders/flat/eye.d.ts +1 -0
  103. package/dist/render/shaders/flat/eye.js +81 -0
  104. package/dist/render/shaders/flat/hair.d.ts +1 -0
  105. package/dist/render/shaders/flat/hair.js +101 -0
  106. package/dist/render/shaders/flat/index.js +42 -3
  107. package/dist/render/shaders/flat/main.d.ts +1 -1
  108. package/dist/render/shaders/flat/main.js +113 -17
  109. package/dist/render/shaders/flat/models.d.ts +14 -0
  110. package/dist/render/shaders/flat/models.js +273 -0
  111. package/dist/render/shaders/flat/preamble.js +26 -24
  112. package/dist/render/shaders/flat/skin.d.ts +1 -0
  113. package/dist/render/shaders/flat/skin.js +114 -0
  114. package/dist/render/shaders/generated/depth.wgsl.d.ts +66 -24
  115. package/dist/render/shaders/generated/depth.wgsl.js +66 -24
  116. package/dist/render/shaders/generated/flat.wgsl.d.ts +1815 -1704
  117. package/dist/render/shaders/generated/flat.wgsl.js +1853 -1742
  118. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
  121. package/dist/render/shaders/generated/particle.wgsl.js +3 -3
  122. package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/rush.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
  125. package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
  126. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  127. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  128. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
  130. package/dist/render/shaders/line.d.ts +1 -1
  131. package/dist/render/shaders/outputTransform.d.ts +14 -1
  132. package/dist/render/shaders/outputTransform.js +23 -1
  133. package/dist/render/shaders/particle.d.ts +3 -3
  134. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  135. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  136. package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
  137. package/dist/render/shaders/rush.js +13 -2
  138. package/dist/render/shaders/sdfText.d.ts +1 -1
  139. package/dist/render/shaders/skinBlur.d.ts +1 -0
  140. package/dist/render/shaders/skinBlur.js +103 -0
  141. package/dist/render/shaders/skinning.d.ts +3 -1
  142. package/dist/render/shaders/skinning.js +31 -4
  143. package/dist/render/shaders/temporalResolve.js +12 -5
  144. package/dist/render/shadowCasters.d.ts +13 -1
  145. package/dist/render/skinBlur.d.ts +41 -0
  146. package/dist/render/skinBlur.js +49 -0
  147. package/dist/render/skinScatter.d.ts +67 -0
  148. package/dist/render/skinScatter.js +80 -0
  149. package/dist/render/skinnedClothRun.d.ts +26 -0
  150. package/dist/render/skinnedClothRun.js +84 -0
  151. package/dist/render/ssrPass.js +2 -1
  152. package/dist/render/surfaceModel.d.ts +129 -0
  153. package/dist/render/surfaceModel.js +152 -0
  154. package/dist/render/surfaceTexture.d.ts +55 -4
  155. package/dist/render/surfaceTexture.js +60 -46
  156. package/dist/render/textRenderer.js +2 -1
  157. package/dist/render/vertexDefaults.js +8 -0
  158. package/dist/render/waterRenderer.js +2 -1
  159. package/dist/render/windStreakRenderer.js +2 -1
  160. package/package.json +6 -6
  161. package/src/geometry/meshConcat.ts +2 -0
  162. package/src/index.ts +53 -1
  163. package/src/math/scalar.ts +1 -1
  164. package/src/render/anisotropicLobe.ts +41 -0
  165. package/src/render/backend/api.ts +52 -3
  166. package/src/render/backend/materialChanges.ts +8 -3
  167. package/src/render/backend/select.ts +8 -0
  168. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  169. package/src/render/backend/webgl2/renderer.ts +750 -80
  170. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  171. package/src/render/backend/webgpu/buffers.ts +137 -63
  172. package/src/render/backend/webgpu/causticsPass.ts +2 -1
  173. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  174. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  175. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  176. package/src/render/backend/webgpu/computeHost.ts +25 -0
  177. package/src/render/backend/webgpu/depthPass.ts +43 -10
  178. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  179. package/src/render/backend/webgpu/filmPass.ts +2 -1
  180. package/src/render/backend/webgpu/flatPass.ts +147 -8
  181. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  182. package/src/render/backend/webgpu/linePass.ts +2 -1
  183. package/src/render/backend/webgpu/motionPass.ts +128 -9
  184. package/src/render/backend/webgpu/panelPass.ts +2 -1
  185. package/src/render/backend/webgpu/particlePass.ts +2 -1
  186. package/src/render/backend/webgpu/plumePass.ts +2 -1
  187. package/src/render/backend/webgpu/postPass.ts +25 -6
  188. package/src/render/backend/webgpu/renderer.ts +701 -52
  189. package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
  190. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  191. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  192. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  193. package/src/render/backend/webgpu/textPass.ts +2 -1
  194. package/src/render/backend/webgpu/waterPass.ts +2 -1
  195. package/src/render/backend/webgpu/windStreakPass.ts +2 -1
  196. package/src/render/boltBatch.ts +2 -1
  197. package/src/render/causticsRenderer.ts +2 -1
  198. package/src/render/clothBindingData.ts +89 -0
  199. package/src/render/clothPlacement.ts +114 -0
  200. package/src/render/compressedSource.ts +198 -0
  201. package/src/render/cutoutCaster.ts +20 -2
  202. package/src/render/cutoutDither.ts +99 -0
  203. package/src/render/decalPass.ts +2 -1
  204. package/src/render/eyeRefraction.ts +72 -0
  205. package/src/render/glCompressed.ts +86 -0
  206. package/src/render/globalMediumPass.ts +3 -2
  207. package/src/render/hairLobes.ts +188 -0
  208. package/src/render/lightBudget.ts +25 -0
  209. package/src/render/lineBatch.ts +3 -1
  210. package/src/render/mesh.ts +23 -0
  211. package/src/render/oitPass.ts +2 -1
  212. package/src/render/particleBatch.ts +3 -2
  213. package/src/render/particlePool.ts +6 -2
  214. package/src/render/plumeRenderer.ts +3 -2
  215. package/src/render/recon/motionVectors.ts +6 -3
  216. package/src/render/renderQuality.ts +37 -2
  217. package/src/render/rendererHarness.ts +4 -0
  218. package/src/render/sceneCoverage.ts +78 -0
  219. package/src/render/sceneTarget.ts +21 -0
  220. package/src/render/sdfTextRenderer.ts +2 -1
  221. package/src/render/shaders/clothBinding.ts +81 -0
  222. package/src/render/shaders/depth.ts +45 -8
  223. package/src/render/shaders/flat/anisotropic.ts +68 -0
  224. package/src/render/shaders/flat/eye.ts +82 -0
  225. package/src/render/shaders/flat/hair.ts +102 -0
  226. package/src/render/shaders/flat/index.ts +42 -3
  227. package/src/render/shaders/flat/main.ts +113 -17
  228. package/src/render/shaders/flat/models.ts +286 -0
  229. package/src/render/shaders/flat/preamble.ts +26 -24
  230. package/src/render/shaders/flat/skin.ts +116 -0
  231. package/src/render/shaders/generated/depth.wgsl.ts +66 -24
  232. package/src/render/shaders/generated/flat.wgsl.ts +1853 -1742
  233. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  234. package/src/render/shaders/generated/particle.wgsl.ts +3 -3
  235. package/src/render/shaders/generated/rush.wgsl.ts +1 -1
  236. package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
  237. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  238. package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
  239. package/src/render/shaders/outputTransform.ts +24 -1
  240. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  241. package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
  242. package/src/render/shaders/rush.ts +13 -2
  243. package/src/render/shaders/skinBlur.ts +105 -0
  244. package/src/render/shaders/skinning.ts +31 -4
  245. package/src/render/shaders/temporalResolve.ts +12 -5
  246. package/src/render/shadowCasters.ts +13 -0
  247. package/src/render/skinBlur.ts +53 -0
  248. package/src/render/skinScatter.ts +92 -0
  249. package/src/render/skinnedClothRun.ts +128 -0
  250. package/src/render/ssrPass.ts +2 -1
  251. package/src/render/surfaceModel.ts +274 -0
  252. package/src/render/surfaceTexture.ts +134 -63
  253. package/src/render/textRenderer.ts +2 -1
  254. package/src/render/vertexDefaults.ts +8 -0
  255. package/src/render/waterRenderer.ts +2 -1
  256. package/src/render/windStreakRenderer.ts +2 -1
  257. package/dist/math/exact.d.ts +0 -76
  258. package/dist/math/exact.js +0 -327
  259. package/src/math/exact.ts +0 -340
@@ -52,6 +52,10 @@ import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, p
52
52
  import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
53
53
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
54
54
  import { GpuSurfaceTexture } from './surfaceTexturePass.js';
55
+ import { gpuCompressedFormats } from './compressedUpload.js';
56
+ import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
57
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
58
+ import { validateClothBinding } from '../../clothBindingData.js';
55
59
  import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
56
60
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
57
61
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
@@ -61,7 +65,7 @@ import { ExposurePass } from './exposurePass.js';
61
65
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
62
66
  import { clampAutoExposure } from '../../shaders/exposure.js';
63
67
  import { clampLocalExposure } from '../../shaders/localExposure.js';
64
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
68
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, ADDITIVE_BLEND, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
65
69
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
66
70
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
67
71
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -87,6 +91,7 @@ import { SkinPaletteRing } from './skinPaletteRing.js';
87
91
  import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
88
92
  import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
89
93
  import { PipelineCache } from './pipelineCache.js';
94
+ import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
90
95
  import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
91
96
  import { SCATTER_DEPTH_FIELDS, SCATTER_DEPTH_SIZE, SCATTER_FRAG_FIELDS, SCATTER_FRAG_SIZE, SCATTER_VERT_FIELDS, SCATTER_VERT_SIZE, createGpuScatter, createScatterBindGroup, createScatterBindGroupLayout, createScatterDepthBindGroup, createScatterDepthBindGroupLayout, scatterDepthPipeline, scatterPipeline, } from './scatterPass.js';
92
97
  import { WATER_FRAG_FIELDS, WATER_FRAG_SIZE, WATER_VERT_FIELDS, WATER_VERT_SIZE, createGpuWater, createWaterBindGroup, createWaterBindGroupLayout, waterPipeline, } from './waterPass.js';
@@ -100,7 +105,7 @@ import { LIGHT_VOLUME_VERT_FIELDS, LIGHT_VOLUME_VERT_SIZE, createLightVolumeBind
100
105
  import { aimReflection, createGpuReflection } from './reflectionPass.js';
101
106
  import { FrameBudget } from '../budget.js';
102
107
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
103
- import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
108
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.js';
104
109
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
105
110
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
106
111
  import { DistanceFieldScene } from '../../gi/fieldScene.js';
@@ -137,6 +142,7 @@ import { clusteredMode, createClusterLightSet, fillClusterLightSet, } from '../.
137
142
  import { computeAt, createComputeRegistry, } from '../../compute.js';
138
143
  import { GpuInstancedBatch } from './instanced.js';
139
144
  import { cutoutOf } from '../../cutoutCaster.js';
145
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
140
146
  import { LightField } from '../../driftLight/lightField.js';
141
147
  import { WorldLightField } from '../../driftLight/worldLightField.js';
142
148
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
@@ -864,6 +870,12 @@ export class WebGPURenderer {
864
870
  skinnedGroupsBeside = [];
865
871
  skinnedGroupsPalette = [];
866
872
  skinnedGroupsDeltas = [];
873
+ /** The cloth views each skinned twin was built with, compared by identity like the palette. */
874
+ skinnedGroupsCloth = [];
875
+ skinnedGroupsClothBinding = [];
876
+ /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
877
+ cloth = null;
878
+ clothStandCache = null;
867
879
  /**
868
880
  * Which palette slot the following draws skin by, or -1 for none.
869
881
  *
@@ -1257,6 +1269,16 @@ export class WebGPURenderer {
1257
1269
  refractSnapshotView = null;
1258
1270
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1259
1271
  refractSnapshotTaken = false;
1272
+ /**
1273
+ * Skin's screen-space scattering, made where the profile asks for it and has a composite to add
1274
+ * into, and null otherwise — which is every profile by default. See `SkinScatterPass`.
1275
+ */
1276
+ skinScatter;
1277
+ /** The pipelines skin's diffuse half is built for: the blur target's format, the frame's samples. */
1278
+ skinPipelines;
1279
+ /** Whether this frame has spread its skin: once, at its first blended draw or before its end. */
1280
+ skinScattered = false;
1281
+ skinInverseProjection = mat4.create();
1260
1282
  sceneColorView = null;
1261
1283
  /** The multisampled twin, when the profile asks for samples. Resolved into `sceneColor`. */
1262
1284
  sceneColorMsaa = null;
@@ -1435,6 +1457,17 @@ export class WebGPURenderer {
1435
1457
  temporalJitterX = 0;
1436
1458
  temporalJitterY = 0;
1437
1459
  temporalJittering = false;
1460
+ /**
1461
+ * The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64.
1462
+ * See `cutoutDither.ts`.
1463
+ */
1464
+ cutoutFrame = 0;
1465
+ /**
1466
+ * How the current material's cutout edge is drawn this frame, decided when the material is staged
1467
+ * — after `bindMeshPass` has settled whether the frame is temporal — and read by every draw: a
1468
+ * coverage edge is pipeline state here (`|a2c`), and a blended draw tests hard.
1469
+ */
1470
+ cutoutResolveStaged = 'hard';
1438
1471
  /**
1439
1472
  * Whether this frame is being reconstructed, settled once in `bindMeshPass`.
1440
1473
  *
@@ -2100,6 +2133,12 @@ export class WebGPURenderer {
2100
2133
  ormMap = null;
2101
2134
  /** The caller's emissive map, or null for the stand-in. See `setMaterial`. */
2102
2135
  emissiveMap = null;
2136
+ /** The material's model map, or null for the stand-in. See `setMaterial` and `surfaceModel.ts`. */
2137
+ modelMap = null;
2138
+ /** The material's shading model, which picks the lit pipeline: null the standard. */
2139
+ materialModel = null;
2140
+ /** Its numbers, `uModelParams`, packed by `packModel`. */
2141
+ modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2103
2142
  /**
2104
2143
  * The flat group for a material, keyed on **all four** of its maps.
2105
2144
  *
@@ -2131,16 +2170,33 @@ export class WebGPURenderer {
2131
2170
  blankAlbedoBindGroup;
2132
2171
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
2133
2172
  uvScale = new Float32Array([1, 1]);
2173
+ /** Where the texture starts, after the scale. Per draw, beside `uvScale`. */
2174
+ uvOffset = new Float32Array([0, 0]);
2175
+ /**
2176
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
2177
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
2178
+ */
2179
+ surfaceFog = FOG_RECEDE;
2134
2180
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2135
2181
  materialDoubleSided = false;
2136
2182
  /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
2137
2183
  materialHasMaps = false;
2138
2184
  createSurfaceTexture(source, options = {}) {
2139
- return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2185
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options, this.compressedFormats);
2186
+ }
2187
+ compressedCache = null;
2188
+ /**
2189
+ * The BC formats this device takes as blocks: every one where the device carries
2190
+ * `texture-compression-bc`, which `select.ts` asks for wherever the adapter offers it, and none
2191
+ * where it does not. See `Renderer.compressedFormats`.
2192
+ */
2193
+ get compressedFormats() {
2194
+ this.compressedCache ??= gpuCompressedFormats(this.surface.device);
2195
+ return this.compressedCache;
2140
2196
  }
2141
2197
  /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2142
2198
  createSurfaceTextureArray(sources, options = {}) {
2143
- return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2199
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options, this.compressedFormats);
2144
2200
  }
2145
2201
  /**
2146
2202
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
@@ -2191,6 +2247,8 @@ export class WebGPURenderer {
2191
2247
  this.ormMap = null;
2192
2248
  if (this.emissiveMap === texture)
2193
2249
  this.emissiveMap = null;
2250
+ if (this.modelMap === texture)
2251
+ this.modelMap = null;
2194
2252
  this.forgetBindingsOf(texture);
2195
2253
  texture.dispose();
2196
2254
  }
@@ -2215,7 +2273,7 @@ export class WebGPURenderer {
2215
2273
  this.albedo = null;
2216
2274
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
2217
2275
  this.albedo = held;
2218
- this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
2276
+ this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
2219
2277
  }
2220
2278
  /**
2221
2279
  * Choose the surface texture the following `drawMesh` calls sample, or null for none.
@@ -2243,6 +2301,104 @@ export class WebGPURenderer {
2243
2301
  one. See `SkinPaletteRing`; a full ring declines the draw instead of mixing two rigs. */
2244
2302
  this.skinPaletteSlot = this.skinPalettes.take(this.surface.device, palette) ?? -1;
2245
2303
  }
2304
+ /**
2305
+ * A mesh's cloth binding. See `RendererApi.createClothBinding`. The pipelines that draw it bound
2306
+ * are warmed here, every way this mesh can be drawn, so none compiles inside the first frame
2307
+ * that draws a garment.
2308
+ */
2309
+ createClothBinding(mesh, data) {
2310
+ if (!mesh.isSkinned) {
2311
+ throw new Error('createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
2312
+ 'skinning and the cloth');
2313
+ }
2314
+ validateClothBinding(data, mesh.vertexCount);
2315
+ const base = mesh.key ?? 'flat:s0:u0';
2316
+ const present = this.meshPresent.get(base);
2317
+ if (present !== undefined) {
2318
+ const skin = mesh.isSkinnedEight ? '|skin8' : '|skin';
2319
+ for (const morphed of mesh.morph !== null ? [false, true] : [false]) {
2320
+ for (const translucent of [false, true]) {
2321
+ void flatPipelineAsync(this.targetPipelines(), this.surface.device, this.flatLayoutFor(true, morphed), this.variant, `${base}${morphed ? '|morph' : ''}${skin}|cloth${translucent ? '|blend' : ''}`, present, translucent, true, morphed, mesh.isSkinnedEight, true);
2322
+ }
2323
+ }
2324
+ }
2325
+ return new GpuClothBinding(this.surface.device, data);
2326
+ }
2327
+ /** One character's particles. See `RendererApi.createClothParticles`. */
2328
+ createClothParticles(count) {
2329
+ return new GpuClothParticles(this.surface.device, count);
2330
+ }
2331
+ /**
2332
+ * This frame's particles. **Once a frame, before the character's draws**: a texture write lands
2333
+ * ahead of every draw of the frame, so a second update between two draws would reach both.
2334
+ */
2335
+ updateClothParticles(particles, positions) {
2336
+ if (this.surface.lost)
2337
+ return;
2338
+ /* A write between frames is the next frame's, as a rewritten mesh's is. See `changeFrames.ts`. */
2339
+ particles.update(this.surface.device, positions, frameOfChange(this.moverFrame, this.inFrame));
2340
+ }
2341
+ /** Place the following skinned draws by a cloth, or by none. See `RendererApi.setCloth`. */
2342
+ setCloth(binding, particles = null) {
2343
+ if (binding === null || particles === null) {
2344
+ this.cloth = null;
2345
+ return;
2346
+ }
2347
+ if (binding.particles !== particles.count) {
2348
+ throw new Error(`setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`);
2349
+ }
2350
+ this.cloth = { binding, particles };
2351
+ }
2352
+ /** Retired rather than destroyed: a draw recorded earlier in this frame may still read them. */
2353
+ disposeClothBinding(binding) {
2354
+ this.retiredTextures.push(...binding.textures);
2355
+ }
2356
+ disposeClothParticles(particles) {
2357
+ this.retiredTextures.push(...particles.textures);
2358
+ }
2359
+ /**
2360
+ * The device and the frame's bookkeeping, for compute a consumer opts into by importing it —
2361
+ * `createSkinnedCloth` — rather than for anything this renderer runs itself. See `computeHost.ts`.
2362
+ */
2363
+ computeHost() {
2364
+ return this.computeHostCache;
2365
+ }
2366
+ computeHostCache = {
2367
+ device: () => this.surface.device,
2368
+ lost: () => this.surface.lost,
2369
+ changeFrame: () => frameOfChange(this.moverFrame, this.inFrame),
2370
+ retire: (textures) => {
2371
+ this.retiredTextures.push(...textures);
2372
+ },
2373
+ };
2374
+ /**
2375
+ * An eye's axis in the world for this draw, into the open material's \`uModelParams\`, and a new
2376
+ * material slot when it moved: the axis is per draw, and a slot already taken by an earlier draw
2377
+ * of the same eye must not be written under it. See \`eyeAxisInWorld\`.
2378
+ */
2379
+ writeEyeAxis(model, skinned) {
2380
+ const eye = this.materialModel;
2381
+ if (eye?.kind !== 'eye')
2382
+ return;
2383
+ const at = this.materialField('uModelParams') + 4;
2384
+ const f = this.perFrameFloats;
2385
+ const before0 = f[at];
2386
+ const before1 = f[at + 1];
2387
+ const before2 = f[at + 2];
2388
+ eyeAxisInWorld(eye, model, skinned ? this.skinPaletteData : null, f, at);
2389
+ if (f[at] !== before0 || f[at + 1] !== before1 || f[at + 2] !== before2)
2390
+ this.materials.dirty();
2391
+ }
2392
+ /** The views the draw's cloth binds, or the stand-in's where no cloth is set. */
2393
+ clothViews() {
2394
+ const cloth = this.cloth;
2395
+ return cloth === null ? this.clothStand() : clothViewsOf(cloth.binding, cloth.particles);
2396
+ }
2397
+ /** The one-texel stand-in a skinned group binds where there is no cloth, made on first need. */
2398
+ clothStand() {
2399
+ this.clothStandCache ??= clothStandIn(this.surface.device);
2400
+ return this.clothStandCache.views;
2401
+ }
2246
2402
  /**
2247
2403
  * Choose the morph weights the following `drawMesh` calls deform by, or null for none.
2248
2404
  *
@@ -2260,19 +2416,39 @@ export class WebGPURenderer {
2260
2416
  const albedo = material?.albedo ?? null;
2261
2417
  this.uvScale[0] = material?.uScale ?? 1;
2262
2418
  this.uvScale[1] = material?.vScale ?? 1;
2419
+ this.uvOffset[0] = material?.uOffset ?? 0;
2420
+ this.uvOffset[1] = material?.vOffset ?? 0;
2263
2421
  this.materialDoubleSided = material?.doubleSided === true;
2264
2422
  this.materialHasMaps = materialHasMaps(material);
2265
2423
  const normal = (material?.normal ?? null);
2266
2424
  const orm = (material?.orm ?? null);
2267
2425
  const emissiveMap = (material?.emissive ?? null);
2426
+ const modelMap = (material?.modelMap ?? null);
2427
+ this.materialModel = material?.model ?? null;
2428
+ /* What the blur spreads this skin by: the last material to name a profile says. */
2429
+ const model = material?.model;
2430
+ if (model?.kind === 'skin' && this.skinScatter !== null) {
2431
+ this.skinScatter.setProfile(model.profile, model.scatterColor[0] * model.radius, model.scatterColor[1] * model.radius, model.scatterColor[2] * model.radius);
2432
+ }
2268
2433
  this.material((f, i) => {
2269
- i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
2270
- f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
2271
- i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
2434
+ /* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
2435
+ const flags = this.materialField('uMaterialFlags');
2436
+ i[flags] = albedo === null ? 0 : 1;
2437
+ /* The cutoff, how this frame resolves a dithered edge, and the frame its pattern moves with. */
2438
+ const cutoff = albedo === null ? 0 : (material?.cutout ?? 0);
2439
+ this.cutoutResolveStaged =
2440
+ cutoff > 0
2441
+ ? resolveCutout(material?.cutoutMode ?? 'hard', this.reconstructing || this.temporalJittering, this.samples)
2442
+ : 'hard';
2443
+ const cut = this.materialField('uCutout');
2444
+ f[cut] = cutoff;
2445
+ f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
2446
+ f[cut + 2] = this.cutoutFrame;
2447
+ i[flags + 3] = material?.doubleSided === true ? 1 : 0;
2272
2448
  /* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
2273
2449
  f[this.materialField('uNormalStrength')] =
2274
2450
  normal === null ? 0 : (material?.normalStrength ?? 1);
2275
- i[this.materialField('uOrmEnabled')] = orm === null ? 0 : 1;
2451
+ i[flags + 1] = orm === null ? 0 : 1;
2276
2452
  /*
2277
2453
  * Component-aligned with the map — r occlusion, g roughness, b metallic — so this order is
2278
2454
  * the channel order rather than the field order of `SurfaceMaterial`. Written component by
@@ -2283,7 +2459,7 @@ export class WebGPURenderer {
2283
2459
  f[scale] = material?.occlusionStrength ?? 1;
2284
2460
  f[scale + 1] = material?.roughnessScale ?? 1;
2285
2461
  f[scale + 2] = material?.metallicScale ?? 1;
2286
- i[this.materialField('uEmissiveMapEnabled')] = emissiveMap === null ? 0 : 1;
2462
+ i[flags + 2] = emissiveMap === null ? 0 : 1;
2287
2463
  /*
2288
2464
  * One rather than zero on every component, because this multiplies rather than adds: an
2289
2465
  * unwritten float is zero, and zero here would switch off every glow the mesh's own vertex
@@ -2294,6 +2470,9 @@ export class WebGPURenderer {
2294
2470
  f[emissive] = asked?.[0] ?? 1;
2295
2471
  f[emissive + 1] = asked?.[1] ?? 1;
2296
2472
  f[emissive + 2] = asked?.[2] ?? 1;
2473
+ /* The model's numbers and whether its map is bound. See `packModel`. */
2474
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams);
2475
+ f.set(this.modelParams, this.materialField('uModelParams'));
2297
2476
  });
2298
2477
  /*
2299
2478
  * **One lookup for the whole material, and nothing thrown away.**
@@ -2307,14 +2486,16 @@ export class WebGPURenderer {
2307
2486
  if (this.albedo === albedo &&
2308
2487
  this.normalMap === normal &&
2309
2488
  this.ormMap === orm &&
2310
- this.emissiveMap === emissiveMap) {
2489
+ this.emissiveMap === emissiveMap &&
2490
+ this.modelMap === modelMap) {
2311
2491
  return;
2312
2492
  }
2313
2493
  this.albedo = albedo;
2314
2494
  this.normalMap = normal;
2315
2495
  this.ormMap = orm;
2316
2496
  this.emissiveMap = emissiveMap;
2317
- this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap);
2497
+ this.modelMap = modelMap;
2498
+ this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap, modelMap);
2318
2499
  }
2319
2500
  /**
2320
2501
  * @deprecated Use `setMaterial`. See `renderer.ts` for why this stays: 56 call sites across four
@@ -2336,8 +2517,14 @@ export class WebGPURenderer {
2336
2517
  * asking. Passing them in as well is deliberate: it is what makes the key and the group agree by
2337
2518
  * construction rather than by the caller having remembered.
2338
2519
  */
2339
- flatGroupForMaps(albedo, normal, orm, emissive) {
2340
- if (albedo === null && normal === null && orm === null && emissive === null) {
2520
+ flatGroupForMaps(albedo, normal, orm, emissive,
2521
+ /** The model map, a fifth level of the same cache. See `flatBindGroups`. */
2522
+ modelMap) {
2523
+ if (albedo === null &&
2524
+ normal === null &&
2525
+ orm === null &&
2526
+ emissive === null &&
2527
+ modelMap === null) {
2341
2528
  return this.blankAlbedoBindGroup;
2342
2529
  }
2343
2530
  let byNormal = this.flatBindGroups.get(albedo);
@@ -2355,12 +2542,17 @@ export class WebGPURenderer {
2355
2542
  byEmissive = new Map();
2356
2543
  byOrm.set(orm, byEmissive);
2357
2544
  }
2358
- const existing = byEmissive.get(emissive);
2545
+ let byModel = byEmissive.get(emissive);
2546
+ if (byModel === undefined) {
2547
+ byModel = new Map();
2548
+ byEmissive.set(emissive, byModel);
2549
+ }
2550
+ const existing = byModel.get(modelMap);
2359
2551
  if (existing !== undefined)
2360
2552
  return existing;
2361
2553
  const built = this.buildFlatBindGroup();
2362
2554
  this.bindGroupBudget.ask();
2363
- byEmissive.set(emissive, built);
2555
+ byModel.set(modelMap, built);
2364
2556
  return built;
2365
2557
  }
2366
2558
  /* -- Material state ------------------------------------------------------------------ */
@@ -2408,6 +2600,13 @@ export class WebGPURenderer {
2408
2600
  f[this.materialField('uGrain')] = Math.min(1, Math.max(0, amount));
2409
2601
  });
2410
2602
  }
2603
+ /** Whether the surfaces drawn next meet the medium. See `renderer.ts`. */
2604
+ setSurfaceFog(enabled) {
2605
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
2606
+ this.material((_f, i) => {
2607
+ i[this.materialField('uFogEnabled')] = this.surfaceFog;
2608
+ });
2609
+ }
2411
2610
  /**
2412
2611
  * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
2413
2612
  *
@@ -4249,6 +4448,8 @@ export class WebGPURenderer {
4249
4448
  shadowSkinnedGroups = [];
4250
4449
  shadowSkinnedGroupsBeside = [];
4251
4450
  shadowSkinnedGroupsPalette = [];
4451
+ shadowSkinnedGroupsCloth = [];
4452
+ shadowSkinnedGroupsClothBinding = [];
4252
4453
  /** Held from construction so the skinned group can be rebuilt without them being threaded. */
4253
4454
  shadowBlankView = null;
4254
4455
  shadowBlankSampler = null;
@@ -4363,11 +4564,14 @@ export class WebGPURenderer {
4363
4564
  groups.set(texture, group);
4364
4565
  return group;
4365
4566
  }
4366
- /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
4567
+ /** The cutoff and UV scale and offset into a cutout draw's slot, at its own variant's offsets. */
4367
4568
  writeCutout(slot, cutout, fields) {
4368
4569
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
4369
4570
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
4571
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
4572
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
4370
4573
  this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
4574
+ this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset + 4, cutout.dithered);
4371
4575
  }
4372
4576
  shadowGroup() {
4373
4577
  return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
@@ -4381,14 +4585,18 @@ export class WebGPURenderer {
4381
4585
  * second independently fading occluder can be recorded, and a character is not one — it is the
4382
4586
  * thing in front. Serving it would mean a second cached group for a case nothing produces.
4383
4587
  */
4384
- shadowSkinnedGroup(palette, at) {
4588
+ shadowSkinnedGroup(palette, at, cloth) {
4385
4589
  const beside = this.shadowBindGroup;
4386
4590
  if ((this.shadowSkinnedGroups[at] ?? null) === null ||
4387
4591
  this.shadowSkinnedGroupsBeside[at] !== beside ||
4388
- this.shadowSkinnedGroupsPalette[at] !== palette) {
4389
- this.shadowSkinnedGroups[at] = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette);
4592
+ this.shadowSkinnedGroupsPalette[at] !== palette ||
4593
+ this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
4594
+ this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding) {
4595
+ this.shadowSkinnedGroups[at] = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette, null, cloth);
4390
4596
  this.shadowSkinnedGroupsBeside[at] = beside;
4391
4597
  this.shadowSkinnedGroupsPalette[at] = palette;
4598
+ this.shadowSkinnedGroupsCloth[at] = cloth.particles;
4599
+ this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
4392
4600
  }
4393
4601
  return this.shadowSkinnedGroups[at];
4394
4602
  }
@@ -4427,6 +4635,8 @@ export class WebGPURenderer {
4427
4635
  void this.overlayPipelines.enable(feature);
4428
4636
  if (this.latePipelines !== this.pipelines)
4429
4637
  void this.latePipelines.enable(feature);
4638
+ if (this.skinPipelines !== null)
4639
+ void this.skinPipelines.enable(feature);
4430
4640
  }
4431
4641
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
4432
4642
  glassCasterScratch = {
@@ -4517,7 +4727,7 @@ export class WebGPURenderer {
4517
4727
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
4518
4728
  pass.drawIndexed(geometry.indexCount, count);
4519
4729
  },
4520
- skinnedMesh: (mesh, model, palette, material) => {
4730
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
4521
4731
  if (!this.drawingGlassDepth && !castsDepth(material)) {
4522
4732
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4523
4733
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -4547,8 +4757,13 @@ export class WebGPURenderer {
4547
4757
  const geometry = mesh;
4548
4758
  if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
4549
4759
  return;
4550
- pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowSkinnedLayout, `depth-skinned|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, true));
4551
- pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot), [slot]);
4760
+ /* A garment casts placed by its cloth, as it draws; `|cloth` keys the pipeline that does. */
4761
+ const clothed = cloth !== undefined;
4762
+ const clothViews = clothed
4763
+ ? clothViewsOf(cloth.binding, cloth.particles)
4764
+ : this.clothStand();
4765
+ pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowSkinnedLayout, `${geometry.isSkinnedEight ? 'depth-skinned8' : 'depth-skinned'}${clothed ? '|cloth' : ''}|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, true, false, false, geometry.isSkinnedEight, clothed));
4766
+ pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot, clothViews), [slot]);
4552
4767
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
4553
4768
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
4554
4769
  }
@@ -4881,6 +5096,18 @@ export class WebGPURenderer {
4881
5096
  this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
4882
5097
  ? this.pipelines
4883
5098
  : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
5099
+ /*
5100
+ * Screen-space skin needs the composite: linear light to add into, and the depth it resolves.
5101
+ * Without one it is the pre-integrated fit alone, which is the default anyway, and said once.
5102
+ */
5103
+ const skinScreen = quality.skinScattering === 'screen-space' && quality.screenEffects;
5104
+ if (quality.skinScattering === 'screen-space' && !quality.screenEffects) {
5105
+ console.warn("WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
5106
+ }
5107
+ this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
5108
+ this.skinPipelines = skinScreen
5109
+ ? new PipelineCache(device, SKIN_TARGET_FORMAT, this.samples, glassShadows, quality.clusteredLights)
5110
+ : null;
4884
5111
  this.issueBound = this.issueCommand.bind(this);
4885
5112
  this.pipelineTargets =
4886
5113
  this.overlayPipelines === this.pipelines
@@ -4935,7 +5162,7 @@ export class WebGPURenderer {
4935
5162
  */
4936
5163
  this.perFrame = new UniformRing(device, this.fragment.uniformSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.fragRing', this.materialBudget);
4937
5164
  /*
4938
- * A one-pixel stand-in for the albedo binding. `uAlbedoEnabled` is zero for untextured
5165
+ * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
4939
5166
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
4940
5167
  * requires every declared binding to be filled. WebGL2 keeps an `emptyTexture2D` for
4941
5168
  * exactly the same reason.
@@ -5139,6 +5366,13 @@ export class WebGPURenderer {
5139
5366
  ? { view: blankSurfaceView, sampler: blankSampler }
5140
5367
  : { view: map.view, sampler: map.sampler };
5141
5368
  }
5369
+ /* The material's model map, or the stand-in, for the reason `uNormalMap` gives above. */
5370
+ if (name === 'uModelMap') {
5371
+ const map = this.modelMap;
5372
+ return map === null
5373
+ ? { view: blankSurfaceView, sampler: blankSampler }
5374
+ : { view: map.view, sampler: map.sampler };
5375
+ }
5142
5376
  if (name === 'uOrmMap') {
5143
5377
  const map = this.ormMap;
5144
5378
  return map === null
@@ -5714,6 +5948,8 @@ export class WebGPURenderer {
5714
5948
  }
5715
5949
  /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
5716
5950
  present['layers'] = key.includes(':layers');
5951
+ /* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
5952
+ present['dynamic'] = key.includes(':dynamic');
5717
5953
  return present;
5718
5954
  }
5719
5955
  /**
@@ -5726,6 +5962,7 @@ export class WebGPURenderer {
5726
5962
  */
5727
5963
  beginFrame(clearColor) {
5728
5964
  this.cullFrame += 1;
5965
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
5729
5966
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
5730
5967
  for (const texture of this.retiredTextures)
5731
5968
  texture.destroy();
@@ -5789,6 +6026,8 @@ export class WebGPURenderer {
5789
6026
  this.volumeDepthTaken = false;
5790
6027
  /* Last frame's snapshot is last frame's scene. */
5791
6028
  this.refractSnapshotTaken = false;
6029
+ this.skinScattered = false;
6030
+ this.skinScatter?.reset();
5792
6031
  this.plumeVerts.reset();
5793
6032
  this.plumeFrags.reset();
5794
6033
  this.waterVerts.reset();
@@ -5965,7 +6204,7 @@ export class WebGPURenderer {
5965
6204
  * neither the pipeline set nor the key depends on whether the geometry lands in one call or
5966
6205
  * over six frames.
5967
6206
  */
5968
- warmFlatPipelines(data) {
6207
+ warmFlatPipelines(data, dynamic) {
5969
6208
  /*
5970
6209
  * **Every optional attribute, not the two that come to mind.** `present` decides where
5971
6210
  * each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
@@ -5992,6 +6231,15 @@ export class WebGPURenderer {
5992
6231
  present['layers'] = true;
5993
6232
  key += ':layers';
5994
6233
  }
6234
+ /*
6235
+ * A dynamic mesh keeps its positions and normals in buffers of their own (`GpuMesh.update`), so
6236
+ * it binds four buffers where a static one binds two: a different layout, and a key that says
6237
+ * so, or the two would share a pipeline whose buffers one of them does not have.
6238
+ */
6239
+ if (dynamic) {
6240
+ present['dynamic'] = true;
6241
+ key += ':dynamic';
6242
+ }
5995
6243
  const fullKey = `${this.variant}|${key}`;
5996
6244
  /*
5997
6245
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -6047,14 +6295,16 @@ export class WebGPURenderer {
6047
6295
  * guessing which the scene will use.
6048
6296
  */
6049
6297
  const skinnable = data.joints !== undefined;
6298
+ /* An eight-influence rig takes its own skinned pipelines, keyed `|skin8`. See skinning.ts. */
6299
+ const eight = skinnable && data.joints2 !== undefined;
6050
6300
  const morphable = data.morphTargets !== undefined;
6051
6301
  for (const wantSkin of skinnable ? [false, true] : [false]) {
6052
6302
  for (const wantMorph of morphable ? [false, true] : [false]) {
6053
6303
  if (!wantSkin && !wantMorph)
6054
6304
  continue;
6055
- const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? '|skin' : ''}`;
6305
+ const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? (eight ? '|skin8' : '|skin') : ''}`;
6056
6306
  for (const translucent of [false, true]) {
6057
- void flatPipelineAsync(pipelines, this.surface.device, this.flatLayoutFor(wantSkin, wantMorph), this.variant, `${fullKey}${suffix}${translucent ? '|blend' : ''}`, present, translucent, wantSkin, wantMorph);
6307
+ void flatPipelineAsync(pipelines, this.surface.device, this.flatLayoutFor(wantSkin, wantMorph), this.variant, `${fullKey}${suffix}${translucent ? '|blend' : ''}`, present, translucent, wantSkin, wantMorph, wantSkin && eight);
6058
6308
  }
6059
6309
  }
6060
6310
  }
@@ -6075,7 +6325,7 @@ export class WebGPURenderer {
6075
6325
  return keyed;
6076
6326
  }
6077
6327
  createMesh(data, options = {}) {
6078
- const fullKey = this.warmFlatPipelines(data);
6328
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
6079
6329
  return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
6080
6330
  }
6081
6331
  /**
@@ -6088,7 +6338,7 @@ export class WebGPURenderer {
6088
6338
  * can do nothing about. Done with `complete` still false is the signal that it was abandoned.
6089
6339
  */
6090
6340
  createMeshIncremental(data, options = {}) {
6091
- const fullKey = this.warmFlatPipelines(data);
6341
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
6092
6342
  const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, options.dynamic === true);
6093
6343
  const surface = this.surface;
6094
6344
  return {
@@ -6105,18 +6355,18 @@ export class WebGPURenderer {
6105
6355
  /**
6106
6356
  * Rewrite a mesh's positions, and its normals where the caller has them.
6107
6357
  *
6108
- * Refused in words on a mesh that did not declare itself dynamic, because the thing that makes
6109
- * an update possible on this backend — the interleaved array the vertex data was built from —
6110
- * is only kept when a consumer asked for it. See `GpuMesh.update`.
6358
+ * Refused in words on a mesh that did not declare itself dynamic, because what makes an update
6359
+ * cheap on this backend — positions and normals in buffers of their own — is a layout chosen at
6360
+ * creation. See `GpuMesh.update`.
6111
6361
  */
6112
6362
  updateMesh(mesh, positions, normals) {
6113
6363
  if (mesh.update === null) {
6114
- throw new Error('this mesh was not created with { dynamic: true }, so the interleaved vertex data it ' +
6115
- 'would be patched from was not kept. Say so at `createMesh` — the flag is what decides ' +
6116
- 'what is kept and what a buffer is hinted as.');
6364
+ throw new Error('this mesh was not created with { dynamic: true }, so its positions share one buffer with ' +
6365
+ 'every other attribute and cannot be rewritten alone. Say so at `createMesh` — the flag ' +
6366
+ 'decides the layout, and what a buffer is hinted as.');
6117
6367
  }
6118
6368
  /*
6119
- * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
6369
+ * **The positions before this rewrite are last frame's**, kept only for a reconstruction and only
6120
6370
  * on the first rewrite of a frame — a second must not replace where the vertices were last
6121
6371
  * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
6122
6372
  * `changeFrames.ts`.
@@ -6248,8 +6498,13 @@ export class WebGPURenderer {
6248
6498
  * these per draw through the material block, the same shape `uOpacity` already uses.
6249
6499
  */
6250
6500
  i[at('uLightingEnabled')] = 1;
6251
- i[at('uFogEnabled')] = 1;
6252
- i[at('uAlbedoEnabled')] = 0;
6501
+ i[at('uFogEnabled')] = FOG_RECEDE;
6502
+ this.surfaceFog = FOG_RECEDE;
6503
+ /* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
6504
+ i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
6505
+ f[at('uCutout')] = 0;
6506
+ f[at('uCutout') + 1] = 0;
6507
+ this.cutoutResolveStaged = 'hard';
6253
6508
  this.materialHasMaps = false;
6254
6509
  /*
6255
6510
  * **The camera medium, which this pass did not upload until 2026-08-29.**
@@ -6380,7 +6635,7 @@ export class WebGPURenderer {
6380
6635
  /*
6381
6636
  * The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
6382
6637
  *
6383
- * `uOrmEnabled` needs no line here: an unwritten int is zero, and zero is exactly "no map".
6638
+ * The ORM map's flag is zeroed with the others above, and zero is exactly "no map".
6384
6639
  * These three are the opposite case, the one this block's own header is about: a scale of
6385
6640
  * zero is a real and wrong value, so leaving them silent would not switch the map off, it
6386
6641
  * would read every channel as its floor the moment one was bound.
@@ -6666,6 +6921,8 @@ export class WebGPURenderer {
6666
6921
  this.bindGroup = this.blankAlbedoBindGroup;
6667
6922
  this.uvScale[0] = 1;
6668
6923
  this.uvScale[1] = 1;
6924
+ this.uvOffset[0] = 0;
6925
+ this.uvOffset[1] = 0;
6669
6926
  }
6670
6927
  /**
6671
6928
  * The slot the next draw's material lives in, taking a new one when something dirtied it.
@@ -6744,7 +7001,10 @@ export class WebGPURenderer {
6744
7001
  if (this.tracksMotionNow())
6745
7002
  this.recordDrawMotion(mesh, model, previousModel);
6746
7003
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
6747
- this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
7004
+ /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
7005
+ true, which is a translucent draw's default and would put every opaque draw back in the fog
7006
+ `setSurfaceFog(false)` took it out of. */
7007
+ this.submitMesh(mesh, model, tint, 1, false, { depthLayer, fog: this.surfaceFog !== FOG_OFF });
6748
7008
  }
6749
7009
  /**
6750
7010
  * Draw a mesh that lets the world through, at `opacity`.
@@ -6894,6 +7154,7 @@ export class WebGPURenderer {
6894
7154
  /* The material's repeats, not a constant: geometry authored in metres is textured at
6895
7155
  whatever density the material asks for without rebuilding it. */
6896
7156
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7157
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
6897
7158
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
6898
7159
  /*
6899
7160
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
@@ -6926,7 +7187,7 @@ export class WebGPURenderer {
6926
7187
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
6927
7188
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
6928
7189
  const toneMapped = options.toneMapped ?? true;
6929
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
7190
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
6930
7191
  if (unlitOrUnfogged) {
6931
7192
  if (!lit)
6932
7193
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
@@ -6947,11 +7208,20 @@ export class WebGPURenderer {
6947
7208
  * before last. That is the defined state the two-backends rule asks for.
6948
7209
  */
6949
7210
  const refracting = this.bindSeeThrough(options);
7211
+ /* A dithered cutout tests hard on a blended draw, which has nothing to average a pattern with;
7212
+ put back below with the rest. See `cutoutDither.ts`. */
7213
+ const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
7214
+ if (ditheredBlend)
7215
+ this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
6950
7216
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
6951
7217
  backends count material changes by. See `materialChanges.ts`. */
6952
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
7218
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
6953
7219
  if (own)
6954
7220
  this.materials.dirty();
7221
+ /* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
7222
+ const modelKind = this.materialModel?.kind ?? null;
7223
+ if (modelKind === 'eye')
7224
+ this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
6955
7225
  const base = mesh.key ?? 'flat:s0:u0';
6956
7226
  /*
6957
7227
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
@@ -6963,7 +7233,12 @@ export class WebGPURenderer {
6963
7233
  const skinned = this.skinPaletteSlot >= 0 && mesh.isSkinned;
6964
7234
  const morphed = this.morphWeights !== null && mesh.morph !== null;
6965
7235
  const deltas = morphed && mesh.morph !== null ? mesh.morph.view() : null;
6966
- const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? '|skin' : ''}`;
7236
+ const eight = skinned && mesh.isSkinnedEight;
7237
+ /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
7238
+ binding was made. See `createClothBinding`. */
7239
+ const clothed = skinned && this.cloth !== null;
7240
+ const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
7241
+ `${clothed ? '|cloth' : ''}`;
6967
7242
  /*
6968
7243
  * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
6969
7244
  * the key: two draws of one mesh differing only in these would otherwise share a cached
@@ -6978,13 +7253,22 @@ export class WebGPURenderer {
6978
7253
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
6979
7254
  /* Added rather than blended over: its own blend state, so its own pipeline. */
6980
7255
  const adds = blend && options.additive === true;
7256
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
7257
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
7258
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
6981
7259
  const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
6982
7260
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
6983
7261
  /* The two order-independent buffers are two formats and two blend states, so a draw into
6984
7262
  one must never share a cached pipeline with the same mesh drawn into the other. */
6985
7263
  `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
6986
7264
  /* Culling is pipeline state here: a two-sided surface culls nothing. */
6987
- `${this.materialDoubleSided ? '|2s' : ''}`;
7265
+ `${this.materialDoubleSided ? '|2s' : ''}` +
7266
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
7267
+ `${coverage ? '|a2c' : ''}` +
7268
+ /* And the material's shading model, which is the fragment stage's own switch. */
7269
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
7270
+ /* And which half of a skin it draws, under the screen-space blur. */
7271
+ `${splits ? '|ss' : ''}`;
6988
7272
  /*
6989
7273
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
6990
7274
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -7014,7 +7298,7 @@ export class WebGPURenderer {
7014
7298
  if (present === undefined) {
7015
7299
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
7016
7300
  }
7017
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
7301
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided, coverage, eight, clothed, modelKind, splits ? 'scene' : 'whole');
7018
7302
  }
7019
7303
  /*
7020
7304
  * Guards the draw call only, not the restore below.
@@ -7068,6 +7352,9 @@ export class WebGPURenderer {
7068
7352
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
7069
7353
  pass.drawIndexed(mesh.indexCount);
7070
7354
  }
7355
+ if (splits) {
7356
+ this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
7357
+ }
7071
7358
  }
7072
7359
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
7073
7360
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -7079,7 +7366,7 @@ export class WebGPURenderer {
7079
7366
  if (unlitOrUnfogged) {
7080
7367
  if (!lit)
7081
7368
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
7082
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
7369
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
7083
7370
  }
7084
7371
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
7085
7372
  shared, so a strength left set is worn by everything drawn after it. */
@@ -7088,6 +7375,10 @@ export class WebGPURenderer {
7088
7375
  if (!toneMapped) {
7089
7376
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
7090
7377
  }
7378
+ if (ditheredBlend) {
7379
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
7380
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
7381
+ }
7091
7382
  if (own)
7092
7383
  this.materials.dirty();
7093
7384
  }
@@ -7284,6 +7575,7 @@ export class WebGPURenderer {
7284
7575
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7285
7576
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, lands ? this.correctedViewProj : this.viewProj);
7286
7577
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7578
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7287
7579
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7288
7580
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
7289
7581
  const dimmed = opacity < 1;
@@ -7293,7 +7585,7 @@ export class WebGPURenderer {
7293
7585
  const lit = options.lit ?? true;
7294
7586
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
7295
7587
  const toneMapped = options.toneMapped ?? true;
7296
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
7588
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
7297
7589
  if (unlitOrUnfogged) {
7298
7590
  if (!lit)
7299
7591
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
@@ -7306,9 +7598,17 @@ export class WebGPURenderer {
7306
7598
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7307
7599
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
7308
7600
  const refracting = blend && this.bindSeeThrough(options);
7309
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
7601
+ /* As a single draw: a blended batch tests a dithered cutout hard, an opaque one in a
7602
+ multisampled frame takes coverage. See `cutoutDither.ts`. */
7603
+ const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
7604
+ if (ditheredBlend)
7605
+ this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
7606
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
7607
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
7310
7608
  if (own)
7311
7609
  this.materials.dirty();
7610
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
7611
+ const modelKind = this.materialModel?.kind ?? null;
7312
7612
  const base = mesh.key ?? 'flat:s0:u0';
7313
7613
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
7314
7614
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
@@ -7316,7 +7616,8 @@ export class WebGPURenderer {
7316
7616
  const adds = blend && options.additive === true;
7317
7617
  const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
7318
7618
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7319
- `${this.materialDoubleSided ? '|2s' : ''}`;
7619
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
7620
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
7320
7621
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
7321
7622
  let pipeline = pipelines.peek(key);
7322
7623
  if (pipeline === undefined) {
@@ -7324,7 +7625,7 @@ export class WebGPURenderer {
7324
7625
  if (present === undefined) {
7325
7626
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
7326
7627
  }
7327
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
7628
+ pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind);
7328
7629
  }
7329
7630
  const material = this.materialSlotForDraw();
7330
7631
  if (material !== null) {
@@ -7375,13 +7676,17 @@ export class WebGPURenderer {
7375
7676
  }
7376
7677
  if (unlitOrUnfogged) {
7377
7678
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
7378
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
7679
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
7379
7680
  }
7380
7681
  if (!toneMapped) {
7381
7682
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
7382
7683
  }
7383
7684
  if (refracting)
7384
7685
  this.releaseSeeThrough();
7686
+ if (ditheredBlend) {
7687
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
7688
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
7689
+ }
7385
7690
  if (own)
7386
7691
  this.materials.dirty();
7387
7692
  }
@@ -7599,12 +7904,16 @@ export class WebGPURenderer {
7599
7904
  this.bindSceneCasterMaterial(material);
7600
7905
  this.drawMesh(mesh, model);
7601
7906
  },
7602
- skinnedMesh: (mesh, model, palette, material) => {
7907
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
7603
7908
  this.bindSceneCasterMaterial(material);
7604
7909
  this.setSkinPalette(palette);
7910
+ if (cloth !== undefined) {
7911
+ this.setCloth(cloth.binding, cloth.particles);
7912
+ }
7605
7913
  this.drawMesh(mesh, model);
7606
7914
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
7607
7915
  this.setSkinPalette(null);
7916
+ this.setCloth(null);
7608
7917
  },
7609
7918
  instanced: (batch, data, material) => {
7610
7919
  this.bindSceneCasterMaterial(material);
@@ -8354,14 +8663,20 @@ export class WebGPURenderer {
8354
8663
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
8355
8664
  slot of its own. See `skinnedGroups`. */
8356
8665
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
8666
+ /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
8667
+ const cloth = skinned ? this.clothViews() : null;
8357
8668
  if ((this.skinnedGroups[at] ?? null) === null ||
8358
8669
  this.skinnedGroupsBeside[at] !== this.bindGroup ||
8359
8670
  this.skinnedGroupsPalette[at] !== palette ||
8360
- this.skinnedGroupsDeltas[at] !== deltas) {
8361
- this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas);
8671
+ this.skinnedGroupsDeltas[at] !== deltas ||
8672
+ this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
8673
+ this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
8674
+ this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
8362
8675
  this.skinnedGroupsBeside[at] = this.bindGroup;
8363
8676
  this.skinnedGroupsPalette[at] = palette;
8364
8677
  this.skinnedGroupsDeltas[at] = deltas;
8678
+ this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
8679
+ this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
8365
8680
  }
8366
8681
  return this.skinnedGroups[at];
8367
8682
  }
@@ -8430,16 +8745,19 @@ export class WebGPURenderer {
8430
8745
  const normalMap = this.normalMap;
8431
8746
  const ormMap = this.ormMap;
8432
8747
  const emissiveMap = this.emissiveMap;
8748
+ const modelMap = this.modelMap;
8433
8749
  this.albedo = null;
8434
8750
  this.normalMap = null;
8435
8751
  this.ormMap = null;
8436
8752
  this.emissiveMap = null;
8753
+ this.modelMap = null;
8437
8754
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
8438
8755
  this.albedo = albedo;
8439
8756
  this.normalMap = normalMap;
8440
8757
  this.ormMap = ormMap;
8441
8758
  this.emissiveMap = emissiveMap;
8442
- this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap);
8759
+ this.modelMap = modelMap;
8760
+ this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
8443
8761
  /* The open material lives in a slot of a buffer that no longer exists. */
8444
8762
  this.materials.dirty();
8445
8763
  }
@@ -8477,13 +8795,14 @@ export class WebGPURenderer {
8477
8795
  if (this.albedo === null &&
8478
8796
  this.normalMap === null &&
8479
8797
  this.ormMap === null &&
8480
- this.emissiveMap === null) {
8798
+ this.emissiveMap === null &&
8799
+ this.modelMap === null) {
8481
8800
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
8482
8801
  this.bindGroupBudget.ask();
8483
8802
  this.bindGroup = this.blankAlbedoBindGroup;
8484
8803
  return;
8485
8804
  }
8486
- this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap);
8805
+ this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
8487
8806
  }
8488
8807
  /**
8489
8808
  * The sky, as one triangle covering the screen with the world reconstructed per fragment.
@@ -9142,6 +9461,9 @@ export class WebGPURenderer {
9142
9461
  * the only moment they can land; everything else keeps the path it has.
9143
9462
  */
9144
9463
  recordBlended(reads, lands = this.drawsLate()) {
9464
+ /* Skin is spread and added before anything blended lands over it. See `runSkinScatter`. */
9465
+ if (this.skinScatter !== null && !this.skinScattered)
9466
+ this.runSkinScatter();
9145
9467
  const late = this.latePass;
9146
9468
  if (late !== null && lands)
9147
9469
  return late.take();
@@ -9207,6 +9529,14 @@ export class WebGPURenderer {
9207
9529
  const dynamic = mesh.motion;
9208
9530
  const rows = dynamic?.previous ?? null;
9209
9531
  const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
9532
+ const cloth = this.cloth;
9533
+ if (cloth !== null &&
9534
+ this.skinPaletteSlot >= 0 &&
9535
+ mesh.isSkinned &&
9536
+ mesh.skinOffsets !== null) {
9537
+ this.recordClothMotion(mesh, model, previous, cloth);
9538
+ return;
9539
+ }
9210
9540
  if (previous === null) {
9211
9541
  if (rewritten)
9212
9542
  this.motionPass?.recordDynamic(mesh, model, model, rows);
@@ -9251,6 +9581,41 @@ export class WebGPURenderer {
9251
9581
  return;
9252
9582
  this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
9253
9583
  }
9584
+ /**
9585
+ * A cloth-bound draw's motion. **Recorded even where the caller named no previous place**, as a
9586
+ * rewritten mesh's is: the particles carry their own past, and a garment swinging on a character
9587
+ * standing still is motion nobody else can see. Where the rig is a `Mover` its last pose is taken
9588
+ * as a skinned mover's is; otherwise the rig is taken not to have moved.
9589
+ */
9590
+ recordClothMotion(mesh, model, previous, cloth) {
9591
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
9592
+ if (now === null)
9593
+ return;
9594
+ let was = now;
9595
+ let wasModel = model;
9596
+ if (previous !== null && isMover(previous)) {
9597
+ const verdict = advanceMover(previous, this.moverRenderer, this.moverFrame, model, this.skinPaletteData);
9598
+ if (verdict === MOVER_TWICE) {
9599
+ this.warnMoverTwice();
9600
+ return;
9601
+ }
9602
+ if (verdict === MOVER_MOTION) {
9603
+ const slot = this.skinPalettes.take(this.surface.device, previous.previousPalette);
9604
+ was = slot === null ? null : this.skinPalettes.view(slot);
9605
+ wasModel = previous.previousModel;
9606
+ }
9607
+ }
9608
+ else if (previous !== null) {
9609
+ wasModel = previous;
9610
+ }
9611
+ if (was === null)
9612
+ return;
9613
+ const particlesWas = cloth.particles.previousAt(this.moverFrame);
9614
+ /* Nothing said it moved and nothing did: the camera's motion, derived from depth, is the answer. */
9615
+ if (previous === null && particlesWas === cloth.particles.current)
9616
+ return;
9617
+ this.motionPass?.recordCloth(mesh, model, wasModel, now, was, cloth.binding.bindingView, cloth.particles.current, particlesWas);
9618
+ }
9254
9619
  warnMoverTwice() {
9255
9620
  if (this.warnedMoverTwice)
9256
9621
  return;
@@ -9583,7 +9948,7 @@ export class WebGPURenderer {
9583
9948
  label: 'post.ssr.resolve',
9584
9949
  colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
9585
9950
  });
9586
- resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrResolveLayout, 'post.ssrResolve', SSR_RESOLVE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND));
9951
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrResolveLayout, 'post.ssrResolve', SSR_RESOLVE_FRAG_WGSL, this.pipelines.format, REFLECTION_OVER_BLEND));
9587
9952
  resolve.setBindGroup(0, resolveGroup);
9588
9953
  resolve.draw(3);
9589
9954
  resolve.end();
@@ -10173,6 +10538,106 @@ export class WebGPURenderer {
10173
10538
  this.refractSnapshotTaken = true;
10174
10539
  return true;
10175
10540
  }
10541
+ /**
10542
+ * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
10543
+ * skin yet, and this is the frame's own picture — not a mirror's, a probe's, an overlay after
10544
+ * the frame, or an order-independent set, none of which the spread reads.
10545
+ */
10546
+ skinSplits() {
10547
+ return (this.skinScatter !== null &&
10548
+ !this.skinScattered &&
10549
+ !this.reflectionPassActive &&
10550
+ !this.probePassActive &&
10551
+ !this.framePresented &&
10552
+ !this.overlayActive &&
10553
+ this.oitMode === 'none');
10554
+ }
10555
+ /**
10556
+ * A skin draw's diffuse half, kept for the spread: the frame's draw again, with the same slots,
10557
+ * buffers and bind group, through the pipeline that writes the diffuse alone into the blur's
10558
+ * target and tests for the depth its frame half wrote.
10559
+ */
10560
+ keepSkinDiffuse(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, slot, material) {
10561
+ const skin = this.skinScatter;
10562
+ const pipelines = this.skinPipelines;
10563
+ const present = this.meshPresent.get(base);
10564
+ if (skin === null || pipelines === null || present === undefined)
10565
+ return;
10566
+ const key = `${frameKey}|sd`;
10567
+ const pipeline = pipelines.peek(key) ??
10568
+ flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, false, 'none', this.materialDoubleSided, false, eight, clothed, 'skin', 'diffuse');
10569
+ const command = skin.take();
10570
+ command.pipeline = pipeline;
10571
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
10572
+ command.offsetA = slot;
10573
+ command.offsetB = material;
10574
+ command.offsetCount = 2;
10575
+ for (let index = 0; index < mesh.vertexBuffers.length; index++) {
10576
+ command.vertexBuffers[index] = mesh.vertexBuffers[index];
10577
+ }
10578
+ command.vertexCount = mesh.vertexBuffers.length;
10579
+ command.indexBuffer = mesh.indexBuffer;
10580
+ command.indexed = true;
10581
+ command.count = mesh.indexCount;
10582
+ command.instances = 1;
10583
+ command.indirect = null;
10584
+ }
10585
+ /**
10586
+ * Spread the frame's skin and add it back: once, at the first blended draw or before the frame's
10587
+ * last flush. **A fifth instance of the boundary `takeRefractSnapshot` describes** — flush, end
10588
+ * the pass, work between passes, reopen with `loadOp: 'load'` — and refused where that one is,
10589
+ * for its reasons. Between the passes: the kept diffuse halves into their target against the
10590
+ * frame's depth, the depth resolved, and `SkinScatterPass.spread` across and then down into the
10591
+ * frame's colour.
10592
+ */
10593
+ runSkinScatter() {
10594
+ const skin = this.skinScatter;
10595
+ if (skin === null || this.skinScattered)
10596
+ return;
10597
+ /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
10598
+ this.skinScattered = true;
10599
+ if (!skin.pending)
10600
+ return;
10601
+ if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
10602
+ return;
10603
+ const encoder = this.encoder;
10604
+ const depth = this.depthView;
10605
+ const resolved = this.resolvedDepthView;
10606
+ const target = this.compositeTarget();
10607
+ const projection = this.frameProjection;
10608
+ if (encoder === null || depth === null || resolved === null)
10609
+ return;
10610
+ if (target === null || projection === null)
10611
+ return;
10612
+ if (this.pass === null && this.openPass() === null)
10613
+ return;
10614
+ if (this.pass === null)
10615
+ return;
10616
+ if (this.quality.frameGraph)
10617
+ this.flushGraph();
10618
+ this.pass.end();
10619
+ const size = this.sceneSize();
10620
+ skin.size(size.width, size.height, this.samples);
10621
+ skin.drawDiffuse(encoder, depth, this.issueBound, this.gpuTimer.writesFor());
10622
+ this.resolveDepth(encoder);
10623
+ mat4.invert(this.skinInverseProjection, projection);
10624
+ const multisampled = this.compositeMsaa();
10625
+ skin.spread(encoder, this.pipelines, resolved, multisampled ?? target, multisampled === null ? null : target, this.samples, projection, this.skinInverseProjection, () => this.gpuTimer.writesFor());
10626
+ this.pass = encoder.beginRenderPass({
10627
+ label: 'skin.scattered',
10628
+ timestampWrites: this.gpuTimer.writesFor(),
10629
+ colorAttachments: [
10630
+ {
10631
+ view: multisampled ?? target,
10632
+ resolveTarget: multisampled !== null ? target : undefined,
10633
+ /* Loaded, not cleared: the frame so far, with its skin added. */
10634
+ loadOp: 'load',
10635
+ storeOp: resolvedStoreOp(multisampled !== null, false, this.quality.discardResolvedAttachments),
10636
+ },
10637
+ ],
10638
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
10639
+ });
10640
+ }
10176
10641
  takeVolumeDepth() {
10177
10642
  /*
10178
10643
  * **Never while the mirror is open, and this is a correctness fix rather than a saving.**
@@ -10795,6 +11260,8 @@ export class WebGPURenderer {
10795
11260
  * `beginFrame` resets it. Silent, and exactly the kind of loss the arena is designed
10796
11261
  * against everywhere else.
10797
11262
  */
11263
+ /* Before the last flush, which may let the depth go that the spread still reads. */
11264
+ this.runSkinScatter();
10798
11265
  this.flushGraph('boundary', this.liveOutFinalFlush());
10799
11266
  /*
10800
11267
  * `ensurePass` rather than a null check: the frame's pass is opened on demand now, so a
@@ -13612,6 +14079,7 @@ export class WebGPURenderer {
13612
14079
  * the 2026-08-13 rule asks for — one decision, bound twice.
13613
14080
  */
13614
14081
  dispose() {
14082
+ this.skinScatter?.dispose();
13615
14083
  this.motionPass?.dispose();
13616
14084
  this.motionPass = null;
13617
14085
  this.latePass?.dispose();
@@ -13651,3 +14119,11 @@ export class WebGPURenderer {
13651
14119
  this.surface.dispose();
13652
14120
  }
13653
14121
  }
14122
+ /**
14123
+ * The views a binding and a character's particles bind as, built once a pair and kept on the
14124
+ * particles: a bind group is cached by its views' identity, so a fresh object a draw would rebuild
14125
+ * the group every draw.
14126
+ */
14127
+ function clothViewsOf(binding, particles) {
14128
+ return particles.viewsWith(binding);
14129
+ }