@driftengine/core 4.4.0 → 4.4.2

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 (49) hide show
  1. package/CHANGELOG.json +47 -0
  2. package/README.md +1 -1
  3. package/dist/render/ambientOcclusionPass.js +5 -1
  4. package/dist/render/backend/webgl2/renderer.d.ts +3 -0
  5. package/dist/render/backend/webgl2/renderer.js +13 -0
  6. package/dist/render/backend/webgpu/postPass.d.ts +20 -4
  7. package/dist/render/backend/webgpu/postPass.js +17 -4
  8. package/dist/render/backend/webgpu/renderer.d.ts +10 -0
  9. package/dist/render/backend/webgpu/renderer.js +74 -7
  10. package/dist/render/flameLight.js +15 -2
  11. package/dist/render/pointShadowBudget.d.ts +12 -2
  12. package/dist/render/pointShadowBudget.js +67 -51
  13. package/dist/render/recon/disocclusion.d.ts +36 -3
  14. package/dist/render/recon/disocclusion.js +56 -4
  15. package/dist/render/recon/resolve.js +95 -12
  16. package/dist/render/sceneTarget.d.ts +2 -0
  17. package/dist/render/sceneTarget.js +1 -1
  18. package/dist/render/shaders/ambientOcclusion.d.ts +39 -1
  19. package/dist/render/shaders/ambientOcclusion.js +69 -9
  20. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +8 -3
  21. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +8 -3
  22. package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +31 -6
  23. package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
  24. package/dist/render/shaders/recon/resolve.wgsl.d.ts +1 -1
  25. package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
  26. package/dist/render/shaders/temporalResolve.js +133 -5
  27. package/dist/render/temporalAa.d.ts +145 -0
  28. package/dist/render/temporalAa.js +190 -0
  29. package/dist/render/temporalPass.d.ts +16 -14
  30. package/dist/render/temporalPass.js +59 -14
  31. package/package.json +6 -6
  32. package/src/render/ambientOcclusionPass.ts +5 -1
  33. package/src/render/backend/webgl2/renderer.ts +21 -1
  34. package/src/render/backend/webgpu/postPass.ts +34 -4
  35. package/src/render/backend/webgpu/renderer.ts +77 -7
  36. package/src/render/flameLight.ts +15 -2
  37. package/src/render/pointShadowBudget.ts +65 -47
  38. package/src/render/recon/disocclusion.ts +54 -3
  39. package/src/render/recon/resolve.ts +137 -10
  40. package/src/render/sceneTarget.ts +8 -1
  41. package/src/render/shaders/ambientOcclusion.ts +81 -9
  42. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +8 -3
  43. package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
  44. package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
  45. package/src/render/shaders/temporalResolve.ts +141 -5
  46. package/src/render/temporalAa.ts +227 -0
  47. package/src/render/temporalPass.ts +88 -12
  48. package/dist/render/shaders/agx.d.ts +0 -31
  49. package/dist/render/shaders/agx.js +0 -81
@@ -15,6 +15,9 @@ import { TEMPORAL_RESOLVE_FRAG } from './shaders/temporalResolve.ts';
15
15
  * grade all read the scene texture, and reading the unresolved one would grade a stable picture
16
16
  * from an unstable source and put the crawl back on screen through the bloom.
17
17
  */
