@driftengine/core 4.5.0 → 4.6.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 (207) hide show
  1. package/CHANGELOG.json +51 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +238 -121
  33. package/dist/render/backend/webgpu/buffers.d.ts +10 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  48. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  49. package/dist/render/backend/webgpu/renderer.js +462 -201
  50. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  51. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  53. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  54. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  55. package/dist/render/driftLight/bake.d.ts +7 -0
  56. package/dist/render/driftLight/bake.js +61 -56
  57. package/dist/render/driftLight/denseField.d.ts +46 -0
  58. package/dist/render/driftLight/denseField.js +109 -0
  59. package/dist/render/driftLight/lightField.d.ts +10 -5
  60. package/dist/render/driftLight/lightField.js +25 -19
  61. package/dist/render/driftLight/presence.d.ts +50 -0
  62. package/dist/render/driftLight/presence.js +43 -0
  63. package/dist/render/driftLight/uniforms.d.ts +3 -3
  64. package/dist/render/driftLight/uniforms.js +5 -6
  65. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  66. package/dist/render/driftLight/worldLightField.js +66 -0
  67. package/dist/render/fixtureAtlas.d.ts +20 -0
  68. package/dist/render/fixtureAtlas.js +49 -0
  69. package/dist/render/instanceCull.d.ts +63 -0
  70. package/dist/render/instanceCull.js +160 -0
  71. package/dist/render/instances.d.ts +15 -0
  72. package/dist/render/lightBudget.d.ts +22 -33
  73. package/dist/render/lightBudget.js +24 -35
  74. package/dist/render/lightGrid.d.ts +42 -0
  75. package/dist/render/lightGrid.js +87 -0
  76. package/dist/render/mesh.d.ts +18 -24
  77. package/dist/render/mesh.js +51 -68
  78. package/dist/render/occlusion.d.ts +10 -0
  79. package/dist/render/occlusion.js +36 -7
  80. package/dist/render/pointLightSelection.d.ts +7 -0
  81. package/dist/render/pointLightSelection.js +29 -4
  82. package/dist/render/rainField.d.ts +13 -0
  83. package/dist/render/rainField.js +27 -5
  84. package/dist/render/recon/lateDepth.d.ts +11 -0
  85. package/dist/render/recon/lateDepth.js +70 -0
  86. package/dist/render/recon/resolve.d.ts +34 -4
  87. package/dist/render/recon/resolve.js +161 -20
  88. package/dist/render/rendererHarness.js +4 -0
  89. package/dist/render/sceneTarget.d.ts +2 -0
  90. package/dist/render/sceneTarget.js +2 -0
  91. package/dist/render/shaders/depth.js +12 -9
  92. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  93. package/dist/render/shaders/flat/driftLight.js +32 -7
  94. package/dist/render/shaders/flat/index.js +5 -3
  95. package/dist/render/shaders/flat/main.d.ts +1 -1
  96. package/dist/render/shaders/flat/main.js +75 -23
  97. package/dist/render/shaders/flat/preamble.js +75 -20
  98. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  99. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  100. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  101. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  102. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  103. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  104. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  105. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  106. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  107. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  108. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  109. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  110. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  111. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  113. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  114. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  115. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  116. package/dist/render/shaders/rush.js +11 -1
  117. package/dist/render/shaders/sdfText.d.ts +1 -1
  118. package/dist/render/shaders/sdfText.js +3 -2
  119. package/dist/render/shaders/text.d.ts +1 -1
  120. package/dist/render/shaders/text.js +18 -9
  121. package/dist/render/surfaceEffects.d.ts +83 -0
  122. package/dist/render/surfaceEffects.js +75 -0
  123. package/dist/render/surfaceScene.d.ts +8 -0
  124. package/dist/render/surfaceScene.js +8 -0
  125. package/dist/render/surfaceTexture.d.ts +18 -2
  126. package/dist/render/surfaceTexture.js +68 -15
  127. package/dist/render/temporalAa.d.ts +25 -6
  128. package/dist/render/temporalAa.js +45 -15
  129. package/dist/render/textureSource.d.ts +30 -0
  130. package/dist/render/textureSource.js +53 -0
  131. package/dist/render/vertexDefaults.js +2 -0
  132. package/dist/world/hlod.d.ts +104 -0
  133. package/dist/world/hlod.js +214 -0
  134. package/package.json +6 -6
  135. package/src/geometry/boxCsg.ts +186 -0
  136. package/src/geometry/csg.ts +384 -0
  137. package/src/geometry/meshBuilder.ts +15 -0
  138. package/src/geometry/meshConcat.ts +1 -0
  139. package/src/geometry/pixelFont.ts +2 -0
  140. package/src/geometry/solid.ts +189 -0
  141. package/src/geometry/solidFlat.ts +522 -0
  142. package/src/geometry/solidHarness.ts +83 -0
  143. package/src/geometry/solidNormals.ts +103 -0
  144. package/src/geometry/solidRound.ts +443 -0
  145. package/src/geometry/solidWriter.ts +58 -0
  146. package/src/index.ts +51 -1
  147. package/src/render/backend/api.ts +21 -2
  148. package/src/render/backend/drawFog.ts +31 -0
  149. package/src/render/backend/materialChanges.ts +9 -2
  150. package/src/render/backend/webgl2/instanced.ts +80 -14
  151. package/src/render/backend/webgl2/renderer.ts +300 -143
  152. package/src/render/backend/webgpu/buffers.ts +42 -3
  153. package/src/render/backend/webgpu/depthPass.ts +5 -1
  154. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  155. package/src/render/backend/webgpu/flatPass.ts +41 -11
  156. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  157. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  158. package/src/render/backend/webgpu/instanced.ts +21 -3
  159. package/src/render/backend/webgpu/latePass.ts +12 -3
  160. package/src/render/backend/webgpu/renderer.ts +540 -226
  161. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  162. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  163. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  164. package/src/render/driftLight/bake.ts +85 -62
  165. package/src/render/driftLight/denseField.ts +149 -0
  166. package/src/render/driftLight/lightField.ts +33 -18
  167. package/src/render/driftLight/presence.ts +69 -0
  168. package/src/render/driftLight/uniforms.ts +8 -9
  169. package/src/render/driftLight/worldLightField.ts +83 -0
  170. package/src/render/fixtureAtlas.ts +63 -0
  171. package/src/render/instanceCull.ts +205 -0
  172. package/src/render/instances.ts +16 -0
  173. package/src/render/lightBudget.ts +25 -36
  174. package/src/render/lightGrid.ts +130 -0
  175. package/src/render/mesh.ts +59 -72
  176. package/src/render/occlusion.ts +51 -7
  177. package/src/render/pointLightSelection.ts +72 -4
  178. package/src/render/rainField.ts +39 -5
  179. package/src/render/recon/lateDepth.ts +83 -0
  180. package/src/render/recon/resolve.ts +206 -20
  181. package/src/render/rendererHarness.ts +4 -0
  182. package/src/render/sceneTarget.ts +4 -0
  183. package/src/render/shaders/depth.ts +12 -9
  184. package/src/render/shaders/flat/driftLight.ts +32 -7
  185. package/src/render/shaders/flat/index.ts +5 -3
  186. package/src/render/shaders/flat/main.ts +75 -23
  187. package/src/render/shaders/flat/preamble.ts +75 -20
  188. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  189. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  190. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  191. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  192. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  193. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  194. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  195. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  196. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  197. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  198. package/src/render/shaders/rush.ts +11 -1
  199. package/src/render/shaders/sdfText.ts +3 -2
  200. package/src/render/shaders/text.ts +18 -9
  201. package/src/render/surfaceEffects.ts +132 -0
  202. package/src/render/surfaceScene.ts +16 -0
  203. package/src/render/surfaceTexture.ts +110 -19
  204. package/src/render/temporalAa.ts +78 -14
  205. package/src/render/textureSource.ts +72 -0
  206. package/src/render/vertexDefaults.ts +2 -0
  207. package/src/world/hlod.ts +290 -0
