@driftengine/core 4.8.3 → 4.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.json +82 -0
  2. package/README.md +1 -1
  3. package/dist/build/shaderComments.js +0 -0
  4. package/dist/geometry/meshConcat.js +2 -0
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.js +16 -1
  7. package/dist/math/scalar.js +1 -1
  8. package/dist/render/anisotropicLobe.d.ts +7 -0
  9. package/dist/render/anisotropicLobe.js +33 -0
  10. package/dist/render/backend/api.d.ts +41 -4
  11. package/dist/render/backend/select.js +8 -0
  12. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  13. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  14. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  15. package/dist/render/backend/webgl2/renderer.js +604 -63
  16. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  17. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  18. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  19. package/dist/render/backend/webgpu/buffers.js +98 -49
  20. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  21. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  22. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  23. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  24. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  25. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  26. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  27. package/dist/render/backend/webgpu/computeHost.js +5 -0
  28. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  29. package/dist/render/backend/webgpu/depthPass.js +39 -9
  30. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  31. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  32. package/dist/render/backend/webgpu/flatPass.js +109 -12
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  34. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  35. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  36. package/dist/render/backend/webgpu/motionPass.js +106 -9
  37. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  38. package/dist/render/backend/webgpu/postPass.js +6 -3
  39. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  40. package/dist/render/backend/webgpu/renderer.js +498 -48
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  42. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  44. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  47. package/dist/render/clothBindingData.d.ts +41 -0
  48. package/dist/render/clothBindingData.js +72 -0
  49. package/dist/render/clothPlacement.d.ts +25 -0
  50. package/dist/render/clothPlacement.js +85 -0
  51. package/dist/render/compressedSource.d.ts +66 -0
  52. package/dist/render/compressedSource.js +138 -0
  53. package/dist/render/cutoutCaster.d.ts +2 -0
  54. package/dist/render/cutoutCaster.js +2 -0
  55. package/dist/render/cutoutDither.d.ts +51 -0
  56. package/dist/render/cutoutDither.js +90 -0
  57. package/dist/render/eyeRefraction.d.ts +35 -0
  58. package/dist/render/eyeRefraction.js +40 -0
  59. package/dist/render/glCompressed.d.ts +27 -0
  60. package/dist/render/glCompressed.js +53 -0
  61. package/dist/render/hairLobes.d.ts +70 -0
  62. package/dist/render/hairLobes.js +120 -0
  63. package/dist/render/lightBudget.d.ts +22 -0
  64. package/dist/render/lightBudget.js +22 -0
  65. package/dist/render/mesh.d.ts +7 -0
  66. package/dist/render/mesh.js +24 -0
  67. package/dist/render/recon/motionVectors.d.ts +4 -2
  68. package/dist/render/recon/motionVectors.js +6 -3
  69. package/dist/render/renderQuality.d.ts +20 -0
  70. package/dist/render/renderQuality.js +5 -0
  71. package/dist/render/rendererHarness.js +4 -0
  72. package/dist/render/sceneTarget.d.ts +12 -0
  73. package/dist/render/sceneTarget.js +14 -0
  74. package/dist/render/shaders/clothBinding.d.ts +22 -0
  75. package/dist/render/shaders/clothBinding.js +80 -0
  76. package/dist/render/shaders/depth.js +42 -6
  77. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  78. package/dist/render/shaders/flat/anisotropic.js +68 -0
  79. package/dist/render/shaders/flat/eye.d.ts +1 -0
  80. package/dist/render/shaders/flat/eye.js +81 -0
  81. package/dist/render/shaders/flat/hair.d.ts +1 -0
  82. package/dist/render/shaders/flat/hair.js +101 -0
  83. package/dist/render/shaders/flat/index.js +36 -2
  84. package/dist/render/shaders/flat/main.d.ts +1 -1
  85. package/dist/render/shaders/flat/main.js +113 -17
  86. package/dist/render/shaders/flat/models.d.ts +14 -0
  87. package/dist/render/shaders/flat/models.js +273 -0
  88. package/dist/render/shaders/flat/preamble.js +26 -24
  89. package/dist/render/shaders/flat/skin.d.ts +1 -0
  90. package/dist/render/shaders/flat/skin.js +114 -0
  91. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  92. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  93. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  94. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  95. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  96. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  97. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  98. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  99. package/dist/render/shaders/skinBlur.d.ts +1 -0
  100. package/dist/render/shaders/skinBlur.js +103 -0
  101. package/dist/render/shaders/skinning.d.ts +3 -1
  102. package/dist/render/shaders/skinning.js +31 -4
  103. package/dist/render/shadowCasters.d.ts +13 -1
  104. package/dist/render/skinBlur.d.ts +41 -0
  105. package/dist/render/skinBlur.js +49 -0
  106. package/dist/render/skinScatter.d.ts +67 -0
  107. package/dist/render/skinScatter.js +80 -0
  108. package/dist/render/skinnedClothRun.d.ts +26 -0
  109. package/dist/render/skinnedClothRun.js +84 -0
  110. package/dist/render/surfaceModel.d.ts +129 -0
  111. package/dist/render/surfaceModel.js +152 -0
  112. package/dist/render/surfaceTexture.d.ts +44 -4
  113. package/dist/render/surfaceTexture.js +60 -46
  114. package/dist/render/vertexDefaults.js +7 -0
  115. package/package.json +6 -6
  116. package/src/build/shaderComments.ts +0 -0
  117. package/src/geometry/meshConcat.ts +2 -0
  118. package/src/index.ts +40 -1
  119. package/src/math/scalar.ts +1 -1
  120. package/src/render/anisotropicLobe.ts +41 -0
  121. package/src/render/backend/api.ts +52 -3
  122. package/src/render/backend/select.ts +8 -0
  123. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  124. package/src/render/backend/webgl2/renderer.ts +693 -64
  125. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  126. package/src/render/backend/webgpu/buffers.ts +137 -63
  127. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  128. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  129. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  130. package/src/render/backend/webgpu/computeHost.ts +25 -0
  131. package/src/render/backend/webgpu/depthPass.ts +39 -7
  132. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  133. package/src/render/backend/webgpu/flatPass.ts +145 -7
  134. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  135. package/src/render/backend/webgpu/motionPass.ts +128 -9
  136. package/src/render/backend/webgpu/postPass.ts +5 -1
  137. package/src/render/backend/webgpu/renderer.ts +663 -43
  138. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  139. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  140. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  141. package/src/render/clothBindingData.ts +89 -0
  142. package/src/render/clothPlacement.ts +114 -0
  143. package/src/render/compressedSource.ts +198 -0
  144. package/src/render/cutoutCaster.ts +5 -0
  145. package/src/render/cutoutDither.ts +99 -0
  146. package/src/render/eyeRefraction.ts +72 -0
  147. package/src/render/glCompressed.ts +86 -0
  148. package/src/render/hairLobes.ts +188 -0
  149. package/src/render/lightBudget.ts +25 -0
  150. package/src/render/mesh.ts +23 -0
  151. package/src/render/recon/motionVectors.ts +6 -3
  152. package/src/render/renderQuality.ts +28 -0
  153. package/src/render/rendererHarness.ts +4 -0
  154. package/src/render/sceneTarget.ts +21 -0
  155. package/src/render/shaders/clothBinding.ts +81 -0
  156. package/src/render/shaders/depth.ts +42 -6
  157. package/src/render/shaders/flat/anisotropic.ts +68 -0
  158. package/src/render/shaders/flat/eye.ts +82 -0
  159. package/src/render/shaders/flat/hair.ts +102 -0
  160. package/src/render/shaders/flat/index.ts +36 -2
  161. package/src/render/shaders/flat/main.ts +113 -17
  162. package/src/render/shaders/flat/models.ts +286 -0
  163. package/src/render/shaders/flat/preamble.ts +26 -24
  164. package/src/render/shaders/flat/skin.ts +116 -0
  165. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  166. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  167. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  168. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  169. package/src/render/shaders/skinBlur.ts +105 -0
  170. package/src/render/shaders/skinning.ts +31 -4
  171. package/src/render/shadowCasters.ts +13 -0
  172. package/src/render/skinBlur.ts +53 -0
  173. package/src/render/skinScatter.ts +92 -0
  174. package/src/render/skinnedClothRun.ts +128 -0
  175. package/src/render/surfaceModel.ts +274 -0
  176. package/src/render/surfaceTexture.ts +123 -63
  177. package/src/render/vertexDefaults.ts +7 -0
  178. package/dist/math/exact.d.ts +0 -76
  179. package/dist/math/exact.js +0 -327
  180. package/src/math/exact.ts +0 -340
