rayzee 8.5.0 → 8.6.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 (60) hide show
  1. package/README.md +60 -4
  2. package/dist/{DLSSNeural-BNSJ8V3z.js → DLSSNeural-BhBCBgKf.js} +5 -5
  3. package/dist/{DLSSSuperRes-BoEW89Cl.js → DLSSSuperRes-BCA7F_k5.js} +5 -5
  4. package/dist/ToneMapGPU-BzcLheoR.js +533 -0
  5. package/dist/rayzee.es.js +27198 -9047
  6. package/dist/rayzee.umd.js +255 -187
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +15 -2
  9. package/src/Color/BakedViews.js +134 -0
  10. package/src/Color/BuiltinViews.js +261 -0
  11. package/src/Color/ColorManagement.js +1130 -0
  12. package/src/Color/ColorSpaces.js +432 -0
  13. package/src/Color/Displays.js +19 -0
  14. package/src/Color/InputColorSpaces.js +243 -0
  15. package/src/Color/LutBake.js +371 -0
  16. package/src/Color/OcioRuntime.js +338 -0
  17. package/src/Color/OcioViews.js +419 -0
  18. package/src/Color/ViewTransforms.js +323 -0
  19. package/src/Color/WorkingMatrix.js +86 -0
  20. package/src/EngineDefaults.js +55 -3
  21. package/src/EngineIssues.js +3 -0
  22. package/src/Passes/AIUpscaler.js +16 -7
  23. package/src/PathTracerApp.js +227 -7
  24. package/src/Processor/AssetLoader.js +1 -2
  25. package/src/Processor/EmissiveTriangleBuilder.js +34 -2
  26. package/src/Processor/GeometryExtractor.js +151 -138
  27. package/src/Processor/LightSerializer.js +8 -3
  28. package/src/Processor/MaterialPacking.js +168 -0
  29. package/src/Processor/PackedRayBuffer.js +6 -2
  30. package/src/Processor/ProceduralSky.js +4 -0
  31. package/src/Processor/SceneProcessor.js +25 -1
  32. package/src/Processor/SimpleSky.js +8 -0
  33. package/src/Processor/TextureCreator.js +137 -91
  34. package/src/Processor/TextureReadback.js +92 -0
  35. package/src/Processor/ToneMapCPU.js +80 -165
  36. package/src/Processor/ToneMapGPU.js +122 -151
  37. package/src/RenderSettings.js +1 -0
  38. package/src/Stages/Compositor.js +11 -2
  39. package/src/Stages/PathTracer.js +2 -0
  40. package/src/Stages/PathTracerStage.js +1 -1
  41. package/src/TSL/Common.js +15 -0
  42. package/src/TSL/Debugger.js +2 -3
  43. package/src/TSL/EmissiveSampling.js +6 -8
  44. package/src/TSL/ExtendKernel.js +9 -0
  45. package/src/TSL/Fresnel.js +23 -1
  46. package/src/TSL/HitFacet.js +102 -0
  47. package/src/TSL/LightsDirect.js +12 -33
  48. package/src/TSL/LightsSampling.js +15 -7
  49. package/src/TSL/MaterialEvaluation.js +26 -19
  50. package/src/TSL/MaterialProperties.js +74 -37
  51. package/src/TSL/MaterialTransmission.js +2 -3
  52. package/src/TSL/ShadeKernel.js +34 -21
  53. package/src/TSL/ShadowTerminator.js +125 -0
  54. package/src/TSL/Struct.js +9 -0
  55. package/src/TSL/Subsurface.js +2 -3
  56. package/src/index.js +29 -1
  57. package/src/managers/EnvironmentManager.js +20 -0
  58. package/src/managers/MaterialDataManager.js +118 -208
  59. package/src/managers/UniformManager.js +1 -0
  60. package/dist/ToneMapGPU-DzqSyzTx.js +0 -341
