@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
@@ -1,6 +1,7 @@
1
1
  import { FrameBudget } from '../budget.ts';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
3
- import { FOG_RECEDE, fogModeOf } from '../drawFog.ts';
3
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.ts';
4
+ import { blendCovering, blendKeeping } from '../../sceneCoverage.ts';
4
5
  import { mat4 } from 'gl-matrix';
5
6
  import {
6
7
  GL_DEPTH_REMAP,
@@ -120,8 +121,31 @@ import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/un
120
121
 
121
122
  import type { RendererApi } from '../api.ts';
122
123
  import { flatFrag, flatVert } from '../../shaders/flat/index.ts';
123
- 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';
124
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);
125
149
 
126
150
  /** Shader-side encoding of `RenderQuality.outputTransform`. */
127
151
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
@@ -210,6 +234,8 @@ import type { IncrementalMesh } from '../../mesh.ts';
210
234
  import type { MeshData } from '../../mesh.ts';
211
235
  import type { MeshOptions } from '../api.ts';
212
236
  import { SurfaceTexture } from '../../surfaceTexture.ts';
237
+ import { glCompressedFormats } from '../../glCompressed.ts';
238
+ import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
213
239
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
214
240
  import { PlumeRenderer } from '../../plumeRenderer.ts';
215
241
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.ts';
@@ -255,6 +281,8 @@ import type { FlockParams } from '../../flockRenderer.ts';
255
281
  import { InstancedBatch } from './instanced.ts';
256
282
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
257
283
  import { cutoutOf } from '../../cutoutCaster.ts';
284
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
285
+ import type { CutoutResolve } from '../../cutoutDither.ts';
258
286
  import type { CutoutCaster } from '../../cutoutCaster.ts';
259
287
  import type { SurfaceTextureHandle } from '../api.ts';
