rayzee 8.5.0 → 8.6.1
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/README.md +60 -56
- package/dist/{DLSSNeural-BNSJ8V3z.js → NeuralRetouch-CYOJ21Wn.js} +33 -33
- package/dist/{DLSSSuperRes-BoEW89Cl.js → NeuralSuperRes-Dc6jRYke.js} +25 -25
- package/dist/ToneMapGPU-BVE852iN.js +533 -0
- package/dist/rayzee.es.js +27235 -9074
- package/dist/rayzee.umd.js +255 -187
- package/package.json +1 -1
- package/src/AssetConfig.js +21 -7
- package/src/Color/BakedViews.js +134 -0
- package/src/Color/BuiltinViews.js +261 -0
- package/src/Color/ColorManagement.js +1130 -0
- package/src/Color/ColorSpaces.js +432 -0
- package/src/Color/Displays.js +19 -0
- package/src/Color/InputColorSpaces.js +243 -0
- package/src/Color/LutBake.js +371 -0
- package/src/Color/OcioRuntime.js +338 -0
- package/src/Color/OcioViews.js +419 -0
- package/src/Color/ViewTransforms.js +323 -0
- package/src/Color/WorkingMatrix.js +86 -0
- package/src/EngineDefaults.js +55 -3
- package/src/EngineIssues.js +3 -0
- package/src/Passes/AIUpscaler.js +16 -7
- package/src/Passes/OIDNDenoiser.js +13 -5
- package/src/PathTracerApp.js +227 -7
- package/src/Processor/AssetLoader.js +1 -2
- package/src/Processor/EmissiveTriangleBuilder.js +34 -2
- package/src/Processor/GeometryExtractor.js +151 -138
- package/src/Processor/LightSerializer.js +8 -3
- package/src/Processor/MaterialPacking.js +168 -0
- package/src/Processor/PackedRayBuffer.js +6 -2
- package/src/Processor/ProceduralSky.js +4 -0
- package/src/Processor/SceneProcessor.js +25 -1
- package/src/Processor/SimpleSky.js +8 -0
- package/src/Processor/TextureCreator.js +137 -91
- package/src/Processor/TextureReadback.js +92 -0
- package/src/Processor/ToneMapCPU.js +80 -165
- package/src/Processor/ToneMapGPU.js +122 -151
- package/src/RenderSettings.js +1 -0
- package/src/Stages/Compositor.js +11 -2
- package/src/Stages/PathTracer.js +2 -0
- package/src/Stages/PathTracerStage.js +1 -1
- package/src/TSL/Common.js +15 -0
- package/src/TSL/Debugger.js +2 -3
- package/src/TSL/EmissiveSampling.js +6 -8
- package/src/TSL/ExtendKernel.js +9 -0
- package/src/TSL/Fresnel.js +23 -1
- package/src/TSL/HitFacet.js +102 -0
- package/src/TSL/LightsDirect.js +12 -33
- package/src/TSL/LightsSampling.js +15 -7
- package/src/TSL/MaterialEvaluation.js +26 -19
- package/src/TSL/MaterialProperties.js +74 -37
- package/src/TSL/MaterialTransmission.js +2 -3
- package/src/TSL/ShadeKernel.js +34 -21
- package/src/TSL/ShadowTerminator.js +125 -0
- package/src/TSL/Struct.js +9 -0
- package/src/TSL/Subsurface.js +2 -3
- package/src/index.js +32 -4
- package/src/managers/DenoisingManager.js +60 -44
- package/src/managers/EnvironmentManager.js +20 -0
- package/src/managers/MaterialDataManager.js +118 -208
- package/src/managers/UniformManager.js +1 -0
- package/src/{dlss/DLSSNeural.js → neural/NeuralRetouch.js} +42 -41
- package/src/{dlss/DLSSSuperRes.js → neural/NeuralSuperRes.js} +27 -26
- package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
|
@@ -1,197 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The readback half of the view transform: linear float pixels to display bytes, in the output
|
|
3
|
+
* pass's order. Shared by OIDN, the AI upscaler and `renderToBuffer`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* The curves themselves live in `../Color/ViewTransforms.js`, declared beside their WGSL and TSL
|
|
6
|
+
* counterparts, because a saved image that differs from the viewport is the failure this file
|
|
7
|
+
* exists to prevent. What stays here is everything around the curve — exposure, the saturation
|
|
8
|
+
* grade, the transfer function and the byte rounding.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ Rounding is deliberately bug-compatible. This writes `srgb * 255 + 0.5` into a
|
|
11
|
+
* `Uint8ClampedArray`, which rounds again, so the result has always been half a level bright.
|
|
12
|
+
* `ToneMapGPU.js` reproduces it exactly. Dropping the extra half would shift every image.
|
|
6
13
|
*/
|
|
7
|
-
import {
|
|
8
|
-
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
9
|
-
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
10
|
-
} from 'three';
|
|
11
|
-
|
|
12
|
-
const clamp01 = x => Math.min( Math.max( x, 0 ), 1 );
|
|
13
|
-
|
|
14
|
-
function noToneMap( r, g, b, _exposure, out ) {
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
out[ 2 ] = clamp01( b );
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function linearToneMap( r, g, b, exposure, out ) {
|
|
23
|
-
|
|
24
|
-
out[ 0 ] = clamp01( r * exposure );
|
|
25
|
-
out[ 1 ] = clamp01( g * exposure );
|
|
26
|
-
out[ 2 ] = clamp01( b * exposure );
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function reinhardToneMap( r, g, b, exposure, out ) {
|
|
31
|
-
|
|
32
|
-
r *= exposure; g *= exposure; b *= exposure;
|
|
33
|
-
out[ 0 ] = clamp01( r / ( r + 1 ) );
|
|
34
|
-
out[ 1 ] = clamp01( g / ( g + 1 ) );
|
|
35
|
-
out[ 2 ] = clamp01( b / ( b + 1 ) );
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function cineonToneMap( r, g, b, exposure, out ) {
|
|
40
|
-
|
|
41
|
-
r = Math.max( r * exposure - 0.004, 0 );
|
|
42
|
-
g = Math.max( g * exposure - 0.004, 0 );
|
|
43
|
-
b = Math.max( b * exposure - 0.004, 0 );
|
|
44
|
-
const f = c => Math.pow( ( c * ( 6.2 * c + 0.5 ) ) / ( c * ( 6.2 * c + 1.7 ) + 0.06 ), 2.2 );
|
|
45
|
-
out[ 0 ] = f( r );
|
|
46
|
-
out[ 1 ] = f( g );
|
|
47
|
-
out[ 2 ] = f( b );
|
|
48
|
-
|
|
49
|
-
}
|
|
15
|
+
import { NoToneMapping } from 'three';
|
|
16
|
+
import { VIEW_TRANSFORMS, getViewTransform, onRegistryChange } from '../Color/ViewTransforms.js';
|
|
50
17
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Three.js clamps the fragment output with `.max( 0 )` *before* tone mapping (NodeMaterial.js,
|
|
20
|
+
* "force unsigned floats"), so the GPU curves never see a negative channel. The Compositor's
|
|
21
|
+
* saturation grade (default 1.2) drives complementary channels below zero on a third of a typical
|
|
22
|
+
* frame, and AgX/Neutral mix those negatives across channels instead of clipping them — measured
|
|
23
|
+
* 7-10 levels of shadow error against the viewport until the readback clamps the same way.
|
|
24
|
+
*/
|
|
25
|
+
const clampNegative = fn => ( r, g, b, exposure, out ) =>
|
|
26
|
+
fn( r > 0 ? r : 0, g > 0 ? g : 0, b > 0 ? b : 0, exposure, out );
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Three.js ToneMapping constant → CPU function.
|
|
30
|
+
*
|
|
31
|
+
* Rebuilt in place rather than replaced: callers hold this Map, and loading a config adds entries
|
|
32
|
+
* to it long after they captured it.
|
|
33
|
+
*/
|
|
34
|
+
export const TONE_MAP_FNS = new Map();
|
|
60
35
|
|
|
61
|
-
|
|
62
|
-
ir = fit( ir ); ig = fit( ig ); ib = fit( ib );
|
|
36
|
+
function rebuild() {
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
out[ 2 ] = clamp01( - 0.00327 * ir - 0.07276 * ig + 1.07602 * ib );
|
|
38
|
+
TONE_MAP_FNS.clear();
|
|
39
|
+
for ( const t of VIEW_TRANSFORMS.values() ) TONE_MAP_FNS.set( t.id, clampNegative( t.cpu ) );
|
|
67
40
|
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
r *= exposure; g *= exposure; b *= exposure;
|
|
73
|
-
|
|
74
|
-
let cr = 0.6274 * r + 0.3293 * g + 0.0433 * b;
|
|
75
|
-
let cg = 0.0691 * r + 0.9195 * g + 0.0113 * b;
|
|
76
|
-
let cb = 0.0164 * r + 0.0880 * g + 0.8956 * b;
|
|
77
|
-
|
|
78
|
-
let ar = 0.856627153315983 * cr + 0.0951212405381588 * cg + 0.0482516061458583 * cb;
|
|
79
|
-
let ag = 0.137318972929847 * cr + 0.761241990602591 * cg + 0.101439036467562 * cb;
|
|
80
|
-
let ab = 0.11189821299995 * cr + 0.0767994186031903 * cg + 0.811302368396859 * cb;
|
|
81
|
-
|
|
82
|
-
const AgxMinEv = - 12.47393, AgxMaxEv = 4.026069, range = AgxMaxEv - AgxMinEv;
|
|
83
|
-
ar = clamp01( ( Math.log2( Math.max( ar, 1e-10 ) ) - AgxMinEv ) / range );
|
|
84
|
-
ag = clamp01( ( Math.log2( Math.max( ag, 1e-10 ) ) - AgxMinEv ) / range );
|
|
85
|
-
ab = clamp01( ( Math.log2( Math.max( ab, 1e-10 ) ) - AgxMinEv ) / range );
|
|
86
|
-
|
|
87
|
-
const approx = x => {
|
|
88
|
-
|
|
89
|
-
const x2 = x * x, x4 = x2 * x2;
|
|
90
|
-
return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
ar = approx( ar ); ag = approx( ag ); ab = approx( ab );
|
|
43
|
+
rebuild();
|
|
44
|
+
onRegistryChange( rebuild );
|
|
95
45
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Whether this transform already returned display-encoded colour.
|
|
48
|
+
*
|
|
49
|
+
* Three.js's seven curves return linear and the output pass encodes them. An OCIO view returns
|
|
50
|
+
* colour encoded for its own display — sRGB, Rec.1886 or PQ — and encoding that again is the
|
|
51
|
+
* single most visible way to get colour management wrong.
|
|
52
|
+
*/
|
|
53
|
+
export function isOutputEncoded( toneMapping ) {
|
|
103
54
|
|
|
104
|
-
|
|
105
|
-
out[ 1 ] = clamp01( - 0.1246 * or + 1.1329 * og - 0.0083 * ob );
|
|
106
|
-
out[ 2 ] = clamp01( - 0.0182 * or - 0.1006 * og + 1.1187 * ob );
|
|
55
|
+
return getViewTransform( toneMapping )?.outputEncoded === true;
|
|
107
56
|
|
|
108
57
|
}
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const StartCompression = 0.8 - 0.04;
|
|
113
|
-
const Desaturation = 0.15;
|
|
114
|
-
|
|
115
|
-
r *= exposure; g *= exposure; b *= exposure;
|
|
116
|
-
|
|
117
|
-
const x = Math.min( r, Math.min( g, b ) );
|
|
118
|
-
const offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
|
|
119
|
-
|
|
120
|
-
r -= offset; g -= offset; b -= offset;
|
|
121
|
-
|
|
122
|
-
const peak = Math.max( r, Math.max( g, b ) );
|
|
123
|
-
|
|
124
|
-
if ( peak < StartCompression ) {
|
|
125
|
-
|
|
126
|
-
out[ 0 ] = r; out[ 1 ] = g; out[ 2 ] = b;
|
|
127
|
-
return;
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const d = 1 - StartCompression;
|
|
132
|
-
const newPeak = 1 - d * d / ( peak + d - StartCompression );
|
|
133
|
-
const scale = newPeak / peak;
|
|
134
|
-
r *= scale; g *= scale; b *= scale;
|
|
135
|
-
const gFactor = 1 - 1 / ( Desaturation * ( peak - newPeak ) + 1 );
|
|
136
|
-
|
|
137
|
-
out[ 0 ] = r + ( newPeak - r ) * gFactor;
|
|
138
|
-
out[ 1 ] = g + ( newPeak - g ) * gFactor;
|
|
139
|
-
out[ 2 ] = b + ( newPeak - b ) * gFactor;
|
|
140
|
-
|
|
141
|
-
}
|
|
59
|
+
/** sRGB gamma (1/2.2) — fast pow approximation. Prefer `linearToSRGB` when matching three.js. */
|
|
60
|
+
export const SRGB_GAMMA = 1 / 2.2;
|
|
142
61
|
|
|
143
62
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* channel. Compositor's saturation grade (default 1.2) drives complementary channels
|
|
147
|
-
* below zero on a third of a typical frame, and AgX/Neutral mix those negatives across
|
|
148
|
-
* channels instead of clipping them — measured 7-10 levels of shadow error against the
|
|
149
|
-
* viewport until the readback clamps the same way.
|
|
63
|
+
* Proper sRGB OETF, matching three.js `sRGBTransferOETF` (`1.055 * c^(1/2.4) - 0.055` with a
|
|
64
|
+
* `12.92 * c` linear segment below 0.0031308).
|
|
150
65
|
*/
|
|
151
|
-
|
|
152
|
-
fn( r > 0 ? r : 0, g > 0 ? g : 0, b > 0 ? b : 0, exposure, out );
|
|
66
|
+
export function linearToSRGB( c ) {
|
|
153
67
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
[ LinearToneMapping, clampNegative( linearToneMap ) ],
|
|
158
|
-
[ ReinhardToneMapping, clampNegative( reinhardToneMap ) ],
|
|
159
|
-
[ CineonToneMapping, clampNegative( cineonToneMap ) ],
|
|
160
|
-
[ ACESFilmicToneMapping, clampNegative( acesFilmicToneMap ) ],
|
|
161
|
-
[ AgXToneMapping, clampNegative( agxToneMap ) ],
|
|
162
|
-
[ NeutralToneMapping, clampNegative( neutralToneMap ) ]
|
|
163
|
-
] );
|
|
164
|
-
|
|
165
|
-
/** sRGB gamma (1/2.2) — fast pow approximation. Prefer `linearToSRGB` when matching Three.js's output. */
|
|
166
|
-
export const SRGB_GAMMA = 1 / 2.2;
|
|
68
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow( c, 1 / 2.4 ) - 0.055;
|
|
69
|
+
|
|
70
|
+
}
|
|
167
71
|
|
|
168
72
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* needs to match the WebGPU output pass's sRGB encoding.
|
|
73
|
+
* The transfer step, skipped for a transform that encoded its own output.
|
|
74
|
+
* @param {number} c - a single channel, post-curve
|
|
172
75
|
*/
|
|
173
|
-
export function
|
|
76
|
+
export function encodeForDisplay( c, toneMapping ) {
|
|
174
77
|
|
|
175
|
-
return
|
|
78
|
+
return isOutputEncoded( toneMapping ) ? Math.min( Math.max( c, 0 ), 1 ) : linearToSRGB( c );
|
|
176
79
|
|
|
177
80
|
}
|
|
178
81
|
|
|
179
82
|
/**
|
|
180
83
|
* Exposure as the WebGPU output pass applies it.
|
|
181
84
|
*
|
|
182
|
-
* Three.js applies `toneMappingExposure` *inside* ToneMappingNode's
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* (measured +33.6 % at exposure 2).
|
|
85
|
+
* Three.js applies `toneMappingExposure` *inside* ToneMappingNode's branch, and that branch
|
|
86
|
+
* returns the colour untouched for NoToneMapping (`if ( toneMapping === NoToneMapping ) return
|
|
87
|
+
* colorNode`). So exposure is a no-op on screen there, and a CPU readback that applied it anyway
|
|
88
|
+
* paints brighter than the viewport it replaces — measured +33.6 % at exposure 2.
|
|
187
89
|
*
|
|
188
90
|
* @param {number} exposure - renderer.toneMappingExposure
|
|
189
|
-
* @param {number} toneMapping -
|
|
190
|
-
* @returns {number}
|
|
91
|
+
* @param {number} toneMapping - a registered view transform id
|
|
191
92
|
*/
|
|
192
93
|
export function effectiveExposure( exposure, toneMapping ) {
|
|
193
94
|
|
|
194
|
-
|
|
95
|
+
const t = getViewTransform( toneMapping );
|
|
96
|
+
if ( ! t ) return toneMapping === NoToneMapping ? 1.0 : exposure;
|
|
97
|
+
return t.appliesExposure ? exposure : 1.0;
|
|
195
98
|
|
|
196
99
|
}
|
|
197
100
|
|
|
@@ -202,7 +105,7 @@ const LUM_R = 0.2126, LUM_G = 0.7152, LUM_B = 0.0722;
|
|
|
202
105
|
* Pre-tonemapping saturation adjustment matching Display's GPU shader:
|
|
203
106
|
* mix( vec3(luma), exposed, saturation )
|
|
204
107
|
* Operates in-place on the `out` array (expects exposed linear RGB).
|
|
205
|
-
* @param {Float32Array} out - [r, g, b] to adjust
|
|
108
|
+
* @param {Float32Array|number[]} out - [r, g, b] to adjust
|
|
206
109
|
* @param {number} saturation - 1.0 = neutral
|
|
207
110
|
*/
|
|
208
111
|
export function applySaturation( out, saturation ) {
|
|
@@ -216,13 +119,13 @@ export function applySaturation( out, saturation ) {
|
|
|
216
119
|
}
|
|
217
120
|
|
|
218
121
|
/**
|
|
219
|
-
* Linear float RGBA →
|
|
122
|
+
* Linear float RGBA → display bytes, in the output pass's order: exposure, saturation, curve,
|
|
220
123
|
* transfer function. Not interchangeable — moving any step shifts every mid-tone.
|
|
221
124
|
*
|
|
222
125
|
* @param {Float32Array} linear - RGBA, 4 floats per pixel
|
|
223
126
|
* @param {Object} options
|
|
224
127
|
* @param {number} options.exposure - renderer.toneMappingExposure, raw
|
|
225
|
-
* @param {number} options.toneMapping -
|
|
128
|
+
* @param {number} options.toneMapping - a registered view transform id
|
|
226
129
|
* @param {number} [options.saturation=1]
|
|
227
130
|
* @param {boolean} [options.preserveAlpha=false]
|
|
228
131
|
* @returns {Uint8ClampedArray} RGBA bytes
|
|
@@ -231,6 +134,7 @@ export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1,
|
|
|
231
134
|
|
|
232
135
|
const curve = TONE_MAP_FNS.get( toneMapping ) ?? TONE_MAP_FNS.get( NoToneMapping );
|
|
233
136
|
const gain = effectiveExposure( exposure, toneMapping );
|
|
137
|
+
const encoded = isOutputEncoded( toneMapping );
|
|
234
138
|
const out = new Uint8ClampedArray( linear.length );
|
|
235
139
|
const scratch = [ 0, 0, 0 ];
|
|
236
140
|
|
|
@@ -243,9 +147,20 @@ export function toneMapToRGBA8( linear, { exposure, toneMapping, saturation = 1,
|
|
|
243
147
|
applySaturation( scratch, saturation );
|
|
244
148
|
curve( scratch[ 0 ], scratch[ 1 ], scratch[ 2 ], 1.0, scratch );
|
|
245
149
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
150
|
+
if ( encoded ) {
|
|
151
|
+
|
|
152
|
+
out[ i ] = scratch[ 0 ] * 255 + 0.5;
|
|
153
|
+
out[ i + 1 ] = scratch[ 1 ] * 255 + 0.5;
|
|
154
|
+
out[ i + 2 ] = scratch[ 2 ] * 255 + 0.5;
|
|
155
|
+
|
|
156
|
+
} else {
|
|
157
|
+
|
|
158
|
+
out[ i ] = linearToSRGB( scratch[ 0 ] ) * 255 + 0.5;
|
|
159
|
+
out[ i + 1 ] = linearToSRGB( scratch[ 1 ] ) * 255 + 0.5;
|
|
160
|
+
out[ i + 2 ] = linearToSRGB( scratch[ 2 ] ) * 255 + 0.5;
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
249
164
|
out[ i + 3 ] = preserveAlpha ? linear[ i + 3 ] * 255 + 0.5 : 255;
|
|
250
165
|
|
|
251
166
|
}
|
|
@@ -1,157 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The engine's
|
|
3
|
-
* bytes on the card.
|
|
2
|
+
* The engine's view transform in WGSL, and a pass that turns a packed rgba16float buffer into
|
|
3
|
+
* display bytes on the card.
|
|
4
4
|
*
|
|
5
|
-
* A mirror of `ToneMapCPU.js` — same curves, same order (exposure, saturation, curve,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* A mirror of `ToneMapCPU.js` — same curves, same order (exposure, saturation, curve, transfer),
|
|
6
|
+
* same rounding. It exists because reading half-floats back and converting them in JavaScript
|
|
7
|
+
* costs more than the neural upscale itself at production sizes. `bench:upscale` compares the two
|
|
8
|
+
* implementations on a real device and fails on any drift.
|
|
9
|
+
*
|
|
10
|
+
* Both sides are generated from `../Color/ViewTransforms.js`, so a view baked from an OCIO config
|
|
11
|
+
* reaches this pass without anything here being edited — the shader is rebuilt when the registry
|
|
12
|
+
* moves, and each table-backed transform gets its own 3D texture binding.
|
|
9
13
|
*
|
|
10
14
|
* ⚠️ Rounding is deliberately bug-compatible. `toneMapToRGBA8` writes `srgb * 255 + 0.5` into a
|
|
11
|
-
* `Uint8ClampedArray`, which rounds again
|
|
12
|
-
* `round()` is round-half-to-even like the clamped array, so `round( srgb * 255 + 0.5 )`
|
|
13
|
-
* it exactly, including black staying at 0.
|
|
14
|
-
* the OIDN and Real-ESRGAN readbacks, which still go through the CPU function.
|
|
15
|
+
* `Uint8ClampedArray`, which rounds again, so the CPU has always been half a level bright. WGSL's
|
|
16
|
+
* `round()` is round-half-to-even like the clamped array, so `round( srgb * 255 + 0.5 )`
|
|
17
|
+
* reproduces it exactly, including black staying at 0.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
|
-
import {
|
|
18
|
-
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
19
|
-
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
20
|
-
} from 'three';
|
|
20
|
+
import { buildToneMapWGSL, getRegistryVersion } from '../Color/ViewTransforms.js';
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
* `mode` is the Three.js ToneMapping constant, interpolated in from the import above so the two
|
|
27
|
-
* sides cannot disagree about which number means which curve.
|
|
28
|
-
*/
|
|
29
|
-
export const TONE_MAP_WGSL = /* wgsl */ `
|
|
30
|
-
const TM_NONE: u32 = ${NoToneMapping}u;
|
|
31
|
-
const TM_LINEAR: u32 = ${LinearToneMapping}u;
|
|
32
|
-
const TM_REINHARD: u32 = ${ReinhardToneMapping}u;
|
|
33
|
-
const TM_CINEON: u32 = ${CineonToneMapping}u;
|
|
34
|
-
const TM_ACES: u32 = ${ACESFilmicToneMapping}u;
|
|
35
|
-
const TM_AGX: u32 = ${AgXToneMapping}u;
|
|
36
|
-
const TM_NEUTRAL: u32 = ${NeutralToneMapping}u;
|
|
37
|
-
|
|
38
|
-
fn tm_reinhard( c: vec3<f32> ) -> vec3<f32> {
|
|
39
|
-
return clamp( c / ( c + vec3<f32>( 1.0 ) ), vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
40
|
-
}
|
|
22
|
+
/** The caller owns bindings 0-2; tables start after them. */
|
|
23
|
+
const TABLE_GROUP = 0;
|
|
24
|
+
const FIRST_TABLE_BINDING = 3;
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
let c = max( c0 - vec3<f32>( 0.004 ), vec3<f32>( 0.0 ) );
|
|
44
|
-
let v = ( c * ( 6.2 * c + vec3<f32>( 0.5 ) ) ) / ( c * ( 6.2 * c + vec3<f32>( 1.7 ) ) + vec3<f32>( 0.06 ) );
|
|
45
|
-
return pow( max( v, vec3<f32>( 0.0 ) ), vec3<f32>( 2.2 ) );
|
|
46
|
-
}
|
|
26
|
+
function currentShader() {
|
|
47
27
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let m_in = mat3x3<f32>(
|
|
51
|
-
vec3<f32>( 0.59719, 0.07600, 0.02840 ),
|
|
52
|
-
vec3<f32>( 0.35458, 0.90834, 0.13383 ),
|
|
53
|
-
vec3<f32>( 0.04823, 0.01566, 0.83777 ) );
|
|
54
|
-
let v = m_in * c;
|
|
55
|
-
let a = v * ( v + vec3<f32>( 0.0245786 ) ) - vec3<f32>( 0.000090537 );
|
|
56
|
-
let b = v * ( 0.983729 * v + vec3<f32>( 0.4329510 ) ) + vec3<f32>( 0.238081 );
|
|
57
|
-
let m_out = mat3x3<f32>(
|
|
58
|
-
vec3<f32>( 1.60475, -0.10208, -0.00327 ),
|
|
59
|
-
vec3<f32>( -0.53108, 1.10813, -0.07276 ),
|
|
60
|
-
vec3<f32>( -0.07367, -0.00605, 1.07602 ) );
|
|
61
|
-
return clamp( m_out * ( a / b ), vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
62
|
-
}
|
|
28
|
+
const { wgsl, bindings } = buildToneMapWGSL( { group: TABLE_GROUP, firstBinding: FIRST_TABLE_BINDING } );
|
|
29
|
+
return { wgsl, bindings, version: getRegistryVersion() };
|
|
63
30
|
|
|
64
|
-
fn tm_agx( c0: vec3<f32> ) -> vec3<f32> {
|
|
65
|
-
let m_in = mat3x3<f32>(
|
|
66
|
-
vec3<f32>( 0.6274, 0.0691, 0.0164 ),
|
|
67
|
-
vec3<f32>( 0.3293, 0.9195, 0.0880 ),
|
|
68
|
-
vec3<f32>( 0.0433, 0.0113, 0.8956 ) );
|
|
69
|
-
let m_agx = mat3x3<f32>(
|
|
70
|
-
vec3<f32>( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),
|
|
71
|
-
vec3<f32>( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),
|
|
72
|
-
vec3<f32>( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) );
|
|
73
|
-
|
|
74
|
-
var v = m_agx * ( m_in * c0 );
|
|
75
|
-
|
|
76
|
-
let minEv = -12.47393;
|
|
77
|
-
let maxEv = 4.026069;
|
|
78
|
-
v = clamp( ( log2( max( v, vec3<f32>( 1e-10 ) ) ) - vec3<f32>( minEv ) ) / ( maxEv - minEv ),
|
|
79
|
-
vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
80
|
-
|
|
81
|
-
let x2 = v * v;
|
|
82
|
-
let x4 = x2 * x2;
|
|
83
|
-
v = 15.5 * x4 * x2 - 40.14 * x4 * v + 31.96 * x4 - 6.868 * x2 * v + 0.4298 * x2 + 0.1191 * v
|
|
84
|
-
- vec3<f32>( 0.00232 );
|
|
85
|
-
|
|
86
|
-
let m_out = mat3x3<f32>(
|
|
87
|
-
vec3<f32>( 1.1271005818144368, -0.1413297634984383, -0.14132976349843826 ),
|
|
88
|
-
vec3<f32>( -0.11060664309660323, 1.157823702216272, -0.11060664309660294 ),
|
|
89
|
-
vec3<f32>( -0.016493938717834573, -0.016493938717834257, 1.2519364065950405 ) );
|
|
90
|
-
let o = pow( max( m_out * v, vec3<f32>( 0.0 ) ), vec3<f32>( 2.2 ) );
|
|
91
|
-
|
|
92
|
-
let m_srgb = mat3x3<f32>(
|
|
93
|
-
vec3<f32>( 1.6605, -0.1246, -0.0182 ),
|
|
94
|
-
vec3<f32>( -0.5876, 1.1329, -0.1006 ),
|
|
95
|
-
vec3<f32>( -0.0728, -0.0083, 1.1187 ) );
|
|
96
|
-
return clamp( m_srgb * o, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
97
31
|
}
|
|
98
32
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let peak = max( c.r, max( c.g, c.b ) );
|
|
109
|
-
if ( peak < startCompression ) { return c; }
|
|
110
|
-
|
|
111
|
-
let d = 1.0 - startCompression;
|
|
112
|
-
let newPeak = 1.0 - d * d / ( peak + d - startCompression );
|
|
113
|
-
c = c * ( newPeak / peak );
|
|
114
|
-
let gFactor = 1.0 - 1.0 / ( desaturation * ( peak - newPeak ) + 1.0 );
|
|
115
|
-
return mix( c, vec3<f32>( newPeak ), gFactor );
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Three.js clamps the fragment output with max(0) before tone mapping, so the curves never see a
|
|
119
|
-
// negative channel. The saturation grade drives channels below zero on much of a typical frame, and
|
|
120
|
-
// AgX/Neutral mix negatives across channels instead of clipping them.
|
|
121
|
-
fn rayzee_tone_curve( color: vec3<f32>, mode: u32 ) -> vec3<f32> {
|
|
122
|
-
let c = max( color, vec3<f32>( 0.0 ) );
|
|
123
|
-
if ( mode == TM_REINHARD ) { return tm_reinhard( c ); }
|
|
124
|
-
if ( mode == TM_CINEON ) { return tm_cineon( c ); }
|
|
125
|
-
if ( mode == TM_ACES ) { return tm_aces( c ); }
|
|
126
|
-
if ( mode == TM_AGX ) { return tm_agx( c ); }
|
|
127
|
-
if ( mode == TM_NEUTRAL ) { return tm_neutral( c ); }
|
|
128
|
-
return clamp( c, vec3<f32>( 0.0 ), vec3<f32>( 1.0 ) );
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
fn rayzee_tone_map( linearRGB: vec3<f32>, mode: u32, exposure: f32, saturation: f32 ) -> vec3<f32> {
|
|
132
|
-
// Three.js returns early for NoToneMapping without applying exposure, so a readback that applied
|
|
133
|
-
// it would paint brighter than the viewport it replaces.
|
|
134
|
-
var c = linearRGB * select( exposure, 1.0, mode == TM_NONE );
|
|
135
|
-
if ( saturation != 1.0 ) {
|
|
136
|
-
let luma = vec3<f32>( dot( c, vec3<f32>( 0.2126, 0.7152, 0.0722 ) ) );
|
|
137
|
-
c = luma + ( c - luma ) * saturation;
|
|
138
|
-
}
|
|
139
|
-
return rayzee_tone_curve( c, mode );
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fn rayzee_linear_to_srgb( c: vec3<f32> ) -> vec3<f32> {
|
|
143
|
-
return select(
|
|
144
|
-
1.055 * pow( max( c, vec3<f32>( 0.0 ) ), vec3<f32>( 1.0 / 2.4 ) ) - vec3<f32>( 0.055 ),
|
|
145
|
-
12.92 * c,
|
|
146
|
-
c <= vec3<f32>( 0.0031308 ) );
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fn rayzee_to_u8( srgb: vec3<f32> ) -> vec3<u32> {
|
|
150
|
-
return vec3<u32>( clamp( round( srgb * 255.0 + vec3<f32>( 0.5 ) ), vec3<f32>( 0.0 ), vec3<f32>( 255.0 ) ) );
|
|
151
|
-
}
|
|
152
|
-
`;
|
|
33
|
+
/**
|
|
34
|
+
* `rayzee_tone_map( linearRGB, mode, exposure, saturation )`, `rayzee_encode( mapped, mode )` and
|
|
35
|
+
* `rayzee_to_u8( c )` for the registry as it stands right now.
|
|
36
|
+
*
|
|
37
|
+
* A snapshot, not a live value: `PackedToneMapper` regenerates its own copy when the registry
|
|
38
|
+
* changes. Exported for tests and for a host embedding the curve in its own pass.
|
|
39
|
+
*/
|
|
40
|
+
export const TONE_MAP_WGSL = currentShader().wgsl;
|
|
153
41
|
|
|
154
|
-
const
|
|
42
|
+
const packedWGSL = transformWGSL => /* wgsl */ `
|
|
155
43
|
struct Params {
|
|
156
44
|
width: u32,
|
|
157
45
|
height: u32,
|
|
@@ -165,7 +53,7 @@ struct Params {
|
|
|
165
53
|
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
166
54
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
167
55
|
|
|
168
|
-
${
|
|
56
|
+
${transformWGSL}
|
|
169
57
|
|
|
170
58
|
@compute @workgroup_size(8, 8)
|
|
171
59
|
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
@@ -179,7 +67,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
179
67
|
let ba = unpack2x16float( src[ a + 1u ] );
|
|
180
68
|
|
|
181
69
|
let mapped = rayzee_tone_map( vec3<f32>( rg.x, rg.y, ba.x ), params.mode, params.exposure, params.saturation );
|
|
182
|
-
let b8 = rayzee_to_u8(
|
|
70
|
+
let b8 = rayzee_to_u8( rayzee_encode( mapped, params.mode ) );
|
|
183
71
|
dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( 255u << 24u );
|
|
184
72
|
}
|
|
185
73
|
`;
|
|
@@ -207,13 +95,91 @@ export class PackedToneMapper {
|
|
|
207
95
|
this._paramData = new ArrayBuffer( PARAMS_BYTES );
|
|
208
96
|
this._paramU32 = new Uint32Array( this._paramData );
|
|
209
97
|
this._paramF32 = new Float32Array( this._paramData );
|
|
98
|
+
this._shaderVersion = - 1;
|
|
99
|
+
this._bindings = [];
|
|
100
|
+
this._tables = new Map();
|
|
210
101
|
this.disposed = false;
|
|
211
102
|
|
|
212
103
|
}
|
|
213
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Rebuild the pipeline when the registry has moved.
|
|
107
|
+
*
|
|
108
|
+
* A config loaded after this pass was first compiled adds curves the compiled shader has never
|
|
109
|
+
* heard of. Without this the readback silently falls through to the clamp and a saved image
|
|
110
|
+
* comes back untone-mapped.
|
|
111
|
+
*/
|
|
112
|
+
_ensurePipeline() {
|
|
113
|
+
|
|
114
|
+
const { wgsl, bindings, version } = currentShader();
|
|
115
|
+
if ( this._pipeline && this._shaderVersion === version ) return;
|
|
116
|
+
|
|
117
|
+
this._bindings = bindings;
|
|
118
|
+
this._shaderVersion = version;
|
|
119
|
+
this._pipeline = this.device.createComputePipeline( {
|
|
120
|
+
label: this.label,
|
|
121
|
+
layout: 'auto',
|
|
122
|
+
compute: {
|
|
123
|
+
module: this.device.createShaderModule( { label: this.label, code: packedWGSL( wgsl ) } ),
|
|
124
|
+
entryPoint: 'main',
|
|
125
|
+
},
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One 3D texture per table-backed transform, uploaded once and kept until it is replaced. */
|
|
131
|
+
_ensureTables() {
|
|
132
|
+
|
|
133
|
+
const live = new Set();
|
|
134
|
+
|
|
135
|
+
for ( const { index, transform } of this._bindings ) {
|
|
136
|
+
|
|
137
|
+
const { data, size } = transform.table;
|
|
138
|
+
live.add( index );
|
|
139
|
+
|
|
140
|
+
const held = this._tables.get( index );
|
|
141
|
+
if ( held && held.data === data ) continue;
|
|
142
|
+
|
|
143
|
+
held?.texture.destroy();
|
|
144
|
+
|
|
145
|
+
const texture = this.device.createTexture( {
|
|
146
|
+
label: `${this.label}-${transform.wgslConst}`,
|
|
147
|
+
size: [ size, size, size ],
|
|
148
|
+
dimension: '3d',
|
|
149
|
+
format: 'rgba16float',
|
|
150
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
this.device.queue.writeTexture(
|
|
154
|
+
{ texture },
|
|
155
|
+
data,
|
|
156
|
+
{ bytesPerRow: size * 8, rowsPerImage: size },
|
|
157
|
+
[ size, size, size ]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
this._tables.set( index, { texture, data, view: texture.createView() } );
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for ( const [ index, held ] of this._tables ) {
|
|
165
|
+
|
|
166
|
+
if ( ! live.has( index ) ) {
|
|
167
|
+
|
|
168
|
+
held.texture.destroy();
|
|
169
|
+
this._tables.delete( index );
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
214
177
|
ensureSize( width, height ) {
|
|
215
178
|
|
|
216
|
-
|
|
179
|
+
this._ensurePipeline();
|
|
180
|
+
this._ensureTables();
|
|
181
|
+
|
|
182
|
+
if ( this.width === width && this.height === height && this._storage ) return;
|
|
217
183
|
|
|
218
184
|
this._releaseBuffers();
|
|
219
185
|
|
|
@@ -234,12 +200,6 @@ export class PackedToneMapper {
|
|
|
234
200
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
235
201
|
} );
|
|
236
202
|
|
|
237
|
-
this._pipeline ??= this.device.createComputePipeline( {
|
|
238
|
-
label: this.label,
|
|
239
|
-
layout: 'auto',
|
|
240
|
-
compute: { module: this.device.createShaderModule( { label: this.label, code: PACKED_WGSL } ), entryPoint: 'main' },
|
|
241
|
-
} );
|
|
242
|
-
|
|
243
203
|
this.width = width;
|
|
244
204
|
this.height = height;
|
|
245
205
|
|
|
@@ -257,7 +217,12 @@ export class PackedToneMapper {
|
|
|
257
217
|
async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false } = {} ) {
|
|
258
218
|
|
|
259
219
|
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
260
|
-
if ( ! this.
|
|
220
|
+
if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
|
221
|
+
|
|
222
|
+
// A config can be loaded between two readbacks, so the registry is re-checked here rather
|
|
223
|
+
// than only on resize.
|
|
224
|
+
this._ensurePipeline();
|
|
225
|
+
this._ensureTables();
|
|
261
226
|
|
|
262
227
|
const { width, height } = this;
|
|
263
228
|
|
|
@@ -269,9 +234,13 @@ export class PackedToneMapper {
|
|
|
269
234
|
this._paramF32[ 5 ] = saturation;
|
|
270
235
|
this.device.queue.writeBuffer( this._params, 0, this._paramData );
|
|
271
236
|
|
|
237
|
+
const tableEntries = [ ...this._tables.entries() ]
|
|
238
|
+
.map( ( [ binding, held ] ) => ( { binding, resource: held.view } ) );
|
|
239
|
+
|
|
272
240
|
const group = this.device.createBindGroup( {
|
|
273
241
|
layout: this._pipeline.getBindGroupLayout( 0 ),
|
|
274
242
|
entries: [
|
|
243
|
+
...tableEntries,
|
|
275
244
|
{ binding: 0, resource: { buffer: src, size: width * height * 8 } },
|
|
276
245
|
{ binding: 1, resource: { buffer: this._storage } },
|
|
277
246
|
{ binding: 2, resource: { buffer: this._params } },
|
|
@@ -310,6 +279,8 @@ export class PackedToneMapper {
|
|
|
310
279
|
if ( this.disposed ) return;
|
|
311
280
|
this.disposed = true;
|
|
312
281
|
this._releaseBuffers();
|
|
282
|
+
for ( const held of this._tables.values() ) held.texture.destroy();
|
|
283
|
+
this._tables.clear();
|
|
313
284
|
this._pipeline = null;
|
|
314
285
|
|
|
315
286
|
}
|
package/src/RenderSettings.js
CHANGED
|
@@ -42,6 +42,7 @@ const SETTING_ROUTES = {
|
|
|
42
42
|
anamorphicRatio: { uniform: 'anamorphicRatio', reset: true },
|
|
43
43
|
samplingTechnique: { uniform: 'samplingTechnique', reset: true },
|
|
44
44
|
fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
|
|
45
|
+
shadowTerminatorOffset: { uniform: 'shadowTerminatorOffset', reset: true },
|
|
45
46
|
enableAlphaShadows: { uniform: 'enableAlphaShadows', reset: true },
|
|
46
47
|
// Adaptive sampling — whole-frame early-stop (useAdaptiveSampling) + per-pixel freeze (usePixelFreeze)
|
|
47
48
|
useAdaptiveSampling: { uniform: 'useAdaptiveSampling', reset: true },
|