rayzee 6.0.1 → 6.2.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 (43) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2421 -2078
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +55 -52
  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 +4 -6
  20. package/src/TSL/Common.js +1 -1
  21. package/src/TSL/Debugger.js +0 -2
  22. package/src/TSL/EmissiveSampling.js +20 -22
  23. package/src/TSL/Environment.js +60 -14
  24. package/src/TSL/Fresnel.js +13 -4
  25. package/src/TSL/LightsCore.js +238 -5
  26. package/src/TSL/LightsDirect.js +16 -5
  27. package/src/TSL/LightsIndirect.js +4 -37
  28. package/src/TSL/LightsSampling.js +119 -185
  29. package/src/TSL/MaterialEvaluation.js +25 -14
  30. package/src/TSL/MaterialProperties.js +14 -34
  31. package/src/TSL/MaterialTransmission.js +18 -37
  32. package/src/TSL/PathTracer.js +25 -7
  33. package/src/TSL/PathTracerCore.js +144 -139
  34. package/src/TSL/Struct.js +7 -1
  35. package/src/TSL/TextureSampling.js +2 -2
  36. package/src/index.js +2 -0
  37. package/src/managers/AnimationManager.js +3 -6
  38. package/src/managers/DenoisingManager.js +1 -1
  39. package/src/managers/GoboManager.js +277 -0
  40. package/src/managers/IESManager.js +268 -0
  41. package/src/managers/LightManager.js +33 -1
  42. package/src/managers/TransformManager.js +3 -3
  43. 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,30 +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
15
  dot,
19
16
  reflect,
20
17
  refract,
21
18
  max,
22
- min,
23
19
  mix,
24
20
  clamp,
25
21
  exp,
26
22
  } from 'three/tsl';
27
23
 
28
24
  import { struct } from './patches.js';
29
- import { Ray, RayTracingMaterial, RenderState, HitInfo, DotProducts, DirectionSample } from './Struct.js';
30
- import { PI, EPSILON, MIN_ROUGHNESS, MIN_CLEARCOAT_ROUGHNESS, computeDotProducts } from './Common.js';
25
+ import { EPSILON, MIN_ROUGHNESS, MIN_PDF } from './Common.js';
31
26
  import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
32
- import { DistributionGGX, calculateGGXPDF } from './MaterialProperties.js';
27
+ import { DistributionGGX } from './MaterialProperties.js';
33
28
  import { ImportanceSampleGGX } from './MaterialSampling.js';
34
29
  import { RandomValue, pcgHash } from './Random.js';
35
30
 
@@ -78,9 +73,6 @@ export const MicrofacetTransmissionResult = struct( {
78
73
  pathWavelength: 'float', // 0 if path is not yet spectral, else locked wavelength in nm
79
74
  } );
80
75
 
81
- // Maximum number of nested media
82
- const MAX_MEDIA_STACK = 4;
83
-
84
76
  // MediumStack as a struct with fixed-size slots