@@ -38,8 +38,19 @@ import { LightField } from '../../driftLight/lightField.js';
38
38
  import { WorldLightField } from '../../driftLight/worldLightField.js';
39
39
  import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
40
40
  import { flatFrag, flatVert } from '../../shaders/flat/index.js';
41
- 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';
42
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);
43
54
  /** Shader-side encoding of `RenderQuality.outputTransform`. */
44
55
  /* The codes the shader reads, stated once in `vertexDefaults.ts` for both backends. */
45
56
  import { LIGHT_VOLUME_VERT, lightVolumeFrag } from '../../shaders/lightVolume.js';
@@ -73,6 +84,7 @@ import { resolveRenderQuality } from '../../renderQuality.js';
73
84
  import { drawingBufferSize } from '../../drawingBuffer.js';
74
85
  import { Mesh, createMeshIncremental } from '../../mesh.js';
75
86
  import { SurfaceTexture } from '../../surfaceTexture.js';
87
+ import { glCompressedFormats } from '../../glCompressed.js';
76
88
  import { PlumeRenderer } from '../../plumeRenderer.js';
77
89
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.js';
78
90
  import { DEPTH_01_TO_CLIP, createResolvedLightVolume, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
@@ -94,6 +106,7 @@ import { deviceSnappedCellSize } from '../../textLayout.js';
94
106
  import { WindStreakRenderer } from '../../windStreakRenderer.js';
95
107
  import { InstancedBatch } from './instanced.js';
96
108
  import { cutoutOf } from '../../cutoutCaster.js';
109
+ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
97
110
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
98
111
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
99
112
  import { noLitSwitches } from '../../shaders/flat/glassTint.js';
@@ -448,6 +461,17 @@ export class WebGL2Renderer {
448
461
  flatMorphedUniforms = null;
449
462
  flatBothProgram = null;
450
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;
451
475
  /** The morph weights the following draws use, or null for none. */
452
476
  morphWeights = null;
453
477
  /**
@@ -481,6 +505,14 @@ export class WebGL2Renderer {
481
505
  * before this existed.
482
506
  */
483
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();
484
516
  /** So the refusal below is once per renderer rather than once per frame. See `setColourGrade`. */
485
517
  warnedGradeWithoutComposite = false;
486
518
  /** Scratch for a volume's `uDepthToLocal`. Built per draw and never allocated in the loop. */
@@ -503,6 +535,21 @@ export class WebGL2Renderer {
503
535
  skinPalette = new SkinPaletteTexture();
504
536
  /** Whether a palette is currently chosen. Distinct from whether one was ever uploaded. */
505
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;
506
553
  emptyTexture2D;
507
554
  /** One glass resolution reused by every draw, so a pane allocates nothing. See `glass.ts`. */
508
555
  glassScratch = { transmission: 0, frost: 0, tint: [1, 1, 1] };
@@ -974,6 +1021,22 @@ export class WebGL2Renderer {
974
1021
  materialHasMaps = false;
975
1022
  /** The material last set, so a lit program rebuilt for glass can be given it. */
976
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;
977
1040
  /**
978
1041
  * The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
979
1042
  * profile and the content have asked for since (`litWanted`).
@@ -1110,7 +1173,7 @@ export class WebGL2Renderer {
1110
1173
  gl.enable(gl.CULL_FACE);
1111
1174
  gl.useProgram(this.depthProgram);
1112
1175
  },
1113
- skinnedMesh: (mesh, model, palette, material) => {
1176
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
1114
1177
  if (!this.drawingGlassDepth && !castsDepth(material)) {
1115
1178
  if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
1116
1179
  this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
@@ -1120,8 +1183,20 @@ export class WebGL2Renderer {
1120
1183
  }
1121
1184
  this.shadowDrawBudget.ask();
1122
1185
  const { gl } = this;
1123
- const u = this.depthSkinnedUniforms;
1124
- 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));
1125
1200
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
1126
1201
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
1127
1202
  /* The peel layer reads the first pass's depths; `beginShadowPass` bound that texture to
@@ -1132,6 +1207,12 @@ export class WebGL2Renderer {
1132
1207
  this.skinPalette.update(gl, palette);
1133
1208
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
1134
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
+ }
1135
1216
  /* Narrowing is safe for the reason `mesh` above gives: this renderer only ever handed
1136
1217
  out its own `Mesh`. */
1137
1218
  mesh.draw(gl);
@@ -1176,7 +1257,7 @@ export class WebGL2Renderer {
1176
1257
  gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
1177
1258
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
1178
1259
  gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
1179
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
1260
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
1180
1261
  /* Narrowed for the reason the sink's own `mesh` gives: this renderer only hands out its own. */
1181
1262
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
1182
1263
  }
@@ -1971,11 +2052,20 @@ export class WebGL2Renderer {
1971
2052
  this.sceneTarget = this.quality.screenEffects
1972
2053
  ? new SceneTarget(gl, this.quality.sceneSamples, this.quality.hdrScene, this.quality.discardResolvedAttachments)
1973
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;
1974
2063
  /*
1975
2064
  * Said once at init, because the number a caller asked for and the number the part gave
1976
2065
  * them are not always the same, and a silently clamped sample count is a quality setting
1977
2066
  * that does not apply — the same failure mode `capabilityClamp` exists to avoid.
1978
2067
  */
2068
+ this.canvasSamples = Math.max(1, gl.getParameter(gl.SAMPLES) ?? 1);
1979
2069
  const got = this.sceneTarget?.sampleCount ?? 1;
1980
2070
  if (this.quality.sceneSamples > 1 && got !== this.quality.sceneSamples) {
1981
2071
  console.warn(`Renderer: ${this.quality.sceneSamples}x scene multisampling was requested and this ` +
@@ -2136,6 +2226,8 @@ export class WebGL2Renderer {
2136
2226
  return;
2137
2227
  this.disposed = true;
2138
2228
  const { gl } = this;
2229
+ if (!this.contextLost)
2230
+ this.skinScatter?.dispose();
2139
2231
  this.detachContextLoss();
2140
2232
  this.contextLostListeners.length = 0;
2141
2233
  this.contextRestoredListeners.length = 0;
@@ -2175,11 +2267,17 @@ export class WebGL2Renderer {
2175
2267
  this.flatSkinnedProgram,
2176
2268
  this.flatMorphedProgram,
2177
2269
  this.flatBothProgram,
2270
+ this.flatSkinned8Program,
2271
+ this.flatBoth8Program,
2272
+ this.depthSkinned8Program,
2178
2273
  this.flatInstancedProgram,
2179
2274
  ]) {
2180
2275
  if (extra !== null)
2181
2276
  gl.deleteProgram(extra);
2182
2277
  }
2278
+ for (const entry of [...this.clothFlat.values(), ...this.clothDepth.values()]) {
2279
+ gl.deleteProgram(entry.program);
2280
+ }
2183
2281
  for (const program of [
2184
2282
  this.flatProgram,
2185
2283
  this.panelProgram,
@@ -2276,13 +2374,19 @@ export class WebGL2Renderer {
2276
2374
  */
2277
2375
  warmVariants(data) {
2278
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;
2279
2379
  const morphable = data.morphTargets !== undefined;
2280
2380
  for (const skinned of skinnable ? [false, true] : [false]) {
2281
2381
  for (const morphed of morphable ? [false, true] : [false]) {
2282
2382
  if (skinned || morphed)
2283
- this.ensureVariant(skinned, morphed);
2383
+ this.ensureVariant(skinned, morphed, skinned && eight);
2284
2384
  }
2285
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
+ }
2286
2390
  }
2287
2391
  /**
2288
2392
  * Rewrite a mesh's positions, and its normals where the caller has them.
@@ -2324,24 +2428,39 @@ export class WebGL2Renderer {
2324
2428
  * `bindMeshPass` would otherwise carry every uniform at zero, and zero is a real value for most
2325
2429
  * of them — the failure this file records for `uGrain` and `uAmbientGround`.
2326
2430
  */
2327
- ensureVariant(skinned, morphed) {
2431
+ ensureVariant(skinned, morphed, eight = false) {
2328
2432
  if (this.contextLost)
2329
2433
  return;
2330
- if (skinned && morphed && this.flatBothProgram !== null)
2331
- return;
2332
- if (skinned && !morphed && this.flatSkinnedProgram !== null)
2333
- return;
2334
- if (!skinned && morphed && this.flatMorphedProgram !== null)
2335
- return;
2336
- if (!skinned && !morphed)
2337
- 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
+ }
2338
2448
  const { gl } = this;
2339
- const label = `flat.${morphed ? 'morphed' : ''}${skinned ? 'skinned' : ''}`;
2449
+ const label = `flat.${morphed ? 'morphed' : ''}${skinned ? (eight ? 'skinned8' : 'skinned') : ''}`;
2340
2450
  /* `flatFragSource`, not a fresh `flatFrag`: every variant shades through one fragment stage
2341
2451
  at one light budget, and the uploads are written for that budget. */
2342
- 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);
2343
2454
  const uniforms = uniformLocations(gl, program, label);
2344
- 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) {
2345
2464
  this.flatBothProgram = program;
2346
2465
  this.flatBothUniforms = uniforms;
2347
2466
  }
@@ -2359,6 +2478,8 @@ export class WebGL2Renderer {
2359
2478
  if (camera !== null && env !== null) {
2360
2479
  gl.useProgram(program);
2361
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);
2362
2483
  this.useFlatProgram();
2363
2484
  }
2364
2485
  }
@@ -2371,7 +2492,21 @@ export class WebGL2Renderer {
2371
2492
  * the consumer's decision. What it gains is a GPU object with sane sampler state.
2372
2493
  */
2373
2494
  createSurfaceTexture(source, options = {}) {
2374
- 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;
2375
2510
  }
2376
2511
  /**
2377
2512
  * Upload several images of one size as the layers of one array, for a merged mesh that names a
@@ -2380,7 +2515,7 @@ export class WebGL2Renderer {
2380
2515
  * refusal names the first that is not.
2381
2516
  */
2382
2517
  createSurfaceTextureArray(sources, options = {}) {
2383
- return new SurfaceTexture(this.gl, sources, options);
2518
+ return new SurfaceTexture(this.gl, sources, options, this.compressedFormats);
2384
2519
  }
2385
2520
  /**
2386
2521
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
@@ -2438,6 +2573,148 @@ export class WebGL2Renderer {
2438
2573
  }
2439
2574
  this.skinPalette.update(this.gl, palette);
2440
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);
2441
2718
  }
2442
2719
  /**
2443
2720
  * Choose the morph weights the following `drawMesh` calls deform by, or null for none.
@@ -2459,6 +2736,11 @@ export class WebGL2Renderer {
2459
2736
  this.materialDoubleSided = material?.doubleSided === true;
2460
2737
  this.materialHasMaps = materialHasMaps(material);
2461
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
+ }
2462
2744
  /*
2463
2745
  * Written to every flat program, because material state persists across draws and a skinned
2464
2746
  * draw is entitled to the material the caller set before it. A second program holding none of
@@ -2475,6 +2757,25 @@ export class WebGL2Renderer {
2475
2757
  }
2476
2758
  this.useFlatProgram();
2477
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
+ }
2478
2779
  /**
2479
2780
  * One flat program's view of the current material.
2480
2781
  *
@@ -2519,7 +2820,7 @@ export class WebGL2Renderer {
2519
2820
  if (orm !== null)
2520
2821
  orm.bind(gl, ORM_TEXTURE_UNIT);
2521
2822
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
2522
- gl.uniform1i(u['uOrmEnabled'] ?? null, orm === null ? 0 : 1);
2823
+ this.materialFlags[1] = orm === null ? 0 : 1;
2523
2824
  gl.uniform3f(u['uOrmScale'] ?? null, material?.occlusionStrength ?? 1, material?.roughnessScale ?? 1, material?.metallicScale ?? 1);
2524
2825
  /*
2525
2826
  * The emissive map, on exactly the terms the two maps above it are bound: before the albedo
@@ -2527,7 +2828,7 @@ export class WebGL2Renderer {
2527
2828
  *
2528
2829
  * **The stand-in is black, so the gate is what keeps a glow on.** The shader multiplies by
2529
2830
  * this map, and `emptyTexture2D` is opaque black rather than the multiplicative identity, so an
2530
- * 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
2531
2832
  * and the read never happens. What would make that wrong is a read taken outside that branch:
2532
2833
  * every glow on a material without a map would go out.
2533
2834
  */
@@ -2537,7 +2838,7 @@ export class WebGL2Renderer {
2537
2838
  if (emissiveMap !== null)
2538
2839
  emissiveMap.bind(gl, EMISSIVE_TEXTURE_UNIT);
2539
2840
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
2540
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, emissiveMap === null ? 0 : 1);
2841
+ this.materialFlags[2] = emissiveMap === null ? 0 : 1;
2541
2842
  const emissiveScale = material?.emissiveScale ?? null;
2542
2843
  gl.uniform3f(u['uEmissiveScale'] ?? null, emissiveScale?.[0] ?? 1, emissiveScale?.[1] ?? 1, emissiveScale?.[2] ?? 1);
2543
2844
  /*
@@ -2549,7 +2850,20 @@ export class WebGL2Renderer {
2549
2850
  */
2550
2851
  gl.uniform2f(u['uUvScale'] ?? null, material?.uScale ?? 1, material?.vScale ?? 1);
2551
2852
  gl.uniform2f(u['uUvOffset'] ?? null, material?.uOffset ?? 0, material?.vOffset ?? 0);
2552
- gl.uniform1i(u['uDoubleSided'] ?? null, material?.doubleSided === true ? 1 : 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);
2553
2867
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
2554
2868
  return, so a material with no albedo still leaves a complete texture on the unit. */
2555
2869
  if (albedo === null) {
@@ -2568,12 +2882,14 @@ export class WebGL2Renderer {
2568
2882
  array target, and with nothing there it is incomplete. */
2569
2883
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
2570
2884
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
2571
- gl.uniform1i(u['uAlbedoEnabled'] ?? null, 0);
2885
+ this.materialFlags[0] = 0;
2886
+ gl.uniform4iv(u['uMaterialFlags'] ?? null, this.materialFlags);
2572
2887
  return;
2573
2888
  }
2574
2889
  albedo.bind(gl, SURFACE_TEXTURE_UNIT);
2575
- gl.uniform1f(u['uAlbedoCutout'] ?? null, material?.cutout ?? 0);
2576
- 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);
2577
2893
  }
2578
2894
  /**
2579
2895
  * @deprecated Use `setMaterial`.
@@ -2590,7 +2906,7 @@ export class WebGL2Renderer {
2590
2906
  * Rebind `SURFACE_TEXTURE_UNIT` to whatever `setSurfaceTexture` last put there.
2591
2907
  *
2592
2908
  * Only `drawSdfText` calls this, after the atlas has finished borrowing the same unit.
2593
- * 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
2594
2910
  * touched by the text draw and are still correct, since that draw runs its own separate
2595
2911
  * program. What moved is unit 15's actual `WebGLTexture` binding, which is GL state, not
2596
2912
  * program state, and this puts it back to what `currentSurfaceTexture` says it should be.
@@ -2867,6 +3183,8 @@ export class WebGL2Renderer {
2867
3183
  if (camera !== null && env !== null) {
2868
3184
  gl.useProgram(program);
2869
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);
2870
3188
  this.useFlatProgram();
2871
3189
  }
2872
3190
  }
@@ -2898,6 +3216,7 @@ export class WebGL2Renderer {
2898
3216
  }
2899
3217
  /** Draw every live instance, blended. See `drawTranslucentMesh` for what the options mean. */
2900
3218
  drawTranslucentInstanced(batch, data, opacity, options = {}) {
3219
+ this.scatterSkin();
2901
3220
  this.submitInstanced(batch, data, opacity, true, options);
2902
3221
  }
2903
3222
  /** Release the placement. The mesh is the caller's and is not released. */
@@ -2937,8 +3256,11 @@ export class WebGL2Renderer {
2937
3256
  return;
2938
3257
  this.drawBudget.ask();
2939
3258
  this.ensureInstancedProgram();
2940
- const program = this.flatInstancedProgram;
2941
- 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;
2942
3264
  if (program === null || u === null)
2943
3265
  return;
2944
3266
  const { gl } = this;
@@ -3001,10 +3323,23 @@ export class WebGL2Renderer {
3001
3323
  for both: without it a batch's far faces were culled here alone. */
3002
3324
  if (this.materialDoubleSided)
3003
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));
3004
3335
  if (culling && cullsInstances(count, mesh.indexCount))
3005
3336
  batch.drawCulled(gl, data, this.frustum);
3006
3337
  else
3007
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));
3008
3343
  if (this.materialDoubleSided)
3009
3344
  gl.enable(gl.CULL_FACE);
3010
3345
  if (layer > 0) {
@@ -3164,6 +3499,7 @@ export class WebGL2Renderer {
3164
3499
  }
3165
3500
  /** Draw after the opaque scene: streaks are blended and write no depth. */
3166
3501
  drawWindStreaks(streaks, camera, wind, timeSeconds, tint, env) {
3502
+ this.scatterSkin();
3167
3503
  // The engine decides, not the caller: every pass already knows where the
3168
3504
  // waterline is, and leaving it to each call site is how one of them ends up
3169
3505
  // raining dust into the sea.
@@ -3196,6 +3532,7 @@ export class WebGL2Renderer {
3196
3532
  * whatever box the caller lays out in, and the game's own card is not always the canvas.
3197
3533
  */
3198
3534
  drawText(text, viewportWidth, viewportHeight, originX, originY, style, timeSec) {
3535
+ this.scatterSkin();
3199
3536
  if (text.draw(viewportWidth, viewportHeight, originX, originY, style, timeSec)) {
3200
3537
  this.textBudget.ask();
3201
3538
  }
@@ -3287,6 +3624,7 @@ export class WebGL2Renderer {
3287
3624
  return this.sceneTarget === null || !this.quality.hdrScene ? this.exposure : 1;
3288
3625
  }
3289
3626
  drawSdfText(handle, model, color, opacity) {
3627
+ this.scatterSkin();
3290
3628
  if (this.contextLost)
3291
3629
  return;
3292
3630
  const viewProj = this.frameViewProj;
@@ -3358,6 +3696,7 @@ export class WebGL2Renderer {
3358
3696
  * whole milestone from a buffer nothing had ever written.
3359
3697
  */
3360
3698
  drawParticles(batch, data, camera, env, timeSeconds) {
3699
+ this.scatterSkin();
3361
3700
  if (data.count === 0)
3362
3701
  return;
3363
3702
  const { gl } = this;
@@ -3403,6 +3742,7 @@ export class WebGL2Renderer {
3403
3742
  * it strobes as the rasteriser catches it on some frames and not others.
3404
3743
  */
3405
3744
  drawBolts(batch, data, camera, env, timeSeconds, core, edge, widthM, coreGain, minWidthPerMetre = 0.004) {
3745
+ this.scatterSkin();
3406
3746
  if (data.count === 0)
3407
3747
  return;
3408
3748
  const { gl } = this;
@@ -3459,6 +3799,7 @@ export class WebGL2Renderer {
3459
3799
  * edge inward, as a fraction of the half-width; zero is a clean edge.
3460
3800
  */
3461
3801
  drawLines(lines, data, model, camera, env, color, widthM, opacity, softness = 0, minWidthPerMetre = 0, additive = false) {
3802
+ this.scatterSkin();
3462
3803
  if (data.count === 0)
3463
3804
  return;
3464
3805
  const { gl } = this;
@@ -3642,6 +3983,7 @@ export class WebGL2Renderer {
3642
3983
  }
3643
3984
  // --- Draws that own their context ----------------------------------------
3644
3985
  drawPlumes(plumes, camera, timeSeconds, env, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0) {
3986
+ this.scatterSkin();
3645
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);
3646
3988
  }
3647
3989
  drawWater(water, camera, timeSeconds, settings, env, windX = 0, windZ = 0) {
@@ -3655,6 +3997,7 @@ export class WebGL2Renderer {
3655
3997
  * from the water under them.
3656
3998
  */
3657
3999
  drawCaustics(caustics, camera, timeSeconds, env, windX = 0, windZ = 0, strength = 1) {
4000
+ this.scatterSkin();
3658
4001
  if (caustics === null || !this.quality.water)
3659
4002
  return;
3660
4003
  const drew = caustics.draw(this.gl, this.frameViewFor(camera), timeSeconds, env.directionalDir, env.directionalColor, env.ambient, env, this.quality.underwaterAtmosphere, windX, windZ, strength);
@@ -3950,6 +4293,14 @@ export class WebGL2Renderer {
3950
4293
  this.uploadWind(this.depthUniforms, this.frameWind);
3951
4294
  gl.useProgram(this.depthSkinnedProgram);
3952
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
+ }
3953
4304
  gl.useProgram(this.depthCutoutProgram);
3954
4305
  this.uploadWind(this.depthCutoutUniforms, this.frameWind);
3955
4306
  gl.useProgram(this.depthProgram);
@@ -3975,14 +4326,18 @@ export class WebGL2Renderer {
3975
4326
  this.bindSceneCasterMaterial(material);
3976
4327
  this.drawMesh(mesh, model);
3977
4328
  },
3978
- skinnedMesh: (mesh, model, palette, material) => {
4329
+ skinnedMesh: (mesh, model, palette, material, cloth) => {
3979
4330
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
3980
4331
  gives about the mesh: nothing else ever handed one out. */
3981
4332
  this.bindSceneCasterMaterial(material);
3982
4333
  this.setSkinPalette(palette);
4334
+ if (cloth !== undefined) {
4335
+ this.setCloth(cloth.binding, cloth.particles);
4336
+ }
3983
4337
  this.drawMesh(mesh, model);
3984
4338
  /* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
3985
4339
  this.setSkinPalette(null);
4340
+ this.setCloth(null);
3986
4341
  },
3987
4342
  instanced: (batch, data, material) => {
3988
4343
  /* The opaque handle narrowed to this backend's own, for the reason `casterSink`
@@ -4107,7 +4462,7 @@ export class WebGL2Renderer {
4107
4462
  if (cutout !== null) {
4108
4463
  gl.uniform2f(u['uUvScale'] ?? null, cutout.u, cutout.v);
4109
4464
  gl.uniform2f(u['uUvOffset'] ?? null, cutout.uOffset, cutout.vOffset);
4110
- gl.uniform1f(u['uAlphaCutout'] ?? null, cutout.cutoff);
4465
+ gl.uniform2f(u['uAlphaCutout'] ?? null, cutout.cutoff, cutout.dithered);
4111
4466
  cutout.albedo.bind(gl, SURFACE_TEXTURE_UNIT);
4112
4467
  gl.uniform1i(u['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
4113
4468
  }
@@ -4245,10 +4600,11 @@ export class WebGL2Renderer {
4245
4600
  const { gl } = this;
4246
4601
  Object.assign(this.litOn, this.litWanted);
4247
4602
  this.flatFragSource = this.flatFragAt(this.lightBudget);
4248
- const recompile = (old, skinned, morphed, instanced, label) => {
4603
+ const recompile = (old, skinned, morphed, instanced, label, eight = false) => {
4249
4604
  if (old === null)
4250
4605
  return null;
4251
- 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);
4252
4608
  gl.deleteProgram(old);
4253
4609
  return { program, uniforms: uniformLocations(gl, program, label) };
4254
4610
  };
@@ -4266,14 +4622,40 @@ export class WebGL2Renderer {
4266
4622
  const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
4267
4623
  this.flatBothProgram = both?.program ?? null;
4268
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;
4269
4631
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
4270
4632
  this.flatInstancedProgram = instanced?.program ?? null;
4271
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
+ }
4272
4650
  this.flatTargets.length = 0;
4273
- for (const target of [plain, skinned, morphed, both, instanced]) {
4651
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
4274
4652
  if (target !== null)
4275
4653
  this.flatTargets.push(target);
4276
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);
4277
4659
  const camera = this.lastPassCamera;
4278
4660
  const env = this.lastPassEnv;
4279
4661
  for (const target of this.flatTargets) {
@@ -5334,12 +5716,15 @@ export class WebGL2Renderer {
5334
5716
  beginFrame(clearColor) {
5335
5717
  if (this.contextLost)
5336
5718
  return;
5719
+ this.skinScattered = false;
5720
+ this.skinScatter?.reset();
5337
5721
  if (this.litAsked) {
5338
5722
  this.litAsked = false;
5339
5723
  this.rebuildLit();
5340
5724
  }
5341
5725
  this.budget.reset();
5342
5726
  this.materials.dirty();
5727
+ this.cutoutFrame = (this.cutoutFrame + 1) % 64;
5343
5728
  /*
5344
5729
  * Said once, for the reason `refuseOitMultisampled` gives: a quality setting that is enabled
5345
5730
  * and does nothing is the fault the capability clamp exists to prevent.
@@ -5414,6 +5799,8 @@ export class WebGL2Renderer {
5414
5799
  /* A view model left open would squeeze every pass after the world, the composite included. */
5415
5800
  if (this.viewModelActive)
5416
5801
  this.endViewModel();
5802
+ /* Skin not yet spread, because nothing blended came after it, is spread now. */
5803
+ this.scatterSkin();
5417
5804
  /*
5418
5805
  * Camera motion blur, built from the camera the frame was *rendered* with.
5419
5806
  *
@@ -5900,6 +6287,16 @@ export class WebGL2Renderer {
5900
6287
  * is a non-idempotent step entering this body; there is none today, and one added would show
5901
6288
  * as a skinned game behaving differently from an unskinned one.
5902
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;
5903
6300
  for (const target of this.flatTargets) {
5904
6301
  this.gl.useProgram(target.program);
5905
6302
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -6190,7 +6587,7 @@ export class WebGL2Renderer {
6190
6587
  * the sort of per-draw cost this renderer avoids everywhere else.
6191
6588
  */
6192
6589
  /*
6193
- * 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`
6194
6591
  * is 0 until a caller asks for one, and a world that never asks would otherwise leave
6195
6592
  * this sampler incomplete for the whole session.
6196
6593
  */
@@ -6267,19 +6664,15 @@ export class WebGL2Renderer {
6267
6664
  gl.activeTexture(gl.TEXTURE0 + SURFACE_TEXTURE_UNIT);
6268
6665
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6269
6666
  gl.uniform1i(u['uAlbedo'] ?? null, SURFACE_TEXTURE_UNIT);
6270
- 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);
6271
6669
  /* No albedo, so no effects table: the stand-in, which the shader reads as none. */
6272
6670
  gl.activeTexture(gl.TEXTURE0 + SURFACE_EFFECTS_TEXTURE_UNIT);
6273
6671
  gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
6274
6672
  gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
6275
6673
  gl.uniform2f(u['uUvScale'] ?? null, 1, 1);
6276
6674
  gl.uniform2f(u['uUvOffset'] ?? null, 0, 0);
6277
- gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
6278
- gl.uniform1i(u['uDoubleSided'] ?? null, 0);
6279
- this.materialDoubleSided = false;
6280
- this.materialHasMaps = false;
6281
- this.currentMaterial = null;
6282
- this.currentSurfaceTexture = null;
6675
+ gl.uniform4f(u['uCutout'] ?? null, 0, 0, 0, 0);
6283
6676
  /*
6284
6677
  * **And the normal map, which this did not reset and had to.**
6285
6678
  *
@@ -6305,14 +6698,17 @@ export class WebGL2Renderer {
6305
6698
  gl.activeTexture(gl.TEXTURE0 + ORM_TEXTURE_UNIT);
6306
6699
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6307
6700
  gl.uniform1i(u['uOrmMap'] ?? null, ORM_TEXTURE_UNIT);
6308
- gl.uniform1i(u['uOrmEnabled'] ?? null, 0);
6309
6701
  gl.uniform3f(u['uOrmScale'] ?? null, 1, 1, 1);
6310
6702
  /* And the emissive map. The scale goes back to one, which is the identity it multiplies by. */
6311
6703
  gl.activeTexture(gl.TEXTURE0 + EMISSIVE_TEXTURE_UNIT);
6312
6704
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
6313
6705
  gl.uniform1i(u['uEmissiveMap'] ?? null, EMISSIVE_TEXTURE_UNIT);
6314
- gl.uniform1i(u['uEmissiveMapEnabled'] ?? null, 0);
6315
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);
6316
6712
  /*
6317
6713
  * White, because a uniform nobody has written is **zero**, and this one multiplies
6318
6714
  * vertex colour.
@@ -6421,6 +6817,58 @@ export class WebGL2Renderer {
6421
6817
  return;
6422
6818
  this.drawBudget.ask();
6423
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) {
6424
6872
  const { gl } = this;
6425
6873
  /*
6426
6874
  * A rigged mesh takes the skinned program, an unrigged one the plain program.
@@ -6440,37 +6888,80 @@ export class WebGL2Renderer {
6440
6888
  * belongs to was never compiled. Deriving one from the other, as the first version did, made a
6441
6889
  * missing skinned-only program silently switch morph off as well.
6442
6890
  */
6443
- const both = wantSkin && wantMorph && this.flatBothUniforms !== null;
6444
- 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);
6445
6907
  const morphed = both || (wantMorph && !wantSkin && this.flatMorphedUniforms !== null);
6446
6908
  const variant = skinned && morphed
6447
- ? this.flatBothUniforms
6909
+ ? bothUniforms
6448
6910
  : skinned
6449
- ? this.flatSkinnedUniforms
6911
+ ? skinnedUniforms
6450
6912
  : morphed
6451
6913
  ? this.flatMorphedUniforms
6452
6914
  : null;
6453
- const u = variant ?? this.flatUniforms;
6454
- const program = skinned && morphed
6455
- ? this.flatBothProgram
6456
- : skinned
6457
- ? this.flatSkinnedProgram
6458
- : morphed
6459
- ? this.flatMorphedProgram
6460
- : 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
+ }
6461
6948
  if (program !== null) {
6462
6949
  gl.useProgram(program);
6463
- if (skinned) {
6950
+ if (skinned || clothed !== undefined) {
6464
6951
  this.skinPalette.bind(gl, SKIN_PALETTE_TEXTURE_UNIT);
6465
6952
  gl.uniform1i(u['uJointPalette'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
6466
6953
  }
6467
- if (morphed && mesh.morph !== null) {
6954
+ if ((morphed || clothed !== undefined) && mesh.morph !== null) {
6468
6955
  mesh.morph.bind(gl, MORPH_DELTA_TEXTURE_UNIT);
6469
6956
  gl.uniform1i(u['uMorphDeltas'] ?? null, MORPH_DELTA_TEXTURE_UNIT);
6470
6957
  gl.uniform1i(u['uMorphTargetCount'] ?? null, mesh.morph.targetCount);
6471
6958
  gl.uniform1i(u['uMorphTextureWidth'] ?? null, mesh.morph.width);
6472
- gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights);
6959
+ gl.uniform1fv(u['uMorphWeights'] ?? null, this.morphWeights ?? NO_MORPH_WEIGHTS);
6473
6960
  }
6961
+ if (clothed !== undefined && cloth !== null)
6962
+ this.bindCloth(u, cloth);
6963
+ if (modelKind === 'eye')
6964
+ this.writeEyeAxis(u, model);
6474
6965
  }
6475
6966
  else {
6476
6967
  this.useFlatProgram();
@@ -6505,13 +6996,26 @@ export class WebGL2Renderer {
6505
6996
  * depth 1.0 against a buffer cleared to 1.0, and under `LESS` it would not draw at
6506
6997
  * all. Film, caustics and text lie flush on surfaces for the same reason.
6507
6998
  */
6508
- 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);
6509
7009
  if (layer === 0) {
6510
7010
  if (this.materialDoubleSided)
6511
7011
  gl.disable(gl.CULL_FACE);
6512
7012
  mesh.draw(gl);
6513
7013
  if (this.materialDoubleSided)
6514
7014
  gl.enable(gl.CULL_FACE);
7015
+ if (coverage)
7016
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
7017
+ if (again)
7018
+ gl.depthMask(true);
6515
7019
  gl.depthFunc(this.reversedDepth ? gl.GEQUAL : gl.LEQUAL);
6516
7020
  if (tint !== null)
6517
7021
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
@@ -6537,6 +7041,10 @@ export class WebGL2Renderer {
6537
7041
  mesh.draw(gl);
6538
7042
  if (this.materialDoubleSided)
6539
7043
  gl.enable(gl.CULL_FACE);
7044
+ if (coverage)
7045
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
7046
+ if (again)
7047
+ gl.depthMask(true);
6540
7048
  gl.polygonOffset(0, 0);
6541
7049
  gl.disable(gl.POLYGON_OFFSET_FILL);
6542
7050
  if (program !== null)
@@ -6585,6 +7093,7 @@ export class WebGL2Renderer {
6585
7093
  * handed straight back.
6586
7094
  */
6587
7095
  drawTranslucentMesh(mesh, model, opacity, options = {}) {
7096
+ this.scatterSkin();
6588
7097
  if (this.contextLost)
6589
7098
  return;
6590
7099
  /* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
@@ -6607,10 +7116,22 @@ export class WebGL2Renderer {
6607
7116
  order-independent transparency, and not once more when it was recorded. */
6608
7117
  this.drawBudget.ask();
6609
7118
  const { gl } = this;
6610
- const u = this.flatUniforms;
6611
- 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);
6612
7129
  gl.uniformMatrix4fv(u['uModel'] ?? null, false, model);
6613
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));
6614
7135
  /*
6615
7136
  * Both default to what `bindMeshPass` already put there — lit and fogged — so the
6616
7137
  * common call with no fourth argument writes the same two values it always implicitly
@@ -6700,6 +7221,8 @@ export class WebGL2Renderer {
6700
7221
  if (ownsState)
6701
7222
  gl.disable(gl.BLEND);
6702
7223
  gl.uniform1f(u['uOpacity'] ?? null, 1);
7224
+ if (ditheredHere)
7225
+ gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, false));
6703
7226
  if (tint !== null)
6704
7227
  gl.uniform3fv(u['uTint'] ?? null, WHITE_TINT);
6705
7228
  if (refracting)
@@ -7107,6 +7630,7 @@ export class WebGL2Renderer {
7107
7630
  * `beginPlanarReflection` for when that target exists.
7108
7631
  */
7109
7632
  drawFilm(mesh, camera, timeSeconds, env, sheen, options = {}) {
7633
+ this.scatterSkin();
7110
7634
  const { gl } = this;
7111
7635
  const u = this.filmUniforms;
7112
7636
  gl.useProgram(this.filmProgram);
@@ -7228,3 +7752,20 @@ function angularDiscExponent(angularRadius) {
7228
7752
  * name to prefer. It goes when something else forces a major, and not before.
7229
7753
  */
7230
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
+ }