@@ -1,4 +1,16 @@
1
- /** A GPU image the caller supplies, sampled by the flat shader as surface colour. */
1
+ /**
2
+ * A GPU image the caller supplies, sampled by the flat shader as surface colour — or several, as
3
+ * the layers of one array.
4
+ *
5
+ * **Every surface texture is an array; a plain image is an array of one.** The lit pass has three of
6
+ * WebGL2's guaranteed sixteen texture units free, so a second set of array samplers beside the 2D
7
+ * ones was never affordable. Making the one set of samplers arrays costs no unit, and a one-layer
8
+ * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
+ * so a merged block wearing forty facades is one draw.
10
+ */
11
+ import { isSourceList, layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
12
+ import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.ts';
13
+ import type { SurfaceLayerEffect } from './surfaceEffects.ts';
2
14
 
3
15
  /**
4
16
  * How a texture behaves past its edges and between its texels.
@@ -103,6 +115,12 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
103
115
  }
104
116
 
105
117
  export interface SurfaceTextureOptions {
118
+ /**
119
+ * What each layer does beyond its picture — rooms behind windows, windows lit by night, wear,
120
+ * animation, staying dry — one entry a layer, `undefined` for a layer with none. Read only where
121
+ * this texture is a material's albedo. See `surfaceEffects.ts`.
122
+ */
123
+ effects?: readonly (SurfaceLayerEffect | undefined)[];
106
124
  /**
107
125
  * `repeat` tiles the image, `clamp` stretches its edge texels.
108
126
  *
@@ -189,11 +207,23 @@ export class SurfaceTexture {
189
207
  /** Whether the sampler decodes sRGB on fetch. See `colorSpace`. */
190
208
  private readonly srgb: boolean;
191
209
 
210
+ /** How many images the array holds: 1 for `createSurfaceTexture`, the list's length otherwise. */
211
+ readonly layers: number;
212
+ /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
213
+ private effectsTable: WebGLTexture | null = null;
214
+
215
+ /**
216
+ * One image, or an array of images that must share one size (`layerSize` refuses otherwise).
217
+ * Uploaded unflipped either way; see the constructor body for why.
218
+ */
192
219
  constructor(
193
220
  gl: WebGL2RenderingContext,
194
- source: TexImageSource,
221
+ source: TexImageSource | readonly TexImageSource[],
195
222
  options: SurfaceTextureOptions = {},
196
223
  ) {
224
+ const sources: readonly TexImageSource[] = isSourceList(source) ? source : [source];
225
+ const { width, height } = layerSize(sources);
226
+ this.layers = sources.length;
197
227
  const texture = gl.createTexture();
198
228
  if (texture === null) throw new Error('SurfaceTexture: createTexture failed');
199
229
  this.texture = texture;
@@ -202,7 +232,7 @@ export class SurfaceTexture {
202
232
 
203
233
  const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
204
234
 
205
- gl.bindTexture(gl.TEXTURE_2D, texture);
235
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
206
236
  /*
207
237
  * **Not flipped, and this used to be, which is the bug it is written down for.**
208
238
  *
@@ -224,24 +254,45 @@ export class SurfaceTexture {
224
254
  *
225
255
  * A caller that was mirroring its own canvas to cancel this must stop.
226
256
  */
227
- gl.texImage2D(
228
- gl.TEXTURE_2D,
257
+ /* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
258
+ new size, as texImage2D always could. Each layer is then written in place. */
259
+ gl.texImage3D(
260
+ gl.TEXTURE_2D_ARRAY,
229
261
  0,
230
262
  this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
263
+ width,
264
+ height,
265
+ sources.length,
266
+ 0,
231
267
  gl.RGBA,
232
268
  gl.UNSIGNED_BYTE,
233
- source,
269
+ null,
234
270
  );
271
+ for (let layer = 0; layer < sources.length; layer++) {
272
+ gl.texSubImage3D(
273
+ gl.TEXTURE_2D_ARRAY,
274
+ 0,
275
+ 0,
276
+ 0,
277
+ layer,
278
+ width,
279
+ height,
280
+ 1,
281
+ gl.RGBA,
282
+ gl.UNSIGNED_BYTE,
283
+ sources[layer] as TexImageSource,
284
+ );
285
+ }
235
286
 
236
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
237
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
287
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
288
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
238
289
  /* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
239
290
  still blended between levels, because that is minification and this option is about
240
291
  magnification. */
241
292
  const nearest = options.filter === 'nearest';
242
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
293
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
243
294
  gl.texParameteri(
244
- gl.TEXTURE_2D,
295
+ gl.TEXTURE_2D_ARRAY,
245
296
  gl.TEXTURE_MIN_FILTER,
246
297
  this.mipmapped
247
298
  ? nearest
@@ -252,9 +303,41 @@ export class SurfaceTexture {
252
303
  : gl.LINEAR,
253
304
  );
254
305
 
255
- if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D);
306
+ if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
256
307
  applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
257
- gl.bindTexture(gl.TEXTURE_2D, null);
308
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
309
+
310
+ /* Half floats, nearest, clamped: a table read by texelFetch, whose every whole number is under
311
+ what a half holds exactly. */
312
+ const effects = options.effects ?? [];
313
+ if (effects.length > 0) {
314
+ const table = gl.createTexture();
315
+ if (table === null) throw new Error('SurfaceTexture: createTexture failed');
316
+ gl.bindTexture(gl.TEXTURE_2D, table);
317
+ gl.texImage2D(
318
+ gl.TEXTURE_2D,
319
+ 0,
320
+ gl.RGBA16F,
321
+ SURFACE_EFFECT_TEXELS,
322
+ effects.length,
323
+ 0,
324
+ gl.RGBA,
325
+ gl.FLOAT,
326
+ packSurfaceEffects(effects),
327
+ );
328
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
329
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
330
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
331
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
332
+ gl.bindTexture(gl.TEXTURE_2D, null);
333
+ this.effectsTable = table;
334
+ }
335
+ }
336
+
337
+ /** Bind the effects table to `unit`, or `fallback` for a texture given none. */
338
+ bindEffects(gl: WebGL2RenderingContext, unit: number, fallback: WebGLTexture): void {
339
+ gl.activeTexture(gl.TEXTURE0 + unit);
340
+ gl.bindTexture(gl.TEXTURE_2D, this.effectsTable ?? fallback);
258
341
  }
259
342
 
260
343
  /**
@@ -270,30 +353,38 @@ export class SurfaceTexture {
270
353
  */
271
354
  update(gl: WebGL2RenderingContext, source: TexImageSource): void {
272
355
  if (this.texture === null) return;
273
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
356
+ refuseArrayUpdate(this.layers);
357
+ const { width, height } = sourceSize(source);
358
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
274
359
  /* Unflipped, matching the constructor. See it for why, and for what it cost. */
275
- gl.texImage2D(
276
- gl.TEXTURE_2D,
360
+ gl.texImage3D(
361
+ gl.TEXTURE_2D_ARRAY,
277
362
  0,
278
363
  this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
364
+ width,
365
+ height,
366
+ 1,
367
+ 0,
279
368
  gl.RGBA,
280
369
  gl.UNSIGNED_BYTE,
281
370
  source,
282
371
  );
283
- if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D);
284
- gl.bindTexture(gl.TEXTURE_2D, null);
372
+ if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
373
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
285
374
  }
286
375
 
287
376
  /** Bind to a unit for sampling. Engine-internal: the renderer owns unit assignment. */
288
377
  bind(gl: WebGL2RenderingContext, unit: number): void {
289
378
  gl.activeTexture(gl.TEXTURE0 + unit);
290
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
379
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
291
380
  }
292
381
 
293
382
  dispose(gl: WebGL2RenderingContext): void {
294
383
  if (this.texture === null) return;
295
384
  gl.deleteTexture(this.texture);
296
385
  this.texture = null;
386
+ if (this.effectsTable !== null) gl.deleteTexture(this.effectsTable);
387
+ this.effectsTable = null;
297
388
  }
298
389
  }
299
390
 
@@ -311,5 +402,5 @@ function applyAnisotropy(gl: WebGL2RenderingContext, requested: number): void {
311
402
  gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
312
403
  if (ext === null) return;
313
404
  const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) as number;
314
- gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(requested, max));
405
+ gl.texParameterf(gl.TEXTURE_2D_ARRAY, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(requested, max));
315
406
  }