18
+ /** The resolve's three outputs: the picture and the two halves of the flicker record. */
19
+ const DRAW_ALL = [0x8ce0, 0x8ce1, 0x8ce2]; // COLOR_ATTACHMENT0, 1, 2
20
+
18
21
  export class TemporalPass {
19
22
  private readonly program: WebGLProgram;
20
23
  private readonly uniforms: Record<string, WebGLUniformLocation | null>;
@@ -22,6 +25,21 @@ export class TemporalPass {
22
25
 
23
26
  /** The pair, and which of them the next resolve writes into. */
24
27
  private textures: (WebGLTexture | null)[] = [null, null];
28
+ /**
29
+ * Each pixel's flicker record, a pair beside the history's and swapped with it: see the
30
+ * anti-flicker in `temporalAa.ts`. In the history's own format, which holds what the record does —
31
+ * a change and a sample are colours' luma, and eight bits hold them wherever eight bits hold the
32
+ * colour.
33
+ */
34
+ private records: (WebGLTexture | null)[] = [null, null];
35
+ /** The rest of each pixel's record — depth, pull and recent swing — beside the first. */
36
+ private motions: (WebGLTexture | null)[] = [null, null];
37
+ /**
38
+ * Whether the records can be half float, which the depth in them needs. Asked of the context
39
+ * rather than taken from the scene's format, since a record of an eight-bit scene still holds
40
+ * metres. Without it the anti-flicker is off and the resolve is the one before it existed.
41
+ */
42
+ private readonly recordFloat: boolean;
25
43
  private framebuffers: (WebGLFramebuffer | null)[] = [null, null];
26
44
  private write = 0;
27
45
 
@@ -40,6 +58,7 @@ export class TemporalPass {
40
58
  */
41
59
  private readonly floatColor: boolean,
42
60
  ) {
61
+ this.recordFloat = gl.getExtension('EXT_color_buffer_float') !== null;
43
62
  this.program = compileProgram(gl, FULLSCREEN_VERT, TEMPORAL_RESOLVE_FRAG, 'temporalResolve');
44
63
  this.uniforms = uniformLocations(gl, this.program, 'temporalPass');
45
64
  const vao = gl.createVertexArray();
@@ -55,19 +74,45 @@ export class TemporalPass {
55
74
 
56
75
  for (let i = 0; i < 2; i++) {
57
76
  if (this.textures[i] === null) this.textures[i] = gl.createTexture();
58
- gl.bindTexture(gl.TEXTURE_2D, this.textures[i]);
59
- if (this.floatColor) {
60
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
61
- } else {
62
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
77
+ if (this.records[i] === null) this.records[i] = gl.createTexture();
78
+ if (this.motions[i] === null) this.motions[i] = gl.createTexture();
79
+ for (const texture of [this.textures[i], this.records[i], this.motions[i]]) {
80
+ gl.bindTexture(gl.TEXTURE_2D, texture);
81
+ if (texture === this.textures[i] ? this.floatColor : this.recordFloat) {
82
+ /* The depth record is two numbers a pixel: metres, and how far they swing. */
83
+ const pair = texture === this.motions[i];
84
+ gl.texImage2D(
85
+ gl.TEXTURE_2D,
86
+ 0,
87
+ pair ? gl.RG16F : gl.RGBA16F,
88
+ width,
89
+ height,
90
+ 0,
91
+ pair ? gl.RG : gl.RGBA,
92
+ gl.HALF_FLOAT,
93
+ null,
94
+ );
95
+ } else {
96
+ gl.texImage2D(
97
+ gl.TEXTURE_2D,
98
+ 0,
99
+ gl.RGBA,
100
+ width,
101
+ height,
102
+ 0,
103
+ gl.RGBA,
104
+ gl.UNSIGNED_BYTE,
105
+ null,
106
+ );
107
+ }
108
+ /* Linear, because the history is sampled between texels wherever the reprojection lands;
109
+ clamped, because a sample that wrapped would pull the far edge of the screen into the
110
+ corner — and the shader rejects an off-screen reprojection before it can anyway. */
111
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
112
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
113
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
114
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
63
115
  }
64
- /* Linear, because the history is sampled between texels wherever the reprojection lands;
65
- clamped, because a sample that wrapped would pull the far edge of the screen into the
66
- corner — and the shader rejects an off-screen reprojection before it can anyway. */
67
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
68
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
69
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
70
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
71
116
 
72
117
  if (this.framebuffers[i] === null) this.framebuffers[i] = gl.createFramebuffer();
73
118
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i]);
@@ -78,6 +123,22 @@ export class TemporalPass {
78
123
  this.textures[i],
79
124
  0,
80
125
  );
126
+ gl.framebufferTexture2D(
127
+ gl.FRAMEBUFFER,
128
+ gl.COLOR_ATTACHMENT1,
129
+ gl.TEXTURE_2D,
130
+ this.records[i],
131
+ 0,
132
+ );
133
+ gl.framebufferTexture2D(
134
+ gl.FRAMEBUFFER,
135
+ gl.COLOR_ATTACHMENT2,
136
+ gl.TEXTURE_2D,
137
+ this.motions[i],
138
+ 0,
139
+ );
140
+ /* Per framebuffer, so set once here: the resolve writes the picture and the record. */
141
+ gl.drawBuffers(DRAW_ALL);
81
142
  }
82
143
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
83
144
  }
@@ -96,6 +157,8 @@ export class TemporalPass {
96
157
  width: number,
97
158
  height: number,
98
159
  blend: number,
160
+ periodStart: boolean,
161
+ depthToViewZ: Float32Array,
99
162
  ): WebGLTexture {
100
163
  const { gl } = this;
101
164
  this.ensureSize(width, height);
@@ -120,10 +183,19 @@ export class TemporalPass {
120
183
  gl.activeTexture(gl.TEXTURE2);
121
184
  gl.bindTexture(gl.TEXTURE_2D, depth);
122
185
  gl.uniform1i(this.uniforms['uDepth'] ?? null, 2);
186
+ gl.activeTexture(gl.TEXTURE3);
187
+ gl.bindTexture(gl.TEXTURE_2D, this.records[history]);
188
+ gl.uniform1i(this.uniforms['uFlicker'] ?? null, 3);
189
+ gl.activeTexture(gl.TEXTURE4);
190
+ gl.bindTexture(gl.TEXTURE_2D, this.motions[history]);
191
+ gl.uniform1i(this.uniforms['uFlickerMotion'] ?? null, 4);
192
+ gl.uniform4fv(this.uniforms['uDepthToViewZ'] ?? null, depthToViewZ);
193
+ gl.uniform1f(this.uniforms['uAntiFlicker'] ?? null, this.recordFloat ? 1 : 0);
123
194
 
124
195
  gl.uniformMatrix4fv(this.uniforms['uReprojection'] ?? null, false, reprojection);
125
196
  gl.uniform2f(this.uniforms['uTexel'] ?? null, 1 / width, 1 / height);
126
197
  gl.uniform1f(this.uniforms['uHistoryBlend'] ?? null, blend);
198
+ gl.uniform1f(this.uniforms['uPeriodStart'] ?? null, periodStart ? 1 : 0);
127
199
 
128
200
  gl.bindVertexArray(this.vao);
129
201
  gl.drawArrays(gl.TRIANGLES, 0, 3);
@@ -141,8 +213,12 @@ export class TemporalPass {
141
213
  const { gl } = this;
142
214
  for (let i = 0; i < 2; i++) {
143
215
  if (this.textures[i] !== null) gl.deleteTexture(this.textures[i]);
216
+ if (this.records[i] !== null) gl.deleteTexture(this.records[i]);
217
+ if (this.motions[i] !== null) gl.deleteTexture(this.motions[i]);
144
218
  if (this.framebuffers[i] !== null) gl.deleteFramebuffer(this.framebuffers[i]);
145
219
  this.textures[i] = null;
220
+ this.records[i] = null;
221
+ this.motions[i] = null;
146
222
  this.framebuffers[i] = null;
147
223
  }
148
224
  gl.deleteProgram(this.program);
@@ -1,31 +0,0 @@
1
- /**
2
- * AgX: a tone curve whose shade stays open, for scenes lit mostly by bounce.
3
- *
4
- * **What ACES does to a courtyard.** The reference RRT and ODT are graded for a dark cinema, and
5
- * their toe is steep: a surface at 1% of white leaves the curve at 4 of 255. A room lit by light
6
- * bounced off its own walls lives in exactly that range, so its arcades read black beside renders
7
- * of the same model, and the grime in its stone reads as ink. Measured on a bought courtyard: 39%
8
- * of the frame below 16 of 255, against about 5% in the model maker's own renders.
9
- *
10
- * **AgX encodes the scene logarithmically and draws one sigmoid through it**, Troy Sobotka's
11
- * construction as Blender, Filament and three.js ship it. The same 1% leaves at 18 of 255, middle
12
- * grey lands at half the display range, and a highlight desaturates toward white instead of
13
- * clipping to a hue. What it gives up is ACES's punch in the midtones: the same scene reads
14
- * flatter, which a grade adds back as a look where ACES bakes it into the conversion. What would
15
- * make it the wrong choice is a world graded against three.js's or Unreal's ACES, which is the
16
- * match `aces` exists for.
17
- *
18
- * The constants are the Filament and three.js ones: Rec. 2020 primaries, the inset and outset
19
- * matrices, an encoding from 2^-10 to 2^6.5 times middle grey, and the iolite polynomial fit of
20
- * the default contrast curve, whose mean squared error against Blender's is 3.7e-6.
21
- */
22
- /**
23
- * `agxFilmic(vec3)`: linear sRGB in, linear sRGB out, clamped to [0, 1]. Reads `uOutputExposure`,
24
- * which the including source declares.
25
- */
26
- export declare const AGX_GLSL = "\nvec3 agxContrast(vec3 x) {\n vec3 x2 = x * x;\n vec3 x4 = x2 * x2;\n return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;\n}\n\nvec3 agxFilmic(vec3 x) {\n x *= uOutputExposure;\n // Columns, as GLSL reads them: linear sRGB to linear Rec. 2020, then the inset.\n const mat3 SRGB_TO_REC2020 = mat3(\n 0.6274, 0.0691, 0.0164,\n 0.3293, 0.9195, 0.0880,\n 0.0433, 0.0113, 0.8956\n );\n const mat3 AGX_INSET = mat3(\n 0.856627153315983, 0.137318972929847, 0.11189821299995,\n 0.0951212405381588, 0.761241990602591, 0.0767994186031903,\n 0.0482516061458583, 0.101439036467562, 0.811302368396859\n );\n const mat3 AGX_OUTSET = mat3(\n 1.1271005818144368, -0.1413297634984383, -0.14132976349843826,\n -0.11060664309660323, 1.157823702216272, -0.11060664309660294,\n -0.016493938717834573, -0.016493938717834257, 1.2519364065950405\n );\n const mat3 REC2020_TO_SRGB = mat3(\n 1.6605, -0.1246, -0.0182,\n -0.5876, 1.1329, -0.1006,\n -0.0728, -0.0083, 1.1187\n );\n vec3 c = AGX_INSET * (SRGB_TO_REC2020 * x);\n /* log2 of middle grey times 2^-10 and 2^6.5: -12.47393 and 4.026069, 16.5 stops apart. */\n c = clamp((log2(max(c, vec3(1e-10))) + 12.47393) / 16.5, 0.0, 1.0);\n c = AGX_OUTSET * agxContrast(c);\n c = pow(max(c, vec3(0.0)), vec3(2.2));\n return clamp(REC2020_TO_SRGB * c, 0.0, 1.0);\n}\n";
27
- /**
28
- * The curve for a neutral grey, as the shader computes it: every matrix above keeps grey grey, so
29
- * one channel is the whole of it. For a test to pin to hand-derived values.
30
- */
31
- export declare function agxGrey(linear: number, exposure?: number): number;
@@ -1,81 +0,0 @@
1
- /**
2
- * AgX: a tone curve whose shade stays open, for scenes lit mostly by bounce.
3
- *
4
- * **What ACES does to a courtyard.** The reference RRT and ODT are graded for a dark cinema, and
5
- * their toe is steep: a surface at 1% of white leaves the curve at 4 of 255. A room lit by light
6
- * bounced off its own walls lives in exactly that range, so its arcades read black beside renders
7
- * of the same model, and the grime in its stone reads as ink. Measured on a bought courtyard: 39%
8
- * of the frame below 16 of 255, against about 5% in the model maker's own renders.
9
- *
10
- * **AgX encodes the scene logarithmically and draws one sigmoid through it**, Troy Sobotka's
11
- * construction as Blender, Filament and three.js ship it. The same 1% leaves at 18 of 255, middle
12
- * grey lands at half the display range, and a highlight desaturates toward white instead of
13
- * clipping to a hue. What it gives up is ACES's punch in the midtones: the same scene reads
14
- * flatter, which a grade adds back as a look where ACES bakes it into the conversion. What would
15
- * make it the wrong choice is a world graded against three.js's or Unreal's ACES, which is the
16
- * match `aces` exists for.
17
- *
18
- * The constants are the Filament and three.js ones: Rec. 2020 primaries, the inset and outset
19
- * matrices, an encoding from 2^-10 to 2^6.5 times middle grey, and the iolite polynomial fit of
20
- * the default contrast curve, whose mean squared error against Blender's is 3.7e-6.
21
- */
22
- /**
23
- * `agxFilmic(vec3)`: linear sRGB in, linear sRGB out, clamped to [0, 1]. Reads `uOutputExposure`,
24
- * which the including source declares.
25
- */
26
- export const AGX_GLSL = `
27
- vec3 agxContrast(vec3 x) {
28
- vec3 x2 = x * x;
29
- vec3 x4 = x2 * x2;
30
- return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
31
- }
32
-
33
- vec3 agxFilmic(vec3 x) {
34
- x *= uOutputExposure;
35
- // Columns, as GLSL reads them: linear sRGB to linear Rec. 2020, then the inset.
36
- const mat3 SRGB_TO_REC2020 = mat3(
37
- 0.6274, 0.0691, 0.0164,
38
- 0.3293, 0.9195, 0.0880,
39
- 0.0433, 0.0113, 0.8956
40
- );
41
- const mat3 AGX_INSET = mat3(
42
- 0.856627153315983, 0.137318972929847, 0.11189821299995,
43
- 0.0951212405381588, 0.761241990602591, 0.0767994186031903,
44
- 0.0482516061458583, 0.101439036467562, 0.811302368396859
45
- );
46
- const mat3 AGX_OUTSET = mat3(
47
- 1.1271005818144368, -0.1413297634984383, -0.14132976349843826,
48
- -0.11060664309660323, 1.157823702216272, -0.11060664309660294,
49
- -0.016493938717834573, -0.016493938717834257, 1.2519364065950405
50
- );
51
- const mat3 REC2020_TO_SRGB = mat3(
52
- 1.6605, -0.1246, -0.0182,
53
- -0.5876, 1.1329, -0.1006,
54
- -0.0728, -0.0083, 1.1187
55
- );
56
- vec3 c = AGX_INSET * (SRGB_TO_REC2020 * x);
57
- /* log2 of middle grey times 2^-10 and 2^6.5: -12.47393 and 4.026069, 16.5 stops apart. */
58
- c = clamp((log2(max(c, vec3(1e-10))) + 12.47393) / 16.5, 0.0, 1.0);
59
- c = AGX_OUTSET * agxContrast(c);
60
- c = pow(max(c, vec3(0.0)), vec3(2.2));
61
- return clamp(REC2020_TO_SRGB * c, 0.0, 1.0);
62
- }
63
- `;
64
- /**
65
- * The curve for a neutral grey, as the shader computes it: every matrix above keeps grey grey, so
66
- * one channel is the whole of it. For a test to pin to hand-derived values.
67
- */
68
- export function agxGrey(linear, exposure = 1) {
69
- const x = Math.max(linear * exposure, 1e-10);
70
- const t = Math.min(Math.max((Math.log2(x) + 12.47393) / 16.5, 0), 1);
71
- const t2 = t * t;
72
- const t4 = t2 * t2;
73
- const s = 15.5 * t4 * t2 -
74
- 40.14 * t4 * t +
75
- 31.96 * t4 -
76
- 6.868 * t2 * t +
77
- 0.4298 * t2 +
78
- 0.1191 * t -
79
- 0.00232;
80
- return Math.min(Math.max(Math.max(s, 0) ** 2.2, 0), 1);
81
- }