rayzee 5.4.0 → 5.4.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.
@@ -30,7 +30,7 @@ import {
30
30
  fract,
31
31
  } from 'three/tsl';
32
32
 
33
- import { struct } from './structProxy.js';
33
+ import { struct } from './patches.js';
34
34
  import { Ray, RayTracingMaterial, RenderState, HitInfo, DotProducts, DirectionSample } from './Struct.js';
35
35
  import { PI, EPSILON, MIN_ROUGHNESS, MIN_CLEARCOAT_ROUGHNESS, computeDotProducts } from './Common.js';
36
36
  import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
@@ -137,14 +137,14 @@ export const pathTracerMain = ( params ) => {
137
137
  pointLightsBuffer, numPointLights,
138
138
  spotLightsBuffer, numSpotLights,
139
139
  envTexture, environmentIntensity, envMatrix,
140
- envMarginalWeights, envConditionalWeights,
140
+ envCDFBuffer,
141
141
  envTotalSum, envResolution,
142
142
  enableEnvironmentLight, useEnvMapIS,
143
143
  maxBounceCount, transmissiveBounces,
144
144
  showBackground, transparentBackground, backgroundIntensity,
145
145
  fireflyThreshold, globalIlluminationIntensity,
146
146
  totalTriangleCount, enableEmissiveTriangleSampling,
147
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
147
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
148
148
  lightBVHBuffer, lightBVHNodeCount,
149
149
  debugVisScale,
150
150
  enableAccumulation, hasPreviousAccumulated,
@@ -284,14 +284,14 @@ export const pathTracerMain = ( params ) => {
284
284
  pointLightsBuffer, numPointLights,
285
285
  spotLightsBuffer, numSpotLights,
286
286
  envTexture, environmentIntensity, envMatrix,
287
- envMarginalWeights, envConditionalWeights,
287
+ envCDFBuffer,
288
288
  envTotalSum, envResolution,
289
289
  enableEnvironmentLight, useEnvMapIS,
290
290
  maxBounceCount, transmissiveBounces,
291
291
  backgroundIntensity, showBackground, transparentBackground,
292
292
  fireflyThreshold, globalIlluminationIntensity,
293
293
  totalTriangleCount, enableEmissiveTriangleSampling,
294
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
294
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
295
295
  lightBVHBuffer, lightBVHNodeCount,
296
296
  pixelCoord, resolution, frame,
297
297
  ) );
@@ -41,7 +41,7 @@ import {
41
41
  sampler,
42
42
  } from 'three/tsl';
43
43
 
44
- import { struct } from './structProxy.js';
44
+ import { struct } from './patches.js';
45
45
 
