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,161 +3,467 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Produces a small SAH BVH where leaves are BLAS-pointer nodes (marker -2)
|
|
5
5
|
* that reference per-mesh BLAS root indices in the combined BVH buffer.
|
|
6
|
+
*
|
|
7
|
+
* Single pass: entry indices are partitioned in place inside one Int32Array and nodes are
|
|
8
|
+
* written straight into the final flat buffer. There is no intermediate node tree — the
|
|
9
|
+
* previous build allocated one JS object per node plus two fresh index arrays per split and
|
|
10
|
+
* then walked the result three times, which at 5M placements cost 20.8 s and 7.7 GB of
|
|
11
|
+
* transient heap and was the scene's hard ceiling, well before triangles or VRAM.
|
|
12
|
+
*
|
|
13
|
+
* Every leaf holds exactly one entry, so a subtree over k entries occupies exactly 2k-1
|
|
14
|
+
* consecutive pre-order nodes. That makes both the total node count and each child's index
|
|
15
|
+
* known before its subtree is built, which is what lets the write be single-pass.
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView, TLAS_LEAF_IDENTITY, TLAS_PLACEMENT_MASK
|
|
20
|
+
} from '../EngineDefaults.js';
|
|
21
|
+
import { invertAffineInto, isIdentityAt } from './InstanceTable.js';
|
|
9
22
|
|
|
10
23
|
const FLOATS_PER_NODE = 16;
|
|
24
|
+
// 16 mis-split near the root: +42 % SAH cost, 3.5 % of frame time. 24 and up match an exact sweep.
|
|
25
|
+
const SAH_BINS = 32;
|
|
26
|
+
// Most nodes in a deep tree hold a handful of entries, and clearing 3x32 bins then sweeping
|
|
27
|
+
// them costs far more than binning four AABBs. Bin count follows the range instead.
|
|
28
|
+
const binsFor = count => ( count < SAH_BINS ? count : SAH_BINS );
|
|
29
|
+
// Below this, clearing bins and sweeping them costs more than the split is worth; a median on
|
|
30
|
+
// the widest centroid axis is a rounding error apart in tree quality at this size.
|
|
31
|
+
const SMALL_NODE = 8;
|
|
32
|
+
// Recursing into the smaller half and looping on the larger keeps depth at O(log n), so a
|
|
33
|
+
// pathological split chain cannot grow the stack to n frames.
|
|
34
|
+
const INITIAL_STACK_FRAMES = 64;
|
|
35
|
+
// One affine inverse is live at a time while leaves are filled.
|
|
36
|
+
const _inverseScratch = new Float64Array( 16 );
|
|
11
37
|
|
|
12
|
-
class
|
|
38
|
+
export class TLASBuilder {
|
|
13
39
|
|
|
14
40
|
constructor() {
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
// Cached flatten buffer — reused across rebuilds to avoid per-refit allocation.
|
|
43
|
+
this._flatBuffer = null;
|
|
44
|
+
this._flatBufferCapacity = 0;
|
|
45
|
+
|
|
46
|
+
// Binning scratch, reused across every node.
|
|
47
|
+
this._binCounts = new Int32Array( SAH_BINS * 3 ); // all three axes, binned in one pass
|
|
48
|
+
this._binBounds = new Float64Array( SAH_BINS * 6 * 3 );
|
|
49
|
+
this._binSuffix = new Float64Array( SAH_BINS );
|
|
50
|
+
this._binSuffixCount = new Int32Array( SAH_BINS );
|
|
51
|
+
|
|
52
|
+
this._order = null; // entry indices, partitioned in place
|
|
53
|
+
this._stackI = null; // nodeIndex, start, end per frame
|
|
54
|
+
this._stackB = null; // the frame's AABB, 6 doubles
|
|
55
|
+
this._stackFrames = 0;
|
|
56
|
+
this._sortScratch = [];
|
|
57
|
+
this._suffix = null;
|
|
58
|
+
this._bounds = new Float64Array( 12 ); // left half at 0, right half at 6
|
|
59
|
+
this._extent = new Float64Array( 3 );
|
|
60
|
+
this._cMin = new Float64Array( 3 );
|
|
21
61
|
|
|
22
62
|
}
|
|
23
63
|
|
|
24
|
-
|
|
64
|
+
/** Nodes a TLAS over `entryCount` entries will occupy. Exact: every leaf holds one entry. */
|
|
65
|
+
static nodeCountFor( entryCount ) {
|
|
25
66
|
|
|
26
|
-
|
|
67
|
+
return entryCount > 0 ? entryCount * 2 - 1 : 0;
|
|
27
68
|
|
|
28
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Drop the cached flatten buffer. Worth it when the scene is large enough that holding a
|
|
72
|
+
* spare copy of the TLAS costs more than rebuilding it does. */
|
|
73
|
+
releaseFlattenBuffer() {
|
|
29
74
|
|
|
30
|
-
// Cached flatten buffer — reused across rebuilds to avoid per-refit allocation.
|
|
31
75
|
this._flatBuffer = null;
|
|
32
76
|
this._flatBufferCapacity = 0;
|
|
33
77
|
|
|
34
78
|
}
|
|
35
79
|
|
|
36
80
|
/**
|
|
37
|
-
* Build TLAS
|
|
81
|
+
* Build and flatten the TLAS in one pass.
|
|
82
|
+
*
|
|
83
|
+
* Inner nodes carry their children's AABBs and indices; leaves carry
|
|
84
|
+
* [blasRootNodeIndex, entryIndex, visibility, -2] followed by the instance's
|
|
85
|
+
* world-to-object matrix as three rows of four.
|
|
86
|
+
*
|
|
87
|
+
* Callers must assign BLAS offsets before calling — leaf payloads are written as the tree
|
|
88
|
+
* is built, so `entry.blasOffset` has to be final. Use {@link TLASBuilder.nodeCountFor} to
|
|
89
|
+
* get the node count the offsets depend on.
|
|
38
90
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
91
|
+
* Side effect: records each entry's flat leaf index on `entry.tlasLeafIndex` so visibility
|
|
92
|
+
* can later be patched in place (combinedBvhData[tlasLeafIndex*16 + 2]).
|
|
93
|
+
*
|
|
94
|
+
* @param {import('./InstanceTable.js').InstanceTable} table
|
|
95
|
+
* @returns {{ data: Float32Array, nodeCount: number }}
|
|
41
96
|
*/
|
|
42
|
-
build(
|
|
97
|
+
build( table ) {
|
|
98
|
+
|
|
99
|
+
const aabbs = new Float64Array( table.count * 6 );
|
|
100
|
+
table.writeWorldAABBs( aabbs );
|
|
101
|
+
const built = this.buildStructure( aabbs, table.count );
|
|
102
|
+
TLASBuilder.fillLeaves( built.data, built.nodeCount, table );
|
|
103
|
+
return built;
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The expensive half: SAH tree over the AABBs alone, with no reference to the entry objects.
|
|
109
|
+
* Leaves are written as [0, entryIndex, 0, -2]; {@link TLASBuilder.fillLeaves} completes them.
|
|
110
|
+
* Split out so it can run in a worker — at 6M placements it is 25 s of frozen main thread.
|
|
111
|
+
*
|
|
112
|
+
* @param {ArrayLike<number>} aabb - 6 floats per entry (min xyz, max xyz)
|
|
113
|
+
* @param {number} n - entry count
|
|
114
|
+
* @returns {{ data: Float32Array, nodeCount: number }}
|
|
115
|
+
*/
|
|
116
|
+
buildStructure( aabb, n ) {
|
|
117
|
+
|
|
118
|
+
const nodeCount = TLASBuilder.nodeCountFor( n );
|
|
119
|
+
if ( n === 0 ) return { data: new Float32Array( 0 ), nodeCount: 0 };
|
|
120
|
+
assertBVHIndexFits( nodeCount, 'TLAS node count' );
|
|
121
|
+
|
|
122
|
+
const required = nodeCount * FLOATS_PER_NODE;
|
|
123
|
+
if ( required > this._flatBufferCapacity ) {
|
|
124
|
+
|
|
125
|
+
this._flatBuffer = new Float32Array( required );
|
|
126
|
+
this._flatBufferCapacity = required;
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data = this._flatBuffer;
|
|
131
|
+
const idx = bvhIndexView( data ); // index fields are u32 bit patterns, never float values
|
|
132
|
+
|
|
133
|
+
if ( ! this._order || this._order.length < n ) this._order = new Int32Array( n );
|
|
134
|
+
const order = this._order;
|
|
135
|
+
for ( let i = 0; i < n; i ++ ) order[ i ] = i;
|
|
136
|
+
|
|
137
|
+
this._ensureStack( INITIAL_STACK_FRAMES );
|
|
138
|
+
|
|
139
|
+
let depth = 0;
|
|
140
|
+
let nodeIndex = 0, start = 0, end = n;
|
|
141
|
+
const bb = this._bounds;
|
|
142
|
+
this._rangeBounds( aabb, order, start, end, bb, 0 );
|
|
143
|
+
let minX = bb[ 0 ], minY = bb[ 1 ], minZ = bb[ 2 ];
|
|
144
|
+
let maxX = bb[ 3 ], maxY = bb[ 4 ], maxZ = bb[ 5 ];
|
|
145
|
+
|
|
146
|
+
for ( ;; ) {
|
|
147
|
+
|
|
148
|
+
const count = end - start;
|
|
149
|
+
|
|
150
|
+
if ( count === 1 ) {
|
|
151
|
+
|
|
152
|
+
const o = nodeIndex * FLOATS_PER_NODE;
|
|
153
|
+
idx[ o ] = 0;
|
|
154
|
+
idx[ o + 1 ] = order[ start ];
|
|
155
|
+
data[ o + 2 ] = 0;
|
|
156
|
+
idx[ o + 3 ] = BVH_LEAF_MARKERS.BLAS_POINTER_LEAF;
|
|
43
157
|
|
|
44
|
-
|
|
158
|
+
} else {
|
|
159
|
+
|
|
160
|
+
const mid = this._partition( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ );
|
|
161
|
+
const leftCount = mid - start;
|
|
162
|
+
const leftIndex = nodeIndex + 1;
|
|
163
|
+
const rightIndex = leftIndex + ( leftCount * 2 - 1 );
|
|
164
|
+
|
|
165
|
+
const o = nodeIndex * FLOATS_PER_NODE;
|
|
166
|
+
data[ o ] = bb[ 0 ]; data[ o + 1 ] = bb[ 1 ]; data[ o + 2 ] = bb[ 2 ]; idx[ o + 3 ] = leftIndex;
|
|
167
|
+
data[ o + 4 ] = bb[ 3 ]; data[ o + 5 ] = bb[ 4 ]; data[ o + 6 ] = bb[ 5 ]; idx[ o + 7 ] = rightIndex;
|
|
168
|
+
data[ o + 8 ] = bb[ 6 ]; data[ o + 9 ] = bb[ 7 ]; data[ o + 10 ] = bb[ 8 ]; data[ o + 11 ] = 0;
|
|
169
|
+
data[ o + 12 ] = bb[ 9 ]; data[ o + 13 ] = bb[ 10 ]; data[ o + 14 ] = bb[ 11 ]; data[ o + 15 ] = 0;
|
|
170
|
+
|
|
171
|
+
// Continue into the smaller half, stack the larger — bounds the stack at O(log n).
|
|
172
|
+
const keepLeft = leftCount <= end - mid;
|
|
173
|
+
const k = keepLeft ? 0 : 6, p = keepLeft ? 6 : 0;
|
|
174
|
+
depth = this._push(
|
|
175
|
+
depth, keepLeft ? rightIndex : leftIndex,
|
|
176
|
+
keepLeft ? mid : start, keepLeft ? end : mid, bb, p
|
|
177
|
+
);
|
|
178
|
+
nodeIndex = keepLeft ? leftIndex : rightIndex;
|
|
179
|
+
if ( keepLeft ) end = mid; else start = mid;
|
|
180
|
+
minX = bb[ k ]; minY = bb[ k + 1 ]; minZ = bb[ k + 2 ];
|
|
181
|
+
maxX = bb[ k + 3 ]; maxY = bb[ k + 4 ]; maxZ = bb[ k + 5 ];
|
|
182
|
+
|
|
183
|
+
continue;
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if ( depth === 0 ) break;
|
|
45
188
|
|
|
46
|
-
|
|
189
|
+
depth --;
|
|
190
|
+
const fi = depth * 3, fb = depth * 6;
|
|
191
|
+
nodeIndex = this._stackI[ fi ]; start = this._stackI[ fi + 1 ]; end = this._stackI[ fi + 2 ];
|
|
192
|
+
minX = this._stackB[ fb ]; minY = this._stackB[ fb + 1 ]; minZ = this._stackB[ fb + 2 ];
|
|
193
|
+
maxX = this._stackB[ fb + 3 ]; maxY = this._stackB[ fb + 4 ]; maxZ = this._stackB[ fb + 5 ];
|
|
47
194
|
|
|
48
195
|
}
|
|
49
196
|
|
|
50
|
-
|
|
51
|
-
const indices = [];
|
|
52
|
-
for ( let i = 0; i < entries.length; i ++ ) {
|
|
197
|
+
return { data: data.subarray( 0, required ), nodeCount };
|
|
53
198
|
|
|
54
|
-
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @private */
|
|
202
|
+
_push( depth, nodeIndex, start, end, b, off ) {
|
|
203
|
+
|
|
204
|
+
if ( depth >= this._stackFrames ) this._ensureStack( this._stackFrames * 2 );
|
|
205
|
+
const fi = depth * 3, fb = depth * 6;
|
|
206
|
+
this._stackI[ fi ] = nodeIndex; this._stackI[ fi + 1 ] = start; this._stackI[ fi + 2 ] = end;
|
|
207
|
+
for ( let i = 0; i < 6; i ++ ) this._stackB[ fb + i ] = b[ off + i ];
|
|
208
|
+
return depth + 1;
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** @private */
|
|
213
|
+
_ensureStack( frames ) {
|
|
214
|
+
|
|
215
|
+
if ( this._stackFrames >= frames ) return;
|
|
216
|
+
const grownI = new Int32Array( frames * 3 );
|
|
217
|
+
const grownB = new Float64Array( frames * 6 );
|
|
218
|
+
if ( this._stackI ) {
|
|
219
|
+
|
|
220
|
+
grownI.set( this._stackI );
|
|
221
|
+
grownB.set( this._stackB );
|
|
55
222
|
|
|
56
223
|
}
|
|
57
224
|
|
|
58
|
-
|
|
59
|
-
|
|
225
|
+
this._stackI = grownI; this._stackB = grownB; this._stackFrames = frames;
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Fill in each leaf's BLAS pointer, visibility and world-to-object matrix, and record where
|
|
231
|
+
* the leaf landed. One O(n) pass, so it stays on the main thread while the SAH tree does not.
|
|
232
|
+
*
|
|
233
|
+
* A node is 16 floats and the BLAS pointer needs 4, so the affine inverse fits in the
|
|
234
|
+
* remaining 12: the ray moves into object space with no second binding, which matters
|
|
235
|
+
* because Shade is already at the 10 storage buffers Metal allows. The inverse is derived
|
|
236
|
+
* here rather than stored: a column of it costs 64 bytes a placement.
|
|
237
|
+
*/
|
|
238
|
+
static fillLeaves( data, nodeCount, table ) {
|
|
60
239
|
|
|
61
|
-
|
|
240
|
+
const world = table.world, src = table.sourceMesh;
|
|
241
|
+
const blasOffset = table.tplBlasOffset, visible = table.visible, leafOf = table.tlasLeafIndex;
|
|
242
|
+
const idx = bvhIndexView( data );
|
|
243
|
+
|
|
244
|
+
for ( let node = 0; node < nodeCount; node ++ ) {
|
|
245
|
+
|
|
246
|
+
const o = node * FLOATS_PER_NODE;
|
|
247
|
+
if ( idx[ o + 3 ] !== BVH_LEAF_MARKERS.BLAS_POINTER_LEAF ) continue;
|
|
248
|
+
|
|
249
|
+
const i = idx[ o + 1 ] & TLAS_PLACEMENT_MASK;
|
|
250
|
+
idx[ o ] = blasOffset[ src[ i ] ];
|
|
251
|
+
data[ o + 2 ] = visible[ i ] ? 1.0 : 0.0;
|
|
252
|
+
|
|
253
|
+
TLASBuilder.writeLeafMatrix( data, idx, o, world, i );
|
|
254
|
+
|
|
255
|
+
leafOf[ i ] = node;
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Write one leaf's world-to-object rows, the twelve floats after its payload. Called again on
|
|
263
|
+
* its own whenever a placement moves, so a rigid transform costs a matrix rather than a
|
|
264
|
+
* rewrite of the geometry the placement may be sharing.
|
|
265
|
+
*
|
|
266
|
+
* Slot [1] is rewritten too: the placement index, with {@link TLAS_LEAF_IDENTITY} set when
|
|
267
|
+
* the matrix is identity so traversal can skip the ray transform for that leaf.
|
|
268
|
+
*
|
|
269
|
+
* @param {Float32Array} data - the node buffer, or one chunk of it
|
|
270
|
+
* @param {Uint32Array} idx - u32 view over the same memory as `data`
|
|
271
|
+
* @param {number} off - float offset of the leaf within `data`
|
|
272
|
+
* @param {Float32Array} world - the instance table's object-to-world column
|
|
273
|
+
* @param {number} placement
|
|
274
|
+
*/
|
|
275
|
+
static writeLeafMatrix( data, idx, off, world, placement ) {
|
|
276
|
+
|
|
277
|
+
const inv = _inverseScratch;
|
|
278
|
+
invertAffineInto( world, placement * 16, inv );
|
|
279
|
+
|
|
280
|
+
data[ off + 4 ] = inv[ 0 ]; data[ off + 5 ] = inv[ 4 ]; data[ off + 6 ] = inv[ 8 ]; data[ off + 7 ] = inv[ 12 ];
|
|
281
|
+
data[ off + 8 ] = inv[ 1 ]; data[ off + 9 ] = inv[ 5 ]; data[ off + 10 ] = inv[ 9 ]; data[ off + 11 ] = inv[ 13 ];
|
|
282
|
+
data[ off + 12 ] = inv[ 2 ]; data[ off + 13 ] = inv[ 6 ]; data[ off + 14 ] = inv[ 10 ]; data[ off + 15 ] = inv[ 14 ];
|
|
283
|
+
|
|
284
|
+
idx[ off + 1 ] = ( placement | ( isIdentityAt( world, placement * 16 ) ? TLAS_LEAF_IDENTITY : 0 ) ) >>> 0;
|
|
62
285
|
|
|
63
286
|
}
|
|
64
287
|
|
|
65
288
|
/**
|
|
66
|
-
*
|
|
289
|
+
* Choose a split for order[start..end) and partition it in place.
|
|
290
|
+
* Always returns a mid strictly inside the range, so every leaf ends up holding one entry.
|
|
67
291
|
* @private
|
|
68
292
|
*/
|
|
69
|
-
|
|
293
|
+
_partition( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ ) {
|
|
294
|
+
|
|
295
|
+
const count = end - start;
|
|
296
|
+
let mid = 0;
|
|
70
297
|
|
|
71
|
-
|
|
298
|
+
if ( count === 2 ) mid = start + 1;
|
|
299
|
+
else if ( count <= SMALL_NODE ) {
|
|
72
300
|
|
|
73
|
-
|
|
301
|
+
mid = start + ( count >> 1 );
|
|
302
|
+
this._insertionSortRange( aabb, order, start, end, this._widestCentroidAxis( aabb, order, start, end ) );
|
|
74
303
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
304
|
+
} else {
|
|
305
|
+
|
|
306
|
+
// Binned SAH at every size, not just large ranges. Almost every node in a deep tree
|
|
307
|
+
// holds a handful of entries, so gating the sort-based exact sweep on "small range"
|
|
308
|
+
// ran it ~9.8M times at 5M placements — three sorts each, and the bulk of the build.
|
|
309
|
+
mid = this._binnedSplit( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ );
|
|
310
|
+
// The binned partition already accumulated both halves' bounds.
|
|
311
|
+
if ( mid > start && mid < end ) return mid;
|
|
312
|
+
|
|
313
|
+
mid = this._sweepSplit( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ );
|
|
314
|
+
|
|
315
|
+
if ( ! ( mid > start && mid < end ) ) {
|
|
316
|
+
|
|
317
|
+
// Median on the widest axis — degenerate AABBs, overflowed surface area, or
|
|
318
|
+
// coincident centroids all land here.
|
|
319
|
+
const dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ;
|
|
320
|
+
const axis = dy > dx && dy > dz ? 1 : dz > dx ? 2 : 0;
|
|
321
|
+
this._sortRange( aabb, order, start, end, axis );
|
|
322
|
+
mid = start + ( count >> 1 );
|
|
323
|
+
|
|
324
|
+
}
|
|
82
325
|
|
|
83
326
|
}
|
|
84
327
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
328
|
+
const bb = this._bounds;
|
|
329
|
+
this._rangeBounds( aabb, order, start, mid, bb, 0 );
|
|
330
|
+
this._rangeBounds( aabb, order, mid, end, bb, 6 );
|
|
331
|
+
return mid;
|
|
88
332
|
|
|
89
|
-
|
|
333
|
+
}
|
|
90
334
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Binned SAH — one pass to bin, one over the bin boundaries, then an in-place partition.
|
|
337
|
+
* A node costs O(n) instead of sorting three times over.
|
|
338
|
+
* Returns `start` when the centroids are degenerate on every axis.
|
|
339
|
+
* @private
|
|
340
|
+
*/
|
|
341
|
+
_binnedSplit( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ ) {
|
|
342
|
+
|
|
343
|
+
const parentSA = this._surfaceArea( minX, minY, minZ, maxX, maxY, maxZ );
|
|
344
|
+
if ( ! ( parentSA > 0 ) || ! isFinite( parentSA ) ) return start;
|
|
345
|
+
|
|
346
|
+
// Centroid bounds decide the bin mapping; object bounds decide the cost.
|
|
347
|
+
let cMinX = Infinity, cMinY = Infinity, cMinZ = Infinity;
|
|
348
|
+
let cMaxX = - Infinity, cMaxY = - Infinity, cMaxZ = - Infinity;
|
|
349
|
+
for ( let i = start; i < end; i ++ ) {
|
|
350
|
+
|
|
351
|
+
const a = order[ i ] * 6;
|
|
352
|
+
const cx = ( aabb[ a ] + aabb[ a + 3 ] ) * 0.5;
|
|
353
|
+
const cy = ( aabb[ a + 1 ] + aabb[ a + 4 ] ) * 0.5;
|
|
354
|
+
const cz = ( aabb[ a + 2 ] + aabb[ a + 5 ] ) * 0.5;
|
|
355
|
+
if ( cx < cMinX ) cMinX = cx; if ( cx > cMaxX ) cMaxX = cx;
|
|
356
|
+
if ( cy < cMinY ) cMinY = cy; if ( cy > cMaxY ) cMaxY = cy;
|
|
357
|
+
if ( cz < cMinZ ) cMinZ = cz; if ( cz > cMaxZ ) cMaxZ = cz;
|
|
98
358
|
|
|
99
359
|
}
|
|
100
360
|
|
|
101
|
-
|
|
102
|
-
|
|
361
|
+
// Reused, not fresh literals: at ~10M calls two small arrays per call is most of the
|
|
362
|
+
// collector's work.
|
|
363
|
+
const extent = this._extent, cMin = this._cMin;
|
|
364
|
+
extent[ 0 ] = cMaxX - cMinX; extent[ 1 ] = cMaxY - cMinY; extent[ 2 ] = cMaxZ - cMinZ;
|
|
365
|
+
cMin[ 0 ] = cMinX; cMin[ 1 ] = cMinY; cMin[ 2 ] = cMinZ;
|
|
366
|
+
|
|
367
|
+
let bestCost = Infinity, bestAxis = - 1, bestBin = - 1;
|
|
368
|
+
const counts = this._binCounts;
|
|
369
|
+
const bounds = this._binBounds;
|
|
370
|
+
|
|
371
|
+
const liveX = extent[ 0 ] > 1e-12, liveY = extent[ 1 ] > 1e-12, liveZ = extent[ 2 ] > 1e-12;
|
|
372
|
+
if ( ! liveX && ! liveY && ! liveZ ) return start;
|
|
373
|
+
|
|
374
|
+
const nBins = binsFor( end - start );
|
|
375
|
+
const scaleX = liveX ? nBins / extent[ 0 ] : 0;
|
|
376
|
+
const scaleY = liveY ? nBins / extent[ 1 ] : 0;
|
|
377
|
+
const scaleZ = liveZ ? nBins / extent[ 2 ] : 0;
|
|
103
378
|
|
|
104
|
-
|
|
105
|
-
|
|
379
|
+
counts.fill( 0, 0, nBins * 3 );
|
|
380
|
+
for ( let b = 0; b < nBins * 3; b ++ ) {
|
|
106
381
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
382
|
+
const o = b * 6;
|
|
383
|
+
bounds[ o ] = bounds[ o + 1 ] = bounds[ o + 2 ] = Infinity;
|
|
384
|
+
bounds[ o + 3 ] = bounds[ o + 4 ] = bounds[ o + 5 ] = - Infinity;
|
|
110
385
|
|
|
111
386
|
}
|
|
112
387
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
let bestAxis = 0;
|
|
117
|
-
let bestSplit = 0;
|
|
388
|
+
// All three axes binned in ONE sweep. Three separate passes re-read the same range
|
|
389
|
+
// (and the same cache lines) three times for no extra information.
|
|
390
|
+
for ( let i = start; i < end; i ++ ) {
|
|
118
391
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
392
|
+
const a = order[ i ] * 6;
|
|
393
|
+
const aMinX = aabb[ a ], aMinY = aabb[ a + 1 ], aMinZ = aabb[ a + 2 ];
|
|
394
|
+
const aMaxX = aabb[ a + 3 ], aMaxY = aabb[ a + 4 ], aMaxZ = aabb[ a + 5 ];
|
|
122
395
|
|
|
123
396
|
for ( let axis = 0; axis < 3; axis ++ ) {
|
|
124
397
|
|
|
125
|
-
|
|
126
|
-
|
|
398
|
+
const scale = axis === 0 ? scaleX : axis === 1 ? scaleY : scaleZ;
|
|
399
|
+
if ( scale === 0 ) continue;
|
|
400
|
+
const c = axis === 0 ? ( aMinX + aMaxX ) * 0.5
|
|
401
|
+
: axis === 1 ? ( aMinY + aMaxY ) * 0.5 : ( aMinZ + aMaxZ ) * 0.5;
|
|
402
|
+
let b = ( ( c - cMin[ axis ] ) * scale ) | 0;
|
|
403
|
+
if ( b < 0 ) b = 0; else if ( b >= nBins ) b = nBins - 1;
|
|
404
|
+
|
|
405
|
+
b += axis * nBins;
|
|
406
|
+
counts[ b ] ++;
|
|
407
|
+
const o = b * 6;
|
|
408
|
+
if ( aMinX < bounds[ o ] ) bounds[ o ] = aMinX;
|
|
409
|
+
if ( aMinY < bounds[ o + 1 ] ) bounds[ o + 1 ] = aMinY;
|
|
410
|
+
if ( aMinZ < bounds[ o + 2 ] ) bounds[ o + 2 ] = aMinZ;
|
|
411
|
+
if ( aMaxX > bounds[ o + 3 ] ) bounds[ o + 3 ] = aMaxX;
|
|
412
|
+
if ( aMaxY > bounds[ o + 4 ] ) bounds[ o + 4 ] = aMaxY;
|
|
413
|
+
if ( aMaxZ > bounds[ o + 5 ] ) bounds[ o + 5 ] = aMaxZ;
|
|
414
|
+
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
}
|
|
127
418
|
|
|
128
|
-
|
|
129
|
-
const aabbB = entries[ b ].worldAABB;
|
|
130
|
-
const cA = this._centroid( aabbA, axis );
|
|
131
|
-
const cB = this._centroid( aabbB, axis );
|
|
132
|
-
return cA - cB;
|
|
419
|
+
for ( let axis = 0; axis < 3; axis ++ ) {
|
|
133
420
|
|
|
134
|
-
|
|
421
|
+
if ( ! ( extent[ axis ] > 1e-12 ) ) continue;
|
|
422
|
+
const binBase = axis * nBins;
|
|
135
423
|
|
|
136
|
-
|
|
137
|
-
|
|
424
|
+
// Suffix pass over bin boundaries, then a prefix sweep — same shape as the exact
|
|
425
|
+
// version, but over the bins instead of n entries.
|
|
426
|
+
const rightSA = this._binSuffix;
|
|
427
|
+
const rightN = this._binSuffixCount;
|
|
428
|
+
let sMinX = Infinity, sMinY = Infinity, sMinZ = Infinity;
|
|
429
|
+
let sMaxX = - Infinity, sMaxY = - Infinity, sMaxZ = - Infinity, sCount = 0;
|
|
430
|
+
|
|
431
|
+
for ( let b = nBins - 1; b >= 1; b -- ) {
|
|
432
|
+
|
|
433
|
+
const o = ( binBase + b ) * 6;
|
|
434
|
+
if ( bounds[ o ] < sMinX ) sMinX = bounds[ o ];
|
|
435
|
+
if ( bounds[ o + 1 ] < sMinY ) sMinY = bounds[ o + 1 ];
|
|
436
|
+
if ( bounds[ o + 2 ] < sMinZ ) sMinZ = bounds[ o + 2 ];
|
|
437
|
+
if ( bounds[ o + 3 ] > sMaxX ) sMaxX = bounds[ o + 3 ];
|
|
438
|
+
if ( bounds[ o + 4 ] > sMaxY ) sMaxY = bounds[ o + 4 ];
|
|
439
|
+
if ( bounds[ o + 5 ] > sMaxZ ) sMaxZ = bounds[ o + 5 ];
|
|
440
|
+
sCount += counts[ binBase + b ];
|
|
441
|
+
rightSA[ b ] = sCount ? this._surfaceArea( sMinX, sMinY, sMinZ, sMaxX, sMaxY, sMaxZ ) : 0;
|
|
442
|
+
rightN[ b ] = sCount;
|
|
443
|
+
|
|
444
|
+
}
|
|
138
445
|
|
|
139
|
-
|
|
140
|
-
|
|
446
|
+
let lMinX = Infinity, lMinY = Infinity, lMinZ = Infinity;
|
|
447
|
+
let lMaxX = - Infinity, lMaxY = - Infinity, lMaxZ = - Infinity, lCount = 0;
|
|
141
448
|
|
|
142
|
-
|
|
143
|
-
leftAABB.minX, leftAABB.minY, leftAABB.minZ,
|
|
144
|
-
leftAABB.maxX, leftAABB.maxY, leftAABB.maxZ
|
|
145
|
-
);
|
|
146
|
-
const rightSA = this._surfaceArea(
|
|
147
|
-
rightAABB.minX, rightAABB.minY, rightAABB.minZ,
|
|
148
|
-
rightAABB.maxX, rightAABB.maxY, rightAABB.maxZ
|
|
149
|
-
);
|
|
449
|
+
for ( let b = 1; b < nBins; b ++ ) {
|
|
150
450
|
|
|
151
|
-
|
|
152
|
-
|
|
451
|
+
const o = ( binBase + b - 1 ) * 6;
|
|
452
|
+
if ( bounds[ o ] < lMinX ) lMinX = bounds[ o ];
|
|
453
|
+
if ( bounds[ o + 1 ] < lMinY ) lMinY = bounds[ o + 1 ];
|
|
454
|
+
if ( bounds[ o + 2 ] < lMinZ ) lMinZ = bounds[ o + 2 ];
|
|
455
|
+
if ( bounds[ o + 3 ] > lMaxX ) lMaxX = bounds[ o + 3 ];
|
|
456
|
+
if ( bounds[ o + 4 ] > lMaxY ) lMaxY = bounds[ o + 4 ];
|
|
457
|
+
if ( bounds[ o + 5 ] > lMaxZ ) lMaxZ = bounds[ o + 5 ];
|
|
458
|
+
lCount += counts[ binBase + b - 1 ];
|
|
153
459
|
|
|
154
|
-
|
|
460
|
+
if ( lCount === 0 || rightN[ b ] === 0 ) continue;
|
|
155
461
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
462
|
+
const leftSA = this._surfaceArea( lMinX, lMinY, lMinZ, lMaxX, lMaxY, lMaxZ );
|
|
463
|
+
const cost = 1.0 + ( leftSA * lCount + rightSA[ b ] * rightN[ b ] ) / parentSA;
|
|
464
|
+
if ( cost < bestCost ) {
|
|
159
465
|
|
|
160
|
-
|
|
466
|
+
bestCost = cost; bestAxis = axis; bestBin = b;
|
|
161
467
|
|
|
162
468
|
}
|
|
163
469
|
|
|
@@ -165,182 +471,221 @@ export class TLASBuilder {
|
|
|
165
471
|
|
|
166
472
|
}
|
|
167
473
|
|
|
168
|
-
|
|
169
|
-
// (degenerate AABB, overflow surface area, or coincident centroids).
|
|
170
|
-
if ( bestSplit <= 0 || bestSplit >= indices.length ) {
|
|
474
|
+
if ( bestAxis < 0 ) return start;
|
|
171
475
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
476
|
+
// Two-pointer partition in place: everything below the chosen bin moves left. Both
|
|
477
|
+
// halves' bounds fall out of the same sweep, so the caller needs no extra passes.
|
|
478
|
+
const scale = nBins / extent[ bestAxis ];
|
|
479
|
+
const base = cMin[ bestAxis ];
|
|
480
|
+
const bb = this._bounds;
|
|
481
|
+
bb[ 0 ] = bb[ 1 ] = bb[ 2 ] = bb[ 6 ] = bb[ 7 ] = bb[ 8 ] = Infinity;
|
|
482
|
+
bb[ 3 ] = bb[ 4 ] = bb[ 5 ] = bb[ 9 ] = bb[ 10 ] = bb[ 11 ] = - Infinity;
|
|
176
483
|
|
|
177
|
-
|
|
484
|
+
let i = start, j = end - 1;
|
|
485
|
+
while ( i <= j ) {
|
|
178
486
|
|
|
179
|
-
|
|
487
|
+
const a = order[ i ] * 6;
|
|
488
|
+
const c = ( aabb[ a + bestAxis ] + aabb[ a + 3 + bestAxis ] ) * 0.5;
|
|
489
|
+
let b = ( ( c - base ) * scale ) | 0;
|
|
490
|
+
if ( b < 0 ) b = 0; else if ( b >= nBins ) b = nBins - 1;
|
|
180
491
|
|
|
181
|
-
|
|
182
|
-
|
|
492
|
+
const h = b < bestBin ? 0 : 6;
|
|
493
|
+
if ( aabb[ a ] < bb[ h ] ) bb[ h ] = aabb[ a ];
|
|
494
|
+
if ( aabb[ a + 1 ] < bb[ h + 1 ] ) bb[ h + 1 ] = aabb[ a + 1 ];
|
|
495
|
+
if ( aabb[ a + 2 ] < bb[ h + 2 ] ) bb[ h + 2 ] = aabb[ a + 2 ];
|
|
496
|
+
if ( aabb[ a + 3 ] > bb[ h + 3 ] ) bb[ h + 3 ] = aabb[ a + 3 ];
|
|
497
|
+
if ( aabb[ a + 4 ] > bb[ h + 4 ] ) bb[ h + 4 ] = aabb[ a + 4 ];
|
|
498
|
+
if ( aabb[ a + 5 ] > bb[ h + 5 ] ) bb[ h + 5 ] = aabb[ a + 5 ];
|
|
183
499
|
|
|
184
|
-
|
|
185
|
-
|
|
500
|
+
if ( h === 0 ) i ++;
|
|
501
|
+
else {
|
|
186
502
|
|
|
187
|
-
|
|
503
|
+
const t = order[ i ]; order[ i ] = order[ j ]; order[ j ] = t;
|
|
504
|
+
j --;
|
|
188
505
|
|
|
189
|
-
|
|
190
|
-
const rightIndices = sorted.slice( bestSplit );
|
|
506
|
+
}
|
|
191
507
|
|
|
192
|
-
|
|
193
|
-
node.rightChild = this._buildRecursive( entries, rightIndices );
|
|
508
|
+
}
|
|
194
509
|
|
|
195
|
-
return
|
|
510
|
+
return i;
|
|
196
511
|
|
|
197
512
|
}
|
|
198
513
|
|
|
199
514
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* Side effect: records each entry's flat leaf index on `entry.tlasLeafIndex` so that
|
|
205
|
-
* visibility can later be patched in place (combinedBvhData[tlasLeafIndex*16 + 2]).
|
|
206
|
-
*
|
|
207
|
-
* @param {TLASNode} root
|
|
208
|
-
* @param {Array<{blasOffset: number, visible: boolean, tlasLeafIndex: number}>} entries
|
|
209
|
-
* @returns {Float32Array}
|
|
515
|
+
* Exact SAH sweep over all three axes. Sorts the range once per axis into scratch, then
|
|
516
|
+
* leaves it sorted along the winning axis so the partition is already done.
|
|
517
|
+
* Returns `start` when no axis yields a valid split.
|
|
518
|
+
* @private
|
|
210
519
|
*/
|
|
211
|
-
|
|
520
|
+
_sweepSplit( aabb, order, start, end, minX, minY, minZ, maxX, maxY, maxZ ) {
|
|
521
|
+
|
|
522
|
+
const n = end - start;
|
|
523
|
+
const parentSA = this._surfaceArea( minX, minY, minZ, maxX, maxY, maxZ );
|
|
524
|
+
if ( ! ( parentSA > 0 ) || ! isFinite( parentSA ) ) return start;
|
|
525
|
+
const suffix = this._suffixBuffer( n );
|
|
526
|
+
let bestCost = Infinity, bestAxis = - 1, bestSplit = - 1;
|
|
212
527
|
|
|
213
|
-
|
|
528
|
+
for ( let axis = 0; axis < 3; axis ++ ) {
|
|
214
529
|
|
|
215
|
-
|
|
216
|
-
const nodes = [];
|
|
217
|
-
const stack = [ root ];
|
|
218
|
-
while ( stack.length > 0 ) {
|
|
530
|
+
this._sortRange( aabb, order, start, end, axis );
|
|
219
531
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if ( n.leftChild ) stack.push( n.leftChild );
|
|
532
|
+
// suffix[i] = bounds of order[start+i .. end-1]
|
|
533
|
+
let sMinX = Infinity, sMinY = Infinity, sMinZ = Infinity;
|
|
534
|
+
let sMaxX = - Infinity, sMaxY = - Infinity, sMaxZ = - Infinity;
|
|
535
|
+
for ( let i = n - 1; i >= 1; i -- ) {
|
|
225
536
|
|
|
226
|
-
|
|
537
|
+
const a = order[ start + i ] * 6;
|
|
538
|
+
if ( aabb[ a ] < sMinX ) sMinX = aabb[ a ];
|
|
539
|
+
if ( aabb[ a + 1 ] < sMinY ) sMinY = aabb[ a + 1 ];
|
|
540
|
+
if ( aabb[ a + 2 ] < sMinZ ) sMinZ = aabb[ a + 2 ];
|
|
541
|
+
if ( aabb[ a + 3 ] > sMaxX ) sMaxX = aabb[ a + 3 ];
|
|
542
|
+
if ( aabb[ a + 4 ] > sMaxY ) sMaxY = aabb[ a + 4 ];
|
|
543
|
+
if ( aabb[ a + 5 ] > sMaxZ ) sMaxZ = aabb[ a + 5 ];
|
|
227
544
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
545
|
+
const o = i * 6;
|
|
546
|
+
suffix[ o ] = sMinX; suffix[ o + 1 ] = sMinY; suffix[ o + 2 ] = sMinZ;
|
|
547
|
+
suffix[ o + 3 ] = sMaxX; suffix[ o + 4 ] = sMaxY; suffix[ o + 5 ] = sMaxZ;
|
|
231
548
|
|
|
232
|
-
|
|
233
|
-
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let lMinX = Infinity, lMinY = Infinity, lMinZ = Infinity;
|
|
552
|
+
let lMaxX = - Infinity, lMaxY = - Infinity, lMaxZ = - Infinity;
|
|
553
|
+
|
|
554
|
+
for ( let i = 1; i < n; i ++ ) {
|
|
555
|
+
|
|
556
|
+
const a = order[ start + i - 1 ] * 6;
|
|
557
|
+
if ( aabb[ a ] < lMinX ) lMinX = aabb[ a ];
|
|
558
|
+
if ( aabb[ a + 1 ] < lMinY ) lMinY = aabb[ a + 1 ];
|
|
559
|
+
if ( aabb[ a + 2 ] < lMinZ ) lMinZ = aabb[ a + 2 ];
|
|
560
|
+
if ( aabb[ a + 3 ] > lMaxX ) lMaxX = aabb[ a + 3 ];
|
|
561
|
+
if ( aabb[ a + 4 ] > lMaxY ) lMaxY = aabb[ a + 4 ];
|
|
562
|
+
if ( aabb[ a + 5 ] > lMaxZ ) lMaxZ = aabb[ a + 5 ];
|
|
563
|
+
|
|
564
|
+
const o = i * 6;
|
|
565
|
+
const leftSA = this._surfaceArea( lMinX, lMinY, lMinZ, lMaxX, lMaxY, lMaxZ );
|
|
566
|
+
const rightSA = this._surfaceArea(
|
|
567
|
+
suffix[ o ], suffix[ o + 1 ], suffix[ o + 2 ],
|
|
568
|
+
suffix[ o + 3 ], suffix[ o + 4 ], suffix[ o + 5 ]
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const cost = 1.0 + ( leftSA * i + rightSA * ( n - i ) ) / parentSA;
|
|
572
|
+
if ( cost < bestCost ) {
|
|
573
|
+
|
|
574
|
+
bestCost = cost; bestAxis = axis; bestSplit = i;
|
|
575
|
+
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
}
|
|
234
579
|
|
|
235
580
|
}
|
|
236
581
|
|
|
237
|
-
|
|
238
|
-
data.fill( 0, 0, requiredSize ); // Clear stale data
|
|
582
|
+
if ( bestAxis < 0 || bestSplit <= 0 ) return start;
|
|
239
583
|
|
|
240
|
-
|
|
584
|
+
// The last axis swept is 2; re-sort only if the winner was a different one.
|
|
585
|
+
if ( bestAxis !== 2 ) this._sortRange( aabb, order, start, end, bestAxis );
|
|
586
|
+
return start + bestSplit;
|
|
241
587
|
|
|
242
|
-
|
|
243
|
-
const o = i * FLOATS_PER_NODE;
|
|
588
|
+
}
|
|
244
589
|
|
|
245
|
-
|
|
590
|
+
/** Widest centroid extent of order[start..end). @private */
|
|
591
|
+
_widestCentroidAxis( aabb, order, start, end ) {
|
|
246
592
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
593
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
594
|
+
let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
|
|
595
|
+
for ( let i = start; i < end; i ++ ) {
|
|
250
596
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
597
|
+
const a = order[ i ] * 6;
|
|
598
|
+
const cx = ( aabb[ a ] + aabb[ a + 3 ] ) * 0.5;
|
|
599
|
+
const cy = ( aabb[ a + 1 ] + aabb[ a + 4 ] ) * 0.5;
|
|
600
|
+
const cz = ( aabb[ a + 2 ] + aabb[ a + 5 ] ) * 0.5;
|
|
601
|
+
if ( cx < minX ) minX = cx; if ( cx > maxX ) maxX = cx;
|
|
602
|
+
if ( cy < minY ) minY = cy; if ( cy > maxY ) maxY = cy;
|
|
603
|
+
if ( cz < minZ ) minZ = cz; if ( cz > maxZ ) maxZ = cz;
|
|
255
604
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ;
|
|
608
|
+
return dy > dx && dy > dz ? 1 : dz > dx ? 2 : 0;
|
|
260
609
|
|
|
261
|
-
|
|
262
|
-
data[ o + 9 ] = right.minY;
|
|
263
|
-
data[ o + 10 ] = right.minZ;
|
|
610
|
+
}
|
|
264
611
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
data[ o + 14 ] = right.maxZ;
|
|
612
|
+
/** Insertion sort by centroid along `axis`; allocation-free, for a handful of entries. @private */
|
|
613
|
+
_insertionSortRange( aabb, order, start, end, axis ) {
|
|
268
614
|
|
|
269
|
-
|
|
615
|
+
const lo = axis, hi = 3 + axis;
|
|
616
|
+
for ( let i = start + 1; i < end; i ++ ) {
|
|
270
617
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
data[ o + 2 ] = entry.visible === false ? 0.0 : 1.0; // Per-mesh visibility (packed — frees a binding)
|
|
276
|
-
data[ o + 3 ] = BVH_LEAF_MARKERS.BLAS_POINTER_LEAF; // -2 marker
|
|
618
|
+
const v = order[ i ];
|
|
619
|
+
const key = aabb[ v * 6 + lo ] + aabb[ v * 6 + hi ];
|
|
620
|
+
let j = i - 1;
|
|
621
|
+
while ( j >= start && ( aabb[ order[ j ] * 6 + lo ] + aabb[ order[ j ] * 6 + hi ] ) > key ) {
|
|
277
622
|
|
|
278
|
-
|
|
623
|
+
order[ j + 1 ] = order[ j ];
|
|
624
|
+
j --;
|
|
279
625
|
|
|
280
626
|
}
|
|
281
627
|
|
|
282
|
-
|
|
628
|
+
order[ j + 1 ] = v;
|
|
283
629
|
|
|
284
|
-
|
|
630
|
+
}
|
|
285
631
|
|
|
286
632
|
}
|
|
287
633
|
|
|
288
|
-
|
|
634
|
+
/** Sort order[start..end) by centroid along `axis`, in place. @private */
|
|
635
|
+
_sortRange( aabb, order, start, end, axis ) {
|
|
289
636
|
|
|
290
|
-
|
|
637
|
+
const n = end - start;
|
|
638
|
+
const scratch = this._sortScratch;
|
|
639
|
+
scratch.length = n;
|
|
640
|
+
for ( let i = 0; i < n; i ++ ) scratch[ i ] = order[ start + i ];
|
|
291
641
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
642
|
+
const lo = axis, hi = 3 + axis;
|
|
643
|
+
scratch.sort( ( a, b ) =>
|
|
644
|
+
( aabb[ a * 6 + lo ] + aabb[ a * 6 + hi ] ) - ( aabb[ b * 6 + lo ] + aabb[ b * 6 + hi ] ) );
|
|
645
|
+
for ( let i = 0; i < n; i ++ ) order[ start + i ] = scratch[ i ];
|
|
295
646
|
|
|
296
647
|
}
|
|
297
648
|
|
|
298
|
-
|
|
649
|
+
/** Grow-only scratch for the split sweep; one buffer serves every node. @private */
|
|
650
|
+
_suffixBuffer( n ) {
|
|
299
651
|
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return 2.0 * ( dx * dy + dy * dz + dz * dx );
|
|
652
|
+
const need = n * 6;
|
|
653
|
+
if ( ! this._suffix || this._suffix.length < need ) this._suffix = new Float64Array( need );
|
|
654
|
+
return this._suffix;
|
|
304
655
|
|
|
305
656
|
}
|
|
306
657
|
|
|
307
|
-
|
|
658
|
+
/** Writes 6 doubles (min xyz, max xyz) into `out` at `off`. @private */
|
|
659
|
+
_rangeBounds( aabb, order, start, end, out, off ) {
|
|
308
660
|
|
|
309
661
|
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
310
662
|
let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
|
|
311
663
|
|
|
312
|
-
for ( let i =
|
|
664
|
+
for ( let i = start; i < end; i ++ ) {
|
|
313
665
|
|
|
314
|
-
const
|
|
315
|
-
if ( aabb
|
|
316
|
-
if ( aabb
|
|
317
|
-
if ( aabb
|
|
318
|
-
if ( aabb
|
|
319
|
-
if ( aabb
|
|
320
|
-
if ( aabb
|
|
666
|
+
const a = order[ i ] * 6;
|
|
667
|
+
if ( aabb[ a ] < minX ) minX = aabb[ a ];
|
|
668
|
+
if ( aabb[ a + 1 ] < minY ) minY = aabb[ a + 1 ];
|
|
669
|
+
if ( aabb[ a + 2 ] < minZ ) minZ = aabb[ a + 2 ];
|
|
670
|
+
if ( aabb[ a + 3 ] > maxX ) maxX = aabb[ a + 3 ];
|
|
671
|
+
if ( aabb[ a + 4 ] > maxY ) maxY = aabb[ a + 4 ];
|
|
672
|
+
if ( aabb[ a + 5 ] > maxZ ) maxZ = aabb[ a + 5 ];
|
|
321
673
|
|
|
322
674
|
}
|
|
323
675
|
|
|
324
|
-
|
|
676
|
+
out[ off ] = minX; out[ off + 1 ] = minY; out[ off + 2 ] = minZ;
|
|
677
|
+
out[ off + 3 ] = maxX; out[ off + 4 ] = maxY; out[ off + 5 ] = maxZ;
|
|
325
678
|
|
|
326
679
|
}
|
|
327
680
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if ( ! root ) return 0;
|
|
331
|
-
|
|
332
|
-
let count = 0;
|
|
333
|
-
const stack = [ root ];
|
|
334
|
-
while ( stack.length > 0 ) {
|
|
335
|
-
|
|
336
|
-
const node = stack.pop();
|
|
337
|
-
count ++;
|
|
338
|
-
if ( node.leftChild ) stack.push( node.leftChild );
|
|
339
|
-
if ( node.rightChild ) stack.push( node.rightChild );
|
|
681
|
+
// ── Helpers ──
|
|
340
682
|
|
|
341
|
-
|
|
683
|
+
_surfaceArea( minX, minY, minZ, maxX, maxY, maxZ ) {
|
|
342
684
|
|
|
343
|
-
|
|
685
|
+
const dx = maxX - minX;
|
|
686
|
+
const dy = maxY - minY;
|
|
687
|
+
const dz = maxZ - minZ;
|
|
688
|
+
return 2.0 * ( dx * dy + dy * dz + dz * dx );
|
|
344
689
|
|
|
345
690
|
}
|
|
346
691
|
|