@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.
- package/package.json +1 -1
- package/src/format/lut/cube/CubeLut.d.ts +38 -0
- package/src/format/lut/cube/CubeLut.d.ts.map +1 -0
- package/src/format/lut/cube/CubeLut.js +23 -0
- package/src/format/lut/cube/cube_read.d.ts +23 -0
- package/src/format/lut/cube/cube_read.d.ts.map +1 -0
- package/src/format/lut/cube/cube_read.js +199 -0
- package/src/format/lut/cube/cube_write.d.ts +15 -0
- package/src/format/lut/cube/cube_write.d.ts.map +1 -0
- package/src/format/lut/cube/cube_write.js +90 -0
- package/src/shade/playground/probe_clipmap/README.md +1 -0
- package/src/shade/playground/probe_clipmap/index.html +5 -0
- package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -1
- package/src/shade/playground/probe_clipmap/main.js +7 -0
- package/src/shade/renderer/Renderer.js +12 -12
- package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts +73 -4
- package/src/shade/renderer/deferred/tonemap/ColorGrade.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/ColorGrade.js +113 -7
- package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.d.ts +12 -0
- package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/TONEMAP_PURKINJE_SHIFT.js +12 -0
- package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts +15 -1
- package/src/shade/renderer/deferred/tonemap/ToneMapper.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/ToneMapper.js +31 -3
- package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.d.ts +25 -0
- package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.d.ts.map +1 -0
- package/src/shade/renderer/deferred/tonemap/chunk_tonemap_purkinje.js +51 -0
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts +2 -2
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_HDR_lut.js +6 -3
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts +3 -2
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.d.ts.map +1 -1
- package/src/shade/renderer/deferred/tonemap/shader_tonemap_LDR_lut.js +7 -3
- package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts +35 -9
- package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.js +81 -11
- package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.js +4 -1
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts +6 -0
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/color/conversion/chunk_CRM_REC709_AP1.js +7 -0
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts +4 -4
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/color/grade/chunk_color_grade.js +86 -5
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.d.ts +65 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinje/chunk_apply_purkinje_shift.js +136 -0
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.d.ts +0 -11
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.d.ts.map +0 -1
- package/src/shade/renderer/shader/chunk/color/tonemap/purkinjie/chunk_apply_purkinjie_shift.js +0 -56
package/package.json
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
68
|
-
* 3. **
|
|
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
|
-
*
|
|
147
|
-
*
|
|
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
|
|
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"}
|