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
@@ -1,16 +1,16 @@
1
- import { Fn, float, vec3, vec4, int, If, dot, max, min, sqrt, cos, exp, mix, clamp, smoothstep } from 'three/tsl';
1
+ import { Fn, float, vec3, int, If, dot, max, min, sqrt, cos, exp, mix, clamp, smoothstep } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  BRDFWeights,
5
- MaterialClassification,
6
5
  MaterialCache,
7
6
  ImportanceSamplingInfo,
7
+ DFGResult,
8
8
 
9
9
  } from './Struct.js';
10
10
 
11
11
  import {
12
- PI, TWO_PI, EPSILON, MIN_ROUGHNESS, REC709_LUMINANCE_COEFFICIENTS,
13
- XYZ_TO_REC709, square, squareVec3, maxComponent,
12
+ PI, TWO_PI, EPSILON, MIN_ROUGHNESS,
13
+ XYZ_TO_REC709, square,
14
14
  } from './Common.js';
15
15
 
16
16
  import {
@@ -68,10 +68,14 @@ export const GeometrySmith = Fn( ( [ NoV, NoL, roughness ] ) => {
68
68
  // multiplicative factor for the specular BRDF that compensates for this loss.
69
69
  // Based on: Kulla & Conty 2017 + Karis 2014 analytical DFG approximation.
70
70
 
71
- export const multiscatterCompensation = Fn( ( [ F0, NoV, roughness ] ) => {
71
+ // Single Karis DFG evaluation that returns both outputs the BRDF needs:
72
+ // compensation — multiscatter energy compensation factor for the specular lobe
73
+ // E_total — total specular directional albedo (single-scatter × compensation)
74
+ // Both share the same dfgScale/dfgBias/Ew polynomial, so computing them together
75
+ // halves the polynomial work versus calling two separate functions.
76
+ export const evaluateDFG = Fn( ( [ F0, NoV, roughness ] ) => {
72
77
 
73
78
  // Analytical DFG approximation (Karis 2014)
74
- // Computes scale and bias where E(F0) = F0 * scale + bias
75
79
  const r0 = float( 1.0 ).sub( roughness );
76
80
  const r1 = roughness.mul( - 0.0275 ).add( 0.0425 );
77
81
  const r2 = roughness.mul( - 0.572 ).add( 1.04 );
@@ -87,35 +91,13 @@ export const multiscatterCompensation = Fn( ( [ F0, NoV, roughness ] ) => {
87
91
  const Ew = max( dfgScale.add( dfgBias ), 0.1 );
88
92
 
89
93
  // Energy compensation: 1 + F0 * (1/Ew - 1)
90
- // At F0=1 (metals): fully compensates to 1/Ew
91
- // At F0=0.04 (dielectrics): negligible correction
92
- return vec3( 1.0 ).add( F0.mul( float( 1.0 ).div( Ew ).sub( 1.0 ) ) );
93
-
94
- } );
95
-
96
- // Compute the total specular directional albedo including multiscatter compensation.
97
- // Returns per-channel fraction of energy captured by specular reflection,
98
- // used for energy-conserving diffuse weight: kD = (1 - E_total) * (1 - metalness).
99
- export const specularDirectionalAlbedo = Fn( ( [ F0, NoV, roughness ] ) => {
100
-
101
- // Analytical DFG approximation (same as multiscatterCompensation)
102
- const r0 = float( 1.0 ).sub( roughness );
103
- const r1 = roughness.mul( - 0.0275 ).add( 0.0425 );
104
- const r2 = roughness.mul( - 0.572 ).add( 1.04 );
105
- const r3 = roughness.mul( 0.022 ).sub( 0.04 );
106
- const a004 = min( r0.mul( r0 ), exp( float( - 6.4308 ).mul( NoV ) ) ).mul( r0 ).add( r1 );
107
- const dfgScale = float( - 1.04 ).mul( a004 ).add( r2 );
108
- const dfgBias = float( 1.04 ).mul( a004 ).add( r3 );
94
+ const compensation = vec3( 1.0 ).add( F0.mul( float( 1.0 ).div( Ew ).sub( 1.0 ) ) );
109
95
 
110
- // Single-scatter directional albedo per channel: E_ss = F0 * scale + bias
96
+ // Single-scatter directional albedo per channel, then total with compensation
111
97
  const E_ss = max( F0.mul( dfgScale ).add( vec3( dfgBias ) ), vec3( 0.0 ) );
98
+ const E_total = clamp( E_ss.mul( compensation ), vec3( 0.0 ), vec3( 1.0 ) );
112
99
 
113
- // Directional albedo at F0=1 (white furnace test)
114
- const Ew = max( dfgScale.add( dfgBias ), 0.1 );
115
-
116
- // Apply multiscatter compensation to get total specular albedo
117
- const compensation = vec3( 1.0 ).add( F0.mul( float( 1.0 ).div( Ew ).sub( 1.0 ) ) );
118
- return clamp( E_ss.mul( compensation ), vec3( 0.0 ), vec3( 1.0 ) );
100
+ return DFGResult( { compensation, E_total } );
119
101
 
120
102
  } );
121
103
 
@@ -329,7 +311,6 @@ export const calculateBRDFWeights = Fn( ( [ material, mc, cache ] ) => {
329
311
 
330
312
  export const getImportanceSamplingInfo = Fn( ( [
331
313
  material, bounceIndex, mc,
332
- environmentIntensity, useEnvMapIS, enableEnvironmentLight
333
314
  ] ) => {
334
315
 
335
316
  // Base BRDF weights using temporary cache
@@ -418,7 +399,6 @@ export const getImportanceSamplingInfo = Fn( ( [
418
399
  specularImportance,
419
400
  transmissionImportance,
420
401
  clearcoatImportance,
421
- envmapImportance: float( 0.0 ),
422
402
  } );
423
403
 
424
404
  } );
@@ -6,35 +6,25 @@ import {
6
6
  wgslFn,
7
7
  vec2,
8
8
  vec3,
9
- vec4,
10
9
  float,
11
10
  int,
12
11
  bool as tslBool,
13
- uint,
14
12
  If,
15
- Loop,
16
13
  select,
17
14
  abs,
18
- acos,
19
- sin,
20
- cos,
21
15
  dot,
22
- normalize,
23
16
  reflect,
24
17
  refract,
25
18
  max,
26
- min,
27
19
  mix,
28
20
  clamp,
29
- pow,
30
- fract,
21
+ exp,
31
22
  } from 'three/tsl';
32
23
 
33
24
  import { struct } from './patches.js';
34
- import { Ray, RayTracingMaterial, RenderState, HitInfo, DotProducts, DirectionSample } from './Struct.js';
35
- import { PI, EPSILON, MIN_ROUGHNESS, MIN_CLEARCOAT_ROUGHNESS, computeDotProducts } from './Common.js';
25
+ import { EPSILON, MIN_ROUGHNESS, MIN_PDF } from './Common.js';
36
26
  import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
37
- import { DistributionGGX, calculateGGXPDF } from './MaterialProperties.js';
27
+ import { DistributionGGX } from './MaterialProperties.js';
38
28
  import { ImportanceSampleGGX } from './MaterialSampling.js';
39
29
  import { RandomValue, pcgHash } from './Random.js';
40
30
 
@@ -46,6 +36,7 @@ export const TransmissionResult = struct( {
46
36
  direction: 'vec3', // New ray direction after transmission/reflection
47
37
  throughput: 'vec3', // Color throughput including absorption
48
38
  didReflect: 'bool', // Whether the ray was reflected instead of transmitted
39
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
49
40
  } );
50
41
 
51
42
  export const MaterialInteractionResult = struct( {
@@ -57,6 +48,7 @@ export const MaterialInteractionResult = struct( {
57
48
  direction: 'vec3', // New ray direction if continuing
58
49
  throughput: 'vec3', // Color modification for the ray
59
50
  alpha: 'float', // Alpha modification
51
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
60
52
  } );
61
53
 
62
54
  export const Medium = struct( {
@@ -77,11 +69,10 @@ export const MicrofacetTransmissionResult = struct( {
77
69
  halfVector: 'vec3', // Sampled half-vector
78
70
  didReflect: 'bool', // Whether TIR occurred
79
71
  pdf: 'float', // PDF of the sampled direction
72
+ colorWeight: 'vec3', // Spectral tint to apply once; vec3(1) if locked or non-dispersive
73
+ pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
80
74
  } );
81
75
 
82
- // Maximum number of nested media
83
- const MAX_MEDIA_STACK = 4;
84
-
85
76
  // MediumStack as a struct with fixed-size slots
86
77
  export const MediumStack = struct( {
87
78
  m0_ior: 'float',
@@ -107,75 +98,52 @@ export const MediumStack = struct( {
107
98
  // DISPERSION
108
99
  // ================================================================================
109
100
 
110
- // Enhanced spectral sampling for realistic dispersion
111
- export const sampleWavelengthForDispersion = Fn( ( [ baseIOR, dispersionStrength, random ] ) => {
112
-
113
- // Map random value to visible spectrum (380-700nm)
114
- const wl = mix( float( 380.0 ), float( 700.0 ), random ).toVar();
115
-
116
- // Convert to micrometers for Cauchy equation
117
- const wlMicron = wl.div( 1000.0 );
118
-
119
- // Strong IOR calculation for dramatic dispersion
120
- const A = baseIOR;
121
- const B = dispersionStrength.mul( 0.03 );
122
- const sampledIOR = A.add( B.div( wlMicron.mul( wlMicron ) ) ).toVar();
123
-
124
- // PURE SATURATED spectral colors
125
- const colorWeight = vec3( 0.0 ).toVar();
126
-
127
- // Deep Violet: 380-420
128
- If( wl.greaterThanEqual( 380.0 ).and( wl.lessThan( 420.0 ) ), () => {
129
-
130
- colorWeight.assign( vec3( 0.9, 0.0, 1.0 ) );
131
-
132
- } );
133
-
134
- // Blue: 420-480
135
- If( wl.greaterThanEqual( 420.0 ).and( wl.lessThan( 480.0 ) ), () => {
136
-
137
- colorWeight.assign( vec3( 0.0, 0.0, 1.0 ) );
138
-
139
- } );
140
-
141
- // Cyan: 480-500
142
- If( wl.greaterThanEqual( 480.0 ).and( wl.lessThan( 500.0 ) ), () => {
101
+ // Cauchy IOR n(λ) = baseIOR + 0.03·dispersion / λ_µm²
102
+ export const iorFromWavelength = /*@__PURE__*/ Fn( ( [ baseIOR, dispersionStrength, wavelength ] ) => {
143
103
 
144
- colorWeight.assign( vec3( 0.0, 1.0, 1.0 ) );
104
+ const wlMicron = wavelength.div( 1000.0 );
105
+ return baseIOR.add( dispersionStrength.mul( 0.03 ).div( wlMicron.mul( wlMicron ) ) );
145
106
 
146
- } );
147
-
148
- // Green: 500-530
149
- If( wl.greaterThanEqual( 500.0 ).and( wl.lessThan( 530.0 ) ), () => {
150
-
151
- colorWeight.assign( vec3( 0.0, 1.0, 0.0 ) );
152
-
153
- } );
107
+ } );
154
108
 
155
- // Yellow: 530-570
156
- If( wl.greaterThanEqual( 530.0 ).and( wl.lessThan( 570.0 ) ), () => {
109
+ // Wyman et al. JCGT 2013 piecewise-Gaussian fit to CIE 1931 2° observer
110
+ const cieGauss = /*@__PURE__*/ Fn( ( [ x, mu, sigmaLo, sigmaHi ] ) => {
157
111
 
158
- colorWeight.assign( vec3( 1.0, 1.0, 0.0 ) );
112
+ const sigma = select( x.lessThan( mu ), sigmaLo, sigmaHi );
113
+ const t = x.sub( mu ).mul( sigma );
114
+ return exp( float( - 0.5 ).mul( t ).mul( t ) );
159
115
 
160
- } );
116
+ } );
161
117
 
162
- // Orange: 570-620
163
- If( wl.greaterThanEqual( 570.0 ).and( wl.lessThan( 620.0 ) ), () => {
118
+ const wavelengthToXYZ = /*@__PURE__*/ Fn( ( [ wl ] ) => {
164
119
 
165
- colorWeight.assign( vec3( 1.0, 0.5, 0.0 ) );
120
+ const X = cieGauss( wl, 442.0, 0.0624, 0.0374 ).mul( 0.362 )
121
+ .add( cieGauss( wl, 599.8, 0.0264, 0.0323 ).mul( 1.056 ) )
122
+ .sub( cieGauss( wl, 501.1, 0.0490, 0.0382 ).mul( 0.065 ) );
123
+ const Y = cieGauss( wl, 568.8, 0.0213, 0.0247 ).mul( 0.821 )
124
+ .add( cieGauss( wl, 530.9, 0.0613, 0.0322 ).mul( 0.286 ) );
125
+ const Z = cieGauss( wl, 437.0, 0.0845, 0.0278 ).mul( 1.217 )
126
+ .add( cieGauss( wl, 459.0, 0.0385, 0.0725 ).mul( 0.681 ) );
127
+ return vec3( X, Y, Z );
166
128
 
167
- } );
129
+ } );
168
130
 
169
- // Red: 620-700
170
- If( wl.greaterThanEqual( 620.0 ).and( wl.lessThanEqual( 700.0 ) ), () => {
131
+ // Sample a wavelength on [380,700]nm and return its IOR + sRGB colorWeight (CIE 1931 →
132
+ // sRGB, gamut-clipped). The (1.819, 2.773, 2.928) factors normalize the clipped per-λ
133
+ // average to vec3(1), so clear glass converges to white as samples accumulate.
134
+ export const sampleWavelengthForDispersion = Fn( ( [ baseIOR, dispersionStrength, random ] ) => {
171
135
 
172
- colorWeight.assign( vec3( 1.0, 0.0, 0.0 ) );
136
+ const wl = mix( float( 380.0 ), float( 700.0 ), random ).toVar();
137
+ const sampledIOR = iorFromWavelength( baseIOR, dispersionStrength, wl ).toVar();
173
138
 
174
- } );
139
+ const xyz = wavelengthToXYZ( wl ).toVar();
140
+ const rgb = vec3(
141
+ xyz.x.mul( 3.2406 ).sub( xyz.y.mul( 1.5372 ) ).sub( xyz.z.mul( 0.4986 ) ),
142
+ xyz.x.mul( - 0.9689 ).add( xyz.y.mul( 1.8758 ) ).add( xyz.z.mul( 0.0415 ) ),
143
+ xyz.x.mul( 0.0557 ).sub( xyz.y.mul( 0.2040 ) ).add( xyz.z.mul( 1.0570 ) ),
144
+ ).toVar();
175
145
 
176
- // Maximum saturation
177
- colorWeight.assign( pow( colorWeight, vec3( 0.4 ) ) );
178
- colorWeight.assign( clamp( colorWeight, vec3( 0.0 ), vec3( 2.0 ) ) );
146
+ const colorWeight = max( rgb, vec3( 0.0 ) ).mul( vec3( 1.819, 2.773, 2.928 ) ).toVar();
179
147
 
180
148
  return SpectralSample( {
181
149
  wavelength: wl,
@@ -245,7 +213,7 @@ export const calculateShadowTransmittance = Fn( ( [ rayDir, normal, material, en
245
213
  // ================================================================================
246
214
 
247
215
  export const sampleMicrofacetTransmission = Fn( ( [
248
- V, N, ior, roughness, entering, dispersion, xi, rngState
216
+ V, N, ior, roughness, entering, dispersion, xi, rngState, pathWavelength
249
217
  ] ) => {
250
218
 
251
219
  const result = MicrofacetTransmissionResult( {
@@ -253,6 +221,8 @@ export const sampleMicrofacetTransmission = Fn( ( [
253
221
  halfVector: vec3( 0.0 ),
254
222
  didReflect: false,
255
223
  pdf: float( 0.0 ),
224
+ colorWeight: vec3( 1.0 ),
225
+ pathWavelength: pathWavelength,
256
226
  } ).toVar();
257
227
 
258
228
  // For smooth surfaces with dispersion, use perfect refraction with spectral IOR
@@ -264,9 +234,20 @@ export const sampleMicrofacetTransmission = Fn( ( [
264
234
  const eta = ior;
265
235
  const etaRatio = select( entering, float( 1.0 ).div( eta ), eta ).toVar();
266
236
 
267
- // Handle dispersion with spectral sampling
268
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
269
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
237
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
238
+ If( pathWavelength.greaterThan( 0.0 ), () => {
239
+
240
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
241
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
242
+
243
+ } ).Else( () => {
244
+
245
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
246
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
247
+ result.colorWeight.assign( spectralSample.colorWeight );
248
+ result.pathWavelength.assign( spectralSample.wavelength );
249
+
250
+ } );
270
251
 
271
252
  // Perfect refraction using surface normal
272
253
  const refractDir = refract( V.negate(), N, etaRatio ).toVar();
@@ -297,16 +278,33 @@ export const sampleMicrofacetTransmission = Fn( ( [
297
278
  // Compute IOR ratio
298
279
  const etaRatio = select( entering, float( 1.0 ).div( ior ), ior ).toVar();
299
280
 
300
- // Handle dispersion with improved spectral sampling
281
+ // Reuse the path's locked wavelength if any; else sample a new one and tint once.
301
282
  If( dispersion.greaterThan( 0.0 ), () => {
302
283
 
303
- const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
304
- etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
284
+ If( pathWavelength.greaterThan( 0.0 ), () => {
285
+
286
+ const lockedIOR = iorFromWavelength( ior, dispersion, pathWavelength );
287
+ etaRatio.assign( select( entering, float( 1.0 ).div( lockedIOR ), lockedIOR ) );
288
+
289
+ } ).Else( () => {
290
+
291
+ const spectralSample = SpectralSample.wrap( sampleWavelengthForDispersion( ior, dispersion, RandomValue( rngState ) ) );
292
+ etaRatio.assign( select( entering, float( 1.0 ).div( spectralSample.ior ), spectralSample.ior ) );
293
+ result.colorWeight.assign( spectralSample.colorWeight );
294
+ result.pathWavelength.assign( spectralSample.wavelength );
295
+
296
+ } );
305
297
 
306
298
  } );
307
299
 
308
- // Compute refracted direction using the sampled half-vector
309
- const HoV = clamp( dot( H, V ), 0.001, 1.0 );
300
+ // Compute refracted direction using the sampled half-vector. HoV and NoH
301
+ // are needed by both the TIR and the transmission PDF branches below, so
302
+ // hoist them once here (VoH == HoV — same dot product). DistributionGGX D
303
+ // is also identical between the two branches (calculateGGXPDF builds the
304
+ // same D internally for the TIR branch), so share it too.
305
+ const HoV = clamp( dot( H, V ), 0.001, 1.0 ).toVar();
306
+ const NoH = clamp( dot( N, H ), 0.001, 1.0 ).toVar();
307
+ const D = DistributionGGX( NoH, transmissionRoughness ).toVar();
310
308
  const refractDir = refract( V.negate(), H, etaRatio ).toVar();
311
309
 
312
310
  // Check for total internal reflection
@@ -316,10 +314,8 @@ export const sampleMicrofacetTransmission = Fn( ( [
316
314
  result.direction.assign( reflect( V.negate(), H ) );
317
315
  result.didReflect.assign( true );
318
316
 
319
- // Calculate PDF for reflection (standard GGX sampling)
320
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
321
- const VoH = clamp( dot( V, H ), 0.001, 1.0 );
322
- result.pdf.assign( calculateGGXPDF( NoH, VoH, transmissionRoughness ) );
317
+ // Reflection PDF: D(H) * NoH / (4 * VoH) — reuses the hoisted D + dots.
318
+ result.pdf.assign( D.mul( NoH ).div( max( float( 4.0 ).mul( HoV ), MIN_PDF ) ) );
323
319
 
324
320
  } ).Else( () => {
325
321
 
@@ -327,10 +323,7 @@ export const sampleMicrofacetTransmission = Fn( ( [
327
323
  result.direction.assign( refractDir );
328
324
  result.didReflect.assign( false );
329
325
 
330
- // Calculate proper PDF for microfacet transmission
331
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
332
326
  const HoL = clamp( dot( H, refractDir ), 0.001, 1.0 );
333
- const D = DistributionGGX( NoH, transmissionRoughness );
334
327
 
335
328
  // Account for change of measure due to refraction (Jacobian)
336
329
  const sqrtDenom = HoV.add( etaRatio.mul( HoL ) );
@@ -353,13 +346,14 @@ export const sampleMicrofacetTransmission = Fn( ( [
353
346
 
354
347
  export const handleTransmission = Fn( ( [
355
348
  rayDir, normal, material, entering, rngState,
356
- currentMediumIOR, previousMediumIOR,
349
+ currentMediumIOR, previousMediumIOR, pathWavelength,
357
350
  ] ) => {
358
351
 
359
352
  const result = TransmissionResult( {
360
353
  direction: vec3( 0.0 ),
361
354
  throughput: vec3( 1.0 ),
362
355
  didReflect: false,
356
+ pathWavelength: pathWavelength,
363
357
  } ).toVar();
364
358
 
365
359
  // Setup surface normal based on ray direction
@@ -416,10 +410,10 @@ export const handleTransmission = Fn( ( [
416
410
 
417
411
  If( doReflect, () => {
418
412
 
419
- // Reflection path
413
+ // Reflection at a transmissive surface — no wavelength locking
420
414
  If( material.roughness.greaterThan( 0.05 ), () => {
421
415
 
422
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, float( 0.0 ), xi, rngState ) );
416
+ const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, float( 0.0 ), xi, rngState, float( 0.0 ) ) );
423
417
  result.direction.assign( mtResult.direction );
424
418
 
425
419
  } ).Else( () => {
@@ -436,125 +430,20 @@ export const handleTransmission = Fn( ( [
436
430
  // Transmission/refraction path
437
431
  If( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ), () => {
438
432
 
439
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState ) );
433
+ const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission( V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState, pathWavelength ) );
434
+ result.pathWavelength.assign( mtResult.pathWavelength );
440
435
 
441
- // If TIR occurred during sampling, respect it
442
436
  If( mtResult.didReflect, () => {
443
437
 
438
+ // TIR during intended transmission: compensate for selection probability
444
439
  result.direction.assign( mtResult.direction );
445
440
  result.didReflect.assign( true );
446
- // TIR during intended transmission: compensate for selection probability
447
441
  result.throughput.assign( material.color.xyz.div( max( float( 1.0 ).sub( reflectProb ), 0.05 ) ) );
448
442
 
449
443
  } ).Else( () => {
450
444
 
451
445
  result.direction.assign( mtResult.direction );
452
-
453
- // Handle dispersion coloring
454
- If( material.dispersion.greaterThan( 0.0 ), () => {
455
-
456
- // Calculate refracted ray deviation from original direction
457
- const originalDir = normalize( rayDir );
458
- const refractedDir = normalize( result.direction );
459
-
460
- // Calculate angle-dependent dispersion factor
461
- const edgeFactor = float( 1.0 ).sub( abs( dot( N, originalDir ) ) );
462
- const deviationAngle = acos( clamp( dot( originalDir, refractedDir ), - 1.0, 1.0 ) );
463
-
464
- // Create spatial variation using ray direction and normal
465
- const combinedVec = normalize( originalDir.add( N ) );
466
- const spatialVariation = sin( combinedVec.x.mul( 15.0 ) ).mul( cos( combinedVec.y.mul( 12.0 ) ) ).mul( sin( combinedVec.z.mul( 18.0 ) ) );
467
-
468
- // Add additional variation using refracted direction
469
- const refractVariation = sin( refractedDir.x.mul( 8.0 ).add( refractedDir.y.mul( 6.0 ) ).add( refractedDir.z.mul( 10.0 ) ) );
470
-
471
- // Combine multiple factors for better color distribution
472
- const baseColorIndex = deviationAngle.mul( material.dispersion ).mul( 3.0 );
473
- const spatialBoost = spatialVariation.mul( 0.3 );
474
- const refractBoost = refractVariation.mul( 0.2 );
475
- const edgeBoost = edgeFactor.mul( 0.4 );
476
-
477
- // Create continuous color mapping across the prism
478
- const colorIndex = fract( baseColorIndex.add( spatialBoost ).add( refractBoost ).add( edgeBoost ) ).toVar();
479
-
480
- // ROYGBIV spectrum mapping with smooth transitions
481
- const rainbowColor = vec3( 0.0 ).toVar();
482
-
483
- // Red zone
484
- If( colorIndex.lessThan( 0.143 ), () => {
485
-
486
- const t = colorIndex.div( 0.143 );
487
- rainbowColor.assign( mix( vec3( 0.8, 0.0, 0.0 ), vec3( 1.0, 0.0, 0.0 ), t ) );
488
-
489
- } );
490
-
491
- // Red to Orange
492
- If( colorIndex.greaterThanEqual( 0.143 ).and( colorIndex.lessThan( 0.286 ) ), () => {
493
-
494
- const t = colorIndex.sub( 0.143 ).div( 0.143 );
495
- rainbowColor.assign( mix( vec3( 1.0, 0.0, 0.0 ), vec3( 1.0, 0.6, 0.0 ), t ) );
496
-
497
- } );
498
-
499
- // Orange to Yellow
500
- If( colorIndex.greaterThanEqual( 0.286 ).and( colorIndex.lessThan( 0.429 ) ), () => {
501
-
502
- const t = colorIndex.sub( 0.286 ).div( 0.143 );
503
- rainbowColor.assign( mix( vec3( 1.0, 0.6, 0.0 ), vec3( 1.0, 1.0, 0.0 ), t ) );
504
-
505
- } );
506
-
507
- // Yellow to Green
508
- If( colorIndex.greaterThanEqual( 0.429 ).and( colorIndex.lessThan( 0.571 ) ), () => {
509
-
510
- const t = colorIndex.sub( 0.429 ).div( 0.142 );
511
- rainbowColor.assign( mix( vec3( 1.0, 1.0, 0.0 ), vec3( 0.0, 1.0, 0.0 ), t ) );
512
-
513
- } );
514
-
515
- // Green to Blue
516
- If( colorIndex.greaterThanEqual( 0.571 ).and( colorIndex.lessThan( 0.714 ) ), () => {
517
-
518
- const t = colorIndex.sub( 0.571 ).div( 0.143 );
519
- rainbowColor.assign( mix( vec3( 0.0, 1.0, 0.0 ), vec3( 0.0, 0.4, 1.0 ), t ) );
520
-
521
- } );
522
-
523
- // Blue to Indigo
524
- If( colorIndex.greaterThanEqual( 0.714 ).and( colorIndex.lessThan( 0.857 ) ), () => {
525
-
526
- const t = colorIndex.sub( 0.714 ).div( 0.143 );
527
- rainbowColor.assign( mix( vec3( 0.0, 0.4, 1.0 ), vec3( 0.3, 0.0, 0.8 ), t ) );
528
-
529
- } );
530
-
531
- // Indigo to Violet
532
- If( colorIndex.greaterThanEqual( 0.857 ), () => {
533
-
534
- const t = colorIndex.sub( 0.857 ).div( 0.143 );
535
- rainbowColor.assign( mix( vec3( 0.3, 0.0, 0.8 ), vec3( 0.6, 0.0, 1.0 ), t ) );
536
-
537
- } );
538
-
539
- // Calculate dispersion strength with proper variation
540
- const normalizedDispersion = clamp( material.dispersion.div( 5.0 ), 0.0, 1.0 );
541
- const angleBoost = float( 1.0 ).add( edgeFactor.mul( 1.5 ) );
542
-
543
- // Make dispersion visibility more gradual
544
- const baseVisibility = normalizedDispersion.mul( angleBoost );
545
- const combinedVariation = spatialVariation.add( refractVariation );
546
- const spatialMod = float( 0.5 ).add( float( 0.5 ).mul( sin( combinedVariation.mul( 3.14159 ) ) ) );
547
- const dispersionVisibility = clamp( baseVisibility.mul( spatialMod ), 0.0, 0.8 );
548
-
549
- // Mix rainbow color with clear base for realistic prism effect
550
- result.throughput.assign( mix( vec3( 1.0 ), rainbowColor, dispersionVisibility ) );
551
-
552
- } ).Else( () => {
553
-
554
- // No dispersion - pure white transmission
555
- result.throughput.assign( vec3( 1.0 ) );
556
-
557
- } );
446
+ result.throughput.assign( mtResult.colorWeight );
558
447
 
559
448
  } );
560
449
 
@@ -576,12 +465,9 @@ export const handleTransmission = Fn( ( [
576
465
  // due to solid angle compression/expansion (cancels for round-trip enter+exit paths)
577
466
  result.throughput.mulAssign( n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ) );
578
467
 
579
- // Apply Beer's law absorption when entering medium
580
- If( entering.and( material.attenuationDistance.greaterThan( 0.0 ) ), () => {
581
-
582
- result.throughput.mulAssign( calculateBeerLawAbsorption( { attenuationColor: material.attenuationColor, attenuationDistance: material.attenuationDistance, thickness: material.thickness } ) );
583
-
584
- } );
468
+ // KHR_materials_volume absorption is applied per-bounce in PathTracerCore based
469
+ // on the actual ray path length inside the medium (driven by the medium stack).
470
+ // No entry-point thickness approximation here — that would double-count.
585
471
 
586
472
  // Fresnel transmission factor with PDF compensation
587
473
  result.throughput.mulAssign( float( 1.0 ).sub( Fr ).div( max( float( 1.0 ).sub( reflectProb ), 0.05 ) ) );
@@ -599,11 +485,10 @@ export const handleTransmission = Fn( ( [
599
485
  // ================================================================================
600
486
 
601
487
  export const handleMaterialTransparency = Fn( ( [
602
- ray, hitPoint, normal, material, rngState,
603
- // RenderState fields passed individually (since inout not supported)
488
+ ray, normal, material, rngState,
604
489
  transmissiveTraversals,
605
- // Precomputed medium IOR values from stack
606
490
  currentMediumIOR, previousMediumIOR,
491
+ pathWavelength,
607
492
  ] ) => {
608
493
 
609
494
  const result = MaterialInteractionResult( {
@@ -615,21 +500,16 @@ export const handleMaterialTransparency = Fn( ( [
615
500
  direction: ray.direction,
616
501
  throughput: vec3( 1.0 ),
617
502
  alpha: float( 1.0 ),
503
+ pathWavelength: pathWavelength,
618
504
  } ).toVar();
619
505
 
620
- // -----------------------------------------------------------------
621
- // Step 1: Fast path for completely opaque materials
622
- // -----------------------------------------------------------------
623
- // Quick early exit for fully opaque materials (most common case)
506
+ // Fast path for fully opaque materials (most common case)
624
507
  If( material.alphaMode.equal( int( 0 ) ).and( material.transmission.lessThanEqual( 0.0 ) ), () => {
625
508
 
626
- // Return default (no interaction needed)
509
+ // no interaction needed
627
510
 
628
511
  } ).Else( () => {
629
512
 
630
- // -----------------------------------------------------------------
631
- // Step 2: Handle alpha modes according to glTF spec
632
- // -----------------------------------------------------------------
633
513
  const alphaRand = RandomValue( rngState );
634
514
  const transmissionRand = RandomValue( rngState );
635
515
  const transmissionSeed = pcgHash( { state: rngState } );
@@ -681,9 +561,6 @@ export const handleMaterialTransparency = Fn( ( [
681
561
 
682
562
  } );
683
563
 
684
- // -----------------------------------------------------------------
685
- // Step 3: Handle transmission if present
686
- // -----------------------------------------------------------------
687
564
  If( handled.not().and( material.transmission.greaterThan( 0.0 ) ).and( transmissiveTraversals.greaterThan( int( 0 ) ) ), () => {
688
565
 
689
566
  // Only apply transmission with probability equal to the transmission value
@@ -693,7 +570,7 @@ export const handleMaterialTransparency = Fn( ( [
693
570
 
694
571
  const transResult = TransmissionResult.wrap( handleTransmission(
695
572
  ray.direction, normal, material, entering, transmissionSeed,
696
- currentMediumIOR, previousMediumIOR,
573
+ currentMediumIOR, previousMediumIOR, pathWavelength,
697
574
  ) );
698
575
 
699
576
  result.direction.assign( transResult.direction );
@@ -703,6 +580,7 @@ export const handleMaterialTransparency = Fn( ( [
703
580
  result.didReflect.assign( transResult.didReflect );
704
581
  result.entering.assign( entering );
705
582
  result.alpha.assign( float( 1.0 ).sub( material.transmission ) );
583
+ result.pathWavelength.assign( transResult.pathWavelength );
706
584
 
707
585
  } );
708
586
 
@@ -140,10 +140,11 @@ export const pathTracerMain = ( params ) => {
140
140
  envCDFBuffer,
141
141
  envTotalSum, envCompensationDelta, envResolution,
142
142
  enableEnvironmentLight, useEnvMapIS,
143
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
143
144
  maxBounceCount, transmissiveBounces,
144
145
  showBackground, transparentBackground, backgroundIntensity,
145
146
  fireflyThreshold, globalIlluminationIntensity,
146
- totalTriangleCount, enableEmissiveTriangleSampling,
147
+ enableEmissiveTriangleSampling,
147
148
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
148
149
  lightBVHBuffer, lightBVHNodeCount,
149
150
  debugVisScale,
@@ -170,7 +171,6 @@ export const pathTracerMain = ( params ) => {
170
171
  const pixelSamples = int( 0 ).toVar();
171
172
 
172
173
  const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
173
- const pixelIndex = int( pixelCoord.y ).mul( int( resolution.x ) ).add( int( pixelCoord.x ) ).toVar();
174
174
 
175
175
  // MRT data
176
176
  const worldNormal = vec3( 0.0, 0.0, 1.0 ).toVar();
@@ -272,7 +272,7 @@ export const pathTracerMain = ( params ) => {
272
272
 
273
273
  // Normal path tracing
274
274
  const traceResult = TraceResult.wrap( Trace(
275
- ray, seed, rayIndex, pixelIndex,
275
+ ray, seed, rayIndex,
276
276
  bvhBuffer,
277
277
  triangleBuffer,
278
278
  materialBuffer,
@@ -287,10 +287,11 @@ export const pathTracerMain = ( params ) => {
287
287
  envCDFBuffer,
288
288
  envTotalSum, envCompensationDelta, envResolution,
289
289
  enableEnvironmentLight, useEnvMapIS,
290
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
290
291
  maxBounceCount, transmissiveBounces,
291
292
  backgroundIntensity, showBackground, transparentBackground,
292
293
  fireflyThreshold, globalIlluminationIntensity,
293
- totalTriangleCount, enableEmissiveTriangleSampling,
294
+ enableEmissiveTriangleSampling,
294
295
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
295
296
  lightBVHBuffer, lightBVHNodeCount,
296
297
  pixelCoord, resolution, frame,