@driftengine/core 4.8.4 → 4.8.6

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 (138) hide show
  1. package/CHANGELOG.json +4332 -4250
  2. package/dist/build/shaderComments.js +0 -0
  3. package/dist/index.d.ts +10 -2
  4. package/dist/index.js +11 -1
  5. package/dist/render/ambientHarmonics.d.ts +46 -0
  6. package/dist/render/ambientHarmonics.js +80 -0
  7. package/dist/render/areaLights.d.ts +19 -0
  8. package/dist/render/areaLights.js +20 -0
  9. package/dist/render/backend/api.d.ts +13 -2
  10. package/dist/render/backend/webgl2/instanced.d.ts +2 -0
  11. package/dist/render/backend/webgl2/instanced.js +11 -2
  12. package/dist/render/backend/webgl2/renderer.d.ts +74 -1
  13. package/dist/render/backend/webgl2/renderer.js +229 -37
  14. package/dist/render/backend/webgl2/skinScatter.d.ts +14 -0
  15. package/dist/render/backend/webgl2/skinScatter.js +88 -26
  16. package/dist/render/backend/webgpu/buffers.js +2 -1
  17. package/dist/render/backend/webgpu/clusterBinner.js +3 -2
  18. package/dist/render/backend/webgpu/flatPass.d.ts +7 -5
  19. package/dist/render/backend/webgpu/flatPass.js +15 -13
  20. package/dist/render/backend/webgpu/particlePass.d.ts +37 -4
  21. package/dist/render/backend/webgpu/particlePass.js +80 -12
  22. package/dist/render/backend/webgpu/postPass.d.ts +6 -1
  23. package/dist/render/backend/webgpu/postPass.js +7 -2
  24. package/dist/render/backend/webgpu/renderer.d.ts +58 -7
  25. package/dist/render/backend/webgpu/renderer.js +329 -78
  26. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  27. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +41 -1
  28. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +40 -28
  29. package/dist/render/backend/webgpu/skinPaletteRing.js +69 -32
  30. package/dist/render/backend/webgpu/skinScatterPass.d.ts +10 -2
  31. package/dist/render/backend/webgpu/skinScatterPass.js +59 -9
  32. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +2 -1
  33. package/dist/render/backend/webgpu/surfaceTexturePass.js +18 -4
  34. package/dist/render/bloomChain.d.ts +44 -0
  35. package/dist/render/bloomChain.js +28 -0
  36. package/dist/render/bloomPass.d.ts +3 -1
  37. package/dist/render/bloomPass.js +16 -2
  38. package/dist/render/clusteredLights.d.ts +55 -1
  39. package/dist/render/clusteredLights.js +182 -8
  40. package/dist/render/fixtureAtlas.js +1 -1
  41. package/dist/render/instanceCull.js +7 -0
  42. package/dist/render/instances.d.ts +32 -10
  43. package/dist/render/instances.js +17 -8
  44. package/dist/render/lightBudget.d.ts +26 -1
  45. package/dist/render/lightBudget.js +26 -3
  46. package/dist/render/lightmap.d.ts +102 -0
  47. package/dist/render/lightmap.js +118 -0
  48. package/dist/render/mesh.js +2 -1
  49. package/dist/render/particleBatch.d.ts +59 -3
  50. package/dist/render/particleBatch.js +53 -2
  51. package/dist/render/particleMaterial.d.ts +6 -1
  52. package/dist/render/particleMaterial.js +10 -2
  53. package/dist/render/particlePool.d.ts +11 -0
  54. package/dist/render/particlePool.js +2 -0
  55. package/dist/render/particleSort.d.ts +34 -0
  56. package/dist/render/particleSort.js +96 -0
  57. package/dist/render/sceneTarget.d.ts +5 -0
  58. package/dist/render/sceneTarget.js +8 -1
  59. package/dist/render/shaders/bloom.d.ts +8 -4
  60. package/dist/render/shaders/bloom.js +20 -3
  61. package/dist/render/shaders/flat/areaLight.d.ts +10 -0
  62. package/dist/render/shaders/flat/areaLight.js +96 -0
  63. package/dist/render/shaders/flat/index.js +30 -4
  64. package/dist/render/shaders/flat/main.d.ts +1 -1
  65. package/dist/render/shaders/flat/main.js +107 -105
  66. package/dist/render/shaders/flat/models.d.ts +16 -4
  67. package/dist/render/shaders/flat/models.js +64 -4
  68. package/dist/render/shaders/flat/normalMap.d.ts +10 -0
  69. package/dist/render/shaders/flat/normalMap.js +25 -0
  70. package/dist/render/shaders/flat/preamble.js +74 -37
  71. package/dist/render/shaders/flat/skin.js +6 -5
  72. package/dist/render/shaders/generated/bloom.wgsl.d.ts +13 -3
  73. package/dist/render/shaders/generated/bloom.wgsl.js +13 -3
  74. package/dist/render/shaders/generated/flat.wgsl.d.ts +1631 -2044
  75. package/dist/render/shaders/generated/flat.wgsl.js +1669 -2082
  76. package/dist/render/shaders/generated/particle.wgsl.d.ts +115 -1
  77. package/dist/render/shaders/generated/particle.wgsl.js +115 -1
  78. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +12 -2
  79. package/dist/render/shaders/generated/skinBlur.wgsl.js +12 -2
  80. package/dist/render/shaders/particle.d.ts +17 -1
  81. package/dist/render/shaders/particle.js +133 -1
  82. package/dist/render/shaders/skinBlur.js +25 -3
  83. package/dist/render/skinBlur.d.ts +8 -5
  84. package/dist/render/skinBlur.js +8 -5
  85. package/dist/render/surfaceModel.d.ts +21 -1
  86. package/dist/render/surfaceModel.js +22 -0
  87. package/dist/render/surfaceTexture.d.ts +14 -1
  88. package/dist/render/surfaceTexture.js +15 -2
  89. package/package.json +6 -6
  90. package/src/build/shaderComments.ts +0 -0
  91. package/src/index.ts +20 -1
  92. package/src/render/ambientHarmonics.ts +92 -0
  93. package/src/render/areaLights.ts +39 -0
  94. package/src/render/backend/api.ts +14 -1
  95. package/src/render/backend/webgl2/instanced.ts +12 -2
  96. package/src/render/backend/webgl2/renderer.ts +292 -35
  97. package/src/render/backend/webgl2/skinScatter.ts +120 -43
  98. package/src/render/backend/webgpu/buffers.ts +2 -1
  99. package/src/render/backend/webgpu/clusterBinner.ts +3 -1
  100. package/src/render/backend/webgpu/flatPass.ts +18 -13
  101. package/src/render/backend/webgpu/particlePass.ts +118 -12
  102. package/src/render/backend/webgpu/postPass.ts +7 -2
  103. package/src/render/backend/webgpu/renderer.ts +396 -95
  104. package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +41 -1
  105. package/src/render/backend/webgpu/skinPaletteRing.ts +65 -33
  106. package/src/render/backend/webgpu/skinScatterPass.ts +59 -9
  107. package/src/render/backend/webgpu/surfaceTexturePass.ts +26 -5
  108. package/src/render/bloomChain.ts +69 -0
  109. package/src/render/bloomPass.ts +26 -2
  110. package/src/render/clusteredLights.ts +221 -7
  111. package/src/render/fixtureAtlas.ts +1 -1
  112. package/src/render/instanceCull.ts +5 -0
  113. package/src/render/instances.ts +40 -11
  114. package/src/render/lightBudget.ts +48 -2
  115. package/src/render/lightmap.ts +202 -0
  116. package/src/render/mesh.ts +2 -1
  117. package/src/render/particleBatch.ts +95 -4
  118. package/src/render/particleMaterial.ts +11 -1
  119. package/src/render/particlePool.ts +13 -0
  120. package/src/render/particleSort.ts +133 -0
  121. package/src/render/sceneTarget.ts +18 -1
  122. package/src/render/shaders/bloom.ts +20 -3
  123. package/src/render/shaders/flat/areaLight.ts +96 -0
  124. package/src/render/shaders/flat/index.ts +30 -4
  125. package/src/render/shaders/flat/main.ts +107 -105
  126. package/src/render/shaders/flat/models.ts +73 -8
  127. package/src/render/shaders/flat/normalMap.ts +25 -0
  128. package/src/render/shaders/flat/preamble.ts +74 -37
  129. package/src/render/shaders/flat/skin.ts +6 -5
  130. package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
  131. package/src/render/shaders/generated/flat.wgsl.ts +1669 -2082
  132. package/src/render/shaders/generated/particle.wgsl.ts +116 -1
  133. package/src/render/shaders/generated/skinBlur.wgsl.ts +12 -2
  134. package/src/render/shaders/particle.ts +134 -1
  135. package/src/render/shaders/skinBlur.ts +25 -3
  136. package/src/render/skinBlur.ts +8 -5
  137. package/src/render/surfaceModel.ts +36 -1
  138. package/src/render/surfaceTexture.ts +41 -3
