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.
Files changed (53) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6728 -4735
  3. package/dist/rayzee.umd.js +121 -73
  4. package/package.json +2 -2
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +5 -0
  7. package/src/PathTracerApp.js +151 -25
  8. package/src/Processor/ArchiveReader.js +538 -0
  9. package/src/Processor/AssetLoader.js +265 -23
  10. package/src/Processor/BVHBuilder.js +48 -32
  11. package/src/Processor/BVHRefitter.js +244 -99
  12. package/src/Processor/ChunkedRecords.js +302 -0
  13. package/src/Processor/EmissiveTriangleBuilder.js +48 -21
  14. package/src/Processor/GeometryExtractor.js +580 -93
  15. package/src/Processor/HostMemory.js +222 -0
  16. package/src/Processor/InstanceTable.js +548 -97
  17. package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
  18. package/src/Processor/PBRT/PBRTCurves.js +249 -0
  19. package/src/Processor/PBRT/PBRTMath.js +19 -0
  20. package/src/Processor/PBRT/PBRTParser.js +187 -41
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
  22. package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
  23. package/src/Processor/PBRT/index.js +257 -50
  24. package/src/Processor/PackedRayBuffer.js +29 -18
  25. package/src/Processor/ParallelBVHBuilder.js +9 -8
  26. package/src/Processor/QueueManager.js +5 -3
  27. package/src/Processor/SceneProcessor.js +1046 -365
  28. package/src/Processor/TLASBuilder.js +546 -201
  29. package/src/Processor/TextureCreator.js +11 -9
  30. package/src/Processor/VRAMTracker.js +12 -3
  31. package/src/Processor/Workers/BVHRefitWorker.js +16 -11
  32. package/src/Processor/Workers/BVHWorker.js +12 -12
  33. package/src/Processor/Workers/TLASWorker.js +37 -0
  34. package/src/Processor/utils.js +8 -20
  35. package/src/Stages/NormalDepth.js +6 -2
  36. package/src/Stages/PathTracer.js +5 -5
  37. package/src/Stages/PathTracerStage.js +163 -41
  38. package/src/TSL/BVHTraversal.js +208 -90
  39. package/src/TSL/Common.js +135 -2
  40. package/src/TSL/Displacement.js +21 -11
  41. package/src/TSL/EmissiveSampling.js +49 -25
  42. package/src/TSL/ExtendKernel.js +3 -2
  43. package/src/TSL/LightBVHSampling.js +7 -4
  44. package/src/TSL/LightsDirect.js +30 -29
  45. package/src/TSL/ShadeKernel.js +8 -6
  46. package/src/TSL/SortGlobalKernels.js +3 -4
  47. package/src/TSL/Struct.js +3 -0
  48. package/src/TSL/TextureSampling.js +24 -9
  49. package/src/TSL/patches.js +149 -84
  50. package/src/index.js +3 -0
  51. package/src/managers/AnimationManager.js +86 -76
  52. package/src/managers/MaterialDataManager.js +29 -23
  53. package/src/managers/TransformManager.js +9 -192
