@driftengine/core 4.8.3 → 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 (178) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +10 -4
  5. package/dist/index.js +16 -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/select.js +8 -0
  11. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  12. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  13. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  14. package/dist/render/backend/webgl2/renderer.js +604 -63
  15. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  16. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  17. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  18. package/dist/render/backend/webgpu/buffers.js +98 -49
  19. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  20. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  21. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  22. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  24. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  25. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  26. package/dist/render/backend/webgpu/computeHost.js +5 -0
  27. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  28. package/dist/render/backend/webgpu/depthPass.js +39 -9
  29. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  30. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  31. package/dist/render/backend/webgpu/flatPass.js +109 -12
  32. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  34. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  35. package/dist/render/backend/webgpu/motionPass.js +106 -9
  36. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  37. package/dist/render/backend/webgpu/postPass.js +6 -3
  38. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  39. package/dist/render/backend/webgpu/renderer.js +498 -48
  40. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  42. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  44. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  45. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  46. package/dist/render/clothBindingData.d.ts +41 -0
  47. package/dist/render/clothBindingData.js +72 -0
  48. package/dist/render/clothPlacement.d.ts +25 -0
  49. package/dist/render/clothPlacement.js +85 -0
  50. package/dist/render/compressedSource.d.ts +66 -0
  51. package/dist/render/compressedSource.js +138 -0
  52. package/dist/render/cutoutCaster.d.ts +2 -0
  53. package/dist/render/cutoutCaster.js +2 -0
  54. package/dist/render/cutoutDither.d.ts +51 -0
  55. package/dist/render/cutoutDither.js +90 -0
  56. package/dist/render/eyeRefraction.d.ts +35 -0
  57. package/dist/render/eyeRefraction.js +40 -0
  58. package/dist/render/glCompressed.d.ts +27 -0
  59. package/dist/render/glCompressed.js +53 -0
  60. package/dist/render/hairLobes.d.ts +70 -0
  61. package/dist/render/hairLobes.js +120 -0
  62. package/dist/render/lightBudget.d.ts +22 -0
  63. package/dist/render/lightBudget.js +22 -0
  64. package/dist/render/mesh.d.ts +7 -0
  65. package/dist/render/mesh.js +24 -0
  66. package/dist/render/recon/motionVectors.d.ts +4 -2
  67. package/dist/render/recon/motionVectors.js +6 -3
  68. package/dist/render/renderQuality.d.ts +20 -0
  69. package/dist/render/renderQuality.js +5 -0
  70. package/dist/render/rendererHarness.js +4 -0
  71. package/dist/render/sceneTarget.d.ts +12 -0
  72. package/dist/render/sceneTarget.js +14 -0
  73. package/dist/render/shaders/clothBinding.d.ts +22 -0
  74. package/dist/render/shaders/clothBinding.js +80 -0
  75. package/dist/render/shaders/depth.js +42 -6
  76. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  77. package/dist/render/shaders/flat/anisotropic.js +68 -0
  78. package/dist/render/shaders/flat/eye.d.ts +1 -0
  79. package/dist/render/shaders/flat/eye.js +81 -0
  80. package/dist/render/shaders/flat/hair.d.ts +1 -0
  81. package/dist/render/shaders/flat/hair.js +101 -0
  82. package/dist/render/shaders/flat/index.js +36 -2
  83. package/dist/render/shaders/flat/main.d.ts +1 -1
  84. package/dist/render/shaders/flat/main.js +113 -17
  85. package/dist/render/shaders/flat/models.d.ts +14 -0
  86. package/dist/render/shaders/flat/models.js +273 -0
  87. package/dist/render/shaders/flat/preamble.js +26 -24
  88. package/dist/render/shaders/flat/skin.d.ts +1 -0
  89. package/dist/render/shaders/flat/skin.js +114 -0
  90. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  91. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  92. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  93. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  94. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  95. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  96. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  97. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  98. package/dist/render/shaders/skinBlur.d.ts +1 -0
  99. package/dist/render/shaders/skinBlur.js +103 -0
  100. package/dist/render/shaders/skinning.d.ts +3 -1
  101. package/dist/render/shaders/skinning.js +31 -4
  102. package/dist/render/shadowCasters.d.ts +13 -1
  103. package/dist/render/skinBlur.d.ts +41 -0
  104. package/dist/render/skinBlur.js +49 -0
  105. package/dist/render/skinScatter.d.ts +67 -0
  106. package/dist/render/skinScatter.js +80 -0
  107. package/dist/render/skinnedClothRun.d.ts +26 -0
  108. package/dist/render/skinnedClothRun.js +84 -0
  109. package/dist/render/surfaceModel.d.ts +129 -0
  110. package/dist/render/surfaceModel.js +152 -0
  111. package/dist/render/surfaceTexture.d.ts +44 -4
  112. package/dist/render/surfaceTexture.js +60 -46
  113. package/dist/render/vertexDefaults.js +7 -0
  114. package/package.json +6 -6
  115. package/src/geometry/meshConcat.ts +2 -0
  116. package/src/index.ts +40 -1
  117. package/src/math/scalar.ts +1 -1
  118. package/src/render/anisotropicLobe.ts +41 -0
  119. package/src/render/backend/api.ts +52 -3
  120. package/src/render/backend/select.ts +8 -0
  121. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  122. package/src/render/backend/webgl2/renderer.ts +693 -64
  123. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  124. package/src/render/backend/webgpu/buffers.ts +137 -63
  125. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  126. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  127. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  128. package/src/render/backend/webgpu/computeHost.ts +25 -0
  129. package/src/render/backend/webgpu/depthPass.ts +39 -7
  130. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  131. package/src/render/backend/webgpu/flatPass.ts +145 -7
  132. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  133. package/src/render/backend/webgpu/motionPass.ts +128 -9
  134. package/src/render/backend/webgpu/postPass.ts +5 -1
  135. package/src/render/backend/webgpu/renderer.ts +663 -43
  136. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  137. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  138. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  139. package/src/render/clothBindingData.ts +89 -0
  140. package/src/render/clothPlacement.ts +114 -0
  141. package/src/render/compressedSource.ts +198 -0
  142. package/src/render/cutoutCaster.ts +5 -0
  143. package/src/render/cutoutDither.ts +99 -0
  144. package/src/render/eyeRefraction.ts +72 -0
  145. package/src/render/glCompressed.ts +86 -0
  146. package/src/render/hairLobes.ts +188 -0
  147. package/src/render/lightBudget.ts +25 -0
  148. package/src/render/mesh.ts +23 -0
  149. package/src/render/recon/motionVectors.ts +6 -3
  150. package/src/render/renderQuality.ts +28 -0
  151. package/src/render/rendererHarness.ts +4 -0
  152. package/src/render/sceneTarget.ts +21 -0
  153. package/src/render/shaders/clothBinding.ts +81 -0
  154. package/src/render/shaders/depth.ts +42 -6
  155. package/src/render/shaders/flat/anisotropic.ts +68 -0
  156. package/src/render/shaders/flat/eye.ts +82 -0
  157. package/src/render/shaders/flat/hair.ts +102 -0
  158. package/src/render/shaders/flat/index.ts +36 -2
  159. package/src/render/shaders/flat/main.ts +113 -17
  160. package/src/render/shaders/flat/models.ts +286 -0
  161. package/src/render/shaders/flat/preamble.ts +26 -24
  162. package/src/render/shaders/flat/skin.ts +116 -0
  163. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  164. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  165. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  166. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  167. package/src/render/shaders/skinBlur.ts +105 -0
  168. package/src/render/shaders/skinning.ts +31 -4
  169. package/src/render/shadowCasters.ts +13 -0
  170. package/src/render/skinBlur.ts +53 -0
  171. package/src/render/skinScatter.ts +92 -0
  172. package/src/render/skinnedClothRun.ts +128 -0
  173. package/src/render/surfaceModel.ts +274 -0
  174. package/src/render/surfaceTexture.ts +123 -63
  175. package/src/render/vertexDefaults.ts +7 -0
  176. package/dist/math/exact.d.ts +0 -76
  177. package/dist/math/exact.js +0 -327
  178. 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';
