rayzee 7.31.0 → 8.0.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 +164 -0
- package/dist/rayzee.es.js +6547 -4334
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +6 -0
- package/src/PathTracerApp.js +128 -20
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +357 -22
- package/src/Processor/BVHBuilder.js +48 -32
- package/src/Processor/BVHRefitter.js +244 -99
- package/src/Processor/ChunkedRecords.js +302 -0
- package/src/Processor/EmissiveTriangleBuilder.js +48 -21
- package/src/Processor/GeometryExtractor.js +577 -92
- package/src/Processor/HostMemory.js +222 -0
- package/src/Processor/InstanceTable.js +548 -97
- package/src/Processor/PBRT/EqualAreaOctahedral.js +173 -0
- package/src/Processor/PBRT/LoopSubdivision.js +240 -0
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +28 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +659 -34
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +306 -45
- package/src/Processor/PackedRayBuffer.js +29 -18
- package/src/Processor/ParallelBVHBuilder.js +9 -8
- package/src/Processor/QueueManager.js +5 -3
- package/src/Processor/SceneProcessor.js +1063 -366
- package/src/Processor/TLASBuilder.js +546 -201
- package/src/Processor/TextureCreator.js +11 -9
- package/src/Processor/VRAMTracker.js +12 -3
- package/src/Processor/Workers/BVHRefitWorker.js +16 -11
- package/src/Processor/Workers/BVHWorker.js +12 -12
- package/src/Processor/Workers/TLASWorker.js +37 -0
- package/src/Stages/NormalDepth.js +6 -2
- package/src/Stages/PathTracer.js +5 -5
- package/src/Stages/PathTracerStage.js +163 -41
- package/src/TSL/BVHTraversal.js +208 -90
- package/src/TSL/Common.js +135 -2
- package/src/TSL/Displacement.js +21 -11
- package/src/TSL/EmissiveSampling.js +49 -25
- package/src/TSL/ExtendKernel.js +3 -2
- package/src/TSL/LightBVHSampling.js +7 -4
- package/src/TSL/LightsDirect.js +30 -29
- package/src/TSL/ShadeKernel.js +8 -6
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +128 -3
- package/src/index.js +3 -0
- package/src/managers/AnimationManager.js +86 -76
- package/src/managers/CameraManager.js +11 -0
- package/src/managers/MaterialDataManager.js +29 -23
- package/src/managers/TransformManager.js +9 -192
|
@@ -3,18 +3,29 @@ 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 } from './InstanceTable.js';
|
|
6
|
+
import { InstanceTable, isIdentity, multiplyAffine } from './InstanceTable.js';
|
|
7
|
+
import { ChunkedRecords, SHARED_MEMORY_AVAILABLE, setChunkObserver } from './ChunkedRecords.js';
|
|
8
|
+
import {
|
|
9
|
+
MemoryLedger, estimateSceneBytes, probeAddressSpace,
|
|
10
|
+
PREFLIGHT_MIN_BYTES, PREFLIGHT_SAFETY, SAFE_SCENE_BYTES, MAX_SCENE_BYTES,
|
|
11
|
+
} from './HostMemory.js';
|
|
7
12
|
import { TextureCreator } from './TextureCreator.js';
|
|
8
|
-
import { GeometryExtractor } from './GeometryExtractor.js';
|
|
13
|
+
import { GeometryExtractor, geometryBytesOf } from './GeometryExtractor.js';
|
|
9
14
|
import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
|
|
10
15
|
import { updateLoading } from '../Processor/utils.js';
|
|
11
16
|
import { BuildTimer } from './BuildTimer.js';
|
|
12
17
|
import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
|
|
13
18
|
import { SRGBColorSpace } from 'three';
|
|
14
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex, planTextureBuckets,
|
|
21
|
+
packNormalOct, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView, TLAS_PLACEMENT_MASK } from '../EngineDefaults.js';
|
|
15
22
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
16
23
|
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
17
24
|
import BVHRefitWorker from './Workers/BVHRefitWorker.js?worker&inline';
|
|
25
|
+
import TLASWorker from './Workers/TLASWorker.js?worker&inline';
|
|
26
|
+
|
|
27
|
+
// Under this the TLAS build is a few ms; the worker round trip would cost more than it saves.
|
|
28
|
+
const TLAS_WORKER_MIN_ENTRIES = 50_000;
|
|
18
29
|
|
|
19
30
|
const log = createLogger( 'scene' );
|
|
20
31
|
|
|
@@ -35,6 +46,9 @@ export class SceneProcessor {
|
|
|
35
46
|
* @param {string} [options.textureQuality='adaptive'] - Texture quality mode
|
|
36
47
|
* @param {boolean} [options.enableTextureCache=true] - Enable texture caching
|
|
37
48
|
* @param {import('../EngineIssues.js').IssueLog} [options.issues] - forwarded to TextureCreator
|
|
49
|
+
* @param {number} [options.maxSceneBytes] - refuse a scene estimated above this many CPU
|
|
50
|
+
* bytes. Defaults to {@link MAX_SCENE_BYTES}; past it the renderer process dies rather
|
|
51
|
+
* than throwing, so there is nothing to catch. Raise it to try a bigger scene anyway.
|
|
38
52
|
*/
|
|
39
53
|
constructor( options = {} ) {
|
|
40
54
|
|
|
@@ -49,6 +63,7 @@ export class SceneProcessor {
|
|
|
49
63
|
maxTextureSize: TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE, // longest-edge cap for material textures
|
|
50
64
|
enableTextureCache: true,
|
|
51
65
|
maxConcurrentTextureTasks: Math.min( navigator.hardwareConcurrency || 4, 6 ),
|
|
66
|
+
maxSceneBytes: MAX_SCENE_BYTES,
|
|
52
67
|
// Treelet optimization configuration
|
|
53
68
|
// Keep: `_buildBVH` sends `enabled: value !== false`, so undefined re-enables treelets.
|
|
54
69
|
enableTreeletOptimization: false,
|
|
@@ -61,7 +76,8 @@ export class SceneProcessor {
|
|
|
61
76
|
};
|
|
62
77
|
|
|
63
78
|
// Initialize geometry data containers
|
|
64
|
-
this.triangleData = null; //
|
|
79
|
+
this.triangleData = null; // uint lanes; this.triangleFloats views the same memory
|
|
80
|
+
this.triangleFloats = null;
|
|
65
81
|
this.triangleCount = 0; // Number of triangles
|
|
66
82
|
this.materials = [];
|
|
67
83
|
this.maps = [];
|
|
@@ -87,12 +103,14 @@ export class SceneProcessor {
|
|
|
87
103
|
this.bvhRoot = null;
|
|
88
104
|
|
|
89
105
|
// Raw data for storage buffers
|
|
106
|
+
this.bvh = null;
|
|
90
107
|
this.bvhData = null;
|
|
108
|
+
this.bvhIndexChunks = null;
|
|
109
|
+
this.bvhIndex = null;
|
|
91
110
|
this.materialData = null;
|
|
92
111
|
|
|
93
112
|
// Two-level BVH (TLAS/BLAS) support
|
|
94
113
|
this.instanceTable = null; // Per-mesh BLAS metadata
|
|
95
|
-
this.originalToBvhMap = null; // Uint32Array: original tri index → BVH-order index (global, for legacy compat)
|
|
96
114
|
this._refitWorker = null;
|
|
97
115
|
this._refitSharedBuffers = null; // SharedArrayBuffer refs for zero-copy refit
|
|
98
116
|
this._rebuildGeneration = 0; // Monotonic counter to discard stale background rebuilds
|
|
@@ -118,11 +136,18 @@ export class SceneProcessor {
|
|
|
118
136
|
this.isProcessing = false;
|
|
119
137
|
this.processingStage = null;
|
|
120
138
|
|
|
139
|
+
// What the build spent, by phase, and what the preflight expected it to spend.
|
|
140
|
+
this.memory = new MemoryLedger();
|
|
141
|
+
this.memoryPreflight = null;
|
|
142
|
+
|
|
121
143
|
// Performance tracking
|
|
122
144
|
this.performanceMetrics = {
|
|
123
145
|
textureCreationTime: 0,
|
|
124
146
|
geometryExtractionTime: 0,
|
|
125
147
|
bvhBuildTime: 0,
|
|
148
|
+
blasBuildTime: 0,
|
|
149
|
+
tlasBuildTime: 0,
|
|
150
|
+
bvhAssembleTime: 0,
|
|
126
151
|
totalProcessingTime: 0
|
|
127
152
|
};
|
|
128
153
|
|
|
@@ -170,6 +195,8 @@ export class SceneProcessor {
|
|
|
170
195
|
|
|
171
196
|
// Create TLAS builder for two-level BVH
|
|
172
197
|
this.tlasBuilder = new TLASBuilder();
|
|
198
|
+
this._tlasWorker = null;
|
|
199
|
+
this._tlasWorkerFailed = false;
|
|
173
200
|
|
|
174
201
|
}
|
|
175
202
|
|
|
@@ -185,6 +212,191 @@ export class SceneProcessor {
|
|
|
185
212
|
|
|
186
213
|
}
|
|
187
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Price the scene before the build starts spending, and say so if it is over what a healthy
|
|
217
|
+
* session can place.
|
|
218
|
+
*
|
|
219
|
+
* Estimate only — no allocation. Probing the whole scene up front was measurably worse than
|
|
220
|
+
* not checking: the probe's peak lands on top of the parse's, and a 9 GB probe taken while
|
|
221
|
+
* the parser still held 3.6 GB doubled a 40M load from 135 s to 268 s. The allocation test
|
|
222
|
+
* belongs at the step that actually runs out, which is
|
|
223
|
+
* {@link SceneProcessor#_checkAssemblyHeadroom}.
|
|
224
|
+
*
|
|
225
|
+
* The geometry figure reads before `_compressAttributes` halves normals and colours, so it
|
|
226
|
+
* runs ~7% high on a typical scene. That direction is the safe one.
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
_preflightMemory( object ) {
|
|
230
|
+
|
|
231
|
+
const maxBytes = this.config.maxSceneBytes ?? MAX_SCENE_BYTES;
|
|
232
|
+
const survey = this.geometryExtractor.surveyScene( object );
|
|
233
|
+
const estimate = estimateSceneBytes( survey );
|
|
234
|
+
const report = {
|
|
235
|
+
...survey, estimate,
|
|
236
|
+
safeBytes: SAFE_SCENE_BYTES, maxBytes,
|
|
237
|
+
fits: estimate.total <= SAFE_SCENE_BYTES,
|
|
238
|
+
};
|
|
239
|
+
this.memoryPreflight = report;
|
|
240
|
+
|
|
241
|
+
if ( estimate.total < PREFLIGHT_MIN_BYTES ) return report;
|
|
242
|
+
|
|
243
|
+
log.debug( fmt.list( [
|
|
244
|
+
`memory preflight: ${fmt.mb( estimate.total )} estimated`,
|
|
245
|
+
`triangles ${fmt.mb( estimate.triangles )} · bvh ${fmt.mb( estimate.bvh )} · geometry ${fmt.mb( estimate.geometry )}`,
|
|
246
|
+
`${fmt.n( survey.triangles )} tris · ${fmt.n( survey.placements )} placements`,
|
|
247
|
+
] ) );
|
|
248
|
+
|
|
249
|
+
// Past the hard line the renderer process dies rather than throwing, so there is nothing
|
|
250
|
+
// to catch and nothing to degrade to. Refusing with a reason is the only useful answer.
|
|
251
|
+
if ( estimate.total > maxBytes ) {
|
|
252
|
+
|
|
253
|
+
const message = `Scene needs about ${fmt.mb( estimate.total )} of CPU memory, past the ${fmt.mb( maxBytes )} `
|
|
254
|
+
+ 'a browser tab can hold — loading it would crash the tab rather than fail. '
|
|
255
|
+
+ `Reduce the scene (it has ${fmt.n( survey.triangles )} triangles), or raise maxSceneBytes to try anyway.`;
|
|
256
|
+
|
|
257
|
+
this.config.issues?.record(
|
|
258
|
+
ISSUE_CODES.SCENE_MEMORY_BUDGET, message, { ...survey, estimate, maxBytes }
|
|
259
|
+
);
|
|
260
|
+
throw new Error( message );
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if ( ! report.fits ) {
|
|
265
|
+
|
|
266
|
+
this.config.issues?.warn(
|
|
267
|
+
ISSUE_CODES.SCENE_MEMORY_BUDGET,
|
|
268
|
+
`Scene needs about ${fmt.mb( estimate.total )} of CPU memory, above the ${fmt.mb( SAFE_SCENE_BYTES )} a fresh browser `
|
|
269
|
+
+ 'session can usually place. It may still load, but a long-running session will probably fail during BVH assembly; '
|
|
270
|
+
+ 'restarting the browser recovers the address space.',
|
|
271
|
+
{ ...survey, estimate, safeBytes: SAFE_SCENE_BYTES }
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return report;
|
|
277
|
+
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Test the one allocation that has failed every time: the combined BVH.
|
|
282
|
+
*
|
|
283
|
+
* By this point the archive and the parser's scratch are gone and the BLASes are about to be
|
|
284
|
+
* handed over chunk by chunk, so the probe is small (the BVH's own size plus headroom, ~2 GB
|
|
285
|
+
* at 40M rather than the whole scene's 9 GB) and it is asking the question at the moment the
|
|
286
|
+
* answer matters. A failure here is still only a warning — the build is allowed to try.
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
_checkAssemblyHeadroom( totalNodes ) {
|
|
290
|
+
|
|
291
|
+
const need = totalNodes * 64;
|
|
292
|
+
if ( need < PREFLIGHT_MIN_BYTES ) return true;
|
|
293
|
+
|
|
294
|
+
// Same flavour the store uses: shared and non-shared buffers need not come from one pool.
|
|
295
|
+
const probe = probeAddressSpace( Math.ceil( need * PREFLIGHT_SAFETY ), { shared: SHARED_MEMORY_AVAILABLE } );
|
|
296
|
+
if ( this.memoryPreflight ) this.memoryPreflight.assemblyProbed = probe.placed;
|
|
297
|
+
|
|
298
|
+
if ( probe.exhausted ) {
|
|
299
|
+
|
|
300
|
+
this.config.issues?.warn(
|
|
301
|
+
ISSUE_CODES.SCENE_MEMORY_BUDGET,
|
|
302
|
+
`BVH assembly needs ${fmt.mb( need )} but only ${fmt.mb( probe.placed )} could be allocated. `
|
|
303
|
+
+ 'Restarting the browser usually recovers the address space.',
|
|
304
|
+
{ totalNodes, need, placeable: probe.placed }
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return ! probe.exhausted;
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Byte sizes of the CPU structures alive right now. Anything already released is absent,
|
|
315
|
+
* which is the point: this is what the peak is made of, not what was ever allocated.
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
_liveMemoryParts() {
|
|
319
|
+
|
|
320
|
+
const table = this.instanceTable;
|
|
321
|
+
const parts = {
|
|
322
|
+
triangles: this.triangles?.byteLength ?? 0,
|
|
323
|
+
bvh: this.bvh?.byteLength ?? 0,
|
|
324
|
+
geometry: this._geometryBytes ?? this.memoryPreflight?.geometryBytes ?? 0,
|
|
325
|
+
placements: table?.world?.byteLength ?? 0,
|
|
326
|
+
blasScratch: 0,
|
|
327
|
+
orderMaps: 0,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if ( table ) {
|
|
331
|
+
|
|
332
|
+
for ( const blas of table.blasData.values() ) parts.blasScratch += blas?.byteLength ?? 0;
|
|
333
|
+
for ( const m of table.bvhToOriginal.values() ) parts.orderMaps += m?.byteLength ?? 0;
|
|
334
|
+
for ( const m of table.originalToBvhMap.values() ) parts.orderMaps += m?.byteLength ?? 0;
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return parts;
|
|
339
|
+
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* World-space bounds of everything in the BVH, as `{ min: [x,y,z], max: [x,y,z] }`.
|
|
344
|
+
*
|
|
345
|
+
* ⚠️ Node 0 is not "the root's box". A BVH inner node stores its two CHILDREN's boxes —
|
|
346
|
+
* `[Amin, Aidx, Amax, _, Bmin, Bidx, Bmax, _]` — so the first six floats are one subtree,
|
|
347
|
+
* not the scene. Reading them as the scene box is wrong and looks plausible: the two halves
|
|
348
|
+
* legitimately rebalance between a build and a refit, so the number moves while the actual
|
|
349
|
+
* bounds are unchanged. Verified against the union of all 3.7M placement AABBs at 40M.
|
|
350
|
+
*
|
|
351
|
+
* @returns {?{min: number[], max: number[]}} null before a BVH is built
|
|
352
|
+
*/
|
|
353
|
+
sceneBounds() {
|
|
354
|
+
|
|
355
|
+
if ( ! this.bvh || this.bvh.recordCount === 0 ) return null;
|
|
356
|
+
|
|
357
|
+
const chunk = this.bvh.chunkFor( 0 );
|
|
358
|
+
const base = this.bvh.baseOf( 0 );
|
|
359
|
+
const min = [], max = [];
|
|
360
|
+
|
|
361
|
+
for ( let k = 0; k < 3; k ++ ) {
|
|
362
|
+
|
|
363
|
+
min.push( Math.min( chunk[ base + k ], chunk[ base + 8 + k ] ) );
|
|
364
|
+
max.push( Math.max( chunk[ base + 4 + k ], chunk[ base + 12 + k ] ) );
|
|
365
|
+
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { min, max };
|
|
369
|
+
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** @private */
|
|
373
|
+
_sampleMemory( label ) {
|
|
374
|
+
|
|
375
|
+
this.memory.sample( label, this._liveMemoryParts() );
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** One line on what the build actually cost, and whether the preflight called it right. @private */
|
|
380
|
+
_logMemoryReport() {
|
|
381
|
+
|
|
382
|
+
const { allocatedBytes, peakLiveBytes, byPhase } = this.memory;
|
|
383
|
+
if ( allocatedBytes === 0 ) return;
|
|
384
|
+
|
|
385
|
+
const phases = Object.keys( byPhase )
|
|
386
|
+
.filter( name => byPhase[ name ].allocated > 0 )
|
|
387
|
+
.map( name => `${name} ${fmt.mb( byPhase[ name ].allocated )}` );
|
|
388
|
+
|
|
389
|
+
const predicted = this.memoryPreflight?.estimate?.total;
|
|
390
|
+
|
|
391
|
+
log.debug( fmt.list( [
|
|
392
|
+
`memory: peak live ${fmt.mb( peakLiveBytes )}`,
|
|
393
|
+
`allocated ${fmt.mb( allocatedBytes )}`,
|
|
394
|
+
predicted ? `predicted ${fmt.mb( predicted )}` : null,
|
|
395
|
+
phases.join( ' · ' ),
|
|
396
|
+
] ) );
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
|
|
188
400
|
/**
|
|
189
401
|
* Build the BVH from a 3D object/scene
|
|
190
402
|
* @param {Object3D} object - Three.js object to process
|
|
@@ -200,6 +412,10 @@ export class SceneProcessor {
|
|
|
200
412
|
|
|
201
413
|
this.isProcessing = true;
|
|
202
414
|
this.processingStage = 'init';
|
|
415
|
+
this.memory.reset();
|
|
416
|
+
this._geometryBytes = 0;
|
|
417
|
+
// Module-global by design: one build at a time, and `isProcessing` above enforces it.
|
|
418
|
+
setChunkObserver( bytes => this.memory.alloc( bytes ) );
|
|
203
419
|
|
|
204
420
|
const timer = new BuildTimer( object.name ?? '', { namespace: 'scene' } );
|
|
205
421
|
|
|
@@ -209,12 +425,21 @@ export class SceneProcessor {
|
|
|
209
425
|
this._reset();
|
|
210
426
|
this._log( 'Starting scene processing' );
|
|
211
427
|
|
|
428
|
+
// Step 0: will this scene fit in the address space this process has left?
|
|
429
|
+
this.memory.mark( 'preflight' );
|
|
430
|
+
this._preflightMemory( object );
|
|
431
|
+
|
|
212
432
|
// Step 1: Extract geometry (0-20%)
|
|
213
433
|
this.processingStage = 'extraction';
|
|
434
|
+
this.memory.mark( 'extraction' );
|
|
214
435
|
timer.start( 'Geometry extraction' );
|
|
215
436
|
await this._extractGeometry( object );
|
|
216
437
|
timer.end( 'Geometry extraction' );
|
|
217
438
|
this.performanceMetrics.geometryExtractionTime = timer.getDuration( 'Geometry extraction' );
|
|
439
|
+
// The preflight figure was taken before `_compressAttributes` halved normals and
|
|
440
|
+
// colours; every sample from here on should use what the mirror actually holds.
|
|
441
|
+
this._geometryBytes = geometryBytesOf( object );
|
|
442
|
+
this._sampleMemory( 'after extraction' );
|
|
218
443
|
|
|
219
444
|
// Step 2: BVH + textures in parallel (20-95%)
|
|
220
445
|
// Texture creation only needs GeometryExtractor output (materials + texture maps)
|
|
@@ -257,9 +482,9 @@ export class SceneProcessor {
|
|
|
257
482
|
// Only fall back to main-thread flattening if bvhData wasn't produced.
|
|
258
483
|
this.processingStage = 'finalize';
|
|
259
484
|
timer.start( 'BVH data packing' );
|
|
260
|
-
if ( this.bvhRoot && ! this.
|
|
485
|
+
if ( this.bvhRoot && ! this.bvh ) {
|
|
261
486
|
|
|
262
|
-
this.
|
|
487
|
+
this._setBVHData( this.textureCreator.createBVHRawData( this.bvhRoot ) );
|
|
263
488
|
|
|
264
489
|
}
|
|
265
490
|
|
|
@@ -283,12 +508,16 @@ export class SceneProcessor {
|
|
|
283
508
|
log.error( 'processing failed:', error );
|
|
284
509
|
updateLoading( {
|
|
285
510
|
status: `Error: ${error.message}`,
|
|
511
|
+
failed: true,
|
|
286
512
|
progress: 100
|
|
287
513
|
} );
|
|
288
514
|
throw error;
|
|
289
515
|
|
|
290
516
|
} finally {
|
|
291
517
|
|
|
518
|
+
setChunkObserver( null );
|
|
519
|
+
this._sampleMemory( `at ${this.processingStage}` );
|
|
520
|
+
this._logMemoryReport();
|
|
292
521
|
this.isProcessing = false;
|
|
293
522
|
|
|
294
523
|
}
|
|
@@ -319,10 +548,17 @@ export class SceneProcessor {
|
|
|
319
548
|
// Extract geometry data
|
|
320
549
|
const extractedData = this.geometryExtractor.extract( object );
|
|
321
550
|
|
|
322
|
-
this.
|
|
323
|
-
this.triangleCount = extractedData.triangleCount;
|
|
551
|
+
this._setTriangleData( extractedData.triangleData );
|
|
552
|
+
this.triangleCount = assertBVHIndexFits( extractedData.triangleCount, 'triangle count' );
|
|
553
|
+
// Callers build refit buffers by walking meshes, which counts a shared geometry
|
|
554
|
+
// once per placement; storage counts it once.
|
|
555
|
+
this.expandedTriangleCount = extractedData.expandedTriangleCount ?? extractedData.triangleCount;
|
|
556
|
+
this.instanceSource = extractedData.instanceSource || null;
|
|
557
|
+
this.instanceMatrices = extractedData.instanceMatrices || null;
|
|
558
|
+
this.instanceCount = extractedData.instanceCount || 0;
|
|
559
|
+
this.bakeInverse = extractedData.bakeInverse || null;
|
|
324
560
|
|
|
325
|
-
this._log( `Using Float32Array format: ${this.triangleCount} triangles, ${( this.
|
|
561
|
+
this._log( `Using Float32Array format: ${this.triangleCount} triangles, ${( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 )}MB` );
|
|
326
562
|
|
|
327
563
|
// Store other extracted data
|
|
328
564
|
this.materials = extractedData.materials;
|
|
@@ -365,6 +601,7 @@ export class SceneProcessor {
|
|
|
365
601
|
log.error( 'geometry extraction failed:', error );
|
|
366
602
|
updateLoading( {
|
|
367
603
|
status: `Extraction error: ${error.message}`,
|
|
604
|
+
failed: true,
|
|
368
605
|
progress: 25
|
|
369
606
|
} );
|
|
370
607
|
throw error;
|
|
@@ -391,11 +628,11 @@ export class SceneProcessor {
|
|
|
391
628
|
}
|
|
392
629
|
|
|
393
630
|
this._log( 'Building two-level BVH (TLAS/BLAS)' );
|
|
631
|
+
this.memory.mark( 'blas' );
|
|
394
632
|
const startTime = performance.now();
|
|
395
633
|
|
|
396
634
|
try {
|
|
397
635
|
|
|
398
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
399
636
|
const ranges = this.meshTriangleRanges;
|
|
400
637
|
|
|
401
638
|
if ( ! ranges || ranges.length === 0 ) {
|
|
@@ -406,9 +643,18 @@ export class SceneProcessor {
|
|
|
406
643
|
|
|
407
644
|
// ── Step 1: Build per-mesh BLASes ──
|
|
408
645
|
|
|
646
|
+
// One entry per PLACEMENT, not per object: an InstancedMesh contributes a matrix per
|
|
647
|
+
// instance while the scene graph still holds one object. The extractor hands over the
|
|
648
|
+
// transforms as one contiguous pool, which the table adopts rather than copying.
|
|
649
|
+
const pooled = this.instanceCount > 0;
|
|
650
|
+
const meshCount = pooled ? this.instanceCount : ranges.length;
|
|
651
|
+
const instSource = this.instanceSource;
|
|
652
|
+
const worldPool = pooled ? this.instanceMatrices : null;
|
|
653
|
+
const sourceOf = m => ( pooled ? instSource[ m ] : m );
|
|
654
|
+
|
|
409
655
|
this.instanceTable = new InstanceTable();
|
|
410
|
-
this.instanceTable.allocate( ranges.length );
|
|
411
|
-
|
|
656
|
+
this.instanceTable.allocate( meshCount, ranges.length, worldPool, pooled ? instSource : null );
|
|
657
|
+
this.instanceTable.tplBakeInverse = this.bakeInverse;
|
|
412
658
|
|
|
413
659
|
const originalTreeletEnabled = this.config.enableTreeletOptimization;
|
|
414
660
|
const LARGE_MESH_THRESHOLD = 200000;
|
|
@@ -417,10 +663,25 @@ export class SceneProcessor {
|
|
|
417
663
|
const poolTasks = [];
|
|
418
664
|
const parallelTasks = [];
|
|
419
665
|
|
|
666
|
+
// Placements that reuse another's triangles reuse its BLAS too — the whole point of
|
|
667
|
+
// storing geometry in object space. Only the first placement of a range is built.
|
|
668
|
+
const aliasOf = new Map();
|
|
669
|
+
const ownerOfRange = new Map();
|
|
670
|
+
|
|
420
671
|
for ( let m = 0; m < meshCount; m ++ ) {
|
|
421
672
|
|
|
422
|
-
const range = ranges[ m ];
|
|
423
|
-
if ( range.count === 0 ) continue;
|
|
673
|
+
const range = ranges[ sourceOf( m ) ];
|
|
674
|
+
if ( ! range || range.count === 0 ) continue;
|
|
675
|
+
|
|
676
|
+
const owner = ownerOfRange.get( range.start );
|
|
677
|
+
if ( owner !== undefined ) {
|
|
678
|
+
|
|
679
|
+
aliasOf.set( m, owner );
|
|
680
|
+
continue;
|
|
681
|
+
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
ownerOfRange.set( range.start, m );
|
|
424
685
|
|
|
425
686
|
if ( range.count >= LARGE_MESH_THRESHOLD && shouldUseParallelBuild( range.count ) ) {
|
|
426
687
|
|
|
@@ -459,52 +720,58 @@ export class SceneProcessor {
|
|
|
459
720
|
};
|
|
460
721
|
|
|
461
722
|
const totalTasks = poolTasks.length + parallelTasks.length;
|
|
723
|
+
let doneTasks = 0;
|
|
724
|
+
const reportBLASProgress = () => {
|
|
462
725
|
|
|
463
|
-
|
|
464
|
-
const poolPromise = this._buildBLASesWithPool( poolTasks, workerOpts, ( done ) => {
|
|
465
|
-
|
|
726
|
+
doneTasks ++;
|
|
466
727
|
updateLoading( {
|
|
467
|
-
status: `Building BLAS ${
|
|
468
|
-
progress: 25 + Math.floor( (
|
|
728
|
+
status: `Building BLAS ${doneTasks}/${totalTasks}...`,
|
|
729
|
+
progress: 25 + Math.floor( ( doneTasks / totalTasks ) * 45 )
|
|
469
730
|
} );
|
|
470
731
|
|
|
471
|
-
}
|
|
732
|
+
};
|
|
472
733
|
|
|
473
|
-
//
|
|
474
|
-
const
|
|
734
|
+
// Build all meshes via bounded worker pool (main thread stays free)
|
|
735
|
+
const poolPromise = this._buildBLASesWithPool( poolTasks, workerOpts, reportBLASProgress );
|
|
475
736
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
737
|
+
// One at a time: each build already spreads over every core and pins ~200 bytes per
|
|
738
|
+
// triangle of SharedArrayBuffer until it finishes. This is a memory bound, not a core one.
|
|
739
|
+
const parallelResults = [];
|
|
740
|
+
const parallelPromise = ( async () => {
|
|
480
741
|
|
|
481
|
-
|
|
482
|
-
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
483
|
-
numBins: this.bvhBuilder.numBins,
|
|
484
|
-
maxBins: this.bvhBuilder.maxBins,
|
|
485
|
-
minBins: this.bvhBuilder.minBins,
|
|
486
|
-
...workerOpts
|
|
487
|
-
} ).then( result => ( { m, range, result } ) );
|
|
742
|
+
for ( const { m, range } of parallelTasks ) {
|
|
488
743
|
|
|
489
|
-
|
|
744
|
+
const meshTriData = this.triangles.copyOf( range.start, range.count );
|
|
490
745
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
746
|
+
const result = await buildBVHParallel( meshTriData, this.config.bvhDepth, null, {
|
|
747
|
+
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
748
|
+
numBins: this.bvhBuilder.numBins,
|
|
749
|
+
maxBins: this.bvhBuilder.maxBins,
|
|
750
|
+
minBins: this.bvhBuilder.minBins,
|
|
751
|
+
...workerOpts
|
|
752
|
+
} );
|
|
496
753
|
|
|
497
|
-
|
|
498
|
-
const blasStats = { sah: 0, objMed: 0, spatMed: 0, failed: 0, treeletsImproved: 0, treeletsProcessed: 0 };
|
|
754
|
+
if ( result.reorderedTriangles ) {
|
|
499
755
|
|
|
500
|
-
|
|
756
|
+
this.triangles.setRecords( range.start, result.reorderedTriangles );
|
|
757
|
+
delete result.reorderedTriangles;
|
|
501
758
|
|
|
502
|
-
|
|
759
|
+
}
|
|
503
760
|
|
|
504
|
-
|
|
761
|
+
parallelResults.push( { m, range, result } );
|
|
762
|
+
reportBLASProgress();
|
|
505
763
|
|
|
506
764
|
}
|
|
507
765
|
|
|
766
|
+
} )();
|
|
767
|
+
|
|
768
|
+
const [ poolResults ] = await Promise.all( [ poolPromise, parallelPromise ] );
|
|
769
|
+
|
|
770
|
+
// Store all results, summing per-mesh split stats for one aggregate BVH line
|
|
771
|
+
const blasStats = { sah: 0, objMed: 0, spatMed: 0, failed: 0, treeletsImproved: 0, treeletsProcessed: 0 };
|
|
772
|
+
|
|
773
|
+
for ( const { m, range, result } of [ ...poolResults, ...parallelResults ] ) {
|
|
774
|
+
|
|
508
775
|
const st = result.splitStats;
|
|
509
776
|
if ( st ) {
|
|
510
777
|
|
|
@@ -524,50 +791,94 @@ export class SceneProcessor {
|
|
|
524
791
|
triCount: range.count,
|
|
525
792
|
originalToBvhMap: result.originalToBvh || null,
|
|
526
793
|
bvhData: result.bvhData,
|
|
794
|
+
matrixWorld: pooled ? worldPool : ( this.meshes?.[ m ]?.matrixWorld?.elements ?? null ),
|
|
795
|
+
matrixOffset: pooled ? m * 16 : 0,
|
|
796
|
+
expandedStart: range.expandedStart,
|
|
797
|
+
sourceMesh: sourceOf( m ),
|
|
527
798
|
} );
|
|
528
799
|
|
|
529
800
|
}
|
|
530
801
|
|
|
802
|
+
for ( const [ m, owner ] of aliasOf ) {
|
|
803
|
+
|
|
804
|
+
this.instanceTable.setAlias(
|
|
805
|
+
m, owner,
|
|
806
|
+
pooled ? worldPool : ( this.meshes?.[ m ]?.matrixWorld?.elements ?? null ),
|
|
807
|
+
ranges[ sourceOf( m ) ].expandedStart,
|
|
808
|
+
sourceOf( m ),
|
|
809
|
+
pooled ? m * 16 : 0
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
|
|
531
814
|
updateLoading( { status: 'Built all BLASes', progress: 70 } );
|
|
532
815
|
|
|
816
|
+
this.performanceMetrics.blasBuildTime = performance.now() - startTime;
|
|
817
|
+
|
|
533
818
|
// ── Step 2: Assemble BVH buffer ──
|
|
534
819
|
|
|
535
820
|
updateLoading( { status: "Building TLAS...", progress: 72 } );
|
|
821
|
+
this._sampleMemory( 'BLASes built' );
|
|
822
|
+
this.memory.mark( 'tlas' );
|
|
823
|
+
const tlasStart = performance.now();
|
|
536
824
|
|
|
537
|
-
const
|
|
825
|
+
const table = this.instanceTable;
|
|
538
826
|
|
|
539
827
|
// Always build a TLAS — even for a single mesh — so the BLAS-pointer leaf
|
|
540
828
|
// carries packed per-mesh visibility in its slot [2]. The 1-node TLAS
|
|
541
829
|
// overhead (one extra leaf fetch per ray) is negligible and eliminates
|
|
542
830
|
// a dedicated visibility storage buffer binding.
|
|
543
|
-
this.instanceTable.computeAABBs( this.
|
|
544
|
-
const { root: tlasRoot, nodeCount: tlasNodeCount } = this.tlasBuilder.build( validEntries );
|
|
831
|
+
this.instanceTable.computeAABBs( this.triangles );
|
|
545
832
|
|
|
546
|
-
|
|
833
|
+
// Node count is exact up front (every leaf holds one entry), so BLAS offsets can be
|
|
834
|
+
// assigned before the build and the TLAS is written in a single pass.
|
|
835
|
+
this.instanceTable.assignOffsets( TLASBuilder.nodeCountFor( table.count ) );
|
|
547
836
|
const totalNodes = this.instanceTable.totalNodeCount;
|
|
548
837
|
|
|
549
|
-
|
|
838
|
+
let tlasData = await this._buildTLAS( table );
|
|
839
|
+
this.performanceMetrics.tlasBuildTime = performance.now() - tlasStart;
|
|
550
840
|
|
|
551
841
|
// Assemble combined buffer: [TLAS][BLAS_0][BLAS_1]...[BLAS_M]
|
|
552
|
-
this.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
842
|
+
this.memory.mark( 'assemble' );
|
|
843
|
+
const assembleStart = performance.now();
|
|
844
|
+
// Chunked for the same reason triangles are: 64 B a node puts the 2 GB array cap at
|
|
845
|
+
// 33.4M nodes, which a large instanced scene reaches well before the GPU's 4 GB.
|
|
846
|
+
// Lazy, and walked in template order so offsets ascend: chunks are allocated as the fill
|
|
847
|
+
// reaches them and each BLAS is released as it lands, never both fully resident.
|
|
848
|
+
this._checkAssemblyHeadroom( totalNodes );
|
|
849
|
+
this._setBVHData( ChunkedRecords.lazy( totalNodes, 16, Float32Array, undefined, SHARED_MEMORY_AVAILABLE ) );
|
|
850
|
+
this.bvh.setRecords( 0, tlasData );
|
|
851
|
+
// Hundreds of megabytes at millions of placements, and the chunks below need the room
|
|
852
|
+
// more than a later refit needs the cache.
|
|
853
|
+
tlasData = null;
|
|
854
|
+
this.tlasBuilder.releaseFlattenBuffer();
|
|
855
|
+
|
|
856
|
+
// Sample ~32 times across the fill rather than per template: this is where every
|
|
857
|
+
// allocation failure so far has landed, and the live total moves in both directions
|
|
858
|
+
// as chunks are taken and BLASes released.
|
|
859
|
+
const sampleEvery = Math.max( 1, Math.ceil( table.templateCount / 32 ) );
|
|
860
|
+
|
|
861
|
+
for ( let t = 0; t < table.templateCount; t ++ ) {
|
|
862
|
+
|
|
863
|
+
const blas = table.blasData.get( t ); // only owning templates hold one
|
|
864
|
+
if ( ! blas ) continue;
|
|
865
|
+
|
|
866
|
+
const blasOffset = table.tplBlasOffset[ t ];
|
|
867
|
+
this.bvh.setRecords( blasOffset, blas );
|
|
868
|
+
this._offsetBLASInPlace( blasOffset, blas.length / 16, blasOffset, table.tplTriOffset[ t ] );
|
|
869
|
+
table.blasData.delete( t );
|
|
870
|
+
|
|
871
|
+
if ( t % sampleEvery === 0 ) this._sampleMemory( `assembling ${t}/${table.templateCount}` );
|
|
560
872
|
|
|
561
873
|
}
|
|
562
874
|
|
|
563
|
-
this.
|
|
564
|
-
|
|
565
|
-
for ( const entry of validEntries ) {
|
|
875
|
+
this._setBVHData( this.bvh.materializeAll() );
|
|
566
876
|
|
|
567
|
-
|
|
568
|
-
|
|
877
|
+
this._buildBvhToOriginalMaps();
|
|
878
|
+
this.performanceMetrics.bvhAssembleTime = performance.now() - assembleStart;
|
|
569
879
|
|
|
570
|
-
|
|
880
|
+
table.originalToBvhMap.clear();
|
|
881
|
+
table.blasData.clear();
|
|
571
882
|
|
|
572
883
|
this.bvhRoot = true;
|
|
573
884
|
this._disposeRefitWorker();
|
|
@@ -576,8 +887,8 @@ export class SceneProcessor {
|
|
|
576
887
|
// One aggregate line for the whole two-level build; the workers' per-mesh
|
|
577
888
|
// detail sits a level below at `debug`.
|
|
578
889
|
log.debug( fmt.list( [
|
|
579
|
-
`${fmt.n(
|
|
580
|
-
`${fmt.n( this.
|
|
890
|
+
`${fmt.n( table.setCount )} BLASes + TLAS`,
|
|
891
|
+
`${fmt.n( this.bvh.recordCount )} nodes`,
|
|
581
892
|
`SAH ${fmt.n( blasStats.sah )} · objMed ${blasStats.objMed} · spatMed ${blasStats.spatMed} · failed ${blasStats.failed}`,
|
|
582
893
|
blasStats.treeletsProcessed ? `treelets ${blasStats.treeletsImproved}/${blasStats.treeletsProcessed} improved` : null,
|
|
583
894
|
fmt.ms( duration ),
|
|
@@ -593,6 +904,7 @@ export class SceneProcessor {
|
|
|
593
904
|
log.error( 'BVH build failed:', error );
|
|
594
905
|
updateLoading( {
|
|
595
906
|
status: `BVH error: ${error.message}`,
|
|
907
|
+
failed: true,
|
|
596
908
|
progress: 75
|
|
597
909
|
} );
|
|
598
910
|
throw error;
|
|
@@ -605,20 +917,24 @@ export class SceneProcessor {
|
|
|
605
917
|
* Adjust BLAS node indices in-place within the combined bvhData buffer.
|
|
606
918
|
* @private
|
|
607
919
|
*/
|
|
608
|
-
_offsetBLASInPlace(
|
|
920
|
+
_offsetBLASInPlace( startNode, nodeCount, nodeOffset, triOffset ) {
|
|
921
|
+
|
|
922
|
+
const idx = this.bvhIndexChunks;
|
|
609
923
|
|
|
610
924
|
for ( let i = 0; i < nodeCount; i ++ ) {
|
|
611
925
|
|
|
612
|
-
const
|
|
926
|
+
const n = startNode + i;
|
|
927
|
+
const chunk = idx.chunkFor( n );
|
|
928
|
+
const o = idx.baseOf( n );
|
|
613
929
|
|
|
614
|
-
if (
|
|
930
|
+
if ( chunk[ o + 3 ] === BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
|
|
615
931
|
|
|
616
|
-
|
|
932
|
+
chunk[ o ] += triOffset;
|
|
617
933
|
|
|
618
934
|
} else {
|
|
619
935
|
|
|
620
|
-
|
|
621
|
-
|
|
936
|
+
chunk[ o + 3 ] += nodeOffset;
|
|
937
|
+
chunk[ o + 7 ] += nodeOffset;
|
|
622
938
|
|
|
623
939
|
}
|
|
624
940
|
|
|
@@ -640,7 +956,6 @@ export class SceneProcessor {
|
|
|
640
956
|
|
|
641
957
|
if ( tasks.length === 0 ) return Promise.resolve( [] );
|
|
642
958
|
|
|
643
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
644
959
|
const poolSize = Math.min( tasks.length, this.config.maxConcurrentTextureTasks || 4 );
|
|
645
960
|
const results = [];
|
|
646
961
|
let nextTask = 0;
|
|
@@ -663,10 +978,7 @@ export class SceneProcessor {
|
|
|
663
978
|
}
|
|
664
979
|
|
|
665
980
|
const { m, range } = tasks[ nextTask ++ ];
|
|
666
|
-
const meshTriData = this.
|
|
667
|
-
range.start * FPT,
|
|
668
|
-
( range.start + range.count ) * FPT
|
|
669
|
-
);
|
|
981
|
+
const meshTriData = this.triangles.copyOf( range.start, range.count );
|
|
670
982
|
|
|
671
983
|
// Disable treelet for tiny meshes
|
|
672
984
|
const triCount = range.count;
|
|
@@ -709,12 +1021,16 @@ export class SceneProcessor {
|
|
|
709
1021
|
if ( data.progress !== undefined ) return; // Ignore progress messages
|
|
710
1022
|
|
|
711
1023
|
const { m, range } = worker._currentTask;
|
|
1024
|
+
|
|
1025
|
+
// Write back now: holding one copy per mesh until the pool drains is the whole
|
|
1026
|
+
// triangle buffer over again, ~3 GB at 40M.
|
|
1027
|
+
if ( data.triangles ) this.triangles.setRecords( range.start, data.triangles );
|
|
1028
|
+
|
|
712
1029
|
results.push( {
|
|
713
1030
|
m,
|
|
714
1031
|
range,
|
|
715
1032
|
result: {
|
|
716
1033
|
bvhData: data.bvhData,
|
|
717
|
-
reorderedTriangles: data.triangles || null,
|
|
718
1034
|
originalToBvh: data.originalToBvh || null,
|
|
719
1035
|
splitStats: data.treeletStats || null,
|
|
720
1036
|
}
|
|
@@ -764,43 +1080,38 @@ export class SceneProcessor {
|
|
|
764
1080
|
}
|
|
765
1081
|
|
|
766
1082
|
/**
|
|
767
|
-
* Build
|
|
768
|
-
*
|
|
1083
|
+
* Build the per-template bvhToOriginal maps: stored-order index → the caller's triangle index.
|
|
1084
|
+
* Only this direction is kept — every reader walks stored order, so the forward map was a
|
|
1085
|
+
* second copy of the same permutation (114 MB at 30M triangles) that nothing needed.
|
|
769
1086
|
* @private
|
|
770
1087
|
*/
|
|
771
|
-
|
|
1088
|
+
_buildBvhToOriginalMaps() {
|
|
772
1089
|
|
|
773
|
-
|
|
1090
|
+
const table = this.instanceTable;
|
|
1091
|
+
for ( let m = 0; m < table.count; m ++ ) {
|
|
774
1092
|
|
|
775
|
-
|
|
1093
|
+
// Aliases read the owner's map straight out of the table; nothing to store.
|
|
1094
|
+
if ( ! table.isSet[ m ] || ! table.isOwner( m ) ) continue;
|
|
776
1095
|
|
|
777
|
-
|
|
1096
|
+
const t = table.sourceMesh[ m ];
|
|
1097
|
+
const triCount = table.tplTriCount[ t ];
|
|
1098
|
+
const originalToBvh = table.originalToBvhMap.get( t );
|
|
778
1099
|
|
|
779
1100
|
// Build per-mesh bvhToOriginal (inverse map for sequential writes)
|
|
780
|
-
const bvhToOrig = new Uint32Array(
|
|
781
|
-
|
|
782
|
-
if ( entry.originalToBvhMap ) {
|
|
783
|
-
|
|
784
|
-
for ( let i = 0; i < entry.triCount; i ++ ) {
|
|
1101
|
+
const bvhToOrig = new Uint32Array( triCount );
|
|
785
1102
|
|
|
786
|
-
|
|
787
|
-
this.originalToBvhMap[ entry.triOffset + i ] = entry.triOffset + bvhLocal;
|
|
788
|
-
bvhToOrig[ bvhLocal ] = i;
|
|
1103
|
+
if ( originalToBvh ) {
|
|
789
1104
|
|
|
790
|
-
|
|
1105
|
+
for ( let i = 0; i < triCount; i ++ ) bvhToOrig[ originalToBvh[ i ] ] = i;
|
|
791
1106
|
|
|
792
1107
|
} else {
|
|
793
1108
|
|
|
794
|
-
for ( let i = 0; i <
|
|
795
|
-
|
|
796
|
-
this.originalToBvhMap[ entry.triOffset + i ] = entry.triOffset + i;
|
|
797
|
-
bvhToOrig[ i ] = i;
|
|
798
|
-
|
|
799
|
-
}
|
|
1109
|
+
for ( let i = 0; i < triCount; i ++ ) bvhToOrig[ i ] = i;
|
|
800
1110
|
|
|
801
1111
|
}
|
|
802
1112
|
|
|
803
|
-
|
|
1113
|
+
table.bvhToOriginal.set( t, bvhToOrig );
|
|
1114
|
+
table.originalToBvhMap.delete( t ); // consumed; the caller clears the rest anyway
|
|
804
1115
|
|
|
805
1116
|
}
|
|
806
1117
|
|
|
@@ -931,11 +1242,27 @@ export class SceneProcessor {
|
|
|
931
1242
|
this._linearTexPacked = new Map();
|
|
932
1243
|
|
|
933
1244
|
let bucketOverflowReported = false;
|
|
1245
|
+
let undecodedReported = false;
|
|
934
1246
|
|
|
935
1247
|
// Assign one texture to its (bucket, layer) within a pool; dedup by source uuid.
|
|
936
1248
|
const assign = ( tex, lists, dedup, flat, shapes ) => {
|
|
937
1249
|
|
|
938
|
-
if ( ! tex
|
|
1250
|
+
if ( ! tex ) return - 1;
|
|
1251
|
+
|
|
1252
|
+
// A map whose image has not landed yet is indistinguishable here from no map at
|
|
1253
|
+
// all, and the material would render untextured with nothing in the log.
|
|
1254
|
+
if ( ! tex.image ) {
|
|
1255
|
+
|
|
1256
|
+
if ( ! undecodedReported ) this.config.issues?.record(
|
|
1257
|
+
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
1258
|
+
'a material map had not finished decoding when the scene was packed; it renders untextured',
|
|
1259
|
+
{ texture: tex.name || tex.source?.uuid || tex.uuid }
|
|
1260
|
+
);
|
|
1261
|
+
undecodedReported = true;
|
|
1262
|
+
return - 1;
|
|
1263
|
+
|
|
1264
|
+
}
|
|
1265
|
+
|
|
939
1266
|
const bucket = getTextureBucketId( tex.image.width, tex.image.height, shapes );
|
|
940
1267
|
const uuid = tex.source?.uuid ?? tex.uuid;
|
|
941
1268
|
const seen = dedup[ bucket ].get( uuid );
|
|
@@ -1033,9 +1360,10 @@ export class SceneProcessor {
|
|
|
1033
1360
|
_buildEmissiveData() {
|
|
1034
1361
|
|
|
1035
1362
|
this.emissiveTriangleCount = this.emissiveTriangleBuilder.extractEmissiveTriangles(
|
|
1036
|
-
this.
|
|
1363
|
+
this.triangles,
|
|
1037
1364
|
this.materials,
|
|
1038
|
-
this.triangleCount
|
|
1365
|
+
this.triangleCount,
|
|
1366
|
+
this.instanceTable ?? null
|
|
1039
1367
|
);
|
|
1040
1368
|
|
|
1041
1369
|
this.emissiveTriangleData = this.emissiveTriangleBuilder.createEmissiveRawData();
|
|
@@ -1088,7 +1416,7 @@ export class SceneProcessor {
|
|
|
1088
1416
|
|
|
1089
1417
|
// Reset all containers
|
|
1090
1418
|
this.triangles = [];
|
|
1091
|
-
this.
|
|
1419
|
+
this._setTriangleData( null );
|
|
1092
1420
|
this.triangleCount = 0;
|
|
1093
1421
|
this.materials = [];
|
|
1094
1422
|
this.meshTriangleRanges = null;
|
|
@@ -1113,7 +1441,10 @@ export class SceneProcessor {
|
|
|
1113
1441
|
this.cameras = [];
|
|
1114
1442
|
this.spheres = [];
|
|
1115
1443
|
this.bvhRoot = null;
|
|
1444
|
+
this.bvh = null;
|
|
1116
1445
|
this.bvhData = null;
|
|
1446
|
+
this.bvhIndexChunks = null;
|
|
1447
|
+
this.bvhIndex = null;
|
|
1117
1448
|
this.instanceTable = null;
|
|
1118
1449
|
this.lightBVHNodeData = null;
|
|
1119
1450
|
this.lightBVHNodeCount = 0;
|
|
@@ -1124,6 +1455,9 @@ export class SceneProcessor {
|
|
|
1124
1455
|
textureCreationTime: 0,
|
|
1125
1456
|
geometryExtractionTime: 0,
|
|
1126
1457
|
bvhBuildTime: 0,
|
|
1458
|
+
blasBuildTime: 0,
|
|
1459
|
+
tlasBuildTime: 0,
|
|
1460
|
+
bvhAssembleTime: 0,
|
|
1127
1461
|
totalProcessingTime: 0
|
|
1128
1462
|
};
|
|
1129
1463
|
|
|
@@ -1270,9 +1604,9 @@ export class SceneProcessor {
|
|
|
1270
1604
|
cameraCount: this.cameras.length,
|
|
1271
1605
|
processingComplete: this.processingStage === 'complete',
|
|
1272
1606
|
hasBVH: !! this.bvhRoot,
|
|
1273
|
-
hasTextures: !! this.materialData && !! this.
|
|
1607
|
+
hasTextures: !! this.materialData && !! this.bvh,
|
|
1274
1608
|
useFloat32Array: this.config.useFloat32Array,
|
|
1275
|
-
triangleDataSize: this.
|
|
1609
|
+
triangleDataSize: this.triangles ? ( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 ) + 'MB' : '0MB'
|
|
1276
1610
|
};
|
|
1277
1611
|
|
|
1278
1612
|
// Add performance metrics
|
|
@@ -1333,45 +1667,115 @@ export class SceneProcessor {
|
|
|
1333
1667
|
// ===== BVH REFIT (Animation Support) =====
|
|
1334
1668
|
|
|
1335
1669
|
/**
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
* @param {Float32Array} newPositions - 9 floats per triangle (ax,ay,az, bx,by,bz, cx,cy,cz) in original mesh order
|
|
1340
|
-
* @returns {Promise<{ refitTimeMs: number }>}
|
|
1670
|
+
* Make sure a store's chunks live in shared memory. Already-shared chunks are left alone; a
|
|
1671
|
+
* page without cross-origin isolation falls back to copying, which is the old cost.
|
|
1672
|
+
* @private
|
|
1341
1673
|
*/
|
|
1342
|
-
|
|
1674
|
+
_adoptSharedChunks( field, LaneType ) {
|
|
1343
1675
|
|
|
1344
|
-
|
|
1676
|
+
const records = this[ field ];
|
|
1677
|
+
if ( records.shared ) return;
|
|
1345
1678
|
|
|
1346
|
-
|
|
1679
|
+
const chunks = records.chunks.map( c => {
|
|
1347
1680
|
|
|
1348
|
-
|
|
1681
|
+
const view = new LaneType( new SharedArrayBuffer( c.byteLength ) );
|
|
1682
|
+
view.set( c );
|
|
1683
|
+
return view;
|
|
1349
1684
|
|
|
1350
|
-
|
|
1351
|
-
// is sized from the FIRST call's argument — so a short array reads past its end and
|
|
1352
|
-
// writes NaN into triangleData and every AABB above it, with no error anywhere. A
|
|
1353
|
-
// caller that misses a mesh (the hidden ground disk is easy to miss) sees the scene
|
|
1354
|
-
// silently vanish instead of a thrown exception, so check the length up front.
|
|
1355
|
-
const expectedFloats = this.triangleCount * 9;
|
|
1685
|
+
} );
|
|
1356
1686
|
|
|
1357
|
-
|
|
1687
|
+
const adopted = ChunkedRecords.adopt(
|
|
1688
|
+
chunks, records.recordCount, records.lanesPerRecord, records.recordsPerChunk
|
|
1689
|
+
);
|
|
1358
1690
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1691
|
+
if ( field === 'bvh' ) this._setBVHData( adopted );
|
|
1692
|
+
else this._setTriangleData( adopted );
|
|
1693
|
+
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Normalize a caller's positions or normals into a per-mesh reader. A full-scene array is
|
|
1698
|
+
* handed back as views into itself, so the old contract costs nothing extra; a callback lets a
|
|
1699
|
+
* caller build one mesh at a time and never hold 9 floats × every triangle at once — 1,030 MB
|
|
1700
|
+
* at 30M triangles, which is past what a renderer can allocate.
|
|
1701
|
+
* @private
|
|
1702
|
+
*/
|
|
1703
|
+
_meshSource( source, what ) {
|
|
1704
|
+
|
|
1705
|
+
if ( source == null ) return null;
|
|
1706
|
+
|
|
1707
|
+
const table = this.instanceTable;
|
|
1708
|
+
|
|
1709
|
+
if ( typeof source === 'function' ) {
|
|
1710
|
+
|
|
1711
|
+
// The table is keyed by placement and one instanced mesh holds several, so a raw
|
|
1712
|
+
// placement index is not something the caller can look up. Hand back the mesh.
|
|
1713
|
+
return i => {
|
|
1714
|
+
|
|
1715
|
+
const mesh = table.sourceMesh[ i ];
|
|
1716
|
+
const got = source( mesh, table.triCountOf( i ) );
|
|
1717
|
+
const want = table.triCountOf( i ) * 9;
|
|
1718
|
+
if ( got && got.length !== want ) {
|
|
1719
|
+
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`SceneProcessor: ${what}s for mesh ${mesh} must be ${want} floats ` +
|
|
1722
|
+
`(${table.triCountOf( i )} triangles × 9), got ${got.length}.`
|
|
1723
|
+
);
|
|
1724
|
+
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
return got ?? null;
|
|
1728
|
+
|
|
1729
|
+
};
|
|
1364
1730
|
|
|
1365
1731
|
}
|
|
1366
1732
|
|
|
1367
|
-
|
|
1733
|
+
// A short array reads past its end and writes NaN through every AABB above it with no
|
|
1734
|
+
// error anywhere, so the scene just vanishes. Check once here instead.
|
|
1735
|
+
const expected = ( this.expandedTriangleCount ?? this.triangleCount ) * 9;
|
|
1736
|
+
if ( source.length !== expected ) {
|
|
1368
1737
|
|
|
1369
1738
|
throw new Error(
|
|
1370
|
-
`SceneProcessor
|
|
1739
|
+
`SceneProcessor: expected ${expected} ${what} floats (${expected / 9} triangles × 9, ` +
|
|
1740
|
+
`full scene), got ${source.length}. Pass a per-mesh callback instead to avoid ` +
|
|
1741
|
+
'building one array for the whole scene.'
|
|
1371
1742
|
);
|
|
1372
1743
|
|
|
1373
1744
|
}
|
|
1374
1745
|
|
|
1746
|
+
return i => {
|
|
1747
|
+
|
|
1748
|
+
const from = table.expandedStartOf( i ) * 9;
|
|
1749
|
+
return source.subarray( from, from + table.triCountOf( i ) * 9 );
|
|
1750
|
+
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Refit BVH with updated vertex positions (same topology — no triangle add/remove).
|
|
1757
|
+
* O(N) bottom-up AABB update instead of full O(N log N) SAH rebuild.
|
|
1758
|
+
*
|
|
1759
|
+
* @param {Float32Array|function(number, number): Float32Array} newPositions - either 9 floats
|
|
1760
|
+
* per triangle for the whole scene (meshes in `this.meshes` order), or a callback given a
|
|
1761
|
+
* mesh index and its triangle count that returns just that mesh's 9-floats-per-triangle
|
|
1762
|
+
* slice. Prefer the callback: the scene-wide form is 1,030 MB at 30M triangles.
|
|
1763
|
+
* @param {Float32Array|function(number, number): Float32Array} [newNormals] - same two shapes
|
|
1764
|
+
* @returns {Promise<{ refitTimeMs: number }>}
|
|
1765
|
+
*/
|
|
1766
|
+
async refitBVH( newPositions, newNormals ) {
|
|
1767
|
+
|
|
1768
|
+
if ( ! this.bvh || ! this.triangles || ! this.instanceTable ) {
|
|
1769
|
+
|
|
1770
|
+
throw new Error( 'No BVH data available for refit. Run buildBVH() first.' );
|
|
1771
|
+
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const positionsFor = this._meshSource( newPositions, 'position' );
|
|
1775
|
+
const normalsFor = this._meshSource( newNormals, 'normal' );
|
|
1776
|
+
|
|
1777
|
+
if ( ! positionsFor ) throw new Error( 'SceneProcessor.refitBVH: positions are required.' );
|
|
1778
|
+
|
|
1375
1779
|
// Lazy-create worker
|
|
1376
1780
|
if ( ! this._refitWorker ) {
|
|
1377
1781
|
|
|
@@ -1379,55 +1783,55 @@ export class SceneProcessor {
|
|
|
1379
1783
|
|
|
1380
1784
|
}
|
|
1381
1785
|
|
|
1382
|
-
// First call:
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1786
|
+
// First call: move triangles and nodes into shared memory so the worker refits them in
|
|
1787
|
+
// place. Positions never cross the boundary — they are scattered into the triangle records
|
|
1788
|
+
// below, one mesh at a time, so nothing scene-sized is allocated on either side.
|
|
1385
1789
|
if ( ! this._refitSharedBuffers ) {
|
|
1386
1790
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1791
|
+
// Both stores are built on SharedArrayBuffer when the page allows it, so the worker
|
|
1792
|
+
// takes them as they are. Copying instead would duplicate the two largest structures
|
|
1793
|
+
// in the scene — 3,968 MB at 30M triangles, which is where this used to fail.
|
|
1794
|
+
this._adoptSharedChunks( 'bvh', Float32Array );
|
|
1795
|
+
this._adoptSharedChunks( 'triangles', Uint32Array );
|
|
1390
1796
|
|
|
1391
|
-
const
|
|
1392
|
-
const
|
|
1797
|
+
const sharedBvhBufs = this.bvh.chunks.map( c => c.buffer );
|
|
1798
|
+
const sharedTriBufs = this.triangles.chunks.map( c => c.buffer );
|
|
1799
|
+
this._refitSharedBuffers = { bvhBufs: sharedBvhBufs, triBufs: sharedTriBufs };
|
|
1393
1800
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1801
|
+
this._refitWorker.postMessage( {
|
|
1802
|
+
type: 'init',
|
|
1803
|
+
sharedBvhBufs,
|
|
1804
|
+
bvhRecordCount: this.bvh.recordCount,
|
|
1805
|
+
bvhRecordsPerChunk: this.bvh.recordsPerChunk,
|
|
1806
|
+
sharedTriBufs,
|
|
1807
|
+
triRecordCount: this.triangles.recordCount,
|
|
1808
|
+
triLanesPerRecord: this.triangles.lanesPerRecord,
|
|
1809
|
+
triRecordsPerChunk: this.triangles.recordsPerChunk,
|
|
1810
|
+
} );
|
|
1396
1811
|
|
|
1397
|
-
|
|
1398
|
-
this.bvhData = sharedBvhData;
|
|
1399
|
-
this.triangleData = sharedTriData;
|
|
1812
|
+
}
|
|
1400
1813
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1814
|
+
// Scatter straight into the shared triangle records, mesh by mesh, so the caller's slice
|
|
1815
|
+
// can be released as soon as it is read. Callers hand over world space; triangles are
|
|
1816
|
+
// stored per instance, so each mesh comes back through its own inverse.
|
|
1817
|
+
const table = this.instanceTable;
|
|
1818
|
+
for ( let i = 0; i < table.count; i ++ ) {
|
|
1406
1819
|
|
|
1407
|
-
|
|
1820
|
+
if ( ! table.isSet[ i ] || ! table.isOwner( i ) ) continue;
|
|
1408
1821
|
|
|
1409
|
-
|
|
1822
|
+
const p = positionsFor( i );
|
|
1823
|
+
if ( ! p ) continue;
|
|
1824
|
+
this._updateMeshTrianglePositions( i, p );
|
|
1410
1825
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
triBuf: sharedTriBuf,
|
|
1414
|
-
posBuf: sharedPosBuf,
|
|
1415
|
-
posView: new Float32Array( sharedPosBuf ),
|
|
1416
|
-
};
|
|
1826
|
+
// Smooth normals overwrite the face normals just computed, so they follow per mesh.
|
|
1827
|
+
if ( normalsFor ) {
|
|
1417
1828
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
type: 'init',
|
|
1421
|
-
sharedBvhBuf,
|
|
1422
|
-
sharedTriBuf,
|
|
1423
|
-
sharedPosBuf,
|
|
1424
|
-
bvhToOriginal,
|
|
1425
|
-
}, [ bvhToOriginal.buffer ] );
|
|
1829
|
+
const n = normalsFor( i );
|
|
1830
|
+
if ( n ) this._patchMeshSmoothNormals( i, n );
|
|
1426
1831
|
|
|
1427
|
-
|
|
1832
|
+
}
|
|
1428
1833
|
|
|
1429
|
-
|
|
1430
|
-
this._refitSharedBuffers.posView.set( newPositions );
|
|
1834
|
+
}
|
|
1431
1835
|
|
|
1432
1836
|
return new Promise( ( resolve, reject ) => {
|
|
1433
1837
|
|
|
@@ -1436,14 +1840,6 @@ export class SceneProcessor {
|
|
|
1436
1840
|
const msg = e.data;
|
|
1437
1841
|
if ( msg.type === 'refitComplete' ) {
|
|
1438
1842
|
|
|
1439
|
-
// bvhData/triangleData already updated via shared memory.
|
|
1440
|
-
// If smooth normals provided, overwrite the face normals the worker computed.
|
|
1441
|
-
if ( newNormals ) {
|
|
1442
|
-
|
|
1443
|
-
this._patchSmoothNormals( newNormals );
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
1843
|
resolve( { refitTimeMs: msg.refitTimeMs } );
|
|
1448
1844
|
|
|
1449
1845
|
} else if ( msg.type === 'error' ) {
|
|
@@ -1465,9 +1861,18 @@ export class SceneProcessor {
|
|
|
1465
1861
|
* Overwrite face normals in triangleData with smooth vertex normals (full scene).
|
|
1466
1862
|
* @private
|
|
1467
1863
|
*/
|
|
1468
|
-
_patchSmoothNormals(
|
|
1864
|
+
_patchSmoothNormals( normalsFor ) {
|
|
1865
|
+
|
|
1866
|
+
const table = this.instanceTable;
|
|
1867
|
+
if ( ! table ) return;
|
|
1469
1868
|
|
|
1470
|
-
|
|
1869
|
+
for ( let i = 0; i < table.count; i ++ ) {
|
|
1870
|
+
|
|
1871
|
+
if ( ! table.isSet[ i ] || ! table.isOwner( i ) ) continue;
|
|
1872
|
+
const n = normalsFor( i );
|
|
1873
|
+
if ( n ) this._patchMeshSmoothNormals( i, n );
|
|
1874
|
+
|
|
1875
|
+
}
|
|
1471
1876
|
|
|
1472
1877
|
}
|
|
1473
1878
|
|
|
@@ -1475,39 +1880,38 @@ export class SceneProcessor {
|
|
|
1475
1880
|
* Refit specific BLASes and rebuild TLAS after object transform or per-mesh animation.
|
|
1476
1881
|
* Runs on the main thread (fast for per-mesh updates).
|
|
1477
1882
|
*
|
|
1478
|
-
* @param {number[]} affectedMeshIndices - Indices into
|
|
1883
|
+
* @param {number[]} affectedMeshIndices - Indices into `this.meshes`, the same space
|
|
1884
|
+
* {@link updateMeshTransforms} takes
|
|
1479
1885
|
* @param {Float32Array} newPositions - 9 floats per triangle in original mesh order (full scene)
|
|
1480
1886
|
* @param {Float32Array} [newNormals] - Optional smooth normals (9 floats per tri)
|
|
1481
1887
|
* @returns {{ refitTimeMs: number }}
|
|
1482
1888
|
*/
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1889
|
+
/**
|
|
1890
|
+
* The placement holding a mesh's triangles. The instance table is keyed by placement and one
|
|
1891
|
+
* instanced mesh contributes several, so every entry point taking mesh indices converts here.
|
|
1892
|
+
*
|
|
1893
|
+
* @param {number} meshIndex
|
|
1894
|
+
* @returns {number} placement index, or -1 when the mesh placed nothing
|
|
1895
|
+
* @private
|
|
1896
|
+
*/
|
|
1897
|
+
_placementOf( meshIndex ) {
|
|
1486
1898
|
|
|
1487
|
-
|
|
1899
|
+
return this.instanceTable?.placementRunOf( meshIndex )?.start ?? - 1;
|
|
1488
1900
|
|
|
1489
|
-
|
|
1901
|
+
}
|
|
1490
1902
|
|
|
1491
|
-
|
|
1492
|
-
// affected meshes with no error. Same silent-corruption trap as refitBVH.
|
|
1493
|
-
const expectedFloats = this.triangleCount * 9;
|
|
1903
|
+
refitBLASes( affectedMeshIndices, newPositions, newNormals ) {
|
|
1494
1904
|
|
|
1495
|
-
if (
|
|
1905
|
+
if ( ! this.instanceTable || ! this.bvh || ! this.triangles ) {
|
|
1496
1906
|
|
|
1497
|
-
throw new Error(
|
|
1498
|
-
`SceneProcessor.refitBLASes: expected ${expectedFloats} position floats ` +
|
|
1499
|
-
`(${this.triangleCount} triangles × 9, full scene), got ${newPositions?.length ?? 'none'}.`
|
|
1500
|
-
);
|
|
1907
|
+
throw new Error( 'No TLAS/BLAS data available. Run buildBVH() first.' );
|
|
1501
1908
|
|
|
1502
1909
|
}
|
|
1503
1910
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
throw new Error(
|
|
1507
|
-
`SceneProcessor.refitBLASes: expected ${expectedFloats} normal floats, got ${newNormals.length}.`
|
|
1508
|
-
);
|
|
1911
|
+
const positionsFor = this._meshSource( newPositions, 'position' );
|
|
1912
|
+
const normalsFor = this._meshSource( newNormals, 'normal' );
|
|
1509
1913
|
|
|
1510
|
-
|
|
1914
|
+
if ( ! positionsFor ) throw new Error( 'SceneProcessor.refitBLASes: positions are required.' );
|
|
1511
1915
|
|
|
1512
1916
|
const start = performance.now();
|
|
1513
1917
|
|
|
@@ -1519,31 +1923,49 @@ export class SceneProcessor {
|
|
|
1519
1923
|
}
|
|
1520
1924
|
|
|
1521
1925
|
// Step 1: Update triangle positions and refit each affected BLAS
|
|
1522
|
-
|
|
1926
|
+
const table = this.instanceTable;
|
|
1927
|
+
|
|
1928
|
+
for ( const meshIndex of affectedMeshIndices ) {
|
|
1523
1929
|
|
|
1524
|
-
const
|
|
1525
|
-
if ( !
|
|
1930
|
+
const placement = this._placementOf( meshIndex );
|
|
1931
|
+
if ( placement < 0 || ! table.isSet[ placement ] ) continue;
|
|
1526
1932
|
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1933
|
+
// Triangles are shared between placements of one geometry, so writing this mesh's
|
|
1934
|
+
// vertices would move every other copy with them. Anything that deforms is extracted
|
|
1935
|
+
// with triangles of its own, so reaching here means the wrong mesh was handed over.
|
|
1936
|
+
if ( ! table.isOwner( placement ) ) {
|
|
1529
1937
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1938
|
+
const owner = table.tplOwner[ table.sourceMesh[ placement ] ];
|
|
1939
|
+
this.config.issues?.record(
|
|
1940
|
+
ISSUE_CODES.REFIT_SHARED_GEOMETRY,
|
|
1941
|
+
`mesh ${meshIndex} shares its triangles with another placement; deforming it would move every copy, so it was skipped`,
|
|
1942
|
+
{ meshIndex, owner: table.sourceMesh[ owner ] ?? owner }
|
|
1943
|
+
);
|
|
1944
|
+
continue;
|
|
1945
|
+
|
|
1946
|
+
}
|
|
1532
1947
|
|
|
1533
|
-
|
|
1948
|
+
const p = positionsFor( placement );
|
|
1949
|
+
if ( ! p ) continue;
|
|
1950
|
+
this._updateMeshTrianglePositions( placement, p );
|
|
1951
|
+
|
|
1952
|
+
if ( normalsFor ) {
|
|
1953
|
+
|
|
1954
|
+
const n = normalsFor( placement );
|
|
1955
|
+
if ( n ) this._patchMeshSmoothNormals( placement, n );
|
|
1534
1956
|
|
|
1535
1957
|
}
|
|
1536
1958
|
|
|
1537
1959
|
// Refit this BLAS's nodes
|
|
1538
1960
|
this._blasRefitter.refitRange(
|
|
1539
|
-
this.
|
|
1540
|
-
this.
|
|
1541
|
-
|
|
1542
|
-
|
|
1961
|
+
this.bvh,
|
|
1962
|
+
this.triangles,
|
|
1963
|
+
table.blasOffsetOf( placement ),
|
|
1964
|
+
table.blasNodeCountOf( placement )
|
|
1543
1965
|
);
|
|
1544
1966
|
|
|
1545
1967
|
// Recompute this mesh's AABB for TLAS rebuild
|
|
1546
|
-
this.instanceTable.recomputeAABB(
|
|
1968
|
+
this.instanceTable.recomputeAABB( placement, this.bvh, this.triangles );
|
|
1547
1969
|
|
|
1548
1970
|
}
|
|
1549
1971
|
|
|
@@ -1554,6 +1976,121 @@ export class SceneProcessor {
|
|
|
1554
1976
|
|
|
1555
1977
|
}
|
|
1556
1978
|
|
|
1979
|
+
/**
|
|
1980
|
+
* Move objects without touching their geometry: the placements a mesh contributed get new
|
|
1981
|
+
* transforms, the TLAS leaves get the matching world-to-object matrices, and the TLAS boxes
|
|
1982
|
+
* are refit. Triangles, BLASes and the GPU triangle buffer are all left alone.
|
|
1983
|
+
*
|
|
1984
|
+
* This is what a gizmo drag wants. {@link refitBLASes} exists for geometry that actually
|
|
1985
|
+
* deformed; used for a rigid move it would bake the new world positions into triangles that
|
|
1986
|
+
* a second placement of the same geometry may be sharing, and drag that one along too.
|
|
1987
|
+
*
|
|
1988
|
+
* @param {number[]} meshIndices - indices into `this.meshes`
|
|
1989
|
+
* @returns {{ refitTimeMs: number, placements: number }}
|
|
1990
|
+
*/
|
|
1991
|
+
updateMeshTransforms( meshIndices ) {
|
|
1992
|
+
|
|
1993
|
+
if ( ! this.instanceTable || ! this.bvh ) {
|
|
1994
|
+
|
|
1995
|
+
throw new Error( 'No TLAS/BLAS data available. Run buildBVH() first.' );
|
|
1996
|
+
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
const start = performance.now();
|
|
2000
|
+
const table = this.instanceTable;
|
|
2001
|
+
const composed = this._transformScratch ??= new Float32Array( 16 );
|
|
2002
|
+
let moved = 0;
|
|
2003
|
+
|
|
2004
|
+
for ( const meshIndex of meshIndices ) {
|
|
2005
|
+
|
|
2006
|
+
const mesh = this.meshes?.[ meshIndex ];
|
|
2007
|
+
const run = table.placementRunOf( meshIndex );
|
|
2008
|
+
if ( ! mesh || ! run ) continue;
|
|
2009
|
+
|
|
2010
|
+
mesh.updateMatrixWorld( true );
|
|
2011
|
+
const world = mesh.matrixWorld.elements;
|
|
2012
|
+
const instances = mesh.isInstancedMesh ? this._ownInstanceMatrices( mesh ) : null;
|
|
2013
|
+
// Baked triangles hold the pose they were extracted at; the leaf only carries the delta.
|
|
2014
|
+
const bakeInverse = table.tplBakeInverse?.get( table.sourceMesh[ run.start ] ) ?? null;
|
|
2015
|
+
|
|
2016
|
+
for ( let k = 0; k < run.count; k ++ ) {
|
|
2017
|
+
|
|
2018
|
+
const p = run.start + k;
|
|
2019
|
+
if ( ! table.isSet[ p ] ) continue;
|
|
2020
|
+
|
|
2021
|
+
if ( instances ) {
|
|
2022
|
+
|
|
2023
|
+
multiplyAffine( world, instances, k * 16, composed );
|
|
2024
|
+
table.setPlacementMatrix( p, composed );
|
|
2025
|
+
|
|
2026
|
+
} else if ( bakeInverse ) {
|
|
2027
|
+
|
|
2028
|
+
multiplyAffine( world, bakeInverse, 0, composed );
|
|
2029
|
+
table.setPlacementMatrix( p, composed );
|
|
2030
|
+
|
|
2031
|
+
} else {
|
|
2032
|
+
|
|
2033
|
+
table.setPlacementMatrix( p, world );
|
|
2034
|
+
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
this._writeLeafMatrix( p );
|
|
2038
|
+
moved ++;
|
|
2039
|
+
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
this._refitTLAS();
|
|
2045
|
+
|
|
2046
|
+
return { refitTimeMs: performance.now() - start, placements: moved };
|
|
2047
|
+
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* An InstancedMesh's own instance matrices, split off the placement pool if they still alias it.
|
|
2052
|
+
*
|
|
2053
|
+
* The extractor points a host-at-origin InstancedMesh straight at the pool, since world and
|
|
2054
|
+
* instance matrices hold identical bytes there. Composing a moved host back into the pool would
|
|
2055
|
+
* overwrite the matrices it just read, so the next move would compose onto its own result.
|
|
2056
|
+
* @private
|
|
2057
|
+
*/
|
|
2058
|
+
_ownInstanceMatrices( mesh ) {
|
|
2059
|
+
|
|
2060
|
+
const attr = mesh.instanceMatrix;
|
|
2061
|
+
if ( ! attr?.array ) return null;
|
|
2062
|
+
|
|
2063
|
+
if ( attr.array.buffer === this.instanceTable.world.buffer ) {
|
|
2064
|
+
|
|
2065
|
+
attr.array = attr.array.slice();
|
|
2066
|
+
attr.needsUpdate = true;
|
|
2067
|
+
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
return attr.array;
|
|
2071
|
+
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/** Refresh one TLAS leaf's world-to-object matrix from the table. @private */
|
|
2075
|
+
_writeLeafMatrix( placement ) {
|
|
2076
|
+
|
|
2077
|
+
const node = this.instanceTable.tlasLeafIndex[ placement ];
|
|
2078
|
+
if ( node < 0 ) return;
|
|
2079
|
+
|
|
2080
|
+
TLASBuilder.writeLeafMatrix(
|
|
2081
|
+
this.bvh.chunkFor( node ), this.bvhIndexChunks.chunkFor( node ), this.bvh.baseOf( node ),
|
|
2082
|
+
this.instanceTable.world, placement
|
|
2083
|
+
);
|
|
2084
|
+
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/** The TLAS occupies the front of the BVH buffer and is the only part a move rewrites. */
|
|
2088
|
+
computeTLASDirtyRange() {
|
|
2089
|
+
|
|
2090
|
+
return { offset: 0, count: this.instanceTable.tlasNodeCount * 16 };
|
|
2091
|
+
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1557
2094
|
/**
|
|
1558
2095
|
* Computes the dirty buffer ranges for a set of affected mesh BLASes.
|
|
1559
2096
|
* Used for partial GPU upload after per-mesh refit instead of full buffer copy.
|
|
@@ -1568,13 +2105,14 @@ export class SceneProcessor {
|
|
|
1568
2105
|
const triRanges = [];
|
|
1569
2106
|
const bvhRanges = [];
|
|
1570
2107
|
|
|
1571
|
-
for ( const
|
|
2108
|
+
for ( const meshIndex of affectedMeshIndices ) {
|
|
1572
2109
|
|
|
1573
|
-
const
|
|
1574
|
-
|
|
2110
|
+
const table = this.instanceTable;
|
|
2111
|
+
const placement = this._placementOf( meshIndex );
|
|
2112
|
+
if ( placement < 0 || ! table.isSet[ placement ] ) continue;
|
|
1575
2113
|
|
|
1576
|
-
triRanges.push( { offset:
|
|
1577
|
-
bvhRanges.push( { offset:
|
|
2114
|
+
triRanges.push( { offset: table.triOffsetOf( placement ) * FPT, count: table.triCountOf( placement ) * FPT } );
|
|
2115
|
+
bvhRanges.push( { offset: table.blasOffsetOf( placement ) * FPN, count: table.blasNodeCountOf( placement ) * FPN } );
|
|
1578
2116
|
|
|
1579
2117
|
}
|
|
1580
2118
|
|
|
@@ -1597,23 +2135,23 @@ export class SceneProcessor {
|
|
|
1597
2135
|
*/
|
|
1598
2136
|
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture ) {
|
|
1599
2137
|
|
|
1600
|
-
if ( ! this.
|
|
2138
|
+
if ( ! this.triangles ) {
|
|
1601
2139
|
|
|
1602
2140
|
log.error( 'failed to get triangle data' );
|
|
1603
2141
|
return false;
|
|
1604
2142
|
|
|
1605
2143
|
}
|
|
1606
2144
|
|
|
1607
|
-
pathTracer.setTriangleData( this.
|
|
2145
|
+
pathTracer.setTriangleData( this.triangles, this.triangleCount );
|
|
1608
2146
|
|
|
1609
|
-
if ( ! this.
|
|
2147
|
+
if ( ! this.bvh ) {
|
|
1610
2148
|
|
|
1611
2149
|
log.error( 'failed to get BVH data' );
|
|
1612
2150
|
return false;
|
|
1613
2151
|
|
|
1614
2152
|
}
|
|
1615
2153
|
|
|
1616
|
-
pathTracer.setBVHData( this.
|
|
2154
|
+
pathTracer.setBVHData( this.bvh );
|
|
1617
2155
|
pathTracer.setInstanceTable( this.instanceTable );
|
|
1618
2156
|
|
|
1619
2157
|
if ( this.materialData ) {
|
|
@@ -1686,7 +2224,7 @@ export class SceneProcessor {
|
|
|
1686
2224
|
|
|
1687
2225
|
const changed = this.emissiveTriangleBuilder.updateMaterialEmissive(
|
|
1688
2226
|
materialIndex, mat,
|
|
1689
|
-
this.
|
|
2227
|
+
this.triangles, this.materials, this.triangleCount,
|
|
1690
2228
|
);
|
|
1691
2229
|
|
|
1692
2230
|
if ( ! changed ) return null;
|
|
@@ -1739,64 +2277,194 @@ export class SceneProcessor {
|
|
|
1739
2277
|
|
|
1740
2278
|
}
|
|
1741
2279
|
|
|
2280
|
+
/**
|
|
2281
|
+
* Copy world-space positions into `dst`, each mesh's range carried into its own space.
|
|
2282
|
+
* @private
|
|
2283
|
+
*/
|
|
2284
|
+
/**
|
|
2285
|
+
* Build the TLAS, off the main thread when a worker is available.
|
|
2286
|
+
*
|
|
2287
|
+
* The worker returns structure only; leaf payloads are filled here. On any failure the
|
|
2288
|
+
* synchronous path runs instead, so a blocked or unavailable worker costs responsiveness,
|
|
2289
|
+
* never a scene.
|
|
2290
|
+
* @private
|
|
2291
|
+
*/
|
|
2292
|
+
async _buildTLAS( table ) {
|
|
2293
|
+
|
|
2294
|
+
const n = table.count;
|
|
2295
|
+
|
|
2296
|
+
// Below this the build is a few ms and the round trip costs more than it saves.
|
|
2297
|
+
if ( n >= TLAS_WORKER_MIN_ENTRIES && ! this._tlasWorkerFailed ) {
|
|
2298
|
+
|
|
2299
|
+
try {
|
|
2300
|
+
|
|
2301
|
+
// The worker needs a buffer it can own, and world bounds are derived anyway.
|
|
2302
|
+
const aabbs = new Float64Array( n * 6 );
|
|
2303
|
+
table.writeWorldAABBs( aabbs );
|
|
2304
|
+
const { tlasData, nodeCount } = await this._runTLASWorker( aabbs, n );
|
|
2305
|
+
TLASBuilder.fillLeaves( tlasData, nodeCount, table );
|
|
2306
|
+
return tlasData;
|
|
2307
|
+
|
|
2308
|
+
} catch ( error ) {
|
|
2309
|
+
|
|
2310
|
+
this._tlasWorkerFailed = true;
|
|
2311
|
+
log.warn( `TLAS worker unavailable (${error.message}); building on the main thread` );
|
|
2312
|
+
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
return this.tlasBuilder.build( table ).data;
|
|
2318
|
+
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
/** @private */
|
|
2322
|
+
_runTLASWorker( aabbs, count ) {
|
|
2323
|
+
|
|
2324
|
+
if ( ! this._tlasWorker ) this._tlasWorker = new TLASWorker();
|
|
2325
|
+
const worker = this._tlasWorker;
|
|
2326
|
+
|
|
2327
|
+
return new Promise( ( resolve, reject ) => {
|
|
2328
|
+
|
|
2329
|
+
const done = ( event ) => {
|
|
2330
|
+
|
|
2331
|
+
worker.removeEventListener( 'message', done );
|
|
2332
|
+
worker.removeEventListener( 'error', failed );
|
|
2333
|
+
if ( event.data?.error ) reject( new Error( event.data.error ) );
|
|
2334
|
+
else resolve( event.data );
|
|
2335
|
+
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
const failed = ( event ) => {
|
|
2339
|
+
|
|
2340
|
+
worker.removeEventListener( 'message', done );
|
|
2341
|
+
worker.removeEventListener( 'error', failed );
|
|
2342
|
+
reject( new Error( event.message || 'worker error' ) );
|
|
2343
|
+
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
worker.addEventListener( 'message', done );
|
|
2347
|
+
worker.addEventListener( 'error', failed );
|
|
2348
|
+
worker.postMessage(
|
|
2349
|
+
{ aabbs: aabbs.buffer, count, logLevel: workerLogLevel() },
|
|
2350
|
+
[ aabbs.buffer ]
|
|
2351
|
+
);
|
|
2352
|
+
|
|
2353
|
+
} );
|
|
2354
|
+
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/** Keep the f32 view in step with the uint record buffer. @private */
|
|
2358
|
+
/** Keep the chunked BVH and its u32 index view in step. @private */
|
|
2359
|
+
_setBVHData( data ) {
|
|
2360
|
+
|
|
2361
|
+
const records = ! data ? null
|
|
2362
|
+
: ( data instanceof ChunkedRecords
|
|
2363
|
+
? data
|
|
2364
|
+
: ChunkedRecords.adopt( [ data ], data.length / 16, 16, data.length / 16 ) );
|
|
2365
|
+
|
|
2366
|
+
this.bvh = records;
|
|
2367
|
+
this.bvhIndexChunks = records ? records.viewAs( Uint32Array ) : null;
|
|
2368
|
+
// Null once the BVH needs more than one chunk; hot paths use `bvh` / `bvhIndexChunks`.
|
|
2369
|
+
this.bvhData = records ? records.single : null;
|
|
2370
|
+
this.bvhIndex = this.bvhIndexChunks ? this.bvhIndexChunks.single : null;
|
|
2371
|
+
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
_setTriangleData( data ) {
|
|
2375
|
+
|
|
2376
|
+
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
2377
|
+
const records = ! data ? null
|
|
2378
|
+
: ( data instanceof ChunkedRecords
|
|
2379
|
+
? data
|
|
2380
|
+
: ChunkedRecords.adopt( [ data ], data.length / FPT, FPT, data.length / FPT ) );
|
|
2381
|
+
|
|
2382
|
+
this.triangles = records;
|
|
2383
|
+
this.triangleFloatChunks = records ? records.viewAs( Float32Array ) : null;
|
|
2384
|
+
// Null once the scene needs more than one chunk; everything hot goes through
|
|
2385
|
+
// `triangles` / `triangleFloatChunks` instead.
|
|
2386
|
+
this.triangleData = records ? records.single : null;
|
|
2387
|
+
this.triangleFloats = this.triangleFloatChunks ? this.triangleFloatChunks.single : null;
|
|
2388
|
+
|
|
2389
|
+
}
|
|
2390
|
+
|
|
1742
2391
|
/**
|
|
1743
2392
|
* Update triangle positions for a single mesh entry.
|
|
1744
2393
|
* Iterates in BVH order for sequential writes (cache-friendly), random reads from newPositions.
|
|
1745
2394
|
* @private
|
|
1746
2395
|
*/
|
|
1747
|
-
_updateMeshTrianglePositions(
|
|
2396
|
+
_updateMeshTrianglePositions( i, meshPositions ) {
|
|
1748
2397
|
|
|
1749
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
1750
2398
|
const PA = TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET;
|
|
1751
2399
|
const PB = TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET;
|
|
1752
2400
|
const PC = TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET;
|
|
1753
|
-
const NA = TRIANGLE_DATA_LAYOUT.
|
|
1754
|
-
const NB = TRIANGLE_DATA_LAYOUT.
|
|
1755
|
-
const NC = TRIANGLE_DATA_LAYOUT.
|
|
1756
|
-
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
const
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
1772
|
-
const
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2401
|
+
const NA = TRIANGLE_DATA_LAYOUT.NORMAL_A_PACKED_OFFSET;
|
|
2402
|
+
const NB = TRIANGLE_DATA_LAYOUT.NORMAL_B_PACKED_OFFSET;
|
|
2403
|
+
const NC = TRIANGLE_DATA_LAYOUT.NORMAL_C_PACKED_OFFSET;
|
|
2404
|
+
const tri = this.triangles, triF = this.triangleFloatChunks;
|
|
2405
|
+
const table = this.instanceTable;
|
|
2406
|
+
|
|
2407
|
+
const bvhToOrig = table.bvhToOriginalOf( i );
|
|
2408
|
+
const triOffset = table.triOffsetOf( i ), triCount = table.triCountOf( i );
|
|
2409
|
+
|
|
2410
|
+
// Callers hand over world-space positions, which is the only space they can build
|
|
2411
|
+
// one in; triangles are stored per instance, so they come back through the inverse.
|
|
2412
|
+
const m = table.matrixInverseOf( i );
|
|
2413
|
+
const identity = isIdentity( m );
|
|
2414
|
+
|
|
2415
|
+
for ( let bvhLocal = 0; bvhLocal < triCount; bvhLocal ++ ) {
|
|
2416
|
+
|
|
2417
|
+
const origLocal = bvhToOrig ? bvhToOrig[ bvhLocal ] : bvhLocal;
|
|
2418
|
+
const t = triOffset + bvhLocal;
|
|
2419
|
+
const f = triF.chunkFor( t ), u = tri.chunkFor( t );
|
|
2420
|
+
const dst = tri.baseOf( t );
|
|
2421
|
+
const src = origLocal * 9;
|
|
2422
|
+
|
|
2423
|
+
let ax = meshPositions[ src ];
|
|
2424
|
+
let ay = meshPositions[ src + 1 ];
|
|
2425
|
+
let az = meshPositions[ src + 2 ];
|
|
2426
|
+
let bx = meshPositions[ src + 3 ];
|
|
2427
|
+
let by = meshPositions[ src + 4 ];
|
|
2428
|
+
let bz = meshPositions[ src + 5 ];
|
|
2429
|
+
let cx = meshPositions[ src + 6 ];
|
|
2430
|
+
let cy = meshPositions[ src + 7 ];
|
|
2431
|
+
let cz = meshPositions[ src + 8 ];
|
|
2432
|
+
|
|
2433
|
+
if ( ! identity ) {
|
|
2434
|
+
|
|
2435
|
+
const px = ( x, y, z ) => m[ 0 ] * x + m[ 4 ] * y + m[ 8 ] * z + m[ 12 ];
|
|
2436
|
+
const py = ( x, y, z ) => m[ 1 ] * x + m[ 5 ] * y + m[ 9 ] * z + m[ 13 ];
|
|
2437
|
+
const pz = ( x, y, z ) => m[ 2 ] * x + m[ 6 ] * y + m[ 10 ] * z + m[ 14 ];
|
|
2438
|
+
const a = [ px( ax, ay, az ), py( ax, ay, az ), pz( ax, ay, az ) ];
|
|
2439
|
+
const b = [ px( bx, by, bz ), py( bx, by, bz ), pz( bx, by, bz ) ];
|
|
2440
|
+
const c = [ px( cx, cy, cz ), py( cx, cy, cz ), pz( cx, cy, cz ) ];
|
|
2441
|
+
ax = a[ 0 ]; ay = a[ 1 ]; az = a[ 2 ];
|
|
2442
|
+
bx = b[ 0 ]; by = b[ 1 ]; bz = b[ 2 ];
|
|
2443
|
+
cx = c[ 0 ]; cy = c[ 1 ]; cz = c[ 2 ];
|
|
2444
|
+
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
f[ dst + PA ] = ax;
|
|
2448
|
+
f[ dst + PA + 1 ] = ay;
|
|
2449
|
+
f[ dst + PA + 2 ] = az;
|
|
2450
|
+
f[ dst + PB ] = bx;
|
|
2451
|
+
f[ dst + PB + 1 ] = by;
|
|
2452
|
+
f[ dst + PB + 2 ] = bz;
|
|
2453
|
+
f[ dst + PC ] = cx;
|
|
2454
|
+
f[ dst + PC + 1 ] = cy;
|
|
2455
|
+
f[ dst + PC + 2 ] = cz;
|
|
1784
2456
|
|
|
1785
2457
|
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
1786
2458
|
const acx = cx - ax, acy = cy - ay, acz = cz - az;
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
this.triangleData[ dst + NB + 2 ] = nz;
|
|
1797
|
-
this.triangleData[ dst + NC ] = nx;
|
|
1798
|
-
this.triangleData[ dst + NC + 1 ] = ny;
|
|
1799
|
-
this.triangleData[ dst + NC + 2 ] = nz;
|
|
2459
|
+
const packed = packNormalOct(
|
|
2460
|
+
aby * acz - abz * acy,
|
|
2461
|
+
abz * acx - abx * acz,
|
|
2462
|
+
abx * acy - aby * acx
|
|
2463
|
+
);
|
|
2464
|
+
|
|
2465
|
+
u[ dst + NA ] = packed;
|
|
2466
|
+
u[ dst + NB ] = packed;
|
|
2467
|
+
u[ dst + NC ] = packed;
|
|
1800
2468
|
|
|
1801
2469
|
}
|
|
1802
2470
|
|
|
@@ -1806,9 +2474,13 @@ export class SceneProcessor {
|
|
|
1806
2474
|
* Patch smooth normals for a single mesh's triangles.
|
|
1807
2475
|
* @private
|
|
1808
2476
|
*/
|
|
1809
|
-
_patchMeshSmoothNormals(
|
|
2477
|
+
_patchMeshSmoothNormals( i, meshNormals ) {
|
|
1810
2478
|
|
|
1811
|
-
|
|
2479
|
+
const table = this.instanceTable;
|
|
2480
|
+
this._patchNormalsRange(
|
|
2481
|
+
meshNormals, table.triOffsetOf( i ), table.triCountOf( i ),
|
|
2482
|
+
table.matrixWorldOf( i ), table.bvhToOriginalOf( i )
|
|
2483
|
+
);
|
|
1812
2484
|
|
|
1813
2485
|
}
|
|
1814
2486
|
|
|
@@ -1816,29 +2488,47 @@ export class SceneProcessor {
|
|
|
1816
2488
|
* Shared normal-patching loop for a range of triangles.
|
|
1817
2489
|
* @private
|
|
1818
2490
|
*/
|
|
1819
|
-
_patchNormalsRange( normals,
|
|
2491
|
+
_patchNormalsRange( normals, triOffset, count, matrixWorld, bvhToOrig ) {
|
|
1820
2492
|
|
|
1821
|
-
const
|
|
1822
|
-
const
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2493
|
+
const NA = TRIANGLE_DATA_LAYOUT.NORMAL_A_PACKED_OFFSET;
|
|
2494
|
+
const NB = TRIANGLE_DATA_LAYOUT.NORMAL_B_PACKED_OFFSET;
|
|
2495
|
+
const NC = TRIANGLE_DATA_LAYOUT.NORMAL_C_PACKED_OFFSET;
|
|
2496
|
+
|
|
2497
|
+
// Callers supply world-space normals. Traversal takes object normals out through the
|
|
2498
|
+
// transpose of world-to-object, so coming the other way is the transpose of
|
|
2499
|
+
// object-to-world — not its inverse, which is what a position would use.
|
|
2500
|
+
const m = matrixWorld;
|
|
2501
|
+
const plain = ! m || isIdentity( m );
|
|
2502
|
+
const slots = [ NA, NB, NC ];
|
|
2503
|
+
|
|
2504
|
+
// Walked in stored order so the writes stay sequential; the inverse map is the only
|
|
2505
|
+
// permutation kept, so the read is the scattered side.
|
|
2506
|
+
for ( let k = 0; k < count; k ++ ) {
|
|
2507
|
+
|
|
2508
|
+
const bvhIdx = triOffset + k;
|
|
2509
|
+
const u = this.triangles.chunkFor( bvhIdx );
|
|
2510
|
+
const dst = this.triangles.baseOf( bvhIdx );
|
|
2511
|
+
const src = ( bvhToOrig ? bvhToOrig[ k ] : k ) * 9;
|
|
2512
|
+
|
|
2513
|
+
for ( let v = 0; v < 3; v ++ ) {
|
|
2514
|
+
|
|
2515
|
+
const s = src + v * 3;
|
|
2516
|
+
const nx = normals[ s ], ny = normals[ s + 1 ], nz = normals[ s + 2 ];
|
|
2517
|
+
let ox = nx, oy = ny, oz = nz;
|
|
2518
|
+
|
|
2519
|
+
if ( ! plain ) {
|
|
2520
|
+
|
|
2521
|
+
ox = m[ 0 ] * nx + m[ 1 ] * ny + m[ 2 ] * nz;
|
|
2522
|
+
oy = m[ 4 ] * nx + m[ 5 ] * ny + m[ 6 ] * nz;
|
|
2523
|
+
oz = m[ 8 ] * nx + m[ 9 ] * ny + m[ 10 ] * nz;
|
|
2524
|
+
const len = Math.hypot( ox, oy, oz ) || 1;
|
|
2525
|
+
ox /= len; oy /= len; oz /= len;
|
|
2526
|
+
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
u[ dst + slots[ v ] ] = packNormalOct( ox, oy, oz );
|
|
2530
|
+
|
|
2531
|
+
}
|
|
1842
2532
|
|
|
1843
2533
|
}
|
|
1844
2534
|
|
|
@@ -1861,66 +2551,50 @@ export class SceneProcessor {
|
|
|
1861
2551
|
|
|
1862
2552
|
}
|
|
1863
2553
|
|
|
1864
|
-
|
|
1865
|
-
if ( ! this._blasOffsetMap ) {
|
|
1866
|
-
|
|
1867
|
-
this._blasOffsetMap = new Map();
|
|
1868
|
-
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
this._blasOffsetMap.clear();
|
|
1872
|
-
for ( const entry of this.instanceTable.entries ) {
|
|
1873
|
-
|
|
1874
|
-
if ( ! entry ) continue;
|
|
1875
|
-
this._blasOffsetMap.set( entry.blasOffset, entry );
|
|
1876
|
-
|
|
1877
|
-
}
|
|
2554
|
+
const table = this.instanceTable;
|
|
1878
2555
|
|
|
1879
2556
|
// Bottom-up pass: reverse iteration over TLAS nodes
|
|
1880
2557
|
for ( let i = tlasNodeCount - 1; i >= 0; i -- ) {
|
|
1881
2558
|
|
|
1882
|
-
const
|
|
1883
|
-
const
|
|
2559
|
+
const idxChunk = this.bvhIndexChunks.chunkFor( i );
|
|
2560
|
+
const fChunk = this.bvh.chunkFor( i );
|
|
2561
|
+
const o = this.bvh.baseOf( i );
|
|
2562
|
+
const marker = idxChunk[ o + 3 ];
|
|
1884
2563
|
|
|
1885
|
-
if ( marker ===
|
|
2564
|
+
if ( marker === BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) {
|
|
1886
2565
|
|
|
1887
|
-
//
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2566
|
+
// Slot [1] is this leaf's own placement. Keying off slot [0] instead collapsed
|
|
2567
|
+
// every placement of a shared geometry onto one box, so all but one copy sat
|
|
2568
|
+
// outside its own bounds and rays walked straight past it.
|
|
2569
|
+
const entryIndex = idxChunk[ o + 1 ] & TLAS_PLACEMENT_MASK;
|
|
2570
|
+
if ( entryIndex < table.count ) {
|
|
1891
2571
|
|
|
1892
|
-
|
|
1893
|
-
this._tlasBounds[ b ] = entry.worldAABB.minX;
|
|
1894
|
-
this._tlasBounds[ b + 1 ] = entry.worldAABB.minY;
|
|
1895
|
-
this._tlasBounds[ b + 2 ] = entry.worldAABB.minZ;
|
|
1896
|
-
this._tlasBounds[ b + 3 ] = entry.worldAABB.maxX;
|
|
1897
|
-
this._tlasBounds[ b + 4 ] = entry.worldAABB.maxY;
|
|
1898
|
-
this._tlasBounds[ b + 5 ] = entry.worldAABB.maxZ;
|
|
2572
|
+
table.writeWorldAABB( entryIndex, this._tlasBounds, i * 6 );
|
|
1899
2573
|
|
|
1900
2574
|
}
|
|
1901
2575
|
|
|
1902
|
-
} else if ( marker
|
|
2576
|
+
} else if ( marker < BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
|
|
1903
2577
|
|
|
1904
2578
|
// Inner node: union of children bounds, update bvhData in-place
|
|
1905
|
-
const leftIdx =
|
|
1906
|
-
const rightIdx =
|
|
2579
|
+
const leftIdx = marker;
|
|
2580
|
+
const rightIdx = idxChunk[ o + 7 ];
|
|
1907
2581
|
const lb = leftIdx * 6;
|
|
1908
2582
|
const rb = rightIdx * 6;
|
|
1909
2583
|
const bounds = this._tlasBounds;
|
|
1910
2584
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2585
|
+
fChunk[ o ] = bounds[ lb ];
|
|
2586
|
+
fChunk[ o + 1 ] = bounds[ lb + 1 ];
|
|
2587
|
+
fChunk[ o + 2 ] = bounds[ lb + 2 ];
|
|
2588
|
+
fChunk[ o + 4 ] = bounds[ lb + 3 ];
|
|
2589
|
+
fChunk[ o + 5 ] = bounds[ lb + 4 ];
|
|
2590
|
+
fChunk[ o + 6 ] = bounds[ lb + 5 ];
|
|
1917
2591
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2592
|
+
fChunk[ o + 8 ] = bounds[ rb ];
|
|
2593
|
+
fChunk[ o + 9 ] = bounds[ rb + 1 ];
|
|
2594
|
+
fChunk[ o + 10 ] = bounds[ rb + 2 ];
|
|
2595
|
+
fChunk[ o + 12 ] = bounds[ rb + 3 ];
|
|
2596
|
+
fChunk[ o + 13 ] = bounds[ rb + 4 ];
|
|
2597
|
+
fChunk[ o + 14 ] = bounds[ rb + 5 ];
|
|
1924
2598
|
|
|
1925
2599
|
const b = i * 6;
|
|
1926
2600
|
bounds[ b ] = Math.min( bounds[ lb ], bounds[ rb ] );
|
|
@@ -1946,18 +2620,14 @@ export class SceneProcessor {
|
|
|
1946
2620
|
*/
|
|
1947
2621
|
scheduleBackgroundRebuild( meshIndices, onSwap ) {
|
|
1948
2622
|
|
|
1949
|
-
if ( ! this.instanceTable || ! this.
|
|
2623
|
+
if ( ! this.instanceTable || ! this.triangles ) return;
|
|
1950
2624
|
|
|
1951
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
1952
2625
|
this._rebuildGeneration ++;
|
|
1953
2626
|
const generation = this._rebuildGeneration;
|
|
1954
2627
|
|
|
1955
2628
|
const dispatchRebuild = ( meshIdx, entry, worker ) => {
|
|
1956
2629
|
|
|
1957
|
-
const meshTriData = this.
|
|
1958
|
-
entry.triOffset * FPT,
|
|
1959
|
-
( entry.triOffset + entry.triCount ) * FPT
|
|
1960
|
-
);
|
|
2630
|
+
const meshTriData = this.triangles.copyOf( entry.triOffset, entry.triCount );
|
|
1961
2631
|
|
|
1962
2632
|
this._pendingRebuilds.set( meshIdx, worker );
|
|
1963
2633
|
|
|
@@ -1998,6 +2668,13 @@ export class SceneProcessor {
|
|
|
1998
2668
|
triangleByteLength: meshTriData.byteLength,
|
|
1999
2669
|
triangleCount: entry.triCount,
|
|
2000
2670
|
depth: this.config.bvhDepth,
|
|
2671
|
+
// Without these the worker builds at its own defaults (leaf size 8 against the
|
|
2672
|
+
// scene's 4), producing a tree of a different size that the swap below rejects —
|
|
2673
|
+
// so every rebuild was thrown away.
|
|
2674
|
+
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
2675
|
+
numBins: this.bvhBuilder.numBins,
|
|
2676
|
+
maxBins: this.bvhBuilder.maxBins,
|
|
2677
|
+
minBins: this.bvhBuilder.minBins,
|
|
2001
2678
|
reportProgress: false,
|
|
2002
2679
|
sharedReorderBuffer: null,
|
|
2003
2680
|
treeletOptimization: {
|
|
@@ -2016,16 +2693,20 @@ export class SceneProcessor {
|
|
|
2016
2693
|
|
|
2017
2694
|
};
|
|
2018
2695
|
|
|
2019
|
-
for ( const
|
|
2696
|
+
for ( const meshIndex of meshIndices ) {
|
|
2020
2697
|
|
|
2021
|
-
const
|
|
2022
|
-
if (
|
|
2698
|
+
const placement = this._placementOf( meshIndex );
|
|
2699
|
+
if ( placement < 0 ) continue;
|
|
2700
|
+
const entry = this.instanceTable.entryAt( placement );
|
|
2701
|
+
// A placement that borrows another's BLAS must not rebuild it: the work is the
|
|
2702
|
+
// owner's, and two placements would dispatch the same rebuild twice.
|
|
2703
|
+
if ( ! entry || ! this.instanceTable.isOwner( placement ) ) continue;
|
|
2023
2704
|
|
|
2024
2705
|
// Cancel any in-flight rebuild for this mesh
|
|
2025
|
-
const existing = this._pendingRebuilds.get(
|
|
2706
|
+
const existing = this._pendingRebuilds.get( placement );
|
|
2026
2707
|
if ( existing ) existing.terminate();
|
|
2027
2708
|
|
|
2028
|
-
dispatchRebuild(
|
|
2709
|
+
dispatchRebuild( placement, entry, new BVHWorker() );
|
|
2029
2710
|
|
|
2030
2711
|
}
|
|
2031
2712
|
|
|
@@ -2041,59 +2722,62 @@ export class SceneProcessor {
|
|
|
2041
2722
|
const newBvhData = workerData.bvhData;
|
|
2042
2723
|
const newNodeCount = newBvhData.length / FPN;
|
|
2043
2724
|
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
|
|
2725
|
+
// The rebuilt tree only has to fit the range the build reserved. Demanding an exact match
|
|
2726
|
+
// was too strict: the triangles have moved, so the splits land differently and the count
|
|
2727
|
+
// legitimately drifts. Anything past the reserved range would overwrite the next BLAS.
|
|
2728
|
+
if ( newNodeCount > entry.blasNodeCount ) {
|
|
2047
2729
|
|
|
2048
|
-
log.warn( `background rebuild
|
|
2730
|
+
log.warn( `background rebuild does not fit for mesh ${meshIdx} (${newNodeCount} nodes vs ${entry.blasNodeCount} reserved), skipping swap` );
|
|
2049
2731
|
return;
|
|
2050
2732
|
|
|
2051
2733
|
}
|
|
2052
2734
|
|
|
2053
2735
|
// Write rebuilt BLAS nodes into the combined buffer at the entry's offset
|
|
2054
|
-
|
|
2055
|
-
this.
|
|
2056
|
-
this._offsetBLASInPlace( destOffset, newNodeCount, entry.blasOffset, entry.triOffset );
|
|
2736
|
+
this.bvh.setRecords( entry.blasOffset, newBvhData );
|
|
2737
|
+
this._offsetBLASInPlace( entry.blasOffset, newNodeCount, entry.blasOffset, entry.triOffset );
|
|
2057
2738
|
|
|
2058
2739
|
// Write reordered triangles back into global array
|
|
2059
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
2060
2740
|
const reorderedTris = workerData.triangles;
|
|
2061
2741
|
if ( reorderedTris ) {
|
|
2062
2742
|
|
|
2063
|
-
this.
|
|
2743
|
+
this.triangles.setRecords( entry.triOffset, reorderedTris );
|
|
2064
2744
|
|
|
2065
2745
|
}
|
|
2066
2746
|
|
|
2067
|
-
//
|
|
2747
|
+
// The live tree is whatever the rebuild produced; any reserved nodes past it are dead and
|
|
2748
|
+
// nothing reaches them, but a later refit would walk them as garbage.
|
|
2749
|
+
this.instanceTable.setBlasNodeCount( meshIdx, newNodeCount );
|
|
2750
|
+
|
|
2751
|
+
// The rebuild reordered the triangles, so the map from stored order back to the caller's
|
|
2752
|
+
// order has to follow. `entryAt()` hands back a snapshot, so writing it there was lost and
|
|
2753
|
+
// the next refit scattered every position through the previous permutation.
|
|
2754
|
+
//
|
|
2755
|
+
// The worker was handed the triangles already in stored order, so its permutation is
|
|
2756
|
+
// relative to that, not to the caller's order. Overwriting rather than composing would
|
|
2757
|
+
// discard how the original build shuffled them.
|
|
2068
2758
|
const newOrigToBvh = workerData.originalToBvh;
|
|
2069
2759
|
if ( newOrigToBvh ) {
|
|
2070
2760
|
|
|
2071
|
-
|
|
2072
|
-
for ( let i = 0; i < entry.triCount; i ++ ) {
|
|
2073
|
-
|
|
2074
|
-
this.originalToBvhMap[ entry.triOffset + i ] = entry.triOffset + newOrigToBvh[ i ];
|
|
2075
|
-
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
// Update per-mesh bvhToOriginal
|
|
2761
|
+
const prev = this.instanceTable.bvhToOriginalOf( meshIdx );
|
|
2079
2762
|
const bvhToOrig = new Uint32Array( entry.triCount );
|
|
2080
2763
|
for ( let i = 0; i < entry.triCount; i ++ ) {
|
|
2081
2764
|
|
|
2082
|
-
bvhToOrig[ newOrigToBvh[ i ] ] = i;
|
|
2765
|
+
bvhToOrig[ newOrigToBvh[ i ] ] = prev ? prev[ i ] : i;
|
|
2083
2766
|
|
|
2084
2767
|
}
|
|
2085
2768
|
|
|
2086
|
-
|
|
2769
|
+
this.instanceTable.setBvhToOriginal( meshIdx, bvhToOrig );
|
|
2087
2770
|
|
|
2088
2771
|
}
|
|
2089
2772
|
|
|
2090
2773
|
// Recompute AABB and refit TLAS
|
|
2091
|
-
this.instanceTable.recomputeAABB( meshIdx, this.
|
|
2774
|
+
this.instanceTable.recomputeAABB( meshIdx, this.bvh, this.triangles );
|
|
2092
2775
|
this._refitTLAS();
|
|
2093
2776
|
|
|
2094
2777
|
this._log( `Background BLAS rebuild complete for mesh ${meshIdx}` );
|
|
2095
2778
|
|
|
2096
|
-
|
|
2779
|
+
// The caller asked in mesh indices and gets one back, not the placement used here.
|
|
2780
|
+
onSwap?.( this.instanceTable.sourceMesh[ meshIdx ] ?? meshIdx );
|
|
2097
2781
|
|
|
2098
2782
|
}
|
|
2099
2783
|
|
|
@@ -2130,6 +2814,18 @@ export class SceneProcessor {
|
|
|
2130
2814
|
|
|
2131
2815
|
}
|
|
2132
2816
|
|
|
2817
|
+
/** @private */
|
|
2818
|
+
_disposeTLASWorker() {
|
|
2819
|
+
|
|
2820
|
+
if ( this._tlasWorker ) {
|
|
2821
|
+
|
|
2822
|
+
this._tlasWorker.terminate();
|
|
2823
|
+
this._tlasWorker = null;
|
|
2824
|
+
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2133
2829
|
/**
|
|
2134
2830
|
* Completely dispose of all resources
|
|
2135
2831
|
* Call this when the instance is no longer needed
|
|
@@ -2138,8 +2834,9 @@ export class SceneProcessor {
|
|
|
2138
2834
|
|
|
2139
2835
|
this._log( 'Disposing resources' );
|
|
2140
2836
|
|
|
2141
|
-
// Dispose
|
|
2837
|
+
// Dispose workers
|
|
2142
2838
|
this._disposeRefitWorker();
|
|
2839
|
+
this._disposeTLASWorker();
|
|
2143
2840
|
|
|
2144
2841
|
// Dispose textures
|
|
2145
2842
|
this._disposeTextures();
|