@@ -204,6 +204,49 @@ export const FLICKER_MEMORY = 1 / JITTER_PERIOD;
204
204
  */
205
205
  export const FLICKER_WIDTH = 6;
206
206
 
207
+ /**
208
+ * The two numbers the proof takes from the jitter's period: how much of a period one frame is, and
209
+ * how many spreads a proven pixel's box widens by. This resolve's are `FLICKER_MEMORY` and
210
+ * `FLICKER_WIDTH`; a reconstruction jitters along a longer sequence and needs its own.
211
+ */
212
+ export interface FlickerRule {
213
+ readonly memory: number;
214
+ readonly width: number;
215
+ }
216
+
217
+ /** The temporal resolve's own: eight phases. */
218
+ export const TEMPORAL_FLICKER: FlickerRule = { memory: FLICKER_MEMORY, width: FLICKER_WIDTH };
219
+
220
+ /**
221
+ * The running spread of the hardest thread — one caught in a single phase of `phases` — just before
222
+ * its catch, as a share of its contrast: the steady state `FLICKER_WIDTH`'s comment solves for eight
223
+ * phases, solved for any. With m one over the period and r = (1 − m)^(p − 1), the seven, or p − 1,
224
+ * misses that wear it down: a = (m + ((p − 1)m² − m)·r) / (1 − (1 − m)·r).
225
+ */
226
+ function hardestSpread(phases: number): number {
227
+ const m = 1 / phases;
228
+ const r = (1 - m) ** (phases - 1);
229
+ return (m + ((phases - 1) * m * m - m) * r) / (1 - (1 - m) * r);
230
+ }
231
+
232
+ /** How many of those spreads the hardest thread's catch lands from its period's mean. */
233
+ function widthNeeded(phases: number): number {
234
+ return (1 - 1 / phases) / hardestSpread(phases);
235
+ }
236
+
237
+ /**
238
+ * The rule for a jitter of `phases` positions. **The width keeps the room `FLICKER_WIDTH` leaves over
239
+ * what eight phases need** — 4.8 — in proportion: a longer period wears the spread down further
240
+ * between catches, so over eighteen phases the hardest thread needs eleven spreads, and a box that
241
+ * reached six reset the count of exactly the pixels this is for.
242
+ */
243
+ export function flickerRule(phases: number): FlickerRule {
244
+ return {
245
+ memory: 1 / phases,
246
+ width: (FLICKER_WIDTH * widthNeeded(phases)) / widthNeeded(JITTER_PERIOD),
247
+ };
248
+ }
249
+
207
250
  /**
208
251
  * How far apart two periods' means may be, relative to the larger, and still repeat: fully below
209
252
  * this, not at all at twice it. Above an eight-bit record's rounding and a lamp's slow breathing; far
@@ -292,9 +335,13 @@ export interface Flicker {
292
335
  * record of another surface is no record**, so the resolve starts one over wherever last frame's
293
336
  * depth says the surface is not the one it followed, before this frame widens by anything.
294
337
  */
