rayzee 7.15.0 → 7.15.2
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 +86 -17
- package/dist/rayzee.es.js +117 -83
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +4 -4
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +1 -1
- package/src/PathTracerApp.js +59 -15
- package/src/Processor/EmissiveTriangleBuilder.js +74 -20
- package/src/Processor/SceneProcessor.js +35 -5
- package/src/Stages/PathTracer.js +14 -0
- package/src/Stages/PathTracerStage.js +36 -8
- package/src/TSL/EmissiveSampling.js +11 -2
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -91,7 +91,7 @@ export const ENGINE_DEFAULTS = {
|
|
|
91
91
|
adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
|
|
92
92
|
adaptiveStopFraction: 0.95, // retire the frame once this fraction of pixels has converged
|
|
93
93
|
// Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
|
|
94
|
-
// which would bake dim regions too dark). Naturally engages only on static/idle views.
|
|
94
|
+
// which would bake dim regions too dark). Naturally engages only on static/idle views.
|
|
95
95
|
usePixelFreeze: false,
|
|
96
96
|
pixelFreezeThreshold: 0.02, // per-pixel noise below which a pixel becomes a freeze candidate
|
|
97
97
|
pixelFreezeStability: 8, // consecutive candidate frames before a pixel freezes
|
package/src/PathTracerApp.js
CHANGED
|
@@ -838,6 +838,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
838
838
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
839
839
|
this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
|
|
840
840
|
|
|
841
|
+
// Drop authored-hidden meshes' triangles from the emissive-NEE structure
|
|
842
|
+
// (runs before setupMaterial so the kernels compile against the final buffer)
|
|
843
|
+
this._refreshEmissiveForVisibility();
|
|
844
|
+
|
|
841
845
|
timer.end( 'GPU data transfer' );
|
|
842
846
|
|
|
843
847
|
// Compile shaders
|
|
@@ -882,7 +886,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
882
886
|
// Runs before applyAll()/SceneRebuild so the uniform and UI both pick up the new value.
|
|
883
887
|
if ( ! this._emissiveSamplingUserSet ) {
|
|
884
888
|
|
|
885
|
-
|
|
889
|
+
// Keyed on the canonical (unfiltered) emissive set — emissiveTriangleCount
|
|
890
|
+
// reflects only the visible subset, and hidden emitters can be shown later.
|
|
891
|
+
const hasEmissive = ( this._sdf?.emissiveTriangleBuilder?.emissiveTriangles?.length
|
|
892
|
+
?? this._sdf?.emissiveTriangleCount ?? 0 ) > 0;
|
|
886
893
|
this.settings.set( 'enableEmissiveTriangleSampling', hasEmissive, { reset: false } );
|
|
887
894
|
|
|
888
895
|
}
|
|
@@ -1504,27 +1511,60 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1504
1511
|
|
|
1505
1512
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
1506
1513
|
|
|
1514
|
+
// Keep the emissive-NEE structure in sync unconditionally (not gated on the
|
|
1515
|
+
// sampling toggle) so edits made while NEE is off aren't lost on re-enable.
|
|
1507
1516
|
const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity' ];
|
|
1508
|
-
if ( emissiveAffectingProps.includes( property )
|
|
1509
|
-
&& this.stages.pathTracer?.enableEmissiveTriangleSampling?.value ) {
|
|
1517
|
+
if ( emissiveAffectingProps.includes( property ) && this._sdf ) {
|
|
1510
1518
|
|
|
1511
|
-
|
|
1512
|
-
if ( result ) {
|
|
1519
|
+
this._uploadEmissivePayload( this._sdf.updateMaterialEmissive( materialIndex, property, value ) );
|
|
1513
1520
|
|
|
1514
|
-
|
|
1515
|
-
result.rawData, result.emissiveCount, result.totalPower, result.bitTrailMap,
|
|
1516
|
-
);
|
|
1517
|
-
if ( result.lightBVHNodeData ) {
|
|
1521
|
+
}
|
|
1518
1522
|
|
|
1519
|
-
|
|
1523
|
+
this.reset();
|
|
1520
1524
|
|
|
1521
|
-
|
|
1525
|
+
}
|
|
1522
1526
|
|
|
1523
|
-
|
|
1527
|
+
/**
|
|
1528
|
+
* Upload a rebuilt emissive-NEE payload (sorted emissive data + Light BVH +
|
|
1529
|
+
* bit-trail map) to the path tracer stage. No-op for a null payload.
|
|
1530
|
+
* @param {object|null} payload
|
|
1531
|
+
* @private
|
|
1532
|
+
*/
|
|
1533
|
+
_uploadEmissivePayload( payload ) {
|
|
1534
|
+
|
|
1535
|
+
if ( ! payload || ! this.stages.pathTracer ) return;
|
|
1536
|
+
|
|
1537
|
+
this.stages.pathTracer.setEmissiveTriangleData(
|
|
1538
|
+
payload.rawData, payload.emissiveCount, payload.totalPower, payload.bitTrailMap,
|
|
1539
|
+
);
|
|
1540
|
+
if ( payload.lightBVHNodeData ) {
|
|
1541
|
+
|
|
1542
|
+
this.stages.pathTracer.setLightBVHData( payload.lightBVHNodeData, payload.lightBVHNodeCount );
|
|
1524
1543
|
|
|
1525
1544
|
}
|
|
1526
1545
|
|
|
1527
|
-
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Re-derive the emissive-NEE sampled set from current per-mesh world-visibility
|
|
1550
|
+
* (InstanceTable is the source of truth — patched by the stage's visibility API).
|
|
1551
|
+
* Hidden meshes' triangles are dropped from the Light BVH so they stop casting
|
|
1552
|
+
* NEE light; the shadow ray can't do it (a hidden mesh no longer self-occludes).
|
|
1553
|
+
* @private
|
|
1554
|
+
*/
|
|
1555
|
+
_refreshEmissiveForVisibility() {
|
|
1556
|
+
|
|
1557
|
+
const entries = this._sdf?.instanceTable?.entries;
|
|
1558
|
+
if ( ! entries ) return;
|
|
1559
|
+
|
|
1560
|
+
const hidden = new Set();
|
|
1561
|
+
for ( const entry of entries ) {
|
|
1562
|
+
|
|
1563
|
+
if ( entry && entry.visible === false ) hidden.add( entry.meshIndex );
|
|
1564
|
+
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
this._uploadEmissivePayload( this._sdf.rebuildEmissiveForVisibility( hidden ) );
|
|
1528
1568
|
|
|
1529
1569
|
}
|
|
1530
1570
|
|
|
@@ -1542,11 +1582,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1542
1582
|
}
|
|
1543
1583
|
|
|
1544
1584
|
/**
|
|
1545
|
-
* @returns {boolean} True if the current scene contains emissive geometry
|
|
1585
|
+
* @returns {boolean} True if the current scene contains emissive geometry
|
|
1586
|
+
* (regardless of per-mesh visibility).
|
|
1546
1587
|
*/
|
|
1547
1588
|
hasEmissiveGeometry() {
|
|
1548
1589
|
|
|
1549
|
-
return ( this._sdf?.
|
|
1590
|
+
return ( this._sdf?.emissiveTriangleBuilder?.emissiveTriangles?.length
|
|
1591
|
+
?? this._sdf?.emissiveTriangleCount ?? 0 ) > 0;
|
|
1550
1592
|
|
|
1551
1593
|
}
|
|
1552
1594
|
|
|
@@ -1559,6 +1601,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1559
1601
|
setMeshVisibility( meshIndex, visible ) {
|
|
1560
1602
|
|
|
1561
1603
|
this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible );
|
|
1604
|
+
this._refreshEmissiveForVisibility();
|
|
1562
1605
|
this.reset();
|
|
1563
1606
|
|
|
1564
1607
|
}
|
|
@@ -1570,6 +1613,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1570
1613
|
updateAllMeshVisibility() {
|
|
1571
1614
|
|
|
1572
1615
|
this.stages.pathTracer?.updateAllMeshVisibility();
|
|
1616
|
+
this._refreshEmissiveForVisibility();
|
|
1573
1617
|
this.reset();
|
|
1574
1618
|
|
|
1575
1619
|
}
|
|
@@ -26,6 +26,10 @@ export class EmissiveTriangleBuilder {
|
|
|
26
26
|
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
27
27
|
this.emissiveBitTrailMap = null;
|
|
28
28
|
this._totalTriangleCount = 0;
|
|
29
|
+
// Mesh indices whose emissive triangles are excluded from the sampled set
|
|
30
|
+
// (per-mesh visibility). Canonical `emissiveTriangles` is never filtered —
|
|
31
|
+
// the GPU-facing arrays are rebuilt from the visible subset in buildLightBVH().
|
|
32
|
+
this._hiddenMeshes = new Set();
|
|
29
33
|
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -45,11 +49,13 @@ export class EmissiveTriangleBuilder {
|
|
|
45
49
|
|
|
46
50
|
const FLOATS_PER_TRIANGLE = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
47
51
|
const MATERIAL_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 2; // materialIndex within vec4
|
|
52
|
+
const MESH_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 3; // meshIndex within vec4
|
|
48
53
|
|
|
49
54
|
for ( let i = 0; i < triangleCount; i ++ ) {
|
|
50
55
|
|
|
51
56
|
const baseOffset = i * FLOATS_PER_TRIANGLE;
|
|
52
57
|
const materialIndex = Math.floor( triangleData[ baseOffset + MATERIAL_INDEX_OFFSET ] );
|
|
58
|
+
const meshIndex = Math.floor( triangleData[ baseOffset + MESH_INDEX_OFFSET ] );
|
|
53
59
|
|
|
54
60
|
// Get material
|
|
55
61
|
const material = materials[ materialIndex ];
|
|
@@ -111,6 +117,7 @@ export class EmissiveTriangleBuilder {
|
|
|
111
117
|
this.emissiveTriangles.push( {
|
|
112
118
|
triangleIndex: i,
|
|
113
119
|
materialIndex: materialIndex,
|
|
120
|
+
meshIndex: meshIndex,
|
|
114
121
|
power: power,
|
|
115
122
|
area: area,
|
|
116
123
|
emissive: { r: emissive.r, g: emissive.g, b: emissive.b },
|
|
@@ -443,40 +450,81 @@ export class EmissiveTriangleBuilder {
|
|
|
443
450
|
// If not emissive before and not now, nothing to do
|
|
444
451
|
if ( ! isNowEmissive ) return false;
|
|
445
452
|
|
|
446
|
-
// Fast path:
|
|
453
|
+
// Fast path: update the canonical entries only. Derived arrays, CDF and
|
|
454
|
+
// totals are rebuilt by the buildLightBVH() call that always follows.
|
|
447
455
|
// Rec.709 luma — must match the build path + shader MIS weighting.
|
|
448
456
|
const luma = 0.2126 * emissive.r + 0.7152 * emissive.g + 0.0722 * emissive.b;
|
|
449
457
|
|
|
450
|
-
this.
|
|
451
|
-
for ( let i = 0; i < this.emissiveCount; i ++ ) {
|
|
458
|
+
for ( const tri of this.emissiveTriangles ) {
|
|
452
459
|
|
|
453
|
-
const tri = this.emissiveTriangles[ i ];
|
|
454
460
|
if ( tri.materialIndex === materialIndex ) {
|
|
455
461
|
|
|
456
462
|
tri.power = luma * emissiveIntensity * tri.area;
|
|
457
463
|
tri.emissive = { r: emissive.r, g: emissive.g, b: emissive.b };
|
|
458
464
|
tri.emissiveIntensity = emissiveIntensity;
|
|
459
|
-
this.emissivePowerArray[ i ] = tri.power;
|
|
460
465
|
|
|
461
466
|
}
|
|
462
467
|
|
|
463
|
-
this.totalEmissivePower += this.emissiveTriangles[ i ].power;
|
|
464
|
-
|
|
465
468
|
}
|
|
466
469
|
|
|
467
|
-
this._buildCDF();
|
|
468
470
|
return true;
|
|
469
471
|
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
/**
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
+
* Set the meshes whose emissive triangles must be excluded from sampling
|
|
476
|
+
* (per-mesh visibility). Indices that don't own emissive triangles are
|
|
477
|
+
* ignored so non-emissive visibility toggles are no-ops.
|
|
478
|
+
* @param {Set<number>|Iterable<number>} hiddenMeshIndices
|
|
479
|
+
* @returns {boolean} true if the effective hidden set changed (caller should rebuild)
|
|
480
|
+
*/
|
|
481
|
+
setHiddenMeshes( hiddenMeshIndices ) {
|
|
482
|
+
|
|
483
|
+
const emissiveMeshes = new Set();
|
|
484
|
+
for ( const tri of this.emissiveTriangles ) emissiveMeshes.add( tri.meshIndex );
|
|
485
|
+
|
|
486
|
+
const next = new Set();
|
|
487
|
+
for ( const m of hiddenMeshIndices ) if ( emissiveMeshes.has( m ) ) next.add( m );
|
|
488
|
+
|
|
489
|
+
const prev = this._hiddenMeshes;
|
|
490
|
+
let changed = next.size !== prev.size;
|
|
491
|
+
if ( ! changed ) {
|
|
492
|
+
|
|
493
|
+
for ( const m of next ) {
|
|
494
|
+
|
|
495
|
+
if ( ! prev.has( m ) ) {
|
|
496
|
+
|
|
497
|
+
changed = true;
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this._hiddenMeshes = next;
|
|
507
|
+
return changed;
|
|
508
|
+
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Build a Light BVH over the VISIBLE emissive triangles (canonical set minus
|
|
513
|
+
* hidden meshes). Authoritative for the GPU-facing emissiveCount /
|
|
514
|
+
* totalEmissivePower / sorted raw data / bit-trail map.
|
|
475
515
|
* @returns {number} nodeCount
|
|
476
516
|
*/
|
|
477
517
|
buildLightBVH() {
|
|
478
518
|
|
|
479
|
-
|
|
519
|
+
const active = this._hiddenMeshes.size === 0
|
|
520
|
+
? this.emissiveTriangles
|
|
521
|
+
: this.emissiveTriangles.filter( t => ! this._hiddenMeshes.has( t.meshIndex ) );
|
|
522
|
+
|
|
523
|
+
this.emissiveCount = active.length;
|
|
524
|
+
this.totalEmissivePower = 0;
|
|
525
|
+
for ( const tri of active ) this.totalEmissivePower += tri.power;
|
|
526
|
+
|
|
527
|
+
if ( active.length === 0 ) {
|
|
480
528
|
|
|
481
529
|
// Dummy single leaf node (whole-sphere cone)
|
|
482
530
|
this.lightBVHNodeData = new Float32Array( 16 );
|
|
@@ -485,23 +533,27 @@ export class EmissiveTriangleBuilder {
|
|
|
485
533
|
this.lightBVHNodeData[ 15 ] = - 1.0; // cosThetaO = whole sphere
|
|
486
534
|
this.lightBVHNodeCount = 1;
|
|
487
535
|
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
536
|
+
this.emissiveIndicesArray = new Int32Array( 0 );
|
|
537
|
+
this.emissivePowerArray = new Float32Array( 0 );
|
|
538
|
+
this._buildCDF();
|
|
539
|
+
this.emissiveTriangleData = new Float32Array( 8 ); // 2 dummy vec4s
|
|
488
540
|
return 1;
|
|
489
541
|
|
|
490
542
|
}
|
|
491
543
|
|
|
492
544
|
const builder = new LightBVHBuilder();
|
|
493
|
-
const { nodeData, nodeCount, sortedPerm, bitTrails } = builder.build(
|
|
545
|
+
const { nodeData, nodeCount, sortedPerm, bitTrails } = builder.build( active );
|
|
494
546
|
this.lightBVHNodeData = nodeData;
|
|
495
547
|
this.lightBVHNodeCount = nodeCount;
|
|
496
548
|
|
|
497
549
|
// Rebuild emissive raw data in sorted leaf order so node start/count refs are valid
|
|
498
|
-
this._rebuildSortedEmissiveData( sortedPerm );
|
|
550
|
+
this._rebuildSortedEmissiveData( sortedPerm, active );
|
|
499
551
|
|
|
500
|
-
// Build the per-triangle bit-trail map (indexed by absolute triangleIndex; -1 = non-emissive)
|
|
552
|
+
// Build the per-triangle bit-trail map (indexed by absolute triangleIndex; -1 = non-emissive/hidden)
|
|
501
553
|
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
502
554
|
for ( let i = 0; i < sortedPerm.length; i ++ ) {
|
|
503
555
|
|
|
504
|
-
const triIndex =
|
|
556
|
+
const triIndex = active[ sortedPerm[ i ] ].triangleIndex;
|
|
505
557
|
this.emissiveBitTrailMap[ triIndex ] = bitTrails[ i ];
|
|
506
558
|
|
|
507
559
|
}
|
|
@@ -512,10 +564,11 @@ export class EmissiveTriangleBuilder {
|
|
|
512
564
|
|
|
513
565
|
/**
|
|
514
566
|
* Rebuild emissive arrays and raw GPU data in the sorted leaf order given by sortedPerm.
|
|
515
|
-
* @param {Int32Array} sortedPerm - sortedPerm[i] =
|
|
567
|
+
* @param {Int32Array} sortedPerm - sortedPerm[i] = index into `tris` for position i
|
|
568
|
+
* @param {Array} tris - The (visible) triangle set the Light BVH was built over
|
|
516
569
|
* @private
|
|
517
570
|
*/
|
|
518
|
-
_rebuildSortedEmissiveData( sortedPerm ) {
|
|
571
|
+
_rebuildSortedEmissiveData( sortedPerm, tris ) {
|
|
519
572
|
|
|
520
573
|
const n = sortedPerm.length;
|
|
521
574
|
|
|
@@ -526,8 +579,8 @@ export class EmissiveTriangleBuilder {
|
|
|
526
579
|
for ( let i = 0; i < n; i ++ ) {
|
|
527
580
|
|
|
528
581
|
const origIdx = sortedPerm[ i ];
|
|
529
|
-
this.emissiveIndicesArray[ i ] =
|
|
530
|
-
this.emissivePowerArray[ i ] =
|
|
582
|
+
this.emissiveIndicesArray[ i ] = tris[ origIdx ].triangleIndex;
|
|
583
|
+
this.emissivePowerArray[ i ] = tris[ origIdx ].power;
|
|
531
584
|
|
|
532
585
|
}
|
|
533
586
|
|
|
@@ -540,7 +593,7 @@ export class EmissiveTriangleBuilder {
|
|
|
540
593
|
for ( let i = 0; i < n; i ++ ) {
|
|
541
594
|
|
|
542
595
|
const origIdx = sortedPerm[ i ];
|
|
543
|
-
const tri =
|
|
596
|
+
const tri = tris[ origIdx ];
|
|
544
597
|
const offset = i * 8;
|
|
545
598
|
|
|
546
599
|
// vec4[0]: triangleIndex, power, cdf, selectionPdf
|
|
@@ -579,6 +632,7 @@ export class EmissiveTriangleBuilder {
|
|
|
579
632
|
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
580
633
|
this.emissiveBitTrailMap = null;
|
|
581
634
|
this._totalTriangleCount = 0;
|
|
635
|
+
this._hiddenMeshes = new Set();
|
|
582
636
|
|
|
583
637
|
}
|
|
584
638
|
|
|
@@ -979,6 +979,9 @@ export class SceneProcessor {
|
|
|
979
979
|
this.emissiveTriangleData = this.emissiveTriangleBuilder.emissiveTriangleData || this.emissiveTriangleData;
|
|
980
980
|
// Per-triangle bit-trail map for the bounce-hit MIS re-walk
|
|
981
981
|
this.emissiveBitTrailMap = this.emissiveTriangleBuilder.emissiveBitTrailMap;
|
|
982
|
+
// buildLightBVH is authoritative for the sampled (visible-subset) count/power
|
|
983
|
+
this.emissiveTriangleCount = this.emissiveTriangleBuilder.emissiveCount;
|
|
984
|
+
this.emissiveTotalPower = this.emissiveTriangleBuilder.totalEmissivePower;
|
|
982
985
|
|
|
983
986
|
}
|
|
984
987
|
|
|
@@ -1581,8 +1584,35 @@ export class SceneProcessor {
|
|
|
1581
1584
|
|
|
1582
1585
|
if ( ! changed ) return null;
|
|
1583
1586
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1587
|
+
return this._collectEmissivePayload();
|
|
1588
|
+
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* Re-derive the sampled emissive set from per-mesh visibility.
|
|
1593
|
+
* @param {Set<number>|Iterable<number>} hiddenMeshIndices - meshIndex values that are world-hidden
|
|
1594
|
+
* @param {boolean} force - rebuild even if the effective hidden set is unchanged
|
|
1595
|
+
* @returns {object|null} GPU upload payload, or null when nothing changed
|
|
1596
|
+
*/
|
|
1597
|
+
rebuildEmissiveForVisibility( hiddenMeshIndices, force = false ) {
|
|
1598
|
+
|
|
1599
|
+
if ( ! this.emissiveTriangleBuilder ) return null;
|
|
1600
|
+
|
|
1601
|
+
const changed = this.emissiveTriangleBuilder.setHiddenMeshes( hiddenMeshIndices );
|
|
1602
|
+
if ( ! changed && ! force ) return null;
|
|
1603
|
+
|
|
1604
|
+
return this._collectEmissivePayload();
|
|
1605
|
+
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Rebuild the Light BVH + sorted emissive data + bit-trail map (over the visible
|
|
1610
|
+
* subset) so the stochastic descent and the bounce-hit MIS re-walk stay consistent,
|
|
1611
|
+
* then sync the processor fields and return the GPU upload payload.
|
|
1612
|
+
* @private
|
|
1613
|
+
*/
|
|
1614
|
+
_collectEmissivePayload() {
|
|
1615
|
+
|
|
1586
1616
|
this.emissiveTriangleBuilder.buildLightBVH();
|
|
1587
1617
|
this.lightBVHNodeData = this.emissiveTriangleBuilder.lightBVHNodeData;
|
|
1588
1618
|
this.lightBVHNodeCount = this.emissiveTriangleBuilder.lightBVHNodeCount;
|
|
@@ -1592,9 +1622,9 @@ export class SceneProcessor {
|
|
|
1592
1622
|
this.emissiveTotalPower = this.emissiveTriangleBuilder.totalEmissivePower;
|
|
1593
1623
|
|
|
1594
1624
|
return {
|
|
1595
|
-
rawData: this.
|
|
1596
|
-
emissiveCount: this.
|
|
1597
|
-
totalPower: this.
|
|
1625
|
+
rawData: this.emissiveTriangleData,
|
|
1626
|
+
emissiveCount: this.emissiveTriangleCount,
|
|
1627
|
+
totalPower: this.emissiveTotalPower,
|
|
1598
1628
|
bitTrailMap: this.emissiveBitTrailMap,
|
|
1599
1629
|
lightBVHNodeData: this.lightBVHNodeData,
|
|
1600
1630
|
lightBVHNodeCount: this.lightBVHNodeCount,
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -199,6 +199,17 @@ export class PathTracer extends PathTracerStage {
|
|
|
199
199
|
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
200
200
|
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
201
201
|
|
|
202
|
+
// The packed light buffer was grow-reallocated at runtime (emissive set grew) —
|
|
203
|
+
// the compiled kernels still bind the old attribute, so rebuild before rendering.
|
|
204
|
+
if ( this._lightBufferRealloc ) {
|
|
205
|
+
|
|
206
|
+
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
207
|
+
this._wavefrontReady = false;
|
|
208
|
+
this._buildWavefrontKernels();
|
|
209
|
+
if ( ! this._wavefrontReady ) return;
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
|
|
202
213
|
if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
|
|
203
214
|
|
|
204
215
|
if ( ! this.isComplete ) this.isComplete = true;
|
|
@@ -685,6 +696,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
685
696
|
const texNodes = this.shaderBuilder.getSceneTextureNodes();
|
|
686
697
|
if ( ! texNodes ) return;
|
|
687
698
|
|
|
699
|
+
// A fresh build binds the current lightStorageAttr — any pending realloc is covered.
|
|
700
|
+
this._lightBufferRealloc = false;
|
|
701
|
+
|
|
688
702
|
const w = this.storageTextures.renderWidth;
|
|
689
703
|
const h = this.storageTextures.renderHeight;
|
|
690
704
|
const maxRays = w * h;
|
|
@@ -197,6 +197,9 @@ export class PathTracerStage extends RenderStage {
|
|
|
197
197
|
// Initialized with dummy data so TSL compilation never sees null.
|
|
198
198
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
|
|
199
199
|
this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
|
|
200
|
+
// Set when _rebuildLightBuffer had to grow-reallocate the attribute: compiled
|
|
201
|
+
// kernels still bind the old one and must be rebuilt to see the new data.
|
|
202
|
+
this._lightBufferRealloc = false;
|
|
200
203
|
|
|
201
204
|
// Cached CPU-side data — rebuilt into the packed buffer whenever any source changes.
|
|
202
205
|
this._lbvhDataCache = null;
|
|
@@ -1279,6 +1282,12 @@ export class PathTracerStage extends RenderStage {
|
|
|
1279
1282
|
* Rebuild the packed light buffer from cached lightBVH + emissive data.
|
|
1280
1283
|
* Layout: [ lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE vec4s per entry) ].
|
|
1281
1284
|
* Also updates `emissiveVec4Offset` uniform (in vec4 elements).
|
|
1285
|
+
*
|
|
1286
|
+
* The compiled wavefront kernels bind the attribute that existed at kernel-build
|
|
1287
|
+
* time and NEVER pick up a `lightStorageNode.value` reassignment — so runtime
|
|
1288
|
+
* updates MUST write in-place into the bound attribute (+ needsUpdate). Only
|
|
1289
|
+
* grow-reallocate when the data outgrows capacity, and flag it so the kernels
|
|
1290
|
+
* get rebuilt against the new attribute.
|
|
1282
1291
|
* @private
|
|
1283
1292
|
*/
|
|
1284
1293
|
_rebuildLightBuffer() {
|
|
@@ -1294,14 +1303,33 @@ export class PathTracerStage extends RenderStage {
|
|
|
1294
1303
|
|
|
1295
1304
|
// Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
|
|
1296
1305
|
const totalLen = Math.max( lbvhLen + emisLen + trailPadded, 4 );
|
|
1297
|
-
|
|
1298
|
-
if (
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1306
|
+
|
|
1307
|
+
if ( this.lightStorageAttr && totalLen <= this.lightStorageAttr.array.length ) {
|
|
1308
|
+
|
|
1309
|
+
// In-place update of the bound attribute. Stale floats past the new data
|
|
1310
|
+
// are unreachable — all reads are gated by the count/offset uniforms.
|
|
1311
|
+
const arr = this.lightStorageAttr.array;
|
|
1312
|
+
if ( lbvh ) arr.set( lbvh, 0 );
|
|
1313
|
+
if ( emis ) arr.set( emis, lbvhLen );
|
|
1314
|
+
if ( trail ) arr.set( trail, lbvhLen + emisLen );
|
|
1315
|
+
this.lightStorageAttr.needsUpdate = true;
|
|
1316
|
+
|
|
1317
|
+
} else {
|
|
1318
|
+
|
|
1319
|
+
const combined = new Float32Array( totalLen );
|
|
1320
|
+
if ( lbvh ) combined.set( lbvh, 0 );
|
|
1321
|
+
if ( emis ) combined.set( emis, lbvhLen );
|
|
1322
|
+
if ( trail ) combined.set( trail, lbvhLen + emisLen );
|
|
1323
|
+
|
|
1324
|
+
this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
|
|
1325
|
+
this.lightStorageNode.value = this.lightStorageAttr;
|
|
1326
|
+
this.lightStorageNode.bufferCount = combined.length / 4;
|
|
1327
|
+
|
|
1328
|
+
// Already-compiled kernels still bind the old attribute — request a rebuild.
|
|
1329
|
+
// (During scene load this is a no-op: setupMaterial rebuilds kernels anyway.)
|
|
1330
|
+
this._lightBufferRealloc = true;
|
|
1331
|
+
|
|
1332
|
+
}
|
|
1305
1333
|
|
|
1306
1334
|
// Offset (in vec4 elements) where emissive data starts.
|
|
1307
1335
|
this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
|
|
@@ -96,7 +96,9 @@ export const sphericalTriangleSolidAngle = Fn( ( [ v0, v1, v2, p ] ) => {
|
|
|
96
96
|
|
|
97
97
|
} );
|
|
98
98
|
|
|
99
|
-
// Heuristic: use spherical sampling when triangle is close/large (Blender Cycles approach)
|
|
99
|
+
// Heuristic: use spherical sampling when triangle is close/large (Blender Cycles approach).
|
|
100
|
+
// Shared by the samplers AND the MIS pdf re-walks (EmissiveSampling + LightBVHSampling) —
|
|
101
|
+
// both sides MUST take the same branch or MIS partition-of-unity breaks.
|
|
100
102
|
export const useSphericalSampling = Fn( ( [ v0, v1, v2, hitPoint ] ) => {
|
|
101
103
|
|
|
102
104
|
const e0 = v1.sub( v0 );
|
|
@@ -114,7 +116,14 @@ export const useSphericalSampling = Fn( ( [ v0, v1, v2, hitPoint ] ) => {
|
|
|
114
116
|
const d = dot( triNormal, hitPoint.sub( v0 ) );
|
|
115
117
|
// planeDist² = d² / |triNormal|²
|
|
116
118
|
const planeDistSq = d.mul( d ).div( triNormalLenSq );
|
|
117
|
-
|
|
119
|
+
|
|
120
|
+
// Sliver guard: Arvo spherical-triangle sampling is numerically unstable on
|
|
121
|
+
// high-aspect slivers (NaN/garbage → colored fireflies). |triNormal|² = (2A)²
|
|
122
|
+
// and 2A = longestEdge·height, so longestEdgeSq²/triNormalLenSq = (edge/height)².
|
|
123
|
+
// Require aspect < 8 (64 = 8²), else fall back to stable area sampling.
|
|
124
|
+
const notSliver = longestEdgeSq.mul( longestEdgeSq ).lessThan( triNormalLenSq.mul( 64.0 ) );
|
|
125
|
+
|
|
126
|
+
result.assign( longestEdgeSq.greaterThan( planeDistSq ).and( notSliver ) );
|
|
118
127
|
|
|
119
128
|
} );
|
|
120
129
|
|