rayzee 7.31.1 → 8.1.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 +6728 -4735
- package/dist/rayzee.umd.js +121 -73
- package/package.json +2 -2
- package/src/EngineDefaults.js +168 -13
- package/src/EngineIssues.js +5 -0
- package/src/PathTracerApp.js +151 -25
- 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 +580 -93
- 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/Processor/utils.js +8 -20
- 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/SortGlobalKernels.js +3 -4
- package/src/TSL/Struct.js +3 -0
- package/src/TSL/TextureSampling.js +24 -9
- package/src/TSL/patches.js +149 -84
- 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
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import { Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
|
|
2
|
-
import {
|
|
1
|
+
import { BufferAttribute, Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
|
|
2
|
+
import {
|
|
3
|
+
TEXTURE_CONSTANTS, TRIANGLE_DATA_LAYOUT, packNormalOct, packTriangleFlags
|
|
4
|
+
} from '../EngineDefaults.js';
|
|
3
5
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
6
|
+
import { ChunkedRecords, SHARED_MEMORY_AVAILABLE } from './ChunkedRecords.js';
|
|
4
7
|
import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
|
|
5
8
|
|
|
6
9
|
const log = createLogger( 'geometry' );
|
|
7
10
|
|
|
8
11
|
const MAX_TEXTURES_LIMIT = TEXTURE_CONSTANTS.MAX_TEXTURES_LIMIT;
|
|
9
12
|
|
|
13
|
+
const IDENTITY_ELEMENTS = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
|
|
14
|
+
|
|
15
|
+
function isIdentityElements( e ) {
|
|
16
|
+
|
|
17
|
+
for ( let i = 0; i < 16; i ++ ) if ( e[ i ] !== IDENTITY_ELEMENTS[ i ] ) return false;
|
|
18
|
+
return true;
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* glTF 2.0 alphaMode for a three.js material: 0 OPAQUE, 1 MASK, 2 BLEND.
|
|
12
24
|
*
|
|
@@ -54,6 +66,80 @@ export function deriveAlphaMode( material ) {
|
|
|
54
66
|
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Bytes the three.js geometry behind `object` holds, counting each `BufferGeometry` once.
|
|
71
|
+
*
|
|
72
|
+
* Instanced meshes share one geometry, so counting per placement would over-report by orders of
|
|
73
|
+
* magnitude on a scene with millions of placements. Worth re-reading after extraction:
|
|
74
|
+
* `_compressAttributes` halves normals and colours, which is ~370 MB on a 40M-triangle scene.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object3D} object
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
export function geometryBytesOf( object ) {
|
|
80
|
+
|
|
81
|
+
let bytes = 0;
|
|
82
|
+
const counted = new Set();
|
|
83
|
+
|
|
84
|
+
const walk = node => {
|
|
85
|
+
|
|
86
|
+
const g = node.isMesh && node.material ? node.geometry : null;
|
|
87
|
+
if ( g && ! counted.has( g.uuid ) ) {
|
|
88
|
+
|
|
89
|
+
counted.add( g.uuid );
|
|
90
|
+
for ( const name in g.attributes ) bytes += g.attributes[ name ].array?.byteLength ?? 0;
|
|
91
|
+
bytes += g.index?.array?.byteLength ?? 0;
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ( node.children ) for ( const child of node.children ) walk( child );
|
|
96
|
+
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
walk( object );
|
|
100
|
+
return bytes;
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Affine point and linear direction transforms straight off the matrix elements: no perspective
|
|
105
|
+
// divide, no method dispatch, on the hottest per-vertex path of a baked mesh.
|
|
106
|
+
function affinePoint( v, e ) {
|
|
107
|
+
|
|
108
|
+
const x = v.x, y = v.y, z = v.z;
|
|
109
|
+
v.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ];
|
|
110
|
+
v.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ];
|
|
111
|
+
v.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ];
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function linearDir( v, e ) {
|
|
116
|
+
|
|
117
|
+
const x = v.x, y = v.y, z = v.z;
|
|
118
|
+
v.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z;
|
|
119
|
+
v.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z;
|
|
120
|
+
v.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z;
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Emissive instances become real triangles, so the expansion is capped.
|
|
125
|
+
const MAX_EXPANDED_EMISSIVE_TRIANGLES = 1 << 21;
|
|
126
|
+
|
|
127
|
+
// A skinned or morphed mesh is posed per copy, so it cannot share one set of triangles.
|
|
128
|
+
function isDeformable( mesh ) {
|
|
129
|
+
|
|
130
|
+
return !! ( mesh.isSkinnedMesh || Object.keys( mesh.geometry?.morphAttributes ?? {} ).length );
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Determinant of a column-major matrix's 3x3 basis; negative means the transform mirrors.
|
|
135
|
+
function determinant3( e ) {
|
|
136
|
+
|
|
137
|
+
return e[ 0 ] * ( e[ 5 ] * e[ 10 ] - e[ 9 ] * e[ 6 ] )
|
|
138
|
+
- e[ 4 ] * ( e[ 1 ] * e[ 10 ] - e[ 9 ] * e[ 2 ] )
|
|
139
|
+
+ e[ 8 ] * ( e[ 1 ] * e[ 6 ] - e[ 5 ] * e[ 2 ] );
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
57
143
|
export class GeometryExtractor {
|
|
58
144
|
|
|
59
145
|
/** @param {{issues?: import('../EngineIssues.js').IssueLog}} [options] */
|
|
@@ -68,6 +154,9 @@ export class GeometryExtractor {
|
|
|
68
154
|
vec2: Array( 6 ).fill().map( () => new Vector2() )
|
|
69
155
|
};
|
|
70
156
|
|
|
157
|
+
this._geometryRanges = new Map();
|
|
158
|
+
this.expandedTriangleCount = 0;
|
|
159
|
+
this._allocatePlacements( 0 );
|
|
71
160
|
this._matrixPool = {
|
|
72
161
|
mat3: new Matrix3(),
|
|
73
162
|
mat4: new Matrix4()
|
|
@@ -106,17 +195,198 @@ export class GeometryExtractor {
|
|
|
106
195
|
// which overflowed the browser's ArrayBuffer allocator. Exact avoids both.
|
|
107
196
|
const totalTriangles = this._countTriangles( object );
|
|
108
197
|
this._triangleCapacity = Math.max( 1024, totalTriangles );
|
|
109
|
-
this.
|
|
198
|
+
this._allocateTriangles( this._triangleCapacity );
|
|
110
199
|
this.currentTriangleIndex = 0;
|
|
111
200
|
|
|
201
|
+
// One `{ sourceMesh, matrixWorld }` object per placement cost 1.6 GB at 6M of them.
|
|
202
|
+
this._allocatePlacements( this._countPlacements( object ) );
|
|
203
|
+
this._geometryUses = this._countGeometryUses( object );
|
|
204
|
+
this.bakeInverse = new Map();
|
|
205
|
+
|
|
112
206
|
// Single traversal: extract geometry, materials, lights, and cameras
|
|
113
207
|
this.traverseObject( object );
|
|
208
|
+
this._shareInstanceMatrices();
|
|
209
|
+
this._compressAttributes( object );
|
|
114
210
|
|
|
115
211
|
this.logStats();
|
|
116
212
|
return this.getExtractedData();
|
|
117
213
|
|
|
118
214
|
}
|
|
119
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The sizes a memory preflight needs, without doing any of the work: what `extract()` would
|
|
218
|
+
* store, plus the three.js geometry already resident behind it.
|
|
219
|
+
*
|
|
220
|
+
* Geometry is counted once per unique `BufferGeometry` — instanced meshes share one, so
|
|
221
|
+
* counting per placement would over-report by orders of magnitude on a scene like Moana.
|
|
222
|
+
*
|
|
223
|
+
* @param {Object3D} object
|
|
224
|
+
* @returns {{triangles: number, placements: number, meshes: number, geometryBytes: number}}
|
|
225
|
+
*/
|
|
226
|
+
surveyScene( object ) {
|
|
227
|
+
|
|
228
|
+
let meshes = 0;
|
|
229
|
+
const walk = node => {
|
|
230
|
+
|
|
231
|
+
if ( node.isMesh && node.geometry && node.material ) meshes ++;
|
|
232
|
+
if ( node.children ) for ( const child of node.children ) walk( child );
|
|
233
|
+
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
walk( object );
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
triangles: this._countTriangles( object ),
|
|
240
|
+
placements: this._countPlacements( object ),
|
|
241
|
+
meshes,
|
|
242
|
+
geometryBytes: geometryBytesOf( object ),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* How many meshes use each geometry+material pair. A pair used once has nothing to share, so
|
|
249
|
+
* it is baked to world space and its ray is never transformed. Walks everything, so a mesh
|
|
250
|
+
* the extraction later skips only ever over-counts — towards object space, never a bad bake.
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
_countGeometryUses( object ) {
|
|
254
|
+
|
|
255
|
+
const uses = new Map();
|
|
256
|
+
|
|
257
|
+
object.traverse( ( o ) => {
|
|
258
|
+
|
|
259
|
+
if ( ! o.isMesh || ! o.geometry || ! o.material ) return;
|
|
260
|
+
const key = `${o.geometry.uuid}|${o.material.uuid}`;
|
|
261
|
+
uses.set( key, ( uses.get( key ) ?? 0 ) + 1 );
|
|
262
|
+
|
|
263
|
+
} );
|
|
264
|
+
|
|
265
|
+
return uses;
|
|
266
|
+
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Placements extract() will record: one per mesh, or one per instance of an InstancedMesh.
|
|
270
|
+
_countPlacements( object ) {
|
|
271
|
+
|
|
272
|
+
let count = 0;
|
|
273
|
+
|
|
274
|
+
if ( object.isMesh && object.geometry && object.material ) {
|
|
275
|
+
|
|
276
|
+
count += object.isInstancedMesh && object.instanceMatrix
|
|
277
|
+
? ( object.count ?? object.instanceMatrix.array.length / 16 )
|
|
278
|
+
: 1;
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if ( object.children ) {
|
|
283
|
+
|
|
284
|
+
for ( const child of object.children ) count += this._countPlacements( child );
|
|
285
|
+
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return count;
|
|
289
|
+
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_allocatePlacements( count ) {
|
|
293
|
+
|
|
294
|
+
this.instanceCount = 0;
|
|
295
|
+
this.instanceCapacity = count;
|
|
296
|
+
this.instanceSource = new Int32Array( count );
|
|
297
|
+
this.instanceMatrices = new Float32Array( count * 16 );
|
|
298
|
+
this._shareable = [];
|
|
299
|
+
this._compressed = new Set();
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Halve the scene graph's normal/tangent/colour attributes as normalized 16-bit integers.
|
|
305
|
+
*
|
|
306
|
+
* three.js denormalizes on read and uploads them as `snorm16`, so every CPU-side consumer
|
|
307
|
+
* keeps working; the triangle buffer already stores normals as a coarser oct16 pair.
|
|
308
|
+
* Positions and UVs keep the full float range.
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
_compressAttributes( object ) {
|
|
312
|
+
|
|
313
|
+
const done = this._compressed;
|
|
314
|
+
|
|
315
|
+
// A host's own Object3D is rendered as a copy, but the copy shares its geometry by
|
|
316
|
+
// reference, so rewriting an attribute there would rewrite the host's data. Its whole
|
|
317
|
+
// subtree is skipped rather than the root alone.
|
|
318
|
+
const walk = ( o, visit ) => {
|
|
319
|
+
|
|
320
|
+
if ( o.userData?.__rayzeeExternal ) return;
|
|
321
|
+
visit( o );
|
|
322
|
+
for ( const child of o.children ) walk( child, visit );
|
|
323
|
+
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
walk( object, o => {
|
|
327
|
+
|
|
328
|
+
const g = o.geometry;
|
|
329
|
+
if ( ! o.isMesh || ! g || done.has( g.uuid ) ) return;
|
|
330
|
+
done.add( g.uuid );
|
|
331
|
+
|
|
332
|
+
for ( const name of [ 'normal', 'tangent', 'color' ] ) {
|
|
333
|
+
|
|
334
|
+
const attr = g.getAttribute( name );
|
|
335
|
+
if ( ! attr || attr.normalized || ! ( attr.array instanceof Float32Array ) ) continue;
|
|
336
|
+
if ( g.morphAttributes?.[ name ]?.length ) continue;
|
|
337
|
+
|
|
338
|
+
const src = attr.array;
|
|
339
|
+
let ok = true;
|
|
340
|
+
for ( let i = 0; i < src.length; i ++ ) {
|
|
341
|
+
|
|
342
|
+
const v = src[ i ];
|
|
343
|
+
if ( ! ( v >= - 1.0001 && v <= 1.0001 ) ) {
|
|
344
|
+
|
|
345
|
+
ok = false; break;
|
|
346
|
+
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Out of snorm range (or NaN) — leave it as floats rather than silently clamp.
|
|
352
|
+
if ( ! ok ) continue;
|
|
353
|
+
|
|
354
|
+
const packed = new Int16Array( src.length );
|
|
355
|
+
for ( let i = 0; i < src.length; i ++ ) {
|
|
356
|
+
|
|
357
|
+
packed[ i ] = Math.round( Math.max( - 1, Math.min( 1, src[ i ] ) ) * 32767 );
|
|
358
|
+
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
g.setAttribute( name, new BufferAttribute( packed, attr.itemSize, true ) );
|
|
362
|
+
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
} );
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Point each InstancedMesh's matrix attribute at the placement pool that duplicates it.
|
|
371
|
+
*
|
|
372
|
+
* With the host at the origin the two hold identical bytes — 366 MB apart at 6M instances.
|
|
373
|
+
* Runs after the traversal, since growth reallocates the pool.
|
|
374
|
+
* @private
|
|
375
|
+
*/
|
|
376
|
+
_shareInstanceMatrices() {
|
|
377
|
+
|
|
378
|
+
for ( const { mesh, start, count } of this._shareable ) {
|
|
379
|
+
|
|
380
|
+
const attr = mesh.instanceMatrix;
|
|
381
|
+
if ( ! attr || attr.array.length !== count * 16 ) continue;
|
|
382
|
+
attr.array = this.instanceMatrices.subarray( start * 16, ( start + count ) * 16 );
|
|
383
|
+
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this._shareable = [];
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
|
|
120
390
|
// Sum the exact triangle count over every mesh extract() will process
|
|
121
391
|
// (mirrors traverseObject/processMesh guards) so we can allocate once.
|
|
122
392
|
_countTriangles( object ) {
|
|
@@ -148,13 +418,44 @@ export class GeometryExtractor {
|
|
|
148
418
|
|
|
149
419
|
if ( needed <= this._triangleCapacity ) return;
|
|
150
420
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
this.
|
|
421
|
+
const previous = this.triangles;
|
|
422
|
+
const used = this.currentTriangleIndex;
|
|
423
|
+
this._allocateTriangles( needed );
|
|
424
|
+
for ( let t = 0; t < used; t ++ ) {
|
|
425
|
+
|
|
426
|
+
this.triangles.chunkFor( t ).set(
|
|
427
|
+
previous.chunkFor( t ).subarray( previous.baseOf( t ), previous.baseOf( t ) + previous.lanesPerRecord ),
|
|
428
|
+
this.triangles.baseOf( t )
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
}
|
|
432
|
+
|
|
154
433
|
this._triangleCapacity = needed;
|
|
155
434
|
|
|
156
435
|
}
|
|
157
436
|
|
|
437
|
+
/**
|
|
438
|
+
* The record is a uint buffer; positions and UVs are written as f32 through a view of the
|
|
439
|
+
* same memory, so a packed lane's bit pattern is never round-tripped through an f32.
|
|
440
|
+
*
|
|
441
|
+
* Chunked, because one array cannot hold more than ~26M triangles at 80 bytes each — the
|
|
442
|
+
* V8 ArrayBuffer cap, well below what the GPU buffer can take.
|
|
443
|
+
* @private
|
|
444
|
+
*/
|
|
445
|
+
_allocateTriangles( capacity ) {
|
|
446
|
+
|
|
447
|
+
// Shared-backed so a refit reads and writes them in place; copying them into shared memory
|
|
448
|
+
// on first refit would be a second copy of the largest thing in the scene.
|
|
449
|
+
this.triangles = new ChunkedRecords(
|
|
450
|
+
capacity, TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE, Uint32Array,
|
|
451
|
+
undefined, SHARED_MEMORY_AVAILABLE
|
|
452
|
+
);
|
|
453
|
+
this.triangleFloatChunks = this.triangles.viewAs( Float32Array );
|
|
454
|
+
this.triangleData = this.triangles.single;
|
|
455
|
+
this.triangleFloats = this.triangleFloatChunks.single;
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
158
459
|
traverseObject( object ) {
|
|
159
460
|
|
|
160
461
|
// Process the current object
|
|
@@ -203,14 +504,190 @@ export class GeometryExtractor {
|
|
|
203
504
|
this.meshes.push( mesh );
|
|
204
505
|
mesh.userData.meshIndex = meshIndex;
|
|
205
506
|
|
|
206
|
-
//
|
|
507
|
+
// Triangles are stored in object space, so two placements of the same geometry can
|
|
508
|
+
// point at one copy — and later at one BLAS. The material is part of the key because
|
|
509
|
+
// the material index is baked per triangle.
|
|
510
|
+
// Emissive geometry opts out: each placement is a separate light, and the light BVH
|
|
511
|
+
// keys its entries by triangle index, which sharing would make ambiguous. So does
|
|
512
|
+
// anything that deforms: a skinned or morphed copy needs triangles of its own, or an
|
|
513
|
+
// animation refit writes one copy's pose into every other.
|
|
514
|
+
const key = `${mesh.geometry.uuid}|${materialIndex}`;
|
|
515
|
+
const deformable = isDeformable( mesh );
|
|
516
|
+
const shareable = ! this._emissiveMaterial( materialIndex ) && ! deformable;
|
|
517
|
+
const shared = shareable ? this._geometryRanges.get( key ) : null;
|
|
518
|
+
|
|
519
|
+
// `expandedStart` is where this mesh's triangles sit in a per-mesh walk of the scene,
|
|
520
|
+
// which is the shape refit callers can build. `start` is where they are actually
|
|
521
|
+
// stored, which is the owner's copy when the geometry is shared.
|
|
522
|
+
if ( shared ) {
|
|
523
|
+
|
|
524
|
+
const range = {
|
|
525
|
+
start: shared.start, count: shared.count,
|
|
526
|
+
expandedStart: this.expandedTriangleCount, sharedFrom: shared.meshIndex
|
|
527
|
+
};
|
|
528
|
+
this.expandedTriangleCount += shared.count;
|
|
529
|
+
this.meshTriangleRanges.push( range );
|
|
530
|
+
this._recordPlacements( mesh, meshIndex );
|
|
531
|
+
return;
|
|
532
|
+
|
|
533
|
+
}
|
|
534
|
+
|
|
207
535
|
const rangeStart = this.currentTriangleIndex;
|
|
208
536
|
|
|
209
|
-
//
|
|
210
|
-
|
|
537
|
+
// Emission is measured per triangle, so instances of an emissive geometry have to be
|
|
538
|
+
// real triangles rather than one copy placed many times — otherwise the scene is lit by
|
|
539
|
+
// the first instance alone. Their triangles are baked, so this mesh keeps one placement.
|
|
540
|
+
const expand = mesh.isInstancedMesh && ! deformable
|
|
541
|
+
&& this._emissiveMaterial( materialIndex ) && this._canExpand( mesh );
|
|
542
|
+
|
|
543
|
+
// Object space only pays when a second placement can reuse the copy. A geometry used
|
|
544
|
+
// once — or an emissive one, which never shares — is baked to world space instead, so
|
|
545
|
+
// a ray reaching it skips the per-instance transform entirely.
|
|
546
|
+
const bake = expand || ( ! mesh.isInstancedMesh && ! deformable && (
|
|
547
|
+
this._geometryUses.get( `${mesh.geometry.uuid}|${mesh.material?.uuid}` ) === 1
|
|
548
|
+
|| this._emissiveMaterial( materialIndex )
|
|
549
|
+
) );
|
|
550
|
+
|
|
551
|
+
this.extractGeometry( mesh, materialIndex, meshIndex, bake, expand );
|
|
552
|
+
|
|
553
|
+
const range = {
|
|
554
|
+
start: rangeStart, count: this.currentTriangleIndex - rangeStart,
|
|
555
|
+
expandedStart: this.expandedTriangleCount
|
|
556
|
+
};
|
|
557
|
+
this.expandedTriangleCount += range.count;
|
|
558
|
+
this.meshTriangleRanges.push( range );
|
|
559
|
+
if ( range.count > 0 && ! bake && shareable ) this._geometryRanges.set( key, { ...range, meshIndex } );
|
|
560
|
+
this._recordPlacements( mesh, meshIndex, bake, expand );
|
|
561
|
+
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* One placement per object, or one per instance for an InstancedMesh. The scene graph
|
|
566
|
+
* holds a single object either way — only this list grows, so a million placements cost
|
|
567
|
+
* a matrix each rather than a million Object3Ds.
|
|
568
|
+
* @private
|
|
569
|
+
*/
|
|
570
|
+
_recordPlacements( mesh, meshIndex, baked = false, expanded = false ) {
|
|
571
|
+
|
|
572
|
+
const world = mesh.matrixWorld.elements;
|
|
573
|
+
const dst = this.instanceMatrices;
|
|
574
|
+
const src = this.instanceSource;
|
|
575
|
+
|
|
576
|
+
// Expanded instances live in the triangles, so the mesh keeps a single placement. The
|
|
577
|
+
// recorded inverse is the host's alone: moving the host is a delta that applies to
|
|
578
|
+
// every instance equally, which is exactly what composing against it produces.
|
|
579
|
+
if ( ! mesh.isInstancedMesh || ! mesh.instanceMatrix || expanded ) {
|
|
580
|
+
|
|
581
|
+
const p = this._nextPlacement();
|
|
582
|
+
src[ p ] = meshIndex;
|
|
583
|
+
|
|
584
|
+
if ( baked ) {
|
|
585
|
+
|
|
586
|
+
// The triangles already carry this pose, so the placement starts at identity. A
|
|
587
|
+
// later move composes against the inverse of what was baked in.
|
|
588
|
+
for ( let k = 0; k < 16; k ++ ) dst[ p * 16 + k ] = k % 5 === 0 ? 1 : 0;
|
|
589
|
+
this.bakeInverse.set( meshIndex, Float32Array.from( this._matrixPool.mat4.copy( mesh.matrixWorld ).invert().elements ) );
|
|
590
|
+
|
|
591
|
+
} else {
|
|
592
|
+
|
|
593
|
+
for ( let k = 0; k < 16; k ++ ) dst[ p * 16 + k ] = world[ k ];
|
|
594
|
+
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return;
|
|
598
|
+
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const arr = mesh.instanceMatrix.array;
|
|
602
|
+
const count = mesh.count ?? ( arr.length / 16 );
|
|
603
|
+
// A pbrt archive bakes the CTM into every instance, so the host needs no multiply.
|
|
604
|
+
const identityHost = isIdentityElements( world );
|
|
605
|
+
if ( identityHost && arr.length === count * 16 ) {
|
|
606
|
+
|
|
607
|
+
this._shareable.push( { mesh, start: this.instanceCount, count } );
|
|
608
|
+
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
612
|
+
|
|
613
|
+
const o = i * 16;
|
|
614
|
+
const p = this._nextPlacement();
|
|
615
|
+
const d = p * 16;
|
|
616
|
+
src[ p ] = meshIndex;
|
|
617
|
+
|
|
618
|
+
if ( identityHost ) {
|
|
619
|
+
|
|
620
|
+
for ( let k = 0; k < 16; k ++ ) dst[ d + k ] = arr[ o + k ];
|
|
621
|
+
continue;
|
|
622
|
+
|
|
623
|
+
}
|
|
211
624
|
|
|
212
|
-
|
|
213
|
-
|
|
625
|
+
// world = mesh.matrixWorld * instanceMatrix, both column-major.
|
|
626
|
+
for ( let c = 0; c < 4; c ++ ) {
|
|
627
|
+
|
|
628
|
+
for ( let r = 0; r < 4; r ++ ) {
|
|
629
|
+
|
|
630
|
+
dst[ d + c * 4 + r ] = world[ r ] * arr[ o + c * 4 ]
|
|
631
|
+
+ world[ 4 + r ] * arr[ o + c * 4 + 1 ]
|
|
632
|
+
+ world[ 8 + r ] * arr[ o + c * 4 + 2 ]
|
|
633
|
+
+ world[ 12 + r ] * arr[ o + c * 4 + 3 ];
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Next free placement slot, growing only if the pre-count was short. @private */
|
|
644
|
+
_nextPlacement() {
|
|
645
|
+
|
|
646
|
+
if ( this.instanceCount >= this.instanceCapacity ) {
|
|
647
|
+
|
|
648
|
+
const grown = Math.max( 16, this.instanceCapacity * 2 );
|
|
649
|
+
const srcCol = new Int32Array( grown );
|
|
650
|
+
const matCol = new Float32Array( grown * 16 );
|
|
651
|
+
srcCol.set( this.instanceSource );
|
|
652
|
+
matCol.set( this.instanceMatrices );
|
|
653
|
+
this.instanceSource = srcCol;
|
|
654
|
+
this.instanceMatrices = matCol;
|
|
655
|
+
this.instanceCapacity = grown;
|
|
656
|
+
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return this.instanceCount ++;
|
|
660
|
+
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** True when this material emits — those meshes keep their own triangles. */
|
|
664
|
+
/**
|
|
665
|
+
* Is expanding this instanced mesh into real triangles within budget? Past it the scene is
|
|
666
|
+
* lit by the first instance, which is wrong but bounded.
|
|
667
|
+
* @private
|
|
668
|
+
*/
|
|
669
|
+
_canExpand( mesh ) {
|
|
670
|
+
|
|
671
|
+
const g = mesh.geometry;
|
|
672
|
+
const tris = ( g.index ? g.index.count : g.attributes.position.count ) / 3;
|
|
673
|
+
const count = mesh.count ?? ( mesh.instanceMatrix?.array.length ?? 0 ) / 16;
|
|
674
|
+
if ( tris * count <= MAX_EXPANDED_EMISSIVE_TRIANGLES ) return true;
|
|
675
|
+
|
|
676
|
+
this._issues?.record(
|
|
677
|
+
ISSUE_CODES.EMISSIVE_INSTANCES_COLLAPSED,
|
|
678
|
+
`"${mesh.name || 'instanced mesh'}" would need ${Math.round( tris * count ).toLocaleString()} triangles to light every instance; lighting the first one only`,
|
|
679
|
+
{ instances: count, trianglesPerInstance: tris, limit: MAX_EXPANDED_EMISSIVE_TRIANGLES }
|
|
680
|
+
);
|
|
681
|
+
return false;
|
|
682
|
+
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
_emissiveMaterial( materialIndex ) {
|
|
686
|
+
|
|
687
|
+
const m = this.materials[ materialIndex ];
|
|
688
|
+
if ( ! m || ! ( m.emissiveIntensity > 0 ) ) return false;
|
|
689
|
+
const e = m.emissive;
|
|
690
|
+
return !! e && ( e.r > 0 || e.g > 0 || e.b > 0 );
|
|
214
691
|
|
|
215
692
|
}
|
|
216
693
|
|
|
@@ -488,7 +965,9 @@ export class GeometryExtractor {
|
|
|
488
965
|
getTextureMatrix( texture ) {
|
|
489
966
|
|
|
490
967
|
if ( ! texture ) return new Matrix3().elements;
|
|
491
|
-
|
|
968
|
+
// GLTFLoader writes `matrix` itself for a rotated KHR_texture_transform (glTF is
|
|
969
|
+
// T·R·S, three is T·S·R) and clears the flag; updateMatrix() ignores the flag.
|
|
970
|
+
if ( texture.matrixAutoUpdate ) texture.updateMatrix();
|
|
492
971
|
return texture.matrix.elements;
|
|
493
972
|
|
|
494
973
|
}
|
|
@@ -548,7 +1027,7 @@ export class GeometryExtractor {
|
|
|
548
1027
|
|
|
549
1028
|
}
|
|
550
1029
|
|
|
551
|
-
extractGeometry( mesh, materialIndex, meshIndex ) {
|
|
1030
|
+
extractGeometry( mesh, materialIndex, meshIndex, bake = false, expandInstances = false ) {
|
|
552
1031
|
|
|
553
1032
|
mesh.updateMatrix();
|
|
554
1033
|
mesh.updateMatrixWorld();
|
|
@@ -560,19 +1039,42 @@ export class GeometryExtractor {
|
|
|
560
1039
|
const uvs = geometry.attributes.uv;
|
|
561
1040
|
const indices = geometry.index ? geometry.index.array : null;
|
|
562
1041
|
|
|
563
|
-
// Compute matrices
|
|
564
|
-
this._matrixPool.mat4.copy( mesh.matrixWorld );
|
|
565
|
-
this._matrixPool.mat3.getNormalMatrix( this._matrixPool.mat4 );
|
|
566
|
-
|
|
567
1042
|
const triangleCount = indices ? indices.length / 3 : positions.count / 3;
|
|
568
1043
|
|
|
569
|
-
|
|
570
|
-
|
|
1044
|
+
if ( expandInstances ) {
|
|
1045
|
+
|
|
1046
|
+
const count = mesh.count ?? ( mesh.instanceMatrix.array.length / 16 );
|
|
1047
|
+
const inst = this._matrixPool.mat4;
|
|
1048
|
+
const world = new Matrix4();
|
|
1049
|
+
|
|
1050
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
1051
|
+
|
|
1052
|
+
inst.fromArray( mesh.instanceMatrix.array, i * 16 );
|
|
1053
|
+
world.multiplyMatrices( mesh.matrixWorld, inst );
|
|
1054
|
+
const normalMatrix = new Matrix3().getNormalMatrix( world );
|
|
1055
|
+
this.extractTrianglesInBatch(
|
|
1056
|
+
positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex,
|
|
1057
|
+
world, normalMatrix, determinant3( world.elements ) < 0
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return;
|
|
1063
|
+
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const bakeMatrix = bake ? this._matrixPool.mat4.copy( mesh.matrixWorld ) : null;
|
|
1067
|
+
const bakeNormal = bake ? this._matrixPool.mat3.getNormalMatrix( mesh.matrixWorld ) : null;
|
|
1068
|
+
// A mirroring transform reverses which way the vertices wind, so the face normal the
|
|
1069
|
+
// cross product yields would point into the surface. Swap two corners and it points out.
|
|
1070
|
+
const flip = bake && determinant3( bakeMatrix.elements ) < 0;
|
|
1071
|
+
|
|
1072
|
+
this.extractTrianglesInBatch( positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex, bakeMatrix, bakeNormal, flip );
|
|
571
1073
|
|
|
572
1074
|
}
|
|
573
1075
|
|
|
574
1076
|
// triangle extraction that stores directly in texture format
|
|
575
|
-
extractTrianglesInBatch( positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex ) {
|
|
1077
|
+
extractTrianglesInBatch( positions, normals, uvs, indices, triangleCount, materialIndex, meshIndex, bakeMatrix = null, bakeNormal = null, flipWinding = false ) {
|
|
576
1078
|
|
|
577
1079
|
// Track per-material triangle count for sort-bin remap (item 41)
|
|
578
1080
|
while ( this.materialTriangleCounts.length <= materialIndex ) this.materialTriangleCounts.push( 0 );
|
|
@@ -594,13 +1096,16 @@ export class GeometryExtractor {
|
|
|
594
1096
|
// Ensure capacity for this batch up front (single grow check per mesh)
|
|
595
1097
|
this._ensureCapacity( this.currentTriangleIndex + triangleCount );
|
|
596
1098
|
|
|
1099
|
+
const bm = bakeMatrix?.elements, bn = bakeNormal?.elements;
|
|
1100
|
+
|
|
597
1101
|
// Batch process triangles to avoid excessive function calls
|
|
598
1102
|
for ( let i = 0; i < triangleCount; i ++ ) {
|
|
599
1103
|
|
|
600
1104
|
const i3 = i * 3;
|
|
601
1105
|
const idxA = indices ? indices[ i3 + 0 ] : i3 + 0;
|
|
602
|
-
const
|
|
603
|
-
const
|
|
1106
|
+
const b = flipWinding ? 2 : 1, c = flipWinding ? 1 : 2;
|
|
1107
|
+
const idxB = indices ? indices[ i3 + b ] : i3 + b;
|
|
1108
|
+
const idxC = indices ? indices[ i3 + c ] : i3 + c;
|
|
604
1109
|
|
|
605
1110
|
this.getVertex( positions, idxA, posA );
|
|
606
1111
|
this.getVertex( positions, idxB, posB );
|
|
@@ -624,14 +1129,18 @@ export class GeometryExtractor {
|
|
|
624
1129
|
|
|
625
1130
|
}
|
|
626
1131
|
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
posC.applyMatrix4( this._matrixPool.mat4 );
|
|
1132
|
+
// Shared geometry stays in object space, its transform on the TLAS leaf, so every
|
|
1133
|
+
// placement reads one copy. Single-use geometry is baked to world here instead.
|
|
1134
|
+
if ( bakeMatrix ) {
|
|
631
1135
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1136
|
+
affinePoint( posA, bm ); affinePoint( posB, bm ); affinePoint( posC, bm );
|
|
1137
|
+
linearDir( normalA, bn ); linearDir( normalB, bn ); linearDir( normalC, bn );
|
|
1138
|
+
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
normalA.normalize();
|
|
1142
|
+
normalB.normalize();
|
|
1143
|
+
normalC.normalize();
|
|
635
1144
|
|
|
636
1145
|
// Pack triangle datas
|
|
637
1146
|
this.packTriangleDataTextureFormat(
|
|
@@ -649,82 +1158,48 @@ export class GeometryExtractor {
|
|
|
649
1158
|
|
|
650
1159
|
}
|
|
651
1160
|
|
|
652
|
-
// Pack triangle
|
|
1161
|
+
// Pack one triangle into its 5 uvec4 lanes (see TRIANGLE_DATA_LAYOUT).
|
|
653
1162
|
packTriangleDataTextureFormat( triangleIndex, posA, posB, posC, normalA, normalB, normalC, uvA, uvB, uvC, materialIndex, meshIndex ) {
|
|
654
1163
|
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 2 ] = posA.z;
|
|
661
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 3 ] = 0; // vec4 padding
|
|
662
|
-
|
|
663
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 0 ] = posB.x;
|
|
664
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 1 ] = posB.y;
|
|
665
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 2 ] = posB.z;
|
|
666
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 3 ] = 0; // vec4 padding
|
|
667
|
-
|
|
668
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 0 ] = posC.x;
|
|
669
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 1 ] = posC.y;
|
|
670
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 2 ] = posC.z;
|
|
671
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 3 ] = 0; // vec4 padding
|
|
672
|
-
|
|
673
|
-
// Normals as vec4s (3 vec4s = 12 floats)
|
|
674
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 0 ] = normalA.x;
|
|
675
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 1 ] = normalA.y;
|
|
676
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 2 ] = normalA.z;
|
|
677
|
-
// Repurposed padding: opaque-blocker fast-path flag for shadow rays.
|
|
678
|
-
// 1.0 = surface fully blocks light (no alpha, transmission, or transparency) →
|
|
679
|
-
// traceShadowRay can skip the 7-slot getShadowMaterial fetch.
|
|
680
|
-
// 0.0 = requires full material evaluation.
|
|
681
|
-
{
|
|
682
|
-
|
|
683
|
-
const mat = this.materials[ materialIndex ];
|
|
684
|
-
const isOpaqueBlocker = mat
|
|
685
|
-
&& ( mat.alphaMode | 0 ) === 0
|
|
686
|
-
&& ( mat.transparent | 0 ) === 0
|
|
687
|
-
&& ( mat.transmission || 0 ) === 0
|
|
688
|
-
&& ( mat.opacity ?? 1 ) >= 1
|
|
689
|
-
? 1.0 : 0.0;
|
|
690
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 3 ] = isOpaqueBlocker;
|
|
1164
|
+
const L = TRIANGLE_DATA_LAYOUT;
|
|
1165
|
+
// One chunk resolve per triangle; its 20 lanes are contiguous inside that chunk.
|
|
1166
|
+
const offset = this.triangles.baseOf( triangleIndex );
|
|
1167
|
+
const f = this.triangleFloatChunks.chunkFor( triangleIndex );
|
|
1168
|
+
const u = this.triangles.chunkFor( triangleIndex );
|
|
691
1169
|
|
|
692
|
-
|
|
1170
|
+
f[ offset + L.POSITION_A_OFFSET + 0 ] = posA.x;
|
|
1171
|
+
f[ offset + L.POSITION_A_OFFSET + 1 ] = posA.y;
|
|
1172
|
+
f[ offset + L.POSITION_A_OFFSET + 2 ] = posA.z;
|
|
1173
|
+
u[ offset + L.NORMAL_A_PACKED_OFFSET ] = packNormalOct( normalA.x, normalA.y, normalA.z );
|
|
1174
|
+
|
|
1175
|
+
f[ offset + L.POSITION_B_OFFSET + 0 ] = posB.x;
|
|
1176
|
+
f[ offset + L.POSITION_B_OFFSET + 1 ] = posB.y;
|
|
1177
|
+
f[ offset + L.POSITION_B_OFFSET + 2 ] = posB.z;
|
|
1178
|
+
u[ offset + L.NORMAL_B_PACKED_OFFSET ] = packNormalOct( normalB.x, normalB.y, normalB.z );
|
|
693
1179
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1180
|
+
f[ offset + L.POSITION_C_OFFSET + 0 ] = posC.x;
|
|
1181
|
+
f[ offset + L.POSITION_C_OFFSET + 1 ] = posC.y;
|
|
1182
|
+
f[ offset + L.POSITION_C_OFFSET + 2 ] = posC.z;
|
|
1183
|
+
u[ offset + L.NORMAL_C_PACKED_OFFSET ] = packNormalOct( normalC.x, normalC.y, normalC.z );
|
|
698
1184
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
// Lets BVH traversal do side culling without a material-buffer read per hit.
|
|
704
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 3 ] = this.materials[ materialIndex ]?.side ?? 0;
|
|
1185
|
+
f[ offset + L.UV_AB_OFFSET + 0 ] = uvA.x;
|
|
1186
|
+
f[ offset + L.UV_AB_OFFSET + 1 ] = uvA.y;
|
|
1187
|
+
f[ offset + L.UV_AB_OFFSET + 2 ] = uvB.x;
|
|
1188
|
+
f[ offset + L.UV_AB_OFFSET + 3 ] = uvB.y;
|
|
705
1189
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_AB_OFFSET + 0 ] = uvA.x;
|
|
709
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_AB_OFFSET + 1 ] = uvA.y;
|
|
710
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_AB_OFFSET + 2 ] = uvB.x;
|
|
711
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_AB_OFFSET + 3 ] = uvB.y;
|
|
1190
|
+
f[ offset + L.UV_C_OFFSET + 0 ] = uvC.x;
|
|
1191
|
+
f[ offset + L.UV_C_OFFSET + 1 ] = uvC.y;
|
|
712
1192
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 1 ] = uvC.y;
|
|
716
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 2 ] = materialIndex;
|
|
717
|
-
this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 3 ] = meshIndex; // Store mesh index
|
|
1193
|
+
u[ offset + L.MATERIAL_FLAGS_OFFSET ] = packTriangleFlags( materialIndex, this.materials[ materialIndex ] );
|
|
1194
|
+
u[ offset + L.MESH_INDEX_OFFSET ] = meshIndex;
|
|
718
1195
|
|
|
719
1196
|
}
|
|
720
1197
|
|
|
721
|
-
|
|
1198
|
+
/** The filled triangle records, as one chunk when they fit and several when they do not. */
|
|
722
1199
|
getTriangleData() {
|
|
723
1200
|
|
|
724
|
-
if ( ! this.
|
|
725
|
-
|
|
726
|
-
// Return only the used portion of the array
|
|
727
|
-
return this.triangleData.subarray( 0, this.currentTriangleIndex * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
|
|
1201
|
+
if ( ! this.triangles ) return null;
|
|
1202
|
+
return this.triangles.trimTo( this.currentTriangleIndex );
|
|
728
1203
|
|
|
729
1204
|
}
|
|
730
1205
|
|
|
@@ -819,7 +1294,10 @@ export class GeometryExtractor {
|
|
|
819
1294
|
this._droppedTextures = 0;
|
|
820
1295
|
|
|
821
1296
|
// Reset triangle data
|
|
1297
|
+
this.triangles = null;
|
|
822
1298
|
this.triangleData = null;
|
|
1299
|
+
this.triangleFloatChunks = null;
|
|
1300
|
+
this.triangleFloats = null;
|
|
823
1301
|
this.triangleCount = 0;
|
|
824
1302
|
this.currentTriangleIndex = 0;
|
|
825
1303
|
|
|
@@ -828,6 +1306,9 @@ export class GeometryExtractor {
|
|
|
828
1306
|
this.materialTriangleCounts = []; // Per-material triangle count (for sort-bin remap, item 41)
|
|
829
1307
|
this.meshes = [];
|
|
830
1308
|
this.meshTriangleRanges = []; // Per-mesh { start, count } for TLAS/BLAS
|
|
1309
|
+
this._geometryRanges = new Map(); // geometry+material -> the range that already holds it
|
|
1310
|
+
this.expandedTriangleCount = 0; // triangles as a per-mesh walk would count them
|
|
1311
|
+
this._allocatePlacements( 0 ); // SoA placements: sourceMesh column + 16 floats each
|
|
831
1312
|
this.maps = [];
|
|
832
1313
|
this.normalMaps = [];
|
|
833
1314
|
this.bumpMaps = [];
|
|
@@ -863,6 +1344,12 @@ export class GeometryExtractor {
|
|
|
863
1344
|
materialTriangleCounts: this.materialTriangleCounts,
|
|
864
1345
|
meshes: this.meshes,
|
|
865
1346
|
meshTriangleRanges: this.meshTriangleRanges, // Per-mesh { start, count } for TLAS/BLAS
|
|
1347
|
+
expandedTriangleCount: this.expandedTriangleCount,
|
|
1348
|
+
instanceSource: this.instanceSource.subarray( 0, this.instanceCount ),
|
|
1349
|
+
instanceMatrices: this.instanceMatrices.subarray( 0, this.instanceCount * 16 ),
|
|
1350
|
+
instanceCount: this.instanceCount,
|
|
1351
|
+
// meshIndex -> inverse of the pose baked into its triangles; only baked meshes appear.
|
|
1352
|
+
bakeInverse: this.bakeInverse,
|
|
866
1353
|
maps: this.maps,
|
|
867
1354
|
normalMaps: this.normalMaps,
|
|
868
1355
|
bumpMaps: this.bumpMaps,
|