rayzee 6.0.0 → 6.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 (44) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2396 -2072
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +49 -53
  6. package/dist/rayzee.umd.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/EngineDefaults.js +3 -0
  9. package/src/PathTracerApp.js +18 -8
  10. package/src/Pipeline/RenderStage.js +3 -0
  11. package/src/Processor/IESParser.js +340 -0
  12. package/src/Processor/LightSerializer.js +32 -4
  13. package/src/Processor/SceneProcessor.js +0 -1
  14. package/src/Processor/ShaderBuilder.js +40 -1
  15. package/src/Processor/Workers/TexturesWorker.js +1 -1
  16. package/src/RenderSettings.js +3 -0
  17. package/src/Stages/NormalDepth.js +3 -19
  18. package/src/Stages/PathTracer.js +15 -9
  19. package/src/TSL/BVHTraversal.js +5 -18
  20. package/src/TSL/Clearcoat.js +1 -5
  21. package/src/TSL/Common.js +2 -2
  22. package/src/TSL/Debugger.js +0 -14
  23. package/src/TSL/EmissiveSampling.js +20 -22
  24. package/src/TSL/Environment.js +60 -14
  25. package/src/TSL/Fresnel.js +13 -4
  26. package/src/TSL/LightsCore.js +238 -5
  27. package/src/TSL/LightsDirect.js +16 -5
  28. package/src/TSL/LightsIndirect.js +6 -38
  29. package/src/TSL/LightsSampling.js +119 -185
  30. package/src/TSL/MaterialEvaluation.js +25 -14
  31. package/src/TSL/MaterialProperties.js +14 -34
  32. package/src/TSL/MaterialTransmission.js +100 -222
  33. package/src/TSL/PathTracer.js +5 -4
  34. package/src/TSL/PathTracerCore.js +152 -140
  35. package/src/TSL/Struct.js +7 -1
  36. package/src/TSL/TextureSampling.js +2 -2
  37. package/src/index.js +2 -0
  38. package/src/managers/AnimationManager.js +3 -6
  39. package/src/managers/DenoisingManager.js +1 -1
  40. package/src/managers/GoboManager.js +277 -0
  41. package/src/managers/IESManager.js +268 -0
  42. package/src/managers/LightManager.js +33 -1
  43. package/src/managers/TransformManager.js +3 -3
  44. package/src/managers/UniformManager.js +5 -5
@@ -3,14 +3,25 @@
3
3
 
4
4
  import {
5
5
  Fn, wgslFn,
6
+ vec2,
6
7
  vec3,
7
8
  float,
9
+ int,
8
10
  If,
9
11
  dot,
10
12
  normalize,
11
13
  cross,
12
14
  length,
13
15
  abs,
16
+ select,
17
+ clamp,
18
+ max,
19
+ min,
20
+ mix,
21
+ tan,
22
+ acos,
23
+ atan,
24
+ texture,
14
25
  } from 'three/tsl';
15
26
 
16
27
  import { struct } from './patches.js';
@@ -24,6 +35,9 @@ export const DirectionalLight = struct( {
24
35
  color: 'vec3',
25
36
  intensity: 'float',
26
37
  angle: 'float', // Angular diameter in radians
38
+ goboIndex: 'int', // -1 = no gobo, otherwise index into goboMaps array
39
+ goboIntensity: 'float', // signed: negative = inverted, |value| = strength
40
+ goboScale: 'float', // world units per gobo tile (used by directional projection)
27
41
  } );
28
42
 