@@ -5,14 +5,19 @@
5
5
  *
6
6
  * **Drawn as it goes, not kept**: this backend issues a draw when it is asked for, and the state a
7
7
  * skin draw reads — its joint palette, its uniforms — is rewritten by the draw after it. So the
8
- * renderer draws each skin twice in place, the frame's half and then at once its diffuse half into
9
- * this target, which shares the frame's depth and tests for equality against it.
8
+ * renderer draws each skin three times in place: the frame's half, then at once the diffuse's light
9
+ * alone into this target and its colour alone into the other, both sharing the frame's depth and
10
+ * testing for equality against it. The blur spreads the light and multiplies it by the colour.
10
11
  *
11
12
  * **The units it reads are its own** — `SKIN_BLUR_TEXTURE_UNIT` and the one after — past every
12
13
  * unit a lit program binds, because it runs in the middle of a frame and the draws after it must
13
14
  * find their textures where they left them.
14
15
  */
15
- import { SKIN_BLUR_DEPTH_UNIT, SKIN_BLUR_TEXTURE_UNIT } from '../../lightBudget.ts';
16
+ import {
17
+ SKIN_BLUR_ALBEDO_UNIT,
18
+ SKIN_BLUR_DEPTH_UNIT,
19
+ SKIN_BLUR_TEXTURE_UNIT,
20
+ } from '../../lightBudget.ts';
16
21
  import { blendKeeping } from '../../sceneCoverage.ts';
17
22
  import type { SceneTarget } from '../../sceneTarget.ts';
18
23
  import { compileProgram, uniformLocations } from '../../shader.ts';
