@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
@@ -164,6 +164,15 @@ import {
164
164
  import type { TextStyle } from '../../textLayout.ts';
165
165
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.ts';
166
166
  import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
167
+ import { gpuCompressedFormats } from './compressedUpload.ts';
168
+ import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
169
+ import type { ClothViews } from './clothTextures.ts';
170
+ import type { ComputeHost } from './computeHost.ts';
171
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
172
+ import type { SurfaceModel } from '../../surfaceModel.ts';
173
+ import { validateClothBinding } from '../../clothBindingData.ts';
174
+ import type { ClothBindingData } from '../../clothBindingData.ts';
175
+ import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
167
176
  import {
168
177
  createSdfTextBindGroup,
169
178
  createSdfTextBindGroupLayout,
@@ -254,6 +263,7 @@ import {
254
263
  DECAL_TEXTURES,
255
264
  DECAL_UNIFORMS,
256
265
  PREMULTIPLIED_OVER_BLEND,
266
+ REFLECTION_OVER_BLEND,
257
267
  SSR_FRAG_FIELDS,
258
268
  SSR_FRAG_SIZE,
259
269
  SSR_RESOLVE_FRAG_WGSL,
@@ -382,6 +392,7 @@ import {
382
392
  depthPipeline,
383
393
  } from './depthPass.ts';
384
394
  import { PipelineCache, type LitSwitch } from './pipelineCache.ts';
395
+ import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.ts';
385
396
  import {
386
397
  SKY_FIELDS,
387
398
  SKY_UNIFORM_SIZE,
@@ -492,7 +503,7 @@ import {
492
503
  import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
493
504
  import { FrameBudget } from '../budget.ts';
494
505
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
495
- import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
506
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.ts';
496
507
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
497
508
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
498
509
  import { DistanceFieldScene } from '../../gi/fieldScene.ts';
@@ -582,6 +593,8 @@ import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
582
593
  import { GpuInstancedBatch } from './instanced.ts';
583
594
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
584
595
  import { cutoutOf } from '../../cutoutCaster.ts';
596
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
597
+ import type { CutoutResolve } from '../../cutoutDither.ts';
585
598
  import type { CutoutCaster } from '../../cutoutCaster.ts';
586
599
  import { LightField } from '../../driftLight/lightField.ts';
587
600
  import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
@@ -1456,6 +1469,12 @@ export class WebGPURenderer implements RendererApi {
1456
1469
  private readonly skinnedGroupsBeside: (GPUBindGroup | null)[] = [];
1457
1470
  private readonly skinnedGroupsPalette: (GPUTextureView | null)[] = [];
1458
1471
  private readonly skinnedGroupsDeltas: (GPUTextureView | null)[] = [];
1472
+ /** The cloth views each skinned twin was built with, compared by identity like the palette. */
1473
+ private readonly skinnedGroupsCloth: (GPUTextureView | null)[] = [];
1474
+ private readonly skinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
1475
+ /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
1476
+ private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
1477
+ private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
1459
1478
  /**
1460
1479
  * Which palette slot the following draws skin by, or -1 for none.
1461
1480
  *
@@ -1871,6 +1890,16 @@ export class WebGPURenderer implements RendererApi {
1871
1890
  private refractSnapshotView: GPUTextureView | null = null;
1872
1891
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1873
1892
  private refractSnapshotTaken = false;
1893
+ /**
1894
+ * Skin's screen-space scattering, made where the profile asks for it and has a composite to add
1895
+ * into, and null otherwise — which is every profile by default. See `SkinScatterPass`.
1896
+ */
1897
+ private readonly skinScatter: SkinScatterPass | null;
1898
+ /** The pipelines skin's diffuse half is built for: the blur target's format, the frame's samples. */
1899
+ private readonly skinPipelines: PipelineCache | null;
1900
+ /** Whether this frame has spread its skin: once, at its first blended draw or before its end. */
1901
+ private skinScattered = false;
1902
+ private readonly skinInverseProjection = mat4.create();
1874
1903
  private sceneColorView: GPUTextureView | null = null;
1875
1904
  /** The multisampled twin, when the profile asks for samples. Resolved into `sceneColor`. */
1876
1905
  private sceneColorMsaa: GPUTexture | null = null;
@@ -2051,6 +2080,17 @@ export class WebGPURenderer implements RendererApi {
2051
2080
  private temporalJitterX = 0;
2052
2081
  private temporalJitterY = 0;
2053
2082
  private temporalJittering = false;
2083
+ /**
2084
+ * The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64.
2085
+ * See `cutoutDither.ts`.
2086
+ */
2087
+ private cutoutFrame = 0;
2088
+ /**
2089
+ * How the current material's cutout edge is drawn this frame, decided when the material is staged
2090
+ * — after `bindMeshPass` has settled whether the frame is temporal — and read by every draw: a
2091
+ * coverage edge is pipeline state here (`|a2c`), and a blended draw tests hard.
2092
+ */
2093
+ private cutoutResolveStaged: CutoutResolve = 'hard';
2054
2094
  /**
2055
2095
  * Whether this frame is being reconstructed, settled once in `bindMeshPass`.
2056
2096
  *
@@ -2768,6 +2808,12 @@ export class WebGPURenderer implements RendererApi {
2768
2808
  private ormMap: GpuSurfaceTexture | null = null;
2769
2809
  /** The caller's emissive map, or null for the stand-in. See `setMaterial`. */
2770
2810
  private emissiveMap: GpuSurfaceTexture | null = null;
2811
+ /** The material's model map, or null for the stand-in. See `setMaterial` and `surfaceModel.ts`. */
2812
+ private modelMap: GpuSurfaceTexture | null = null;
2813
+ /** The material's shading model, which picks the lit pipeline: null the standard. */
2814
+ private materialModel: SurfaceModel | null = null;
2815
+ /** Its numbers, `uModelParams`, packed by `packModel`. */
2816
+ private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2771
2817
  /**
2772
2818
  * The flat group for a material, keyed on **all four** of its maps.
2773
2819
  *
@@ -2798,31 +2844,65 @@ export class WebGPURenderer implements RendererApi {
2798
2844
  GpuSurfaceTexture | null,
2799
2845
  Map<
2800
2846
  GpuSurfaceTexture | null,
2801
- Map<GpuSurfaceTexture | null, Map<GpuSurfaceTexture | null, GPUBindGroup>>
2847
+ Map<
2848
+ GpuSurfaceTexture | null,
2849
+ Map<GpuSurfaceTexture | null, Map<GpuSurfaceTexture | null, GPUBindGroup>>
2850
+ >
2802
2851
  >
2803
2852
  >();
2804
2853
  /** The group holding the one-pixel stand-in, for every draw with no texture set. */
2805
2854
  private blankAlbedoBindGroup: GPUBindGroup;
2806
2855
  /** Repeats across the mesh's own UV range. Per draw, because `uUvScale` is a vertex uniform. */
2807
2856
  private readonly uvScale = new Float32Array([1, 1]);
2857
+ /** Where the texture starts, after the scale. Per draw, beside `uvScale`. */
2858
+ private readonly uvOffset = new Float32Array([0, 0]);
2859
+ /**
2860
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
2861
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
2862
+ */
2863
+ private surfaceFog = FOG_RECEDE;
2808
2864
  /** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
2809
2865
  private materialDoubleSided = false;
2810
2866
  /** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
2811
2867
  private materialHasMaps = false;
2812
2868
 
2813
2869
  createSurfaceTexture(
2814
- source: TexImageSource,
2870
+ source: SurfaceSource,
2815
2871
  options: SurfaceTextureOptions = {},
2816
2872
  ): GpuSurfaceTexture {
2817
- return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
2873
+ return new GpuSurfaceTexture(
2874
+ this.surface.device,
2875
+ this.pipelines,
2876
+ source,
2877
+ options,
2878
+ this.compressedFormats,
2879
+ );
2880
+ }
2881
+
2882
+ private compressedCache: readonly CompressedTextureFormat[] | null = null;
2883
+
2884
+ /**
2885
+ * The BC formats this device takes as blocks: every one where the device carries
2886
+ * `texture-compression-bc`, which `select.ts` asks for wherever the adapter offers it, and none
2887
+ * where it does not. See `Renderer.compressedFormats`.
2888
+ */
2889
+ get compressedFormats(): readonly CompressedTextureFormat[] {
2890
+ this.compressedCache ??= gpuCompressedFormats(this.surface.device);
2891
+ return this.compressedCache;
2818
2892
  }
2819
2893
 
2820
2894
  /** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
2821
2895
  createSurfaceTextureArray(
2822
- sources: readonly TexImageSource[],
2896
+ sources: readonly SurfaceSource[],
2823
2897
  options: SurfaceTextureOptions = {},
2824
2898
  ): GpuSurfaceTexture {
2825
- return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
2899
+ return new GpuSurfaceTexture(
2900
+ this.surface.device,
2901
+ this.pipelines,
2902
+ sources,
2903
+ options,
2904
+ this.compressedFormats,
2905
+ );
2826
2906
  }
2827
2907
 
2828
2908
  /**
@@ -2870,6 +2950,7 @@ export class WebGPURenderer implements RendererApi {
2870
2950
  if (this.normalMap === texture) this.normalMap = null;
2871
2951
  if (this.ormMap === texture) this.ormMap = null;
2872
2952
  if (this.emissiveMap === texture) this.emissiveMap = null;
2953
+ if (this.modelMap === texture) this.modelMap = null;
2873
2954
  this.forgetBindingsOf(texture);
2874
2955
  texture.dispose();
2875
2956
  }
@@ -2895,7 +2976,13 @@ export class WebGPURenderer implements RendererApi {
2895
2976
  this.albedo = null;
2896
2977
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
2897
2978
  this.albedo = held;
2898
- this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
2979
+ this.bindGroup = this.flatGroupForMaps(
2980
+ held,
2981
+ this.normalMap,
2982
+ this.ormMap,
2983
+ this.emissiveMap,
2984
+ this.modelMap,
2985
+ );
2899
2986
  }
2900
2987
 
2901
2988
  /**
@@ -2924,6 +3011,128 @@ export class WebGPURenderer implements RendererApi {
2924
3011
  this.skinPaletteSlot = this.skinPalettes.take(this.surface.device, palette) ?? -1;
2925
3012
  }
2926
3013
 
3014
+ /**
3015
+ * A mesh's cloth binding. See `RendererApi.createClothBinding`. The pipelines that draw it bound
3016
+ * are warmed here, every way this mesh can be drawn, so none compiles inside the first frame
3017
+ * that draws a garment.
3018
+ */
3019
+ createClothBinding(mesh: GpuMesh, data: ClothBindingData): GpuClothBinding {
3020
+ if (!mesh.isSkinned) {
3021
+ throw new Error(
3022
+ 'createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
3023
+ 'skinning and the cloth',
3024
+ );
3025
+ }
3026
+ validateClothBinding(data, mesh.vertexCount);
3027
+ const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
3028
+ const present = this.meshPresent.get(base);
3029
+ if (present !== undefined) {
3030
+ const skin = mesh.isSkinnedEight ? '|skin8' : '|skin';
3031
+ for (const morphed of mesh.morph !== null ? [false, true] : [false]) {
3032
+ for (const translucent of [false, true]) {
3033
+ void flatPipelineAsync(
3034
+ this.targetPipelines(),
3035
+ this.surface.device,
3036
+ this.flatLayoutFor(true, morphed),
3037
+ this.variant,
3038
+ `${base}${morphed ? '|morph' : ''}${skin}|cloth${translucent ? '|blend' : ''}`,
3039
+ present,
3040
+ translucent,
3041
+ true,
3042
+ morphed,
3043
+ mesh.isSkinnedEight,
3044
+ true,
3045
+ );
3046
+ }
3047
+ }
3048
+ }
3049
+ return new GpuClothBinding(this.surface.device, data);
3050
+ }
3051
+
3052
+ /** One character's particles. See `RendererApi.createClothParticles`. */
3053
+ createClothParticles(count: number): GpuClothParticles {
3054
+ return new GpuClothParticles(this.surface.device, count);
3055
+ }
3056
+
3057
+ /**
3058
+ * This frame's particles. **Once a frame, before the character's draws**: a texture write lands
3059
+ * ahead of every draw of the frame, so a second update between two draws would reach both.
3060
+ */
3061
+ updateClothParticles(particles: GpuClothParticles, positions: Float32Array): void {
3062
+ if (this.surface.lost) return;
3063
+ /* A write between frames is the next frame's, as a rewritten mesh's is. See `changeFrames.ts`. */
3064
+ particles.update(this.surface.device, positions, frameOfChange(this.moverFrame, this.inFrame));
3065
+ }
3066
+
3067
+ /** Place the following skinned draws by a cloth, or by none. See `RendererApi.setCloth`. */
3068
+ setCloth(binding: GpuClothBinding | null, particles: GpuClothParticles | null = null): void {
3069
+ if (binding === null || particles === null) {
3070
+ this.cloth = null;
3071
+ return;
3072
+ }
3073
+ if (binding.particles !== particles.count) {
3074
+ throw new Error(
3075
+ `setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`,
3076
+ );
3077
+ }
3078
+ this.cloth = { binding, particles };
3079
+ }
3080
+
3081
+ /** Retired rather than destroyed: a draw recorded earlier in this frame may still read them. */
3082
+ disposeClothBinding(binding: GpuClothBinding): void {
3083
+ this.retiredTextures.push(...binding.textures);
3084
+ }
3085
+
3086
+ disposeClothParticles(particles: GpuClothParticles): void {
3087
+ this.retiredTextures.push(...particles.textures);
3088
+ }
3089
+
3090
+ /**
3091
+ * The device and the frame's bookkeeping, for compute a consumer opts into by importing it —
3092
+ * `createSkinnedCloth` — rather than for anything this renderer runs itself. See `computeHost.ts`.
3093
+ */
3094
+ computeHost(): ComputeHost {
3095
+ return this.computeHostCache;
3096
+ }
3097
+
3098
+ private readonly computeHostCache: ComputeHost = {
3099
+ device: () => this.surface.device,
3100
+ lost: () => this.surface.lost,
3101
+ changeFrame: () => frameOfChange(this.moverFrame, this.inFrame),
3102
+ retire: (textures) => {
3103
+ this.retiredTextures.push(...textures);
3104
+ },
3105
+ };
3106
+
3107
+ /**
3108
+ * An eye's axis in the world for this draw, into the open material's \`uModelParams\`, and a new
3109
+ * material slot when it moved: the axis is per draw, and a slot already taken by an earlier draw
3110
+ * of the same eye must not be written under it. See \`eyeAxisInWorld\`.
3111
+ */
3112
+ private writeEyeAxis(model: ArrayLike<number>, skinned: boolean): void {
3113
+ const eye = this.materialModel;
3114
+ if (eye?.kind !== 'eye') return;
3115
+ const at = this.materialField('uModelParams') + 4;
3116
+ const f = this.perFrameFloats;
3117
+ const before0 = f[at];
3118
+ const before1 = f[at + 1];
3119
+ const before2 = f[at + 2];
3120
+ eyeAxisInWorld(eye, model, skinned ? this.skinPaletteData : null, f, at);
3121
+ if (f[at] !== before0 || f[at + 1] !== before1 || f[at + 2] !== before2) this.materials.dirty();
3122
+ }
3123
+
3124
+ /** The views the draw's cloth binds, or the stand-in's where no cloth is set. */
3125
+ private clothViews(): ClothViews {
3126
+ const cloth = this.cloth;
3127
+ return cloth === null ? this.clothStand() : clothViewsOf(cloth.binding, cloth.particles);
3128
+ }
3129
+
3130
+ /** The one-texel stand-in a skinned group binds where there is no cloth, made on first need. */
3131
+ private clothStand(): ClothViews {
3132
+ this.clothStandCache ??= clothStandIn(this.surface.device);
3133
+ return this.clothStandCache.views;
3134
+ }
3135
+
2927
3136
  /**
2928
3137
  * Choose the morph weights the following `drawMesh` calls deform by, or null for none.
2929
3138
  *
@@ -2940,19 +3149,48 @@ export class WebGPURenderer implements RendererApi {
2940
3149
  const albedo = material?.albedo ?? null;
2941
3150
  this.uvScale[0] = material?.uScale ?? 1;
2942
3151
  this.uvScale[1] = material?.vScale ?? 1;
3152
+ this.uvOffset[0] = material?.uOffset ?? 0;
3153
+ this.uvOffset[1] = material?.vOffset ?? 0;
2943
3154
  this.materialDoubleSided = material?.doubleSided === true;
2944
3155
  this.materialHasMaps = materialHasMaps(material);
2945
3156
  const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
2946
3157
  const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
2947
3158
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
3159
+ const modelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3160
+ this.materialModel = material?.model ?? null;
3161
+ /* What the blur spreads this skin by: the last material to name a profile says. */
3162
+ const model = material?.model;
3163
+ if (model?.kind === 'skin' && this.skinScatter !== null) {
3164
+ this.skinScatter.setProfile(
3165
+ model.profile,
3166
+ model.scatterColor[0] * model.radius,
3167
+ model.scatterColor[1] * model.radius,
3168
+ model.scatterColor[2] * model.radius,
3169
+ );
3170
+ }
2948
3171
  this.material((f, i) => {
2949
- i[this.materialField('uAlbedoEnabled')] = albedo === null ? 0 : 1;
2950
- f[this.materialField('uAlbedoCutout')] = material?.cutout ?? 0;
2951
- i[this.materialField('uDoubleSided')] = material?.doubleSided === true ? 1 : 0;
3172
+ /* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
3173
+ const flags = this.materialField('uMaterialFlags');
3174
+ i[flags] = albedo === null ? 0 : 1;
3175
+ /* The cutoff, how this frame resolves a dithered edge, and the frame its pattern moves with. */
3176
+ const cutoff = albedo === null ? 0 : (material?.cutout ?? 0);
3177
+ this.cutoutResolveStaged =
3178
+ cutoff > 0
3179
+ ? resolveCutout(
3180
+ material?.cutoutMode ?? 'hard',
3181
+ this.reconstructing || this.temporalJittering,
3182
+ this.samples,
3183
+ )
3184
+ : 'hard';
3185
+ const cut = this.materialField('uCutout');
3186
+ f[cut] = cutoff;
3187
+ f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
3188
+ f[cut + 2] = this.cutoutFrame;
3189
+ i[flags + 3] = material?.doubleSided === true ? 1 : 0;
2952
3190
  /* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
2953
3191
  f[this.materialField('uNormalStrength')] =
2954
3192
  normal === null ? 0 : (material?.normalStrength ?? 1);
2955
- i[this.materialField('uOrmEnabled')] = orm === null ? 0 : 1;
3193
+ i[flags + 1] = orm === null ? 0 : 1;
2956
3194
  /*
2957
3195
  * Component-aligned with the map — r occlusion, g roughness, b metallic — so this order is
2958
3196
  * the channel order rather than the field order of `SurfaceMaterial`. Written component by
@@ -2963,7 +3201,7 @@ export class WebGPURenderer implements RendererApi {
2963
3201
  f[scale] = material?.occlusionStrength ?? 1;
2964
3202
  f[scale + 1] = material?.roughnessScale ?? 1;
2965
3203
  f[scale + 2] = material?.metallicScale ?? 1;
2966
- i[this.materialField('uEmissiveMapEnabled')] = emissiveMap === null ? 0 : 1;
3204
+ i[flags + 2] = emissiveMap === null ? 0 : 1;
2967
3205
  /*
2968
3206
  * One rather than zero on every component, because this multiplies rather than adds: an
2969
3207
  * unwritten float is zero, and zero here would switch off every glow the mesh's own vertex
@@ -2974,6 +3212,9 @@ export class WebGPURenderer implements RendererApi {
2974
3212
  f[emissive] = asked?.[0] ?? 1;
2975
3213
  f[emissive + 1] = asked?.[1] ?? 1;
2976
3214
  f[emissive + 2] = asked?.[2] ?? 1;
3215
+ /* The model's numbers and whether its map is bound. See `packModel`. */
3216
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams);
3217
+ f.set(this.modelParams, this.materialField('uModelParams'));
2977
3218
  });
2978
3219
  /*
2979
3220
  * **One lookup for the whole material, and nothing thrown away.**
@@ -2988,7 +3229,8 @@ export class WebGPURenderer implements RendererApi {
2988
3229
  this.albedo === albedo &&
2989
3230
  this.normalMap === normal &&
2990
3231
  this.ormMap === orm &&
2991
- this.emissiveMap === emissiveMap
3232
+ this.emissiveMap === emissiveMap &&
3233
+ this.modelMap === modelMap
2992
3234
  ) {
2993
3235
  return;
2994
3236
  }
@@ -2996,7 +3238,8 @@ export class WebGPURenderer implements RendererApi {
2996
3238
  this.normalMap = normal;
2997
3239
  this.ormMap = orm;
2998
3240
  this.emissiveMap = emissiveMap;
2999
- this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap);
3241
+ this.modelMap = modelMap;
3242
+ this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap, modelMap);
3000
3243
  }
3001
3244
 
3002
3245
  /**
@@ -3025,8 +3268,16 @@ export class WebGPURenderer implements RendererApi {
3025
3268
  normal: GpuSurfaceTexture | null,
3026
3269
  orm: GpuSurfaceTexture | null,
3027
3270
  emissive: GpuSurfaceTexture | null,
3271
+ /** The model map, a fifth level of the same cache. See `flatBindGroups`. */
3272
+ modelMap: GpuSurfaceTexture | null,
3028
3273
  ): GPUBindGroup {
3029
- if (albedo === null && normal === null && orm === null && emissive === null) {
3274
+ if (
3275
+ albedo === null &&
3276
+ normal === null &&
3277
+ orm === null &&
3278
+ emissive === null &&
3279
+ modelMap === null
3280
+ ) {
3030
3281
  return this.blankAlbedoBindGroup;
3031
3282
  }
3032
3283
  let byNormal = this.flatBindGroups.get(albedo);
@@ -3044,11 +3295,16 @@ export class WebGPURenderer implements RendererApi {
3044
3295
  byEmissive = new Map();
3045
3296
  byOrm.set(orm, byEmissive);
3046
3297
  }
3047
- const existing = byEmissive.get(emissive);
3298
+ let byModel = byEmissive.get(emissive);
3299
+ if (byModel === undefined) {
3300
+ byModel = new Map();
3301
+ byEmissive.set(emissive, byModel);
3302
+ }
3303
+ const existing = byModel.get(modelMap);
3048
3304
  if (existing !== undefined) return existing;
3049
3305
  const built = this.buildFlatBindGroup();
3050
3306
  this.bindGroupBudget.ask();
3051
- byEmissive.set(emissive, built);
3307
+ byModel.set(modelMap, built);
3052
3308
  return built;
3053
3309
  }
3054
3310
 
@@ -3104,6 +3360,14 @@ export class WebGPURenderer implements RendererApi {
3104
3360
  });
3105
3361
  }
3106
3362
 
3363
+ /** Whether the surfaces drawn next meet the medium. See `renderer.ts`. */
3364
+ setSurfaceFog(enabled: boolean): void {
3365
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
3366
+ this.material((_f, i) => {
3367
+ i[this.materialField('uFogEnabled')] = this.surfaceFog;
3368
+ });
3369
+ }
3370
+
3107
3371
  /**
3108
3372
  * How much of the surfaces drawn next survives a screen-door dither. See `renderer.ts`.
3109
3373
  *
@@ -5269,6 +5533,8 @@ export class WebGPURenderer implements RendererApi {
5269
5533
  private readonly shadowSkinnedGroups: (GPUBindGroup | null)[] = [];
5270
5534
  private readonly shadowSkinnedGroupsBeside: (GPUBindGroup | null)[] = [];
5271
5535
  private readonly shadowSkinnedGroupsPalette: (GPUTextureView | null)[] = [];
5536
+ private readonly shadowSkinnedGroupsCloth: (GPUTextureView | null)[] = [];
5537
+ private readonly shadowSkinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
5272
5538
  /** Held from construction so the skinned group can be rebuilt without them being threaded. */
5273
5539
  private shadowBlankView: GPUTextureView | null = null;
5274
5540
  private shadowBlankSampler: GPUSampler | null = null;
@@ -5392,18 +5658,22 @@ export class WebGPURenderer implements RendererApi {
5392
5658
  return group;
5393
5659
  }
5394
5660
 
5395
- /** The cutoff and UV scale into a cutout draw's slot, at the offsets its own variant declares. */
5661
+ /** The cutoff and UV scale and offset into a cutout draw's slot, at its own variant's offsets. */
5396
5662
  private writeCutout(
5397
5663
  slot: number,
5398
5664
  cutout: CutoutCaster<unknown>,
5399
5665
  fields: {
5400
5666
  readonly uUvScale: { readonly offset: number };
5667
+ readonly uUvOffset: { readonly offset: number };
5401
5668
  readonly uAlphaCutout: { readonly offset: number };
5402
5669
  },
5403
5670
  ): void {
5404
5671
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset, cutout.u);
5405
5672
  this.shadowDraws.writeFloat(slot, fields.uUvScale.offset + 4, cutout.v);
5673
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
5674
+ this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
5406
5675
  this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
5676
+ this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset + 4, cutout.dithered);
5407
5677
  }
5408
5678
 
5409
5679
  private shadowGroup(): GPUBindGroup {
@@ -5419,12 +5689,14 @@ export class WebGPURenderer implements RendererApi {
5419
5689
  * second independently fading occluder can be recorded, and a character is not one — it is the
5420
5690
  * thing in front. Serving it would mean a second cached group for a case nothing produces.
5421
5691
  */
5422
- private shadowSkinnedGroup(palette: GPUTextureView, at: number): GPUBindGroup {
5692
+ private shadowSkinnedGroup(palette: GPUTextureView, at: number, cloth: ClothViews): GPUBindGroup {
5423
5693
  const beside = this.shadowBindGroup;
5424
5694
  if (
5425
5695
  (this.shadowSkinnedGroups[at] ?? null) === null ||
5426
5696
  this.shadowSkinnedGroupsBeside[at] !== beside ||
5427
- this.shadowSkinnedGroupsPalette[at] !== palette
5697
+ this.shadowSkinnedGroupsPalette[at] !== palette ||
5698
+ this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
5699
+ this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding
5428
5700
  ) {
5429
5701
  this.shadowSkinnedGroups[at] = createDepthBindGroup(
5430
5702
  this.surface.device,
@@ -5434,9 +5706,13 @@ export class WebGPURenderer implements RendererApi {
5434
5706
  this.shadowBlankView as GPUTextureView,
5435
5707
  this.shadowBlankSampler as GPUSampler,
5436
5708
  palette,
5709
+ null,
5710
+ cloth,
5437
5711
  );
5438
5712
  this.shadowSkinnedGroupsBeside[at] = beside;
5439
5713
  this.shadowSkinnedGroupsPalette[at] = palette;
5714
+ this.shadowSkinnedGroupsCloth[at] = cloth.particles;
5715
+ this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
5440
5716
  }
5441
5717
  return this.shadowSkinnedGroups[at] as GPUBindGroup;
5442
5718
  }
@@ -5475,6 +5751,7 @@ export class WebGPURenderer implements RendererApi {
5475
5751
  void this.pipelines.enable(feature);
5476
5752
  if (this.overlayPipelines !== this.pipelines) void this.overlayPipelines.enable(feature);
5477
5753
  if (this.latePipelines !== this.pipelines) void this.latePipelines.enable(feature);
5754
+ if (this.skinPipelines !== null) void this.skinPipelines.enable(feature);
5478
5755
  }
5479
5756
  /** One resolution every glass caster is read through, so recording one allocates nothing. */
5480
5757
  private readonly glassCasterScratch: ResolvedGlass = {
@@ -5596,7 +5873,7 @@ export class WebGPURenderer implements RendererApi {
5596
5873
  pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5597
5874
  pass.drawIndexed(geometry.indexCount, count);
5598
5875
  },
5599
- skinnedMesh: (mesh, model, palette, material) => {
5876
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
5600
5877
  if (!this.drawingGlassDepth && !castsDepth(material)) {
5601
5878
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
5602
5879
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -5628,18 +5905,30 @@ export class WebGPURenderer implements RendererApi {
5628
5905
 
5629
5906
  const geometry = mesh as GpuMesh & { key?: string };
5630
5907
  if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
5908
+ /* A garment casts placed by its cloth, as it draws; `|cloth` keys the pipeline that does. */
5909
+ const clothed = cloth !== undefined;
5910
+ const clothViews = clothed
5911
+ ? clothViewsOf(
5912
+ cloth.binding as unknown as GpuClothBinding,
5913
+ cloth.particles as unknown as GpuClothParticles,
5914
+ )
5915
+ : this.clothStand();
5631
5916
  pass.setPipeline(
5632
5917
  depthPipeline(
5633
5918
  this.pipelines,
5634
5919
  this.surface.device,
5635
5920
  this.shadowSkinnedLayout,
5636
- `depth-skinned|${this.depthCullMode}|${geometry.key ?? ''}`,
5921
+ `${geometry.isSkinnedEight ? 'depth-skinned8' : 'depth-skinned'}${clothed ? '|cloth' : ''}|${this.depthCullMode}|${geometry.key ?? ''}`,
5637
5922
  this.presentOf(geometry),
5638
5923
  this.depthCullMode,
5639
5924
  true,
5925
+ false,
5926
+ false,
5927
+ geometry.isSkinnedEight,
5928
+ clothed,
5640
5929
  ),
5641
5930
  );
5642
- pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot), [slot]);
5931
+ pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot, clothViews), [slot]);
5643
5932
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
5644
5933
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
5645
5934
  }
@@ -6015,6 +6304,26 @@ export class WebGPURenderer implements RendererApi {
6015
6304
  this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
6016
6305
  ? this.pipelines
6017
6306
  : new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
6307
+ /*
6308
+ * Screen-space skin needs the composite: linear light to add into, and the depth it resolves.
6309
+ * Without one it is the pre-integrated fit alone, which is the default anyway, and said once.
6310
+ */
6311
+ const skinScreen = quality.skinScattering === 'screen-space' && quality.screenEffects;
6312
+ if (quality.skinScattering === 'screen-space' && !quality.screenEffects) {
6313
+ console.warn(
6314
+ "WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated",
6315
+ );
6316
+ }
6317
+ this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
6318
+ this.skinPipelines = skinScreen
6319
+ ? new PipelineCache(
6320
+ device,
6321
+ SKIN_TARGET_FORMAT,
6322
+ this.samples,
6323
+ glassShadows,
6324
+ quality.clusteredLights,
6325
+ )
6326
+ : null;
6018
6327
  this.issueBound = this.issueCommand.bind(this);
6019
6328
  this.pipelineTargets =
6020
6329
  this.overlayPipelines === this.pipelines
@@ -6084,7 +6393,7 @@ export class WebGPURenderer implements RendererApi {
6084
6393
  );
6085
6394
 
6086
6395
  /*
6087
- * A one-pixel stand-in for the albedo binding. `uAlbedoEnabled` is zero for untextured
6396
+ * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
6088
6397
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
6089
6398
  * requires every declared binding to be filled. WebGL2 keeps an `emptyTexture2D` for
6090
6399
  * exactly the same reason.
@@ -6303,6 +6612,13 @@ export class WebGPURenderer implements RendererApi {
6303
6612
  ? { view: blankSurfaceView, sampler: blankSampler }
6304
6613
  : { view: map.view, sampler: map.sampler };
6305
6614
  }
6615
+ /* The material's model map, or the stand-in, for the reason `uNormalMap` gives above. */
6616
+ if (name === 'uModelMap') {
6617
+ const map = this.modelMap;
6618
+ return map === null
6619
+ ? { view: blankSurfaceView, sampler: blankSampler }
6620
+ : { view: map.view, sampler: map.sampler };
6621
+ }
6306
6622
  if (name === 'uOrmMap') {
6307
6623
  const map = this.ormMap;
6308
6624
  return map === null
@@ -7210,6 +7526,8 @@ export class WebGPURenderer implements RendererApi {
7210
7526
  }
7211
7527
  /* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
7212
7528
  present['layers'] = key.includes(':layers');
7529
+ /* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
7530
+ present['dynamic'] = key.includes(':dynamic');
7213
7531
  return present;
7214
7532
  }
7215
7533
 
@@ -7223,6 +7541,7 @@ export class WebGPURenderer implements RendererApi {
7223
7541
  */
7224
7542
  beginFrame(clearColor: Vec3): void {
7225
7543
  this.cullFrame += 1;
7544
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
7226
7545
  /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
7227
7546
  for (const texture of this.retiredTextures) texture.destroy();
7228
7547
  this.retiredTextures.length = 0;
@@ -7286,6 +7605,8 @@ export class WebGPURenderer implements RendererApi {
7286
7605
  this.volumeDepthTaken = false;
7287
7606
  /* Last frame's snapshot is last frame's scene. */
7288
7607
  this.refractSnapshotTaken = false;
7608
+ this.skinScattered = false;
7609
+ this.skinScatter?.reset();
7289
7610
  this.plumeVerts.reset();
7290
7611
  this.plumeFrags.reset();
7291
7612
  this.waterVerts.reset();
@@ -7468,7 +7789,7 @@ export class WebGPURenderer implements RendererApi {
7468
7789
  * neither the pipeline set nor the key depends on whether the geometry lands in one call or
7469
7790
  * over six frames.
7470
7791
  */
7471
- private warmFlatPipelines(data: MeshData): string {
7792
+ private warmFlatPipelines(data: MeshData, dynamic: boolean): string {
7472
7793
  /*
7473
7794
  * **Every optional attribute, not the two that come to mind.** `present` decides where
7474
7795
  * each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
@@ -7494,6 +7815,15 @@ export class WebGPURenderer implements RendererApi {
7494
7815
  present['layers'] = true;
7495
7816
  key += ':layers';
7496
7817
  }
7818
+ /*
7819
+ * A dynamic mesh keeps its positions and normals in buffers of their own (`GpuMesh.update`), so
7820
+ * it binds four buffers where a static one binds two: a different layout, and a key that says
7821
+ * so, or the two would share a pipeline whose buffers one of them does not have.
7822
+ */
7823
+ if (dynamic) {
7824
+ present['dynamic'] = true;
7825
+ key += ':dynamic';
7826
+ }
7497
7827
  const fullKey = `${this.variant}|${key}`;
7498
7828
  /*
7499
7829
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -7564,11 +7894,13 @@ export class WebGPURenderer implements RendererApi {
7564
7894
  * guessing which the scene will use.
7565
7895
  */
7566
7896
  const skinnable = data.joints !== undefined;
7897
+ /* An eight-influence rig takes its own skinned pipelines, keyed `|skin8`. See skinning.ts. */
7898
+ const eight = skinnable && data.joints2 !== undefined;
7567
7899
  const morphable = data.morphTargets !== undefined;
7568
7900
  for (const wantSkin of skinnable ? [false, true] : [false]) {
7569
7901
  for (const wantMorph of morphable ? [false, true] : [false]) {
7570
7902
  if (!wantSkin && !wantMorph) continue;
7571
- const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? '|skin' : ''}`;
7903
+ const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? (eight ? '|skin8' : '|skin') : ''}`;
7572
7904
  for (const translucent of [false, true]) {
7573
7905
  void flatPipelineAsync(
7574
7906
  pipelines,
@@ -7580,6 +7912,7 @@ export class WebGPURenderer implements RendererApi {
7580
7912
  translucent,
7581
7913
  wantSkin,
7582
7914
  wantMorph,
7915
+ wantSkin && eight,
7583
7916
  );
7584
7917
  }
7585
7918
  }
@@ -7603,7 +7936,7 @@ export class WebGPURenderer implements RendererApi {
7603
7936
  }
7604
7937
 
7605
7938
  createMesh(data: MeshData, options: MeshOptions = {}): GpuMesh {
7606
- const fullKey = this.warmFlatPipelines(data);
7939
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
7607
7940
  return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
7608
7941
  }
7609
7942
 
@@ -7617,7 +7950,7 @@ export class WebGPURenderer implements RendererApi {
7617
7950
  * can do nothing about. Done with `complete` still false is the signal that it was abandoned.
7618
7951
  */
7619
7952
  createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMeshHandle {
7620
- const fullKey = this.warmFlatPipelines(data);
7953
+ const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
7621
7954
  const { mesh, upload } = createGpuMeshIncremental(
7622
7955
  this.surface.device,
7623
7956
  data,
@@ -7638,20 +7971,20 @@ export class WebGPURenderer implements RendererApi {
7638
7971
  /**
7639
7972
  * Rewrite a mesh's positions, and its normals where the caller has them.
7640
7973
  *
7641
- * Refused in words on a mesh that did not declare itself dynamic, because the thing that makes
7642
- * an update possible on this backend — the interleaved array the vertex data was built from —
7643
- * is only kept when a consumer asked for it. See `GpuMesh.update`.
7974
+ * Refused in words on a mesh that did not declare itself dynamic, because what makes an update
7975
+ * cheap on this backend — positions and normals in buffers of their own — is a layout chosen at
7976
+ * creation. See `GpuMesh.update`.
7644
7977
  */
7645
7978
  updateMesh(mesh: GpuMesh, positions: Float32Array, normals?: Float32Array): void {
7646
7979
  if (mesh.update === null) {
7647
7980
  throw new Error(
7648
- 'this mesh was not created with { dynamic: true }, so the interleaved vertex data it ' +
7649
- 'would be patched from was not kept. Say so at `createMesh` — the flag is what decides ' +
7650
- 'what is kept and what a buffer is hinted as.',
7981
+ 'this mesh was not created with { dynamic: true }, so its positions share one buffer with ' +
7982
+ 'every other attribute and cannot be rewritten alone. Say so at `createMesh` — the flag ' +
7983
+ 'decides the layout, and what a buffer is hinted as.',
7651
7984
  );
7652
7985
  }
7653
7986
  /*
7654
- * **The rows before this rewrite are last frame's**, kept only for a reconstruction and only
7987
+ * **The positions before this rewrite are last frame's**, kept only for a reconstruction and only
7655
7988
  * on the first rewrite of a frame — a second must not replace where the vertices were last
7656
7989
  * frame with where they were a moment ago. A rewrite between frames is the next frame's. See
7657
7990
  * `changeFrames.ts`.
@@ -7783,8 +8116,13 @@ export class WebGPURenderer implements RendererApi {
7783
8116
  * these per draw through the material block, the same shape `uOpacity` already uses.
7784
8117
  */
7785
8118
  i[at('uLightingEnabled')] = 1;
7786
- i[at('uFogEnabled')] = 1;
7787
- i[at('uAlbedoEnabled')] = 0;
8119
+ i[at('uFogEnabled')] = FOG_RECEDE;
8120
+ this.surfaceFog = FOG_RECEDE;
8121
+ /* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
8122
+ i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
8123
+ f[at('uCutout')] = 0;
8124
+ f[at('uCutout') + 1] = 0;
8125
+ this.cutoutResolveStaged = 'hard';
7788
8126
  this.materialHasMaps = false;
7789
8127
 
7790
8128
  /*
@@ -7930,7 +8268,7 @@ export class WebGPURenderer implements RendererApi {
7930
8268
  /*
7931
8269
  * The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
7932
8270
  *
7933
- * `uOrmEnabled` needs no line here: an unwritten int is zero, and zero is exactly "no map".
8271
+ * The ORM map's flag is zeroed with the others above, and zero is exactly "no map".
7934
8272
  * These three are the opposite case, the one this block's own header is about: a scale of
7935
8273
  * zero is a real and wrong value, so leaving them silent would not switch the map off, it
7936
8274
  * would read every channel as its floor the moment one was bound.
@@ -8235,6 +8573,8 @@ export class WebGPURenderer implements RendererApi {
8235
8573
  this.bindGroup = this.blankAlbedoBindGroup;
8236
8574
  this.uvScale[0] = 1;
8237
8575
  this.uvScale[1] = 1;
8576
+ this.uvOffset[0] = 0;
8577
+ this.uvOffset[1] = 0;
8238
8578
  }
8239
8579
 
8240
8580
  /**
@@ -8319,7 +8659,10 @@ export class WebGPURenderer implements RendererApi {
8319
8659
  */
8320
8660
  if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
8321
8661
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
8322
- this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
8662
+ /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
8663
+ true, which is a translucent draw's default and would put every opaque draw back in the fog
8664
+ `setSurfaceFog(false)` took it out of. */
8665
+ this.submitMesh(mesh, model, tint, 1, false, { depthLayer, fog: this.surfaceFog !== FOG_OFF });
8323
8666
  }
8324
8667
 
8325
8668
  /**
@@ -8501,6 +8844,7 @@ export class WebGPURenderer implements RendererApi {
8501
8844
  /* The material's repeats, not a constant: geometry authored in metres is textured at
8502
8845
  whatever density the material asks for without rebuilding it. */
8503
8846
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
8847
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
8504
8848
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8505
8849
  /*
8506
8850
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
@@ -8534,7 +8878,7 @@ export class WebGPURenderer implements RendererApi {
8534
8878
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
8535
8879
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8536
8880
  const toneMapped = options.toneMapped ?? true;
8537
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
8881
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
8538
8882
  if (unlitOrUnfogged) {
8539
8883
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8540
8884
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
@@ -8554,10 +8898,18 @@ export class WebGPURenderer implements RendererApi {
8554
8898
  * before last. That is the defined state the two-backends rule asks for.
8555
8899
  */
8556
8900
  const refracting = this.bindSeeThrough(options);
8901
+ /* A dithered cutout tests hard on a blended draw, which has nothing to average a pattern with;
8902
+ put back below with the rest. See `cutoutDither.ts`. */
8903
+ const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
8904
+ if (ditheredBlend) this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
8557
8905
  /* Taken for itself when any of those differs from the pass, and put back below: the rule both
8558
8906
  backends count material changes by. See `materialChanges.ts`. */
8559
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8907
+ const own =
8908
+ ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
8560
8909
  if (own) this.materials.dirty();
8910
+ /* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
8911
+ const modelKind = this.materialModel?.kind ?? null;
8912
+ if (modelKind === 'eye') this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
8561
8913
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8562
8914
  /*
8563
8915
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
@@ -8569,7 +8921,13 @@ export class WebGPURenderer implements RendererApi {
8569
8921
  const skinned = this.skinPaletteSlot >= 0 && mesh.isSkinned;
8570
8922
  const morphed = this.morphWeights !== null && mesh.morph !== null;
8571
8923
  const deltas = morphed && mesh.morph !== null ? mesh.morph.view() : null;
8572
- const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? '|skin' : ''}`;
8924
+ const eight = skinned && mesh.isSkinnedEight;
8925
+ /* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
8926
+ binding was made. See `createClothBinding`. */
8927
+ const clothed = skinned && this.cloth !== null;
8928
+ const keyed =
8929
+ `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
8930
+ `${clothed ? '|cloth' : ''}`;
8573
8931
  /*
8574
8932
  * Depth writing and the overlay layer are pipeline state on this backend, so they belong in
8575
8933
  * the key: two draws of one mesh differing only in these would otherwise share a cached
@@ -8584,6 +8942,9 @@ export class WebGPURenderer implements RendererApi {
8584
8942
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8585
8943
  /* Added rather than blended over: its own blend state, so its own pipeline. */
8586
8944
  const adds = blend && options.additive === true;
8945
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
8946
+ /* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
8947
+ const splits = modelKind === 'skin' && !blend && this.skinSplits();
8587
8948
  const key =
8588
8949
  `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
8589
8950
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
@@ -8591,7 +8952,13 @@ export class WebGPURenderer implements RendererApi {
8591
8952
  one must never share a cached pipeline with the same mesh drawn into the other. */
8592
8953
  `${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
8593
8954
  /* Culling is pipeline state here: a two-sided surface culls nothing. */
8594
- `${this.materialDoubleSided ? '|2s' : ''}`;
8955
+ `${this.materialDoubleSided ? '|2s' : ''}` +
8956
+ /* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
8957
+ `${coverage ? '|a2c' : ''}` +
8958
+ /* And the material's shading model, which is the fragment stage's own switch. */
8959
+ `${modelKind === null ? '' : `|m:${modelKind}`}` +
8960
+ /* And which half of a skin it draws, under the screen-space blur. */
8961
+ `${splits ? '|ss' : ''}`;
8595
8962
 
8596
8963
  /*
8597
8964
  * The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
@@ -8637,6 +9004,11 @@ export class WebGPURenderer implements RendererApi {
8637
9004
  false,
8638
9005
  this.oitMode,
8639
9006
  this.materialDoubleSided,
9007
+ coverage,
9008
+ eight,
9009
+ clothed,
9010
+ modelKind,
9011
+ splits ? 'scene' : 'whole',
8640
9012
  );
8641
9013
  }
8642
9014
 
@@ -8690,6 +9062,21 @@ export class WebGPURenderer implements RendererApi {
8690
9062
  pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8691
9063
  pass.drawIndexed(mesh.indexCount);
8692
9064
  }
9065
+ if (splits) {
9066
+ this.keepSkinDiffuse(
9067
+ mesh,
9068
+ key,
9069
+ base,
9070
+ skinned,
9071
+ morphed,
9072
+ deltas,
9073
+ layer,
9074
+ eight,
9075
+ clothed,
9076
+ slot,
9077
+ material,
9078
+ );
9079
+ }
8693
9080
  }
8694
9081
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
8695
9082
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -8700,7 +9087,7 @@ export class WebGPURenderer implements RendererApi {
8700
9087
  }
8701
9088
  if (unlitOrUnfogged) {
8702
9089
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
8703
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
9090
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
8704
9091
  }
8705
9092
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
8706
9093
  shared, so a strength left set is worn by everything drawn after it. */
@@ -8708,6 +9095,10 @@ export class WebGPURenderer implements RendererApi {
8708
9095
  if (!toneMapped) {
8709
9096
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
8710
9097
  }
9098
+ if (ditheredBlend) {
9099
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
9100
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
9101
+ }
8711
9102
  if (own) this.materials.dirty();
8712
9103
  }
8713
9104
 
@@ -8947,6 +9338,7 @@ export class WebGPURenderer implements RendererApi {
8947
9338
  lands ? this.correctedViewProj : this.viewProj,
8948
9339
  );
8949
9340
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
9341
+ this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
8950
9342
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
8951
9343
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
8952
9344
 
@@ -8957,7 +9349,7 @@ export class WebGPURenderer implements RendererApi {
8957
9349
  const lit = options.lit ?? true;
8958
9350
  const fog = fogModeOf(options.fog ?? true, blend && options.additive === true);
8959
9351
  const toneMapped = options.toneMapped ?? true;
8960
- const unlitOrUnfogged = !lit || fog !== FOG_RECEDE;
9352
+ const unlitOrUnfogged = !lit || fog !== this.surfaceFog;
8961
9353
  if (unlitOrUnfogged) {
8962
9354
  if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
8963
9355
  this.perFrameInts[this.materialField('uFogEnabled')] = fog;
@@ -8970,8 +9362,16 @@ export class WebGPURenderer implements RendererApi {
8970
9362
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
8971
9363
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
8972
9364
  const refracting = blend && this.bindSeeThrough(options);
8973
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
9365
+ /* As a single draw: a blended batch tests a dithered cutout hard, an opaque one in a
9366
+ multisampled frame takes coverage. See `cutoutDither.ts`. */
9367
+ const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
9368
+ if (ditheredBlend) this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
9369
+ const coverage = !blend && this.cutoutResolveStaged === 'coverage';
9370
+ const own =
9371
+ ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
8974
9372
  if (own) this.materials.dirty();
9373
+ /* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
9374
+ const modelKind = this.materialModel?.kind ?? null;
8975
9375
  const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
8976
9376
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
8977
9377
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
@@ -8980,7 +9380,8 @@ export class WebGPURenderer implements RendererApi {
8980
9380
  const key =
8981
9381
  `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8982
9382
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8983
- `${this.materialDoubleSided ? '|2s' : ''}`;
9383
+ `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
9384
+ `${modelKind === null ? '' : `|m:${modelKind}`}`;
8984
9385
 
8985
9386
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8986
9387
  let pipeline = pipelines.peek(key);
@@ -9004,6 +9405,10 @@ export class WebGPURenderer implements RendererApi {
9004
9405
  true,
9005
9406
  'none',
9006
9407
  this.materialDoubleSided,
9408
+ coverage,
9409
+ false,
9410
+ false,
9411
+ modelKind,
9007
9412
  );
9008
9413
  }
9009
9414
 
@@ -9052,12 +9457,16 @@ export class WebGPURenderer implements RendererApi {
9052
9457
  }
9053
9458
  if (unlitOrUnfogged) {
9054
9459
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
9055
- this.perFrameInts[this.materialField('uFogEnabled')] = FOG_RECEDE;
9460
+ this.perFrameInts[this.materialField('uFogEnabled')] = this.surfaceFog;
9056
9461
  }
9057
9462
  if (!toneMapped) {
9058
9463
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
9059
9464
  }
9060
9465
  if (refracting) this.releaseSeeThrough();
9466
+ if (ditheredBlend) {
9467
+ this.perFrameFloats[this.materialField('uCutout') + 1] =
9468
+ CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
9469
+ }
9061
9470
  if (own) this.materials.dirty();
9062
9471
  }
9063
9472
 
@@ -9295,12 +9704,19 @@ export class WebGPURenderer implements RendererApi {
9295
9704
  this.bindSceneCasterMaterial(material);
9296
9705
  this.drawMesh(mesh as GpuMesh, model);
9297
9706
  },
9298
- skinnedMesh: (mesh, model, palette, material) => {
9707
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
9299
9708
  this.bindSceneCasterMaterial(material);
9300
9709
  this.setSkinPalette(palette);
9710
+ if (cloth !== undefined) {
9711
+ this.setCloth(
9712
+ cloth.binding as unknown as GpuClothBinding,
9713
+ cloth.particles as unknown as GpuClothParticles,
9714
+ );
9715
+ }
9301
9716
  this.drawMesh(mesh as GpuMesh, model);
9302
9717
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
9303
9718
  this.setSkinPalette(null);
9719
+ this.setCloth(null);
9304
9720
  },
9305
9721
  instanced: (batch, data, material) => {
9306
9722
  this.bindSceneCasterMaterial(material);
@@ -10208,11 +10624,15 @@ export class WebGPURenderer implements RendererApi {
10208
10624
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
10209
10625
  slot of its own. See `skinnedGroups`. */
10210
10626
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
10627
+ /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
10628
+ const cloth = skinned ? this.clothViews() : null;
10211
10629
  if (
10212
10630
  (this.skinnedGroups[at] ?? null) === null ||
10213
10631
  this.skinnedGroupsBeside[at] !== this.bindGroup ||
10214
10632
  this.skinnedGroupsPalette[at] !== palette ||
10215
- this.skinnedGroupsDeltas[at] !== deltas
10633
+ this.skinnedGroupsDeltas[at] !== deltas ||
10634
+ this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
10635
+ this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
10216
10636
  ) {
10217
10637
  this.skinnedGroups[at] = createFlatBindGroup(
10218
10638
  this.surface.device,
@@ -10223,10 +10643,13 @@ export class WebGPURenderer implements RendererApi {
10223
10643
  this.flatTextures,
10224
10644
  palette,
10225
10645
  deltas,
10646
+ cloth,
10226
10647
  );
10227
10648
  this.skinnedGroupsBeside[at] = this.bindGroup;
10228
10649
  this.skinnedGroupsPalette[at] = palette;
10229
10650
  this.skinnedGroupsDeltas[at] = deltas;
10651
+ this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
10652
+ this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
10230
10653
  }
10231
10654
  return this.skinnedGroups[at] as GPUBindGroup;
10232
10655
  }
@@ -10295,21 +10718,25 @@ export class WebGPURenderer implements RendererApi {
10295
10718
  const normalMap = this.normalMap;
10296
10719
  const ormMap = this.ormMap;
10297
10720
  const emissiveMap = this.emissiveMap;
10721
+ const modelMap = this.modelMap;
10298
10722
  this.albedo = null;
10299
10723
  this.normalMap = null;
10300
10724
  this.ormMap = null;
10301
10725
  this.emissiveMap = null;
10726
+ this.modelMap = null;
10302
10727
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
10303
10728
  this.albedo = albedo;
10304
10729
  this.normalMap = normalMap;
10305
10730
  this.ormMap = ormMap;
10306
10731
  this.emissiveMap = emissiveMap;
10732
+ this.modelMap = modelMap;
10307
10733
 
10308
10734
  this.bindGroup = this.flatGroupForMaps(
10309
10735
  this.albedo,
10310
10736
  this.normalMap,
10311
10737
  this.ormMap,
10312
10738
  this.emissiveMap,
10739
+ this.modelMap,
10313
10740
  );
10314
10741
  /* The open material lives in a slot of a buffer that no longer exists. */
10315
10742
  this.materials.dirty();
@@ -10352,7 +10779,8 @@ export class WebGPURenderer implements RendererApi {
10352
10779
  this.albedo === null &&
10353
10780
  this.normalMap === null &&
10354
10781
  this.ormMap === null &&
10355
- this.emissiveMap === null
10782
+ this.emissiveMap === null &&
10783
+ this.modelMap === null
10356
10784
  ) {
10357
10785
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
10358
10786
  this.bindGroupBudget.ask();
@@ -10364,6 +10792,7 @@ export class WebGPURenderer implements RendererApi {
10364
10792
  this.normalMap,
10365
10793
  this.ormMap,
10366
10794
  this.emissiveMap,
10795
+ this.modelMap,
10367
10796
  );
10368
10797
  }
10369
10798
 
@@ -11096,6 +11525,8 @@ export class WebGPURenderer implements RendererApi {
11096
11525
  * the only moment they can land; everything else keeps the path it has.
11097
11526
  */
11098
11527
  private recordBlended(reads: number, lands = this.drawsLate()): DrawCommand | null {
11528
+ /* Skin is spread and added before anything blended lands over it. See `runSkinScatter`. */
11529
+ if (this.skinScatter !== null && !this.skinScattered) this.runSkinScatter();
11099
11530
  const late = this.latePass;
11100
11531
  if (late !== null && lands) return late.take();
11101
11532
  return this.recordDraw(reads, this.currentTarget());
@@ -11183,6 +11614,16 @@ export class WebGPURenderer implements RendererApi {
11183
11614
  const dynamic = mesh.motion;
11184
11615
  const rows = dynamic?.previous ?? null;
11185
11616
  const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
11617
+ const cloth = this.cloth;
11618
+ if (
11619
+ cloth !== null &&
11620
+ this.skinPaletteSlot >= 0 &&
11621
+ mesh.isSkinned &&
11622
+ mesh.skinOffsets !== null
11623
+ ) {
11624
+ this.recordClothMotion(mesh, model, previous, cloth);
11625
+ return;
11626
+ }
11186
11627
  if (previous === null) {
11187
11628
  if (rewritten) this.motionPass?.recordDynamic(mesh, model, model, rows);
11188
11629
  return;
@@ -11229,6 +11670,58 @@ export class WebGPURenderer implements RendererApi {
11229
11670
  this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
11230
11671
  }
11231
11672
 
11673
+ /**
11674
+ * A cloth-bound draw's motion. **Recorded even where the caller named no previous place**, as a
11675
+ * rewritten mesh's is: the particles carry their own past, and a garment swinging on a character
11676
+ * standing still is motion nobody else can see. Where the rig is a `Mover` its last pose is taken
11677
+ * as a skinned mover's is; otherwise the rig is taken not to have moved.
11678
+ */
11679
+ private recordClothMotion(
11680
+ mesh: GpuMesh,
11681
+ model: ArrayLike<number>,
11682
+ previous: ArrayLike<number> | Mover | null,
11683
+ cloth: { binding: GpuClothBinding; particles: GpuClothParticles },
11684
+ ): void {
11685
+ const now = this.skinPalettes.view(this.skinPaletteSlot);
11686
+ if (now === null) return;
11687
+ let was: GPUTextureView | null = now;
11688
+ let wasModel: ArrayLike<number> = model;
11689
+ if (previous !== null && isMover(previous)) {
11690
+ const verdict = advanceMover(
11691
+ previous,
11692
+ this.moverRenderer,
11693
+ this.moverFrame,
11694
+ model,
11695
+ this.skinPaletteData,
11696
+ );
11697
+ if (verdict === MOVER_TWICE) {
11698
+ this.warnMoverTwice();
11699
+ return;
11700
+ }
11701
+ if (verdict === MOVER_MOTION) {
11702
+ const slot = this.skinPalettes.take(this.surface.device, previous.previousPalette);
11703
+ was = slot === null ? null : this.skinPalettes.view(slot);
11704
+ wasModel = previous.previousModel;
11705
+ }
11706
+ } else if (previous !== null) {
11707
+ wasModel = previous;
11708
+ }
11709
+ if (was === null) return;
11710
+ const particlesWas = cloth.particles.previousAt(this.moverFrame);
11711
+ /* Nothing said it moved and nothing did: the camera's motion, derived from depth, is the answer. */
11712
+ if (previous === null && particlesWas === cloth.particles.current) return;
11713
+ this.motionPass?.recordCloth(
11714
+ mesh,
11715
+ model,
11716
+ wasModel,
11717
+ now,
11718
+ was,
11719
+ cloth.binding.bindingView,
11720
+ cloth.particles.current,
11721
+ particlesWas,
11722
+ );
11723
+ }
11724
+
11232
11725
  private warnMoverTwice(): void {
11233
11726
  if (this.warnedMoverTwice) return;
11234
11727
  this.warnedMoverTwice = true;
@@ -11614,7 +12107,7 @@ export class WebGPURenderer implements RendererApi {
11614
12107
  'post.ssrResolve',
11615
12108
  SSR_RESOLVE_FRAG_WGSL,
11616
12109
  this.pipelines.format,
11617
- PREMULTIPLIED_OVER_BLEND,
12110
+ REFLECTION_OVER_BLEND,
11618
12111
  ),
11619
12112
  );
11620
12113
  resolve.setBindGroup(0, resolveGroup);
@@ -12318,6 +12811,150 @@ export class WebGPURenderer implements RendererApi {
12318
12811
  return true;
12319
12812
  }
12320
12813
 
12814
+ /**
12815
+ * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
12816
+ * skin yet, and this is the frame's own picture — not a mirror's, a probe's, an overlay after
12817
+ * the frame, or an order-independent set, none of which the spread reads.
12818
+ */
12819
+ private skinSplits(): boolean {
12820
+ return (
12821
+ this.skinScatter !== null &&
12822
+ !this.skinScattered &&
12823
+ !this.reflectionPassActive &&
12824
+ !this.probePassActive &&
12825
+ !this.framePresented &&
12826
+ !this.overlayActive &&
12827
+ this.oitMode === 'none'
12828
+ );
12829
+ }
12830
+
12831
+ /**
12832
+ * A skin draw's diffuse half, kept for the spread: the frame's draw again, with the same slots,
12833
+ * buffers and bind group, through the pipeline that writes the diffuse alone into the blur's
12834
+ * target and tests for the depth its frame half wrote.
12835
+ */
12836
+ private keepSkinDiffuse(
12837
+ mesh: GpuMesh,
12838
+ frameKey: string,
12839
+ base: string,
12840
+ skinned: boolean,
12841
+ morphed: boolean,
12842
+ deltas: GPUTextureView | null,
12843
+ layer: number,
12844
+ eight: boolean,
12845
+ clothed: boolean,
12846
+ slot: number,
12847
+ material: number,
12848
+ ): void {
12849
+ const skin = this.skinScatter;
12850
+ const pipelines = this.skinPipelines;
12851
+ const present = this.meshPresent.get(base);
12852
+ if (skin === null || pipelines === null || present === undefined) return;
12853
+ const key = `${frameKey}|sd`;
12854
+ const pipeline =
12855
+ pipelines.peek(key) ??
12856
+ flatPipeline(
12857
+ pipelines,
12858
+ this.surface.device,
12859
+ this.flatLayoutFor(skinned, morphed),
12860
+ this.variant,
12861
+ key,
12862
+ present,
12863
+ false,
12864
+ skinned,
12865
+ morphed,
12866
+ true,
12867
+ layer,
12868
+ false,
12869
+ 'none',
12870
+ this.materialDoubleSided,
12871
+ false,
12872
+ eight,
12873
+ clothed,
12874
+ 'skin',
12875
+ 'diffuse',
12876
+ );
12877
+ const command = skin.take();
12878
+ command.pipeline = pipeline;
12879
+ command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
12880
+ command.offsetA = slot;
12881
+ command.offsetB = material;
12882
+ command.offsetCount = 2;
12883
+ for (let index = 0; index < mesh.vertexBuffers.length; index++) {
12884
+ command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
12885
+ }
12886
+ command.vertexCount = mesh.vertexBuffers.length;
12887
+ command.indexBuffer = mesh.indexBuffer;
12888
+ command.indexed = true;
12889
+ command.count = mesh.indexCount;
12890
+ command.instances = 1;
12891
+ command.indirect = null;
12892
+ }
12893
+
12894
+ /**
12895
+ * Spread the frame's skin and add it back: once, at the first blended draw or before the frame's
12896
+ * last flush. **A fifth instance of the boundary `takeRefractSnapshot` describes** — flush, end
12897
+ * the pass, work between passes, reopen with `loadOp: 'load'` — and refused where that one is,
12898
+ * for its reasons. Between the passes: the kept diffuse halves into their target against the
12899
+ * frame's depth, the depth resolved, and `SkinScatterPass.spread` across and then down into the
12900
+ * frame's colour.
12901
+ */
12902
+ private runSkinScatter(): void {
12903
+ const skin = this.skinScatter;
12904
+ if (skin === null || this.skinScattered) return;
12905
+ /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
12906
+ this.skinScattered = true;
12907
+ if (!skin.pending) return;
12908
+ if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
12909
+ const encoder = this.encoder;
12910
+ const depth = this.depthView;
12911
+ const resolved = this.resolvedDepthView;
12912
+ const target = this.compositeTarget();
12913
+ const projection = this.frameProjection;
12914
+ if (encoder === null || depth === null || resolved === null) return;
12915
+ if (target === null || projection === null) return;
12916
+ if (this.pass === null && this.openPass() === null) return;
12917
+ if (this.pass === null) return;
12918
+ if (this.quality.frameGraph) this.flushGraph();
12919
+
12920
+ this.pass.end();
12921
+ const size = this.sceneSize();
12922
+ skin.size(size.width, size.height, this.samples);
12923
+ skin.drawDiffuse(encoder, depth, this.issueBound, this.gpuTimer.writesFor());
12924
+ this.resolveDepth(encoder);
12925
+ mat4.invert(this.skinInverseProjection, projection);
12926
+ const multisampled = this.compositeMsaa();
12927
+ skin.spread(
12928
+ encoder,
12929
+ this.pipelines,
12930
+ resolved,
12931
+ multisampled ?? target,
12932
+ multisampled === null ? null : target,
12933
+ this.samples,
12934
+ projection,
12935
+ this.skinInverseProjection,
12936
+ () => this.gpuTimer.writesFor(),
12937
+ );
12938
+ this.pass = encoder.beginRenderPass({
12939
+ label: 'skin.scattered',
12940
+ timestampWrites: this.gpuTimer.writesFor(),
12941
+ colorAttachments: [
12942
+ {
12943
+ view: multisampled ?? target,
12944
+ resolveTarget: multisampled !== null ? target : undefined,
12945
+ /* Loaded, not cleared: the frame so far, with its skin added. */
12946
+ loadOp: 'load',
12947
+ storeOp: resolvedStoreOp(
12948
+ multisampled !== null,
12949
+ false,
12950
+ this.quality.discardResolvedAttachments,
12951
+ ),
12952
+ },
12953
+ ],
12954
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
12955
+ });
12956
+ }
12957
+
12321
12958
  private takeVolumeDepth(): boolean {
12322
12959
  /*
12323
12960
  * **Never while the mirror is open, and this is a correctness fix rather than a saving.**
@@ -12940,6 +13577,8 @@ export class WebGPURenderer implements RendererApi {
12940
13577
  * `beginFrame` resets it. Silent, and exactly the kind of loss the arena is designed
12941
13578
  * against everywhere else.
12942
13579
  */
13580
+ /* Before the last flush, which may let the depth go that the spread still reads. */
13581
+ this.runSkinScatter();
12943
13582
  this.flushGraph('boundary', this.liveOutFinalFlush());
12944
13583
  /*
12945
13584
  * `ensurePass` rather than a null check: the frame's pass is opened on demand now, so a
@@ -16112,6 +16751,7 @@ export class WebGPURenderer implements RendererApi {
16112
16751
  * the 2026-08-13 rule asks for — one decision, bound twice.
16113
16752
  */
16114
16753
  dispose(): void {
16754
+ this.skinScatter?.dispose();
16115
16755
  this.motionPass?.dispose();
16116
16756
  this.motionPass = null;
16117
16757
  this.latePass?.dispose();
@@ -16152,3 +16792,12 @@ export class WebGPURenderer implements RendererApi {
16152
16792
  this.surface.dispose();
16153
16793
  }
16154
16794
  }
16795
+
16796
+ /**
16797
+ * The views a binding and a character's particles bind as, built once a pair and kept on the
16798
+ * particles: a bind group is cached by its views' identity, so a fresh object a draw would rebuild
16799
+ * the group every draw.
16800
+ */
16801
+ function clothViewsOf(binding: GpuClothBinding, particles: GpuClothParticles): ClothViews {
16802
+ return particles.viewsWith(binding);
16803
+ }