@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
@@ -48,20 +48,8 @@ vec3 highlightShoulder(vec3 c) {
48
48
  }
49
49
  `;
50
50
 
51
- /**
52
- * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
53
- *
54
- * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
55
- * do besides calling it. Both are ints and floats rather than defines because the choice is a
56
- * caller's, per draw, and a permutation per output transform would double a pipeline count for
57
- * a branch that costs one compare.
58
- */
59
- export const OUTPUT_TRANSFORM_GLSL = `
60
- /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
61
- uniform int uOutputTransform;
62
- /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
63
- uniform float uOutputExposure;
64
-
51
+ /** The fit and `applyOutputTransform`, below whichever declaration of the two uniforms. */
52
+ const OUTPUT_TRANSFORM_BODY_GLSL = `
65
53
  /*
66
54
  * ACES: the reference fit, not the cheap one.
67
55
  *
@@ -128,3 +116,31 @@ vec3 applyOutputTransform(vec3 c) {
128
116
  return linearToSrgb(c);
129
117
  }
130
118
  `;
119
+
120
+ /**
121
+ * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
122
+ *
123
+ * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
124
+ * do besides calling it. Both are ints and floats rather than defines because the choice is a
125
+ * caller's, per draw, and a permutation per output transform would double a pipeline count for
126
+ * a branch that costs one compare.
127
+ */
128
+ export const OUTPUT_TRANSFORM_GLSL = `
129
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
130
+ uniform int uOutputTransform;
131
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
132
+ uniform float uOutputExposure;
133
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
134
+
135
+ /**
136
+ * The same, for the lit stage, whose two uniforms live in its material block: a draw that opts out
137
+ * of the grade sets them, so they change with the material and not with the pass. See
138
+ * `hoistUniformBlock` in `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs
139
+ * the same program it always has.
140
+ */
141
+ export const LIT_OUTPUT_TRANSFORM_GLSL = `
142
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
143
+ uniform int uOutputTransform; // wgsl:material
144
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
145
+ uniform float uOutputExposure; // wgsl:material
146
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
@@ -39,6 +39,33 @@ uniform float uWindTime;
39
39
  uniform vec2 uWindSpatialPhase;
40
40
  `;
41
41
 
