@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
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The vertex-stage bone animation: a crowd instance's vertex turned and placed by its bone, at its
3
+ * instance's moment of the clip — the GLSL of `boneAnimation.ts`'s `animateBoneVertex`, line for
4
+ * line. Shared by the lit stage and the depth stage, so a crowd's shadow moves with it.
5
+ *
6
+ * **Three textures, read by index and never filtered**: the places and the turns, a texel a bone a
7
+ * frame (bones across, frames down, sizes read with `textureSize` so no uniform says them), and the
8
+ * batch's clocks, two floats an instance wrapped onto rows, read at `gl_InstanceID`. The clip's speed
9
+ * and its bone scale are the one uniform, `uBoneClip`; the scene's time is the pass's `uSceneTime`.
10
+ *
11
+ * **The bone is read from the grain lane**, which is where a mesh's second coordinates ride, stored
12
+ * as `-1 - u` (`lightmap.ts`). An instanced batch reads no grain of its own from that lane: a crowd's
13
+ * mesh carries second coordinates, which is what makes it one.
14
+ *
15
+ * Integer arithmetic for the lattice, by the 2026-09-20 rule, except where a float is clamped onto
16
+ * it: the first frame is `min(floor(wrapped), frames - 1)`, so the wrap that division-by-reciprocal
17
+ * lands on `frames` is the last frame fully blended into the first — the first, which is right.
18
+ */
19
+ export const BONE_ANIMATION_GLSL = `
20
+ uniform highp sampler2D uBonePlaces;
21
+ uniform highp sampler2D uBoneTurns;
22
+ uniform highp sampler2D uInstanceClocks;
23
+ /** x the clip's frames a second, y how many bones one unit of the second coordinates' u counts. */
24
+ uniform vec2 uBoneClip;
25
+
26
+ /** v turned by the unit quaternion q. */
27
+ vec3 boneTurn(vec4 q, vec3 v) {
28
+ return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
29
+ }
30
+
31
+ /**
32
+ * Where the clip puts this vertex, from its place at rest; its normal and tangent turned with it.
33
+ */
34
+ vec3 boneAnimate(vec3 position, inout vec3 normal, inout vec3 tangent) {
35
+ ivec2 size = textureSize(uBonePlaces, 0);
36
+ int clocksWidth = textureSize(uInstanceClocks, 0).x;
37
+ vec2 clock = texelFetch(
38
+ uInstanceClocks,
39
+ ivec2(gl_InstanceID % clocksWidth, gl_InstanceID / clocksWidth),
40
+ 0
41
+ ).xy;
42
+ float moment = (uSceneTime * clock.y + clock.x) * uBoneClip.x;
43
+ float frames = float(size.y);
44
+ float wrapped = moment - frames * floor(moment / frames);
45
+ int first = min(int(floor(wrapped)), size.y - 1);
46
+ int second = first + 1 == size.y ? 0 : first + 1;
47
+ float blend = clamp(wrapped - float(first), 0.0, 1.0);
48
+ int bone = clamp(int(floor((-1.0 - aGrain) * uBoneClip.y + 0.5)), 0, size.x - 1);
49
+ vec4 q0 = texelFetch(uBoneTurns, ivec2(bone, first), 0);
50
+ vec4 q1 = texelFetch(uBoneTurns, ivec2(bone, second), 0);
51
+ /* The shorter arc: a quaternion and its negation are one turn. */
52
+ if (dot(q0, q1) < 0.0) q1 = -q1;
53
+ vec4 q = normalize(mix(q0, q1, blend));
54
+ vec3 place = mix(
55
+ texelFetch(uBonePlaces, ivec2(bone, first), 0).xyz,
56
+ texelFetch(uBonePlaces, ivec2(bone, second), 0).xyz,
57
+ blend
58
+ );
59
+ normal = boneTurn(q, normal);
60
+ tangent = boneTurn(q, tangent);
61
+ return boneTurn(q, position) + place;
62
+ }
63
+ `;
@@ -5,6 +5,7 @@ import { CUTOUT_DITHER_GLSL } from '../cutoutDither.ts';
5
5
  import { SKINNING_GLSL } from './skinning.ts';