46
46
  import {
47
47
  PI_INV,
@@ -590,7 +590,7 @@ export const Trace = Fn( ( [
590
590
  spotLightsBuffer, numSpotLights,
591
591
  // Environment
592
592
  envTexture, environmentIntensity, envMatrix,
593
- envMarginalWeights, envConditionalWeights,
593
+ envCDFBuffer,
594
594
  envTotalSum, envResolution,
595
595
  enableEnvironmentLight, useEnvMapIS,
596
596
  // Rendering parameters
@@ -598,7 +598,7 @@ export const Trace = Fn( ( [
598
598
  backgroundIntensity, showBackground, transparentBackground,
599
599
  fireflyThreshold, globalIlluminationIntensity,
600
600
  totalTriangleCount, enableEmissiveTriangleSampling,
601
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
601
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
602
602
  lightBVHBuffer, lightBVHNodeCount,
603
603
  // Per-pixel info
604
604
  pixelCoord, resolution, frame,
@@ -1013,7 +1013,7 @@ export const Trace = Fn( ( [
1013
1013
  triangleBuffer,
1014
1014
  materialBuffer,
1015
1015
  envTexture, environmentIntensity, envMatrix,
1016
- envMarginalWeights, envConditionalWeights,
1016
+ envCDFBuffer,
1017
1017
  envTotalSum, envResolution,
1018
1018
  enableEnvironmentLight,
1019
1019
  );
@@ -1040,6 +1040,7 @@ export const Trace = Fn( ( [
1040
1040
  rngState,
1041
1041
  lightBVHBuffer,
1042
1042
  emissiveTriangleBuffer,
1043
+ emissiveVec4Offset,
1043
1044
  triangleBuffer,
1044
1045
  ) );
1045
1046
 
@@ -1091,7 +1092,7 @@ export const Trace = Fn( ( [
1091
1092
  hitInfo.hitPoint, N, V, material,
1092
1093
  totalTriangleCount, bounceIndex, rngState,
1093
1094
  emissiveBoost,
1094
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
1095
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1095
1096
  triangleBuffer,
1096
1097
  traceShadowRayWrapped,
1097
1098
  evaluateMaterialResponse,
@@ -1132,7 +1133,6 @@ export const Trace = Fn( ( [
1132
1133
  rngState,
1133
1134
  samplingInfo,
1134
1135
  envTexture, environmentIntensity, envMatrix,
1135
- envMarginalWeights, envConditionalWeights,
1136
1136
  envTotalSum, envResolution,
1137
1137
  enableEnvironmentLight, useEnvMapIS,
1138
1138
  ) );
package/src/TSL/Struct.js CHANGED
@@ -1,4 +1,4 @@
1
- import { struct } from './structProxy.js';
1
+ import { struct } from './patches.js';
2
2
 
3
3
  export const Ray = struct( {
4
4
  origin: 'vec3',
@@ -281,7 +281,7 @@ export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ]
281
281
 
282
282
  const result = currentNormal.toVar();
283
283
 
284
- If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ), () => {
284
+ If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ).and( material.bumpScale.greaterThan( 0.0 ) ), () => {
285
285
 
286
286
  // Approximate texel size
287
287
  const texelSize = vec2( 1.0 / 1024.0 ).toVar();
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Rayzee patches for Three.js / TSL.
3
+ *
4
+ * Side-effect on import: installs `WebGPUBackend.createNodeBuilder` override
5
+ * (restores r183 function-scoped `var` emission for compute shaders — prevents
6
+ * a register-allocation regression in the path tracer's hot loop).
7
+ *
8
+ * Export: `struct()` — drop-in replacement for TSL's `struct()` returning
9
+ * a proxy factory that supports GLSL-style dot-notation field access.
10
+ */
11
+
12
+ import { WebGPUBackend } from 'three/webgpu';
13
+ import { struct as _struct } from 'three/tsl';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // 1. WGSL global-variable promotion patch (compute-only)
17
+ // ---------------------------------------------------------------------------
18
+ // Three.js r184 introduced `WGSLNodeBuilder.allowGlobalVariables = true`, which
19
+ // emits `.toVar()` declarations at WGSL module scope as `var<private> name : T`
20
+ // instead of function-local `var name : T` inside `fn main()` (as r183 did).
21
+ //
22
+ // For compute shaders with hundreds of `.toVar()` calls in loops (e.g. the BVH
23
+ // traversal + BRDF path tracer), `var<private>` increases GPU register pressure
24
+ // because the Dawn/Chromium WGSL compiler cannot aggressively register-allocate
25
+ // variables with a stable per-invocation memory address. We measured a ~8% fps
26
+ // regression (120 → 110) on the path tracer after upgrading r183 → r184 that
27
+ // traced entirely to GPU execution, not CPU.
28
+ //
29
+ // `allowGlobalVariables` is ONLY consumed by the compute template
30
+ // (`_getWGSLComputeCode`). The vertex/fragment templates always emit
31
+ // `shaderData.vars` at module scope and REQUIRE `allowGlobalVariables=true`
32
+ // (emitting function-local `var` at module scope is invalid WGSL and crashes
33
+ // pipeline creation with "Invalid ShaderModule"). We install a per-instance
34
+ // accessor that returns `false` only when the builder is for a compute node
35
+ // (material === null) and `true` otherwise, so render pipelines keep r184
36
+ // behavior untouched.
37
+ //
38
+ // Relevant upstream lines:
39
+ // - `node_modules/three/src/renderers/webgpu/nodes/WGSLNodeBuilder.js:247`
40
+ // (`this.allowGlobalVariables = true`)
41
+ // - `...WGSLNodeBuilder.js:2458` (module-scope vars block)
42
+ // - `...WGSLNodeBuilder.js:2467` (function-body vars block)
43
+ //
44
+ // Revisit if upstream adds an official opt-out or fixes register pressure.
45
+
46
+ const _origCreateNodeBuilder = WebGPUBackend.prototype.createNodeBuilder;
47
+
48
+ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
49
+
50
+ const builder = _origCreateNodeBuilder.call( this, object, renderer );
51
+
52
+ Object.defineProperty( builder, 'allowGlobalVariables', {
53
+ get() {
54
+
55
+ return this.material !== null;
56
+
57
+ },
58
+ set() { /* ignore — the value is derived from material presence */ },
59
+ configurable: true,
60
+ } );
61
+
62
+ return builder;
63
+
64
+ };
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // 2. TSL struct proxy — enables GLSL-style dot-notation field access
68
+ // ---------------------------------------------------------------------------
69
+ // TSL structs require `.get('fieldName')` for member access, but GLSL-style
70
+ // dot notation (`.fieldName`) is more natural and matches ported code.
71
+ //
72
+ // This wraps TSL's `struct()` so that:
73
+ // - Direct construction: `MyStruct({...}).toVar('x')` → `.fieldName` works
74
+ // - Fn return values: `MyStruct.wrap(someFn(...))` → `.fieldName` works
75
+ //
76
+ // Property access for known struct member names is redirected to `.get('name')`.
77
+ // Swizzle properties (x, y, z, w, etc.), Node methods (.add, .assign, etc.), and
78
+ // other standard properties pass through to the underlying node unmodified.
79
+
80
+ function createStructProxy( node, memberSet ) {
81
+
82
+ return new Proxy( node, {
83
+
84
+ get( target, prop, receiver ) {
85
+
86
+ // Intercept known struct member names
87
+ if ( typeof prop === 'string' && memberSet.has( prop ) ) {
88
+
89
+ return target.get( prop );
90
+
91
+ }
92
+
93
+ const val = Reflect.get( target, prop, receiver );
94
+
95
+ // Intercept .toVar() to proxy-wrap the result
96
+ if ( prop === 'toVar' && typeof val === 'function' ) {
97
+
98
+ return ( ...args ) => createStructProxy( val.apply( target, args ), memberSet );
99
+
100
+ }
101
+
102
+ return val;
103
+
104
+ }
105
+
106
+ } );
107
+
108
+ }
109
+
110
+ /**
111
+ * Drop-in replacement for TSL's `struct()` that returns a proxy-enhanced factory.
112
+ *
113
+ * The returned factory:
114
+ * - Creates struct nodes where `.toVar()` results support dot-notation field access
115
+ * - Has `.wrap(node)` method to proxy-wrap Fn return values for field access
116
+ * - Has `.layout` and `.isStruct` matching the original TSL struct API
117
+ *
118
+ * @param {Object} members - Struct member layout (e.g., { didHit: 'bool', dst: 'float' })
119
+ * @param {string|null} name - Optional struct name
120
+ * @returns {Function} Enhanced struct factory
121
+ */
122
+ export function struct( members, name = null ) {
123
+
124
+ const factory = _struct( members, name );
125
+ const memberSet = new Set( Object.keys( members ) );
126
+
127
+ const wrappedFactory = ( ...args ) => {
128
+
129
+ const node = factory( ...args );
130
+ return createStructProxy( node, memberSet );
131
+
132
+ };
133
+
134
+ wrappedFactory.layout = factory.layout;
135
+ wrappedFactory.isStruct = true;
136
+
137
+ /**
138
+ * Wrap an existing node (e.g., Fn return value) with struct field access proxy.
139
+ * Usage: `const hit = HitInfo.wrap(traverseBVH(...).toVar('hit'));`
140
+ */
141
+ wrappedFactory.wrap = ( node ) => createStructProxy( node, memberSet );
142
+
143
+ return wrappedFactory;
144
+
145
+ }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  // Patches (side-effect imports — must run before any StorageTexture usage)
9
- import './TSL/wgslGlobalVarsPatch.js';
9
+ import './TSL/patches.js';
10
10
 
11
11
  // Main application
12
12
  export { PathTracerApp } from './PathTracerApp.js';
@@ -43,11 +43,11 @@ export class EnvironmentManager {
43
43
  this.environmentTexture = this._envPlaceholder;
44
44
  this.envTexSize = new Vector2();
45
45
 
46
- // CDF storage buffers
47
- this.envMarginalStorageAttr = null;
48
- this.envMarginalStorageNode = null;
49
- this.envConditionalStorageAttr = null;
50
- this.envConditionalStorageNode = null;
46
+ // CDF storage buffer (marginal + conditional packed into one buffer).
47
+ // Layout: [ marginal (envResolution.y floats) | conditional (envResolution.x * envResolution.y floats) ]
48
+ // Conditional offset is the marginal length, which equals envResolution.y at runtime.
49
+ this.envCDFStorageAttr = null;
50
+ this.envCDFStorageNode = null;
51
51
  this._initCDFStorageBuffers();
52
52
 
53
53
  // Environment rotation
@@ -177,74 +177,52 @@ export class EnvironmentManager {
177
177
 
178
178
  }
179
179
 
180
- // ===== CDF STORAGE BUFFERS =====
180
+ // ===== CDF STORAGE BUFFER =====
181
181
 
182
182
  /**
183
- * Initialize CDF storage buffers with placeholder data.
184
- * Must be called before shader compilation so the nodes exist in the graph.
183
+ * Initialize the packed CDF storage buffer with placeholder data.
184
+ * Must be called before shader compilation so the node exists in the graph.
185
+ *
186
+ * Layout: [ marginal (size = envResolution.y) | conditional (size = envResolution.x * envResolution.y) ]
187
+ * Placeholder shape is a 1x2 env map: marginal=[0,1], conditional=[0,0,1,1].
185
188
  * @private
186
189
  */
187
190
  _initCDFStorageBuffers() {
188
191
 
189
- // Marginal: 1 float per entry, default placeholder
190
- const marginalPlaceholder = new Float32Array( [ 0, 1 ] );
191
- this.envMarginalStorageAttr = new StorageInstancedBufferAttribute( marginalPlaceholder, 1 );
192
- this.envMarginalStorageNode = storage( this.envMarginalStorageAttr, 'float', 2 ).toReadOnly();
193
-
194
- // Conditional: 1 float per entry, default placeholder
195
- const conditionalPlaceholder = new Float32Array( [ 0, 0, 1, 1 ] );
196
- this.envConditionalStorageAttr = new StorageInstancedBufferAttribute( conditionalPlaceholder, 1 );
197
- this.envConditionalStorageNode = storage( this.envConditionalStorageAttr, 'float', 4 ).toReadOnly();
198
-
199
- }
200
-
201
- /**
202
- * Update marginal CDF storage buffer from Float32Array.
203
- */
204
- setEnvMarginalData( floatData ) {
205
-
206
- if ( ! floatData ) return;
207
-
208
- this.envMarginalStorageAttr = new StorageInstancedBufferAttribute( floatData, 1 );
209
- this.envMarginalStorageNode.value = this.envMarginalStorageAttr;
210
- this.envMarginalStorageNode.bufferCount = floatData.length;
211
-
212
- }
213
-
214
- /**
215
- * Update conditional CDF storage buffer from Float32Array.
216
- */
217
- setEnvConditionalData( floatData ) {
218
-
219
- if ( ! floatData ) return;
220
-
221
- this.envConditionalStorageAttr = new StorageInstancedBufferAttribute( floatData, 1 );
222
- this.envConditionalStorageNode.value = this.envConditionalStorageAttr;
223
- this.envConditionalStorageNode.bufferCount = floatData.length;
192
+ const placeholder = new Float32Array( [ 0, 1, 0, 0, 1, 1 ] );
193
+ this.envCDFStorageAttr = new StorageInstancedBufferAttribute( placeholder, 1 );
194
+ this.envCDFStorageNode = storage( this.envCDFStorageAttr, 'float', placeholder.length ).toReadOnly();
224
195
 
225
196
  }
226
197
 
227
198
  /**
228
- * Update both CDF storage buffers from equirectHdrInfo.
199
+ * Update the packed CDF storage buffer from equirectHdrInfo.
200
+ * Concatenates marginal + conditional into one buffer.
229
201
  * @private
230
202
  */
231
203
  _updateCDFStorageBuffers() {
232
204
 
233
- this.setEnvMarginalData( this.equirectHdrInfo.marginalData );
234
- this.setEnvConditionalData( this.equirectHdrInfo.conditionalData );
205
+ const marginal = this.equirectHdrInfo.marginalData;
206
+ const conditional = this.equirectHdrInfo.conditionalData;
207
+ if ( ! marginal || ! conditional ) return;
208
+
209
+ const combined = new Float32Array( marginal.length + conditional.length );
210
+ combined.set( marginal, 0 );
211
+ combined.set( conditional, marginal.length );
212
+
213
+ this.envCDFStorageAttr = new StorageInstancedBufferAttribute( combined, 1 );
214
+ this.envCDFStorageNode.value = this.envCDFStorageAttr;
215
+ this.envCDFStorageNode.bufferCount = combined.length;
235
216
 
236
217
  }
237
218
 
238
219
  /**
239
- * Get CDF storage nodes for shader graph.
240
- * @returns {{ marginalNode: StorageNode, conditionalNode: StorageNode }}
220
+ * Get the packed CDF storage node for shader graph.
221
+ * @returns {{ cdfNode: StorageNode }}
241
222
  */
242
223
  getCDFStorageNodes() {
243
224
 
244
- return {
245
- marginalNode: this.envMarginalStorageNode,
246
- conditionalNode: this.envConditionalStorageNode,
247
- };
225
+ return { cdfNode: this.envCDFStorageNode };
248
226
 
249
227
  }
250
228
 
@@ -559,10 +537,8 @@ export class EnvironmentManager {
559
537
 
560
538
  this.proceduralSkyRenderer = null;
561
539
  this.simpleSkyRenderer = null;
562
- this.envMarginalStorageAttr = null;
563
- this.envMarginalStorageNode = null;
564
- this.envConditionalStorageAttr = null;
565
- this.envConditionalStorageNode = null;
540
+ this.envCDFStorageAttr = null;
541
+ this.envCDFStorageNode = null;
566
542
  this._envPlaceholder?.dispose();
567
543
  this._envPlaceholder = null;
568
544
  this.environmentTexture = null;
@@ -9,9 +9,16 @@
9
9
 
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
- import { TEXTURE_CONSTANTS, MATERIAL_DATA_LAYOUT as M } from '../EngineDefaults.js';
12
+ import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
13
13
 
14
14
  const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
15
+ // Per-triangle float offsets used by _patchTriangleSideForMaterial / _patchTriangleBlockerForMaterial.
16
+ const TRI_MAT_IDX_OFFSET = T.UV_C_MAT_OFFSET + 2; // uvData2.z in shader
17
+ const TRI_SIDE_OFFSET = T.NORMAL_C_OFFSET + 3; // normalCData.w in shader
18
+ const TRI_BLOCKER_OFFSET = T.NORMAL_A_OFFSET + 3; // nA.w in shader (opaque-blocker fast path)
19
+
20
+ // Material properties that affect the shadow-ray opaque-blocker flag.
21
+ const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
15
22
 
16
23
  export class MaterialDataManager {
17
24
 
@@ -41,7 +48,7 @@ export class MaterialDataManager {
41
48
 
42
49
  /**
43
50
  * Optional callbacks set by the owning stage.
44
- * @type {{ onReset?: Function, onFeaturesChanged?: Function }}
51
+ * @type {{ onReset?: Function, onFeaturesChanged?: Function, getTriangleData?: Function, onTriangleDataChanged?: Function }}
45
52
  */
46
53
  this.callbacks = {};
47
54
 
@@ -275,7 +282,11 @@ export class MaterialDataManager {
275
282
  case 'clearcoat': data[ stride + M.CLEARCOAT ] = value; break;
276
283
  case 'clearcoatRoughness': data[ stride + M.CLEARCOAT_ROUGHNESS ] = value; break;
277
284
  case 'opacity': data[ stride + M.OPACITY ] = value; break;
278
- case 'side': data[ stride + M.SIDE ] = value; break;
285
+ case 'side': data[ stride + M.SIDE ] = value;
286
+ // Side is also mirrored into per-triangle data (NORMAL_C.w) so BVH
287
+ // traversal can do side culling without reading the material buffer.
288
+ this._patchTriangleSideForMaterial( materialIndex, value );
289
+ break;
279
290
  case 'transparent': data[ stride + M.TRANSPARENT ] = value; break;
280
291
  case 'alphaTest': data[ stride + M.ALPHA_TEST ] = value; break;
281
292
  case 'alphaMode': data[ stride + M.ALPHA_MODE ] = value; break;
@@ -304,6 +315,13 @@ export class MaterialDataManager {
304
315
 
305
316
  this.materialStorageAttr.needsUpdate = true;
306
317
 
318
+ // Recompute triangle-data opaque-blocker flag when any input to it changes.
319
+ if ( BLOCKER_PROPS.has( property ) ) {
320
+
321
+ this._recomputeOpaqueBlockerForMaterial( materialIndex );
322
+
323
+ }
324
+
307
325
  const featureProperties = [ 'transmission', 'clearcoat', 'sheen', 'iridescence', 'dispersion', 'transparent', 'opacity', 'alphaTest' ];
308
326
  if ( featureProperties.includes( property ) ) {
309
327
 
@@ -414,6 +432,10 @@ export class MaterialDataManager {
414
432
  data[ stride + M.CLEARCOAT_ROUGHNESS ] = materialData.clearcoatRoughness ?? 0;
415
433
  data[ stride + M.OPACITY ] = materialData.opacity ?? 1;
416
434
  data[ stride + M.SIDE ] = materialData.side ?? 0;
435
+ // Mirror side into per-triangle data so BVH traversal avoids a material-buffer read.
436
+ this._patchTriangleSideForMaterial( materialIndex, materialData.side ?? 0 );
437
+ // Recompute shadow-ray opaque-blocker flag (reads alphaMode/transparent/transmission/opacity from buffer).
438
+ this._recomputeOpaqueBlockerForMaterial( materialIndex );
417
439
  data[ stride + M.TRANSPARENT ] = materialData.transparent ?? 0;
418
440
  data[ stride + M.ALPHA_TEST ] = materialData.alphaTest ?? 0;
419
441
  data[ stride + M.ALPHA_MODE ] = materialData.alphaMode ?? 0;
@@ -657,6 +679,74 @@ export class MaterialDataManager {
657
679
 
658
680
  }
659
681
 
682
+ /**
683
+ * Rewrite the per-triangle `side` flag (NORMAL_C.w) for every triangle whose
684
+ * materialIndex matches. Linear over triangles because there's no reverse
685
+ * index — side edits are a rare UI action so the scan cost is acceptable.
686
+ * @private
687
+ */
688
+ /**
689
+ * Re-derive the shadow-ray opaque-blocker flag for a material from its
690
+ * current buffer values and patch NORMAL_A.w on every matching triangle.
691
+ * Kept in sync with the blocker definition in GeometryExtractor.
692
+ * @private
693
+ */
694
+ _recomputeOpaqueBlockerForMaterial( materialIndex ) {
695
+
696
+ const matBuf = this.materialStorageAttr?.array;
697
+ if ( ! matBuf ) return;
698
+
699
+ const matStride = materialIndex * M.FLOATS_PER_MATERIAL;
700
+ const alphaMode = matBuf[ matStride + M.ALPHA_MODE ] | 0;
701
+ const transparent = matBuf[ matStride + M.TRANSPARENT ] | 0;
702
+ const transmission = matBuf[ matStride + M.TRANSMISSION ] || 0;
703
+ const opacity = matBuf[ matStride + M.OPACITY ] ?? 1;
704
+ const isOpaqueBlocker = ( alphaMode === 0 && transparent === 0 && transmission === 0 && opacity >= 1 ) ? 1.0 : 0.0;
705
+
706
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_BLOCKER_OFFSET, isOpaqueBlocker );
707
+
708
+ }
709
+
710
+ /**
711
+ * Generic helper: patch a single per-triangle float at `triOffset` for every
712
+ * triangle whose materialIndex matches, then fire onTriangleDataChanged.
713
+ * @private
714
+ */
715
+ _patchTriangleFlagForMaterial( materialIndex, triOffset, value ) {
716
+
717
+ const triInfo = this.callbacks.getTriangleData?.();
718
+ const triData = triInfo?.array;
719
+ const triCount = triInfo?.count | 0;
720
+ if ( ! triData || triCount === 0 ) return;
721
+
722
+ const stride = T.FLOATS_PER_TRIANGLE;
723
+ let patched = 0;
724
+ for ( let i = 0; i < triCount; i ++ ) {
725
+
726
+ const base = i * stride;
727
+ if ( triData[ base + TRI_MAT_IDX_OFFSET ] === materialIndex ) {
728
+
729
+ triData[ base + triOffset ] = value;
730
+ patched ++;
731
+
732
+ }
733
+
734
+ }
735
+
736
+ if ( patched > 0 && this.callbacks.onTriangleDataChanged ) {
737
+
738
+ this.callbacks.onTriangleDataChanged();
739
+
740
+ }
741
+
742
+ }
743
+
744
+ _patchTriangleSideForMaterial( materialIndex, sideValue ) {
745
+
746
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_SIDE_OFFSET, sideValue );
747
+
748
+ }
749
+
660
750
  // ===== DISPOSAL =====
661
751
 
662
752
  dispose() {
@@ -237,6 +237,9 @@ export class UniformManager {
237
237
  u( 'emissiveTriangleCount', 0, 'int' );
238
238
  u( 'emissiveTotalPower', 0.0, 'float' );
239
239
  u( 'lightBVHNodeCount', 0, 'int' );
240
+ // Offset (in vec4 elements) within the packed light buffer where emissive
241
+ // triangle data starts. Equals lightBVHNodeCount * LBVH_STRIDE; computed on upload.
242
+ u( 'emissiveVec4Offset', 0, 'int' );
240
243
 
241
244
  // Render mode
242
245
  u( 'renderMode', DEFAULT_STATE.renderMode, 'int' );
@@ -1,87 +0,0 @@
1
- /**
2
- * Proxy-enhanced struct factory for TSL.
3
- *
4
- * TSL structs require `.get('fieldName')` for member access, but GLSL-style
5
- * dot notation (`.fieldName`) is more natural and matches the ported code.
6
- *
7
- * This utility wraps TSL's `struct()` so that:
8
- * - Direct construction: `MyStruct({...}).toVar('x')` → `.fieldName` works automatically
9
- * - Fn return values: `MyStruct.wrap(someFn(...))` → `.fieldName` works automatically
10
- *
11
- * Internally, property access for known struct member names is redirected to `.get('name')`.
12
- * Swizzle properties (x, y, z, w, etc.), Node methods (.add, .assign, etc.), and other
13
- * standard properties pass through to the underlying node unmodified.
14
- */
15
-
16
- import { struct as _struct } from 'three/tsl';
17
-
18
- /**
19
- * Creates a Proxy around a TSL node that redirects struct member access to `.get('name')`.
20
- * Also intercepts `.toVar()` to ensure the resulting VarNode is also proxy-wrapped.
21
- */
22
- function createStructProxy( node, memberSet ) {
23
-
24
- return new Proxy( node, {
25
-
26
- get( target, prop, receiver ) {
27
-
28
- // Intercept known struct member names
29
- if ( typeof prop === 'string' && memberSet.has( prop ) ) {
30
-
31
- return target.get( prop );
32
-
33
- }
34
-
35
- const val = Reflect.get( target, prop, receiver );
36
-
37
- // Intercept .toVar() to proxy-wrap the result
38
- if ( prop === 'toVar' && typeof val === 'function' ) {
39
-
40
- return ( ...args ) => createStructProxy( val.apply( target, args ), memberSet );
41
-
42
- }
43
-
44
- return val;
45
-
46
- }
47
-
48
- } );
49
-
50
- }
51
-
52
- /**
53
- * Drop-in replacement for TSL's `struct()` that returns a proxy-enhanced factory.
54
- *
55
- * The returned factory:
56
- * - Creates struct nodes where `.toVar()` results support dot-notation field access
57
- * - Has `.wrap(node)` method to proxy-wrap Fn return values for field access
58
- * - Has `.layout` and `.isStruct` matching the original TSL struct API
59
- *
60
- * @param {Object} members - Struct member layout (e.g., { didHit: 'bool', dst: 'float' })
61
- * @param {string|null} name - Optional struct name
62
- * @returns {Function} Enhanced struct factory
63
- */
64
- export function struct( members, name = null ) {
65
-
66
- const factory = _struct( members, name );
67
- const memberSet = new Set( Object.keys( members ) );
68
-
69
- const wrappedFactory = ( ...args ) => {
70
-
71
- const node = factory( ...args );
72
- return createStructProxy( node, memberSet );
73
-
74
- };
75
-
76
- wrappedFactory.layout = factory.layout;
77
- wrappedFactory.isStruct = true;
78
-
79
- /**
80
- * Wrap an existing node (e.g., Fn return value) with struct field access proxy.
81
- * Usage: `const hit = HitInfo.wrap(traverseBVH(...).toVar('hit'));`
82
- */
83
- wrappedFactory.wrap = ( node ) => createStructProxy( node, memberSet );
84
-
85
- return wrappedFactory;
86
-
87
- }