rayzee 7.28.1 → 7.29.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/dist/rayzee.es.js +961 -913
- 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/Processor/SceneProcessor.js +1 -1
- package/src/Processor/TextureCreator.js +102 -5
- 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
|
|
|
@@ -836,7 +836,7 @@ export class SceneProcessor {
|
|
|
836
836
|
// (the prior model-load path omitted this, leaving albedo un-decoded / too bright).
|
|
837
837
|
const buildBucket = ( list, srgb ) => list.length === 0
|
|
838
838
|
? Promise.resolve( null )
|
|
839
|
-
: this.textureCreator.createTexturesToDataTexture( list ).then( tex => {
|
|
839
|
+
: this.textureCreator.createTexturesToDataTexture( list, { srgbPool: srgb } ).then( tex => {
|
|
840
840
|
|
|
841
841
|
if ( tex && srgb ) tex.colorSpace = SRGBColorSpace;
|
|
842
842
|
return tex;
|
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
|
|
1
|
+
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, LinearSRGBColorSpace, RepeatWrapping } from "three";
|
|
2
2
|
import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT, normalizeAttenuationDistance } from '../EngineDefaults.js';
|
|
3
3
|
import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
|
|
4
4
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
5
|
+
import { linearToSRGB } from './ToneMapCPU.js';
|
|
6
|
+
import { createLogger } from '../utils/Logger.js';
|
|
7
|
+
|
|
8
|
+
const log = createLogger( 'textures' );
|
|
9
|
+
|
|
10
|
+
// A bucket array carries ONE colorSpace and the shader reads each slot from a fixed pool, so a
|
|
11
|
+
// texture disagreeing with its pool cannot be re-routed — its pixels are re-encoded instead.
|
|
12
|
+
const TRANSFER_LUT = { toSRGB: null, toLinear: null };
|
|
13
|
+
|
|
14
|
+
function transferLUT( direction ) {
|
|
15
|
+
|
|
16
|
+
if ( ! TRANSFER_LUT[ direction ] ) {
|
|
17
|
+
|
|
18
|
+
const lut = new Uint8Array( 256 );
|
|
19
|
+
for ( let i = 0; i < 256; i ++ ) {
|
|
20
|
+
|
|
21
|
+
const c = i / 255;
|
|
22
|
+
const v = direction === 'toSRGB'
|
|
23
|
+
? linearToSRGB( c )
|
|
24
|
+
: ( c <= 0.04045 ? c / 12.92 : Math.pow( ( c + 0.055 ) / 1.055, 2.4 ) );
|
|
25
|
+
lut[ i ] = Math.max( 0, Math.min( 255, Math.round( v * 255 ) ) );
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
TRANSFER_LUT[ direction ] = lut;
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return TRANSFER_LUT[ direction ];
|
|
34
|
+
|
|
35
|
+
}
|
|
5
36
|
|
|
6
37
|
// Canvas pooling for efficient reuse of canvas elements
|
|
7
38
|
class CanvasPool {
|
|
@@ -541,13 +572,76 @@ export class TextureCreator {
|
|
|
541
572
|
|
|
542
573
|
}
|
|
543
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Re-encodes any layer whose declared colour space disagrees with its pool, so the GPU's
|
|
577
|
+
* decode — or absence of one — lands on the values the author stored. An untagged texture
|
|
578
|
+
* keeps its pool's convention, which is what every loader produces for glTF.
|
|
579
|
+
*
|
|
580
|
+
* @param {import('three').DataArrayTexture} arrayTex
|
|
581
|
+
* @param {Array<import('three').Texture>} textures - pre-normalization list, in layer order
|
|
582
|
+
* @param {boolean} srgbPool - true when the array is tagged SRGBColorSpace
|
|
583
|
+
*/
|
|
584
|
+
_harmonizeTransfer( arrayTex, textures, srgbPool ) {
|
|
585
|
+
|
|
586
|
+
const data = arrayTex?.image?.data;
|
|
587
|
+
if ( ! data ) return;
|
|
588
|
+
|
|
589
|
+
// Mirrors _normalizeTexturesForProcessing's skip rule, which is what sets layer order.
|
|
590
|
+
const kept = textures.filter( t => t?.image );
|
|
591
|
+
const want = srgbPool ? SRGBColorSpace : LinearSRGBColorSpace;
|
|
592
|
+
const layerSize = arrayTex.image.width * arrayTex.image.height * 4;
|
|
593
|
+
const converted = [];
|
|
594
|
+
|
|
595
|
+
for ( let layer = 0; layer < kept.length; layer ++ ) {
|
|
596
|
+
|
|
597
|
+
const declared = kept[ layer ].colorSpace;
|
|
598
|
+
// Only an explicit disagreement is acted on; NoColorSpace defers to the pool.
|
|
599
|
+
if ( declared !== SRGBColorSpace && declared !== LinearSRGBColorSpace ) continue;
|
|
600
|
+
if ( declared === want ) continue;
|
|
601
|
+
|
|
602
|
+
const lut = transferLUT( srgbPool ? 'toSRGB' : 'toLinear' );
|
|
603
|
+
const start = layer * layerSize;
|
|
604
|
+
const end = Math.min( start + layerSize, data.length );
|
|
605
|
+
// RGB only — alpha is never transfer-encoded.
|
|
606
|
+
for ( let i = start; i < end; i += 4 ) {
|
|
607
|
+
|
|
608
|
+
data[ i ] = lut[ data[ i ] ];
|
|
609
|
+
data[ i + 1 ] = lut[ data[ i + 1 ] ];
|
|
610
|
+
data[ i + 2 ] = lut[ data[ i + 2 ] ];
|
|
611
|
+
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
converted.push( kept[ layer ].name || kept[ layer ].source?.uuid?.slice( 0, 8 ) || `layer ${layer}` );
|
|
615
|
+
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if ( converted.length === 0 ) return;
|
|
619
|
+
|
|
620
|
+
arrayTex.needsUpdate = true;
|
|
621
|
+
const names = converted.slice( 0, 4 ).join( ', ' ) + ( converted.length > 4 ? `, +${converted.length - 4} more` : '' );
|
|
622
|
+
|
|
623
|
+
if ( srgbPool ) {
|
|
624
|
+
|
|
625
|
+
log.info( `re-encoded ${converted.length}/${kept.length} linear-declared colour map(s) to sRGB so the GPU decode round-trips: ${names}` );
|
|
626
|
+
|
|
627
|
+
} else {
|
|
628
|
+
|
|
629
|
+
// Storing a decoded value in an 8-bit linear array is the lossy direction, and a
|
|
630
|
+
// transfer-encoded data map is almost always an authoring slip rather than intent.
|
|
631
|
+
log.warn( `decoded ${converted.length}/${kept.length} sRGB-declared data map(s) to linear — precision is lost in the darks, check the asset: ${names}` );
|
|
632
|
+
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
|
|
544
637
|
// Unified texture processing with strategy selection
|
|
545
|
-
async createTexturesToDataTexture( textures ) {
|
|
638
|
+
async createTexturesToDataTexture( textures, { srgbPool = false } = {} ) {
|
|
546
639
|
|
|
547
640
|
if ( ! textures || textures.length === 0 ) return null;
|
|
548
641
|
|
|
549
|
-
//
|
|
550
|
-
|
|
642
|
+
// Pool is part of the identity: the same list packed for the colour pool and the data pool
|
|
643
|
+
// gets different pixels (see _harmonizeTransfer), so one key must not serve both.
|
|
644
|
+
const cacheKey = `${srgbPool ? 's' : 'l'}:${this.textureCache.generateHash( textures )}`;
|
|
551
645
|
const cached = this.textureCache.get( cacheKey );
|
|
552
646
|
if ( cached ) return cached;
|
|
553
647
|
|
|
@@ -579,6 +673,7 @@ export class TextureCreator {
|
|
|
579
673
|
// Cache successful result
|
|
580
674
|
if ( result ) {
|
|
581
675
|
|
|
676
|
+
this._harmonizeTransfer( result, textures, srgbPool );
|
|
582
677
|
this.textureCache.set( cacheKey, result );
|
|
583
678
|
|
|
584
679
|
}
|
|
@@ -592,7 +687,9 @@ export class TextureCreator {
|
|
|
592
687
|
'worker texture processing failed — retrying on the main thread',
|
|
593
688
|
{ cause: String( error?.message ?? error ) }
|
|
594
689
|
);
|
|
595
|
-
|
|
690
|
+
const fallback = await this.processOnMainThreadSync( normalized );
|
|
691
|
+
this._harmonizeTransfer( fallback, textures, srgbPool );
|
|
692
|
+
return fallback;
|
|
596
693
|
|
|
597
694
|
} finally {
|
|
598
695
|
|
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
|
|