@driftengine/core 4.9.0 → 4.11.0

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 (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -90,7 +90,8 @@ import { clamp } from '../../../math/scalar.ts';
90
90
  import { attachContextLoss } from '../../contextLoss.ts';
91
91
  import { isWeakGpuFamily, webgl2RendererName } from '../../gpuCapability.ts';
92
92
  import { GpuTimer } from '../../gpuTimer.ts';
93
- import { compileProgram, uniformLocations } from '../../shader.ts';
93
+ import { compileProgram, finishProgram, startProgram, uniformLocations } from '../../shader.ts';
94
+ import type { StartedProgram } from '../../shader.ts';
94
95
  import {
95
96
  countUniformVectors,
96
97
  FULL_LIGHT_BUDGET,
@@ -104,6 +105,11 @@ import {
104
105
  packSurfaceOverlay,
105
106
  type SurfaceOverlay,
106
107
  } from '../../surfaceOverlay.ts';
108
+ import { PROJECTION_FLOATS, packSurfaceProjection, projects } from '../../surfaceProjection.ts';
109
+ import { orthographicDepthSpan } from '../../lightMatrix.ts';
110
+ import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.ts';
111
+ import type { SurfaceLayers } from '../../surfaceLayers.ts';
112
+ import type { SurfaceProjection } from '../../surfaceProjection.ts';
107
113
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.ts';
108
114
  import { SceneTarget } from '../../sceneTarget.ts';
109
115
  import type { ColourGradeLut } from '../../colourGrade.ts';
@@ -218,6 +224,8 @@ import {
218
224
  DEPTH_CUTOUT_FRAG,
219
225
  DEPTH_CUTOUT_VERT,
220
226
  DEPTH_FRAG,
227
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
228
+ DEPTH_INSTANCED_ANIMATED_VERT,
221
229
  DEPTH_INSTANCED_CUTOUT_VERT,
222
230
  DEPTH_INSTANCED_VERT,
223
231
  DEPTH_SKINNED_VERT,
@@ -232,6 +240,8 @@ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
232
240
  import { SunGlassTint } from '../../sunGlassTint.ts';
233
241
  import { GlassTintPrograms, type GlassTintProgram } from './glassTintPrograms.ts';
234
242
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
243
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.ts';
244
+ import type { StaticDrawsHandle, StaticDrawsList } from '../../staticDraws.ts';
235
245
  import { FACE_COUNT, PointShadowMap, type PointGlassDraw } from '../../pointShadowMap.ts';
236
246
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.ts';
237
247
  import {
@@ -277,7 +287,11 @@ import { SurfaceTexture } from '../../surfaceTexture.ts';
277
287
  import { sceneCaptureTexels } from '../../sceneCapture.ts';
278
288
  import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
279
289
  import { glCompressedFormats } from '../../glCompressed.ts';
280
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
290
+ import type {
291
+ CompressedTextureFormat,
292
+ CompressedTextureSource,
293
+ SurfaceSource,
294
+ } from '../../compressedSource.ts';
281
295
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
282
296
  import { PlumeRenderer } from '../../plumeRenderer.ts';
283
297
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.ts';
@@ -335,6 +349,14 @@ import type { WindStreakOptions } from '../../windStreakRenderer.ts';
335
349
  import type { WindField } from '../../windField.ts';
336
350
  import type { FlockParams } from '../../flockRenderer.ts';
337
351
  import { InstancedBatch } from './instanced.ts';
352
+ import { GlBoneAnimation } from './boneAnimations.ts';
353
+ import { packBoneAnimation } from '../../boneAnimation.ts';
354
+ import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
355
+ import {
356
+ BONE_PLACES_TEXTURE_UNIT,
357
+ BONE_TURNS_TEXTURE_UNIT,
358
+ INSTANCE_CLOCKS_TEXTURE_UNIT,
359
+ } from '../../lightBudget.ts';
338
360
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
339
361
  import { cutoutOf } from '../../cutoutCaster.ts';
340
362
  import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
@@ -1033,6 +1055,37 @@ const IDENTITY_CLIP = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0
1033
1055
  /** Standing still, for a caller that asked for dust and not for wind. */
1034
1056
  const NO_DRIFT: Vec3 = [0, 0, 0];
1035
1057
 
1058
+ /** Whether two matrices hold the same sixteen numbers. */
1059
+ function sameMatrix(a: ArrayLike<number>, b: ArrayLike<number>): boolean {
1060
+ for (let k = 0; k < 16; k++) if (a[k] !== b[k]) return false;
1061
+ return true;
1062
+ }
1063
+
1064
+ /** How often `ready` asks the driver whether a program is done, and how many times before waiting. */
1065
+ const READY_POLL_MS = 4;
1066
+ const READY_MAX_POLLS = 500;
1067
+
1068
+ /** The halves a skin draws under the screen-space blur, each a program of its own. */
1069
+ const SPLIT_SKIN_HALVES = ['scene', 'diffuse', 'albedo'] as const;
1070
+
1071
+ /** A lit program with its uniform table, as every flat program is kept. */
1072
+ interface FlatProgram {
1073
+ program: WebGLProgram;
1074
+ uniforms: Record<string, WebGLUniformLocation>;
1075
+ }
1076
+
1077
+ /** The vertex variant a draw takes, and its program where it is not the plain one. */
1078
+ interface DrawVariant {
1079
+ /** What a model's program is keyed by: `plain`, `skinned8`, `cloth4m` and the rest. */
1080
+ name: string;
1081
+ skinned: boolean;
1082
+ morphed: boolean;
1083
+ cloth: { binding: GlClothBinding; particles: GlClothParticles } | null;
1084
+ clothed: FlatProgram | undefined;
1085
+ program: WebGLProgram | null;
1086
+ uniforms: Record<string, WebGLUniformLocation>;
1087
+ }
1088
+
1036
1089
  export class WebGL2Renderer implements RendererApi {
1037
1090
  /**
1038
1091
  * Private on purpose. The GL context never leaves this directory, so a
@@ -1331,6 +1384,23 @@ export class WebGL2Renderer implements RendererApi {
1331
1384
  string,
1332
1385
  { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> }
1333
1386
  >();
1387
+ /**
1388
+ * Model programs still compiling, where draws skip a compile (`pipelineCompile: 'skip'`) and the
1389
+ * context offers `KHR_parallel_shader_compile`; `parallelCompile` is its completion query then,
1390
+ * and null where draws wait for a program here instead. See `modelProgram`.
1391
+ */
1392
+ private readonly modelPending = new Map<string, StartedProgram>();
1393
+ /** What `drawVariantOf` answers, rewritten each draw rather than made. */
1394
+ private readonly drawVariant: DrawVariant = {
1395
+ name: 'plain',
1396
+ skinned: false,
1397
+ morphed: false,
1398
+ cloth: null,
1399
+ clothed: undefined,
1400
+ program: null,
1401
+ uniforms: {},
1402
+ };
1403
+ private readonly parallelCompile: { readonly COMPLETION_STATUS_KHR: number } | null;
1334
1404
  /** The last palette chosen, which an eye's axis is turned by. See `setSkinPalette`. */
1335
1405
  private paletteData: Float32Array | null = null;
1336
1406
 
@@ -1394,7 +1464,7 @@ export class WebGL2Renderer implements RendererApi {
1394
1464
  private oitReplaying = false;
1395
1465
  /** `uSurfaceScene`, resolved once a pass. See `surfaceScene.ts`. */
1396
1466
  private readonly surfaceScene = new Float32Array(4);
1397
- /** The two halves of `uWriteMode`, kept here because a `vec2` is written whole. */
1467
+ /** The three lanes of `uWriteMode`, kept here because a `vec3` is written whole. */
1398
1468
  private oitWeight = 0;
1399
1469
  /**
1400
1470
  * How the surfaces drawn next meet the medium: what `setSurfaceFog` set, and `FOG_RECEDE` from
@@ -1402,6 +1472,7 @@ export class WebGL2Renderer implements RendererApi {
1402
1472
  */
1403
1473
  private surfaceFog = FOG_RECEDE;
1404
1474
  private ditherFade = 0;
1475
+ private ditherOpacity = 0;
1405
1476
  /** Said once, not per frame, where the context cannot give the float target the sum needs. */
1406
1477
  private oitRefused = false;
1407
1478
  /** Said once for the whole renderer. See `refuseOitMultisampled`. */
@@ -1582,6 +1653,11 @@ export class WebGL2Renderer implements RendererApi {
1582
1653
  private readonly depthCutoutUniforms: Record<string, WebGLUniformLocation>;
1583
1654
  private readonly depthInstancedCutoutProgram: WebGLProgram;
1584
1655
  private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1656
+ /** A crowd's casters: the instanced depth programs that play a bone animation. */
1657
+ private readonly depthAnimatedProgram: WebGLProgram;
1658
+ private readonly depthAnimatedUniforms: Record<string, WebGLUniformLocation>;
1659
+ private readonly depthAnimatedCutoutProgram: WebGLProgram;
1660
+ private readonly depthAnimatedCutoutUniforms: Record<string, WebGLUniformLocation>;
1585
1661
  private readonly scatterDepthProgram: WebGLProgram;
1586
1662
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1587
1663
  /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
@@ -1676,6 +1752,28 @@ export class WebGL2Renderer implements RendererApi {
1676
1752
  private overlayTexture: WebGLTexture | null = null;
1677
1753
  /** Whether this device's lit stage has room for the overlay's vectors: asked once. */
1678
1754
  private overlayRoom: boolean | null = null;
1755
+ /**
1756
+ * The matrix the sun's dynamic shadow pass was last drawn with, uncorrected, and whether one has
1757
+ * been: what the moving layer is read through. See `movingLightFor`.
1758
+ */
1759
+ private readonly movingLightViewProj = new Float32Array(16);
1760
+ private movingLightKnown = false;
1761
+ /** Whether the lit stage has room for the moving layer's matrix, and whether its refusal was said. */
1762
+ private movingRoom: boolean | null = null;
1763
+ private warnedMovingRoom = false;
1764
+ /** The same answer for a material's projection, and whether its refusal was said. */
1765
+ private projectionRoom: boolean | null = null;
1766
+ private warnedProjectionRoom = false;
1767
+ /** `uWorldUv`, the current material's projection packed. See `surfaceProjection.ts`. */
1768
+ private readonly projection = new Float32Array(PROJECTION_FLOATS);
1769
+ /** The current material's `diffuseTransmission`, `uGlassTint.w`, 0 to 1. */
1770
+ private diffuseTransmission = 0;
1771
+ /** `uLayers`, the current material's layers packed. See `surfaceLayers.ts`. */
1772
+ private readonly layerFloats = new Float32Array(LAYER_FLOATS);
1773
+ /** Whether the lit stage has room for layers' two vectors, and which refusals were said. */
1774
+ private layersRoom: boolean | null = null;
1775
+ private warnedLayersRoom = false;
1776
+ private warnedLayersModelMap = false;
1679
1777
  private warnedOverlayRoom = false;
1680
1778
  private warnedOverlayBlocks = false;
1681
1779
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
@@ -2002,24 +2100,33 @@ export class WebGL2Renderer implements RendererApi {
2002
2100
  const gpuBatch = batch as InstancedBatch;
2003
2101
  const count = Math.min(data.count, gpuBatch.capacity);
2004
2102
  if (count === 0) return;
2103
+ const clip = gpuBatch.animation;
2104
+ if (clip !== null && clip.disposed) return;
2005
2105
  this.shadowDrawBudget.ask();
2006
2106
  const twoSided = material?.doubleSided === true;
2007
2107
  if (twoSided) gl.disable(gl.CULL_FACE);
2008
2108
  const cutout = cutoutOf(material);
2009
2109
  if (cutout !== null) {
2010
- gl.useProgram(this.depthInstancedCutoutProgram);
2011
- this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
2110
+ /* A crowd casts where its clip has moved it, through the animated variant. */
2111
+ const uniforms =
2112
+ clip === null ? this.depthInstancedCutoutUniforms : this.depthAnimatedCutoutUniforms;
2113
+ gl.useProgram(
2114
+ clip === null ? this.depthInstancedCutoutProgram : this.depthAnimatedCutoutProgram,
2115
+ );
2116
+ this.bindCutoutDepth(uniforms, cutout);
2117
+ if (clip !== null) this.bindAnimation(uniforms, gpuBatch);
2012
2118
  gpuBatch.draw(gl, count);
2013
2119
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
2014
2120
  gl.useProgram(this.depthProgram);
2015
2121
  this.restoreSurfaceTextureUnit();
2016
2122
  return;
2017
2123
  }
2018
- const u = this.depthInstancedUniforms;
2019
- gl.useProgram(this.depthInstancedProgram);
2124
+ const u = clip === null ? this.depthInstancedUniforms : this.depthAnimatedUniforms;
2125
+ gl.useProgram(clip === null ? this.depthInstancedProgram : this.depthAnimatedProgram);
2020
2126
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
2021
2127
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
2022
2128
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
2129
+ if (clip !== null) this.bindAnimation(u, gpuBatch);
2023
2130
  gpuBatch.draw(gl, count);
2024
2131
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
2025
2132
  gl.useProgram(this.depthProgram);
@@ -2994,6 +3101,16 @@ export class WebGL2Renderer implements RendererApi {
2994
3101
  * Measured on an RX 9070 XT through ANGLE/Vulkan: offered. `scripts/depth-survey.mjs` is what
2995
3102
  * asks, and it also reports what WebGPU can do.
2996
3103
  */
3104
+ /*
3105
+ * Where draws skip a compile, the query that says a program is done without waiting for it;
3106
+ * where the context has none, a program is waited for as it always was. See `modelProgram`.
3107
+ */
3108
+ this.parallelCompile =
3109
+ this.quality.pipelineCompile === 'skip'
3110
+ ? (gl.getExtension('KHR_parallel_shader_compile') as {
3111
+ readonly COMPLETION_STATUS_KHR: number;
3112
+ } | null)
3113
+ : null;
2997
3114
  this.clipControl = gl.getExtension('EXT_clip_control') as ClipControlExtension | null;
2998
3115
  if (REVERSED_DEPTH && this.clipControl !== null) {
2999
3116
  this.clipControl.clipControlEXT(
@@ -3182,10 +3299,33 @@ export class WebGL2Renderer implements RendererApi {
3182
3299
  this.depthInstancedCutoutProgram,
3183
3300
  'depth-instanced-cutout',
3184
3301
  );
3302
+ this.depthAnimatedProgram = compileProgram(
3303
+ gl,
3304
+ DEPTH_INSTANCED_ANIMATED_VERT,
3305
+ DEPTH_FRAG,
3306
+ 'depth-instanced-animated',
3307
+ );
3308
+ this.depthAnimatedUniforms = uniformLocations(
3309
+ gl,
3310
+ this.depthAnimatedProgram,
3311
+ 'depth-instanced-animated',
3312
+ );
3313
+ this.depthAnimatedCutoutProgram = compileProgram(
3314
+ gl,
3315
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
3316
+ DEPTH_CUTOUT_FRAG,
3317
+ 'depth-instanced-animated-cutout',
3318
+ );
3319
+ this.depthAnimatedCutoutUniforms = uniformLocations(
3320
+ gl,
3321
+ this.depthAnimatedCutoutProgram,
3322
+ 'depth-instanced-animated-cutout',
3323
+ );
3185
3324
  /* The cutout map is always read from the surface unit, which the draw borrows and gives back. */
3186
3325
  for (const [program, uniforms] of [
3187
3326
  [this.depthCutoutProgram, this.depthCutoutUniforms],
3188
3327
  [this.depthInstancedCutoutProgram, this.depthInstancedCutoutUniforms],
3328
+ [this.depthAnimatedCutoutProgram, this.depthAnimatedCutoutUniforms],
3189
3329
  ] as const) {
3190
3330
  gl.useProgram(program);
3191
3331
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
@@ -3308,7 +3448,36 @@ export class WebGL2Renderer implements RendererApi {
3308
3448
  * this before its first frame gets the right behaviour on both without asking which backend
3309
3449
  * it has, which is the contract `RendererApi` exists to keep.
3310
3450
  */
3311
- async ready(): Promise<void> {}
3451
+ /**
3452
+ * Resolve once every program a draw has started compiling has finished, adopting each: what a
3453
+ * loading screen awaits after drawing what its scene will use, where draws skip a compile. See
3454
+ * `RenderQuality.pipelineCompile`; nothing is started anywhere else here, so otherwise it is now.
3455
+ *
3456
+ * **It waits without holding the thread**, as the other backend's does: it asks the driver
3457
+ * whether each is done, which does not wait, and yields between asks. A driver that never says
3458
+ * done is asked the way that waits after `READY_MAX_POLLS`, so this cannot hang a loading screen.
3459
+ */
3460
+ async ready(): Promise<void> {
3461
+ for (let polls = 0; this.modelPending.size > 0 && !this.contextLost; polls++) {
3462
+ for (const [key, started] of this.modelPending) {
3463
+ const done =
3464
+ polls >= READY_MAX_POLLS ||
3465
+ this.parallelCompile === null ||
3466
+ this.gl.getProgramParameter(
3467
+ started.program,
3468
+ this.parallelCompile.COMPLETION_STATUS_KHR,
3469
+ ) === true;
3470
+ if (!done) continue;
3471
+ this.modelPending.delete(key);
3472
+ this.adoptModel(key, finishProgram(this.gl, started), started.label);
3473
+ }
3474
+ this.useFlatProgram();
3475
+ if (this.modelPending.size > 0) {
3476
+ // platform: browser default — a yield between two asks of the driver, in a browser module
3477
+ await new Promise<void>((resolve) => setTimeout(resolve, READY_POLL_MS));
3478
+ }
3479
+ }
3480
+ }
3312
3481
 
3313
3482
  dispose(options: { releaseContext?: boolean } = {}): void {
3314
3483
  if (this.disposed) return;
@@ -3379,6 +3548,8 @@ export class WebGL2Renderer implements RendererApi {
3379
3548
  this.lightVolumeProgram,
3380
3549
  this.depthCutoutProgram,
3381
3550
  this.depthInstancedCutoutProgram,
3551
+ this.depthAnimatedProgram,
3552
+ this.depthAnimatedCutoutProgram,
3382
3553
  ]) {
3383
3554
  gl.deleteProgram(program);
3384
3555
  }
@@ -3725,9 +3896,12 @@ export class WebGL2Renderer implements RendererApi {
3725
3896
  *
3726
3897
  * Not a hot path — it re-uploads the whole image and regenerates the mip chain.
3727
3898
  */
3728
- updateSurfaceTexture(texture: SurfaceTexture, source: TexImageSource): void {
3899
+ updateSurfaceTexture(
3900
+ texture: SurfaceTexture,
3901
+ source: TexImageSource | CompressedTextureSource,
3902
+ ): void {
3729
3903
  if (this.contextLost) return;
3730
- texture.update(this.gl, source);
3904
+ texture.update(this.gl, source, this.compressedFormats);
3731
3905
  /* New pixels are a new copy for the overlay's slot: see `SurfaceTexture.flatLayer`. */
3732
3906
  if (texture === this.overlayMaps) this.bindOverlayMaps(texture);
3733
3907
  }
@@ -3787,18 +3961,43 @@ export class WebGL2Renderer implements RendererApi {
3787
3961
  kind: SurfaceModelKind,
3788
3962
  /** Which half of a skin it draws under the screen-space blur. See `SkinHalf`. */
3789
3963
  half: SkinHalf = 'whole',
3790
- ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } {
3964
+ ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null {
3791
3965
  const key = `${variant}|${kind}|${half}`;
3792
3966
  const held = this.modelFlat.get(key);
3793
3967
  if (held !== undefined) return held;
3794
3968
  const { gl } = this;
3795
3969
  const label = `flat.${variant}.${kind}.${half}`;
3796
- const program = compileProgram(
3797
- gl,
3798
- modelVertexOf(variant, kind),
3799
- skinHalfBound(modelBound(this.flatFragSource, kind), half),
3800
- label,
3801
- );
3970
+ const vertex = modelVertexOf(variant, kind);
3971
+ const fragment = skinHalfBound(modelBound(this.flatFragSource, kind), half);
3972
+ /*
3973
+ * **Where draws skip a compile, none until the driver says asking will not wait**: the program
3974
+ * is started at the first draw that wants it and that draw, and every one before it is ready,
3975
+ * is left out. See `RenderQuality.pipelineCompile`.
3976
+ */
3977
+ if (this.parallelCompile !== null) {
3978
+ const started = this.modelPending.get(key);
3979
+ if (started === undefined) {
3980
+ this.modelPending.set(key, startProgram(gl, vertex, fragment, label));
3981
+ return null;
3982
+ }
3983
+ if (
3984
+ gl.getProgramParameter(started.program, this.parallelCompile.COMPLETION_STATUS_KHR) !== true
3985
+ ) {
3986
+ return null;
3987
+ }
3988
+ this.modelPending.delete(key);
3989
+ return this.adoptModel(key, finishProgram(gl, started), label);
3990
+ }
3991
+ return this.adoptModel(key, compileProgram(gl, vertex, fragment, label), label);
3992
+ }
3993
+
3994
+ /** A model program compiled: kept, written to by every material setter, and fed the pass. */
3995
+ private adoptModel(
3996
+ key: string,
3997
+ program: WebGLProgram,
3998
+ label: string,
3999
+ ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } {
4000
+ const { gl } = this;
3802
4001
  const made = { program, uniforms: uniformLocations(gl, program, label) };
3803
4002
  this.modelFlat.set(key, made);
3804
4003
  this.flatTargets.push(made);
@@ -3964,6 +4163,13 @@ export class WebGL2Renderer implements RendererApi {
3964
4163
  this.materialDoubleSided = material?.doubleSided === true;
3965
4164
  this.materialHasMaps = materialHasMaps(material);
3966
4165
  this.currentMaterial = material;
4166
+ const reflectivity = material?.reflectivity;
4167
+ const environmentGain = material?.environmentGain;
4168
+ this.materialReflectivity =
4169
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
4170
+ this.materialEnvironmentGain =
4171
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
4172
+ this.writeEnvironmentDials();
3967
4173
  /* What the blur spreads this skin by: the last material to name a profile says. */
3968
4174
  const model = material?.model;
3969
4175
  warnUnpagedLightmap(model, material?.modelMap);
@@ -4011,7 +4217,8 @@ export class WebGL2Renderer implements RendererApi {
4011
4217
  const v = this.cutoutVector;
4012
4218
  v[0] = material?.cutout ?? 0;
4013
4219
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
4014
- v[2] = this.cutoutFrame;
4220
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
4221
+ v[2] = 0;
4015
4222
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
4016
4223
  v[3] = lightChannelsOf(material?.lightChannels);
4017
4224
  return v;
@@ -4116,9 +4323,15 @@ export class WebGL2Renderer implements RendererApi {
4116
4323
  * and read neither — one decision for every program, as `setMaterial` says.
4117
4324
  */
4118
4325
  const modelMap = material?.modelMap ?? null;
4326
+ /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
4327
+ const layers = this.layersOf(material);
4328
+ const boundMap = modelMap ?? layers?.mask ?? null;
4119
4329
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
4120
4330
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
4121
- if (modelMap !== null) modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
4331
+ if (boundMap !== null) boundMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
4332
+ packSurfaceLayers(layers, this.layerFloats);
4333
+ if (layers !== null) this.askLit('LAYERED');
4334
+ gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
4122
4335
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
4123
4336
  packModel(
4124
4337
  material?.model ?? null,
@@ -4131,6 +4344,15 @@ export class WebGL2Renderer implements RendererApi {
4131
4344
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
4132
4345
  /* The draw's overlay, which a program born mid-pass must wear too: see `setSurfaceOverlay`. */
4133
4346
  gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
4347
+ gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
4348
+ /* How much light from behind the surface lets through, in the glass tint's spare lane. */
4349
+ this.diffuseTransmission = Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
4350
+ gl.uniform4f(u['uGlassTint'] ?? null, 1, 1, 1, this.diffuseTransmission);
4351
+ /* Maps placed by the world, where the material asks and the lit stage has room: worldUv.ts. */
4352
+ const projection = material?.projection ?? null;
4353
+ packSurfaceProjection(this.projectionFits(projection) ? projection : null, this.projection);
4354
+ if ((this.projection[0] as number) > 0) this.askLit('WORLD_UVS');
4355
+ gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
4134
4356
 
4135
4357
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
4136
4358
  return, so a material with no albedo still leaves a complete texture on the unit. */
@@ -4225,8 +4447,24 @@ export class WebGL2Renderer implements RendererApi {
4225
4447
  */
4226
4448
  setSurfaceReflectivity(amount: number): void {
4227
4449
  if (this.contextLost) return;
4450
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
4451
+ this.writeEnvironmentDials();
4452
+ }
4453
+
4454
+ /**
4455
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
4456
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
4457
+ * material's where it has one and the pass's otherwise.
4458
+ */
4459
+ private passReflectivity = 0;
4460
+ private passEnvironmentGain = 1;
4461
+ private materialReflectivity: number | null = null;
4462
+ private materialEnvironmentGain: number | null = null;
4463
+
4464
+ private writeEnvironmentDials(): void {
4228
4465
  this.materials.dirty();
4229
- this.environmentDials[1] = Math.min(1, Math.max(0, amount));
4466
+ this.environmentDials[1] = this.materialReflectivity ?? this.passReflectivity;
4467
+ this.environmentDials[2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
4230
4468
  this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4231
4469
  }
4232
4470
 
@@ -4289,17 +4527,110 @@ export class WebGL2Renderer implements RendererApi {
4289
4527
  /** What the lit shader at this renderer's budget leaves of the limit, with no overlay. */
4290
4528
  private overlayRoomLeft(): number {
4291
4529
  return (
4292
- this.fragmentVectorLimit() - countUniformVectors(this.flatFragAt(this.lightBudget, false))
4530
+ this.fragmentVectorLimit() -
4531
+ countUniformVectors(this.flatFragAt(this.lightBudget, { SURFACE_OVERLAY: false }))
4293
4532
  );
4294
4533
  }
4295
4534
 
4296
4535
  /** Whether the lit shader with the overlay compiled in fits this GPU: counted once. */
4297
4536
  private overlayFits(): boolean {
4298
- this.overlayRoom ??=
4299
- countUniformVectors(this.flatFragAt(this.lightBudget, true)) <= this.fragmentVectorLimit();
4537
+ this.overlayRoom ??= this.switchFits('SURFACE_OVERLAY');
4300
4538
  return this.overlayRoom;
4301
4539
  }
4302
4540
 
4541
+ /**
4542
+ * Whether a material's projection can be drawn: none asked, or the lit stage has the room for its
4543
+ * one vector. **Refused, said once, where it has not**, as the overlay is, and the material's
4544
+ * maps are then laid by the mesh's coordinates, as they were before it existed.
4545
+ */
4546
+ private projectionFits(projection: SurfaceProjection | null): boolean {
4547
+ if (!projects(projection)) return true;
4548
+ this.projectionRoom ??= this.switchFits('WORLD_UVS');
4549
+ if (!this.projectionRoom && !this.warnedProjectionRoom) {
4550
+ this.warnedProjectionRoom = true;
4551
+ console.warn(
4552
+ "Renderer: a material's projection is refused on this device. It takes one fragment " +
4553
+ 'uniform vector and the lit shader already uses all ' +
4554
+ `${this.fragmentVectorLimit()} this GPU offers; its maps follow the mesh's coordinates. ` +
4555
+ 'A lower `maxLights` makes the room, or WebGPU has it.',
4556
+ );
4557
+ }
4558
+ return this.projectionRoom;
4559
+ }
4560
+
4561
+ /**
4562
+ * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
4563
+ * where the lit stage carries one (`MOVING_SUN`); the environment's otherwise, which every caller
4564
+ * drawing both layers with one matrix hands it anyway.
4565
+ */
4566
+ private movingLightFor(env: Environment): ReadonlyMat4 {
4567
+ return this.movingLightKnown && this.litOn.MOVING_SUN
4568
+ ? this.movingLightViewProj
4569
+ : env.lightViewProj;
4570
+ }
4571
+
4572
+ /**
4573
+ * Turn the moving layer's own matrix on the first time a frame draws that layer with one the
4574
+ * environment's does not share. **Refused, said once, where the lit stage has no room for its five
4575
+ * vectors**: the layer is then read through the environment's matrix, so its shadows land where
4576
+ * that matrix puts them, and a caller there draws both layers with one.
4577
+ */
4578
+ private askMovingSun(env: Environment): void {
4579
+ if (this.litWanted.MOVING_SUN || !this.movingLightKnown) return;
4580
+ if (sameMatrix(this.movingLightViewProj, env.lightViewProj)) return;
4581
+ this.movingRoom ??= this.switchFits('MOVING_SUN');
4582
+ if (this.movingRoom) {
4583
+ this.askLit('MOVING_SUN');
4584
+ return;
4585
+ }
4586
+ if (this.warnedMovingRoom) return;
4587
+ this.warnedMovingRoom = true;
4588
+ console.warn(
4589
+ "Renderer: the sun's moving layer cannot be read through a matrix of its own on this " +
4590
+ 'device; it takes five fragment uniform vectors the lit shader does not have. It is read ' +
4591
+ "through the environment's `lightViewProj`, so draw the dynamic shadow pass with that " +
4592
+ 'matrix here, or lower `maxLights` to make the room. WebGPU has it.',
4593
+ );
4594
+ }
4595
+
4596
+ /**
4597
+ * A material's layers where they can be drawn: none asked, a model's map in the mask's place, or
4598
+ * no room for their two vectors each leave one layer, the last two said once each.
4599
+ */
4600
+ private layersOf(material: SurfaceMaterial | null): SurfaceLayers<SurfaceTexture> | null {
4601
+ const layers = material?.layers ?? null;
4602
+ if (layers === null || layers.repeats.length === 0) return null;
4603
+ if ((material?.modelMap ?? null) !== null) {
4604
+ if (!this.warnedLayersModelMap) {
4605
+ this.warnedLayersModelMap = true;
4606
+ console.warn(
4607
+ "Renderer: a material's layers are drawn as one: its mask is read where a model's map " +
4608
+ 'goes, and this material carries a `modelMap` too.',
4609
+ );
4610
+ }
4611
+ return null;
4612
+ }
4613
+ this.layersRoom ??= this.switchFits('LAYERED');
4614
+ if (this.layersRoom) return layers as SurfaceLayers<SurfaceTexture>;
4615
+ if (!this.warnedLayersRoom) {
4616
+ this.warnedLayersRoom = true;
4617
+ console.warn(
4618
+ "Renderer: a material's layers are refused on this device. They take two fragment uniform " +
4619
+ 'vectors the lit shader does not have; the material is drawn as its base layer. A lower ' +
4620
+ '`maxLights` makes the room, or WebGPU has it.',
4621
+ );
4622
+ }
4623
+ return null;
4624
+ }
4625
+
4626
+ /** Whether the lit shader with `feature` compiled in fits this GPU's fragment uniform vectors. */
4627
+ private switchFits(feature: LitSwitch): boolean {
4628
+ const asIf: Partial<Record<LitSwitch, boolean>> = { [feature]: true };
4629
+ return (
4630
+ countUniformVectors(this.flatFragAt(this.lightBudget, asIf)) <= this.fragmentVectorLimit()
4631
+ );
4632
+ }
4633
+
4303
4634
  /**
4304
4635
  * How bright the environment the surfaces drawn next reflect is. 1 is the default and identity.
4305
4636
  *
@@ -4314,9 +4645,8 @@ export class WebGL2Renderer implements RendererApi {
4314
4645
  */
4315
4646
  setEnvironmentGain(gain: number): void {
4316
4647
  if (this.contextLost) return;
4317
- this.materials.dirty();
4318
- this.environmentDials[2] = Math.max(0, gain);
4319
- this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4648
+ this.passEnvironmentGain = Math.max(0, gain);
4649
+ this.writeEnvironmentDials();
4320
4650
  }
4321
4651
 
4322
4652
  /**
@@ -4444,9 +4774,37 @@ export class WebGL2Renderer implements RendererApi {
4444
4774
  this.useFlatProgram();
4445
4775
  }
4446
4776
 
4447
- /** `uWriteMode`: the order-independent weight and the dither, which share one uniform row. */
4777
+ /**
4778
+ * Whether the draws that follow spend their own opacity on a screen door rather than on blending:
4779
+ * an instance's `MeshInstances.alphas`, or a vertex's channel alpha, keeps that share of the
4780
+ * pixels `setDitherFade` would and draws the rest not at all. So an opaque or cut-out batch fades
4781
+ * one instance at a time, with no sorting and no blend, and an instance at 1 is drawn whole.
4782
+ *
4783
+ * Off is what `bindMeshPass` restores. It shares the fade's pattern, so a crossfade at `t` and
4784
+ * `-t` of an instance at opacity `a` covers that instance's share once, without holes. Material
4785
+ * state like `setDitherFade`; instanced draws read it, and shadow casters do not.
4786
+ *
4787
+ * What it gives up: a fade reads as grain while it lasts, which a temporal resolve smooths and a
4788
+ * plain frame does not; a blended draw that asks spends its opacity here and blends none of it;
4789
+ * and a lightmapped batch carries regions where the opacities would be, so it has none to fade.
4790
+ */
4791
+ setDitherOpacity(on: boolean): void {
4792
+ if (this.contextLost) return;
4793
+ this.materials.dirty();
4794
+ this.ditherOpacity = on ? 1 : 0;
4795
+ for (const target of this.flatTargets) {
4796
+ this.gl.useProgram(target.program);
4797
+ this.writeMode(target.uniforms);
4798
+ }
4799
+ this.useFlatProgram();
4800
+ }
4801
+
4802
+ /**
4803
+ * `uWriteMode`: the order-independent weight, the fade and whether the opacity is dithered, which
4804
+ * share one uniform row.
4805
+ */
4448
4806
  private writeMode(u: Record<string, WebGLUniformLocation>): void {
4449
- this.gl.uniform2f(u['uWriteMode'] ?? null, this.oitWeight, this.ditherFade);
4807
+ this.gl.uniform3f(u['uWriteMode'] ?? null, this.oitWeight, this.ditherFade, this.ditherOpacity);
4450
4808
  }
4451
4809
 
4452
4810
  /**
@@ -4599,8 +4957,104 @@ export class WebGL2Renderer implements RendererApi {
4599
4957
  'locations the joint indices and weights occupy, and this mesh supplies them.',
4600
4958
  );
4601
4959
  }
4960
+ const animation = (options.animation ?? null) as GlBoneAnimation | null;
4961
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
4962
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
4963
+ if (animation !== null && !mesh.hasGrain) {
4964
+ throw new Error(
4965
+ 'WebGL2: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
4966
+ '(`MeshData.lightmapUvs`), and this mesh has none.',
4967
+ );
4968
+ }
4602
4969
  this.ensureInstancedProgram();
4603
- return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false);
4970
+ if (animation !== null) this.ensureAnimatedProgram();
4971
+ return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false, animation);
4972
+ }
4973
+
4974
+ /**
4975
+ * A bone animation an instanced batch can play (`InstancedOptions.animation`): per frame, a turn
4976
+ * and a place for each bone, which every vertex of the batch's mesh follows — the bone its second
4977
+ * coordinates' u names, times `boneScale` — at each instance's own moment of the clip. What a
4978
+ * crowd is made of: every instance moves at the frame's rate on its own clock, in the colour pass
4979
+ * and in every shadow, with nothing on the processor after this. See `boneAnimation.ts` for the
4980
+ * clip's layout and the arithmetic, which both backends run.
4981
+ *
4982
+ * What it gives up against skinning is the blend: a vertex follows one bone. Refused, with what
4983
+ * was wrong, for a clip whose numbers are not its size, or past 2,048 bones or frames.
4984
+ */
4985
+ createBoneAnimation(clip: BoneAnimationClip): BoneAnimationHandle {
4986
+ return new GlBoneAnimation(this.gl, packBoneAnimation(clip));
4987
+ }
4988
+
4989
+ /** Release a clip. A batch still playing it draws nothing, and casts nothing, after. */
4990
+ disposeBoneAnimation(animation: BoneAnimationHandle): void {
4991
+ const clip = animation as GlBoneAnimation;
4992
+ if (this.contextLost || clip.disposed) return;
4993
+ clip.dispose(this.gl);
4994
+ }
4995
+
4996
+ /**
4997
+ * The clock a bone animation plays by this frame, in seconds on the caller's clock: an instance's
4998
+ * moment is this times its rate plus its phase (`MeshInstances.clocks`). **Set it before the
4999
+ * shadows are drawn**, once a frame, as the wind is set: a shadow drawn under one clock and its
5000
+ * caster under another come apart at the frame's rate. 0 until set, which holds every crowd at
5001
+ * the moment its phases name.
5002
+ */
5003
+ setAnimationTime(seconds: number): void {
5004
+ if (Number.isFinite(seconds)) this.animationTime = seconds;
5005
+ }
5006
+
5007
+ /** The clock `setAnimationTime` set. */
5008
+ private animationTime = 0;
5009
+
5010
+ /**
5011
+ * An animated batch's clip, clocks and numbers into whichever program is bound: the three vertex
5012
+ * textures on units of their own (`lightBudget.ts`), the clip's speed and bone scale, the clock.
5013
+ */
5014
+ private bindAnimation(u: Record<string, WebGLUniformLocation>, batch: InstancedBatch): void {
5015
+ const clip = batch.animation;
5016
+ const clocks = batch.clocks;
5017
+ if (clip === null || clocks === null) return;
5018
+ const { gl } = this;
5019
+ gl.activeTexture(gl.TEXTURE0 + BONE_PLACES_TEXTURE_UNIT);
5020
+ gl.bindTexture(gl.TEXTURE_2D, clip.places);
5021
+ gl.activeTexture(gl.TEXTURE0 + BONE_TURNS_TEXTURE_UNIT);
5022
+ gl.bindTexture(gl.TEXTURE_2D, clip.turns);
5023
+ gl.activeTexture(gl.TEXTURE0 + INSTANCE_CLOCKS_TEXTURE_UNIT);
5024
+ gl.bindTexture(gl.TEXTURE_2D, clocks.texture);
5025
+ gl.uniform1i(u['uBonePlaces'] ?? null, BONE_PLACES_TEXTURE_UNIT);
5026
+ gl.uniform1i(u['uBoneTurns'] ?? null, BONE_TURNS_TEXTURE_UNIT);
5027
+ gl.uniform1i(u['uInstanceClocks'] ?? null, INSTANCE_CLOCKS_TEXTURE_UNIT);
5028
+ gl.uniform2f(u['uBoneClip'] ?? null, clip.framesPerSecond, clip.boneScale);
5029
+ gl.uniform1f(u['uSceneTime'] ?? null, this.animationTime);
5030
+ }
5031
+
5032
+ /** The animated instanced program, compiled on first use. See `ensureInstancedProgram`. */
5033
+ private flatAnimatedProgram: WebGLProgram | null = null;
5034
+ private flatAnimatedUniforms: Record<string, WebGLUniformLocation> | null = null;
5035
+
5036
+ /** Compile the animated variant, once, and feed it this frame's pass state. */
5037
+ private ensureAnimatedProgram(): void {
5038
+ if (this.contextLost || this.flatAnimatedProgram !== null) return;
5039
+ const { gl } = this;
5040
+ const program = compileProgram(
5041
+ gl,
5042
+ flatVert({ skinned: false, morphed: false, instanced: true, animated: true }),
5043
+ this.flatFragSource,
5044
+ 'flat.animated',
5045
+ );
5046
+ const uniforms = uniformLocations(gl, program, 'flat.animated');
5047
+ this.flatAnimatedProgram = program;
5048
+ this.flatAnimatedUniforms = uniforms;
5049
+ this.flatTargets.push({ program, uniforms });
5050
+ const camera = this.lastPassCamera;
5051
+ const env = this.lastPassEnv;
5052
+ if (camera !== null && env !== null) {
5053
+ gl.useProgram(program);
5054
+ this.writeMeshPassState(uniforms, camera, env);
5055
+ this.writeMaterialState(uniforms, this.currentMaterial);
5056
+ this.useFlatProgram();
5057
+ }
4604
5058
  }
4605
5059
 
4606
5060
  /** Push placement and colour. Only the live prefix. */
@@ -4663,18 +5117,31 @@ export class WebGL2Renderer implements RendererApi {
4663
5117
  order. See `InstancedOptions.cull`. */
4664
5118
  const culling = !blend && batch.cull;
4665
5119
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
5120
+ /* A batch playing a bone animation takes the animated variant; a disposed clip draws nothing. */
5121
+ const clip = batch.animation;
5122
+ if (clip !== null && clip.disposed) return;
4666
5123
  this.drawBudget.ask();
4667
5124
  this.ensureInstancedProgram();
5125
+ if (clip !== null) this.ensureAnimatedProgram();
4668
5126
  /* A material with a model takes that model's instanced program; see `modelProgram`. */
4669
5127
  const modelKind = this.currentMaterial?.model?.kind ?? null;
4670
- const modelled = modelKind === null ? null : this.modelProgram('instanced', modelKind);
4671
- const program = modelled?.program ?? this.flatInstancedProgram;
4672
- const u = modelled?.uniforms ?? this.flatInstancedUniforms;
5128
+ const modelled =
5129
+ modelKind === null
5130
+ ? null
5131
+ : this.modelProgram(clip === null ? 'instanced' : 'animated', modelKind);
5132
+ /* Its model's program still compiling, where draws skip a compile: left out until it lands. */
5133
+ if (modelKind !== null && modelled === null) return;
5134
+ const program =
5135
+ modelled?.program ?? (clip === null ? this.flatInstancedProgram : this.flatAnimatedProgram);
5136
+ const u =
5137
+ modelled?.uniforms ??
5138
+ (clip === null ? this.flatInstancedUniforms : this.flatAnimatedUniforms);
4673
5139
  if (program === null || u === null) return;
4674
5140
 
4675
5141
  const { gl } = this;
4676
5142
  gl.useProgram(program);
4677
5143
  gl.uniform1i(u['uHasTangents'] ?? null, mesh.hasTangents ? 1 : 0);
5144
+ if (clip !== null) this.bindAnimation(u, batch);
4678
5145
 
4679
5146
  const lit = options.lit ?? true;
4680
5147
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
@@ -4737,8 +5204,13 @@ export class WebGL2Renderer implements RendererApi {
4737
5204
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
4738
5205
  const keeps = !blend && this.reflectionKeeps();
4739
5206
  if (keeps) this.reflectionTargets?.on();
4740
- if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
4741
- else batch.draw(gl, count);
5207
+ /* An animated batch reads its clocks by instance index, which the culled copy would reorder:
5208
+ it is culled whole and never instance by instance. */
5209
+ if (culling && clip === null && cullsInstances(count, mesh.indexCount)) {
5210
+ batch.drawCulled(gl, data, this.frustum);
5211
+ } else {
5212
+ batch.draw(gl, count);
5213
+ }
4742
5214
  if (keeps) this.reflectionTargets?.off();
4743
5215
  if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4744
5216
  if (ditheredBlend)
@@ -4800,8 +5272,17 @@ export class WebGL2Renderer implements RendererApi {
4800
5272
  glass.transmission,
4801
5273
  glass.frost,
4802
5274
  );
4803
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
4804
- gl.uniform3fv(u['uGlassTint'] ?? null, glass.tint);
5275
+ /* w: this framebuffer's first row is the bottom, so up the screen is up the rows. */
5276
+ const tint = options.refractTint ?? WHITE_TINT;
5277
+ gl.uniform4f(u['uRefractTint'] ?? null, tint[0] ?? 1, tint[1] ?? 1, tint[2] ?? 1, 1);
5278
+ /* The pane's colour; w is the material's light through from behind, which glass never reads. */
5279
+ gl.uniform4f(
5280
+ u['uGlassTint'] ?? null,
5281
+ glass.tint[0] ?? 1,
5282
+ glass.tint[1] ?? 1,
5283
+ glass.tint[2] ?? 1,
5284
+ this.diffuseTransmission,
5285
+ );
4805
5286
  return true;
4806
5287
  }
4807
5288
 
@@ -5949,6 +6430,11 @@ export class WebGL2Renderer implements RendererApi {
5949
6430
  this.shadowPassActive = true;
5950
6431
  this.activeSunLayer = layer;
5951
6432
  this.glassCasters.clear();
6433
+ /* The moving layer is read through the matrix it is drawn with: see `movingLightFor`. */
6434
+ if (layer === 'dynamic') {
6435
+ this.movingLightViewProj.set(lightViewProj as Float32Array);
6436
+ this.movingLightKnown = true;
6437
+ }
5952
6438
  /* The static layer opens first, so a frame's peel counts from here. See `peelFilled`. */
5953
6439
  if (layer === 'static') this.peelFilled = false;
5954
6440
  /*
@@ -6111,6 +6597,71 @@ export class WebGL2Renderer implements RendererApi {
6111
6597
  casters(this.casterSink);
6112
6598
  }
6113
6599
 
6600
+ /**
6601
+ * Draws that do not change between frames, enumerated once and kept: what a stage, a set or any
6602
+ * world that stands still draws, recorded so a frame replays it rather than issuing it.
6603
+ *
6604
+ * **The same enumeration `drawSceneCasters` takes**, called once, here: rigid meshes and instanced
6605
+ * batches, each with its material. A skinned mesh or a scatter batch is refused by name, since
6606
+ * both change every frame. What it captures is what the enumeration said at the time — the
6607
+ * matrices and the instance counts are copied, so a caller's scratch is free to move on — while
6608
+ * the meshes, batches, textures and materials stay the caller's: a list must be disposed, or made
6609
+ * again, before anything it holds is, and made again when a material it holds is changed.
6610
+ *
6611
+ * **What it is for is the frame's processor time.** WebGPU records the list into a render bundle
6612
+ * per view and replays it with one call, where a draw issued a frame at a time costs its encoding
6613
+ * every frame and again for every view that draws it. This backend has no bundles and replays the
6614
+ * entries through the scene-caster path every frame, at the cost a frame drawing them itself pays;
6615
+ * the picture is the same.
6616
+ *
6617
+ * What a list gives up against drawing each frame is culling: every entry is drawn in every view,
6618
+ * because a bundle cannot leave one out. A world split into lists by region culls by list.
6619
+ */
6620
+ createStaticDraws(casters: ShadowCasters): StaticDrawsHandle {
6621
+ return captureStaticDraws(casters);
6622
+ }
6623
+
6624
+ /**
6625
+ * Draw a list into the open mesh pass, under the camera, the lights and the fog `bindMeshPass`
6626
+ * set — the frame's, a mirror's, a capture's. Opaque draws, through each entry's material.
6627
+ *
6628
+ * **Its draws are rigid and wear no overlay**, so a skin palette, morph weights, cloth or a surface
6629
+ * overlay set before it is put back to none, as `bindMeshPass` puts an overlay back; and no
6630
+ * material is left set after it, which is what `setMaterial(null)` leaves. The state a pass set —
6631
+ * the grain, the relief, the fog, the grade — is the list's to draw with, as it is any draw's, and
6632
+ * a list drawn under a different one is recorded again for it on WebGPU.
6633
+ */
6634
+ drawStaticDraws(draws: StaticDrawsHandle): void {
6635
+ if (this.contextLost) return;
6636
+ const list = draws as StaticDrawsList;
6637
+ if (list.disposed) {
6638
+ this.warnStaticDisposed();
6639
+ return;
6640
+ }
6641
+ this.setSkinPalette(null);
6642
+ this.setMorphWeights(null);
6643
+ this.setCloth(null);
6644
+ this.setSurfaceOverlay(null);
6645
+ this.setMaterial(null);
6646
+ this.sceneCasterMaterial = undefined;
6647
+ replayStaticDraws(list, this.sceneCasterSink);
6648
+ this.setMaterial(null);
6649
+ this.sceneCasterMaterial = undefined;
6650
+ }
6651
+
6652
+ /** Release a list. Its meshes, batches and materials are the caller's and are not released. */
6653
+ disposeStaticDraws(draws: StaticDrawsHandle): void {
6654
+ (draws as StaticDrawsList).disposed = true;
6655
+ }
6656
+
6657
+ /** A disposed list drawn, said once: the frame loop does not throw. */
6658
+ private warnStaticDisposed(): void {
6659
+ if (this.warnedStaticDisposed) return;
6660
+ this.warnedStaticDisposed = true;
6661
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
6662
+ }
6663
+ private warnedStaticDisposed = false;
6664
+
6114
6665
  /**
6115
6666
  * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
6116
6667
  *
@@ -6300,7 +6851,11 @@ export class WebGL2Renderer implements RendererApi {
6300
6851
  * throws a sentence with the device's numbers in it rather than a bare driver string.
6301
6852
  */
6302
6853
  /** The lit fragment source this renderer's profile compiles, at a light budget. */
6303
- private flatFragAt(budget: LightBudget, overlay = this.litOn.SURFACE_OVERLAY): string {
6854
+ private flatFragAt(
6855
+ budget: LightBudget,
6856
+ /* A switch counted as it would be, for asking whether it fits: see `switchFits`. */
6857
+ asIf: Partial<Record<LitSwitch, boolean>> = {},
6858
+ ): string {
6304
6859
  return flatFrag({
6305
6860
  pointShadows: this.quality.pointShadows,
6306
6861
  directionalShadows: this.quality.directionalShadows,
@@ -6313,8 +6868,11 @@ export class WebGL2Renderer implements RendererApi {
6313
6868
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
6314
6869
  driftLight: this.litOn.DRIFT_LIGHT,
6315
6870
  physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
6316
- surfaceOverlay: overlay,
6871
+ surfaceOverlay: asIf.SURFACE_OVERLAY ?? this.litOn.SURFACE_OVERLAY,
6317
6872
  reflectionMaps: this.litOn.REFLECTION_MAPS,
6873
+ worldUvs: asIf.WORLD_UVS ?? this.litOn.WORLD_UVS,
6874
+ movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
6875
+ layered: asIf.LAYERED ?? this.litOn.LAYERED,
6318
6876
  maxLights: budget.maxLights,
6319
6877
  maxAreaLights: budget.maxAreaLights,
6320
6878
  });
@@ -6339,9 +6897,10 @@ export class WebGL2Renderer implements RendererApi {
6339
6897
  instanced: boolean,
6340
6898
  label: string,
6341
6899
  eight = false,
6900
+ animated = false,
6342
6901
  ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
6343
6902
  if (old === null) return null;
6344
- const vertex = flatVert({ skinned, morphed, instanced });
6903
+ const vertex = flatVert({ skinned, morphed, instanced, animated });
6345
6904
  const program = compileProgram(
6346
6905
  gl,
6347
6906
  eight ? skinEight(vertex) : vertex,
@@ -6374,6 +6933,17 @@ export class WebGL2Renderer implements RendererApi {
6374
6933
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
6375
6934
  this.flatInstancedProgram = instanced?.program ?? null;
6376
6935
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
6936
+ const animated = recompile(
6937
+ this.flatAnimatedProgram,
6938
+ false,
6939
+ false,
6940
+ true,
6941
+ 'flat.animated',
6942
+ false,
6943
+ true,
6944
+ );
6945
+ this.flatAnimatedProgram = animated?.program ?? null;
6946
+ this.flatAnimatedUniforms = animated?.uniforms ?? null;
6377
6947
 
6378
6948
  for (const [key, entry] of this.clothFlat) {
6379
6949
  const eight = key.startsWith('8');
@@ -6390,6 +6960,9 @@ export class WebGL2Renderer implements RendererApi {
6390
6960
  this.clothFlat.set(key, { program, uniforms: uniformLocations(gl, program, label) });
6391
6961
  }
6392
6962
 
6963
+ /* Started against the source before this switch: begun again at the next draw that asks. */
6964
+ for (const started of this.modelPending.values()) gl.deleteProgram(started.program);
6965
+ this.modelPending.clear();
6393
6966
  for (const [key, entry] of this.modelFlat) {
6394
6967
  const [variant, kind, half] = key.split('|') as [string, SurfaceModelKind, SkinHalf];
6395
6968
  const label = `flat.${variant}.${kind}.${half}`;
@@ -6404,7 +6977,7 @@ export class WebGL2Renderer implements RendererApi {
6404
6977
  }
6405
6978
 
6406
6979
  this.flatTargets.length = 0;
6407
- for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
6980
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced, animated]) {
6408
6981
  if (target !== null) this.flatTargets.push(target);
6409
6982
  }
6410
6983
  for (const target of this.clothFlat.values()) this.flatTargets.push(target);
@@ -8178,6 +8751,7 @@ export class WebGL2Renderer implements RendererApi {
8178
8751
  ambient: env?.ambient ?? [0, 0, 0],
8179
8752
  sunShadow,
8180
8753
  lightViewProj: env?.lightViewProj ?? IDENTITY_MAT4,
8754
+ movingLightViewProj: env === null ? IDENTITY_MAT4 : this.movingLightFor(env),
8181
8755
  /* The placeholder rather than null where a map is merely absent, for the reason
8182
8756
  `emptyTexture.ts` gives. It is black, which as a depth is an occluder at the light,
8183
8757
  so it is safe only because `sunShadow` is 0 whenever these maps do not exist. */
@@ -8433,15 +9007,31 @@ export class WebGL2Renderer implements RendererApi {
8433
9007
  this.currentSurfaceTexture = null;
8434
9008
  /* Nor an overlay: the atlas's unit is given the empty texture with the refraction copy's. */
8435
9009
  this.overlayFloats.fill(0);
9010
+ /* Nor a projection: maps follow the mesh's coordinates until a material asks otherwise. */
9011
+ this.projection.fill(0);
9012
+ /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
9013
+ this.diffuseTransmission = 0;
9014
+ /* Nor layers: one layer until a material asks for more. */
9015
+ this.layerFloats.fill(0);
8436
9016
  this.overlayMaps = null;
8437
9017
  this.overlayTexture = null;
8438
9018
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
8439
9019
  this.ambientSH.fill(0);
8440
9020
  /* No reflection and an environment at its own brightness; the emission the world asks for. */
9021
+ this.passReflectivity = 0;
9022
+ this.passEnvironmentGain = 1;
9023
+ this.materialReflectivity = null;
9024
+ this.materialEnvironmentGain = null;
8441
9025
  this.environmentDials[1] = 0;
8442
9026
  this.environmentDials[2] = 1;
8443
9027
  this.emission[0] = env.emissiveGain;
8444
9028
  this.emission[1] = env.nightFactor;
9029
+ /* The moving layer's own matrix, the first time a frame draws that layer with one. */
9030
+ this.askMovingSun(env);
9031
+ /* No dither and no weighting: every draw whole, until a crossfade or an opacity asks. */
9032
+ this.oitWeight = 0;
9033
+ this.ditherFade = 0;
9034
+ this.ditherOpacity = 0;
8445
9035
  for (const target of this.flatTargets) {
8446
9036
  this.gl.useProgram(target.program);
8447
9037
  this.writeMeshPassState(target.uniforms, camera, env);
@@ -8595,11 +9185,14 @@ export class WebGL2Renderer implements RendererApi {
8595
9185
  gl.uniform1i(u['uRefractScene'] ?? null, REFRACT_SCENE_TEXTURE_UNIT);
8596
9186
  /* The draws' overlay, none at a pass's start, and its atlas above: see `setSurfaceOverlay`. */
8597
9187
  gl.uniform4fv(u['uOverlay'] ?? null, this.overlayFloats);
9188
+ gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
9189
+ gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
8598
9190
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
8599
9191
  `bindSeeThrough` for the four numbers in the one vector. */
8600
9192
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
8601
- gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
8602
- gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
9193
+ gl.uniform4f(u['uRefractTint'] ?? null, 1, 1, 1, 1);
9194
+ /* White, and no light through from behind: no material at a pass's start. */
9195
+ gl.uniform4f(u['uGlassTint'] ?? null, 1, 1, 1, this.diffuseTransmission);
8603
9196
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
8604
9197
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
8605
9198
  gl.uniform3fv(u['uAmbient'] ?? null, env.ambient);
@@ -8666,10 +9259,9 @@ export class WebGL2Renderer implements RendererApi {
8666
9259
  this.surfaceFog = FOG_RECEDE;
8667
9260
  /* Grain on by default, so every scene written before it was a choice looks unchanged. */
8668
9261
  gl.uniform1f(u['uGrain'] ?? null, 1);
8669
- /* No dither and no weighting: every draw whole, until a crossfade asks. See `setDitherFade`. */
8670
- this.oitWeight = 0;
8671
- this.ditherFade = 0;
8672
- gl.uniform2f(u['uWriteMode'] ?? null, 0, 0);
9262
+ /* The weighting and the dither as they stand, which `bindMeshPass` has just cleared: a program
9263
+ built in the middle of a pass takes what the caller set. See `setDitherFade`. */
9264
+ this.writeMode(u);
8673
9265
  /* The draw's own ambient as it stands, which `bindMeshPass` has just cleared: a program built
8674
9266
  in the middle of a pass takes what the caller set. See `setAmbientSH`. */
8675
9267
  gl.uniform4fv(u['uAmbientSH[0]'] ?? null, this.ambientSH);
@@ -8717,11 +9309,24 @@ export class WebGL2Renderer implements RendererApi {
8717
9309
  gl.uniform1f(u['uShadowStrength'] ?? null, this.sunShadows === null ? 0 : env.shadowStrength);
8718
9310
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
8719
9311
  gl.uniform1f(u['uShadowDepthSpan'] ?? null, env.shadowDepthSpan);
9312
+ /* The moving layer's own matrix and span, where the program reads one: see `movingLightFor`. */
9313
+ const moving = this.movingLightFor(env);
9314
+ gl.uniformMatrix4fv(u['uMovingLightViewProj'] ?? null, false, moving);
9315
+ gl.uniform1f(u['uMovingShadowDepthSpan'] ?? null, orthographicDepthSpan(moving));
8720
9316
  gl.uniform1f(u['uShadowMaxDistance'] ?? null, this.quality.directionalShadowMaxDistance);
8721
9317
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
8722
9318
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
8723
9319
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
8724
- gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
9320
+ /* The near corner, and in its w the frame the cutout dither moves with. See `uHighlightMin`.
9321
+ An environment written by hand may leave the box out, as the far corner says below. */
9322
+ const min = env.highlightMin as ArrayLike<number> | undefined;
9323
+ gl.uniform4f(
9324
+ u['uHighlightMin'] ?? null,
9325
+ min?.[0] ?? 0,
9326
+ min?.[1] ?? 0,
9327
+ min?.[2] ?? 0,
9328
+ this.cutoutFrame,
9329
+ );
8725
9330
  /* The far corner, and the gain in its w. See `uHighlightMax`. */
8726
9331
  /* An environment written by hand may leave the box out; then there is none, as before. */
8727
9332
  const max = env.highlightMax ?? NO_HIGHLIGHT;
@@ -9045,6 +9650,8 @@ export class WebGL2Renderer implements RendererApi {
9045
9650
  * light and its colour, each into the blur's own target.
9046
9651
  */
9047
9652
  const splits = this.skinSplits();
9653
+ /* A split skin draws its three halves or none, where a half may still be compiling. */
9654
+ if (splits && this.parallelCompile !== null && !this.skinHalvesReady(mesh)) return;
9048
9655
  /* An opaque lit draw into the frame writes its material's reflection too: see `reflectionKeeps`. */
9049
9656
  const keeps = this.reflectionKeeps();
9050
9657
  if (keeps) this.reflectionTargets?.on();
@@ -9125,14 +9732,25 @@ export class WebGL2Renderer implements RendererApi {
9125
9732
  }
9126
9733
 
9127
9734
  /** One draw of a mesh, as `drawMesh` describes, of the half of a skin `half` names. */
9128
- private drawMeshHalf(
9129
- mesh: Mesh,
9130
- model: ReadonlyMat4,
9131
- depthLayer: number,
9132
- tint: Vec3 | null,
9133
- half: SkinHalf,
9134
- ): void {
9135
- const { gl } = this;
9735
+ /**
9736
+ * Whether every half a split skin draws has its program, starting any that has not: a skin drawn
9737
+ * without its kept halves would lose its diffuse light for the frames they take to compile.
9738
+ */
9739
+ private skinHalvesReady(mesh: Mesh): boolean {
9740
+ const variant = this.drawVariantOf(mesh).name;
9741
+ let ready = true;
9742
+ for (const half of SPLIT_SKIN_HALVES) {
9743
+ if (this.modelProgram(variant, 'skin', half) === null) ready = false;
9744
+ }
9745
+ return ready;
9746
+ }
9747
+
9748
+ /**
9749
+ * The vertex variant a draw of `mesh` takes now, and its program and uniforms where it is not the
9750
+ * plain one: what a model's program is keyed by, and what `drawMeshHalf` draws with.
9751
+ */
9752
+ private drawVariantOf(mesh: Mesh): DrawVariant {
9753
+ const out = this.drawVariant;
9136
9754
  /*
9137
9755
  * A rigged mesh takes the skinned program, an unrigged one the plain program.
9138
9756
  *
@@ -9160,10 +9778,8 @@ export class WebGL2Renderer implements RendererApi {
9160
9778
  * when none were asked for — an unbound sampler would default to a shadow map's unit.
9161
9779
  */
9162
9780
  const cloth = wantSkin ? this.cloth : null;
9163
- const clothed =
9164
- cloth === null
9165
- ? undefined
9166
- : this.clothFlat.get(`${eight ? 8 : 4}|${mesh.morph !== null ? 'm' : '-'}`);
9781
+ const clothKey = `${eight ? 8 : 4}|${mesh.morph !== null ? 'm' : '-'}`;
9782
+ const clothed = cloth === null ? undefined : this.clothFlat.get(clothKey);
9167
9783
  const bothUniforms = eight ? this.flatBoth8Uniforms : this.flatBothUniforms;
9168
9784
  const skinnedUniforms = eight ? this.flatSkinned8Uniforms : this.flatSkinnedUniforms;
9169
9785
  const both = wantSkin && wantMorph && bothUniforms !== null;
@@ -9177,8 +9793,12 @@ export class WebGL2Renderer implements RendererApi {
9177
9793
  : morphed
9178
9794
  ? this.flatMorphedUniforms
9179
9795
  : null;
9180
- let u = clothed?.uniforms ?? variant ?? this.flatUniforms;
9181
- let program =
9796
+ out.skinned = skinned;
9797
+ out.morphed = morphed;
9798
+ out.cloth = cloth;
9799
+ out.clothed = clothed;
9800
+ out.uniforms = clothed?.uniforms ?? variant ?? this.flatUniforms;
9801
+ out.program =
9182
9802
  clothed !== undefined
9183
9803
  ? clothed.program
9184
9804
  : skinned && morphed
@@ -9192,26 +9812,41 @@ export class WebGL2Renderer implements RendererApi {
9192
9812
  : morphed
9193
9813
  ? this.flatMorphedProgram
9194
9814
  : null;
9815
+ out.name =
9816
+ clothed !== undefined
9817
+ ? `cloth${eight ? 8 : 4}${mesh.morph !== null ? 'm' : '-'}`
9818
+ : skinned && morphed
9819
+ ? eight
9820
+ ? 'both8'
9821
+ : 'both'
9822
+ : skinned
9823
+ ? eight
9824
+ ? 'skinned8'
9825
+ : 'skinned'
9826
+ : morphed
9827
+ ? 'morphed'
9828
+ : 'plain';
9829
+ return out;
9830
+ }
9831
+
9832
+ private drawMeshHalf(
9833
+ mesh: Mesh,
9834
+ model: ReadonlyMat4,
9835
+ depthLayer: number,
9836
+ tint: Vec3 | null,
9837
+ half: SkinHalf,
9838
+ ): void {
9839
+ const { gl } = this;
9840
+ const drawn = this.drawVariantOf(mesh);
9841
+ const { skinned, morphed, cloth, clothed } = drawn;
9842
+ let u = drawn.uniforms;
9843
+ let program = drawn.program;
9195
9844
  /* A material with a model takes that model's program for the same vertex variant. */
9196
9845
  const modelKind = this.currentMaterial?.model?.kind ?? null;
9197
9846
  if (modelKind !== null) {
9198
- const modelled = this.modelProgram(
9199
- clothed !== undefined
9200
- ? `cloth${eight ? 8 : 4}${mesh.morph !== null ? 'm' : '-'}`
9201
- : skinned && morphed
9202
- ? eight
9203
- ? 'both8'
9204
- : 'both'
9205
- : skinned
9206
- ? eight
9207
- ? 'skinned8'
9208
- : 'skinned'
9209
- : morphed
9210
- ? 'morphed'
9211
- : 'plain',
9212
- modelKind,
9213
- half,
9214
- );
9847
+ const modelled = this.modelProgram(drawn.name, modelKind, half);
9848
+ /* Still compiling, where draws skip a compile: left out until it lands. */
9849
+ if (modelled === null) return;
9215
9850
  program = modelled.program;
9216
9851
  u = modelled.uniforms;
9217
9852
  }
@@ -9379,6 +10014,8 @@ export class WebGL2Renderer implements RendererApi {
9379
10014
  /* A material with a model takes that model's plain program; see `modelProgram`. */
9380
10015
  const modelKind = this.currentMaterial?.model?.kind ?? null;
9381
10016
  const modelled = modelKind === null ? null : this.modelProgram('plain', modelKind);
10017
+ /* Still compiling, where draws skip a compile: left out until it lands. */
10018
+ if (modelKind !== null && modelled === null) return;
9382
10019
  const u = modelled?.uniforms ?? this.flatUniforms;
9383
10020
  if (modelled === null) this.useFlatProgram();
9384
10021
  else gl.useProgram(modelled.program);
@@ -9910,6 +10547,7 @@ export class WebGL2Renderer implements RendererApi {
9910
10547
  /* Uncorrected, for the reason the lit program's copy of this line carries: the shaft's
9911
10548
  lookup does its own `* 0.5 + 0.5` and correcting the matrix as well doubles it. */
9912
10549
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, env.lightViewProj);
10550
+ gl.uniformMatrix4fv(u['uMovingLightViewProj'] ?? null, false, this.movingLightFor(env));
9913
10551
  gl.uniform1f(u['uShadowMapSize'] ?? null, this.quality.directionalShadowMapSize);
9914
10552
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
9915
10553
 
@@ -10183,6 +10821,12 @@ function litVertexOf(variant: string): string {
10183
10821
  }
10184
10822
  const skinned = variant.startsWith('skinned') || variant.startsWith('both');
10185
10823
  const morphed = variant === 'morphed' || variant.startsWith('both');
10186
- const vertex = flatVert({ skinned, morphed, instanced: variant === 'instanced' });
10824
+ const animated = variant === 'animated';
10825
+ const vertex = flatVert({
10826
+ skinned,
10827
+ morphed,
10828
+ instanced: variant === 'instanced' || animated,
10829
+ animated,
10830
+ });
10187
10831
  return variant.endsWith('8') ? skinEight(vertex) : vertex;
10188
10832
  }