rayzee 6.5.0 → 7.1.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.
Files changed (51) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +7624 -7063
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +26 -9
  8. package/src/PathTracerApp.js +118 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +6 -0
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +291 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -317
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/VRAMTracker.js +169 -0
  21. package/src/Processor/utils.js +11 -110
  22. package/src/RenderSettings.js +0 -3
  23. package/src/Stages/ASVGF.js +151 -78
  24. package/src/Stages/BilateralFilter.js +34 -10
  25. package/src/Stages/EdgeFilter.js +2 -3
  26. package/src/Stages/MotionVector.js +16 -9
  27. package/src/Stages/NormalDepth.js +17 -5
  28. package/src/Stages/PathTracer.js +671 -1456
  29. package/src/Stages/PathTracerStage.js +1451 -0
  30. package/src/Stages/SSRC.js +32 -15
  31. package/src/Stages/Variance.js +35 -12
  32. package/src/TSL/CompactKernel.js +110 -0
  33. package/src/TSL/DebugKernel.js +98 -0
  34. package/src/TSL/Environment.js +13 -11
  35. package/src/TSL/ExtendKernel.js +75 -0
  36. package/src/TSL/FinalWriteKernel.js +121 -0
  37. package/src/TSL/GenerateKernel.js +111 -0
  38. package/src/TSL/LightsSampling.js +2 -2
  39. package/src/TSL/PathTracerCore.js +43 -1039
  40. package/src/TSL/ShadeKernel.js +876 -0
  41. package/src/TSL/patches.js +81 -4
  42. package/src/index.js +3 -0
  43. package/src/managers/CameraManager.js +1 -1
  44. package/src/managers/DenoisingManager.js +40 -75
  45. package/src/managers/EnvironmentManager.js +30 -39
  46. package/src/managers/OverlayManager.js +7 -22
  47. package/src/managers/UniformManager.js +0 -3
  48. package/src/managers/helpers/TileHelper.js +2 -2
  49. package/src/Stages/AdaptiveSampling.js +0 -483
  50. package/src/TSL/PathTracer.js +0 -384
  51. package/src/managers/TileManager.js +0 -298