@@ -33,9 +38,13 @@ export class GlSkinScatter {
33
38
  private readonly depthToViewZ = new Float32Array(4);
34
39
  private diffuse: WebGLTexture | null = null;
35
40
  private across: WebGLTexture | null = null;
41
+ private albedo: WebGLTexture | null = null;
36
42
  private multisampled: WebGLRenderbuffer | null = null;
43
+ private albedoMultisampled: WebGLRenderbuffer | null = null;
37
44
  private drawFramebuffer: WebGLFramebuffer | null = null;
38
45
  private resolveFramebuffer: WebGLFramebuffer | null = null;
46
+ private albedoFramebuffer: WebGLFramebuffer | null = null;
47
+ private albedoResolveFramebuffer: WebGLFramebuffer | null = null;
39
48
  private acrossFramebuffer: WebGLFramebuffer | null = null;
40
49
  private width = 0;
41
50
  private height = 0;
@@ -44,6 +53,7 @@ export class GlSkinScatter {
44
53
  /** Refused once and for good: a driver that will not render to half floats. */
45
54
  private unavailable = false;
46
55
  private cleared = false;
56
+ private albedoCleared = false;
47
57
  private drawn = false;
48
58
 
49
59
  constructor(gl: WebGL2RenderingContext) {
@@ -115,6 +125,17 @@ export class GlSkinScatter {
115
125
  this.drawn = true;
116
126
  }
117
127
 
128
+ /** Bind the target for the same skin's colour half, cleared the first time a frame does. */
129
+ beginAlbedo(): void {
130
+ const { gl } = this;
131
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.albedoFramebuffer);
132
+ gl.viewport(0, 0, this.width, this.height);
133
+ if (!this.albedoCleared) {
134
+ gl.clearBufferfv(gl.COLOR, 0, CLEAR);
135
+ this.albedoCleared = true;
136
+ }
137
+ }
138
+
118
139
  /**
119
140
  * Across into the second target, then down and added into the frame, which `scene` binds. `depth`
120
141
  * is the frame's depth as a single-sampled texture, or null where none could be had — and then
@@ -129,20 +150,8 @@ export class GlSkinScatter {
129
150
  ): void {
130
151
  const { gl } = this;
131
152
  if (this.multisampled !== null) {
132
- gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.drawFramebuffer);
133
- gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.resolveFramebuffer);
134
- gl.blitFramebuffer(
135
- 0,
136
- 0,
137
- this.width,
138
- this.height,
139
- 0,
140
- 0,
141
- this.width,
142
- this.height,
143
- gl.COLOR_BUFFER_BIT,
144
- gl.NEAREST,
145
- );
153
+ this.resolve(this.drawFramebuffer, this.resolveFramebuffer);
154
+ this.resolve(this.albedoFramebuffer, this.albedoResolveFramebuffer);
146
155
  }
147
156
  gl.disable(gl.DEPTH_TEST);
148
157
  gl.disable(gl.CULL_FACE);
@@ -153,6 +162,9 @@ export class GlSkinScatter {
153
162
  const u = this.uniforms;
154
163
  gl.uniform1i(u['uSkin'] ?? null, SKIN_BLUR_TEXTURE_UNIT);
155
164
  gl.uniform1i(u['uDepth'] ?? null, SKIN_BLUR_DEPTH_UNIT);
165
+ gl.uniform1i(u['uAlbedo'] ?? null, SKIN_BLUR_ALBEDO_UNIT);
166
+ gl.activeTexture(gl.TEXTURE0 + SKIN_BLUR_ALBEDO_UNIT);
167
+ gl.bindTexture(gl.TEXTURE_2D, this.albedo ?? empty);
156
168
  /* The four terms that carry a depth back to view-space metres: the occlusion blur's own. */
157
169
  this.depthToViewZ[0] = (inverse[10] as number) ?? 0;
158
170
  this.depthToViewZ[1] = (inverse[14] as number) ?? 0;
@@ -175,6 +187,8 @@ export class GlSkinScatter {
175
187
  this.step[0] = 1 / this.width;
176
188
  this.step[1] = 0;
177
189
  gl.uniform2fv(u['uStep'] ?? null, this.step);
190
+ /* The light spreads on both axes; the colour is applied once, on the one adding into the frame. */
191
+ gl.uniform1f(u['uApplyAlbedo'] ?? null, 0);
178
192
  gl.drawArrays(gl.TRIANGLES, 0, 3);
179
193
 
180
194
  scene.bind();
@@ -182,6 +196,7 @@ export class GlSkinScatter {
182
196
  this.step[0] = 0;
183
197
  this.step[1] = 1 / this.height;
184
198
  gl.uniform2fv(u['uStep'] ?? null, this.step);
199
+ gl.uniform1f(u['uApplyAlbedo'] ?? null, 1);
185
200
  /* Added: its colour onto the frame's, the frame's alpha kept. */
186
201
  gl.enable(gl.BLEND);
187
202
  gl.blendEquation(gl.FUNC_ADD);
@@ -199,9 +214,29 @@ export class GlSkinScatter {
199
214
  /** Spent at the frame's start: what was drawn into the target was last frame's. */
200
215
  reset(): void {
201
216
  this.cleared = false;
217
+ this.albedoCleared = false;
202
218
  this.drawn = false;
203
219
  }
204
220
 
221
+ /** A multisampled target into its single-sampled texture. */
222
+ private resolve(from: WebGLFramebuffer | null, to: WebGLFramebuffer | null): void {
223
+ const { gl } = this;
224
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, from);
225
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, to);
226
+ gl.blitFramebuffer(
227
+ 0,
228
+ 0,
229
+ this.width,
230
+ this.height,
231
+ 0,
232
+ 0,
233
+ this.width,
234
+ this.height,
235
+ gl.COLOR_BUFFER_BIT,
236
+ gl.NEAREST,
237
+ );
238
+ }
239
+
205
240
  dispose(): void {
206
241
  const { gl } = this;
207
242
  this.release();
@@ -226,19 +261,59 @@ export class GlSkinScatter {
226
261
  this.sharedDepth = depthRenderbuffer ?? depthTexture;
227
262
  this.diffuse = this.halfFloatTexture(width, height);
228
263
  this.across = this.halfFloatTexture(width, height);
264
+ this.albedo = this.halfFloatTexture(width, height);
229
265
 
230
- this.drawFramebuffer = gl.createFramebuffer();
231
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.drawFramebuffer);
232
- if (samples > 1 && depthRenderbuffer !== null) {
233
- this.multisampled = gl.createRenderbuffer();
234
- gl.bindRenderbuffer(gl.RENDERBUFFER, this.multisampled);
235
- gl.renderbufferStorageMultisample(gl.RENDERBUFFER, samples, gl.RGBA16F, width, height);
236
- gl.framebufferRenderbuffer(
237
- gl.FRAMEBUFFER,
238
- gl.COLOR_ATTACHMENT0,
266
+ const multisampled = samples > 1 && depthRenderbuffer !== null;
267
+ const diffuse = this.drawTarget(this.diffuse, multisampled, depthTexture, depthRenderbuffer);
268
+ this.drawFramebuffer = diffuse.framebuffer;
269
+ this.multisampled = diffuse.multisampled;
270
+ this.resolveFramebuffer = diffuse.resolve;
271
+ const albedo = this.drawTarget(this.albedo, multisampled, depthTexture, depthRenderbuffer);
272
+ this.albedoFramebuffer = albedo.framebuffer;
273
+ this.albedoMultisampled = albedo.multisampled;
274
+ this.albedoResolveFramebuffer = albedo.resolve;
275
+
276
+ this.acrossFramebuffer = gl.createFramebuffer();
277
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.acrossFramebuffer);
278
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.across, 0);
279
+ const acrossReady = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
280
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
281
+ if (diffuse.ready && albedo.ready && acrossReady) return true;
282
+ this.release();
283
+ this.refuse();
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * A framebuffer a skin half draws into against the frame's depth: the texture itself, or a
289
+ * multisampled renderbuffer and the framebuffer that resolves it into the texture.
290
+ */
291
+ private drawTarget(
292
+ texture: WebGLTexture | null,
293
+ multisampled: boolean,
294
+ depthTexture: WebGLTexture | null,
295
+ depthRenderbuffer: WebGLRenderbuffer | null,
296
+ ): {
297
+ framebuffer: WebGLFramebuffer | null;
298
+ multisampled: WebGLRenderbuffer | null;
299
+ resolve: WebGLFramebuffer | null;
300
+ ready: boolean;
301
+ } {
302
+ const { gl } = this;
303
+ const framebuffer = gl.createFramebuffer();
304
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
305
+ let samples: WebGLRenderbuffer | null = null;
306
+ if (multisampled && depthRenderbuffer !== null) {
307
+ samples = gl.createRenderbuffer();
308
+ gl.bindRenderbuffer(gl.RENDERBUFFER, samples);
309
+ gl.renderbufferStorageMultisample(
239
310
  gl.RENDERBUFFER,
240
- this.multisampled,
311
+ this.samples,
312
+ gl.RGBA16F,
313
+ this.width,
314
+ this.height,
241
315
  );
316
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, samples);
242
317
  gl.framebufferRenderbuffer(
243
318
  gl.FRAMEBUFFER,
244
319
  gl.DEPTH_ATTACHMENT,
@@ -246,25 +321,17 @@ export class GlSkinScatter {
246
321
  depthRenderbuffer,
247
322
  );
248
323
  } else {
249
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.diffuse, 0);
324
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
250
325
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
251
326
  }
252
- const drawReady = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
253
-
254
- if (this.multisampled !== null) {
255
- this.resolveFramebuffer = gl.createFramebuffer();
256
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.resolveFramebuffer);
257
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.diffuse, 0);
327
+ const ready = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
328
+ let resolve: WebGLFramebuffer | null = null;
329
+ if (samples !== null) {
330
+ resolve = gl.createFramebuffer();
331
+ gl.bindFramebuffer(gl.FRAMEBUFFER, resolve);
332
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
258
333
  }
259
- this.acrossFramebuffer = gl.createFramebuffer();
260
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.acrossFramebuffer);
261
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.across, 0);
262
- const acrossReady = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
263
- gl.bindRenderbuffer(gl.RENDERBUFFER, null);
264
- if (drawReady && acrossReady) return true;
265
- this.release();
266
- this.refuse();
267
- return false;
334
+ return { framebuffer, multisampled: samples, resolve, ready };
268
335
  }