@@ -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';
@@ -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
  *
@@ -2143,11 +2182,21 @@ export class WebGPURenderer {
2143
2182
  /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
2144
2183
  materialHasMaps = false;
2145
2184
  createSurfaceTexture(source, options = {}) {
2146
- 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;
2147
2196
  }
2148
2197
  /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2149
2198
  createSurfaceTextureArray(sources, options = {}) {
2150
- return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2199
+ return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options, this.compressedFormats);
2151
2200
  }
2152
2201
  /**
2153
2202
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
@@ -2198,6 +2247,8 @@ export class WebGPURenderer {
2198
2247
  this.ormMap = null;
2199
2248
  if (this.emissiveMap === texture)
2200
2249
  this.emissiveMap = null;
2250
+ if (this.modelMap === texture)
2251
+ this.modelMap = null;
2201
2252
  this.forgetBindingsOf(texture);
2202
2253
  texture.dispose();
2203
2254
  }
@@ -2222,7 +2273,7 @@ export class WebGPURenderer {
2222
2273
  this.albedo = null;
2223
2274
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
2224
2275
  this.albedo = held;
2225
- 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);
2226
2277
  }
2227
2278
  /**
2228
2279
  * Choose the surface texture the following `drawMesh` calls sample, or null for none.
@@ -2250,6 +2301,104 @@ export class WebGPURenderer {
2250
2301
  one. See `SkinPaletteRing`; a full ring declines the draw instead of mixing two rigs. */
