@driftengine/core 4.8.7 → 4.9.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 (163) hide show
  1. package/CHANGELOG.json +61 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +7 -0
  5. package/dist/render/atmosphere.d.ts +13 -0
  6. package/dist/render/atmosphere.js +9 -1
  7. package/dist/render/backend/api.d.ts +9 -1
  8. package/dist/render/backend/createRenderer.d.ts +6 -0
  9. package/dist/render/backend/createRenderer.js +4 -1
  10. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  11. package/dist/render/backend/webgl2/renderer.js +297 -16
  12. package/dist/render/backend/webgpu/device.d.ts +7 -1
  13. package/dist/render/backend/webgpu/device.js +31 -3
  14. package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
  15. package/dist/render/backend/webgpu/flatPass.js +58 -26
  16. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  17. package/dist/render/backend/webgpu/postPass.js +16 -2
  18. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  19. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +131 -2
  21. package/dist/render/backend/webgpu/renderer.js +586 -70
  22. package/dist/render/displayRange.d.ts +31 -0
  23. package/dist/render/displayRange.js +47 -0
  24. package/dist/render/filmicCurve.d.ts +74 -0
  25. package/dist/render/filmicCurve.js +203 -0
  26. package/dist/render/fog.d.ts +4 -1
  27. package/dist/render/fog.js +12 -2
  28. package/dist/render/frame/resources.d.ts +2 -2
  29. package/dist/render/frame/resources.js +6 -0
  30. package/dist/render/frameReflections.d.ts +40 -0
  31. package/dist/render/frameReflections.js +48 -0
  32. package/dist/render/pass.d.ts +49 -0
  33. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  34. package/dist/render/reflectionSurfaceTargets.js +99 -0
  35. package/dist/render/renderQuality.d.ts +39 -8
  36. package/dist/render/renderQuality.js +4 -0
  37. package/dist/render/sceneTarget.d.ts +5 -0
  38. package/dist/render/sceneTarget.js +24 -1
  39. package/dist/render/screenSpaceReflection.d.ts +5 -0
  40. package/dist/render/screenSpaceReflection.js +5 -0
  41. package/dist/render/shaders/arcane.d.ts +1 -1
  42. package/dist/render/shaders/bolt.d.ts +1 -1
  43. package/dist/render/shaders/caustics.d.ts +1 -1
  44. package/dist/render/shaders/film.d.ts +1 -1
  45. package/dist/render/shaders/filmLook.js +2 -1
  46. package/dist/render/shaders/fire.d.ts +1 -1
  47. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  48. package/dist/render/shaders/flat/glassTint.js +2 -0
  49. package/dist/render/shaders/flat/index.d.ts +10 -0
  50. package/dist/render/shaders/flat/index.js +6 -0
  51. package/dist/render/shaders/flat/main.d.ts +1 -1
  52. package/dist/render/shaders/flat/main.js +28 -0
  53. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  54. package/dist/render/shaders/flat/overlay.js +117 -0
  55. package/dist/render/shaders/flat/pointShadow.js +1 -1
  56. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  57. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  58. package/dist/render/shaders/fog.d.ts +1 -1
  59. package/dist/render/shaders/fog.js +9 -2
  60. package/dist/render/shaders/fringe.d.ts +29 -0
  61. package/dist/render/shaders/fringe.js +55 -0
  62. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  63. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  64. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  65. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  66. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  67. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  68. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  71. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -16
  73. package/dist/render/shaders/generated/flat.wgsl.js +214 -54
  74. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  75. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  76. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  77. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  78. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  79. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  80. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  81. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  82. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  83. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  84. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  85. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  86. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  87. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  88. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  89. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  90. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  91. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  92. package/dist/render/shaders/line.d.ts +1 -1
  93. package/dist/render/shaders/particle.d.ts +3 -3
  94. package/dist/render/shaders/rush.js +54 -11
  95. package/dist/render/shaders/scatter.d.ts +1 -1
  96. package/dist/render/shaders/smoke.d.ts +1 -1
  97. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  98. package/dist/render/shaders/ssrResolve.js +51 -0
  99. package/dist/render/shaders/ssrTrace.js +26 -0
  100. package/dist/render/shaders/water.d.ts +1 -1
  101. package/dist/render/ssrPass.d.ts +16 -0
  102. package/dist/render/ssrPass.js +106 -1
  103. package/dist/render/surfaceOverlay.d.ts +132 -0
  104. package/dist/render/surfaceOverlay.js +85 -0
  105. package/dist/render/surfaceTexture.d.ts +19 -0
  106. package/dist/render/surfaceTexture.js +69 -0
  107. package/dist/render/temporalAa.d.ts +14 -0
  108. package/dist/render/temporalAa.js +20 -0
  109. package/dist/render/vertexDefaults.d.ts +17 -0
  110. package/dist/render/vertexDefaults.js +24 -0
  111. package/package.json +6 -6
  112. package/src/index.ts +16 -0
  113. package/src/render/atmosphere.ts +22 -1
  114. package/src/render/backend/api.ts +9 -0
  115. package/src/render/backend/createRenderer.ts +18 -1
  116. package/src/render/backend/webgl2/renderer.ts +357 -13
  117. package/src/render/backend/webgpu/device.ts +42 -3
  118. package/src/render/backend/webgpu/flatPass.ts +63 -26
  119. package/src/render/backend/webgpu/postPass.ts +18 -0
  120. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  121. package/src/render/backend/webgpu/renderer.ts +735 -83
  122. package/src/render/displayRange.ts +61 -0
  123. package/src/render/filmicCurve.ts +229 -0
  124. package/src/render/fog.ts +19 -3
  125. package/src/render/frame/resources.ts +6 -0
  126. package/src/render/frameReflections.ts +72 -0
  127. package/src/render/pass.ts +53 -1
  128. package/src/render/reflectionSurfaceTargets.ts +105 -0
  129. package/src/render/renderQuality.ts +44 -8
  130. package/src/render/sceneTarget.ts +30 -2
  131. package/src/render/screenSpaceReflection.ts +5 -0
  132. package/src/render/shaders/filmLook.ts +2 -1
  133. package/src/render/shaders/flat/glassTint.ts +5 -1
  134. package/src/render/shaders/flat/index.ts +16 -0
  135. package/src/render/shaders/flat/main.ts +28 -0
  136. package/src/render/shaders/flat/overlay.ts +117 -0
  137. package/src/render/shaders/flat/pointShadow.ts +1 -1
  138. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  139. package/src/render/shaders/fog.ts +9 -2
  140. package/src/render/shaders/fringe.ts +59 -0
  141. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  142. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  143. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  144. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  145. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  146. package/src/render/shaders/generated/flat.wgsl.ts +214 -54
  147. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  148. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  149. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  150. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  151. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  152. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  153. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  154. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  155. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  156. package/src/render/shaders/rush.ts +54 -11
  157. package/src/render/shaders/ssrResolve.ts +52 -0
  158. package/src/render/shaders/ssrTrace.ts +26 -0
  159. package/src/render/ssrPass.ts +138 -1
  160. package/src/render/surfaceOverlay.ts +195 -0
  161. package/src/render/surfaceTexture.ts +67 -0
  162. package/src/render/temporalAa.ts +20 -0
  163. package/src/render/vertexDefaults.ts +28 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * What a draw lays over its surface beyond its material: a rim of light, an emissive overlay that
