rayzee 7.31.1 → 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 +6366 -4361
- package/dist/rayzee.umd.js +122 -73
- package/package.json +1 -1
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +120 -19
- package/src/Processor/ArchiveReader.js +538 -0
- package/src/Processor/AssetLoader.js +265 -23
- 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 +32 -8
- package/src/Processor/PBRT/PBRTCurves.js +249 -0
- package/src/Processor/PBRT/PBRTMath.js +19 -0
- package/src/Processor/PBRT/PBRTParser.js +187 -41
- package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
- package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
- package/src/Processor/PBRT/index.js +257 -50
- 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 +1046 -365
- 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/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(
|
|
1101
|
+
const bvhToOrig = new Uint32Array( triCount );
|
|
781
1102
|
|
|
782
|
-
if (
|
|
1103
|
+
if ( originalToBvh ) {
|
|
783
1104
|
|
|
784
|
-
for ( let i = 0; i <
|
|
785
|
-
|
|
786
|
-
const bvhLocal = entry.originalToBvhMap[ i ];
|
|
787
|
-
this.originalToBvhMap[ entry.triOffset + i ] = entry.triOffset + bvhLocal;
|
|
788
|
-
bvhToOrig[ bvhLocal ] = i;
|
|
789
|
-
|
|
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
|
|
|
@@ -1049,9 +1360,10 @@ export class SceneProcessor {
|
|
|
1049
1360
|
_buildEmissiveData() {
|
|
1050
1361
|
|
|
1051
1362
|
this.emissiveTriangleCount = this.emissiveTriangleBuilder.extractEmissiveTriangles(
|
|
1052
|
-
this.
|
|
1363
|
+
this.triangles,
|
|
1053
1364
|
this.materials,
|
|
1054
|
-
this.triangleCount
|
|
1365
|
+
this.triangleCount,
|
|
1366
|
+
this.instanceTable ?? null
|
|
1055
1367
|
);
|
|
1056
1368
|
|
|
1057
1369
|
this.emissiveTriangleData = this.emissiveTriangleBuilder.createEmissiveRawData();
|
|
@@ -1104,7 +1416,7 @@ export class SceneProcessor {
|
|
|
1104
1416
|
|
|
1105
1417
|
// Reset all containers
|
|
1106
1418
|
this.triangles = [];
|
|
1107
|
-
this.
|
|
1419
|
+
this._setTriangleData( null );
|
|
1108
1420
|
this.triangleCount = 0;
|
|
1109
1421
|
this.materials = [];
|
|
1110
1422
|
this.meshTriangleRanges = null;
|
|
@@ -1129,7 +1441,10 @@ export class SceneProcessor {
|
|
|
1129
1441
|
this.cameras = [];
|
|
1130
1442
|
this.spheres = [];
|
|
1131
1443
|
this.bvhRoot = null;
|
|
1444
|
+
this.bvh = null;
|
|
1132
1445
|
this.bvhData = null;
|
|
1446
|
+
this.bvhIndexChunks = null;
|
|
1447
|
+
this.bvhIndex = null;
|
|
1133
1448
|
this.instanceTable = null;
|
|
1134
1449
|
this.lightBVHNodeData = null;
|
|
1135
1450
|
this.lightBVHNodeCount = 0;
|
|
@@ -1140,6 +1455,9 @@ export class SceneProcessor {
|
|
|
1140
1455
|
textureCreationTime: 0,
|
|
1141
1456
|
geometryExtractionTime: 0,
|
|
1142
1457
|
bvhBuildTime: 0,
|
|
1458
|
+
blasBuildTime: 0,
|
|
1459
|
+
tlasBuildTime: 0,
|
|
1460
|
+
bvhAssembleTime: 0,
|
|
1143
1461
|
totalProcessingTime: 0
|
|
1144
1462
|
};
|
|
1145
1463
|
|
|
@@ -1286,9 +1604,9 @@ export class SceneProcessor {
|
|
|
1286
1604
|
cameraCount: this.cameras.length,
|
|
1287
1605
|
processingComplete: this.processingStage === 'complete',
|
|
1288
1606
|
hasBVH: !! this.bvhRoot,
|
|
1289
|
-
hasTextures: !! this.materialData && !! this.
|
|
1607
|
+
hasTextures: !! this.materialData && !! this.bvh,
|
|
1290
1608
|
useFloat32Array: this.config.useFloat32Array,
|
|
1291
|
-
triangleDataSize: this.
|
|
1609
|
+
triangleDataSize: this.triangles ? ( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 ) + 'MB' : '0MB'
|
|
1292
1610
|
};
|
|
1293
1611
|
|
|
1294
1612
|
// Add performance metrics
|
|
@@ -1349,45 +1667,115 @@ export class SceneProcessor {
|
|
|
1349
1667
|
// ===== BVH REFIT (Animation Support) =====
|
|
1350
1668
|
|
|
1351
1669
|
/**
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
* @param {Float32Array} newPositions - 9 floats per triangle (ax,ay,az, bx,by,bz, cx,cy,cz) in original mesh order
|
|
1356
|
-
* @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
|
|
1357
1673
|
*/
|
|
1358
|
-
|
|
1674
|
+
_adoptSharedChunks( field, LaneType ) {
|
|
1359
1675
|
|
|
1360
|
-
|
|
1676
|
+
const records = this[ field ];
|
|
1677
|
+
if ( records.shared ) return;
|
|
1361
1678
|
|
|
1362
|
-
|
|
1679
|
+
const chunks = records.chunks.map( c => {
|
|
1363
1680
|
|
|
1364
|
-
|
|
1681
|
+
const view = new LaneType( new SharedArrayBuffer( c.byteLength ) );
|
|
1682
|
+
view.set( c );
|
|
1683
|
+
return view;
|
|
1365
1684
|
|
|
1366
|
-
|
|
1367
|
-
// is sized from the FIRST call's argument — so a short array reads past its end and
|
|
1368
|
-
// writes NaN into triangleData and every AABB above it, with no error anywhere. A
|
|
1369
|
-
// caller that misses a mesh (the hidden ground disk is easy to miss) sees the scene
|
|
1370
|
-
// silently vanish instead of a thrown exception, so check the length up front.
|
|
1371
|
-
const expectedFloats = this.triangleCount * 9;
|
|
1685
|
+
} );
|
|
1372
1686
|
|
|
1373
|
-
|
|
1687
|
+
const adopted = ChunkedRecords.adopt(
|
|
1688
|
+
chunks, records.recordCount, records.lanesPerRecord, records.recordsPerChunk
|
|
1689
|
+
);
|
|
1374
1690
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
+
};
|
|
1380
1730
|
|
|
1381
1731
|
}
|
|
1382
1732
|
|
|
1383
|
-
|
|
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 ) {
|
|
1384
1737
|
|
|
1385
1738
|
throw new Error(
|
|
1386
|
-
`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.'
|
|
1387
1742
|
);
|
|
1388
1743
|
|
|
1389
1744
|
}
|
|
1390
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
|
+
|
|
1391
1779
|
// Lazy-create worker
|
|
1392
1780
|
if ( ! this._refitWorker ) {
|
|
1393
1781
|
|
|
@@ -1395,55 +1783,55 @@ export class SceneProcessor {
|
|
|
1395
1783
|
|
|
1396
1784
|
}
|
|
1397
1785
|
|
|
1398
|
-
// First call:
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
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.
|
|
1401
1789
|
if ( ! this._refitSharedBuffers ) {
|
|
1402
1790
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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 );
|
|
1406
1796
|
|
|
1407
|
-
const
|
|
1408
|
-
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 };
|
|
1409
1800
|
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
+
} );
|
|
1412
1811
|
|
|
1413
|
-
|
|
1414
|
-
this.bvhData = sharedBvhData;
|
|
1415
|
-
this.triangleData = sharedTriData;
|
|
1812
|
+
}
|
|
1416
1813
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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 ++ ) {
|
|
1422
1819
|
|
|
1423
|
-
|
|
1820
|
+
if ( ! table.isSet[ i ] || ! table.isOwner( i ) ) continue;
|
|
1424
1821
|
|
|
1425
|
-
|
|
1822
|
+
const p = positionsFor( i );
|
|
1823
|
+
if ( ! p ) continue;
|
|
1824
|
+
this._updateMeshTrianglePositions( i, p );
|
|
1426
1825
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
triBuf: sharedTriBuf,
|
|
1430
|
-
posBuf: sharedPosBuf,
|
|
1431
|
-
posView: new Float32Array( sharedPosBuf ),
|
|
1432
|
-
};
|
|
1826
|
+
// Smooth normals overwrite the face normals just computed, so they follow per mesh.
|
|
1827
|
+
if ( normalsFor ) {
|
|
1433
1828
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
type: 'init',
|
|
1437
|
-
sharedBvhBuf,
|
|
1438
|
-
sharedTriBuf,
|
|
1439
|
-
sharedPosBuf,
|
|
1440
|
-
bvhToOriginal,
|
|
1441
|
-
}, [ bvhToOriginal.buffer ] );
|
|
1829
|
+
const n = normalsFor( i );
|
|
1830
|
+
if ( n ) this._patchMeshSmoothNormals( i, n );
|
|
1442
1831
|
|
|
1443
|
-
|
|
1832
|
+
}
|
|
1444
1833
|
|
|
1445
|
-
|
|
1446
|
-
this._refitSharedBuffers.posView.set( newPositions );
|
|
1834
|
+
}
|
|
1447
1835
|
|
|
1448
1836
|
return new Promise( ( resolve, reject ) => {
|
|
1449
1837
|
|
|
@@ -1452,14 +1840,6 @@ export class SceneProcessor {
|
|
|
1452
1840
|
const msg = e.data;
|
|
1453
1841
|
if ( msg.type === 'refitComplete' ) {
|
|
1454
1842
|
|
|
1455
|
-
// bvhData/triangleData already updated via shared memory.
|
|
1456
|
-
// If smooth normals provided, overwrite the face normals the worker computed.
|
|
1457
|
-
if ( newNormals ) {
|
|
1458
|
-
|
|
1459
|
-
this._patchSmoothNormals( newNormals );
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
1843
|
resolve( { refitTimeMs: msg.refitTimeMs } );
|
|
1464
1844
|
|
|
1465
1845
|
} else if ( msg.type === 'error' ) {
|
|
@@ -1481,9 +1861,18 @@ export class SceneProcessor {
|
|
|
1481
1861
|
* Overwrite face normals in triangleData with smooth vertex normals (full scene).
|
|
1482
1862
|
* @private
|
|
1483
1863
|
*/
|
|
1484
|
-
_patchSmoothNormals(
|
|
1864
|
+
_patchSmoothNormals( normalsFor ) {
|
|
1865
|
+
|
|
1866
|
+
const table = this.instanceTable;
|
|
1867
|
+
if ( ! table ) return;
|
|
1485
1868
|
|
|
1486
|
-
|
|
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
|
+
}
|
|
1487
1876
|
|
|
1488
1877
|
}
|
|
1489
1878
|
|
|
@@ -1491,39 +1880,38 @@ export class SceneProcessor {
|
|
|
1491
1880
|
* Refit specific BLASes and rebuild TLAS after object transform or per-mesh animation.
|
|
1492
1881
|
* Runs on the main thread (fast for per-mesh updates).
|
|
1493
1882
|
*
|
|
1494
|
-
* @param {number[]} affectedMeshIndices - Indices into
|
|
1883
|
+
* @param {number[]} affectedMeshIndices - Indices into `this.meshes`, the same space
|
|
1884
|
+
* {@link updateMeshTransforms} takes
|
|
1495
1885
|
* @param {Float32Array} newPositions - 9 floats per triangle in original mesh order (full scene)
|
|
1496
1886
|
* @param {Float32Array} [newNormals] - Optional smooth normals (9 floats per tri)
|
|
1497
1887
|
* @returns {{ refitTimeMs: number }}
|
|
1498
1888
|
*/
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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 ) {
|
|
1502
1898
|
|
|
1503
|
-
|
|
1899
|
+
return this.instanceTable?.placementRunOf( meshIndex )?.start ?? - 1;
|
|
1504
1900
|
|
|
1505
|
-
|
|
1901
|
+
}
|
|
1506
1902
|
|
|
1507
|
-
|
|
1508
|
-
// affected meshes with no error. Same silent-corruption trap as refitBVH.
|
|
1509
|
-
const expectedFloats = this.triangleCount * 9;
|
|
1903
|
+
refitBLASes( affectedMeshIndices, newPositions, newNormals ) {
|
|
1510
1904
|
|
|
1511
|
-
if (
|
|
1905
|
+
if ( ! this.instanceTable || ! this.bvh || ! this.triangles ) {
|
|
1512
1906
|
|
|
1513
|
-
throw new Error(
|
|
1514
|
-
`SceneProcessor.refitBLASes: expected ${expectedFloats} position floats ` +
|
|
1515
|
-
`(${this.triangleCount} triangles × 9, full scene), got ${newPositions?.length ?? 'none'}.`
|
|
1516
|
-
);
|
|
1907
|
+
throw new Error( 'No TLAS/BLAS data available. Run buildBVH() first.' );
|
|
1517
1908
|
|
|
1518
1909
|
}
|
|
1519
1910
|
|
|
1520
|
-
|
|
1911
|
+
const positionsFor = this._meshSource( newPositions, 'position' );
|
|
1912
|
+
const normalsFor = this._meshSource( newNormals, 'normal' );
|
|
1521
1913
|
|
|
1522
|
-
|
|
1523
|
-
`SceneProcessor.refitBLASes: expected ${expectedFloats} normal floats, got ${newNormals.length}.`
|
|
1524
|
-
);
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1914
|
+
if ( ! positionsFor ) throw new Error( 'SceneProcessor.refitBLASes: positions are required.' );
|
|
1527
1915
|
|
|
1528
1916
|
const start = performance.now();
|
|
1529
1917
|
|
|
@@ -1535,31 +1923,49 @@ export class SceneProcessor {
|
|
|
1535
1923
|
}
|
|
1536
1924
|
|
|
1537
1925
|
// Step 1: Update triangle positions and refit each affected BLAS
|
|
1538
|
-
|
|
1926
|
+
const table = this.instanceTable;
|
|
1927
|
+
|
|
1928
|
+
for ( const meshIndex of affectedMeshIndices ) {
|
|
1929
|
+
|
|
1930
|
+
const placement = this._placementOf( meshIndex );
|
|
1931
|
+
if ( placement < 0 || ! table.isSet[ placement ] ) continue;
|
|
1932
|
+
|
|
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 ) ) {
|
|
1937
|
+
|
|
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;
|
|
1539
1945
|
|
|
1540
|
-
|
|
1541
|
-
if ( ! entry ) continue;
|
|
1946
|
+
}
|
|
1542
1947
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1948
|
+
const p = positionsFor( placement );
|
|
1949
|
+
if ( ! p ) continue;
|
|
1950
|
+
this._updateMeshTrianglePositions( placement, p );
|
|
1545
1951
|
|
|
1546
|
-
|
|
1547
|
-
if ( newNormals ) {
|
|
1952
|
+
if ( normalsFor ) {
|
|
1548
1953
|
|
|
1549
|
-
|
|
1954
|
+
const n = normalsFor( placement );
|
|
1955
|
+
if ( n ) this._patchMeshSmoothNormals( placement, n );
|
|
1550
1956
|
|
|
1551
1957
|
}
|
|
1552
1958
|
|
|
1553
1959
|
// Refit this BLAS's nodes
|
|
1554
1960
|
this._blasRefitter.refitRange(
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
1557
|
-
|
|
1558
|
-
|
|
1961
|
+
this.bvh,
|
|
1962
|
+
this.triangles,
|
|
1963
|
+
table.blasOffsetOf( placement ),
|
|
1964
|
+
table.blasNodeCountOf( placement )
|
|
1559
1965
|
);
|
|
1560
1966
|
|
|
1561
1967
|
// Recompute this mesh's AABB for TLAS rebuild
|
|
1562
|
-
this.instanceTable.recomputeAABB(
|
|
1968
|
+
this.instanceTable.recomputeAABB( placement, this.bvh, this.triangles );
|
|
1563
1969
|
|
|
1564
1970
|
}
|
|
1565
1971
|
|
|
@@ -1570,6 +1976,121 @@ export class SceneProcessor {
|
|
|
1570
1976
|
|
|
1571
1977
|
}
|
|
1572
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
|
+
|
|
1573
2094
|
/**
|
|
1574
2095
|
* Computes the dirty buffer ranges for a set of affected mesh BLASes.
|
|
1575
2096
|
* Used for partial GPU upload after per-mesh refit instead of full buffer copy.
|
|
@@ -1584,13 +2105,14 @@ export class SceneProcessor {
|
|
|
1584
2105
|
const triRanges = [];
|
|
1585
2106
|
const bvhRanges = [];
|
|
1586
2107
|
|
|
1587
|
-
for ( const
|
|
2108
|
+
for ( const meshIndex of affectedMeshIndices ) {
|
|
1588
2109
|
|
|
1589
|
-
const
|
|
1590
|
-
|
|
2110
|
+
const table = this.instanceTable;
|
|
2111
|
+
const placement = this._placementOf( meshIndex );
|
|
2112
|
+
if ( placement < 0 || ! table.isSet[ placement ] ) continue;
|
|
1591
2113
|
|
|
1592
|
-
triRanges.push( { offset:
|
|
1593
|
-
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 } );
|
|
1594
2116
|
|
|
1595
2117
|
}
|
|
1596
2118
|
|
|
@@ -1613,23 +2135,23 @@ export class SceneProcessor {
|
|
|
1613
2135
|
*/
|
|
1614
2136
|
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture ) {
|
|
1615
2137
|
|
|
1616
|
-
if ( ! this.
|
|
2138
|
+
if ( ! this.triangles ) {
|
|
1617
2139
|
|
|
1618
2140
|
log.error( 'failed to get triangle data' );
|
|
1619
2141
|
return false;
|
|
1620
2142
|
|
|
1621
2143
|
}
|
|
1622
2144
|
|
|
1623
|
-
pathTracer.setTriangleData( this.
|
|
2145
|
+
pathTracer.setTriangleData( this.triangles, this.triangleCount );
|
|
1624
2146
|
|
|
1625
|
-
if ( ! this.
|
|
2147
|
+
if ( ! this.bvh ) {
|
|
1626
2148
|
|
|
1627
2149
|
log.error( 'failed to get BVH data' );
|
|
1628
2150
|
return false;
|
|
1629
2151
|
|
|
1630
2152
|
}
|
|
1631
2153
|
|
|
1632
|
-
pathTracer.setBVHData( this.
|
|
2154
|
+
pathTracer.setBVHData( this.bvh );
|
|
1633
2155
|
pathTracer.setInstanceTable( this.instanceTable );
|
|
1634
2156
|
|
|
1635
2157
|
if ( this.materialData ) {
|
|
@@ -1702,7 +2224,7 @@ export class SceneProcessor {
|
|
|
1702
2224
|
|
|
1703
2225
|
const changed = this.emissiveTriangleBuilder.updateMaterialEmissive(
|
|
1704
2226
|
materialIndex, mat,
|
|
1705
|
-
this.
|
|
2227
|
+
this.triangles, this.materials, this.triangleCount,
|
|
1706
2228
|
);
|
|
1707
2229
|
|
|
1708
2230
|
if ( ! changed ) return null;
|
|
@@ -1755,64 +2277,194 @@ export class SceneProcessor {
|
|
|
1755
2277
|
|
|
1756
2278
|
}
|
|
1757
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
|
+
|
|
1758
2391
|
/**
|
|
1759
2392
|
* Update triangle positions for a single mesh entry.
|
|
1760
2393
|
* Iterates in BVH order for sequential writes (cache-friendly), random reads from newPositions.
|
|
1761
2394
|
* @private
|
|
1762
2395
|
*/
|
|
1763
|
-
_updateMeshTrianglePositions(
|
|
2396
|
+
_updateMeshTrianglePositions( i, meshPositions ) {
|
|
1764
2397
|
|
|
1765
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
1766
2398
|
const PA = TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET;
|
|
1767
2399
|
const PB = TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET;
|
|
1768
2400
|
const PC = TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET;
|
|
1769
|
-
const NA = TRIANGLE_DATA_LAYOUT.
|
|
1770
|
-
const NB = TRIANGLE_DATA_LAYOUT.
|
|
1771
|
-
const NC = TRIANGLE_DATA_LAYOUT.
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
const
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
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;
|
|
1800
2456
|
|
|
1801
2457
|
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
1802
2458
|
const acx = cx - ax, acy = cy - ay, acz = cz - az;
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
this.triangleData[ dst + NB + 2 ] = nz;
|
|
1813
|
-
this.triangleData[ dst + NC ] = nx;
|
|
1814
|
-
this.triangleData[ dst + NC + 1 ] = ny;
|
|
1815
|
-
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;
|
|
1816
2468
|
|
|
1817
2469
|
}
|
|
1818
2470
|
|
|
@@ -1822,9 +2474,13 @@ export class SceneProcessor {
|
|
|
1822
2474
|
* Patch smooth normals for a single mesh's triangles.
|
|
1823
2475
|
* @private
|
|
1824
2476
|
*/
|
|
1825
|
-
_patchMeshSmoothNormals(
|
|
2477
|
+
_patchMeshSmoothNormals( i, meshNormals ) {
|
|
1826
2478
|
|
|
1827
|
-
|
|
2479
|
+
const table = this.instanceTable;
|
|
2480
|
+
this._patchNormalsRange(
|
|
2481
|
+
meshNormals, table.triOffsetOf( i ), table.triCountOf( i ),
|
|
2482
|
+
table.matrixWorldOf( i ), table.bvhToOriginalOf( i )
|
|
2483
|
+
);
|
|
1828
2484
|
|
|
1829
2485
|
}
|
|
1830
2486
|
|
|
@@ -1832,29 +2488,47 @@ export class SceneProcessor {
|
|
|
1832
2488
|
* Shared normal-patching loop for a range of triangles.
|
|
1833
2489
|
* @private
|
|
1834
2490
|
*/
|
|
1835
|
-
_patchNormalsRange( normals,
|
|
2491
|
+
_patchNormalsRange( normals, triOffset, count, matrixWorld, bvhToOrig ) {
|
|
1836
2492
|
|
|
1837
|
-
const
|
|
1838
|
-
const
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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
|
+
}
|
|
1858
2532
|
|
|
1859
2533
|
}
|
|
1860
2534
|
|
|
@@ -1877,66 +2551,50 @@ export class SceneProcessor {
|
|
|
1877
2551
|
|
|
1878
2552
|
}
|
|
1879
2553
|
|
|
1880
|
-
|
|
1881
|
-
if ( ! this._blasOffsetMap ) {
|
|
1882
|
-
|
|
1883
|
-
this._blasOffsetMap = new Map();
|
|
1884
|
-
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
this._blasOffsetMap.clear();
|
|
1888
|
-
for ( const entry of this.instanceTable.entries ) {
|
|
1889
|
-
|
|
1890
|
-
if ( ! entry ) continue;
|
|
1891
|
-
this._blasOffsetMap.set( entry.blasOffset, entry );
|
|
1892
|
-
|
|
1893
|
-
}
|
|
2554
|
+
const table = this.instanceTable;
|
|
1894
2555
|
|
|
1895
2556
|
// Bottom-up pass: reverse iteration over TLAS nodes
|
|
1896
2557
|
for ( let i = tlasNodeCount - 1; i >= 0; i -- ) {
|
|
1897
2558
|
|
|
1898
|
-
const
|
|
1899
|
-
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 ];
|
|
1900
2563
|
|
|
1901
|
-
if ( marker ===
|
|
2564
|
+
if ( marker === BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) {
|
|
1902
2565
|
|
|
1903
|
-
//
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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 ) {
|
|
1907
2571
|
|
|
1908
|
-
|
|
1909
|
-
this._tlasBounds[ b ] = entry.worldAABB.minX;
|
|
1910
|
-
this._tlasBounds[ b + 1 ] = entry.worldAABB.minY;
|
|
1911
|
-
this._tlasBounds[ b + 2 ] = entry.worldAABB.minZ;
|
|
1912
|
-
this._tlasBounds[ b + 3 ] = entry.worldAABB.maxX;
|
|
1913
|
-
this._tlasBounds[ b + 4 ] = entry.worldAABB.maxY;
|
|
1914
|
-
this._tlasBounds[ b + 5 ] = entry.worldAABB.maxZ;
|
|
2572
|
+
table.writeWorldAABB( entryIndex, this._tlasBounds, i * 6 );
|
|
1915
2573
|
|
|
1916
2574
|
}
|
|
1917
2575
|
|
|
1918
|
-
} else if ( marker
|
|
2576
|
+
} else if ( marker < BVH_LEAF_MARKERS.TRIANGLE_LEAF ) {
|
|
1919
2577
|
|
|
1920
2578
|
// Inner node: union of children bounds, update bvhData in-place
|
|
1921
|
-
const leftIdx =
|
|
1922
|
-
const rightIdx =
|
|
2579
|
+
const leftIdx = marker;
|
|
2580
|
+
const rightIdx = idxChunk[ o + 7 ];
|
|
1923
2581
|
const lb = leftIdx * 6;
|
|
1924
2582
|
const rb = rightIdx * 6;
|
|
1925
2583
|
const bounds = this._tlasBounds;
|
|
1926
2584
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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 ];
|
|
1933
2591
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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 ];
|
|
1940
2598
|
|
|
1941
2599
|
const b = i * 6;
|
|
1942
2600
|
bounds[ b ] = Math.min( bounds[ lb ], bounds[ rb ] );
|
|
@@ -1962,18 +2620,14 @@ export class SceneProcessor {
|
|
|
1962
2620
|
*/
|
|
1963
2621
|
scheduleBackgroundRebuild( meshIndices, onSwap ) {
|
|
1964
2622
|
|
|
1965
|
-
if ( ! this.instanceTable || ! this.
|
|
2623
|
+
if ( ! this.instanceTable || ! this.triangles ) return;
|
|
1966
2624
|
|
|
1967
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
1968
2625
|
this._rebuildGeneration ++;
|
|
1969
2626
|
const generation = this._rebuildGeneration;
|
|
1970
2627
|
|
|
1971
2628
|
const dispatchRebuild = ( meshIdx, entry, worker ) => {
|
|
1972
2629
|
|
|
1973
|
-
const meshTriData = this.
|
|
1974
|
-
entry.triOffset * FPT,
|
|
1975
|
-
( entry.triOffset + entry.triCount ) * FPT
|
|
1976
|
-
);
|
|
2630
|
+
const meshTriData = this.triangles.copyOf( entry.triOffset, entry.triCount );
|
|
1977
2631
|
|
|
1978
2632
|
this._pendingRebuilds.set( meshIdx, worker );
|
|
1979
2633
|
|
|
@@ -2014,6 +2668,13 @@ export class SceneProcessor {
|
|
|
2014
2668
|
triangleByteLength: meshTriData.byteLength,
|
|
2015
2669
|
triangleCount: entry.triCount,
|
|
2016
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,
|
|
2017
2678
|
reportProgress: false,
|
|
2018
2679
|
sharedReorderBuffer: null,
|
|
2019
2680
|
treeletOptimization: {
|
|
@@ -2032,16 +2693,20 @@ export class SceneProcessor {
|
|
|
2032
2693
|
|
|
2033
2694
|
};
|
|
2034
2695
|
|
|
2035
|
-
for ( const
|
|
2696
|
+
for ( const meshIndex of meshIndices ) {
|
|
2036
2697
|
|
|
2037
|
-
const
|
|
2038
|
-
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;
|
|
2039
2704
|
|
|
2040
2705
|
// Cancel any in-flight rebuild for this mesh
|
|
2041
|
-
const existing = this._pendingRebuilds.get(
|
|
2706
|
+
const existing = this._pendingRebuilds.get( placement );
|
|
2042
2707
|
if ( existing ) existing.terminate();
|
|
2043
2708
|
|
|
2044
|
-
dispatchRebuild(
|
|
2709
|
+
dispatchRebuild( placement, entry, new BVHWorker() );
|
|
2045
2710
|
|
|
2046
2711
|
}
|
|
2047
2712
|
|
|
@@ -2057,59 +2722,62 @@ export class SceneProcessor {
|
|
|
2057
2722
|
const newBvhData = workerData.bvhData;
|
|
2058
2723
|
const newNodeCount = newBvhData.length / FPN;
|
|
2059
2724
|
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
|
|
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 ) {
|
|
2063
2729
|
|
|
2064
|
-
log.warn( `background rebuild
|
|
2730
|
+
log.warn( `background rebuild does not fit for mesh ${meshIdx} (${newNodeCount} nodes vs ${entry.blasNodeCount} reserved), skipping swap` );
|
|
2065
2731
|
return;
|
|
2066
2732
|
|
|
2067
2733
|
}
|
|
2068
2734
|
|
|
2069
2735
|
// Write rebuilt BLAS nodes into the combined buffer at the entry's offset
|
|
2070
|
-
|
|
2071
|
-
this.
|
|
2072
|
-
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 );
|
|
2073
2738
|
|
|
2074
2739
|
// Write reordered triangles back into global array
|
|
2075
|
-
const FPT = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
2076
2740
|
const reorderedTris = workerData.triangles;
|
|
2077
2741
|
if ( reorderedTris ) {
|
|
2078
2742
|
|
|
2079
|
-
this.
|
|
2743
|
+
this.triangles.setRecords( entry.triOffset, reorderedTris );
|
|
2080
2744
|
|
|
2081
2745
|
}
|
|
2082
2746
|
|
|
2083
|
-
//
|
|
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.
|
|
2084
2758
|
const newOrigToBvh = workerData.originalToBvh;
|
|
2085
2759
|
if ( newOrigToBvh ) {
|
|
2086
2760
|
|
|
2087
|
-
|
|
2088
|
-
for ( let i = 0; i < entry.triCount; i ++ ) {
|
|
2089
|
-
|
|
2090
|
-
this.originalToBvhMap[ entry.triOffset + i ] = entry.triOffset + newOrigToBvh[ i ];
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// Update per-mesh bvhToOriginal
|
|
2761
|
+
const prev = this.instanceTable.bvhToOriginalOf( meshIdx );
|
|
2095
2762
|
const bvhToOrig = new Uint32Array( entry.triCount );
|
|
2096
2763
|
for ( let i = 0; i < entry.triCount; i ++ ) {
|
|
2097
2764
|
|
|
2098
|
-
bvhToOrig[ newOrigToBvh[ i ] ] = i;
|
|
2765
|
+
bvhToOrig[ newOrigToBvh[ i ] ] = prev ? prev[ i ] : i;
|
|
2099
2766
|
|
|
2100
2767
|
}
|
|
2101
2768
|
|
|
2102
|
-
|
|
2769
|
+
this.instanceTable.setBvhToOriginal( meshIdx, bvhToOrig );
|
|
2103
2770
|
|
|
2104
2771
|
}
|
|
2105
2772
|
|
|
2106
2773
|
// Recompute AABB and refit TLAS
|
|
2107
|
-
this.instanceTable.recomputeAABB( meshIdx, this.
|
|
2774
|
+
this.instanceTable.recomputeAABB( meshIdx, this.bvh, this.triangles );
|
|
2108
2775
|
this._refitTLAS();
|
|
2109
2776
|
|
|
2110
2777
|
this._log( `Background BLAS rebuild complete for mesh ${meshIdx}` );
|
|
2111
2778
|
|
|
2112
|
-
|
|
2779
|
+
// The caller asked in mesh indices and gets one back, not the placement used here.
|
|
2780
|
+
onSwap?.( this.instanceTable.sourceMesh[ meshIdx ] ?? meshIdx );
|
|
2113
2781
|
|
|
2114
2782
|
}
|
|
2115
2783
|
|
|
@@ -2146,6 +2814,18 @@ export class SceneProcessor {
|
|
|
2146
2814
|
|
|
2147
2815
|
}
|
|
2148
2816
|
|
|
2817
|
+
/** @private */
|
|
2818
|
+
_disposeTLASWorker() {
|
|
2819
|
+
|
|
2820
|
+
if ( this._tlasWorker ) {
|
|
2821
|
+
|
|
2822
|
+
this._tlasWorker.terminate();
|
|
2823
|
+
this._tlasWorker = null;
|
|
2824
|
+
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2149
2829
|
/**
|
|
2150
2830
|
* Completely dispose of all resources
|
|
2151
2831
|
* Call this when the instance is no longer needed
|
|
@@ -2154,8 +2834,9 @@ export class SceneProcessor {
|
|
|
2154
2834
|
|
|
2155
2835
|
this._log( 'Disposing resources' );
|
|
2156
2836
|
|
|
2157
|
-
// Dispose
|
|
2837
|
+
// Dispose workers
|
|
2158
2838
|
this._disposeRefitWorker();
|
|
2839
|
+
this._disposeTLASWorker();
|
|
2159
2840
|
|
|
2160
2841
|
// Dispose textures
|
|
2161
2842
|
this._disposeTextures();
|