@driftengine/core 4.8.3 → 4.8.5

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 (180) hide show
  1. package/CHANGELOG.json +82 -0
  2. package/README.md +1 -1
  3. package/dist/build/shaderComments.js +0 -0
  4. package/dist/geometry/meshConcat.js +2 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.js +16 -1
  7. package/dist/math/scalar.js +1 -1
  8. package/dist/render/anisotropicLobe.d.ts +7 -0
  9. package/dist/render/anisotropicLobe.js +33 -0
  10. package/dist/render/backend/api.d.ts +41 -4
  11. package/dist/render/backend/select.js +8 -0
  12. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  13. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  14. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  15. package/dist/render/backend/webgl2/renderer.js +604 -63
  16. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  17. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  18. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  19. package/dist/render/backend/webgpu/buffers.js +98 -49
  20. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  21. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  22. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  23. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  24. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  25. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  26. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  27. package/dist/render/backend/webgpu/computeHost.js +5 -0
  28. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  29. package/dist/render/backend/webgpu/depthPass.js +39 -9
  30. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  31. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  32. package/dist/render/backend/webgpu/flatPass.js +109 -12
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  34. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  35. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  36. package/dist/render/backend/webgpu/motionPass.js +106 -9
  37. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  38. package/dist/render/backend/webgpu/postPass.js +6 -3
  39. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  40. package/dist/render/backend/webgpu/renderer.js +498 -48
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  42. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  44. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  47. package/dist/render/clothBindingData.d.ts +41 -0
  48. package/dist/render/clothBindingData.js +72 -0
  49. package/dist/render/clothPlacement.d.ts +25 -0
  50. package/dist/render/clothPlacement.js +85 -0
  51. package/dist/render/compressedSource.d.ts +66 -0
  52. package/dist/render/compressedSource.js +138 -0
  53. package/dist/render/cutoutCaster.d.ts +2 -0
  54. package/dist/render/cutoutCaster.js +2 -0
  55. package/dist/render/cutoutDither.d.ts +51 -0
  56. package/dist/render/cutoutDither.js +90 -0
  57. package/dist/render/eyeRefraction.d.ts +35 -0
  58. package/dist/render/eyeRefraction.js +40 -0
  59. package/dist/render/glCompressed.d.ts +27 -0
  60. package/dist/render/glCompressed.js +53 -0
  61. package/dist/render/hairLobes.d.ts +70 -0
  62. package/dist/render/hairLobes.js +120 -0
  63. package/dist/render/lightBudget.d.ts +22 -0
  64. package/dist/render/lightBudget.js +22 -0
  65. package/dist/render/mesh.d.ts +7 -0
  66. package/dist/render/mesh.js +24 -0
  67. package/dist/render/recon/motionVectors.d.ts +4 -2
  68. package/dist/render/recon/motionVectors.js +6 -3
  69. package/dist/render/renderQuality.d.ts +20 -0
  70. package/dist/render/renderQuality.js +5 -0
  71. package/dist/render/rendererHarness.js +4 -0
  72. package/dist/render/sceneTarget.d.ts +12 -0
  73. package/dist/render/sceneTarget.js +14 -0
  74. package/dist/render/shaders/clothBinding.d.ts +22 -0
  75. package/dist/render/shaders/clothBinding.js +80 -0
  76. package/dist/render/shaders/depth.js +42 -6
  77. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  78. package/dist/render/shaders/flat/anisotropic.js +68 -0
  79. package/dist/render/shaders/flat/eye.d.ts +1 -0
  80. package/dist/render/shaders/flat/eye.js +81 -0
  81. package/dist/render/shaders/flat/hair.d.ts +1 -0
  82. package/dist/render/shaders/flat/hair.js +101 -0
  83. package/dist/render/shaders/flat/index.js +36 -2
  84. package/dist/render/shaders/flat/main.d.ts +1 -1
  85. package/dist/render/shaders/flat/main.js +113 -17
  86. package/dist/render/shaders/flat/models.d.ts +14 -0
  87. package/dist/render/shaders/flat/models.js +273 -0
  88. package/dist/render/shaders/flat/preamble.js +26 -24
  89. package/dist/render/shaders/flat/skin.d.ts +1 -0
  90. package/dist/render/shaders/flat/skin.js +114 -0
  91. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  92. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  93. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  94. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  95. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  96. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  97. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  98. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  99. package/dist/render/shaders/skinBlur.d.ts +1 -0
  100. package/dist/render/shaders/skinBlur.js +103 -0
  101. package/dist/render/shaders/skinning.d.ts +3 -1
  102. package/dist/render/shaders/skinning.js +31 -4
  103. package/dist/render/shadowCasters.d.ts +13 -1
  104. package/dist/render/skinBlur.d.ts +41 -0
  105. package/dist/render/skinBlur.js +49 -0
  106. package/dist/render/skinScatter.d.ts +67 -0
  107. package/dist/render/skinScatter.js +80 -0
  108. package/dist/render/skinnedClothRun.d.ts +26 -0
  109. package/dist/render/skinnedClothRun.js +84 -0
  110. package/dist/render/surfaceModel.d.ts +129 -0
  111. package/dist/render/surfaceModel.js +152 -0
  112. package/dist/render/surfaceTexture.d.ts +44 -4
  113. package/dist/render/surfaceTexture.js +60 -46
  114. package/dist/render/vertexDefaults.js +7 -0
  115. package/package.json +6 -6
  116. package/src/build/shaderComments.ts +0 -0
  117. package/src/geometry/meshConcat.ts +2 -0
  118. package/src/index.ts +40 -1
  119. package/src/math/scalar.ts +1 -1
  120. package/src/render/anisotropicLobe.ts +41 -0
  121. package/src/render/backend/api.ts +52 -3
  122. package/src/render/backend/select.ts +8 -0
  123. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  124. package/src/render/backend/webgl2/renderer.ts +693 -64
  125. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  126. package/src/render/backend/webgpu/buffers.ts +137 -63
  127. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  128. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  129. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  130. package/src/render/backend/webgpu/computeHost.ts +25 -0
  131. package/src/render/backend/webgpu/depthPass.ts +39 -7
  132. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  133. package/src/render/backend/webgpu/flatPass.ts +145 -7
  134. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  135. package/src/render/backend/webgpu/motionPass.ts +128 -9
  136. package/src/render/backend/webgpu/postPass.ts +5 -1
  137. package/src/render/backend/webgpu/renderer.ts +663 -43
  138. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  139. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  140. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  141. package/src/render/clothBindingData.ts +89 -0
  142. package/src/render/clothPlacement.ts +114 -0
  143. package/src/render/compressedSource.ts +198 -0
  144. package/src/render/cutoutCaster.ts +5 -0
  145. package/src/render/cutoutDither.ts +99 -0
  146. package/src/render/eyeRefraction.ts +72 -0
  147. package/src/render/glCompressed.ts +86 -0
  148. package/src/render/hairLobes.ts +188 -0
  149. package/src/render/lightBudget.ts +25 -0
  150. package/src/render/mesh.ts +23 -0
  151. package/src/render/recon/motionVectors.ts +6 -3
  152. package/src/render/renderQuality.ts +28 -0
  153. package/src/render/rendererHarness.ts +4 -0
  154. package/src/render/sceneTarget.ts +21 -0
  155. package/src/render/shaders/clothBinding.ts +81 -0
  156. package/src/render/shaders/depth.ts +42 -6
  157. package/src/render/shaders/flat/anisotropic.ts +68 -0
  158. package/src/render/shaders/flat/eye.ts +82 -0
  159. package/src/render/shaders/flat/hair.ts +102 -0
  160. package/src/render/shaders/flat/index.ts +36 -2
  161. package/src/render/shaders/flat/main.ts +113 -17
  162. package/src/render/shaders/flat/models.ts +286 -0
  163. package/src/render/shaders/flat/preamble.ts +26 -24
  164. package/src/render/shaders/flat/skin.ts +116 -0
  165. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  166. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  167. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  168. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  169. package/src/render/shaders/skinBlur.ts +105 -0
  170. package/src/render/shaders/skinning.ts +31 -4
  171. package/src/render/shadowCasters.ts +13 -0
  172. package/src/render/skinBlur.ts +53 -0
  173. package/src/render/skinScatter.ts +92 -0
  174. package/src/render/skinnedClothRun.ts +128 -0
  175. package/src/render/surfaceModel.ts +274 -0
  176. package/src/render/surfaceTexture.ts +123 -63
  177. package/src/render/vertexDefaults.ts +7 -0
  178. package/dist/math/exact.d.ts +0 -76
  179. package/dist/math/exact.js +0 -327
  180. package/src/math/exact.ts +0 -340
@@ -121,8 +121,31 @@ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/un
121
121
 
122
122
  import type { RendererApi } from '../api.ts';
123
123
  import { flatFrag, flatVert } from '../../shaders/flat/index.ts';
