@woosh/meep-engine 3.65.0 → 3.67.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 (50) hide show
  1. package/package.json +1 -1
  2. package/src/format/lut/cube/CubeLut.d.ts +38 -0
  3. package/src/format/lut/cube/CubeLut.d.ts.map +1 -0
  4. package/src/format/lut/cube/CubeLut.js +23 -0
  5. package/src/format/lut/cube/cube_read.d.ts +23 -0
  6. package/src/format/lut/cube/cube_read.d.ts.map +1 -0
  7. package/src/format/lut/cube/cube_read.js +199 -0
  8. package/src/format/lut/cube/cube_write.d.ts +15 -0
  9. package/src/format/lut/cube/cube_write.d.ts.map +1 -0
  10. package/src/format/lut/cube/cube_write.js +90 -0
  11. package/src/shade/playground/probe_clipmap/README.md +1 -0
  12. package/src/shade/playground/probe_clipmap/index.html +5 -0
  13. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
  14. package/src/shade/playground/probe_clipmap/main.js +7 -0
  15. package/src/shade/renderer/Renderer.js +12 -12
  16. package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts +73 -4
  17. package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts.map +1 -1
  18. package/src/shade/renderer/deferred/tonemap/ColorGrade.js +113 -7
  19. package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.d.ts +12 -0
  20. package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.d.ts.map +1 -0
  21. package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.js +12 -0
  22. package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts +15 -1
  23. package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts.map +1 -1
  24. package/src/shade/renderer/deferred/tonemap/ToneMapper.js +31 -3
  25. package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.d.ts +25 -0
  26. package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.d.ts.map +1 -0
  27. package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.js +51 -0
  28. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts +2 -2
  29. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts.map +1 -1
  30. package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.js +6 -3
  31. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts +3 -2
  32. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts.map +1 -1
  33. package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.js +7 -3
  34. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts +35 -9
  35. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts.map +1 -1
  36. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.js +81 -11
  37. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.d.ts.map +1 -1
  38. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.js +4 -1
  39. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts +6 -0
  40. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts.map +1 -1
  41. package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.js +7 -0
  42. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts +4 -4
  43. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts.map +1 -1
  44. package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.js +86 -5
  45. package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.d.ts +65 -0
  46. package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.d.ts.map +1 -0
  47. package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.js +136 -0
  48. package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.d.ts +0 -11
  49. package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.d.ts.map +0 -1
  50. package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.js +0 -56
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Advanced 3D game engine",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.65.0",
9
+ "version": "3.67.0",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A 3D lookup table as a `.cube` file holds it — what {@link cube_read } returns and {@link cube_write } takes.
3
+ *
4
+ * Entry `r + g·size + b·size²` holds the output for the input at lattice point (r, g, b): the input whose channel c
5
+ * is `domain_min[c] + (domain_max[c] - domain_min[c]) · i / (size - 1)` for that channel's index i. Red runs
6
+ * fastest, then green, then blue, as the format lays the entries out.
7
+ *
8
+ * The format carries numbers only. What the input and output mean — their primaries, transfer curves and units — is
9
+ * a contract between the writer and the reader, stated outside the table; a `.cube` file conventionally says it in
10
+ * its comments.
11
+ */
12
+ type CubeLut = {
13
+ /**
14
+ * the `TITLE`, `""` when the file has none
15
+ */
16
+ title: string;
17
+ /**
18
+ * the comment lines in file order, each without its `#` and the space after it
19
+ */
20
+ comments: string[];
21
+ /**
22
+ * entries along each edge, at least 2
23
+ */
24
+ size: number;
25
+ /**
26
+ * the input at lattice index 0, per channel
27
+ */
28
+ domain_min: number[];
29
+ /**
30
+ * the input at lattice index `size - 1`, per channel
31
+ */
32
+ domain_max: number[];
33
+ /**
34
+ * `size³` RGB triples
35
+ */
36
+ data: Float32Array;
37
+ };
38
+ //# sourceMappingURL=CubeLut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CubeLut.d.ts","sourceRoot":"","sources":["../../../../../src/format/lut/cube/CubeLut.js"],"names":[],"mappings":";;;;;;;;;;;;;;;WAYc,MAAM;;;;cACN,MAAM,EAAE;;;;UACR,MAAM;;;;gBACN,MAAM,EAAE;;;;gBACR,MAAM,EAAE;;;;UACR,YAAY"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A 3D lookup table as a `.cube` file holds it — what {@link cube_read} returns and {@link cube_write} takes.
3
+ *
4
+ * Entry `r + g·size + b·size²` holds the output for the input at lattice point (r, g, b): the input whose channel c
5
+ * is `domain_min[c] + (domain_max[c] - domain_min[c]) · i / (size - 1)` for that channel's index i. Red runs
6
+ * fastest, then green, then blue, as the format lays the entries out.
7
+ *
8
+ * The format carries numbers only. What the input and output mean — their primaries, transfer curves and units — is
9
+ * a contract between the writer and the reader, stated outside the table; a `.cube` file conventionally says it in
10
+ * its comments.
11
+ *
12
+ * @typedef {object} CubeLut
13
+ * @property {string} title the `TITLE`, `""` when the file has none
14
+ * @property {string[]} comments the comment lines in file order, each without its `#` and the space after it
15
+ * @property {number} size entries along each edge, at least 2
16
+ * @property {number[]} domain_min the input at lattice index 0, per channel
17
+ * @property {number[]} domain_max the input at lattice index `size - 1`, per channel
18
+ * @property {Float32Array} data `size³` RGB triples
19
+ *
20
+ * @see Adobe Systems, "Cube LUT Specification", version 1.0, 2013
21
+ * @author Alex Goldring
22
+ * @copyright Company Named Limited (c) 2026
23
+ */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Read a 3D LUT in the `.cube` format.
3
+ *
4
+ * **What it reads**: `TITLE`, `LUT_3D_SIZE`, `DOMAIN_MIN`, `DOMAIN_MAX` and comment lines, in any order ahead of the
5
+ * table, then the table: one entry per line, three decimal numbers apart by spaces or tabs, red fastest. Blank lines
6
+ * are skipped anywhere and comments are kept wherever they are. `DOMAIN_MIN` and `DOMAIN_MAX` default to 0 and 1,
7
+ * as the format defines.
8
+ *
9
+ * **What it refuses**, by name: a 1D LUT (`LUT_1D_SIZE`), any other keyword, a keyword after the table has begun, a
10
+ * size under 2, a table with more or fewer than `size³` entries, an entry that is not three decimal numbers, and a
11
+ * domain whose minimum is not under its maximum in every channel. A size is checked against what the text could hold
12
+ * before anything is allocated for the table, so a file cannot claim more entries than its own length allows.
13
+ *
14
+ * @param {string} text the whole file
15
+ * @returns {CubeLut}
16
+ * @throws {Error} naming the line and what is wrong with it
17
+ *
18
+ * @see cube_write
19
+ * @author Alex Goldring
20
+ * @copyright Company Named Limited (c) 2026
21
+ */
22
+ export function cube_read(text: string): CubeLut;
23
+ //# sourceMappingURL=cube_read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cube_read.d.ts","sourceRoot":"","sources":["../../../../../src/format/lut/cube/cube_read.js"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,gCARW,MAAM,GACJ,OAAO,CAmJnB"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * A decimal number as a `.cube` file writes one: optional sign, digits with an optional point, optional exponent.
3
+ * Narrower than `Number()`, which also takes hexadecimal, `Infinity` and the empty string.
4
+ * @type {RegExp}
5
+ */
6
+ const DECIMAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
7
+
8
+ /**
9
+ * @param {string} token
10
+ * @param {number} line_number
11
+ * @returns {number}
12
+ */
13
+ function parse_number(token, line_number) {
14
+ if (!DECIMAL.test(token)) {
15
+ throw new Error(`CUBE: line ${line_number}: '${token}' is not a number`);
16
+ }
17
+
18
+ return Number(token);
19
+ }
20
+
21
+ /**
22
+ * @param {string[]} tokens the keyword and its three values
23
+ * @param {number} line_number
24
+ * @returns {number[]}
25
+ */
26
+ function parse_triple(tokens, line_number) {
27
+ if (tokens.length !== 4) {
28
+ throw new Error(`CUBE: line ${line_number}: ${tokens[0]} takes three numbers, found ${tokens.length - 1}`);
29
+ }
30
+
31
+ return [
32
+ parse_number(tokens[1], line_number),
33
+ parse_number(tokens[2], line_number),
34
+ parse_number(tokens[3], line_number),
35
+ ];
36
+ }
37
+
38
+ /**
39
+ * Read a 3D LUT in the `.cube` format.
40
+ *
41
+ * **What it reads**: `TITLE`, `LUT_3D_SIZE`, `DOMAIN_MIN`, `DOMAIN_MAX` and comment lines, in any order ahead of the
42
+ * table, then the table: one entry per line, three decimal numbers apart by spaces or tabs, red fastest. Blank lines
43
+ * are skipped anywhere and comments are kept wherever they are. `DOMAIN_MIN` and `DOMAIN_MAX` default to 0 and 1,
44
+ * as the format defines.
45
+ *
46
+ * **What it refuses**, by name: a 1D LUT (`LUT_1D_SIZE`), any other keyword, a keyword after the table has begun, a
47
+ * size under 2, a table with more or fewer than `size³` entries, an entry that is not three decimal numbers, and a
48
+ * domain whose minimum is not under its maximum in every channel. A size is checked against what the text could hold
49
+ * before anything is allocated for the table, so a file cannot claim more entries than its own length allows.
50
+ *
51
+ * @param {string} text the whole file
52
+ * @returns {CubeLut}
53
+ * @throws {Error} naming the line and what is wrong with it
54
+ *
55
+ * @see cube_write
56
+ * @author Alex Goldring
57
+ * @copyright Company Named Limited (c) 2026
58
+ */
59
+ export function cube_read(text) {
60
+ const lines = text.split(/\r\n|\n|\r/);
61
+ const line_count = lines.length;
62
+
63
+ let title = "";
64
+ const comments = [];
65
+ let size = 0;
66
+ const domain_min = [0, 0, 0];
67
+ const domain_max = [1, 1, 1];
68
+
69
+ /**
70
+ * @type {Float32Array|null}
71
+ */
72
+ let data = null;
73
+ let entries = 0;
74
+ let entry_count = 0;
75
+
76
+ for (let i = 0; i < line_count; i++) {
77
+ const line_number = i + 1;
78
+ const line = lines[i].trim();
79
+
80
+ if (line.length === 0) {
81
+ continue;
82
+ }
83
+
84
+ if (line.charCodeAt(0) === 35 /* # */) {
85
+ // the text after `# ` as it stands, trailing spaces and all
86
+ comments.push(lines[i].trimStart().substring(1).replace(/^ /, ""));
87
+ continue;
88
+ }
89
+
90
+ const tokens = line.split(/[ \t]+/);
91
+ const head = tokens[0];
92
+
93
+ if (/^[A-Za-z_]/.test(head)) {
94
+ if (entries > 0) {
95
+ throw new Error(`CUBE: line ${line_number}: keyword ${head} after the table has begun`);
96
+ }
97
+
98
+ switch (head) {
99
+ case "TITLE": {
100
+ const match = /^TITLE[ \t]+"([^"]*)"$/.exec(line);
101
+
102
+ if (match === null) {
103
+ throw new Error(`CUBE: line ${line_number}: TITLE takes one quoted string`);
104
+ }
105
+
106
+ title = match[1];
107
+ break;
108
+ }
109
+
110
+ case "LUT_3D_SIZE": {
111
+ if (tokens.length !== 2 || !/^\d+$/.test(tokens[1])) {
112
+ throw new Error(`CUBE: line ${line_number}: LUT_3D_SIZE takes one whole number`);
113
+ }
114
+
115
+ size = Number(tokens[1]);
116
+
117
+ if (size < 2) {
118
+ throw new Error(`CUBE: line ${line_number}: LUT_3D_SIZE is ${size}, under the format's 2`);
119
+ }
120
+
121
+ break;
122
+ }
123
+
124
+ case "DOMAIN_MIN": {
125
+ const values = parse_triple(tokens, line_number);
126
+ domain_min[0] = values[0];
127
+ domain_min[1] = values[1];
128
+ domain_min[2] = values[2];
129
+ break;
130
+ }
131
+
132
+ case "DOMAIN_MAX": {
133
+ const values = parse_triple(tokens, line_number);
134
+ domain_max[0] = values[0];
135
+ domain_max[1] = values[1];
136
+ domain_max[2] = values[2];
137
+ break;
138
+ }
139
+
140
+ case "LUT_1D_SIZE":
141
+ throw new Error(`CUBE: line ${line_number}: a 1D LUT, where a 3D LUT was expected`);
142
+
143
+ default:
144
+ throw new Error(`CUBE: line ${line_number}: unsupported keyword ${head}`);
145
+ }
146
+
147
+ continue;
148
+ }
149
+
150
+ // the table
151
+ if (data === null) {
152
+ if (size === 0) {
153
+ throw new Error(`CUBE: line ${line_number}: table entry before LUT_3D_SIZE`);
154
+ }
155
+
156
+ entry_count = size * size * size;
157
+
158
+ // every entry is at least three one-digit numbers and two separators, five characters, and each but the
159
+ // last ends its line
160
+ if (entry_count * 6 - 1 > text.length) {
161
+ throw new Error(`CUBE: LUT_3D_SIZE ${size} needs ${entry_count} entries, more than a file of ${text.length} characters can hold`);
162
+ }
163
+
164
+ data = new Float32Array(entry_count * 3);
165
+ }
166
+
167
+ if (tokens.length !== 3) {
168
+ throw new Error(`CUBE: line ${line_number}: an entry is three numbers, found ${tokens.length}`);
169
+ }
170
+
171
+ if (entries >= entry_count) {
172
+ throw new Error(`CUBE: line ${line_number}: more than the ${entry_count} entries of LUT_3D_SIZE ${size}`);
173
+ }
174
+
175
+ const at = entries * 3;
176
+
177
+ data[at] = parse_number(tokens[0], line_number);
178
+ data[at + 1] = parse_number(tokens[1], line_number);
179
+ data[at + 2] = parse_number(tokens[2], line_number);
180
+
181
+ entries++;
182
+ }
183
+
184
+ if (size === 0) {
185
+ throw new Error("CUBE: no LUT_3D_SIZE");
186
+ }
187
+
188
+ if (entries !== size * size * size) {
189
+ throw new Error(`CUBE: ${entries} entries, where LUT_3D_SIZE ${size} needs ${size * size * size}`);
190
+ }
191
+
192
+ for (let c = 0; c < 3; c++) {
193
+ if (!(domain_min[c] < domain_max[c])) {
194
+ throw new Error(`CUBE: the domain of channel ${c} runs from ${domain_min[c]} to ${domain_max[c]}`);
195
+ }
196
+ }
197
+
198
+ return { title, comments, size, domain_min, domain_max, data };
199
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Write a 3D LUT in the `.cube` format: the comments, `TITLE` where there is one, `LUT_3D_SIZE`, `DOMAIN_MIN`,
3
+ * `DOMAIN_MAX`, then one entry per line, red fastest. Each number is the shortest decimal that reads back as the same
4
+ * 32-bit float, so {@link cube_read} returns exactly the values written.
5
+ *
6
+ * @param {CubeLut} lut
7
+ * @returns {string} the file, lines ending in `\n`
8
+ * @throws {Error} where a value is not finite, or the title or a comment cannot be written on one line
9
+ *
10
+ * @see cube_read
11
+ * @author Alex Goldring
12
+ * @copyright Company Named Limited (c) 2026
13
+ */
14
+ export function cube_write({ title, comments, size, domain_min, domain_max, data }: CubeLut): string;
15
+ //# sourceMappingURL=cube_write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cube_write.d.ts","sourceRoot":"","sources":["../../../../../src/format/lut/cube/cube_write.js"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;GAYG;AACH,oFARW,OAAO,GACL,MAAM,CAsDlB"}
@@ -0,0 +1,90 @@
1
+ import { assert } from "../../../core/assert.js";
2
+
3
+ /**
4
+ * The shortest decimal that reads back as the same 32-bit float.
5
+ *
6
+ * @param {number} value a float32 value, finite
7
+ * @returns {string}
8
+ */
9
+ function format_f32(value) {
10
+ for (let precision = 1; precision < 9; precision++) {
11
+ const text = value.toPrecision(precision);
12
+
13
+ if (Math.fround(Number(text)) === value) {
14
+ return text;
15
+ }
16
+ }
17
+
18
+ // nine significant digits always identify a float32
19
+ return value.toPrecision(9);
20
+ }
21
+
22
+ /**
23
+ * @param {number[]} triple
24
+ * @returns {string}
25
+ */
26
+ function format_triple(triple) {
27
+ return `${format_f32(Math.fround(triple[0]))} ${format_f32(Math.fround(triple[1]))} ${format_f32(Math.fround(triple[2]))}`;
28
+ }
29
+
30
+ /**
31
+ * Write a 3D LUT in the `.cube` format: the comments, `TITLE` where there is one, `LUT_3D_SIZE`, `DOMAIN_MIN`,
32
+ * `DOMAIN_MAX`, then one entry per line, red fastest. Each number is the shortest decimal that reads back as the same
33
+ * 32-bit float, so {@link cube_read} returns exactly the values written.
34
+ *
35
+ * @param {CubeLut} lut
36
+ * @returns {string} the file, lines ending in `\n`
37
+ * @throws {Error} where a value is not finite, or the title or a comment cannot be written on one line
38
+ *
39
+ * @see cube_read
40
+ * @author Alex Goldring
41
+ * @copyright Company Named Limited (c) 2026
42
+ */
43
+ export function cube_write({ title, comments, size, domain_min, domain_max, data }) {
44
+ assert.isString(title, 'title');
45
+ assert.isArray(comments, 'comments');
46
+ assert.isNonNegativeInteger(size, 'size');
47
+ assert.greaterThanOrEqual(size, 2, 'size');
48
+ assert.isInstanceOf(data, Float32Array, 'data', 'Float32Array');
49
+ assert.equal(data.length, size * size * size * 3, 'data.length !== size³ × 3');
50
+
51
+ if (/["\r\n]/.test(title)) {
52
+ throw new Error(`CUBE: a title cannot hold a quote or a line break, '${title}'`);
53
+ }
54
+
55
+ const lines = [];
56
+
57
+ for (const comment of comments) {
58
+ assert.isString(comment, 'comment');
59
+
60
+ if (/[\r\n]/.test(comment)) {
61
+ throw new Error(`CUBE: a comment cannot hold a line break, '${comment}'`);
62
+ }
63
+
64
+ lines.push(`# ${comment}`);
65
+ }
66
+
67
+ if (title.length > 0) {
68
+ lines.push(`TITLE "${title}"`);
69
+ }
70
+
71
+ lines.push(`LUT_3D_SIZE ${size}`);
72
+ lines.push(`DOMAIN_MIN ${format_triple(domain_min)}`);
73
+ lines.push(`DOMAIN_MAX ${format_triple(domain_max)}`);
74
+
75
+ const value_count = data.length;
76
+
77
+ for (let i = 0; i < value_count; i++) {
78
+ if (!Number.isFinite(data[i])) {
79
+ throw new Error(`CUBE: entry ${Math.floor(i / 3)} holds ${data[i]}, which the format cannot write`);
80
+ }
81
+ }
82
+
83
+ for (let i = 0; i < value_count; i += 3) {
84
+ lines.push(`${format_f32(data[i])} ${format_f32(data[i + 1])} ${format_f32(data[i + 2])}`);
85
+ }
86
+
87
+ lines.push("");
88
+
89
+ return lines.join("\n");
90
+ }
@@ -28,6 +28,7 @@ has the two lines that put it there. Another scene loads with `?scene=/some/othe
28
28
  | Sun height, Sun turn | where the sun stands |
29
29
  | Point lights | eight shadowed point lights along the scene's long axis, which the probes' hits take as local lights |
30
30
  | TAA | the resolve the probe pick's noise is meant for |
31
+ | Auto exposure | the exposure metered from the frame and adapted to it; off, the factor is 2^compensation, 1 on this page |
31
32
  | Bake budget | mesh SDF bricks baked a frame while the caches fill; the page starts above the engine's default so Sponza bakes in seconds |
32
33
 
33
34
  Drag to orbit, wheel to zoom, WASD to move. The stats line reads the frame time and the mesh SDF bake's
@@ -151,6 +151,11 @@
151
151
  <span>TAA <span class="hint">— resolves the probe pick's noise</span></span>
152
152
  </label>
153
153
 
154
+ <label>
155
+ <input type="checkbox" id="auto_exposure" checked>
156
+ <span>Auto exposure <span class="hint">— off, the exposure is 1</span></span>
157
+ </label>
158
+
154
159
  <hr>
155
160
 
156
161
  <label>
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/probe_clipmap/main.js"],"names":[],"mappings":"AAyFA;;;;;GAKG;AACH,yBAFa,QAAQ,IAAI,CAAC,CAuUzB"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/probe_clipmap/main.js"],"names":[],"mappings":"AAyFA;;;;;GAKG;AACH,yBAFa,QAAQ,IAAI,CAAC,CA8UzB"}
@@ -279,6 +279,13 @@ export async function start() {
279
279
  renderer.feature_taa_enabled = event.target.checked;
280
280
  });
281
281
 
282
+ const auto_exposure_toggle = element('auto_exposure');
283
+
284
+ auto_exposure_toggle.checked = renderer.feature_automatic_exposure_enabled;
285
+ auto_exposure_toggle.addEventListener('change', event => {
286
+ renderer.feature_automatic_exposure_enabled = event.target.checked;
287
+ });
288
+
282
289
  /**
283
290
  * What TAA was before the tracer turned it off, to give it back when the tracer goes.
284
291
  * @type {boolean}
@@ -3404,8 +3404,6 @@ export class Renderer {
3404
3404
  });
3405
3405
  }
3406
3406
 
3407
- let exposure = -1;
3408
-
3409
3407
  /*
3410
3408
  The scene as it stands before bloom composites over it: what the metering reads, and what
3411
3409
  bloom reads too.
@@ -3437,21 +3435,22 @@ export class Renderer {
3437
3435
  current_color_out = bloom.composited;
3438
3436
  }
3439
3437
 
3440
- if (this.feature_automatic_exposure_enabled) {
3441
- // TODO force time_delta_second to a very high value on view transition
3438
+ const gr_exposure_adaptation = graph_import_buffer(graph, this.#autoexposure.adaptation, "exposure adaptation");
3442
3439
 
3443
- exposure = this.#autoexposure.update({
3440
+ // TODO force time_delta_second to a very high value on view transition
3441
+ const exposure = this.feature_automatic_exposure_enabled
3442
+ ? this.#autoexposure.update({
3444
3443
  graph,
3445
3444
  input_color: metered_color,
3446
3445
  multiplier: gr_exposure_multiplier,
3446
+ adaptation: gr_exposure_adaptation,
3447
3447
  time_delta_seconds,
3448
+ })
3449
+ : this.#autoexposure.unadapted({
3450
+ graph,
3451
+ multiplier: gr_exposure_multiplier,
3452
+ adaptation: gr_exposure_adaptation,
3448
3453
  });
3449
- }
3450
-
3451
-
3452
- if (this.feature_automatic_exposure_enabled === false) {
3453
- exposure = this.#autoexposure.unadapted({ graph, multiplier: gr_exposure_multiplier });
3454
- }
3455
3454
 
3456
3455
 
3457
3456
  // current_color_out = graph_draw_texture_as_rgba({
@@ -3477,7 +3476,8 @@ export class Renderer {
3477
3476
  current_color_out = this.tone_mapper.render({
3478
3477
  graph,
3479
3478
  input_color: current_color_out,
3480
- exposure,
3479
+ exposure: exposure.multiplier,
3480
+ adaptation: exposure.adaptation,
3481
3481
  hdr: this.#hdr_supported,
3482
3482
  target: gr_texture_canvas,
3483
3483
  });
@@ -9,6 +9,10 @@
9
9
  * - `zone_bounds`: `shadows_max`, `highlights_min`, `highlights_max`.
10
10
  * - `curve_counts`: the keys of the hue, saturation and lightness curves. They follow the uniform in `grade_keys`, in
11
11
  * that order.
12
+ * - `luma_curve_counts`: the keys of the red, green, blue and all-channel saturation-against-luma curves. They follow
13
+ * the hue curves' keys in `grade_keys`, in that order.
14
+ * - `luma_gain_counts`: the keys of the red, green and blue gain-against-luma curves and the exposure-against-luma
15
+ * curve. They follow the saturation-against-luma curves' keys in `grade_keys`, in that order.
12
16
  */
13
17
  export const COLOR_GRADE_STRUCT: WebGPUStruct;
14
18
  /**
@@ -64,8 +68,20 @@ export class ColorGradeZone {
64
68
  * 2. **Primaries**, Unreal's `ColorCorrectAll` in AP1: saturation, contrast, gamma, gain and offset for {@link global},
65
69
  * {@link shadows}, {@link midtones} and {@link highlights}, the zones blended by AP1 luma between
66
70
  * {@link shadows_max}, {@link highlights_min} and {@link highlights_max}. Values from an Unreal project's post-process
67
- * settings give the same result. The colour is clamped at zero in Rec.709 after it, as the operator takes it.
68
- * 3. **Hue curves**, in OkLCh. Each curve's time is the colour's OkLCh hue in degrees, 0 to 360, and is evaluated as
71
+ * settings give the same result.
72
+ * 3. **Gain against luma**, in AP1 after the primaries: each channel multiplied by its gain at the colour's AP1 luma's
73
+ * stops from middle grey — {@link luma_gain_red}, {@link luma_gain_green}, {@link luma_gain_blue} — and every channel
74
+ * by 2 to the power of {@link luma_exposure} there. Grading by tonal range at any luma, with any blend between ranges:
75
+ * a range that fades in and out over spans of stops, as a smoothstep does, is a cubic between its edges, so ranges
76
+ * sum exactly into curves keyed at their edges. A curve with no keys is a gain of 1, or an exposure of 0. Clamped at
77
+ * zero.
78
+ * 4. **Saturation against luma**, after the gain against luma: each channel drawn away from the colour's AP1 luma by its
79
+ * factor at that luma's stops from middle grey, `log2(luma / 0.18)`: {@link luma_saturation} times the channel's
80
+ * own curve — {@link luma_saturation_red}, {@link luma_saturation_green}, {@link luma_saturation_blue}. Unreal's
81
+ * saturation and per-channel saturation, varying across the tonal range. Each curve is evaluated as
82
+ * {@link AnimationCurve#evaluate} would and holds its end keys' values beyond them; a curve with no keys is a factor
83
+ * of 1, and a colour with no luma is left as it is. Clamped at zero, as the primaries' saturation is. The colour is clamped at zero in Rec.709 after it, as the operator takes it.
84
+ * 5. **Hue curves**, in OkLCh. Each curve's time is the colour's OkLCh hue in degrees, 0 to 360, and is evaluated as
69
85
  * {@link AnimationCurve#evaluate} would; a curve with no keys does nothing. The curve holds its end keys' values
70
86
  * outside them, so for a seamless circle key 0 and 360 with the same value and tangent.
71
87
  * - {@link hue_shift}: degrees added to the hue.
@@ -143,8 +159,61 @@ export class ColorGrade {
143
159
  */
144
160
  readonly hue_lightness: AnimationCurve;
145
161
  /**
146
- * The grade as the bake reads it: {@link COLOR_GRADE_STRUCT}, then the three curves' keys, each
147
- * {@link COLOR_GRADE_KEY_FLOATS} floats, with room for one key when there are none. The array is reused while the
162
+ * Saturation of every channel against the colour's AP1 luma in stops from middle grey: 1 leaves it, 0 draws it to
163
+ * the luma. Each channel's own curve multiplies it.
164
+ * @readonly
165
+ * @type {AnimationCurve}
166
+ */
167
+ readonly luma_saturation: AnimationCurve;
168
+ /**
169
+ * Saturation of the red channel against the colour's AP1 luma in stops from middle grey, times
170
+ * {@link luma_saturation}.
171
+ * @readonly
172
+ * @type {AnimationCurve}
173
+ */
174
+ readonly luma_saturation_red: AnimationCurve;
175
+ /**
176
+ * Saturation of the green channel against the colour's AP1 luma in stops from middle grey, times
177
+ * {@link luma_saturation}.
178
+ * @readonly
179
+ * @type {AnimationCurve}
180
+ */
181
+ readonly luma_saturation_green: AnimationCurve;
182
+ /**
183
+ * Saturation of the blue channel against the colour's AP1 luma in stops from middle grey, times
184
+ * {@link luma_saturation}.
185
+ * @readonly
186
+ * @type {AnimationCurve}
187
+ */
188
+ readonly luma_saturation_blue: AnimationCurve;
189
+ /**
190
+ * Gain of the red channel against the colour's AP1 luma in stops from middle grey: 1 leaves it.
191
+ * @readonly
192
+ * @type {AnimationCurve}
193
+ */
194
+ readonly luma_gain_red: AnimationCurve;
195
+ /**
196
+ * Gain of the green channel against the colour's AP1 luma in stops from middle grey: 1 leaves it.
197
+ * @readonly
198
+ * @type {AnimationCurve}
199
+ */
200
+ readonly luma_gain_green: AnimationCurve;
201
+ /**
202
+ * Gain of the blue channel against the colour's AP1 luma in stops from middle grey: 1 leaves it.
203
+ * @readonly
204
+ * @type {AnimationCurve}
205
+ */
206
+ readonly luma_gain_blue: AnimationCurve;
207
+ /**
208
+ * Exposure of every channel against the colour's AP1 luma in stops from middle grey, in stops: 0 leaves it.
209
+ * @readonly
210
+ * @type {AnimationCurve}
211
+ */
212
+ readonly luma_exposure: AnimationCurve;
213
+ /**
214
+ * The grade as the bake reads it: {@link COLOR_GRADE_STRUCT}, then the three hue curves' keys, the four
215
+ * saturation-against-luma curves' keys (red, green, blue, all) and the four gain-against-luma curves' keys (red,
216
+ * green, blue, exposure), each {@link COLOR_GRADE_KEY_FLOATS} floats, with room for one key when there are none. The array is reused while the
148
217
  * key count stays the same.
149
218
  *
150
219
  * @returns {Float32Array}
@@ -1 +1 @@
1
- {"version":3,"file":"ColorGrade.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/deferred/tonemap/ColorGrade.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;GAWG;AACH,8CAyBG;AAEH;;;GAGG;AACH,wCAFU,MAAM,CAE4B;AAE5C;;;GAGG;AACH,qCAFU,MAAM,CAEwB;AAExC;;;GAGG;AACH;IACI;;;;OAIG;IACH,qBAFU,OAAO,CAEoB;IAErC;;;;OAIG;IACH,mBAFU,OAAO,CAEkB;IAEnC;;;;OAIG;IACH,gBAFU,OAAO,CAEe;IAEhC;;;OAGG;IACH,eAFU,OAAO,CAEc;IAE/B;;;;OAIG;IACH,iBAFU,OAAO,CAEgB;CACpC;AAwED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IACI;;;OAGG;IACH,mBAFU,MAAM,CAEsC;IAEtD;;;OAGG;IACH,YAFU,MAAM,CAEwB;IAExC;;;;OAIG;IACH,iBAFU,cAAc,CAEM;IAE9B;;;OAGG;IACH,kBAFU,cAAc,CAEO;IAE/B;;;OAGG;IACH,mBAFU,cAAc,CAEQ;IAEhC;;;OAGG;IACH,qBAFU,cAAc,CAEU;IAElC;;;OAGG;IACH,aAFU,MAAM,CAEG;IAEnB;;;OAGG;IACH,gBAFU,MAAM,CAEK;IAErB;;;OAGG;IACH,gBAFU,MAAM,CAEG;IAEnB;;;;OAIG;IACH,oBAFU,cAAc,CAES;IAEjC;;;;OAIG;IACH,yBAFU,cAAc,CAEc;IAEtC;;;;OAIG;IACH,wBAFU,cAAc,CAEa;IAarC;;;;;;OAMG;IACH,QAFa,YAAY,CAqDxB;;CACJ;6BA5U4B,mCAAmC;wBAHxC,kCAAkC;+BAC3B,sDAAsD"}
1
+ {"version":3,"file":"ColorGrade.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/deferred/tonemap/ColorGrade.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;GAeG;AACH,8CA2BG;AAEH;;;GAGG;AACH,wCAFU,MAAM,CAE4B;AAE5C;;;GAGG;AACH,qCAFU,MAAM,CAEwB;AAExC;;;GAGG;AACH;IACI;;;;OAIG;IACH,qBAFU,OAAO,CAEoB;IAErC;;;;OAIG;IACH,mBAFU,OAAO,CAEkB;IAEnC;;;;OAIG;IACH,gBAFU,OAAO,CAEe;IAEhC;;;OAGG;IACH,eAFU,OAAO,CAEc;IAE/B;;;;OAIG;IACH,iBAFU,OAAO,CAEgB;CACpC;AAwED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH;IACI;;;OAGG;IACH,mBAFU,MAAM,CAEsC;IAEtD;;;OAGG;IACH,YAFU,MAAM,CAEwB;IAExC;;;;OAIG;IACH,iBAFU,cAAc,CAEM;IAE9B;;;OAGG;IACH,kBAFU,cAAc,CAEO;IAE/B;;;OAGG;IACH,mBAFU,cAAc,CAEQ;IAEhC;;;OAGG;IACH,qBAFU,cAAc,CAEU;IAElC;;;OAGG;IACH,aAFU,MAAM,CAEG;IAEnB;;;OAGG;IACH,gBAFU,MAAM,CAEK;IAErB;;;OAGG;IACH,gBAFU,MAAM,CAEG;IAEnB;;;;OAIG;IACH,oBAFU,cAAc,CAES;IAEjC;;;;OAIG;IACH,yBAFU,cAAc,CAEc;IAEtC;;;;OAIG;IACH,wBAFU,cAAc,CAEa;IAErC;;;;;OAKG;IACH,0BAFU,cAAc,CAEe;IAEvC;;;;;OAKG;IACH,8BAFU,cAAc,CAEmB;IAE3C;;;;;OAKG;IACH,gCAFU,cAAc,CAEqB;IAE7C;;;;;OAKG;IACH,+BAFU,cAAc,CAEoB;IAE5C;;;;OAIG;IACH,wBAFU,cAAc,CAEa;IAErC;;;;OAIG;IACH,0BAFU,cAAc,CAEe;IAEvC;;;;OAIG;IACH,yBAFU,cAAc,CAEc;IAEtC;;;;OAIG;IACH,wBAFU,cAAc,CAEa;IAarC;;;;;;;OAOG;IACH,QAFa,YAAY,CAgFxB;;CACJ;6BAtb4B,mCAAmC;wBAHxC,kCAAkC;+BAC3B,sDAAsD"}