2251
2302
  this.skinPaletteSlot = this.skinPalettes.take(this.surface.device, palette) ?? -1;
2252
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
+ }
2253
2402
  /**
2254
2403
  * Choose the morph weights the following `drawMesh` calls deform by, or null for none.
2255
2404
  *
@@ -2274,14 +2423,32 @@ export class WebGPURenderer {
2274
2423
  const normal = (material?.normal ?? null);
2275
2424
  const orm = (material?.orm ?? null);
2276
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
+ }
2277
2433
  this.material((f, i) => {
2278
- i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
2279
- f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
2280
- 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;
2281
2448
  /* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
2282
2449
  f[this.materialField('uNormalStrength')] =
2283
2450
  normal === null ? 0 : (material?.normalStrength ?? 1);
2284
- i[this.materialField('uOrmEnabled')] = orm === null ? 0 : 1;
2451
+ i[flags + 1] = orm === null ? 0 : 1;
2285
2452
  /*
2286
2453
  * Component-aligned with the map — r occlusion, g roughness, b metallic — so this order is
2287
2454
  * the channel order rather than the field order of `SurfaceMaterial`. Written component by
@@ -2292,7 +2459,7 @@ export class WebGPURenderer {
2292
2459
  f[scale] = material?.occlusionStrength ?? 1;
2293
2460
  f[scale + 1] = material?.roughnessScale ?? 1;
2294
2461
  f[scale + 2] = material?.metallicScale ?? 1;
2295
- i[this.materialField('uEmissiveMapEnabled')] = emissiveMap === null ? 0 : 1;
2462
+ i[flags + 2] = emissiveMap === null ? 0 : 1;
2296
2463
  /*
2297
2464
  * One rather than zero on every component, because this multiplies rather than adds: an
2298
2465
  * unwritten float is zero, and zero here would switch off every glow the mesh's own vertex
@@ -2303,6 +2470,9 @@ export class WebGPURenderer {
2303
2470
  f[emissive] = asked?.[0] ?? 1;
2304
2471
  f[emissive + 1] = asked?.[1] ?? 1;
2305
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'));
2306
2476
  });
2307
2477
  /*
2308
2478
  * **One lookup for the whole material, and nothing thrown away.**
@@ -2316,14 +2486,16 @@ export class WebGPURenderer {
2316
2486
  if (this.albedo === albedo &&
2317
2487
  this.normalMap === normal &&
2318
2488
  this.ormMap === orm &&
2319
- this.emissiveMap === emissiveMap) {
2489
+ this.emissiveMap === emissiveMap &&
2490
+ this.modelMap === modelMap) {
2320
2491
  return;
2321
2492
  }
2322
2493
  this.albedo = albedo;
2323
2494
  this.normalMap = normal;
2324
2495
  this.ormMap = orm;
2325
2496
  this.emissiveMap = emissiveMap;
2326
- this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap);
2497
+ this.modelMap = modelMap;
2498
+ this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap, modelMap);
2327
2499
  }
2328
2500
  /**
2329
2501
  * @deprecated Use `setMaterial`. See `renderer.ts` for why this stays: 56 call sites across four
@@ -2345,8 +2517,14 @@ export class WebGPURenderer {
2345
2517
  * asking. Passing them in as well is deliberate: it is what makes the key and the group agree by
2346
2518
  * construction rather than by the caller having remembered.
2347
2519
  */
