rayzee 7.2.0 → 7.2.1
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 +1 -1
- package/dist/rayzee.es.js +857 -728
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +20 -20
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +6 -1
- package/src/Processor/EmissiveTriangleBuilder.js +39 -5
- package/src/Processor/LightBVHBuilder.js +113 -27
- package/src/Processor/SceneProcessor.js +19 -1
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +14 -3
- package/src/TSL/EmissiveSampling.js +3 -2
- package/src/TSL/LightBVHSampling.js +216 -25
- package/src/TSL/ShadeKernel.js +21 -6
- package/src/managers/UniformManager.js +4 -0
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -1190,8 +1190,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1190
1190
|
if ( result ) {
|
|
1191
1191
|
|
|
1192
1192
|
this.stages.pathTracer.setEmissiveTriangleData(
|
|
1193
|
-
result.rawData, result.emissiveCount, result.totalPower,
|
|
1193
|
+
result.rawData, result.emissiveCount, result.totalPower, result.bitTrailMap,
|
|
1194
1194
|
);
|
|
1195
|
+
if ( result.lightBVHNodeData ) {
|
|
1196
|
+
|
|
1197
|
+
this.stages.pathTracer.setLightBVHData( result.lightBVHNodeData, result.lightBVHNodeCount );
|
|
1198
|
+
|
|
1199
|
+
}
|
|
1195
1200
|
|
|
1196
1201
|
}
|
|
1197
1202
|
|
|
@@ -22,6 +22,10 @@ export class EmissiveTriangleBuilder {
|
|
|
22
22
|
this.cdfArray = null;
|
|
23
23
|
this.lightBVHNodeData = null;
|
|
24
24
|
this.lightBVHNodeCount = 0;
|
|
25
|
+
// Per-triangle bit-trail (root→leaf path through the Light BVH), indexed by absolute
|
|
26
|
+
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
27
|
+
this.emissiveBitTrailMap = null;
|
|
28
|
+
this._totalTriangleCount = 0;
|
|
25
29
|
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -37,6 +41,7 @@ export class EmissiveTriangleBuilder {
|
|
|
37
41
|
|
|
38
42
|
this.emissiveTriangles = [];
|
|
39
43
|
this.totalEmissivePower = 0;
|
|
44
|
+
this._totalTriangleCount = triangleCount;
|
|
40
45
|
|
|
41
46
|
const FLOATS_PER_TRIANGLE = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
42
47
|
const MATERIAL_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 2; // materialIndex within vec4
|
|
@@ -74,9 +79,21 @@ export class EmissiveTriangleBuilder {
|
|
|
74
79
|
|
|
75
80
|
const area = this._calculateTriangleArea( v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z );
|
|
76
81
|
|
|
77
|
-
// Calculate emissive power (luminance * intensity * area)
|
|
78
|
-
|
|
79
|
-
const
|
|
82
|
+
// Calculate emissive power (Rec.709 luminance * intensity * area) — must match the
|
|
83
|
+
// shader's calculateEmissiveLightPdf luma weighting for MIS consistency.
|
|
84
|
+
const luma = 0.2126 * emissive.r + 0.7152 * emissive.g + 0.0722 * emissive.b;
|
|
85
|
+
const power = luma * emissiveIntensity * area;
|
|
86
|
+
|
|
87
|
+
// Geometric normal (emission cone axis). BackSide flips it; DoubleSide → two-sided.
|
|
88
|
+
let nx = ( v1y - v0y ) * ( v2z - v0z ) - ( v1z - v0z ) * ( v2y - v0y );
|
|
89
|
+
let ny = ( v1z - v0z ) * ( v2x - v0x ) - ( v1x - v0x ) * ( v2z - v0z );
|
|
90
|
+
let nz = ( v1x - v0x ) * ( v2y - v0y ) - ( v1y - v0y ) * ( v2x - v0x );
|
|
91
|
+
const nl = Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1;
|
|
92
|
+
const sideSign = material.side === 1 ? - 1 : 1; // THREE.BackSide flips emission normal
|
|
93
|
+
nx = nx / nl * sideSign;
|
|
94
|
+
ny = ny / nl * sideSign;
|
|
95
|
+
nz = nz / nl * sideSign;
|
|
96
|
+
const twoSided = material.side === 2; // THREE.DoubleSide
|
|
80
97
|
|
|
81
98
|
// Centroid for BVH split decisions
|
|
82
99
|
const cx = ( v0x + v1x + v2x ) / 3;
|
|
@@ -100,6 +117,7 @@ export class EmissiveTriangleBuilder {
|
|
|
100
117
|
emissiveIntensity: emissiveIntensity,
|
|
101
118
|
cx, cy, cz,
|
|
102
119
|
bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ,
|
|
120
|
+
nx, ny, nz, twoSided,
|
|
103
121
|
} );
|
|
104
122
|
|
|
105
123
|
this.totalEmissivePower += power;
|
|
@@ -459,22 +477,34 @@ export class EmissiveTriangleBuilder {
|
|
|
459
477
|
|
|
460
478
|
if ( this.emissiveCount === 0 ) {
|
|
461
479
|
|
|
462
|
-
// Dummy single leaf node
|
|
480
|
+
// Dummy single leaf node (whole-sphere cone)
|
|
463
481
|
this.lightBVHNodeData = new Float32Array( 16 );
|
|
464
482
|
this.lightBVHNodeData[ 7 ] = 1.0; // isLeaf
|
|
483
|
+
this.lightBVHNodeData[ 14 ] = 1.0; // cone axis z
|
|
484
|
+
this.lightBVHNodeData[ 15 ] = - 1.0; // cosThetaO = whole sphere
|
|
465
485
|
this.lightBVHNodeCount = 1;
|
|
486
|
+
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
466
487
|
return 1;
|
|
467
488
|
|
|
468
489
|
}
|
|
469
490
|
|
|
470
491
|
const builder = new LightBVHBuilder();
|
|
471
|
-
const { nodeData, nodeCount, sortedPerm } = builder.build( this.emissiveTriangles );
|
|
492
|
+
const { nodeData, nodeCount, sortedPerm, bitTrails } = builder.build( this.emissiveTriangles );
|
|
472
493
|
this.lightBVHNodeData = nodeData;
|
|
473
494
|
this.lightBVHNodeCount = nodeCount;
|
|
474
495
|
|
|
475
496
|
// Rebuild emissive raw data in sorted leaf order so node start/count refs are valid
|
|
476
497
|
this._rebuildSortedEmissiveData( sortedPerm );
|
|
477
498
|
|
|
499
|
+
// Build the per-triangle bit-trail map (indexed by absolute triangleIndex; -1 = non-emissive)
|
|
500
|
+
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
501
|
+
for ( let i = 0; i < sortedPerm.length; i ++ ) {
|
|
502
|
+
|
|
503
|
+
const triIndex = this.emissiveTriangles[ sortedPerm[ i ] ].triangleIndex;
|
|
504
|
+
this.emissiveBitTrailMap[ triIndex ] = bitTrails[ i ];
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
478
508
|
return nodeCount;
|
|
479
509
|
|
|
480
510
|
}
|
|
@@ -544,6 +574,10 @@ export class EmissiveTriangleBuilder {
|
|
|
544
574
|
this.cdfArray = null;
|
|
545
575
|
this.lightBVHNodeData = null;
|
|
546
576
|
this.lightBVHNodeCount = 0;
|
|
577
|
+
// Per-triangle bit-trail (root→leaf path through the Light BVH), indexed by absolute
|
|
578
|
+
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
579
|
+
this.emissiveBitTrailMap = null;
|
|
580
|
+
this._totalTriangleCount = 0;
|
|
547
581
|
|
|
548
582
|
}
|
|
549
583
|
|
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
* CPU-side BVH builder over emissive triangles for spatially-aware light sampling.
|
|
5
5
|
* Each node occupies 4 vec4s (16 floats, BVH_STRIDE=4):
|
|
6
6
|
*
|
|
7
|
-
* vec4[0]: [aabb.minX, aabb.minY, aabb.minZ, totalPower]
|
|
8
|
-
* vec4[1]: [aabb.maxX, aabb.maxY, aabb.maxZ, isLeaf]
|
|
7
|
+
* vec4[0]: [aabb.minX, aabb.minY, aabb.minZ, totalPower] // power = Rec.709 luma-weighted
|
|
8
|
+
* vec4[1]: [aabb.maxX, aabb.maxY, aabb.maxZ, isLeaf] // isLeaf: 0.0=inner, 1.0=leaf
|
|
9
9
|
* vec4[2]: inner → [leftChildIdx, rightChildIdx, 0, 0]
|
|
10
10
|
* leaf → [emissiveStart, emissiveCount, 0, 0]
|
|
11
|
-
* vec4[3]: [
|
|
11
|
+
* vec4[3]: [coneAxisX, coneAxisY, coneAxisZ, cosThetaO] // emission orientation cone (Conty-Kulla)
|
|
12
|
+
*
|
|
13
|
+
* The orientation cone bounds the emission directions of the cluster (θ_o = spread of emitter
|
|
14
|
+
* normals; θ_e = π/2 assumed for diffuse emitters, applied at sample time). cosThetaO = -1 means
|
|
15
|
+
* "whole sphere" (mixed/two-sided cluster → never culled by orientation).
|
|
12
16
|
*
|
|
13
17
|
* Build algorithm: median split on longest centroid AABB axis, maxLeafSize=8.
|
|
14
|
-
* Output: pre-order flattened array with
|
|
18
|
+
* Output: pre-order flattened array with left child immediately after parent.
|
|
19
|
+
* Also emits a per-(sorted)-triangle bit-trail: the sequence of left(0)/right(1) child choices
|
|
20
|
+
* from the root to that triangle's leaf, so the GPU can re-walk the exact descent for MIS.
|
|
15
21
|
*/
|
|
16
22
|
export class LightBVHBuilder {
|
|
17
23
|
|
|
@@ -26,19 +32,22 @@ export class LightBVHBuilder {
|
|
|
26
32
|
*
|
|
27
33
|
* @param {Array} emissiveTriangles - Array of objects:
|
|
28
34
|
* { triangleIndex, power, area, emissive, emissiveIntensity, cx, cy, cz,
|
|
29
|
-
* bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ }
|
|
30
|
-
* @returns {{ nodeData: Float32Array, nodeCount: number, sortedPerm: Int32Array }}
|
|
35
|
+
* bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ, nx, ny, nz, twoSided }
|
|
36
|
+
* @returns {{ nodeData: Float32Array, nodeCount: number, sortedPerm: Int32Array, bitTrails: Float32Array }}
|
|
31
37
|
* sortedPerm[i] = original index in emissiveTriangles for position i in sorted leaf order
|
|
38
|
+
* bitTrails[i] = root→leaf bit-trail for the triangle now at sorted position i (as a float-encoded int)
|
|
32
39
|
*/
|
|
33
40
|
build( emissiveTriangles ) {
|
|
34
41
|
|
|
35
42
|
const n = emissiveTriangles.length;
|
|
36
43
|
if ( n === 0 ) {
|
|
37
44
|
|
|
38
|
-
// Dummy leaf node
|
|
45
|
+
// Dummy leaf node — whole-sphere cone so importance never culls it
|
|
39
46
|
const nodeData = new Float32Array( 16 );
|
|
40
47
|
nodeData[ 7 ] = 1.0; // isLeaf
|
|
41
|
-
|
|
48
|
+
nodeData[ 14 ] = 1.0; // cone axis z
|
|
49
|
+
nodeData[ 15 ] = - 1.0; // cosThetaO = whole sphere
|
|
50
|
+
return { nodeData, nodeCount: 1, sortedPerm: new Int32Array( 0 ), bitTrails: new Float32Array( 0 ) };
|
|
42
51
|
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -52,8 +61,11 @@ export class LightBVHBuilder {
|
|
|
52
61
|
const nodeData = new Float32Array( maxNodes * 16 );
|
|
53
62
|
let nodeCount = 0;
|
|
54
63
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
64
|
+
// bit-trail per sorted position (filled at leaves). 24-bit safe → tree depth < 24 (≈16M leaf clusters).
|
|
65
|
+
const bitTrails = new Float32Array( n );
|
|
66
|
+
|
|
67
|
+
// Recursively build; returns { nodeIndex, cone }. `trail`/`depth` accumulate the root→leaf path.
|
|
68
|
+
const buildRecursive = ( start, end, trail, depth ) => {
|
|
57
69
|
|
|
58
70
|
const nodeIndex = nodeCount ++;
|
|
59
71
|
const nodeOffset = nodeIndex * 16;
|
|
@@ -95,16 +107,11 @@ export class LightBVHBuilder {
|
|
|
95
107
|
nodeData[ nodeOffset + 4 ] = maxX;
|
|
96
108
|
nodeData[ nodeOffset + 5 ] = maxY;
|
|
97
109
|
nodeData[ nodeOffset + 6 ] = maxZ;
|
|
98
|
-
// nodeData[nodeOffset + 7] = isLeaf — set below
|
|
99
|
-
|
|
100
|
-
// vec4[3]: zeros (reserved)
|
|
101
|
-
nodeData[ nodeOffset + 12 ] = 0;
|
|
102
|
-
nodeData[ nodeOffset + 13 ] = 0;
|
|
103
|
-
nodeData[ nodeOffset + 14 ] = 0;
|
|
104
|
-
nodeData[ nodeOffset + 15 ] = 0;
|
|
105
110
|
|
|
106
111
|
const count = end - start;
|
|
107
112
|
|
|
113
|
+
let cone;
|
|
114
|
+
|
|
108
115
|
if ( count <= this.maxLeafSize ) {
|
|
109
116
|
|
|
110
117
|
// LEAF NODE
|
|
@@ -114,6 +121,18 @@ export class LightBVHBuilder {
|
|
|
114
121
|
nodeData[ nodeOffset + 10 ] = 0;
|
|
115
122
|
nodeData[ nodeOffset + 11 ] = 0;
|
|
116
123
|
|
|
124
|
+
// Cone = union of this leaf's triangle emission cones; write this leaf's bit-trail
|
|
125
|
+
cone = null;
|
|
126
|
+
for ( let i = start; i < end; i ++ ) {
|
|
127
|
+
|
|
128
|
+
const tri = emissiveTriangles[ indices[ i ] ];
|
|
129
|
+
cone = coneUnion( cone, triangleCone( tri ) );
|
|
130
|
+
bitTrails[ i ] = trail;
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if ( ! cone ) cone = { ax: 0, ay: 0, az: 1, cosO: - 1 };
|
|
135
|
+
|
|
117
136
|
} else {
|
|
118
137
|
|
|
119
138
|
// INNER NODE — find longest centroid axis and split at median
|
|
@@ -135,24 +154,31 @@ export class LightBVHBuilder {
|
|
|
135
154
|
|
|
136
155
|
nodeData[ nodeOffset + 7 ] = 0.0; // isLeaf = false (inner)
|
|
137
156
|
|
|
138
|
-
// Build
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const rightChildIdx = buildRecursive( mid, end );
|
|
157
|
+
// Build left child immediately after this node (pre-order), then right.
|
|
158
|
+
// Trail bit at `depth`: 0 for left, 1 for right.
|
|
159
|
+
const left = buildRecursive( start, mid, trail, depth + 1 );
|
|
160
|
+
const right = buildRecursive( mid, end, trail + Math.pow( 2, depth ), depth + 1 );
|
|
143
161
|
|
|
144
|
-
nodeData[ nodeOffset + 8 ] =
|
|
145
|
-
nodeData[ nodeOffset + 9 ] =
|
|
162
|
+
nodeData[ nodeOffset + 8 ] = left.nodeIndex;
|
|
163
|
+
nodeData[ nodeOffset + 9 ] = right.nodeIndex;
|
|
146
164
|
nodeData[ nodeOffset + 10 ] = 0;
|
|
147
165
|
nodeData[ nodeOffset + 11 ] = 0;
|
|
148
166
|
|
|
167
|
+
cone = coneUnion( left.cone, right.cone );
|
|
168
|
+
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
|
|
171
|
+
// vec4[3]: [coneAxisX, coneAxisY, coneAxisZ, cosThetaO]
|
|
172
|
+
nodeData[ nodeOffset + 12 ] = cone.ax;
|
|
173
|
+
nodeData[ nodeOffset + 13 ] = cone.ay;
|
|
174
|
+
nodeData[ nodeOffset + 14 ] = cone.az;
|
|
175
|
+
nodeData[ nodeOffset + 15 ] = cone.cosO;
|
|
176
|
+
|
|
177
|
+
return { nodeIndex, cone };
|
|
152
178
|
|
|
153
179
|
};
|
|
154
180
|
|
|
155
|
-
buildRecursive( 0, n );
|
|
181
|
+
buildRecursive( 0, n, 0, 0 );
|
|
156
182
|
|
|
157
183
|
// sortedPerm: the rearranged indices array (leaf order)
|
|
158
184
|
const sortedPerm = new Int32Array( n );
|
|
@@ -164,7 +190,7 @@ export class LightBVHBuilder {
|
|
|
164
190
|
|
|
165
191
|
console.log( `[LightBVHBuilder] Built BVH: ${nodeCount} nodes for ${n} emissive triangles` );
|
|
166
192
|
|
|
167
|
-
return { nodeData: trimmedData, nodeCount, sortedPerm };
|
|
193
|
+
return { nodeData: trimmedData, nodeCount, sortedPerm, bitTrails };
|
|
168
194
|
|
|
169
195
|
}
|
|
170
196
|
|
|
@@ -217,3 +243,63 @@ export class LightBVHBuilder {
|
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
}
|
|
246
|
+
|
|
247
|
+
// ================================================================================
|
|
248
|
+
// ORIENTATION CONE HELPERS (Conty-Estevez & Kulla 2018 / PBRT-v4 DirectionCone)
|
|
249
|
+
// ================================================================================
|
|
250
|
+
|
|
251
|
+
// Per-triangle emission cone: axis = geometric normal, θ_o = 0 (single direction).
|
|
252
|
+
// Two-sided emitters emit into both hemispheres → whole sphere (cosO = -1).
|
|
253
|
+
function triangleCone( tri ) {
|
|
254
|
+
|
|
255
|
+
if ( tri.twoSided ) return { ax: tri.nx, ay: tri.ny, az: tri.nz, cosO: - 1 };
|
|
256
|
+
return { ax: tri.nx, ay: tri.ny, az: tri.nz, cosO: 1 };
|
|
257
|
+
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Union of two direction cones (PBRT-v4 DirectionCone::Union). cosO === -1 ⇒ whole sphere.
|
|
261
|
+
function coneUnion( a, b ) {
|
|
262
|
+
|
|
263
|
+
if ( ! a ) return b;
|
|
264
|
+
if ( ! b ) return a;
|
|
265
|
+
if ( a.cosO <= - 1 ) return a; // a is already whole sphere
|
|
266
|
+
if ( b.cosO <= - 1 ) return b;
|
|
267
|
+
|
|
268
|
+
const cosA = Math.min( Math.max( a.cosO, - 1 ), 1 );
|
|
269
|
+
const cosB = Math.min( Math.max( b.cosO, - 1 ), 1 );
|
|
270
|
+
const thetaA = Math.acos( cosA );
|
|
271
|
+
const thetaB = Math.acos( cosB );
|
|
272
|
+
const dotAB = Math.min( Math.max( a.ax * b.ax + a.ay * b.ay + a.az * b.az, - 1 ), 1 );
|
|
273
|
+
const thetaD = Math.acos( dotAB );
|
|
274
|
+
|
|
275
|
+
// One cone already contains the other
|
|
276
|
+
if ( Math.min( thetaD + thetaB, Math.PI ) <= thetaA ) return a;
|
|
277
|
+
if ( Math.min( thetaD + thetaA, Math.PI ) <= thetaB ) return b;
|
|
278
|
+
|
|
279
|
+
const thetaO = ( thetaA + thetaB + thetaD ) * 0.5;
|
|
280
|
+
if ( thetaO >= Math.PI ) return { ax: a.ax, ay: a.ay, az: a.az, cosO: - 1 };
|
|
281
|
+
|
|
282
|
+
const thetaR = thetaO - thetaA;
|
|
283
|
+
|
|
284
|
+
// Rotation axis = normalize(cross(a, b))
|
|
285
|
+
let wx = a.ay * b.az - a.az * b.ay;
|
|
286
|
+
let wy = a.az * b.ax - a.ax * b.az;
|
|
287
|
+
let wz = a.ax * b.ay - a.ay * b.ax;
|
|
288
|
+
const wl = Math.sqrt( wx * wx + wy * wy + wz * wz );
|
|
289
|
+
if ( wl < 1e-8 ) return { ax: a.ax, ay: a.ay, az: a.az, cosO: - 1 }; // (anti)parallel → whole sphere
|
|
290
|
+
wx /= wl; wy /= wl; wz /= wl;
|
|
291
|
+
|
|
292
|
+
// Rodrigues: rotate a.axis around (wx,wy,wz) by thetaR
|
|
293
|
+
const c = Math.cos( thetaR ), s = Math.sin( thetaR );
|
|
294
|
+
const kdotv = wx * a.ax + wy * a.ay + wz * a.az;
|
|
295
|
+
const cx = wy * a.az - wz * a.ay;
|
|
296
|
+
const cy = wz * a.ax - wx * a.az;
|
|
297
|
+
const cz = wx * a.ay - wy * a.ax;
|
|
298
|
+
const ax = a.ax * c + cx * s + wx * kdotv * ( 1 - c );
|
|
299
|
+
const ay = a.ay * c + cy * s + wy * kdotv * ( 1 - c );
|
|
300
|
+
const az = a.az * c + cz * s + wz * kdotv * ( 1 - c );
|
|
301
|
+
const al = Math.sqrt( ax * ax + ay * ay + az * az ) || 1;
|
|
302
|
+
|
|
303
|
+
return { ax: ax / al, ay: ay / al, az: az / al, cosO: Math.cos( thetaO ) };
|
|
304
|
+
|
|
305
|
+
}
|
|
@@ -90,6 +90,7 @@ export class SceneProcessor {
|
|
|
90
90
|
this.emissiveTriangleCount = 0;
|
|
91
91
|
this.lightBVHNodeData = null;
|
|
92
92
|
this.lightBVHNodeCount = 0;
|
|
93
|
+
this.emissiveBitTrailMap = null;
|
|
93
94
|
|
|
94
95
|
// Initialize processing components
|
|
95
96
|
this._initProcessors();
|
|
@@ -818,6 +819,8 @@ export class SceneProcessor {
|
|
|
818
819
|
this.lightBVHNodeCount = this.emissiveTriangleBuilder.lightBVHNodeCount;
|
|
819
820
|
// Replace emissiveTriangleData with sorted version (LBVH reorders it)
|
|
820
821
|
this.emissiveTriangleData = this.emissiveTriangleBuilder.emissiveTriangleData || this.emissiveTriangleData;
|
|
822
|
+
// Per-triangle bit-trail map for the bounce-hit MIS re-walk
|
|
823
|
+
this.emissiveBitTrailMap = this.emissiveTriangleBuilder.emissiveBitTrailMap;
|
|
821
824
|
|
|
822
825
|
}
|
|
823
826
|
|
|
@@ -872,6 +875,7 @@ export class SceneProcessor {
|
|
|
872
875
|
this.instanceTable = null;
|
|
873
876
|
this.lightBVHNodeData = null;
|
|
874
877
|
this.lightBVHNodeCount = 0;
|
|
878
|
+
this.emissiveBitTrailMap = null;
|
|
875
879
|
|
|
876
880
|
// Reset performance metrics
|
|
877
881
|
this.performanceMetrics = {
|
|
@@ -1406,6 +1410,7 @@ export class SceneProcessor {
|
|
|
1406
1410
|
this.emissiveTriangleData,
|
|
1407
1411
|
this.emissiveTriangleCount,
|
|
1408
1412
|
this.emissiveTotalPower,
|
|
1413
|
+
this.emissiveBitTrailMap,
|
|
1409
1414
|
);
|
|
1410
1415
|
|
|
1411
1416
|
}
|
|
@@ -1450,10 +1455,23 @@ export class SceneProcessor {
|
|
|
1450
1455
|
|
|
1451
1456
|
if ( ! changed ) return null;
|
|
1452
1457
|
|
|
1458
|
+
// Rebuild the Light BVH + sorted emissive data + bit-trail map so the stochastic descent and
|
|
1459
|
+
// the bounce-hit MIS re-walk stay consistent after powers / the emissive set change.
|
|
1460
|
+
this.emissiveTriangleBuilder.buildLightBVH();
|
|
1461
|
+
this.lightBVHNodeData = this.emissiveTriangleBuilder.lightBVHNodeData;
|
|
1462
|
+
this.lightBVHNodeCount = this.emissiveTriangleBuilder.lightBVHNodeCount;
|
|
1463
|
+
this.emissiveTriangleData = this.emissiveTriangleBuilder.emissiveTriangleData;
|
|
1464
|
+
this.emissiveBitTrailMap = this.emissiveTriangleBuilder.emissiveBitTrailMap;
|
|
1465
|
+
this.emissiveTriangleCount = this.emissiveTriangleBuilder.emissiveCount;
|
|
1466
|
+
this.emissiveTotalPower = this.emissiveTriangleBuilder.totalEmissivePower;
|
|
1467
|
+
|
|
1453
1468
|
return {
|
|
1454
|
-
rawData: this.emissiveTriangleBuilder.
|
|
1469
|
+
rawData: this.emissiveTriangleBuilder.emissiveTriangleData,
|
|
1455
1470
|
emissiveCount: this.emissiveTriangleBuilder.emissiveCount,
|
|
1456
1471
|
totalPower: this.emissiveTriangleBuilder.totalEmissivePower,
|
|
1472
|
+
bitTrailMap: this.emissiveBitTrailMap,
|
|
1473
|
+
lightBVHNodeData: this.lightBVHNodeData,
|
|
1474
|
+
lightBVHNodeCount: this.lightBVHNodeCount,
|
|
1457
1475
|
};
|
|
1458
1476
|
|
|
1459
1477
|
}
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -754,6 +754,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
754
754
|
totalTriangleCount: this.totalTriangleCount,
|
|
755
755
|
enableEmissiveTriangleSampling: this.enableEmissiveTriangleSampling,
|
|
756
756
|
lightBVHNodeCount: this.lightBVHNodeCount,
|
|
757
|
+
reverseMapVec4Offset: this.reverseMapVec4Offset,
|
|
757
758
|
currentBounce: this._wfCurrentBounce,
|
|
758
759
|
maxRayCount: this._wfMaxRayCount,
|
|
759
760
|
} );
|
|
@@ -198,9 +198,12 @@ export class PathTracerStage extends RenderStage {
|
|
|
198
198
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
|
|
199
199
|
this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
|
|
200
200
|
|
|
201
|
-
// Cached CPU-side data — rebuilt into the packed buffer whenever
|
|
201
|
+
// Cached CPU-side data — rebuilt into the packed buffer whenever any source changes.
|
|
202
202
|
this._lbvhDataCache = null;
|
|
203
203
|
this._emissiveDataCache = null;
|
|
204
|
+
// Per-triangle bit-trail map (root→leaf Light BVH path, 1 float per triangleIndex); packed
|
|
205
|
+
// after the emissive entries so the bounce-hit MIS path can re-walk the descent pdf.
|
|
206
|
+
this._bitTrailMapCache = null;
|
|
204
207
|
|
|
205
208
|
// Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
|
|
206
209
|
// (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
|
|
@@ -1296,14 +1299,18 @@ export class PathTracerStage extends RenderStage {
|
|
|
1296
1299
|
const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
|
|
1297
1300
|
const lbvh = this._lbvhDataCache;
|
|
1298
1301
|
const emis = this._emissiveDataCache;
|
|
1302
|
+
const trail = this._bitTrailMapCache;
|
|
1299
1303
|
const lbvhLen = lbvh ? lbvh.length : 0;
|
|
1300
1304
|
const emisLen = emis ? emis.length : 0;
|
|
1305
|
+
// Bit-trail map packs 4 trails per vec4 → pad to a vec4 boundary.
|
|
1306
|
+
const trailPadded = trail ? Math.ceil( trail.length / 4 ) * 4 : 0;
|
|
1301
1307
|
|
|
1302
1308
|
// Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
|
|
1303
|
-
const totalLen = Math.max( lbvhLen + emisLen, 4 );
|
|
1309
|
+
const totalLen = Math.max( lbvhLen + emisLen + trailPadded, 4 );
|
|
1304
1310
|
const combined = new Float32Array( totalLen );
|
|
1305
1311
|
if ( lbvh ) combined.set( lbvh, 0 );
|
|
1306
1312
|
if ( emis ) combined.set( emis, lbvhLen );
|
|
1313
|
+
if ( trail ) combined.set( trail, lbvhLen + emisLen );
|
|
1307
1314
|
|
|
1308
1315
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
|
|
1309
1316
|
this.lightStorageNode.value = this.lightStorageAttr;
|
|
@@ -1311,14 +1318,18 @@ export class PathTracerStage extends RenderStage {
|
|
|
1311
1318
|
|
|
1312
1319
|
// Offset (in vec4 elements) where emissive data starts.
|
|
1313
1320
|
this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
|
|
1321
|
+
// Offset (in vec4 elements) where the bit-trail map starts (lbvhLen + emisLen are float
|
|
1322
|
+
// counts, both multiples of 4, so this divides cleanly).
|
|
1323
|
+
this.reverseMapVec4Offset.value = ( lbvhLen + emisLen ) / 4;
|
|
1314
1324
|
|
|
1315
1325
|
}
|
|
1316
1326
|
|
|
1317
|
-
setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
|
|
1327
|
+
setEmissiveTriangleData( emissiveData, count, totalPower = 0, bitTrailMap = null ) {
|
|
1318
1328
|
|
|
1319
1329
|
if ( ! emissiveData ) return;
|
|
1320
1330
|
|
|
1321
1331
|
this._emissiveDataCache = emissiveData;
|
|
1332
|
+
if ( bitTrailMap ) this._bitTrailMapCache = bitTrailMap;
|
|
1322
1333
|
this.emissiveTriangleCount.value = count;
|
|
1323
1334
|
this.emissiveTotalPower.value = totalPower;
|
|
1324
1335
|
this._rebuildLightBuffer();
|
|
@@ -312,8 +312,9 @@ export const calculateEmissiveLightPdf = Fn( ( [
|
|
|
312
312
|
// Targeted material read: only fetch emissive data (2 vec4s instead of full 27)
|
|
313
313
|
const matData1 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( MATERIAL_SLOT.EMISSIVE_ROUGHNESS ), MATERIAL_SLOTS );
|
|
314
314
|
const matData2 = getDatafromStorageBuffer( materialBuffer, triData.materialIndex, int( MATERIAL_SLOT.IOR_TRANSMISSION ), MATERIAL_SLOTS );
|
|
315
|
-
|
|
316
|
-
const
|
|
315
|
+
// Rec.709 luma — must match EmissiveTriangleBuilder's power weighting for MIS consistency.
|
|
316
|
+
const luma = matData1.x.mul( 0.2126 ).add( matData1.y.mul( 0.7152 ) ).add( matData1.z.mul( 0.0722 ) );
|
|
317
|
+
const power = max( luma.mul( matData2.a ).mul( area ), float( 1e-10 ) );
|
|
317
318
|
const selectionPdf = power.div( max( emissiveTotalPower, float( 1e-10 ) ) );
|
|
318
319
|
|
|
319
320
|
const result = float( 0.0 ).toVar();
|