rayzee 8.3.0 → 8.4.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 +59 -3
- package/dist/DLSSNeural-BNSJ8V3z.js +202 -0
- package/dist/DLSSSuperRes-BoEW89Cl.js +266 -0
- package/dist/ToneMapGPU-DzqSyzTx.js +341 -0
- package/dist/rayzee.es.js +4603 -4606
- package/dist/rayzee.umd.js +264 -68
- package/dist/rolldown-runtime-CAFD8bLK.js +11 -0
- package/package.json +1 -1
- package/src/AssetConfig.js +7 -0
- package/src/EngineDefaults.js +10 -6
- package/src/EngineIssues.js +1 -0
- package/src/LightUnits.js +122 -0
- package/src/PathTracerApp.js +10 -6
- package/src/Processor/AssetLoader.js +61 -43
- package/src/Processor/LightSerializer.js +17 -9
- package/src/Processor/ToneMapGPU.js +317 -0
- package/src/Stages/PathTracer.js +23 -3
- package/src/TSL/LightsCore.js +8 -3
- package/src/TSL/LightsSampling.js +1 -4
- package/src/dlss/DLSSNeural.js +530 -0
- package/src/dlss/DLSSSuperRes.js +584 -0
- package/src/index.js +7 -0
- package/src/managers/DenoisingManager.js +288 -7
- package/src/managers/LightManager.js +15 -17
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's tone curve in WGSL, and a pass that turns a packed rgba16float buffer into RGBA
|
|
3
|
+
* bytes on the card.
|
|
4
|
+
*
|
|
5
|
+
* A mirror of `ToneMapCPU.js` — same curves, same order (exposure, saturation, curve, sRGB
|
|
6
|
+
* transfer), same rounding. It exists because reading half-floats back and converting them in
|
|
7
|
+
* JavaScript costs more than the neural upscale itself at production sizes. `bench:tonemap`
|
|
8
|
+
* compares the two implementations on a real device and fails on any drift.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ Rounding is deliberately bug-compatible. `toneMapToRGBA8` writes `srgb * 255 + 0.5` into a
|
|
11
|
+
* `Uint8ClampedArray`, which rounds again — so the CPU has always been half a level bright. WGSL's
|
|
12
|
+
* `round()` is round-half-to-even like the clamped array, so `round( srgb * 255 + 0.5 )` reproduces
|
|
13
|
+
* it exactly, including black staying at 0. Dropping the extra half would shift every image against
|
|
14
|
+
* the OIDN and Real-ESRGAN readbacks, which still go through the CPU function.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
NoToneMapping, LinearToneMapping, ReinhardToneMapping,
|
|
19
|
+
CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping
|
|
20
|
+
} from 'three';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* WGSL functions: `rayzee_tone_map( linearRGB, mode, exposure, saturation )` and
|
|
24
|
+
* `rayzee_linear_to_srgb( rgb )`, plus `rayzee_to_u8( channel )`.
|
|
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
|
+
}
|
|
41
|
+
|
|
42
|
+
fn tm_cineon( c0: vec3<f32> ) -> vec3<f32> {
|
|
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
|
+
}
|
|
47
|
+
|
|
48
|
+
fn tm_aces( c0: vec3<f32> ) -> vec3<f32> {
|
|
49
|
+
let c = c0 / 0.6;
|
|
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
|
+
}
|
|
63
|
+
|
|
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
|
+
}
|
|
98
|
+
|
|
99
|
+
fn tm_neutral( c0: vec3<f32> ) -> vec3<f32> {
|
|
100
|
+
let startCompression = 0.8 - 0.04;
|
|
101
|
+
let desaturation = 0.15;
|
|
102
|
+
|
|
103
|
+
let x = min( c0.r, min( c0.g, c0.b ) );
|
|
104
|
+
var offset = 0.04;
|
|
105
|
+
if ( x < 0.08 ) { offset = x - 6.25 * x * x; }
|
|
106
|
+
var c = c0 - vec3<f32>( offset );
|
|
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
|
+
`;
|
|
153
|
+
|
|
154
|
+
const PACKED_WGSL = /* wgsl */ `
|
|
155
|
+
struct Params {
|
|
156
|
+
width: u32,
|
|
157
|
+
height: u32,
|
|
158
|
+
mode: u32,
|
|
159
|
+
flipY: u32,
|
|
160
|
+
exposure: f32,
|
|
161
|
+
saturation: f32,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
@group(0) @binding(0) var<storage, read> src: array<u32>;
|
|
165
|
+
@group(0) @binding(1) var<storage, read_write> dst: array<u32>;
|
|
166
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
167
|
+
|
|
168
|
+
${TONE_MAP_WGSL}
|
|
169
|
+
|
|
170
|
+
@compute @workgroup_size(8, 8)
|
|
171
|
+
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
172
|
+
if ( gid.x >= params.width || gid.y >= params.height ) { return; }
|
|
173
|
+
|
|
174
|
+
var srcY = gid.y;
|
|
175
|
+
if ( params.flipY == 1u ) { srcY = params.height - 1u - gid.y; }
|
|
176
|
+
|
|
177
|
+
let a = ( srcY * params.width + gid.x ) * 2u;
|
|
178
|
+
let rg = unpack2x16float( src[ a ] );
|
|
179
|
+
let ba = unpack2x16float( src[ a + 1u ] );
|
|
180
|
+
|
|
181
|
+
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( rayzee_linear_to_srgb( mapped ) );
|
|
183
|
+
dst[ gid.y * params.width + gid.x ] = b8.x | ( b8.y << 8u ) | ( b8.z << 16u ) | ( 255u << 24u );
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
/** Uniform blocks round up to 16 bytes; the struct above is 24. */
|
|
188
|
+
const PARAMS_BYTES = 32;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Tone-maps a tightly packed rgba16float buffer (two `u32` per pixel) into RGBA bytes.
|
|
192
|
+
*
|
|
193
|
+
* Bound to one device and one image size; `ensureSize()` reallocates when the size changes.
|
|
194
|
+
*/
|
|
195
|
+
export class PackedToneMapper {
|
|
196
|
+
|
|
197
|
+
constructor( device, label = 'rayzee:tonemap' ) {
|
|
198
|
+
|
|
199
|
+
this.device = device;
|
|
200
|
+
this.label = label;
|
|
201
|
+
this.width = 0;
|
|
202
|
+
this.height = 0;
|
|
203
|
+
this._pipeline = null;
|
|
204
|
+
this._storage = null;
|
|
205
|
+
this._map = null;
|
|
206
|
+
this._params = null;
|
|
207
|
+
this._paramData = new ArrayBuffer( PARAMS_BYTES );
|
|
208
|
+
this._paramU32 = new Uint32Array( this._paramData );
|
|
209
|
+
this._paramF32 = new Float32Array( this._paramData );
|
|
210
|
+
this.disposed = false;
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ensureSize( width, height ) {
|
|
215
|
+
|
|
216
|
+
if ( this.width === width && this.height === height && this._pipeline ) return;
|
|
217
|
+
|
|
218
|
+
this._releaseBuffers();
|
|
219
|
+
|
|
220
|
+
const bytes = width * height * 4;
|
|
221
|
+
this._storage = this.device.createBuffer( {
|
|
222
|
+
label: `${this.label}-out`,
|
|
223
|
+
size: bytes,
|
|
224
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
|
|
225
|
+
} );
|
|
226
|
+
this._map = this.device.createBuffer( {
|
|
227
|
+
label: `${this.label}-map`,
|
|
228
|
+
size: bytes,
|
|
229
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
230
|
+
} );
|
|
231
|
+
this._params = this.device.createBuffer( {
|
|
232
|
+
label: `${this.label}-params`,
|
|
233
|
+
size: PARAMS_BYTES,
|
|
234
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
235
|
+
} );
|
|
236
|
+
|
|
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
|
+
this.width = width;
|
|
244
|
+
this.height = height;
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {GPUBuffer} src packed rgba16float, `width * height * 8` bytes, STORAGE-capable
|
|
250
|
+
* @param {object} tone
|
|
251
|
+
* @param {number} tone.exposure raw `renderer.toneMappingExposure`
|
|
252
|
+
* @param {number} tone.toneMapping Three.js ToneMapping constant
|
|
253
|
+
* @param {number} [tone.saturation=1]
|
|
254
|
+
* @param {boolean} [tone.flipY=false]
|
|
255
|
+
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
256
|
+
*/
|
|
257
|
+
async toRGBA8( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false } = {} ) {
|
|
258
|
+
|
|
259
|
+
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
260
|
+
if ( ! this._pipeline ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
|
261
|
+
|
|
262
|
+
const { width, height } = this;
|
|
263
|
+
|
|
264
|
+
this._paramU32[ 0 ] = width;
|
|
265
|
+
this._paramU32[ 1 ] = height;
|
|
266
|
+
this._paramU32[ 2 ] = toneMapping >>> 0;
|
|
267
|
+
this._paramU32[ 3 ] = flipY ? 1 : 0;
|
|
268
|
+
this._paramF32[ 4 ] = exposure;
|
|
269
|
+
this._paramF32[ 5 ] = saturation;
|
|
270
|
+
this.device.queue.writeBuffer( this._params, 0, this._paramData );
|
|
271
|
+
|
|
272
|
+
const group = this.device.createBindGroup( {
|
|
273
|
+
layout: this._pipeline.getBindGroupLayout( 0 ),
|
|
274
|
+
entries: [
|
|
275
|
+
{ binding: 0, resource: { buffer: src, size: width * height * 8 } },
|
|
276
|
+
{ binding: 1, resource: { buffer: this._storage } },
|
|
277
|
+
{ binding: 2, resource: { buffer: this._params } },
|
|
278
|
+
],
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
const encoder = this.device.createCommandEncoder( { label: this.label } );
|
|
282
|
+
const pass = encoder.beginComputePass();
|
|
283
|
+
pass.setPipeline( this._pipeline );
|
|
284
|
+
pass.setBindGroup( 0, group );
|
|
285
|
+
pass.dispatchWorkgroups( Math.ceil( width / 8 ), Math.ceil( height / 8 ) );
|
|
286
|
+
pass.end();
|
|
287
|
+
encoder.copyBufferToBuffer( this._storage, 0, this._map, 0, width * height * 4 );
|
|
288
|
+
this.device.queue.submit( [ encoder.finish() ] );
|
|
289
|
+
|
|
290
|
+
await this._map.mapAsync( GPUMapMode.READ );
|
|
291
|
+
const bytes = new Uint8ClampedArray( this._map.getMappedRange().slice( 0 ) );
|
|
292
|
+
this._map.unmap();
|
|
293
|
+
return bytes;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_releaseBuffers() {
|
|
298
|
+
|
|
299
|
+
this._storage?.destroy();
|
|
300
|
+
this._map?.destroy();
|
|
301
|
+
this._params?.destroy();
|
|
302
|
+
this._storage = null;
|
|
303
|
+
this._map = null;
|
|
304
|
+
this._params = null;
|
|
305
|
+
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
dispose() {
|
|
309
|
+
|
|
310
|
+
if ( this.disposed ) return;
|
|
311
|
+
this.disposed = true;
|
|
312
|
+
this._releaseBuffers();
|
|
313
|
+
this._pipeline = null;
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
}
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -88,6 +88,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
88
88
|
this._lastBounceEnergy = null;
|
|
89
89
|
// maxBounces the curve was measured at; the curve is ignored once this no longer matches (-1 = none).
|
|
90
90
|
this._lastBounceCountsBudget = - 1;
|
|
91
|
+
// Loop length the curve covers; maxBounces alone is not it (free bounces extend the loop).
|
|
92
|
+
this._lastBounceCountsLoopBound = - 1;
|
|
91
93
|
this._readbackPending = false;
|
|
92
94
|
this._readbackEveryNFrames = 4;
|
|
93
95
|
this._readbackFrameCounter = 0;
|
|
@@ -341,7 +343,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
341
343
|
|
|
342
344
|
const maxBounces = this.maxBounces.value;
|
|
343
345
|
// Transmissive/SSS steps consume iterations without advancing camera-bounce depth, so the loop must run far enough for deep glass/subsurface walks (mirror PathTracerCore); the survivor curve + early-exit break it early on non-SSS scenes.
|
|
344
|
-
const loopBound =
|
|
346
|
+
const loopBound = this._bounceLoopBound();
|
|
345
347
|
|
|
346
348
|
const frameHeight = this._wfRenderHeight.value;
|
|
347
349
|
const chunkRows = this._chunkRows;
|
|
@@ -356,9 +358,17 @@ export class PathTracer extends PathTracerStage {
|
|
|
356
358
|
// new camera-bounce caps the counts are cap-independent, so trust it up to that cutoff (whole curve when
|
|
357
359
|
// same/decreasing budget; only the [0, oldBudget) overlap when increasing). budget=-1 → cutoff 0 → full
|
|
358
360
|
// dispatch everywhere. This avoids the full-work spike on every bounce-count change.
|
|
361
|
+
// A curve measured with a smaller free-bounce budget under-counts survivors at EVERY depth, not
|
|
362
|
+
// just past its tail, so sizing off it drops the deep glass paths the bigger budget bought.
|
|
363
|
+
if ( loopBound > this._lastBounceCountsLoopBound ) this._curveSizingValid = false;
|
|
364
|
+
|
|
359
365
|
const curve = this._lastBounceCounts;
|
|
366
|
+
const curveMeasuredThisFar = maxBounces <= this._lastBounceCountsBudget
|
|
367
|
+
&& loopBound <= this._lastBounceCountsLoopBound;
|
|
360
368
|
const curveReliableUpto = ( singleChunk && curve )
|
|
361
|
-
? (
|
|
369
|
+
? ( curveMeasuredThisFar
|
|
370
|
+
? loopBound + 1
|
|
371
|
+
: Math.min( this._lastBounceCountsBudget, this._lastBounceCountsLoopBound ) )
|
|
362
372
|
: 0;
|
|
363
373
|
|
|
364
374
|
// Blender-style row-band streaming: the fixed-budget path pool processes the image in bands of ≤ chunkRows
|
|
@@ -698,6 +708,12 @@ export class PathTracer extends PathTracerStage {
|
|
|
698
708
|
|
|
699
709
|
}
|
|
700
710
|
|
|
711
|
+
_bounceLoopBound() {
|
|
712
|
+
|
|
713
|
+
return this.maxBounces.value + this.transmissiveBounces.value + this.maxSubsurfaceSteps.value;
|
|
714
|
+
|
|
715
|
+
}
|
|
716
|
+
|
|
701
717
|
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
702
718
|
_maybeReadbackCounters() {
|
|
703
719
|
|
|
@@ -722,7 +738,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
722
738
|
// the budget 3 -> 20 doubles the frame's dispatch work (13 -> 26 bounce iterations, 109 -> 214
|
|
723
739
|
// compute passes) for as long as that lasts. Waiting out the N-frame cadence just extends it, so
|
|
724
740
|
// re-measure on the next frame instead — same idiom as the mid-motion priming above.
|
|
725
|
-
if ( this.maxBounces.value !== this._lastBounceCountsBudget
|
|
741
|
+
if ( this.maxBounces.value !== this._lastBounceCountsBudget
|
|
742
|
+
|| this._bounceLoopBound() !== this._lastBounceCountsLoopBound ) {
|
|
726
743
|
|
|
727
744
|
this._readbackFrameCounter = this._readbackEveryNFrames;
|
|
728
745
|
|
|
@@ -738,6 +755,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
738
755
|
this._readbackPending = true;
|
|
739
756
|
const gen = this._readbackGeneration;
|
|
740
757
|
const budget = this.maxBounces.value;
|
|
758
|
+
const measuredLoopBound = this._bounceLoopBound();
|
|
741
759
|
const n = this._queueManager.MAX_BOUNCE_SNAPSHOTS;
|
|
742
760
|
this.renderer.getArrayBufferAsync( attr ).then( ( buf ) => {
|
|
743
761
|
|
|
@@ -750,6 +768,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
750
768
|
this._lastBounceCounts = all.subarray( 0, n );
|
|
751
769
|
this._lastBounceEnergy = all.subarray( n, 2 * n );
|
|
752
770
|
this._lastBounceCountsBudget = budget;
|
|
771
|
+
this._lastBounceCountsLoopBound = measuredLoopBound;
|
|
753
772
|
this._curveSizingValid = true;
|
|
754
773
|
|
|
755
774
|
}
|
|
@@ -837,6 +856,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
837
856
|
this._lastBounceCounts = null;
|
|
838
857
|
this._lastBounceEnergy = null;
|
|
839
858
|
this._lastBounceCountsBudget = - 1;
|
|
859
|
+
this._lastBounceCountsLoopBound = - 1;
|
|
840
860
|
this._curveSizingValid = false;
|
|
841
861
|
this._readbackFrameCounter = 0;
|
|
842
862
|
this._readbackGeneration ++;
|
package/src/TSL/LightsCore.js
CHANGED
|
@@ -251,10 +251,15 @@ export const getDistanceAttenuation = /*@__PURE__*/ wgslFn( `
|
|
|
251
251
|
}
|
|
252
252
|
` );
|
|
253
253
|
|
|
254
|
-
// Spot
|
|
254
|
+
// Spot cone falloff, Blender/Cycles' shape: the soft edge is a smoothstep over a band of
|
|
255
|
+
// COSINE width (1 - cosCone) * blend, not one that ends at the inner cone's angle. The two
|
|
256
|
+
// agree at blend 0 and 1 and diverge in between — three.js' ramp ran twice as wide at 0.5,
|
|
257
|
+
// leaving the penumbra up to 2x dark against a Cycles render of the same lamp.
|
|
255
258
|
export const getSpotAttenuation = /*@__PURE__*/ wgslFn( `
|
|
256
|
-
fn getSpotAttenuation( coneCosine: f32,
|
|
257
|
-
|
|
259
|
+
fn getSpotAttenuation( coneCosine: f32, blend: f32, angleCosine: f32 ) -> f32 {
|
|
260
|
+
let smoothWidth = ( 1.0 - coneCosine ) * blend;
|
|
261
|
+
if ( smoothWidth <= 0.0 ) { return 1.0; }
|
|
262
|
+
return smoothstep( 0.0, 1.0, ( angleCosine - coneCosine ) / smoothWidth );
|
|
258
263
|
}
|
|
259
264
|
` );
|
|
260
265
|
|
|
@@ -261,10 +261,7 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
|
|
|
261
261
|
|
|
262
262
|
If( ls_valid, () => {
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
// Clamp penumbraCosAngle > coneCosAngle to avoid smoothstep UB when penumbra = 0
|
|
266
|
-
const penumbraCosAngle = cos( light.angle.mul( float( 1.0 ).sub( light.penumbra ) ) ).max( coneCosAngle.add( 1e-5 ) ).toVar();
|
|
267
|
-
const coneAttenuation = getSpotAttenuation( { coneCosine: coneCosAngle, penumbraCosine: penumbraCosAngle, angleCosine: spotCosAngle } );
|
|
264
|
+
const coneAttenuation = getSpotAttenuation( { coneCosine: coneCosAngle, blend: light.penumbra, angleCosine: spotCosAngle } );
|
|
268
265
|
const distanceAttenuation = getDistanceAttenuation( { lightDistance: lightDist, cutoffDistance: light.distance, decayExponent: light.decay } );
|
|
269
266
|
|
|
270
267
|
// Gobo projection mask + IES photometric profile — both 1.0 when not assigned.
|