85
77
  export const MediumStack = struct( {
86
78
  m0_ior: 'float',
@@ -305,8 +297,14 @@ export const sampleMicrofacetTransmission = Fn( ( [
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 ) );
@@ -472,12 +465,9 @@ export const handleTransmission = Fn( ( [
472
465
  // due to solid angle compression/expansion (cancels for round-trip enter+exit paths)
473
466
  result.throughput.mulAssign( n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ) );
474
467
 
475
- // Apply Beer's law absorption when entering medium
476
- If( entering.and( material.attenuationDistance.greaterThan( 0.0 ) ), () => {
477
-
478
- result.throughput.mulAssign( calculateBeerLawAbsorption( { attenuationColor: material.attenuationColor, attenuationDistance: material.attenuationDistance, thickness: material.thickness } ) );
479
-
480
- } );
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.
481
471
 
482
472
  // Fresnel transmission factor with PDF compensation
483
473
  result.throughput.mulAssign( float( 1.0 ).sub( Fr ).div( max( float( 1.0 ).sub( reflectProb ), 0.05 ) ) );
@@ -495,7 +485,7 @@ export const handleTransmission = Fn( ( [
495
485
  // ================================================================================
496
486
 
497
487
  export const handleMaterialTransparency = Fn( ( [
498
- ray, hitPoint, normal, material, rngState,
488
+ ray, normal, material, rngState,
499
489
  transmissiveTraversals,
500
490
  currentMediumIOR, previousMediumIOR,
501
491
  pathWavelength,
@@ -513,19 +503,13 @@ export const handleMaterialTransparency = Fn( ( [
513
503
  pathWavelength: pathWavelength,
514
504
  } ).toVar();
515
505
 
516
- // -----------------------------------------------------------------
517
- // Step 1: Fast path for completely opaque materials
518
- // -----------------------------------------------------------------
519
- // Quick early exit for fully opaque materials (most common case)
506
+ // Fast path for fully opaque materials (most common case)
520
507
  If( material.alphaMode.equal( int( 0 ) ).and( material.transmission.lessThanEqual( 0.0 ) ), () => {
521
508
 
522
- // Return default (no interaction needed)
509
+ // no interaction needed
523
510
 
524
511
  } ).Else( () => {
525
512
 
526
- // -----------------------------------------------------------------
527
- // Step 2: Handle alpha modes according to glTF spec
528
- // -----------------------------------------------------------------
529
513
  const alphaRand = RandomValue( rngState );
530
514
  const transmissionRand = RandomValue( rngState );
531
515
  const transmissionSeed = pcgHash( { state: rngState } );
@@ -577,9 +561,6 @@ export const handleMaterialTransparency = Fn( ( [
577
561
 
578
562
  } );
579
563
 
580
- // -----------------------------------------------------------------
581
- // Step 3: Handle transmission if present
582
- // -----------------------------------------------------------------
583
564
  If( handled.not().and( material.transmission.greaterThan( 0.0 ) ).and( transmissiveTraversals.greaterThan( int( 0 ) ) ), () => {
584
565
 
585
566
  // Only apply transmission with probability equal to the transmission value
@@ -74,6 +74,16 @@ export const computeNDCDepth = /*@__PURE__*/ wgslFn( `
74
74
  }
75
75
  ` );
76
76
 
77
+ // NaN/Inf detector for debug mode 11. x != x catches NaN; the abs threshold catches Inf.
78
+ const nanInfToRed = /*@__PURE__*/ wgslFn( `
79
+ fn nanInfToRed( c: vec3f ) -> vec3f {
80
+ let isNan = c.x != c.x || c.y != c.y || c.z != c.z;
81
+ let isInf = abs( c.x ) > 1e30f || abs( c.y ) > 1e30f || abs( c.z ) > 1e30f;
82
+ if ( isNan || isInf ) { return vec3f( 1.0f, 0.0f, 0.0f ); }
83
+ return vec3f( 0.0f );
84
+ }
85
+ ` );
86
+
77
87
  // Get required samples from adaptive sampling texture
78
88
  export const getRequiredSamples = Fn( ( [
79
89
  pixelCoord, resolution,
@@ -140,10 +150,11 @@ export const pathTracerMain = ( params ) => {
140
150
  envCDFBuffer,
141
151
  envTotalSum, envCompensationDelta, envResolution,
142
152
  enableEnvironmentLight, useEnvMapIS,
153
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
143
154
  maxBounceCount, transmissiveBounces,
144
155
  showBackground, transparentBackground, backgroundIntensity,
145
156
  fireflyThreshold, globalIlluminationIntensity,
146
- totalTriangleCount, enableEmissiveTriangleSampling,
157
+ enableEmissiveTriangleSampling,
147
158
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
148
159
  lightBVHBuffer, lightBVHNodeCount,
149
160
  debugVisScale,
@@ -170,7 +181,6 @@ export const pathTracerMain = ( params ) => {
170
181
  const pixelSamples = int( 0 ).toVar();
171
182
 
172
183
  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
184
 
175
185
  // MRT data
176
186
  const worldNormal = vec3( 0.0, 0.0, 1.0 ).toVar();
@@ -251,8 +261,8 @@ export const pathTracerMain = ( params ) => {
251
261
 
252
262
  const sampleColor = vec4( 0.0 ).toVar();
253
263
 
254
- // Debug or normal trace
255
- If( visMode.greaterThan( int( 0 ) ), () => {
264
+ // Debug or normal trace (mode 11 runs the full path tracer so we can sniff NaN at the end)
265
+ If( visMode.greaterThan( int( 0 ) ).and( visMode.notEqual( int( 11 ) ) ), () => {
256
266
 
257
267
  sampleColor.assign( TraceDebugMode(
258
268
  ray.origin, ray.direction,
@@ -272,7 +282,7 @@ export const pathTracerMain = ( params ) => {
272
282
 
273
283
  // Normal path tracing
274
284
  const traceResult = TraceResult.wrap( Trace(
275
- ray, seed, rayIndex, pixelIndex,
285
+ ray, seed, rayIndex,
276
286
  bvhBuffer,
277
287
  triangleBuffer,
278
288
  materialBuffer,
@@ -287,10 +297,11 @@ export const pathTracerMain = ( params ) => {
287
297
  envCDFBuffer,
288
298
  envTotalSum, envCompensationDelta, envResolution,
289
299
  enableEnvironmentLight, useEnvMapIS,
300
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
290
301
  maxBounceCount, transmissiveBounces,
291
302
  backgroundIntensity, showBackground, transparentBackground,
292
303
  fireflyThreshold, globalIlluminationIntensity,
293
- totalTriangleCount, enableEmissiveTriangleSampling,
304
+ enableEmissiveTriangleSampling,
294
305
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
295
306
  lightBVHBuffer, lightBVHNodeCount,
296
307
  pixelCoord, resolution, frame,
@@ -343,7 +354,7 @@ export const pathTracerMain = ( params ) => {
343
354
  // Output alpha: accumulated per-sample alpha when transparent, otherwise 1.0
344
355
  const outputAlpha = select( transparentBackground, pixelAlpha, float( 1.0 ) ).toVar();
345
356
 
346
- If( enableAccumulation.and( cameraIsMoving.not() ).and( frame.greaterThan( uint( 0 ) ) ).and( hasPreviousAccumulated ), () => {
357
+ If( enableAccumulation.and( cameraIsMoving.not() ).and( frame.greaterThan( uint( 0 ) ) ).and( hasPreviousAccumulated ).and( visMode.notEqual( int( 11 ) ) ), () => {
347
358
 
348
359
  const prevAccumSample = texture( prevAccumTexture, prevUV, 0 ).toVar();
349
360
 
@@ -360,6 +371,13 @@ export const pathTracerMain = ( params ) => {
360
371
 
361
372
  } );
362
373
 
374
+ // NaN/Inf debug: red where the path tracer output isn't finite, black otherwise.
375
+ If( visMode.equal( int( 11 ) ), () => {
376
+
377
+ finalColor.assign( nanInfToRed( finalColor ) );
378
+
379
+ } );
380
+
363
381
  // Write outputs to StorageTextures
364
382
  textureStore( writeColorTex, uintCoord, vec4( finalColor.xyz, outputAlpha ) ).toWriteOnly();
365
383
  textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();