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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.28.1",
3
+ "version": "7.28.2",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -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
- if ( object.isRectAreaLight && ! visitedAreaLights.includes( object.uuid ) ) {
1749
+ // An adopted light carries three.js units; convert as point/spot are below.
1750
+ if ( object.isRectAreaLight && ! userData.__radianceConverted ) {
1738
1751
 
1739
- visitedAreaLights.push( object.uuid );
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 shapeFactor = shape === 'ellipse' || shape === 'disk' ? Math.PI / 4 : 1;
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
- COUNT: 7,
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 ACTIVE_RAY_COUNT snapshots; read back async to size/skip late bounces next frame
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
 
@@ -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 (a stale early-exit only trims empty deep bounces).
99
+ // still used for the per-bounce early-exit regardless (it only trims bounces holding negligible energy).
99
100
  this._curveSizingValid = false;
100
- // 0.1% of primary ray count, floored at 100; -1 to disable. Updated per-scene in _buildWavefrontKernels.
101
- this._bounceEarlyExitThreshold = 100;
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
- bounce < curveReliableUpto
482
+ exitEnergy >= 0
483
+ && bounce < curveReliableUpto
478
484
  && bounce < loopBound
479
- && curve[ bounce ] !== undefined
480
- && curve[ bounce ] <= this._bounceEarlyExitThreshold
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
- this._lastBounceCounts = new Uint32Array( buf.slice( 0 ) );
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
  } );
@@ -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();
@@ -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