@driftengine/core 4.9.0 → 4.10.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 (135) hide show
  1. package/CHANGELOG.json +62 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +4 -3
  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 +90 -2
  16. package/dist/render/backend/webgl2/renderer.js +253 -24
  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 +19 -4
  22. package/dist/render/backend/webgpu/buffers.js +18 -11
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  24. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  25. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  26. package/dist/render/backend/webgpu/depthPass.js +49 -14
  27. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  28. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  29. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  30. package/dist/render/backend/webgpu/flatPass.js +136 -20
  31. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  32. package/dist/render/backend/webgpu/instanced.js +24 -1
  33. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  34. package/dist/render/backend/webgpu/materialStore.js +265 -0
  35. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  36. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  37. package/dist/render/backend/webgpu/renderer.d.ts +152 -35
  38. package/dist/render/backend/webgpu/renderer.js +787 -142
  39. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  40. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  41. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  42. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  43. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  44. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  47. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  48. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  49. package/dist/render/bloomChain.d.ts +3 -3
  50. package/dist/render/boneAnimation.d.ts +92 -0
  51. package/dist/render/boneAnimation.js +133 -0
  52. package/dist/render/compressedSource.d.ts +33 -13
  53. package/dist/render/compressedSource.js +70 -25
  54. package/dist/render/glCompressed.js +83 -0
  55. package/dist/render/instances.d.ts +26 -0
  56. package/dist/render/lightBudget.d.ts +10 -0
  57. package/dist/render/lightBudget.js +10 -0
  58. package/dist/render/lightmap.d.ts +1 -1
  59. package/dist/render/mesh.d.ts +5 -0
  60. package/dist/render/mesh.js +6 -0
  61. package/dist/render/pointLightSelection.d.ts +4 -4
  62. package/dist/render/screenSpaceReflection.d.ts +2 -1
  63. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  64. package/dist/render/shaders/boneAnimation.js +63 -0
  65. package/dist/render/shaders/depth.d.ts +7 -0
  66. package/dist/render/shaders/depth.js +35 -10
  67. package/dist/render/shaders/flat/index.d.ts +6 -0
  68. package/dist/render/shaders/flat/index.js +27 -5
  69. package/dist/render/shaders/flat/main.d.ts +1 -1
  70. package/dist/render/shaders/flat/main.js +15 -5
  71. package/dist/render/shaders/flat/models.js +1 -1
  72. package/dist/render/shaders/flat/overlay.js +1 -1
  73. package/dist/render/shaders/flat/pointShadow.js +2 -2
  74. package/dist/render/shaders/flat/preamble.js +38 -24
  75. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  76. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  77. package/dist/render/shaders/generated/flat.wgsl.d.ts +3077 -2903
  78. package/dist/render/shaders/generated/flat.wgsl.js +3100 -2925
  79. package/dist/render/shaders/outputTransform.d.ts +7 -0
  80. package/dist/render/shaders/outputTransform.js +28 -14
  81. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  82. package/dist/render/shaders/vertexChannel.js +26 -0
  83. package/dist/render/staticDraws.d.ts +47 -0
  84. package/dist/render/staticDraws.js +43 -0
  85. package/dist/render/surfaceTexture.d.ts +38 -7
  86. package/dist/render/surfaceTexture.js +56 -17
  87. package/package.json +6 -6
  88. package/src/index.ts +8 -2
  89. package/src/render/ambientHarmonics.ts +1 -1
  90. package/src/render/areaLights.ts +4 -4
  91. package/src/render/backend/api.ts +13 -3
  92. package/src/render/backend/select.ts +10 -5
  93. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  94. package/src/render/backend/webgl2/instanced.ts +20 -1
  95. package/src/render/backend/webgl2/renderer.ts +315 -24
  96. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  97. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  98. package/src/render/backend/webgpu/buffers.ts +42 -16
  99. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  100. package/src/render/backend/webgpu/depthPass.ts +51 -10
  101. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  102. package/src/render/backend/webgpu/flatPass.ts +187 -14
  103. package/src/render/backend/webgpu/instanced.ts +32 -1
  104. package/src/render/backend/webgpu/materialStore.ts +276 -0
  105. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  106. package/src/render/backend/webgpu/renderer.ts +928 -155
  107. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  108. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  109. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  110. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  111. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  112. package/src/render/bloomChain.ts +3 -3
  113. package/src/render/boneAnimation.ts +223 -0
  114. package/src/render/compressedSource.ts +121 -29
  115. package/src/render/glCompressed.ts +83 -0
  116. package/src/render/instances.ts +27 -0
  117. package/src/render/lightBudget.ts +11 -0
  118. package/src/render/lightmap.ts +1 -1
  119. package/src/render/mesh.ts +6 -0
  120. package/src/render/pointLightSelection.ts +4 -4
  121. package/src/render/screenSpaceReflection.ts +2 -1
  122. package/src/render/shaders/boneAnimation.ts +63 -0
  123. package/src/render/shaders/depth.ts +45 -10
  124. package/src/render/shaders/flat/index.ts +33 -5
  125. package/src/render/shaders/flat/main.ts +15 -5
  126. package/src/render/shaders/flat/models.ts +1 -1
  127. package/src/render/shaders/flat/overlay.ts +1 -1
  128. package/src/render/shaders/flat/pointShadow.ts +2 -2
  129. package/src/render/shaders/flat/preamble.ts +38 -24
  130. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  131. package/src/render/shaders/generated/flat.wgsl.ts +3100 -2925
  132. package/src/render/shaders/outputTransform.ts +30 -14
  133. package/src/render/shaders/vertexChannel.ts +27 -0
  134. package/src/render/staticDraws.ts +95 -0
  135. package/src/render/surfaceTexture.ts +94 -27