42
+ /**
43
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
44
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
45
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
46
+ * has; the depth stage keeps the unmarked chunk, having no view block.
47
+ */
48
+ export const LIT_CHANNEL_ATTRIBUTE = `
49
+ layout(location = 13) in vec4 aChannel;
50
+
51
+ /*
52
+ * The frame's wind, already converted by \`resolveScatterDeform\` — a normalised direction, metres
53
+ * of bend, a gust amplitude and a scaled clock.
54
+ *
55
+ * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A
56
+ * second conversion here would be a second wind however identical its inputs, and the failure is
57
+ * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the
58
+ * grass beneath it leans another, with no single element looking wrong. See "One wind, sampled
59
+ * once" in AGENTS.md.
60
+ */
61
+ uniform vec2 uWindDirection; // wgsl:view
62
+ uniform float uWindSpeed; // wgsl:view
63
+ uniform float uWindGust; // wgsl:view
64
+ uniform float uWindTime; // wgsl:view
65
+ /* Spatial frequency of the travelling gust, per metre. */
66
+ uniform vec2 uWindSpatialPhase; // wgsl:view
67
+ `;
68
+
42
69
  /**
43
70
  * Where the wind puts a vertex, given how much of it that vertex takes.
44
71
  *
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Draws that do not change between frames, captured once from an enumeration and replayed into any
3
+ * colour pass: what `createStaticDraws` keeps, and the one rule both backends replay it by.
4
+ *
5
+ * **Captured, not referenced.** An enumeration is a closure over the caller's state, written to
6
+ * allocate nothing, so it hands a sink its scratch: the matrix it fills for every entry and the
7
+ * count it rewrites for the next batch. A list kept across frames copies both when it is made, so
8
+ * what it draws is what the enumeration said then. Meshes, batches and materials are held by
9
+ * reference: they are the caller's, and disposing one a list still holds is the caller's error.
10
+ *
11
+ * **Rigid meshes and instanced batches only.** A skinned mesh is posed every frame and a scatter
12
+ * batch bends with every frame's wind, so neither can be kept, and both are refused by name rather
13
+ * than captured in the pose they happened to have. A caller draws them each frame as it always has.
14
+ *
15
+ * What a backend does with the list is its own: WebGL2 replays the entries every frame through
16
+ * the scene-caster path, and WebGPU records them once into a render bundle per view. See
17
+ * `RendererApi.createStaticDraws` for what a caller is promised, which is the same on both.
18
+ */
19
+ import type { InstancedHandle, MeshHandle } from './backend/api.ts';
20
+ import type { MeshInstances } from './instances.ts';
21
+ import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from './shadowCasters.ts';
22
+
23
+ /** A list from `createStaticDraws`. Opaque but for its size. */
24
+ export interface StaticDrawsHandle {
25
+ /** How many entries it holds: meshes and batches, a batch counting once. */
26
+ readonly draws: number;
27
+ }
28
+
29
+ /** One captured entry. */
30
+ export type StaticEntry =
31
+ | {
32
+ readonly kind: 'mesh';
33
+ readonly mesh: MeshHandle;
34
+ readonly model: Float32Array;
35
+ readonly material: SceneCasterMaterial;
36
+ }
37
+ | {
38
+ readonly kind: 'instanced';
39
+ readonly batch: InstancedHandle;
40
+ readonly data: MeshInstances;
41
+ readonly material: SceneCasterMaterial;
42
+ };
43
+
44
+ /** What both backends hold: the entries, and whether the list has been disposed. */
45
+ export interface StaticDrawsList extends StaticDrawsHandle {
46
+ readonly entries: readonly StaticEntry[];
47
+ disposed: boolean;
48
+ }
49
+
50
+ /** Enumerate `casters` once into a list. Allocates: it runs when the list is made, not per frame. */
51
+ export function captureStaticDraws(casters: ShadowCasters): StaticDrawsList {
52
+ const entries: StaticEntry[] = [];
53
+ const sink: ShadowCasterSink = {
54
+ mesh: (mesh, model, material) => {
55
+ entries.push({
56
+ kind: 'mesh',
57
+ mesh,
58
+ model: new Float32Array(model as Float32Array),
59
+ material: material ?? null,
60
+ });
61
+ },
62
+ instanced: (batch, data, material) => {
63
+ /* The batch's placements are on the device already; the count is the caller's scratch. */
64
+ entries.push({ kind: 'instanced', batch, data: { ...data }, material: material ?? null });
65
+ },
66
+ skinnedMesh: () => {
67
+ throw new Error(
68
+ 'createStaticDraws: a skinned mesh is posed every frame, so a list kept across frames ' +
69
+ 'cannot hold one. Draw it each frame, with drawMesh or drawSceneCasters.',
70
+ );
71
+ },
72
+ scatter: () => {
73
+ throw new Error(
74
+ 'createStaticDraws: a scatter batch bends with every frame’s wind, so a list kept across ' +
75
+ 'frames cannot hold one. Draw it each frame with drawScatter.',
76
+ );
77
+ },
78
+ };
79
+ casters(sink);
80
+ return {
81
+ entries,
82
+ get draws() {
83
+ return entries.length;
84
+ },
85
+ disposed: false,
86
+ };
87
+ }
88
+
89
+ /** Hand every entry to `sink`, in the order it was captured. */
90
+ export function replayStaticDraws(list: StaticDrawsList, sink: ShadowCasterSink): void {
91
+ for (const entry of list.entries) {
92
+ if (entry.kind === 'mesh') sink.mesh(entry.mesh, entry.model, entry.material);
93
+ else sink.instanced?.(entry.batch, entry.data, entry.material);
94
+ }
95
+ }
@@ -10,7 +10,12 @@
10
10
  */
11
11
  import type { SurfaceModel } from './surfaceModel.ts';
12
12
  import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