260
288
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
@@ -1132,6 +1160,17 @@ export class WebGL2Renderer implements RendererApi {
1132
1160
  private flatMorphedUniforms: Record<string, WebGLUniformLocation> | null = null;
1133
1161
  private flatBothProgram: WebGLProgram | null = null;
1134
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;
1135
1174
  /** The morph weights the following draws use, or null for none. */
1136
1175
  private morphWeights: Float32Array | null = null;
1137
1176
  /**
@@ -1168,6 +1207,14 @@ export class WebGL2Renderer implements RendererApi {
1168
1207
  * before this existed.
1169
1208
  */
1170
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();
1171
1218
  /** So the refusal below is once per renderer rather than once per frame. See `setColourGrade`. */
1172
1219
  private warnedGradeWithoutComposite = false;
1173
1220
  /** Scratch for a volume's `uDepthToLocal`. Built per draw and never allocated in the loop. */
@@ -1190,6 +1237,30 @@ export class WebGL2Renderer implements RendererApi {
1190
1237
  private readonly skinPalette = new SkinPaletteTexture();
1191
1238
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
1192
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;
1193
1264
 
1194
1265
  private readonly emptyTexture2D: WebGLTexture;
1195
1266
  /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
@@ -1249,6 +1320,11 @@ export class WebGL2Renderer implements RendererApi {
1249
1320
  private readonly surfaceScene = new Float32Array(4);
1250
1321
  /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
1251
1322
  private oitWeight = 0;
1323
+ /**
1324
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
1325
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
1326
+ */
1327
+ private surfaceFog = FOG_RECEDE;
1252
1328
  private ditherFade = 0;
1253
1329
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
1254
1330
  private oitRefused = false;
@@ -1690,6 +1766,22 @@ export class WebGL2Renderer implements RendererApi {
1690
1766
  private materialHasMaps = false;
1691
1767
  /** The material last set, so a lit program rebuilt for glass can be given it. */
1692
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;
1693
1785
  /**
1694
1786
  * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
1695
1787
  * profile and the content have asked for since (`litWanted`).
@@ -1817,7 +1909,7 @@ export class WebGL2Renderer implements RendererApi {
1817
1909
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
1818
1910
  gl.useProgram(this.depthProgram);
1819
1911
  },
1820
- skinnedMesh: (mesh, model, palette, material) => {
1912
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
1821
1913
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1822
1914
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1823
1915
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -1827,8 +1919,25 @@ export class WebGL2Renderer implements RendererApi {
1827
1919
  }
1828
1920
  this.shadowDrawBudget.ask();
1829
1921
  const { gl } = this;
1830
- const u = this.depthSkinnedUniforms;
1831
- 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
+ );
1832
1941
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1833
1942
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1834
1943
  /* The peel layer reads the first pass's depths; `beginShadowPass` bound that texture to
@@ -1838,6 +1947,12 @@ export class WebGL2Renderer implements RendererApi {
1838
1947
  this.skinPalette.update(gl, palette);
1839
1948
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
1840
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
+ }
1841
1956
  /* Narrowing is safe for the reason `mesh` above gives: this renderer only ever handed
1842
1957
  out its own `Mesh`. */
1843
1958
  (mesh as Mesh).draw(gl);
@@ -1882,7 +1997,8 @@ export class WebGL2Renderer implements RendererApi {
1882
1997
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1883
1998
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1884
1999
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1885
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
2000
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
2001
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
1886
2002
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1887
2003
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
1888
2004
  }
@@ -2795,11 +2911,22 @@ export class WebGL2Renderer implements RendererApi {
2795
2911
  this.quality.discardResolvedAttachments,
2796
2912
  )
2797
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;
2798
2924
  /*
2799
2925
  * Said once at init, because the number a caller asked for and the number the part gave
2800
2926
  * them are not always the same, and a silently clamped sample count is a quality setting
2801
2927
  * that does not apply — the same failure mode `capabilityClamp` exists to avoid.
2802
2928
  */
2929
+ this.canvasSamples = Math.max(1, (gl.getParameter(gl.SAMPLES) as number | null) ?? 1);
2803
2930
  const got = this.sceneTarget?.sampleCount ?? 1;
2804
2931
  if (this.quality.sceneSamples > 1 && got !== this.quality.sceneSamples) {
2805
2932
  console.warn(
@@ -3013,6 +3140,7 @@ export class WebGL2Renderer implements RendererApi {
3013
3140
  this.disposed = true;
3014
3141
 
3015
3142
  const { gl } = this;
3143
+ if (!this.contextLost) this.skinScatter?.dispose();
3016
3144
  this.detachContextLoss();
3017
3145
  this.contextLostListeners.length = 0;
3018
3146
  this.contextRestoredListeners.length = 0;
@@ -3053,10 +3181,16 @@ export class WebGL2Renderer implements RendererApi {
3053
3181
  this.flatSkinnedProgram,
3054
3182
  this.flatMorphedProgram,
3055
3183
  this.flatBothProgram,
3184
+ this.flatSkinned8Program,
3185
+ this.flatBoth8Program,
3186
+ this.depthSkinned8Program,
3056
3187
  this.flatInstancedProgram,
3057
3188
  ]) {
3058
3189
  if (extra !== null) gl.deleteProgram(extra);
3059
3190
  }
3191
+ for (const entry of [...this.clothFlat.values(), ...this.clothDepth.values()]) {
3192
+ gl.deleteProgram(entry.program);
3193
+ }
3060
3194
  for (const program of [
3061
3195
  this.flatProgram,
3062
3196
  this.panelProgram,
@@ -3153,12 +3287,27 @@ export class WebGL2Renderer implements RendererApi {
3153
3287
  */
3154
3288
  private warmVariants(data: MeshData): void {
3155
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;
3156
3292
  const morphable = data.morphTargets !== undefined;
3157
3293
  for (const skinned of skinnable ? [false, true] : [false]) {
3158
3294
  for (const morphed of morphable ? [false, true] : [false]) {
3159
- if (skinned || morphed) this.ensureVariant(skinned, morphed);
3295
+ if (skinned || morphed) this.ensureVariant(skinned, morphed, skinned && eight);
3160
3296
  }
3161
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
+ }
3162
3311
  }
3163
3312
 
3164
3313
  /**
@@ -3203,25 +3352,36 @@ export class WebGL2Renderer implements RendererApi {
3203
3352
  * `bindMeshPass` would otherwise carry every uniform at zero, and zero is a real value for most
3204
3353
  * of them — the failure this file records for `uGrain` and `uAmbientGround`.
3205
3354
  */
3206
- private ensureVariant(skinned: boolean, morphed: boolean): void {
3355
+ private ensureVariant(skinned: boolean, morphed: boolean, eight = false): void {
3207
3356
  if (this.contextLost) return;
3208
- if (skinned && morphed && this.flatBothProgram !== null) return;
3209
- if (skinned && !morphed && this.flatSkinnedProgram !== null) return;
3210
- if (!skinned && morphed && this.flatMorphedProgram !== null) return;
3211
- 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
+ }
3212
3365
 
3213
3366
  const { gl } = this;
3214
- const label = `flat.${morphed ? 'morphed' : ''}${skinned ? 'skinned' : ''}`;
3367
+ const label = `flat.${morphed ? 'morphed' : ''}${skinned ? (eight ? 'skinned8' : 'skinned') : ''}`;
3215
3368
  /* `flatFragSource`, not a fresh `flatFrag`: every variant shades through one fragment stage
3216
3369
  at one light budget, and the uploads are written for that budget. */
3370
+ const vertex = flatVert({ skinned, morphed, instanced: false });
3217
3371
  const program = compileProgram(
3218
3372
  gl,
3219
- flatVert({ skinned, morphed, instanced: false }),
3373
+ skinned && eight ? skinEight(vertex) : vertex,
3220
3374
  this.flatFragSource,
3221
3375
  label,
3222
3376
  );
3223
3377
  const uniforms = uniformLocations(gl, program, label);
3224
- 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) {
3225
3385
  this.flatBothProgram = program;
3226
3386
  this.flatBothUniforms = uniforms;
3227
3387
  } else if (skinned) {
@@ -3238,6 +3398,8 @@ export class WebGL2Renderer implements RendererApi {
3238
3398
  if (camera !== null && env !== null) {
3239
3399
  gl.useProgram(program);
3240
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);
3241
3403
  this.useFlatProgram();
3242
3404
  }
3243
3405
  }
@@ -3250,11 +3412,24 @@ export class WebGL2Renderer implements RendererApi {
3250
3412
  * a canvas drawn procedurally at runtime, a decoded bitmap, a video frame — stays
3251
3413
  * the consumer's decision. What it gains is a GPU object with sane sampler state.
3252
3414
  */
3253
- createSurfaceTexture(
3254
- source: TexImageSource,
3255
- options: SurfaceTextureOptions = {},
3256
- ): SurfaceTexture {
3257
- 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;
3258
3433
  }
3259
3434
 
3260
3435
  /**
@@ -3264,10 +3439,10 @@ export class WebGL2Renderer implements RendererApi {
3264
3439
  * refusal names the first that is not.
3265
3440
  */
3266
3441
  createSurfaceTextureArray(
3267
- sources: readonly TexImageSource[],
3442
+ sources: readonly SurfaceSource[],
3268
3443
  options: SurfaceTextureOptions = {},
3269
3444
  ): SurfaceTexture {
3270
- return new SurfaceTexture(this.gl, sources, options);
3445
+ return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
3271
3446
  }
3272
3447
 
3273
3448
  /**
@@ -3325,6 +3500,176 @@ export class WebGL2Renderer implements RendererApi {
3325
3500
  }
3326
3501
  this.skinPalette.update(this.gl, palette);
3327
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);
3328
3673
  }
3329
3674
 
3330
3675
  /**
@@ -3346,6 +3691,16 @@ export class WebGL2Renderer implements RendererApi {
3346
3691
  this.materialDoubleSided = material?.doubleSided === true;
3347
3692
  this.materialHasMaps = materialHasMaps(material);
3348
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
+ }
3349
3704
  /*
3350
3705
  * Written to every flat program, because material state persists across draws and a skinned
3351
3706
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -3363,6 +3718,30 @@ export class WebGL2Renderer implements RendererApi {
3363
3718
  this.useFlatProgram();
3364
3719
  }
3365
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
+
3366
3745
  /**
3367
3746
  * One flat program's view of the current material.
3368
3747
  *
@@ -3413,7 +3792,7 @@ export class WebGL2Renderer implements RendererApi {
3413
3792
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3414
3793
  if (orm !== null) orm.bind(gl, ORM_TEXTURE_UNIT);
3415
3794
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
3416
- gl.uniform1i(u['uOrmEnabled'] ?? null, orm === null ? 0 : 1);
3795
+ this.materialFlags[1] = orm === null ? 0 : 1;
3417
3796
  gl.uniform3f(
3418
3797
  u['uOrmScale'] ?? null,
3419
3798
  material?.occlusionStrength ?? 1,
@@ -3427,7 +3806,7 @@ export class WebGL2Renderer implements RendererApi {
3427
3806
  *
3428
3807
  * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
3429
3808
  * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
3430
- * 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
3431
3810
  * and the read never happens. What would make that wrong is a read taken outside that branch:
3432
3811
  * every glow on a material without a map would go out.
3433
3812
  */
@@ -3436,7 +3815,7 @@ export class WebGL2Renderer implements RendererApi {
3436
3815
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3437
3816
  if (emissiveMap !== null) emissiveMap.bind(gl, EMISSIVE_TEXTURE_UNIT);
3438
3817
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
3439
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, emissiveMap === null ? 0 : 1);
3818
+ this.materialFlags[2] = emissiveMap === null ? 0 : 1;
3440
3819
  const emissiveScale = material?.emissiveScale ?? null;
3441
3820
  gl.uniform3f(
3442
3821
  u['uEmissiveScale'] ?? null,
@@ -3453,7 +3832,21 @@ export class WebGL2Renderer implements RendererApi {
3453
3832
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
3454
3833
  */
3455
3834
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
3456
- gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
3835
+ gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 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);
3457
3850
 
3458
3851
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
3459
3852
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -3471,12 +3864,14 @@ export class WebGL2Renderer implements RendererApi {
3471
3864
  array target, and with nothing there it is incomplete. */
3472
3865
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
3473
3866
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3474
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
3867
+ this.materialFlags[0] = 0;
3868
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
3475
3869
  return;
3476
3870
  }
3477
3871
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
3478
- gl.uniform1f(u['uAlbedoCutout'] ?? null, material?.cutout ?? 0);
3479
- 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);
3480
3875
  }
3481
3876
 
3482
3877
  /**
@@ -3495,7 +3890,7 @@ export class WebGL2Renderer implements RendererApi {
3495
3890
  * Rebind `SURFACE_TEXTURE_UNIT` to whatever `setSurfaceTexture` last put there.
3496
3891
  *
3497
3892
  * Only `drawSdfText` calls this, after the atlas has finished borrowing the same unit.
3498
- * 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
3499
3894
  * touched by the text draw and are still correct, since that draw runs its own separate
3500
3895
  * program. What moved is unit 15's actual `WebGLTexture` binding, which is GL state, not
3501
3896
  * program state, and this puts it back to what `currentSurfaceTexture` says it should be.
@@ -3585,6 +3980,28 @@ export class WebGL2Renderer implements RendererApi {
3585
3980
  this.writeFlatFloat('uGrain', Math.min(1, Math.max(0, amount)));
3586
3981
  }
3587
3982
 
3983
+ /**
3984
+ * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
3985
+ * True is the default, and what `bindMeshPass` restores.
3986
+ *
3987
+ * **The opaque counterpart of `TranslucentMeshOptions.fog`**, which a translucent draw has had
3988
+ * all along; the shader's per-draw switch was there, and nothing reached it for an ordinary mesh.
3989
+ * What it is for is a medium that darkens one thing and not another — a floor receding into the
3990
+ * distance under figures that must keep their full value however far from the camera they stand.
3991
+ * Material state like `setSurfaceGrain`: instanced and skinned draws read it too, and a
3992
+ * translucent draw's own `fog` is measured against it and puts it back.
3993
+ *
3994
+ * **What it gives up** is the medium's honesty: a surface out of the fog is drawn as though the
3995
+ * air between it and the eye were empty, so it should be a surface the scene means to stand
3996
+ * apart, not one whose distance is the point.
3997
+ */
3998
+ setSurfaceFog(enabled: boolean): void {
3999
+ if (this.contextLost) return;
4000
+ this.materials.dirty();
4001
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
4002
+ this.writeFlatInt('uFogEnabled', this.surfaceFog);
4003
+ }
4004
+
3588
4005
  /**
3589
4006
  * One float of material state, into every flat program.
3590
4007
  *
@@ -3602,6 +4019,15 @@ export class WebGL2Renderer implements RendererApi {
3602
4019
  this.useFlatProgram();
3603
4020
  }
3604
4021
 
4022
+ /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
4023
+ private writeFlatInt(name: string, value: number): void {
4024
+ for (const target of this.flatTargets) {
4025
+ this.gl.useProgram(target.program);
4026
+ this.gl.uniform1i(target.uniforms[name] ?? null, value);
4027
+ }
4028
+ this.useFlatProgram();
4029
+ }
4030
+
3605
4031
  /**
3606
4032
  * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
3607
4033
  * of detail of one thing.
@@ -3754,6 +4180,8 @@ export class WebGL2Renderer implements RendererApi {
3754
4180
  if (camera !== null && env !== null) {
3755
4181
  gl.useProgram(program);
3756
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);
3757
4185
  this.useFlatProgram();
3758
4186
  }
3759
4187
  }
@@ -3795,6 +4223,7 @@ export class WebGL2Renderer implements RendererApi {
3795
4223
  opacity: number,
3796
4224
  options: TranslucentMeshOptions = {},
3797
4225
  ): void {
4226
+ this.scatterSkin();
3798
4227
  this.submitInstanced(batch, data, opacity, true, options);
3799
4228
  }
3800
4229
 
@@ -3837,8 +4266,11 @@ export class WebGL2Renderer implements RendererApi {
3837
4266
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
3838
4267
  this.drawBudget.ask();
3839
4268
  this.ensureInstancedProgram();
3840
- const program = this.flatInstancedProgram;
3841
- 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;
3842
4274
  if (program === null || u === null) return;
3843
4275
 
3844
4276
  const { gl } = this;
@@ -3852,7 +4284,7 @@ export class WebGL2Renderer implements RendererApi {
3852
4284
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
3853
4285
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
3854
4286
  const refracting = blend && this.bindSeeThrough(u, options);
3855
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
4287
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
3856
4288
  if (own) this.materials.dirty();
3857
4289
  this.takeMaterial();
3858
4290
  const depthWrite = options.depthWrite ?? true;
@@ -3876,14 +4308,13 @@ export class WebGL2Renderer implements RendererApi {
3876
4308
  * part of what the count asks.
3877
4309
  */
3878
4310
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
3879
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
4311
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
3880
4312
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
3881
4313
 
3882
4314
  if (blend) {
3883
4315
  gl.enable(gl.BLEND);
3884
- if (options.additive === true) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
3885
- else
3886
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
4316
+ if (options.additive === true) blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
4317
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3887
4318
  if (!depthWrite) gl.depthMask(false);
3888
4319
  }
3889
4320
  /* The sign follows the compare, which `depthOffsetForLayer` owns — see `drawMesh`, where the
@@ -3897,8 +4328,19 @@ export class WebGL2Renderer implements RendererApi {
3897
4328
  /* Both sides of a double-sided material, as every mesh path does and WebGPU's pipeline does
3898
4329
  for both: without it a batch's far faces were culled here alone. */
3899
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));
3900
4339
  if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
3901
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));
3902
4344
  if (this.materialDoubleSided) gl.enable(gl.CULL_FACE);
3903
4345
 
3904
4346
  if (layer > 0) {
@@ -3916,7 +4358,7 @@ export class WebGL2Renderer implements RendererApi {
3916
4358
  outlives its frame, so a snapshot left on unit 14 is held against the next frame's copy. */
3917
4359
  if (refracting) this.unbindSeeThrough(u);
3918
4360
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
3919
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
4361
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
3920
4362
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
3921
4363
  if (own) this.materials.dirty();
3922
4364
  this.useFlatProgram();
@@ -4069,6 +4511,7 @@ export class WebGL2Renderer implements RendererApi {
4069
4511
  tint: Vec3,
4070
4512
  env: Environment,
4071
4513
  ): void {
4514
+ this.scatterSkin();
4072
4515
  // The engine decides, not the caller: every pass already knows where the
4073
4516
  // waterline is, and leaving it to each call site is how one of them ends up
4074
4517
  // raining dust into the sea.
@@ -4113,6 +4556,7 @@ export class WebGL2Renderer implements RendererApi {
4113
4556
  style: TextStyle,
4114
4557
  timeSec: number,
4115
4558
  ): void {
4559
+ this.scatterSkin();
4116
4560
  if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
4117
4561
  this.textBudget.ask();
4118
4562
  }
@@ -4218,6 +4662,7 @@ export class WebGL2Renderer implements RendererApi {
4218
4662
  }
4219
4663
 
4220
4664
  drawSdfText(handle: SdfTextRenderer, model: Float32Array, color: Vec3, opacity: number): void {
4665
+ this.scatterSkin();
4221
4666
  if (this.contextLost) return;
4222
4667
  const viewProj = this.frameViewProj;
4223
4668
  if (viewProj === null) return;
@@ -4317,6 +4762,7 @@ export class WebGL2Renderer implements RendererApi {
4317
4762
  env: Environment,
4318
4763
  timeSeconds: number,
4319
4764
  ): void {
4765
+ this.scatterSkin();
4320
4766
  if (data.count === 0) return;
4321
4767
  const { gl } = this;
4322
4768
  const u = batch.uniforms;
@@ -4378,6 +4824,7 @@ export class WebGL2Renderer implements RendererApi {
4378
4824
  coreGain: number,
4379
4825
  minWidthPerMetre = 0.004,
4380
4826
  ): void {
4827
+ this.scatterSkin();
4381
4828
  if (data.count === 0) return;
4382
4829
  const { gl } = this;
4383
4830
  const u = batch.uniforms;
@@ -4452,6 +4899,7 @@ export class WebGL2Renderer implements RendererApi {
4452
4899
  minWidthPerMetre = 0,
4453
4900
  additive = false,
4454
4901
  ): void {
4902
+ this.scatterSkin();
4455
4903
  if (data.count === 0) return;
4456
4904
  const { gl } = this;
4457
4905
  const u = lines.uniforms;
@@ -4697,6 +5145,7 @@ export class WebGL2Renderer implements RendererApi {
4697
5145
  originY = 0,
4698
5146
  originZ = 0,
4699
5147
  ): void {
5148
+ this.scatterSkin();
4700
5149
  plumes.draw(
4701
5150
  this.gl,
4702
5151
  this.frameViewFor(camera),
@@ -4755,6 +5204,7 @@ export class WebGL2Renderer implements RendererApi {
4755
5204
  windZ = 0,
4756
5205
  strength = 1,
4757
5206
  ): void {
5207
+ this.scatterSkin();
4758
5208
  if (caustics === null || !this.quality.water) return;
4759
5209
  const drew = caustics.draw(
4760
5210
  this.gl,
@@ -5084,6 +5534,14 @@ export class WebGL2Renderer implements RendererApi {
5084
5534
  this.uploadWind(this.depthUniforms, this.frameWind);
5085
5535
  gl.useProgram(this.depthSkinnedProgram);
5086
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
+ }
5087
5545
  gl.useProgram(this.depthCutoutProgram);
5088
5546
  this.uploadWind(this.depthCutoutUniforms, this.frameWind);
5089
5547
  gl.useProgram(this.depthProgram);
@@ -5110,14 +5568,21 @@ export class WebGL2Renderer implements RendererApi {
5110
5568
  this.bindSceneCasterMaterial(material);
5111
5569
  this.drawMesh(mesh as Mesh, model);
5112
5570
  },
5113
- skinnedMesh: (mesh, model, palette, material) => {
5571
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
5114
5572
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
5115
5573
  gives about the mesh: nothing else ever handed one out. */
5116
5574
  this.bindSceneCasterMaterial(material);
5117
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
+ }
5118
5582
  this.drawMesh(mesh as Mesh, model);
5119
5583
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
5120
5584
  this.setSkinPalette(null);
5585
+ this.setCloth(null);
5121
5586
  },
5122
5587
  instanced: (batch, data, material) => {
5123
5588
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
@@ -5256,7 +5721,8 @@ export class WebGL2Renderer implements RendererApi {
5256
5721
  gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
5257
5722
  if (cutout !== null) {
5258
5723
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
5259
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
5724
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
5725
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
5260
5726
  (cutout.albedo as SurfaceTexture).bind(gl, SURFACE_TEXTURE_UNIT);
5261
5727
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
5262
5728
  }
@@ -5395,11 +5861,13 @@ export class WebGL2Renderer implements RendererApi {
5395
5861
  morphed: boolean,
5396
5862
  instanced: boolean,
5397
5863
  label: string,
5864
+ eight = false,
5398
5865
  ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
5399
5866
  if (old === null) return null;
5867
+ const vertex = flatVert({ skinned, morphed, instanced });
5400
5868
  const program = compileProgram(
5401
5869
  gl,
5402
- flatVert({ skinned, morphed, instanced }),
5870
+ eight ? skinEight(vertex) : vertex,
5403
5871
  this.flatFragSource,
5404
5872
  label,
5405
5873
  );
@@ -5420,14 +5888,50 @@ export class WebGL2Renderer implements RendererApi {
5420
5888
  const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
5421
5889
  this.flatBothProgram = both?.program ?? null;
5422
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;
5423
5897
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
5424
5898
  this.flatInstancedProgram = instanced?.program ?? null;
5425
5899
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
5426
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
+
5427
5929
  this.flatTargets.length = 0;
5428
- for (const target of [plain, skinned, morphed, both, instanced]) {
5930
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
5429
5931
  if (target !== null) this.flatTargets.push(target);
5430
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);
5431
5935
  const camera = this.lastPassCamera;
5432
5936
  const env = this.lastPassEnv;
5433
5937
  for (const target of this.flatTargets) {
@@ -6072,7 +6576,7 @@ export class WebGL2Renderer implements RendererApi {
6072
6576
  */
6073
6577
  gl.disable(gl.CULL_FACE);
6074
6578
  gl.enable(gl.BLEND);
6075
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6579
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6076
6580
 
6077
6581
  gl.uniform4f(this.panelUniforms['uRect'] ?? null, rect.left, rect.top, rect.width, rect.height);
6078
6582
  gl.uniform2f(
@@ -6649,12 +7153,15 @@ export class WebGL2Renderer implements RendererApi {
6649
7153
 
6650
7154
  beginFrame(clearColor: Vec3): void {
6651
7155
  if (this.contextLost) return;
7156
+ this.skinScattered = false;
7157
+ this.skinScatter?.reset();
6652
7158
  if (this.litAsked) {
6653
7159
  this.litAsked = false;
6654
7160
  this.rebuildLit();
6655
7161
  }
6656
7162
  this.budget.reset();
6657
7163
  this.materials.dirty();
7164
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
6658
7165
  /*
6659
7166
  * Said once, for the reason `refuseOitMultisampled` gives: a quality setting that is enabled
6660
7167
  * and does nothing is the fault the capability clamp exists to prevent.
@@ -6731,6 +7238,8 @@ export class WebGL2Renderer implements RendererApi {
6731
7238
  if (this.contextLost) return;
6732
7239
  /* A view model left open would squeeze every pass after the world, the composite included. */
6733
7240
  if (this.viewModelActive) this.endViewModel();
7241
+ /* Skin not yet spread, because nothing blended came after it, is spread now. */
7242
+ this.scatterSkin();
6734
7243
 
6735
7244
  /*
6736
7245
  * Camera motion blur, built from the camera the frame was *rendered* with.
@@ -7291,6 +7800,16 @@ export class WebGL2Renderer implements RendererApi {
7291
7800
  * is a non-idempotent step entering this body; there is none today, and one added would show
7292
7801
  * as a skinned game behaving differently from an unskinned one.
7293
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;
7294
7813
  for (const target of this.flatTargets) {
7295
7814
  this.gl.useProgram(target.program);
7296
7815
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -7509,7 +8028,8 @@ export class WebGL2Renderer implements RendererApi {
7509
8028
  * shape `uOpacity` and `uTint` already use.
7510
8029
  */
7511
8030
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
7512
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
8031
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
8032
+ this.surfaceFog = FOG_RECEDE;
7513
8033
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
7514
8034
  gl.uniform1f(u['uGrain'] ?? null, 1);
7515
8035
  /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
@@ -7623,7 +8143,7 @@ export class WebGL2Renderer implements RendererApi {
7623
8143
  * the sort of per-draw cost this renderer avoids everywhere else.
7624
8144
  */
7625
8145
  /*
7626
- * 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`
7627
8147
  * is 0 until a caller asks for one, and a world that never asks would otherwise leave
7628
8148
  * this sampler incomplete for the whole session.
7629
8149
  */
@@ -7703,18 +8223,15 @@ export class WebGL2Renderer implements RendererApi {
7703
8223
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
7704
8224
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7705
8225
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
7706
- 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);
7707
8228
  /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
7708
8229
  gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
7709
8230
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
7710
8231
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
7711
8232
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
7712
- gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
7713
- gl.uniform1i(u['uDoubleSided'] ?? null, 0);
7714
- this.materialDoubleSided = false;
7715
- this.materialHasMaps = false;
7716
- this.currentMaterial = null;
7717
- this.currentSurfaceTexture = null;
8233
+ gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
8234
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, 0);
7718
8235
 
7719
8236
  /*
7720
8237
  * **And the normal map, which this did not reset and had to.**
@@ -7742,16 +8259,20 @@ export class WebGL2Renderer implements RendererApi {
7742
8259
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
7743
8260
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7744
8261
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
7745
- gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
7746
8262
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
7747
8263
 
7748
8264
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
7749
8265
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
7750
8266
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
7751
8267
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
7752
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
7753
8268
  gl.uniform3f(u['uEmissiveScale'] ?? null, 1, 1, 1);
7754
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
+
7755
8276
  /*
7756
8277
  * White, because a uniform nobody has written is **zero**, and this one multiplies
7757
8278
  * vertex colour.
@@ -7862,6 +8383,66 @@ export class WebGL2Renderer implements RendererApi {
7862
8383
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model)) return;
7863
8384
  this.drawBudget.ask();
7864
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 {
7865
8446
  const { gl } = this;
7866
8447
  /*
7867
8448
  * A rigged mesh takes the skinned program, an unrigged one the plain program.
@@ -7881,40 +8462,86 @@ export class WebGL2Renderer implements RendererApi {
7881
8462
  * belongs to was never compiled. Deriving one from the other, as the first version did, made a
7882
8463
  * missing skinned-only program silently switch morph off as well.
7883
8464
  */
7884
- const both = wantSkin && wantMorph && this.flatBothUniforms !== null;
7885
- 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);
7886
8482
  const morphed = both || (wantMorph && !wantSkin && this.flatMorphedUniforms !== null);
7887
8483
  const variant =
7888
8484
  skinned && morphed
7889
- ? this.flatBothUniforms
8485
+ ? bothUniforms
7890
8486
  : skinned
7891
- ? this.flatSkinnedUniforms
8487
+ ? skinnedUniforms
7892
8488
  : morphed
7893
8489
  ? this.flatMorphedUniforms
7894
8490
  : null;
7895
- const u = variant ?? this.flatUniforms;
7896
- const program =
7897
- skinned && morphed
7898
- ? this.flatBothProgram
7899
- : skinned
7900
- ? this.flatSkinnedProgram
7901
- : morphed
7902
- ? this.flatMorphedProgram
7903
- : 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
+ }
7904
8529
 
7905
8530
  if (program !== null) {
7906
8531
  gl.useProgram(program);
7907
- if (skinned) {
8532
+ if (skinned || clothed !== undefined) {
7908
8533
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
7909
8534
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
7910
8535
  }
7911
- if (morphed && mesh.morph !== null) {
8536
+ if ((morphed || clothed !== undefined) && mesh.morph !== null) {
7912
8537
  mesh.morph.bind(gl, MORPH_DELTA_TEXTURE_UNIT);
7913
8538
  gl.uniform1i(u['uMorphDeltas'] ?? null, MORPH_DELTA_TEXTURE_UNIT);
7914
8539
  gl.uniform1i(u['uMorphTargetCount'] ?? null, mesh.morph.targetCount);
7915
8540
  gl.uniform1i(u['uMorphTextureWidth'] ?? null, mesh.morph.width);
7916
- gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights as Float32Array);
8541
+ gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights ?? NO_MORPH_WEIGHTS);
7917
8542
  }
8543
+ if (clothed !== undefined && cloth !== null) this.bindCloth(u, cloth);
8544
+ if (modelKind === 'eye') this.writeEyeAxis(u, model);
7918
8545
  } else {
7919
8546
  this.useFlatProgram();
7920
8547
  }
@@ -7947,11 +8574,20 @@ export class WebGL2Renderer implements RendererApi {
7947
8574
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
7948
8575
  * all. Film, caustics and text lie flush on surfaces for the same reason.
7949
8576
  */
7950
- 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);
7951
8585
  if (layer === 0) {
7952
8586
  if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
7953
8587
  mesh.draw(gl);
7954
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);
7955
8591
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
7956
8592
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
7957
8593
  if (program !== null) this.useFlatProgram();
@@ -7973,6 +8609,8 @@ export class WebGL2Renderer implements RendererApi {
7973
8609
  if (this.materialDoubleSided) gl.disable(gl.CULL_FACE);
7974
8610
  mesh.draw(gl);
7975
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);
7976
8614
  gl.polygonOffset(0, 0);
7977
8615
  gl.disable(gl.POLYGON_OFFSET_FILL);
7978
8616
  if (program !== null) this.useFlatProgram();
@@ -8025,6 +8663,7 @@ export class WebGL2Renderer implements RendererApi {
8025
8663
  opacity: number,
8026
8664
  options: TranslucentMeshOptions = {},
8027
8665
  ): void {
8666
+ this.scatterSkin();
8028
8667
  if (this.contextLost) return;
8029
8668
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
8030
8669
  and `Mesh.indexCount`. */
@@ -8047,10 +8686,19 @@ export class WebGL2Renderer implements RendererApi {
8047
8686
  this.drawBudget.ask();
8048
8687
 
8049
8688
  const { gl } = this;
8050
- const u = this.flatUniforms;
8051
- 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);
8052
8696
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
8053
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));
8054
8702
  /*
8055
8703
  * Both default to what `bindMeshPass` already put there — lit and fogged — so the
8056
8704
  * common call with no fourth argument writes the same two values it always implicitly
@@ -8061,7 +8709,7 @@ export class WebGL2Renderer implements RendererApi {
8061
8709
  const fog = fogModeOf(options.fog ?? true, options.additive === true);
8062
8710
  const toneMapped = options.toneMapped ?? true;
8063
8711
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
8064
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
8712
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, fog);
8065
8713
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
8066
8714
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
8067
8715
  renderer asked for `none` stays at none and one asked for `srgb` is already there. */
@@ -8093,15 +8741,15 @@ export class WebGL2Renderer implements RendererApi {
8093
8741
  */
8094
8742
  /* Taken for itself when any option differs from the pass, and put back below: the rule both
8095
8743
  backends count material changes by. See `materialChanges.ts`. */
8096
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
8744
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
8097
8745
  if (own) this.materials.dirty();
8098
8746
  this.takeMaterial();
8099
8747
  const ownsState = !this.oitReplaying;
8100
8748
  if (ownsState) {
8101
8749
  gl.enable(gl.BLEND);
8102
8750
  /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
8103
- if (options.additive === true) gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
8104
- else gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8751
+ if (options.additive === true) blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
8752
+ else blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8105
8753
  }
8106
8754
  /*
8107
8755
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -8128,12 +8776,14 @@ export class WebGL2Renderer implements RendererApi {
8128
8776
  if (!depthWrite && ownsState) gl.depthMask(true);
8129
8777
  if (ownsState) gl.disable(gl.BLEND);
8130
8778
  gl.uniform1f(u['uOpacity'] ?? null, 1);
8779
+ if (ditheredHere)
8780
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
8131
8781
  if (tint !== null) gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
8132
8782
  if (refracting) this.unbindSeeThrough(u);
8133
8783
  /* Put back for whatever is drawn next, exactly as `uOpacity` and `uTint` are — a pass
8134
8784
  cannot inherit a material from the draw before it. */
8135
8785
  if (!lit) gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
8136
- if (fog !== FOG_RECEDE) gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
8786
+ if (fog !== this.surfaceFog) gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
8137
8787
  if (!toneMapped) gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
8138
8788
  if (own) this.materials.dirty();
8139
8789
  }
@@ -8561,7 +9211,7 @@ export class WebGL2Renderer implements RendererApi {
8561
9211
  }
8562
9212
 
8563
9213
  gl.enable(gl.BLEND);
8564
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
9214
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
8565
9215
  gl.depthMask(false);
8566
9216
  gl.cullFace(inside ? gl.FRONT : gl.BACK);
8567
9217
  mesh.draw(gl);
@@ -8599,6 +9249,7 @@ export class WebGL2Renderer implements RendererApi {
8599
9249
  sheen: number,
8600
9250
  options: FilmOptions = {},
8601
9251
  ): void {
9252
+ this.scatterSkin();
8602
9253
  const { gl } = this;
8603
9254
  const u = this.filmUniforms;
8604
9255
  gl.useProgram(this.filmProgram);
@@ -8656,7 +9307,7 @@ export class WebGL2Renderer implements RendererApi {
8656
9307
  );
8657
9308
 
8658
9309
  gl.enable(gl.BLEND);
8659
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
9310
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
8660
9311
  gl.depthMask(false);
8661
9312
  mesh.draw(gl);
8662
9313
  gl.depthMask(true);
@@ -8761,3 +9412,22 @@ function angularDiscExponent(angularRadius: number): number {
8761
9412
  * name to prefer. It goes when something else forces a major, and not before.
8762
9413
  */
8763
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
+ }