@@ -0,0 +1,277 @@
1
+ /**
2
+ * KernelManager.js
3
+ *
4
+ * Builds, caches, and dispatches individual compute nodes for the wavefront
5
+ * path tracing pipeline. Each kernel is a separate `Fn().compute()` node.
6
+ *
7
+ * Manages workgroup sizes, dispatch dimensions, and provides a unified
8
+ * dispatch interface that wraps `renderer.compute(node)`.
9
+ */
10
+
11
+ /** Default workgroup sizes per kernel type */
12
+ const WORKGROUP_SIZES = {
13
+ generate: [ 16, 16, 1 ], // 2D screen-space
14
+ extend: [ 256, 1, 1 ], // 1D ray-parallel
15
+ sort: [ 256, 1, 1 ], // 1D ray-parallel
16
+ shade: [ 256, 1, 1 ], // 1D ray-parallel (sorted)
17
+ connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
18
+ accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
19
+ compact: [ 256, 1, 1 ], // 1D ray-parallel
20
+ resetCounters: [ 1, 1, 1 ], // Single thread
21
+ finalWrite: [ 16, 16, 1 ], // 2D screen-space
22
+ };
23
+
24
+ export class KernelManager {
25
+
26
+ /**
27
+ * @param {WebGPURenderer} renderer - Three.js WebGPU renderer
28
+ */
29
+ constructor( renderer ) {
30
+
31
+ /**
32
+ * @type {WebGPURenderer}
33
+ */
34
+ this.renderer = renderer;
35
+
36
+ /**
37
+ * Map of kernel name → ComputeNode.
38
+ * @type {Map<string, ComputeNode>}
39
+ */
40
+ this.kernels = new Map();
41
+
42
+ /**
43
+ * Map of kernel name → workgroup size [x, y, z].
44
+ * @type {Map<string, number[]>}
45
+ */
46
+ this.workgroupSizes = new Map();
47
+
48
+ /**
49
+ * Timing data for performance profiling.
50
+ * @type {Map<string, {compiledOnce: boolean, lastDispatchMs: number}>}
51
+ */
52
+ this.timing = new Map();
53
+
54
+ /**
55
+ * Optional per-kernel CPU-side submission timing (encode/dispatch cost only;
56
+ * does NOT measure GPU execution time). Toggle via enableProfiling().
57
+ * @type {boolean}
58
+ */
59
+ this.profiling = false;
60
+
61
+ /**
62
+ * Aggregated profile: kernel name → { calls, totalMs }.
63
+ * @type {Map<string, {calls: number, totalMs: number}>}
64
+ */
65
+ this.profile = new Map();
66
+
67
+ // Initialize workgroup sizes from defaults
68
+ for ( const [ name, wgSize ] of Object.entries( WORKGROUP_SIZES ) ) {
69
+
70
+ this.workgroupSizes.set( name, wgSize );
71
+
72
+ }
73
+
74
+ }
75
+
76
+ /**
77
+ * Register a pre-built compute node.
78
+ * @param {string} name - Kernel name (e.g. 'generate', 'extend')
79
+ * @param {ComputeNode} computeNode - Built via `Fn().compute([dx,dy,dz], [wgx,wgy,wgz])`
80
+ */
81
+ register( name, computeNode ) {
82
+
83
+ this.kernels.set( name, computeNode );
84
+ this.timing.set( name, { compiledOnce: false, lastDispatchMs: 0 } );
85
+
86
+ }
87
+
88
+ /**
89
+ * Dispatch a kernel by name.
90
+ * @param {string} name - Kernel name
91
+ */
92
+ dispatch( name ) {
93
+
94
+ const node = this.kernels.get( name );
95
+
96
+ if ( ! node ) {
97
+
98
+ throw new Error( `KernelManager: Unknown kernel '${name}'` );
99
+
100
+ }
101
+
102
+ const timingEntry = this.timing.get( name );
103
+
104
+ if ( timingEntry && ! timingEntry.compiledOnce ) {
105
+
106
+ const t0 = performance.now();
107
+ this.renderer.compute( node );
108
+ const t1 = performance.now();
109
+ timingEntry.compiledOnce = true;
110
+ timingEntry.lastDispatchMs = t1 - t0;
111
+ console.log( `[Wavefront] Kernel '${name}' first dispatch (includes compilation): ${( t1 - t0 ).toFixed( 1 )}ms` );
112
+
113
+ } else if ( this.profiling ) {
114
+
115
+ const t0 = performance.now();
116
+ this.renderer.compute( node );
117
+ const t1 = performance.now();
118
+ let p = this.profile.get( name );
119
+ if ( ! p ) {
120
+
121
+ p = { calls: 0, totalMs: 0 };
122
+ this.profile.set( name, p );
123
+
124
+ }
125
+
126
+ p.calls ++;
127
+ p.totalMs += t1 - t0;
128
+
129
+ } else {
130
+
131
+ this.renderer.compute( node );
132
+
133
+ }
134
+
135
+ }
136
+
137
+ /**
138
+ * Update dispatch dimensions for a kernel.
139
+ * @param {string} name - Kernel name
140
+ * @param {number[]} count - Dispatch dimensions [x, y, z]
141
+ */
142
+ setDispatchCount( name, count ) {
143
+
144
+ const node = this.kernels.get( name );
145
+ if ( ! node ) return;
146
+ node.dispatchSize = count;
147
+
148
+ }
149
+
150
+ /**
151
+ * Calculate 2D dispatch dimensions for a screen-space kernel.
152
+ * @param {number} width - Render width in pixels
153
+ * @param {number} height - Render height in pixels
154
+ * @param {string} kernelName - Kernel name for WG size lookup
155
+ * @returns {number[]} [dispatchX, dispatchY, 1]
156
+ */
157
+ calcScreenDispatch( width, height, kernelName ) {
158
+
159
+ const wg = this.workgroupSizes.get( kernelName ) || [ 16, 16, 1 ];
160
+ return [
161
+ Math.ceil( width / wg[ 0 ] ),
162
+ Math.ceil( height / wg[ 1 ] ),
163
+ 1
164
+ ];
165
+
166
+ }
167
+
168
+ /**
169
+ * Calculate 1D dispatch dimensions for a ray-parallel kernel.
170
+ * @param {number} rayCount - Number of rays to process
171
+ * @param {string} kernelName - Kernel name for WG size lookup
172
+ * @returns {number[]} [dispatchX, 1, 1]
173
+ */
174
+ calcRayDispatch( rayCount, kernelName ) {
175
+
176
+ const wg = this.workgroupSizes.get( kernelName ) || [ 256, 1, 1 ];
177
+ return [
178
+ Math.ceil( rayCount / wg[ 0 ] ),
179
+ 1,
180
+ 1
181
+ ];
182
+
183
+ }
184
+
185
+ /**
186
+ * Get the workgroup size for a kernel.
187
+ * @param {string} name
188
+ * @returns {number[]}
189
+ */
190
+ getWorkgroupSize( name ) {
191
+
192
+ return this.workgroupSizes.get( name ) || [ 256, 1, 1 ];
193
+
194
+ }
195
+
196
+ /**
197
+ * Check if a kernel has been registered.
198
+ * @param {string} name
199
+ * @returns {boolean}
200
+ */
201
+ has( name ) {
202
+
203
+ return this.kernels.has( name );
204
+
205
+ }
206
+
207
+ /**
208
+ * Get the underlying compute node.
209
+ * @param {string} name
210
+ * @returns {ComputeNode|undefined}
211
+ */
212
+ get( name ) {
213
+
214
+ return this.kernels.get( name );
215
+
216
+ }
217
+
218
+ /**
219
+ * Get first-dispatch compilation timing for all kernels.
220
+ * @returns {Object} name → { compiledOnce, lastDispatchMs }
221
+ */
222
+ getTimingReport() {
223
+
224
+ const report = {};
225
+
226
+ for ( const [ name, data ] of this.timing ) {
227
+
228
+ report[ name ] = { ...data };
229
+
230
+ }
231
+
232
+ return report;
233
+
234
+ }
235
+
236
+ /**
237
+ * Toggle per-kernel CPU-submission profiling. Measures only encode/dispatch
238
+ * cost on CPU (GPU work is async and NOT included).
239
+ * @param {boolean} enabled
240
+ */
241
+ enableProfiling( enabled ) {
242
+
243
+ this.profiling = enabled;
244
+ if ( enabled ) this.profile.clear();
245
+
246
+ }
247
+
248
+ /**
249
+ * Get accumulated profiling data.
250
+ * @returns {Object} name → { calls, totalMs, avgMs }
251
+ */
252
+ getProfileReport() {
253
+
254
+ const rows = [];
255
+ let sum = 0;
256
+ for ( const [ name, { calls, totalMs } ] of this.profile ) {
257
+
258
+ sum += totalMs;
259
+ rows.push( { name, calls, totalMs: + totalMs.toFixed( 2 ), avgMs: + ( totalMs / calls ).toFixed( 3 ) } );
260
+
261
+ }
262
+
263
+ rows.sort( ( a, b ) => b.totalMs - a.totalMs );
264
+ rows.push( { name: 'TOTAL', calls: rows.reduce( ( s, r ) => s + r.calls, 0 ), totalMs: + sum.toFixed( 2 ), avgMs: null } );
265
+ return rows;
266
+
267
+ }
268
+
269
+ dispose() {
270
+
271
+ this.kernels.clear();
272
+ this.timing.clear();
273
+ this.profile.clear();
274
+
275
+ }
276
+
277
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Packed buffer manager for wavefront path tracing — one storage buffer per data category.
3
+ * RAY/HIT are SoA-within-a-buffer (field `slot` of element `id` lives at `id + slot*_cap`).
4
+ */
5
+
6
+ import {
7
+ storage, uintBitsToFloat, floatBitsToUint, vec2, vec3, vec4, uvec4, uint, int,
8
+ packSnorm2x16, packUnorm2x16, unpackSnorm2x16, unpackUnorm2x16,
9
+ } from 'three/tsl';
10
+ import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
+
12
+ export const RAY_STRIDE = 7;
13
+ export const HIT_STRIDE = 2;
14
+ // Per-pixel G-buffer (first-hit MRT staging): 2 uvec4/pixel (AoS, element p*GBUFFER_STRIDE + lane).
15
+ // lane 0 — half-packed normal/depth/albedo (pack2x16, no f32 bitcast); read by FinalWrite:
16
+ // .x=packSnorm2x16(normal.xy) .y=packSnorm2x16(normal.z, depth) .z=packUnorm2x16(albedo.rg) .w=packUnorm2x16(albedo.b, 0)
17
+ // lane 1 — primary-hit surface ID for A-SVGF correlated-gradient re-projection (Tier 1); written at the
18
+ // bounce-0 hit, valid=0 on miss (Generate inits): .x=triIndex .y=meshIndex .z=packUnorm2x16(bary.u,bary.v) .w=valid
19
+ // Separate buffer from RAY (per-pixel, not per-ray×S) — written by Generate/Shade bounce-0.
20
+ export const GBUFFER_STRIDE = 2;
21
+
22
+ export const RAY = {
23
+ ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex+sampleIndex derived from rayID
24
+ DIR_FLAGS: 1, // vec4(direction.xyz, uintBitsToFloat(bounceFlags))
25
+ THROUGHPUT_PDF: 2, // vec4(throughput.xyz, pdf)
26
+ RADIANCE_ALPHA: 3, // vec4(radiance.xyz, alpha)
27
+ MEDIUM_STACK: 4, // vec4(uintBitsToFloat(stackDepth|transTraversals<<8|wavelength<<16), ior1, ior2, ior3)
28
+ MEDIUM_SIGMA_A: 5, // vec4(sigmaA.xyz, _) — Beer-Lambert absorption coeff of the active medium (KHR_materials_volume + SSS)
29
+ SSS_SIGMA_S: 6, // vec4(sigmaS.xyz, g) — SSS scattering coeff + Henyey-Greenstein anisotropy (sigmaS==0 ⇒ glass)
30
+ };
31
+
32
+ export const HIT = {
33
+ DIST_TRI_BARY: 0, // vec4(distance, uintBitsToFloat(triIndex), bary.u, bary.v)
34
+ NORMAL_MAT: 1, // vec4(geoNormal.xyz, uintBitsToFloat(matIndex | meshIndex<<16))
35
+ };
36
+
37
+ // SoA region stride, baked into the shader graph at build time; single instance, rebuilt on resize.
38
+ let _cap = 0;
39
+
40
+ const soa = ( id, slot ) => ( slot === 0 ? id : id.add( slot * _cap ) );
41
+
42
+ export class PackedRayBuffer {
43
+
44
+ // Capacity maxRays would allocate (mirrors allocate()/resize()). 1.25× headroom, NO pow2 rounding —
45
+ // the pow2 jump nearly doubled VRAM (e.g. 2048²: 5.24M→8.39M) for no realloc benefit: the app's
46
+ // discrete resolution presets always exceed the 1.25× margin on a tier change, so they rebuild anyway.
47
+ static requiredCapacity( maxRays ) {
48
+
49
+ return Math.ceil( maxRays * 1.25 );
50
+
51
+ }
52
+
53
+ constructor( maxRays = 0 ) {
54
+
55
+ this.capacity = 0;
56
+ this._attrs = {};
57
+
58
+ // Each: { rw: StorageBufferNode, ro: StorageBufferNode } over one shared GPU buffer.
59
+ this.rayBuffer = null;
60
+ this.rngBuffer = null;
61
+ this.hitBuffer = null;
62
+
63
+ if ( maxRays > 0 ) this.allocate( maxRays );
64
+
65
+ }
66
+
67
+ allocate( maxRays ) {
68
+
69
+ this.dispose();
70
+
71
+ const capacity = Math.ceil( maxRays * 1.25 );
72
+ this.capacity = capacity;
73
+ _cap = capacity;
74
+
75
+ // count=0 so StorageBufferNode.getHash() shares the buffer → RW and RO nodes bind the same GPU data.
76
+ const rayCount = capacity * RAY_STRIDE;
77
+ const rayAttr = new StorageInstancedBufferAttribute( new Float32Array( rayCount * 4 ), 4 );
78
+ this._attrs.ray = rayAttr;
79
+ this.rayBuffer = {
80
+ rw: storage( rayAttr, 'vec4' ),
81
+ ro: storage( rayAttr, 'vec4' ).toReadOnly(),
82
+ };
83
+
84
+ const rngAttr = new StorageInstancedBufferAttribute( new Uint32Array( capacity ), 1 );
85
+ this._attrs.rng = rngAttr;
86
+ this.rngBuffer = {
87
+ rw: storage( rngAttr, 'uint' ),
88
+ ro: storage( rngAttr, 'uint' ).toReadOnly(),
89
+ };
90
+
91
+ const hitCount = capacity * HIT_STRIDE;
92
+ const hitAttr = new StorageInstancedBufferAttribute( new Float32Array( hitCount * 4 ), 4 );
93
+ this._attrs.hit = hitAttr;
94
+ this.hitBuffer = {
95
+ rw: storage( hitAttr, 'vec4' ),
96
+ ro: storage( hitAttr, 'vec4' ).toReadOnly(),
97
+ };
98
+
99
+ const totalMB = (
100
+ rayCount * 16 + capacity * 4 + hitCount * 16
101
+ ) / ( 1024 * 1024 );
102
+
103
+ console.log(
104
+ `PackedRayBuffer: capacity=${capacity}, total=${totalMB.toFixed( 1 )} MB ` +
105
+ `(ray=${( rayCount * 16 / 1048576 ).toFixed( 0 )}MB hit=${( hitCount * 16 / 1048576 ).toFixed( 0 )}MB) [SoA ray/hit]`
106
+ );
107
+
108
+ }
109
+
110
+ // Reallocates only if maxRays needs more capacity; returns true if it did.
111
+ resize( maxRays ) {
112
+
113
+ const needed = Math.ceil( maxRays * 1.25 );
114
+ if ( needed <= this.capacity && this.capacity > 0 ) return false;
115
+ this.allocate( maxRays );
116
+ return true;
117
+
118
+ }
119
+
120
+ dispose() {
121
+
122
+ this._attrs = {};
123
+ this.rayBuffer = null;
124
+ this.rngBuffer = null;
125
+ this.hitBuffer = null;
126
+ this.capacity = 0;
127
+
128
+ }
129
+
130
+ }
131
+
132
+ // TSL accessor helpers — call inside Fn() scopes. `buf` is the .rw/.ro StorageBufferNode, `id` a uint node.
133
+
134
+ export const readRayOrigin = ( buf, id ) =>
135
+ buf.element( soa( id, RAY.ORIGIN_META ) ).xyz;
136
+
137
+ export const readRayDirection = ( buf, id ) =>
138
+ buf.element( soa( id, RAY.DIR_FLAGS ) ).xyz;
139
+
140
+ export const readRayBounceFlags = ( buf, id ) =>
141
+ floatBitsToUint( buf.element( soa( id, RAY.DIR_FLAGS ) ).w );
142
+
143
+ export const readRayThroughput = ( buf, id ) =>
144
+ buf.element( soa( id, RAY.THROUGHPUT_PDF ) ).xyz;
145
+
146
+ export const readRayPdf = ( buf, id ) =>
147
+ buf.element( soa( id, RAY.THROUGHPUT_PDF ) ).w;
148
+
149
+ export const readRayRadiance = ( buf, id ) =>
150
+ buf.element( soa( id, RAY.RADIANCE_ALPHA ) );
151
+
152
+ // ── Per-pixel G-buffer (first-hit MRT). 2 uvec4/pixel (AoS), pack2x16 lanes. ──
153
+ // normal: raw unit vec3; depth: linear [0,1]; albedo: vec3 [0,1]. Packed values live in u32 lanes
154
+ // verbatim (no f32 bitcast) so NaN-range bit patterns (snorm ±1 → 0x7FFF) survive store/load intact.
155
+ // gbLane resolves the AoS slot for a pixel (lane 0 = MRT, lane 1 = surface ID).
156
+ const gbLane = ( pixelIndex, lane ) => {
157
+
158
+ const base = uint( pixelIndex ).mul( GBUFFER_STRIDE );
159
+ return lane === 0 ? base : base.add( lane );
160
+
161
+ };
162
+
163
+ export const writeGBuffer = ( buf, pixelIndex, normal, depth, albedo ) =>
164
+ buf.element( gbLane( pixelIndex, 0 ) ).assign( uvec4(
165
+ packSnorm2x16( vec2( normal.x, normal.y ) ),
166
+ packSnorm2x16( vec2( normal.z, depth ) ),
167
+ packUnorm2x16( vec2( albedo.x, albedo.y ) ),
168
+ packUnorm2x16( vec2( albedo.z, 0.0 ) ),
169
+ ) );
170
+ export const readGBuffer = ( buf, pixelIndex ) => buf.element( gbLane( pixelIndex, 0 ) );
171
+
172
+ // Lane 1 — primary-hit surface ID for A-SVGF correlated gradient re-projection (Tier 1).
173
+ // valid=0 marks a miss (no primary surface); bary packed unorm (both in [0,1]).
174
+ export const writeGBufferSurfaceID = ( buf, pixelIndex, triIndex, meshIndex, baryU, baryV, valid ) =>
175
+ buf.element( gbLane( pixelIndex, 1 ) ).assign( uvec4(
176
+ uint( triIndex ), uint( meshIndex ), packUnorm2x16( vec2( baryU, baryV ) ), uint( valid ),
177
+ ) );
178
+ export const readGBufferSurfaceID = ( buf, pixelIndex ) => {
179
+
180
+ const p = buf.element( gbLane( pixelIndex, 1 ) );
181
+ const bary = unpackUnorm2x16( p.z );
182
+ return { triIndex: p.x, meshIndex: p.y, baryU: bary.x, baryV: bary.y, valid: p.w };
183
+
184
+ };
185
+
186
+ // Decode for FinalWrite. normalDepth.xyz matches the prior path (normal*0.5+0.5), .w = raw depth.
187
+ export const gbDecodeNormalDepth = ( packed ) => {
188
+
189
+ const nxy = unpackSnorm2x16( packed.x );
190
+ const nzd = unpackSnorm2x16( packed.y );
191
+ return vec4( vec3( nxy.x, nxy.y, nzd.x ).mul( 0.5 ).add( 0.5 ), nzd.y );
192
+
193
+ };
194
+
195
+ export const gbDecodeAlbedo = ( packed ) =>
196
+ vec3( unpackUnorm2x16( packed.z ), unpackUnorm2x16( packed.w ).x );
197
+
198
+ // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex +
199
+ // sampleIndex are NOT stored — derived from rayID (= subSample*w*h + pixelIndex) in-kernel.
200
+ export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps ) =>
201
+ buf.element( soa( id, RAY.ORIGIN_META ) )
202
+ .assign( vec4( origin, uintBitsToFloat(
203
+ uint( bounces ).bitOr( uint( sssSteps ).shiftLeft( 8 ) )
204
+ ) ) );
205
+
206
+ export const writeRayDirFlags = ( buf, id, direction, bounceFlags ) =>
207
+ buf.element( soa( id, RAY.DIR_FLAGS ) )
208
+ .assign( vec4( direction, uintBitsToFloat( bounceFlags ) ) );
209
+
210
+ export const writeRayThroughputPdf = ( buf, id, throughput, pdf ) =>
211
+ buf.element( soa( id, RAY.THROUGHPUT_PDF ) )
212
+ .assign( vec4( throughput, pdf ) );
213
+
214
+ export const writeRayRadiance = ( buf, id, radiance ) =>
215
+ buf.element( soa( id, RAY.RADIANCE_ALPHA ) )
216
+ .assign( radiance );
217
+
218
+ export const readHitDistance = ( buf, id ) =>
219
+ buf.element( soa( id, HIT.DIST_TRI_BARY ) ).x;
220
+
221
+ export const readHitTriangleIndex = ( buf, id ) =>
222
+ floatBitsToUint( buf.element( soa( id, HIT.DIST_TRI_BARY ) ).y );
223
+
224
+ export const readHitBarycentrics = ( buf, id ) =>
225
+ buf.element( soa( id, HIT.DIST_TRI_BARY ) ).zw;
226
+
227
+ export const readHitNormal = ( buf, id ) =>
228
+ buf.element( soa( id, HIT.NORMAL_MAT ) ).xyz;
229
+
230
+ export const readHitMaterialIndex = ( buf, id ) =>
231
+ uint( floatBitsToUint( buf.element( soa( id, HIT.NORMAL_MAT ) ).w ).bitAnd( 0xFFFF ) );
232
+
233
+ export const readHitMeshIndex = ( buf, id ) =>
234
+ floatBitsToUint( buf.element( soa( id, HIT.NORMAL_MAT ) ).w ).shiftRight( 16 );
235
+
236
+ export const writeHitPacked = ( buf, id, distance, triIndex, baryU, baryV, normal, matIndex, meshIndex ) => {
237
+
238
+ buf.element( soa( id, HIT.DIST_TRI_BARY ) )
239
+ .assign( vec4( distance, uintBitsToFloat( triIndex ), baryU, baryV ) );
240
+ buf.element( soa( id, HIT.NORMAL_MAT ) )
241
+ .assign( vec4( normal, uintBitsToFloat( matIndex.bitOr( meshIndex.shiftLeft( 16 ) ) ) ) );
242
+
243
+ };
244
+
245
+ // Region 6 word packs stackDepth | transTraversals<<8 | wavelength<<16 (nm, 0=achromatic).
246
+ export const readMediumStack = ( buf, id ) => {
247
+
248
+ const packed = buf.element( soa( id, RAY.MEDIUM_STACK ) );
249
+ const packedInt = floatBitsToUint( packed.x );
250
+ return {
251
+ stackDepth: packedInt.bitAnd( 0xFF ),
252
+ transTraversals: packedInt.shiftRight( 8 ).bitAnd( 0xFF ),
253
+ wavelength: packedInt.shiftRight( 16 ).bitAnd( 0xFFFF ),
254
+ ior1: packed.y,
255
+ ior2: packed.z,
256
+ ior3: packed.w,
257
+ };
258
+
259
+ };
260
+
261
+ export const writeMediumStack = ( buf, id, stackDepth, transTraversals, ior1, ior2, ior3, wavelength = uint( 0 ) ) =>
262
+ buf.element( soa( id, RAY.MEDIUM_STACK ) )
263
+ .assign( vec4( uintBitsToFloat(
264
+ stackDepth.bitOr( transTraversals.shiftLeft( 8 ) ).bitOr( wavelength.shiftLeft( 16 ) )
265
+ ), ior1, ior2, ior3 ) );
266
+
267
+ // Region 7: Beer-Lambert sigmaA of the active medium; single-slot, absorption gated on stackDepth>0.
268
+ export const readMediumSigmaA = ( buf, id ) => buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).xyz;
269
+
270
+ export const writeMediumSigmaA = ( buf, id, sigmaA ) =>
271
+ buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).assign( vec4( sigmaA, 0.0 ) );
272
+
273
+ // Per-ray bounce state packed into ORIGIN_META.w (written by writeRayOriginMeta alongside the origin):
274
+ // perRayBounces = bits 0-7 (camera-bounce depth; the loop index can't track it once free bounces decouple it)
275
+ // sssSteps = bits 8-15 (SSS random-walk step counter)
276
+ // sampleIndex (the multi-sample sub-sample 0..S-1) is derived in-kernel from rayID, not stored.
277
+ export const readPathBounces = ( buf, id ) =>
278
+ int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).bitAnd( 0xFF ) );
279
+ export const readSssSteps = ( buf, id ) =>
280
+ int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 8 ).bitAnd( 0xFF ) );
281
+
282
+ // Region 9: SSS sigmaS + Henyey-Greenstein g. sigmaS==0 marks glass (Beer-Lambert path, not random walk).
283
+ export const readSSSMedium = ( buf, id ) => {
284
+
285
+ const v = buf.element( soa( id, RAY.SSS_SIGMA_S ) );
286
+ return { sigmaS: v.xyz, g: v.w };
287
+
288
+ };
289
+
290
+ export const writeSSSMedium = ( buf, id, sigmaS, g ) =>
291
+ buf.element( soa( id, RAY.SSS_SIGMA_S ) ).assign( vec4( sigmaS, g ) );