295
- export function freshFlicker(out: Flicker, sample: number): Flicker {
338
+ export function freshFlicker(
339
+ out: Flicker,
340
+ sample: number,
341
+ rule: FlickerRule = TEMPORAL_FLICKER,
342
+ ): Flicker {
296
343
  out.spread = 0;
297
- out.sum = sample * FLICKER_MEMORY;
344
+ out.sum = sample * rule.memory;
298
345
  out.mean = sample;
299
346
  out.repeated = 0;
300
347
  return out;
@@ -320,23 +367,38 @@ export function stillness(motionPixels: number): number {
320
367
  * lands within the reach of the box it would widen to. A sample beyond that reach is something
321
368
  * arriving, and it disqualifies the frame it lands in as well as the count it carries out.
322
369
  */
323
- export function proven(record: Flicker, sample: number): boolean {
324
- return record.repeated > 0.75 && within(record, sample);
370
+ export function proven(
371
+ record: Flicker,
372
+ sample: number,
373
+ rule: FlickerRule = TEMPORAL_FLICKER,
374
+ ): boolean {
375
+ return record.repeated > 0.75 && within(record, sample, rule);
325
376
  }
326
377
 
327
378
  /** Whether this frame's sample lands inside the reach of the box the record would widen to. */
328
- function within(record: Flicker, sample: number): boolean {
329
- return Math.abs(sample - record.mean) <= FLICKER_WIDTH * record.spread + 1 / 255;
379
+ function within(record: Flicker, sample: number, rule: FlickerRule): boolean {
380
+ return Math.abs(sample - record.mean) <= rule.width * record.spread + 1 / 255;
330
381
  }
331
382
 
332
383
  /** How far a pixel's box widens this frame: by its spread, once proven, and only while still. */
333
- export function flickerWidening(record: Flicker, sample: number, still: number): number {
334
- return proven(record, sample) ? FLICKER_WIDTH * record.spread * still : 0;
384
+ export function flickerWidening(
385
+ record: Flicker,
386
+ sample: number,
387
+ still: number,
388
+ rule: FlickerRule = TEMPORAL_FLICKER,
389
+ ): number {
390
+ return proven(record, sample, rule) ? rule.width * record.spread * still : 0;
335
391
  }
336
392
 
337
393
  /** How much of the history this frame keeps: the base blend, or more once proven. */
338
- export function flickerBlend(base: number, record: Flicker, sample: number, still: number): number {
339
- return proven(record, sample) ? base + (FLICKER_BLEND - base) * still : base;
394
+ export function flickerBlend(
395
+ base: number,
396
+ record: Flicker,
397
+ sample: number,
398
+ still: number,
399
+ rule: FlickerRule = TEMPORAL_FLICKER,
400
+ ): number {
401
+ return proven(record, sample, rule) ? base + (FLICKER_BLEND - base) * still : base;
340
402
  }
341
403
 
342
404
  /** Whether two periods' means are the same, one to zero. */
@@ -349,7 +411,8 @@ export function repeats(mean: number, previous: number): number {
349
411
  * The record carried out, from the one carried in and this frame's sample in luma. `periodStart`
350
412
  * is the first frame of the jitter's period, where the period just gathered is compared with the
351
413
  * one before. Forgotten in proportion to motion — a moving pixel's record would be a record of
352
- * somewhere else.
414
+ * somewhere else. `rule` is this resolve's eight phases unless a caller jitters along another
415
+ * sequence, as a reconstruction does.
353
416
  */
354
417
  export function nextFlicker(
355
418
  out: Flicker,
@@ -357,17 +420,18 @@ export function nextFlicker(
357
420
  sample: number,
358
421
  still: number,
359
422
  periodStart: boolean,
423
+ rule: FlickerRule = TEMPORAL_FLICKER,
360
424
  ): Flicker {
361
425
  const apart = Math.abs(sample - previous.mean);
362
426
  let { sum, mean, repeated } = previous;
363
- if (apart > FLICKER_WIDTH * previous.spread + 1 / 255) repeated = 0;
427
+ if (apart > rule.width * previous.spread + 1 / 255) repeated = 0;
364
428
  if (periodStart) {
365
429
  repeated = repeats(sum, mean) > 0.5 ? Math.min(1, repeated + 0.5) : 0;
366
430
  mean = sum;
367
431
  sum = 0;
368
432
  }
369
- out.spread = previous.spread + (apart - previous.spread) * FLICKER_MEMORY;
370
- out.sum = sum + sample * FLICKER_MEMORY;
433
+ out.spread = previous.spread + (apart - previous.spread) * rule.memory;
434
+ out.sum = sum + sample * rule.memory;
371
435
  out.mean = mean;
372
436
  out.repeated = repeated * still;
373
437
  return out;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The size of an image a caller hands over, and the one rule a texture array adds to it: every
3
+ * layer the same size.
4
+ *
5
+ * Backend-neutral, because both backends upload the same `TexImageSource` and must agree on its
6
+ * size, and on refusing an array whose layers disagree, before either touches a device.
7
+ */
8
+
9
+ export interface SourceSize {
10
+ readonly width: number;
11
+ readonly height: number;
12
+ }
13
+
14
+ /** Width and height of any `TexImageSource`: a video by its frame, everything else by its own size. */
15
+ export function sourceSize(source: TexImageSource): SourceSize {
16
+ const candidate = source as {
17
+ width?: number;
18
+ height?: number;
19
+ videoWidth?: number;
20
+ videoHeight?: number;
21
+ };
22
+ const width = candidate.videoWidth ?? candidate.width ?? 0;
23
+ const height = candidate.videoHeight ?? candidate.height ?? 0;
24
+ return { width: Math.max(1, width), height: Math.max(1, height) };
25
+ }
26
+
27
+ /**
28
+ * The one size every layer of an array shares, or a refusal naming the first that differs.
29
+ *
30
+ * **Refused rather than resized**, because an array layer has no size of its own — the device
31
+ * allocates one size for all of them — and quietly scaling a 128-pixel sign up to a 512 array, or a
32
+ * facade down, is a decision about quality the caller should make where they can see it. Sorting
33
+ * images into arrays by size is theirs; this makes sure they did.
34
+ */
35
+ export function layerSize(sources: readonly TexImageSource[]): SourceSize {
36
+ const first = sources[0];
37
+ if (first === undefined) {
38
+ throw new Error('createSurfaceTextureArray: an array needs at least one image');
39
+ }
40
+ const size = sourceSize(first);
41
+ for (let layer = 1; layer < sources.length; layer++) {
42
+ const next = sourceSize(sources[layer] as TexImageSource);
43
+ if (next.width !== size.width || next.height !== size.height) {
44
+ throw new Error(
45
+ `createSurfaceTextureArray: layer ${layer} is ${next.width}×${next.height} and layer 0 is ` +
46
+ `${size.width}×${size.height}. Every layer of an array is one size on the device; group ` +
47
+ 'images into arrays by size, or scale them to one, before building the array.',
48
+ );
49
+ }
50
+ }
51
+ return size;
52
+ }
53
+
54
+ /** Whether a constructor argument is a list of images rather than one. */
55
+ export function isSourceList(
56
+ source: TexImageSource | readonly TexImageSource[],
57
+ ): source is readonly TexImageSource[] {
58
+ return Array.isArray(source);
59
+ }
60
+
61
+ /**
62
+ * `update` replaces the one image of a plain texture. An array's layers are its identity — a
63
+ * block's facades by index — and replacing "the image" of a forty-layer array has no single
64
+ * meaning, so it is refused rather than guessed at.
65
+ */
66
+ export function refuseArrayUpdate(layers: number): void {
67
+ if (layers === 1) return;
68
+ throw new Error(
69
+ `updateSurfaceTexture: this texture is an array of ${layers} layers, and update replaces the ` +
70
+ 'one image of a plain texture. Build a new array for new layers.',
71
+ );
72
+ }
@@ -90,6 +90,8 @@ export const ABSENT_ATTRIBUTE: Readonly<Record<string, readonly number[]>> = {
90
90
  * the thing being measured.
91
91
  */
92
92
  channel: [0, 1, 1, 1],
93
+ /* Layer 0: the only image a plain texture has, and the first of an array. */
94
+ layers: [0],
93
95
  };
94
96
 
95
97
  /**