29
43
  export const AreaLight = struct( {
@@ -53,6 +67,10 @@ export const SpotLight = struct( {
53
67
  penumbra: 'float', // penumbra factor [0,1]
54
68
  distance: 'float', // cutoff distance (0 = infinite)
55
69
  decay: 'float', // decay exponent (2 = physically correct)
70
+ goboIndex: 'int', // -1 = no gobo, otherwise index into goboMaps array
71
+ goboIntensity: 'float', // mask strength multiplier (0 = no mask, 1 = full mask)
72
+ iesIndex: 'int', // -1 = no IES, otherwise index into iesProfiles array
73
+ iesIntensity: 'float', // blend [0,1] between flat (0) and full IES distribution (1)
56
74
  } );
57
75
 
58
76
  export const LightSample = struct( {
@@ -84,7 +102,7 @@ export const LIGHT_TYPE_SPOT = 3;
84
102
 
85
103
  export const getDirectionalLight = Fn( ( [ directionalLightsBuffer, index ] ) => {
86
104
 
87
- const baseIndex = index.mul( 8 );
105
+ const baseIndex = index.mul( 12 );
88
106
  return DirectionalLight( {
89
107
  direction: normalize( vec3(
90
108
  directionalLightsBuffer.element( baseIndex ),
@@ -98,6 +116,9 @@ export const getDirectionalLight = Fn( ( [ directionalLightsBuffer, index ] ) =>
98
116
  ),
99
117
  intensity: directionalLightsBuffer.element( baseIndex.add( 6 ) ),
100
118
  angle: directionalLightsBuffer.element( baseIndex.add( 7 ) ),
119
+ goboIndex: int( directionalLightsBuffer.element( baseIndex.add( 8 ) ) ),
120
+ goboIntensity: directionalLightsBuffer.element( baseIndex.add( 9 ) ),
121
+ goboScale: directionalLightsBuffer.element( baseIndex.add( 10 ) ),
101
122
  } );
102
123
 
103
124
  } );
@@ -161,7 +182,7 @@ export const getPointLight = Fn( ( [ pointLightsBuffer, index ] ) => {
161
182
 
162
183
  export const getSpotLight = Fn( ( [ spotLightsBuffer, index ] ) => {
163
184
 
164
- const baseIndex = index.mul( 14 );
185
+ const baseIndex = index.mul( 20 );
165
186
  return SpotLight( {
166
187
  position: vec3(
167
188
  spotLightsBuffer.element( baseIndex ),
@@ -183,6 +204,11 @@ export const getSpotLight = Fn( ( [ spotLightsBuffer, index ] ) => {
183
204
  penumbra: spotLightsBuffer.element( baseIndex.add( 11 ) ),
184
205
  distance: spotLightsBuffer.element( baseIndex.add( 12 ) ),
185
206
  decay: spotLightsBuffer.element( baseIndex.add( 13 ) ),
207
+ goboIndex: int( spotLightsBuffer.element( baseIndex.add( 14 ) ) ),
208
+ goboIntensity: spotLightsBuffer.element( baseIndex.add( 15 ) ),
209
+ iesIndex: int( spotLightsBuffer.element( baseIndex.add( 16 ) ) ),
210
+ iesIntensity: spotLightsBuffer.element( baseIndex.add( 17 ) ),
211
+ // slots 18, 19 reserved (vec4 padding)
186
212
  } );
187
213
 
188
214
  } );
@@ -198,13 +224,17 @@ export const isDirectionValid = /*@__PURE__*/ wgslFn( `
198
224
  }
199
225
  ` );
200
226
 
201
- // Distance attenuation based on Frostbite PBR
227
+ // Distance attenuation based on Frostbite PBR. Integer exponents factored as
228
+ // repeated multiplies — pow(x, 4) and pow(x, 2) are far cheaper this way.
202
229
  export const getDistanceAttenuation = /*@__PURE__*/ wgslFn( `
203
230
  fn getDistanceAttenuation( lightDistance: f32, cutoffDistance: f32, decayExponent: f32 ) -> f32 {
204
231
  var distanceFalloff = 1.0f / max( pow( lightDistance, decayExponent ), 0.01f );
205
232
  if ( cutoffDistance > 0.0f ) {
206
- let ratio = pow( lightDistance / cutoffDistance, 4.0f );
207
- distanceFalloff *= pow( clamp( 1.0f - ratio, 0.0f, 1.0f ), 2.0f );
233
+ let r = lightDistance / cutoffDistance;
234
+ let r2 = r * r;
235
+ let ratio = r2 * r2;
236
+ let window = clamp( 1.0f - ratio, 0.0f, 1.0f );
237
+ distanceFalloff *= window * window;
208
238
  }
209
239
  return distanceFalloff;
210
240
  }
@@ -218,6 +248,209 @@ export const getSpotAttenuation = /*@__PURE__*/ wgslFn( `
218
248
  ` );
219
249
 
220
250
 
251
+ // ================================================================================
252
+ // SPOT LIGHT GOBO (PROJECTION MASK) SAMPLING
253
+ // ================================================================================
254
+
255
+ // Module-level state for spot light gobo masks.
256
+ // Set by ShaderBuilder before graph construction.
257
+ let _goboMapsTexNode = null;
258
+
259
+ /**
260
+ * Set the DataArrayTexture node used to sample spot light gobo masks.
261
+ * Must be called before the shader graph is constructed.
262
+ * @param {TextureNode} node - TSL texture node for the gobo DataArrayTexture
263
+ */
264
+ export function setGoboMapsTexture( node ) {
265
+
266
+ _goboMapsTexNode = node;
267
+
268
+ }
269
+
270
+ // Sample a spot light's gobo mask. Returns 1.0 if no gobo assigned.
271
+ // Projects the surface direction onto a plane perpendicular to the light's
272
+ // forward axis at unit distance; cone edge maps to UV ±0.5 around centre.
273
+ //
274
+ // `lightDir` = unit direction from surface TO light (matches `LightSample.direction`).
275
+ export const sampleSpotGoboMask = /*@__PURE__*/ Fn( ( [ light, lightDir ] ) => {
276
+
277
+ const mask = float( 1.0 ).toVar();
278
+
279
+ If( light.goboIndex.greaterThanEqual( int( 0 ) ), () => {
280
+
281
+ const forward = light.direction.toVar();
282
+ const toSurface = lightDir.negate().toVar();
283
+ const cosAlpha = dot( toSurface, forward ).toVar();
284
+
285
+ If( cosAlpha.greaterThan( 0.0 ), () => {
286
+
287
+ // Orthonormal basis around forward axis
288
+ const up = select(
289
+ abs( forward.z ).lessThan( 0.999 ),
290
+ vec3( 0.0, 0.0, 1.0 ),
291
+ vec3( 1.0, 0.0, 0.0 ),
292
+ );
293
+ const T = normalize( cross( up, forward ) ).toVar();
294
+ const B = cross( forward, T ).toVar();
295
+
296
+ // Project onto plane perpendicular to forward at distance 1
297
+ const invCos = float( 1.0 ).div( cosAlpha ).toVar();
298
+ const px = dot( toSurface, T ).mul( invCos ).toVar();
299
+ const py = dot( toSurface, B ).mul( invCos ).toVar();
300
+
301
+ // Cone edge → ±tan(angle); map to UV [0,1]
302
+ const tanA = max( tan( light.angle ), float( 1e-4 ) ).toVar();
303
+ const invTan = float( 0.5 ).div( tanA ).toVar();
304
+ const u = clamp( px.mul( invTan ).add( 0.5 ), float( 0.0 ), float( 1.0 ) ).toVar();
305
+ const v = clamp( py.mul( invTan ).add( 0.5 ), float( 0.0 ), float( 1.0 ) ).toVar();
306
+
307
+ if ( _goboMapsTexNode ) {
308
+
309
+ // Sample min(.r, .a) so masks encoded in either RGB-luminance
310
+ // or alpha (Kenney's "Transparent" variants store the shape in alpha
311
+ // with RGB=white) both produce the expected result.
312
+ // Sign of goboIntensity encodes inversion: negative = inverted, |value| = strength.
313
+ const tex = texture( _goboMapsTexNode, vec2( u, v ) ).depth( light.goboIndex );
314
+ const sample = min( tex.r, tex.a );
315
+ const inverted = light.goboIntensity.lessThan( 0.0 );
316
+ const effective = select( inverted, float( 1.0 ).sub( sample ), sample );
317
+ const strength = clamp( abs( light.goboIntensity ), float( 0.0 ), float( 1.0 ) );
318
+ mask.assign( mix( float( 1.0 ), effective, strength ) );
319
+
320
+ }
321
+
322
+ } ).Else( () => {
323
+
324
+ // Behind the light — emit zero so back-hemisphere is dark.
325
+ mask.assign( 0.0 );
326
+
327
+ } );
328
+
329
+ } );
330
+
331
+ return mask;
332
+
333
+ } );
334
+
335
+ // Sample a directional light's gobo mask. Returns 1.0 if no gobo assigned.
336
+ // Projects the shading point onto a plane perpendicular to the light direction;
337
+ // the mask is tiled at `light.goboScale` world units per tile so a single mask
338
+ // can cover any scene size by adjusting the scale.
339
+ //
340
+ // `surfacePoint` = world-space position of the surface being shaded.
341
+ export const sampleDirectionalGoboMask = /*@__PURE__*/ Fn( ( [ light, surfacePoint ] ) => {
342
+
343
+ const mask = float( 1.0 ).toVar();
344
+
345
+ If( light.goboIndex.greaterThanEqual( int( 0 ) ), () => {
346
+
347
+ // `light.direction` in this engine points FROM target TOWARD the light.
348
+ // Project surface point onto plane perpendicular to that axis.
349
+ const axis = light.direction.toVar();
350
+
351
+ const up = select(
352
+ abs( axis.z ).lessThan( 0.999 ),
353
+ vec3( 0.0, 0.0, 1.0 ),
354
+ vec3( 1.0, 0.0, 0.0 ),
355
+ );
356
+ const T = normalize( cross( up, axis ) ).toVar();
357
+ const B = cross( axis, T ).toVar();
358
+
359
+ const invScale = float( 1.0 ).div( max( light.goboScale, float( 1e-4 ) ) ).toVar();
360
+ const u = dot( surfacePoint, T ).mul( invScale ).add( 0.5 ).toVar();
361
+ const v = dot( surfacePoint, B ).mul( invScale ).add( 0.5 ).toVar();
362
+
363
+ // Tile by fract so a single mask can cover any scene size.
364
+ const uTiled = u.sub( u.floor() ).toVar();
365
+ const vTiled = v.sub( v.floor() ).toVar();
366
+
367
+ if ( _goboMapsTexNode ) {
368
+
369
+ const tex = texture( _goboMapsTexNode, vec2( uTiled, vTiled ) ).depth( light.goboIndex );
370
+ const sample = min( tex.r, tex.a );
371
+ const inverted = light.goboIntensity.lessThan( 0.0 );
372
+ const effective = select( inverted, float( 1.0 ).sub( sample ), sample );
373
+ const strength = clamp( abs( light.goboIntensity ), float( 0.0 ), float( 1.0 ) );
374
+ mask.assign( mix( float( 1.0 ), effective, strength ) );
375
+
376
+ }
377
+
378
+ } );
379
+
380
+ return mask;
381
+
382
+ } );
383
+
384
+ // ================================================================================
385
+ // IES PROFILE (PHOTOMETRIC INTENSITY) SAMPLING
386
+ // ================================================================================
387
+
388
+ // Module-level texture node for IES profile DataArrayTexture.
389
+ // Set by ShaderBuilder before graph construction.
390
+ let _iesProfilesTexNode = null;
391
+
392
+ /**
393
+ * Bind the DataArrayTexture node carrying all loaded IES profiles.
394
+ * @param {TextureNode} node
395
+ */
396
+ export function setIESProfilesTexture( node ) {
397
+
398
+ _iesProfilesTexNode = node;
399
+
400
+ }
401
+
402
+ // Sample a spot light's IES profile. Returns a normalized multiplier in [0,1]
403
+ // (or 1.0 if no profile assigned).
404
+ //
405
+ // IES texture layout: U = horizontal angle (0..360°), V = vertical angle (0..180°)
406
+ // where V=0 is along the light's "forward" axis (the spot's direction).
407
+ //
408
+ // `lightDir` = unit direction from surface TO light (matches LightSample.direction).
409
+ export const sampleIESProfile = /*@__PURE__*/ Fn( ( [ light, lightDir ] ) => {
410
+
411
+ const result = float( 1.0 ).toVar();
412
+
413
+ If( light.iesIndex.greaterThanEqual( int( 0 ) ), () => {
414
+
415
+ const forward = light.direction.toVar();
416
+ const toSurface = lightDir.negate().toVar();
417
+
418
+ // Vertical angle: between forward axis and emission direction. 0 = on axis (V=0),
419
+ // PI = anti-axis (V=1).
420
+ const cosV = clamp( dot( toSurface, forward ), float( - 1.0 ), float( 1.0 ) ).toVar();
421
+ const vAngle = acos( cosV ).toVar();
422
+ const v = clamp( vAngle.div( float( Math.PI ) ), float( 0.0 ), float( 1.0 ) ).toVar();
423
+
424
+ // Horizontal angle: project emission direction onto plane perpendicular to forward.
425
+ const up = select(
426
+ abs( forward.z ).lessThan( 0.999 ),
427
+ vec3( 0.0, 0.0, 1.0 ),
428
+ vec3( 1.0, 0.0, 0.0 ),
429
+ );
430
+ const T = normalize( cross( up, forward ) ).toVar();
431
+ const B = cross( forward, T ).toVar();
432
+
433
+ const px = dot( toSurface, T );
434
+ const py = dot( toSurface, B );
435
+ // atan2 → [-PI, PI]; remap to [0, 2PI] then to [0, 1].
436
+ const phi = atan( py, px );
437
+ const u = phi.div( float( 2.0 * Math.PI ) ).add( 0.5 ).toVar();
438
+
439
+ if ( _iesProfilesTexNode ) {
440
+
441
+ const sample = texture( _iesProfilesTexNode, vec2( u, v ) ).depth( light.iesIndex ).r;
442
+ // Blend between flat (1.0) and full profile by iesIntensity.
443
+ const strength = clamp( light.iesIntensity, float( 0.0 ), float( 1.0 ) );
444
+ result.assign( mix( float( 1.0 ), sample, strength ) );
445
+
446
+ }
447
+
448
+ } );
449
+
450
+ return result;
451
+
452
+ } );
453
+
221
454
  // ================================================================================
222
455
  // CONE SAMPLING FOR SOFT DIRECTIONAL SHADOWS
223
456
  // ================================================================================
@@ -11,6 +11,8 @@ import {
11
11
  Loop,
12
12
  Break,
13
13
  dot,
14
+ cross,
15
+ normalize,
14
16
  abs,
15
17
  max,
16
18
  min,
@@ -61,7 +63,7 @@ export function setAlphaShadowsUniform( node ) {
61
63
 
62
64
  // Note: traverseBVH is passed as a parameter to avoid circular dependency
63
65
  export const traceShadowRay = Fn( ( [
64
- origin, dir, maxDist, rngState,
66
+ origin, dir, maxDist,
65
67
  // BVH traversal function and textures passed as parameters
66
68
  traverseBVHShadowFn,
67
69
  bvhBuffer,
@@ -83,7 +85,6 @@ export const traceShadowRay = Fn( ( [
83
85
  shadowRay,
84
86
  bvhBuffer,
85
87
  triangleBuffer,
86
- materialBuffer,
87
88
  remainingDist,
88
89
  ) );
89
90
 
@@ -184,8 +185,18 @@ export const traceShadowRay = Fn( ( [
184
185
 
185
186
  } ).ElseIf( shadowMaterial.transmission.greaterThan( 0.0 ), () => {
186
187
 
187
- const entering = dot( dir, shadowHit.normal ).lessThan( 0.0 );
188
- const N = select( entering, shadowHit.normal, shadowHit.normal.negate() );
188
+ // Deferred geometric-normal compute refetch triangle positions and
189
+ // derive the normal here so opaque/alpha-cutout shadow hits don't pay
190
+ // the cross+normalize cost in BVH traversal.
191
+ const TRI_STRIDE_N = int( 8 );
192
+ const pA = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 0 ), TRI_STRIDE_N ).xyz;
193
+ const pB = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 1 ), TRI_STRIDE_N ).xyz;
194
+ const pC = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 2 ), TRI_STRIDE_N ).xyz;
195
+ const geomNormal = normalize( cross( pB.sub( pA ), pC.sub( pA ) ) );
196
+ shadowHit.normal.assign( geomNormal );
197
+
198
+ const entering = dot( dir, geomNormal ).lessThan( 0.0 );
199
+ const N = select( entering, geomNormal, geomNormal.negate() );
189
200
 
190
201
  // Apply absorption if exiting medium
191
202
  If( entering.not().and( shadowMaterial.attenuationDistance.greaterThan( 0.0 ) ), () => {
@@ -285,7 +296,7 @@ export const calculateRayOffset = Fn( ( [ hitPoint, normal, material ] ) => {
285
296
  // LIGHT IMPORTANCE ESTIMATION
286
297
  // ================================================================================
287
298
 
288
- export const calculateDirectionalLightImportance = Fn( ( [ light, hitPoint, normal, material, bounceIndex ] ) => {
299
+ export const calculateDirectionalLightImportance = Fn( ( [ light, normal, material, bounceIndex ] ) => {
289
300
 
290
301
  const NoL = max( float( 0.0 ), dot( normal, light.direction ) );
291
302
  const result = float( 0.0 ).toVar();
@@ -19,18 +19,13 @@ import {
19
19
  float,
20
20
  vec2,
21
21
  vec3,
22
- vec4,
23
22
  int,
24
- uint,
25
23
  bool as tslBool,
26
24
  max,
27
- min,
28
25
  abs,
29
26
  sqrt,
30
27
  dot,
31
28
  normalize,
32
- length,
33
- clamp,
34
29
  If,
35
30
  select,
36
31
  } from 'three/tsl';
@@ -40,10 +35,8 @@ import {
40
35
  } from './LightsCore.js';
41
36
  import {
42
37
  SamplingStrategyWeights,
43
- ImportanceSamplingInfo,
44
38
  } from './Struct.js';
45
39
  import {
46
- PI,
47
40
  PI_INV,
48
41
  EPSILON,
49
42
  MIN_PDF,
@@ -51,7 +44,6 @@ import {
51
44
  import { DistributionGGX, calculateVNDFPDF } from './MaterialProperties.js';
52
45
  import { evaluateMaterialResponse } from './MaterialEvaluation.js';
53
46
  import { RandomValue } from './Random.js';
54
- import { sampleEquirectProbability, sampleEquirect } from './Environment.js';
55
47
  import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
56
48
  import { cosineWeightedSample } from './MaterialSampling.js';
57
49
 
@@ -115,8 +107,7 @@ export const calculateClearcoatPDF = Fn( ( [ V, L, N, clearcoatRoughness ] ) =>
115
107
 
116
108
  // Compute normalized strategy weights based on importance info
117
109
  export const computeSamplingInfo = Fn( ( [
118
- samplingInfo, bounceIndex, material,
119
- enableEnvironmentLight, useEnvMapIS,
110
+ samplingInfo,
120
111
  ] ) => {
121
112
 
122
113
  // Material-only strategies (env handled via deterministic NEE in direct lighting)
@@ -177,13 +168,11 @@ export const computeSamplingInfo = Fn( ( [
177
168
  // Strategy Selection via Cumulative Distribution
178
169
  // =============================================================================
179
170
 
180
- // Returns vec2(selectedStrategy, strategyPdf)
181
171
  // Strategy IDs: 1=specular, 2=diffuse, 3=transmission, 4=clearcoat
182
172
  // (env removed — handled via deterministic NEE in direct lighting)
183
173
  export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
184
174
 
185
175
  const selectedStrategy = int( 2 ).toVar(); // Default: diffuse
186
- const strategyPdf = float( 1.0 ).toVar();
187
176
 
188
177
  const cumulative = float( 0.0 ).toVar();
189
178
  const found = tslBool( false ).toVar();
@@ -195,7 +184,6 @@ export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
195
184
  If( randomValue.lessThan( cumulative ), () => {
196
185
 
197
186
  selectedStrategy.assign( 1 );
198
- strategyPdf.assign( weights.specularWeight );
199
187
  found.assign( tslBool( true ) );
200
188
 
201
189
  } );
@@ -209,7 +197,6 @@ export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
209
197
  If( randomValue.lessThan( cumulative ), () => {
210
198
 
211
199
  selectedStrategy.assign( 2 );
212
- strategyPdf.assign( weights.diffuseWeight );
213
200
  found.assign( tslBool( true ) );
214
201
 
215
202
  } );
@@ -223,7 +210,6 @@ export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
223
210
  If( randomValue.lessThan( cumulative ), () => {
224
211
 
225
212
  selectedStrategy.assign( 3 );
226
- strategyPdf.assign( weights.transmissionWeight );
227
213
  found.assign( tslBool( true ) );
228
214
 
229
215
  } );
@@ -233,20 +219,10 @@ export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
233
219
  If( weights.useClearcoat.and( found.not() ), () => {
234
220
 
235
221
  selectedStrategy.assign( 4 );
236
- strategyPdf.assign( weights.clearcoatWeight );
237
- found.assign( tslBool( true ) );
238
-
239
- } );
240
-
241
- // Fallback
242
- If( found.not(), () => {
243
-
244
- selectedStrategy.assign( 2 ); // Diffuse
245
- strategyPdf.assign( select( weights.useDiffuse, weights.diffuseWeight, float( 1.0 ) ) );
246
222
 
247
223
  } );
248
224
 
249
- return vec2( float( selectedStrategy ), strategyPdf );
225
+ return selectedStrategy;
250
226
 
251
227
  } );
252
228
 
@@ -268,13 +244,8 @@ export const calculateIndirectLighting = Fn( ( [
268
244
  V, N, material,
269
245
  // brdfSample fields (DirectionSample)
270
246
  brdfSampleDirection, brdfSamplePdf, brdfSampleValue,
271
- sampleIndex, bounceIndex,
272
247
  rngState,
273
248
  samplingInfo,
274
- // Environment resources
275
- envTexture, environmentIntensity, envMatrix,
276
- envTotalSum, envCompensationDelta, envResolution,
277
- enableEnvironmentLight, useEnvMapIS,
278
249
  ] ) => {
279
250
 
280
251
  // Initialize result
@@ -289,7 +260,6 @@ export const calculateIndirectLighting = Fn( ( [
289
260
  .and( samplingInfo.specularImportance.greaterThanEqual( 0.0 ) )
290
261
  .and( samplingInfo.transmissionImportance.greaterThanEqual( 0.0 ) )
291
262
  .and( samplingInfo.clearcoatImportance.greaterThanEqual( 0.0 ) )
292
- .and( samplingInfo.envmapImportance.greaterThanEqual( 0.0 ) )
293
263
  .toVar();
294
264
 
295
265
  If( validInput.not(), () => {
@@ -307,8 +277,7 @@ export const calculateIndirectLighting = Fn( ( [
307
277
 
308
278
  // Use corrected sampling info
309
279
  const weights = SamplingStrategyWeights.wrap( computeSamplingInfo(
310
- samplingInfo, bounceIndex, material,
311
- enableEnvironmentLight, useEnvMapIS,
280
+ samplingInfo,
312
281
  ).toVar() );
313
282
 
314
283
  const selectionRand = RandomValue( rngState ).toVar();
@@ -317,9 +286,7 @@ export const calculateIndirectLighting = Fn( ( [
317
286
  const sampleRand = vec2( r1, r2 ).toVar();
318
287
 
319
288
  // Strategy selection
320
- const strategyResult = selectSamplingStrategy( weights, selectionRand ).toVar();
321
- const selectedStrategy = int( strategyResult.x ).toVar();
322
- const strategySelectionPdf = strategyResult.y.toVar();
289
+ const selectedStrategy = selectSamplingStrategy( weights, selectionRand ).toVar();
323
290
 
324
291
  const sampleDir = vec3( 0.0 ).toVar();
325
292
  const samplePdf = float( 0.0 ).toVar();
@@ -346,8 +313,9 @@ export const calculateIndirectLighting = Fn( ( [
346
313
 
347
314
  // Strategy 3: Transmission
348
315
  const entering = dot( V, N ).greaterThan( 0.0 ).toVar();
316
+ // pathWavelength=0 — MIS evaluation reads only direction/PDF, no spectral tint
349
317
  const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
350
- V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState
318
+ V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState, float( 0.0 )
351
319
  ).toVar() );
352
320
  sampleDir.assign( mtResult.direction );
353
321
  samplePdf.assign( mtResult.pdf );