rayzee 7.20.2 → 7.21.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.
@@ -11,8 +11,8 @@
11
11
  import {
12
12
  Fn,
13
13
  wgslFn,
14
+ bool as tslBool,
14
15
  float,
15
- vec2,
16
16
  vec3,
17
17
  int,
18
18
  max,
@@ -28,7 +28,6 @@ import {
28
28
  } from 'three/tsl';
29
29
 
30
30
  import {
31
- PI_INV,
32
31
  MAX_ROUGHNESS,
33
32
  MIN_CLEARCOAT_ROUGHNESS,
34
33
  MIN_PDF,
@@ -36,19 +35,19 @@ import {
36
35
  anisoTangentFrame,
37
36
  calculateFireflyThreshold,
38
37
  applySoftSuppressionRGB,
38
+ computeDotProductsAniso,
39
39
  } from './Common.js';
40
- import { AnisoFrame, DirectionSample, MaterialCache } from './Struct.js';
41
- import { RandomValue } from './Random.js';
40
+ import { AnisoFrame, DirectionSample, DotProducts, MaterialCache } from './Struct.js';
41
+ import { getRandomSample1D } from './Random.js';
42
42
  import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
43
43
  import {
44
- SheenDistribution,
45
- calculateVNDFPDF,
46
- calculateVNDFPDFAniso,
44
+ calculateBSDFSamplingPDF,
47
45
  computeAnisoAlphas,
48
46
  calculateBRDFWeights,
47
+ sheenSamplingRoughness,
49
48
  } from './MaterialProperties.js';
50
- import { evaluateMaterialResponse } from './MaterialEvaluation.js';
51
- import { dielectricF0 } from './Fresnel.js';
49
+ import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
50
+
52
51
  import {
53
52
  ImportanceSampleCosine,
54
53
  ImportanceSampleGGX,
@@ -61,7 +60,8 @@ import {
61
60
  // =============================================================================
62
61
 
63
62
  export const generateSampledDirection = Fn( ( [
64
- V, N, material, xi, rngState,
63
+ V, N, material, xi, lobeXi, rngState,
64
+ pixelCoord, resolution, frame, dimBase,
65
65
  // Caller-resolved material classification (avoids redundant classifyMaterial —
66
66
  // TSL Fn can't write back to caller variables, so the caller is responsible
67
67
  // for keeping psCachedClassification current and passes it in here).
@@ -73,6 +73,8 @@ export const generateSampledDirection = Fn( ( [
73
73
  const resultDirection = vec3( 0.0 ).toVar();
74
74
  const resultValue = vec3( 0.0 ).toVar();
75
75
  const resultPdf = float( 0.0 ).toVar();
76
+ const resultIsTransmission = tslBool( false ).toVar();
77
+ const resultColorWeight = vec3( 1.0 ).toVar();
76
78
 
77
79
  // Compute BRDF weights
78
80
  const weights = cachedBrdfWeights.toVar();
@@ -87,13 +89,6 @@ export const generateSampledDirection = Fn( ( [
87
89
 
88
90
  // Create minimal temporary cache
89
91
  const tempCache = MaterialCache( {
90
- F0: dielectricF0( material.ior ),
91
- NoV: float( 1.0 ),
92
- diffuseColor: vec3( 0.0 ),
93
- isPurelyDiffuse: false,
94
- alpha: float( 0.0 ),
95
- k: float( 0.0 ),
96
- alpha2: float( 0.0 ),
97
92
  invRoughness: float( 1.0 ).sub( material.roughness ),
98
93
  metalFactor: float( 0.5 ).add( float( 0.5 ).mul( material.metalness ) ),
99
94
  iorFactor: min( float( 2.0 ).div( material.ior ), 1.0 ),
@@ -105,8 +100,10 @@ export const generateSampledDirection = Fn( ( [
105
100
 
106
101
  } );
107
102
 
108
- const rand = xi.x.toVar();
109
- const directionSample = vec2( xi.y, RandomValue( rngState ) ).toVar();
103
+ // Own dimension, NOT xi.x — the lobe samplers below consume the full pair, so reusing xi.x
104
+ // would leave it conditioned on the branch taken and confine VNDF's r=sqrt(Xi.x) to
105
+ // [sqrt(lo),1), making the specular peak unreachable whenever a diffuse lobe competes.
106
+ const rand = lobeXi.toVar();
110
107
  const H = vec3( 0.0 ).toVar();
111
108
 
112
109
  // Cumulative probability approach for sampling selection
@@ -115,17 +112,11 @@ export const generateSampledDirection = Fn( ( [
115
112
  const cumulativeSheen = cumulativeSpecular.add( weights.sheen ).toVar();
116
113
  const cumulativeClearcoat = cumulativeSheen.add( weights.clearcoat );
117
114
 
118
- // Hoisted out of the lobe chain: used by both Specular and Clearcoat branches
119
- const NoV = clamp( dot( N, V ), 0.001, 1.0 ).toVar();
120
-
121
115
  // Chained If/ElseIf so emitted WGSL becomes a single mutually-exclusive branch
122
116
  // (replaces five separate If blocks gated on a `sampled` flag — divergence hotspot)
123
117
  If( rand.lessThan( cumulativeDiffuse ), () => {
124
118
 
125
- resultDirection.assign( ImportanceSampleCosine( { N, xi: directionSample } ) );
126
- const NoL = clamp( dot( N, resultDirection ), 0.0, 1.0 );
127
- resultPdf.assign( NoL.mul( PI_INV ) );
128
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
119
+ resultDirection.assign( ImportanceSampleCosine( { N, xi } ) );
129
120
 
130
121
  } ).ElseIf( rand.lessThan( cumulativeSpecular ), () => {
131
122
 
@@ -141,10 +132,7 @@ export const generateSampledDirection = Fn( ( [
141
132
  const localH = sampleGGXVNDFAniso( { V: localV, alphaX: a.x, alphaY: a.y, Xi: xi } );
142
133
  H.assign( Ta.mul( localH.x ).add( Ba.mul( localH.y ) ).add( N.mul( localH.z ) ) );
143
134
 
144
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
145
135
  resultDirection.assign( reflect( V.negate(), H ) );
146
- resultPdf.assign( calculateVNDFPDFAniso( a.x, a.y, NoH, dot( Ta, H ), dot( Ba, H ), NoV, dot( Ta, V ), dot( Ba, V ) ) );
147
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
148
136
 
149
137
  } ).Else( () => {
150
138
 
@@ -155,69 +143,65 @@ export const generateSampledDirection = Fn( ( [
155
143
  const localH = sampleGGXVNDF( { V: localV, roughness: material.roughness, Xi: xi } );
156
144
  H.assign( TBN.mul( localH ) );
157
145
 
158
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
159
-
160
146
  resultDirection.assign( reflect( V.negate(), H ) );
161
- resultPdf.assign( calculateVNDFPDF( NoH, NoV, material.roughness ) );
162
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
163
147
 
164
148
  } );
165
149
 
166
150
  } ).ElseIf( rand.lessThan( cumulativeSheen ), () => {
167
151
 
168
- H.assign( ImportanceSampleGGX( { N, roughness: material.sheenRoughness, Xi: xi } ) );
169
- const NoH = clamp( dot( N, H ), 0.001, 1.0 );
170
- const VoH = clamp( dot( V, H ), 0.001, 1.0 );
152
+ H.assign( ImportanceSampleGGX( { N, roughness: sheenSamplingRoughness( material.sheenRoughness ), Xi: xi } ) );
171
153
  resultDirection.assign( reflect( V.negate(), H ) );
172
- const NoL = dot( N, resultDirection ).toVar();
173
-
174
- // Reject directions below the surface - fall back to diffuse
175
- If( NoL.lessThanEqual( 0.0 ), () => {
176
-
177
- resultDirection.assign( ImportanceSampleCosine( { N, xi } ) );
178
- NoL.assign( clamp( dot( N, resultDirection ), 0.0, 1.0 ) );
179
- resultPdf.assign( NoL.mul( PI_INV ) );
180
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
181
-
182
- } ).Else( () => {
183
154
 
184
- resultPdf.assign( SheenDistribution( NoH, material.sheenRoughness ).mul( NoH ).div( float( 4.0 ).mul( VoH ) ) );
185
- resultPdf.assign( max( resultPdf, MIN_PDF ) );
186
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
187
-
188
- } );
155
+ // Below-surface sheen draws are dropped, not redirected to a cosine direction: a fallback
156
+ // would add a P(reject)·cos term to the true density that calculateBSDFSamplingPDF cannot
157
+ // model, and dividing by the smaller modelled density inflated the lobe 23 %.
189
158
 
190
159
  } ).ElseIf( rand.lessThan( cumulativeClearcoat ), () => {
191
160
 
161
+ // VNDF, not ImportanceSampleGGX: the mixture density reports this lobe with
162
+ // calculateVNDFPDF, and sampling the half-vector GGX distribution instead would make the
163
+ // reported density one this lobe never drew from.
192
164
  const clearcoatRoughness = clamp( material.clearcoatRoughness, MIN_CLEARCOAT_ROUGHNESS, MAX_ROUGHNESS );
193
- H.assign( ImportanceSampleGGX( { N, roughness: clearcoatRoughness, Xi: xi } ) );
194
- const NoH = clamp( dot( N, H ), 0.0, 1.0 );
165
+ const ccTBN = constructTBN( { N } );
166
+ H.assign( ccTBN.mul( sampleGGXVNDF( { V: ccTBN.transpose().mul( V ), roughness: clearcoatRoughness, Xi: xi } ) ) );
195
167
  resultDirection.assign( reflect( V.negate(), H ) );
196
- resultPdf.assign( calculateVNDFPDF( NoH, NoV, clearcoatRoughness ) );
197
- resultPdf.assign( max( resultPdf, MIN_PDF ) );
198
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
199
168
 
200
169
  } ).Else( () => {
201
170
 
202
- // Transmission sampling (fallback)
203
171
  const entering = dot( V, N ).greaterThan( 0.0 );
204
- // pathWavelength=0 — only direction/PDF are consumed here, throughput goes via handleTransmission
172
+ // pathWavelength=0 — the spectral lock happens downstream; colorWeight is carried out on
173
+ // the DirectionSample so the consumer can build the transmission throughput.
205
174
  const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
206
175
  V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState, float( 0.0 ),
176
+ pixelCoord, resolution, frame, dimBase,
207
177
  ) );
208
178
  resultDirection.assign( mtResult.direction );
209
- resultPdf.assign( max( mtResult.pdf, MIN_PDF ) );
210
- resultValue.assign( evaluateMaterialResponse( V, resultDirection, N, material ) );
179
+ // Selection probability included: this lobe is the only one that reaches its hemisphere,
180
+ // so its density is w_transmission · pdf rather than pdf alone.
181
+ resultPdf.assign( max( weights.transmission.mul( mtResult.pdf ), MIN_PDF ) );
182
+ resultColorWeight.assign( mtResult.colorWeight );
183
+ resultIsTransmission.assign( true );
184
+
185
+ } );
186
+
187
+ // One mixture density for every reflection lobe: any of them could have produced this
188
+ // direction, so the chosen lobe's own pdf is not the density we sampled from.
189
+ If( resultIsTransmission.not(), () => {
190
+
191
+ const dotsOut = DotProducts.wrap( computeDotProductsAniso( N, V, resultDirection, material ) );
192
+ resultValue.assign( evaluateMaterialResponseFromDots( material, dotsOut ) );
193
+ resultPdf.assign( calculateBSDFSamplingPDF( material, weights, dotsOut ) );
211
194
 
212
195
  } );
213
196
 
214
- // Ensure PDF is valid
215
197
  resultPdf.assign( max( resultPdf, MIN_PDF ) );
216
198
 
217
199
  return DirectionSample( {
218
200
  direction: resultDirection,
219
201
  value: resultValue,
220
202
  pdf: resultPdf,
203
+ isTransmission: resultIsTransmission,
204
+ colorWeight: resultColorWeight,
221
205
  } );
222
206
 
223
207
  } );
@@ -258,7 +242,8 @@ export const computeNDCDepth = /*@__PURE__*/ wgslFn( `
258
242
  // Takes the already-computed MaterialClassification `mc` directly (the wavefront classifies once per shade).
259
243
  export const handleRussianRoulette = Fn( ( [
260
244
  depth, throughput, mc, rayDirection, rngState,
261
- enableEnvironmentLight, useEnvMapIS,
245
+ pixelCoord, resolution, frame, dimBase,
246
+ enableEnvironmentLight,
262
247
  ] ) => {
263
248
 
264
249
  const result = float( 1.0 ).toVar();
@@ -271,7 +256,7 @@ export const handleRussianRoulette = Fn( ( [
271
256
  If( throughputStrength.lessThan( 0.0008 ).and( depth.greaterThan( int( 4 ) ) ), () => {
272
257
 
273
258
  const lowThroughputProb = max( throughputStrength.mul( 125.0 ), 0.01 );
274
- const rrSample = RandomValue( rngState );
259
+ const rrSample = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame );
275
260
  result.assign( select( rrSample.lessThan( lowThroughputProb ), lowThroughputProb, float( 0.0 ) ) );
276
261
 
277
262
  } ).Else( () => {
@@ -332,7 +317,7 @@ export const handleRussianRoulette = Fn( ( [
332
317
  estMaterialImportance.assign( clamp( estMaterialImportance, 0.0, 1.0 ) );
333
318
 
334
319
  const directionImportance = float( 0.5 ).toVar();
335
- If( enableEnvironmentLight.and( useEnvMapIS ).and( throughputStrength.greaterThan( 0.01 ) ), () => {
320
+ If( enableEnvironmentLight.and( throughputStrength.greaterThan( 0.01 ) ), () => {
336
321
 
337
322
  const cosTheta = clamp( rayDirection.y, 0.0, 1.0 );
338
323
  directionImportance.assign( mix( float( 0.3 ), float( 0.8 ), cosTheta.mul( cosTheta ) ) );
@@ -373,7 +358,7 @@ export const handleRussianRoulette = Fn( ( [
373
358
  const minProb = select( mc.isEmissive, float( 0.04 ), float( 0.02 ) );
374
359
  rrProb.assign( max( rrProb, minProb ) );
375
360
 
376
- const rrSample = RandomValue( rngState );
361
+ const rrSample = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame );
377
362
  result.assign( select( rrSample.lessThan( rrProb ), rrProb, float( 0.0 ) ) );
378
363
 
379
364
  } );