@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
@@ -63,11 +63,27 @@ export interface StageBindings {
63
63
  readonly textures: Readonly<Record<string, TextureBinding>>;
64
64
  /** Each pipeline-overridable constant's id, by the GLSL name it was declared under. */
65
65
  readonly overrides?: Readonly<Record<string, number>>;
66
+ /**
67
+ * The second block, of the uniforms marked `// wgsl:material`: its binding, size and fields.
68
+ * Absent from a stage that marks none, which is every stage but the lit fragment's.
69
+ */
70
+ readonly materialUniforms?: number;
71
+ readonly materialSize?: number;
72
+ readonly materialFields?: StageBindings['fields'];
73
+ /**
74
+ * The vertex stage's view block, of the uniforms marked `// wgsl:view`: the camera, the light's
75
+ * matrix and the wind, which a pass settles. Absent from every stage but the lit vertex one.
76
+ */
77
+ readonly viewUniforms?: number;
78
+ readonly viewSize?: number;
79
+ readonly viewFields?: StageBindings['fields'];
66
80
  }
67
81
  /** The key for a set of vertex flags: the same rule `variantKey` uses, flags on, sorted, joined. */
68
- export declare function flatVertexKey(skinned: boolean, morphed: boolean, instanced?: boolean): string;
82
+ export declare function flatVertexKey(skinned: boolean, morphed: boolean, instanced?: boolean,
83
+ /** Whether an instanced variant plays a bone animation. See `shaders/boneAnimation.ts`. */
84
+ animated?: boolean): string;
69
85
  /** Everything the generator recorded about one variant of the vertex stage. */
70
- export declare function flatVertexBindings(skinned: boolean, morphed?: boolean, instanced?: boolean): StageBindings;
86
+ export declare function flatVertexBindings(skinned: boolean, morphed?: boolean, instanced?: boolean, animated?: boolean): StageBindings;
71
87
  /**
72
88
  * A vertex stage's constants, keyed by each override's **id** as the fragment's are: `SKIN_EIGHT` and
73
89
  * `CLOTH_BOUND` in a skinned stage, `LIGHTMAP_REGIONS` in an instanced one, nothing in any other. Every override a stage declares is set, and one this does
@@ -78,63 +94,51 @@ export declare function vertexConstants(bindings: StageBindings, skinEight: bool
78
94
  /** Whether an instanced stage reads its tint lane as a lightmap region. See `lightmap.ts`. */
79
95
  lightmapRegions?: boolean): Record<string, number>;
