rayzee 5.11.0 → 6.0.1

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 +81 -24
  2. package/dist/assets/AIUpscalerWorker-AXN-lKWN.js +2 -0
  3. package/dist/assets/AIUpscalerWorker-AXN-lKWN.js.map +1 -0
  4. package/dist/rayzee.es.js +1238 -1825
  5. package/dist/rayzee.es.js.map +1 -1
  6. package/dist/rayzee.umd.js +50 -74
  7. package/dist/rayzee.umd.js.map +1 -1
  8. package/package.json +1 -4
  9. package/src/AssetConfig.js +56 -0
  10. package/src/EngineDefaults.js +5 -3
  11. package/src/EngineEvents.js +1 -0
  12. package/src/Passes/AIUpscaler.js +44 -22
  13. package/src/Passes/OIDNDenoiser.js +4 -104
  14. package/src/PathTracerApp.js +54 -65
  15. package/src/Processor/AssetLoader.js +5 -2
  16. package/src/Processor/Workers/AIUpscalerWorker.js +21 -6
  17. package/src/Stages/ASVGF.js +6 -27
  18. package/src/Stages/AdaptiveSampling.js +10 -26
  19. package/src/Stages/PathTracer.js +4 -5
  20. package/src/TSL/BVHTraversal.js +3 -30
  21. package/src/TSL/Clearcoat.js +1 -6
  22. package/src/TSL/Common.js +1 -14
  23. package/src/TSL/Debugger.js +0 -12
  24. package/src/TSL/EmissiveSampling.js +0 -16
  25. package/src/TSL/Environment.js +0 -7
  26. package/src/TSL/LightsDirect.js +3 -379
  27. package/src/TSL/LightsIndirect.js +2 -1
  28. package/src/TSL/LightsSampling.js +0 -171
  29. package/src/TSL/MaterialEvaluation.js +3 -103
  30. package/src/TSL/MaterialProperties.js +1 -56
  31. package/src/TSL/MaterialSampling.js +2 -284
  32. package/src/TSL/MaterialTransmission.js +80 -276
  33. package/src/TSL/PathTracerCore.js +8 -1
  34. package/src/TSL/Random.js +0 -23
  35. package/src/TSL/Struct.js +0 -21
  36. package/src/TSL/TextureSampling.js +0 -69
  37. package/src/index.js +5 -2
  38. package/src/managers/DenoisingManager.js +13 -5
  39. package/src/managers/VideoRenderManager.js +4 -4
  40. package/dist/assets/AIUpscalerWorker-D58dcMrY.js +0 -2
  41. package/dist/assets/AIUpscalerWorker-D58dcMrY.js.map +0 -1
  42. package/src/Processor/createRenderTargetHelper.js +0 -521
  43. package/src/TSL/RayIntersection.js +0 -162
  44. package/src/managers/helpers/StatsHelper.js +0 -45
@@ -15,19 +15,14 @@ import {
15
15
  Loop,
16
16
  select,
17
17
  abs,
18
- acos,
19
- sin,
20
- cos,
21
18
  dot,
22
- normalize,
23
19
  reflect,
24
20
  refract,
25
21
  max,
26
22
  min,
27
23
  mix,
28
24
  clamp,
29
- pow,
30
- fract,
25
+ exp,
31
26
  } from 'three/tsl';
32
27
 
33
28
  import { struct } from './patches.js';
@@ -46,6 +41,7 @@ export const TransmissionResult = struct( {
46
41
  direction: 'vec3', // New ray direction after transmission/reflection
47
42
  throughput: 'vec3', // Color throughput including absorption
48
43
  didReflect: 'bool', // Whether the ray was reflected instead of transmitted
44
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
49
45
  } );
50
46
 
