rayzee 7.15.1 → 7.16.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 +1 -1
- package/dist/rayzee.es.js +219 -122
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +5 -5
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +113 -20
- package/src/Processor/EmissiveTriangleBuilder.js +74 -20
- package/src/Processor/SceneProcessor.js +35 -5
- package/src/RenderSettings.js +4 -2
- package/src/Stages/PathTracer.js +14 -0
- package/src/Stages/PathTracerStage.js +36 -8
- package/src/TSL/EmissiveSampling.js +11 -2
- package/src/managers/CameraManager.js +160 -9
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -775,11 +775,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
775
775
|
this.reset();
|
|
776
776
|
this.cameraManager.currentCameraIndex = 0;
|
|
777
777
|
this.dispatchEvent( eventPayload );
|
|
778
|
-
this.
|
|
779
|
-
type: 'CamerasUpdated',
|
|
780
|
-
cameras: this.cameraManager.cameras,
|
|
781
|
-
cameraNames: this.cameraManager.getCameraNames()
|
|
782
|
-
} );
|
|
778
|
+
this._dispatchCamerasUpdated();
|
|
783
779
|
|
|
784
780
|
} finally {
|
|
785
781
|
|
|
@@ -838,6 +834,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
838
834
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
839
835
|
this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
|
|
840
836
|
|
|
837
|
+
// Drop authored-hidden meshes' triangles from the emissive-NEE structure
|
|
838
|
+
// (runs before setupMaterial so the kernels compile against the final buffer)
|
|
839
|
+
this._refreshEmissiveForVisibility();
|
|
840
|
+
|
|
841
841
|
timer.end( 'GPU data transfer' );
|
|
842
842
|
|
|
843
843
|
// Compile shaders
|
|
@@ -882,7 +882,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
882
882
|
// Runs before applyAll()/SceneRebuild so the uniform and UI both pick up the new value.
|
|
883
883
|
if ( ! this._emissiveSamplingUserSet ) {
|
|
884
884
|
|
|
885
|
-
|
|
885
|
+
// Keyed on the canonical (unfiltered) emissive set — emissiveTriangleCount
|
|
886
|
+
// reflects only the visible subset, and hidden emitters can be shown later.
|
|
887
|
+
const hasEmissive = ( this._sdf?.emissiveTriangleBuilder?.emissiveTriangles?.length
|
|
888
|
+
?? this._sdf?.emissiveTriangleCount ?? 0 ) > 0;
|
|
886
889
|
this.settings.set( 'enableEmissiveTriangleSampling', hasEmissive, { reset: false } );
|
|
887
890
|
|
|
888
891
|
}
|
|
@@ -1086,6 +1089,57 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1086
1089
|
|
|
1087
1090
|
}
|
|
1088
1091
|
|
|
1092
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1093
|
+
// Dynamic cameras (add / remove)
|
|
1094
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1095
|
+
|
|
1096
|
+
/** The index of the currently active camera (0 = built-in default). */
|
|
1097
|
+
get currentCameraIndex() {
|
|
1098
|
+
|
|
1099
|
+
return this.cameraManager?.currentCameraIndex ?? 0;
|
|
1100
|
+
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Snapshot the current view as a new named camera and switch to it.
|
|
1105
|
+
* @param {Object} [opts]
|
|
1106
|
+
* @param {string} [opts.name] - Display name (auto-generated otherwise).
|
|
1107
|
+
* @returns {number} The index of the newly added camera.
|
|
1108
|
+
*/
|
|
1109
|
+
addCamera( { name } = {} ) {
|
|
1110
|
+
|
|
1111
|
+
const index = this.cameraManager.addCameraFromView( name );
|
|
1112
|
+
this.cameraManager.switchCamera( index );
|
|
1113
|
+
this._dispatchCamerasUpdated();
|
|
1114
|
+
return index;
|
|
1115
|
+
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Remove a user-added camera by index. Built-in and model-embedded cameras
|
|
1120
|
+
* are protected. Falls back to the default camera if the active one is removed.
|
|
1121
|
+
* @param {number} index
|
|
1122
|
+
* @returns {boolean} true if a camera was removed.
|
|
1123
|
+
*/
|
|
1124
|
+
removeCamera( index ) {
|
|
1125
|
+
|
|
1126
|
+
const removed = this.cameraManager.removeCamera( index );
|
|
1127
|
+
if ( removed ) this._dispatchCamerasUpdated();
|
|
1128
|
+
return removed;
|
|
1129
|
+
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Notify consumers that the camera list changed (names / count). */
|
|
1133
|
+
_dispatchCamerasUpdated() {
|
|
1134
|
+
|
|
1135
|
+
this.dispatchEvent( {
|
|
1136
|
+
type: 'CamerasUpdated',
|
|
1137
|
+
cameras: this.cameraManager.cameras,
|
|
1138
|
+
cameraNames: this.cameraManager.getCameraNames(),
|
|
1139
|
+
} );
|
|
1140
|
+
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1089
1143
|
// ═══════════════════════════════════════════════════════════════
|
|
1090
1144
|
// BVH Refit (Animation)
|
|
1091
1145
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1504,27 +1558,60 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1504
1558
|
|
|
1505
1559
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
1506
1560
|
|
|
1561
|
+
// Keep the emissive-NEE structure in sync unconditionally (not gated on the
|
|
1562
|
+
// sampling toggle) so edits made while NEE is off aren't lost on re-enable.
|
|
1507
1563
|
const emissiveAffectingProps = [ 'emissive', 'emissiveIntensity' ];
|
|
1508
|
-
if ( emissiveAffectingProps.includes( property )
|
|
1509
|
-
&& this.stages.pathTracer?.enableEmissiveTriangleSampling?.value ) {
|
|
1564
|
+
if ( emissiveAffectingProps.includes( property ) && this._sdf ) {
|
|
1510
1565
|
|
|
1511
|
-
|
|
1512
|
-
if ( result ) {
|
|
1566
|
+
this._uploadEmissivePayload( this._sdf.updateMaterialEmissive( materialIndex, property, value ) );
|
|
1513
1567
|
|
|
1514
|
-
|
|
1515
|
-
result.rawData, result.emissiveCount, result.totalPower, result.bitTrailMap,
|
|
1516
|
-
);
|
|
1517
|
-
if ( result.lightBVHNodeData ) {
|
|
1568
|
+
}
|
|
1518
1569
|
|
|
1519
|
-
|
|
1570
|
+
this.reset();
|
|
1520
1571
|
|
|
1521
|
-
|
|
1572
|
+
}
|
|
1522
1573
|
|
|
1523
|
-
|
|
1574
|
+
/**
|
|
1575
|
+
* Upload a rebuilt emissive-NEE payload (sorted emissive data + Light BVH +
|
|
1576
|
+
* bit-trail map) to the path tracer stage. No-op for a null payload.
|
|
1577
|
+
* @param {object|null} payload
|
|
1578
|
+
* @private
|
|
1579
|
+
*/
|
|
1580
|
+
_uploadEmissivePayload( payload ) {
|
|
1581
|
+
|
|
1582
|
+
if ( ! payload || ! this.stages.pathTracer ) return;
|
|
1583
|
+
|
|
1584
|
+
this.stages.pathTracer.setEmissiveTriangleData(
|
|
1585
|
+
payload.rawData, payload.emissiveCount, payload.totalPower, payload.bitTrailMap,
|
|
1586
|
+
);
|
|
1587
|
+
if ( payload.lightBVHNodeData ) {
|
|
1588
|
+
|
|
1589
|
+
this.stages.pathTracer.setLightBVHData( payload.lightBVHNodeData, payload.lightBVHNodeCount );
|
|
1524
1590
|
|
|
1525
1591
|
}
|
|
1526
1592
|
|
|
1527
|
-
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Re-derive the emissive-NEE sampled set from current per-mesh world-visibility
|
|
1597
|
+
* (InstanceTable is the source of truth — patched by the stage's visibility API).
|
|
1598
|
+
* Hidden meshes' triangles are dropped from the Light BVH so they stop casting
|
|
1599
|
+
* NEE light; the shadow ray can't do it (a hidden mesh no longer self-occludes).
|
|
1600
|
+
* @private
|
|
1601
|
+
*/
|
|
1602
|
+
_refreshEmissiveForVisibility() {
|
|
1603
|
+
|
|
1604
|
+
const entries = this._sdf?.instanceTable?.entries;
|
|
1605
|
+
if ( ! entries ) return;
|
|
1606
|
+
|
|
1607
|
+
const hidden = new Set();
|
|
1608
|
+
for ( const entry of entries ) {
|
|
1609
|
+
|
|
1610
|
+
if ( entry && entry.visible === false ) hidden.add( entry.meshIndex );
|
|
1611
|
+
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
this._uploadEmissivePayload( this._sdf.rebuildEmissiveForVisibility( hidden ) );
|
|
1528
1615
|
|
|
1529
1616
|
}
|
|
1530
1617
|
|
|
@@ -1542,11 +1629,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1542
1629
|
}
|
|
1543
1630
|
|
|
1544
1631
|
/**
|
|
1545
|
-
* @returns {boolean} True if the current scene contains emissive geometry
|
|
1632
|
+
* @returns {boolean} True if the current scene contains emissive geometry
|
|
1633
|
+
* (regardless of per-mesh visibility).
|
|
1546
1634
|
*/
|
|
1547
1635
|
hasEmissiveGeometry() {
|
|
1548
1636
|
|
|
1549
|
-
return ( this._sdf?.
|
|
1637
|
+
return ( this._sdf?.emissiveTriangleBuilder?.emissiveTriangles?.length
|
|
1638
|
+
?? this._sdf?.emissiveTriangleCount ?? 0 ) > 0;
|
|
1550
1639
|
|
|
1551
1640
|
}
|
|
1552
1641
|
|
|
@@ -1559,6 +1648,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1559
1648
|
setMeshVisibility( meshIndex, visible ) {
|
|
1560
1649
|
|
|
1561
1650
|
this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible );
|
|
1651
|
+
this._refreshEmissiveForVisibility();
|
|
1562
1652
|
this.reset();
|
|
1563
1653
|
|
|
1564
1654
|
}
|
|
@@ -1570,6 +1660,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1570
1660
|
updateAllMeshVisibility() {
|
|
1571
1661
|
|
|
1572
1662
|
this.stages.pathTracer?.updateAllMeshVisibility();
|
|
1663
|
+
this._refreshEmissiveForVisibility();
|
|
1573
1664
|
this.reset();
|
|
1574
1665
|
|
|
1575
1666
|
}
|
|
@@ -1830,6 +1921,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1830
1921
|
onResize: () => this.onResize(),
|
|
1831
1922
|
onReset: () => this.reset(),
|
|
1832
1923
|
getSettings: ( k ) => this.settings.get( k ),
|
|
1924
|
+
// Per-camera DOF restore — silent + reset:false so switchCamera's own onReset() is the single reset.
|
|
1925
|
+
applySettings: ( updates ) => this.settings.setMany( updates, { silent: true, reset: false } ),
|
|
1833
1926
|
} );
|
|
1834
1927
|
|
|
1835
1928
|
// Auto-focus context — CameraManager stores it, reads it each frame
|
|
@@ -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/RenderSettings.js
CHANGED
|
@@ -252,8 +252,9 @@ export class RenderSettings extends EventDispatcher {
|
|
|
252
252
|
* @param {Object} updates - Key/value pairs
|
|
253
253
|
* @param {Object} [options]
|
|
254
254
|
* @param {boolean} [options.silent] - Suppress settingChanged events
|
|
255
|
+
* @param {boolean} [options.reset] - Override the routes' default reset behavior
|
|
255
256
|
*/
|
|
256
|
-
setMany( updates, { silent } = {} ) {
|
|
257
|
+
setMany( updates, { silent, reset } = {} ) {
|
|
257
258
|
|
|
258
259
|
let needsReset = false;
|
|
259
260
|
|
|
@@ -279,7 +280,8 @@ export class RenderSettings extends EventDispatcher {
|
|
|
279
280
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
const shouldReset = reset !== undefined ? reset : needsReset;
|
|
284
|
+
if ( shouldReset ) this._resetCallback?.();
|
|
283
285
|
|
|
284
286
|
}
|
|
285
287
|
|
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
|
|