2348
- flatGroupForMaps(albedo, normal, orm, emissive) {
2349
- 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) {
2350
2528
  return this.blankAlbedoBindGroup;
2351
2529
  }
2352
2530
  let byNormal = this.flatBindGroups.get(albedo);
@@ -2364,12 +2542,17 @@ export class WebGPURenderer {
2364
2542
  byEmissive = new Map();
2365
2543
  byOrm.set(orm, byEmissive);
2366
2544
  }
2367
- 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);
2368
2551
  if (existing !== undefined)
2369
2552
  return existing;
2370
2553
  const built = this.buildFlatBindGroup();
2371
2554
  this.bindGroupBudget.ask();
2372
- byEmissive.set(emissive, built);
2555
+ byModel.set(modelMap, built);
2373
2556
  return built;
2374
2557
  }
2375
2558
  /* -- Material state ------------------------------------------------------------------ */
@@ -4265,6 +4448,8 @@ export class WebGPURenderer {
4265
4448
  shadowSkinnedGroups = [];
4266
4449
  shadowSkinnedGroupsBeside = [];
4267
4450
  shadowSkinnedGroupsPalette = [];
4451
+ shadowSkinnedGroupsCloth = [];
4452
+ shadowSkinnedGroupsClothBinding = [];
4268
4453
  /** Held from construction so the skinned group can be rebuilt without them being threaded. */
4269
4454
  shadowBlankView = null;
4270
4455
  shadowBlankSampler = null;
@@ -4386,6 +4571,7 @@ export class WebGPURenderer {
4386
4571
  this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
4387
4572
  this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
4388
4573
  this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
4574
+ this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset + 4, cutout.dithered);
4389
4575
  }