269
336
 
270
337
  private refuse(): void {
@@ -294,16 +361,26 @@ export class GlSkinScatter {
294
361
  const { gl } = this;
295
362
  if (this.drawFramebuffer !== null) gl.deleteFramebuffer(this.drawFramebuffer);
296
363
  if (this.resolveFramebuffer !== null) gl.deleteFramebuffer(this.resolveFramebuffer);
364
+ if (this.albedoFramebuffer !== null) gl.deleteFramebuffer(this.albedoFramebuffer);
365
+ if (this.albedoResolveFramebuffer !== null) {
366
+ gl.deleteFramebuffer(this.albedoResolveFramebuffer);
367
+ }
297
368
  if (this.acrossFramebuffer !== null) gl.deleteFramebuffer(this.acrossFramebuffer);
298
369
  if (this.multisampled !== null) gl.deleteRenderbuffer(this.multisampled);
370
+ if (this.albedoMultisampled !== null) gl.deleteRenderbuffer(this.albedoMultisampled);
299
371
  if (this.diffuse !== null) gl.deleteTexture(this.diffuse);
300
372
  if (this.across !== null) gl.deleteTexture(this.across);
373
+ if (this.albedo !== null) gl.deleteTexture(this.albedo);
301
374
  this.drawFramebuffer = null;
302
375
  this.resolveFramebuffer = null;
376
+ this.albedoFramebuffer = null;
377
+ this.albedoResolveFramebuffer = null;
303
378
  this.acrossFramebuffer = null;
304
379
  this.multisampled = null;
380
+ this.albedoMultisampled = null;
305
381
  this.diffuse = null;
306
382
  this.across = null;
383
+ this.albedo = null;
307
384
  this.sharedDepth = null;
308
385
  }
309
386
  }
