rayzee 9.3.2 → 9.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 +8 -3
- package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-vkdXXyK2.js} +1 -1
- package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-FVqRJKQV.js} +1 -1
- package/dist/{ToneMapGPU-B6XIe3DU.js → ToneMapGPU-DKQM1pMd.js} +86 -78
- package/dist/rayzee.es.js +3600 -3370
- package/dist/rayzee.umd.js +124 -95
- package/package.json +1 -1
- package/src/EngineIssues.js +1 -0
- package/src/Passes/AIUpscaler.js +58 -141
- package/src/Passes/OIDNDenoiser.js +92 -65
- package/src/PathTracerApp.js +27 -15
- package/src/Processor/BVHRefitter.js +64 -0
- package/src/Processor/SceneProcessor.js +27 -5
- package/src/Processor/TextureCreator.js +170 -6
- package/src/Processor/ToneMapGPU.js +64 -15
- package/src/Processor/Workers/BVHRefitWorker.js +5 -2
- package/src/Processor/Workers/TexturesWorker.js +98 -12
- package/src/Stages/PathTracer.js +13 -2
- package/src/Stages/PathTracerStage.js +48 -46
- package/src/Storage/StorageManager.js +43 -5
- package/src/managers/AnimationManager.js +36 -5
- package/src/managers/GoboManager.js +9 -0
- package/src/managers/UniformManager.js +30 -7
|
@@ -2,15 +2,112 @@ let canvas, ctx;
|
|
|
2
2
|
|
|
3
3
|
// Memory limits and chunking configuration
|
|
4
4
|
const MEMORY_LIMITS = {
|
|
5
|
-
MAX_BYTES_PER_TEXTURE: 256 * 1024 * 1024, // 256MB per texture array
|
|
6
5
|
MAX_TEXTURE_DIMENSION: 8192, // Hardware ceiling (WebGPU maxTextureDimension2D guaranteed min); the per-scene maxTextureSize setting is the actual knob
|
|
7
6
|
CHUNK_SIZE: 8, // Optimized: Process textures in chunks of 8 for better memory locality
|
|
8
7
|
ADAPTIVE_CHUNK_SIZE: true, // Enable adaptive chunk sizing based on texture dimensions
|
|
9
8
|
MEMORY_SAFETY_FACTOR: 0.8 // Use only 80% of estimated available memory
|
|
10
9
|
};
|
|
11
10
|
|
|
11
|
+
let stream = null;
|
|
12
|
+
|
|
13
|
+
function handleStream( msg ) {
|
|
14
|
+
|
|
15
|
+
if ( msg.stream === 'begin' ) {
|
|
16
|
+
|
|
17
|
+
const { width, height, depth } = msg;
|
|
18
|
+
const target = new OffscreenCanvas( width, height );
|
|
19
|
+
const context = target.getContext( '2d', { willReadFrequently: true, alpha: true, desynchronized: true } );
|
|
20
|
+
stream = { width, height, depth, context, data: new Uint8Array( width * height * depth * 4 ) };
|
|
21
|
+
return;
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { width, height, depth, context, data } = stream;
|
|
26
|
+
|
|
27
|
+
if ( msg.stream === 'layer' ) {
|
|
28
|
+
|
|
29
|
+
context.clearRect( 0, 0, width, height );
|
|
30
|
+
context.drawImage( msg.bitmap, 0, 0 );
|
|
31
|
+
msg.bitmap.close();
|
|
32
|
+
data.set( context.getImageData( 0, 0, width, height ).data, width * height * 4 * msg.index );
|
|
33
|
+
self.postMessage( { stream: 'ack', index: msg.index } );
|
|
34
|
+
return;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
stream = null;
|
|
39
|
+
self.postMessage( { data: data.buffer, width, height, depth }, [ data.buffer ] );
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function processIntoShared( { shared, width, height, layers } ) {
|
|
44
|
+
|
|
45
|
+
if ( canvas.width !== width || canvas.height !== height ) {
|
|
46
|
+
|
|
47
|
+
canvas.width = width;
|
|
48
|
+
canvas.height = height;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ctx.imageSmoothingEnabled = true;
|
|
53
|
+
ctx.imageSmoothingQuality = 'high';
|
|
54
|
+
|
|
55
|
+
const data = new Uint8Array( shared );
|
|
56
|
+
const bytes = width * height * 4;
|
|
57
|
+
|
|
58
|
+
for ( const { index, texture } of layers ) {
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
|
|
62
|
+
await processSingleTextureOptimized( texture, data, index * bytes, width, height );
|
|
63
|
+
|
|
64
|
+
} catch ( error ) {
|
|
65
|
+
|
|
66
|
+
console.warn( `Failed to process texture ${index}:`, error );
|
|
67
|
+
data.fill( 0, index * bytes, ( index + 1 ) * bytes );
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
12
75
|
self.onmessage = async function ( e ) {
|
|
13
76
|
|
|
77
|
+
if ( e.data.shared ) {
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
|
|
81
|
+
if ( ! canvas ) initializeWorker( e.data.maxTextureSize );
|
|
82
|
+
await processIntoShared( e.data );
|
|
83
|
+
self.postMessage( { done: true } );
|
|
84
|
+
|
|
85
|
+
} catch ( error ) {
|
|
86
|
+
|
|
87
|
+
self.postMessage( { error: error.message } );
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ( e.data.stream ) {
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
|
|
99
|
+
handleStream( e.data );
|
|
100
|
+
|
|
101
|
+
} catch ( error ) {
|
|
102
|
+
|
|
103
|
+
self.postMessage( { error: error.message } );
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return;
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
14
111
|
const { textures, maxTextureSize, method = 'direct-transfer' } = e.data;
|
|
15
112
|
|
|
16
113
|
try {
|
|
@@ -51,17 +148,6 @@ function initializeWorker( maxTextureSize ) {
|
|
|
51
148
|
|
|
52
149
|
async function processTextures( textures, maxTextureSize, method ) {
|
|
53
150
|
|
|
54
|
-
// Check if we need to use chunked processing
|
|
55
|
-
const dimensions = calculateOptimalDimensions( textures, maxTextureSize );
|
|
56
|
-
const estimatedBytes = dimensions.maxWidth * dimensions.maxHeight * textures.length * 4;
|
|
57
|
-
|
|
58
|
-
if ( estimatedBytes > MEMORY_LIMITS.MAX_BYTES_PER_TEXTURE ) {
|
|
59
|
-
|
|
60
|
-
console.log( `Large texture array detected (${( estimatedBytes / 1024 / 1024 ).toFixed( 2 )}MB), using chunked processing` );
|
|
61
|
-
return await processTexturesInChunks( textures, maxTextureSize, method );
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
151
|
switch ( method ) {
|
|
66
152
|
|
|
67
153
|
case 'direct-transfer':
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -118,6 +118,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
118
118
|
// so when one lands cannot change the image. { due, read, apply } while one is in flight.
|
|
119
119
|
this._lockstep = false;
|
|
120
120
|
this._lockstepRead = null;
|
|
121
|
+
this._accumulationLockstep = false;
|
|
121
122
|
|
|
122
123
|
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
123
124
|
// 0 until a settled readback lands (and on reset/camera-move/resize) → grid stays full-size until then.
|
|
@@ -240,8 +241,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
240
241
|
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
241
242
|
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
242
243
|
|
|
243
|
-
// The packed light buffer was grow-reallocated at runtime (emissive set grew) —
|
|
244
|
-
// the compiled kernels still bind the old
|
|
244
|
+
// The packed light buffer was grow-reallocated at runtime (emissive set grew) or a light list grew —
|
|
245
|
+
// the compiled kernels still bind the old one, so rebuild before rendering.
|
|
245
246
|
if ( this._lightBufferRealloc ) {
|
|
246
247
|
|
|
247
248
|
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
@@ -320,6 +321,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
320
321
|
this.frame.value = frameValue;
|
|
321
322
|
this.seedFrame.value = this._seedTick ++;
|
|
322
323
|
this.tracedFrames ++;
|
|
324
|
+
if ( ! this._lockstep ) this._accumulationLockstep = false;
|
|
323
325
|
|
|
324
326
|
this._setWfDispatch();
|
|
325
327
|
|
|
@@ -661,6 +663,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
661
663
|
this._convergedFraction = state.convergedFraction ?? 0;
|
|
662
664
|
this._convergedGeometryFraction = state.convergedGeometryFraction ?? 0;
|
|
663
665
|
this.isComplete = false;
|
|
666
|
+
// The survivor curve is not in a checkpoint, so a continuation is not the lockstep render.
|
|
667
|
+
this._accumulationLockstep = false;
|
|
664
668
|
|
|
665
669
|
}
|
|
666
670
|
|
|
@@ -711,6 +715,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
711
715
|
|
|
712
716
|
}
|
|
713
717
|
|
|
718
|
+
/** Whether the current image was traced in lockstep from a reset made in it; with no sample yet, the setting. */
|
|
719
|
+
get accumulationLockstep() {
|
|
720
|
+
|
|
721
|
+
return this.frameCount > 0 ? this._accumulationLockstep : this._lockstep;
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
714
725
|
/**
|
|
715
726
|
* While a lockstep readback is due and has not landed, render() traces nothing: await this
|
|
716
727
|
* before the next frame. Null otherwise.
|
|
@@ -10,7 +10,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
10
10
|
import { CameraOptimizer } from '../Processor/CameraOptimizer.js';
|
|
11
11
|
import { createPerformanceMonitor, calculateAccumulationAlpha, updateCompletionThreshold } from '../Processor/utils.js';
|
|
12
12
|
import { StorageTexturePool } from '../Processor/StorageTexturePool.js';
|
|
13
|
-
import { UniformManager } from '../managers/UniformManager.js';
|
|
13
|
+
import { UniformManager, LIGHT_FLOATS, lightListCapacity } from '../managers/UniformManager.js';
|
|
14
14
|
import { MaterialDataManager } from '../managers/MaterialDataManager.js';
|
|
15
15
|
import { EnvironmentManager } from '../managers/EnvironmentManager.js';
|
|
16
16
|
import { ShaderBuilder } from '../Processor/ShaderBuilder.js';
|
|
@@ -218,8 +218,8 @@ export class PathTracerStage extends RenderStage {
|
|
|
218
218
|
// Initialized with dummy data so TSL compilation never sees null.
|
|
219
219
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
|
|
220
220
|
this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
|
|
221
|
-
// Set when _rebuildLightBuffer had to grow-reallocate the attribute
|
|
222
|
-
// kernels still bind the old one and must be rebuilt to see the new data.
|
|
221
|
+
// Set when _rebuildLightBuffer had to grow-reallocate the attribute, or a light list grew
|
|
222
|
+
// (_writeLightList): compiled kernels still bind the old one and must be rebuilt to see the new data.
|
|
223
223
|
this._lightBufferRealloc = false;
|
|
224
224
|
|
|
225
225
|
// Cached CPU-side data — rebuilt into the packed buffer whenever any source changes.
|
|
@@ -492,53 +492,36 @@ export class PathTracerStage extends RenderStage {
|
|
|
492
492
|
*/
|
|
493
493
|
_updateLightBufferNodes() {
|
|
494
494
|
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
this._writeLightList( 'directional', this.directionalLightsData, this.numDirectionalLights );
|
|
496
|
+
this._writeLightList( 'area', this.areaLightsData, this.numAreaLights );
|
|
497
|
+
this._writeLightList( 'point', this.pointLightsData, this.numPointLights );
|
|
498
|
+
this._writeLightList( 'spot', this.spotLightsData, this.numSpotLights );
|
|
497
499
|
|
|
498
|
-
|
|
499
|
-
this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 12 );
|
|
500
|
-
|
|
501
|
-
} else {
|
|
502
|
-
|
|
503
|
-
this.numDirectionalLights.value = 0;
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Area lights (16 floats per light — 13 base + normalize/spread/shape)
|
|
508
|
-
if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
|
|
509
|
-
|
|
510
|
-
this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
|
|
511
|
-
this.numAreaLights.value = Math.floor( this.areaLightsData.length / 16 );
|
|
512
|
-
|
|
513
|
-
} else {
|
|
514
|
-
|
|
515
|
-
this.numAreaLights.value = 0;
|
|
500
|
+
}
|
|
516
501
|
|
|
517
|
-
|
|
502
|
+
/**
|
|
503
|
+
* In place, because the shader bakes the list's length: sized per scene it compiled a program per
|
|
504
|
+
* light count, and three.js uploads only what fits the length it was built with, so a light added
|
|
505
|
+
* after a build was dropped. A list grows only past its capacity, and that rebuilds the kernels.
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
_writeLightList( type, data, count ) {
|
|
518
509
|
|
|
519
|
-
|
|
520
|
-
|
|
510
|
+
const floats = LIGHT_FLOATS[ type ];
|
|
511
|
+
const node = this[ `${type}LightsBufferNode` ];
|
|
512
|
+
const lights = data ? Math.floor( data.length / floats ) : 0;
|
|
513
|
+
const used = lights * floats;
|
|
521
514
|
|
|
522
|
-
|
|
523
|
-
this.numPointLights.value = Math.floor( this.pointLightsData.length / 9 );
|
|
515
|
+
if ( used > node.array.length ) {
|
|
524
516
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
this.numPointLights.value = 0;
|
|
517
|
+
node.array = new Float32Array( lightListCapacity( type, lights ) * floats );
|
|
518
|
+
this._lightBufferRealloc = true;
|
|
528
519
|
|
|
529
520
|
}
|
|
530
521
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
|
|
535
|
-
this.numSpotLights.value = Math.floor( this.spotLightsData.length / 20 );
|
|
536
|
-
|
|
537
|
-
} else {
|
|
538
|
-
|
|
539
|
-
this.numSpotLights.value = 0;
|
|
540
|
-
|
|
541
|
-
}
|
|
522
|
+
if ( used > 0 ) node.array.set( data.subarray( 0, used ) );
|
|
523
|
+
node.array.fill( 0, used );
|
|
524
|
+
count.value = lights;
|
|
542
525
|
|
|
543
526
|
}
|
|
544
527
|
|
|
@@ -565,6 +548,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
565
548
|
|
|
566
549
|
// Only deterministic mode and lockstep readbacks rewind the seed axis; see _pinSeedToFrame.
|
|
567
550
|
if ( this._pinSeedToFrame || this._lockstep ) this._seedTick = 0;
|
|
551
|
+
this._accumulationLockstep = !! this._lockstep;
|
|
568
552
|
|
|
569
553
|
}
|
|
570
554
|
|
|
@@ -1102,17 +1086,34 @@ export class PathTracerStage extends RenderStage {
|
|
|
1102
1086
|
|
|
1103
1087
|
}
|
|
1104
1088
|
|
|
1105
|
-
/**
|
|
1106
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Update BVH node data in the existing GPU buffer.
|
|
1091
|
+
* @param {Object} [bvhData]
|
|
1092
|
+
* @param {Array<{offset:number,count:number}>} [ranges] - only these, in floats; every node when omitted
|
|
1093
|
+
*/
|
|
1094
|
+
updateBVHData( bvhData, ranges ) {
|
|
1107
1095
|
|
|
1108
1096
|
if ( this._bvhRecords ) {
|
|
1109
1097
|
|
|
1110
|
-
|
|
1098
|
+
if ( bvhData?.chunks?.length > 1 ) this._bvhRecords = bvhData;
|
|
1099
|
+
if ( ranges ) this.updateBufferRanges( [], ranges );
|
|
1100
|
+
else uploadStorageChunks( this.renderer, this.bvhStorageAttr, this._bvhRecords.chunks );
|
|
1101
|
+
return;
|
|
1102
|
+
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const flat = bvhData?.chunks ? bvhData.chunks[ 0 ] : bvhData;
|
|
1106
|
+
if ( ! ranges ) {
|
|
1107
|
+
|
|
1108
|
+
this._updateStorageBuffer( this.bvhStorageAttr, flat );
|
|
1111
1109
|
return;
|
|
1112
1110
|
|
|
1113
1111
|
}
|
|
1114
1112
|
|
|
1115
|
-
|
|
1113
|
+
// The attribute keeps its own array once the refit has moved the nodes into shared memory.
|
|
1114
|
+
const own = this.bvhStorageAttr?.array;
|
|
1115
|
+
if ( flat && own && flat !== own ) for ( const r of ranges ) own.set( flat.subarray( r.offset, r.offset + r.count ), r.offset );
|
|
1116
|
+
this.updateBufferRanges( [], ranges );
|
|
1116
1117
|
|
|
1117
1118
|
}
|
|
1118
1119
|
|
|
@@ -1271,6 +1272,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
1271
1272
|
|
|
1272
1273
|
this.createStorageTextures( width, height );
|
|
1273
1274
|
this.frameCount = 0;
|
|
1275
|
+
this._accumulationLockstep = false;
|
|
1274
1276
|
|
|
1275
1277
|
}
|
|
1276
1278
|
|
|
@@ -17,6 +17,8 @@ const MAX_DEFAULT_BUDGET = 100 * 1024 ** 3;
|
|
|
17
17
|
const FREE_MARGIN = 64 * 1024 ** 2;
|
|
18
18
|
const STREAM_BATCH = 8 * 1024 ** 2;
|
|
19
19
|
const STREAM_WINDOW = 4;
|
|
20
|
+
// Other tabs write too, so the cache total kept in memory is listed again at most this often.
|
|
21
|
+
const CACHE_TOTAL_TTL_MS = 30_000;
|
|
20
22
|
|
|
21
23
|
const encoder = new TextEncoder();
|
|
22
24
|
|
|
@@ -152,6 +154,7 @@ export class EntryWriter {
|
|
|
152
154
|
this._sizes = { ...sizes };
|
|
153
155
|
this._append = append;
|
|
154
156
|
this._done = false;
|
|
157
|
+
this._committedBytes = append ? Object.values( sizes ).reduce( ( n, b ) => n + b, 0 ) : 0;
|
|
155
158
|
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -313,6 +316,7 @@ export class EntryWriter {
|
|
|
313
316
|
|
|
314
317
|
const { args, list } = transferable( JSON.stringify( meta ), { copy: false } );
|
|
315
318
|
await this._transport.call( 'writeFile', { path: this._path, name: META, ...args }, list );
|
|
319
|
+
if ( this.area.kind === STORAGE_KIND.CACHE ) this.area.manager._countCacheBytes( meta.bytes - this._committedBytes );
|
|
316
320
|
this.area.manager._changed( this.area.name );
|
|
317
321
|
return meta;
|
|
318
322
|
|
|
@@ -700,6 +704,7 @@ export class StorageManager extends Emitter {
|
|
|
700
704
|
this._transport = transport;
|
|
701
705
|
this._areas = new Map();
|
|
702
706
|
this._budget = null;
|
|
707
|
+
this._cacheTotal = null;
|
|
703
708
|
|
|
704
709
|
for ( const name of Object.values( ENGINE_AREAS ) ) this.defineArea( name, { kind: name === ENGINE_AREAS.SPILL ? STORAGE_KIND.SCRATCH : STORAGE_KIND.CACHE } );
|
|
705
710
|
|
|
@@ -775,6 +780,40 @@ export class StorageManager extends Emitter {
|
|
|
775
780
|
|
|
776
781
|
}
|
|
777
782
|
|
|
783
|
+
/**
|
|
784
|
+
* What cache areas hold together. Every new download asks, and a listing reads every entry's
|
|
785
|
+
* metadata, so it is listed once and kept current by this manager's own commits.
|
|
786
|
+
*/
|
|
787
|
+
async _cacheBytes() {
|
|
788
|
+
|
|
789
|
+
let total = this._cacheTotal;
|
|
790
|
+
if ( ! total || Date.now() - total.at > CACHE_TOTAL_TTL_MS ) {
|
|
791
|
+
|
|
792
|
+
total = this._cacheTotal = { at: Date.now(), bytes: 0, ready: null };
|
|
793
|
+
total.ready = this._cacheEntries().then( ( entries ) => {
|
|
794
|
+
|
|
795
|
+
total.bytes += entries.reduce( ( n, e ) => n + ( e.meta.bytes ?? 0 ), 0 );
|
|
796
|
+
|
|
797
|
+
} );
|
|
798
|
+
total.ready.catch( () => {
|
|
799
|
+
|
|
800
|
+
if ( this._cacheTotal === total ) this._cacheTotal = null;
|
|
801
|
+
|
|
802
|
+
} );
|
|
803
|
+
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
await total.ready;
|
|
807
|
+
return total.bytes;
|
|
808
|
+
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
_countCacheBytes( bytes ) {
|
|
812
|
+
|
|
813
|
+
if ( this._cacheTotal ) this._cacheTotal.bytes += bytes;
|
|
814
|
+
|
|
815
|
+
}
|
|
816
|
+
|
|
778
817
|
async _cacheEntries() {
|
|
779
818
|
|
|
780
819
|
const out = [];
|
|
@@ -816,8 +855,8 @@ export class StorageManager extends Emitter {
|
|
|
816
855
|
|
|
817
856
|
const entries = await this._cacheEntries();
|
|
818
857
|
const budget = await this.budget();
|
|
819
|
-
|
|
820
|
-
const target = freeBytes ?? Math.max( 0,
|
|
858
|
+
const total = this._cacheTotal = { at: Date.now(), bytes: entries.reduce( ( n, e ) => n + ( e.meta.bytes ?? 0 ), 0 ), ready: null };
|
|
859
|
+
const target = freeBytes ?? Math.max( 0, total.bytes - budget );
|
|
821
860
|
if ( target <= 0 ) return 0;
|
|
822
861
|
|
|
823
862
|
const held = await heldLockNames();
|
|
@@ -832,7 +871,7 @@ export class StorageManager extends Emitter {
|
|
|
832
871
|
if ( await area.removeById( meta.id ) ) {
|
|
833
872
|
|
|
834
873
|
freed += meta.bytes ?? 0;
|
|
835
|
-
|
|
874
|
+
total.bytes -= meta.bytes ?? 0;
|
|
836
875
|
|
|
837
876
|
}
|
|
838
877
|
|
|
@@ -855,8 +894,7 @@ export class StorageManager extends Emitter {
|
|
|
855
894
|
const free = bytes + FREE_MARGIN - ( quota - usage );
|
|
856
895
|
if ( kind !== STORAGE_KIND.CACHE ) return { free, budget: 0 };
|
|
857
896
|
|
|
858
|
-
|
|
859
|
-
return { free, budget: cacheBytes + bytes - await this.budget() };
|
|
897
|
+
return { free, budget: await this._cacheBytes() + bytes - await this.budget() };
|
|
860
898
|
|
|
861
899
|
};
|
|
862
900
|
|
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
|
|
11
11
|
import { EngineEvents } from '../EngineEvents.js';
|
|
12
12
|
|
|
13
|
+
/** The nodes of `nodes` with no ancestor in it, or null when updating each would cost more than one pass over `root`. */
|
|
14
|
+
function subtreeRoots( nodes, root ) {
|
|
15
|
+
|
|
16
|
+
const roots = [ ...nodes ].filter( node => {
|
|
17
|
+
|
|
18
|
+
for ( let o = node.parent; o; o = o.parent ) if ( nodes.has( o ) ) return false;
|
|
19
|
+
return true;
|
|
20
|
+
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
let total = 0, covered = 0;
|
|
24
|
+
root.traverse?.( () => total ++ );
|
|
25
|
+
for ( const node of roots ) node.traverse?.( () => covered ++ );
|
|
26
|
+
return covered + roots.length / 2 < total ? roots : null;
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
export class AnimationManager extends EventDispatcher {
|
|
14
31
|
|
|
15
32
|
constructor() {
|
|
@@ -162,11 +179,19 @@ export class AnimationManager extends EventDispatcher {
|
|
|
162
179
|
|
|
163
180
|
const animated = new Set();
|
|
164
181
|
let visibility = false;
|
|
182
|
+
let unresolved = false;
|
|
165
183
|
for ( const track of clip.tracks ) {
|
|
166
184
|
|
|
167
185
|
const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
// As PropertyBinding.findNode: no node name binds the root itself.
|
|
187
|
+
const node = nodeName === '' || nodeName === '.' ? root : byName.get( nodeName );
|
|
188
|
+
if ( ! node ) {
|
|
189
|
+
|
|
190
|
+
unresolved = true;
|
|
191
|
+
continue;
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
170
195
|
animated.add( node );
|
|
171
196
|
if ( propertyName === 'visible' ) visibility = true;
|
|
172
197
|
|
|
@@ -191,7 +216,7 @@ export class AnimationManager extends EventDispatcher {
|
|
|
191
216
|
const cameras = new Set();
|
|
192
217
|
for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
|
|
193
218
|
|
|
194
|
-
return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
219
|
+
return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes: unresolved ? null : animated, roots: unresolved ? null : subtreeRoots( animated, root ) };
|
|
195
220
|
|
|
196
221
|
} );
|
|
197
222
|
|
|
@@ -218,15 +243,18 @@ export class AnimationManager extends EventDispatcher {
|
|
|
218
243
|
|
|
219
244
|
const meshes = new Set(), cameras = new Set();
|
|
220
245
|
let visibility = false;
|
|
246
|
+
let nodes = new Set();
|
|
221
247
|
for ( const plan of this._plans ) {
|
|
222
248
|
|
|
223
249
|
plan.meshes.forEach( m => meshes.add( m ) );
|
|
224
250
|
plan.cameras.forEach( c => cameras.add( c ) );
|
|
225
251
|
visibility ||= plan.visibility;
|
|
252
|
+
if ( nodes && plan.nodes ) plan.nodes.forEach( n => nodes.add( n ) );
|
|
253
|
+
else nodes = null;
|
|
226
254
|
|
|
227
255
|
}
|
|
228
256
|
|
|
229
|
-
this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
257
|
+
this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ], nodes, roots: nodes ? subtreeRoots( nodes, this._mixerRoot ) : null };
|
|
230
258
|
|
|
231
259
|
}
|
|
232
260
|
|
|
@@ -491,7 +519,10 @@ export class AnimationManager extends EventDispatcher {
|
|
|
491
519
|
|
|
492
520
|
// Bones live outside mesh subtrees so per-mesh updateMatrixWorld() misses them. Doing it
|
|
493
521
|
// once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
|
|
494
|
-
|
|
522
|
+
// A rigid clip moves only what its tracks name, so only those subtrees are updated.
|
|
523
|
+
const roots = this._rigid ? this._planFor( this._activeClip )?.roots : null;
|
|
524
|
+
if ( roots ) for ( const node of roots ) node.updateMatrixWorld( true );
|
|
525
|
+
else this._mixerRoot.updateMatrixWorld( true );
|
|
495
526
|
|
|
496
527
|
if ( this._rigid ) {
|
|
497
528
|
|
|
@@ -71,6 +71,7 @@ export class GoboManager {
|
|
|
71
71
|
cnv.width = width;
|
|
72
72
|
cnv.height = height;
|
|
73
73
|
const ctx = cnv.getContext( '2d', { willReadFrequently: true } );
|
|
74
|
+
let lastYield = performance.now();
|
|
74
75
|
|
|
75
76
|
for ( let i = 0; i < depth; i ++ ) {
|
|
76
77
|
|
|
@@ -79,6 +80,14 @@ export class GoboManager {
|
|
|
79
80
|
const img = ctx.getImageData( 0, 0, width, height );
|
|
80
81
|
data.set( img.data, i * width * height * 4 );
|
|
81
82
|
|
|
83
|
+
// Each first draw decodes its image, so the library packs in short tasks.
|
|
84
|
+
if ( performance.now() - lastYield > 16 ) {
|
|
85
|
+
|
|
86
|
+
await new Promise( resolve => setTimeout( resolve, 0 ) );
|
|
87
|
+
lastYield = performance.now();
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
const tex = new DataArrayTexture( data, width, height, depth );
|
|
@@ -19,6 +19,31 @@ const SHADER_NAMES = {
|
|
|
19
19
|
cameraProjectionMatrix: 'ptCameraProjectionMatrix',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
/** Floats per light in each type's list (see LightSerializer). */
|
|
23
|
+
export const LIGHT_FLOATS = Object.freeze( {
|
|
24
|
+
directional: 12, // 8 light fields + gobo { index, signed intensity, scale, pad }
|
|
25
|
+
area: 16, // 13 base + normalize, spread, shape
|
|
26
|
+
point: 9,
|
|
27
|
+
spot: 20, // 14 light fields + gobo { index, signed intensity } + IES { index, intensity } + 2 reserved
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
/** Lights a list holds before it has to grow. */
|
|
31
|
+
export const LIGHT_LIST_STEP = 16;
|
|
32
|
+
|
|
33
|
+
// A uniform array gives each float its own vec4; WebGPU guarantees 64 KiB a binding.
|
|
34
|
+
const MAX_UNIFORM_ELEMENTS = 65536 / 16;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lights a list of this type must hold for `lights`: a whole number of steps, but never past what
|
|
38
|
+
* fits a binding unless `lights` alone already does — a count that did not fit never did.
|
|
39
|
+
*/
|
|
40
|
+
export function lightListCapacity( type, lights ) {
|
|
41
|
+
|
|
42
|
+
const stepped = Math.ceil( lights / LIGHT_LIST_STEP ) * LIGHT_LIST_STEP;
|
|
43
|
+
return Math.max( lights, Math.min( stepped, Math.floor( MAX_UNIFORM_ELEMENTS / LIGHT_FLOATS[ type ] ) ) );
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
export class UniformManager {
|
|
23
48
|
|
|
24
49
|
constructor( width = 1920, height = 1080 ) {
|
|
@@ -266,13 +291,11 @@ export class UniformManager {
|
|
|
266
291
|
u( 'numPointLights', 0, 'int' );
|
|
267
292
|
u( 'numSpotLights', 0, 'int' );
|
|
268
293
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
spot: uniformArray( new Float32Array( 20 * 16 ), 'float' ),
|
|
275
|
-
};
|
|
294
|
+
// The shader bakes each list's length, so lights are written into these in place and a list is
|
|
295
|
+
// only replaced when a scene has more lights than it holds (PathTracerStage._writeLightList).
|
|
296
|
+
this._lightBuffers = Object.fromEntries( Object.entries( LIGHT_FLOATS ).map(
|
|
297
|
+
( [ type, floats ] ) => [ type, uniformArray( new Float32Array( floats * LIGHT_LIST_STEP ), 'float' ) ]
|
|
298
|
+
) );
|
|
276
299
|
|
|
277
300
|
// Camera matrices
|
|
278
301
|
u( 'cameraWorldMatrix', new Matrix4(), 'mat4' );
|