@@ -66,8 +66,17 @@ export class Compositor extends RenderStage {
66
66
  */
67
67
  _resolveSourceTexture( context ) {
68
68
 
69
- return context.getTexture( 'bloom:output' )
70
- || context.getTexture( 'oidn:output' )
69
+ return context.getTexture( 'bloom:output' ) || this.resolveLightTexture( context );
70
+
71
+ }
72
+
73
+ /**
74
+ * The newest scene-referred image the pipeline holds — denoised when a denoiser has run — without
75
+ * bloom, which is a look added on top rather than light in the scene.
76
+ */
77
+ resolveLightTexture( context ) {
78
+
79
+ return context.getTexture( 'oidn:output' )
71
80
  || context.getTexture( 'edgeFiltering:output' )
72
81
  || context.getTexture( 'bilateralFiltering:output' )
73
82
  || context.getTexture( 'asvgf:output' )
@@ -1393,6 +1393,7 @@ export class PathTracer extends PathTracerStage {
1393
1393
  activeIndicesRO: qm.getActiveReadRO(),
1394
1394
  counters,
1395
1395
  maxRayCount: this._wfMaxRayCount,
1396
+ shadowTerminatorOffset: this.shadowTerminatorOffset,
1396
1397
  } );
1397
1398
  this._kernelManager.register( 'extend',
1398
1399
  extFn().compute(
@@ -1491,6 +1492,7 @@ export class PathTracer extends PathTracerStage {
1491
1492
  cameraProjectionMatrix: this.cameraProjectionMatrix,
1492
1493
  cameraViewMatrix: this.cameraViewMatrix,
1493
1494
  fireflyThreshold: this.fireflyThreshold,
1495
+ shadowTerminatorOffset: this.shadowTerminatorOffset,
1494
1496
  // RNG axis only (keys STBN via frame & 63).
1495
1497
  frame: this.seedFrame,
1496
1498
  accumFrame: this.frame,
@@ -480,7 +480,7 @@ export class PathTracerStage extends RenderStage {
480
480
  this.setTriangleData( this.sdfs.triangleData, this.sdfs.triangleCount );
481
481
  this.setBVHData( this.sdfs.bvhData );
482
482
  this.setInstanceTable( this.sdfs.instanceTable );
483
- this.materialData.setMaterialData( this.sdfs.materialData );
483
+ this.materialData.setMaterialData( this.sdfs.materialData, this.sdfs.materials?.map( m => m.sources ) );
484
484
 
485
485
  // Material texture arrays
486
486
  this.materialData.loadTexturesFromSdfs();
package/src/TSL/Common.js CHANGED
@@ -8,6 +8,21 @@ import {
8
8
  ShadowMaterial,
9
9
  } from './Struct.js';
10
10
 
11
+ // Spawn point for a ray leaving a surface, Cycles' classic ray_offset: within 1 unit of the origin,
12
+ // 1e-5 along n; beyond, 32 float ULPs per axis toward n. A fixed 1 mm let rays slip out of
13
+ // sub-millimetre grooves — a 14 cm camera read up to 14 % bright in its crevices against Cycles.
14
+ export const offsetRayOrigin = /*@__PURE__*/ wgslFn( `
15
+ fn offsetRayOrigin( p: vec3f, n: vec3f ) -> vec3f {
16
+ let step = select( vec3<i32>( 32 ), vec3<i32>( -32 ), ( p < vec3f( 0.0 ) ) != ( n < vec3f( 0.0 ) ) );
17
+ let far = bitcast<vec3f>( bitcast<vec3<i32>>( p ) + step );
18
+ return select( far, p + n * 1e-5, abs( p ) < vec3f( 1.0 ) );
19
+ }
20
+ ` );
21
+
22
+ // Visibility rays stop this fraction short of the sampled light point: unit-free, and ≫ float
23
+ // error, so an emitter coplanar with its fixture is never occluded by it.
24
+ export const SHADOW_END = 1.0 - 1e-4;
25
+
11
26
  export const PI = 3.14159;
12
27
  export const PI_INV = 1.0 / PI;
13
28
  export const TWO_PI = 2.0 * PI;
@@ -23,7 +23,7 @@ import {
23
23
  import { Ray, HitInfo, RayTracingMaterial, MaterialSamples } from './Struct.js';
24
24
  import { traverseBVHDebug as traverseBVH } from './BVHTraversal.js';
25
25
  import { sampleEnvironment } from './Environment.js';
26
- import { REC709_LUMINANCE_COEFFICIENTS, getMaterial } from './Common.js';
26
+ import { REC709_LUMINANCE_COEFFICIENTS, getMaterial, offsetRayOrigin } from './Common.js';
27
27
  import { sampleAllMaterialTextures } from './TextureSampling.js';
28
28
  import { pcgHash, wang_hash, RandomValue } from './Random.js';
29
29
  import { cosineWeightedSample } from './MaterialSampling.js';
@@ -336,8 +336,7 @@ export const TraceDebugMode = Fn( ( [
336
336
  const xi = vec2( xi_r1, xi_r2 );
337
337
  const bounceDir = cosineWeightedSample( { N: normalA, xi } ).toVar();
338
338
 
339
- // Trace secondary ray from the hit point (offset along normal to avoid self-intersection)
340
- const bounceOrigin = hitInfo.hitPoint.add( normalA.mul( 0.001 ) );
339
+ const bounceOrigin = offsetRayOrigin( hitInfo.hitPoint, normalA );
341
340
  const bounceRay = Ray( { origin: bounceOrigin, direction: bounceDir } );
342
341
 
343
342
  const bounceHit = HitInfo.wrap( traverseBVH(
@@ -30,7 +30,7 @@ import { struct } from './patches.js';
30
30
  import {
31
31
  MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT,
32
32
  computeDotProductsAniso, instanceRows, instanceNormalToWorld, instancePointToWorld,
33
- unpackTriangleNormal, TRI_STRIDE
33
+ unpackTriangleNormal, TRI_STRIDE, SHADOW_END
34
34
  } from './Common.js';
35
35
  import { TRI_MATERIAL_MASK } from '../EngineDefaults.js';
36
36
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
@@ -591,13 +591,11 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
591
591
 
592
592
  If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ), () => {
593
593
 
594
- // Calculate ray offset for shadow ray
595
- const rayOffset = calculateRayOffsetFn( hitPoint, geomNormal, material );
596
- const rayOrigin = hitPoint.add( rayOffset );
597
-
598
- // Trace shadow ray
599
- const shadowDist = emissiveSample.distance.sub( 0.001 );
600
- const visibility = traceShadowRayFn( rayOrigin, emissiveSample.direction, shadowDist );
594
+ // Aimed at the sampled point and stopped a relative hair short, as for area lights.
595
+ const rayOrigin = hitPoint.add( calculateRayOffsetFn( hitPoint, geomNormal, emissiveSample.direction ) );
596
+ const toSample = emissiveSample.position.sub( rayOrigin ).toVar();
597
+ const shadowDist = length( toSample ).toVar();
598
+ const visibility = traceShadowRayFn( rayOrigin, toSample.div( shadowDist ), shadowDist.mul( SHADOW_END ) );
601
599
 
602
600
  If( visibility.greaterThan( 0.0 ), () => {
603
601
 
@@ -17,6 +17,7 @@ import {
17
17
  writeHitPacked,
18
18
  } from '../Processor/PackedRayBuffer.js';
19
19
  import { COUNTER, RAY_FLAG } from '../Processor/QueueManager.js';
20
+ import { hitFacet, packHitFacet } from './HitFacet.js';
20
21
 
21
22
  const WG_SIZE = 256;
22
23
 
@@ -29,6 +30,7 @@ export function buildExtendKernel( params ) {
29
30
  activeIndicesRO,
30
31
  counters,
31
32
  maxRayCount,
33
+ shadowTerminatorOffset,
32
34
  } = params;
33
35
 
34
36
  const computeFn = Fn( () => {
@@ -64,6 +66,12 @@ export function buildExtendKernel( params ) {
64
66
  ray, bvhBuffer, triangleBuffer, insideMedium,
65
67
  ) ).toVar();
66
68
 
69
+ const facet = hitFacet( {
70
+ triangleBuffer, bvhBuffer, triIdx: hitInfo.triangleIndex, instanceLeaf: hitInfo.instanceLeaf,
71
+ hitPoint: hitInfo.hitPoint, smoothNormal: hitInfo.normal, viewDir: direction.negate(),
72
+ didHit: hitInfo.didHit, liftEnabled: shadowTerminatorOffset.greaterThan( 0.0 ),
73
+ } );
74
+
67
75
  writeHitPacked(
68
76
  hitBufferRW, rayID,
69
77
  hitInfo.dst,
@@ -73,6 +81,7 @@ export function buildExtendKernel( params ) {
73
81
  uint( hitInfo.materialIndex ),
74
82
  // Biased by one: leaf 0 is real, so 0 has to mean "no instance".
75
83
  uint( hitInfo.instanceLeaf.add( int( 1 ) ) ),
84
+ packHitFacet( facet.faceN, facet.liftScale ),
76
85
  );
77
86
 
78
87
  } );
@@ -1,4 +1,4 @@
1
- import { Fn, float, vec3, max, clamp, sqrt } from 'three/tsl';
1
+ import { Fn, float, vec3, max, clamp, sqrt, select } from 'three/tsl';
2
2
 
3
3
  const EPSILON = 1e-6;
4
4
 
@@ -58,3 +58,25 @@ export const iorToFresnel0 = Fn( ( [ transmittedIor, incidentIor ] ) => {
58
58
  } );
59
59
 
60
60
  export const dielectricF0 = ( ior ) => vec3( iorToFresnel0( ior, float( 1.0 ) ) );
61
+
62
+ // Unpolarised reflectance of a smooth dielectric interface, eta = n_transmitted / n_incident.
63
+ // Returns 1 under total internal reflection. Schlick runs 16-23 % low at 45-65° for eta 1.5.
64
+ export const fresnelDielectric = Fn( ( [ cosI, eta ] ) => {
65
+
66
+ const c = clamp( cosI, 0.0, 1.0 );
67
+ const g2 = eta.mul( eta ).sub( 1.0 ).add( c.mul( c ) );
68
+ const g = sqrt( max( g2, 0.0 ) );
69
+ const A = g.sub( c ).div( max( g.add( c ), EPSILON ) );
70
+ const B = c.mul( g.add( c ) ).sub( 1.0 ).div( max( c.mul( g.sub( c ) ).add( 1.0 ), EPSILON ) );
71
+ return select( g2.greaterThan( 0.0 ), clamp( A.mul( A ).mul( 0.5 ).mul( B.mul( B ).add( 1.0 ) ), 0.0, 1.0 ), float( 1.0 ) );
72
+
73
+ } );
74
+
75
+ // The exact curve rescaled to run 0 head-on to 1 at grazing, so F = mix( f0, f90, weight ) keeps
76
+ // KHR_materials_specular's f0/f90 controls. With f0 = F0(eta) and f90 = 1 it is fresnelDielectric.
77
+ export const dielectricFresnelWeight = Fn( ( [ cosI, eta ] ) => {
78
+
79
+ const F0 = iorToFresnel0( eta, float( 1.0 ) );
80
+ return clamp( fresnelDielectric( cosI, eta ).sub( F0 ).div( max( float( 1.0 ).sub( F0 ), EPSILON ) ), 0.0, 1.0 );
81
+
82
+ } );
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The hit triangle's facet normal, packed into the hit record's spare lane by Extend.
3
+ *
4
+ * The hit record's `normal` is the interpolated vertex normal. Rays spawned from a surface must be
5
+ * offset along the facet itself — Cycles' ray_offset uses Ng — or a vertex normal bent away from its
6
+ * facet (foliage cards whose normals all point up) moves the origin within the card's own plane,
7
+ * and the continued ray hits the same card again. The lane also carries the shadow terminator lift.
8
+ *
9
+ * Layout: facet normal as an 11:11 octahedral pair (about 0.1°), then the lift's half float
10
+ * truncated to its top 10 bits (sign, exponent, 4 mantissa bits).
11
+ */
12
+
13
+ import {
14
+ vec2, vec3, float, int, uint, max, abs, dot, cross, normalize, clamp, round, select, If, uintBitsToFloat,
15
+ packHalf2x16, unpackHalf2x16,
16
+ } from 'three/tsl';
17
+ import {
18
+ getDatafromStorageBuffer, unpackTriangleNormal, TRI_STRIDE, instanceRows, instanceFaceNormalToWorld,
19
+ } from './Common.js';
20
+ import { shadowTerminatorLift } from './ShadowTerminator.js';
21
+
22
+ /**
23
+ * @returns {{ faceN: Node, liftScale: Node }} the facet normal on the viewer's side (the
24
+ * interpolated one for a degenerate triangle), and the terminator lift (0 unless `liftEnabled`)
25
+ */
26
+ export function hitFacet( { triangleBuffer, bvhBuffer, triIdx, instanceLeaf, hitPoint, smoothNormal, viewDir, didHit, liftEnabled } ) {
27
+
28
+ const faceN = vec3( 0.0, 0.0, 1.0 ).toVar();
29
+ const liftScale = float( 0.0 ).toVar();
30
+
31
+ If( didHit, () => {
32
+
33
+ const recA = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 0 ), int( TRI_STRIDE ) ).toVar();
34
+ const recB = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 1 ), int( TRI_STRIDE ) ).toVar();
35
+ const recC = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 2 ), int( TRI_STRIDE ) ).toVar();
36
+ const V0 = uintBitsToFloat( recA.xyz ).toVar();
37
+ const V1 = uintBitsToFloat( recB.xyz ).toVar();
38
+ const V2 = uintBitsToFloat( recC.xyz ).toVar();
39
+
40
+ // Shared geometry is stored in object space.
41
+ const faceLocal = cross( V1.sub( V0 ), V2.sub( V0 ) ).toVar();
42
+ const face = faceLocal.toVar();
43
+ const instanced = instanceLeaf.greaterThanEqual( int( 0 ) );
44
+ If( instanced, () => {
45
+
46
+ face.assign( instanceFaceNormalToWorld( instanceRows( bvhBuffer, instanceLeaf ), faceLocal ) );
47
+
48
+ } );
49
+
50
+ const n = select( dot( face, face ).greaterThan( 0.0 ), normalize( face ), smoothNormal ).toVar();
51
+ faceN.assign( select( dot( n, viewDir ).lessThan( 0.0 ), n.negate(), n ) );
52
+
53
+ If( liftEnabled, () => {
54
+
55
+ liftScale.assign( shadowTerminatorLift( {
56
+ V0, V1, V2,
57
+ N0: unpackTriangleNormal( recA.w ), N1: unpackTriangleNormal( recB.w ), N2: unpackTriangleNormal( recC.w ),
58
+ hitPoint, instanced, bvhBuffer, instanceLeaf, faceN,
59
+ } ) );
60
+
61
+ } );
62
+
63
+ } );
64
+
65
+ return { faceN, liftScale };
66
+
67
+ }
68
+
69
+ // Per component: TSL's select() on a bvec is not component-wise.
70
+ const signNotZero = v => vec2( select( v.x.greaterThanEqual( 0.0 ), 1.0, - 1.0 ), select( v.y.greaterThanEqual( 0.0 ), 1.0, - 1.0 ) );
71
+
72
+ export function packHitFacet( faceN, liftScale ) {
73
+
74
+ const p = faceN.xy.div( max( abs( faceN.x ).add( abs( faceN.y ) ).add( abs( faceN.z ) ), 1e-20 ) ).toVar();
75
+ If( faceN.z.lessThan( 0.0 ), () => {
76
+
77
+ p.assign( vec2( 1.0 ).sub( abs( p.yx ) ).mul( signNotZero( p ) ) );
78
+
79
+ } );
80
+ const q = round( clamp( p, - 1.0, 1.0 ).mul( 0.5 ).add( 0.5 ).mul( 2047.0 ) );
81
+ const h = packHalf2x16( vec2( liftScale, 0.0 ) ).bitAnd( uint( 0xffff ) ).shiftRight( uint( 6 ) );
82
+ return uint( q.x ).bitOr( uint( q.y ).shiftLeft( uint( 11 ) ) ).bitOr( h.shiftLeft( uint( 22 ) ) );
83
+
84
+ }
85
+
86
+ /** @returns {{ faceN: Node, liftScale: Node }} */
87
+ export function unpackHitFacet( bits ) {
88
+
89
+ const e = vec2(
90
+ float( bits.bitAnd( uint( 0x7ff ) ) ),
91
+ float( bits.shiftRight( uint( 11 ) ).bitAnd( uint( 0x7ff ) ) ),
92
+ ).div( 2047.0 ).mul( 2.0 ).sub( 1.0 ).toVar();
93
+ const z = float( 1.0 ).sub( abs( e.x ) ).sub( abs( e.y ) ).toVar();
94
+ If( z.lessThan( 0.0 ), () => {
95
+
96
+ e.assign( vec2( 1.0 ).sub( abs( e.yx ) ).mul( signNotZero( e ) ) );
97
+
98
+ } );
99
+ const liftScale = unpackHalf2x16( bits.shiftRight( uint( 22 ) ).shiftLeft( uint( 6 ) ) ).x;
100
+ return { faceN: normalize( vec3( e, z ) ), liftScale };
101
+
102
+ }
@@ -28,9 +28,9 @@ import {
28
28
  import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
29
29
  import {
30
30
  REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, getDatafromStorageBuffer, instanceRows,
31
- instanceFaceNormalToWorld, TRI_STRIDE, getAlphaShadowsUniform, shadowFlagsSettle
31
+ instanceFaceNormalToWorld, TRI_STRIDE, getAlphaShadowsUniform, shadowFlagsSettle, offsetRayOrigin,
32
32
  } from './Common.js';
33
- import { fresnelSchlickFloat, iorToFresnel0 } from './Fresnel.js';
33
+ import { fresnelDielectric } from './Fresnel.js';
34
34
  import { calculateBeerLawAbsorption } from './MaterialTransmission.js';
35
35
  import { getTransformedUV, sampleBucket } from './TextureSampling.js';
36
36
 
@@ -212,11 +212,8 @@ export const traceShadowRay = Fn( ( [
212
212
 
213
213
  } );
214
214
 
215
- // Compute transmittance based on material properties
216
- const fresnel = fresnelSchlickFloat(
217
- abs( dot( dir, N ) ),
218
- iorToFresnel0( shadowMaterial.ior, float( 1.0 ) ),
219
- );
215
+ // Shadow rays go straight through, so both faces see the outside-in angle and eta.
216
+ const fresnel = fresnelDielectric( abs( dot( dir, N ) ), max( shadowMaterial.ior, 1.0 ) );
220
217
 
221
218
  const matTransmittance = float( 1.0 ).sub( fresnel ).mul( shadowMaterial.transmission );
222
219
  transmittance.mulAssign( matTransmittance );
@@ -230,8 +227,9 @@ export const traceShadowRay = Fn( ( [
230
227
  } );
231
228
 
232
229
  // Continue ray past transmissive surface
233
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
234
- remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
230
+ const passEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
231
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( passEps ) ) );
232
+ remainingDist.subAssign( shadowHit.dst.add( passEps ) );
235
233
 
236
234
  } ).ElseIf( shadowMaterial.transparent, () => {
237
235
 
@@ -246,8 +244,9 @@ export const traceShadowRay = Fn( ( [
246
244
  } );
247
245
 
248
246
  // Continue ray past transparent surface
249
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
250
- remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
247
+ const passEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
248
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( passEps ) ) );
249
+ remainingDist.subAssign( shadowHit.dst.add( passEps ) );
251
250
 
252
251
  } ).Else( () => {
253
252
 
@@ -267,28 +266,8 @@ export const traceShadowRay = Fn( ( [
267
266
  // RAY OFFSET CALCULATION
268
267
  // ================================================================================
269
268
 
270
- export const calculateRayOffset = Fn( ( [ hitPoint, normal, material ] ) => {
271
-
272
- // Base epsilon scaled by scene size; adjusted by material properties below.
273
- const materialEpsilon = max( float( 1e-4 ), length( hitPoint ).mul( 1e-6 ) ).toVar();
274
-
275
- If( material.transmission.greaterThan( 0.0 ), () => {
276
-
277
- // Transmissive materials need larger offsets
278
- materialEpsilon.mulAssign( 2.0 );
279
-
280
- } );
281
-
282
- If( material.roughness.lessThan( 0.1 ), () => {
283
-
284
- // Smooth materials are more sensitive to precision issues
285
- materialEpsilon.mulAssign( 1.5 );
286
-
287
- } );
288
-
289
- return normal.mul( materialEpsilon );
290
-
291
- } );
269
+ // Vector from hitPoint to its ray spawn point (offsetRayOrigin), for callers that add it themselves.
270
+ export const calculateRayOffset = Fn( ( [ hitPoint, normal ] ) => offsetRayOrigin( hitPoint, normal ).sub( hitPoint ) );
292
271
 
293
272
  // ================================================================================
294
273
  // LIGHT IMPORTANCE ESTIMATION
@@ -81,15 +81,15 @@ import {
81
81
  powerHeuristic,
82
82
  balanceHeuristic,
83
83
  computeDotProductsAniso,
84
+ offsetRayOrigin,
85
+ SHADOW_END,
84
86
  } from './Common.js';
85
87
  import {
86
88
  sampleEquirectProbability,
87
89
  } from './Environment.js';
90
+ import { shadowTerminatorOrigin } from './ShadowTerminator.js';
88
91
 
89
92
  const TWO_PI = 2.0 * PI;
90
- // Visibility rays stop this fraction short of the sampled light point: unit-free, and ≫ float
91
- // error, so an emitter coplanar with its fixture is never occluded by it.
92
- const SHADOW_END = 1.0 - 1e-4;
93
93
 
94
94
  // =============================================================================
95
95
  // Light Sampling Functions
@@ -795,12 +795,19 @@ export const calculateDirectLightingUnified = Fn( ( [
795
795
  // Shadow catcher: when true, also accumulate the unoccluded (visibility=1) reference
796
796
  // at every light/env site so the caller can form a shadow ratio. Dead path otherwise.
797
797
  wantUnoccluded,
798
+ // The facet on the viewer's side, which shadow rays are offset off, and the shadow terminator lift
799
+ // (ShadowTerminator.js) for the light and environment ones
800
+ terminatorLift, facetNormal, terminatorCutoff,
798
801
  ] ) => {
799
802
 
800
803
  const totalContribution = vec3( 0.0 ).toVar();
801
804
  // Unoccluded reference (visibility forced to 1) — only filled when wantUnoccluded.
802
805
  const unoccludedContribution = vec3( 0.0 ).toVar();
803
- const rayOrigin = hitPoint.add( geomNormal.mul( 0.001 ) ).toVar();
806
+ const rayOrigin = offsetRayOrigin( hitPoint, facetNormal ).toVar();
807
+ const lightShadowOrigin = L => shadowTerminatorOrigin( {
808
+ hitPoint, offsetNormal: facetNormal, lift: terminatorLift, faceN: facetNormal, smoothN: geomNormal, L,
809
+ cutoff: terminatorCutoff,
810
+ } );
804
811
 
805
812
  // Binds BVH params so shadow-ray sites at varying call depths use a 3-arg call
806
813
  const shadow = Fn( ( [ origin, dir, maxDist ] ) =>
@@ -845,9 +852,10 @@ export const calculateDirectLightingUnified = Fn( ( [
845
852
 
846
853
  // Light geometry is measured from the hit point; only the visibility ray starts on the lifted
847
854
  // origin, aimed at the sampled point and stopped a relative hair short of it.
848
- const toSample = hitPoint.add( lightSample.direction.mul( lightSample.distance ) ).sub( rayOrigin ).toVar();
855
+ const lightOrigin = lightShadowOrigin( lightSample.direction ).toVar();
856
+ const toSample = hitPoint.add( lightSample.direction.mul( lightSample.distance ) ).sub( lightOrigin ).toVar();
849
857
  const shadowDistance = length( toSample ).toVar();
850
- const visibility = shadow( rayOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
858
+ const visibility = shadow( lightOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
851
859
 
852
860
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
853
861
 
@@ -1004,7 +1012,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1004
1012
 
1005
1013
  If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: envDirection, surfaceNormal: geomNormal } ) ), () => {
1006
1014
 
1007
- const visibility = shadow( rayOrigin, envDirection, float( 1e20 ) );
1015
+ const visibility = shadow( lightShadowOrigin( envDirection ), envDirection, float( 1e20 ) );
1008
1016
 
1009
1017
  If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
1010
1018
 
@@ -3,12 +3,12 @@ import {
3
3
  If, max, min, clamp, mix
4
4
  } from 'three/tsl';
5
5
 
6
- import { DotProducts, DFGResult } from './Struct.js';
6
+ import { DotProducts, DFGResult, BaseFresnel } from './Struct.js';
7
7
  import { PI_INV, MIN_CLEARCOAT_ROUGHNESS, computeDotProductsAniso } from './Common.js';
8
- import { fresnelSchlick, fresnelSchlickFloat, dielectricF0 } from './Fresnel.js';
8
+ import { fresnelSchlick, fresnelDielectric, dielectricFresnelWeight } from './Fresnel.js';
9
9
  import {
10
10
  DistributionGGX, SheenDistribution, VisibilitySheen, VisibilityGGXSmithCorrelated,
11
- sheenDirectionalAlbedo, evaluateDFG,
11
+ sheenDirectionalAlbedo, evaluateSpecularDFG, baseFresnelParams,
12
12
  computeAnisoAlphas, DistributionGGXAniso, VisibilityGGXAniso,
13
13
  } from './MaterialProperties.js';
14
14
  import { evalIridescence } from './MaterialProperties.js';
@@ -43,12 +43,9 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
43
43
 
44
44
  } ).Else( () => {
45
45
 
46
- // Calculate base F0 with specular parameters, clamped to physically valid range
47
- const F0 = clamp(
48
- mix( dielectricF0( material.ior ).mul( material.specularColor ), material.color.rgb, material.metalness )
49
- .mul( material.specularIntensity ),
50
- vec3( 0.0 ), vec3( 1.0 )
51
- ).toVar();
46
+ const bf = BaseFresnel.wrap( baseFresnelParams( material, material.color.rgb ) ).toVar();
47
+ const F0 = bf.F0.toVar();
48
+ const iridF = vec3( 0.0 ).toVar();
52
49
 
53
50
  // Modify material color for dispersive materials to enhance color separation
54
51
  const materialColor = material.color.rgb.toVar();
@@ -72,13 +69,18 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
72
69
 
73
70
  // Per glTF KHR_materials_iridescence spec: use max thickness when no texture
74
71
  const thickness = material.iridescenceThicknessRange.y;
75
- const iridescenceFresnel = evalIridescence( float( 1.0 ), material.iridescenceIOR, dots.VoH, thickness, F0 );
76
- F0.assign( mix( F0, iridescenceFresnel, material.iridescence ) );
72
+ iridF.assign( evalIridescence( float( 1.0 ), material.iridescenceIOR, dots.VoH, thickness, F0 ) );
73
+ F0.assign( mix( F0, iridF, material.iridescence ) );
77
74
 
78
75
  } );
79
76
 
80
- // Precalculate shared terms
81
- const F = fresnelSchlick( dots.VoH, F0 );
77
+ // Dielectric on the exact Fresnel curve, metal and iridescence on Schlick.
78
+ const Fd = mix( bf.f0, vec3( bf.f90 ), dielectricFresnelWeight( dots.VoH, bf.eta ) );
79
+ const F = mix(
80
+ mix( Fd, fresnelSchlick( dots.VoH, bf.F0m ), material.metalness ),
81
+ fresnelSchlick( dots.VoH, iridF ),
82
+ material.iridescence,
83
+ ).toVar();
82
84
 
83
85
  // Single-scatter specular BRDF (anisotropic when material.anisotropy > 0; the aniso
84
86
  // visibility term already carries the 1/(4·NoV·NoL) denominator)
@@ -98,9 +100,11 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
98
100
 
99
101
  } );
100
102
 
101
- // Shared DFG evaluation — compensation factor and total directional albedo
102
- // come from the same polynomial.
103
- const dfg = DFGResult.wrap( evaluateDFG( F0, dots.NoV, material.roughness ) );
103
+ // Compensation factor and total directional albedo from the same table fetch.
104
+ const dfg = DFGResult.wrap( evaluateSpecularDFG(
105
+ bf.f0, bf.f90, bf.eta, bf.F0m, material.metalness, iridF, material.iridescence, F0,
106
+ dots.NoV, material.roughness,
107
+ ) );
104
108
  const specular = specularSS.mul( dfg.compensation );
105
109
 
106
110
  // Diffuse energy budget from hemisphere-integrated specular albedo (includes multiscatter)
@@ -140,15 +144,18 @@ export const evaluateMaterialResponseFromDots = Fn( ( [ material, dots ] ) => {
140
144
  If( material.clearcoat.greaterThan( 0.0 ), () => {
141
145
 
142
146
  const ccRoughness = max( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS );
143
- const ccF0 = vec3( 0.04 );
147
+ const ccF0 = vec3( 0.04 ); // IOR 1.5
144
148
 
145
149
  // A GGX lobe like any other, so same multiscatter treatment; its albedo is what the
146
150
  // base underneath loses.
147
- const ccDfg = DFGResult.wrap( evaluateDFG( ccF0, dots.NoV, ccRoughness ) );
151
+ const ccDfg = DFGResult.wrap( evaluateSpecularDFG(
152
+ ccF0, float( 1.0 ), float( 1.5 ), vec3( 0.0 ), float( 0.0 ), vec3( 0.0 ), float( 0.0 ), ccF0,
153
+ dots.NoV, ccRoughness,
154
+ ) );
148
155
 
149
156
  const ccD = DistributionGGX( dots.NoH, ccRoughness );
150
157
  const ccVis = VisibilityGGXSmithCorrelated( dots.NoV, dots.NoL, ccRoughness );
151
- const ccF = fresnelSchlickFloat( dots.VoH, float( 0.04 ) );
158
+ const ccF = fresnelDielectric( dots.VoH, float( 1.5 ) );
152
159
  const ccLobe = vec3( ccD.mul( ccVis ).mul( ccF ) ).mul( ccDfg.compensation );
153
160
 
154
161