3
+ * dissolves the surface away, and a second normal map blended in by region. Set per draw with
4
+ * `RendererApi.setSurfaceOverlay`, so one character's whole set of materials changes together
5
+ * while another's does not.
6
+ *
7
+ * **Per draw and held, like `setEnvironmentGain`**: set before a character's draws, cleared after,
8
+ * and every draw between them wears it. **Images come from one atlas** (`maps`), each named by the
9
+ * rectangle it occupies — a scale and an offset in the atlas's uv — because the lit stage binds every
10
+ * sampler a device is guaranteed and has none left for a texture of its own: the atlas is bound in
11
+ * the slot the frame's refraction copy takes, which an opaque surface never reads. A draw that both
12
+ * refracts and wears an overlay's images keeps its refraction and loses the images, said once.
13
+ *
14
+ * **Fifteen vectors of numbers**, in the material's uniform block. WebGPU has the room; WebGL2 has
15
+ * room where its device reports more than the lit stage's 254 vectors, and refuses the overlay in
16
+ * words where it does not. See `packSurfaceOverlay` for the layout.
17
+ */
18
+ import type { Vec3 } from '../math/color.ts';
19
+ import type { SurfaceTextureHandle } from './backend/api.ts';
20
+
21
+ /** Where an image sits in the atlas: its size and its corner, each in the atlas's uv. */
22
+ export interface OverlayRegion {
23
+ readonly scale: readonly [number, number];
24
+ readonly offset: readonly [number, number];
25
+ }
26
+
27
+ /**
28
+ * Light along the silhouette: the edge `(1 - N·V)^falloff`, leaning toward normals that face up,
29
+ * raised to a contrast, times a noise laid in screen space, a slow pulse and a mask, in a colour.
30
+ * Added to what the surface emits, and not gated on the night as emission is: it is a glow the
31
+ * surface wears, not light it gives off.
32
+ */
33
+ export interface SurfaceRim {
34
+ readonly colour: Vec3;
35
+ /** How bright, 0 for none. Multiplied with `alpha`. */
36
+ readonly intensity: number;
37
+ readonly alpha?: number;
38
+ /** The edge's exponent: larger keeps the rim nearer the silhouette. 1.5 unless given. */
39
+ readonly falloff?: number;
40
+ /**
41
+ * How far the rim leans toward normals that face up: the weight is `(N·up · ½ + ½)^upward`, so
42
+ * 0 is no lean. 0.4 unless given.
43
+ */
44
+ readonly upward?: number;
45
+ /** The exponent the whole edge is raised to. 1 unless given. */
46
+ readonly contrast?: number;
47
+ /** A noise image laid in screen space and scrolled, the rim taken times it, clamped to 1. */
48
+ readonly noise?: {
49
+ readonly region: OverlayRegion;
50
+ /** Screens a second, along x and y. */
51
+ readonly scroll?: readonly [number, number];
52
+ /** Repeats across the screen's height. 1 unless given. */
53
+ readonly tiling?: number;
54
+ };
55
+ /** A pulse `½ + ½ sin(rate · t)`, held between `low` and `high`, on `Environment.surfaceTime`. */
56
+ readonly pulse?: { readonly rate: number; readonly low: number; readonly high: number };
57
+ /** A mask in the mesh's uv where the rim is erased: the rim times `1 - weight · mask`. */
58
+ readonly mask?: { readonly region: OverlayRegion; readonly weight: number };
59
+ }
60
+
61
+ /**
62
+ * The surface cut away where a noise in its uv falls under `threshold`, with a glowing band just
63
+ * above it, and a colour laid over the surface through the same noise.
64
+ */
65
+ export interface SurfaceDissolve {
66
+ readonly noise: { readonly region: OverlayRegion; readonly tiling?: readonly [number, number] };
67
+ /** 0 keeps everything; 1 cuts everything. */
68
+ readonly threshold: number;
69
+ /** The band's width, in the noise's own units. 0.05 unless given. */
70
+ readonly edge?: number;
71
+ /** The band's light. */
72
+ readonly edgeColour?: Vec3;
73
+ readonly edgeIntensity?: number;
74
+ /** A colour laid over the surface through the noise, as light, and how much of it. */
75
+ readonly overlay?: { readonly colour: Vec3; readonly amount: number };
76
+ }
77
+
78
+ /**
79
+ * A second normal map blended into the surface's by six region weights, read through two masks:
80
+ * the first mask's red, green and blue carry regions 1 to 3, the second's regions 4 to 6. A face's
81
+ * expressions, or veins brought up all over.
82
+ */
83
+ export interface SurfaceWrinkle {
84
+ readonly normal: OverlayRegion;
85
+ readonly masks: readonly [OverlayRegion, OverlayRegion];
86
+ readonly weights: readonly [number, number, number, number, number, number];
87
+ }
88
+
89
+ /**
90
+ * What a draw lays over its surface beyond its material, for `setSurfaceOverlay`: a rim, a dissolve
91
+ * and wrinkles, each optional, and the atlas their images are regions of.
92
+ */
93
+ export interface SurfaceOverlay {
94
+ /**
95
+ * The atlas every image above is a region of. Absent: no image is read whatever regions are
96
+ * named — rims are smooth, nothing dissolves and nothing wrinkles — because the slot then holds a
97
+ * stand-in whose colour would read as a noise and a mask.
98
+ */
99
+ readonly maps?: SurfaceTextureHandle;
100
+ readonly rim?: SurfaceRim;
101
+ readonly dissolve?: SurfaceDissolve;
102
+ readonly wrinkle?: SurfaceWrinkle;
103
+ }
104
+
105
+ /** Vectors the overlay takes in the material's block: `uOverlay[OVERLAY_VECTORS]`. */
106
+ export const OVERLAY_VECTORS = 15;
107
+ /** Floats `packSurfaceOverlay` fills. */
108
+ export const OVERLAY_FLOATS = OVERLAY_VECTORS * 4;
109
+
110
+ const finite = (value: number | undefined, fallback: number): number =>
111
+ value !== undefined && Number.isFinite(value) ? value : fallback;
112
+
113
+ function region(out: Float32Array, at: number, r: OverlayRegion | undefined | null): void {
114
+ out[at] = finite(r?.scale[0], 0);
115
+ out[at + 1] = finite(r?.scale[1], 0);
116
+ out[at + 2] = finite(r?.offset[0], 0);
117
+ out[at + 3] = finite(r?.offset[1], 0);
118
+ }
119
+
120
+ /**
121
+ * An overlay as the shader reads it, into `out` (`OVERLAY_FLOATS`); all zeros for `null`, which is
122
+ * no overlay. A region with a zero scale is an image not given, and its term reads as none — a
123
+ * noise of 1, a mask of 0, no wrinkle — and every region is one without `maps`. Fifteen vectors:
124
+ *
125
+ * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim)
126
+ * - 1: falloff, upward lean, contrast, mask weight
127
+ * - 2: the rim's noise region; 3: its scroll along x and y, its tiling, the pulse's rate
128
+ * - 4: the pulse's low and high (1 and 1 where none)
129
+ * - 5: the rim's mask region
130
+ * - 6: the dissolve's noise region (a zero scale is no dissolve); 7: its tiling, threshold, edge
131
+ * - 8: the edge's light and its strength; 9: the laid-over colour and its amount
132
+ * - 10: the wrinkle normal's region; 11 and 12: the two masks'; 13 and 14: the six weights
133
+ */
134
+ export function packSurfaceOverlay(overlay: SurfaceOverlay | null, out: Float32Array): void {
135
+ out.fill(0);
136
+ if (overlay === null) return;
137
+ /* No atlas, no region: see `SurfaceOverlay.maps`. */
138
+ const images = overlay.maps !== undefined;
139
+ const rim = overlay.rim;
140
+ if (rim !== undefined) {
141
+ out[0] = finite(rim.colour[0], 0);
142
+ out[1] = finite(rim.colour[1], 0);
143
+ out[2] = finite(rim.colour[2], 0);
144
+ out[3] = Math.max(0, finite(rim.intensity, 0) * finite(rim.alpha, 1));
145
+ out[4] = Math.max(0, finite(rim.falloff, 1.5));
146
+ out[5] = Math.max(0, finite(rim.upward, 0.4));
147
+ out[6] = Math.max(0, finite(rim.contrast, 1));
148
+ out[7] = rim.mask === undefined ? 0 : Math.min(Math.max(finite(rim.mask.weight, 0), 0), 1);
149
+ region(out, 8, images ? rim.noise?.region : null);
150
+ out[12] = finite(rim.noise?.scroll?.[0], 0);
151
+ out[13] = finite(rim.noise?.scroll?.[1], 0);
152
+ out[14] = Math.max(0, finite(rim.noise?.tiling, 1));
153
+ out[15] = finite(rim.pulse?.rate, 0);
154
+ out[16] = rim.pulse === undefined ? 1 : finite(rim.pulse.low, 1);
155
+ out[17] = rim.pulse === undefined ? 1 : finite(rim.pulse.high, 1);
156
+ region(out, 20, images ? rim.mask?.region : null);
157
+ }
158
+ const dissolve = overlay.dissolve;
159
+ if (dissolve !== undefined) {
160
+ region(out, 24, images ? dissolve.noise.region : null);
161
+ out[28] = finite(dissolve.noise.tiling?.[0], 1);
162
+ out[29] = finite(dissolve.noise.tiling?.[1], 1);
163
+ out[30] = Math.min(Math.max(finite(dissolve.threshold, 0), 0), 1);
164
+ out[31] = Math.max(1e-4, finite(dissolve.edge, 0.05));
165
+ const edge = dissolve.edgeColour ?? [0, 0, 0];
166
+ out[32] = finite(edge[0], 0);
167
+ out[33] = finite(edge[1], 0);
168
+ out[34] = finite(edge[2], 0);
169
+ out[35] = Math.max(0, finite(dissolve.edgeIntensity, 1));
170
+ const laid = dissolve.overlay;
171
+ if (laid !== undefined) {
172
+ out[36] = finite(laid.colour[0], 0);
173
+ out[37] = finite(laid.colour[1], 0);
174
+ out[38] = finite(laid.colour[2], 0);
175
+ out[39] = Math.max(0, finite(laid.amount, 0));
176
+ }
177
+ }
178
+ const wrinkle = overlay.wrinkle;
179
+ if (wrinkle !== undefined) {
180
+ region(out, 40, images ? wrinkle.normal : null);
181
+ region(out, 44, images ? wrinkle.masks[0] : null);
182
+ region(out, 48, images ? wrinkle.masks[1] : null);
183
+ for (let i = 0; i < 6; i++) {
184
+ out[52 + i] = Math.min(Math.max(finite(wrinkle.weights[i], 0), 0), 1);
185
+ }
186
+ }
187
+ }
188
+
189
+ /** Whether an overlay lays anything at all, so a renderer can leave the lit switch off. */
190
+ export function overlayLays(overlay: SurfaceOverlay | null): boolean {
191
+ return (
192
+ overlay !== null &&
193
+ (overlay.rim !== undefined || overlay.dissolve !== undefined || overlay.wrinkle !== undefined)
194
+ );
195
+ }
@@ -293,6 +293,13 @@ export class SurfaceTexture {
293
293
  private effectsTable: WebGLTexture | null = null;
294
294
  /** The block format a compressed texture holds, or null for one uploaded from images. */
295
295
  private readonly blockFormat: BlockFormat | null;
296
+ /** Level 0's size, which `flatLayer` copies; kept as `update` changes it. */
297
+ private width = 0;
298
+ private height = 0;
299
+ /** The texture's storage format, which `flatLayer`'s copy matches so the copy converts nothing. */
300
+ private readonly storage: number;
301
+ /** Layer 0 as a plain 2D texture, made by `flatLayer` and dropped when the pixels change. */
302
+ private flat: WebGLTexture | null = null;
296
303
 
297
304
  /**
298
305
  * One image, or an array of images that must share one size (`layerSize` refuses otherwise).
@@ -327,6 +334,9 @@ export class SurfaceTexture {
327
334
  this.layers = 1;
328
335
  this.blockFormat = null;
329
336
  this.mipmapped = false;
337
+ this.width = capture.width;
338
+ this.height = capture.height;
339
+ this.storage = capture.float ? gl.RGBA16F : gl.RGBA8;
330
340
  gl.texStorage3D(
331
341
  gl.TEXTURE_2D_ARRAY,
332
342
  1,
@@ -340,6 +350,9 @@ export class SurfaceTexture {
340
350
  this.layers = baked.layers.length;
341
351
  this.blockFormat = null;
342
352
  this.mipmapped = false;
353
+ this.width = baked.width;
354
+ this.height = baked.height;
355
+ this.storage = gl.RGBA8;
343
356
  /* A typed array refuses either flag set, so both are put down here rather than assumed. */
344
357
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
345
358
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
@@ -377,6 +390,7 @@ export class SurfaceTexture {
377
390
  `generateMipmap` cannot write a compressed format. */
378
391
  this.layers = blocks.length;
379
392
  this.blockFormat = (blocks[0] as CompressedTextureSource).format;
393
+ this.storage = gl.NONE;
380
394
  this.mipmapped = (options.mipmap ?? true) && plan.levels > 1;
381
395
  uploadCompressedArray(gl, plan.name, blocks, this.mipmapped ? plan.levels : 1);
382
396
  } else {
@@ -384,6 +398,10 @@ export class SurfaceTexture {
384
398
  this.layers = sources.length;
385
399
  this.blockFormat = null;
386
400
  this.mipmapped = options.mipmap ?? true;
401
+ const { width, height } = layerSize(sources);
402
+ this.width = width;
403
+ this.height = height;
404
+ this.storage = this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA8;
387
405
  uploadImages(gl, sources, this.srgb);
388
406
  }
389
407
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
@@ -462,6 +480,9 @@ export class SurfaceTexture {
462
480
  refuseArrayUpdate(this.layers);
463
481
  refuseBlockUpdate(this.blockFormat);
464
482
  const { width, height } = sourceSize(source);
483
+ this.width = width;
484
+ this.height = height;
485
+ this.dropFlat(gl);
465
486
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
466
487
  /* Unflipped, matching the constructor. See it for why, and for what it cost. */
467
488
  gl.texImage3D(
@@ -494,8 +515,54 @@ export class SurfaceTexture {
494
515
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
495
516
  }
496
517
 
518
+ /**
519
+ * Layer 0 as a plain 2D texture: what a `sampler2D` can be handed, which an array cannot. Copied
520
+ * the first time it is asked for, by a framebuffer blit into storage of the same format, and kept
521
+ * until `update` or `dispose`. Engine-internal: a surface overlay's atlas is bound where the
522
+ * lit stage's one 2D image slot is (`surfaceOverlay.ts`).
523
+ *
524
+ * **Null for blocks**, which no framebuffer can read; the caller says so. What it costs is a copy
525
+ * of level 0 in memory beside the array, and the bindings of both framebuffer targets, which are
526
+ * read back before the blit and put back after it — a sync query, paid once per image.
527
+ */
528
+ flatLayer(gl: WebGL2RenderingContext): WebGLTexture | null {
529
+ if (this.texture === null || this.blockFormat !== null) return null;
530
+ if (this.flat !== null) return this.flat;
531
+ const flat = gl.createTexture();
532
+ const read = gl.createFramebuffer();
533
+ const draw = gl.createFramebuffer();
534
+ if (flat === null || read === null || draw === null) return null;
535
+ gl.bindTexture(gl.TEXTURE_2D, flat);
536
+ gl.texStorage2D(gl.TEXTURE_2D, 1, this.storage, this.width, this.height);
537
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
538
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
539
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
540
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
541
+ gl.bindTexture(gl.TEXTURE_2D, null);
542
+ const heldRead = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
543
+ const heldDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
544
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, read);
545
+ gl.framebufferTextureLayer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, 0);
546
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, draw);
547
+ gl.framebufferTexture2D(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, flat, 0);
548
+ const [w, h] = [this.width, this.height];
549
+ gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, gl.COLOR_BUFFER_BIT, gl.NEAREST);
550
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, heldRead);
551
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, heldDraw);
552
+ gl.deleteFramebuffer(read);
553
+ gl.deleteFramebuffer(draw);
554
+ this.flat = flat;
555
+ return flat;
556
+ }
557
+
558
+ private dropFlat(gl: WebGL2RenderingContext): void {
559
+ if (this.flat !== null) gl.deleteTexture(this.flat);
560
+ this.flat = null;
561
+ }
562
+
497
563
  dispose(gl: WebGL2RenderingContext): void {
498
564
  if (this.texture === null) return;
565
+ this.dropFlat(gl);
499
566
  gl.deleteTexture(this.texture);
500
567
  this.texture = null;
501
568
  if (this.effectsTable !== null) gl.deleteTexture(this.effectsTable);
@@ -451,6 +451,17 @@ export class TemporalHistory {
451
451
  private picture = false;
452
452
  private width = 0;
453
453
  private height = 0;
454
+ /** Whether this frame of the renderer's has opened one of the sequence, and what it answered. */
455
+ private opened = false;
456
+ private answer = false;
457
+
458
+ /**
459
+ * A frame of the renderer's began: the next `openFrame` opens a frame of the sequence. Called at
460
+ * `beginFrame`, by both backends.
461
+ */
462
+ nextFrame(): void {
463
+ this.opened = false;
464
+ }
454
465
 
455
466
  /**
456
467
  * Open a frame at this target size, and answer whether the history may be sampled.
@@ -458,13 +469,22 @@ export class TemporalHistory {
458
469
  * **The jitter advances whether or not it may be.** Holding the sequence still on the frames a
459
470
  * resize or a cut discarded would sample the same position twice running, which is one sample
460
471
  * and not two.
472
+ *
473
+ * **Once a frame, however many times it is asked.** A renderer asks where the scene's camera
474
+ * arrives, and a frame binds that camera again after a scene capture or anything else that bound
475
+ * another; each asking opened a frame of its own, so the sequence stepped twice in such a frame
476
+ * and the edges of the picture swam. Asked again, it gives the answer it gave. What it gives up:
477
+ * a size changed between two asks in one frame is seen at the next frame, not at once.
461
478
  */
462
479
  openFrame(width: number, height: number): boolean {
480
+ if (this.opened) return this.answer;
481
+ this.opened = true;
463
482
  this.frameIndex++;
464
483
  const usable = this.picture && width === this.width && height === this.height;
465
484
  if (!usable) this.picture = false;
466
485
  this.width = width;
467
486
  this.height = height;
487
+ this.answer = usable;
468
488
  return usable;
469
489
  }
470
490
 
@@ -113,8 +113,18 @@ export const OUTPUT_TRANSFORM_CODE: Readonly<Record<string, number>> = {
113
113
  srgb: 1,
114
114
  aces: 2,
115
115
  shoulder: 3,
116
+ filmic: 4,
116
117
  };
117
118
 
119
+ /**
120
+ * The code a **forward** pass grades with: the transform's own, except `filmic`, which only the
121
+ * composite carries the constants for, and which a pass grading itself therefore draws as `aces`.
122
+ * Both backends' `gradeCode` read this, so neither can hand a forward stage a code it lacks.
123
+ */
124
+ export function forwardTransformCode(transform: string): number {
125
+ return transform === 'filmic' ? 2 : (OUTPUT_TRANSFORM_CODE[transform] ?? 0);
126
+ }
127
+
118
128
  /**
119
129
  * How far the speed rush reaches, and how far a motion-blurred pixel may travel, both in UV.
120
130
  *
@@ -123,3 +133,21 @@ export const OUTPUT_TRANSFORM_CODE: Readonly<Record<string, number>> = {
123
133
  */
124
134
  export const RUSH_REACH_UV = 0.012;
125
135
  export const MOTION_BLUR_MAX_UV = 0.03;
136
+
137
+ /**
138
+ * A longest smear as `setCameraMotionBlur` takes it: a share of the frame, above zero and at most
139
+ * half, and the default where the number is not one. Shared, so both backends cap one way.
140
+ */
141
+ export function clampMotionShare(share: number): number {
142
+ return Number.isFinite(share) && share > 0 ? Math.min(share, 0.5) : MOTION_BLUR_MAX_UV;
143
+ }
144
+
145
+ /**
146
+ * Said once where a draw names where it was last frame and the frame blurs by motion, but keeps no
147
+ * motion target to blur that draw by: each object's own motion is drawn only under a
148
+ * reconstruction on WebGPU, and anywhere else the camera's motion alone smears the frame.
149
+ */
150
+ export const OBJECT_BLUR_UNKEPT =
151
+ "[driftengine] motion blur follows each draw's own motion only under a reconstruction " +
152
+ "(DriftTR, WebGPU), which draws the motion target it reads; here the camera's motion alone " +
153
+ 'blurs the frame, and a moving object is smeared only as the camera moved past it.';