rayzee 7.8.0 → 7.9.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 -1
- package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js → BVHSubtreeWorker-sNzvxn66.js} +2 -2
- package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js.map → BVHSubtreeWorker-sNzvxn66.js.map} +1 -1
- package/dist/assets/{BVHWorker-CNJ0UBQz.js → BVHWorker-CiVdFrwe.js} +2 -2
- package/dist/assets/{BVHWorker-CNJ0UBQz.js.map → BVHWorker-CiVdFrwe.js.map} +1 -1
- package/dist/rayzee.es.js +714 -693
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +21 -21
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineEvents.js +1 -0
- package/src/Passes/OIDNDenoiser.js +25 -1
- package/src/PathTracerApp.js +36 -0
- package/src/Processor/AssetLoader.js +3 -0
- package/src/Processor/GeometryExtractor.js +35 -9
- package/src/Processor/PackedRayBuffer.js +15 -1
- package/src/Processor/QueueManager.js +13 -1
- package/src/Processor/SceneProcessor.js +11 -1
- package/src/Processor/TextureCreator.js +16 -32
- package/src/Processor/Workers/BVHWorker.js +6 -1
- package/src/Stages/EdgeFilter.js +4 -3
- package/src/Stages/MotionVector.js +4 -4
- package/src/Stages/NormalDepth.js +4 -3
- package/src/Stages/PathTracer.js +4 -3
- package/src/TSL/GenerateKernel.js +5 -1
- package/src/TSL/LightsIndirect.js +15 -9
- package/src/TSL/LightsSampling.js +9 -19
- package/src/TSL/Random.js +11 -1
- package/src/managers/EnvironmentManager.js +11 -0
- package/src/managers/RenderTargetManager.js +0 -522
package/package.json
CHANGED
package/src/EngineEvents.js
CHANGED
|
@@ -142,6 +142,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
142
142
|
|
|
143
143
|
this.currentTZAUrl = null;
|
|
144
144
|
this.unet = null;
|
|
145
|
+
// A start() requested while the UNet is still loading is deferred here and fired
|
|
146
|
+
// once loading finishes, instead of being silently dropped.
|
|
147
|
+
this._pendingStart = false;
|
|
145
148
|
|
|
146
149
|
// Initialize asynchronously
|
|
147
150
|
this._initialize().catch( error => {
|
|
@@ -257,6 +260,15 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
257
260
|
|
|
258
261
|
this.state.isLoading = false;
|
|
259
262
|
|
|
263
|
+
// Fire a start() that arrived mid-load. Guard on unet+enabled so a failed load
|
|
264
|
+
// or a disable during loading doesn't kick off a denoise.
|
|
265
|
+
if ( this._pendingStart && this.unet && this.enabled ) {
|
|
266
|
+
|
|
267
|
+
this._pendingStart = false;
|
|
268
|
+
this.start();
|
|
269
|
+
|
|
270
|
+
}
|
|
271
|
+
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
}
|
|
@@ -302,8 +314,17 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
302
314
|
|
|
303
315
|
async start() {
|
|
304
316
|
|
|
305
|
-
if ( ! this.enabled || this.state.isDenoising
|
|
317
|
+
if ( ! this.enabled || this.state.isDenoising ) {
|
|
318
|
+
|
|
319
|
+
return false;
|
|
320
|
+
|
|
321
|
+
}
|
|
306
322
|
|
|
323
|
+
// UNet weights still loading — defer the start (fired from _setupUNetDenoiser's
|
|
324
|
+
// finally) rather than silently dropping this frame's denoise request.
|
|
325
|
+
if ( this.state.isLoading ) {
|
|
326
|
+
|
|
327
|
+
this._pendingStart = true;
|
|
307
328
|
return false;
|
|
308
329
|
|
|
309
330
|
}
|
|
@@ -870,6 +891,9 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
870
891
|
|
|
871
892
|
abort() {
|
|
872
893
|
|
|
894
|
+
// Cancel any start deferred during loading so a reset supersedes it.
|
|
895
|
+
this._pendingStart = false;
|
|
896
|
+
|
|
873
897
|
if ( ! this.enabled || ! this.state.isDenoising ) return;
|
|
874
898
|
|
|
875
899
|
// Signal abort to current operation
|
package/src/PathTracerApp.js
CHANGED
|
@@ -157,6 +157,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
157
157
|
// Tracked listeners for clean dispose()
|
|
158
158
|
this._trackedListeners = [];
|
|
159
159
|
this._disposed = false;
|
|
160
|
+
this._deviceLost = false;
|
|
160
161
|
|
|
161
162
|
}
|
|
162
163
|
|
|
@@ -230,6 +231,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
230
231
|
*/
|
|
231
232
|
animate() {
|
|
232
233
|
|
|
234
|
+
// Device lost: stop the loop rather than rescheduling render() on a dead device.
|
|
235
|
+
if ( this._deviceLost ) return;
|
|
236
|
+
|
|
233
237
|
this.animationManagerId = requestAnimationFrame( () => this.animate() );
|
|
234
238
|
|
|
235
239
|
if ( this._loadingInProgress || this._sdf?.isProcessing ) {
|
|
@@ -368,9 +372,25 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
368
372
|
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Handle GPU device loss: halt the render loop and notify hosts so they can surface a
|
|
377
|
+
* "renderer lost — reload" prompt. Full auto-recovery would require rebuilding every GPU
|
|
378
|
+
* resource, so this deliberately stops cleanly rather than attempting to re-init.
|
|
379
|
+
*/
|
|
380
|
+
_handleDeviceLost( info ) {
|
|
381
|
+
|
|
382
|
+
if ( this._deviceLost ) return;
|
|
383
|
+
this._deviceLost = true;
|
|
384
|
+
console.error( `WebGPU device lost (${info?.reason || 'unknown'}): ${info?.message || ''}` );
|
|
385
|
+
this.stopAnimation();
|
|
386
|
+
this.dispatchEvent( { type: EngineEvents.DEVICE_LOST, reason: info?.reason, message: info?.message } );
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
|
|
371
390
|
/** Wakes the animation loop if it was stopped due to idle. */
|
|
372
391
|
wake() {
|
|
373
392
|
|
|
393
|
+
if ( this._deviceLost ) return;
|
|
374
394
|
if ( ! this.animationManagerId && this.isInitialized && ! this._paused ) this.animate();
|
|
375
395
|
|
|
376
396
|
}
|
|
@@ -1385,6 +1405,22 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1385
1405
|
|
|
1386
1406
|
await this.renderer.init();
|
|
1387
1407
|
|
|
1408
|
+
// Detect GPU device loss (dGPU/iGPU switch, driver reset, TDR watchdog on heavy
|
|
1409
|
+
// compute). Without this the rAF loop keeps calling render() on a dead device,
|
|
1410
|
+
// spewing errors forever. reason 'destroyed' during dispose() is intentional teardown.
|
|
1411
|
+
const gpuDevice = this.renderer.backend?.device;
|
|
1412
|
+
if ( gpuDevice?.lost ) {
|
|
1413
|
+
|
|
1414
|
+
gpuDevice.lost.then( ( info ) => {
|
|
1415
|
+
|
|
1416
|
+
if ( this._disposed ) return;
|
|
1417
|
+
this._handleDeviceLost( info );
|
|
1418
|
+
|
|
1419
|
+
} );
|
|
1420
|
+
gpuDevice.onuncapturederror = ( event ) => console.error( 'WebGPU uncaptured error:', event.error );
|
|
1421
|
+
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1388
1424
|
RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
|
|
1389
1425
|
|
|
1390
1426
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
@@ -292,6 +292,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
292
292
|
|
|
293
293
|
if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader();
|
|
294
294
|
texture = await this.loaderCache.texture.loadAsync( url );
|
|
295
|
+
// LDR env maps (jpg/png/webp) are authored in sRGB; tag them so the backend
|
|
296
|
+
// decodes to linear. HDR/EXR are already linear and keep the loader's setting.
|
|
297
|
+
texture.colorSpace = SRGBColorSpace;
|
|
295
298
|
|
|
296
299
|
}
|
|
297
300
|
|
|
@@ -45,8 +45,12 @@ export class GeometryExtractor {
|
|
|
45
45
|
|
|
46
46
|
this.resetArrays();
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
|
|
48
|
+
// Pre-count the exact triangle total so the buffer is allocated once.
|
|
49
|
+
// Doubling-with-copy rounded up to the next power of two (wasting ~1GB on
|
|
50
|
+
// an 8.5M-tri scene) and held old+new during the copy (~3GB transient),
|
|
51
|
+
// which overflowed the browser's ArrayBuffer allocator. Exact avoids both.
|
|
52
|
+
const totalTriangles = this._countTriangles( object );
|
|
53
|
+
this._triangleCapacity = Math.max( 1024, totalTriangles );
|
|
50
54
|
this.triangleData = new Float32Array( this._triangleCapacity * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
|
|
51
55
|
this.currentTriangleIndex = 0;
|
|
52
56
|
|
|
@@ -58,19 +62,41 @@ export class GeometryExtractor {
|
|
|
58
62
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
//
|
|
65
|
+
// Sum the exact triangle count over every mesh extract() will process
|
|
66
|
+
// (mirrors traverseObject/processMesh guards) so we can allocate once.
|
|
67
|
+
_countTriangles( object ) {
|
|
68
|
+
|
|
69
|
+
let count = 0;
|
|
70
|
+
|
|
71
|
+
if ( object.isMesh && object.geometry && object.material ) {
|
|
72
|
+
|
|
73
|
+
const indices = object.geometry.index;
|
|
74
|
+
const positions = object.geometry.attributes.position;
|
|
75
|
+
if ( indices ) count += Math.ceil( indices.count / 3 );
|
|
76
|
+
else if ( positions ) count += Math.ceil( positions.count / 3 );
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if ( object.children ) {
|
|
81
|
+
|
|
82
|
+
for ( const child of object.children ) count += this._countTriangles( child );
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return count;
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Safety net only: extract() pre-counts and allocates exactly, so this should
|
|
91
|
+
// not fire. Grow to exactly `needed` (never double) to avoid a memory spike.
|
|
62
92
|
_ensureCapacity( needed ) {
|
|
63
93
|
|
|
64
94
|
if ( needed <= this._triangleCapacity ) return;
|
|
65
95
|
|
|
66
|
-
|
|
67
|
-
let newCapacity = this._triangleCapacity;
|
|
68
|
-
while ( newCapacity < needed ) newCapacity *= 2;
|
|
69
|
-
|
|
70
|
-
const newData = new Float32Array( newCapacity * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
|
|
96
|
+
const newData = new Float32Array( needed * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
|
|
71
97
|
newData.set( this.triangleData );
|
|
72
98
|
this.triangleData = newData;
|
|
73
|
-
this._triangleCapacity =
|
|
99
|
+
this._triangleCapacity = needed;
|
|
74
100
|
|
|
75
101
|
}
|
|
76
102
|
|
|
@@ -38,6 +38,16 @@ let _cap = 0;
|
|
|
38
38
|
|
|
39
39
|
const soa = ( id, slot ) => ( slot === 0 ? id : id.add( slot * _cap ) );
|
|
40
40
|
|
|
41
|
+
// Free a compute storage attribute's GPU buffer. Nulling the JS reference alone leaks it —
|
|
42
|
+
// these attributes hang off no geometry, so nothing triggers the renderer's attribute cleanup.
|
|
43
|
+
// Deleting from the renderer's attribute map calls backend.destroyAttribute(). Guarded no-op
|
|
44
|
+
// when the attribute was never uploaded or no renderer was provided (e.g. unit tests).
|
|
45
|
+
export function freeStorageAttribute( renderer, attr ) {
|
|
46
|
+
|
|
47
|
+
if ( attr ) renderer?._attributes?.delete?.( attr );
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
export class PackedRayBuffer {
|
|
42
52
|
|
|
43
53
|
// Capacity maxRays would allocate (mirrors allocate()/resize()). 1.25× headroom, NO pow2 rounding —
|
|
@@ -49,9 +59,10 @@ export class PackedRayBuffer {
|
|
|
49
59
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
constructor( maxRays = 0 ) {
|
|
62
|
+
constructor( maxRays = 0, renderer = null ) {
|
|
53
63
|
|
|
54
64
|
this.capacity = 0;
|
|
65
|
+
this._renderer = renderer;
|
|
55
66
|
this._attrs = {};
|
|
56
67
|
|
|
57
68
|
// Each: { rw: StorageBufferNode, ro: StorageBufferNode } over one shared GPU buffer.
|
|
@@ -118,6 +129,9 @@ export class PackedRayBuffer {
|
|
|
118
129
|
|
|
119
130
|
dispose() {
|
|
120
131
|
|
|
132
|
+
freeStorageAttribute( this._renderer, this._attrs.ray );
|
|
133
|
+
freeStorageAttribute( this._renderer, this._attrs.rng );
|
|
134
|
+
freeStorageAttribute( this._renderer, this._attrs.hit );
|
|
121
135
|
this._attrs = {};
|
|
122
136
|
this.rayBuffer = null;
|
|
123
137
|
this.rngBuffer = null;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { storage } from 'three/tsl';
|
|
6
6
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
7
|
+
import { freeStorageAttribute } from './PackedRayBuffer.js';
|
|
7
8
|
|
|
8
9
|
/** Counter indices — must match ResetCounters kernel */
|
|
9
10
|
export const COUNTER = {
|
|
@@ -33,8 +34,9 @@ export class QueueManager {
|
|
|
33
34
|
/**
|
|
34
35
|
* @param {number} maxRays - Maximum number of rays (typically width * height)
|
|
35
36
|
*/
|
|
36
|
-
constructor( maxRays = 0 ) {
|
|
37
|
+
constructor( maxRays = 0, renderer = null ) {
|
|
37
38
|
|
|
39
|
+
this._renderer = renderer;
|
|
38
40
|
this.capacity = 0;
|
|
39
41
|
this.counters = null;
|
|
40
42
|
// A/B alternate: one read by current bounce, other written by compaction
|
|
@@ -197,6 +199,16 @@ export class QueueManager {
|
|
|
197
199
|
|
|
198
200
|
dispose() {
|
|
199
201
|
|
|
202
|
+
// Free the GPU buffers before dropping the node references (nulling alone leaks them).
|
|
203
|
+
freeStorageAttribute( this._renderer, this._countersAttr );
|
|
204
|
+
freeStorageAttribute( this._renderer, this._bounceCountsAttr );
|
|
205
|
+
freeStorageAttribute( this._renderer, this._attrA );
|
|
206
|
+
freeStorageAttribute( this._renderer, this._attrB );
|
|
207
|
+
freeStorageAttribute( this._renderer, this._sortAttr );
|
|
208
|
+
freeStorageAttribute( this._renderer, this._sortGlobalHistAttr );
|
|
209
|
+
this._countersAttr = this._bounceCountsAttr = null;
|
|
210
|
+
this._attrA = this._attrB = this._sortAttr = this._sortGlobalHistAttr = null;
|
|
211
|
+
|
|
200
212
|
this.counters = null;
|
|
201
213
|
this.activeIndices = null;
|
|
202
214
|
this.activeIndicesRO = null;
|
|
@@ -422,7 +422,13 @@ export class SceneProcessor {
|
|
|
422
422
|
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
423
423
|
batchSizeRatio: this.bvhBuilder.reinsertionBatchSizeRatio,
|
|
424
424
|
maxIterations: this.bvhBuilder.reinsertionMaxIterations
|
|
425
|
-
}
|
|
425
|
+
},
|
|
426
|
+
// BVH build params — previously omitted, so the pool path built at the
|
|
427
|
+
// BVHBuilder default (leaf 8) instead of the configured value.
|
|
428
|
+
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
429
|
+
numBins: this.bvhBuilder.numBins,
|
|
430
|
+
maxBins: this.bvhBuilder.maxBins,
|
|
431
|
+
minBins: this.bvhBuilder.minBins,
|
|
426
432
|
};
|
|
427
433
|
|
|
428
434
|
const totalTasks = poolTasks.length + parallelTasks.length;
|
|
@@ -630,6 +636,10 @@ export class SceneProcessor {
|
|
|
630
636
|
sharedReorderBuffer: null,
|
|
631
637
|
treeletOptimization: treeletOpts,
|
|
632
638
|
reinsertionOptimization: opts.reinsertionOptimization,
|
|
639
|
+
maxLeafSize: opts.maxLeafSize,
|
|
640
|
+
numBins: opts.numBins,
|
|
641
|
+
maxBins: opts.maxBins,
|
|
642
|
+
minBins: opts.minBins,
|
|
633
643
|
}, [ meshTriData.buffer ] );
|
|
634
644
|
|
|
635
645
|
};
|
|
@@ -537,9 +537,6 @@ export class TextureCreator {
|
|
|
537
537
|
case 'worker-direct':
|
|
538
538
|
result = await this.processWithWorkerDirect( normalized );
|
|
539
539
|
break;
|
|
540
|
-
case 'worker-chunked':
|
|
541
|
-
result = await this.processWithWorkerChunked( normalized, strategy.chunkSize );
|
|
542
|
-
break;
|
|
543
540
|
case 'main-batch':
|
|
544
541
|
result = await this.processOnMainThreadBatch( normalized, strategy.batchSize );
|
|
545
542
|
break;
|
|
@@ -587,10 +584,10 @@ export class TextureCreator {
|
|
|
587
584
|
|
|
588
585
|
if ( this.capabilities.workers && estimatedMemory > MEMORY_CONSTANTS.MAX_TEXTURE_MEMORY ) {
|
|
589
586
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
};
|
|
587
|
+
// Very large texture set: stream on the main thread (GC-yielding, builds the
|
|
588
|
+
// full array correctly). The former 'worker-chunked' path combined its chunks
|
|
589
|
+
// by returning only the first, silently dropping the rest — removed.
|
|
590
|
+
return { method: 'main-streaming' };
|
|
594
591
|
|
|
595
592
|
} else if ( this.capabilities.workers && totalPixels > 2097152 ) {
|
|
596
593
|
|
|
@@ -710,8 +707,18 @@ export class TextureCreator {
|
|
|
710
707
|
|
|
711
708
|
try {
|
|
712
709
|
|
|
713
|
-
// Option 1: Direct ImageBitmap transfer (when supported)
|
|
714
|
-
|
|
710
|
+
// Option 1: Direct ImageBitmap transfer (when supported). Covers HTMLImageElement
|
|
711
|
+
// AND ImageBitmap (what GLTFLoader's ImageBitmapLoader produces) and canvases — the
|
|
712
|
+
// worker extracts pixels on its OffscreenCanvas, so this keeps the full-res
|
|
713
|
+
// getImageData off the main thread (Option 2 blocks it per texture).
|
|
714
|
+
const img = texture.image;
|
|
715
|
+
const canDirect = typeof createImageBitmap !== 'undefined' && (
|
|
716
|
+
img instanceof HTMLImageElement
|
|
717
|
+
|| ( typeof ImageBitmap !== 'undefined' && img instanceof ImageBitmap )
|
|
718
|
+
|| ( typeof HTMLCanvasElement !== 'undefined' && img instanceof HTMLCanvasElement )
|
|
719
|
+
|| ( typeof OffscreenCanvas !== 'undefined' && img instanceof OffscreenCanvas )
|
|
720
|
+
);
|
|
721
|
+
if ( canDirect ) {
|
|
715
722
|
|
|
716
723
|
const bitmap = await createImageBitmap( texture.image, {
|
|
717
724
|
imageOrientation: flipY ? 'flipY' : 'none',
|
|
@@ -760,21 +767,6 @@ export class TextureCreator {
|
|
|
760
767
|
|
|
761
768
|
}
|
|
762
769
|
|
|
763
|
-
async processWithWorkerChunked( textures, chunkSize ) {
|
|
764
|
-
|
|
765
|
-
const results = [];
|
|
766
|
-
for ( let i = 0; i < textures.length; i += chunkSize ) {
|
|
767
|
-
|
|
768
|
-
const chunk = textures.slice( i, i + chunkSize );
|
|
769
|
-
const chunkResult = await this.processWithWorkerDirect( chunk );
|
|
770
|
-
results.push( chunkResult );
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
return this.combineTextureResults( results );
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
|
|
778
770
|
async processOnMainThreadBatch( textures, batchSize ) {
|
|
779
771
|
|
|
780
772
|
const validTextures = textures.filter( tex => tex?.image );
|
|
@@ -1451,14 +1443,6 @@ export class TextureCreator {
|
|
|
1451
1443
|
|
|
1452
1444
|
}
|
|
1453
1445
|
|
|
1454
|
-
combineTextureResults( results ) {
|
|
1455
|
-
|
|
1456
|
-
// Combine multiple texture array results into one
|
|
1457
|
-
// This is a simplified implementation - you may need more sophisticated merging
|
|
1458
|
-
return results[ 0 ]; // For now, return first result
|
|
1459
|
-
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
1446
|
dispose() {
|
|
1463
1447
|
|
|
1464
1448
|
this.canvasPool.dispose();
|
|
@@ -184,8 +184,13 @@ function handleAssemble( data ) {
|
|
|
184
184
|
|
|
185
185
|
function handleFullBuild( data ) {
|
|
186
186
|
|
|
187
|
-
const { triangleData, triangleByteOffset, triangleByteLength, depth, reportProgress, treeletOptimization, reinsertionOptimization, sharedReorderBuffer } = data;
|
|
187
|
+
const { triangleData, triangleByteOffset, triangleByteLength, depth, reportProgress, treeletOptimization, reinsertionOptimization, sharedReorderBuffer, maxLeafSize, numBins, maxBins, minBins } = data;
|
|
188
188
|
const builder = new BVHBuilder();
|
|
189
|
+
// Honor the build params forwarded from SceneProcessor (previously ignored → default leaf 8).
|
|
190
|
+
if ( maxLeafSize !== undefined ) builder.maxLeafSize = maxLeafSize;
|
|
191
|
+
if ( numBins !== undefined ) builder.numBins = numBins;
|
|
192
|
+
if ( maxBins !== undefined ) builder.maxBins = maxBins;
|
|
193
|
+
if ( minBins !== undefined ) builder.minBins = minBins;
|
|
189
194
|
|
|
190
195
|
try {
|
|
191
196
|
|
package/src/Stages/EdgeFilter.js
CHANGED
|
@@ -105,10 +105,11 @@ export class EdgeFilter extends RenderStage {
|
|
|
105
105
|
const center = textureLoad( inputTex, coord ).xyz;
|
|
106
106
|
const centerLum = dot( center, REC709_LUMINANCE_COEFFICIENTS );
|
|
107
107
|
|
|
108
|
-
// NormalDepth writes (0,0,0,
|
|
108
|
+
// NormalDepth writes (0,0,0, 65504) for miss rays. Decoded normal
|
|
109
109
|
// (-1,-1,-1) is non-unit and explodes pow(dot, phi); use the depth
|
|
110
|
-
// sentinel as a 0/1 hit flag and zero out cross-kind weights.
|
|
111
|
-
|
|
110
|
+
// sentinel as a 0/1 hit flag and zero out cross-kind weights. Threshold
|
|
111
|
+
// sits below the 65504 sentinel (and above any real depth).
|
|
112
|
+
const MISS_THRESHOLD = 6e4;
|
|
112
113
|
const centerND = textureLoad( ndTex, coord );
|
|
113
114
|
const centerNormal = centerND.xyz.mul( 2.0 ).sub( 1.0 );
|
|
114
115
|
const centerDepth = centerND.w;
|
|
@@ -172,8 +172,8 @@ export class MotionVector extends RenderStage {
|
|
|
172
172
|
|
|
173
173
|
const result = vec4( 0.0, 0.0, linearDepth, 1.0 ).toVar();
|
|
174
174
|
|
|
175
|
-
// Sky / background (depth >=
|
|
176
|
-
If( linearDepth.lessThan( float(
|
|
175
|
+
// Sky / background (depth >= 65504 sentinel) — no motion
|
|
176
|
+
If( linearDepth.lessThan( float( 6e4 ) ), () => {
|
|
177
177
|
|
|
178
178
|
// Pixel coordinate → NDC
|
|
179
179
|
// Negate Y to match PathTracer convention
|
|
@@ -275,8 +275,8 @@ export class MotionVector extends RenderStage {
|
|
|
275
275
|
|
|
276
276
|
const result = vec4( 0.0, 0.0, 0.0, 0.0 ).toVar();
|
|
277
277
|
|
|
278
|
-
// Skip first frame and sky
|
|
279
|
-
If( isFirstFrameU.lessThan( 0.5 ).and( linearDepth.lessThan( float(
|
|
278
|
+
// Skip first frame and sky (depth >= 65504 sentinel)
|
|
279
|
+
If( isFirstFrameU.lessThan( 0.5 ).and( linearDepth.lessThan( float( 6e4 ) ) ), () => {
|
|
280
280
|
|
|
281
281
|
// Pixel coordinate → NDC
|
|
282
282
|
// Negate Y to match PathTracer convention
|
|
@@ -12,7 +12,8 @@ import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, re
|
|
|
12
12
|
/**
|
|
13
13
|
* NormalDepth — primary-ray G-buffer for SVGF gates.
|
|
14
14
|
*
|
|
15
|
-
* RGB = geometric world normal · 0.5 + 0.5, A = linear ray distance (sky=
|
|
15
|
+
* RGB = geometric world normal · 0.5 + 0.5, A = linear ray distance (sky=65504, the
|
|
16
|
+
* max HalfFloat value — a finite sentinel so miss−miss depth diffs stay 0, not Inf−Inf=NaN).
|
|
16
17
|
* Geometric (not shading) normals because shading normals carry sub-pixel
|
|
17
18
|
* jitter that breaks the temporal gate's same-pixel-across-frames comparison.
|
|
18
19
|
* The path tracer's MRT already carries shading normals for OIDN; this stage
|
|
@@ -237,7 +238,7 @@ export class NormalDepth extends RenderStage {
|
|
|
237
238
|
|
|
238
239
|
const result = hit.didHit.select(
|
|
239
240
|
vec4( encodedNormal, depth ),
|
|
240
|
-
vec4( 0.0, 0.0, 0.0, float(
|
|
241
|
+
vec4( 0.0, 0.0, 0.0, float( 65504.0 ) )
|
|
241
242
|
);
|
|
242
243
|
|
|
243
244
|
textureStore(
|
|
@@ -262,7 +263,7 @@ export class NormalDepth extends RenderStage {
|
|
|
262
263
|
|
|
263
264
|
const shadingResult = hit.didHit.select(
|
|
264
265
|
vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), depth ),
|
|
265
|
-
vec4( 0.0, 0.0, 0.0, float(
|
|
266
|
+
vec4( 0.0, 0.0, 0.0, float( 65504.0 ) )
|
|
266
267
|
);
|
|
267
268
|
|
|
268
269
|
textureStore(
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { uniform, texture, storage } from 'three/tsl';
|
|
8
8
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
9
9
|
import { PathTracerStage } from './PathTracerStage.js';
|
|
10
|
-
import { PackedRayBuffer, GBUFFER_STRIDE } from '../Processor/PackedRayBuffer.js';
|
|
10
|
+
import { PackedRayBuffer, GBUFFER_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
11
11
|
import { QueueManager, COUNTER } from '../Processor/QueueManager.js';
|
|
12
12
|
import { VRAMTracker } from '../Processor/VRAMTracker.js';
|
|
13
13
|
import { KernelManager } from '../Processor/KernelManager.js';
|
|
@@ -524,7 +524,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
524
524
|
|
|
525
525
|
if ( ! this._packedBuffers ) {
|
|
526
526
|
|
|
527
|
-
this._packedBuffers = new PackedRayBuffer( maxRays );
|
|
527
|
+
this._packedBuffers = new PackedRayBuffer( maxRays, this.renderer );
|
|
528
528
|
|
|
529
529
|
} else {
|
|
530
530
|
|
|
@@ -538,13 +538,14 @@ export class PathTracer extends PathTracerStage {
|
|
|
538
538
|
// per-pixel, written by Generate/Shade bounce-0 and read only by FinalWrite.
|
|
539
539
|
// 1.25× margin (same as the per-ray buffers) so it survives the in-place-resize range.
|
|
540
540
|
const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRays ) * GBUFFER_STRIDE;
|
|
541
|
+
freeStorageAttribute( this.renderer, this._gBufferAttr ); // free the outgoing G-buffer on capacity growth
|
|
541
542
|
this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
|
|
542
543
|
const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
|
|
543
544
|
const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
|
|
544
545
|
|
|
545
546
|
if ( ! this._queueManager ) {
|
|
546
547
|
|
|
547
|
-
this._queueManager = new QueueManager( this._packedBuffers.capacity );
|
|
548
|
+
this._queueManager = new QueueManager( this._packedBuffers.capacity, this.renderer );
|
|
548
549
|
|
|
549
550
|
} else {
|
|
550
551
|
|
|
@@ -58,7 +58,11 @@ export function buildGenerateKernel( params ) {
|
|
|
58
58
|
const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
|
|
59
59
|
const seed = pcgHash( { state: baseSeed } ).toVar();
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// Sample index 1 (not 0) so the AA sub-pixel jitter draws a DIFFERENT STBN cell
|
|
62
|
+
// than the first-bounce BSDF sample (ShadeKernel uses sampleIndex 0). Every bounce
|
|
63
|
+
// samples at index 0, so index 1 is collision-free — this decorrelates the sub-pixel
|
|
64
|
+
// position from the first scatter direction (they were reading the identical cell).
|
|
65
|
+
const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
|
|
62
66
|
|
|
63
67
|
const jitterScale = vec2( 2.0 ).div( resolution );
|
|
64
68
|
const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
|
|
@@ -290,6 +290,9 @@ export const calculateIndirectLighting = Fn( ( [
|
|
|
290
290
|
const sampleDir = vec3( 0.0 ).toVar();
|
|
291
291
|
const samplePdf = float( 0.0 ).toVar();
|
|
292
292
|
const sampleBrdfValue = vec3( 0.0 ).toVar();
|
|
293
|
+
// Transmission lobe uses this weight instead of a reflection-BRDF eval, which is
|
|
294
|
+
// invalid for the below-surface refraction direction.
|
|
295
|
+
const transColorWeight = vec3( 1.0 ).toVar();
|
|
293
296
|
|
|
294
297
|
// Execute selected strategy (chained If/ElseIf/Else for exclusive branches)
|
|
295
298
|
// Environment removed — handled via deterministic NEE in direct lighting
|
|
@@ -312,13 +315,16 @@ export const calculateIndirectLighting = Fn( ( [
|
|
|
312
315
|
|
|
313
316
|
// Strategy 3: Transmission
|
|
314
317
|
const entering = dot( V, N ).greaterThan( 0.0 );
|
|
315
|
-
// pathWavelength=0 — MIS evaluation reads only direction/PDF, no spectral tint
|
|
316
318
|
const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
|
|
317
319
|
V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState, float( 0.0 )
|
|
318
320
|
).toVar() );
|
|
319
321
|
sampleDir.assign( mtResult.direction );
|
|
320
322
|
samplePdf.assign( mtResult.pdf );
|
|
321
|
-
|
|
323
|
+
// evaluateMaterialResponse is reflection-only and returns a non-physical value for
|
|
324
|
+
// the below-surface refraction direction (its dots are floored to 0.001). Carry the
|
|
325
|
+
// sampler's spectral tint and apply an energy-consistent transmission throughput
|
|
326
|
+
// below (mirrors handleTransmission's material.color × colorWeight convention).
|
|
327
|
+
transColorWeight.assign( mtResult.colorWeight );
|
|
322
328
|
|
|
323
329
|
} ).Else( () => {
|
|
324
330
|
|
|
@@ -329,10 +335,7 @@ export const calculateIndirectLighting = Fn( ( [
|
|
|
329
335
|
|
|
330
336
|
} );
|
|
331
337
|
|
|
332
|
-
|
|
333
|
-
const rawNoL = dot( N, sampleDir ).toVar();
|
|
334
|
-
const NoL = max( rawNoL, 0.0 ).toVar();
|
|
335
|
-
const absNoL = abs( rawNoL );
|
|
338
|
+
const NoL = max( dot( N, sampleDir ), 0.0 ).toVar();
|
|
336
339
|
|
|
337
340
|
// Calculate combined PDF for MIS (material strategies only)
|
|
338
341
|
const combinedPdf = float( 0.0 ).toVar();
|
|
@@ -380,11 +383,14 @@ export const calculateIndirectLighting = Fn( ( [
|
|
|
380
383
|
// MIS weight calculation
|
|
381
384
|
const misWeight = samplePdf.div( combinedPdf ).toVar();
|
|
382
385
|
|
|
383
|
-
//
|
|
384
|
-
|
|
386
|
+
// Reflection lobes: f·NoL·mis/pdf. Transmission lobe: the sampler's energy-consistent
|
|
387
|
+
// weight (material tint × dispersion colorWeight × mis) — the reflection BRDF value is
|
|
388
|
+
// invalid below the surface, so it is not used for transmission.
|
|
389
|
+
const reflThroughput = sampleBrdfValue.mul( NoL ).mul( misWeight ).div( samplePdf );
|
|
390
|
+
const transThroughput = material.color.xyz.mul( transColorWeight ).mul( misWeight );
|
|
385
391
|
|
|
386
392
|
r_direction.assign( sampleDir );
|
|
387
|
-
r_throughput.assign(
|
|
393
|
+
r_throughput.assign( select( selectedStrategy.equal( int( 3 ) ), transThroughput, reflThroughput ) );
|
|
388
394
|
r_misWeight.assign( misWeight );
|
|
389
395
|
r_pdf.assign( samplePdf );
|
|
390
396
|
r_combinedPdf.assign( combinedPdf );
|