@@ -0,0 +1,302 @@
1
+ /**
2
+ * ChunkedRecords — a logically contiguous array of fixed-size records, stored as several
3
+ * TypedArrays so it can exceed the ~2 GB cap V8 puts on a single ArrayBuffer.
4
+ *
5
+ * A GPU buffer may be 4 GB here while one JS array tops out near 2,040 MB, so the triangle and
6
+ * BVH buffers hit a CPU-side wall long before the GPU one. Splitting the CPU side lifts that
7
+ * without changing a single shader binding: the chunks are written into one GPU buffer in order.
8
+ *
9
+ * Chunk boundaries always fall on a record boundary, so a record never straddles two arrays.
10
+ * Callers resolve the chunk once per record and then index its lanes directly, which keeps the
11
+ * hot loops as fast as a flat array — one extra shift and mask per record, nothing per lane.
12
+ *
13
+ * One chunk is the overwhelmingly common case and is the fast path throughout: `single` returns
14
+ * the whole array, and callers that only work on flat data can keep using it.
15
+ */
16
+
17
+ /**
18
+ * Below this a scene stays single-chunk. Far under the 2,040 MB V8 cap on purpose: a big
19
+ * ArrayBuffer needs that much *contiguous* address space, so chunk size sets the usable total.
20
+ * Placed before the allocator gave up, fresh renderer: 256 MB chunks 5,120 MB · 64 MB 7,040 MB ·
21
+ * 32 MB 7,744 MB.
22
+ */
23
+ export const DEFAULT_CHUNK_BYTES = 64 * 1024 * 1024;
24
+
25
+ /** Whether a worker can be handed these arrays without copying them first. */
26
+ export const SHARED_MEMORY_AVAILABLE = typeof SharedArrayBuffer !== 'undefined';
27
+
28
+ /**
29
+ * Notified of every chunk allocated, so a build can attribute memory to the phase that spent it.
30
+ * At most one watcher, and it must not throw — this sits on the allocation path.
31
+ * @type {?function(number, boolean): void}
32
+ */
33
+ let chunkObserver = null;
34
+
35
+ /** Watch chunk allocations. Pass null to stop. @param {?function(number, boolean): void} fn */
36
+ export function setChunkObserver( fn ) {
37
+
38
+ chunkObserver = fn;
39
+
40
+ }
41
+
42
+ /** One chunk's storage, shared with workers when the page is cross-origin isolated. */
43
+ function allocChunk( LaneType, lanes, shared ) {
44
+
45
+ const chunk = shared
46
+ ? new LaneType( new SharedArrayBuffer( lanes * LaneType.BYTES_PER_ELEMENT ) )
47
+ : new LaneType( lanes );
48
+
49
+ if ( chunkObserver && chunk.byteLength > 0 ) chunkObserver( chunk.byteLength, shared );
50
+ return chunk;
51
+
52
+ }
53
+
54
+ export class ChunkedRecords {
55
+
56
+ /**
57
+ * @param {number} recordCount - total records
58
+ * @param {number} lanesPerRecord - elements per record (20 for a triangle, 16 for a BVH node)
59
+ * @param {Function} LaneType - typed array constructor for one lane
60
+ * @param {number} [maxBytesPerChunk]
61
+ * @param {boolean} [shared] - back the chunks with SharedArrayBuffer so a worker can read and
62
+ * write them in place. Without it a refit has to copy the whole store into shared memory,
63
+ * which is a second copy of the largest thing in the scene.
64
+ */
65
+ constructor( recordCount, lanesPerRecord, LaneType, maxBytesPerChunk = DEFAULT_CHUNK_BYTES, shared = false ) {
66
+
67
+ const recordBytes = lanesPerRecord * LaneType.BYTES_PER_ELEMENT;
68
+ const perChunk = Math.max( 1, Math.floor( maxBytesPerChunk / recordBytes ) );
69
+
70
+ this.recordCount = recordCount;
71
+ this.lanesPerRecord = lanesPerRecord;
72
+ this.LaneType = LaneType;
73
+ this.recordsPerChunk = recordCount <= perChunk ? Math.max( recordCount, 1 ) : perChunk;
74
+ this.shared = shared;
75
+
76
+ this.chunks = [];
77
+ let remaining = recordCount;
78
+ while ( remaining > 0 ) {
79
+
80
+ const n = Math.min( remaining, this.recordsPerChunk );
81
+ this.chunks.push( allocChunk( LaneType, n * lanesPerRecord, shared ) );
82
+ remaining -= n;
83
+
84
+ }
85
+
86
+ if ( this.chunks.length === 0 ) this.chunks.push( allocChunk( LaneType, 0, shared ) );
87
+
88
+ }
89
+
90
+ /** Adopt already-built chunks (e.g. handed over by the extractor) without copying. */
91
+ static adopt( chunks, recordCount, lanesPerRecord, recordsPerChunk ) {
92
+
93
+ const c = Object.create( ChunkedRecords.prototype );
94
+ c.recordCount = recordCount;
95
+ c.lanesPerRecord = lanesPerRecord;
96
+ c.LaneType = chunks[ 0 ]?.constructor ?? null;
97
+ c.recordsPerChunk = recordsPerChunk;
98
+ c.chunks = chunks;
99
+ c.shared = typeof SharedArrayBuffer !== 'undefined' && chunks[ 0 ]?.buffer instanceof SharedArrayBuffer;
100
+ return c;
101
+
102
+ }
103
+
104
+ /**
105
+ * Like the constructor, but a chunk is allocated the first time it is touched, so a fill that
106
+ * releases each source as it writes it never holds the whole source and destination at once.
107
+ */
108
+ static lazy( recordCount, lanesPerRecord, LaneType, maxBytesPerChunk = DEFAULT_CHUNK_BYTES, shared = false ) {
109
+
110
+ const perChunk = Math.max( 1, Math.floor( maxBytesPerChunk / ( lanesPerRecord * LaneType.BYTES_PER_ELEMENT ) ) );
111
+ const c = Object.create( ChunkedRecords.prototype );
112
+ c.recordCount = recordCount;
113
+ c.lanesPerRecord = lanesPerRecord;
114
+ c.LaneType = LaneType;
115
+ c.recordsPerChunk = recordCount <= perChunk ? Math.max( recordCount, 1 ) : perChunk;
116
+ c.shared = shared;
117
+ c.chunks = new Array( Math.max( 1, Math.ceil( recordCount / c.recordsPerChunk ) ) );
118
+ c._views = [];
119
+ return c;
120
+
121
+ }
122
+
123
+ /** Allocate chunk `k`, plus the matching chunk of every view taken over this storage. @private */
124
+ _materialize( k ) {
125
+
126
+ if ( this._owner ) {
127
+
128
+ this._owner._materialize( k );
129
+ return this.chunks[ k ];
130
+
131
+ }
132
+
133
+ const from = k * this.recordsPerChunk;
134
+ const lanes = Math.min( this.recordsPerChunk, this.recordCount - from ) * this.lanesPerRecord;
135
+ const chunk = this.chunks[ k ] = allocChunk( this.LaneType, lanes, this.shared );
136
+
137
+ for ( const v of this._views ) {
138
+
139
+ v.chunks[ k ] = new v.LaneType( chunk.buffer, chunk.byteOffset, chunk.byteLength / v.LaneType.BYTES_PER_ELEMENT );
140
+
141
+ }
142
+
143
+ return chunk;
144
+
145
+ }
146
+
147
+ /** Allocate whatever a lazy fill never touched, so the result behaves like an eager one. */
148
+ materializeAll() {
149
+
150
+ for ( let k = 0; k < this.chunks.length; k ++ ) if ( ! this.chunks[ k ] ) this._materialize( k );
151
+ return this;
152
+
153
+ }
154
+
155
+ get chunkCount() {
156
+
157
+ return this.chunks.length;
158
+
159
+ }
160
+
161
+ /** The whole thing as one array when it fits in one, else null. */
162
+ get single() {
163
+
164
+ return this.chunks.length === 1 ? this.chunks[ 0 ] : null;
165
+
166
+ }
167
+
168
+ get byteLength() {
169
+
170
+ let n = 0;
171
+ for ( const c of this.chunks ) if ( c ) n += c.byteLength;
172
+ return n;
173
+
174
+ }
175
+
176
+ /** The array holding `record`. */
177
+ chunkFor( record ) {
178
+
179
+ const k = ( record / this.recordsPerChunk ) | 0;
180
+ return this.chunks[ k ] ?? this._materialize( k );
181
+
182
+ }
183
+
184
+ /** Lane offset of `record` within its own chunk. */
185
+ baseOf( record ) {
186
+
187
+ return ( record % this.recordsPerChunk ) * this.lanesPerRecord;
188
+
189
+ }
190
+
191
+ /** True when records [start, start+count) sit inside one chunk. */
192
+ isContiguous( start, count ) {
193
+
194
+ if ( count <= 0 ) return true;
195
+ return ( ( start / this.recordsPerChunk ) | 0 ) === ( ( ( start + count - 1 ) / this.recordsPerChunk ) | 0 );
196
+
197
+ }
198
+
199
+ /**
200
+ * A view over records [start, start+count) when they share a chunk, otherwise a copy.
201
+ * Callers that only read may use it freely; callers that write must use {@link setRecords}.
202
+ */
203
+ slice( start, count ) {
204
+
205
+ const base = this.baseOf( start );
206
+ const lanes = count * this.lanesPerRecord;
207
+
208
+ if ( this.isContiguous( start, count ) ) return this.chunkFor( start ).subarray( base, base + lanes );
209
+
210
+ const out = new this.LaneType( lanes );
211
+ this.readRecords( start, count, out );
212
+ return out;
213
+
214
+ }
215
+
216
+ /** Copy records [start, start+count) into `out`. */
217
+ readRecords( start, count, out ) {
218
+
219
+ let done = 0;
220
+ while ( done < count ) {
221
+
222
+ const rec = start + done;
223
+ const chunk = this.chunkFor( rec );
224
+ const base = this.baseOf( rec );
225
+ const room = this.recordsPerChunk - ( rec % this.recordsPerChunk );
226
+ const n = Math.min( room, count - done );
227
+ out.set( chunk.subarray( base, base + n * this.lanesPerRecord ), done * this.lanesPerRecord );
228
+ done += n;
229
+
230
+ }
231
+
232
+ return out;
233
+
234
+ }
235
+
236
+ /** Write `src` (whole records) starting at record `start`, spanning chunks as needed. */
237
+ setRecords( start, src ) {
238
+
239
+ const count = src.length / this.lanesPerRecord;
240
+ let done = 0;
241
+ while ( done < count ) {
242
+
243
+ const rec = start + done;
244
+ const chunk = this.chunkFor( rec );
245
+ const base = this.baseOf( rec );
246
+ const room = this.recordsPerChunk - ( rec % this.recordsPerChunk );
247
+ const n = Math.min( room, count - done );
248
+ chunk.set(
249
+ src.subarray( done * this.lanesPerRecord, ( done + n ) * this.lanesPerRecord ),
250
+ base
251
+ );
252
+ done += n;
253
+
254
+ }
255
+
256
+ }
257
+
258
+ /** A fresh copy of records [start, start+count). Always a copy, so it is safe to transfer. */
259
+ copyOf( start, count ) {
260
+
261
+ return this.readRecords( start, count, new this.LaneType( count * this.lanesPerRecord ) );
262
+
263
+ }
264
+
265
+ /**
266
+ * The same storage narrowed to the first `recordCount` records: whole unused chunks are
267
+ * dropped and the last one is trimmed, so the upload writes exactly what was filled.
268
+ */
269
+ trimTo( recordCount ) {
270
+
271
+ const needed = Math.max( 1, Math.ceil( recordCount / this.recordsPerChunk ) );
272
+ const chunks = this.chunks.slice( 0, needed );
273
+ const lanes = ( recordCount - ( needed - 1 ) * this.recordsPerChunk ) * this.lanesPerRecord;
274
+ const last = chunks[ needed - 1 ];
275
+ if ( last.length > lanes ) chunks[ needed - 1 ] = last.subarray( 0, Math.max( 0, lanes ) );
276
+
277
+ return ChunkedRecords.adopt( chunks, recordCount, this.lanesPerRecord, this.recordsPerChunk );
278
+
279
+ }
280
+
281
+ /** A parallel set of views of another lane type over the same memory (u32 records read as f32). */
282
+ viewAs( LaneType ) {
283
+
284
+ const wrap = c => new LaneType( c.buffer, c.byteOffset, c.byteLength / LaneType.BYTES_PER_ELEMENT );
285
+ const v = ChunkedRecords.adopt(
286
+ this.chunks.map( c => ( c ? wrap( c ) : undefined ) ),
287
+ this.recordCount, this.lanesPerRecord, this.recordsPerChunk
288
+ );
289
+ v.LaneType = LaneType;
290
+
291
+ if ( this._views ) {
292
+
293
+ v._owner = this;
294
+ this._views.push( v );
295
+
296
+ }
297
+
298
+ return v;
299
+
300
+ }
301
+
302
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { DataTexture, RGBAFormat, FloatType, NearestFilter } from 'three';
10
- import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
10
+ import { TRIANGLE_DATA_LAYOUT, TRI_MATERIAL_MASK } from '../EngineDefaults.js';
11
11
  import { LightBVHBuilder } from './LightBVHBuilder.js';
12
12
  import { createLogger, fmt } from '../utils/Logger.js';
13
13
 
@@ -38,25 +38,37 @@ export class EmissiveTriangleBuilder {
38
38
 
39
39
  /**
40
40
  * Extract emissive triangles from processed geometry
41
- * @param {Array} triangleData - Flat array of triangle data
41
+ * @param {Uint32Array} triangleData - Packed triangle records
42
42
  * @param {Array} materials - Array of material objects
43
43
  * @param {number} triangleCount - Total number of triangles
44
+ * @param {import('./InstanceTable.js').InstanceTable} [table] - per placement; triangles are
45
+ * stored in object space, so power, bounds and the emission cone all have to be
46
+ * measured after the instance transform. One record per triangle, so a template
47
+ * placed more than once is lit by its first placement — the extractor expands an
48
+ * emissive instanced mesh into per-instance triangles to avoid exactly that.
44
49
  */
45
- extractEmissiveTriangles( triangleData, materials, triangleCount ) {
50
+ extractEmissiveTriangles( triangleData, materials, triangleCount, table = null ) {
46
51
 
47
52
  this.emissiveTriangles = [];
48
53
  this.totalEmissivePower = 0;
49
54
  this._totalTriangleCount = triangleCount;
50
55
 
51
56
  const FLOATS_PER_TRIANGLE = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
52
- const MATERIAL_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 2; // materialIndex within vec4
53
- const MESH_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 3; // meshIndex within vec4
57
+ const MATERIAL_FLAGS_OFFSET = TRIANGLE_DATA_LAYOUT.MATERIAL_FLAGS_OFFSET;
58
+ const MESH_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.MESH_INDEX_OFFSET;
59
+ // Flat array or ChunkedRecords; the chunk is resolved once per triangle.
60
+ const chunked = triangleData && triangleData.chunks ? triangleData : null;
61
+ const chunkedF = chunked ? chunked.viewAs( Float32Array ) : null;
62
+ const flatU = chunked ? null : triangleData;
63
+ const flatF = chunked ? null : new Float32Array( triangleData.buffer, triangleData.byteOffset, triangleData.length );
54
64
 
55
65
  for ( let i = 0; i < triangleCount; i ++ ) {
56
66
 
57
- const baseOffset = i * FLOATS_PER_TRIANGLE;
58
- const materialIndex = Math.floor( triangleData[ baseOffset + MATERIAL_INDEX_OFFSET ] );
59
- const meshIndex = Math.floor( triangleData[ baseOffset + MESH_INDEX_OFFSET ] );
67
+ const triU = chunked ? chunked.chunkFor( i ) : flatU;
68
+ const triFloats = chunked ? chunkedF.chunkFor( i ) : flatF;
69
+ const baseOffset = chunked ? chunked.baseOf( i ) : i * FLOATS_PER_TRIANGLE;
70
+ const materialIndex = triU[ baseOffset + MATERIAL_FLAGS_OFFSET ] & TRI_MATERIAL_MASK;
71
+ const meshIndex = triU[ baseOffset + MESH_INDEX_OFFSET ];
60
72
 
61
73
  // Get material
62
74
  const material = materials[ materialIndex ];
@@ -73,16 +85,25 @@ export class EmissiveTriangleBuilder {
73
85
  if ( isEmissive ) {
74
86
 
75
87
  // Calculate triangle area for power weighting
76
- // Positions are at offsets 0-11
77
- const v0x = triangleData[ baseOffset + 0 ];
78
- const v0y = triangleData[ baseOffset + 1 ];
79
- const v0z = triangleData[ baseOffset + 2 ];
80
- const v1x = triangleData[ baseOffset + 4 ];
81
- const v1y = triangleData[ baseOffset + 5 ];
82
- const v1z = triangleData[ baseOffset + 6 ];
83
- const v2x = triangleData[ baseOffset + 8 ];
84
- const v2y = triangleData[ baseOffset + 9 ];
85
- const v2z = triangleData[ baseOffset + 10 ];
88
+ // Positions are at offsets 0-11, in the instance's own space
89
+ // `meshIndex` names the template the triangle belongs to, never a row of the
90
+ // per-placement table: indexing the table with it put one object's matrix on
91
+ // another object's light as soon as any instanced mesh came earlier.
92
+ const placement = table?.placementRunOf?.( meshIndex )?.start ?? - 1;
93
+ const hasInstance = placement >= 0 && table.isSet[ placement ];
94
+ const m = hasInstance ? table.world : null;
95
+ const mo = hasInstance ? placement * 16 : 0;
96
+ const px = ( x, y, z ) => ( m ? m[ mo ] * x + m[ mo + 4 ] * y + m[ mo + 8 ] * z + m[ mo + 12 ] : x );
97
+ const py = ( x, y, z ) => ( m ? m[ mo + 1 ] * x + m[ mo + 5 ] * y + m[ mo + 9 ] * z + m[ mo + 13 ] : y );
98
+ const pz = ( x, y, z ) => ( m ? m[ mo + 2 ] * x + m[ mo + 6 ] * y + m[ mo + 10 ] * z + m[ mo + 14 ] : z );
99
+
100
+ const o0x = triFloats[ baseOffset + 0 ], o0y = triFloats[ baseOffset + 1 ], o0z = triFloats[ baseOffset + 2 ];
101
+ const o1x = triFloats[ baseOffset + 4 ], o1y = triFloats[ baseOffset + 5 ], o1z = triFloats[ baseOffset + 6 ];
102
+ const o2x = triFloats[ baseOffset + 8 ], o2y = triFloats[ baseOffset + 9 ], o2z = triFloats[ baseOffset + 10 ];
103
+
104
+ const v0x = px( o0x, o0y, o0z ), v0y = py( o0x, o0y, o0z ), v0z = pz( o0x, o0y, o0z );
105
+ const v1x = px( o1x, o1y, o1z ), v1y = py( o1x, o1y, o1z ), v1z = pz( o1x, o1y, o1z );
106
+ const v2x = px( o2x, o2y, o2z ), v2y = py( o2x, o2y, o2z ), v2z = pz( o2x, o2y, o2z );
86
107
 
87
108
  const area = this._calculateTriangleArea( v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z );
88
109
 
@@ -119,6 +140,7 @@ export class EmissiveTriangleBuilder {
119
140
  triangleIndex: i,
120
141
  materialIndex: materialIndex,
121
142
  meshIndex: meshIndex,
143
+ instanceLeaf: hasInstance ? table.tlasLeafIndex[ placement ] : - 1,
122
144
  power: power,
123
145
  area: area,
124
146
  emissive: { r: emissive.r, g: emissive.g, b: emissive.b },
@@ -369,7 +391,10 @@ export class EmissiveTriangleBuilder {
369
391
  data[ offset + 0 ] = tri.triangleIndex;
370
392
  data[ offset + 1 ] = tri.power;
371
393
  data[ offset + 2 ] = this.cdfArray[ i ];
372
- data[ offset + 3 ] = this.totalEmissivePower > 0 ? tri.power / this.totalEmissivePower : 0;
394
+ // Slot 3 used to repeat power/total, which the shader recomputes anyway. It now
395
+ // names the TLAS leaf that owns this emitter, so sampling can put the triangle
396
+ // back into world space.
397
+ data[ offset + 3 ] = tri.instanceLeaf;
373
398
 
374
399
  // vec4[1]: pre-multiplied emission (emissive * intensity), area
375
400
  data[ offset + 4 ] = tri.emissive.r * tri.emissiveIntensity;
@@ -600,11 +625,13 @@ export class EmissiveTriangleBuilder {
600
625
  const tri = tris[ origIdx ];
601
626
  const offset = i * 8;
602
627
 
603
- // vec4[0]: triangleIndex, power, cdf, selectionPdf
628
+ // vec4[0]: triangleIndex, power, cdf, instance. This is the authoritative writer —
629
+ // buildLightBVH re-sorts the entries and replaces emissiveTriangleData, so the
630
+ // instance has to be carried here too, not only in createEmissiveRawData.
604
631
  data[ offset + 0 ] = tri.triangleIndex;
605
632
  data[ offset + 1 ] = tri.power;
606
633
  data[ offset + 2 ] = this.cdfArray[ i ];
607
- data[ offset + 3 ] = this.totalEmissivePower > 0 ? tri.power / this.totalEmissivePower : 0;
634
+ data[ offset + 3 ] = tri.instanceLeaf;
608
635
 
609
636
  // vec4[1]: pre-multiplied emission (emissive * intensity), area
610
637
  data[ offset + 4 ] = tri.emissive.r * tri.emissiveIntensity;