@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.js';
2
2
  import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
3
- import { FOG_RECEDE, fogModeOf } from '../drawFog.js';
3
+ import { FOG_OFF, FOG_RECEDE, fogModeOf } from '../drawFog.js';
4
+ import { blendCovering, blendKeeping } from '../../sceneCoverage.js';
4
5
  import { mat4 } from 'gl-matrix';
5
6
  import { GL_DEPTH_REMAP, MAX_DEPTH_LAYER, depthClearFor, depthOffsetForLayer, GL_SHADOW_REMAP, REVERSED_DEPTH, SHADOW_DEPTH_CLEAR, glDepthFuncEqual, glShadowDepthFunc, } from '../../depthConvention.js';
6
7
  import { resolveGlass } from '../../glass.js';
@@ -37,8 +38,19 @@ import { LightField } from '../../driftLight/lightField.js';
37
38
  import { WorldLightField } from '../../driftLight/worldLightField.js';
38
39
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
39
40
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
40
- import { MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT } from '../../lightBudget.js';
41
+ import { skinEight } from '../../shaders/skinning.js';
42
+ import { MODEL_MAP_TEXTURE_UNIT, MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT, } from '../../lightBudget.js';
43
+ import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
44
+ import { modelBound, skinHalfBound } from '../../shaders/flat/models.js';
45
+ import { GlSkinScatter } from './skinScatter.js';
46
+ import { CLOTH_BINDING_TEXTURE_UNIT, CLOTH_PARTICLES_TEXTURE_UNIT, CLOTH_REST_TEXTURE_UNIT, } from '../../lightBudget.js';
41
47
  import { SkinPaletteTexture } from './skinPaletteTexture.js';
48
+ import { clothBound } from '../../shaders/clothBinding.js';
49
+ import { validateClothBinding } from '../../clothBindingData.js';
50
+ import { GlClothBinding, GlClothParticles } from './clothTextures.js';
51
+ import { MAX_MORPH_TARGETS } from '../../skinPalette.js';
52
+ /** The weights a cloth-bound draw of a morphing mesh deforms by when none were set: none. */
53
+ const NO_MORPH_WEIGHTS = new Float32Array(MAX_MORPH_TARGETS);
42
54
  /** Shader-side encoding of `RenderQuality.outputTransform`. */
43
55
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
44
56
  import { LIGHT_VOLUME_VERT, lightVolumeFrag } from '../../shaders/lightVolume.js';
@@ -72,6 +84,7 @@ import { resolveRenderQuality } from '../../renderQuality.js';
72
84
  import { drawingBufferSize } from '../../drawingBuffer.js';
73
85
  import { Mesh, createMeshIncremental } from '../../mesh.js';
74
86
  import { SurfaceTexture } from '../../surfaceTexture.js';
87
+ import { glCompressedFormats } from '../../glCompressed.js';
75
88
  import { PlumeRenderer } from '../../plumeRenderer.js';