@@ -766,7 +766,8 @@ const INSTANCE_ATTRIBUTES: GPUVertexAttribute[] = [
766
766
  { shaderLocation: 12, offset: 16, format: 'float32x4' },
767
767
  { shaderLocation: 13, offset: 32, format: 'float32x4' },
768
768
  { shaderLocation: 14, offset: 48, format: 'float32x4' },
769
- { shaderLocation: 15, offset: 64, format: 'float32x3' },
769
+ /* The tint and, in w, the opacity. See `INSTANCE_FLOATS`. */
770
+ { shaderLocation: 15, offset: 64, format: 'float32x4' },
770
771
  ];
771
772
 
772
773
  /** The locations above, as the set the base layout must stand clear of on an instanced draw. */
@@ -5,6 +5,7 @@ import {
5
5
  TABLE_HEIGHT,
6
6
  TABLE_WIDTH,
7
7
  type ClusterLightSet,
8
+ clusteredLightTotal,
8
9
  writeLightRecord,
9
10
  } from '../../clusteredLights.ts';
10
11
  import { MAX_POINT_LIGHTS } from '../../lightBudget.ts';
@@ -167,7 +168,8 @@ export class ClusterBinner {
167
168
  if (frame === null || device === null || pipeline === null || group === null) return;
168
169
  if (params === null || lightBuffer === null) return;
169
170
 
170
- const count = Math.min(frame.lights.count, MAX_CLUSTERED_LIGHTS);
171
+ /* The lamps and the clustered rectangles after them. See `ClusterLightSet.areas`. */
172
+ const count = clusteredLightTotal(frame.lights);
171
173
 
172
174
  /*
173
175
  * The records, by the function `buildLightClusters` writes them with, because the shader
@@ -1,6 +1,6 @@
1
1
  import { SURFACE_MODEL_SWITCH } from '../../surfaceModel.ts';
2
2
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
3
- import type { SkinHalf } from '../../shaders/flat/models.ts';
3
+ import { SKIN_HALF_SWITCH, type SkinHalf } from '../../shaders/flat/models.ts';
4
4
  import { DEPTH_COMPARE, DEPTH_FORMAT, depthOffsetForLayer } from '../../depthConvention.ts';
5
5
  import {
6
6
  FLAT_BINDINGS,
@@ -153,8 +153,8 @@ export function flatVertexBindings(
153
153
  const PLAIN_VERTEX = flatVertexBindings(false);
154
154
 
155
155
  /**
156
- * A vertex stage's constants, keyed by each override's **id** as the fragment's are: `SKIN_EIGHT` in
157
- * a skinned stage, nothing in any other. Every override a stage declares is set, and one this does
156
+ * A vertex stage's constants, keyed by each override's **id** as the fragment's are: `SKIN_EIGHT` and
157
+ * `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
158
158
  * not know is refused here rather than defaulted. Shared with the shadow and glass-tint casters,
159
159
  * whose skinned stages declare the same switch.
160
160
  */
@@ -162,11 +162,14 @@ export function vertexConstants(
162
162
  bindings: StageBindings,
163
163
  skinEight: boolean,
164
164
  cloth = false,
165
+ /** Whether an instanced stage reads its tint lane as a lightmap region. See `lightmap.ts`. */
166
+ lightmapRegions = false,
165
167
  ): Record<string, number> {
166
168
  const constants: Record<string, number> = {};
167
169
  for (const [name, id] of Object.entries(bindings.overrides ?? {})) {
168
170
  if (name === 'SKIN_EIGHT') constants[String(id)] = skinEight ? 1 : 0;
169
171
  else if (name === 'CLOTH_BOUND') constants[String(id)] = cloth ? 1 : 0;
172
+ else if (name === 'LIGHTMAP_REGIONS') constants[String(id)] = lightmapRegions ? 1 : 0;
170
173
  else throw new Error(`flatPass: a vertex stage declares a switch "${name}" nothing sets`);
171
174
  }
172
175
  return constants;
@@ -263,13 +266,9 @@ function flatFragmentConstants(
263
266
  constants[String(id)] = name === modelSwitch ? 1 : 0;
264
267
  continue;
265
268
  }
266
- /* Skin's two halves under the screen-space blur, set from the key as a model's switch is. */
267
- if (name === 'SKIN_SCREEN' || name === 'SKIN_DIFFUSE') {
268
- constants[String(id)] =
269
- (name === 'SKIN_SCREEN' && skinPass === 'scene') ||
270
- (name === 'SKIN_DIFFUSE' && skinPass === 'diffuse')
271
- ? 1
272
- : 0;
269
+ /* Skin's halves under the screen-space blur, set from the key as a model's switch is. */
270
+ if (name === 'SKIN_SCREEN' || name === 'SKIN_DIFFUSE' || name === 'SKIN_ALBEDO') {
271
+ constants[String(id)] = skinPass !== 'whole' && name === SKIN_HALF_SWITCH[skinPass] ? 1 : 0;
273
272
  continue;
274
273
  }
275
274
  if (!(name in cache.litSwitches)) {
@@ -608,7 +607,7 @@ export function flatPipeline(
608
607
  cloth = false,
609
608
  /** Which shading model its fragment stage shades by, null the standard. The key must carry it. */
610
609
  model: SurfaceModelKind | null = null,
611
- /** Which half of a skin it draws. The key must carry it: `|ss` or `|sd`. See `SkinHalf`. */
610
+ /** Which half of a skin it draws. The key must carry it: `|ss`, `|sd` or `|sa`. See `SkinHalf`. */
612
611
  skinPass: SkinHalf = 'whole',
613
612
  ): GPURenderPipeline {
614
613
  return cache.get(
@@ -768,7 +767,8 @@ function flatDescriptor(
768
767
  skinPass: SkinHalf = 'whole',
769
768
  ): GPURenderPipelineDescriptor {
770
769
  const offset = depthOffsetForLayer(depthLayer);
771
- const diffuseAlone = skinPass === 'diffuse';
770
+ /* The diffuse and colour halves are the same surface again, into the blur's targets. */
771
+ const diffuseAlone = skinPass === 'diffuse' || skinPass === 'albedo';
772
772
  return {
773
773
  label: key,
774
774
  layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
@@ -784,7 +784,12 @@ function flatDescriptor(
784
784
  }),
785
785
  entryPoint: 'main',
786
786
  buffers: vertexBufferLayouts(present, instanced),
787
- constants: vertexConstants(flatVertexBindings(skinned, morphed, instanced), skinEight, cloth),
787
+ constants: vertexConstants(
788
+ flatVertexBindings(skinned, morphed, instanced),
789
+ skinEight,
790
+ cloth,
791
+ model === 'lightmap',
792
+ ),
788
793
  },
789
794
  fragment: {
790
795
  module: shaderModule(device, {
@@ -3,7 +3,8 @@ import { particleShaders, type ParticleMaterial } from '../../particleMaterial.t
3
3
  import { PARTICLE_BINDINGS } from '../../shaders/generated/particle.wgsl.ts';
4
4
  import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from '../../shaders/particle.ts';
5
5
  import type { ParticleInstances } from '../../particlePool.ts';
6
- import type { ParticleBlend, ParticleFacing } from '../../particleBatch.ts';
6
+ import { packSpriteStream, type ParticleBlend, type ParticleFacing } from '../../particleBatch.ts';
7
+ import { createParticleSort, type ParticleSort } from '../../particleSort.ts';
7
8
  import { DEPTH_FORMAT } from './flatPass.ts';
8
9
  import type { PipelineCache } from './pipelineCache.ts';
9
10
  import type { UniformFields } from './scatterPass.ts';
@@ -34,7 +35,9 @@ export function particleFragBindings(material: ParticleMaterial): {
34
35
  ? PARTICLE_BINDINGS.PARTICLE_SPARK_FRAG
35
36
  : material === 'mote'
36
37
  ? PARTICLE_BINDINGS.PARTICLE_MOTE_FRAG
37
- : PARTICLE_BINDINGS.PARTICLE_SMOKE_FRAG;
38
+ : material === 'sprite'
39
+ ? PARTICLE_BINDINGS.PARTICLE_SPRITE_FRAG
40
+ : PARTICLE_BINDINGS.PARTICLE_SMOKE_FRAG;
38
41
  return { fields: stage.fields as UniformFields, size: stage.uniformSize };
39
42
  }
40
43
 
@@ -45,17 +48,17 @@ const USAGE_INDEX = 0x0010 | 0x0008;
45
48
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
46
49
 
47
50
  /**
48
- * Ten attributes and eight slots, so the per-instance eight share one buffer.
51
+ * Eleven attributes and eight slots, so the per-instance nine share one buffer.
49
52
  *
50
- * **`maxVertexBuffers` is eight and this shader declares ten inputs**, so something has to
51
- * interleave. The per-instance streams are the ones that do: the engine holds them as eight
52
- * separate arrays and they are packed into one interleaved buffer per frame, which is a copy
53
- * the upload was already paying per stream rather than new work.
53
+ * **`maxVertexBuffers` is eight and this shader declares eleven inputs**, so something has to
54
+ * interleave. The per-instance streams are the ones that do: the engine holds them as separate
55
+ * arrays and they are packed into one interleaved buffer per frame, which is a copy the upload
56
+ * was already paying per stream rather than new work.
54
57
  *
55
58
  * Floats per instance, in the order they are packed: position 3, size 1, spin 1, colour 3,
56
- * alpha 1, age 1, seed 1, velocity 3.
59
+ * alpha 1, age 1, seed 1, velocity 3, and a sprite's frame and half-height 2.
57
60
  */
58
- const INSTANCE_FLOATS = 14;
61
+ const INSTANCE_FLOATS = 16;
59
62
 
60
63
  const INSTANCE_ATTRIBUTES: GPUVertexAttribute[] = [
61
64
  { shaderLocation: 2, offset: 0, format: 'float32x3' },
@@ -66,6 +69,7 @@ const INSTANCE_ATTRIBUTES: GPUVertexAttribute[] = [
66
69
  { shaderLocation: 7, offset: 36, format: 'float32' },
67
70
  { shaderLocation: 8, offset: 40, format: 'float32' },
68
71
  { shaderLocation: 9, offset: 44, format: 'float32x3' },
72
+ { shaderLocation: 10, offset: 56, format: 'float32x2' },
69
73
  ];
70
74
 
71
75
  export function particleVertexLayouts(): GPUVertexBufferLayout[] {
@@ -102,8 +106,22 @@ export interface GpuParticles {
102
106
  readonly coreGain: number;
103
107
  /** Whether the mote fragment stage mixes toward the medium. Ignored by the other materials. */
104
108
  readonly fog: boolean;
109
+ /** A sprite's image view, or null for a procedural material. See `ParticleBatchOptions`. */
110
+ readonly texture: GPUTextureView | null;
111
+ readonly cells: readonly [number, number];
112
+ readonly blendCells: boolean;
113
+ readonly softDepth: number;
114
+ readonly cameraFade: number;
115
+ readonly cameraOffset: number;
116
+ /** Room to draw farthest first, where the batch asked. See `particleSort.ts`. */
117
+ readonly order: ParticleSort | null;
118
+ /** A sprite's bind group, which holds the frame's depth copy and is rebuilt when that moves. */
119
+ spriteGroup: GPUBindGroup | null;
120
+ spriteGroupDepth: GPUTextureView | null;
105
121
  /** Reused every frame; see `packInstances`. */
106
122
  readonly staging: Float32Array;
123
+ /** The sprite stream, two floats a particle, before it is woven into `staging`. */
124
+ readonly spriteStaging: Float32Array;
107
125
  /**
108
126
  * This batch's own uniform buffers, staging and bind group — never shared with another
109
127
  * batch of the same material. See the note on `createGpuParticles` for why: two pools of one
@@ -118,7 +136,8 @@ export interface GpuParticles {
118
136
  readonly fragFloats: Float32Array;
119
137
  readonly fragInts: Int32Array;
120
138
  readonly fields: UniformFields;
121
- readonly bindGroup: GPUBindGroup;
139
+ /** The procedural materials' group; a sprite's holds the depth copy, so it is `spriteGroup`. */
140
+ readonly bindGroup: GPUBindGroup | null;
122
141
  /**
123
142
  * The encoder this batch was last drawn into, by the renderer's count, or -1.
124
143
  *
@@ -160,6 +179,7 @@ export function createGpuParticles(
160
179
  layout: GPUBindGroupLayout,
161
180
  fragFields: UniformFields,
162
181
  fragSize: number,
182
+ sprite: SpriteOptions | null = null,
163
183
  ): GpuParticles {
164
184
  /* The cross, built once: two blades of four corners, interleaved with the blade index. */
165
185
  const corners = new Float32Array(PARTICLE_VERTS * 3);
@@ -236,7 +256,17 @@ export function createGpuParticles(
236
256
  erosion,
237
257
  coreGain,
238
258
  fog,
259
+ texture: sprite?.texture ?? null,
260
+ cells: sprite?.cells ?? [1, 1],
261
+ blendCells: sprite?.blendCells ?? false,
262
+ softDepth: sprite?.softDepth ?? 0,
263
+ cameraFade: sprite?.cameraFade ?? 0,
264
+ cameraOffset: sprite?.cameraOffset ?? 0,
265
+ order: sprite?.sort === true ? createParticleSort(capacity) : null,
266
+ spriteGroup: null,
267
+ spriteGroupDepth: null,
239
268
  staging: new Float32Array(capacity * INSTANCE_FLOATS),
269
+ spriteStaging: new Float32Array(capacity * 2),
240
270
  vertUniforms,
241
271
  fragUniforms,
242
272
  vertStaging,
@@ -245,7 +275,10 @@ export function createGpuParticles(
245
275
  fragFloats: new Float32Array(fragStaging),
246
276
  fragInts: new Int32Array(fragStaging),
247
277
  fields: fragFields,
248
- bindGroup: createParticleBindGroup(device, layout, vertUniforms, fragUniforms, fragSize),
278
+ bindGroup:
279
+ material === 'sprite'
280
+ ? null
281
+ : createParticleBindGroup(device, layout, vertUniforms, fragUniforms, fragSize),
249
282
  dispose(): void {
250
283
  cornerBuffer.destroy();
251
284
  instances.destroy();
@@ -265,6 +298,8 @@ export function createGpuParticles(
265
298
  export function packInstances(batch: GpuParticles, data: ParticleInstances): number {
266
299
  const n = Math.min(data.count, batch.capacity);
267
300
  const out = batch.staging;
301
+ /* The sprite's frame and half-height, two floats a particle, woven in below. */
302
+ packSpriteStream(data, n, batch.spriteStaging);
268
303
  for (let i = 0; i < n; i++) {
269
304
  const o = i * INSTANCE_FLOATS;
270
305
  const p = i * 3;
@@ -282,16 +317,34 @@ export function packInstances(batch: GpuParticles, data: ParticleInstances): num
282
317
  out[o + 11] = data.velocities[p] as number;
283
318
  out[o + 12] = data.velocities[p + 1] as number;
284
319
  out[o + 13] = data.velocities[p + 2] as number;
320
+ out[o + 14] = batch.spriteStaging[i * 2] as number;
321
+ out[o + 15] = batch.spriteStaging[i * 2 + 1] as number;
285
322
  }
286
323
  return n;
287
324
  }
288
325
 
326
+ /** The floats `packInstances` writes per particle, for the caller's upload. */
327
+ export const PARTICLE_INSTANCE_FLOATS = INSTANCE_FLOATS;
328
+
329
+ /** What a `'sprite'` batch carries beyond a procedural one. See `ParticleBatchOptions`. */
330
+ export interface SpriteOptions {
331
+ readonly texture: GPUTextureView | null;
332
+ readonly cells: readonly [number, number];
333
+ readonly blendCells: boolean;
334
+ readonly softDepth: number;
335
+ readonly cameraFade: number;
336
+ readonly cameraOffset: number;
337
+ readonly sort: boolean;
338
+ }
339
+
289
340
  export function createParticleBindGroupLayout(
290
341
  device: GPUDevice,
291
342
  fragSize: number,
343
+ sprite = false,
292
344
  ): GPUBindGroupLayout {
345
+ const textures = PARTICLE_BINDINGS.PARTICLE_SPRITE_FRAG.textures;
293
346
  return device.createBindGroupLayout({
294
- label: 'particle.layout',
347
+ label: sprite ? 'particle.layout:sprite' : 'particle.layout',
295
348
  entries: [
296
349
  {
297
350
  binding: VERT_BINDING,
@@ -303,6 +356,59 @@ export function createParticleBindGroupLayout(
303
356
  visibility: VISIBILITY_FRAGMENT,
304
357
  buffer: { type: 'uniform', minBindingSize: fragSize },
305
358
  },
359
+ /* The image, filtered, and the frame's depth copy, read texel for texel. */
360
+ ...(sprite
361
+ ? [
362
+ {
363
+ binding: textures.uSprite.texture,
364
+ visibility: VISIBILITY_FRAGMENT,
365
+ texture: { sampleType: 'float' as const, viewDimension: '2d-array' as const },
366
+ },
367
+ {
368
+ binding: textures.uSprite.sampler,
369
+ visibility: VISIBILITY_FRAGMENT,
370
+ sampler: { type: 'filtering' as const },
371
+ },
372
+ {
373
+ binding: textures.uDepth.texture,
374
+ visibility: VISIBILITY_FRAGMENT,
375
+ texture: { sampleType: 'unfilterable-float' as const },
376
+ },
377
+ {
378
+ binding: textures.uDepth.sampler,
379
+ visibility: VISIBILITY_FRAGMENT,
380
+ sampler: { type: 'non-filtering' as const },
381
+ },
382
+ ]
383
+ : []),
384
+ ],
385
+ });
386
+ }
387
+
388
+ /**
389
+ * A sprite batch's bind group: its uniforms, its image, and the frame's depth copy — or any
390
+ * single-sample float texture standing in, where the batch asks for no soft edge.
391
+ */
392
+ export function createSpriteBindGroup(
393
+ device: GPUDevice,
394
+ layout: GPUBindGroupLayout,
395
+ batch: GpuParticles,
396
+ fragSize: number,
397
+ depth: GPUTextureView,
398
+ linear: GPUSampler,
399
+ nearest: GPUSampler,
400
+ ): GPUBindGroup {
401
+ const textures = PARTICLE_BINDINGS.PARTICLE_SPRITE_FRAG.textures;
402
+ return device.createBindGroup({
403
+ label: 'particle.bindGroup:sprite',
404
+ layout,
405
+ entries: [
406
+ { binding: VERT_BINDING, resource: { buffer: batch.vertUniforms, size: PARTICLE_VERT_SIZE } },
407
+ { binding: FRAG_BINDING, resource: { buffer: batch.fragUniforms, size: fragSize } },
408
+ { binding: textures.uSprite.texture, resource: batch.texture as GPUTextureView },
409
+ { binding: textures.uSprite.sampler, resource: linear },
410
+ { binding: textures.uDepth.texture, resource: depth },
411
+ { binding: textures.uDepth.sampler, resource: nearest },
306
412
  ],
307
413
  });
308
414
  }
@@ -331,8 +331,13 @@ export const REFLECTION_OVER_BLEND: GPUBlendState = {
331
331
  alpha: SCENE_ALPHA_KEEPS,
332
332
  };
333
333
 
334
- export const ADDITIVE_BLEND: GPUBlendState = {
335
- color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
334
+ /**
335
+ * The bloom pyramid's way back up: an octave added onto a level that the blend multiplies by that
336
+ * level's tint first — the blend constant, which white makes `one, one` exactly. Into the pyramid's
337
+ * own levels, never the scene. See `BloomResponse`.
338
+ */
339
+ export const TINTED_ADD_BLEND: GPUBlendState = {
340
+ color: { srcFactor: 'one', dstFactor: 'constant', operation: 'add' },
336
341
  alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
337
342
  };
338
343