rayzee 11.4.0 → 11.6.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 +1 -0
- package/dist/{ArchiveImporter-Cgyx5AF7.js → ArchiveImporter-iofnC8zB.js} +2 -2
- package/dist/{BidirectionalIntegrator-BKF-cwH2.js → BidirectionalIntegrator-CqAilt2I.js} +289 -289
- package/dist/{EngineEvents-Ci8dVWCP.js → EngineEvents-CLckhYbL.js} +1 -0
- package/dist/{NeuralRetouch-C3Q2XqbF.js → NeuralRetouch-DFI4pOjO.js} +2 -2
- package/dist/{NeuralSuperRes-DV5TtURP.js → NeuralSuperRes-ChCiiilS.js} +3 -3
- package/dist/{PathTracer-D3MvEaZK.js → PathTracer-BX4sJLx7.js} +3194 -3162
- package/dist/{PhysicalSky-CY4jiz6a.js → PhysicalSky-Biv39eTA.js} +1 -1
- package/dist/{RayzeeRenderer-D23m6NqG.js → RayzeeRenderer-D8XRKU3A.js} +406 -437
- package/dist/{ToneMapGPU-CTDBvmmS.js → ToneMapGPU-sAs8wdgI.js} +14 -1
- 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 +5 -5
- package/dist/rayzee.es.js +544 -460
- package/dist/rayzee.umd.js +63 -55
- package/dist/{sceneCachePolicy-AKOEu2lf.js → sceneCachePolicy-W5PLbzuR.js} +2 -2
- package/package.json +2 -2
- package/src/EngineEvents.js +2 -0
- package/src/Passes/AIUpscaler.js +15 -12
- package/src/Passes/OIDNDenoiser.js +106 -98
- package/src/PathTracerApp.js +2 -2
- package/src/Processor/TextureReadback.js +18 -1
- package/src/Processor/ToneMapGPU.js +22 -3
- package/src/RayzeeRenderer.js +4 -4
- package/src/Stages/LocalExposure.js +25 -2
- package/src/Stages/PathTracer.js +2 -10
- package/src/managers/DenoisingManager.js +97 -19
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as e } from "./EngineEvents-
|
|
1
|
+
import { t as e } from "./EngineEvents-CLckhYbL.js";
|
|
2
2
|
import { a as t, i as n, r } from "./Logger-dDDGj9Av.js";
|
|
3
3
|
import { f as i, v as a, y as o } from "./BufferLayout-xPRH9jYD.js";
|
|
4
4
|
import { n as s } from "./EngineIssues-CIZnXHsF.js";
|
|
@@ -1594,7 +1594,7 @@ var vt = class {
|
|
|
1594
1594
|
samples: this.samples.slice()
|
|
1595
1595
|
};
|
|
1596
1596
|
}
|
|
1597
|
-
}, yt = "11.
|
|
1597
|
+
}, yt = "11.6.0", $ = 1e4, bt = 15e5;
|
|
1598
1598
|
function xt(e, t, n = $) {
|
|
1599
1599
|
return e >= n && t / bt < e / 3;
|
|
1600
1600
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time WebGPU path tracing engine built on Three.js",
|
|
6
6
|
"main": "dist/rayzee.umd.js",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"three": ">=0.186.0"
|
|
61
61
|
},
|
|
62
62
|
"optionalDependencies": {
|
|
63
|
-
"oidn-web": ">=0.
|
|
63
|
+
"oidn-web": ">=0.5.0",
|
|
64
64
|
"onnxruntime-web": ">=1.0.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/EngineEvents.js
CHANGED
|
@@ -14,6 +14,8 @@ export const EngineEvents = {
|
|
|
14
14
|
// Denoiser
|
|
15
15
|
DENOISING_START: 'engine:denoisingStart',
|
|
16
16
|
DENOISING_END: 'engine:denoisingEnd',
|
|
17
|
+
// The denoiser fetches and builds its network ({ loading }); a denoise asked for meanwhile waits for it.
|
|
18
|
+
DENOISER_LOADING: 'engine:denoiserLoading',
|
|
17
19
|
|
|
18
20
|
// Upscaler
|
|
19
21
|
UPSCALING_START: 'engine:upscalingStart',
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -123,7 +123,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
123
123
|
this._worker = null;
|
|
124
124
|
this._currentModelUrl = null;
|
|
125
125
|
this._tileId = 0;
|
|
126
|
-
this._pendingWorkerHandlers = new
|
|
126
|
+
this._pendingWorkerHandlers = new Map();
|
|
127
127
|
|
|
128
128
|
// Alpha channel cache (bilinear-upscaled from source, applied per tile)
|
|
129
129
|
this._upscaledAlpha = null;
|
|
@@ -227,7 +227,8 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
227
227
|
|
|
228
228
|
// ─── Public Lifecycle ─────────────────────────────────────────────────────
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
/** @param {ImageData} [source] - what to enlarge instead of the render canvas */
|
|
231
|
+
async start( source = null ) {
|
|
231
232
|
|
|
232
233
|
if ( ! this.enabled || this.state.isUpscaling || this.state.isLoading ) {
|
|
233
234
|
|
|
@@ -238,7 +239,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
238
239
|
this.dispatchEvent( { type: 'start' } );
|
|
239
240
|
|
|
240
241
|
const startTime = performance.now();
|
|
241
|
-
const success = await this.execute();
|
|
242
|
+
const success = await this.execute( source );
|
|
242
243
|
|
|
243
244
|
if ( success ) {
|
|
244
245
|
|
|
@@ -260,7 +261,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
260
261
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
async execute() {
|
|
264
|
+
async execute( source = null ) {
|
|
264
265
|
|
|
265
266
|
if ( ! this.enabled ) return false;
|
|
266
267
|
|
|
@@ -270,7 +271,11 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
270
271
|
// Capture source image SYNCHRONOUSLY before any async work.
|
|
271
272
|
// WebGPU canvas textures expire after each compositor frame,
|
|
272
273
|
// so we must grab the pixels before awaiting model load.
|
|
273
|
-
if (
|
|
274
|
+
if ( source ) {
|
|
275
|
+
|
|
276
|
+
this._capturedSource = source;
|
|
277
|
+
|
|
278
|
+
} else if ( this.hdr && this.getGPUTextures ) {
|
|
274
279
|
|
|
275
280
|
// HDR path: float planes tone-mapped on the GPU, no 8-bit step before the network
|
|
276
281
|
this._capturedSource = await this._captureSourceHDR();
|
|
@@ -586,7 +591,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
586
591
|
|
|
587
592
|
};
|
|
588
593
|
|
|
589
|
-
this._pendingWorkerHandlers.
|
|
594
|
+
this._pendingWorkerHandlers.set( handler, reject );
|
|
590
595
|
this._worker.addEventListener( 'message', handler );
|
|
591
596
|
this._worker.postMessage(
|
|
592
597
|
{ type: 'infer', tileData, width, height, id },
|
|
@@ -814,15 +819,13 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
814
819
|
|
|
815
820
|
// ─── Disposal ─────────────────────────────────────────────────────────────
|
|
816
821
|
|
|
822
|
+
// Rejected, not dropped: an aborted run has to end before the next one starts.
|
|
817
823
|
_cleanupPendingWorkerHandlers() {
|
|
818
824
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
for ( const handler of this._pendingWorkerHandlers ) {
|
|
825
|
+
for ( const [ handler, reject ] of this._pendingWorkerHandlers ) {
|
|
822
826
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
827
|
+
this._worker?.removeEventListener( 'message', handler );
|
|
828
|
+
reject( new DOMException( 'Aborted', 'AbortError' ) );
|
|
826
829
|
|
|
827
830
|
}
|
|
828
831
|
|
|
@@ -4,9 +4,22 @@ import { createLogger, fmt } from '../utils/Logger.js';
|
|
|
4
4
|
|
|
5
5
|
const log = createLogger( 'oidn' );
|
|
6
6
|
|
|
7
|
-
// The tile alignment oidn-web's own
|
|
7
|
+
// The tile alignment oidn-web's own planner uses (its tileScheduler.js).
|
|
8
8
|
const OIDN_TILE_ALIGNMENT = 16;
|
|
9
9
|
|
|
10
|
+
// Mirrored around the frame: at the network's own boundary (zero padding, or oidn-web's repeated
|
|
11
|
+
// edge pixel) the outer pixels come out worse than not denoising at all. A multiple of 16 keeps
|
|
12
|
+
// the frame on the network's pooling grid, so pixels away from the edges denoise as before.
|
|
13
|
+
const OIDN_BORDER = 16;
|
|
14
|
+
|
|
15
|
+
/** The buffer oidn-web reads: the frame inside a mirrored border, both sides a multiple of 16. */
|
|
16
|
+
function oidnInputSize( width, height ) {
|
|
17
|
+
|
|
18
|
+
const align = n => Math.ceil( ( n + 2 * OIDN_BORDER ) / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT;
|
|
19
|
+
return { width: align( width ), height: align( height ) };
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
let _initUNetFromBuffer = null;
|
|
11
24
|
async function getInitUNetFromBuffer() {
|
|
12
25
|
|
|
@@ -42,11 +55,8 @@ const MODEL_CONFIG = {
|
|
|
42
55
|
DEFAULT_OPTIONS: {
|
|
43
56
|
enableOIDN: true,
|
|
44
57
|
oidnQuality: 'fast',
|
|
45
|
-
// A cap, not a
|
|
46
|
-
//
|
|
47
|
-
// tileSize + 2*overlap (96px, 112 for _large): at 1024²/high, 4x512 tiles measured
|
|
48
|
-
// 422ms against 229ms for one 1024 tile. 1024 caps the one-time activation
|
|
49
|
-
// allocation at ~430MB; beyond it larger frames tile and stay bounded.
|
|
58
|
+
// A cap on the frame, not a tile size: a frame that fits is one tile, with no overlap.
|
|
59
|
+
// It bounds the network's activation memory (~430 MB at 1024²).
|
|
50
60
|
tileSize: 1024
|
|
51
61
|
}
|
|
52
62
|
};
|
|
@@ -89,12 +99,13 @@ fn reduce( @builtin(global_invocation_id) gid: vec3<u32>,
|
|
|
89
99
|
@builtin(local_invocation_id) lid: vec3<u32>,
|
|
90
100
|
@builtin(workgroup_id) wid: vec3<u32> ) {
|
|
91
101
|
|
|
102
|
+
// x: frame pixels, y: frame width, z: buffer width, w: border. The border is not metered.
|
|
92
103
|
let total = params.x;
|
|
93
104
|
var sum = 0.0;
|
|
94
105
|
var i = gid.x;
|
|
95
106
|
loop {
|
|
96
107
|
if ( i >= total ) { break; }
|
|
97
|
-
let c = col[ i ].xyz;
|
|
108
|
+
let c = col[ ( i / params.y + params.w ) * params.z + i % params.y + params.w ].xyz;
|
|
98
109
|
let lum = 0.212671 * c.r + 0.71516 * c.g + 0.072169 * c.b;
|
|
99
110
|
sum = sum + log2( max( lum, 0.0 ) + 0.0001 );
|
|
100
111
|
i = i + WG * GROUPS;
|
|
@@ -155,7 +166,7 @@ struct UnpackParams {
|
|
|
155
166
|
tileY: u32,
|
|
156
167
|
tileW: u32,
|
|
157
168
|
tileH: u32,
|
|
158
|
-
|
|
169
|
+
border: u32,
|
|
159
170
|
pad1: u32,
|
|
160
171
|
pad2: u32,
|
|
161
172
|
};
|
|
@@ -173,7 +184,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
|
173
184
|
|
|
174
185
|
let px = P.tileX + gid.x;
|
|
175
186
|
let py = P.tileY + gid.y;
|
|
176
|
-
let si = py * P.srcWidth + px;
|
|
187
|
+
let si = ( py + P.border ) * P.srcWidth + px + P.border;
|
|
177
188
|
|
|
178
189
|
// scaleBuf is the autoexposure factor the input was multiplied by; dividing it out returns
|
|
179
190
|
// the renderer's units. Alpha rides through from the path tracer so a transparent background
|
|
@@ -186,13 +197,20 @@ const PACK_WG_SIZE = 8;
|
|
|
186
197
|
const PACK_WGSL = /* wgsl */`
|
|
187
198
|
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
188
199
|
@group(0) @binding(1) var<storage, read_write> dst: array<vec4<f32>>;
|
|
189
|
-
@group(0) @binding(2) var<uniform> size:
|
|
200
|
+
@group(0) @binding(2) var<uniform> size: vec4<u32>; // buffer w, h; frame w, h
|
|
201
|
+
|
|
202
|
+
fn mirror( i: i32, n: i32 ) -> i32 {
|
|
203
|
+
let m = abs( i );
|
|
204
|
+
return clamp( select( m, 2 * n - 2 - m, m >= n ), 0, n - 1 );
|
|
205
|
+
}
|
|
190
206
|
|
|
191
207
|
@compute @workgroup_size(${PACK_WG_SIZE}, ${PACK_WG_SIZE})
|
|
192
208
|
fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
|
|
193
209
|
|
|
194
210
|
if ( gid.x >= size.x || gid.y >= size.y ) { return; }
|
|
195
|
-
|
|
211
|
+
let p = vec2<i32>( gid.xy ) - ${OIDN_BORDER};
|
|
212
|
+
let s = vec2<i32>( mirror( p.x, i32( size.z ) ), mirror( p.y, i32( size.w ) ) );
|
|
213
|
+
dst[ gid.y * size.x + gid.x ] = textureLoad( src, s, 0 );
|
|
196
214
|
}
|
|
197
215
|
`;
|
|
198
216
|
|
|
@@ -223,8 +241,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
223
241
|
|
|
224
242
|
// Cached GPU storage buffers for texture→buffer copies (reused across denoise calls)
|
|
225
243
|
this._gpuInputBuffers = { color: null, albedo: null, normal: null };
|
|
244
|
+
// The frame the buffers hold; they are allocated at oidnInputSize() of it.
|
|
226
245
|
this._gpuInputBufferSize = { width: 0, height: 0 };
|
|
227
|
-
// Packs
|
|
246
|
+
// Packs the frame and its mirrored border; see _beginPack.
|
|
228
247
|
this._packPipeline = null;
|
|
229
248
|
this._packLayout = null;
|
|
230
249
|
this._packParams = null;
|
|
@@ -366,16 +385,14 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
366
385
|
const longest = Math.max( this._renderWidth, this._renderHeight );
|
|
367
386
|
if ( ! longest ) return this.maxTileSize;
|
|
368
387
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
// The cap is on the frame, so the border never splits a frame that fits one tile.
|
|
389
|
+
if ( longest <= this.maxTileSize ) {
|
|
390
|
+
|
|
391
|
+
const input = oidnInputSize( this._renderWidth, this._renderHeight );
|
|
392
|
+
return Math.max( input.width, input.height );
|
|
393
|
+
|
|
394
|
+
}
|
|
376
395
|
|
|
377
|
-
// The cap has to be aligned too, or a cap somebody picked by hand lands on the same slow
|
|
378
|
-
// path. Rounded down, so it never rises above what the caller asked for.
|
|
379
396
|
return Math.max( OIDN_TILE_ALIGNMENT, Math.floor( this.maxTileSize / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT );
|
|
380
397
|
|
|
381
398
|
}
|
|
@@ -470,7 +487,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
470
487
|
|
|
471
488
|
const params = this.backendParamsGetter();
|
|
472
489
|
this.gpuDevice = params?.device ?? null;
|
|
473
|
-
backendParams = params?.device ? params : undefined;
|
|
490
|
+
backendParams = params?.device ? { device: params.device } : undefined;
|
|
474
491
|
|
|
475
492
|
}
|
|
476
493
|
|
|
@@ -648,8 +665,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
648
665
|
|
|
649
666
|
/**
|
|
650
667
|
* GPU-native execution path. Copies render target textures into GPU storage buffers
|
|
651
|
-
*
|
|
652
|
-
* oidn-web's well-tested GPUBuffer path.
|
|
668
|
+
* (GPU-only, no CPU roundtrip), then passes those buffers to oidn-web's GPUBuffer path.
|
|
653
669
|
*
|
|
654
670
|
* Note: oidn-web's GPUTexture input path produces NaN outputs — using GPUBuffer instead.
|
|
655
671
|
*/
|
|
@@ -688,71 +704,55 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
688
704
|
|
|
689
705
|
// Autoexposure and the pre-multiply it drives, both on the GPU and both queued behind the
|
|
690
706
|
// copies above. Nothing is awaited here — an await would drain the queue before the UNet.
|
|
691
|
-
|
|
692
|
-
this.
|
|
707
|
+
const input = oidnInputSize( width, height );
|
|
708
|
+
this._computeInputScale( device, width, height );
|
|
709
|
+
this._applyColorScale( device, input.width * input.height );
|
|
693
710
|
|
|
694
711
|
// A final denoise is shown tile by tile, so the tiles still to come must show this render
|
|
695
712
|
// rather than the blank or previous-view picture the output holds between runs.
|
|
696
713
|
if ( ! continuous && ! this._hasOutput ) {
|
|
697
714
|
|
|
698
|
-
this._unpackToTexture( this._gpuInputBuffers.color,
|
|
715
|
+
this._unpackToTexture( this._gpuInputBuffers.color, { x: 0, y: 0, width, height }, false );
|
|
699
716
|
|
|
700
717
|
}
|
|
701
718
|
|
|
702
|
-
|
|
719
|
+
const image = data => ( { data, width: input.width, height: input.height } );
|
|
703
720
|
const config = {
|
|
704
|
-
color:
|
|
705
|
-
albedo:
|
|
706
|
-
normal:
|
|
721
|
+
color: image( this._gpuInputBuffers.color ),
|
|
722
|
+
albedo: image( this._gpuInputBuffers.albedo ),
|
|
723
|
+
normal: image( this._gpuInputBuffers.normal )
|
|
707
724
|
};
|
|
708
725
|
|
|
709
726
|
return this._executeWithAbortGPU( config, continuous );
|
|
710
727
|
|
|
711
728
|
}
|
|
712
729
|
|
|
713
|
-
/** Copies the render's colour, albedo and normal into the
|
|
730
|
+
/** Copies the render's colour, albedo and normal into the buffers oidn-web reads, border included. @private */
|
|
714
731
|
_copyInputs( device, textures, width, height ) {
|
|
715
732
|
|
|
716
|
-
|
|
733
|
+
const input = oidnInputSize( width, height );
|
|
717
734
|
const encoder = device.createCommandEncoder( { label: 'oidn-tex-to-buf' } );
|
|
718
|
-
const
|
|
719
|
-
const pass = tightRowBytes % 256 === 0 ? null : this._beginPack( device, encoder, width, height );
|
|
720
|
-
|
|
721
|
-
const copyTex = ( tex, tightBuf ) => {
|
|
722
|
-
|
|
723
|
-
if ( pass ) {
|
|
724
|
-
|
|
725
|
-
pass.setBindGroup( 0, device.createBindGroup( {
|
|
726
|
-
layout: this._packLayout,
|
|
727
|
-
entries: [
|
|
728
|
-
{ binding: 0, resource: tex.createView() },
|
|
729
|
-
{ binding: 1, resource: { buffer: tightBuf } },
|
|
730
|
-
{ binding: 2, resource: { buffer: this._packParams } },
|
|
731
|
-
],
|
|
732
|
-
} ) );
|
|
733
|
-
pass.dispatchWorkgroups( Math.ceil( width / PACK_WG_SIZE ), Math.ceil( height / PACK_WG_SIZE ) );
|
|
734
|
-
|
|
735
|
-
} else {
|
|
736
|
-
|
|
737
|
-
encoder.copyTextureToBuffer(
|
|
738
|
-
{ texture: tex, mipLevel: 0 },
|
|
739
|
-
{ buffer: tightBuf, offset: 0, bytesPerRow: tightRowBytes, rowsPerImage: height },
|
|
740
|
-
{ width, height, depthOrArrayLayers: 1 }
|
|
741
|
-
);
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
};
|
|
735
|
+
const pass = this._beginPack( device, encoder, input, width, height );
|
|
746
736
|
|
|
747
|
-
copyTex( textures.color, this._gpuInputBuffers.color );
|
|
748
|
-
copyTex( textures.albedo, this._gpuInputBuffers.albedo );
|
|
749
737
|
// The normal stays [0,1]-encoded. This contradicts OIDN's API ("must be in the [-1,1]
|
|
750
738
|
// range") but is right for this port: OIDN's own getNormal (cpu_input_process.isph) does
|
|
751
739
|
// `value*0.5+0.5` before the network, and oidn-web omits that remap. Decoding to [-1,1]
|
|
752
740
|
// measured 0.878 -> 2.439 denoise ratio at 64 spp. Do not "fix" it.
|
|
753
|
-
|
|
741
|
+
for ( const name of [ 'color', 'albedo', 'normal' ] ) {
|
|
754
742
|
|
|
755
|
-
|
|
743
|
+
pass.setBindGroup( 0, device.createBindGroup( {
|
|
744
|
+
layout: this._packLayout,
|
|
745
|
+
entries: [
|
|
746
|
+
{ binding: 0, resource: textures[ name ].createView() },
|
|
747
|
+
{ binding: 1, resource: { buffer: this._gpuInputBuffers[ name ] } },
|
|
748
|
+
{ binding: 2, resource: { buffer: this._packParams } },
|
|
749
|
+
],
|
|
750
|
+
} ) );
|
|
751
|
+
pass.dispatchWorkgroups( Math.ceil( input.width / PACK_WG_SIZE ), Math.ceil( input.height / PACK_WG_SIZE ) );
|
|
752
|
+
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
pass.end();
|
|
756
756
|
device.queue.submit( [ encoder.finish() ] );
|
|
757
757
|
|
|
758
758
|
}
|
|
@@ -760,7 +760,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
760
760
|
/**
|
|
761
761
|
* Creates or recreates the GPU storage buffers used as oidn-web inputs.
|
|
762
762
|
* Reuses existing buffers if the resolution hasn't changed.
|
|
763
|
-
* Usage: COPY_DST (for copyTextureToBuffer) | STORAGE (for oidn-web WGSL read) | COPY_SRC
|
|
764
763
|
*/
|
|
765
764
|
_ensureGPUInputBuffers( width, height ) {
|
|
766
765
|
|
|
@@ -771,7 +770,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
771
770
|
this._destroyGPUInputBuffers();
|
|
772
771
|
|
|
773
772
|
const device = this.gpuDevice;
|
|
774
|
-
const
|
|
773
|
+
const input = oidnInputSize( width, height );
|
|
774
|
+
const byteSize = input.width * input.height * 16; // rgba32float
|
|
775
775
|
const usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC;
|
|
776
776
|
|
|
777
777
|
this._gpuInputBuffers.color = device.createBuffer( { label: 'oidn-in-color', size: byteSize, usage } );
|
|
@@ -781,8 +781,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
781
781
|
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
-
/** Opens the compute pass that packs
|
|
785
|
-
_beginPack( device, encoder, width, height ) {
|
|
784
|
+
/** Opens the compute pass that packs a frame into its bordered buffer; one dispatch per texture follows. @private */
|
|
785
|
+
_beginPack( device, encoder, input, width, height ) {
|
|
786
786
|
|
|
787
787
|
if ( ! this._packPipeline ) {
|
|
788
788
|
|
|
@@ -803,12 +803,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
803
803
|
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
this._packParams = device.createBuffer( { label: 'oidn-pack-size', size: 8, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
|
|
809
|
-
device.queue.writeBuffer( this._packParams, 0, new Uint32Array( [ width, height ] ) );
|
|
810
|
-
|
|
811
|
-
}
|
|
806
|
+
this._packParams ??= device.createBuffer( { label: 'oidn-pack-size', size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
|
|
807
|
+
device.queue.writeBuffer( this._packParams, 0, new Uint32Array( [ input.width, input.height, width, height ] ) );
|
|
812
808
|
|
|
813
809
|
const pass = encoder.beginComputePass( { label: 'oidn-pack' } );
|
|
814
810
|
pass.setPipeline( this._packPipeline );
|
|
@@ -953,7 +949,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
953
949
|
}
|
|
954
950
|
|
|
955
951
|
/** Writes OIDN's autoexposure scale into `_inputScaleBuffer`. Never read back. */
|
|
956
|
-
_computeInputScale( device,
|
|
952
|
+
_computeInputScale( device, width, height ) {
|
|
957
953
|
|
|
958
954
|
if ( ! this._ensureScalePipelines( device ) ) return;
|
|
959
955
|
|
|
@@ -964,7 +960,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
964
960
|
|
|
965
961
|
}
|
|
966
962
|
|
|
967
|
-
|
|
963
|
+
const input = oidnInputSize( width, height );
|
|
964
|
+
device.queue.writeBuffer( this._lumParams, 0, new Uint32Array( [ width * height, width, input.width, OIDN_BORDER ] ) );
|
|
968
965
|
|
|
969
966
|
const encoder = device.createCommandEncoder( { label: 'oidn-autoexposure' } );
|
|
970
967
|
|
|
@@ -1078,12 +1075,11 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1078
1075
|
/**
|
|
1079
1076
|
* Writes a rectangle of the denoised buffer into the output picture, on the card.
|
|
1080
1077
|
*
|
|
1081
|
-
* @param {GPUBuffer} src - rgba32float,
|
|
1082
|
-
* @param {number}
|
|
1083
|
-
* @param {{x: number, y: number, width: number, height: number}} rect
|
|
1078
|
+
* @param {GPUBuffer} src - rgba32float, the bordered input size, scaled by the input autoexposure
|
|
1079
|
+
* @param {{x: number, y: number, width: number, height: number}} rect - in frame pixels
|
|
1084
1080
|
* @param {boolean} [denoised=true] - false for the raw render laid under a tiled run
|
|
1085
1081
|
*/
|
|
1086
|
-
_unpackToTexture( src,
|
|
1082
|
+
_unpackToTexture( src, rect, denoised = true ) {
|
|
1087
1083
|
|
|
1088
1084
|
const device = this.gpuDevice;
|
|
1089
1085
|
if ( ! device || ! this._ensureScalePipelines( device ) ) return;
|
|
@@ -1101,7 +1097,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1101
1097
|
|
|
1102
1098
|
try {
|
|
1103
1099
|
|
|
1104
|
-
|
|
1100
|
+
const srcWidth = oidnInputSize( this._gpuInputBufferSize.width, this._gpuInputBufferSize.height ).width;
|
|
1101
|
+
device.queue.writeBuffer( params, 0, new Uint32Array( [ srcWidth, x, y, width, height, OIDN_BORDER, 0, 0 ] ) );
|
|
1105
1102
|
|
|
1106
1103
|
const bindGroup = device.createBindGroup( {
|
|
1107
1104
|
label: 'oidn-output-unpack',
|
|
@@ -1174,13 +1171,23 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1174
1171
|
|
|
1175
1172
|
};
|
|
1176
1173
|
|
|
1177
|
-
this.state.abortController.signal
|
|
1174
|
+
const signal = this.state.abortController.signal;
|
|
1175
|
+
signal.addEventListener( 'abort', abortHandler, { once: true } );
|
|
1178
1176
|
|
|
1179
1177
|
abortDenoise = this.unet.tileExecute( {
|
|
1180
1178
|
...config,
|
|
1179
|
+
// Refreshes share the GPU with the render; the closing denoise has it to itself.
|
|
1180
|
+
scheduling: continuous ? 'animation-frame' : 'event-loop',
|
|
1181
|
+
error: ( reason ) => {
|
|
1182
|
+
|
|
1183
|
+
signal.removeEventListener( 'abort', abortHandler );
|
|
1184
|
+
abortDenoise = null;
|
|
1185
|
+
reject( reason );
|
|
1186
|
+
|
|
1187
|
+
},
|
|
1181
1188
|
done: async ( output ) => {
|
|
1182
1189
|
|
|
1183
|
-
|
|
1190
|
+
signal.removeEventListener( 'abort', abortHandler );
|
|
1184
1191
|
abortDenoise = null;
|
|
1185
1192
|
|
|
1186
1193
|
try {
|
|
@@ -1203,28 +1210,28 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1203
1210
|
progress: ( outputData, _tileData, tile ) => {
|
|
1204
1211
|
|
|
1205
1212
|
// oidn-web GPU path: tileData is null, but outputData holds the assembled
|
|
1206
|
-
// full-image buffer updated after each tile. The
|
|
1213
|
+
// full-image buffer updated after each tile. The unpack pass reads that tile's
|
|
1207
1214
|
// rectangle straight out of it, so there are no row-by-row buffer copies.
|
|
1208
1215
|
if ( ! outputData?.data || ! tile ) return;
|
|
1209
1216
|
|
|
1210
|
-
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const width = Math.min( tile.width,
|
|
1215
|
-
const height = Math.min( tile.height,
|
|
1217
|
+
// The tile in frame pixels, without the border.
|
|
1218
|
+
const { width: frameWidth, height: frameHeight } = this._gpuInputBufferSize;
|
|
1219
|
+
const x = Math.max( tile.x - OIDN_BORDER, 0 );
|
|
1220
|
+
const y = Math.max( tile.y - OIDN_BORDER, 0 );
|
|
1221
|
+
const width = Math.min( tile.x + tile.width - OIDN_BORDER, frameWidth ) - x;
|
|
1222
|
+
const height = Math.min( tile.y + tile.height - OIDN_BORDER, frameHeight ) - y;
|
|
1216
1223
|
if ( width <= 0 || height <= 0 ) return;
|
|
1217
1224
|
|
|
1218
|
-
const rect = { x
|
|
1225
|
+
const rect = { x, y, width, height };
|
|
1219
1226
|
|
|
1220
|
-
this._unpackToTexture( outputData.data,
|
|
1227
|
+
this._unpackToTexture( outputData.data, rect );
|
|
1221
1228
|
this._tilesWritten ++;
|
|
1222
1229
|
|
|
1223
1230
|
this.dispatchEvent( {
|
|
1224
1231
|
type: 'tileProgress',
|
|
1225
1232
|
tile: rect,
|
|
1226
|
-
imageWidth:
|
|
1227
|
-
imageHeight:
|
|
1233
|
+
imageWidth: frameWidth,
|
|
1234
|
+
imageHeight: frameHeight,
|
|
1228
1235
|
continuous
|
|
1229
1236
|
} );
|
|
1230
1237
|
|
|
@@ -1237,9 +1244,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1237
1244
|
|
|
1238
1245
|
/**
|
|
1239
1246
|
* Degenerate fallback when no per-tile progress was emitted: one authoritative full paint.
|
|
1240
|
-
* @param {{ data: GPUBuffer
|
|
1247
|
+
* @param {{ data: GPUBuffer }} output
|
|
1241
1248
|
*/
|
|
1242
|
-
_displayGPUOutput( { data: gpuBuffer
|
|
1249
|
+
_displayGPUOutput( { data: gpuBuffer } ) {
|
|
1243
1250
|
|
|
1244
1251
|
if ( ! this.gpuDevice ) {
|
|
1245
1252
|
|
|
@@ -1248,7 +1255,8 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
1248
1255
|
|
|
1249
1256
|
}
|
|
1250
1257
|
|
|
1251
|
-
|
|
1258
|
+
const { width, height } = this._gpuInputBufferSize;
|
|
1259
|
+
this._unpackToTexture( gpuBuffer, { x: 0, y: 0, width, height } );
|
|
1252
1260
|
|
|
1253
1261
|
}
|
|
1254
1262
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -252,7 +252,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
252
252
|
this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
|
|
253
253
|
|
|
254
254
|
this._forwardEvents( this.denoisingManager, [
|
|
255
|
-
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
|
|
255
|
+
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END, EngineEvents.DENOISER_LOADING,
|
|
256
256
|
EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
|
|
257
257
|
EngineEvents.RESOLUTION_CHANGED,
|
|
258
258
|
] );
|
|
@@ -844,7 +844,7 @@ export class PathTracerApp extends RayzeeRenderer {
|
|
|
844
844
|
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
-
// OIDN
|
|
847
|
+
// Live OIDN keeps full size: a resize drops its motion history and shows the raw render until it denoises again.
|
|
848
848
|
_interactionRenderScale() {
|
|
849
849
|
|
|
850
850
|
if ( this.denoisingManager?.continuousDenoise ) return 1;
|
|
@@ -14,6 +14,23 @@ import { RenderTarget, FloatType, RGBAFormat, NearestFilter } from 'three';
|
|
|
14
14
|
import { QuadMesh, NodeMaterial } from 'three/webgpu';
|
|
15
15
|
import { texture as tslTexture, screenCoordinate, ivec2 } from 'three/tsl';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* An RGBA render target's pixels without the 256-byte row padding WebGPU copies them with.
|
|
19
|
+
* @returns {Promise<TypedArray>} `width * height * 4`, top row first
|
|
20
|
+
*/
|
|
21
|
+
export async function readPixels( renderer, target, width, height, index = 0 ) {
|
|
22
|
+
|
|
23
|
+
const data = await renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, index );
|
|
24
|
+
const row = width * 4;
|
|
25
|
+
const stride = Math.ceil( row * data.BYTES_PER_ELEMENT / 256 ) * 256 / data.BYTES_PER_ELEMENT;
|
|
26
|
+
if ( stride === row ) return data;
|
|
27
|
+
|
|
28
|
+
const tight = new data.constructor( row * height );
|
|
29
|
+
for ( let y = 0; y < height; y ++ ) tight.set( data.subarray( y * stride, y * stride + row ), y * row );
|
|
30
|
+
return tight;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
export class TextureReadback {
|
|
18
35
|
|
|
19
36
|
constructor( renderer ) {
|
|
@@ -48,7 +65,7 @@ export class TextureReadback {
|
|
|
48
65
|
|
|
49
66
|
try {
|
|
50
67
|
|
|
51
|
-
return await this.renderer
|
|
68
|
+
return await readPixels( this.renderer, this.draw( source, width, height ), width, height );
|
|
52
69
|
|
|
53
70
|
} finally {
|
|
54
71
|
|
|
@@ -275,8 +275,9 @@ export class PackedToneMapper {
|
|
|
275
275
|
* @param {number} [tone.saturation=1]
|
|
276
276
|
* @param {boolean} [tone.flipY=false]
|
|
277
277
|
* @param {boolean} [tone.preserveAlpha=false] - the source's alpha, rounded as the CPU rounds it; else 255
|
|
278
|
-
* @param {{key: string, wgsl: string, entries: GPUBindGroupEntry[]}} [tone.gain] - a
|
|
279
|
-
* curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1
|
|
278
|
+
* @param {{key: string, wgsl: string, entries: GPUBindGroupEntry[], device?: GPUDevice, copyTo?: Function}} [tone.gain] - a
|
|
279
|
+
* per-pixel gain before the curve: WGSL defining `rayzee_gain( uv, linear, exposure ) -> f32` over bind group 1.
|
|
280
|
+
* One on another device is copied to this one for the run (`copyTo`).
|
|
280
281
|
* @returns {Promise<Uint8ClampedArray>} RGBA bytes, `width * height * 4`
|
|
281
282
|
*/
|
|
282
283
|
async toRGBA8( src, tone ) {
|
|
@@ -298,7 +299,25 @@ export class PackedToneMapper {
|
|
|
298
299
|
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
async _run( src,
|
|
302
|
+
async _run( src, tone = {} ) {
|
|
303
|
+
|
|
304
|
+
const gain = tone.gain;
|
|
305
|
+
if ( ! gain?.device || gain.device === this.device ) return this._dispatch( src, tone );
|
|
306
|
+
|
|
307
|
+
const copy = await gain.copyTo( this.device );
|
|
308
|
+
try {
|
|
309
|
+
|
|
310
|
+
return await this._dispatch( src, { ...tone, gain: copy } );
|
|
311
|
+
|
|
312
|
+
} finally {
|
|
313
|
+
|
|
314
|
+
copy.destroy();
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async _dispatch( src, { exposure = 1, toneMapping = 0, saturation = 1, flipY = false, preserveAlpha = false, gain = null } = {} ) {
|
|
302
321
|
|
|
303
322
|
if ( this.disposed ) throw new Error( 'PackedToneMapper: disposed' );
|
|
304
323
|
if ( ! this._storage ) throw new Error( 'PackedToneMapper: call ensureSize() first' );
|
package/src/RayzeeRenderer.js
CHANGED
|
@@ -15,7 +15,7 @@ import { usesTextureCoordinates } from './Processor/GeometryExtractor.js';
|
|
|
15
15
|
import { MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize } from './Processor/StorageTexturePool.js';
|
|
16
16
|
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
|
|
17
17
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
18
|
-
import { TextureReadback } from './Processor/TextureReadback.js';
|
|
18
|
+
import { TextureReadback, readPixels } from './Processor/TextureReadback.js';
|
|
19
19
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
20
20
|
import { EngineEvents, LEGACY_EVENT_NAMES } from './EngineEvents.js';
|
|
21
21
|
import { IssueLog, ISSUE_CODES, EngineIssueError } from './EngineIssues.js';
|
|
@@ -2766,14 +2766,14 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
2766
2766
|
const fn = ! gpu && tone.gain ? await tone.gain.cpu() : null;
|
|
2767
2767
|
const data = gpu ?? toneMapToRGBA8( shown
|
|
2768
2768
|
? await this._readTexture( shown.texture, width, height )
|
|
2769
|
-
: await this.renderer
|
|
2769
|
+
: await readPixels( this.renderer, target, width, height ), { ...tone, pixelGain: fn && { fn, width, height } } );
|
|
2770
2770
|
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation', toneMappedOn: gpu ? 'gpu' : 'cpu' };
|
|
2771
2771
|
|
|
2772
2772
|
}
|
|
2773
2773
|
|
|
2774
2774
|
const { data: linear, source: read } = source === 'display'
|
|
2775
2775
|
? await this._readDisplaySource( target, width, height )
|
|
2776
|
-
: { data: await this.renderer
|
|
2776
|
+
: { data: await readPixels( this.renderer, target, width, height ), source: 'accumulation' };
|
|
2777
2777
|
|
|
2778
2778
|
// `colorSpace` stays 'linear' — callers branch on it. `workingSpace` is the new, additive
|
|
2779
2779
|
// answer to "linear in what primaries", which only means something once a config is loaded.
|
|
@@ -3450,7 +3450,7 @@ export class RayzeeRenderer extends EventDispatcher {
|
|
|
3450
3450
|
async _readDisplaySource( accumulation, width, height ) {
|
|
3451
3451
|
|
|
3452
3452
|
const shown = this._displaySource( accumulation );
|
|
3453
|
-
if ( ! shown ) return { data: await this.renderer
|
|
3453
|
+
if ( ! shown ) return { data: await readPixels( this.renderer, accumulation, width, height ), source: 'accumulation' };
|
|
3454
3454
|
return { data: await this._readTexture( shown.texture, width, height ), source: shown.source };
|
|
3455
3455
|
|
|
3456
3456
|
}
|