76
89
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.js';
77
90
  import { DEPTH_01_TO_CLIP, createResolvedLightVolume, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
@@ -93,6 +106,7 @@ import { deviceSnappedCellSize } from '../../textLayout.js';
93
106
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
94
107
  import { InstancedBatch } from './instanced.js';
95
108
  import { cutoutOf } from '../../cutoutCaster.js';
109
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
96
110
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
97
111
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
98
112
  import { noLitSwitches } from '../../shaders/flat/glassTint.js';
@@ -447,6 +461,17 @@ export class WebGL2Renderer {
447
461
  flatMorphedUniforms = null;
448
462
  flatBothProgram = null;
449
463
  flatBothUniforms = null;
464
+ /**
465
+ * The two skinned variants built with `SKIN_EIGHT` on, for a rig whose vertices name eight
466
+ * joints: compiled the first time such a mesh is created, and only then. See `skinning.ts`.
467
+ */
468
+ flatSkinned8Program = null;
469
+ flatSkinned8Uniforms = null;
470
+ flatBoth8Program = null;
471
+ flatBoth8Uniforms = null;
472
+ /** And its shadow caster, so the shadow parts from nothing the body does. */
473
+ depthSkinned8Program = null;
474
+ depthSkinned8Uniforms = null;
450
475
  /** The morph weights the following draws use, or null for none. */
451
476
  morphWeights = null;
452
477
  /**
@@ -480,6 +505,14 @@ export class WebGL2Renderer {
480
505
  * before this existed.
481
506
  */
482
507
  sceneTarget;
508
+ /**
509
+ * Skin's screen-space scattering, made where the profile asks for it and has a composite to add
510
+ * into, and null otherwise — which is every profile by default. See `GlSkinScatter`.
511
+ */
512
+ skinScatter;
513
+ /** Whether this frame has spread its skin: once, at its first blended draw or at its end. */
514
+ skinScattered = false;
515
+ skinInverseProjection = mat4.create();
483
516
  /** So the refusal below is once per renderer rather than once per frame. See `setColourGrade`. */
484
517
  warnedGradeWithoutComposite = false;
485
518
  /** Scratch for a volume's `uDepthToLocal`. Built per draw and never allocated in the loop. */
@@ -502,6 +535,21 @@ export class WebGL2Renderer {
502
535
  skinPalette = new SkinPaletteTexture();
503
536
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
504
537
  skinPaletteSet = false;
538
+ /**
539
+ * Cloth-bound skinned lit programs, keyed `eight|morphed`, compiled when a binding is made for a
540
+ * mesh of that kind; and their casters, keyed by `eight`. See `ensureClothPrograms`.
541
+ */
542
+ clothFlat = new Map();
543
+ clothDepth = new Map();
544
+ /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
545
+ cloth = null;
546
+ /**
547
+ * Each shading model's lit programs, keyed `variant|model` by the vertex variant they pair with,
548
+ * compiled the first time a draw asks for one. See `modelProgram`.
549
+ */
550
+ modelFlat = new Map();
551
+ /** The last palette chosen, which an eye's axis is turned by. See `setSkinPalette`. */
552
+ paletteData = null;
505
553
  emptyTexture2D;
506
554
  /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
507
555
  glassScratch = { transmission: 0, frost: 0, tint: [1, 1, 1] };
@@ -560,6 +608,11 @@ export class WebGL2Renderer {
560
608
  surfaceScene = new Float32Array(4);
561
609
  /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
562
610
  oitWeight = 0;
611
+ /**
612
+ * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
613
+ * each `bindMeshPass`. A translucent draw's own fog is measured against this and puts it back.
614
+ */
615
+ surfaceFog = FOG_RECEDE;
563
616
  ditherFade = 0;
564
617
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
565
618
  oitRefused = false;
@@ -968,6 +1021,22 @@ export class WebGL2Renderer {
968
1021
  materialHasMaps = false;
969
1022
  /** The material last set, so a lit program rebuilt for glass can be given it. */
970
1023
  currentMaterial = null;
1024
+ /**
1025
+ * The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64
1026
+ * so the noise's argument stays small. See `cutoutDither.ts`.
1027
+ */
1028
+ cutoutFrame = 0;
1029
+ /** Scratch for `uCutout`, so a material write allocates nothing. */
1030
+ cutoutVector = new Float32Array(4);
1031
+ /** The material's model numbers, `uModelParams`, packed by `packModel`. */
1032
+ modelParams = new Float32Array(MODEL_PARAM_FLOATS);
1033
+ /** The material's four switches, `uMaterialFlags`: albedo, ORM map, emissive map, two-sided. */
1034
+ materialFlags = new Int32Array(4);
1035
+ /**
1036
+ * The default framebuffer's sample count, read once, for a frame drawn with no scene target: a
1037
+ * dithered cutout there takes alpha-to-coverage when the canvas itself is multisampled.
1038
+ */
1039
+ canvasSamples;
971
1040
  /**
972
1041
  * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
973
1042
  * profile and the content have asked for since (`litWanted`).
@@ -1104,7 +1173,7 @@ export class WebGL2Renderer {
1104
1173
  gl.enable(gl.CULL_FACE);
1105
1174
  gl.useProgram(this.depthProgram);
1106
1175
  },
1107
- skinnedMesh: (mesh, model, palette, material) => {
1176
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
1108
1177
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1109
1178
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1110
1179
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -1114,8 +1183,20 @@ export class WebGL2Renderer {
1114
1183
  }
1115
1184
  this.shadowDrawBudget.ask();
1116
1185
  const { gl } = this;
1117
- const u = this.depthSkinnedUniforms;
1118
- gl.useProgram(this.depthSkinnedProgram);
1186
+ /* A rig of eight casts through the caster built with SKIN_EIGHT on, as it draws. */
1187
+ const eightCaster = mesh.isSkinnedEight &&
1188
+ this.depthSkinned8Program !== null &&
1189
+ this.depthSkinned8Uniforms !== null;
1190
+ /* A garment casts through the cloth-bound caster of its rig's kind, as it draws. */
1191
+ const clothCaster = cloth === undefined
1192
+ ? undefined
1193
+ : this.clothDepth.get(mesh.isSkinnedEight ? '8' : '4');
1194
+ const u = clothCaster?.uniforms ??
1195
+ (eightCaster
1196
+ ? this.depthSkinned8Uniforms
1197
+ : this.depthSkinnedUniforms);
1198
+ gl.useProgram(clothCaster?.program ??
1199
+ (eightCaster ? this.depthSkinned8Program : this.depthSkinnedProgram));
1119
1200
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1120
1201
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1121
1202
  /* The peel layer reads the first pass's depths; `beginShadowPass` bound that texture to
@@ -1126,6 +1207,12 @@ export class WebGL2Renderer {
1126
1207
  this.skinPalette.update(gl, palette);
1127
1208
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
1128
1209
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
1210
+ if (clothCaster !== undefined && cloth !== undefined) {
1211
+ this.bindCloth(u, {
1212
+ binding: cloth.binding,
1213
+ particles: cloth.particles,
1214
+ });
1215
+ }
1129
1216
  /* Narrowing is safe for the reason `mesh` above gives: this renderer only ever handed
1130
1217
  out its own `Mesh`. */
1131
1218
  mesh.draw(gl);
@@ -1169,7 +1256,8 @@ export class WebGL2Renderer {
1169
1256
  if (this.activeDepthPeel)
1170
1257
  gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1171
1258
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1172
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1259
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
1260
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
1173
1261
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1174
1262
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
1175
1263
  }
@@ -1964,11 +2052,20 @@ export class WebGL2Renderer {
1964
2052
  this.sceneTarget = this.quality.screenEffects
1965
2053
  ? new SceneTarget(gl, this.quality.sceneSamples, this.quality.hdrScene, this.quality.discardResolvedAttachments)
1966
2054
  : null;
2055
+ /* Screen-space skin needs the composite to add into; without one it is pre-integrated, said once. */
2056
+ if (this.quality.skinScattering === 'screen-space' && this.sceneTarget === null) {
2057
+ console.warn("WebGL2: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
2058
+ }
2059
+ this.skinScatter =
2060
+ this.quality.skinScattering === 'screen-space' && this.sceneTarget !== null
2061
+ ? new GlSkinScatter(gl)
2062
+ : null;
1967
2063
  /*
1968
2064
  * Said once at init, because the number a caller asked for and the number the part gave
1969
2065
  * them are not always the same, and a silently clamped sample count is a quality setting
1970
2066
  * that does not apply — the same failure mode `capabilityClamp` exists to avoid.
1971
2067
  */
2068
+ this.canvasSamples = Math.max(1, gl.getParameter(gl.SAMPLES) ?? 1);
1972
2069
  const got = this.sceneTarget?.sampleCount ?? 1;
1973
2070
  if (this.quality.sceneSamples > 1 && got !== this.quality.sceneSamples) {
1974
2071
  console.warn(`Renderer: ${this.quality.sceneSamples}x scene multisampling was requested and this ` +
@@ -2129,6 +2226,8 @@ export class WebGL2Renderer {
2129
2226
  return;
2130
2227
  this.disposed = true;
2131
2228
  const { gl } = this;
2229
+ if (!this.contextLost)
2230
+ this.skinScatter?.dispose();
2132
2231
  this.detachContextLoss();
2133
2232
  this.contextLostListeners.length = 0;
2134
2233
  this.contextRestoredListeners.length = 0;
@@ -2168,11 +2267,17 @@ export class WebGL2Renderer {
2168
2267
  this.flatSkinnedProgram,
2169
2268
  this.flatMorphedProgram,
2170
2269
  this.flatBothProgram,
2270
+ this.flatSkinned8Program,
2271
+ this.flatBoth8Program,
2272
+ this.depthSkinned8Program,
2171
2273
  this.flatInstancedProgram,
2172
2274
  ]) {
2173
2275
  if (extra !== null)
2174
2276
  gl.deleteProgram(extra);
2175
2277
  }
2278
+ for (const entry of [...this.clothFlat.values(), ...this.clothDepth.values()]) {
2279
+ gl.deleteProgram(entry.program);
2280
+ }
2176
2281
  for (const program of [
2177
2282
  this.flatProgram,
2178
2283
  this.panelProgram,
@@ -2269,13 +2374,19 @@ export class WebGL2Renderer {
2269
2374
  */
2270
2375
  warmVariants(data) {
2271
2376
  const skinnable = data.joints !== undefined;
2377
+ /* A rig of eight takes the variants built with SKIN_EIGHT on, and a caster of its own. */
2378
+ const eight = skinnable && data.joints2 !== undefined;
2272
2379
  const morphable = data.morphTargets !== undefined;
2273
2380
  for (const skinned of skinnable ? [false, true] : [false]) {
2274
2381
  for (const morphed of morphable ? [false, true] : [false]) {
2275
2382
  if (skinned || morphed)
2276
- this.ensureVariant(skinned, morphed);
2383
+ this.ensureVariant(skinned, morphed, skinned && eight);
2277
2384
  }
2278
2385
  }
2386
+ if (eight && this.depthSkinned8Program === null && !this.contextLost) {
2387
+ this.depthSkinned8Program = compileProgram(this.gl, skinEight(DEPTH_SKINNED_VERT), DEPTH_FRAG, 'depth-skinned8');
2388
+ this.depthSkinned8Uniforms = uniformLocations(this.gl, this.depthSkinned8Program, 'depth-skinned8');
2389
+ }
2279
2390
  }
2280
2391
  /**
2281
2392
  * Rewrite a mesh's positions, and its normals where the caller has them.
@@ -2317,24 +2428,39 @@ export class WebGL2Renderer {
2317
2428
  * `bindMeshPass` would otherwise carry every uniform at zero, and zero is a real value for most
2318
2429
  * of them — the failure this file records for `uGrain` and `uAmbientGround`.
2319
2430
  */
2320
- ensureVariant(skinned, morphed) {
2431
+ ensureVariant(skinned, morphed, eight = false) {
2321
2432
  if (this.contextLost)
2322
2433
  return;
2323
- if (skinned && morphed && this.flatBothProgram !== null)
2324
- return;
2325
- if (skinned && !morphed && this.flatSkinnedProgram !== null)
2326
- return;
2327
- if (!skinned && morphed && this.flatMorphedProgram !== null)
2328
- return;
2329
- if (!skinned && !morphed)
2330
- return;
2434
+ if (skinned && eight) {
2435
+ if (morphed ? this.flatBoth8Program !== null : this.flatSkinned8Program !== null)
2436
+ return;
2437
+ }
2438
+ else {
2439
+ if (skinned && morphed && this.flatBothProgram !== null)
2440
+ return;
2441
+ if (skinned && !morphed && this.flatSkinnedProgram !== null)
2442
+ return;
2443
+ if (!skinned && morphed && this.flatMorphedProgram !== null)
2444
+ return;
2445
+ if (!skinned && !morphed)
2446
+ return;
2447
+ }
2331
2448
  const { gl } = this;
2332
- const label = `flat.${morphed ? 'morphed' : ''}${skinned ? 'skinned' : ''}`;
2449
+ const label = `flat.${morphed ? 'morphed' : ''}${skinned ? (eight ? 'skinned8' : 'skinned') : ''}`;
2333
2450
  /* `flatFragSource`, not a fresh `flatFrag`: every variant shades through one fragment stage
2334
2451
  at one light budget, and the uploads are written for that budget. */
2335
- const program = compileProgram(gl, flatVert({ skinned, morphed, instanced: false }), this.flatFragSource, label);
2452
+ const vertex = flatVert({ skinned, morphed, instanced: false });
2453
+ const program = compileProgram(gl, skinned && eight ? skinEight(vertex) : vertex, this.flatFragSource, label);
2336
2454
  const uniforms = uniformLocations(gl, program, label);
2337
- if (skinned && morphed) {
2455
+ if (skinned && eight && morphed) {
2456
+ this.flatBoth8Program = program;
2457
+ this.flatBoth8Uniforms = uniforms;
2458
+ }
2459
+ else if (skinned && eight) {
2460
+ this.flatSkinned8Program = program;
2461
+ this.flatSkinned8Uniforms = uniforms;
2462
+ }
2463
+ else if (skinned && morphed) {
2338
2464
  this.flatBothProgram = program;
2339
2465
  this.flatBothUniforms = uniforms;
2340
2466
  }
@@ -2352,6 +2478,8 @@ export class WebGL2Renderer {
2352
2478
  if (camera !== null && env !== null) {
2353
2479
  gl.useProgram(program);
2354
2480
  this.writeMeshPassState(uniforms, camera, env);
2481
+ /* And the material the caller has set, which a program born mid-pass must carry too. */
2482
+ this.writeMaterialState(uniforms, this.currentMaterial);
2355
2483
  this.useFlatProgram();
2356
2484
  }
2357
2485
  }
@@ -2364,7 +2492,21 @@ export class WebGL2Renderer {
2364
2492
  * the consumer's decision. What it gains is a GPU object with sane sampler state.
2365
2493
  */
2366
2494
  createSurfaceTexture(source, options = {}) {
2367
- return new SurfaceTexture(this.gl, source, options);
2495
+ return new SurfaceTexture(this.gl, source, options, this.compressedFormats);
2496
+ }
2497
+ compressedCache = null;
2498
+ /**
2499
+ * The BC formats this device takes as blocks, each named with its colour space (`'bc7-srgb'`).
2500
+ *
2501
+ * **Ask before choosing**, because a phone usually answers none: a BC texture handed to
2502
+ * `createSurfaceTexture` where its format is not here is refused by name rather than decoded,
2503
+ * since core ships no decoder. `@driftengine/assets`' loader asks this for every BC texture in a
2504
+ * container and decodes at load where the answer is no. Read from the four extensions WebGL2
2505
+ * spreads BC across; see `glCompressed.ts`.
2506
+ */
2507
+ get compressedFormats() {
2508
+ this.compressedCache ??= glCompressedFormats(this.gl);
2509
+ return this.compressedCache;
2368
2510
  }
2369
2511
  /**
2370
2512
  * Upload several images of one size as the layers of one array, for a merged mesh that names a
@@ -2373,7 +2515,7 @@ export class WebGL2Renderer {
2373
2515
  * refusal names the first that is not.
2374
2516
  */
2375
2517
  createSurfaceTextureArray(sources, options = {}) {
2376
- return new SurfaceTexture(this.gl, sources, options);
2518
+ return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
2377
2519
  }
2378
2520
  /**
2379
2521
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
@@ -2431,6 +2573,148 @@ export class WebGL2Renderer {
2431
2573
  }
2432
2574
  this.skinPalette.update(this.gl, palette);
2433
2575
  this.skinPaletteSet = true;
2576
+ this.paletteData = palette;
2577
+ }
2578
+ /**
2579
+ * The lit program for one vertex variant shading by a model: the standard program's vertex
2580
+ * stage, and the fragment stage with the model's switch on (`modelBound`). Compiled the first
2581
+ * time a draw asks — a scene that names no model compiles none — and fed the pass and the
2582
+ * material at once, as `ensureClothPrograms` feeds its own.
2583
+ */
2584
+ modelProgram(variant, kind,
2585
+ /** Which half of a skin it draws under the screen-space blur. See `SkinHalf`. */
2586
+ half = 'whole') {
2587
+ const key = `${variant}|${kind}|${half}`;
2588
+ const held = this.modelFlat.get(key);
2589
+ if (held !== undefined)
2590
+ return held;
2591
+ const { gl } = this;
2592
+ const label = `flat.${variant}.${kind}.${half}`;
2593
+ const program = compileProgram(gl, litVertexOf(variant), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
2594
+ const made = { program, uniforms: uniformLocations(gl, program, label) };
2595
+ this.modelFlat.set(key, made);
2596
+ this.flatTargets.push(made);
2597
+ gl.useProgram(program);
2598
+ const camera = this.lastPassCamera;
2599
+ const env = this.lastPassEnv;
2600
+ if (camera !== null && env !== null)
2601
+ this.writeMeshPassState(made.uniforms, camera, env);
2602
+ this.writeMaterialState(made.uniforms, this.currentMaterial);
2603
+ return made;
2604
+ }
2605
+ /**
2606
+ * An eye's axis in the world for this draw, into its program's `uModelParams`: the material's
2607
+ * own numbers with the axis turned by the draw's model matrix and the eye's joint. See
2608
+ * `eyeAxisInWorld`.
2609
+ */
2610
+ writeEyeAxis(u, model) {
2611
+ const eye = this.currentMaterial?.model;
2612
+ if (eye?.kind !== 'eye')
2613
+ return;
2614
+ eyeAxisInWorld(eye, model, this.skinPaletteSet ? this.paletteData : null, this.modelParams, 4);
2615
+ this.gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
2616
+ }
2617
+ /**
2618
+ * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it,
2619
+ * and by how much. See `ClothBindingData`.
2620
+ *
2621
+ * Refused by name for a mesh with no rig — a binding blends a vertex between its skinning and the
2622
+ * cloth — and for one that disagrees with the mesh. Compiles the programs that draw and cast it
2623
+ * here, at creation, so no frame ever waits on a compile.
2624
+ */
2625
+ createClothBinding(mesh, data) {
2626
+ const target = mesh;
2627
+ if (!target.isSkinned) {
2628
+ throw new Error('createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
2629
+ 'skinning and the cloth');
2630
+ }
2631
+ validateClothBinding(data, target.vertexCount);
2632
+ this.ensureClothPrograms(target.isSkinnedEight, target.morph !== null);
2633
+ return new GlClothBinding(this.gl, data);
2634
+ }
2635
+ /** One character's particles, `count` of them, to be updated every frame. */
2636
+ createClothParticles(count) {
2637
+ return new GlClothParticles(this.gl, count);
2638
+ }
2639
+ /**
2640
+ * This frame's particles, three floats each, world space; last frame's are kept for motion. A
2641
+ * simulation's output, as it is.
2642
+ */
2643
+ updateClothParticles(particles, positions) {
2644
+ if (this.contextLost)
2645
+ return;
2646
+ particles.update(this.gl, positions);
2647
+ }
2648
+ /**
2649
+ * Place the following skinned draws by a cloth: `binding` the mesh's, `particles` the
2650
+ * character's. Null for none, which is every draw until this is called. Both or neither, and
2651
+ * the particles must be as many as the binding names.
2652
+ */
2653
+ setCloth(binding, particles = null) {
2654
+ if (binding === null || particles === null) {
2655
+ this.cloth = null;
2656
+ return;
2657
+ }
2658
+ if (binding.particles !== particles.count) {
2659
+ throw new Error(`setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`);
2660
+ }
2661
+ this.cloth = { binding, particles };
2662
+ }
2663
+ disposeClothBinding(binding) {
2664
+ if (this.contextLost)
2665
+ return;
2666
+ binding.dispose(this.gl);
2667
+ }
2668
+ disposeClothParticles(particles) {
2669
+ if (this.contextLost)
2670
+ return;
2671
+ particles.dispose(this.gl);
2672
+ }
2673
+ /**
2674
+ * The cloth-bound lit program and caster for a rig of this kind, once. Fed at once if a pass is
2675
+ * bound, for the reason `ensureVariant` gives: a program compiled after `bindMeshPass` would
2676
+ * otherwise carry every uniform at zero.
2677
+ */
2678
+ ensureClothPrograms(eight, morphed) {
2679
+ if (this.contextLost)
2680
+ return;
2681
+ const { gl } = this;
2682
+ const key = `${eight ? 8 : 4}|${morphed ? 'm' : '-'}`;
2683
+ if (!this.clothFlat.has(key)) {
2684
+ const label = `flat.cloth${eight ? '8' : ''}${morphed ? '.morphed' : ''}`;
2685
+ const vertex = flatVert({ skinned: true, morphed, instanced: false });
2686
+ const program = compileProgram(gl, clothBound(eight ? skinEight(vertex) : vertex), this.flatFragSource, label);
2687
+ const uniforms = uniformLocations(gl, program, label);
2688
+ this.clothFlat.set(key, { program, uniforms });
2689
+ this.flatTargets.push({ program, uniforms });
2690
+ const camera = this.lastPassCamera;
2691
+ const env = this.lastPassEnv;
2692
+ if (camera !== null && env !== null) {
2693
+ gl.useProgram(program);
2694
+ this.writeMeshPassState(uniforms, camera, env);
2695
+ this.writeMaterialState(uniforms, this.currentMaterial);
2696
+ this.useFlatProgram();
2697
+ }
2698
+ }
2699
+ const depthKey = eight ? '8' : '4';
2700
+ if (!this.clothDepth.has(depthKey)) {
2701
+ const label = `depth-cloth${eight ? '8' : ''}`;
2702
+ const program = compileProgram(gl, clothBound(eight ? skinEight(DEPTH_SKINNED_VERT) : DEPTH_SKINNED_VERT), DEPTH_FRAG, label);
2703
+ this.clothDepth.set(depthKey, { program, uniforms: uniformLocations(gl, program, label) });
2704
+ }
2705
+ }
2706
+ /** Bind the cloth's three textures to their units and name them to `u`. */
2707
+ bindCloth(u, cloth) {
2708
+ const { gl } = this;
2709
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_BINDING_TEXTURE_UNIT);
2710
+ gl.bindTexture(gl.TEXTURE_2D, cloth.binding.binding);
2711
+ gl.uniform1i(u['uClothBinding'] ?? null, CLOTH_BINDING_TEXTURE_UNIT);
2712
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_PARTICLES_TEXTURE_UNIT);
2713
+ gl.bindTexture(gl.TEXTURE_2D, cloth.particles.current);
2714
+ gl.uniform1i(u['uClothParticles'] ?? null, CLOTH_PARTICLES_TEXTURE_UNIT);
2715
+ gl.activeTexture(gl.TEXTURE0 + CLOTH_REST_TEXTURE_UNIT);
2716
+ gl.bindTexture(gl.TEXTURE_2D, cloth.binding.rest);
2717
+ gl.uniform1i(u['uClothRest'] ?? null, CLOTH_REST_TEXTURE_UNIT);
2434
2718
  }
2435
2719
  /**
2436
2720
  * Choose the morph weights the following `drawMesh` calls deform by, or null for none.
@@ -2452,6 +2736,11 @@ export class WebGL2Renderer {
2452
2736
  this.materialDoubleSided = material?.doubleSided === true;
2453
2737
  this.materialHasMaps = materialHasMaps(material);
2454
2738
  this.currentMaterial = material;
2739
+ /* What the blur spreads this skin by: the last material to name a profile says. */
2740
+ const model = material?.model;
2741
+ if (model?.kind === 'skin' && this.skinScatter !== null) {
2742
+ this.skinScatter.setProfile(model.profile, model.scatterColor[0] * model.radius, model.scatterColor[1] * model.radius, model.scatterColor[2] * model.radius);
2743
+ }
2455
2744
  /*
2456
2745
  * Written to every flat program, because material state persists across draws and a skinned
2457
2746
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -2468,6 +2757,25 @@ export class WebGL2Renderer {
2468
2757
  }
2469
2758
  this.useFlatProgram();
2470
2759
  }
2760
+ /**
2761
+ * How a material's cutout edge is drawn this frame: the material's mode against what the frame can
2762
+ * resolve. A translucent draw tests hard whatever the material says, because a blended draw has
2763
+ * nothing to average a pattern with. See `cutoutDither.ts`.
2764
+ */
2765
+ cutoutResolveNow(material, translucent) {
2766
+ if (translucent || material === null)
2767
+ return 'hard';
2768
+ return resolveCutout(material.cutoutMode ?? 'hard', this.temporalJittering, this.sceneTarget?.sampleCount ?? this.canvasSamples);
2769
+ }
2770
+ /** The lit pass's `uCutout` for a material this frame, into scratch. See `uCutout`. */
2771
+ cutoutUniform(material, translucent) {
2772
+ const v = this.cutoutVector;
2773
+ v[0] = material?.cutout ?? 0;
2774
+ v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
2775
+ v[2] = this.cutoutFrame;
2776
+ v[3] = 0;
2777
+ return v;
2778
+ }
2471
2779
  /**
2472
2780
  * One flat program's view of the current material.
2473
2781
  *
@@ -2512,7 +2820,7 @@ export class WebGL2Renderer {
2512
2820
  if (orm !== null)
2513
2821
  orm.bind(gl, ORM_TEXTURE_UNIT);
2514
2822
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
2515
- gl.uniform1i(u['uOrmEnabled'] ?? null, orm === null ? 0 : 1);
2823
+ this.materialFlags[1] = orm === null ? 0 : 1;
2516
2824
  gl.uniform3f(u['uOrmScale'] ?? null, material?.occlusionStrength ?? 1, material?.roughnessScale ?? 1, material?.metallicScale ?? 1);
2517
2825
  /*
2518
2826
  * The emissive map, on exactly the terms the two maps above it are bound: before the albedo
@@ -2520,7 +2828,7 @@ export class WebGL2Renderer {
2520
2828
  *
2521
2829
  * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
2522
2830
  * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
2523
- * unbound material keeps the arithmetic it had before only because `uEmissiveMapEnabled` is 0
2831
+ * unbound material keeps the arithmetic it had before only because its flag in `uMaterialFlags` is 0
2524
2832
  * and the read never happens. What would make that wrong is a read taken outside that branch:
2525
2833
  * every glow on a material without a map would go out.
2526
2834
  */
@@ -2530,7 +2838,7 @@ export class WebGL2Renderer {
2530
2838
  if (emissiveMap !== null)
2531
2839
  emissiveMap.bind(gl, EMISSIVE_TEXTURE_UNIT);
2532
2840
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
2533
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, emissiveMap === null ? 0 : 1);
2841
+ this.materialFlags[2] = emissiveMap === null ? 0 : 1;
2534
2842
  const emissiveScale = material?.emissiveScale ?? null;
2535
2843
  gl.uniform3f(u['uEmissiveScale'] ?? null, emissiveScale?.[0] ?? 1, emissiveScale?.[1] ?? 1, emissiveScale?.[2] ?? 1);
2536
2844
  /*
@@ -2541,7 +2849,21 @@ export class WebGL2Renderer {
2541
2849
  * how big a dome was — WebGPU writes it before its own early return and WebGL2 did not.
2542
2850
  */
2543
2851
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
2544
- gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 0);
2852
+ gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 0);
2853
+ this.materialFlags[3] = material?.doubleSided === true ? 1 : 0;
2854
+ /*
2855
+ * The material's shading model: its numbers, and its map or the stand-in, before the albedo
2856
+ * early return as every map above is. Written to the standard programs too, which declare both
2857
+ * and read neither — one decision for every program, as `setMaterial` says.
2858
+ */
2859
+ const modelMap = material?.modelMap ?? null;
2860
+ gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
2861
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2862
+ if (modelMap !== null)
2863
+ modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
2864
+ gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
2865
+ packModel(material?.model ?? null, modelMap !== null, this.modelParams);
2866
+ gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
2545
2867
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
2546
2868
  return, so a material with no albedo still leaves a complete texture on the unit. */
2547
2869
  if (albedo === null) {
@@ -2560,12 +2882,14 @@ export class WebGL2Renderer {
2560
2882
  array target, and with nothing there it is incomplete. */
2561
2883
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
2562
2884
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2563
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
2885
+ this.materialFlags[0] = 0;
2886
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
2564
2887
  return;
2565
2888
  }
2566
2889
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
2567
- gl.uniform1f(u['uAlbedoCutout'] ?? null, material?.cutout ?? 0);
2568
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 1);
2890
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(material, false));
2891
+ this.materialFlags[0] = 1;
2892
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
2569
2893
  }
2570
2894
  /**
2571
2895
  * @deprecated Use `setMaterial`.
@@ -2582,7 +2906,7 @@ export class WebGL2Renderer {
2582
2906
  * Rebind `SURFACE_TEXTURE_UNIT` to whatever `setSurfaceTexture` last put there.
2583
2907
  *
2584
2908
  * Only `drawSdfText` calls this, after the atlas has finished borrowing the same unit.
2585
- * No uniform is written here — the flat program's `uAlbedo`/`uAlbedoEnabled` were never
2909
+ * No uniform is written here — the flat program's `uAlbedo` and its flag in `uMaterialFlags` were never
2586
2910
  * touched by the text draw and are still correct, since that draw runs its own separate
2587
2911
  * program. What moved is unit 15's actual `WebGLTexture` binding, which is GL state, not
2588
2912
  * program state, and this puts it back to what `currentSurfaceTexture` says it should be.
@@ -2669,6 +2993,28 @@ export class WebGL2Renderer {
2669
2993
  this.materials.dirty();
2670
2994
  this.writeFlatFloat('uGrain', Math.min(1, Math.max(0, amount)));
2671
2995
  }
2996
+ /**
2997
+ * Whether the surfaces drawn next meet the medium: distance fog and the global medium's haze.
2998
+ * True is the default, and what `bindMeshPass` restores.
2999
+ *
3000
+ * **The opaque counterpart of `TranslucentMeshOptions.fog`**, which a translucent draw has had
3001
+ * all along; the shader's per-draw switch was there, and nothing reached it for an ordinary mesh.
3002
+ * What it is for is a medium that darkens one thing and not another — a floor receding into the
3003
+ * distance under figures that must keep their full value however far from the camera they stand.
3004
+ * Material state like `setSurfaceGrain`: instanced and skinned draws read it too, and a
3005
+ * translucent draw's own `fog` is measured against it and puts it back.
3006
+ *
3007
+ * **What it gives up** is the medium's honesty: a surface out of the fog is drawn as though the
3008
+ * air between it and the eye were empty, so it should be a surface the scene means to stand
3009
+ * apart, not one whose distance is the point.
3010
+ */
3011
+ setSurfaceFog(enabled) {
3012
+ if (this.contextLost)
3013
+ return;
3014
+ this.materials.dirty();
3015
+ this.surfaceFog = enabled ? FOG_RECEDE : FOG_OFF;
3016
+ this.writeFlatInt('uFogEnabled', this.surfaceFog);
3017
+ }
2672
3018
  /**
2673
3019
  * One float of material state, into every flat program.
2674
3020
  *
@@ -2685,6 +3031,14 @@ export class WebGL2Renderer {
2685
3031
  }
2686
3032
  this.useFlatProgram();
2687
3033
  }
3034
+ /** One int of material state, into every flat program, for `writeFlatFloat`'s reason. */
3035
+ writeFlatInt(name, value) {
3036
+ for (const target of this.flatTargets) {
3037
+ this.gl.useProgram(target.program);
3038
+ this.gl.uniform1i(target.uniforms[name] ?? null, value);
3039
+ }
3040
+ this.useFlatProgram();
3041
+ }
2688
3042
  /**
2689
3043
  * How much of the surfaces drawn next survives a screen-door dither, for crossfading two levels
2690
3044
  * of detail of one thing.
@@ -2829,6 +3183,8 @@ export class WebGL2Renderer {
2829
3183
  if (camera !== null && env !== null) {
2830
3184
  gl.useProgram(program);
2831
3185
  this.writeMeshPassState(uniforms, camera, env);
3186
+ /* And the material the caller has set, which a program born mid-pass must carry too. */
3187
+ this.writeMaterialState(uniforms, this.currentMaterial);
2832
3188
  this.useFlatProgram();
2833
3189
  }
2834
3190
  }
@@ -2860,6 +3216,7 @@ export class WebGL2Renderer {
2860
3216
  }
2861
3217
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
2862
3218
  drawTranslucentInstanced(batch, data, opacity, options = {}) {
3219
+ this.scatterSkin();
2863
3220
  this.submitInstanced(batch, data, opacity, true, options);
2864
3221
  }
2865
3222
  /** Release the placement. The mesh is the caller's and is not released. */
@@ -2899,8 +3256,11 @@ export class WebGL2Renderer {
2899
3256
  return;
2900
3257
  this.drawBudget.ask();
2901
3258
  this.ensureInstancedProgram();
2902
- const program = this.flatInstancedProgram;
2903
- const u = this.flatInstancedUniforms;
3259
+ /* A material with a model takes that model's instanced program; see `modelProgram`. */
3260
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
3261
+ const modelled = modelKind === null ? null : this.modelProgram('instanced', modelKind);
3262
+ const program = modelled?.program ?? this.flatInstancedProgram;
3263
+ const u = modelled?.uniforms ?? this.flatInstancedUniforms;
2904
3264
  if (program === null || u === null)
2905
3265
  return;
2906
3266
  const { gl } = this;
@@ -2913,7 +3273,7 @@ export class WebGL2Renderer {
2913
3273
  /* A blended batch shows what is behind it — refraction or glass — as a single draw does, and
2914
3274
  counts a material of its own for it by the rule both backends share (`materialChanges.ts`). */
2915
3275
  const refracting = blend && this.bindSeeThrough(u, options);
2916
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
3276
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
2917
3277
  if (own)
2918
3278
  this.materials.dirty();
2919
3279
  this.takeMaterial();
@@ -2939,16 +3299,16 @@ export class WebGL2Renderer {
2939
3299
  */
2940
3300
  if (!lit)
2941
3301
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
2942
- if (fog !== FOG_RECEDE)
3302
+ if (fog !== this.surfaceFog)
2943
3303
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
2944
3304
  if (!toneMapped)
2945
3305
  gl.uniform1i(u['uOutputTransform'] ?? null, Math.min(this.gradeCode(), 1));
2946
3306
  if (blend) {
2947
3307
  gl.enable(gl.BLEND);
2948
3308
  if (options.additive === true)
2949
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
3309
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
2950
3310
  else
2951
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
3311
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2952
3312
  if (!depthWrite)
2953
3313
  gl.depthMask(false);
2954
3314
  }
@@ -2963,10 +3323,23 @@ export class WebGL2Renderer {
2963
3323
  for both: without it a batch's far faces were culled here alone. */
2964
3324
  if (this.materialDoubleSided)
2965
3325
  gl.disable(gl.CULL_FACE);
3326
+ /* A dithered cutout: coverage when this batch is opaque in a multisampled frame, the hard test
3327
+ when it is blended. See `cutoutDither.ts`. */
3328
+ const cutoutResolve = this.cutoutResolveNow(this.currentMaterial, false);
3329
+ const coverage = !blend && cutoutResolve === 'coverage';
3330
+ const ditheredBlend = blend && cutoutResolve !== 'hard';
3331
+ if (coverage)
3332
+ gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
3333
+ if (ditheredBlend)
3334
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
2966
3335
  if (culling && cullsInstances(count, mesh.indexCount))
2967
3336
  batch.drawCulled(gl, data, this.frustum);
2968
3337
  else
2969
3338
  batch.draw(gl, count);
3339
+ if (coverage)
3340
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
3341
+ if (ditheredBlend)
3342
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
2970
3343
  if (this.materialDoubleSided)
2971
3344
  gl.enable(gl.CULL_FACE);
2972
3345
  if (layer > 0) {
@@ -2988,8 +3361,8 @@ export class WebGL2Renderer {
2988
3361
  this.unbindSeeThrough(u);
2989
3362
  if (!lit)
2990
3363
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
2991
- if (fog !== FOG_RECEDE)
2992
- gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
3364
+ if (fog !== this.surfaceFog)
3365
+ gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
2993
3366
  if (!toneMapped)
2994
3367
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
2995
3368
  if (own)
@@ -3126,6 +3499,7 @@ export class WebGL2Renderer {
3126
3499
  }
3127
3500
  /** Draw after the opaque scene: streaks are blended and write no depth. */
3128
3501
  drawWindStreaks(streaks, camera, wind, timeSeconds, tint, env) {
3502
+ this.scatterSkin();
3129
3503
  // The engine decides, not the caller: every pass already knows where the
3130
3504
  // waterline is, and leaving it to each call site is how one of them ends up
3131
3505
  // raining dust into the sea.
@@ -3158,6 +3532,7 @@ export class WebGL2Renderer {
3158
3532
  * whatever box the caller lays out in, and the game's own card is not always the canvas.
3159
3533
  */
3160
3534
  drawText(text, viewportWidth, viewportHeight, originX, originY, style, timeSec) {
3535
+ this.scatterSkin();
3161
3536
  if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
3162
3537
  this.textBudget.ask();
3163
3538
  }
@@ -3249,6 +3624,7 @@ export class WebGL2Renderer {
3249
3624
  return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
3250
3625
  }
3251
3626
  drawSdfText(handle, model, color, opacity) {
3627
+ this.scatterSkin();
3252
3628
  if (this.contextLost)
3253
3629
  return;
3254
3630
  const viewProj = this.frameViewProj;
@@ -3320,6 +3696,7 @@ export class WebGL2Renderer {
3320
3696
  * whole milestone from a buffer nothing had ever written.
3321
3697
  */
3322
3698
  drawParticles(batch, data, camera, env, timeSeconds) {
3699
+ this.scatterSkin();
3323
3700
  if (data.count === 0)
3324
3701
  return;
3325
3702
  const { gl } = this;
@@ -3365,6 +3742,7 @@ export class WebGL2Renderer {
3365
3742
  * it strobes as the rasteriser catches it on some frames and not others.
3366
3743
  */
3367
3744
  drawBolts(batch, data, camera, env, timeSeconds, core, edge, widthM, coreGain, minWidthPerMetre = 0.004) {
3745
+ this.scatterSkin();
3368
3746
  if (data.count === 0)
3369
3747
  return;
3370
3748
  const { gl } = this;
@@ -3421,6 +3799,7 @@ export class WebGL2Renderer {
3421
3799
  * edge inward, as a fraction of the half-width; zero is a clean edge.
3422
3800
  */
3423
3801
  drawLines(lines, data, model, camera, env, color, widthM, opacity, softness = 0, minWidthPerMetre = 0, additive = false) {
3802
+ this.scatterSkin();
3424
3803
  if (data.count === 0)
3425
3804
  return;
3426
3805
  const { gl } = this;
@@ -3604,6 +3983,7 @@ export class WebGL2Renderer {
3604
3983
  }
3605
3984
  // --- Draws that own their context ----------------------------------------
3606
3985
  drawPlumes(plumes, camera, timeSeconds, env, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0) {
3986
+ this.scatterSkin();
3607
3987
  plumes.draw(this.gl, this.frameViewFor(camera), timeSeconds, env, this.quality.underwaterAtmosphere, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.reflectionPassActive ? (this.planarReflection?.clipPlane ?? null) : null, windX, windZ, originX, originY, originZ);
3608
3988
  }
3609
3989
  drawWater(water, camera, timeSeconds, settings, env, windX = 0, windZ = 0) {
@@ -3617,6 +3997,7 @@ export class WebGL2Renderer {
3617
3997
  * from the water under them.
3618
3998
  */
3619
3999
  drawCaustics(caustics, camera, timeSeconds, env, windX = 0, windZ = 0, strength = 1) {
4000
+ this.scatterSkin();
3620
4001
  if (caustics === null || !this.quality.water)
3621
4002
  return;
3622
4003
  const drew = caustics.draw(this.gl, this.frameViewFor(camera), timeSeconds, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, windX, windZ, strength);
@@ -3912,6 +4293,14 @@ export class WebGL2Renderer {
3912
4293
  this.uploadWind(this.depthUniforms, this.frameWind);
3913
4294
  gl.useProgram(this.depthSkinnedProgram);
3914
4295
  this.uploadWind(this.depthSkinnedUniforms, this.frameWind);
4296
+ if (this.depthSkinned8Program !== null && this.depthSkinned8Uniforms !== null) {
4297
+ gl.useProgram(this.depthSkinned8Program);
4298
+ this.uploadWind(this.depthSkinned8Uniforms, this.frameWind);
4299
+ }
4300
+ for (const caster of this.clothDepth.values()) {
4301
+ gl.useProgram(caster.program);
4302
+ this.uploadWind(caster.uniforms, this.frameWind);
4303
+ }
3915
4304
  gl.useProgram(this.depthCutoutProgram);
3916
4305
  this.uploadWind(this.depthCutoutUniforms, this.frameWind);
3917
4306
  gl.useProgram(this.depthProgram);
@@ -3937,14 +4326,18 @@ export class WebGL2Renderer {
3937
4326
  this.bindSceneCasterMaterial(material);
3938
4327
  this.drawMesh(mesh, model);
3939
4328
  },
3940
- skinnedMesh: (mesh, model, palette, material) => {
4329
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
3941
4330
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
3942
4331
  gives about the mesh: nothing else ever handed one out. */
3943
4332
  this.bindSceneCasterMaterial(material);
3944
4333
  this.setSkinPalette(palette);
4334
+ if (cloth !== undefined) {
4335
+ this.setCloth(cloth.binding, cloth.particles);
4336
+ }
3945
4337
  this.drawMesh(mesh, model);
3946
4338
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
3947
4339
  this.setSkinPalette(null);
4340
+ this.setCloth(null);
3948
4341
  },
3949
4342
  instanced: (batch, data, material) => {
3950
4343
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
@@ -4068,7 +4461,8 @@ export class WebGL2Renderer {
4068
4461
  gl.uniform4fv(u['uGlassLight'] ?? null, this.glassLight);
4069
4462
  if (cutout !== null) {
4070
4463
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
4071
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
4464
+ gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
4465
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
4072
4466
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
4073
4467
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
4074
4468
  }
@@ -4206,10 +4600,11 @@ export class WebGL2Renderer {
4206
4600
  const { gl } = this;
4207
4601
  Object.assign(this.litOn, this.litWanted);
4208
4602
  this.flatFragSource = this.flatFragAt(this.lightBudget);
4209
- const recompile = (old, skinned, morphed, instanced, label) => {
4603
+ const recompile = (old, skinned, morphed, instanced, label, eight = false) => {
4210
4604
  if (old === null)
4211
4605
  return null;
4212
- const program = compileProgram(gl, flatVert({ skinned, morphed, instanced }), this.flatFragSource, label);
4606
+ const vertex = flatVert({ skinned, morphed, instanced });
4607
+ const program = compileProgram(gl, eight ? skinEight(vertex) : vertex, this.flatFragSource, label);
4213
4608
  gl.deleteProgram(old);
4214
4609
  return { program, uniforms: uniformLocations(gl, program, label) };
4215
4610
  };
@@ -4227,14 +4622,40 @@ export class WebGL2Renderer {
4227
4622
  const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
4228
4623
  this.flatBothProgram = both?.program ?? null;
4229
4624
  this.flatBothUniforms = both?.uniforms ?? null;
4625
+ const skinned8 = recompile(this.flatSkinned8Program, true, false, false, 'flat.skinned8', true);
4626
+ this.flatSkinned8Program = skinned8?.program ?? null;
4627
+ this.flatSkinned8Uniforms = skinned8?.uniforms ?? null;
4628
+ const both8 = recompile(this.flatBoth8Program, true, true, false, 'flat.morphedskinned8', true);
4629
+ this.flatBoth8Program = both8?.program ?? null;
4630
+ this.flatBoth8Uniforms = both8?.uniforms ?? null;
4230
4631
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
4231
4632
  this.flatInstancedProgram = instanced?.program ?? null;
4232
4633
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
4634
+ for (const [key, entry] of this.clothFlat) {
4635
+ const eight = key.startsWith('8');
4636
+ const withMorph = key.endsWith('m');
4637
+ const label = `flat.cloth${eight ? '8' : ''}${withMorph ? '.morphed' : ''}`;
4638
+ const vertex = flatVert({ skinned: true, morphed: withMorph, instanced: false });
4639
+ const program = compileProgram(gl, clothBound(eight ? skinEight(vertex) : vertex), this.flatFragSource, label);
4640
+ gl.deleteProgram(entry.program);
4641
+ this.clothFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
4642
+ }
4643
+ for (const [key, entry] of this.modelFlat) {
4644
+ const [variant, kind, half] = key.split('|');
4645
+ const label = `flat.${variant}.${kind}.${half}`;
4646
+ const program = compileProgram(gl, litVertexOf(variant), skinHalfBound(modelBound(this.flatFragSource, kind), half), label);
4647
+ gl.deleteProgram(entry.program);
4648
+ this.modelFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
4649
+ }
4233
4650
  this.flatTargets.length = 0;
4234
- for (const target of [plain, skinned, morphed, both, instanced]) {
4651
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
4235
4652
  if (target !== null)
4236
4653
  this.flatTargets.push(target);
4237
4654
  }
4655
+ for (const target of this.clothFlat.values())
4656
+ this.flatTargets.push(target);
4657
+ for (const target of this.modelFlat.values())
4658
+ this.flatTargets.push(target);
4238
4659
  const camera = this.lastPassCamera;
4239
4660
  const env = this.lastPassEnv;
4240
4661
  for (const target of this.flatTargets) {
@@ -4757,7 +5178,7 @@ export class WebGL2Renderer {
4757
5178
  */
4758
5179
  gl.disable(gl.CULL_FACE);
4759
5180
  gl.enable(gl.BLEND);
4760
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5181
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
4761
5182
  gl.uniform4f(this.panelUniforms['uRect'] ?? null, rect.left, rect.top, rect.width, rect.height);
4762
5183
  gl.uniform2f(this.panelUniforms['uViewport'] ?? null, Math.max(this.canvas.clientWidth, 1), Math.max(this.canvas.clientHeight, 1));
4763
5184
  /* Identity: this backend's clip space is the one the shader was written against. */
@@ -5295,12 +5716,15 @@ export class WebGL2Renderer {
5295
5716
  beginFrame(clearColor) {
5296
5717
  if (this.contextLost)
5297
5718
  return;
5719
+ this.skinScattered = false;
5720
+ this.skinScatter?.reset();
5298
5721
  if (this.litAsked) {
5299
5722
  this.litAsked = false;
5300
5723
  this.rebuildLit();
5301
5724
  }
5302
5725
  this.budget.reset();
5303
5726
  this.materials.dirty();
5727
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
5304
5728
  /*
5305
5729
  * Said once, for the reason `refuseOitMultisampled` gives: a quality setting that is enabled
5306
5730
  * and does nothing is the fault the capability clamp exists to prevent.
@@ -5375,6 +5799,8 @@ export class WebGL2Renderer {
5375
5799
  /* A view model left open would squeeze every pass after the world, the composite included. */
5376
5800
  if (this.viewModelActive)
5377
5801
  this.endViewModel();
5802
+ /* Skin not yet spread, because nothing blended came after it, is spread now. */
5803
+ this.scatterSkin();
5378
5804
  /*
5379
5805
  * Camera motion blur, built from the camera the frame was *rendered* with.
5380
5806
  *
@@ -5861,6 +6287,16 @@ export class WebGL2Renderer {
5861
6287
  * is a non-idempotent step entering this body; there is none today, and one added would show
5862
6288
  * as a skinned game behaving differently from an unskinned one.
5863
6289
  */
6290
+ /*
6291
+ * **A pass starts with no material**, so it cannot inherit one from the pass before it. Here
6292
+ * rather than in `writeMeshPassState`, which also feeds a program compiled mid-pass — a model's,
6293
+ * a cloth's, the instanced one — and there forgot the material the caller had set, so the next
6294
+ * draws drew without it until somebody called `setMaterial` again.
6295
+ */
6296
+ this.materialDoubleSided = false;
6297
+ this.materialHasMaps = false;
6298
+ this.currentMaterial = null;
6299
+ this.currentSurfaceTexture = null;
5864
6300
  for (const target of this.flatTargets) {
5865
6301
  this.gl.useProgram(target.program);
5866
6302
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -6048,7 +6484,8 @@ export class WebGL2Renderer {
6048
6484
  * shape `uOpacity` and `uTint` already use.
6049
6485
  */
6050
6486
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
6051
- gl.uniform1i(u['uFogEnabled'] ?? null, 1);
6487
+ gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
6488
+ this.surfaceFog = FOG_RECEDE;
6052
6489
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
6053
6490
  gl.uniform1f(u['uGrain'] ?? null, 1);
6054
6491
  /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
@@ -6150,7 +6587,7 @@ export class WebGL2Renderer {
6150
6587
  * the sort of per-draw cost this renderer avoids everywhere else.
6151
6588
  */
6152
6589
  /*
6153
- * And the surface unit, which starts every pass with no texture on it: `uAlbedoEnabled`
6590
+ * And the surface unit, which starts every pass with no texture on it: its flag in `uMaterialFlags`
6154
6591
  * is 0 until a caller asks for one, and a world that never asks would otherwise leave
6155
6592
  * this sampler incomplete for the whole session.
6156
6593
  */
@@ -6227,18 +6664,15 @@ export class WebGL2Renderer {
6227
6664
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
6228
6665
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6229
6666
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
6230
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
6667
+ /* Every material switch off — the albedo, both maps, two-sided — in their one row. */
6668
+ gl.uniform4i(u['uMaterialFlags'] ?? null, 0, 0, 0, 0);
6231
6669
  /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
6232
6670
  gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
6233
6671
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6234
6672
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
6235
6673
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
6236
- gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
6237
- gl.uniform1i(u['uDoubleSided'] ?? null, 0);
6238
- this.materialDoubleSided = false;
6239
- this.materialHasMaps = false;
6240
- this.currentMaterial = null;
6241
- this.currentSurfaceTexture = null;
6674
+ gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
6675
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, 0);
6242
6676
  /*
6243
6677
  * **And the normal map, which this did not reset and had to.**
6244
6678
  *
@@ -6264,14 +6698,17 @@ export class WebGL2Renderer {
6264
6698
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
6265
6699
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6266
6700
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
6267
- gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
6268
6701
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
6269
6702
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
6270
6703
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
6271
6704
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6272
6705
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
6273
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
6274
6706
  gl.uniform3f(u['uEmissiveScale'] ?? null, 1, 1, 1);
6707
+ /* And the shading model: its map back to the stand-in and its numbers to the standard's. */
6708
+ gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
6709
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6710
+ gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
6711
+ gl.uniform4fv(u['uModelParams'] ?? null, NO_MODEL_PARAMS);
6275
6712
  /*
6276
6713
  * White, because a uniform nobody has written is **zero**, and this one multiplies
6277
6714
  * vertex colour.
@@ -6380,6 +6817,58 @@ export class WebGL2Renderer {
6380
6817
  return;
6381
6818
  this.drawBudget.ask();
6382
6819
  this.takeMaterial();
6820
+ /* Skin under the screen-space blur draws its two halves: the frame's, then at once its diffuse. */
6821
+ const splits = this.skinSplits();
6822
+ this.drawMeshHalf(mesh, model, depthLayer, tint, splits ? 'scene' : 'whole');
6823
+ if (splits && this.skinScatter !== null) {
6824
+ this.skinScatter.begin();
6825
+ this.drawMeshHalf(mesh, model, depthLayer, tint, 'diffuse');
6826
+ this.sceneTarget?.bind();
6827
+ }
6828
+ }
6829
+ /**
6830
+ * Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
6831
+ * skin yet, this is the frame's own picture — not a mirror's or a probe's, nor an overlay after
6832
+ * the frame — and the blur's target can be had.
6833
+ */
6834
+ skinSplits() {
6835
+ const skin = this.skinScatter;
6836
+ const scene = this.sceneTarget;
6837
+ return (skin !== null &&
6838
+ scene !== null &&
6839
+ this.currentMaterial?.model?.kind === 'skin' &&
6840
+ !this.skinScattered &&
6841
+ !this.reflectionPassActive &&
6842
+ !this.probePassActive &&
6843
+ !this.framePresented &&
6844
+ skin.ready(scene));
6845
+ }
6846
+ /**
6847
+ * Spread the frame's skin and add it back: once, at the first blended draw or at the frame's end.
6848
+ * Called first in every draw WebGPU records as blended — translucent meshes and batches, text,
6849
+ * particles, plumes, wind streaks, bolts, lines, caustics, film — so the two backends add skin at
6850
+ * the same point. The frame's depth is copied for the blur to read, and where it cannot be the
6851
+ * diffuse goes back unspread rather than not at all.
6852
+ */
6853
+ scatterSkin() {
6854
+ const skin = this.skinScatter;
6855
+ const scene = this.sceneTarget;
6856
+ const projection = this.frameProjection;
6857
+ if (skin === null || scene === null || this.skinScattered)
6858
+ return;
6859
+ /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
6860
+ this.skinScattered = true;
6861
+ if (!skin.pending)
6862
+ return;
6863
+ if (this.reflectionPassActive || this.probePassActive || projection === null)
6864
+ return;
6865
+ const depth = scene.snapshotDepth(true);
6866
+ mat4.invert(this.skinInverseProjection, projection);
6867
+ skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);
6868
+ this.useFlatProgram();
6869
+ }
6870
+ /** One draw of a mesh, as `drawMesh` describes, of the half of a skin `half` names. */
6871
+ drawMeshHalf(mesh, model, depthLayer, tint, half) {
6383
6872
  const { gl } = this;
6384
6873
  /*
6385
6874
  * A rigged mesh takes the skinned program, an unrigged one the plain program.
@@ -6399,37 +6888,80 @@ export class WebGL2Renderer {
6399
6888
  * belongs to was never compiled. Deriving one from the other, as the first version did, made a
6400
6889
  * missing skinned-only program silently switch morph off as well.
6401
6890
  */
6402
- const both = wantSkin && wantMorph && this.flatBothUniforms !== null;
6403
- const skinned = both || (wantSkin && this.flatSkinnedUniforms !== null);
6891
+ /* A rig of eight takes the pair built with SKIN_EIGHT on; `warmVariants` compiled them. */
6892
+ const eight = wantSkin && mesh.isSkinnedEight;
6893
+ /*
6894
+ * **A cloth set and a skinned draw take the cloth-bound program of this rig's kind**, compiled
6895
+ * when the binding was made. Its morph half is the mesh's whether weights are set or not: the
6896
+ * program declares the deltas whenever the mesh has them, so they are bound, at zero weight
6897
+ * when none were asked for — an unbound sampler would default to a shadow map's unit.
6898
+ */
6899
+ const cloth = wantSkin ? this.cloth : null;
6900
+ const clothed = cloth === null
6901
+ ? undefined
6902
+ : this.clothFlat.get(`${eight ? 8 : 4}|${mesh.morph !== null ? 'm' : '-'}`);
6903
+ const bothUniforms = eight ? this.flatBoth8Uniforms : this.flatBothUniforms;
6904
+ const skinnedUniforms = eight ? this.flatSkinned8Uniforms : this.flatSkinnedUniforms;
6905
+ const both = wantSkin && wantMorph && bothUniforms !== null;
6906
+ const skinned = both || (wantSkin && skinnedUniforms !== null);
6404
6907
  const morphed = both || (wantMorph && !wantSkin && this.flatMorphedUniforms !== null);
6405
6908
  const variant = skinned && morphed
6406
- ? this.flatBothUniforms
6909
+ ? bothUniforms
6407
6910
  : skinned
6408
- ? this.flatSkinnedUniforms
6911
+ ? skinnedUniforms
6409
6912
  : morphed
6410
6913
  ? this.flatMorphedUniforms
6411
6914
  : null;
6412
- const u = variant ?? this.flatUniforms;
6413
- const program = skinned && morphed
6414
- ? this.flatBothProgram
6415
- : skinned
6416
- ? this.flatSkinnedProgram
6417
- : morphed
6418
- ? this.flatMorphedProgram
6419
- : null;
6915
+ let u = clothed?.uniforms ?? variant ?? this.flatUniforms;
6916
+ let program = clothed !== undefined
6917
+ ? clothed.program
6918
+ : skinned && morphed
6919
+ ? eight
6920
+ ? this.flatBoth8Program
6921
+ : this.flatBothProgram
6922
+ : skinned
6923
+ ? eight
6924
+ ? this.flatSkinned8Program
6925
+ : this.flatSkinnedProgram
6926
+ : morphed
6927
+ ? this.flatMorphedProgram
6928
+ : null;
6929
+ /* A material with a model takes that model's program for the same vertex variant. */
6930
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
6931
+ if (modelKind !== null) {
6932
+ const modelled = this.modelProgram(clothed !== undefined
6933
+ ? `cloth${eight ? 8 : 4}${mesh.morph !== null ? 'm' : '-'}`
6934
+ : skinned && morphed
6935
+ ? eight
6936
+ ? 'both8'
6937
+ : 'both'
6938
+ : skinned
6939
+ ? eight
6940
+ ? 'skinned8'
6941
+ : 'skinned'
6942
+ : morphed
6943
+ ? 'morphed'
6944
+ : 'plain', modelKind, half);
6945
+ program = modelled.program;
6946
+ u = modelled.uniforms;
6947
+ }
6420
6948
  if (program !== null) {
6421
6949
  gl.useProgram(program);
6422
- if (skinned) {
6950
+ if (skinned || clothed !== undefined) {
6423
6951
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
6424
6952
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
6425
6953
  }
6426
- if (morphed && mesh.morph !== null) {
6954
+ if ((morphed || clothed !== undefined) && mesh.morph !== null) {
6427
6955
  mesh.morph.bind(gl, MORPH_DELTA_TEXTURE_UNIT);
6428
6956
  gl.uniform1i(u['uMorphDeltas'] ?? null, MORPH_DELTA_TEXTURE_UNIT);
6429
6957
  gl.uniform1i(u['uMorphTargetCount'] ?? null, mesh.morph.targetCount);
6430
6958
  gl.uniform1i(u['uMorphTextureWidth'] ?? null, mesh.morph.width);
6431
- gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights);
6959
+ gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights ?? NO_MORPH_WEIGHTS);
6432
6960
  }
6961
+ if (clothed !== undefined && cloth !== null)
6962
+ this.bindCloth(u, cloth);
6963
+ if (modelKind === 'eye')
6964
+ this.writeEyeAxis(u, model);
6433
6965
  }
6434
6966
  else {
6435
6967
  this.useFlatProgram();
@@ -6464,13 +6996,26 @@ export class WebGL2Renderer {
6464
6996
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
6465
6997
  * all. Film, caustics and text lie flush on surfaces for the same reason.
6466
6998
  */
6467
- gl.depthFunc(this.reversedDepth ? gl.GREATER : gl.LESS);
6999
+ /* A skin's diffuse half is the same surface again: it finds the depth its frame half wrote. */
7000
+ const again = half === 'diffuse';
7001
+ gl.depthFunc(again ? gl.EQUAL : this.reversedDepth ? gl.GREATER : gl.LESS);
7002
+ if (again)
7003
+ gl.depthMask(false);
7004
+ /* A dithered cutout in a multisampled frame with no temporal resolve: its share becomes the
7005
+ pixel's samples. Scoped to this draw, as the depth compare above is. See `cutoutDither.ts`. */
7006
+ const coverage = !again && this.cutoutResolveNow(this.currentMaterial, false) === 'coverage';
7007
+ if (coverage)
7008
+ gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
6468
7009
  if (layer === 0) {
6469
7010
  if (this.materialDoubleSided)
6470
7011
  gl.disable(gl.CULL_FACE);
6471
7012
  mesh.draw(gl);
6472
7013
  if (this.materialDoubleSided)
6473
7014
  gl.enable(gl.CULL_FACE);
7015
+ if (coverage)
7016
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
7017
+ if (again)
7018
+ gl.depthMask(true);
6474
7019
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
6475
7020
  if (tint !== null)
6476
7021
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
@@ -6496,6 +7041,10 @@ export class WebGL2Renderer {
6496
7041
  mesh.draw(gl);
6497
7042
  if (this.materialDoubleSided)
6498
7043
  gl.enable(gl.CULL_FACE);
7044
+ if (coverage)
7045
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
7046
+ if (again)
7047
+ gl.depthMask(true);
6499
7048
  gl.polygonOffset(0, 0);
6500
7049
  gl.disable(gl.POLYGON_OFFSET_FILL);
6501
7050
  if (program !== null)
@@ -6544,6 +7093,7 @@ export class WebGL2Renderer {
6544
7093
  * handed straight back.
6545
7094
  */
6546
7095
  drawTranslucentMesh(mesh, model, opacity, options = {}) {
7096
+ this.scatterSkin();
6547
7097
  if (this.contextLost)
6548
7098
  return;
6549
7099
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
@@ -6566,10 +7116,22 @@ export class WebGL2Renderer {
6566
7116
  order-independent transparency, and not once more when it was recorded. */
6567
7117
  this.drawBudget.ask();
6568
7118
  const { gl } = this;
6569
- const u = this.flatUniforms;
6570
- this.useFlatProgram();
7119
+ /* A material with a model takes that model's plain program; see `modelProgram`. */
7120
+ const modelKind = this.currentMaterial?.model?.kind ?? null;
7121
+ const modelled = modelKind === null ? null : this.modelProgram('plain', modelKind);
7122
+ const u = modelled?.uniforms ?? this.flatUniforms;
7123
+ if (modelled === null)
7124
+ this.useFlatProgram();
7125
+ else
7126
+ gl.useProgram(modelled.program);
7127
+ if (modelKind === 'eye')
7128
+ this.writeEyeAxis(u, model);
6571
7129
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
6572
7130
  gl.uniform1f(u['uOpacity'] ?? null, Math.min(opacity, 1));
7131
+ /* A dithered cutout tests hard on a blended draw, and is put back below with the rest. */
7132
+ const ditheredHere = this.cutoutResolveNow(this.currentMaterial, false) !== 'hard';
7133
+ if (ditheredHere)
7134
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
6573
7135
  /*
6574
7136
  * Both default to what `bindMeshPass` already put there — lit and fogged — so the
6575
7137
  * common call with no fourth argument writes the same two values it always implicitly
@@ -6581,7 +7143,7 @@ export class WebGL2Renderer {
6581
7143
  const toneMapped = options.toneMapped ?? true;
6582
7144
  if (!lit)
6583
7145
  gl.uniform1i(u['uLightingEnabled'] ?? null, 0);
6584
- if (fog !== FOG_RECEDE)
7146
+ if (fog !== this.surfaceFog)
6585
7147
  gl.uniform1i(u['uFogEnabled'] ?? null, fog);
6586
7148
  /* `1` is sRGB alone: the conversion without the curve, which is what dropping the tone map
6587
7149
  means rather than dropping the whole transform. `Math.min` rather than a literal, so a
@@ -6616,7 +7178,7 @@ export class WebGL2Renderer {
6616
7178
  */
6617
7179
  /* Taken for itself when any option differs from the pass, and put back below: the rule both
6618
7180
  backends count material changes by. See `materialChanges.ts`. */
6619
- const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
7181
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
6620
7182
  if (own)
6621
7183
  this.materials.dirty();
6622
7184
  this.takeMaterial();
@@ -6625,9 +7187,9 @@ export class WebGL2Renderer {
6625
7187
  gl.enable(gl.BLEND);
6626
7188
  /* Added, where asked: colour times alpha onto what is there, and the target's alpha kept. */
6627
7189
  if (options.additive === true)
6628
- gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ZERO, gl.ONE);
7190
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
6629
7191
  else
6630
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7192
+ blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6631
7193
  }
6632
7194
  /*
6633
7195
  * The two depth options, both scoped to this draw and handed straight back — the same
@@ -6659,6 +7221,8 @@ export class WebGL2Renderer {
6659
7221
  if (ownsState)
6660
7222
  gl.disable(gl.BLEND);
6661
7223
  gl.uniform1f(u['uOpacity'] ?? null, 1);
7224
+ if (ditheredHere)
7225
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
6662
7226
  if (tint !== null)
6663
7227
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
6664
7228
  if (refracting)
@@ -6667,8 +7231,8 @@ export class WebGL2Renderer {
6667
7231
  cannot inherit a material from the draw before it. */
6668
7232
  if (!lit)
6669
7233
  gl.uniform1i(u['uLightingEnabled'] ?? null, 1);
6670
- if (fog !== FOG_RECEDE)
6671
- gl.uniform1i(u['uFogEnabled'] ?? null, FOG_RECEDE);
7234
+ if (fog !== this.surfaceFog)
7235
+ gl.uniform1i(u['uFogEnabled'] ?? null, this.surfaceFog);
6672
7236
  if (!toneMapped)
6673
7237
  gl.uniform1i(u['uOutputTransform'] ?? null, this.gradeCode());
6674
7238
  if (own)
@@ -7036,7 +7600,7 @@ export class WebGL2Renderer {
7036
7600
  gl.uniform1i(u['uSceneDepth'] ?? null, 3);
7037
7601
  }
7038
7602
  gl.enable(gl.BLEND);
7039
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
7603
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE);
7040
7604
  gl.depthMask(false);
7041
7605
  gl.cullFace(inside ? gl.FRONT : gl.BACK);
7042
7606
  mesh.draw(gl);
@@ -7066,6 +7630,7 @@ export class WebGL2Renderer {
7066
7630
  * `beginPlanarReflection` for when that target exists.
7067
7631
  */
7068
7632
  drawFilm(mesh, camera, timeSeconds, env, sheen, options = {}) {
7633
+ this.scatterSkin();
7069
7634
  const { gl } = this;
7070
7635
  const u = this.filmUniforms;
7071
7636
  gl.useProgram(this.filmProgram);
@@ -7104,7 +7669,7 @@ export class WebGL2Renderer {
7104
7669
  }
7105
7670
  bindAtmosphere(gl, u, env, this.reflectionPassActive ? this.reflectionAtmosphereY : (camera.position[1] ?? 0), this.quality.underwaterAtmosphere);
7106
7671
  gl.enable(gl.BLEND);
7107
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7672
+ blendKeeping(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
7108
7673
  gl.depthMask(false);
7109
7674
  mesh.draw(gl);
7110
7675
  gl.depthMask(true);
@@ -7187,3 +7752,20 @@ function angularDiscExponent(angularRadius) {
7187
7752
  * name to prefer. It goes when something else forces a major, and not before.
7188
7753
  */
7189
7754
  export { WebGL2Renderer as Renderer };
7755
+ /** The standard model's numbers: no model, no map. */
7756
+ const NO_MODEL_PARAMS = new Float32Array(MODEL_PARAM_FLOATS);
7757
+ /**
7758
+ * The lit vertex stage a program of this variant compiles: the standard programs' and the cloth's,
7759
+ * which `modelProgram` pairs a model's fragment stage with.
7760
+ */
7761
+ function litVertexOf(variant) {
7762
+ if (variant.startsWith('cloth')) {
7763
+ const eight = variant[5] === '8';
7764
+ const vertex = flatVert({ skinned: true, morphed: variant.endsWith('m'), instanced: false });
7765
+ return clothBound(eight ? skinEight(vertex) : vertex);
7766
+ }
7767
+ const skinned = variant.startsWith('skinned') || variant.startsWith('both');
7768
+ const morphed = variant === 'morphed' || variant.startsWith('both');
7769
+ const vertex = flatVert({ skinned, morphed, instanced: variant === 'instanced' });
7770
+ return variant.endsWith('8') ? skinEight(vertex) : vertex;
7771
+ }