13
- import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.ts';
13
+ import {
14
+ compressedLayers,
15
+ isCompressedSource,
16
+ planBlocks,
17
+ refuseBlockUpdate,
18
+ } from './compressedSource.ts';
14
19
  import type {
15
20
  BlockFormat,
16
21
  CompressedTextureFormat,
@@ -119,7 +124,7 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
119
124
  * which is every light's default too, so a scene that names no channel is lit as it always was.
120
125
  *
121
126
  * A point or spot light shades this surface only where its `PointLightSet.lightChannels` and this
122
- * share a bit — Unreal's lighting channels. A character's own key and rim lights on channel 2,
127
+ * share a bit — lighting channels. A character's own key and rim lights on channel 2,
123
128
  * and the character's materials on 3, light the character and leave the floor around it as the
124
129
  * stage's lights alone have it. **What it does not reach**: the sun, the sky and the probes, area
125
130
  * lights, DriftLight's volume and the GPU-driven pipeline light every surface whatever its mask.
@@ -153,6 +158,22 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
153
158
  * which is the opposite of what the caller asked for.
154
159
  */
155
160
  occlusionStrength?: number;
161
+ /**
162
+ * How much of the environment this material's draws mirror, 0 to 1: the number
163
+ * `setSurfaceReflectivity` sets for every draw, stated by the material for its own. Absent, its
164
+ * draws wear the setter's, as every material's did before this existed.
165
+ *
166
+ * **Here so a static list can carry it.** A list keeps each entry's material and none of the
167
+ * renderer's state between draws (`createStaticDraws`), so a stage whose batches each mirror their
168
+ * own share of a baked reflection says so in the material it records. What it gives up is nothing a
169
+ * setter could do: one called after this material is set is ignored for the material's draws.
170
+ */
171
+ reflectivity?: number;
172
+ /**
173
+ * How bright the environment this material's draws reflect is: `setEnvironmentGain`'s number,
174
+ * stated by the material for its own, and absent the setter's. Held non-negative.
175
+ */
176
+ environmentGain?: number;
156
177
  /**
157
178
  * Scales the emissive map, per channel. Default 1.
158
179
  *
@@ -176,14 +197,14 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
176
197
  modelMap?: Texture | null;
177
198
  /**
178
199
  * Whether a lamp's and the sun's highlight on this surface is GGX's own, `π · D · Vis · F · N·L`,
179
- * as Unreal and this engine's skin and eye shade it, rather than the engine's lobe scaled to a
180
- * peak of one. **False by default**, every surface as it was.
200
+ * as physically based renderers and this engine's skin and eye shade it, rather than the engine's
201
+ * lobe scaled to a peak of one. **False by default**, every surface as it was.
181
202
  *
182
203
  * The peak-normalised lobe is a look control: the specular attribute says how strong a highlight
183
204
  * is and roughness how wide, apart. Physical, they are entangled as they are in a real surface —
184
205
  * the peak is `1 / (4 α²)` of the light head-on, so a polished surface's highlight is many times
185
206
  * the look's and a rough one's lower and broader — and the specular attribute is read as the
186
- * reflectance at normal incidence, F0: Unreal's `0.08 × Specular`, 0.04 at its default. Fresnel
207
+ * reflectance at normal incidence, F0: `0.08 × Specular`, 0.04 at its default. Fresnel
187
208
  * then brightens every surface toward grazing, not only a metal. A rectangle's highlight is the
188
209
  * lobe integrated over it with the attribute as F0 already, so it is unchanged either way.
189
210
  *
@@ -292,7 +313,9 @@ export class SurfaceTexture {
292
313
  /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
293
314
  private effectsTable: WebGLTexture | null = null;
294
315
  /** The block format a compressed texture holds, or null for one uploaded from images. */
295
- private readonly blockFormat: BlockFormat | null;
316
+ private blockFormat: BlockFormat | null;
317
+ /** How the texture is sampled, kept so blocks swapped in by `update` are sampled the same way. */
318
+ private readonly sampling: { wrap: number; nearest: boolean; anisotropy: number };
296
319
  /** Level 0's size, which `flatLayer` copies; kept as `update` changes it. */
297
320
  private width = 0;
298
321
  private height = 0;
@@ -404,27 +427,13 @@ export class SurfaceTexture {
404
427
  this.storage = this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA8;
405
428
  uploadImages(gl, sources, this.srgb);
406
429
  }
407
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
408
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
409
- /* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
410
- still blended between levels, because that is minification and this option is about
411
- magnification. */
412
- const nearest = options.filter === 'nearest';
413
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
414
- gl.texParameteri(
415
- gl.TEXTURE_2D_ARRAY,
416
- gl.TEXTURE_MIN_FILTER,
417
- this.mipmapped
418
- ? nearest
419
- ? gl.NEAREST_MIPMAP_LINEAR
420
- : gl.LINEAR_MIPMAP_LINEAR
421
- : nearest
422
- ? gl.NEAREST
423
- : gl.LINEAR,
424
- );
425
-
430
+ this.sampling = {
431
+ wrap,
432
+ nearest: options.filter === 'nearest',
433
+ anisotropy: options.anisotropy ?? DEFAULT_ANISOTROPY,
434
+ };
435
+ this.applySampling(gl);
426
436
  if (this.mipmapped && this.blockFormat === null) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
427
- applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
428
437
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
429
438
 
430
439
  /* Half floats, nearest, clamped: a table read by texelFetch, whose every whole number is under
@@ -475,9 +484,17 @@ export class SurfaceTexture {
475
484
  * Not a hot path: it re-uploads the whole image and regenerates the chain. A caller
476
485
  * doing this every frame at any size wants to know that it costs what it costs.
477
486
  */
478
- update(gl: WebGL2RenderingContext, source: TexImageSource): void {
487
+ update(
488
+ gl: WebGL2RenderingContext,
489
+ source: TexImageSource | CompressedTextureSource,
490
+ compressed: readonly CompressedTextureFormat[] = [],
491
+ ): void {
479
492
  if (this.texture === null) return;
480
493
  refuseArrayUpdate(this.layers);
494
+ if (isCompressedSource(source)) {
495
+ this.swapToBlocks(gl, source, compressed);
496
+ return;
497
+ }
481
498
  refuseBlockUpdate(this.blockFormat);
482
499
  const { width, height } = sourceSize(source);
483
500
  this.width = width;
@@ -501,6 +518,56 @@ export class SurfaceTexture {
501
518
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
502
519
  }
503
520
 
521
+ /**
522
+ * Blocks in place of the image, behind the same handle: **a new GL object** rather than the
523
+ * image's levels respecified, because a chain shorter than the image's would leave the levels
524
+ * past it allocated, and immutable storage could not be respecified at all. Sampled as the image
525
+ * was, and refused before anything changes where the context does not take the format.
526
+ */
527
+ private swapToBlocks(
528
+ gl: WebGL2RenderingContext,
529
+ source: CompressedTextureSource,
530
+ compressed: readonly CompressedTextureFormat[],
531
+ ): void {
532
+ const plan = planBlocks([source], this.srgb, compressed);
533
+ const texture = gl.createTexture();
534
+ if (texture === null) throw new Error('SurfaceTexture: createTexture failed');
535
+ this.dropFlat(gl);
536
+ if (this.texture !== null) gl.deleteTexture(this.texture);
537
+ this.texture = texture;
538
+ this.blockFormat = source.format;
539
+ this.width = source.width;
540
+ this.height = source.height;
541
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
542
+ uploadCompressedArray(gl, plan.name, [source], this.mipmapped ? plan.levels : 1);
543
+ this.applySampling(gl);
544
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
545
+ }
546
+
547
+ /**
548
+ * Wrap, filters and anisotropy on the bound texture. Nearest where the caller said their pixels
549
+ * are the subject; see `filter`. The mip chain is still blended between levels, because that is
550
+ * minification and this option is about magnification.
551
+ */
552
+ private applySampling(gl: WebGL2RenderingContext): void {
553
+ const { wrap, nearest, anisotropy } = this.sampling;
554
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
555
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
556
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
557
+ gl.texParameteri(
558
+ gl.TEXTURE_2D_ARRAY,
559
+ gl.TEXTURE_MIN_FILTER,
560
+ this.mipmapped
561
+ ? nearest
562
+ ? gl.NEAREST_MIPMAP_LINEAR
563
+ : gl.LINEAR_MIPMAP_LINEAR
564
+ : nearest
565
+ ? gl.NEAREST
566
+ : gl.LINEAR,
567
+ );
568
+ applyAnisotropy(gl, anisotropy);
569
+ }
570
+
504
571
  /**
505
572
  * Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
506
573
  * by `captureScene`. Engine-internal, as `bind` is.