6
6
  import { CLOTH_BINDING_GLSL } from './clothBinding.ts';
7
7
  import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from './vertexChannel.ts';
8
+ import { BONE_ANIMATION_GLSL } from './boneAnimation.ts';
8
9
 
9
10
  /**
10
11
  * Depth-only shadow pass. Its optional peel mode rejects the first recorded
@@ -78,6 +79,17 @@ uniform vec2 uUvOffset;
78
79
  uniform vec2 uAlphaCutout;
79
80
  #endif
80
81
 
82
+ #if ANIMATED
83
+ /*
84
+ * **A bone animation's, after every other declaration** so no field of the variants before it
85
+ * moves: the grain lane the vertex's bone rides in, the scene's clock, and the chunk the colour
86
+ * pass runs — the same expression, or a crowd's shadow stands still while the crowd moves.
87
+ */
88
+ layout(location = 8) in float aGrain;
89
+ uniform float uSceneTime;
90
+ ${BONE_ANIMATION_GLSL}
91
+ #endif
92
+
81
93
  out vec4 vLightPosition;
82
94
  #if CUTOUT
83
95
  /* The texture coordinate and, in z, the texture-array layer the face wears. */
@@ -99,6 +111,12 @@ void main() {
99
111
  #else
100
112
  vec4 local = vec4(aPosition, 1.0);
101
113
  #endif
114
+ #if ANIMATED
115
+ /* Turned and placed as the colour pass turns and places it; its normal and tangent unused. */
116
+ vec3 animatedNormal = vec3(0.0, 1.0, 0.0);
117
+ vec3 animatedTangent = vec3(1.0, 0.0, 0.0);
118
+ local = vec4(boneAnimate(local.xyz, animatedNormal, animatedTangent), 1.0);
119
+ #endif
102
120
  #if INSTANCED
103
121
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
104
122
  /* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
@@ -148,14 +166,14 @@ void main() {
148
166
  /** The rigid variant: the world, a prop, a mesh with no rig behind it. */
149
167
  export const DEPTH_VERT = resolveConditionals(
150
168
  DEPTH_VERT_SOURCE,
151
- { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false },
169
+ { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: false, ANIMATED: false },
152
170
  'depth',
153
171
  );
154
172
 
155
173
  /** The skinned variant, which reads a joint palette and moves the vertex by it. */
156
174
  export const DEPTH_SKINNED_VERT = resolveConditionals(
157
175
  DEPTH_VERT_SOURCE,
158
- { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false },
176
+ { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: false, ANIMATED: false },
159
177
  'depth-skinned',
160
178
  );
161
179
 
@@ -169,7 +187,7 @@ export const DEPTH_SKINNED_VERT = resolveConditionals(
169
187
  */
170
188
  export const DEPTH_INSTANCED_VERT = resolveConditionals(
171
189
  DEPTH_VERT_SOURCE,
172
- { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false },
190
+ { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false, ANIMATED: false },
173
191
  'depth-instanced',
174
192
  );
175
193
 
@@ -185,16 +203,33 @@ export const DEPTH_INSTANCED_VERT = resolveConditionals(
185
203
  */
186
204
  export const DEPTH_CUTOUT_VERT = resolveConditionals(
187
205
  DEPTH_VERT_SOURCE,
188
- { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false },
206
+ { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: false, ANIMATED: false },
189
207
  'depth-cutout',
190
208
  );
191
209
 
192
210
  export const DEPTH_INSTANCED_CUTOUT_VERT = resolveConditionals(
193
211
  DEPTH_VERT_SOURCE,
194
- { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false },
212
+ { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false, ANIMATED: false },
195
213
  'depth-instanced-cutout',
196
214
  );
197
215
 
216
+ /**
217
+ * The variants a crowd casts through: an instanced batch playing a bone animation, its vertices
218
+ * where the clip puts them at each instance's moment. See `shaders/boneAnimation.ts`. No glass
219
+ * variant: a crowd is not glass, and a glass batch that animates casts its tint from rest.
220
+ */
221
+ export const DEPTH_INSTANCED_ANIMATED_VERT = resolveConditionals(
222
+ DEPTH_VERT_SOURCE,
223
+ { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: false, ANIMATED: true },
224
+ 'depth-instanced-animated',
225
+ );
226
+
227
+ export const DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT = resolveConditionals(
228
+ DEPTH_VERT_SOURCE,
229
+ { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: false, ANIMATED: true },
230
+ 'depth-instanced-animated-cutout',
231
+ );
232
+
198
233
  export const DEPTH_FRAG = `#version 300 es
199
234
  precision highp float;
200
235
  in vec4 vLightPosition;
@@ -269,27 +304,27 @@ void main() {
269
304
  */
270
305
  export const GLASS_TINT_VERT = resolveConditionals(
271
306
  DEPTH_VERT_SOURCE,
272
- { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true },
307
+ { SKINNED: false, INSTANCED: false, CUTOUT: false, GLASS: true, ANIMATED: false },
273
308
  'glass-tint',
274
309
  );
275
310
  export const GLASS_TINT_SKINNED_VERT = resolveConditionals(
276
311
  DEPTH_VERT_SOURCE,
277
- { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true },
312
+ { SKINNED: true, INSTANCED: false, CUTOUT: false, GLASS: true, ANIMATED: false },
278
313
  'glass-tint-skinned',
279
314
  );
280
315
  export const GLASS_TINT_INSTANCED_VERT = resolveConditionals(
281
316
  DEPTH_VERT_SOURCE,
282
- { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true },
317
+ { SKINNED: false, INSTANCED: true, CUTOUT: false, GLASS: true, ANIMATED: false },
283
318
  'glass-tint-instanced',
284
319
  );
285
320
  export const GLASS_TINT_CUTOUT_VERT = resolveConditionals(
286
321
  DEPTH_VERT_SOURCE,
287
- { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true },
322
+ { SKINNED: false, INSTANCED: false, CUTOUT: true, GLASS: true, ANIMATED: false },
288
323
  'glass-tint-cutout',
289
324
  );
290
325
  export const GLASS_TINT_INSTANCED_CUTOUT_VERT = resolveConditionals(
291
326
  DEPTH_VERT_SOURCE,
292
- { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true },
327
+ { SKINNED: false, INSTANCED: true, CUTOUT: true, GLASS: true, ANIMATED: false },
293
328
  'glass-tint-instanced-cutout',
294
329
  );
295
330
 
@@ -21,7 +21,8 @@ import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
21
21
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.ts';
22
22
  import { overlayGlsl } from './overlay.ts';
23
23
  import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
24
- import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
24
+ import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.ts';
25
+ import { BONE_ANIMATION_GLSL } from '../boneAnimation.ts';
25
26
  import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
26
27
 
27
28
  /**
@@ -56,10 +57,16 @@ layout(location = 10) in vec4 aTangent;
56
57
  ${SKINNING_GLSL}
57
58
  #endif
58
59
 
59
- uniform mat4 uViewProj;
60
+ uniform mat4 uViewProj; // wgsl:view
60
61
  /** 1 when the mesh carries a tangent frame. See vHasTangents. */
61
62
  uniform int uHasTangents;
62
- uniform mat4 uLightViewProj;
63
+ uniform mat4 uLightViewProj; // wgsl:view
64
+ /**
65
+ * The scene's clock, what \`setAnimationTime\` set: what a bone animation's instances read
66
+ * their moment from. In the view block, before the wind, so every variant's view block is the
67
+ * plain one's or a prefix of it — the instanced variant declares no wind and keeps this.
68
+ */
69
+ uniform float uSceneTime; // wgsl:view
63
70
  /**
64
71
  * How often the surface texture repeats across this mesh's UV range, per axis.
65
72
  *
@@ -122,6 +129,9 @@ layout(location = 15) in vec4 aInstanceTint;
122
129
  * opacity of one. See lightmap.ts.
123
130
  */
124
131
  const bool LIGHTMAP_REGIONS = false; // wgsl:override
132
+ #if ANIMATED
133
+ ${BONE_ANIMATION_GLSL}
134
+ #endif
125
135
  #else
126
136
  uniform mat4 uModel;
127
137
  /**
@@ -147,7 +157,7 @@ uniform vec3 uTint;
147
157
  * agreeing about where the shared uniforms sit: the instanced variant drops these five along with
148
158
  * the two above it, and a dropped field moves nothing. flatPass.ts asserts exactly that.
149
159
  */
150
- ${CHANNEL_ATTRIBUTE}
160
+ ${LIT_CHANNEL_ATTRIBUTE}
151
161
  ${CHANNEL_BEND}
152
162
  #endif
153
163
 
@@ -245,6 +255,10 @@ void main() {
245
255
  vec3 localNormal = aNormal;
246
256
  vec3 localTangent = aTangent.xyz;
247
257
  #endif
258
+ #if ANIMATED
259
+ /* The clip's turn and place, in the mesh's own space, before the instance places it. */
260
+ local = vec4(boneAnimate(local.xyz, localNormal, localTangent), 1.0);
261
+ #endif
248
262
  #if INSTANCED
249
263
  mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
250
264
  /*
@@ -379,6 +393,12 @@ export interface FlatVertexOptions {
379
393
  * Excludes `skinned`: the joint attributes and the instance matrix want the same locations.
380
394
  */
381
395
  readonly instanced: boolean;
396
+ /**
397
+ * Whether an instanced variant follows a bone animation: \`shaders/boneAnimation.ts\`. A
398
+ * variant rather than a pipeline constant, because it declares three textures the other
399
+ * instanced pipelines would otherwise have to bind stand-ins for. Instanced only.
400
+ */
401
+ readonly animated?: boolean;
382
402
  }
383
403
 
384
404
  /**
@@ -401,9 +421,17 @@ export function flatVert(options: FlatVertexOptions): string {
401
421
  'so every instance of the batch would wear one expression between them.',
402
422
  );
403
423
  }
424
+ if (options.animated === true && !options.instanced) {
425
+ throw new Error('flatVert: a bone animation is played by an instanced variant alone.');
426
+ }
404
427
  return resolveConditionals(
405
428
  FLAT_VERT_SOURCE,
406
- { SKINNED: options.skinned, MORPHED: options.morphed, INSTANCED: options.instanced },
429
+ {
430
+ SKINNED: options.skinned,
431
+ MORPHED: options.morphed,
432
+ INSTANCED: options.instanced,
433
+ ANIMATED: options.animated === true,
434
+ },
407
435
  'flatVert',
408
436
  );
409
437
  }
@@ -82,7 +82,7 @@ export const MAIN_GLSL = `void main() {
82
82
  */
83
83
  float share = cutoutShare(tested, uCutout.x);
84
84
  if (uCutout.y == 1.0) {
85
- if (!cutoutKeeps(share, gl_FragCoord.xy, uCutout.z)) discard;
85
+ if (!cutoutKeeps(share, gl_FragCoord.xy, uHighlightMin.w)) discard;
86
86
  kept = 1.0;
87
87
  } else {
88
88
  kept = share;
@@ -1281,7 +1281,7 @@ export const MAIN_GLSL = `void main() {
1281
1281
  */
1282
1282
  float falloff;
1283
1283
  /*
1284
- * **A light may name its own falloff, and then it is Unreal's for a light without
1284
+ * **A light may name its own falloff, and then it is the usual one for a light without
1285
1285
  * inverse-square falloff**: \`(1 - (d/R)^2)^n\` with no distance term, so a fill authored to
1286
1286
  * light a whole hall across a hundred metres does, where the inverse square would have it
1287
1287
  * gone a few metres out. Zero is the frame's own rule below, which every light was before.
@@ -1997,7 +1997,7 @@ export const MAIN_GLSL = `void main() {
1997
1997
  // finishing a run reads the same at noon and at midnight. Costs one compare
1998
1998
  // per fragment and no extra draw call — the box is a uniform, not geometry.
1999
1999
  if (uHighlightMax.w > 0.0) {
2000
- vec3 inside = step(uHighlightMin, vWorldPos) * step(vWorldPos, uHighlightMax.xyz);
2000
+ vec3 inside = step(uHighlightMin.xyz, vWorldPos) * step(vWorldPos, uHighlightMax.xyz);
2001
2001
  lit += albedo * uHighlightMax.w * inside.x * inside.y * inside.z;
2002
2002
  }
2003
2003
  }
@@ -2101,7 +2101,17 @@ export const MAIN_GLSL = `void main() {
2101
2101
  vec3 refractV = normalize(uCameraPos - vWorldPos);
2102
2102
  ivec2 snapSize = textureSize(uRefractScene, 0);
2103
2103
  vec2 screenUv = gl_FragCoord.xy / vec2(snapSize);
2104
- vec2 bent = clamp(screenUv + refractN.xy * uSeeThrough.x, vec2(0.0), vec2(1.0));
2104
+ /*
2105
+ * **The normal's sideways part as the eye sees it**: the shading normal in view space, its x
2106
+ * and y across the screen, y turned to the way this frame's rows run (w of the tint). It was the
2107
+ * normal's world x and y until 4.10.0, which on a quad facing the camera is the quad's own lean
2108
+ * against the world's axes: one offset over the whole quad, a lens, with a normal map's tilt a
2109
+ * small change on top. In view space a quad facing the eye bends by nothing but its map, which
2110
+ * is a shimmer, and a pane bends by how it leans toward the eye wherever the eye stands.
2111
+ */
2112
+ vec3 seenN = mat3(uView) * refractN;
2113
+ vec2 bend = vec2(seenN.x, seenN.y * uRefractTint.w);
2114
+ vec2 bent = clamp(screenUv + bend * uSeeThrough.x, vec2(0.0), vec2(1.0));
2105
2115
  /*
2106
2116
  * **An explicit level, and frost is what picks it** — the 2026-08-07 rule, and the reason a
2107
2117
  * frosted pane blurs rather than fades: the copy carries a mip chain on a frame that draws glass,
@@ -2115,7 +2125,7 @@ export const MAIN_GLSL = `void main() {
2115
2125
  /* Unsigned: a pane passes the same light seen from either side, and so does its Fresnel. */
2116
2126
  float cosView = max(abs(dot(refractN, refractV)), 0.05);
2117
2127
  float pathLength = uSeeThrough.y * vThickness / cosView;
2118
- vec3 seen = behind * pow(uRefractTint, vec3(pathLength));
2128
+ vec3 seen = behind * pow(uRefractTint.rgb, vec3(pathLength));
2119
2129
  if (glassTransmission > 0.0) {
2120
2130
  /*
2121
2131
  * **Glass keeps its own surface and shows what is behind over it**, by how much it lets
@@ -59,7 +59,7 @@ const bool PHYSICAL_SPECULAR = PHYSICAL_SPECULAR_ON; // wgsl:override
59
59
  * A model's numbers, two vectors a material — the lit stage's uniform budget has no more room
60
60
  * (surfaceModel.ts) — and whether a model map is bound, in the last component.
61
61
  */
62
- uniform vec4 uModelParams[2];
62
+ uniform vec4 uModelParams[2]; // wgsl:material
63
63
  /* The model's own channels, in the albedo's layout: what each means is each model's to say. */
64
64
  uniform highp sampler2DArray uModelMap;
65
65
 
@@ -26,7 +26,7 @@ vec3 overlayGlow(vec2 uv, vec3 n) { return vec3(0.0); }
26
26
  /* Whether this program carries a draw's overlay. Off but where one has been set: overlay.ts. */
27
27
  const bool SURFACE_OVERLAY = true; // wgsl:override
28
28
  /* The overlay's fifteen vectors; packSurfaceOverlay in surfaceOverlay.ts says what each holds. */
29
- uniform vec4 uOverlay[15];
29
+ uniform vec4 uOverlay[15]; // wgsl:material
30
30
 
31
31
  /* Whether the atlas is what the refraction slot holds: not where this draw refracts or is glass. */
32
32
  bool overlayImagesBound() {
@@ -13,7 +13,7 @@
13
13
  * picture and is its own change.
14
14
  */
15
15
  import { MAX_SHADOW_FILTER_TAPS } from '../../renderQuality.ts';
16
- import { OUTPUT_TRANSFORM_GLSL } from '../outputTransform.ts';
16
+ import { LIT_OUTPUT_TRANSFORM_GLSL } from '../outputTransform.ts';
17
17
  import { LAMP_GLASS_GLSL } from './lampGlass.ts';
18
18
 
19
19
  export const POINTSHADOW_GLSL = `/**
@@ -344,7 +344,7 @@ ${LAMP_GLASS_GLSL}
344
344
 
345
345
  layout(location = 0) out vec4 outColor;
346
346
 
347
- ${OUTPUT_TRANSFORM_GLSL}
347
+ ${LIT_OUTPUT_TRANSFORM_GLSL}
348
348
 
349
349
  /**
350
350
  * A fixed Poisson disk avoids the grid-shaped edge left by square 3x3 PCF.
@@ -121,9 +121,15 @@ uniform sampler2D uRefractScene;
121
121
  * gives up, and four separate floats cost half the shadowed lights such a device can have. The tints
122
122
  * stay colours of their own. Zero transmission is not glass — see glass.ts.
123
123
  */
124
- uniform vec4 uSeeThrough;
125
- uniform vec3 uRefractTint;
126
- uniform vec3 uGlassTint;
124
+ uniform vec4 uSeeThrough; // wgsl:material
125
+ /*
126
+ * What a metre of the medium leaves of each channel (rgb), and in w which way this frame's rows run
127
+ * up the screen: +1 where the first row is the bottom (WebGL2), -1 where it is the top (WebGPU). The
128
+ * bend is taken in view space, whose y is up whatever the framebuffer says, so it is turned here to
129
+ * the rows a screen coordinate counts. A vec4 where a vec3 already spent the row.
130
+ */
131
+ uniform vec4 uRefractTint; // wgsl:material
132
+ uniform vec3 uGlassTint; // wgsl:material
127
133
  flat in int vHasTangents;
128
134
 
129
135
  /**
@@ -154,7 +160,7 @@ uniform mediump sampler2DArray uAlbedo;
154
160
  * card. Its draw culls nothing, and a back face is lit as a front, with the shading normal turned
155
161
  * toward the viewer.
156
162
  */
157
- uniform ivec4 uMaterialFlags;
163
+ uniform ivec4 uMaterialFlags; // wgsl:material
158
164
  /**
159
165
  * Alpha below which a textured fragment is thrown away entirely, 0 to disable.
160
166
  *
@@ -168,13 +174,17 @@ uniform ivec4 uMaterialFlags;
168
174
  * else changes.
169
175
  *
170
176
  * **Four numbers in one row**: x is the cutoff, y how this frame resolves the edge (0 the hard test,
171
- * 1 a dither the temporal resolve averages, 2 alpha-to-coverage; see \`cutoutDither.ts\`), z the
172
- * frame the dither's pattern is offset by, w the material's lighting channels — a mask a light
173
- * shades it only where its own shares a bit with (\`SurfaceMaterial.lightChannels\`, 1 unless a
174
- * material names others). One vector rather than a float and three more, because every loose
175
- * scalar is a row of WebGL2's 224 that the light budget gives up.
177
+ * 1 a dither the temporal resolve averages, 2 alpha-to-coverage; see \`cutoutDither.ts\`), z unused,
178
+ * w the material's lighting channels — a mask a light shades it only where its own shares a bit
179
+ * with (\`SurfaceMaterial.lightChannels\`, 1 unless a material names others). One vector rather than
180
+ * a float and three more, because every loose scalar is a row of WebGL2's 224 that the light budget
181
+ * gives up.
182
+ *
183
+ * **z was the frame the dither's pattern is offset by until 4.10.0**, and that made every material
184
+ * new every frame: a material is kept on WebGPU by its numbers, and one of them changed with the
185
+ * clock. The frame is the pass's, in \`uHighlightMin.w\`.
176
186
  */
177
- uniform vec4 uCutout;
187
+ uniform vec4 uCutout; // wgsl:material
178
188
  /**
179
189
  * A draw's own ambient, as nine spherical-harmonic coefficients of incoming radiance, red green blue
180
190
  * in turn, twenty-seven floats, and in the last of the twenty-eight a 1 where they are on — 0 is the
@@ -182,7 +192,7 @@ uniform vec4 uCutout;
182
192
  * and the basis, and its tests hold this to it. Seven rows of the material block, which is per draw
183
193
  * as far as anything here is: \`setAmbientSH\` moves it between two draws as \`setMaterial\` does.
184
194
  */
185
- uniform vec4 uAmbientSH[7];
195
+ uniform vec4 uAmbientSH[7]; // wgsl:material
186
196
 
187
197
  /** The ambient a surface facing \`n\` receives from \`uAmbientSH\`: \`ambientFromSH\`'s arithmetic. */
188
198
  vec3 ambientHarmonics(vec3 n) {
@@ -229,7 +239,7 @@ uniform highp sampler2DArray uNormalMap;
229
239
  * shadow samplers, where a profile with shadows off should declare none of them; it is the wrong
230
240
  * tool for one image on a shader that already declares one.
231
241
  */
232
- uniform float uNormalStrength;
242
+ uniform float uNormalStrength; // wgsl:material
233
243
 
234
244
  /**
235
245
  * Occlusion, roughness and metallic in one image. glTF's packing, because that is what an import
@@ -250,7 +260,7 @@ uniform highp sampler2DArray uOrmMap;
250
260
  * occlusionTexture.strength is: 1 + strength * (texel - 1). A multiply at 0 means fully occluded,
251
261
  * which is the opposite of what a caller asking for no occlusion means.
252
262
  */
253
- uniform vec3 uOrmScale;
263
+ uniform vec3 uOrmScale; // wgsl:material
254
264
  /**
255
265
  * 1 when \`uEnvironment\` holds the GGX-prefiltered cube, 0 when it holds the capture's box chain.
256
266
  *
@@ -266,7 +276,7 @@ uniform vec3 uOrmScale;
266
276
  * (\`uniformVectorBudget.ts\`). y is how much of the environment the material mirrors, z how
267
277
  * bright what it mirrors is.
268
278
  */
269
- uniform vec3 uEnvironmentDials;
279
+ uniform vec3 uEnvironmentDials; // wgsl:material
270
280
 
271
281
  /**
272
282
  * Where a surface glows, and in what colour.
@@ -288,7 +298,7 @@ uniform highp sampler2DArray uEmissiveMap;
288
298
  * The counterpart of \`uOrmScale\`: a factor that multiplies a texture is not a value, and this is
289
299
  * where the multiplier lives once an image supplies the shape.
290
300
  */
291
- uniform vec3 uEmissiveScale;
301
+ uniform vec3 uEmissiveScale; // wgsl:material
292
302
 
293
303
  #if DIRECTIONAL_SHADOWS
294
304
  /*
@@ -375,7 +385,7 @@ uniform vec4 uSurfaceScene;
375
385
  * draw moves it), y the night factor emission is gated on. One row where two floats were two
376
386
  * (\`uniformVectorBudget.ts\`).
377
387
  */
378
- uniform vec2 uEmission;
388
+ uniform vec2 uEmission; // wgsl:material
379
389
  #if NIGHT_EMISSIVE
380
390
  /**
381
391
  * Emission that only shows where the dominant light does not reach. 0 is off and is the default.
@@ -400,7 +410,7 @@ uniform float uNightEmissive;
400
410
  * of that to change one channel is how a renderer ends up with two shaders that
401
411
  * disagree about the sun. The caller owns the blend state; this owns the number.
402
412
  */
403
- uniform float uOpacity;
413
+ uniform float uOpacity; // wgsl:material
404
414
 
405
415
  /**
406
416
  * Two per-draw switches on how a fragment is *written* rather than what colour it is, sharing one
@@ -432,7 +442,7 @@ uniform float uOpacity;
432
442
  * crossfade reads as grain for as long as it lasts, which is why it should be short; and a blended
433
443
  * draw that asks for it is dithered too, since the test is a discard.
434
444
  */
435
- uniform vec2 uWriteMode;
445
+ uniform vec2 uWriteMode; // wgsl:material
436
446
 
437
447
  /** Whether this fragment is one \`uWriteMode.y\` keeps. Integer arithmetic, per the device rule. */
438
448
  bool ditherKeeps(float amount) {
@@ -465,7 +475,7 @@ bool ditherKeeps(float amount) {
465
475
  * one uniform skips the untaken side rather than computing both and discarding one, which is
466
476
  * only true because nothing here reads it through a *varying*-dependent branch.
467
477
  */
468
- uniform int uLightingEnabled;
478
+ uniform int uLightingEnabled; // wgsl:material
469
479
  /**
470
480
  * How this draw meets the medium: the atmospheric haze and the underwater tint both, folded
471
481
  * together because both are the *camera's* medium rather than a property of the surface — see
@@ -487,7 +497,7 @@ uniform int uLightingEnabled;
487
497
  * is what "exactly as its colour says" has to mean for it; the cost is that such a draw will
488
498
  * not tint if the camera goes under water, unlike everything else in the frame.
489
499
  */
490
- uniform int uFogEnabled;
500
+ uniform int uFogEnabled; // wgsl:material
491
501
 
492
502
  /**
493
503
  * A pass-level scale over the grain the geometry declared, 0 to 1.
@@ -497,7 +507,7 @@ uniform int uFogEnabled;
497
507
  * imported model that declares nothing is drawn with the pass's own answer, and a scene
498
508
  * can dial a whole draw down without rebuilding its geometry.
499
509
  */
500
- uniform float uGrain;
510
+ uniform float uGrain; // wgsl:material
501
511
  /*
502
512
  * The material's own relief: how strong, and how many bumps to a metre.
503
513
  *
@@ -506,7 +516,7 @@ uniform float uGrain;
506
516
  * between them, and the geometry carrying them is identical in all three cases. See
507
517
  * Renderer.setSurfaceRelief. x is the strength and y the bumps to a metre, in one row.
508
518
  */
509
- uniform vec2 uRelief;
519
+ uniform vec2 uRelief; // wgsl:material
510
520
  /**
511
521
  * How hard the bound surface texture's own luminance turns the shading normal, 0 for not at all.
512
522
  *
@@ -529,7 +539,7 @@ uniform vec2 uRelief;
529
539
  * 0..1 like its neighbours for the same reason: this is an amplitude against a luminance
530
540
  * gradient, not a fraction of anything.
531
541
  */
532
- uniform float uTextureRelief;
542
+ uniform float uTextureRelief; // wgsl:material
533
543
 
534
544
  /**
535
545
  * How much of the environment this material mirrors, 0 to 1.
@@ -664,8 +674,12 @@ uniform vec3 uProbeGridSets;
664
674
  /**
665
675
  * A world-space box that lights up regardless of time of day: its corners, and in the far corner's
666
676
  * w the gain — one row where a loose float was another (\`uniformVectorBudget.ts\`).
677
+ *
678
+ * **And in the near corner's w the frame the cutout dither's pattern is offset by**, for the same
679
+ * reason: a row the lit stage already pays for, holding a number the pass sets and no material does.
680
+ * See \`uCutout\`.
667
681
  */
668
- uniform vec3 uHighlightMin;
682
+ uniform vec4 uHighlightMin;
669
683
  uniform vec4 uHighlightMax;
670
684
 
671
685
  /*