@@ -218,6 +218,8 @@ import {
218
218
  DEPTH_CUTOUT_FRAG,
219
219
  DEPTH_CUTOUT_VERT,
220
220
  DEPTH_FRAG,
221
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
222
+ DEPTH_INSTANCED_ANIMATED_VERT,
221
223
  DEPTH_INSTANCED_CUTOUT_VERT,
222
224
  DEPTH_INSTANCED_VERT,
223
225
  DEPTH_SKINNED_VERT,
@@ -232,6 +234,8 @@ import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
232
234
  import { SunGlassTint } from '../../sunGlassTint.ts';
233
235
  import { GlassTintPrograms, type GlassTintProgram } from './glassTintPrograms.ts';
234
236
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
237
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.ts';
238
+ import type { StaticDrawsHandle, StaticDrawsList } from '../../staticDraws.ts';
235
239
  import { FACE_COUNT, PointShadowMap, type PointGlassDraw } from '../../pointShadowMap.ts';
236
240
  import { createResolvedPointShadows, DEFAULT_SOURCE_RADIUS } from '../../pointShadowImage.ts';
237
241
  import {
@@ -277,7 +281,11 @@ import { SurfaceTexture } from '../../surfaceTexture.ts';
277
281
  import { sceneCaptureTexels } from '../../sceneCapture.ts';
278
282
  import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
279
283
  import { glCompressedFormats } from '../../glCompressed.ts';
280
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
284
+ import type {
285
+ CompressedTextureFormat,
286
+ CompressedTextureSource,
287
+ SurfaceSource,
288
+ } from '../../compressedSource.ts';
281
289
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
282
290
  import { PlumeRenderer } from '../../plumeRenderer.ts';
283
291
  import { createScatterDeform, resolveScatterDeform } from '../../scatterDeform.ts';
@@ -335,6 +343,14 @@ import type { WindStreakOptions } from '../../windStreakRenderer.ts';
335
343
  import type { WindField } from '../../windField.ts';
336
344
  import type { FlockParams } from '../../flockRenderer.ts';
337
345
  import { InstancedBatch } from './instanced.ts';
346
+ import { GlBoneAnimation } from './boneAnimations.ts';
347
+ import { packBoneAnimation } from '../../boneAnimation.ts';
348
+ import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
349
+ import {
350
+ BONE_PLACES_TEXTURE_UNIT,
351
+ BONE_TURNS_TEXTURE_UNIT,
352
+ INSTANCE_CLOCKS_TEXTURE_UNIT,
353
+ } from '../../lightBudget.ts';
338
354
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
339
355
  import { cutoutOf } from '../../cutoutCaster.ts';
340
356
  import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
@@ -1582,6 +1598,11 @@ export class WebGL2Renderer implements RendererApi {
1582
1598
  private readonly depthCutoutUniforms: Record<string, WebGLUniformLocation>;
1583
1599
  private readonly depthInstancedCutoutProgram: WebGLProgram;
1584
1600
  private readonly depthInstancedCutoutUniforms: Record<string, WebGLUniformLocation>;
1601
+ /** A crowd's casters: the instanced depth programs that play a bone animation. */
1602
+ private readonly depthAnimatedProgram: WebGLProgram;
1603
+ private readonly depthAnimatedUniforms: Record<string, WebGLUniformLocation>;
1604
+ private readonly depthAnimatedCutoutProgram: WebGLProgram;
1605
+ private readonly depthAnimatedCutoutUniforms: Record<string, WebGLUniformLocation>;
1585
1606
  private readonly scatterDepthProgram: WebGLProgram;
1586
1607
  private readonly scatterDepthUniforms: Record<string, WebGLUniformLocation>;
1587
1608
  /** The sun's static, moving and peeled maps, as layers of one array. See `shadowMap.ts`. */
@@ -2002,24 +2023,33 @@ export class WebGL2Renderer implements RendererApi {
2002
2023
  const gpuBatch = batch as InstancedBatch;
2003
2024
  const count = Math.min(data.count, gpuBatch.capacity);
2004
2025
  if (count === 0) return;
2026
+ const clip = gpuBatch.animation;
2027
+ if (clip !== null && clip.disposed) return;
2005
2028
  this.shadowDrawBudget.ask();
2006
2029
  const twoSided = material?.doubleSided === true;
2007
2030
  if (twoSided) gl.disable(gl.CULL_FACE);
2008
2031
  const cutout = cutoutOf(material);
2009
2032
  if (cutout !== null) {
2010
- gl.useProgram(this.depthInstancedCutoutProgram);
2011
- this.bindCutoutDepth(this.depthInstancedCutoutUniforms, cutout);
2033
+ /* A crowd casts where its clip has moved it, through the animated variant. */
2034
+ const uniforms =
2035
+ clip === null ? this.depthInstancedCutoutUniforms : this.depthAnimatedCutoutUniforms;
2036
+ gl.useProgram(
2037
+ clip === null ? this.depthInstancedCutoutProgram : this.depthAnimatedCutoutProgram,
2038
+ );
2039
+ this.bindCutoutDepth(uniforms, cutout);
2040
+ if (clip !== null) this.bindAnimation(uniforms, gpuBatch);
2012
2041
  gpuBatch.draw(gl, count);
2013
2042
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
2014
2043
  gl.useProgram(this.depthProgram);
2015
2044
  this.restoreSurfaceTextureUnit();
2016
2045
  return;
2017
2046
  }
2018
- const u = this.depthInstancedUniforms;
2019
- gl.useProgram(this.depthInstancedProgram);
2047
+ const u = clip === null ? this.depthInstancedUniforms : this.depthAnimatedUniforms;
2048
+ gl.useProgram(clip === null ? this.depthInstancedProgram : this.depthAnimatedProgram);
2020
2049
  gl.uniformMatrix4fv(u['uLightViewProj'] ?? null, false, this.activeDepthViewProj);
2021
2050
  gl.uniform1i(u['uPeelShadowLayer'] ?? null, this.activeDepthPeel ? 1 : 0);
2022
2051
  if (this.activeDepthPeel) gl.uniform1i(u['uPreviousShadowMap'] ?? null, 0);
2052
+ if (clip !== null) this.bindAnimation(u, gpuBatch);
2023
2053
  gpuBatch.draw(gl, count);
2024
2054
  if (twoSided && this.depthPassCullsFaces) gl.enable(gl.CULL_FACE);
2025
2055
  gl.useProgram(this.depthProgram);
@@ -3182,10 +3212,33 @@ export class WebGL2Renderer implements RendererApi {
3182
3212
  this.depthInstancedCutoutProgram,
3183
3213
  'depth-instanced-cutout',
3184
3214
  );
3215
+ this.depthAnimatedProgram = compileProgram(
3216
+ gl,
3217
+ DEPTH_INSTANCED_ANIMATED_VERT,
3218
+ DEPTH_FRAG,
3219
+ 'depth-instanced-animated',
3220
+ );
3221
+ this.depthAnimatedUniforms = uniformLocations(
3222
+ gl,
3223
+ this.depthAnimatedProgram,
3224
+ 'depth-instanced-animated',
3225
+ );
3226
+ this.depthAnimatedCutoutProgram = compileProgram(
3227
+ gl,
3228
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT,
3229
+ DEPTH_CUTOUT_FRAG,
3230
+ 'depth-instanced-animated-cutout',
3231
+ );
3232
+ this.depthAnimatedCutoutUniforms = uniformLocations(
3233
+ gl,
3234
+ this.depthAnimatedCutoutProgram,
3235
+ 'depth-instanced-animated-cutout',
3236
+ );
3185
3237
  /* The cutout map is always read from the surface unit, which the draw borrows and gives back. */
3186
3238
  for (const [program, uniforms] of [
3187
3239
  [this.depthCutoutProgram, this.depthCutoutUniforms],
3188
3240
  [this.depthInstancedCutoutProgram, this.depthInstancedCutoutUniforms],
3241
+ [this.depthAnimatedCutoutProgram, this.depthAnimatedCutoutUniforms],
3189
3242
  ] as const) {
3190
3243
  gl.useProgram(program);
3191
3244
  gl.uniform1i(uniforms['uCutoutMap'] ?? null, SURFACE_TEXTURE_UNIT);
@@ -3379,6 +3432,8 @@ export class WebGL2Renderer implements RendererApi {
3379
3432
  this.lightVolumeProgram,
3380
3433
  this.depthCutoutProgram,
3381
3434
  this.depthInstancedCutoutProgram,
3435
+ this.depthAnimatedProgram,
3436
+ this.depthAnimatedCutoutProgram,
3382
3437
  ]) {
3383
3438
  gl.deleteProgram(program);
3384
3439
  }
@@ -3725,9 +3780,12 @@ export class WebGL2Renderer implements RendererApi {
3725
3780
  *
3726
3781
  * Not a hot path — it re-uploads the whole image and regenerates the mip chain.
3727
3782
  */
3728
- updateSurfaceTexture(texture: SurfaceTexture, source: TexImageSource): void {
3783
+ updateSurfaceTexture(
3784
+ texture: SurfaceTexture,
3785
+ source: TexImageSource | CompressedTextureSource,
3786
+ ): void {
3729
3787
  if (this.contextLost) return;
3730
- texture.update(this.gl, source);
3788
+ texture.update(this.gl, source, this.compressedFormats);
3731
3789
  /* New pixels are a new copy for the overlay's slot: see `SurfaceTexture.flatLayer`. */
3732
3790
  if (texture === this.overlayMaps) this.bindOverlayMaps(texture);
3733
3791
  }
@@ -3964,6 +4022,13 @@ export class WebGL2Renderer implements RendererApi {
3964
4022
  this.materialDoubleSided = material?.doubleSided === true;
3965
4023
  this.materialHasMaps = materialHasMaps(material);
3966
4024
  this.currentMaterial = material;
4025
+ const reflectivity = material?.reflectivity;
4026
+ const environmentGain = material?.environmentGain;
4027
+ this.materialReflectivity =
4028
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
4029
+ this.materialEnvironmentGain =
4030
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
4031
+ this.writeEnvironmentDials();
3967
4032
  /* What the blur spreads this skin by: the last material to name a profile says. */
3968
4033
  const model = material?.model;
3969
4034
  warnUnpagedLightmap(model, material?.modelMap);
@@ -4011,7 +4076,8 @@ export class WebGL2Renderer implements RendererApi {
4011
4076
  const v = this.cutoutVector;
4012
4077
  v[0] = material?.cutout ?? 0;
4013
4078
  v[1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveNow(material, translucent)];
4014
- v[2] = this.cutoutFrame;
4079
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
4080
+ v[2] = 0;
4015
4081
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
4016
4082
  v[3] = lightChannelsOf(material?.lightChannels);
4017
4083
  return v;
@@ -4225,8 +4291,24 @@ export class WebGL2Renderer implements RendererApi {
4225
4291
  */
4226
4292
  setSurfaceReflectivity(amount: number): void {
4227
4293
  if (this.contextLost) return;
4294
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
4295
+ this.writeEnvironmentDials();
4296
+ }
4297
+
4298
+ /**
4299
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
4300
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
4301
+ * material's where it has one and the pass's otherwise.
4302
+ */
4303
+ private passReflectivity = 0;
4304
+ private passEnvironmentGain = 1;
4305
+ private materialReflectivity: number | null = null;
4306
+ private materialEnvironmentGain: number | null = null;
4307
+
4308
+ private writeEnvironmentDials(): void {
4228
4309
  this.materials.dirty();
4229
- this.environmentDials[1] = Math.min(1, Math.max(0, amount));
4310
+ this.environmentDials[1] = this.materialReflectivity ?? this.passReflectivity;
4311
+ this.environmentDials[2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
4230
4312
  this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4231
4313
  }
4232
4314
 
@@ -4314,9 +4396,8 @@ export class WebGL2Renderer implements RendererApi {
4314
4396
  */
4315
4397
  setEnvironmentGain(gain: number): void {
4316
4398
  if (this.contextLost) return;
4317
- this.materials.dirty();
4318
- this.environmentDials[2] = Math.max(0, gain);
4319
- this.writeFlatVector('uEnvironmentDials', this.environmentDials);
4399
+ this.passEnvironmentGain = Math.max(0, gain);
4400
+ this.writeEnvironmentDials();
4320
4401
  }
4321
4402
 
4322
4403
  /**
@@ -4599,8 +4680,104 @@ export class WebGL2Renderer implements RendererApi {
4599
4680
  'locations the joint indices and weights occupy, and this mesh supplies them.',
4600
4681
  );
4601
4682
  }
4683
+ const animation = (options.animation ?? null) as GlBoneAnimation | null;
4684
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
4685
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
4686
+ if (animation !== null && !mesh.hasGrain) {
4687
+ throw new Error(
4688
+ 'WebGL2: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
4689
+ '(`MeshData.lightmapUvs`), and this mesh has none.',
4690
+ );
4691
+ }
4602
4692
  this.ensureInstancedProgram();
4603
- return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false);
4693
+ if (animation !== null) this.ensureAnimatedProgram();
4694
+ return new InstancedBatch(this.gl, mesh, capacity, options.cull ?? false, animation);
4695
+ }
4696
+
4697
+ /**
4698
+ * A bone animation an instanced batch can play (`InstancedOptions.animation`): per frame, a turn
4699
+ * and a place for each bone, which every vertex of the batch's mesh follows — the bone its second
4700
+ * coordinates' u names, times `boneScale` — at each instance's own moment of the clip. What a
4701
+ * crowd is made of: every instance moves at the frame's rate on its own clock, in the colour pass
4702
+ * and in every shadow, with nothing on the processor after this. See `boneAnimation.ts` for the
4703
+ * clip's layout and the arithmetic, which both backends run.
4704
+ *
4705
+ * What it gives up against skinning is the blend: a vertex follows one bone. Refused, with what
4706
+ * was wrong, for a clip whose numbers are not its size, or past 2,048 bones or frames.
4707
+ */
4708
+ createBoneAnimation(clip: BoneAnimationClip): BoneAnimationHandle {
4709
+ return new GlBoneAnimation(this.gl, packBoneAnimation(clip));
4710
+ }
4711
+
4712
+ /** Release a clip. A batch still playing it draws nothing, and casts nothing, after. */
4713
+ disposeBoneAnimation(animation: BoneAnimationHandle): void {
4714
+ const clip = animation as GlBoneAnimation;
4715
+ if (this.contextLost || clip.disposed) return;
4716
+ clip.dispose(this.gl);
4717
+ }
4718
+
4719
+ /**
4720
+ * The clock a bone animation plays by this frame, in seconds on the caller's clock: an instance's
4721
+ * moment is this times its rate plus its phase (`MeshInstances.clocks`). **Set it before the
4722
+ * shadows are drawn**, once a frame, as the wind is set: a shadow drawn under one clock and its
4723
+ * caster under another come apart at the frame's rate. 0 until set, which holds every crowd at
4724
+ * the moment its phases name.
4725
+ */
4726
+ setAnimationTime(seconds: number): void {
4727
+ if (Number.isFinite(seconds)) this.animationTime = seconds;
4728
+ }
4729
+
4730
+ /** The clock `setAnimationTime` set. */
4731
+ private animationTime = 0;
4732
+
4733
+ /**
4734
+ * An animated batch's clip, clocks and numbers into whichever program is bound: the three vertex
4735
+ * textures on units of their own (`lightBudget.ts`), the clip's speed and bone scale, the clock.
4736
+ */
4737
+ private bindAnimation(u: Record<string, WebGLUniformLocation>, batch: InstancedBatch): void {
4738
+ const clip = batch.animation;
4739
+ const clocks = batch.clocks;
4740
+ if (clip === null || clocks === null) return;
4741
+ const { gl } = this;
4742
+ gl.activeTexture(gl.TEXTURE0 + BONE_PLACES_TEXTURE_UNIT);
4743
+ gl.bindTexture(gl.TEXTURE_2D, clip.places);
4744
+ gl.activeTexture(gl.TEXTURE0 + BONE_TURNS_TEXTURE_UNIT);
4745
+ gl.bindTexture(gl.TEXTURE_2D, clip.turns);
4746
+ gl.activeTexture(gl.TEXTURE0 + INSTANCE_CLOCKS_TEXTURE_UNIT);
4747
+ gl.bindTexture(gl.TEXTURE_2D, clocks.texture);
4748
+ gl.uniform1i(u['uBonePlaces'] ?? null, BONE_PLACES_TEXTURE_UNIT);
4749
+ gl.uniform1i(u['uBoneTurns'] ?? null, BONE_TURNS_TEXTURE_UNIT);
4750
+ gl.uniform1i(u['uInstanceClocks'] ?? null, INSTANCE_CLOCKS_TEXTURE_UNIT);
4751
+ gl.uniform2f(u['uBoneClip'] ?? null, clip.framesPerSecond, clip.boneScale);
4752
+ gl.uniform1f(u['uSceneTime'] ?? null, this.animationTime);
4753
+ }
4754
+
4755
+ /** The animated instanced program, compiled on first use. See `ensureInstancedProgram`. */
4756
+ private flatAnimatedProgram: WebGLProgram | null = null;
4757
+ private flatAnimatedUniforms: Record<string, WebGLUniformLocation> | null = null;
4758
+
4759
+ /** Compile the animated variant, once, and feed it this frame's pass state. */
4760
+ private ensureAnimatedProgram(): void {
4761
+ if (this.contextLost || this.flatAnimatedProgram !== null) return;
4762
+ const { gl } = this;
4763
+ const program = compileProgram(
4764
+ gl,
4765
+ flatVert({ skinned: false, morphed: false, instanced: true, animated: true }),
4766
+ this.flatFragSource,
4767
+ 'flat.animated',
4768
+ );
4769
+ const uniforms = uniformLocations(gl, program, 'flat.animated');
4770
+ this.flatAnimatedProgram = program;
4771
+ this.flatAnimatedUniforms = uniforms;
4772
+ this.flatTargets.push({ program, uniforms });
4773
+ const camera = this.lastPassCamera;
4774
+ const env = this.lastPassEnv;
4775
+ if (camera !== null && env !== null) {
4776
+ gl.useProgram(program);
4777
+ this.writeMeshPassState(uniforms, camera, env);
4778
+ this.writeMaterialState(uniforms, this.currentMaterial);
4779
+ this.useFlatProgram();
4780
+ }
4604
4781
  }
4605
4782
 
4606
4783
  /** Push placement and colour. Only the live prefix. */
@@ -4663,18 +4840,29 @@ export class WebGL2Renderer implements RendererApi {
4663
4840
  order. See `InstancedOptions.cull`. */
4664
4841
  const culling = !blend && batch.cull;
4665
4842
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
4843
+ /* A batch playing a bone animation takes the animated variant; a disposed clip draws nothing. */
4844
+ const clip = batch.animation;
4845
+ if (clip !== null && clip.disposed) return;
4666
4846
  this.drawBudget.ask();
4667
4847
  this.ensureInstancedProgram();
4848
+ if (clip !== null) this.ensureAnimatedProgram();
4668
4849
  /* A material with a model takes that model's instanced program; see `modelProgram`. */
4669
4850
  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;
4851
+ const modelled =
4852
+ modelKind === null
4853
+ ? null
4854
+ : this.modelProgram(clip === null ? 'instanced' : 'animated', modelKind);
4855
+ const program =
4856
+ modelled?.program ?? (clip === null ? this.flatInstancedProgram : this.flatAnimatedProgram);
4857
+ const u =
4858
+ modelled?.uniforms ??
4859
+ (clip === null ? this.flatInstancedUniforms : this.flatAnimatedUniforms);
4673
4860
  if (program === null || u === null) return;
4674
4861
 
4675
4862
  const { gl } = this;
4676
4863
  gl.useProgram(program);
4677
4864
  gl.uniform1i(u['uHasTangents'] ?? null, mesh.hasTangents ? 1 : 0);
4865
+ if (clip !== null) this.bindAnimation(u, batch);
4678
4866
 
4679
4867
  const lit = options.lit ?? true;
4680
4868
  /* Added light fades in the medium rather than receding into it. See `drawFog.ts`. */
@@ -4737,8 +4925,13 @@ export class WebGL2Renderer implements RendererApi {
4737
4925
  gl.uniform4fv(u['uCutout'] ?? null, this.cutoutUniform(this.currentMaterial, true));
4738
4926
  const keeps = !blend && this.reflectionKeeps();
4739
4927
  if (keeps) this.reflectionTargets?.on();
4740
- if (culling && cullsInstances(count, mesh.indexCount)) batch.drawCulled(gl, data, this.frustum);
4741
- else batch.draw(gl, count);
4928
+ /* An animated batch reads its clocks by instance index, which the culled copy would reorder:
4929
+ it is culled whole and never instance by instance. */
4930
+ if (culling && clip === null && cullsInstances(count, mesh.indexCount)) {
4931
+ batch.drawCulled(gl, data, this.frustum);
4932
+ } else {
4933
+ batch.draw(gl, count);
4934
+ }
4742
4935
  if (keeps) this.reflectionTargets?.off();
4743
4936
  if (coverage) gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
4744
4937
  if (ditheredBlend)
@@ -4800,7 +4993,9 @@ export class WebGL2Renderer implements RendererApi {
4800
4993
  glass.transmission,
4801
4994
  glass.frost,
4802
4995
  );
4803
- gl.uniform3fv(u['uRefractTint'] ?? null, options.refractTint ?? WHITE_TINT);
4996
+ /* w: this framebuffer's first row is the bottom, so up the screen is up the rows. */
4997
+ const tint = options.refractTint ?? WHITE_TINT;
4998
+ gl.uniform4f(u['uRefractTint'] ?? null, tint[0] ?? 1, tint[1] ?? 1, tint[2] ?? 1, 1);
4804
4999
  gl.uniform3fv(u['uGlassTint'] ?? null, glass.tint);
4805
5000
  return true;
4806
5001
  }
@@ -6111,6 +6306,71 @@ export class WebGL2Renderer implements RendererApi {
6111
6306
  casters(this.casterSink);
6112
6307
  }
6113
6308
 
6309
+ /**
6310
+ * Draws that do not change between frames, enumerated once and kept: what a stage, a set or any
6311
+ * world that stands still draws, recorded so a frame replays it rather than issuing it.
6312
+ *
6313
+ * **The same enumeration `drawSceneCasters` takes**, called once, here: rigid meshes and instanced
6314
+ * batches, each with its material. A skinned mesh or a scatter batch is refused by name, since
6315
+ * both change every frame. What it captures is what the enumeration said at the time — the
6316
+ * matrices and the instance counts are copied, so a caller's scratch is free to move on — while
6317
+ * the meshes, batches, textures and materials stay the caller's: a list must be disposed, or made
6318
+ * again, before anything it holds is, and made again when a material it holds is changed.
6319
+ *
6320
+ * **What it is for is the frame's processor time.** WebGPU records the list into a render bundle
6321
+ * per view and replays it with one call, where a draw issued a frame at a time costs its encoding
6322
+ * every frame and again for every view that draws it. This backend has no bundles and replays the
6323
+ * entries through the scene-caster path every frame, at the cost a frame drawing them itself pays;
6324
+ * the picture is the same.
6325
+ *
6326
+ * What a list gives up against drawing each frame is culling: every entry is drawn in every view,
6327
+ * because a bundle cannot leave one out. A world split into lists by region culls by list.
6328
+ */
6329
+ createStaticDraws(casters: ShadowCasters): StaticDrawsHandle {
6330
+ return captureStaticDraws(casters);
6331
+ }
6332
+
6333
+ /**
6334
+ * Draw a list into the open mesh pass, under the camera, the lights and the fog `bindMeshPass`
6335
+ * set — the frame's, a mirror's, a capture's. Opaque draws, through each entry's material.
6336
+ *
6337
+ * **Its draws are rigid and wear no overlay**, so a skin palette, morph weights, cloth or a surface
6338
+ * overlay set before it is put back to none, as `bindMeshPass` puts an overlay back; and no
6339
+ * material is left set after it, which is what `setMaterial(null)` leaves. The state a pass set —
6340
+ * the grain, the relief, the fog, the grade — is the list's to draw with, as it is any draw's, and
6341
+ * a list drawn under a different one is recorded again for it on WebGPU.
6342
+ */
6343
+ drawStaticDraws(draws: StaticDrawsHandle): void {
6344
+ if (this.contextLost) return;
6345
+ const list = draws as StaticDrawsList;
6346
+ if (list.disposed) {
6347
+ this.warnStaticDisposed();
6348
+ return;
6349
+ }
6350
+ this.setSkinPalette(null);
6351
+ this.setMorphWeights(null);
6352
+ this.setCloth(null);
6353
+ this.setSurfaceOverlay(null);
6354
+ this.setMaterial(null);
6355
+ this.sceneCasterMaterial = undefined;
6356
+ replayStaticDraws(list, this.sceneCasterSink);
6357
+ this.setMaterial(null);
6358
+ this.sceneCasterMaterial = undefined;
6359
+ }
6360
+
6361
+ /** Release a list. Its meshes, batches and materials are the caller's and are not released. */
6362
+ disposeStaticDraws(draws: StaticDrawsHandle): void {
6363
+ (draws as StaticDrawsList).disposed = true;
6364
+ }
6365
+
6366
+ /** A disposed list drawn, said once: the frame loop does not throw. */
6367
+ private warnStaticDisposed(): void {
6368
+ if (this.warnedStaticDisposed) return;
6369
+ this.warnedStaticDisposed = true;
6370
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
6371
+ }
6372
+ private warnedStaticDisposed = false;
6373
+
6114
6374
  /**
6115
6375
  * Draw the pass's glass twice: where the nearest pane is, and what the panes let through.
6116
6376
  *
@@ -6339,9 +6599,10 @@ export class WebGL2Renderer implements RendererApi {
6339
6599
  instanced: boolean,
6340
6600
  label: string,
6341
6601
  eight = false,
6602
+ animated = false,
6342
6603
  ): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
6343
6604
  if (old === null) return null;
6344
- const vertex = flatVert({ skinned, morphed, instanced });
6605
+ const vertex = flatVert({ skinned, morphed, instanced, animated });
6345
6606
  const program = compileProgram(
6346
6607
  gl,
6347
6608
  eight ? skinEight(vertex) : vertex,
@@ -6374,6 +6635,17 @@ export class WebGL2Renderer implements RendererApi {
6374
6635
  const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
6375
6636
  this.flatInstancedProgram = instanced?.program ?? null;
6376
6637
  this.flatInstancedUniforms = instanced?.uniforms ?? null;
6638
+ const animated = recompile(
6639
+ this.flatAnimatedProgram,
6640
+ false,
6641
+ false,
6642
+ true,
6643
+ 'flat.animated',
6644
+ false,
6645
+ true,
6646
+ );
6647
+ this.flatAnimatedProgram = animated?.program ?? null;
6648
+ this.flatAnimatedUniforms = animated?.uniforms ?? null;
6377
6649
 
6378
6650
  for (const [key, entry] of this.clothFlat) {
6379
6651
  const eight = key.startsWith('8');
@@ -6404,7 +6676,7 @@ export class WebGL2Renderer implements RendererApi {
6404
6676
  }
6405
6677
 
6406
6678
  this.flatTargets.length = 0;
6407
- for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced]) {
6679
+ for (const target of [plain, skinned, morphed, both, skinned8, both8, instanced, animated]) {
6408
6680
  if (target !== null) this.flatTargets.push(target);
6409
6681
  }
6410
6682
  for (const target of this.clothFlat.values()) this.flatTargets.push(target);
@@ -8438,6 +8710,10 @@ export class WebGL2Renderer implements RendererApi {
8438
8710
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
8439
8711
  this.ambientSH.fill(0);
8440
8712
  /* No reflection and an environment at its own brightness; the emission the world asks for. */
8713
+ this.passReflectivity = 0;
8714
+ this.passEnvironmentGain = 1;
8715
+ this.materialReflectivity = null;
8716
+ this.materialEnvironmentGain = null;
8441
8717
  this.environmentDials[1] = 0;
8442
8718
  this.environmentDials[2] = 1;
8443
8719
  this.emission[0] = env.emissiveGain;
@@ -8598,7 +8874,7 @@ export class WebGL2Renderer implements RendererApi {
8598
8874
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
8599
8875
  `bindSeeThrough` for the four numbers in the one vector. */
8600
8876
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
8601
- gl.uniform3fv(u['uRefractTint'] ?? null, WHITE_TINT);
8877
+ gl.uniform4f(u['uRefractTint'] ?? null, 1, 1, 1, 1);
8602
8878
  gl.uniform3fv(u['uGlassTint'] ?? null, WHITE_TINT);
8603
8879
  gl.uniform3fv(u['uDirectionalDir'] ?? null, env.directionalDir);
8604
8880
  gl.uniform3fv(u['uDirectionalColor'] ?? null, env.directionalColor);
@@ -8721,7 +8997,16 @@ export class WebGL2Renderer implements RendererApi {
8721
8997
  gl.uniform1i(u['uPeeledShadowEnabled'] ?? null, this.peelFilled ? 1 : 0);
8722
8998
  gl.uniform1f(u['uShadowMaxSlope'] ?? null, this.quality.directionalShadowMaxSlope);
8723
8999
  gl.uniform1i(u['uShadowFilterTaps'] ?? null, this.quality.shadowFilterTaps);
8724
- gl.uniform3fv(u['uHighlightMin'] ?? null, env.highlightMin);
9000
+ /* The near corner, and in its w the frame the cutout dither moves with. See `uHighlightMin`.
9001
+ An environment written by hand may leave the box out, as the far corner says below. */
9002
+ const min = env.highlightMin as ArrayLike<number> | undefined;
9003
+ gl.uniform4f(
9004
+ u['uHighlightMin'] ?? null,
9005
+ min?.[0] ?? 0,
9006
+ min?.[1] ?? 0,
9007
+ min?.[2] ?? 0,
9008
+ this.cutoutFrame,
9009
+ );
8725
9010
  /* The far corner, and the gain in its w. See `uHighlightMax`. */
8726
9011
  /* An environment written by hand may leave the box out; then there is none, as before. */
8727
9012
  const max = env.highlightMax ?? NO_HIGHLIGHT;
@@ -10183,6 +10468,12 @@ function litVertexOf(variant: string): string {
10183
10468
  }
10184
10469
  const skinned = variant.startsWith('skinned') || variant.startsWith('both');
10185
10470
  const morphed = variant === 'morphed' || variant.startsWith('both');
10186
- const vertex = flatVert({ skinned, morphed, instanced: variant === 'instanced' });
10471
+ const animated = variant === 'animated';
10472
+ const vertex = flatVert({
10473
+ skinned,
10474
+ morphed,
10475
+ instanced: variant === 'instanced' || animated,
10476
+ animated,
10477
+ });
10187
10478
  return variant.endsWith('8') ? skinEight(vertex) : vertex;
10188
10479
  }
@@ -22,16 +22,18 @@ export class SkinPaletteTexture {
22
22
  /**
23
23
  * Upload a palette, reallocating only when the joint count changes.
24
24
  *
25
- * A rig's joint count is fixed for its life, so the reallocation path runs once per skeleton and
26
- * the steady state is one `texSubImage2D`. Reallocating per frame would be a GPU allocation in
27
- * the frame loop, which is the same class of mistake as allocating on the CPU there.
25
+ * **The texture grows and never shrinks**, so the steady state is one `texSubImage2D` whatever
26
+ * mix of rigs a frame draws. It was rebuilt whenever the joint count changed, which with one
27
+ * texture for every skinned draw meant a character and a prop drawn in turn reallocated it at
28
+ * every draw: a GPU allocation in the frame loop. The shader reads joints by index, so a texture
29
+ * wider than the rig is read exactly as one that fits.
28
30
  */
29
31
  update(gl: WebGL2RenderingContext, palette: Float32Array): void {
30
32
  validateSkinPalette(palette);
31
33
  const joints = palette.length / 16;
32
34
  const width = paletteTextureWidth(joints);
33
35
 
34
- if (this.texture === null || this.joints !== joints) {
36
+ if (this.texture === null || joints > this.joints) {
35
37
  if (this.texture !== null) gl.deleteTexture(this.texture);
36
38
  const created = gl.createTexture();
37
39
  if (created === null) throw new Error('skin palette: failed to create the palette texture');
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A bone animation's textures on this backend, and a batch's clocks: what
3
+ * `shaders/boneAnimation.ts` reads by index, made once and never filtered.
4
+ *
5
+ * **`rgba32float` places and turns, and `rg32float` clocks**, all read with `textureLoad` and bound
6
+ * `unfilterable-float`, as the joint palette is: a turn in half floats moves a crowd's hands by a
7
+ * thousandth of their reach, a jitter at the frame's rate, and the bytes are a clip's, once.
8
+ */
9
+ import {
10
+ CLOCK_TEXTURE_WIDTH,
11
+ type BoneAnimationHandle,
12
+ type BoneAnimationTexels,
13
+ } from '../../boneAnimation.ts';
14
+
15
+ const USAGE_TEXTURE = 0x4 | 0x2; // TEXTURE_BINDING | COPY_DST
16
+
17
+ /** A clip on the device, from `createBoneAnimation`. */
18
+ export class GpuBoneAnimation implements BoneAnimationHandle {
19
+ readonly bones: number;
20
+ readonly frames: number;
21
+ readonly framesPerSecond: number;
22
+ readonly boneScale: number;
23
+ readonly placesView: GPUTextureView;
24
+ readonly turnsView: GPUTextureView;
25
+ disposed = false;
26
+ private readonly places: GPUTexture;
27
+ private readonly turns: GPUTexture;
28
+
29
+ constructor(device: GPUDevice, texels: BoneAnimationTexels) {
30
+ this.bones = texels.bones;
31
+ this.frames = texels.frames;
32
+ this.framesPerSecond = texels.framesPerSecond;
33
+ this.boneScale = texels.boneScale;
34
+ const make = (label: string, data: Float32Array): GPUTexture => {
35
+ const texture = device.createTexture({
36
+ label,
37
+ size: [texels.bones, texels.frames],
38
+ format: 'rgba32float',
39
+ usage: USAGE_TEXTURE,
40
+ });
41
+ device.queue.writeTexture(
42
+ { texture },
43
+ data,
44
+ { bytesPerRow: texels.bones * 16, rowsPerImage: texels.frames },
45
+ [texels.bones, texels.frames],
46
+ );
47
+ return texture;
48
+ };
49
+ this.places = make('bones.places', texels.places);
50
+ this.turns = make('bones.turns', texels.turns);
51
+ this.placesView = this.places.createView();
52
+ this.turnsView = this.turns.createView();
53
+ }
54
+
55
+ dispose(): void {
56
+ this.disposed = true;
57
+ this.places.destroy();
58
+ this.turns.destroy();
59
+ }
60
+ }
61
+
62
+ /** A batch's clocks: two floats an instance, wrapped onto rows `CLOCK_TEXTURE_WIDTH` wide. */
63
+ export class GpuInstanceClocks {
64
+ readonly view: GPUTextureView;
65
+ readonly staging: Float32Array;
66
+ private readonly texture: GPUTexture;
67
+ private readonly width: number;
68
+
69
+ constructor(device: GPUDevice, capacity: number, label: string) {
70
+ this.width = Math.max(1, Math.min(capacity, CLOCK_TEXTURE_WIDTH));
71
+ const rows = Math.max(1, Math.ceil(capacity / this.width));
72
+ this.staging = new Float32Array(this.width * rows * 2);
73
+ this.texture = device.createTexture({
74
+ label,
75
+ size: [this.width, rows],
76
+ format: 'rg32float',
77
+ usage: USAGE_TEXTURE,
78
+ });
79
+ this.view = this.texture.createView();
80
+ }
81
+
82
+ /** The rows that hold the first `count` instances, from `staging`. */
83
+ upload(queue: GPUQueue, count: number): void {
84
+ if (count <= 0) return;
85
+ const rows = Math.ceil(count / this.width);
86
+ queue.writeTexture(
87
+ { texture: this.texture },
88
+ this.staging,
89
+ { bytesPerRow: this.width * 8, rowsPerImage: rows },
90
+ [this.width, rows],
91
+ );
92
+ }
93
+
94
+ dispose(): void {
95
+ this.texture.destroy();
96
+ }
97
+ }