124
- import { MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT } from '../../lightBudget.ts';
124
+ import { skinEight } from '../../shaders/skinning.ts';
125
+ import {
126
+ MODEL_MAP_TEXTURE_UNIT,
127
+ MORPH_DELTA_TEXTURE_UNIT,
128
+ SKIN_PALETTE_TEXTURE_UNIT,
129
+ } from '../../lightBudget.ts';
130
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
131
+ import type { SurfaceModelKind } from '../../surfaceModel.ts';
132
+ import { modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
133
+ import type { SkinHalf } from '../../shaders/flat/models.ts';
134
+ import { GlSkinScatter } from './skinScatter.ts';
135
+ import {
136
+ CLOTH_BINDING_TEXTURE_UNIT,
137
+ CLOTH_PARTICLES_TEXTURE_UNIT,
138
+ CLOTH_REST_TEXTURE_UNIT,
139
+ } from '../../lightBudget.ts';
125
140
  import { SkinPaletteTexture } from './skinPaletteTexture.ts';
141
+ import { clothBound } from '../../shaders/clothBinding.ts';
142
+ import { validateClothBinding } from '../../clothBindingData.ts';
143
+ import type { ClothBindingData } from '../../clothBindingData.ts';
144
+ import { GlClothBinding, GlClothParticles } from './clothTextures.ts';
145
+ import { MAX_MORPH_TARGETS } from '../../skinPalette.ts';
146
+
147
+ /** The weights a cloth-bound draw of a morphing mesh deforms by when none were set: none. */
148
+ const NO_MORPH_WEIGHTS = new Float32Array(MAX_MORPH_TARGETS);
126
149
 
127
150
  /** Shader-side encoding of `RenderQuality.outputTransform`. */
128
151
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
@@ -211,6 +234,8 @@ import type { IncrementalMesh } from '../../mesh.ts';
211
234
  import type { MeshData } from '../../mesh.ts';
212
235
  import type { MeshOptions } from '../api.ts';
213
236
  import { SurfaceTexture } from '../../surfaceTexture.ts';
237
+ import { glCompressedFormats } from '../../glCompressed.ts';
238
+ import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
214
239
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
215
240
  import { PlumeRenderer } from '../../plumeRenderer.ts';
216
241
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.ts';
@@ -256,6 +281,8 @@ import type { FlockParams } from '../../flockRenderer.ts';
256
281
  import { InstancedBatch } from './instanced.ts';
257
282
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
258
283
  import { cutoutOf } from '../../cutoutCaster.ts';
284
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
285
+ import type { CutoutResolve } from '../../cutoutDither.ts';
259
286
  import type { CutoutCaster } from '../../cutoutCaster.ts';
260
287
  import type { SurfaceTextureHandle } from '../api.ts';
261
288
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
@@ -1133,6 +1160,17 @@ export class WebGL2Renderer implements RendererApi {
1133
1160
  private flatMorphedUniforms: Record<string, WebGLUniformLocation> | null = null;
1134
1161
  private flatBothProgram: WebGLProgram | null = null;
1135
1162
  private flatBothUniforms: Record<string, WebGLUniformLocation> | null = null;
1163
+ /**
1164
+ * The two skinned variants built with `SKIN_EIGHT` on, for a rig whose vertices name eight
1165
+ * joints: compiled the first time such a mesh is created, and only then. See `skinning.ts`.
1166
+ */
1167
+ private flatSkinned8Program: WebGLProgram | null = null;
1168
+ private flatSkinned8Uniforms: Record<string, WebGLUniformLocation> | null = null;
1169
+ private flatBoth8Program: WebGLProgram | null = null;
1170
+ private flatBoth8Uniforms: Record<string, WebGLUniformLocation> | null = null;
1171
+ /** And its shadow caster, so the shadow parts from nothing the body does. */
1172
+ private depthSkinned8Program: WebGLProgram | null = null;
1173
+ private depthSkinned8Uniforms: Record<string, WebGLUniformLocation | null> | null = null;
1136
1174
  /** The morph weights the following draws use, or null for none. */
1137
1175
  private morphWeights: Float32Array | null = null;
1138
1176
  /**
@@ -1169,6 +1207,14 @@ export class WebGL2Renderer implements RendererApi {
1169
1207
  * before this existed.
1170
1208
  */
1171
1209
  private readonly sceneTarget: SceneTarget | null;
1210
+ /**
1211
+ * Skin's screen-space scattering, made where the profile asks for it and has a composite to add
1212
+ * into, and null otherwise — which is every profile by default. See `GlSkinScatter`.
1213
+ */
1214
+ private readonly skinScatter: GlSkinScatter | null;
1215
+ /** Whether this frame has spread its skin: once, at its first blended draw or at its end. */
1216
+ private skinScattered = false;
1217
+ private readonly skinInverseProjection = mat4.create();
1172
1218
  /** So the refusal below is once per renderer rather than once per frame. See `setColourGrade`. */
1173
1219
  private warnedGradeWithoutComposite = false;
1174
1220
  /** Scratch for a volume's `uDepthToLocal`. Built per draw and never allocated in the loop. */
@@ -1191,6 +1237,30 @@ export class WebGL2Renderer implements RendererApi {
1191
1237
  private readonly skinPalette = new SkinPaletteTexture();
1192
1238
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
1193
1239
  private skinPaletteSet = false;
1240
+ /**
1241
+ * Cloth-bound skinned lit programs, keyed `eight|morphed`, compiled when a binding is made for a
1242
+ * mesh of that kind; and their casters, keyed by `eight`. See `ensureClothPrograms`.
1243
+ */
1244
+ private readonly clothFlat = new Map<
1245
+ string,
1246
+ { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> }
1247
+ >();
1248
+ private readonly clothDepth = new Map<
1249
+ string,
1250
+ { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation | null> }
1251
+ >();
1252
+ /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
1253
+ private cloth: { binding: GlClothBinding; particles: GlClothParticles } | null = null;
1254
+ /**
1255
+ * Each shading model's lit programs, keyed `variant|model` by the vertex variant they pair with,
1256
+ * compiled the first time a draw asks for one. See `modelProgram`.
1257
+ */
1258
+ private readonly modelFlat = new Map<
1259
+ string,
1260
+ { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> }
1261
+ >();
1262
+ /** The last palette chosen, which an eye's axis is turned by. See `setSkinPalette`. */
1263
+ private paletteData: Float32Array | null = null;
1194
1264
 
1195
1265
  private readonly emptyTexture2D: WebGLTexture;
1196
1266
  /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
@@ -1696,6 +1766,22 @@ export class WebGL2Renderer implements RendererApi {
1696
1766
  private materialHasMaps = false;
1697
1767
  /** The material last set, so a lit program rebuilt for glass can be given it. */
1698
1768
  private currentMaterial: SurfaceMaterial | null = null;
1769
+ /**
1770
+ * The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64
1771
+ * so the noise's argument stays small. See `cutoutDither.ts`.
1772
+ */
1773
+ private cutoutFrame = 0;
1774
+ /** Scratch for `uCutout`, so a material write allocates nothing. */
1775
+ private readonly cutoutVector = new Float32Array(4);
1776
+ /** The material's model numbers, `uModelParams`, packed by `packModel`. */
1777
+ private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
1778
+ /** The material's four switches, `uMaterialFlags`: albedo, ORM map, emissive map, two-sided. */
1779
+ private readonly materialFlags = new Int32Array(4);
1780
+ /**
1781
+ * The default framebuffer's sample count, read once, for a frame drawn with no scene target: a
1782
+ * dithered cutout there takes alpha-to-coverage when the canvas itself is multisampled.
1783
+ */
1784
+ private readonly canvasSamples: number;
1699
1785
  /**
1700
1786
  * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
1701
1787
  * profile and the content have asked for since (`litWanted`).
@@ -1823,7 +1909,7 @@ export class WebGL2Renderer implements RendererApi {
1823
1909
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1824
1910
  gl.useProgram(this.depthProgram);
1825
1911
  },
1826
- skinnedMesh: (mesh, model, palette, material) => {
1912
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
1827
1913
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1828
1914
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1829
1915
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -1833,8 +1919,25 @@ export class WebGL2Renderer implements RendererApi {
1833
1919
  }
1834
1920
  this.shadowDrawBudget.ask();
1835
1921
  const { gl } = this;
1836
- const u = this.depthSkinnedUniforms;
1837
- gl.useProgram(this.depthSkinnedProgram);
1922
+ /* A rig of eight casts through the caster built with SKIN_EIGHT on, as it draws. */
1923
+ const eightCaster =
1924
+ (mesh as Mesh).isSkinnedEight &&
1925
+ this.depthSkinned8Program !== null &&
1926
+ this.depthSkinned8Uniforms !== null;
1927
+ /* A garment casts through the cloth-bound caster of its rig's kind, as it draws. */
1928
+ const clothCaster =
1929
+ cloth === undefined
1930
+ ? undefined
1931
+ : this.clothDepth.get((mesh as Mesh).isSkinnedEight ? '8' : '4');
1932
+ const u =
1933
+ clothCaster?.uniforms ??
1934
+ (eightCaster
1935
+ ? (this.depthSkinned8Uniforms as Record<string, WebGLUniformLocation | null>)
1936
+ : this.depthSkinnedUniforms);
1937
+ gl.useProgram(
1938
+ clothCaster?.program ??
1939
+ (eightCaster ? this.depthSkinned8Program : this.depthSkinnedProgram),
1940
+ );
1838
1941
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1839
1942
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1840
1943
  /* The peel layer reads the first pass's depths; `beginShadowPass` bound that texture to
@@ -1844,6 +1947,12 @@ export class WebGL2Renderer implements RendererApi {
1844
1947
  this.skinPalette.update(gl, palette);
1845
1948
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
1846
1949
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
1950
+ if (clothCaster !== undefined && cloth !== undefined) {
1951
+ this.bindCloth(u, {
1952
+ binding: cloth.binding as unknown as GlClothBinding,
1953
+ particles: cloth.particles as unknown as GlClothParticles,
1954
+ });
1955
+ }
1847
1956
  /* Narrowing is safe for the reason `mesh` above gives: this renderer only ever handed
1848
1957
  out its own `Mesh`. */
1849
1958
  (mesh as Mesh).draw(gl);
@@ -1889,7 +1998,7 @@ export class WebGL2Renderer implements RendererApi {
1889
1998
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1890
1999
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1891
2000
  gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
1892
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
2001
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
1893
2002
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1894
2003
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
1895
2004
  }
@@ -2802,11 +2911,22 @@ export class WebGL2Renderer implements RendererApi {
2802
2911
  this.quality.discardResolvedAttachments,
2803
2912
  )
2804
2913
  : null;
2914
+ /* Screen-space skin needs the composite to add into; without one it is pre-integrated, said once. */
2915
+ if (this.quality.skinScattering === 'screen-space' && this.sceneTarget === null) {
2916
+ console.warn(
2917
+ "WebGL2: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated",
2918
+ );
2919
+ }
2920
+ this.skinScatter =
2921
+ this.quality.skinScattering === 'screen-space' && this.sceneTarget !== null
2922
+ ? new GlSkinScatter(gl)
2923
+ : null;
2805
2924
  /*
2806
2925
  * Said once at init, because the number a caller asked for and the number the part gave
2807
2926
  * them are not always the same, and a silently clamped sample count is a quality setting
2808
2927
  * that does not apply — the same failure mode `capabilityClamp` exists to avoid.
2809
2928
  */
2929
+ this.canvasSamples = Math.max(1, (gl.getParameter(gl.SAMPLES) as number | null) ?? 1);
2810
2930
  const got = this.sceneTarget?.sampleCount ?? 1;
2811
2931
  if (this.quality.sceneSamples > 1 && got !== this.quality.sceneSamples) {
2812
2932
  console.warn(
@@ -3020,6 +3140,7 @@ export class WebGL2Renderer implements RendererApi {
3020
3140
  this.disposed = true;
3021
3141
 
3022
3142
  const { gl } = this;
3143
+ if (!this.contextLost) this.skinScatter?.dispose();
3023
3144
  this.detachContextLoss();
3024
3145
  this.contextLostListeners.length = 0;
3025
3146
  this.contextRestoredListeners.length = 0;
@@ -3060,10 +3181,16 @@ export class WebGL2Renderer implements RendererApi {
3060
3181
  this.flatSkinnedProgram,
3061
3182
  this.flatMorphedProgram,
3062
3183
  this.flatBothProgram,
3184
+ this.flatSkinned8Program,
3185
+ this.flatBoth8Program,
3186
+ this.depthSkinned8Program,
3063
3187
  this.flatInstancedProgram,
3064
3188
  ]) {
3065
3189
  if (extra !== null) gl.deleteProgram(extra);
3066
3190
  }
3191
+ for (const entry of [...this.clothFlat.values(), ...this.clothDepth.values()]) {
3192
+ gl.deleteProgram(entry.program);
3193
+ }
3067
3194
  for (const program of [
3068
3195
  this.flatProgram,
3069
3196
  this.panelProgram,
@@ -3160,12 +3287,27 @@ export class WebGL2Renderer implements RendererApi {
3160
3287
  */
3161
3288
  private warmVariants(data: MeshData): void {
3162
3289
  const skinnable = data.joints !== undefined;
3290
+ /* A rig of eight takes the variants built with SKIN_EIGHT on, and a caster of its own. */
3291
+ const eight = skinnable && data.joints2 !== undefined;
3163
3292
  const morphable = data.morphTargets !== undefined;
3164
3293
  for (const skinned of skinnable ? [false, true] : [false]) {
3165
3294
  for (const morphed of morphable ? [false, true] : [false]) {
3166
- if (skinned || morphed) this.ensureVariant(skinned, morphed);
3295
+ if (skinned || morphed) this.ensureVariant(skinned, morphed, skinned && eight);
3167
3296
  }
3168
3297
  }
3298
+ if (eight && this.depthSkinned8Program === null && !this.contextLost) {
3299
+ this.depthSkinned8Program = compileProgram(
3300
+ this.gl,
3301
+ skinEight(DEPTH_SKINNED_VERT),
3302
+ DEPTH_FRAG,
3303
+ 'depth-skinned8',
3304
+ );
3305
+ this.depthSkinned8Uniforms = uniformLocations(
3306
+ this.gl,
3307
+ this.depthSkinned8Program,
3308
+ 'depth-skinned8',
3309
+ );
3310
+ }
3169
3311
  }
3170
3312
 
3171
3313
  /**
@@ -3210,25 +3352,36 @@ export class WebGL2Renderer implements RendererApi {
3210
3352
  * `bindMeshPass` would otherwise carry every uniform at zero, and zero is a real value for most
3211
3353
  * of them — the failure this file records for `uGrain` and `uAmbientGround`.
3212
3354
  */
3213
- private ensureVariant(skinned: boolean, morphed: boolean): void {
3355
+ private ensureVariant(skinned: boolean, morphed: boolean, eight = false): void {
3214
3356
  if (this.contextLost) return;
3215
- if (skinned && morphed && this.flatBothProgram !== null) return;
3216
- if (skinned && !morphed && this.flatSkinnedProgram !== null) return;
3217
- if (!skinned && morphed && this.flatMorphedProgram !== null) return;
3218
- if (!skinned && !morphed) return;
3357
+ if (skinned && eight) {
3358
+ if (morphed ? this.flatBoth8Program !== null : this.flatSkinned8Program !== null) return;
3359
+ } else {
3360
+ if (skinned && morphed && this.flatBothProgram !== null) return;
3361
+ if (skinned && !morphed && this.flatSkinnedProgram !== null) return;
3362
+ if (!skinned && morphed && this.flatMorphedProgram !== null) return;
3363
+ if (!skinned && !morphed) return;
3364
+ }
3219
3365
 
3220
3366
  const { gl } = this;
3221
- const label = `flat.${morphed ? 'morphed' : ''}${skinned ? 'skinned' : ''}`;
3367
+ const label = `flat.${morphed ? 'morphed' : ''}${skinned ? (eight ? 'skinned8' : 'skinned') : ''}`;
3222
3368
  /* `flatFragSource`, not a fresh `flatFrag`: every variant shades through one fragment stage
3223
3369
  at one light budget, and the uploads are written for that budget. */
3370
+ const vertex = flatVert({ skinned, morphed, instanced: false });
3224
3371
  const program = compileProgram(
3225
3372
  gl,
3226
- flatVert({ skinned, morphed, instanced: false }),
3373
+ skinned && eight ? skinEight(vertex) : vertex,
3227
3374
  this.flatFragSource,
3228
3375
  label,
3229
3376
  );
3230
3377
  const uniforms = uniformLocations(gl, program, label);
3231
- if (skinned && morphed) {
3378
+ if (skinned && eight && morphed) {
3379
+ this.flatBoth8Program = program;
3380
+ this.flatBoth8Uniforms = uniforms;
3381
+ } else if (skinned && eight) {
3382
+ this.flatSkinned8Program = program;
3383
+ this.flatSkinned8Uniforms = uniforms;
3384
+ } else if (skinned && morphed) {
3232
3385
  this.flatBothProgram = program;
3233
3386
  this.flatBothUniforms = uniforms;
3234
3387
  } else if (skinned) {
@@ -3245,6 +3398,8 @@ export class WebGL2Renderer implements RendererApi {
3245
3398
  if (camera !== null && env !== null) {
3246
3399
  gl.useProgram(program);
3247
3400
  this.writeMeshPassState(uniforms, camera, env);
3401
+ /* And the material the caller has set, which a program born mid-pass must carry too. */
3402
+ this.writeMaterialState(uniforms, this.currentMaterial);
3248
3403
  this.useFlatProgram();
3249
3404
  }
3250
3405
  }
@@ -3257,11 +3412,24 @@ export class WebGL2Renderer implements RendererApi {
3257
3412
  * a canvas drawn procedurally at runtime, a decoded bitmap, a video frame — stays
3258
3413
  * the consumer's decision. What it gains is a GPU object with sane sampler state.
3259
3414
  */
3260
- createSurfaceTexture(
3261
- source: TexImageSource,
3262
- options: SurfaceTextureOptions = {},
3263
- ): SurfaceTexture {
3264
- return new SurfaceTexture(this.gl, source, options);
3415
+ createSurfaceTexture(source: SurfaceSource, options: SurfaceTextureOptions = {}): SurfaceTexture {
3416
+ return new SurfaceTexture(this.gl, source, options, this.compressedFormats);
3417
+ }
3418
+
3419
+ private compressedCache: readonly CompressedTextureFormat[] | null = null;
3420
+
3421
+ /**
3422
+ * The BC formats this device takes as blocks, each named with its colour space (`'bc7-srgb'`).
3423
+ *
3424
+ * **Ask before choosing**, because a phone usually answers none: a BC texture handed to
3425
+ * `createSurfaceTexture` where its format is not here is refused by name rather than decoded,
3426
+ * since core ships no decoder. `@driftengine/assets`' loader asks this for every BC texture in a
3427
+ * container and decodes at load where the answer is no. Read from the four extensions WebGL2
3428
+ * spreads BC across; see `glCompressed.ts`.
3429
+ */
3430
+ get compressedFormats(): readonly CompressedTextureFormat[] {
3431
+ this.compressedCache ??= glCompressedFormats(this.gl);
3432
+ return this.compressedCache;
3265
3433
  }
3266
3434
 
3267
3435
  /**
@@ -3271,10 +3439,10 @@ export class WebGL2Renderer implements RendererApi {
3271
3439
  * refusal names the first that is not.
3272
3440
  */
3273
3441
  createSurfaceTextureArray(
3274
- sources: readonly TexImageSource[],
3442
+ sources: readonly SurfaceSource[],
3275
3443
  options: SurfaceTextureOptions = {},
3276
3444
  ): SurfaceTexture {
3277
- return new SurfaceTexture(this.gl, sources, options);
3445
+ return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
3278
3446
  }
3279
3447
 
3280
3448
  /**
@@ -3332,6 +3500,176 @@ export class WebGL2Renderer implements RendererApi {
3332
3500
  }
3333
3501
  this.skinPalette.update(this.gl, palette);
3334
3502
  this.skinPaletteSet = true;
3503
+ this.paletteData = palette;
3504
+ }
3505
+
3506
+ /**
3507
+ * The lit program for one vertex variant shading by a model: the standard program's vertex
3508
+ * stage, and the fragment stage with the model's switch on (`modelBound`). Compiled the first
3509
+ * time a draw asks — a scene that names no model compiles none — and fed the pass and the
3510
+ * material at once, as `ensureClothPrograms` feeds its own.
3511
+ */
3512
+ private modelProgram(
3513
+ variant: string,
3514
+ kind: SurfaceModelKind,
3515
+ /** Which half of a skin it draws under the screen-space blur. See `SkinHalf`. */
3516
+ half: SkinHalf = 'whole',
3517
+ ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } {
3518
+ const key = `${variant}|${kind}|${half}`;
3519
+ const held = this.modelFlat.get(key);
3520
+ if (held !== undefined) return held;
3521
+ const { gl } = this;
3522
+ const label = `flat.${variant}.${kind}.${half}`;
3523
+ const program = compileProgram(
3524
+ gl,
3525
+ litVertexOf(variant),
3526
+ skinHalfBound(modelBound(this.flatFragSource, kind), half),
3527
+ label,
3528
+ );
3529
+ const made = { program, uniforms: uniformLocations(gl, program, label) };
3530
+ this.modelFlat.set(key, made);
3531
+ this.flatTargets.push(made);
3532
+ gl.useProgram(program);
3533
+ const camera = this.lastPassCamera;
3534
+ const env = this.lastPassEnv;
3535
+ if (camera !== null && env !== null) this.writeMeshPassState(made.uniforms, camera, env);
3536
+ this.writeMaterialState(made.uniforms, this.currentMaterial);
3537
+ return made;
3538
+ }
3539
+
3540
+ /**
3541
+ * An eye's axis in the world for this draw, into its program's `uModelParams`: the material's
3542
+ * own numbers with the axis turned by the draw's model matrix and the eye's joint. See
3543
+ * `eyeAxisInWorld`.
3544
+ */
3545
+ private writeEyeAxis(u: Record<string, WebGLUniformLocation>, model: ArrayLike<number>): void {
3546
+ const eye = this.currentMaterial?.model;
3547
+ if (eye?.kind !== 'eye') return;
3548
+ eyeAxisInWorld(eye, model, this.skinPaletteSet ? this.paletteData : null, this.modelParams, 4);
3549
+ this.gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
3550
+ }
3551
+
3552
+ /**
3553
+ * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it,
3554
+ * and by how much. See `ClothBindingData`.
3555
+ *
3556
+ * Refused by name for a mesh with no rig — a binding blends a vertex between its skinning and the
3557
+ * cloth — and for one that disagrees with the mesh. Compiles the programs that draw and cast it
3558
+ * here, at creation, so no frame ever waits on a compile.
3559
+ */
3560
+ createClothBinding(mesh: Mesh, data: ClothBindingData): GlClothBinding {
3561
+ const target = mesh;
3562
+ if (!target.isSkinned) {
3563
+ throw new Error(
3564
+ 'createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
3565
+ 'skinning and the cloth',
3566
+ );
3567
+ }
3568
+ validateClothBinding(data, target.vertexCount);
3569
+ this.ensureClothPrograms(target.isSkinnedEight, target.morph !== null);
3570
+ return new GlClothBinding(this.gl, data);
3571
+ }
3572
+
3573
+ /** One character's particles, `count` of them, to be updated every frame. */
3574
+ createClothParticles(count: number): GlClothParticles {
3575
+ return new GlClothParticles(this.gl, count);
3576
+ }
3577
+
3578
+ /**
3579
+ * This frame's particles, three floats each, world space; last frame's are kept for motion. A
3580
+ * simulation's output, as it is.
3581
+ */
3582
+ updateClothParticles(particles: GlClothParticles, positions: Float32Array): void {
3583
+ if (this.contextLost) return;
3584
+ particles.update(this.gl, positions);
3585
+ }
3586
+
3587
+ /**
3588
+ * Place the following skinned draws by a cloth: `binding` the mesh's, `particles` the
3589
+ * character's. Null for none, which is every draw until this is called. Both or neither, and
3590
+ * the particles must be as many as the binding names.
3591
+ */
3592
+ setCloth(binding: GlClothBinding | null, particles: GlClothParticles | null = null): void {
3593
+ if (binding === null || particles === null) {
3594
+ this.cloth = null;
3595
+ return;
3596
+ }
3597
+ if (binding.particles !== particles.count) {
3598
+ throw new Error(
3599
+ `setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`,
3600
+ );
3601
+ }
3602
+ this.cloth = { binding, particles };
3603
+ }
3604
+
3605
+ disposeClothBinding(binding: GlClothBinding): void {
3606
+ if (this.contextLost) return;
3607
+ binding.dispose(this.gl);
3608
+ }
3609
+
3610
+ disposeClothParticles(particles: GlClothParticles): void {
3611
+ if (this.contextLost) return;
3612
+ particles.dispose(this.gl);
3613
+ }
3614
+
3615
+ /**
3616
+ * The cloth-bound lit program and caster for a rig of this kind, once. Fed at once if a pass is
3617
+ * bound, for the reason `ensureVariant` gives: a program compiled after `bindMeshPass` would
3618
+ * otherwise carry every uniform at zero.
3619
+ */
3620
+ private ensureClothPrograms(eight: boolean, morphed: boolean): void {
3621
+ if (this.contextLost) return;
3622
+ const { gl } = this;
3623
+ const key = `${eight ? 8 : 4}|${morphed ? 'm' : '-'}`;
3624
+ if (!this.clothFlat.has(key)) {
3625
+ const label = `flat.cloth${eight ? '8' : ''}${morphed ? '.morphed' : ''}`;
3626
+ const vertex = flatVert({ skinned: true, morphed, instanced: false });
3627
+ const program = compileProgram(
3628
+ gl,
3629
+ clothBound(eight ? skinEight(vertex) : vertex),
3630
+ this.flatFragSource,
3631
+ label,
3632
+ );
3633
+ const uniforms = uniformLocations(gl, program, label);
3634
+ this.clothFlat.set(key, { program, uniforms });
3635
+ this.flatTargets.push({ program, uniforms });
3636
+ const camera = this.lastPassCamera;
3637
+ const env = this.lastPassEnv;
3638
+ if (camera !== null && env !== null) {
3639
+ gl.useProgram(program);
3640
+ this.writeMeshPassState(uniforms, camera, env);
3641
+ this.writeMaterialState(uniforms, this.currentMaterial);
3642
+ this.useFlatProgram();
3643
+ }
3644
+ }
3645
+ const depthKey = eight ? '8' : '4';
3646
+ if (!this.clothDepth.has(depthKey)) {
3647
+ const label = `depth-cloth${eight ? '8' : ''}`;
3648
+ const program = compileProgram(
3649
+ gl,
3650
+ clothBound(eight ? skinEight(DEPTH_SKINNED_VERT) : DEPTH_SKINNED_VERT),
3651
+ DEPTH_FRAG,
3652
+ label,
3653
+ );
3654
+ this.clothDepth.set(depthKey, { program, uniforms: uniformLocations(gl, program, label) });
3655
+ }
3656
+ }
3657
+
3658
+ /** Bind the cloth's three textures to their units and name them to `u`. */
3659
+ private bindCloth(
3660
+ u: Record<string, WebGLUniformLocation | null>,
3661
+ cloth: { binding: GlClothBinding; particles: GlClothParticles },
3662
+ ): void {
3663
+ const { gl } = this;
3664
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_BINDING_TEXTURE_UNIT);
3665
+ gl.bindTexture(gl.TEXTURE_2D, cloth.binding.binding);
3666
+ gl.uniform1i(u['uClothBinding'] ?? null, CLOTH_BINDING_TEXTURE_UNIT);
3667
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_PARTICLES_TEXTURE_UNIT);
3668
+ gl.bindTexture(gl.TEXTURE_2D, cloth.particles.current);
3669
+ gl.uniform1i(u['uClothParticles'] ?? null, CLOTH_PARTICLES_TEXTURE_UNIT);
3670
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_REST_TEXTURE_UNIT);
3671
+ gl.bindTexture(gl.TEXTURE_2D, cloth.binding.rest);
3672
+ gl.uniform1i(u['uClothRest'] ?? null, CLOTH_REST_TEXTURE_UNIT);
3335
3673
  }
3336
3674
 
3337
3675
  /**
@@ -3353,6 +3691,16 @@ export class WebGL2Renderer implements RendererApi {
3353
3691
  this.materialDoubleSided = material?.doubleSided === true;
3354
3692
  this.materialHasMaps = materialHasMaps(material);
3355
3693
  this.currentMaterial = material;
3694
+ /* What the blur spreads this skin by: the last material to name a profile says. */
3695
+ const model = material?.model;
3696
+ if (model?.kind === 'skin' && this.skinScatter !== null) {
3697
+ this.skinScatter.setProfile(
3698
+ model.profile,
3699
+ model.scatterColor[0] * model.radius,
3700
+ model.scatterColor[1] * model.radius,
3701
+ model.scatterColor[2] * model.radius,
3702
+ );
3703
+ }
3356
3704
  /*
3357
3705
  * Written to every flat program, because material state persists across draws and a skinned
3358
3706
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -3370,6 +3718,30 @@ export class WebGL2Renderer implements RendererApi {
3370
3718
  this.useFlatProgram();
3371
3719
  }
3372
3720
 
3721
+ /**
3722
+ * How a material's cutout edge is drawn this frame: the material's mode against what the frame can
3723
+ * resolve. A translucent draw tests hard whatever the material says, because a blended draw has
3724
+ * nothing to average a pattern with. See `cutoutDither.ts`.
3725
+ */
3726
+ private cutoutResolveNow(material: SurfaceMaterial | null, translucent: boolean): CutoutResolve {
3727
+ if (translucent || material === null) return 'hard';
3728
+ return resolveCutout(
3729
+ material.cutoutMode ?? 'hard',
3730
+ this.temporalJittering,
3731
+ this.sceneTarget?.sampleCount ?? this.canvasSamples,
3732
+ );
3733
+ }
3734
+
3735
+ /** The lit pass's `uCutout` for a material this frame, into scratch. See `uCutout`. */
3736
+ private cutoutUniform(material: SurfaceMaterial | null, translucent: boolean): Float32Array {
3737
+ const v = this.cutoutVector;
3738
+ v[0] = material?.cutout ?? 0;
3739
+ v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
3740
+ v[2] = this.cutoutFrame;
3741
+ v[3] = 0;
3742
+ return v;
3743
+ }
3744
+
3373
3745
  /**
3374
3746
  * One flat program's view of the current material.
3375
3747
  *
@@ -3420,7 +3792,7 @@ export class WebGL2Renderer implements RendererApi {
3420
3792
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3421
3793
  if (orm !== null) orm.bind(gl, ORM_TEXTURE_UNIT);
3422
3794
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
3423
- gl.uniform1i(u['uOrmEnabled'] ?? null, orm === null ? 0 : 1);
3795
+ this.materialFlags[1] = orm === null ? 0 : 1;
3424
3796
  gl.uniform3f(
3425
3797
  u['uOrmScale'] ?? null,
3426
3798
  material?.occlusionStrength ?? 1,
@@ -3434,7 +3806,7 @@ export class WebGL2Renderer implements RendererApi {
3434
3806
  *
3435
3807
  * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
3436
3808
  * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
3437
- * unbound material keeps the arithmetic it had before only because `uEmissiveMapEnabled` is 0
3809
+ * unbound material keeps the arithmetic it had before only because its flag in `uMaterialFlags` is 0
3438
3810
  * and the read never happens. What would make that wrong is a read taken outside that branch:
3439
3811
  * every glow on a material without a map would go out.
3440
3812
  */
@@ -3443,7 +3815,7 @@ export class WebGL2Renderer implements RendererApi {
3443
3815
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3444
3816
  if (emissiveMap !== null) emissiveMap.bind(gl, EMISSIVE_TEXTURE_UNIT);
3445
3817
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
3446
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, emissiveMap === null ? 0 : 1);
3818
+ this.materialFlags[2] = emissiveMap === null ? 0 : 1;
3447
3819
  const emissiveScale = material?.emissiveScale ?? null;
3448
3820
  gl.uniform3f(
3449
3821
  u['uEmissiveScale'] ?? null,
@@ -3461,7 +3833,20 @@ export class WebGL2Renderer implements RendererApi {
3461
3833
  */
3462
3834
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
3463
3835
  gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 0);
3464
- gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
3836
+ this.materialFlags[3] = material?.doubleSided === true ? 1 : 0;
3837
+
3838
+ /*
3839
+ * The material's shading model: its numbers, and its map or the stand-in, before the albedo
3840
+ * early return as every map above is. Written to the standard programs too, which declare both
3841
+ * and read neither — one decision for every program, as `setMaterial` says.
3842
+ */
3843
+ const modelMap = material?.modelMap ?? null;
3844
+ gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
3845
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3846
+ if (modelMap !== null) modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
3847
+ gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
3848
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams);
3849
+ gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
3465
3850
 
3466
3851
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
3467
3852
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -3479,12 +3864,14 @@ export class WebGL2Renderer implements RendererApi {
3479
3864
  array target, and with nothing there it is incomplete. */
3480
3865
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
3481
3866
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3482
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
3867
+ this.materialFlags[0] = 0;
3868
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
3483
3869
  return;
3484
3870
  }
3485
3871
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
3486
- gl.uniform1f(u['uAlbedoCutout'] ?? null, material?.cutout ?? 0);
3487
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 1);
3872
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(material, false));
3873
+ this.materialFlags[0] = 1;
3874
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
3488
3875
  }
3489
3876
 
3490
3877
  /**
@@ -3503,7 +3890,7 @@ export class WebGL2Renderer implements RendererApi {
3503
3890
  * Rebind `SURFACE_TEXTURE_UNIT` to whatever `setSurfaceTexture` last put there.
3504
3891
  *
3505
3892
  * Only `drawSdfText` calls this, after the atlas has finished borrowing the same unit.
3506
- * No uniform is written here — the flat program's `uAlbedo`/`uAlbedoEnabled` were never
3893
+ * No uniform is written here — the flat program's `uAlbedo` and its flag in `uMaterialFlags` were never
3507
3894
  * touched by the text draw and are still correct, since that draw runs its own separate
3508
3895
  * program. What moved is unit 15's actual `WebGLTexture` binding, which is GL state, not
3509
3896
  * program state, and this puts it back to what `currentSurfaceTexture` says it should be.
@@ -3793,6 +4180,8 @@ export class WebGL2Renderer implements RendererApi {
3793
4180
  if (camera !== null && env !== null) {
3794
4181
  gl.useProgram(program);
3795
4182
  this.writeMeshPassState(uniforms, camera, env);
4183
+ /* And the material the caller has set, which a program born mid-pass must carry too. */
4184
+ this.writeMaterialState(uniforms, this.currentMaterial);
3796
4185
  this.useFlatProgram();
3797
4186
  }
3798
4187
  }
@@ -3834,6 +4223,7 @@ export class WebGL2Renderer implements RendererApi {
3834
4223
  opacity: number,
3835
4224
  options: TranslucentMeshOptions = {},
3836
4225
  ): void {
4226
+ this.scatterSkin();
3837
4227
  this.submitInstanced(batch, data, opacity, true, options);
3838
4228
  }
3839
4229
 
@@ -3876,8 +4266,11 @@ export class WebGL2Renderer implements RendererApi {
3876
4266
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
3877
4267
  this.drawBudget.ask();
3878
4268
  this.ensureInstancedProgram();
3879
- const program = this.flatInstancedProgram;
3880
- const u = this.flatInstancedUniforms;
4269
+ /* A material with a model takes that model's instanced program; see `modelProgram`. */
4270
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
4271
+ const modelled = modelKind === null ? null : this.modelProgram('instanced', modelKind);
4272
+ const program = modelled?.program ?? this.flatInstancedProgram;
4273
+ const u = modelled?.uniforms ?? this.flatInstancedUniforms;
3881
4274
  if (program === null || u === null) return;
3882
4275
 
3883
4276
  const { gl } = this;
@@ -3935,8 +4328,19 @@ export class WebGL2Renderer implements RendererApi {
3935
4328
  /* Both sides of a double-sided material, as every mesh path does and WebGPU's pipeline does
3936
4329
  for both: without it a batch's far faces were culled here alone. */
3937
4330
  if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
4331
+ /* A dithered cutout: coverage when this batch is opaque in a multisampled frame, the hard test
4332
+ when it is blended. See `cutoutDither.ts`. */
4333
+ const cutoutResolve = this.cutoutResolveNow(this.currentMaterial, false);
4334
+ const coverage = !blend && cutoutResolve === 'coverage';
4335
+ const ditheredBlend = blend && cutoutResolve !== 'hard';
4336
+ if (coverage) gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4337
+ if (ditheredBlend)
4338
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
3938
4339
  if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
3939
4340
  else batch.draw(gl, count);
4341
+ if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4342
+ if (ditheredBlend)
4343
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
3940
4344
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
3941
4345
 
3942
4346
  if (layer > 0) {
@@ -4107,6 +4511,7 @@ export class WebGL2Renderer implements RendererApi {
4107
4511
  tint: Vec3,
4108
4512
  env: Environment,
4109
4513
  ): void {
4514
+ this.scatterSkin();
4110
4515
  // The engine decides, not the caller: every pass already knows where the
4111
4516
  // waterline is, and leaving it to each call site is how one of them ends up
4112
4517
  // raining dust into the sea.
@@ -4151,6 +4556,7 @@ export class WebGL2Renderer implements RendererApi {
4151
4556
  style: TextStyle,
4152
4557
  timeSec: number,
4153
4558
  ): void {
4559
+ this.scatterSkin();
4154
4560
  if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
4155
4561
  this.textBudget.ask();
4156
4562
  }
@@ -4256,6 +4662,7 @@ export class WebGL2Renderer implements RendererApi {
4256
4662
  }
4257
4663
 
4258
4664
  drawSdfText(handle: SdfTextRenderer, model: Float32Array, color: Vec3, opacity: number): void {
4665
+ this.scatterSkin();
4259
4666
  if (this.contextLost) return;
4260
4667
  const viewProj = this.frameViewProj;
4261
4668
  if (viewProj === null) return;
@@ -4355,6 +4762,7 @@ export class WebGL2Renderer implements RendererApi {
4355
4762
  env: Environment,
4356
4763
  timeSeconds: number,
4357
4764
  ): void {
4765
+ this.scatterSkin();
4358
4766
  if (data.count === 0) return;
4359
4767
  const { gl } = this;
4360
4768
  const u = batch.uniforms;
@@ -4416,6 +4824,7 @@ export class WebGL2Renderer implements RendererApi {
4416
4824
  coreGain: number,
4417
4825
  minWidthPerMetre = 0.004,
4418
4826
  ): void {
4827
+ this.scatterSkin();
4419
4828
  if (data.count === 0) return;
4420
4829
  const { gl } = this;
4421
4830
  const u = batch.uniforms;
@@ -4490,6 +4899,7 @@ export class WebGL2Renderer implements RendererApi {
4490
4899
  minWidthPerMetre = 0,
4491
4900
  additive = false,
4492
4901
  ): void {
4902
+ this.scatterSkin();
4493
4903
  if (data.count === 0) return;
4494
4904
  const { gl } = this;
4495
4905
  const u = lines.uniforms;
@@ -4735,6 +5145,7 @@ export class WebGL2Renderer implements RendererApi {
4735
5145
  originY = 0,
4736
5146
  originZ = 0,
4737
5147
  ): void {
5148
+ this.scatterSkin();
4738
5149
  plumes.draw(
4739
5150
  this.gl,
4740
5151
  this.frameViewFor(camera),
@@ -4793,6 +5204,7 @@ export class WebGL2Renderer implements RendererApi {
4793
5204
  windZ = 0,
4794
5205
  strength = 1,
4795
5206
  ): void {
5207
+ this.scatterSkin();
4796
5208
  if (caustics === null || !this.quality.water) return;
4797
5209
  const drew = caustics.draw(
4798
5210
  this.gl,
@@ -5122,6 +5534,14 @@ export class WebGL2Renderer implements RendererApi {
5122
5534
  this.uploadWind(this.depthUniforms, this.frameWind);
5123
5535
  gl.useProgram(this.depthSkinnedProgram);
5124
5536
  this.uploadWind(this.depthSkinnedUniforms, this.frameWind);
5537
+ if (this.depthSkinned8Program !== null && this.depthSkinned8Uniforms !== null) {
5538
+ gl.useProgram(this.depthSkinned8Program);
5539
+ this.uploadWind(this.depthSkinned8Uniforms, this.frameWind);
5540
+ }
5541
+ for (const caster of this.clothDepth.values()) {
5542
+ gl.useProgram(caster.program);
5543
+ this.uploadWind(caster.uniforms, this.frameWind);
5544
+ }
5125
5545
  gl.useProgram(this.depthCutoutProgram);
5126
5546
  this.uploadWind(this.depthCutoutUniforms, this.frameWind);
5127
5547
  gl.useProgram(this.depthProgram);
@@ -5148,14 +5568,21 @@ export class WebGL2Renderer implements RendererApi {
5148
5568
  this.bindSceneCasterMaterial(material);
5149
5569
  this.drawMesh(mesh as Mesh, model);
5150
5570
  },
5151
- skinnedMesh: (mesh, model, palette, material) => {
5571
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
5152
5572
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
5153
5573
  gives about the mesh: nothing else ever handed one out. */
5154
5574
  this.bindSceneCasterMaterial(material);
5155
5575
  this.setSkinPalette(palette);
5576
+ if (cloth !== undefined) {
5577
+ this.setCloth(
5578
+ cloth.binding as unknown as GlClothBinding,
5579
+ cloth.particles as unknown as GlClothParticles,
5580
+ );
5581
+ }
5156
5582
  this.drawMesh(mesh as Mesh, model);
5157
5583
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
5158
5584
  this.setSkinPalette(null);
5585
+ this.setCloth(null);
5159
5586
  },
5160
5587
  instanced: (batch, data, material) => {
5161
5588
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
@@ -5295,7 +5722,7 @@ export class WebGL2Renderer implements RendererApi {
5295
5722
  if (cutout !== null) {
5296
5723
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
5297
5724
  gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
5298
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
5725
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
5299
5726
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
5300
5727
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
5301
5728
  }
@@ -5434,11 +5861,13 @@ export class WebGL2Renderer implements RendererApi {
5434
5861
  morphed: boolean,
5435
5862
  instanced: boolean,
5436
5863
  label: string,
5864
+ eight = false,
5437
5865
  ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
5438
5866
  if (old === null) return null;
5867
+ const vertex = flatVert({ skinned, morphed, instanced });
5439
5868
  const program = compileProgram(
5440
5869
  gl,
5441
- flatVert({ skinned, morphed, instanced }),
5870
+ eight ? skinEight(vertex) : vertex,
5442
5871
  this.flatFragSource,
5443
5872
  label,
5444
5873
  );
@@ -5459,14 +5888,50 @@ export class WebGL2Renderer implements RendererApi {
5459
5888
  const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
5460
5889
  this.flatBothProgram = both?.program ?? null;
5461
5890
  this.flatBothUniforms = both?.uniforms ?? null;
5891
+ const skinned8 = recompile(this.flatSkinned8Program, true, false, false, 'flat.skinned8', true);
5892
+ this.flatSkinned8Program = skinned8?.program ?? null;
5893
+ this.flatSkinned8Uniforms = skinned8?.uniforms ?? null;
5894
+ const both8 = recompile(this.flatBoth8Program, true, true, false, 'flat.morphedskinned8', true);
5895
+ this.flatBoth8Program = both8?.program ?? null;
5896
+ this.flatBoth8Uniforms = both8?.uniforms ?? null;
5462
5897
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
5463
5898
  this.flatInstancedProgram = instanced?.program ?? null;
5464
5899
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
5465
5900
 
5901
+ for (const [key, entry] of this.clothFlat) {
5902
+ const eight = key.startsWith('8');
5903
+ const withMorph = key.endsWith('m');
5904
+ const label = `flat.cloth${eight ? '8' : ''}${withMorph ? '.morphed' : ''}`;
5905
+ const vertex = flatVert({ skinned: true, morphed: withMorph, instanced: false });
5906
+ const program = compileProgram(
5907
+ gl,
5908
+ clothBound(eight ? skinEight(vertex) : vertex),
5909
+ this.flatFragSource,
5910
+ label,
5911
+ );
5912
+ gl.deleteProgram(entry.program);
5913
+ this.clothFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
5914
+ }
5915
+
5916
+ for (const [key, entry] of this.modelFlat) {
5917
+ const [variant, kind, half] = key.split('|') as [string, SurfaceModelKind, SkinHalf];
5918
+ const label = `flat.${variant}.${kind}.${half}`;
5919
+ const program = compileProgram(
5920
+ gl,
5921
+ litVertexOf(variant),
5922
+ skinHalfBound(modelBound(this.flatFragSource, kind), half),
5923
+ label,
5924
+ );
5925
+ gl.deleteProgram(entry.program);
5926
+ this.modelFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
5927
+ }
5928
+
5466
5929
  this.flatTargets.length = 0;
5467
- for (const target of [plain, skinned, morphed, both, instanced]) {
5930
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
5468
5931
  if (target !== null) this.flatTargets.push(target);
5469
5932
  }
5933
+ for (const target of this.clothFlat.values()) this.flatTargets.push(target);
5934
+ for (const target of this.modelFlat.values()) this.flatTargets.push(target);
5470
5935
  const camera = this.lastPassCamera;
5471
5936
  const env = this.lastPassEnv;
5472
5937
  for (const target of this.flatTargets) {
@@ -6688,12 +7153,15 @@ export class WebGL2Renderer implements RendererApi {
6688
7153
 
6689
7154
  beginFrame(clearColor: Vec3): void {
6690
7155
  if (this.contextLost) return;
7156
+ this.skinScattered = false;
7157
+ this.skinScatter?.reset();
6691
7158
  if (this.litAsked) {
6692
7159
  this.litAsked = false;
6693
7160
  this.rebuildLit();
6694
7161
  }
6695
7162
  this.budget.reset();
6696
7163
  this.materials.dirty();
7164
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
6697
7165
  /*
6698
7166
  * Said once, for the reason `refuseOitMultisampled` gives: a quality setting that is enabled
6699
7167
  * and does nothing is the fault the capability clamp exists to prevent.
@@ -6770,6 +7238,8 @@ export class WebGL2Renderer implements RendererApi {
6770
7238
  if (this.contextLost) return;
6771
7239
  /* A view model left open would squeeze every pass after the world, the composite included. */
6772
7240
  if (this.viewModelActive) this.endViewModel();
7241
+ /* Skin not yet spread, because nothing blended came after it, is spread now. */
7242
+ this.scatterSkin();
6773
7243
 
6774
7244
  /*
6775
7245
  * Camera motion blur, built from the camera the frame was *rendered* with.
@@ -7330,6 +7800,16 @@ export class WebGL2Renderer implements RendererApi {
7330
7800
  * is a non-idempotent step entering this body; there is none today, and one added would show
7331
7801
  * as a skinned game behaving differently from an unskinned one.
7332
7802
  */
7803
+ /*
7804
+ * **A pass starts with no material**, so it cannot inherit one from the pass before it. Here
7805
+ * rather than in `writeMeshPassState`, which also feeds a program compiled mid-pass — a model's,
7806
+ * a cloth's, the instanced one — and there forgot the material the caller had set, so the next
7807
+ * draws drew without it until somebody called `setMaterial` again.
7808
+ */
7809
+ this.materialDoubleSided = false;
7810
+ this.materialHasMaps = false;
7811
+ this.currentMaterial = null;
7812
+ this.currentSurfaceTexture = null;
7333
7813
  for (const target of this.flatTargets) {
7334
7814
  this.gl.useProgram(target.program);
7335
7815
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -7663,7 +8143,7 @@ export class WebGL2Renderer implements RendererApi {
7663
8143
  * the sort of per-draw cost this renderer avoids everywhere else.
7664
8144
  */
7665
8145
  /*
7666
- * And the surface unit, which starts every pass with no texture on it: `uAlbedoEnabled`
8146
+ * And the surface unit, which starts every pass with no texture on it: its flag in `uMaterialFlags`
7667
8147
  * is 0 until a caller asks for one, and a world that never asks would otherwise leave
7668
8148
  * this sampler incomplete for the whole session.
7669
8149
  */
@@ -7743,19 +8223,15 @@ export class WebGL2Renderer implements RendererApi {
7743
8223
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
7744
8224
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7745
8225
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
7746
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
8226
+ /* Every material switch off — the albedo, both maps, two-sided — in their one row. */
8227
+ gl.uniform4i(u['uMaterialFlags'] ?? null, 0, 0, 0, 0);
7747
8228
  /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
7748
8229
  gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
7749
8230
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
7750
8231
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
7751
8232
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
7752
8233
  gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
7753
- gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
7754
- gl.uniform1i(u['uDoubleSided'] ?? null, 0);
7755
- this.materialDoubleSided = false;
7756
- this.materialHasMaps = false;
7757
- this.currentMaterial = null;
7758
- this.currentSurfaceTexture = null;
8234
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, 0);
7759
8235
 
7760
8236
  /*
7761
8237
  * **And the normal map, which this did not reset and had to.**
@@ -7783,16 +8259,20 @@ export class WebGL2Renderer implements RendererApi {
7783
8259
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
7784
8260
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7785
8261
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
7786
- gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
7787
8262
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
7788
8263
 
7789
8264
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
7790
8265
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
7791
8266
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7792
8267
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
7793
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
7794
8268
  gl.uniform3f(u['uEmissiveScale'] ?? null, 1, 1, 1);
7795
8269
 
8270
+ /* And the shading model: its map back to the stand-in and its numbers to the standard's. */
8271
+ gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
8272
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
8273
+ gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
8274
+ gl.uniform4fv(u['uModelParams'] ?? null, NO_MODEL_PARAMS);
8275
+
7796
8276
  /*
7797
8277
  * White, because a uniform nobody has written is **zero**, and this one multiplies
7798
8278
  * vertex colour.
@@ -7903,6 +8383,66 @@ export class WebGL2Renderer implements RendererApi {
7903
8383
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model)) return;
7904
8384
  this.drawBudget.ask();
7905
8385
  this.takeMaterial();
8386
+ /* Skin under the screen-space blur draws its two halves: the frame's, then at once its diffuse. */
8387
+ const splits = this.skinSplits();
8388
+ this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
8389
+ if (splits && this.skinScatter !== null) {
8390
+ this.skinScatter.begin();
8391
+ this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
8392
+ this.sceneTarget?.bind();
8393
+ }
8394
+ }
8395
+
8396
+ /**
8397
+ * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
8398
+ * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
8399
+ * the frame — and the blur's target can be had.
8400
+ */
8401
+ private skinSplits(): boolean {
8402
+ const skin = this.skinScatter;
8403
+ const scene = this.sceneTarget;
8404
+ return (
8405
+ skin !== null &&
8406
+ scene !== null &&
8407
+ this.currentMaterial?.model?.kind === 'skin' &&
8408
+ !this.skinScattered &&
8409
+ !this.reflectionPassActive &&
8410
+ !this.probePassActive &&
8411
+ !this.framePresented &&
8412
+ skin.ready(scene)
8413
+ );
8414
+ }
8415
+
8416
+ /**
8417
+ * Spread the frame's skin and add it back: once, at the first blended draw or at the frame's end.
8418
+ * Called first in every draw WebGPU records as blended — translucent meshes and batches, text,
8419
+ * particles, plumes, wind streaks, bolts, lines, caustics, film — so the two backends add skin at
8420
+ * the same point. The frame's depth is copied for the blur to read, and where it cannot be the
8421
+ * diffuse goes back unspread rather than not at all.
8422
+ */
8423
+ private scatterSkin(): void {
8424
+ const skin = this.skinScatter;
8425
+ const scene = this.sceneTarget;
8426
+ const projection = this.frameProjection;
8427
+ if (skin === null || scene === null || this.skinScattered) return;
8428
+ /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8429
+ this.skinScattered = true;
8430
+ if (!skin.pending) return;
8431
+ if (this.reflectionPassActive || this.probePassActive || projection === null) return;
8432
+ const depth = scene.snapshotDepth(true);
8433
+ mat4.invert(this.skinInverseProjection, projection);
8434
+ skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
8435
+ this.useFlatProgram();
8436
+ }
8437
+
8438
+ /** One draw of a mesh, as `drawMesh` describes, of the half of a skin `half` names. */
8439
+ private drawMeshHalf(
8440
+ mesh: Mesh,
8441
+ model: ReadonlyMat4,
8442
+ depthLayer: number,
8443
+ tint: Vec3 | null,
8444
+ half: SkinHalf,
8445
+ ): void {
7906
8446
  const { gl } = this;
7907
8447
  /*
7908
8448
  * A rigged mesh takes the skinned program, an unrigged one the plain program.
@@ -7922,40 +8462,86 @@ export class WebGL2Renderer implements RendererApi {
7922
8462
  * belongs to was never compiled. Deriving one from the other, as the first version did, made a
7923
8463
  * missing skinned-only program silently switch morph off as well.
7924
8464
  */
7925
- const both = wantSkin && wantMorph && this.flatBothUniforms !== null;
7926
- const skinned = both || (wantSkin && this.flatSkinnedUniforms !== null);
8465
+ /* A rig of eight takes the pair built with SKIN_EIGHT on; `warmVariants` compiled them. */
8466
+ const eight = wantSkin && mesh.isSkinnedEight;
8467
+ /*
8468
+ * **A cloth set and a skinned draw take the cloth-bound program of this rig's kind**, compiled
8469
+ * when the binding was made. Its morph half is the mesh's whether weights are set or not: the
8470
+ * program declares the deltas whenever the mesh has them, so they are bound, at zero weight
8471
+ * when none were asked for — an unbound sampler would default to a shadow map's unit.
8472
+ */
8473
+ const cloth = wantSkin ? this.cloth : null;
8474
+ const clothed =
8475
+ cloth === null
8476
+ ? undefined
8477
+ : this.clothFlat.get(`${eight ? 8 : 4}|${mesh.morph !== null ? 'm' : '-'}`);
8478
+ const bothUniforms = eight ? this.flatBoth8Uniforms : this.flatBothUniforms;
8479
+ const skinnedUniforms = eight ? this.flatSkinned8Uniforms : this.flatSkinnedUniforms;
8480
+ const both = wantSkin && wantMorph && bothUniforms !== null;
8481
+ const skinned = both || (wantSkin && skinnedUniforms !== null);
7927
8482
  const morphed = both || (wantMorph && !wantSkin && this.flatMorphedUniforms !== null);
7928
8483
  const variant =
7929
8484
  skinned && morphed
7930
- ? this.flatBothUniforms
8485
+ ? bothUniforms
7931
8486
  : skinned
7932
- ? this.flatSkinnedUniforms
8487
+ ? skinnedUniforms
7933
8488
  : morphed
7934
8489
  ? this.flatMorphedUniforms
7935
8490
  : null;
7936
- const u = variant ?? this.flatUniforms;
7937
- const program =
7938
- skinned && morphed
7939
- ? this.flatBothProgram
7940
- : skinned
7941
- ? this.flatSkinnedProgram
7942
- : morphed
7943
- ? this.flatMorphedProgram
7944
- : null;
8491
+ let u = clothed?.uniforms ?? variant ?? this.flatUniforms;
8492
+ let program =
8493
+ clothed !== undefined
8494
+ ? clothed.program
8495
+ : skinned && morphed
8496
+ ? eight
8497
+ ? this.flatBoth8Program
8498
+ : this.flatBothProgram
8499
+ : skinned
8500
+ ? eight
8501
+ ? this.flatSkinned8Program
8502
+ : this.flatSkinnedProgram
8503
+ : morphed
8504
+ ? this.flatMorphedProgram
8505
+ : null;
8506
+ /* A material with a model takes that model's program for the same vertex variant. */
8507
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
8508
+ if (modelKind !== null) {
8509
+ const modelled = this.modelProgram(
8510
+ clothed !== undefined
8511
+ ? `cloth${eight ? 8 : 4}${mesh.morph !== null ? 'm' : '-'}`
8512
+ : skinned && morphed
8513
+ ? eight
8514
+ ? 'both8'
8515
+ : 'both'
8516
+ : skinned
8517
+ ? eight
8518
+ ? 'skinned8'
8519
+ : 'skinned'
8520
+ : morphed
8521
+ ? 'morphed'
8522
+ : 'plain',
8523
+ modelKind,
8524
+ half,
8525
+ );
8526
+ program = modelled.program;
8527
+ u = modelled.uniforms;
8528
+ }
7945
8529
 
7946
8530
  if (program !== null) {
7947
8531
  gl.useProgram(program);
7948
- if (skinned) {
8532
+ if (skinned || clothed !== undefined) {
7949
8533
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
7950
8534
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
7951
8535
  }
7952
- if (morphed && mesh.morph !== null) {
8536
+ if ((morphed || clothed !== undefined) && mesh.morph !== null) {
7953
8537
  mesh.morph.bind(gl, MORPH_DELTA_TEXTURE_UNIT);
7954
8538
  gl.uniform1i(u['uMorphDeltas'] ?? null, MORPH_DELTA_TEXTURE_UNIT);
7955
8539
  gl.uniform1i(u['uMorphTargetCount'] ?? null, mesh.morph.targetCount);
7956
8540
  gl.uniform1i(u['uMorphTextureWidth'] ?? null, mesh.morph.width);
7957
- gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights as Float32Array);
8541
+ gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights ?? NO_MORPH_WEIGHTS);
7958
8542
  }
8543
+ if (clothed !== undefined && cloth !== null) this.bindCloth(u, cloth);
8544
+ if (modelKind === 'eye') this.writeEyeAxis(u, model);
7959
8545
  } else {
7960
8546
  this.useFlatProgram();
7961
8547
  }
@@ -7988,11 +8574,20 @@ export class WebGL2Renderer implements RendererApi {
7988
8574
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
7989
8575
  * all. Film, caustics and text lie flush on surfaces for the same reason.
7990
8576
  */
7991
- gl.depthFunc(this.reversedDepth ? gl.GREATER : gl.LESS);
8577
+ /* A skin's diffuse half is the same surface again: it finds the depth its frame half wrote. */
8578
+ const again = half === 'diffuse';
8579
+ gl.depthFunc(again ? gl.EQUAL : this.reversedDepth ? gl.GREATER : gl.LESS);
8580
+ if (again) gl.depthMask(false);
8581
+ /* A dithered cutout in a multisampled frame with no temporal resolve: its share becomes the
8582
+ pixel's samples. Scoped to this draw, as the depth compare above is. See `cutoutDither.ts`. */
8583
+ const coverage = !again && this.cutoutResolveNow(this.currentMaterial, false) === 'coverage';
8584
+ if (coverage) gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
7992
8585
  if (layer === 0) {
7993
8586
  if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
7994
8587
  mesh.draw(gl);
7995
8588
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
8589
+ if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
8590
+ if (again) gl.depthMask(true);
7996
8591
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
7997
8592
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
7998
8593
  if (program !== null) this.useFlatProgram();
@@ -8014,6 +8609,8 @@ export class WebGL2Renderer implements RendererApi {
8014
8609
  if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
8015
8610
  mesh.draw(gl);
8016
8611
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
8612
+ if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
8613
+ if (again) gl.depthMask(true);
8017
8614
  gl.polygonOffset(0, 0);
8018
8615
  gl.disable(gl.POLYGON_OFFSET_FILL);
8019
8616
  if (program !== null) this.useFlatProgram();
@@ -8066,6 +8663,7 @@ export class WebGL2Renderer implements RendererApi {
8066
8663
  opacity: number,
8067
8664
  options: TranslucentMeshOptions = {},
8068
8665
  ): void {
8666
+ this.scatterSkin();
8069
8667
  if (this.contextLost) return;
8070
8668
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
8071
8669
  and `Mesh.indexCount`. */
@@ -8088,10 +8686,19 @@ export class WebGL2Renderer implements RendererApi {
8088
8686
  this.drawBudget.ask();
8089
8687
 
8090
8688
  const { gl } = this;
8091
- const u = this.flatUniforms;
8092
- this.useFlatProgram();
8689
+ /* A material with a model takes that model's plain program; see `modelProgram`. */
8690
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
8691
+ const modelled = modelKind === null ? null : this.modelProgram('plain', modelKind);
8692
+ const u = modelled?.uniforms ?? this.flatUniforms;
8693
+ if (modelled === null) this.useFlatProgram();
8694
+ else gl.useProgram(modelled.program);
8695
+ if (modelKind === 'eye') this.writeEyeAxis(u, model);
8093
8696
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
8094
8697
  gl.uniform1f(u['uOpacity'] ?? null, Math.min(opacity, 1));
8698
+ /* A dithered cutout tests hard on a blended draw, and is put back below with the rest. */
8699
+ const ditheredHere = this.cutoutResolveNow(this.currentMaterial, false) !== 'hard';
8700
+ if (ditheredHere)
8701
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
8095
8702
  /*
8096
8703
  * Both default to what `bindMeshPass` already put there — lit and fogged — so the
8097
8704
  * common call with no fourth argument writes the same two values it always implicitly
@@ -8169,6 +8776,8 @@ export class WebGL2Renderer implements RendererApi {
8169
8776
  if (!depthWrite && ownsState) gl.depthMask(true);
8170
8777
  if (ownsState) gl.disable(gl.BLEND);
8171
8778
  gl.uniform1f(u['uOpacity'] ?? null, 1);
8779
+ if (ditheredHere)
8780
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
8172
8781
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
8173
8782
  if (refracting) this.unbindSeeThrough(u);
8174
8783
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
@@ -8640,6 +9249,7 @@ export class WebGL2Renderer implements RendererApi {
8640
9249
  sheen: number,
8641
9250
  options: FilmOptions = {},
8642
9251
  ): void {
9252
+ this.scatterSkin();
8643
9253
  const { gl } = this;
8644
9254
  const u = this.filmUniforms;
8645
9255
  gl.useProgram(this.filmProgram);
@@ -8802,3 +9412,22 @@ function angularDiscExponent(angularRadius: number): number {
8802
9412
  * name to prefer. It goes when something else forces a major, and not before.
8803
9413
  */
8804
9414
  export { WebGL2Renderer as Renderer };
9415
+
9416
+ /** The standard model's numbers: no model, no map. */
9417
+ const NO_MODEL_PARAMS = new Float32Array(MODEL_PARAM_FLOATS);
9418
+
9419
+ /**
9420
+ * The lit vertex stage a program of this variant compiles: the standard programs' and the cloth's,
9421
+ * which `modelProgram` pairs a model's fragment stage with.
9422
+ */
9423
+ function litVertexOf(variant: string): string {
9424
+ if (variant.startsWith('cloth')) {
9425
+ const eight = variant[5] === '8';
9426
+ const vertex = flatVert({ skinned: true, morphed: variant.endsWith('m'), instanced: false });
9427
+ return clothBound(eight ? skinEight(vertex) : vertex);
9428
+ }
9429
+ const skinned = variant.startsWith('skinned') || variant.startsWith('both');
9430
+ const morphed = variant === 'morphed' || variant.startsWith('both');
9431
+ const vertex = flatVert({ skinned, morphed, instanced: variant === 'instanced' });
9432
+ return variant.endsWith('8') ? skinEight(vertex) : vertex;
9433
+ }