80
96
  export declare const FLAT_VERT_FIELDS: {
81
- readonly uViewProj: {
82
- readonly offset: 0;
83
- readonly size: 64;
84
- readonly type: "mat4";
85
- };
86
97
  readonly uHasTangents: {
87
- readonly offset: 64;
98
+ readonly offset: 0;
88
99
  readonly size: 4;
89
100
  readonly type: "int";
90
101
  };
91
- readonly uLightViewProj: {
92
- readonly offset: 80;
93
- readonly size: 64;
94
- readonly type: "mat4";
95
- };
96
102
  readonly uUvScale: {
97
- readonly offset: 144;
103
+ readonly offset: 8;
98
104
  readonly size: 8;
99
105
  readonly type: "vec2";
100
106
  };
101
107
  readonly uUvOffset: {
102
- readonly offset: 152;
108
+ readonly offset: 16;
103
109
  readonly size: 8;
104
110
  readonly type: "vec2";
105
111
  };
106
112
  readonly uModel: {
107
- readonly offset: 160;
113
+ readonly offset: 32;
108
114
  readonly size: 64;
109
115
  readonly type: "mat4";
110
116
  };
111
117
  readonly uTint: {
112
- readonly offset: 224;
118
+ readonly offset: 96;
113
119
  readonly size: 12;
114
120
  readonly type: "vec3";
115
121
  };
116
- readonly uWindDirection: {
117
- readonly offset: 240;
118
- readonly size: 8;
119
- readonly type: "vec2";
120
- };
121
- readonly uWindSpeed: {
122
- readonly offset: 248;
122
+ };
123
+ /** The animated instanced variant's block: the instanced fields, and the clip's `uBoneClip`. */
124
+ export declare const FLAT_ANIMATED_VERT_FIELDS: {
125
+ readonly uHasTangents: {
126
+ readonly offset: 0;
123
127
  readonly size: 4;
124
- readonly type: "float";
128
+ readonly type: "int";
125
129
  };
126
- readonly uWindGust: {
127
- readonly offset: 252;
128
- readonly size: 4;
129
- readonly type: "float";
130
+ readonly uUvScale: {
131
+ readonly offset: 8;
132
+ readonly size: 8;
133
+ readonly type: "vec2";
130
134
  };
131
- readonly uWindTime: {
132
- readonly offset: 256;
133
- readonly size: 4;
134
- readonly type: "float";
135
+ readonly uUvOffset: {
136
+ readonly offset: 16;
137
+ readonly size: 8;
138
+ readonly type: "vec2";
135
139
  };
136
- readonly uWindSpatialPhase: {
137
- readonly offset: 264;
140
+ readonly uBoneClip: {
141
+ readonly offset: 24;
138
142
  readonly size: 8;
139
143
  readonly type: "vec2";
140
144
  };
@@ -147,6 +151,14 @@ export declare const FLAT_VERT_FIELDS: {
147
151
  * 144 bytes a slot and is what a single ring costs.
148
152
  */
149
153
  export declare const FLAT_VERT_SIZE: number;
154
+ /** The view block's fields, binding and size: the largest variant's, as `FLAT_VERT_SIZE` is. */
155
+ export declare const FLAT_VIEW_FIELDS: Readonly<Record<string, {
156
+ readonly offset: number;
157
+ readonly size: number;
158
+ readonly length?: number;
159
+ readonly stride?: number;
160
+ }>>;
161
+ export declare const FLAT_VIEW_SIZE: number;
150
162
  /**
151
163
  * The most sampled textures and distinct samplers any lit variant carrying the environment probe
152
164
  * binds in its fragment stage, counted from the generated bindings: what a device must allow per
@@ -159,16 +171,43 @@ export declare function probeStageCeilings(): {
159
171
  };
160
172
  /** Everything the generator recorded about one permutation of the fragment stage. */
161
173
  export declare function flatFragmentBindings(variant: FlatVariant): StageBindings;
174
+ /**
175
+ * The lit fragment stage's two blocks as the renderer keeps them on the CPU: the pass's, then the
176
+ * material's straight after it, in one scratch, with every field at its offset in that scratch.
177
+ *
178
+ * **One scratch rather than two, so no setter has to know which block its field is in.** A setter
179
+ * writes by name, as it always has; the draw copies the material's bytes — `passSize` onwards — into
180
+ * the store and the pass's bytes into a ring slot, each only when something has changed it.
181
+ */
182
+ export interface FlatFragmentScratch {
183
+ readonly fields: StageBindings['fields'];
184
+ /** Bytes of the pass's block, which is where the material's starts in the scratch. */
185
+ readonly passSize: number;
186
+ readonly passBinding: number;
187
+ readonly materialSize: number;
188
+ readonly materialBinding: number;
189
+ }
190
+ export declare function flatFragmentScratch(variant: FlatVariant): FlatFragmentScratch;
162
191
  /** The depth format both backends compare with. */
163
192
  export { DEPTH_FORMAT } from '../../depthConvention.ts';
164
193
  /**
165
194
  * The layout this variant binds against.
166
195
  *
167
- * The vertex block is **dynamic**, because every draw supplies its own model matrix and tint
168
- * from its own slot of one buffer. The fragment block is not: it holds the lights, the fog
169
- * and the camera, settled once by `bindMeshPass` and unchanged between draws.
196
+ * All four blocks are **dynamic**: the vertex block because every draw supplies its own model
197
+ * matrix and tint from its own slot of one buffer; the fragment stage's pass block — the lights, the
198
+ * fog, the camera — and the vertex stage's view block because a frame binds more than one pass; and
199
+ * the material block because a material changes between draws. Their dynamic offsets go in binding
200
+ * order: the draw's, the pass's, the material's, the view's.
170
201
  */
171
- export declare function createFlatBindGroupLayout(device: GPUDevice, variant: FlatVariant, skinned?: boolean, morphed?: boolean): GPUBindGroupLayout;
202
+ export declare function createFlatBindGroupLayout(device: GPUDevice, variant: FlatVariant, skinned?: boolean, morphed?: boolean,
203
+ /** The instanced variant that plays a bone animation: its three vertex textures. */
204
+ animated?: boolean): GPUBindGroupLayout;
205
+ /** What an animated draw binds besides its material: the clip's two textures and its batch's clocks. */
206
+ export interface AnimationViews {
207
+ readonly places: GPUTextureView;
208
+ readonly turns: GPUTextureView;
209
+ readonly clocks: GPUTextureView;
210
+ }
172
211
  /**
173
212
  * Bind the buffers and one view per declared texture.
174
213
  *
@@ -178,7 +217,7 @@ export declare function createFlatBindGroupLayout(device: GPUDevice, variant: Fl
178
217
  * and WebGPU still requires something there, which is why `renderer.ts` keeps an
179
218
  * `emptyTexture2D` for the same reason on the other side.
180
219
  */
181
- export declare function createFlatBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, variant: FlatVariant, perDraw: GPUBuffer, perFrame: GPUBuffer, resolve: (name: string) => {
220
+ export declare function createFlatBindGroup(device: GPUDevice, layout: GPUBindGroupLayout, variant: FlatVariant, perDraw: GPUBuffer, passes: GPUBuffer, materials: GPUBuffer, views: GPUBuffer, resolve: (name: string) => {
182
221
  view: GPUTextureView;
183
222
  sampler: GPUSampler;
184
223
  },
@@ -187,7 +226,9 @@ palette?: GPUTextureView | null,
187
226
  /** The delta view for a morphed bind group, or null for an unmorphed one. */
188
227
  deltas?: GPUTextureView | null,
189
228
  /** A skinned bind group's cloth views — the draw's, or the stand-in where it sets none. */
190
- cloth?: ClothViews | null): GPUBindGroup;
229
+ cloth?: ClothViews | null,
230
+ /** An animated instanced group's views. See `AnimationViews`. */
231
+ animation?: AnimationViews | null): GPUBindGroup;
191
232
  /**
192
233
  * The pipeline, cached by the state that distinguishes it.
193
234
  *
@@ -251,7 +292,9 @@ skinPass?: SkinHalf,
251
292
  * The reflection pass's surface half: the draw again, against its own depth, into the two
252
293
  * targets `reflectionSurfacePass.ts` holds. The key must carry it: `|rs`.
253
294
  */
254
- reflectionSurface?: boolean): GPURenderPipeline;
295
+ reflectionSurface?: boolean,
296
+ /** An instanced pipeline that plays a bone animation. The key must carry it: `|anim`. */
297
+ animated?: boolean): GPURenderPipeline;
255
298
  /**
256
299
  * The same pipeline, compiled off the main thread.
257
300
  *
@@ -52,9 +52,13 @@ const FRAGMENT_BINDINGS = FLAT_BINDINGS.flatFrag;
52
52
  */
53
53
  const VERTEX_BINDINGS = FLAT_BINDINGS.flatVert;
54
54
  /** The key for a set of vertex flags: the same rule `variantKey` uses, flags on, sorted, joined. */
55
- export function flatVertexKey(skinned, morphed, instanced = false) {
55
+ export function flatVertexKey(skinned, morphed, instanced = false,
56
+ /** Whether an instanced variant plays a bone animation. See `shaders/boneAnimation.ts`. */
57
+ animated = false) {
56
58
  const on = [];
57
59
  /* Alphabetical, because the generator's own `variantKey` sorts and the two keys must agree. */
60
+ if (animated)
61
+ on.push('animated');
58
62
  if (instanced)
59
63
  on.push('instanced');
60
64
  if (morphed)
@@ -64,8 +68,8 @@ export function flatVertexKey(skinned, morphed, instanced = false) {
64
68
  return on.length === 0 ? 'none' : on.join('+');
65
69
  }
66
70
  /** Everything the generator recorded about one variant of the vertex stage. */
67
- export function flatVertexBindings(skinned, morphed = false, instanced = false) {
68
- const key = flatVertexKey(skinned, morphed, instanced);
71
+ export function flatVertexBindings(skinned, morphed = false, instanced = false, animated = false) {
72
+ const key = flatVertexKey(skinned, morphed, instanced, animated);
69
73
  const found = VERTEX_BINDINGS[key];
70
74
  if (found === undefined)
71
75
  throw new Error(`flatPass: no vertex bindings for variant "${key}"`);
@@ -136,6 +140,8 @@ for (const [key, bindings] of Object.entries(VERTEX_BINDINGS)) {
136
140
  }
137
141
  }
138
142
  export const FLAT_VERT_FIELDS = FLAT_BINDINGS.flatVert.none.fields;
143
+ /** The animated instanced variant's block: the instanced fields, and the clip's `uBoneClip`. */
144
+ export const FLAT_ANIMATED_VERT_FIELDS = FLAT_BINDINGS.flatVert['animated+instanced'].fields;
139
145
  /**
140
146
  * The **largest** variant's block, because one ring serves every draw.
141
147
  *
@@ -145,6 +151,30 @@ export const FLAT_VERT_FIELDS = FLAT_BINDINGS.flatVert.none.fields;
145
151
  */
146
152
  export const FLAT_VERT_SIZE = Math.max(...Object.values(VERTEX_BINDINGS).map((bindings) => bindings.uniformSize));
147
153
  const FLAT_VERT_BINDING = PLAIN_VERTEX.uniforms;
154
+ /*
155
+ * **The view block may be shorter in a variant, never different.** One slot serves every lit draw
156
+ * of a pass, plain and instanced alike, so every field a variant's view block declares must sit
157
+ * where the plain variant has it. The instanced variant declares no wind, and its block is the plain
158
+ * one's first two matrices.
159
+ */
160
+ const PLAIN_VIEW_FIELDS = PLAIN_VERTEX.viewFields ?? {};
161
+ for (const [key, bindings] of Object.entries(VERTEX_BINDINGS)) {
162
+ if (bindings.viewUniforms !== PLAIN_VERTEX.viewUniforms) {
163
+ throw new Error(`flatPass: vertex variant "${key}" binds its view block elsewhere`);
164
+ }
165
+ for (const [field, here] of Object.entries(bindings.viewFields ?? {})) {
166
+ if (PLAIN_VIEW_FIELDS[field]?.offset !== here.offset) {
167
+ throw new Error(`flatPass: vertex variant "${key}" puts view field ${field} at ${here.offset} where the ` +
168
+ `plain variant has it at ${PLAIN_VIEW_FIELDS[field]?.offset}.`);
169
+ }
170
+ }
171
+ }
172
+ /** The view block's fields, binding and size: the largest variant's, as `FLAT_VERT_SIZE` is. */
173
+ export const FLAT_VIEW_FIELDS = PLAIN_VIEW_FIELDS;
174
+ export const FLAT_VIEW_SIZE = Math.max(...Object.values(VERTEX_BINDINGS).map((bindings) => bindings.viewSize ?? 0));
175
+ const FLAT_VIEW_BINDING = PLAIN_VERTEX.viewUniforms ?? -1;
176
+ if (FLAT_VIEW_BINDING < 0)
177
+ throw new Error('flatPass: the lit vertex stage declares no view block');
148
178
  /**
149
179
  * The most sampled textures and distinct samplers any lit variant carrying the environment probe
150
180
  * binds in its fragment stage, counted from the generated bindings: what a device must allow per
@@ -170,6 +200,26 @@ export function flatFragmentBindings(variant) {
170
200
  throw new Error(`flatPass: no bindings for variant "${variant}"`);
171
201
  return found;
172
202
  }
203
+ export function flatFragmentScratch(variant) {
204
+ const bindings = flatFragmentBindings(variant);
205
+ const { materialUniforms, materialSize, materialFields } = bindings;
206
+ if (materialUniforms === undefined ||
207
+ materialSize === undefined ||
208
+ materialFields === undefined) {
209
+ throw new Error(`flatPass: variant "${variant}" has no material block`);
210
+ }
211
+ const fields = { ...bindings.fields };
212
+ for (const [name, field] of Object.entries(materialFields)) {
213
+ fields[name] = { ...field, offset: bindings.uniformSize + field.offset };
214
+ }
215
+ return {
216
+ fields,
217
+ passSize: bindings.uniformSize,
218
+ passBinding: bindings.uniforms,
219
+ materialSize,
220
+ materialBinding: materialUniforms,
221
+ };
222
+ }
173
223
  /**
174
224
  * The lit fragment stage's constants, set from the cache's switches, keyed by each one's **id**.
175
225
  *
@@ -209,6 +259,13 @@ function flatFragmentConstants(variant, cache, model, skinPass = 'whole', reflec
209
259
  }
210
260
  return constants;
211
261
  }
262
+ /** The material block's binding, which every lit variant declares. */
263
+ function materialBinding(fragment) {
264
+ if (fragment.materialUniforms === undefined) {
265
+ throw new Error('flatPass: the lit fragment stage declares no material block');
266
+ }
267
+ return fragment.materialUniforms;
268
+ }
212
269
  /** The depth format both backends compare with. */
213
270
  /* Re-exported so the twenty passes that already import it from here do not each learn a new
214
271
  module; `depthConvention.ts` is where the decision lives and why. */
@@ -218,11 +275,15 @@ const VISIBILITY_FRAGMENT = 0x2;
218
275
  /**
219
276
  * The layout this variant binds against.
220
277
  *
221
- * The vertex block is **dynamic**, because every draw supplies its own model matrix and tint
222
- * from its own slot of one buffer. The fragment block is not: it holds the lights, the fog
223
- * and the camera, settled once by `bindMeshPass` and unchanged between draws.
278
+ * All four blocks are **dynamic**: the vertex block because every draw supplies its own model
279
+ * matrix and tint from its own slot of one buffer; the fragment stage's pass block — the lights, the
280
+ * fog, the camera — and the vertex stage's view block because a frame binds more than one pass; and
281
+ * the material block because a material changes between draws. Their dynamic offsets go in binding
282
+ * order: the draw's, the pass's, the material's, the view's.
224
283
  */
225
- export function createFlatBindGroupLayout(device, variant, skinned = false, morphed = false) {
284
+ export function createFlatBindGroupLayout(device, variant, skinned = false, morphed = false,
285
+ /** The instanced variant that plays a bone animation: its three vertex textures. */
286
+ animated = false) {
226
287
  const fragment = flatFragmentBindings(variant);
227
288
  const entries = [
228
289
  {
@@ -230,15 +291,31 @@ export function createFlatBindGroupLayout(device, variant, skinned = false, morp
230
291
  visibility: VISIBILITY_VERTEX,
231
292
  buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: FLAT_VERT_SIZE },
232
293
  },
294
+ {
295
+ binding: FLAT_VIEW_BINDING,
296
+ visibility: VISIBILITY_VERTEX,
297
+ /* Dynamic, a slot a pass, for the reason the fragment stage's pass block is. */
298
+ buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: FLAT_VIEW_SIZE },
299
+ },
233
300
  {
234
301
  binding: fragment.uniforms,
235
302
  visibility: VISIBILITY_FRAGMENT,
236
303
  /*
237
- * Dynamic, because this block carries the material and a material changes between draws.
238
- * See `WebGPURenderer.perFrame` for why one buffer cannot hold two answers.
304
+ * Dynamic, because a pass's lights and camera change between passes of one frame — a
305
+ * mirror's, a capture's — and one buffer cannot hold two answers. See `WebGPURenderer.passRing`.
239
306
  */
240
307
  buffer: { type: 'uniform', hasDynamicOffset: true, minBindingSize: fragment.uniformSize },
241
308
  },
309
+ {
310
+ binding: materialBinding(fragment),
311
+ visibility: VISIBILITY_FRAGMENT,
312
+ /* Dynamic, because a material changes between draws. See `MaterialStore`. */
313
+ buffer: {
314
+ type: 'uniform',
315
+ hasDynamicOffset: true,
316
+ minBindingSize: fragment.materialSize ?? 0,
317
+ },
318
+ },
242
319
  ];
243
320
  /** What each sampler binding was declared as, so a shared one is declared once and agrees. */
244
321
  const samplerKind = new Map();
@@ -379,11 +456,33 @@ export function createFlatBindGroupLayout(device, variant, skinned = false, morp
379
456
  });
380
457
  }
381
458
  }
459
+ /* A bone animation's places, turns and the batch's clocks, read by index as the palette is. */
460
+ if (animated) {
461
+ for (const binding of animationBindings()) {
462
+ entries.push({
463
+ binding,
464
+ visibility: VISIBILITY_VERTEX,
465
+ texture: { sampleType: 'unfilterable-float' },
466
+ });
467
+ }
468
+ }
382
469
  return device.createBindGroupLayout({
383
- label: `flat.layout:${variant}|${flatVertexKey(skinned, morphed)}`,
470
+ label: `flat.layout:${variant}|${flatVertexKey(skinned, morphed, animated, animated)}`,
384
471
  entries,
385
472
  });
386
473
  }
474
+ /** The three textures a bone animation binds, in the order `AnimationViews` holds them. */
475
+ function animationBindings() {
476
+ const textures = flatVertexBindings(false, false, true, true).textures;
477
+ const at = (name) => {
478
+ const found = textures[name];
479
+ if (found === undefined) {
480
+ throw new Error(`flatPass: the animated vertex shader declares no ${name}`);
481
+ }
482
+ return found.texture;
483
+ };
484
+ return [at('uBonePlaces'), at('uBoneTurns'), at('uInstanceClocks')];
485
+ }
387
486
  /**
388
487
  * Bind the buffers and one view per declared texture.
389
488
  *
@@ -393,20 +492,27 @@ export function createFlatBindGroupLayout(device, variant, skinned = false, morp
393
492
  * and WebGPU still requires something there, which is why `renderer.ts` keeps an
394
493
  * `emptyTexture2D` for the same reason on the other side.
395
494
  */
396
- export function createFlatBindGroup(device, layout, variant, perDraw, perFrame, resolve,
495
+ export function createFlatBindGroup(device, layout, variant, perDraw, passes, materials, views, resolve,
397
496
  /** The palette view for a skinned bind group, or null for an unskinned one. */
398
497
  palette = null,
399
498
  /** The delta view for a morphed bind group, or null for an unmorphed one. */
400
499
  deltas = null,
401
500
  /** A skinned bind group's cloth views — the draw's, or the stand-in where it sets none. */
402
- cloth = null) {
501
+ cloth = null,
502
+ /** An animated instanced group's views. See `AnimationViews`. */
503
+ animation = null) {
403
504
  const fragment = flatFragmentBindings(variant);
404
505
  const entries = [
405
506
  {
406
507
  binding: FLAT_VERT_BINDING,
407
508
  resource: { buffer: perDraw, size: FLAT_VERT_SIZE },
408
509
  },
409
- { binding: fragment.uniforms, resource: { buffer: perFrame, size: fragment.uniformSize } },
510
+ { binding: FLAT_VIEW_BINDING, resource: { buffer: views, size: FLAT_VIEW_SIZE } },
511
+ { binding: fragment.uniforms, resource: { buffer: passes, size: fragment.uniformSize } },
512
+ {
513
+ binding: materialBinding(fragment),
514
+ resource: { buffer: materials, size: fragment.materialSize ?? 0 },
515
+ },
410
516
  ];
411
517
  /* The same dedupe the layout does, and for the same reason: the shadow bindings share one
412
518
  sampler, and a bind group that filled it fifteen times would be rejected. Every member
@@ -441,8 +547,14 @@ cloth = null) {
441
547
  entries.push({ binding: particlesAt, resource: cloth.particles });
442
548
  entries.push({ binding: restAt, resource: cloth.rest });
443
549
  }
550
+ if (animation !== null) {
551
+ const [places, turns, clocks] = animationBindings();
552
+ entries.push({ binding: places, resource: animation.places });
553
+ entries.push({ binding: turns, resource: animation.turns });
554
+ entries.push({ binding: clocks, resource: animation.clocks });
555
+ }
444
556
  return device.createBindGroup({
445
- label: `flat.bindGroup:${variant}|${flatVertexKey(palette !== null, deltas !== null)}`,
557
+ label: `flat.bindGroup:${variant}|${flatVertexKey(palette !== null, deltas !== null, animation !== null, animation !== null)}`,
446
558
  layout,
447
559
  entries,
448
560
  });
@@ -510,8 +622,10 @@ skinPass = 'whole',
510
622
  * The reflection pass's surface half: the draw again, against its own depth, into the two
511
623
  * targets `reflectionSurfacePass.ts` holds. The key must carry it: `|rs`.
512
624
  */
513
- reflectionSurface = false) {
514
- return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided, alphaToCoverage, skinEight, cloth, model, skinPass, reflectionSurface), true);
625
+ reflectionSurface = false,
626
+ /** An instanced pipeline that plays a bone animation. The key must carry it: `|anim`. */
627
+ animated = false) {
628
+ return cache.get(key, () => flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed, depthWrite, depthLayer, instanced, oit, doubleSided, alphaToCoverage, skinEight, cloth, model, skinPass, reflectionSurface, animated), true);
515
629
  }
516
630
  /**
517
631
  * The same pipeline, compiled off the main thread.
@@ -591,7 +705,9 @@ oit = 'none',
591
705
  /** Culls nothing: a surface seen from both faces. */
592
706
  doubleSided = false, alphaToCoverage = false, skinEight = false,
593
707
  /** Whether its vertex stage places vertices by a cloth binding. See `clothBinding.ts`. */
594
- cloth = false, model = null, skinPass = 'whole', reflectionSurface = false) {
708
+ cloth = false, model = null, skinPass = 'whole', reflectionSurface = false,
709
+ /** An instanced pipeline that plays a bone animation. See `shaders/boneAnimation.ts`. */
710
+ animated = false) {
595
711
  const offset = depthOffsetForLayer(depthLayer);
596
712
  /* The diffuse and colour halves are the same surface again, into the blur's targets; and so is
597
713
  the reflection pass's surface half, into its two. */
@@ -606,12 +722,12 @@ cloth = false, model = null, skinPass = 'whole', reflectionSurface = false) {
606
722
  * layout validates, draws, and is wrong, exactly as this file's header says.
607
723
  */
608
724
  module: shaderModule(device, {
609
- label: `flat.vert:${flatVertexKey(skinned, morphed, instanced)}`,
610
- code: FLAT_VERT_WGSL[flatVertexKey(skinned, morphed, instanced)] ?? '',
725
+ label: `flat.vert:${flatVertexKey(skinned, morphed, instanced, animated)}`,
726
+ code: FLAT_VERT_WGSL[flatVertexKey(skinned, morphed, instanced, animated)] ?? '',
611
727
  }),
612
728
  entryPoint: 'main',
613
729
  buffers: vertexBufferLayouts(present, instanced),
614
- constants: vertexConstants(flatVertexBindings(skinned, morphed, instanced), skinEight, cloth, model === 'lightmap'),
730
+ constants: vertexConstants(flatVertexBindings(skinned, morphed, instanced, animated), skinEight, cloth, model === 'lightmap'),
615
731
  },
616
732
  fragment: {
617
733
  module: shaderModule(device, {
@@ -1,6 +1,7 @@
1
1
  import type { CullSlot } from './instanceCullPass.ts';
2
2
  import type { MeshInstances } from '../../instances.ts';
3
3
  import type { GpuMesh } from './buffers.ts';
4
+ import { GpuInstanceClocks, type GpuBoneAnimation } from './boneAnimations.ts';
4
5
  /**
5
6
  * One mesh's per-instance buffer on the device.
6
7
  *
@@ -25,7 +26,19 @@ export declare class GpuInstancedBatch {
25
26
  /** The frame the slots were last taken in, and how many were taken. */
26
27
  slotFrame: number;
27
28
  slotsTaken: number;
28
- constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string, cull?: boolean);
29
+ /** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
30
+ readonly animation: GpuBoneAnimation | null;
31
+ readonly clocks: GpuInstanceClocks | null;
32
+ /**
33
+ * An animated batch's own groups — its lit twins by the material group they were built beside, its
34
+ * shadow groups by what they cut with — and the renderer's group epoch they were built in, after
35
+ * which every one is dropped. See `WebGPURenderer.animatedGroup`.
36
+ */
37
+ readonly litGroups: Map<GPUBindGroup, GPUBindGroup>;
38
+ readonly depthGroups: Map<unknown, GPUBindGroup>;
39
+ readonly depthPeelGroups: Map<unknown, GPUBindGroup>;
40
+ groupEpoch: number;
41
+ constructor(device: GPUDevice, mesh: GpuMesh, capacity: number, label: string, cull?: boolean, animation?: GpuBoneAnimation | null);
29
42
  /** Last frame's packed slots, for a reconstruction; made on first use by the renderer. */
30
43
  previousBuffer: GPUBuffer | null;
31
44
  /** How many slots the upload before the last one held, and how many the last one holds. */
@@ -1,6 +1,8 @@
1
1
  import { instancesBox } from '../../instanceCull.js';
2
2
  import { INSTANCE_FLOATS, INSTANCE_STRIDE, packInstances } from '../../instances.js';
3
3
  import { destroySlot } from './instanceCullPass.js';
4
+ import { packInstanceClocks } from '../../boneAnimation.js';
5
+ import { GpuInstanceClocks } from './boneAnimations.js';
4
6
  /** VERTEX | COPY_DST, spelled the way every other pass here spells it. */
5
7
  const USAGE_VERTEX = 0x0020 | 0x0008;
6
8
  /** A culling batch's source is read by the cull as storage too. */
@@ -29,10 +31,26 @@ export class GpuInstancedBatch {
29
31
  /** The frame the slots were last taken in, and how many were taken. */
30
32
  slotFrame = -1;
31
33
  slotsTaken = 0;
32
- constructor(device, mesh, capacity, label, cull = false) {
34
+ /** The bone animation every instance plays, and its clocks; null for a batch that plays none. */
35
+ animation;
36
+ clocks;
37
+ /**
38
+ * An animated batch's own groups — its lit twins by the material group they were built beside, its
39
+ * shadow groups by what they cut with — and the renderer's group epoch they were built in, after
40
+ * which every one is dropped. See `WebGPURenderer.animatedGroup`.
41
+ */
42
+ litGroups = new Map();
43
+ depthGroups = new Map();
44
+ depthPeelGroups = new Map();
45
+ groupEpoch = -1;
46
+ constructor(device, mesh, capacity, label, cull = false, animation = null) {
33
47
  this.mesh = mesh;
34
48
  this.capacity = capacity;
49
+ /* An animated batch is culled whole and never by instance: see `submitInstanced`. */
35
50
  this.cull = cull;
51
+ this.animation = animation;
52
+ this.clocks =
53
+ animation === null ? null : new GpuInstanceClocks(device, capacity, `${label}.clocks`);
36
54
  this.staging = new Float32Array(capacity * INSTANCE_FLOATS);
37
55
  this.buffer = device.createBuffer({
38
56
  /*
@@ -76,10 +94,15 @@ export class GpuInstancedBatch {
76
94
  return;
77
95
  packInstances(data, this.staging);
78
96
  queue.writeBuffer(this.buffer, 0, this.staging, 0, count * INSTANCE_FLOATS);
97
+ if (this.clocks !== null) {
98
+ packInstanceClocks(data, this.clocks.staging);
99
+ this.clocks.upload(queue, count);
100
+ }
79
101
  }
80
102
  dispose() {
81
103
  this.previousBuffer?.destroy();
82
104
  this.buffer.destroy();
105
+ this.clocks?.dispose();
83
106
  for (const slot of this.slots)
84
107
  destroySlot(slot);
85
108
  }