rayzee 11.2.1 → 11.3.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 +58 -29
- package/dist/ArchiveImporter-D735xX-4.js +7860 -0
- package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
- package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
- package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-DiBH63kz.js} +1 -1
- package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-CZNQP13W.js} +1 -1
- package/dist/{PathTracer-D6nbknj5.js → PathTracer-BXCCVrPK.js} +4371 -4612
- package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-DqmaS0lb.js} +2 -2
- package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
- package/dist/SolarLimb-bZE6y63S.js +762 -0
- 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 +7 -7
- package/dist/rayzee.es.js +472 -468
- package/dist/rayzee.umd.js +106 -105
- package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
- package/package.json +1 -1
- package/src/PathTracerApp.js +4 -3
- package/src/Processor/ArchiveImporter.js +173 -94
- package/src/Processor/ArchiveReader.js +20 -23
- package/src/Processor/AssetLoader.js +48 -1
- package/src/Processor/BVHRefitter.js +109 -13
- package/src/Processor/BufferLayout.js +115 -0
- package/src/Processor/ChunkedRecords.js +3 -2
- package/src/Processor/EmissiveTriangleBuilder.js +6 -6
- package/src/Processor/GeometryExtractor.js +189 -79
- package/src/Processor/HostMemory.js +39 -9
- package/src/Processor/InstanceTable.js +714 -42
- package/src/Processor/PBRT/PBRTCurves.js +224 -6
- package/src/Processor/PBRT/PBRTMaterials.js +3 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
- package/src/Processor/PackedRayBuffer.js +5 -2
- package/src/Processor/SceneProcessor.js +747 -141
- package/src/Processor/TLASBuilder.js +32 -17
- package/src/Processor/USD/USDLayer.js +1149 -0
- package/src/Processor/USD/USDScene.js +1655 -0
- package/src/Processor/USD/USDStage.js +682 -0
- package/src/Processor/USD/USDText.js +674 -0
- package/src/Processor/USD/index.js +138 -0
- package/src/Processor/Workers/BVHRefitWorker.js +3 -3
- package/src/RayzeeRenderer.js +167 -26
- package/src/Stages/NormalDepth.js +7 -12
- package/src/Stages/PathTracer.js +12 -17
- package/src/Stages/PathTracerStage.js +273 -96
- package/src/Storage/GeometrySpill.js +46 -14
- package/src/Storage/SceneGraphCodec.js +17 -3
- package/src/Storage/SpillStore.js +9 -0
- package/src/TSL/BVHTraversal.js +183 -7
- package/src/TSL/Common.js +57 -13
- package/src/TSL/ShadeKernel.js +3 -2
- package/src/TSL/TextureSampling.js +11 -8
- package/src/TSL/patches.js +84 -0
- package/src/integrators/BidirectionalIntegrator.js +20 -2
- package/src/managers/DenoisingManager.js +4 -4
- package/src/managers/InteractionManager.js +19 -6
- package/src/managers/MaterialDataManager.js +22 -5
- package/src/managers/UniformManager.js +3 -0
- package/dist/ArchiveImporter-D_x863yu.js +0 -5198
- package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
- package/dist/SolarLimb-CPaeZVwz.js +0 -721
- package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
|
@@ -3,22 +3,22 @@ import { BVHBuilder } from './BVHBuilder.js';
|
|
|
3
3
|
import { BVHRefitter } from './BVHRefitter.js';
|
|
4
4
|
import { buildBVHParallel, shouldUseParallelBuild } from './ParallelBVHBuilder.js';
|
|
5
5
|
import { TLASBuilder } from './TLASBuilder.js';
|
|
6
|
-
import { InstanceTable, isIdentity, multiplyAffine } from './InstanceTable.js';
|
|
6
|
+
import { InstanceTable, IN_PLACE, isIdentity, multiplyAffine } from './InstanceTable.js';
|
|
7
7
|
import { ChunkedRecords, SHARED_MEMORY_AVAILABLE, setChunkObserver, DEFAULT_CHUNK_BYTES } from './ChunkedRecords.js';
|
|
8
8
|
import {
|
|
9
|
-
MemoryLedger, estimateSceneBytes, probeAddressSpace,
|
|
9
|
+
MemoryLedger, estimateSceneBytes, spillingPeakBytes, probeAddressSpace,
|
|
10
10
|
PREFLIGHT_MIN_BYTES, PREFLIGHT_SAFETY, SAFE_SCENE_BYTES, MAX_SCENE_BYTES,
|
|
11
11
|
} from './HostMemory.js';
|
|
12
12
|
import { TextureCreator } from './TextureCreator.js';
|
|
13
|
-
import { GeometryExtractor, geometryBytesOf } from './GeometryExtractor.js';
|
|
13
|
+
import { GeometryExtractor, geometryBytesOf, usesTextureCoordinates } from './GeometryExtractor.js';
|
|
14
14
|
import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
|
|
15
15
|
import { updateLoading } from '../Processor/utils.js';
|
|
16
16
|
import { BuildTimer } from './BuildTimer.js';
|
|
17
17
|
import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
|
|
18
|
-
import { SRGBColorSpace } from 'three';
|
|
18
|
+
import { Box3, Matrix4, SRGBColorSpace, Sphere, Vector3 } from 'three';
|
|
19
19
|
import {
|
|
20
20
|
TRIANGLE_DATA_LAYOUT, packNormalOct, TRI_MATERIAL_MASK, BVH_LEAF_MARKERS, BVH_FOLDED_FIRST_LIMIT,
|
|
21
|
-
assertBVHIndexFits, TLAS_PLACEMENT_MASK,
|
|
21
|
+
assertBVHIndexFits, TLAS_PLACEMENT_MASK, RECORD_VEC4, CLUSTER_FIRST_MASK, TLAS_LEAF_IDENTITY,
|
|
22
22
|
} from './BufferLayout.js';
|
|
23
23
|
import { TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex, planTextureBuckets } from './TextureBuckets.js';
|
|
24
24
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
@@ -47,8 +47,17 @@ const SPLIT_PIECE_TRIANGLES = 1 << 19;
|
|
|
47
47
|
// Past this many stored triangles, small leaves are folded into their parents: the BVH halves,
|
|
48
48
|
// at up to 4 % of traversal time, so only a scene whose BVH nears 2 GB pays it.
|
|
49
49
|
const FOLD_LEAVES_TRIANGLES = 40_000_000;
|
|
50
|
+
// Past this many TLAS entries the TLAS is built over copy clusters (BufferLayout CLUSTER_LEAF): 128 → ~80 B a copy on the
|
|
51
|
+
// GPU, and a quarter of the TLAS build's memory. Below it the old leaves stay, and with them the old code.
|
|
52
|
+
const CLUSTER_MIN_ENTRIES = 1_000_000;
|
|
50
53
|
// A streamed build's extraction waits while more triangle records than this are in memory.
|
|
51
|
-
const STREAM_RESIDENT_BYTES =
|
|
54
|
+
const STREAM_RESIDENT_BYTES = 1024 ** 3;
|
|
55
|
+
// Less three.js geometry than this comes back at the end of a spilling build: left on disk it costs picking and the
|
|
56
|
+
// selection outline, and 24155522.glb's 140 MB was not worth that.
|
|
57
|
+
const GEOMETRY_ON_DISK_BYTES = 1024 ** 3;
|
|
58
|
+
// A spilled build's order maps go to disk in writes of this size and come back in reads of the other.
|
|
59
|
+
const ORDER_PACK_BYTES = 32 * 1024 * 1024;
|
|
60
|
+
const ORDER_WINDOW_BYTES = 64 * 1024 * 1024;
|
|
52
61
|
// Extraction waits while more three.js geometry than this is queued for disk.
|
|
53
62
|
const GEOMETRY_QUEUE_BYTES = 256 * 1024 * 1024;
|
|
54
63
|
|
|
@@ -87,6 +96,35 @@ function largestMeshTriangles( object ) {
|
|
|
87
96
|
* SceneProcessor - Processes scene geometry into GPU-ready data:
|
|
88
97
|
* BVH acceleration, texture atlas, material buffers.
|
|
89
98
|
*/
|
|
99
|
+
/** Fills a TLAS child's bounds left NaN from the box its parent stores for it (slot: its min at 0–2, max at 4–6). */
|
|
100
|
+
// A move's TLAS upload: touched nodes this close share a range, and past MAX_TLAS_RANGES ranges one spans them all.
|
|
101
|
+
const TLAS_RANGE_GAP = 1024;
|
|
102
|
+
const MAX_TLAS_RANGES = 64;
|
|
103
|
+
|
|
104
|
+
/** Float ranges of the BVH covering `nodes`, for an upload. */
|
|
105
|
+
function nodeRanges( nodes ) {
|
|
106
|
+
|
|
107
|
+
const sorted = Int32Array.from( nodes ).sort();
|
|
108
|
+
const out = [];
|
|
109
|
+
let lo = sorted[ 0 ], hi = lo;
|
|
110
|
+
for ( let k = 1; k <= sorted.length; k ++ ) {
|
|
111
|
+
|
|
112
|
+
if ( k < sorted.length && sorted[ k ] - hi <= TLAS_RANGE_GAP ) {
|
|
113
|
+
|
|
114
|
+
hi = sorted[ k ];
|
|
115
|
+
continue;
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
out.push( { offset: lo * 16, count: ( hi - lo + 1 ) * 16 } );
|
|
120
|
+
lo = hi = sorted[ k ];
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return out.length > MAX_TLAS_RANGES ? [ { offset: sorted[ 0 ] * 16, count: ( sorted.at( - 1 ) - sorted[ 0 ] + 1 ) * 16 } ] : out;
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
|
|
90
128
|
export class SceneProcessor {
|
|
91
129
|
|
|
92
130
|
/**
|
|
@@ -257,6 +295,20 @@ export class SceneProcessor {
|
|
|
257
295
|
|
|
258
296
|
}
|
|
259
297
|
|
|
298
|
+
/** Whether a scene built in memory would pass SAFE_SCENE_BYTES, so `memorySpill: 'auto'` builds it through disk. */
|
|
299
|
+
needsSpill( object ) {
|
|
300
|
+
|
|
301
|
+
return estimateSceneBytes( this.geometryExtractor.surveyScene( object ) ).total > SAFE_SCENE_BYTES;
|
|
302
|
+
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Whether the current scene goes to disk: built to spill (see {@link buildBVH}'s `progressive`) and over a chunk. */
|
|
306
|
+
get spilling() {
|
|
307
|
+
|
|
308
|
+
return !! this._progressive && ( this.triangles?.chunks?.length ?? 0 ) > 1;
|
|
309
|
+
|
|
310
|
+
}
|
|
311
|
+
|
|
260
312
|
/**
|
|
261
313
|
* Price the scene before the build starts spending, and say so if it is over what a healthy
|
|
262
314
|
* session can place.
|
|
@@ -276,15 +328,17 @@ export class SceneProcessor {
|
|
|
276
328
|
const maxBytes = this.config.maxSceneBytes ?? MAX_SCENE_BYTES;
|
|
277
329
|
const survey = this.geometryExtractor.surveyScene( object );
|
|
278
330
|
const estimate = estimateSceneBytes( survey );
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
// Streamed, neither are the triangles: what is held is the resident cap plus the largest
|
|
282
|
-
// mesh in the parallel builder — its copy and 44 B a triangle of scratch, the store's own
|
|
283
|
-
// chunks for it let go meanwhile.
|
|
331
|
+
// Streamed, the triangles held are the resident cap plus the largest mesh in the parallel builder — its copy
|
|
332
|
+
// and 44 B a triangle of scratch, the store's own chunks for it let go meanwhile — and the peak is one phase.
|
|
284
333
|
if ( this._streaming ) {
|
|
285
334
|
|
|
286
335
|
const resident = STREAM_RESIDENT_BYTES + largestMeshTriangles( object ) * ( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4 + 44 );
|
|
287
|
-
estimate.total
|
|
336
|
+
estimate.total = spillingPeakBytes( estimate, survey.placements, Math.min( estimate.triangles, resident ) );
|
|
337
|
+
|
|
338
|
+
} else if ( this._progressive ) {
|
|
339
|
+
|
|
340
|
+
// Spilled as it is built, the BVH is never resident as a whole.
|
|
341
|
+
estimate.total -= estimate.bvh;
|
|
288
342
|
|
|
289
343
|
}
|
|
290
344
|
|
|
@@ -379,7 +433,7 @@ export class SceneProcessor {
|
|
|
379
433
|
triangles: this.triangles?.byteLength ?? 0,
|
|
380
434
|
bvh: this.bvh?.byteLength ?? 0,
|
|
381
435
|
geometry: this._geometryBytes ?? this.memoryPreflight?.geometryBytes ?? 0,
|
|
382
|
-
placements: table?.
|
|
436
|
+
placements: table?.matrixBytes ?? 0,
|
|
383
437
|
blasScratch: 0,
|
|
384
438
|
orderMaps: 0,
|
|
385
439
|
};
|
|
@@ -410,6 +464,7 @@ export class SceneProcessor {
|
|
|
410
464
|
sceneBounds() {
|
|
411
465
|
|
|
412
466
|
if ( ! this.bvh || this.bvh.recordCount === 0 ) return null;
|
|
467
|
+
if ( ! this.bvh.isResident( 0, 1 ) ) return this._spilledRootBounds ?? null;
|
|
413
468
|
|
|
414
469
|
const chunk = this.bvh.chunkFor( 0 );
|
|
415
470
|
const base = this.bvh.baseOf( 0 );
|
|
@@ -482,6 +537,9 @@ export class SceneProcessor {
|
|
|
482
537
|
|
|
483
538
|
try {
|
|
484
539
|
|
|
540
|
+
// A rebuild of the same model reads its geometry and matrix lists back first: _reset drops the files.
|
|
541
|
+
if ( this._geometryOwner === object ) await Promise.all( [ this._restoreGeometry( object ), this.ensureMovable() ] );
|
|
542
|
+
|
|
485
543
|
// Reset state before beginning
|
|
486
544
|
this._reset();
|
|
487
545
|
this._sceneKey = sceneKey;
|
|
@@ -492,6 +550,8 @@ export class SceneProcessor {
|
|
|
492
550
|
// A spilling build of more than one chunk extracts and builds its BLASes together.
|
|
493
551
|
this._streaming = !! this._progressive && stored > Math.floor( DEFAULT_CHUNK_BYTES / ( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4 ) );
|
|
494
552
|
this._foldLeaves = stored > FOLD_LEAVES_TRIANGLES;
|
|
553
|
+
// No material samples a texture: the GPU keeps no texture coordinates (24 B a triangle).
|
|
554
|
+
this.textureCoordinates = usesTextureCoordinates( object );
|
|
495
555
|
this.bvhBuilder.foldLeaves = this._foldLeaves;
|
|
496
556
|
this._log( 'Starting scene processing' );
|
|
497
557
|
|
|
@@ -570,7 +630,12 @@ export class SceneProcessor {
|
|
|
570
630
|
timer.print();
|
|
571
631
|
|
|
572
632
|
this.processingStage = 'complete';
|
|
573
|
-
|
|
633
|
+
// A spilling build leaves a large model's geometry on disk until something reads it (ensureGeometryResident).
|
|
634
|
+
if ( this._streaming && this._geometrySpill?.bytes >= GEOMETRY_ON_DISK_BYTES ) {
|
|
635
|
+
|
|
636
|
+
this._geometryOwner = object;
|
|
637
|
+
|
|
638
|
+
} else await this._restoreGeometry( object );
|
|
574
639
|
updateLoading( { status: "Scene data ready", progress: 85 } );
|
|
575
640
|
if ( this._blasKey && ! this._blasRestored && this.performanceMetrics.totalProcessingTime >= this.config.sceneCacheMinBuildMs ) {
|
|
576
641
|
|
|
@@ -594,7 +659,7 @@ export class SceneProcessor {
|
|
|
594
659
|
} finally {
|
|
595
660
|
|
|
596
661
|
// A failed build's model is discarded, so its geometry is not read back.
|
|
597
|
-
this._disposeGeometrySpill();
|
|
662
|
+
if ( ! this._geometryOwner ) this._disposeGeometrySpill();
|
|
598
663
|
setChunkObserver( null );
|
|
599
664
|
this._sampleMemory( `at ${this.processingStage}` );
|
|
600
665
|
this._logMemoryReport();
|
|
@@ -661,7 +726,7 @@ export class SceneProcessor {
|
|
|
661
726
|
*/
|
|
662
727
|
async _extractStreaming( object ) {
|
|
663
728
|
|
|
664
|
-
updateLoading( { isLoading: true, title: 'Processing', status: '
|
|
729
|
+
updateLoading( { isLoading: true, title: 'Processing', status: 'Large scene: building through disk to save memory...', progress: 15 } );
|
|
665
730
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
666
731
|
const startTime = performance.now();
|
|
667
732
|
|
|
@@ -756,7 +821,7 @@ export class SceneProcessor {
|
|
|
756
821
|
|
|
757
822
|
tri = store;
|
|
758
823
|
this._setTriangleData( store );
|
|
759
|
-
state.uploadTriangles = uploader( store, 'triangles' );
|
|
824
|
+
state.uploadTriangles = uploader( store, 'triangles', { textureCoordinates: this.textureCoordinates } );
|
|
760
825
|
|
|
761
826
|
},
|
|
762
827
|
|
|
@@ -836,8 +901,9 @@ export class SceneProcessor {
|
|
|
836
901
|
// once per placement; storage counts it once.
|
|
837
902
|
this.expandedTriangleCount = extractedData.expandedTriangleCount ?? extractedData.triangleCount;
|
|
838
903
|
this.instanceSource = extractedData.instanceSource || null;
|
|
839
|
-
this.
|
|
904
|
+
this.matrixRuns = extractedData.matrixRuns || null;
|
|
840
905
|
this.instanceCount = extractedData.instanceCount || 0;
|
|
906
|
+
this.instanceGroups = extractedData.instanceGroups ?? [];
|
|
841
907
|
this.bakeInverse = extractedData.bakeInverse || null;
|
|
842
908
|
|
|
843
909
|
this._log( `Using Float32Array format: ${this.triangleCount} triangles, ${( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 )}MB` );
|
|
@@ -910,12 +976,17 @@ export class SceneProcessor {
|
|
|
910
976
|
const pooled = this.instanceCount > 0;
|
|
911
977
|
const meshCount = pooled ? this.instanceCount : ranges.length;
|
|
912
978
|
const instSource = this.instanceSource;
|
|
913
|
-
const worldPool = pooled ? this.
|
|
979
|
+
const worldPool = pooled ? this.matrixRuns : null;
|
|
914
980
|
const sourceOf = m => ( pooled ? instSource[ m ] : m );
|
|
915
981
|
|
|
916
982
|
this.instanceTable = new InstanceTable();
|
|
917
983
|
this.instanceTable.allocate( meshCount, ranges.length, worldPool, pooled ? instSource : null );
|
|
918
984
|
this.instanceTable.tplBakeInverse = this.bakeInverse;
|
|
985
|
+
this.instanceTable.setGroups( pooled ? this.instanceGroups : null );
|
|
986
|
+
const clustered = this._clusterCopies( this.instanceTable );
|
|
987
|
+
if ( clustered ) this.instanceTable.planClusters();
|
|
988
|
+
const tlasEntries = clustered ? this.instanceTable.clusterCount : this.instanceTable.entryCount;
|
|
989
|
+
const blasBase = TLASBuilder.nodeCountFor( tlasEntries ) + this.instanceTable.groupNodeCount;
|
|
919
990
|
|
|
920
991
|
// Built while the geometry was extracted (the streaming path): only adopt the results.
|
|
921
992
|
const streamed = this._streamed;
|
|
@@ -927,8 +998,9 @@ export class SceneProcessor {
|
|
|
927
998
|
const streamedTasks = [];
|
|
928
999
|
|
|
929
1000
|
// Placements that reuse another's triangles reuse its BLAS too — the whole point of
|
|
930
|
-
// storing geometry in object space. Only the first placement of a range is built.
|
|
931
|
-
|
|
1001
|
+
// storing geometry in object space. Only the first placement of a range is built. A column, not a Map: a Map
|
|
1002
|
+
// holds at most 2^24 entries, and the Moana island has 51M placements.
|
|
1003
|
+
const aliasOf = new Int32Array( meshCount ).fill( - 1 );
|
|
932
1004
|
const ownerOfRange = new Map();
|
|
933
1005
|
|
|
934
1006
|
for ( let m = 0; m < meshCount; m ++ ) {
|
|
@@ -939,7 +1011,7 @@ export class SceneProcessor {
|
|
|
939
1011
|
const owner = ownerOfRange.get( range.start );
|
|
940
1012
|
if ( owner !== undefined ) {
|
|
941
1013
|
|
|
942
|
-
aliasOf
|
|
1014
|
+
aliasOf[ m ] = owner;
|
|
943
1015
|
continue;
|
|
944
1016
|
|
|
945
1017
|
}
|
|
@@ -969,7 +1041,7 @@ export class SceneProcessor {
|
|
|
969
1041
|
const owners = [ ...poolTasks, ...parallelTasks ]
|
|
970
1042
|
.map( ( { m, range } ) => ( { m, range, t: sourceOf( m ), triOffset: range.start, triCount: range.count } ) )
|
|
971
1043
|
.sort( ( a, b ) => a.t - b.t );
|
|
972
|
-
const restored = streamed ? null : await this._restoreBLASes( owners,
|
|
1044
|
+
const restored = streamed ? null : await this._restoreBLASes( owners, blasBase, workerOpts );
|
|
973
1045
|
const spill = streamed ? streamed.state
|
|
974
1046
|
: ! restored && this._progressive && this.triangles.chunks.length > 1 ? await this._beginProgressiveSpill( owners ) : null;
|
|
975
1047
|
const onBuilt = spill && ! streamed ? ( m, range, result ) => this._onBLASBuilt( spill, sourceOf( m ), range, result ) : null;
|
|
@@ -1043,22 +1115,22 @@ export class SceneProcessor {
|
|
|
1043
1115
|
triCount: range.count,
|
|
1044
1116
|
originalToBvhMap: result.originalToBvh || null,
|
|
1045
1117
|
bvhData: result.bvhData,
|
|
1046
|
-
matrixWorld: pooled ?
|
|
1047
|
-
matrixOffset: pooled ? m * 16 : 0,
|
|
1118
|
+
matrixWorld: pooled ? IN_PLACE : ( this.meshes?.[ m ]?.matrixWorld?.elements ?? null ),
|
|
1048
1119
|
expandedStart: range.expandedStart,
|
|
1049
1120
|
sourceMesh: sourceOf( m ),
|
|
1050
1121
|
} );
|
|
1051
1122
|
|
|
1052
1123
|
}
|
|
1053
1124
|
|
|
1054
|
-
for (
|
|
1125
|
+
for ( let m = 0; m < meshCount; m ++ ) {
|
|
1055
1126
|
|
|
1127
|
+
const owner = aliasOf[ m ];
|
|
1128
|
+
if ( owner < 0 ) continue;
|
|
1056
1129
|
this.instanceTable.setAlias(
|
|
1057
1130
|
m, owner,
|
|
1058
|
-
pooled ?
|
|
1131
|
+
pooled ? IN_PLACE : ( this.meshes?.[ m ]?.matrixWorld?.elements ?? null ),
|
|
1059
1132
|
ranges[ sourceOf( m ) ].expandedStart,
|
|
1060
|
-
sourceOf( m )
|
|
1061
|
-
pooled ? m * 16 : 0
|
|
1133
|
+
sourceOf( m )
|
|
1062
1134
|
);
|
|
1063
1135
|
|
|
1064
1136
|
}
|
|
@@ -1081,13 +1153,35 @@ export class SceneProcessor {
|
|
|
1081
1153
|
// overhead (one extra leaf fetch per ray) is negligible and eliminates
|
|
1082
1154
|
// a dedicated visibility storage buffer binding.
|
|
1083
1155
|
this.instanceTable.computeAABBs( this.triangles, { owners: ! spill } );
|
|
1156
|
+
this.instanceTable.computeGroupAABBs();
|
|
1157
|
+
if ( clustered ) {
|
|
1158
|
+
|
|
1159
|
+
// Each entry's world box picks its cluster and sizes its byte box; dropped once the leaves are written.
|
|
1160
|
+
table.clusterWorld = new Float32Array( table.entryCount * 6 );
|
|
1161
|
+
table.writeEntryWorldAABBs( table.clusterWorld );
|
|
1162
|
+
table.clusterBounds = table.formClusters( table.clusterWorld );
|
|
1163
|
+
|
|
1164
|
+
}
|
|
1084
1165
|
|
|
1085
1166
|
// Node count is exact up front (every leaf holds one entry), so BLAS offsets can be
|
|
1086
1167
|
// assigned before the build and the TLAS is written in a single pass.
|
|
1087
|
-
this.instanceTable.assignOffsets( TLASBuilder.nodeCountFor(
|
|
1168
|
+
this.instanceTable.assignOffsets( TLASBuilder.nodeCountFor( tlasEntries ) );
|
|
1088
1169
|
const totalNodes = this.instanceTable.totalNodeCount;
|
|
1089
1170
|
|
|
1090
1171
|
let tlasData = await this._buildTLAS( table );
|
|
1172
|
+
if ( this._streaming && clustered ) {
|
|
1173
|
+
|
|
1174
|
+
const world = table.clusterWorld;
|
|
1175
|
+
this._spillMeshBounds = this._instancedMeshBounds( ( e, out ) => {
|
|
1176
|
+
|
|
1177
|
+
for ( let k = 0; k < 6; k ++ ) out[ k ] = world[ e * 6 + k ];
|
|
1178
|
+
|
|
1179
|
+
} );
|
|
1180
|
+
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
table.clusterWorld = null;
|
|
1184
|
+
table.clusterBounds = null;
|
|
1091
1185
|
this.performanceMetrics.tlasBuildTime = performance.now() - tlasStart;
|
|
1092
1186
|
|
|
1093
1187
|
// Assemble combined buffer: [TLAS][BLAS_0][BLAS_1]...[BLAS_M]
|
|
@@ -1099,8 +1193,9 @@ export class SceneProcessor {
|
|
|
1099
1193
|
// reaches them and each BLAS is released as it lands, never both fully resident.
|
|
1100
1194
|
if ( ! spill ) this._checkAssemblyHeadroom( totalNodes );
|
|
1101
1195
|
this._setBVHData( ChunkedRecords.lazy( totalNodes, 16, Float32Array, undefined, SHARED_MEMORY_AVAILABLE ) );
|
|
1102
|
-
const flushBVH = spill ? await this._beginBVHSpill( spill, table.
|
|
1196
|
+
const flushBVH = spill ? await this._beginBVHSpill( spill, table.blasBase ) : null;
|
|
1103
1197
|
this.bvh.setRecords( 0, tlasData );
|
|
1198
|
+
if ( table.groupNodeCount ) this.bvh.setRecords( table.groupNodeStart, table.groupNodeBlock() );
|
|
1104
1199
|
// Hundreds of megabytes at millions of placements, and the chunks below need the room
|
|
1105
1200
|
// more than a later refit needs the cache.
|
|
1106
1201
|
tlasData = null;
|
|
@@ -1117,7 +1212,7 @@ export class SceneProcessor {
|
|
|
1117
1212
|
try {
|
|
1118
1213
|
|
|
1119
1214
|
const readStart = performance.now();
|
|
1120
|
-
await readNodesInto( restored.nodes, this.bvh, table.
|
|
1215
|
+
await readNodesInto( restored.nodes, this.bvh, table.blasBase, table.totalBLASNodes );
|
|
1121
1216
|
this.performanceMetrics.blasRestore.readMs = performance.now() - readStart;
|
|
1122
1217
|
|
|
1123
1218
|
} finally {
|
|
@@ -1153,11 +1248,13 @@ export class SceneProcessor {
|
|
|
1153
1248
|
|
|
1154
1249
|
}
|
|
1155
1250
|
|
|
1251
|
+
if ( clustered ) await this._writeCopyRecords( flushBVH );
|
|
1156
1252
|
await flushBVH?.( totalNodes, true );
|
|
1157
1253
|
if ( spill ) await spill.blasStore.dispose();
|
|
1158
1254
|
|
|
1159
1255
|
}
|
|
1160
1256
|
|
|
1257
|
+
if ( clustered && restored ) await this._writeCopyRecords( null );
|
|
1161
1258
|
this._setBVHData( this.bvh.materializeAll() );
|
|
1162
1259
|
|
|
1163
1260
|
if ( restored ) for ( const [ t, map ] of restored.orders ) table.bvhToOriginal.set( t, map );
|
|
@@ -1307,7 +1404,7 @@ export class SceneProcessor {
|
|
|
1307
1404
|
* @returns {Promise<?{results: Array, orders: Map<number, Uint32Array>, nodes: File, release: function(): void}>}
|
|
1308
1405
|
* @private
|
|
1309
1406
|
*/
|
|
1310
|
-
async _restoreBLASes( owners,
|
|
1407
|
+
async _restoreBLASes( owners, blasBase, workerOpts ) {
|
|
1311
1408
|
|
|
1312
1409
|
if ( ! this._sceneKey || this.config.sceneCache === false ) return null;
|
|
1313
1410
|
const storage = sharedStorage( getAssetConfig().cacheNamespace );
|
|
@@ -1326,9 +1423,10 @@ export class SceneProcessor {
|
|
|
1326
1423
|
};
|
|
1327
1424
|
|
|
1328
1425
|
const { index } = cached;
|
|
1329
|
-
|
|
1426
|
+
// `tlasNodeCount` is where the stored BLASes start: past the TLAS and any group trees.
|
|
1427
|
+
if ( index.tlasNodeCount !== blasBase || index.templates.length !== owners.length ) return miss( 'layout changed' );
|
|
1330
1428
|
|
|
1331
|
-
let offset =
|
|
1429
|
+
let offset = blasBase;
|
|
1332
1430
|
for ( let i = 0; i < owners.length; i ++ ) {
|
|
1333
1431
|
|
|
1334
1432
|
const [ t, triOffset, triCount, nodeCount, blasOffset ] = index.templates[ i ];
|
|
@@ -1479,7 +1577,7 @@ export class SceneProcessor {
|
|
|
1479
1577
|
|
|
1480
1578
|
} );
|
|
1481
1579
|
|
|
1482
|
-
const index = { v: BLAS_CACHE_FORMAT, tlasNodeCount: table.
|
|
1580
|
+
const index = { v: BLAS_CACHE_FORMAT, tlasNodeCount: table.blasBase, totalNodes: table.treeNodeCount, templates };
|
|
1483
1581
|
const stored = await saveBLASCache( storage, key, { index, bvh: this.bvh, orders, isStale, label } );
|
|
1484
1582
|
if ( stored ) log.debug( `stored ${fmt.n( owners.length )} BLASes for the next load` );
|
|
1485
1583
|
return stored;
|
|
@@ -1524,7 +1622,7 @@ export class SceneProcessor {
|
|
|
1524
1622
|
return ( m?.emissiveIntensity ?? 0 ) > 0 && !! m.emissive && ( m.emissive.r > 0 || m.emissive.g > 0 || m.emissive.b > 0 );
|
|
1525
1623
|
|
|
1526
1624
|
},
|
|
1527
|
-
uploadTriangles: uploader( tri, 'triangles' ),
|
|
1625
|
+
uploadTriangles: uploader( tri, 'triangles', { textureCoordinates: this.textureCoordinates } ),
|
|
1528
1626
|
chain: Promise.resolve(),
|
|
1529
1627
|
error: null,
|
|
1530
1628
|
};
|
|
@@ -1589,7 +1687,7 @@ export class SceneProcessor {
|
|
|
1589
1687
|
const tri = this.triangles;
|
|
1590
1688
|
const chunk = tri.chunks[ k ];
|
|
1591
1689
|
if ( ! chunk ) return;
|
|
1592
|
-
state.uploadTriangles( k );
|
|
1690
|
+
await state.uploadTriangles( k );
|
|
1593
1691
|
|
|
1594
1692
|
const lanes = tri.lanesPerRecord;
|
|
1595
1693
|
for ( let o = TRIANGLE_DATA_LAYOUT.MATERIAL_FLAGS_OFFSET; o < chunk.length; o += lanes ) {
|
|
@@ -1636,6 +1734,7 @@ export class SceneProcessor {
|
|
|
1636
1734
|
const upload = uploader( bvh );
|
|
1637
1735
|
const tlasChunks = Math.ceil( tlasNodeCount / bvh.recordsPerChunk );
|
|
1638
1736
|
if ( bvhStore ) this._spillStores.push( bvhStore );
|
|
1737
|
+
this._bvhSpillStore = bvhStore;
|
|
1639
1738
|
let next = tlasChunks;
|
|
1640
1739
|
|
|
1641
1740
|
return async ( filledTo, last = false ) => {
|
|
@@ -1643,7 +1742,7 @@ export class SceneProcessor {
|
|
|
1643
1742
|
while ( next < bvh.chunks.length && ( last || ( next + 1 ) * bvh.recordsPerChunk <= filledTo ) ) {
|
|
1644
1743
|
|
|
1645
1744
|
if ( ! bvh.chunks[ next ] ) bvh._materialize( next );
|
|
1646
|
-
upload( next );
|
|
1745
|
+
await upload( next );
|
|
1647
1746
|
if ( bvhStore ) await bvh.spillChunk( next, bvhStore );
|
|
1648
1747
|
next ++;
|
|
1649
1748
|
|
|
@@ -1652,7 +1751,7 @@ export class SceneProcessor {
|
|
|
1652
1751
|
if ( last ) for ( let k = 0; k < Math.min( tlasChunks, bvh.chunks.length ); k ++ ) {
|
|
1653
1752
|
|
|
1654
1753
|
if ( ! bvh.chunks[ k ] ) bvh._materialize( k );
|
|
1655
|
-
upload( k );
|
|
1754
|
+
await upload( k );
|
|
1656
1755
|
|
|
1657
1756
|
}
|
|
1658
1757
|
|
|
@@ -2217,6 +2316,10 @@ export class SceneProcessor {
|
|
|
2217
2316
|
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2218
2317
|
this._disposeSpill();
|
|
2219
2318
|
this._disposeGeometrySpill();
|
|
2319
|
+
this._disposeMatrixSpill();
|
|
2320
|
+
this._orderSpill?.store.dispose().catch( () => {} );
|
|
2321
|
+
this._orderSpill = null;
|
|
2322
|
+
this._spilledAfterLoad = false;
|
|
2220
2323
|
|
|
2221
2324
|
// First dispose any existing resources
|
|
2222
2325
|
this._disposeTextures();
|
|
@@ -2569,7 +2672,6 @@ export class SceneProcessor {
|
|
|
2569
2672
|
async refitBVH( newPositions, newNormals ) {
|
|
2570
2673
|
|
|
2571
2674
|
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2572
|
-
this._tlasLeafBoxesOf = null;
|
|
2573
2675
|
|
|
2574
2676
|
if ( ! this.bvh || ! this.triangles || ! this.instanceTable ) {
|
|
2575
2677
|
|
|
@@ -2673,7 +2775,11 @@ export class SceneProcessor {
|
|
|
2673
2775
|
};
|
|
2674
2776
|
|
|
2675
2777
|
// Signal worker — no data transfer needed, everything is in shared memory
|
|
2676
|
-
this._refitWorker.postMessage( {
|
|
2778
|
+
this._refitWorker.postMessage( {
|
|
2779
|
+
type: 'refit', blasRanges: partial ? blasRanges : null, tlasNodeCount,
|
|
2780
|
+
groupRange: table.groupNodeCount ? [ table.groupNodeStart, table.groupNodeCount ] : null,
|
|
2781
|
+
treeNodeCount: table.treeNodeCount, recordNodeStart: table.clusterCount ? table.recordNodeStart : - 1,
|
|
2782
|
+
} );
|
|
2677
2783
|
|
|
2678
2784
|
} );
|
|
2679
2785
|
|
|
@@ -2793,7 +2899,8 @@ export class SceneProcessor {
|
|
|
2793
2899
|
|
|
2794
2900
|
}
|
|
2795
2901
|
|
|
2796
|
-
// Step 2:
|
|
2902
|
+
// Step 2: group trees over the refit BLASes, then the TLAS AABBs in place (no SAH rebuild)
|
|
2903
|
+
this._writeGroupNodes();
|
|
2797
2904
|
this._refitTLAS();
|
|
2798
2905
|
|
|
2799
2906
|
return { refitTimeMs: performance.now() - start };
|
|
@@ -2823,11 +2930,9 @@ export class SceneProcessor {
|
|
|
2823
2930
|
const start = performance.now();
|
|
2824
2931
|
const table = this.instanceTable;
|
|
2825
2932
|
const composed = this._transformScratch ??= new Float32Array( 16 );
|
|
2826
|
-
|
|
2827
|
-
const movedMask = this._movedPlacements;
|
|
2828
|
-
let moved = 0;
|
|
2933
|
+
const leaves = [];
|
|
2829
2934
|
|
|
2830
|
-
for ( const meshIndex of meshIndices ) {
|
|
2935
|
+
for ( const meshIndex of this._withGroupSiblings( meshIndices ) ) {
|
|
2831
2936
|
|
|
2832
2937
|
const mesh = this.meshes?.[ meshIndex ];
|
|
2833
2938
|
const run = table.placementRunOf( meshIndex );
|
|
@@ -2835,7 +2940,7 @@ export class SceneProcessor {
|
|
|
2835
2940
|
|
|
2836
2941
|
mesh.updateMatrixWorld( true );
|
|
2837
2942
|
const world = mesh.matrixWorld.elements;
|
|
2838
|
-
const instances = mesh.isInstancedMesh ?
|
|
2943
|
+
const instances = mesh.isInstancedMesh ? mesh.instanceMatrix?.array ?? null : null;
|
|
2839
2944
|
// Baked triangles hold the pose they were extracted at; the leaf only carries the delta.
|
|
2840
2945
|
const bakeInverse = table.tplBakeInverse?.get( table.sourceMesh[ run.start ] ) ?? null;
|
|
2841
2946
|
|
|
@@ -2861,61 +2966,159 @@ export class SceneProcessor {
|
|
|
2861
2966
|
}
|
|
2862
2967
|
|
|
2863
2968
|
this._writeLeafMatrix( p );
|
|
2864
|
-
|
|
2865
|
-
|
|
2969
|
+
const leaf = table.leafOfPlacement( p );
|
|
2970
|
+
if ( leaf >= 0 ) leaves.push( leaf );
|
|
2866
2971
|
|
|
2867
2972
|
}
|
|
2868
2973
|
|
|
2869
2974
|
}
|
|
2870
2975
|
|
|
2871
|
-
this._refitTLAS(
|
|
2872
|
-
movedMask.fill( 0 );
|
|
2976
|
+
this._refitTLAS( leaves );
|
|
2873
2977
|
|
|
2874
|
-
return { refitTimeMs: performance.now() - start, placements:
|
|
2978
|
+
return { refitTimeMs: performance.now() - start, placements: leaves.length };
|
|
2875
2979
|
|
|
2876
2980
|
}
|
|
2877
2981
|
|
|
2878
2982
|
/**
|
|
2879
|
-
*
|
|
2880
|
-
*
|
|
2881
|
-
* The extractor points a host-at-origin InstancedMesh straight at the pool, since world and
|
|
2882
|
-
* instance matrices hold identical bytes there. Composing a moved host back into the pool would
|
|
2883
|
-
* overwrite the matrices it just read, so the next move would compose onto its own result.
|
|
2983
|
+
* A grouped mesh's parts are traced as one object, so a part moved alone takes its siblings with it: each sibling
|
|
2984
|
+
* is given the first moved member's world transform, and all of them are moved.
|
|
2884
2985
|
* @private
|
|
2885
2986
|
*/
|
|
2886
|
-
|
|
2987
|
+
_withGroupSiblings( meshIndices ) {
|
|
2988
|
+
|
|
2989
|
+
const table = this.instanceTable;
|
|
2990
|
+
if ( ! table.groups.length ) return meshIndices;
|
|
2991
|
+
|
|
2992
|
+
const out = new Set( meshIndices );
|
|
2993
|
+
const synced = new Set();
|
|
2994
|
+
const parentInverse = this._parentInverse ??= new Matrix4();
|
|
2995
|
+
|
|
2996
|
+
for ( const meshIndex of meshIndices ) {
|
|
2997
|
+
|
|
2998
|
+
const g = table.groupOfTemplate[ meshIndex ] ?? - 1;
|
|
2999
|
+
if ( g < 0 || synced.has( g ) ) continue;
|
|
3000
|
+
synced.add( g );
|
|
2887
3001
|
|
|
2888
|
-
|
|
2889
|
-
|
|
3002
|
+
const mover = this.meshes?.[ meshIndex ];
|
|
3003
|
+
if ( ! mover ) continue;
|
|
3004
|
+
mover.updateMatrixWorld( true );
|
|
2890
3005
|
|
|
2891
|
-
|
|
3006
|
+
for ( const sibling of table.groups[ g ].members ) {
|
|
2892
3007
|
|
|
2893
|
-
|
|
2894
|
-
|
|
3008
|
+
out.add( sibling );
|
|
3009
|
+
const mesh = this.meshes[ sibling ];
|
|
3010
|
+
if ( ! mesh || mesh === mover ) continue;
|
|
3011
|
+
if ( mesh.parent ) mesh.parent.updateMatrixWorld( true );
|
|
3012
|
+
mesh.matrix.copy( mover.matrixWorld );
|
|
3013
|
+
if ( mesh.parent ) mesh.matrix.premultiply( parentInverse.copy( mesh.parent.matrixWorld ).invert() );
|
|
3014
|
+
mesh.matrix.decompose( mesh.position, mesh.quaternion, mesh.scale );
|
|
3015
|
+
mesh.updateMatrixWorld( true );
|
|
3016
|
+
|
|
3017
|
+
}
|
|
2895
3018
|
|
|
2896
3019
|
}
|
|
2897
3020
|
|
|
2898
|
-
return
|
|
3021
|
+
return out;
|
|
2899
3022
|
|
|
2900
3023
|
}
|
|
2901
3024
|
|
|
2902
|
-
/** Refresh one TLAS leaf's world-to-object matrix from the table. @private */
|
|
3025
|
+
/** Refresh one TLAS leaf's world-to-object matrix from the table — its copy record, when clustered. @private */
|
|
2903
3026
|
_writeLeafMatrix( placement ) {
|
|
2904
3027
|
|
|
2905
|
-
const
|
|
3028
|
+
const table = this.instanceTable;
|
|
3029
|
+
const node = table.tlasLeafIndex[ placement ];
|
|
2906
3030
|
if ( node < 0 ) return;
|
|
2907
3031
|
|
|
3032
|
+
if ( table.clusterCount ) {
|
|
3033
|
+
|
|
3034
|
+
this._writeCopyRecord( node, table.repOf( table.entryOf( placement ) ) );
|
|
3035
|
+
return;
|
|
3036
|
+
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2908
3039
|
TLASBuilder.writeLeafMatrix(
|
|
2909
3040
|
this.bvh.chunkFor( node ), this.bvhIndexChunks.chunkFor( node ), this.bvh.baseOf( node ),
|
|
2910
|
-
|
|
3041
|
+
table, placement, table.entryOf( placement )
|
|
2911
3042
|
);
|
|
2912
3043
|
|
|
2913
3044
|
}
|
|
2914
3045
|
|
|
2915
|
-
/**
|
|
3046
|
+
/**
|
|
3047
|
+
* Rewrites copy record `r` from placement `p`'s transform, and its identity bit (a moved baked placement stops being
|
|
3048
|
+
* identity); records the range for the next upload. @private
|
|
3049
|
+
*/
|
|
3050
|
+
_writeCopyRecord( r, p ) {
|
|
3051
|
+
|
|
3052
|
+
const table = this.instanceTable;
|
|
3053
|
+
const rows = this._recordScratch ??= new Float32Array( 12 );
|
|
3054
|
+
table.writeRecord( rows, 0, p );
|
|
3055
|
+
const lane = table.recordNodeStart * 16 + r * RECORD_VEC4 * 4;
|
|
3056
|
+
for ( let i = 0; i < 12; i ++ ) {
|
|
3057
|
+
|
|
3058
|
+
const node = ( lane + i ) >> 4;
|
|
3059
|
+
this.bvh.chunkFor( node )[ this.bvh.baseOf( node ) + ( ( lane + i ) & 15 ) ] = rows[ i ];
|
|
3060
|
+
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
const leaf = table.clusterLeaf[ table.clusterOf( r ) ];
|
|
3064
|
+
const idx = this.bvhIndexChunks.chunkFor( leaf ), o = this.bvh.baseOf( leaf ) + table.copyWordSlot( r );
|
|
3065
|
+
const identity = isIdentity( table.matrixWorldOf( p ) );
|
|
3066
|
+
idx[ o ] = ( ( idx[ o ] & ~ TLAS_LEAF_IDENTITY ) | ( identity ? TLAS_LEAF_IDENTITY : 0 ) ) >>> 0;
|
|
3067
|
+
|
|
3068
|
+
// One range a chunk: a mesh's copies sit all over the records, and a spilled scene has only their chunks back.
|
|
3069
|
+
const moved = this._movedRecords ??= new Map();
|
|
3070
|
+
const lanesPerChunk = this.bvh.recordsPerChunk * 16;
|
|
3071
|
+
for ( let at = lane, end = lane + 12; at < end; ) {
|
|
3072
|
+
|
|
3073
|
+
const k = Math.floor( at / lanesPerChunk ), stop = Math.min( end, ( k + 1 ) * lanesPerChunk );
|
|
3074
|
+
const range = moved.get( k );
|
|
3075
|
+
if ( range ) {
|
|
3076
|
+
|
|
3077
|
+
range.lo = Math.min( range.lo, at );
|
|
3078
|
+
range.hi = Math.max( range.hi, stop );
|
|
3079
|
+
|
|
3080
|
+
} else moved.set( k, { lo: at, hi: stop } );
|
|
3081
|
+
at = stop;
|
|
3082
|
+
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
/**
|
|
3088
|
+
* What the last {@link updateMeshTransforms} rewrote, as float ranges of the BVH for an upload: the TLAS nodes its
|
|
3089
|
+
* refit touched (all of them after a full refit) and the copy records.
|
|
3090
|
+
*/
|
|
3091
|
+
takeMoveRanges() {
|
|
3092
|
+
|
|
3093
|
+
const tlas = this._tlasRanges ?? [ this.computeTLASDirtyRange() ];
|
|
3094
|
+
this._tlasRanges = null;
|
|
3095
|
+
return [ ...tlas, ...this.takeMovedRecordRanges() ];
|
|
3096
|
+
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
/** The copy records rewritten since the last call, as float ranges of the BVH for an upload, one a chunk. */
|
|
3100
|
+
takeMovedRecordRanges() {
|
|
3101
|
+
|
|
3102
|
+
const moved = this._movedRecords;
|
|
3103
|
+
this._movedRecords = null;
|
|
3104
|
+
return moved ? [ ...moved.values() ].map( r => ( { offset: r.lo, count: r.hi - r.lo } ) ) : [];
|
|
3105
|
+
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
/** Rewrites the group trees from the members' current bounds and visibility. @private */
|
|
3109
|
+
_writeGroupNodes() {
|
|
3110
|
+
|
|
3111
|
+
const table = this.instanceTable;
|
|
3112
|
+
if ( ! table?.groupNodeCount || ! this.bvh ) return;
|
|
3113
|
+
table.computeGroupAABBs();
|
|
3114
|
+
this.bvh.setRecords( table.groupNodeStart, table.groupNodeBlock() );
|
|
3115
|
+
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
/** The TLAS and the group trees occupy the front of the BVH buffer, the only part a move rewrites. */
|
|
2916
3119
|
computeTLASDirtyRange() {
|
|
2917
3120
|
|
|
2918
|
-
return { offset: 0, count: this.instanceTable.
|
|
3121
|
+
return { offset: 0, count: this.instanceTable.blasBase * 16 };
|
|
2919
3122
|
|
|
2920
3123
|
}
|
|
2921
3124
|
|
|
@@ -2944,8 +3147,8 @@ export class SceneProcessor {
|
|
|
2944
3147
|
|
|
2945
3148
|
}
|
|
2946
3149
|
|
|
2947
|
-
// Always include TLAS
|
|
2948
|
-
bvhRanges.push( { offset: 0, count: this.instanceTable.
|
|
3150
|
+
// Always include the TLAS and group trees (rebuilt on every refit)
|
|
3151
|
+
bvhRanges.push( { offset: 0, count: this.instanceTable.blasBase * FPN } );
|
|
2949
3152
|
|
|
2950
3153
|
return { triRanges, bvhRanges };
|
|
2951
3154
|
|
|
@@ -2962,6 +3165,24 @@ export class SceneProcessor {
|
|
|
2962
3165
|
* @param {{keepUserLights?: boolean}} [options] - an incremental rebuild keeps the lights a host added
|
|
2963
3166
|
* @returns {boolean} false if critical data is missing
|
|
2964
3167
|
*/
|
|
3168
|
+
/**
|
|
3169
|
+
* Uploads the triangle and BVH stores chunk by chunk, each awaited ({@link PathTracerStage#drainUploads}), so
|
|
3170
|
+
* {@link uploadToPathTracer} adopts them rather than send a scene's worth at once. A store of one chunk, or one a
|
|
3171
|
+
* spilling build already uploaded, is left to it.
|
|
3172
|
+
* @param {function(Object, string=, Object=): function(number): Promise<void>} uploader
|
|
3173
|
+
*/
|
|
3174
|
+
async uploadChunks( uploader ) {
|
|
3175
|
+
|
|
3176
|
+
for ( const [ records, kind ] of [[ this.triangles, 'triangles' ], [ this.bvh, 'bvh' ]] ) {
|
|
3177
|
+
|
|
3178
|
+
if ( ! records?.chunks || records.chunks.length < 2 || records._gpuUpload ) continue;
|
|
3179
|
+
const upload = uploader( records, kind, { textureCoordinates: this.textureCoordinates } );
|
|
3180
|
+
for ( let k = 0; k < records.chunks.length; k ++ ) if ( records.chunks[ k ] ) await upload( k );
|
|
3181
|
+
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
}
|
|
3185
|
+
|
|
2965
3186
|
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture, { keepUserLights = false } = {} ) {
|
|
2966
3187
|
|
|
2967
3188
|
if ( ! this.triangles ) {
|
|
@@ -2971,7 +3192,7 @@ export class SceneProcessor {
|
|
|
2971
3192
|
|
|
2972
3193
|
}
|
|
2973
3194
|
|
|
2974
|
-
pathTracer.setTriangleData( this.triangles, this.triangleCount );
|
|
3195
|
+
pathTracer.setTriangleData( this.triangles, this.triangleCount, { textureCoordinates: this.textureCoordinates } );
|
|
2975
3196
|
|
|
2976
3197
|
if ( ! this.bvh ) {
|
|
2977
3198
|
|
|
@@ -3160,7 +3381,7 @@ export class SceneProcessor {
|
|
|
3160
3381
|
/** Whether any triangle records or BLAS nodes are on disk (see {@link spillToDisk}). */
|
|
3161
3382
|
get spilled() {
|
|
3162
3383
|
|
|
3163
|
-
return !! ( this.triangles?.spilledChunks || this.bvh?.spilledChunks );
|
|
3384
|
+
return !! ( this.triangles?.spilledChunks || this.bvh?.spilledChunks || this._orderSpill );
|
|
3164
3385
|
|
|
3165
3386
|
}
|
|
3166
3387
|
|
|
@@ -3194,7 +3415,7 @@ export class SceneProcessor {
|
|
|
3194
3415
|
}
|
|
3195
3416
|
|
|
3196
3417
|
this._spillStores = [ triStore, bvhStore ];
|
|
3197
|
-
const tlasChunks = Math.ceil( ( this.instanceTable?.
|
|
3418
|
+
const tlasChunks = Math.ceil( ( this.instanceTable?.blasBase ?? 1 ) / bvh.recordsPerChunk );
|
|
3198
3419
|
|
|
3199
3420
|
let bytes = await triangles.spill( triStore );
|
|
3200
3421
|
bytes += await bvh.spill( bvhStore, { keep: k => k < tlasChunks } );
|
|
@@ -3208,13 +3429,14 @@ export class SceneProcessor {
|
|
|
3208
3429
|
|
|
3209
3430
|
}
|
|
3210
3431
|
|
|
3211
|
-
/** Reads every spilled triangle record
|
|
3432
|
+
/** Reads every spilled triangle record, BLAS node and order map back, then drops the scratch files. */
|
|
3212
3433
|
ensureResident() {
|
|
3213
3434
|
|
|
3214
3435
|
this._paging ??= ( async () => {
|
|
3215
3436
|
|
|
3216
3437
|
await this.triangles?.ensureResident?.();
|
|
3217
3438
|
await this.bvh?.ensureResident?.();
|
|
3439
|
+
await this._restoreOrderMaps();
|
|
3218
3440
|
if ( ! this.spilled ) this._disposeSpill();
|
|
3219
3441
|
|
|
3220
3442
|
} )().finally( () => {
|
|
@@ -3227,12 +3449,350 @@ export class SceneProcessor {
|
|
|
3227
3449
|
|
|
3228
3450
|
}
|
|
3229
3451
|
|
|
3452
|
+
/** Whether the model's three.js geometry is on disk: its arrays are empty until {@link ensureGeometryResident}. */
|
|
3453
|
+
get geometryOnDisk() {
|
|
3454
|
+
|
|
3455
|
+
return !! this._geometryOwner;
|
|
3456
|
+
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
/** Reads back the three.js geometry and matrix lists a spilling build left on disk. */
|
|
3460
|
+
ensureGeometryResident() {
|
|
3461
|
+
|
|
3462
|
+
const geometry = this._geometryOwner ? this._geometryReading ??= this._restoreGeometry( this._geometryOwner ).finally( () => {
|
|
3463
|
+
|
|
3464
|
+
this._geometryReading = null;
|
|
3465
|
+
|
|
3466
|
+
} ) : null;
|
|
3467
|
+
return Promise.all( [ geometry, this.ensureMovable() ] );
|
|
3468
|
+
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
/** Whether the InstancedMesh matrix lists the instance table reads are on disk (a move reads them back). */
|
|
3472
|
+
get matricesOnDisk() {
|
|
3473
|
+
|
|
3474
|
+
return !! this._matrixSpill;
|
|
3475
|
+
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
/**
|
|
3479
|
+
* Reads back what moving `meshIndices` needs that a spilling build left on disk: the matrix lists, the TLAS and the
|
|
3480
|
+
* copy records of those meshes. Without meshes, the matrix lists alone.
|
|
3481
|
+
*/
|
|
3482
|
+
async ensureMovable( meshIndices = [] ) {
|
|
3483
|
+
|
|
3484
|
+
this._movePageIns = ( this._movePageIns ?? 0 ) + 1;
|
|
3485
|
+
try {
|
|
3486
|
+
|
|
3487
|
+
await this._afterLoadSpill;
|
|
3488
|
+
const spill = this._matrixSpill;
|
|
3489
|
+
if ( spill ) await ( this._matrixReading ??= spill.restore().then( () => this._disposeMatrixSpill() ).finally( () => {
|
|
3490
|
+
|
|
3491
|
+
this._matrixReading = null;
|
|
3492
|
+
|
|
3493
|
+
} ) );
|
|
3494
|
+
|
|
3495
|
+
const table = this.instanceTable, bvh = this.bvh;
|
|
3496
|
+
if ( ! meshIndices.length || ! table || ! bvh?.spilledChunks ) return;
|
|
3497
|
+
await bvh.ensureResident( 0, table.blasBase );
|
|
3498
|
+
for ( const k of this._recordChunksOf( meshIndices ) ) await bvh.ensureResident( k * bvh.recordsPerChunk, 1 );
|
|
3499
|
+
|
|
3500
|
+
} finally {
|
|
3501
|
+
|
|
3502
|
+
this._movePageIns --;
|
|
3503
|
+
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
/**
|
|
3509
|
+
* A spilling build's matrix lists go to disk as well — 2.4 GB on the Moana island; only a move reads them. Each
|
|
3510
|
+
* InstancedMesh gets its bounds from its copies' boxes first, so three.js never derives them from the empty list;
|
|
3511
|
+
* `again`, only one without bounds yet.
|
|
3512
|
+
* @private
|
|
3513
|
+
*/
|
|
3514
|
+
async _spillMatrices( again = false ) {
|
|
3515
|
+
|
|
3516
|
+
const table = this.instanceTable;
|
|
3517
|
+
const lists = table?.adoptedMatrices() ?? [];
|
|
3518
|
+
const bounds = this._spillMeshBounds;
|
|
3519
|
+
this._spillMeshBounds = null;
|
|
3520
|
+
if ( ! lists.length || ! this._progressive?.storage ) return;
|
|
3521
|
+
|
|
3522
|
+
const spill = await GeometrySpill.create( this._progressive.storage, `spill:${Date.now().toString( 36 )}:${Math.random().toString( 36 ).slice( 2 )}:mat` );
|
|
3523
|
+
if ( ! spill ) return;
|
|
3524
|
+
|
|
3525
|
+
for ( const [ mesh, b ] of bounds ?? this._instancedMeshBounds( ( e, out ) => table.writeEntryWorldAABB( e, out, 0 ), again ) ) {
|
|
3526
|
+
|
|
3527
|
+
mesh.boundingBox = new Box3( new Vector3( b[ 0 ], b[ 1 ], b[ 2 ] ), new Vector3( b[ 3 ], b[ 4 ], b[ 5 ] ) );
|
|
3528
|
+
mesh.boundingSphere = mesh.boundingBox.getBoundingSphere( new Sphere() );
|
|
3529
|
+
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
const bytes = lists.reduce( ( n, a ) => n + a.array.byteLength, 0 );
|
|
3533
|
+
spill.addAttributes( lists );
|
|
3534
|
+
this._matrixSpill = spill;
|
|
3535
|
+
log.info( `${fmt.mb( bytes )} of instance matrices left for disk` );
|
|
3536
|
+
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
/**
|
|
3540
|
+
* A spilling build's TLAS goes to disk too when copies are clustered — 1.3 GB on the Moana island — and any chunk a
|
|
3541
|
+
* move read back (its copy records), keeping only the chunks over the group trees, which a visibility change
|
|
3542
|
+
* rewrites. An edit reads them back first (the stage's `_whenTLASResident`, {@link ensureMovable}).
|
|
3543
|
+
* @private
|
|
3544
|
+
*/
|
|
3545
|
+
async _spillTLAS() {
|
|
3546
|
+
|
|
3547
|
+
const table = this.instanceTable, bvh = this.bvh, store = this._bvhSpillStore;
|
|
3548
|
+
if ( ! store || ! table?.clusterCount ) return;
|
|
3549
|
+
const per = bvh.recordsPerChunk;
|
|
3550
|
+
const groupFirst = ( table.groupNodeStart / per ) | 0;
|
|
3551
|
+
const groupLast = ( ( table.groupNodeStart + Math.max( 1, table.groupNodeCount ) - 1 ) / per ) | 0;
|
|
3552
|
+
this._spilledRootBounds = this.sceneBounds();
|
|
3553
|
+
const bytes = await bvh.spill( store, { keep: k => table.groupNodeCount > 0 && k >= groupFirst && k <= groupLast } );
|
|
3554
|
+
log.info( `spilled ${fmt.mb( bytes )} of TLAS to disk` );
|
|
3555
|
+
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
/**
|
|
3559
|
+
* A spilling build's triangle order maps go to disk too (0.43 GB on the Moana island): only refits and BLAS swaps
|
|
3560
|
+
* read them, after {@link ensureResident}, which reads them back. Packed into 32 MB writes.
|
|
3561
|
+
* @private
|
|
3562
|
+
*/
|
|
3563
|
+
async _spillOrderMaps() {
|
|
3564
|
+
|
|
3565
|
+
const table = this.instanceTable, storage = this._progressive?.storage;
|
|
3566
|
+
if ( ! table?.bvhToOriginal.size || ! storage ) return;
|
|
3567
|
+
let bytes = 0;
|
|
3568
|
+
for ( const map of table.bvhToOriginal.values() ) bytes += map.byteLength;
|
|
3569
|
+
const store = await SpillStore.create( storage, `spill:${Date.now().toString( 36 )}:${Math.random().toString( 36 ).slice( 2 )}:order`, 0, { label: 'Triangle order maps', expectedBytes: bytes } );
|
|
3570
|
+
if ( ! store ) return;
|
|
3571
|
+
|
|
3572
|
+
const index = [], pack = new Uint8Array( ORDER_PACK_BYTES );
|
|
3573
|
+
let at = 0, packed = 0;
|
|
3574
|
+
const flush = async () => {
|
|
3575
|
+
|
|
3576
|
+
if ( packed ) await store.writeAt( at - packed, pack.subarray( 0, packed ) );
|
|
3577
|
+
packed = 0;
|
|
3578
|
+
|
|
3579
|
+
};
|
|
3580
|
+
|
|
3581
|
+
for ( const [ t, map ] of table.bvhToOriginal ) {
|
|
3582
|
+
|
|
3583
|
+
const data = new Uint8Array( map.buffer, map.byteOffset, map.byteLength );
|
|
3584
|
+
index.push( t, at, map.length );
|
|
3585
|
+
if ( packed + data.length > pack.length ) await flush();
|
|
3586
|
+
if ( data.length > pack.length ) await store.writeAt( at, data );
|
|
3587
|
+
else {
|
|
3588
|
+
|
|
3589
|
+
pack.set( data, packed );
|
|
3590
|
+
packed += data.length;
|
|
3591
|
+
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
at += data.length;
|
|
3595
|
+
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
await flush();
|
|
3599
|
+
await store.flush();
|
|
3600
|
+
table.bvhToOriginal.clear();
|
|
3601
|
+
this._orderSpill = { store, index, bytes };
|
|
3602
|
+
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
/** Reads the order maps {@link _spillOrderMaps} moved to disk back, in 64 MB windows. @private */
|
|
3606
|
+
async _restoreOrderMaps() {
|
|
3607
|
+
|
|
3608
|
+
const spill = this._orderSpill;
|
|
3609
|
+
if ( ! spill ) return;
|
|
3610
|
+
this._orderSpill = null;
|
|
3611
|
+
const { store, index, bytes } = spill, maps = this.instanceTable.bvhToOriginal;
|
|
3612
|
+
let windowAt = 0, window = null;
|
|
3613
|
+
for ( let i = 0; i < index.length; i += 3 ) {
|
|
3614
|
+
|
|
3615
|
+
const t = index[ i ], at = index[ i + 1 ], length = index[ i + 2 ] * 4;
|
|
3616
|
+
if ( length > ORDER_WINDOW_BYTES ) {
|
|
3617
|
+
|
|
3618
|
+
maps.set( t, new Uint32Array( await store.readAt( at, length ) ) );
|
|
3619
|
+
continue;
|
|
3620
|
+
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
if ( ! window || at + length > windowAt + window.byteLength ) {
|
|
3624
|
+
|
|
3625
|
+
windowAt = at;
|
|
3626
|
+
window = await store.readAt( at, Math.min( ORDER_WINDOW_BYTES, bytes - at ) );
|
|
3627
|
+
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
maps.set( t, new Uint32Array( window.slice( at - windowAt, at - windowAt + length ) ) );
|
|
3631
|
+
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
await store.dispose();
|
|
3635
|
+
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
/**
|
|
3639
|
+
* Once the load no longer needs them (initial visibility, emitters), a spilling build's matrix lists, TLAS and order
|
|
3640
|
+
* maps go to disk. Edits wait for it ({@link whenTLASEditable}, {@link ensureMovable}).
|
|
3641
|
+
*/
|
|
3642
|
+
spillAfterLoad() {
|
|
3643
|
+
|
|
3644
|
+
if ( ! this._geometryOwner || this._spilledAfterLoad ) return this._afterLoadSpill ?? Promise.resolve();
|
|
3645
|
+
this._spilledAfterLoad = true;
|
|
3646
|
+
return this._spillUnused( false );
|
|
3647
|
+
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
/**
|
|
3651
|
+
* Puts back on disk what edits have read back since ({@link ensureMovable}, {@link whenTLASEditable}) — 3.7 GB after a
|
|
3652
|
+
* move on the Moana island. The renderer calls it once edits have settled; skipped while one is reading.
|
|
3653
|
+
*/
|
|
3654
|
+
respill() {
|
|
3655
|
+
|
|
3656
|
+
if ( ! this._geometryOwner || ! this._spilledAfterLoad || this._afterLoadSpill ) return this._afterLoadSpill ?? Promise.resolve();
|
|
3657
|
+
if ( this._matrixReading || this._tlasReading || this._movePageIns ) return Promise.resolve();
|
|
3658
|
+
return this._spillUnused( true );
|
|
3659
|
+
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
/** @private */
|
|
3663
|
+
_spillUnused( again ) {
|
|
3664
|
+
|
|
3665
|
+
this._afterLoadSpill = ( async () => {
|
|
3666
|
+
|
|
3667
|
+
await this._spillMatrices( again );
|
|
3668
|
+
await this._spillTLAS();
|
|
3669
|
+
await this._spillOrderMaps();
|
|
3670
|
+
|
|
3671
|
+
} )().finally( () => {
|
|
3672
|
+
|
|
3673
|
+
this._afterLoadSpill = null;
|
|
3674
|
+
|
|
3675
|
+
} );
|
|
3676
|
+
return this._afterLoadSpill;
|
|
3677
|
+
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/** Null when the TLAS can be edited in memory now, else a promise for when it can: it is being spilled, or is on disk. */
|
|
3681
|
+
whenTLASEditable() {
|
|
3682
|
+
|
|
3683
|
+
if ( this._afterLoadSpill ) return this._afterLoadSpill.then( () => this.whenTLASEditable() );
|
|
3684
|
+
const bvh = this.bvh, table = this.instanceTable;
|
|
3685
|
+
if ( ! bvh || ! table || bvh.isResident( 0, table.blasBase ) ) return null;
|
|
3686
|
+
return this._tlasReading ??= bvh.ensureResident( 0, table.blasBase ).finally( () => {
|
|
3687
|
+
|
|
3688
|
+
this._tlasReading = null;
|
|
3689
|
+
|
|
3690
|
+
} );
|
|
3691
|
+
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
/** Whether moving `meshIndices` has to read anything back from disk first ({@link ensureMovable}). */
|
|
3695
|
+
needsPageInForMove( meshIndices ) {
|
|
3696
|
+
|
|
3697
|
+
if ( this._matrixSpill || this._afterLoadSpill ) return true;
|
|
3698
|
+
const table = this.instanceTable, bvh = this.bvh;
|
|
3699
|
+
if ( ! table || ! bvh?.spilledChunks ) return false;
|
|
3700
|
+
if ( ! bvh.isResident( 0, table.blasBase ) ) return true;
|
|
3701
|
+
for ( const k of this._recordChunksOf( meshIndices ) ) if ( ! bvh.isResident( k * bvh.recordsPerChunk, 1 ) ) return true;
|
|
3702
|
+
return false;
|
|
3703
|
+
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/** The BVH chunks holding the copy records of `meshIndices` and their group siblings. @private */
|
|
3707
|
+
_recordChunksOf( meshIndices ) {
|
|
3708
|
+
|
|
3709
|
+
const table = this.instanceTable, per = this.bvh.recordsPerChunk, chunks = new Set();
|
|
3710
|
+
if ( ! table.clusterCount ) return chunks;
|
|
3711
|
+
const meshes = new Set( meshIndices );
|
|
3712
|
+
for ( const m of meshIndices ) {
|
|
3713
|
+
|
|
3714
|
+
const g = table.groupOfTemplate?.[ m ] ?? - 1;
|
|
3715
|
+
if ( g >= 0 ) for ( const member of table.groups[ g ].members ) meshes.add( member );
|
|
3716
|
+
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
for ( const m of meshes ) {
|
|
3720
|
+
|
|
3721
|
+
const run = table.placementRunOf( m );
|
|
3722
|
+
if ( ! run ) continue;
|
|
3723
|
+
for ( let p = run.start; p < run.start + run.count; p ++ ) {
|
|
3724
|
+
|
|
3725
|
+
const r = table.tlasLeafIndex[ p ];
|
|
3726
|
+
if ( r < 0 ) continue;
|
|
3727
|
+
const lane = table.recordNodeStart * 16 + r * RECORD_VEC4 * 4;
|
|
3728
|
+
chunks.add( Math.floor( lane / 16 / per ) );
|
|
3729
|
+
chunks.add( Math.floor( ( lane + 11 ) / 16 / per ) );
|
|
3730
|
+
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
return chunks;
|
|
3736
|
+
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
/**
|
|
3740
|
+
* Each InstancedMesh the table reads in place, with its world bounds: the union of its copies' boxes, `boxOf( entry,
|
|
3741
|
+
* out )`. Its host is at the origin, so world is its own space. `missingOnly` skips a mesh that has bounds.
|
|
3742
|
+
* @private
|
|
3743
|
+
*/
|
|
3744
|
+
_instancedMeshBounds( boxOf, missingOnly = false ) {
|
|
3745
|
+
|
|
3746
|
+
const table = this.instanceTable;
|
|
3747
|
+
const adopted = new Set( table.adoptedMatrices() );
|
|
3748
|
+
const out = new Map();
|
|
3749
|
+
const box = new Float64Array( 6 );
|
|
3750
|
+
( this.meshes ?? [] ).forEach( ( mesh, m ) => {
|
|
3751
|
+
|
|
3752
|
+
if ( ! mesh?.isInstancedMesh || ! adopted.has( mesh.instanceMatrix ) || ( missingOnly && mesh.boundingBox ) ) return;
|
|
3753
|
+
const run = table.placementRunOf( m );
|
|
3754
|
+
if ( ! run ) return;
|
|
3755
|
+
const b = [ Infinity, Infinity, Infinity, - Infinity, - Infinity, - Infinity ];
|
|
3756
|
+
for ( let p = run.start; p < run.start + run.count; p ++ ) {
|
|
3757
|
+
|
|
3758
|
+
boxOf( table.entryOf( p ), box );
|
|
3759
|
+
for ( let a = 0; a < 3; a ++ ) {
|
|
3760
|
+
|
|
3761
|
+
if ( box[ a ] < b[ a ] ) b[ a ] = box[ a ];
|
|
3762
|
+
if ( box[ 3 + a ] > b[ 3 + a ] ) b[ 3 + a ] = box[ 3 + a ];
|
|
3763
|
+
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
out.set( mesh, b );
|
|
3769
|
+
|
|
3770
|
+
} );
|
|
3771
|
+
return out;
|
|
3772
|
+
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/** @private */
|
|
3776
|
+
_disposeMatrixSpill() {
|
|
3777
|
+
|
|
3778
|
+
const spill = this._matrixSpill;
|
|
3779
|
+
this._matrixSpill = null;
|
|
3780
|
+
spill?.dispose().catch( () => {} );
|
|
3781
|
+
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3230
3784
|
/** Reads the three.js geometry a streamed build moved to disk back into place. @private */
|
|
3231
3785
|
async _restoreGeometry( object ) {
|
|
3232
3786
|
|
|
3233
3787
|
const spill = this._geometrySpill;
|
|
3234
|
-
if ( ! spill?.bytes )
|
|
3235
|
-
|
|
3788
|
+
if ( ! spill?.bytes ) {
|
|
3789
|
+
|
|
3790
|
+
this._geometryOwner = null;
|
|
3791
|
+
return;
|
|
3792
|
+
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
if ( ! this._geometryOwner ) updateLoading( { status: 'Reading scene geometry back...', progress: 84 } );
|
|
3236
3796
|
const started = performance.now();
|
|
3237
3797
|
const bytes = spill.bytes;
|
|
3238
3798
|
await spill.restore();
|
|
@@ -3248,6 +3808,7 @@ export class SceneProcessor {
|
|
|
3248
3808
|
|
|
3249
3809
|
const spill = this._geometrySpill;
|
|
3250
3810
|
this._geometrySpill = null;
|
|
3811
|
+
this._geometryOwner = null;
|
|
3251
3812
|
spill?.dispose().catch( () => {} );
|
|
3252
3813
|
|
|
3253
3814
|
}
|
|
@@ -3281,9 +3842,43 @@ export class SceneProcessor {
|
|
|
3281
3842
|
* never a scene.
|
|
3282
3843
|
* @private
|
|
3283
3844
|
*/
|
|
3845
|
+
/** Whether to build the TLAS over copy clusters: `config.clusterCopies`, or by default past CLUSTER_MIN_ENTRIES. @private */
|
|
3846
|
+
_clusterCopies( table ) {
|
|
3847
|
+
|
|
3848
|
+
const choice = this.config.clusterCopies ?? 'auto';
|
|
3849
|
+
return choice === 'auto' ? table.entryCount >= CLUSTER_MIN_ENTRIES : choice === true;
|
|
3850
|
+
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/**
|
|
3854
|
+
* The copy records behind the BLASes: each entry's world-to-object rows in cluster order, four records to three
|
|
3855
|
+
* nodes, written a batch at a time and handed to the spill as the fill passes its chunks. @private
|
|
3856
|
+
*/
|
|
3857
|
+
async _writeCopyRecords( flushBVH ) {
|
|
3858
|
+
|
|
3859
|
+
const table = this.instanceTable;
|
|
3860
|
+
const n = table.entryCount;
|
|
3861
|
+
const batch = 4096; // records, a multiple of 4
|
|
3862
|
+
const nodes = batch * RECORD_VEC4 / 4;
|
|
3863
|
+
const stage = new Float32Array( nodes * 16 );
|
|
3864
|
+
for ( let r = 0; r < n; r += batch ) {
|
|
3865
|
+
|
|
3866
|
+
const count = Math.min( batch, n - r );
|
|
3867
|
+
stage.fill( 0 );
|
|
3868
|
+
for ( let i = 0; i < count; i ++ ) table.writeRecord( stage, i * RECORD_VEC4 * 4, table.repOf( table.recordEntry[ r + i ] ) );
|
|
3869
|
+
const at = table.recordNodeStart + r * RECORD_VEC4 / 4;
|
|
3870
|
+
const used = Math.ceil( count * RECORD_VEC4 / 4 );
|
|
3871
|
+
this.bvh.setRecords( at, used === nodes ? stage : stage.subarray( 0, used * 16 ) );
|
|
3872
|
+
await flushBVH?.( at + used );
|
|
3873
|
+
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3284
3878
|
async _buildTLAS( table ) {
|
|
3285
3879
|
|
|
3286
|
-
const
|
|
3880
|
+
const clustered = table.clusterCount > 0;
|
|
3881
|
+
const n = clustered ? table.clusterCount : table.entryCount;
|
|
3287
3882
|
|
|
3288
3883
|
// Below this the build is a few ms and the round trip costs more than it saves.
|
|
3289
3884
|
if ( n >= TLAS_WORKER_MIN_ENTRIES && ! this._tlasWorkerFailed ) {
|
|
@@ -3291,8 +3886,8 @@ export class SceneProcessor {
|
|
|
3291
3886
|
try {
|
|
3292
3887
|
|
|
3293
3888
|
// The worker needs a buffer it can own, and world bounds are derived anyway.
|
|
3294
|
-
const aabbs = new Float64Array( n * 6 );
|
|
3295
|
-
table.
|
|
3889
|
+
const aabbs = clustered ? table.clusterBounds : new Float64Array( n * 6 );
|
|
3890
|
+
if ( ! clustered ) table.writeEntryWorldAABBs( aabbs );
|
|
3296
3891
|
const { tlasData, nodeCount } = await this._runTLASWorker( aabbs, n );
|
|
3297
3892
|
TLASBuilder.fillLeaves( tlasData, nodeCount, table );
|
|
3298
3893
|
return tlasData;
|
|
@@ -3350,16 +3945,15 @@ export class SceneProcessor {
|
|
|
3350
3945
|
/** Keep the chunked BVH and its u32 index view in step. @private */
|
|
3351
3946
|
_setBVHData( data ) {
|
|
3352
3947
|
|
|
3353
|
-
this._tlasLeafBoxesOf = null;
|
|
3354
|
-
|
|
3355
3948
|
const records = ! data ? null
|
|
3356
3949
|
: ( data instanceof ChunkedRecords
|
|
3357
3950
|
? data
|
|
3358
3951
|
: ChunkedRecords.adopt( [ data ], data.length / 16, 16, data.length / 16 ) );
|
|
3359
3952
|
|
|
3360
3953
|
this.bvh = records;
|
|
3361
|
-
// Read by the traversal when it is built: only a folded BVH gets that code.
|
|
3954
|
+
// Read by the traversal when it is built: only a folded BVH gets that code, only a clustered one the copy records.
|
|
3362
3955
|
if ( records && this._foldLeaves ) records.foldedLeaves = true;
|
|
3956
|
+
if ( records && this.instanceTable?.clusterCount ) records.copyRecordBase = this.instanceTable.recordNodeStart * 4;
|
|
3363
3957
|
this.bvhIndexChunks = records ? records.viewAs( Uint32Array ) : null;
|
|
3364
3958
|
// Null once the BVH needs more than one chunk; hot paths use `bvh` / `bvhIndexChunks`.
|
|
3365
3959
|
this.bvhData = records ? records.single : null;
|
|
@@ -3531,83 +4125,95 @@ export class SceneProcessor {
|
|
|
3531
4125
|
}
|
|
3532
4126
|
|
|
3533
4127
|
/**
|
|
3534
|
-
* Refit TLAS
|
|
3535
|
-
*
|
|
3536
|
-
*
|
|
3537
|
-
*
|
|
4128
|
+
* Refit the TLAS boxes in place, top down. A node keeps each child's box in its own slots, so a subtree holding none
|
|
4129
|
+
* of `leaves` is skipped: a move costs its leaves' paths to the root, not the tree (moving 69,856 copies on the Moana
|
|
4130
|
+
* island, 20M nodes: 11.6 s → 93 ms). Every leaf when `leaves` is omitted.
|
|
4131
|
+
* @param {ArrayLike<number>} [leaves] - TLAS leaf nodes whose boxes changed, in any order
|
|
3538
4132
|
* @private
|
|
3539
4133
|
*/
|
|
3540
|
-
_refitTLAS(
|
|
4134
|
+
_refitTLAS( leaves = null ) {
|
|
3541
4135
|
|
|
3542
|
-
const
|
|
4136
|
+
const table = this.instanceTable;
|
|
4137
|
+
const count = table.tlasNodeCount;
|
|
4138
|
+
const marks = leaves ? Int32Array.from( leaves ).sort() : null;
|
|
4139
|
+
this._tlasRanges = marks ? [] : null;
|
|
4140
|
+
if ( ! count || marks?.length === 0 ) return;
|
|
4141
|
+
const touched = marks ? Array.from( marks ) : null;
|
|
3543
4142
|
|
|
3544
|
-
//
|
|
3545
|
-
|
|
4143
|
+
// Frames of node, end of its subtree (pre-order: [node, end)), parent slot (parent * 2 + side, -1 at the root),
|
|
4144
|
+
// and whether its children are done.
|
|
4145
|
+
let stack = this._refitStack ??= new Int32Array( 256 );
|
|
4146
|
+
let top = 0, cursor = 0;
|
|
4147
|
+
const box = this._refitBox ??= new Float32Array( 6 );
|
|
4148
|
+
const push = ( node, end, slot, done ) => {
|
|
3546
4149
|
|
|
3547
|
-
|
|
3548
|
-
this._tlasLeafBoxesOf = null;
|
|
4150
|
+
if ( top + 4 > stack.length ) {
|
|
3549
4151
|
|
|
3550
|
-
|
|
4152
|
+
const grown = new Int32Array( stack.length * 2 );
|
|
4153
|
+
grown.set( stack );
|
|
4154
|
+
stack = this._refitStack = grown;
|
|
3551
4155
|
|
|
3552
|
-
|
|
3553
|
-
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
stack[ top ++ ] = node; stack[ top ++ ] = end; stack[ top ++ ] = slot; stack[ top ++ ] = done;
|
|
4159
|
+
|
|
4160
|
+
};
|
|
4161
|
+
|
|
4162
|
+
push( 0, count, - 1, 0 );
|
|
4163
|
+
while ( top ) {
|
|
4164
|
+
|
|
4165
|
+
const done = stack[ -- top ], slot = stack[ -- top ], end = stack[ -- top ], i = stack[ -- top ];
|
|
4166
|
+
const f = this.bvh.chunkFor( i ), o = this.bvh.baseOf( i );
|
|
3554
4167
|
|
|
3555
|
-
|
|
3556
|
-
for ( let i = tlasNodeCount - 1; i >= 0; i -- ) {
|
|
4168
|
+
if ( done ) {
|
|
3557
4169
|
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
const o = this.bvh.baseOf( i );
|
|
3561
|
-
const marker = idxChunk[ o + 3 ];
|
|
4170
|
+
box[ 0 ] = Math.min( f[ o ], f[ o + 8 ] ); box[ 1 ] = Math.min( f[ o + 1 ], f[ o + 9 ] ); box[ 2 ] = Math.min( f[ o + 2 ], f[ o + 10 ] );
|
|
4171
|
+
box[ 3 ] = Math.max( f[ o + 4 ], f[ o + 12 ] ); box[ 4 ] = Math.max( f[ o + 5 ], f[ o + 13 ] ); box[ 5 ] = Math.max( f[ o + 6 ], f[ o + 14 ] );
|
|
3562
4172
|
|
|
3563
|
-
|
|
4173
|
+
} else {
|
|
3564
4174
|
|
|
3565
|
-
|
|
3566
|
-
// every placement of a shared geometry onto one box, so all but one copy sat
|
|
3567
|
-
// outside its own bounds and rays walked straight past it.
|
|
3568
|
-
const entryIndex = idxChunk[ o + 1 ] & TLAS_PLACEMENT_MASK;
|
|
3569
|
-
if ( entryIndex < table.count && ! ( keep && ! moved[ entryIndex ] ) ) {
|
|
4175
|
+
if ( marks ) {
|
|
3570
4176
|
|
|
3571
|
-
|
|
4177
|
+
while ( cursor < marks.length && marks[ cursor ] < i ) cursor ++;
|
|
4178
|
+
if ( cursor === marks.length || marks[ cursor ] >= end ) continue;
|
|
3572
4179
|
|
|
3573
4180
|
}
|
|
3574
4181
|
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const b = i * 6;
|
|
3599
|
-
bounds[ b ] = Math.min( bounds[ lb ], bounds[ rb ] );
|
|
3600
|
-
bounds[ b + 1 ] = Math.min( bounds[ lb + 1 ], bounds[ rb + 1 ] );
|
|
3601
|
-
bounds[ b + 2 ] = Math.min( bounds[ lb + 2 ], bounds[ rb + 2 ] );
|
|
3602
|
-
bounds[ b + 3 ] = Math.max( bounds[ lb + 3 ], bounds[ rb + 3 ] );
|
|
3603
|
-
bounds[ b + 4 ] = Math.max( bounds[ lb + 4 ], bounds[ rb + 4 ] );
|
|
3604
|
-
bounds[ b + 5 ] = Math.max( bounds[ lb + 5 ], bounds[ rb + 5 ] );
|
|
4182
|
+
const idx = this.bvhIndexChunks.chunkFor( i );
|
|
4183
|
+
const marker = idx[ o + 3 ];
|
|
4184
|
+
if ( marker === BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) {
|
|
4185
|
+
|
|
4186
|
+
// Slot [1] is this leaf's own entry. Keying off slot [0] instead collapsed every placement of a shared
|
|
4187
|
+
// geometry onto one box, so all but one copy sat outside its own bounds and rays walked past it.
|
|
4188
|
+
const entry = idx[ o + 1 ] & TLAS_PLACEMENT_MASK;
|
|
4189
|
+
if ( entry >= table.entryCount ) continue;
|
|
4190
|
+
table.writeEntryWorldAABB( entry, box, 0 );
|
|
4191
|
+
|
|
4192
|
+
} else if ( marker === BVH_LEAF_MARKERS.CLUSTER_LEAF ) {
|
|
4193
|
+
|
|
4194
|
+
table.encodeClusterBoxes( f, idx, o, table.clusterOf( idx[ o ] & CLUSTER_FIRST_MASK ), null, box, 0 );
|
|
4195
|
+
|
|
4196
|
+
} else if ( marker < BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
|
|
4197
|
+
|
|
4198
|
+
const right = idx[ o + 7 ];
|
|
4199
|
+
push( i, end, slot, 1 );
|
|
4200
|
+
push( right, end, i * 2 + 1, 0 );
|
|
4201
|
+
push( marker, right, i * 2, 0 );
|
|
4202
|
+
continue;
|
|
4203
|
+
|
|
4204
|
+
} else continue;
|
|
3605
4205
|
|
|
3606
4206
|
}
|
|
3607
4207
|
|
|
4208
|
+
if ( slot < 0 ) continue;
|
|
4209
|
+
const p = slot >> 1, pf = this.bvh.chunkFor( p ), po = this.bvh.baseOf( p ) + ( slot & 1 ) * 8;
|
|
4210
|
+
pf[ po ] = box[ 0 ]; pf[ po + 1 ] = box[ 1 ]; pf[ po + 2 ] = box[ 2 ];
|
|
4211
|
+
pf[ po + 4 ] = box[ 3 ]; pf[ po + 5 ] = box[ 4 ]; pf[ po + 6 ] = box[ 5 ];
|
|
4212
|
+
touched?.push( p );
|
|
4213
|
+
|
|
3608
4214
|
}
|
|
3609
4215
|
|
|
3610
|
-
this.
|
|
4216
|
+
if ( touched ) this._tlasRanges = nodeRanges( touched );
|
|
3611
4217
|
|
|
3612
4218
|
}
|
|
3613
4219
|
|