rayzee 11.3.0 → 11.4.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/README.md +33 -5
- package/dist/{ArchiveImporter-D735xX-4.js → ArchiveImporter-Cgyx5AF7.js} +2 -2
- package/dist/{BidirectionalIntegrator-M89c0AlP.js → BidirectionalIntegrator-BKF-cwH2.js} +2 -2
- package/dist/{NeuralRetouch-DiBH63kz.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
- package/dist/{NeuralSuperRes-CZNQP13W.js → NeuralSuperRes-DV5TtURP.js} +11 -5
- package/dist/{PathTracer-BXCCVrPK.js → PathTracer-D3MvEaZK.js} +19 -12
- package/dist/{PhysicalSky-DqmaS0lb.js → PhysicalSky-CY4jiz6a.js} +1 -1
- package/dist/{RayzeeRenderer-DUSMFhmc.js → RayzeeRenderer-D23m6NqG.js} +313 -281
- package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
- package/dist/addons/archives.es.js +1 -1
- package/dist/addons/bidirectional.es.js +1 -1
- package/dist/addons/physical-sky.es.js +1 -1
- package/dist/rayzee-core.es.js +3 -3
- package/dist/rayzee.es.js +1343 -858
- package/dist/rayzee.umd.js +124 -113
- package/dist/{sceneCachePolicy-BAib5fCp.js → sceneCachePolicy-AKOEu2lf.js} +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -2
- package/src/Passes/AIUpscaler.js +2 -0
- package/src/PathTracerApp.js +122 -16
- package/src/Pipeline/CompletionTracker.js +7 -3
- package/src/Processor/ToneMapCPU.js +16 -4
- package/src/Processor/ToneMapGPU.js +30 -14
- package/src/RayzeeRenderer.js +54 -7
- package/src/RenderSettings.js +4 -2
- package/src/Stages/AutoExposure.js +484 -494
- package/src/Stages/Compositor.js +27 -6
- package/src/Stages/LocalExposure.js +546 -0
- package/src/Stages/PathTracer.js +6 -4
- package/src/Stages/PathTracerStage.js +9 -7
- package/src/index.js +1 -0
- package/src/managers/DenoisingManager.js +11 -15
- package/src/managers/VideoRenderManager.js +30 -4
- package/src/neural/NeuralRetouch.js +1 -0
- package/src/neural/NeuralSuperRes.js +6 -1
package/src/Stages/Compositor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { vec4, vec3, uv, uniform, select, dot, mix, float, int, uint, storage, Fn, If } from 'three/tsl';
|
|
1
|
+
import { vec4, vec3, uv, screenUV, uniform, select, dot, mix, float, int, uint, storage, Fn, If } from 'three/tsl';
|
|
2
2
|
import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
|
|
3
3
|
import { NoBlending, NoToneMapping } from 'three';
|
|
4
4
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
@@ -49,19 +49,40 @@ export class Compositor extends RenderStage {
|
|
|
49
49
|
|
|
50
50
|
// TextureNode reused across frames — only `.value` mutates, shader doesn't recompile.
|
|
51
51
|
this._sourceTexNode = new TextureNode();
|
|
52
|
+
this._displayGain = null;
|
|
53
|
+
|
|
54
|
+
this.compositorMaterial = new MeshBasicNodeMaterial();
|
|
55
|
+
this.compositorMaterial.blending = NoBlending;
|
|
56
|
+
this._buildColorNode();
|
|
57
|
+
|
|
58
|
+
this.compositorQuad = new QuadMesh( this.compositorMaterial );
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_buildColorNode() {
|
|
52
63
|
|
|
53
64
|
const texSample = this._sourceTexNode.sample( uv() );
|
|
65
|
+
const color = this._displayGain ? texSample.xyz.mul( this._displayGain( texSample.xyz, screenUV ) ) : texSample.xyz;
|
|
54
66
|
|
|
55
|
-
const luma = dot(
|
|
56
|
-
const gradedColor = mix( vec3( luma ),
|
|
67
|
+
const luma = dot( color, REC709_LUMINANCE_COEFFICIENTS );
|
|
68
|
+
const gradedColor = mix( vec3( luma ), color, this.saturation );
|
|
57
69
|
|
|
58
70
|
const outputAlpha = select( this._transparentBackground, texSample.w, 1.0 );
|
|
59
71
|
|
|
60
|
-
this.compositorMaterial = new MeshBasicNodeMaterial();
|
|
61
72
|
this.compositorMaterial.colorNode = vec4( gradedColor, outputAlpha );
|
|
62
|
-
this.compositorMaterial.
|
|
73
|
+
this.compositorMaterial.needsUpdate = true;
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A per-pixel gain on the shown picture, before exposure and the view: `( rgb, uv ) => float node`,
|
|
79
|
+
* `uv` the image position with y down. A readback applies the same through `_displayGain()`. Null removes it.
|
|
80
|
+
*/
|
|
81
|
+
setDisplayGain( gain ) {
|
|
82
|
+
|
|
83
|
+
if ( gain === this._displayGain ) return;
|
|
84
|
+
this._displayGain = gain;
|
|
85
|
+
this._buildColorNode();
|
|
65
86
|
|
|
66
87
|
}
|
|
67
88
|
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { Fn, float, int, uint, ivec2, vec2, vec4, uniform, If, Loop, max, min, select, exp, exp2, log2, dot, mix, floor,
|
|
2
|
+
floatBitsToUint, textureLoad, workgroupArray, workgroupBarrier, localId, workgroupId, attributeArray, storage,
|
|
3
|
+
atomicAdd, atomicLoad, atomicStore, toneMappingExposure } from 'three/tsl';
|
|
4
|
+
import { TextureNode } from 'three/webgpu';
|
|
5
|
+
import { Vector4 } from 'three';
|
|
6
|
+
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
7
|
+
import { MAX_RESERVABLE_RENDER_SIZE } from '../Processor/StorageTexturePool.js';
|
|
8
|
+
|
|
9
|
+
export const LOCAL_EXPOSURE_DEFAULTS = {
|
|
10
|
+
localExposure: false,
|
|
11
|
+
localExposureHighlightContrast: 0.6,
|
|
12
|
+
localExposureShadowContrast: 1.0,
|
|
13
|
+
localExposureDetailStrength: 1.0,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Unreal's sizes: grid cells of 64 half-resolution texels, 32 bins, a 1/32 blurred picture.
|
|
17
|
+
const CELL = 128;
|
|
18
|
+
const CELL_TEXELS = ( CELL / 2 ) ** 2;
|
|
19
|
+
const BINS = 32;
|
|
20
|
+
const BIN_MIN = - 16; // exposed log2 luminance the bins span
|
|
21
|
+
const BIN_MAX = 16;
|
|
22
|
+
const LOG_FLOOR = - 40;
|
|
23
|
+
const LOG_CEIL = 40;
|
|
24
|
+
const FIXED = 256;
|
|
25
|
+
const BLUR_TEXEL = 32;
|
|
26
|
+
const BLUR_BLEND = 0.6;
|
|
27
|
+
const BLUR_PERCENT = 50;
|
|
28
|
+
const MIDDLE_GREY = Math.log2( 0.18 );
|
|
29
|
+
const DARKEST = 2 ** - 24;
|
|
30
|
+
const MAX_CELLS = Math.ceil( MAX_RESERVABLE_RENDER_SIZE / CELL );
|
|
31
|
+
const MAX_BLUR = Math.ceil( MAX_RESERVABLE_RENDER_SIZE / BLUR_TEXEL );
|
|
32
|
+
const Y = [ 0.2126, 0.7152, 0.0722 ];
|
|
33
|
+
|
|
34
|
+
const meterInterval = samples => samples < 16 ? 1 : samples < 64 ? 4 : 16;
|
|
35
|
+
const clamp = ( x, a, b ) => Math.min( Math.max( x, a ), b );
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The gain local exposure applies to one pixel, from the grid and blurred picture as stored. `p` is the
|
|
39
|
+
* stage's 16 parameter floats (`_params`); the shader twins below must stay identical.
|
|
40
|
+
* @param {Float32Array} p
|
|
41
|
+
* @param {Float32Array} grid - two floats a bin: Σ log2 luminance and Σ weight, per cell texel
|
|
42
|
+
* @param {Float32Array} blur - log2 luminance of the blurred picture
|
|
43
|
+
* @param {number} u - image x in [0, 1]
|
|
44
|
+
* @param {number} v - image y in [0, 1], down
|
|
45
|
+
* @param {number} lum - the pixel's linear luminance
|
|
46
|
+
* @param {number} logExposure - log2 of the exposure it is shown at
|
|
47
|
+
*/
|
|
48
|
+
export function localExposureGain( p, grid, blur, u, v, lum, logExposure ) {
|
|
49
|
+
|
|
50
|
+
// Capped: an infinite pixel's y − y would be NaN.
|
|
51
|
+
const logL = Math.min( Math.log2( Math.max( lum, DARKEST ) ), LOG_CEIL );
|
|
52
|
+
|
|
53
|
+
const bw = p[ 6 ], bh = p[ 7 ];
|
|
54
|
+
const bx = clamp( u * p[ 2 ] - 0.5, 0, bw - 1 ), by = clamp( v * p[ 3 ] - 0.5, 0, bh - 1 );
|
|
55
|
+
const bx0 = Math.floor( bx ), by0 = Math.floor( by ), bx1 = Math.min( bx0 + 1, bw - 1 ), by1 = Math.min( by0 + 1, bh - 1 );
|
|
56
|
+
const bfx = bx - bx0, bfy = by - by0;
|
|
57
|
+
const blurRow = ( row ) => blur[ row * bw + bx0 ] + ( blur[ row * bw + bx1 ] - blur[ row * bw + bx0 ] ) * bfx;
|
|
58
|
+
const blurred = blurRow( by0 ) + ( blurRow( by1 ) - blurRow( by0 ) ) * bfy;
|
|
59
|
+
|
|
60
|
+
const cw = p[ 4 ], ch = p[ 5 ];
|
|
61
|
+
const gx = clamp( u * p[ 0 ] - 0.5, 0, cw - 1 ), gy = clamp( v * p[ 1 ] - 0.5, 0, ch - 1 );
|
|
62
|
+
const gz = clamp( ( logL + p[ 8 ] - BIN_MIN ) / ( BIN_MAX - BIN_MIN ), 0, 1 ) * ( BINS - 1 );
|
|
63
|
+
const gx0 = Math.floor( gx ), gy0 = Math.floor( gy ), gz0 = Math.floor( gz );
|
|
64
|
+
const fx = gx - gx0, fy = gy - gy0, fz = gz - gz0;
|
|
65
|
+
let s = 0, w = 0;
|
|
66
|
+
for ( let k = 0; k < 8; k ++ ) {
|
|
67
|
+
|
|
68
|
+
const dx = k & 1, dy = ( k >> 1 ) & 1, dz = k >> 2;
|
|
69
|
+
const weight = ( dx ? fx : 1 - fx ) * ( dy ? fy : 1 - fy ) * ( dz ? fz : 1 - fz );
|
|
70
|
+
const i = ( ( Math.min( gy0 + dy, ch - 1 ) * cw + Math.min( gx0 + dx, cw - 1 ) ) * BINS + Math.min( gz0 + dz, BINS - 1 ) ) * 2;
|
|
71
|
+
s += grid[ i ] * weight;
|
|
72
|
+
w += grid[ i + 1 ] * weight;
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bilateral = w * CELL_TEXELS < 1e-3 ? blurred : s / w;
|
|
77
|
+
const base = bilateral + ( blurred - bilateral ) * p[ 12 ] + logExposure;
|
|
78
|
+
const y = logL + logExposure;
|
|
79
|
+
const contrast = base > p[ 13 ] ? p[ 9 ] : p[ 10 ];
|
|
80
|
+
return 2 ** ( p[ 13 ] + ( base - p[ 13 ] ) * contrast + ( y - base ) * p[ 11 ] - y );
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The same gain in WGSL, for a pass outside three.js: group 1 holds the grid, the blurred picture and the params. */
|
|
85
|
+
export const LOCAL_EXPOSURE_WGSL = /* wgsl */ `
|
|
86
|
+
@group(1) @binding(0) var<storage, read> leGrid: array<vec2<f32>>;
|
|
87
|
+
@group(1) @binding(1) var<storage, read> leBlur: array<f32>;
|
|
88
|
+
@group(1) @binding(2) var<uniform> le: array<vec4<f32>, 4>;
|
|
89
|
+
|
|
90
|
+
fn rayzee_gain( uv: vec2<f32>, linear: vec3<f32>, exposure: f32 ) -> f32 {
|
|
91
|
+
let logL = min( log2( max( dot( linear, vec3<f32>( ${Y.join( ', ' )} ) ), ${DARKEST} ) ), ${LOG_CEIL}.0 );
|
|
92
|
+
let logExposure = log2( exposure );
|
|
93
|
+
|
|
94
|
+
let bs = vec2<i32>( le[ 1 ].zw );
|
|
95
|
+
let bp = clamp( uv * le[ 0 ].zw - 0.5, vec2<f32>( 0.0 ), vec2<f32>( bs - 1 ) );
|
|
96
|
+
let b0 = vec2<i32>( floor( bp ) );
|
|
97
|
+
let b1 = min( b0 + 1, bs - 1 );
|
|
98
|
+
let bf = bp - floor( bp );
|
|
99
|
+
let top = mix( leBlur[ b0.y * bs.x + b0.x ], leBlur[ b0.y * bs.x + b1.x ], bf.x );
|
|
100
|
+
let bottom = mix( leBlur[ b1.y * bs.x + b0.x ], leBlur[ b1.y * bs.x + b1.x ], bf.x );
|
|
101
|
+
let blurred = mix( top, bottom, bf.y );
|
|
102
|
+
|
|
103
|
+
let cs = vec2<i32>( le[ 1 ].xy );
|
|
104
|
+
let gp = clamp( uv * le[ 0 ].xy - 0.5, vec2<f32>( 0.0 ), vec2<f32>( cs - 1 ) );
|
|
105
|
+
let gz = clamp( ( logL + le[ 2 ].x - (${BIN_MIN}.0) ) / ${BIN_MAX - BIN_MIN}.0, 0.0, 1.0 ) * ${BINS - 1}.0;
|
|
106
|
+
let g0 = vec3<i32>( vec2<i32>( floor( gp ) ), i32( floor( gz ) ) );
|
|
107
|
+
let gf = vec3<f32>( gp - floor( gp ), gz - floor( gz ) );
|
|
108
|
+
var acc = vec2<f32>( 0.0 );
|
|
109
|
+
for ( var k = 0; k < 8; k++ ) {
|
|
110
|
+
let d = vec3<i32>( k & 1, ( k >> 1u ) & 1, k >> 2u );
|
|
111
|
+
let wv = select( 1.0 - gf, gf, d == vec3<i32>( 1 ) );
|
|
112
|
+
let c = min( g0 + d, vec3<i32>( cs - 1, ${BINS - 1} ) );
|
|
113
|
+
acc += leGrid[ ( c.y * cs.x + c.x ) * ${BINS} + c.z ] * ( wv.x * wv.y * wv.z );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let bilateral = select( acc.x / acc.y, blurred, acc.y * ${CELL_TEXELS}.0 < 1e-3 );
|
|
117
|
+
let base = mix( bilateral, blurred, le[ 3 ].x ) + logExposure;
|
|
118
|
+
let y = logL + logExposure;
|
|
119
|
+
let contrast = select( le[ 2 ].z, le[ 2 ].y, base > le[ 3 ].y );
|
|
120
|
+
return exp2( le[ 3 ].y + ( base - le[ 3 ].y ) * contrast + ( y - base ) * le[ 2 ].w - y );
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Local exposure — Unreal Engine 5's: the frame's log luminance is split into a base layer (a bilateral grid
|
|
126
|
+
* blended with a blurred picture) and detail; the base's contrast around middle grey is scaled, highlights and
|
|
127
|
+
* shadows apart, and the detail kept. It changes only what is shown: the compositor and every tone-mapped
|
|
128
|
+
* readback apply the same per-pixel gain (`gainNode`, `toneGain`), so a saved picture matches the canvas.
|
|
129
|
+
*
|
|
130
|
+
* Built from `pathtracer:color` when the image restarts, less often as it converges.
|
|
131
|
+
* Events listened: pipeline:reset
|
|
132
|
+
* Textures read: pathtracer:color
|
|
133
|
+
*/
|
|
134
|
+
export class LocalExposure extends RenderStage {
|
|
135
|
+
|
|
136
|
+
constructor( renderer, options = {} ) {
|
|
137
|
+
|
|
138
|
+
super( 'LocalExposure', { ...options, executionMode: StageExecutionMode.ALWAYS } );
|
|
139
|
+
|
|
140
|
+
this.renderer = renderer;
|
|
141
|
+
this.highlightContrast = options.highlightContrast ?? LOCAL_EXPOSURE_DEFAULTS.localExposureHighlightContrast;
|
|
142
|
+
this.shadowContrast = options.shadowContrast ?? LOCAL_EXPOSURE_DEFAULTS.localExposureShadowContrast;
|
|
143
|
+
this.detailStrength = options.detailStrength ?? LOCAL_EXPOSURE_DEFAULTS.localExposureDetailStrength;
|
|
144
|
+
/** Called when the shown picture should be redrawn; the viewer wakes its loop. */
|
|
145
|
+
this.onChange = null;
|
|
146
|
+
|
|
147
|
+
this._grid = attributeArray( MAX_CELLS * MAX_CELLS * BINS, 'vec2' );
|
|
148
|
+
this._blur = attributeArray( MAX_BLUR * MAX_BLUR, 'float' );
|
|
149
|
+
this._blurTmp = attributeArray( MAX_BLUR * MAX_BLUR, 'float' );
|
|
150
|
+
this._inputNode = new TextureNode();
|
|
151
|
+
this._widthU = uniform( 1, 'int' );
|
|
152
|
+
this._heightU = uniform( 1, 'int' );
|
|
153
|
+
this._cellsXU = uniform( 1, 'int' );
|
|
154
|
+
this._blurWU = uniform( 1, 'int' );
|
|
155
|
+
this._blurHU = uniform( 1, 'int' );
|
|
156
|
+
this._radiusU = uniform( 1 );
|
|
157
|
+
this._logExposureU = uniform( 0 );
|
|
158
|
+
this._p = [ new Vector4(), new Vector4(), new Vector4(), new Vector4() ].map( v => uniform( v ) );
|
|
159
|
+
this._params = new Float32Array( 16 );
|
|
160
|
+
|
|
161
|
+
this._kernels = [ this._buildGridKernel(), this._buildBlurSourceKernel(), this._buildBlurKernel( this._blur, this._blurTmp, true ), this._buildBlurKernel( this._blurTmp, this._blur, false ) ];
|
|
162
|
+
|
|
163
|
+
this._built = false;
|
|
164
|
+
this._restarts = 0;
|
|
165
|
+
this._builtAt = { restarts: - 1, samples: - 1 };
|
|
166
|
+
this._stale = false;
|
|
167
|
+
this._paramsBuffer = null;
|
|
168
|
+
this._disposed = false;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Kernels ──────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
_buildGridKernel() {
|
|
175
|
+
|
|
176
|
+
const input = this._inputNode, grid = this._grid;
|
|
177
|
+
const width = this._widthU, height = this._heightU, cellsX = this._cellsXU, logExposure = this._logExposureU;
|
|
178
|
+
const yw = vec4( ...Y, 0 ).xyz;
|
|
179
|
+
|
|
180
|
+
return Fn( () => {
|
|
181
|
+
|
|
182
|
+
const lid = localId.y.mul( uint( 16 ) ).add( localId.x );
|
|
183
|
+
const sums = workgroupArray( 'uint', BINS ).toAtomic();
|
|
184
|
+
const weights = workgroupArray( 'uint', BINS ).toAtomic();
|
|
185
|
+
If( lid.lessThan( uint( BINS ) ), () => {
|
|
186
|
+
|
|
187
|
+
atomicStore( sums.element( lid ), uint( 0 ) );
|
|
188
|
+
atomicStore( weights.element( lid ), uint( 0 ) );
|
|
189
|
+
|
|
190
|
+
} );
|
|
191
|
+
workgroupBarrier();
|
|
192
|
+
|
|
193
|
+
const ox = int( workgroupId.x ).mul( int( CELL ) ).add( int( localId.x ).mul( int( 8 ) ) );
|
|
194
|
+
const oy = int( workgroupId.y ).mul( int( CELL ) ).add( int( localId.y ).mul( int( 8 ) ) );
|
|
195
|
+
|
|
196
|
+
// Each thread bins its 4×4 half-resolution texels (2×2 pixels each), split between the two nearest bins.
|
|
197
|
+
Loop( { start: int( 0 ), end: int( 8 ), type: 'int', condition: '<', update: 2, name: 'ty' }, ( { ty } ) => {
|
|
198
|
+
|
|
199
|
+
Loop( { start: int( 0 ), end: int( 8 ), type: 'int', condition: '<', update: 2, name: 'tx' }, ( { tx } ) => {
|
|
200
|
+
|
|
201
|
+
const x = ox.add( tx ), y = oy.add( ty );
|
|
202
|
+
If( x.lessThan( width ).and( y.lessThan( height ) ), () => {
|
|
203
|
+
|
|
204
|
+
const x1 = min( x.add( int( 1 ) ), width.sub( int( 1 ) ) ), y1 = min( y.add( int( 1 ) ), height.sub( int( 1 ) ) );
|
|
205
|
+
const lum = dot( textureLoad( input, ivec2( x, y ) ).xyz, yw )
|
|
206
|
+
.add( dot( textureLoad( input, ivec2( x1, y ) ).xyz, yw ) )
|
|
207
|
+
.add( dot( textureLoad( input, ivec2( x, y1 ) ).xyz, yw ) )
|
|
208
|
+
.add( dot( textureLoad( input, ivec2( x1, y1 ) ).xyz, yw ) ).mul( 0.25 ).toVar();
|
|
209
|
+
// A NaN or infinite texel is left out, as the meter leaves it out: max() may keep a NaN on some
|
|
210
|
+
// GPUs, and an infinite one would read as 2^40 and darken its whole cell.
|
|
211
|
+
If( floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) ), () => {
|
|
212
|
+
|
|
213
|
+
const logL = log2( max( lum, DARKEST ) ).clamp( LOG_FLOOR, LOG_CEIL ).toVar();
|
|
214
|
+
const f = logL.add( logExposure ).sub( BIN_MIN ).div( BIN_MAX - BIN_MIN ).clamp( 0.0, 1.0 ).mul( BINS - 1 ).toVar();
|
|
215
|
+
const b0 = uint( floor( f ) ).min( uint( BINS - 1 ) );
|
|
216
|
+
const b1 = b0.add( uint( 1 ) ).min( uint( BINS - 1 ) );
|
|
217
|
+
const w1 = f.sub( floor( f ) );
|
|
218
|
+
const w0 = float( 1 ).sub( w1 );
|
|
219
|
+
const level = logL.sub( LOG_FLOOR ).mul( FIXED );
|
|
220
|
+
atomicAdd( sums.element( b0 ), uint( level.mul( w0 ).add( 0.5 ) ) );
|
|
221
|
+
atomicAdd( weights.element( b0 ), uint( w0.mul( FIXED ).add( 0.5 ) ) );
|
|
222
|
+
atomicAdd( sums.element( b1 ), uint( level.mul( w1 ).add( 0.5 ) ) );
|
|
223
|
+
atomicAdd( weights.element( b1 ), uint( w1.mul( FIXED ).add( 0.5 ) ) );
|
|
224
|
+
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
} );
|
|
230
|
+
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
workgroupBarrier();
|
|
234
|
+
|
|
235
|
+
If( lid.lessThan( uint( BINS ) ), () => {
|
|
236
|
+
|
|
237
|
+
const w = float( atomicLoad( weights.element( lid ) ) ).div( FIXED );
|
|
238
|
+
const s = float( atomicLoad( sums.element( lid ) ) ).div( FIXED ).add( w.mul( LOG_FLOOR ) );
|
|
239
|
+
const cell = uint( workgroupId.y ).mul( uint( cellsX ) ).add( uint( workgroupId.x ) );
|
|
240
|
+
grid.element( cell.mul( uint( BINS ) ).add( lid ) ).assign( vec2( s, w ).div( CELL_TEXELS ) );
|
|
241
|
+
|
|
242
|
+
} );
|
|
243
|
+
|
|
244
|
+
} )().compute( [ 1, 1, 1 ], [ 16, 16, 1 ] );
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The log of each 32×32 block's mean luminance: averaged in linear light, like the meter, so noise does not darken it.
|
|
249
|
+
_buildBlurSourceKernel() {
|
|
250
|
+
|
|
251
|
+
const input = this._inputNode, out = this._blur;
|
|
252
|
+
const width = this._widthU, height = this._heightU, blurW = this._blurWU;
|
|
253
|
+
const yw = vec4( ...Y, 0 ).xyz;
|
|
254
|
+
|
|
255
|
+
return Fn( () => {
|
|
256
|
+
|
|
257
|
+
const lid = localId.y.mul( uint( 8 ) ).add( localId.x );
|
|
258
|
+
const ox = int( workgroupId.x ).mul( int( BLUR_TEXEL ) ), oy = int( workgroupId.y ).mul( int( BLUR_TEXEL ) );
|
|
259
|
+
const sum = float( 0 ).toVar();
|
|
260
|
+
const count = float( 0 ).toVar();
|
|
261
|
+
|
|
262
|
+
Loop( { start: int( localId.y ), end: int( BLUR_TEXEL ), type: 'int', condition: '<', update: 8, name: 'by' }, ( { by } ) => {
|
|
263
|
+
|
|
264
|
+
Loop( { start: int( localId.x ), end: int( BLUR_TEXEL ), type: 'int', condition: '<', update: 8, name: 'bx' }, ( { bx } ) => {
|
|
265
|
+
|
|
266
|
+
const x = ox.add( bx ), y = oy.add( by );
|
|
267
|
+
If( x.lessThan( width ).and( y.lessThan( height ) ), () => {
|
|
268
|
+
|
|
269
|
+
const lum = dot( textureLoad( input, ivec2( x, y ) ).xyz, yw ).toVar();
|
|
270
|
+
const finite = floatBitsToUint( lum ).bitAnd( uint( 0x7f800000 ) ).notEqual( uint( 0x7f800000 ) );
|
|
271
|
+
sum.addAssign( select( finite, lum.max( 0.0 ), float( 0 ) ) );
|
|
272
|
+
count.addAssign( select( finite, float( 1 ), float( 0 ) ) );
|
|
273
|
+
|
|
274
|
+
} );
|
|
275
|
+
|
|
276
|
+
} );
|
|
277
|
+
|
|
278
|
+
} );
|
|
279
|
+
|
|
280
|
+
const sharedSum = workgroupArray( 'float', 64 );
|
|
281
|
+
const sharedCount = workgroupArray( 'float', 64 );
|
|
282
|
+
sharedSum.element( lid ).assign( sum );
|
|
283
|
+
sharedCount.element( lid ).assign( count );
|
|
284
|
+
workgroupBarrier();
|
|
285
|
+
|
|
286
|
+
for ( let s = 32; s > 0; s >>= 1 ) {
|
|
287
|
+
|
|
288
|
+
If( lid.lessThan( uint( s ) ), () => {
|
|
289
|
+
|
|
290
|
+
sharedSum.element( lid ).addAssign( sharedSum.element( lid.add( uint( s ) ) ) );
|
|
291
|
+
sharedCount.element( lid ).addAssign( sharedCount.element( lid.add( uint( s ) ) ) );
|
|
292
|
+
|
|
293
|
+
} );
|
|
294
|
+
workgroupBarrier();
|
|
295
|
+
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
If( lid.equal( uint( 0 ) ), () => {
|
|
299
|
+
|
|
300
|
+
const mean = sharedSum.element( uint( 0 ) ).div( max( sharedCount.element( uint( 0 ) ), 1.0 ) );
|
|
301
|
+
out.element( uint( int( workgroupId.y ).mul( blurW ).add( int( workgroupId.x ) ) ) ).assign( log2( max( mean, DARKEST ) ).clamp( LOG_FLOOR, LOG_CEIL ) );
|
|
302
|
+
|
|
303
|
+
} );
|
|
304
|
+
|
|
305
|
+
} )().compute( [ 1, 1, 1 ], [ 8, 8, 1 ] );
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Unreal's Gaussian, exp( -16.7 ( x / r )² ), along one axis, mirrored at the edges.
|
|
310
|
+
_buildBlurKernel( src, dst, ax ) {
|
|
311
|
+
|
|
312
|
+
const blurW = this._blurWU, blurH = this._blurHU, radius = this._radiusU;
|
|
313
|
+
|
|
314
|
+
return Fn( () => {
|
|
315
|
+
|
|
316
|
+
const x = int( workgroupId.x ).mul( int( 8 ) ).add( int( localId.x ) );
|
|
317
|
+
const y = int( workgroupId.y ).mul( int( 8 ) ).add( int( localId.y ) );
|
|
318
|
+
If( x.lessThan( blurW ).and( y.lessThan( blurH ) ), () => {
|
|
319
|
+
|
|
320
|
+
const size = ax ? blurW : blurH;
|
|
321
|
+
const at = ax ? x : y;
|
|
322
|
+
const reach = int( radius.ceil() );
|
|
323
|
+
const sum = float( 0 ).toVar();
|
|
324
|
+
const total = float( 0 ).toVar();
|
|
325
|
+
|
|
326
|
+
Loop( { start: reach.negate(), end: reach.add( int( 1 ) ), type: 'int', condition: '<', name: 'o' }, ( { o } ) => {
|
|
327
|
+
|
|
328
|
+
const t = float( o ).div( radius );
|
|
329
|
+
const w = exp( t.mul( t ).mul( - 16.7 ) );
|
|
330
|
+
// Mirrored as often as it takes: the radius follows the width, so across a strip four times wider than
|
|
331
|
+
// tall the vertical pass reaches past one reflection, and clamping there piled the taps on row 0.
|
|
332
|
+
const period = size.mul( int( 2 ) );
|
|
333
|
+
const c = at.add( o ).mod( period ).add( period ).mod( period ).toVar();
|
|
334
|
+
If( c.greaterThanEqual( size ), () => {
|
|
335
|
+
|
|
336
|
+
c.assign( period.sub( c ).sub( int( 1 ) ) );
|
|
337
|
+
|
|
338
|
+
} );
|
|
339
|
+
const i = ax ? y.mul( blurW ).add( c ) : c.mul( blurW ).add( x );
|
|
340
|
+
sum.addAssign( src.element( uint( i ) ).mul( w ) );
|
|
341
|
+
total.addAssign( w );
|
|
342
|
+
|
|
343
|
+
} );
|
|
344
|
+
|
|
345
|
+
dst.element( uint( y.mul( blurW ).add( x ) ) ).assign( sum.div( total ) );
|
|
346
|
+
|
|
347
|
+
} );
|
|
348
|
+
|
|
349
|
+
} )().compute( [ 1, 1, 1 ], [ 8, 8, 1 ] );
|
|
350
|
+
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── Pipeline ─────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
setupEventListeners() {
|
|
356
|
+
|
|
357
|
+
this.on( 'pipeline:reset', () => this._restarts ++ );
|
|
358
|
+
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
render( context ) {
|
|
362
|
+
|
|
363
|
+
if ( ! this.enabled ) return;
|
|
364
|
+
const samples = context.getState( 'pathtracer:samples' ) ?? 0;
|
|
365
|
+
const last = this._builtAt;
|
|
366
|
+
if ( this._stale || ! this._built || last.restarts !== this._restarts || samples - last.samples >= meterInterval( samples ) ) this.build( context );
|
|
367
|
+
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Builds the grid and blurred picture from the image on screen now. */
|
|
371
|
+
build( context = this.context ) {
|
|
372
|
+
|
|
373
|
+
const input = context?.getTexture( 'pathtracer:color' );
|
|
374
|
+
const width = input?.image?.width | 0, height = input?.image?.height | 0;
|
|
375
|
+
if ( ! input || width < 1 || height < 1 || this._disposed ) return false;
|
|
376
|
+
|
|
377
|
+
this._builtAt.restarts = this._restarts;
|
|
378
|
+
this._builtAt.samples = context.getState( 'pathtracer:samples' ) ?? 0;
|
|
379
|
+
this._stale = false;
|
|
380
|
+
|
|
381
|
+
const cellsX = Math.min( MAX_CELLS, Math.ceil( width / CELL ) ), cellsY = Math.min( MAX_CELLS, Math.ceil( height / CELL ) );
|
|
382
|
+
const blurW = Math.min( MAX_BLUR, Math.ceil( width / BLUR_TEXEL ) ), blurH = Math.min( MAX_BLUR, Math.ceil( height / BLUR_TEXEL ) );
|
|
383
|
+
const logExposure = Math.log2( Math.max( this.renderer.toneMappingExposure, 1e-6 ) );
|
|
384
|
+
|
|
385
|
+
this._inputNode.value = input;
|
|
386
|
+
this._widthU.value = width;
|
|
387
|
+
this._heightU.value = height;
|
|
388
|
+
this._cellsXU.value = cellsX;
|
|
389
|
+
this._blurWU.value = blurW;
|
|
390
|
+
this._blurHU.value = blurH;
|
|
391
|
+
this._radiusU.value = Math.max( 1, BLUR_PERCENT / 100 * blurW );
|
|
392
|
+
this._logExposureU.value = logExposure;
|
|
393
|
+
this._kernels[ 0 ].dispatchSize = [ cellsX, cellsY, 1 ];
|
|
394
|
+
this._kernels[ 1 ].dispatchSize = [ blurW, blurH, 1 ];
|
|
395
|
+
this._kernels[ 2 ].dispatchSize = this._kernels[ 3 ].dispatchSize = [ Math.ceil( blurW / 8 ), Math.ceil( blurH / 8 ), 1 ];
|
|
396
|
+
this.renderer.compute( this._kernels );
|
|
397
|
+
|
|
398
|
+
const p = this._params;
|
|
399
|
+
p.set( [ width / CELL, height / CELL, width / BLUR_TEXEL, height / BLUR_TEXEL, cellsX, cellsY, blurW, blurH, logExposure ], 0 );
|
|
400
|
+
this._built = true;
|
|
401
|
+
this._writeParams();
|
|
402
|
+
return true;
|
|
403
|
+
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
_writeParams() {
|
|
407
|
+
|
|
408
|
+
const p = this._params;
|
|
409
|
+
p[ 9 ] = this.highlightContrast;
|
|
410
|
+
p[ 10 ] = this.shadowContrast;
|
|
411
|
+
p[ 11 ] = this.detailStrength;
|
|
412
|
+
p[ 12 ] = BLUR_BLEND;
|
|
413
|
+
p[ 13 ] = MIDDLE_GREY;
|
|
414
|
+
for ( let i = 0; i < 4; i ++ ) this._p[ i ].value.fromArray( p, i * 4 );
|
|
415
|
+
this.onChange?.();
|
|
416
|
+
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Whether the image on screen should be built from again though no new samples arrive. */
|
|
420
|
+
get wantsBuild() {
|
|
421
|
+
|
|
422
|
+
return this.enabled && ( this._stale || ! this._built );
|
|
423
|
+
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
updateParameters( params ) {
|
|
427
|
+
|
|
428
|
+
for ( const key of [ 'highlightContrast', 'shadowContrast', 'detailStrength' ] ) {
|
|
429
|
+
|
|
430
|
+
if ( params[ key ] !== undefined ) this[ key ] = params[ key ];
|
|
431
|
+
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
this._writeParams();
|
|
435
|
+
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
setEnabled( enabled ) {
|
|
439
|
+
|
|
440
|
+
if ( this.enabled === enabled ) return;
|
|
441
|
+
this.enabled = enabled;
|
|
442
|
+
this._stale = true;
|
|
443
|
+
this.onChange?.();
|
|
444
|
+
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Applying ─────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
/** The gain as a TSL node, for the compositor: `rgb` the shown linear colour, `uv` its image position (y down). */
|
|
450
|
+
gainNode( rgb, uv ) {
|
|
451
|
+
|
|
452
|
+
const grid = storage( this._grid.value, 'vec2', MAX_CELLS * MAX_CELLS * BINS ).toReadOnly();
|
|
453
|
+
const blur = storage( this._blur.value, 'float', MAX_BLUR * MAX_BLUR ).toReadOnly();
|
|
454
|
+
const [ p0, p1, p2, p3 ] = this._p;
|
|
455
|
+
|
|
456
|
+
return Fn( ( [ color, at ] ) => {
|
|
457
|
+
|
|
458
|
+
const logL = log2( max( dot( color, vec4( ...Y, 0 ).xyz ), DARKEST ) ).min( LOG_CEIL ).toVar();
|
|
459
|
+
const logExposure = log2( toneMappingExposure ).toVar();
|
|
460
|
+
|
|
461
|
+
const bs = ivec2( p1.zw );
|
|
462
|
+
const bp = at.mul( p0.zw ).sub( 0.5 ).clamp( vec2( 0 ), vec2( bs.sub( int( 1 ) ) ) ).toVar();
|
|
463
|
+
const b0 = ivec2( floor( bp ) ).toVar();
|
|
464
|
+
const b1 = b0.add( int( 1 ) ).min( bs.sub( int( 1 ) ) ).toVar();
|
|
465
|
+
const bf = bp.sub( floor( bp ) ).toVar();
|
|
466
|
+
const blurAt = ( x, y ) => blur.element( uint( y.mul( bs.x ).add( x ) ) );
|
|
467
|
+
const blurred = mix( mix( blurAt( b0.x, b0.y ), blurAt( b1.x, b0.y ), bf.x ), mix( blurAt( b0.x, b1.y ), blurAt( b1.x, b1.y ), bf.x ), bf.y ).toVar();
|
|
468
|
+
|
|
469
|
+
const cs = ivec2( p1.xy );
|
|
470
|
+
const gp = at.mul( p0.xy ).sub( 0.5 ).clamp( vec2( 0 ), vec2( cs.sub( int( 1 ) ) ) ).toVar();
|
|
471
|
+
const gz = logL.add( p2.x ).sub( BIN_MIN ).div( BIN_MAX - BIN_MIN ).clamp( 0.0, 1.0 ).mul( BINS - 1 ).toVar();
|
|
472
|
+
const gx0 = int( floor( gp.x ) ), gy0 = int( floor( gp.y ) ), gz0 = int( floor( gz ) );
|
|
473
|
+
const fx = gp.x.sub( floor( gp.x ) ), fy = gp.y.sub( floor( gp.y ) ), fz = gz.sub( floor( gz ) );
|
|
474
|
+
const acc = vec2( 0 ).toVar();
|
|
475
|
+
for ( let k = 0; k < 8; k ++ ) {
|
|
476
|
+
|
|
477
|
+
const dx = k & 1, dy = ( k >> 1 ) & 1, dz = k >> 2;
|
|
478
|
+
const weight = ( dx ? fx : fx.oneMinus() ).mul( dy ? fy : fy.oneMinus() ).mul( dz ? fz : fz.oneMinus() );
|
|
479
|
+
const cx = gx0.add( int( dx ) ).min( cs.x.sub( int( 1 ) ) );
|
|
480
|
+
const cy = gy0.add( int( dy ) ).min( cs.y.sub( int( 1 ) ) );
|
|
481
|
+
const cz = gz0.add( int( dz ) ).min( int( BINS - 1 ) );
|
|
482
|
+
acc.addAssign( grid.element( uint( cy.mul( cs.x ).add( cx ).mul( int( BINS ) ).add( cz ) ) ).mul( weight ) );
|
|
483
|
+
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const bilateral = select( acc.y.mul( CELL_TEXELS ).lessThan( 1e-3 ), blurred, acc.x.div( acc.y ) );
|
|
487
|
+
const base = mix( bilateral, blurred, p3.x ).add( logExposure ).toVar();
|
|
488
|
+
const y = logL.add( logExposure );
|
|
489
|
+
const contrast = select( base.greaterThan( p3.y ), p2.y, p2.z );
|
|
490
|
+
return exp2( p3.y.add( base.sub( p3.y ).mul( contrast ) ).add( y.sub( base ).mul( p2.w ) ).sub( y ) );
|
|
491
|
+
|
|
492
|
+
} )( rgb, uv );
|
|
493
|
+
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The gain for a tone-mapping pass outside three.js (`PackedToneMapper`'s `gain`), or null before the
|
|
498
|
+
* first build. `cpu()` reads the grid back for the CPU tone map.
|
|
499
|
+
*/
|
|
500
|
+
toneGain() {
|
|
501
|
+
|
|
502
|
+
if ( ! this.enabled || ! this._built ) return null;
|
|
503
|
+
const backend = this.renderer.backend;
|
|
504
|
+
const grid = backend.get( this._grid.value )?.buffer;
|
|
505
|
+
const blur = backend.get( this._blur.value )?.buffer;
|
|
506
|
+
if ( ! grid || ! blur ) return null;
|
|
507
|
+
|
|
508
|
+
this._paramsBuffer ??= backend.device.createBuffer( { label: 'local-exposure-params', size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
|
|
509
|
+
backend.device.queue.writeBuffer( this._paramsBuffer, 0, this._params );
|
|
510
|
+
const params = this._params.slice();
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
key: 'local-exposure',
|
|
514
|
+
wgsl: LOCAL_EXPOSURE_WGSL,
|
|
515
|
+
entries: [
|
|
516
|
+
{ binding: 0, resource: { buffer: grid } },
|
|
517
|
+
{ binding: 1, resource: { buffer: blur } },
|
|
518
|
+
{ binding: 2, resource: { buffer: this._paramsBuffer } },
|
|
519
|
+
],
|
|
520
|
+
cpu: async () => {
|
|
521
|
+
|
|
522
|
+
const [ g, b ] = await Promise.all( [ this.renderer.getArrayBufferAsync( this._grid.value ), this.renderer.getArrayBufferAsync( this._blur.value ) ] );
|
|
523
|
+
const gridData = new Float32Array( g ), blurData = new Float32Array( b );
|
|
524
|
+
return ( u, v, lum, logExposure ) => localExposureGain( params, gridData, blurData, u, v, lum, logExposure );
|
|
525
|
+
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
reset() {}
|
|
532
|
+
|
|
533
|
+
setSize() {}
|
|
534
|
+
|
|
535
|
+
dispose() {
|
|
536
|
+
|
|
537
|
+
this._disposed = true;
|
|
538
|
+
this.onChange = null;
|
|
539
|
+
for ( const kernel of this._kernels ) kernel.dispose();
|
|
540
|
+
this._inputNode.dispose();
|
|
541
|
+
this._paramsBuffer?.destroy();
|
|
542
|
+
this._paramsBuffer = null;
|
|
543
|
+
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
}
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -1093,9 +1093,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
1093
1093
|
*/
|
|
1094
1094
|
_willReadCountersThisFrame() {
|
|
1095
1095
|
|
|
1096
|
-
if ( this.
|
|
1096
|
+
if ( this.cameraOptimizer?.isInInteractionMode() ) return false;
|
|
1097
1097
|
if ( this._lockstep ) return ! this._lockstepRead && this.frameCount % this._readbackEveryNFrames === 0;
|
|
1098
|
-
if ( this._readbackPending || this._convergedReadbackPending ) return false;
|
|
1098
|
+
if ( this.cameraChanged || this._readbackPending || this._convergedReadbackPending ) return false;
|
|
1099
1099
|
return this._readbackFrameCounter + 1 >= this._readbackEveryNFrames;
|
|
1100
1100
|
|
|
1101
1101
|
}
|
|
@@ -1207,9 +1207,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
1207
1207
|
|
|
1208
1208
|
_issueLockstepReadback() {
|
|
1209
1209
|
|
|
1210
|
-
// One in flight, and none
|
|
1210
|
+
// One in flight, and none in interaction mode: frames there do not count, so frameCount stands still. A moved
|
|
1211
|
+
// camera skips none: a reset's frame 0 sees one whenever the frame before it traced another view, and skipping
|
|
1212
|
+
// it there made the render depend on what was traced before.
|
|
1211
1213
|
if ( this._lockstepRead || this.frameCount % this._readbackEveryNFrames !== 0 ) return;
|
|
1212
|
-
if ( this.
|
|
1214
|
+
if ( this.cameraOptimizer?.isInInteractionMode() ) return;
|
|
1213
1215
|
|
|
1214
1216
|
const bounceAttr = this._queueManager?.getBounceCountsAttribute();
|
|
1215
1217
|
const counterAttr = this._queueManager?.getCountersAttribute();
|
|
@@ -115,6 +115,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
115
115
|
this.performanceMonitor = createPerformanceMonitor();
|
|
116
116
|
this.completionThreshold = 0;
|
|
117
117
|
this.renderLimitMode = 'frames';
|
|
118
|
+
this.renderTimeLimit = 0;
|
|
118
119
|
|
|
119
120
|
// Initialize data textures
|
|
120
121
|
this._initDataTextures();
|
|
@@ -1536,21 +1537,22 @@ export class PathTracerStage extends RenderStage {
|
|
|
1536
1537
|
}
|
|
1537
1538
|
|
|
1538
1539
|
/**
|
|
1539
|
-
* Update completion threshold based on render mode. The ceiling holds in
|
|
1540
|
-
* uncapping it under a time budget lets a generous deadline silently slow every render.
|
|
1540
|
+
* Update completion threshold based on render mode. The ceiling holds in 'frames' and 'time' —
|
|
1541
|
+
* uncapping it under a time budget lets a generous deadline silently slow every render. Only
|
|
1542
|
+
* 'timeOnly' lifts it, and only while a deadline is armed: without one nothing would stop the render.
|
|
1541
1543
|
*/
|
|
1542
1544
|
updateCompletionThreshold() {
|
|
1543
1545
|
|
|
1544
|
-
this.completionThreshold =
|
|
1545
|
-
|
|
1546
|
-
this.maxSamples.value
|
|
1547
|
-
);
|
|
1546
|
+
this.completionThreshold = this.renderLimitMode === 'timeOnly' && this.renderTimeLimit > 0
|
|
1547
|
+
? Infinity
|
|
1548
|
+
: updateCompletionThreshold( this.renderMode.value, this.maxSamples.value );
|
|
1548
1549
|
|
|
1549
1550
|
}
|
|
1550
1551
|
|
|
1551
|
-
setRenderLimitMode( mode ) {
|
|
1552
|
+
setRenderLimitMode( mode, timeLimit = this.renderTimeLimit ) {
|
|
1552
1553
|
|
|
1553
1554
|
this.renderLimitMode = mode;
|
|
1555
|
+
this.renderTimeLimit = timeLimit;
|
|
1554
1556
|
this.updateCompletionThreshold();
|
|
1555
1557
|
|
|
1556
1558
|
}
|
package/src/index.js
CHANGED
|
@@ -68,6 +68,7 @@ export {
|
|
|
68
68
|
} from './Stages/DenoiserSettings.js';
|
|
69
69
|
export { SKY_DEFAULTS, DEFAULT_SUN_PATH } from './managers/EnvironmentManager.js';
|
|
70
70
|
export { AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
|
|
71
|
+
export { LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
|
|
71
72
|
export { AUTO_FOCUS_DEFAULTS } from './managers/CameraManager.js';
|
|
72
73
|
export { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS } from './Processor/BufferLayout.js';
|
|
73
74
|
export { sunPosition, timeForSunElevation, dayOfYearForMonth } from './Processor/SunPosition.js';
|