51
47
  export const MaterialInteractionResult = struct( {
@@ -57,6 +53,7 @@ export const MaterialInteractionResult = struct( {
57
53
  direction: 'vec3', // New ray direction if continuing
58
54
  throughput: 'vec3', // Color modification for the ray
59
55
  alpha: 'float', // Alpha modification
56
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
60
57
  } );
61
58
 
62
59
  export const Medium = struct( {
@@ -77,6 +74,8 @@ export const MicrofacetTransmissionResult = struct( {
77
74
  halfVector: 'vec3', // Sampled half-vector
78
75
  didReflect: 'bool', // Whether TIR occurred
79
76
  pdf: 'float', // PDF of the sampled direction
77
+ colorWeight: 'vec3', // Spectral tint to apply once; vec3(1) if locked or non-dispersive
78
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
80
79
  } );
81
80
 
82
81
  // Maximum number of nested media
@@ -104,171 +103,55 @@ export const MediumStack = struct( {
104
103
  } );
105
104
 
106
105
  // ================================================================================
107
- // MEDIUM STACK HELPERS
106
+ // DISPERSION
108
107
  // ================================================================================
109
108
 
110
- export const getCurrentMediumIOR = Fn( ( [ mediumStack ] ) => {
111
-
112
- const result = float( 1.0 ).toVar();
113
-
114
- If( mediumStack.depth.greaterThan( int( 0 ) ), () => {
115
-
116
- // Read IOR from current depth
117
- If( mediumStack.depth.equal( int( 1 ) ), () => {
118
-
119
- result.assign( mediumStack.m1_ior );
120
-
121
- } );
122
- If( mediumStack.depth.equal( int( 2 ) ), () => {
123
-
124
- result.assign( mediumStack.m2_ior );
125
-
126
- } );
127
- If( mediumStack.depth.equal( int( 3 ) ), () => {
109
+ // Cauchy IOR n(λ) = baseIOR + 0.03·dispersion / λ_µm²
110
+ export const iorFromWavelength = /*@__PURE__*/ Fn( ( [ baseIOR, dispersionStrength, wavelength ] ) => {
128
111
 
129
- result.assign( mediumStack.m3_ior );
112
+ const wlMicron = wavelength.div( 1000.0 );
113
+ return baseIOR.add( dispersionStrength.mul( 0.03 ).div( wlMicron.mul( wlMicron ) ) );
130
114
 
131
- } );
115
+ } );
132
116
 
133
- } );
117
+ // Wyman et al. JCGT 2013 piecewise-Gaussian fit to CIE 1931 2° observer
118
+ const cieGauss = /*@__PURE__*/ Fn( ( [ x, mu, sigmaLo, sigmaHi ] ) => {
134
119
 
135
- return result;
120
+ const sigma = select( x.lessThan( mu ), sigmaLo, sigmaHi );
121
+ const t = x.sub( mu ).mul( sigma );
122
+ return exp( float( - 0.5 ).mul( t ).mul( t ) );
136
123
 
137
124
  } );
138
125
 
139
- // ================================================================================
140
- // DISPERSION
141
- // ================================================================================
126
+ const wavelengthToXYZ = /*@__PURE__*/ Fn( ( [ wl ] ) => {
142
127
 
143
- // Calculate wavelength-dependent IOR for dispersion using Cauchy dispersion equation
144
- export const calculateDispersiveIOR = /*@__PURE__*/ wgslFn( `
145
- fn calculateDispersiveIOR( baseIOR: f32, dispersionStrength: f32 ) -> vec3f {
146
- let B = dispersionStrength * 0.03f;
147
- // Standard CIE wavelengths for RGB (in micrometers)
148
- let wavelengths = vec3f( 0.7000f, 0.5461f, 0.4358f );
149
- // Apply Cauchy's equation: n(λ) = A + B/λ²
150
- let dispersiveIOR = baseIOR + B / ( wavelengths * wavelengths );
151
- return max( dispersiveIOR, vec3f( 1.001f ) );
152
- }
153
- ` );
128
+ const X = cieGauss( wl, 442.0, 0.0624, 0.0374 ).mul( 0.362 )
129
+ .add( cieGauss( wl, 599.8, 0.0264, 0.0323 ).mul( 1.056 ) )
130
+ .sub( cieGauss( wl, 501.1, 0.0490, 0.0382 ).mul( 0.065 ) );
131
+ const Y = cieGauss( wl, 568.8, 0.0213, 0.0247 ).mul( 0.821 )
132
+ .add( cieGauss( wl, 530.9, 0.0613, 0.0322 ).mul( 0.286 ) );
133
+ const Z = cieGauss( wl, 437.0, 0.0845, 0.0278 ).mul( 1.217 )
134
+ .add( cieGauss( wl, 459.0, 0.0385, 0.0725 ).mul( 0.681 ) );
135
+ return vec3( X, Y, Z );
154
136
 
155
- // Convert wavelength to RGB using spectral sensitivity curves
156
- export const wavelengthToRGB = /*@__PURE__*/ wgslFn( `
157
- fn wavelengthToRGB( wavelength: f32 ) -> vec3f {
158
- var color = vec3f( 0.0f );
159
- // Violet: 380-440
160
- if ( wavelength >= 380.0f && wavelength < 440.0f ) {
161
- let t = ( wavelength - 380.0f ) / 60.0f;
162
- color = vec3f( 0.6f + 0.4f * ( 1.0f - t ), 0.0f, 1.0f );
163
- }
164
- // Blue: 440-490
165
- if ( wavelength >= 440.0f && wavelength < 490.0f ) {
166
- let t = ( wavelength - 440.0f ) / 50.0f;
167
- color = vec3f( 0.6f * ( 1.0f - t ), 0.0f, 1.0f );
168
- }
169
- // Blue-Cyan: 490-510
170
- if ( wavelength >= 490.0f && wavelength < 510.0f ) {
171
- let t = ( wavelength - 490.0f ) / 20.0f;
172
- color = vec3f( 0.0f, t, 1.0f );
173
- }
174
- // Cyan-Green-Yellow: 510-580
175
- if ( wavelength >= 510.0f && wavelength < 580.0f ) {
176
- let t = ( wavelength - 510.0f ) / 70.0f;
177
- if ( t < 0.5f ) {
178
- color = vec3f( 0.0f, 1.0f, 1.0f - t * 2.0f );
179
- } else {
180
- color = vec3f( ( t - 0.5f ) * 2.0f, 1.0f, 0.0f );
181
- }
182
- }
183
- // Yellow-Orange-Red: 580-645
184
- if ( wavelength >= 580.0f && wavelength < 645.0f ) {
185
- let t = ( wavelength - 580.0f ) / 65.0f;
186
- color = vec3f( 1.0f, 1.0f - t, 0.0f );
187
- }
188
- // Red: 645-700
189
- if ( wavelength >= 645.0f && wavelength <= 700.0f ) {
190
- color = vec3f( 1.0f, 0.0f, 0.0f );
191
- }
192
- // Apply intensity falloff at spectrum edges
193
- if ( wavelength < 420.0f ) {
194
- color = color * ( ( wavelength - 380.0f ) / 40.0f );
195
- }
196
- if ( wavelength > 680.0f ) {
197
- color = color * ( ( 700.0f - wavelength ) / 20.0f );
198
- }
199
- return color;
200
- }
201
- ` );
137
+ } );
202
138
 
203
- // Enhanced spectral sampling for realistic dispersion
139
+ // Sample a wavelength on [380,700]nm and return its IOR + sRGB colorWeight (CIE 1931 →
140
+ // sRGB, gamut-clipped). The (1.819, 2.773, 2.928) factors normalize the clipped per-λ
141
+ // average to vec3(1), so clear glass converges to white as samples accumulate.
204
142
  export const sampleWavelengthForDispersion = Fn( ( [ baseIOR, dispersionStrength, random ] ) => {
205
143
 
206
- // Map random value to visible spectrum (380-700nm)
207
144
  const wl = mix( float( 380.0 ), float( 700.0 ), random ).toVar();
145
+ const sampledIOR = iorFromWavelength( baseIOR, dispersionStrength, wl ).toVar();
208
146
 
209
- // Convert to micrometers for Cauchy equation
210
- const wlMicron = wl.div( 1000.0 );
211
-
212
- // Strong IOR calculation for dramatic dispersion
213
- const A = baseIOR;
214
- const B = dispersionStrength.mul( 0.03 );
215
- const sampledIOR = A.add( B.div( wlMicron.mul( wlMicron ) ) ).toVar();
216
-
217
- // PURE SATURATED spectral colors
218
- const colorWeight = vec3( 0.0 ).toVar();
147
+ const xyz = wavelengthToXYZ( wl ).toVar();
148
+ const rgb = vec3(
149
+ xyz.x.mul( 3.2406 ).sub( xyz.y.mul( 1.5372 ) ).sub( xyz.z.mul( 0.4986 ) ),
150
+ xyz.x.mul( - 0.9689 ).add( xyz.y.mul( 1.8758 ) ).add( xyz.z.mul( 0.0415 ) ),
151
+ xyz.x.mul( 0.0557 ).sub( xyz.y.mul( 0.2040 ) ).add( xyz.z.mul( 1.0570 ) ),
152
+ ).toVar();
219
153
 
220
- // Deep Violet: 380-420
221
- If( wl.greaterThanEqual( 380.0 ).and( wl.lessThan( 420.0 ) ), () => {
222
-
223
- colorWeight.assign( vec3( 0.9, 0.0, 1.0 ) );
224
-
225
- } );
226
-
227
- // Blue: 420-480
228
- If( wl.greaterThanEqual( 420.0 ).and( wl.lessThan( 480.0 ) ), () => {
229
-
230
- colorWeight.assign( vec3( 0.0, 0.0, 1.0 ) );
231
-
232
- } );
233
-
234
- // Cyan: 480-500
235
- If( wl.greaterThanEqual( 480.0 ).and( wl.lessThan( 500.0 ) ), () => {
236
-
237
- colorWeight.assign( vec3( 0.0, 1.0, 1.0 ) );
238
-
239
- } );
240
-
241
- // Green: 500-530
242
- If( wl.greaterThanEqual( 500.0 ).and( wl.lessThan( 530.0 ) ), () => {
243
-
244
- colorWeight.assign( vec3( 0.0, 1.0, 0.0 ) );
245
-
246
- } );
247
-
248
- // Yellow: 530-570
249
- If( wl.greaterThanEqual( 530.0 ).and( wl.lessThan( 570.0 ) ), () => {
250
-
251
- colorWeight.assign( vec3( 1.0, 1.0, 0.0 ) );
252
-
253
- } );
254
-
255
- // Orange: 570-620
256
- If( wl.greaterThanEqual( 570.0 ).and( wl.lessThan( 620.0 ) ), () => {
257
-
258
- colorWeight.assign( vec3( 1.0, 0.5, 0.0 ) );
259
-
260
- } );
261
-
262
- // Red: 620-700
263
- If( wl.greaterThanEqual( 620.0 ).and( wl.lessThanEqual( 700.0 ) ), () => {
264
-
265
- colorWeight.assign( vec3( 1.0, 0.0, 0.0 ) );
266
-
267
- } );
268
-
269
- // Maximum saturation
270
- colorWeight.assign( pow( colorWeight, vec3( 0.4 ) ) );
271
- colorWeight.assign( clamp( colorWeight, vec3( 0.0 ), vec3( 2.0 ) ) );
154
+ const colorWeight = max( rgb, vec3( 0.0 ) ).mul( vec3( 1.819, 2.773, 2.928 ) ).toVar();
272
155
 
273
156
  return SpectralSample( {
274
157
  wavelength: wl,
@@ -338,7 +221,7 @@ export const calculateShadowTransmittance = Fn( ( [ rayDir, normal, material, en
338
221
  // ================================================================================
339
222
 
340
223
  export const sampleMicrofacetTransmission = Fn( ( [
341
- V, N, ior, roughness, entering, dispersion, xi, rngState
224
+ V, N, ior, roughness, entering, dispersion, xi, rngState, pathWavelength
342
225
  ] ) => {
343
226
 
344
227
  const result = MicrofacetTransmissionResult( {
@@ -346,6 +229,8 @@ export const sampleMicrofacetTransmission = Fn( ( [
346
229
  halfVector: vec3( 0.0 ),
347
230
  didReflect: false,
348
231
  pdf: float( 0.0 ),
232
+ colorWeight: vec3( 1.0 ),
233
+ pathWavelength: pathWavelength,
349
234
  } ).toVar();
350
235
 
351
236
  // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
@@ -357,9 +242,20 @@ export const sampleMicrofacetTransmission = Fn( ( [
357
242
  const eta = ior;
358
243
  const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
359
244
 
360
- // Handle dispersion with spectral sampling
361
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
362
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
245
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
246
+ If( pathWavelength.greaterThan( 0.0 ), () => {
247
+
248
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
249
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
250
+
251
+ } ).Else( () => {
252
+
253
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
254
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
255
+ result.colorWeight.assign( spectralSample.colorWeight );
256
+ result.pathWavelength.assign( spectralSample.wavelength );
257
+
258
+ } );
363
259
 
364
260
  // Perfect refraction using surface normal
365
261
  const refractDir = refract( V.negate(), N, etaRatio ).toVar();
@@ -390,11 +286,22 @@ export const sampleMicrofacetTransmission = Fn( ( [
390
286
  // Compute IOR ratio
391
287
  const etaRatio = select( entering, float( 1.0 ).div( ior ), ior ).toVar();
392
288
 
393
- // Handle dispersion with improved spectral sampling
289
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
394
290
  If( dispersion.greaterThan( 0.0 ), () => {
395
291
 
396
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
397
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
292
+ If( pathWavelength.greaterThan( 0.0 ), () => {
293
+
294
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
295
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
296
+
297
+ } ).Else( () => {
298
+
299
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
300
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
301
+ result.colorWeight.assign( spectralSample.colorWeight );
302
+ result.pathWavelength.assign( spectralSample.wavelength );
303
+
304
+ } );
398
305
 
399
306
  } );
400
307
 
@@ -446,13 +353,14 @@ export const sampleMicrofacetTransmission = Fn( ( [
446
353
 
447
354
  export const handleTransmission = Fn( ( [
448
355
  rayDir, normal, material, entering, rngState,
449
- currentMediumIOR, previousMediumIOR,
356
+ currentMediumIOR, previousMediumIOR, pathWavelength,
450
357
  ] ) => {
451
358
 
452
359
  const result = TransmissionResult( {
453
360
  direction: vec3( 0.0 ),
454
361
  throughput: vec3( 1.0 ),
455
362
  didReflect: false,
363
+ pathWavelength: pathWavelength,
456
364
  } ).toVar();
457
365
 
458
366
  // Setup surface normal based on ray direction
@@ -509,10 +417,10 @@ export const handleTransmission = Fn( ( [
509
417
 
510
418
  If( doReflect, () => {
511
419
 
512
- // Reflection path
420
+ // Reflection at a transmissive surface — no wavelength locking
513
421
  If( material.roughness.greaterThan( 0.05 ), () => {
514
422
 
515
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, float( 0.0 ), xi, rngState ) );
423
+ const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, float( 0.0 ), xi, rngState, float( 0.0 ) ) );
516
424
  result.direction.assign( mtResult.direction );
517
425
 
518
426
  } ).Else( () => {
@@ -529,125 +437,20 @@ export const handleTransmission = Fn( ( [
529
437
  // Transmission/refraction path
530
438
  If( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ), () => {
531
439
 
532
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState ) );
440
+ const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState, pathWavelength ) );
441
+ result.pathWavelength.assign( mtResult.pathWavelength );
533
442
 
534
- // If TIR occurred during sampling, respect it
535
443
  If( mtResult.didReflect, () => {
536
444
 
445
+ // TIR during intended transmission: compensate for selection probability
537
446
  result.direction.assign( mtResult.direction );
538
447
  result.didReflect.assign( true );
539
- // TIR during intended transmission: compensate for selection probability
540
448
  result.throughput.assign( material.color.xyz.div( max( float( 1.0 ).sub( reflectProb ), 0.05 ) ) );
541
449
 
542
450
  } ).Else( () => {
543
451
 
544
452
  result.direction.assign( mtResult.direction );
545
-
546
- // Handle dispersion coloring
547
- If( material.dispersion.greaterThan( 0.0 ), () => {
548
-
549
- // Calculate refracted ray deviation from original direction
550
- const originalDir = normalize( rayDir );
551
- const refractedDir = normalize( result.direction );
552
-
553
- // Calculate angle-dependent dispersion factor
554
- const edgeFactor = float( 1.0 ).sub( abs( dot( N, originalDir ) ) );
555
- const deviationAngle = acos( clamp( dot( originalDir, refractedDir ), - 1.0, 1.0 ) );
556
-
557
- // Create spatial variation using ray direction and normal
558
- const combinedVec = normalize( originalDir.add( N ) );
559
- const spatialVariation = sin( combinedVec.x.mul( 15.0 ) ).mul( cos( combinedVec.y.mul( 12.0 ) ) ).mul( sin( combinedVec.z.mul( 18.0 ) ) );
560
-
561
- // Add additional variation using refracted direction
562
- const refractVariation = sin( refractedDir.x.mul( 8.0 ).add( refractedDir.y.mul( 6.0 ) ).add( refractedDir.z.mul( 10.0 ) ) );
563
-
564
- // Combine multiple factors for better color distribution
565
- const baseColorIndex = deviationAngle.mul( material.dispersion ).mul( 3.0 );
566
- const spatialBoost = spatialVariation.mul( 0.3 );
567
- const refractBoost = refractVariation.mul( 0.2 );
568
- const edgeBoost = edgeFactor.mul( 0.4 );
569
-
570
- // Create continuous color mapping across the prism
571
- const colorIndex = fract( baseColorIndex.add( spatialBoost ).add( refractBoost ).add( edgeBoost ) ).toVar();
572
-
573
- // ROYGBIV spectrum mapping with smooth transitions
574
- const rainbowColor = vec3( 0.0 ).toVar();
575
-
576
- // Red zone
577
- If( colorIndex.lessThan( 0.143 ), () => {
578
-
579
- const t = colorIndex.div( 0.143 );
580
- rainbowColor.assign( mix( vec3( 0.8, 0.0, 0.0 ), vec3( 1.0, 0.0, 0.0 ), t ) );
581
-
582
- } );
583
-
584
- // Red to Orange
585
- If( colorIndex.greaterThanEqual( 0.143 ).and( colorIndex.lessThan( 0.286 ) ), () => {
586
-
587
- const t = colorIndex.sub( 0.143 ).div( 0.143 );
588
- rainbowColor.assign( mix( vec3( 1.0, 0.0, 0.0 ), vec3( 1.0, 0.6, 0.0 ), t ) );
589
-
590
- } );
591
-
592
- // Orange to Yellow
593
- If( colorIndex.greaterThanEqual( 0.286 ).and( colorIndex.lessThan( 0.429 ) ), () => {
594
-
595
- const t = colorIndex.sub( 0.286 ).div( 0.143 );
596
- rainbowColor.assign( mix( vec3( 1.0, 0.6, 0.0 ), vec3( 1.0, 1.0, 0.0 ), t ) );
597
-
598
- } );
599
-
600
- // Yellow to Green
601
- If( colorIndex.greaterThanEqual( 0.429 ).and( colorIndex.lessThan( 0.571 ) ), () => {
602
-
603
- const t = colorIndex.sub( 0.429 ).div( 0.142 );
604
- rainbowColor.assign( mix( vec3( 1.0, 1.0, 0.0 ), vec3( 0.0, 1.0, 0.0 ), t ) );
605
-
606
- } );
607
-
608
- // Green to Blue
609
- If( colorIndex.greaterThanEqual( 0.571 ).and( colorIndex.lessThan( 0.714 ) ), () => {
610
-
611
- const t = colorIndex.sub( 0.571 ).div( 0.143 );
612
- rainbowColor.assign( mix( vec3( 0.0, 1.0, 0.0 ), vec3( 0.0, 0.4, 1.0 ), t ) );
613
-
614
- } );
615
-
616
- // Blue to Indigo
617
- If( colorIndex.greaterThanEqual( 0.714 ).and( colorIndex.lessThan( 0.857 ) ), () => {
618
-
619
- const t = colorIndex.sub( 0.714 ).div( 0.143 );
620
- rainbowColor.assign( mix( vec3( 0.0, 0.4, 1.0 ), vec3( 0.3, 0.0, 0.8 ), t ) );
621
-
622
- } );
623
-
624
- // Indigo to Violet
625
- If( colorIndex.greaterThanEqual( 0.857 ), () => {
626
-
627
- const t = colorIndex.sub( 0.857 ).div( 0.143 );
628
- rainbowColor.assign( mix( vec3( 0.3, 0.0, 0.8 ), vec3( 0.6, 0.0, 1.0 ), t ) );
629
-
630
- } );
631
-
632
- // Calculate dispersion strength with proper variation
633
- const normalizedDispersion = clamp( material.dispersion.div( 5.0 ), 0.0, 1.0 );
634
- const angleBoost = float( 1.0 ).add( edgeFactor.mul( 1.5 ) );
635
-
636
- // Make dispersion visibility more gradual
637
- const baseVisibility = normalizedDispersion.mul( angleBoost );
638
- const combinedVariation = spatialVariation.add( refractVariation );
639
- const spatialMod = float( 0.5 ).add( float( 0.5 ).mul( sin( combinedVariation.mul( 3.14159 ) ) ) );
640
- const dispersionVisibility = clamp( baseVisibility.mul( spatialMod ), 0.0, 0.8 );
641
-
642
- // Mix rainbow color with clear base for realistic prism effect
643
- result.throughput.assign( mix( vec3( 1.0 ), rainbowColor, dispersionVisibility ) );
644
-
645
- } ).Else( () => {
646
-
647
- // No dispersion - pure white transmission
648
- result.throughput.assign( vec3( 1.0 ) );
649
-
650
- } );
453
+ result.throughput.assign( mtResult.colorWeight );
651
454
 
652
455
  } );
653
456
 
@@ -693,10 +496,9 @@ export const handleTransmission = Fn( ( [
693
496
 
694
497
  export const handleMaterialTransparency = Fn( ( [
695
498
  ray, hitPoint, normal, material, rngState,
696
- // RenderState fields passed individually (since inout not supported)
697
499
  transmissiveTraversals,
698
- // Precomputed medium IOR values from stack
699
500
  currentMediumIOR, previousMediumIOR,
501
+ pathWavelength,
700
502
  ] ) => {
701
503
 
702
504
  const result = MaterialInteractionResult( {
@@ -708,6 +510,7 @@ export const handleMaterialTransparency = Fn( ( [
708
510
  direction: ray.direction,
709
511
  throughput: vec3( 1.0 ),
710
512
  alpha: float( 1.0 ),
513
+ pathWavelength: pathWavelength,
711
514
  } ).toVar();
712
515
 
713
516
  // -----------------------------------------------------------------
@@ -786,7 +589,7 @@ export const handleMaterialTransparency = Fn( ( [
786
589
 
787
590
  const transResult = TransmissionResult.wrap( handleTransmission(
788
591
  ray.direction, normal, material, entering, transmissionSeed,
789
- currentMediumIOR, previousMediumIOR,
592
+ currentMediumIOR, previousMediumIOR, pathWavelength,
790
593
  ) );
791
594
 
792
595
  result.direction.assign( transResult.direction );
@@ -796,6 +599,7 @@ export const handleMaterialTransparency = Fn( ( [
796
599
  result.didReflect.assign( transResult.didReflect );
797
600
  result.entering.assign( entering );
798
601
  result.alpha.assign( float( 1.0 ).sub( material.transmission ) );
602
+ result.pathWavelength.assign( transResult.pathWavelength );
799
603
 
800
604
  } );
801
605
 
@@ -291,8 +291,9 @@ export const generateSampledDirection = Fn( ( [
291
291
  If( sampled.not(), () => {
292
292
 
293
293
  const entering = dot( V, N ).greaterThan( 0.0 ).toVar();
294
+ // pathWavelength=0 — only direction/PDF are consumed here, throughput goes via handleTransmission
294
295
  const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
295
- V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState,
296
+ V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState, float( 0.0 ),
296
297
  ) );
297
298
  resultDirection.assign( mtResult.direction );
298
299
  resultPdf.assign( max( mtResult.pdf, MIN_PDF ) );
@@ -619,6 +620,10 @@ export const Trace = Fn( ( [
619
620
  const mediumStack_ior_2 = float( 1.0 ).toVar();
620
621
  const mediumStack_ior_3 = float( 1.0 ).toVar();
621
622
 
623
+ // Locked at the first dispersive transmission; reused for subsequent transmissions on
624
+ // the path so multi-bounce dispersion doesn't collapse under repeated colorWeight ×.
625
+ const pathWavelength = float( 0.0 ).toVar();
626
+
622
627
  // Render state
623
628
  const stateTraversals = maxBounceCount.toVar();
624
629
  const stateTransmissiveTraversals = transmissiveBounces.toVar();
@@ -802,7 +807,9 @@ export const Trace = Fn( ( [
802
807
  currentRay, hitInfo.hitPoint, N, material, rngState,
803
808
  stateTransmissiveTraversals,
804
809
  currentMediumIOR, previousMediumIOR,
810
+ pathWavelength,
805
811
  ) ).toVar();
812
+ pathWavelength.assign( interaction.pathWavelength );
806
813
 
807
814
  If( interaction.continueRay, () => {
808
815
 
package/src/TSL/Random.js CHANGED
@@ -138,21 +138,6 @@ export const RandomValue = ( state ) => {
138
138
 
139
139
  };
140
140
 
141
- // Generate random float with better precision
142
-
143
- export const RandomValueHighPrecision = ( state ) => {
144
-
145
- // Capture s1 immediately (.toVar()) before advancing state, so it isn't
146
- // re-evaluated lazily after the second pcgHash advances state further.
147
- const s1 = pcgHash( { state } ).toVar();
148
- state.assign( s1 );
149
- const s2 = pcgHash( { state } ).toVar();
150
-
151
- // Combine two 24-bit values for 48-bit precision
152
- return float( s1.shiftRight( 8 ) ).add( float( s2.shiftRight( 8 ) ).mul( 1.0 / 16777216.0 ) ).mul( 1.0 / 16777216.0 );
153
-
154
- };
155
-
156
141
  // -----------------------------------------------------------------------------
157
142
  // Directional sampling functions
158
143
  // -----------------------------------------------------------------------------
@@ -198,14 +183,6 @@ const computeSTBNAtlasCoord = ( pixelCoords, sampleIndex, dimensionIndex, frame
198
183
 
199
184
  };
200
185
 
201
- // Sample 1D scalar STBN value in [0,1]
202
- export const sampleSTBNScalar = ( pixelCoords, sampleIndex, dimensionIndex, frame ) => {
203
-
204
- const coord = computeSTBNAtlasCoord( pixelCoords, sampleIndex, dimensionIndex, frame );
205
- return stbnScalarTextureNode.load( coord ).x;
206
-
207
- };
208
-
209
186
  // Sample decorrelated 2D STBN pair in [0,1]²
210
187
  export const sampleSTBN2D = ( pixelCoords, sampleIndex, dimensionPairIndex, frame ) => {
211
188
 
package/src/TSL/Struct.js CHANGED
@@ -216,22 +216,6 @@ export const MISStrategy = struct( {
216
216
  useEnvSampling: 'bool',
217
217
  } );
218
218
 
219
- // IMPROVEMENT: Multi-layer MIS type aliases and extensions
220
- // Use existing structs with clear naming for multi-lobe MIS
221
- // #define MaterialWeights BRDFWeights
222
- // #define SamplingResult DirectionSample
223
-
224
- // Enhanced material weights for multi-lobe sampling
225
- export const MultiLobeWeights = struct( {
226
- diffuse: 'float',
227
- specular: 'float',
228
- clearcoat: 'float',
229
- transmission: 'float',
230
- sheen: 'float',
231
- iridescence: 'float',
232
- totalWeight: 'float',
233
- } );
234
-
235
219
  // General rendering state (used across all rendering paths)
236
220
  export const RenderState = struct( {
237
221
  traversals: 'int', // Remaining general bounces
@@ -241,8 +225,3 @@ export const RenderState = struct( {
241
225
  actualBounceDepth: 'int', // True depth without manipulation
242
226
  } );
243
227
 
244
- export const pathTracerOutputStruct = struct( {
245
- gColor: 'vec4', // RGB + alpha
246
- gNormalDepth: 'vec4', // Normal(RGB) + depth(A)
247
- gAlbedo: 'vec4' // Albedo color + alpha
248
- } );
@@ -369,75 +369,6 @@ export const sampleAllMaterialTextures = Fn( ( [
369
369
 
370
370
  } );
371
371
 
372
- // ================================================================================
373
- // LEGACY SAMPLING FUNCTIONS
374
- // ================================================================================
375
-
376
- export const sampleAlbedoTexture = Fn( ( [
377
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
378
- material, uv
379
- ] ) => {
380
-
381
- const samples = MaterialSamples.wrap( sampleAllMaterialTextures(
382
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
383
- material, uv, vec3( 0.0, 1.0, 0.0 )
384
- ) );
385
- return samples.albedo;
386
-
387
- } );
388
-
389
- export const sampleEmissiveMap = Fn( ( [
390
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
391
- material, uv
392
- ] ) => {
393
-
394
- const samples = MaterialSamples.wrap( sampleAllMaterialTextures(
395
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
396
- material, uv, vec3( 0.0, 1.0, 0.0 )
397
- ) );
398
- return samples.emissive;
399
-
400
- } );
401
-
402
- export const sampleMetalnessMap = Fn( ( [
403
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
404
- material, uv
405
- ] ) => {
406
-
407
- const samples = MaterialSamples.wrap( sampleAllMaterialTextures(
408
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
409
- material, uv, vec3( 0.0, 1.0, 0.0 )
410
- ) );
411
- return samples.metalness;
412
-
413
- } );
414
-
415
- export const sampleRoughnessMap = Fn( ( [
416
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
417
- material, uv
418
- ] ) => {
419
-
420
- const samples = MaterialSamples.wrap( sampleAllMaterialTextures(
421
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
422
- material, uv, vec3( 0.0, 1.0, 0.0 )
423
- ) );
424
- return samples.roughness;
425
-
426
- } );
427
-
428
- export const sampleNormalMap = Fn( ( [
429
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
430
- material, uv, normal
431
- ] ) => {
432
-
433
- const samples = MaterialSamples.wrap( sampleAllMaterialTextures(
434
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
435
- material, uv, normal
436
- ) );
437
- return samples.normal;
438
-
439
- } );
440
-
441
372
  // Sample displacement map at given UV coordinates
442
373
  export const sampleDisplacementMap = Fn( ( [ displacementMaps, displacementMapIndex, uv, transform ] ) => {
443
374