4390
4576
  shadowGroup() {
4391
4577
  return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
@@ -4399,14 +4585,18 @@ export class WebGPURenderer {
4399
4585
  * second independently fading occluder can be recorded, and a character is not one — it is the
4400
4586
  * thing in front. Serving it would mean a second cached group for a case nothing produces.
4401
4587
  */
4402
- shadowSkinnedGroup(palette, at) {
4588
+ shadowSkinnedGroup(palette, at, cloth) {
4403
4589
  const beside = this.shadowBindGroup;
4404
4590
  if ((this.shadowSkinnedGroups[at] ?? null) === null ||
4405
4591
  this.shadowSkinnedGroupsBeside[at] !== beside ||
4406
- this.shadowSkinnedGroupsPalette[at] !== palette) {
4407
- 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);
4408
4596
  this.shadowSkinnedGroupsBeside[at] = beside;
4409
4597
  this.shadowSkinnedGroupsPalette[at] = palette;
4598
+ this.shadowSkinnedGroupsCloth[at] = cloth.particles;
4599
+ this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
4410
4600
  }
4411
4601
  return this.shadowSkinnedGroups[at];
4412
4602
  }
@@ -4445,6 +4635,8 @@ export class WebGPURenderer {
4445
4635
  void this.overlayPipelines.enable(feature);
4446
4636
  if (this.latePipelines !== this.pipelines)
4447
4637
  void this.latePipelines.enable(feature);
4638
+ if (this.skinPipelines !== null)
4639
+ void this.skinPipelines.enable(feature);
4448
4640
  }
4449
4641
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
4450
4642
  glassCasterScratch = {
@@ -4535,7 +4727,7 @@ export class WebGPURenderer {
4535
4727
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
4536
4728
  pass.drawIndexed(geometry.indexCount, count);
4537
4729
  },
4538
- skinnedMesh: (mesh, model, palette, material) => {
4730
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
4539
4731
  if (!this.drawingGlassDepth && !castsDepth(material)) {
4540
4732
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
4541
4733
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -4565,8 +4757,13 @@ export class WebGPURenderer {
4565
4757
  const geometry = mesh;
4566
4758
  if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
4567
4759
  return;
4568
- pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowSkinnedLayout, `depth-skinned|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, true));
4569
- 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]);
4570
4767
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
4571
4768
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
4572
4769
  }
@@ -4899,6 +5096,18 @@ export class WebGPURenderer {
4899
5096
  this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
4900
5097
  ? this.pipelines
4901
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;
4902
5111
  this.issueBound = this.issueCommand.bind(this);
4903
5112
  this.pipelineTargets =
4904
5113
  this.overlayPipelines === this.pipelines
@@ -4953,7 +5162,7 @@ export class WebGPURenderer {
4953
5162
  */
4954
5163
  this.perFrame = new UniformRing(device, this.fragment.uniformSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.fragRing', this.materialBudget);
4955
5164
  /*
4956
- * 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
4957
5166
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
4958
5167
  * requires every declared binding to be filled. WebGL2 keeps an `emptyTexture2D` for
4959
5168
  * exactly the same reason.
@@ -5157,6 +5366,13 @@ export class WebGPURenderer {
5157
5366
  ? { view: blankSurfaceView, sampler: blankSampler }
5158
5367
  : { view: map.view, sampler: map.sampler };
5159
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
+ }
5160
5376
  if (name === 'uOrmMap') {
5161
5377
  const map = this.ormMap;
5162
5378
  return map === null
@@ -5732,6 +5948,8 @@ export class WebGPURenderer {
5732
5948
  }
5733
5949
  /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
5734
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');
5735
5953
  return present;
5736
5954
  }
5737
5955
  /**
@@ -5744,6 +5962,7 @@ export class WebGPURenderer {
5744
5962
  */
5745
5963
  beginFrame(clearColor) {
5746
5964
  this.cullFrame += 1;
5965
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
5747
5966
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
5748
5967
  for (const texture of this.retiredTextures)
5749
5968
  texture.destroy();
@@ -5807,6 +6026,8 @@ export class WebGPURenderer {
5807
6026
  this.volumeDepthTaken = false;
5808
6027
  /* Last frame's snapshot is last frame's scene. */
5809
6028
  this.refractSnapshotTaken = false;
6029
+ this.skinScattered = false;
6030
+ this.skinScatter?.reset();
5810
6031
  this.plumeVerts.reset();
5811
6032
  this.plumeFrags.reset();
5812
6033
  this.waterVerts.reset();
@@ -5983,7 +6204,7 @@ export class WebGPURenderer {
5983
6204
  * neither the pipeline set nor the key depends on whether the geometry lands in one call or
5984
6205
  * over six frames.
5985
6206
  */
5986
- warmFlatPipelines(data) {
6207
+ warmFlatPipelines(data, dynamic) {
5987
6208
  /*
5988
6209
  * **Every optional attribute, not the two that come to mind.** `present` decides where
5989
6210
  * each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
@@ -6010,6 +6231,15 @@ export class WebGPURenderer {
6010
6231
  present['layers'] = true;
6011
6232
  key += ':layers';
6012
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
+ }
6013
6243
  const fullKey = `${this.variant}|${key}`;
6014
6244
  /*
6015
6245
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -6065,14 +6295,16 @@ export class WebGPURenderer {
6065
6295
  * guessing which the scene will use.
6066
6296
  */
6067
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;
6068
6300
  const morphable = data.morphTargets !== undefined;
6069
6301
  for (const wantSkin of skinnable ? [false, true] : [false]) {
6070
6302
  for (const wantMorph of morphable ? [false, true] : [false]) {
6071
6303
  if (!wantSkin && !wantMorph)
6072
6304
  continue;
6073
- const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? '|skin' : ''}`;
6305
+ const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? (eight ? '|skin8' : '|skin') : ''}`;
6074
6306
  for (const translucent of [false, true]) {
6075
- 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);
6076
6308
  }
6077
6309
  }
6078
6310
  }
@@ -6093,7 +6325,7 @@ export class WebGPURenderer {
6093
6325
  return keyed;
6094
6326
  }
6095
6327
  createMesh(data, options = {}) {
6096
- const fullKey = this.warmFlatPipelines(data);
6328
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
6097
6329
  return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
6098
6330
  }
6099
6331
  /**
@@ -6106,7 +6338,7 @@ export class WebGPURenderer {
6106
6338
  * can do nothing about. Done with `complete` still false is the signal that it was abandoned.
6107
6339
  */
6108
6340
  createMeshIncremental(data, options = {}) {
6109
- const fullKey = this.warmFlatPipelines(data);
6341
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
6110
6342
  const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, options.dynamic === true);
6111
6343
  const surface = this.surface;
6112
6344
  return {
@@ -6123,18 +6355,18 @@ export class WebGPURenderer {
6123
6355
  /**
6124
6356
  * Rewrite a mesh's positions, and its normals where the caller has them.
6125
6357
  *
6126
- * Refused in words on a mesh that did not declare itself dynamic, because the thing that makes
6127
- * an update possible on this backend — the interleaved array the vertex data was built from —
6128
- * 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`.
6129
6361
  */
6130
6362
  updateMesh(mesh, positions, normals) {
6131
6363
  if (mesh.update === null) {
6132
- throw new Error('this mesh was not created with { dynamic: true }, so the interleaved vertex data it ' +
6133
- 'would be patched from was not kept. Say so at `createMesh` — the flag is what decides ' +
6134
- '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.');
6135
6367
  }
6136
6368
  /*
6137
- * **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
6138
6370
  * on the first rewrite of a frame — a second must not replace where the vertices were last
6139
6371
  * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
6140
6372
  * `changeFrames.ts`.
@@ -6268,7 +6500,11 @@ export class WebGPURenderer {
6268
6500
  i[at('uLightingEnabled')] = 1;
6269
6501
  i[at('uFogEnabled')] = FOG_RECEDE;
6270
6502
  this.surfaceFog = FOG_RECEDE;
6271
- i[at('uAlbedoEnabled')] = 0;
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';
6272
6508
  this.materialHasMaps = false;
6273
6509
  /*
6274
6510
  * **The camera medium, which this pass did not upload until 2026-08-29.**
@@ -6399,7 +6635,7 @@ export class WebGPURenderer {
6399
6635
  /*
6400
6636
  * The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
6401
6637
  *
6402
- * `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".
6403
6639
  * These three are the opposite case, the one this block's own header is about: a scale of
6404
6640
  * zero is a real and wrong value, so leaving them silent would not switch the map off, it
6405
6641
  * would read every channel as its floor the moment one was bound.
@@ -6972,11 +7208,20 @@ export class WebGPURenderer {
6972
7208
  * before last. That is the defined state the two-backends rule asks for.
6973
7209
  */
6974
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;
6975
7216
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
6976
7217
  backends count material changes by. See `materialChanges.ts`. */
6977
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
7218
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
6978
7219
  if (own)
6979
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);
6980
7225
  const base = mesh.key ?? 'flat:s0:u0';
6981
7226
  /*
6982
7227
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
@@ -6988,7 +7233,12 @@ export class WebGPURenderer {
6988
7233
  const skinned = this.skinPaletteSlot >= 0 && mesh.isSkinned;
6989
7234
  const morphed = this.morphWeights !== null && mesh.morph !== null;
6990
7235
  const deltas = morphed && mesh.morph !== null ? mesh.morph.view() : null;
6991
- 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' : ''}`;
6992
7242
  /*
6993
7243
  * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
6994
7244
  * the key: two draws of one mesh differing only in these would otherwise share a cached
@@ -7003,13 +7253,22 @@ export class WebGPURenderer {
7003
7253
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7004
7254
  /* Added rather than blended over: its own blend state, so its own pipeline. */
7005
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();
7006
7259
  const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
7007
7260
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7008
7261
  /* The two order-independent buffers are two formats and two blend states, so a draw into
7009
7262
  one must never share a cached pipeline with the same mesh drawn into the other. */
7010
7263
  `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
7011
7264
  /* Culling is pipeline state here: a two-sided surface culls nothing. */
7012
- `${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' : ''}`;
7013
7272
  /*
7014
7273
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
7015
7274
  * field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
@@ -7039,7 +7298,7 @@ export class WebGPURenderer {
7039
7298
  if (present === undefined) {
7040
7299
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
7041
7300
  }
7042
- 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');
7043
7302
  }
7044
7303
  /*
7045
7304
  * Guards the draw call only, not the restore below.
@@ -7093,6 +7352,9 @@ export class WebGPURenderer {
7093
7352
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
7094
7353
  pass.drawIndexed(mesh.indexCount);
7095
7354
  }
7355
+ if (splits) {
7356
+ this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
7357
+ }
7096
7358
  }
7097
7359
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
7098
7360
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -7113,6 +7375,10 @@ export class WebGPURenderer {
7113
7375
  if (!toneMapped) {
7114
7376
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
7115
7377
  }
7378
+ if (ditheredBlend) {
7379
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
7380
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
7381
+ }
7116
7382
  if (own)
7117
7383
  this.materials.dirty();
7118
7384
  }
@@ -7332,9 +7598,17 @@ export class WebGPURenderer {
7332
7598
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
7333
7599
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
7334
7600
  const refracting = blend && this.bindSeeThrough(options);
7335
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
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;
7336
7608
  if (own)
7337
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;
7338
7612
  const base = mesh.key ?? 'flat:s0:u0';
7339
7613
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
7340
7614
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
@@ -7342,7 +7616,8 @@ export class WebGPURenderer {
7342
7616
  const adds = blend && options.additive === true;
7343
7617
  const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
7344
7618
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7345
- `${this.materialDoubleSided ? '|2s' : ''}`;
7619
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
7620
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
7346
7621
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
7347
7622
  let pipeline = pipelines.peek(key);
7348
7623
  if (pipeline === undefined) {
@@ -7350,7 +7625,7 @@ export class WebGPURenderer {
7350
7625
  if (present === undefined) {
7351
7626
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
7352
7627
  }
7353
- 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);
7354
7629
  }
7355
7630
  const material = this.materialSlotForDraw();
7356
7631
  if (material !== null) {
@@ -7408,6 +7683,10 @@ export class WebGPURenderer {
7408
7683
  }
7409
7684
  if (refracting)
7410
7685
  this.releaseSeeThrough();
7686
+ if (ditheredBlend) {
7687
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
7688
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
7689
+ }
7411
7690
  if (own)
7412
7691
  this.materials.dirty();
7413
7692
  }
@@ -7625,12 +7904,16 @@ export class WebGPURenderer {
7625
7904
  this.bindSceneCasterMaterial(material);
7626
7905
  this.drawMesh(mesh, model);
7627
7906
  },
7628
- skinnedMesh: (mesh, model, palette, material) => {
7907
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
7629
7908
  this.bindSceneCasterMaterial(material);
7630
7909
  this.setSkinPalette(palette);
7910
+ if (cloth !== undefined) {
7911
+ this.setCloth(cloth.binding, cloth.particles);
7912
+ }
7631
7913
  this.drawMesh(mesh, model);
7632
7914
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
7633
7915
  this.setSkinPalette(null);
7916
+ this.setCloth(null);
7634
7917
  },
7635
7918
  instanced: (batch, data, material) => {
7636
7919
  this.bindSceneCasterMaterial(material);
@@ -8380,14 +8663,20 @@ export class WebGPURenderer {
8380
8663
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
8381
8664
  slot of its own. See `skinnedGroups`. */
8382
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;
8383
8668
  if ((this.skinnedGroups[at] ?? null) === null ||
8384
8669
  this.skinnedGroupsBeside[at] !== this.bindGroup ||
8385
8670
  this.skinnedGroupsPalette[at] !== palette ||
8386
- this.skinnedGroupsDeltas[at] !== deltas) {
8387
- 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);
8388
8675
  this.skinnedGroupsBeside[at] = this.bindGroup;
8389
8676
  this.skinnedGroupsPalette[at] = palette;
8390
8677
  this.skinnedGroupsDeltas[at] = deltas;
8678
+ this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
8679
+ this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
8391
8680
  }
8392
8681
  return this.skinnedGroups[at];
8393
8682
  }
@@ -8456,16 +8745,19 @@ export class WebGPURenderer {
8456
8745
  const normalMap = this.normalMap;
8457
8746
  const ormMap = this.ormMap;
8458
8747
  const emissiveMap = this.emissiveMap;
8748
+ const modelMap = this.modelMap;
8459
8749
  this.albedo = null;
8460
8750
  this.normalMap = null;
8461
8751
  this.ormMap = null;
8462
8752
  this.emissiveMap = null;
8753
+ this.modelMap = null;
8463
8754
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
8464
8755
  this.albedo = albedo;
8465
8756
  this.normalMap = normalMap;
8466
8757
  this.ormMap = ormMap;
8467
8758
  this.emissiveMap = emissiveMap;
8468
- 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);
8469
8761
  /* The open material lives in a slot of a buffer that no longer exists. */
8470
8762
  this.materials.dirty();
8471
8763
  }
@@ -8503,13 +8795,14 @@ export class WebGPURenderer {
8503
8795
  if (this.albedo === null &&
8504
8796
  this.normalMap === null &&
8505
8797
  this.ormMap === null &&
8506
- this.emissiveMap === null) {
8798
+ this.emissiveMap === null &&
8799
+ this.modelMap === null) {
8507
8800
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
8508
8801
  this.bindGroupBudget.ask();
8509
8802
  this.bindGroup = this.blankAlbedoBindGroup;
8510
8803
  return;
8511
8804
  }
8512
- 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);
8513
8806
  }
8514
8807
  /**
8515
8808
  * The sky, as one triangle covering the screen with the world reconstructed per fragment.
@@ -9168,6 +9461,9 @@ export class WebGPURenderer {
9168
9461
  * the only moment they can land; everything else keeps the path it has.
9169
9462
  */
9170
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();
9171
9467
  const late = this.latePass;
9172
9468
  if (late !== null && lands)
9173
9469
  return late.take();
@@ -9233,6 +9529,14 @@ export class WebGPURenderer {
9233
9529
  const dynamic = mesh.motion;
9234
9530
  const rows = dynamic?.previous ?? null;
9235
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
+ }
9236
9540
  if (previous === null) {
9237
9541
  if (rewritten)
9238
9542
  this.motionPass?.recordDynamic(mesh, model, model, rows);
@@ -9277,6 +9581,41 @@ export class WebGPURenderer {
9277
9581
  return;
9278
9582
  this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
9279
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
+ }
9280
9619
  warnMoverTwice() {
9281
9620
  if (this.warnedMoverTwice)
9282
9621
  return;
@@ -10199,6 +10538,106 @@ export class WebGPURenderer {
10199
10538
  this.refractSnapshotTaken = true;
10200
10539
  return true;
10201
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
+ }
10202
10641
  takeVolumeDepth() {
10203
10642
  /*
10204
10643
  * **Never while the mirror is open, and this is a correctness fix rather than a saving.**
@@ -10821,6 +11260,8 @@ export class WebGPURenderer {
10821
11260
  * `beginFrame` resets it. Silent, and exactly the kind of loss the arena is designed
10822
11261
  * against everywhere else.
10823
11262
  */
11263
+ /* Before the last flush, which may let the depth go that the spread still reads. */
11264
+ this.runSkinScatter();
10824
11265
  this.flushGraph('boundary', this.liveOutFinalFlush());
10825
11266
  /*
10826
11267
  * `ensurePass` rather than a null check: the frame's pass is opened on demand now, so a
@@ -13638,6 +14079,7 @@ export class WebGPURenderer {
13638
14079
  * the 2026-08-13 rule asks for — one decision, bound twice.
13639
14080
  */
13640
14081
  dispose() {
14082
+ this.skinScatter?.dispose();
13641
14083
  this.motionPass?.dispose();
13642
14084
  this.motionPass = null;
13643
14085
  this.latePass?.dispose();
@@ -13677,3 +14119,11 @@ export class WebGPURenderer {
13677
14119
  this.surface.dispose();
13678
14120
  }
13679
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
+ }