rayzee 7.28.1 → 7.28.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/dist/rayzee.es.js +688 -673
- package/dist/rayzee.umd.js +20 -20
- package/package.json +1 -1
- package/src/PathTracerApp.js +3 -0
- package/src/Processor/AssetLoader.js +19 -10
- package/src/Processor/QueueManager.js +9 -3
- package/src/Stages/PathTracer.js +20 -18
- package/src/TSL/CompactKernel.js +16 -3
- package/src/TSL/ShadeKernel.js +1 -0
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -728,6 +728,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
728
728
|
* by reference, and any ancestor transform is baked in. Later edits to `object3d` do not
|
|
729
729
|
* reach the render — mutate {@link sceneModel}, then {@link refitBVH}/{@link refitBLASes}.
|
|
730
730
|
*
|
|
731
|
+
* Lights keep three.js units — `RectAreaLight.intensity` in nits, point/spot in candela — and are
|
|
732
|
+
* converted to radiant power on the copy. `areaLightIntensityScale` does not apply here.
|
|
733
|
+
*
|
|
731
734
|
* @param {import('three').Object3D} object3d - The Object3D to render; left untouched.
|
|
732
735
|
* @param {string} [name='object3d'] - Display name for the object
|
|
733
736
|
*/
|
|
@@ -51,6 +51,19 @@ function standInForSplit( source ) {
|
|
|
51
51
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// three.js nits → the engine's radiant power, inverting areaLightRadiance.
|
|
55
|
+
function areaLightPowerFactor( node, width, height, userData ) {
|
|
56
|
+
|
|
57
|
+
if ( ! ( userData.normalize ?? true ) ) return Math.PI;
|
|
58
|
+
|
|
59
|
+
const shapeFactor = userData.shape === 'ellipse' || userData.shape === 'disk' ? Math.PI / 4 : 1;
|
|
60
|
+
const scale = node.getWorldScale( new Vector3() );
|
|
61
|
+
// abs: a mirrored ancestor decomposes negative; the serializer takes the area as |u × v|.
|
|
62
|
+
const factor = Math.PI * shapeFactor * Math.abs( width * scale.x * height * scale.y );
|
|
63
|
+
return Number.isFinite( factor ) ? factor : Math.PI;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* AssetLoader class - handles loading of 3D models, environment maps, and archives
|
|
56
69
|
*/
|
|
@@ -1726,7 +1739,6 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1726
1739
|
|
|
1727
1740
|
processModelObjects( model ) {
|
|
1728
1741
|
|
|
1729
|
-
let visitedAreaLights = [];
|
|
1730
1742
|
// Split after the walk: traverse() caches children.length, so splitting in place
|
|
1731
1743
|
// shifts later siblings down a slot and skips one.
|
|
1732
1744
|
const multiMaterialMeshes = [];
|
|
@@ -1734,9 +1746,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1734
1746
|
|
|
1735
1747
|
const userData = object.userData;
|
|
1736
1748
|
|
|
1737
|
-
|
|
1749
|
+
// An adopted light carries three.js units; convert as point/spot are below.
|
|
1750
|
+
if ( object.isRectAreaLight && ! userData.__radianceConverted ) {
|
|
1738
1751
|
|
|
1739
|
-
|
|
1752
|
+
object.intensity *= areaLightPowerFactor( object, object.width, object.height, userData );
|
|
1753
|
+
userData.__radianceConverted = true;
|
|
1740
1754
|
|
|
1741
1755
|
}
|
|
1742
1756
|
|
|
@@ -1759,14 +1773,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1759
1773
|
|
|
1760
1774
|
if ( userData.type === 'RectAreaLight' ) {
|
|
1761
1775
|
|
|
1762
|
-
// Authored intensity is three.js radiance (these files also carry power = intensity·w·h·π);
|
|
1763
|
-
// convert to the engine's radiant power through the world area so Normalize reproduces it.
|
|
1764
|
-
const worldScale = object.getWorldScale( new Vector3() );
|
|
1765
1776
|
const normalize = userData.normalize ?? true;
|
|
1766
1777
|
const shape = userData.shape ?? 'rectangle';
|
|
1767
|
-
const
|
|
1768
|
-
const worldArea = shapeFactor * userData.width * worldScale.x * userData.height * worldScale.y;
|
|
1769
|
-
const power = userData.intensity * Math.PI * ( normalize ? worldArea : 1 );
|
|
1778
|
+
const power = userData.intensity * areaLightPowerFactor( object, userData.width, userData.height, userData );
|
|
1770
1779
|
const light = new RectAreaLight(
|
|
1771
1780
|
new Color( ...userData.color ),
|
|
1772
1781
|
power * this._profile.areaLightIntensityScale,
|
|
@@ -1776,9 +1785,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1776
1785
|
light.userData.normalize = normalize;
|
|
1777
1786
|
light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
|
|
1778
1787
|
light.userData.shape = shape;
|
|
1788
|
+
light.userData.__radianceConverted = true; // already power, and traverse() reaches it
|
|
1779
1789
|
light.name = userData.name;
|
|
1780
1790
|
object.add( light );
|
|
1781
|
-
visitedAreaLights.push( light.uuid );
|
|
1782
1791
|
|
|
1783
1792
|
}
|
|
1784
1793
|
|
|
@@ -27,9 +27,15 @@ export const COUNTER = {
|
|
|
27
27
|
// bits restricted to pixels that hit geometry, so the stop can require both fractions.
|
|
28
28
|
GEOMETRY_COUNT: 5,
|
|
29
29
|
CONVERGED_GEOMETRY_COUNT: 6,
|
|
30
|
-
|
|
30
|
+
// Survivors' summed throughput (max channel, fixed-point ENERGY_SCALE). The early exit gates on this,
|
|
31
|
+
// not the count: past Russian roulette a few survivors carry the weight of many.
|
|
32
|
+
ACTIVE_ENERGY: 7,
|
|
33
|
+
COUNT: 8,
|
|
31
34
|
};
|
|
32
35
|
|
|
36
|
+
export const ENERGY_SCALE = 64;
|
|
37
|
+
export const ENERGY_RAY_CLAMP = 65535;
|
|
38
|
+
|
|
33
39
|
/** Ray flag bits packed into rayBounceFlags (uint) */
|
|
34
40
|
export const RAY_FLAG = {
|
|
35
41
|
BOUNCE_MASK: 0xFF, // bits 0-7: bounce count (0-255)
|
|
@@ -81,10 +87,10 @@ export class QueueManager {
|
|
|
81
87
|
this._countersAttr = new StorageInstancedBufferAttribute( new Uint32Array( COUNTER.COUNT ), 1 );
|
|
82
88
|
this.counters = storage( this._countersAttr, 'uint' ).toAtomic();
|
|
83
89
|
|
|
84
|
-
// per-bounce
|
|
90
|
+
// per-bounce snapshots for the async readback: [0, MAX) ACTIVE_RAY_COUNT, [MAX, 2·MAX) ACTIVE_ENERGY
|
|
85
91
|
this.MAX_BOUNCE_SNAPSHOTS = 32;
|
|
86
92
|
this._bounceCountsAttr = new StorageInstancedBufferAttribute(
|
|
87
|
-
new Uint32Array( this.MAX_BOUNCE_SNAPSHOTS ), 1,
|
|
93
|
+
new Uint32Array( 2 * this.MAX_BOUNCE_SNAPSHOTS ), 1,
|
|
88
94
|
);
|
|
89
95
|
this.bounceCounts = storage( this._bounceCountsAttr, 'uint' );
|
|
90
96
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -8,7 +8,7 @@ import { uniform, texture, storage } from 'three/tsl';
|
|
|
8
8
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
9
9
|
import { PathTracerStage } from './PathTracerStage.js';
|
|
10
10
|
import { PackedRayBuffer, GBUFFER_STRIDE, RAY_STRIDE, HIT_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
|
|
11
|
-
import { QueueManager, COUNTER } from '../Processor/QueueManager.js';
|
|
11
|
+
import { QueueManager, COUNTER, ENERGY_SCALE } from '../Processor/QueueManager.js';
|
|
12
12
|
import { VRAMTracker } from '../Processor/VRAMTracker.js';
|
|
13
13
|
import { KernelManager } from '../Processor/KernelManager.js';
|
|
14
14
|
import { buildGenerateKernel, GENERATE_WG_SIZE } from '../TSL/GenerateKernel.js';
|
|
@@ -85,6 +85,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
85
85
|
this._useSubgroupCompact = false;
|
|
86
86
|
|
|
87
87
|
this._lastBounceCounts = null;
|
|
88
|
+
this._lastBounceEnergy = null;
|
|
88
89
|
// maxBounces the curve was measured at; the curve is ignored once this no longer matches (-1 = none).
|
|
89
90
|
this._lastBounceCountsBudget = - 1;
|
|
90
91
|
this._readbackPending = false;
|
|
@@ -95,10 +96,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
95
96
|
// Whether the survivor curve may be trusted to SIZE the dispatch. False during/after camera motion
|
|
96
97
|
// (or resize) until a readback measured at the settled view re-validates it — full-size until then so
|
|
97
98
|
// a stale/mid-motion curve can't under-size the row-major list and drop the bottom rows. The curve is
|
|
98
|
-
// still used for the per-bounce early-exit regardless (
|
|
99
|
+
// still used for the per-bounce early-exit regardless (it only trims bounces holding negligible energy).
|
|
99
100
|
this._curveSizingValid = false;
|
|
100
|
-
//
|
|
101
|
-
|
|
101
|
+
// Early exit once the survivors' summed throughput is below this fraction of a full frame's; -1
|
|
102
|
+
// disables. Never a ray count: after Russian roulette a handful of survivors carry real weight.
|
|
103
|
+
this._bounceEarlyExitThreshold = 1e-4;
|
|
102
104
|
|
|
103
105
|
// Tier-1 convergence early-stop: fraction of pixels converged in the last settled-view readback, and a
|
|
104
106
|
// single-flight guard for its async counter read. Zeroed on reset/camera-move/resize; never refreshed
|
|
@@ -400,6 +402,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
400
402
|
|
|
401
403
|
}
|
|
402
404
|
|
|
405
|
+
const energyCurve = this._lastBounceEnergy;
|
|
406
|
+
const exitEnergy = this._bounceEarlyExitThreshold >= 0 ? this._bounceEarlyExitThreshold * maxRays * ENERGY_SCALE : - 1;
|
|
407
|
+
|
|
403
408
|
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
404
409
|
|
|
405
410
|
this._wfCurrentBounce.value = bounce;
|
|
@@ -474,10 +479,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
474
479
|
|
|
475
480
|
// Early-exit on last frame's per-bounce snapshot (single-chunk only; curveReliableUpto=0 disables it for multi-chunk).
|
|
476
481
|
if (
|
|
477
|
-
|
|
482
|
+
exitEnergy >= 0
|
|
483
|
+
&& bounce < curveReliableUpto
|
|
478
484
|
&& bounce < loopBound
|
|
479
|
-
&&
|
|
480
|
-
&&
|
|
485
|
+
&& energyCurve?.[ bounce ] !== undefined
|
|
486
|
+
&& energyCurve[ bounce ] <= exitEnergy
|
|
481
487
|
) {
|
|
482
488
|
|
|
483
489
|
break;
|
|
@@ -732,6 +738,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
732
738
|
this._readbackPending = true;
|
|
733
739
|
const gen = this._readbackGeneration;
|
|
734
740
|
const budget = this.maxBounces.value;
|
|
741
|
+
const n = this._queueManager.MAX_BOUNCE_SNAPSHOTS;
|
|
735
742
|
this.renderer.getArrayBufferAsync( attr ).then( ( buf ) => {
|
|
736
743
|
|
|
737
744
|
// Drop counts measured at a now-stale generation (a resize or camera move happened mid-flight).
|
|
@@ -739,7 +746,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
739
746
|
// happened before it resolved, so its counts match the current view — safe to size from.
|
|
740
747
|
if ( gen === this._readbackGeneration ) {
|
|
741
748
|
|
|
742
|
-
|
|
749
|
+
const all = new Uint32Array( buf.slice( 0 ) );
|
|
750
|
+
this._lastBounceCounts = all.subarray( 0, n );
|
|
751
|
+
this._lastBounceEnergy = all.subarray( n, 2 * n );
|
|
743
752
|
this._lastBounceCountsBudget = budget;
|
|
744
753
|
this._curveSizingValid = true;
|
|
745
754
|
|
|
@@ -826,6 +835,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
826
835
|
// until the readback re-measures at the new size; bump the generation so any readback already
|
|
827
836
|
// in flight (carrying the old-resolution counts) is discarded when it resolves.
|
|
828
837
|
this._lastBounceCounts = null;
|
|
838
|
+
this._lastBounceEnergy = null;
|
|
829
839
|
this._lastBounceCountsBudget = - 1;
|
|
830
840
|
this._curveSizingValid = false;
|
|
831
841
|
this._readbackFrameCounter = 0;
|
|
@@ -861,11 +871,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
861
871
|
this._wfRenderWidth.value = w;
|
|
862
872
|
this._wfRenderHeight.value = h;
|
|
863
873
|
this._updateChunkLayout();
|
|
864
|
-
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
865
|
-
|
|
866
|
-
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( ( w * h ) / 1000 ) );
|
|
867
|
-
|
|
868
|
-
}
|
|
869
874
|
|
|
870
875
|
}
|
|
871
876
|
|
|
@@ -969,11 +974,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
969
974
|
// every per-ray kernel's dispatch per chunk; this is just a valid starting grid (always ≤ B).
|
|
970
975
|
const maxRays = this._chunkRows * w;
|
|
971
976
|
|
|
972
|
-
if ( this._bounceEarlyExitThreshold !== - 1 ) {
|
|
973
|
-
|
|
974
|
-
this._bounceEarlyExitThreshold = Math.max( 100, Math.floor( ( w * h ) / 1000 ) );
|
|
975
|
-
|
|
976
|
-
}
|
|
977
977
|
|
|
978
978
|
// Per-path buffers (RAY/HIT/rng) sized to the budget B and indexed by LOCAL slot r ∈ [0,B). _cap = B is
|
|
979
979
|
// baked into the SoA stride but never changes with resolution, so this build happens once (model load).
|
|
@@ -1068,6 +1068,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1068
1068
|
const cnt = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
1069
1069
|
const slot = uint( wfCurrentBounce ).clamp( uint( 0 ), uint( qm.MAX_BOUNCE_SNAPSHOTS - 1 ) );
|
|
1070
1070
|
bounceCountsBuf.element( slot ).assign( cnt );
|
|
1071
|
+
bounceCountsBuf.element( slot.add( uint( qm.MAX_BOUNCE_SNAPSHOTS ) ) ).assign( atomicLoad( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ) ) );
|
|
1071
1072
|
// Also set ENTERING_COUNT for the next bounce; the full-dispatch path's enterFull overrides it.
|
|
1072
1073
|
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), cnt );
|
|
1073
1074
|
|
|
@@ -1522,6 +1523,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1522
1523
|
|
|
1523
1524
|
const slot = uint( wfCurrentBounce ).clamp( uint( 0 ), uint( qm.MAX_BOUNCE_SNAPSHOTS - 1 ) );
|
|
1524
1525
|
bounceCountsBuf.element( slot ).assign( active );
|
|
1526
|
+
bounceCountsBuf.element( slot.add( uint( qm.MAX_BOUNCE_SNAPSHOTS ) ) ).assign( atomicLoad( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ) ) );
|
|
1525
1527
|
atomicStore( counters.element( uint( COUNTER.ENTERING_COUNT ) ), active );
|
|
1526
1528
|
|
|
1527
1529
|
} );
|
package/src/TSL/CompactKernel.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
Fn, uint, select,
|
|
6
|
+
Fn, uint, select, max, min,
|
|
7
7
|
If,
|
|
8
8
|
instanceIndex,
|
|
9
9
|
atomicAdd, atomicLoad,
|
|
@@ -11,11 +11,20 @@ import {
|
|
|
11
11
|
Return,
|
|
12
12
|
} from 'three/tsl';
|
|
13
13
|
|
|
14
|
-
import { readRayBounceFlags } from '../Processor/PackedRayBuffer.js';
|
|
15
|
-
import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
14
|
+
import { readRayBounceFlags, readRayThroughput } from '../Processor/PackedRayBuffer.js';
|
|
15
|
+
import { RAY_FLAG, COUNTER, ENERGY_SCALE, ENERGY_RAY_CLAMP } from '../Processor/QueueManager.js';
|
|
16
16
|
|
|
17
17
|
const WG_SIZE = 256;
|
|
18
18
|
|
|
19
|
+
// Survivor's remaining throughput as fixed-point u32; clamped so a frame's sum cannot wrap.
|
|
20
|
+
const rayEnergy = ( rayBufferRO, rayID ) => {
|
|
21
|
+
|
|
22
|
+
const t = readRayThroughput( rayBufferRO, rayID );
|
|
23
|
+
const m = max( max( t.x, t.y ), t.z ).max( 0.0 );
|
|
24
|
+
return min( uint( m.mul( ENERGY_SCALE ).add( 0.5 ) ), uint( ENERGY_RAY_CLAMP ) );
|
|
25
|
+
|
|
26
|
+
};
|
|
27
|
+
|
|
19
28
|
export function buildCompactKernel( params ) {
|
|
20
29
|
|
|
21
30
|
const {
|
|
@@ -46,6 +55,7 @@ export function buildCompactKernel( params ) {
|
|
|
46
55
|
|
|
47
56
|
const writeIdx = atomicAdd( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 1 ) );
|
|
48
57
|
activeIndicesWriteRW.element( writeIdx ).assign( rayID );
|
|
58
|
+
atomicAdd( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ), rayEnergy( rayBufferRO, rayID ) );
|
|
49
59
|
|
|
50
60
|
} );
|
|
51
61
|
|
|
@@ -80,10 +90,12 @@ export function buildCompactSubgroupKernel( params ) {
|
|
|
80
90
|
const flags = readRayBounceFlags( rayBufferRO, rayID );
|
|
81
91
|
const isActive = inRange.and( flags.bitAnd( uint( RAY_FLAG.ACTIVE ) ).notEqual( uint( 0 ) ) );
|
|
82
92
|
const activeU = select( isActive, uint( 1 ), uint( 0 ) );
|
|
93
|
+
const energyU = select( isActive, rayEnergy( rayBufferRO, rayID ), uint( 0 ) );
|
|
83
94
|
|
|
84
95
|
// .toVar() materializes the subgroup ops at uniform control flow; inlining into the divergent If(isActive) write is rejected by WGSL.
|
|
85
96
|
const localOffset = subgroupExclusiveAdd( activeU ).toVar();
|
|
86
97
|
const sgCount = subgroupAdd( activeU ).toVar();
|
|
98
|
+
const sgEnergy = subgroupAdd( energyU ).toVar();
|
|
87
99
|
|
|
88
100
|
// laneId via exclusiveAdd(1) since TSL lacks subgroup_invocation_id; lane 0 does the single per-subgroup atomicAdd.
|
|
89
101
|
const laneId = subgroupExclusiveAdd( uint( 1 ) ).toVar();
|
|
@@ -91,6 +103,7 @@ export function buildCompactSubgroupKernel( params ) {
|
|
|
91
103
|
If( laneId.equal( uint( 0 ) ), () => {
|
|
92
104
|
|
|
93
105
|
base.assign( atomicAdd( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), sgCount ) );
|
|
106
|
+
atomicAdd( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ), sgEnergy );
|
|
94
107
|
|
|
95
108
|
} );
|
|
96
109
|
const sgBase = subgroupBroadcast( base, uint( 0 ) ).toVar();
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -155,6 +155,7 @@ export function buildShadeKernel( params ) {
|
|
|
155
155
|
If( threadIdx.equal( uint( 0 ) ), () => {
|
|
156
156
|
|
|
157
157
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
158
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ), uint( 0 ) );
|
|
158
159
|
|
|
159
160
|
} );
|
|
160
161
|
|