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.
@@ -3,7 +3,6 @@
3
3
 
4
4
  import {
5
5
  Fn,
6
- vec2,
7
6
  vec3,
8
7
  float,
9
8
  int,
@@ -21,7 +20,7 @@ import {
21
20
  length,
22
21
  } from 'three/tsl';
23
22
  import { MIN_PDF } from './Common.js';
24
- import { RandomValue } from './Random.js';
23
+ import { getRandomSample1D, getRandomSample2D } from './Random.js';
25
24
  import {
26
25
  EmissiveSample,
27
26
  sampleTriangle,
@@ -107,6 +106,7 @@ export const lbvhNodeImportance = Fn( ( [ nMin, power, nMax, coneAxis, cosThetaO
107
106
  export const sampleLightBVHTriangle = Fn( ( [
108
107
  hitPoint, surfaceNormal,
109
108
  rngState,
109
+ pixelCoord, resolution, frame, dimBase,
110
110
  lbvhBuffer,
111
111
  emissiveTriangleBuffer,
112
112
  emissiveVec4Offset,
@@ -130,6 +130,13 @@ export const sampleLightBVHTriangle = Fn( ( [
130
130
  const nodeIndex = int( 0 ).toVar();
131
131
  const foundLeaf = tslBool( false ).toVar();
132
132
 
133
+ // Hierarchical sample warping: ONE variate recycled down the descent and into the leaf scan,
134
+ // rescaled to [0,1) after each choice. A per-level draw has no fixed dimension to key on
135
+ // (depth varies per pixel), so the descent — which dominates emissive-NEE variance — would
136
+ // otherwise stay white-noise however well the point-on-triangle pair is sampled. Unbiased:
137
+ // the rescaled value is uniform conditional on the branch taken; selectionPdf is unchanged.
138
+ const u = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
139
+
133
140
  // Tree descent: at most MAX_LBVH_DEPTH iterations
134
141
  Loop( MAX_LBVH_DEPTH, () => {
135
142
 
@@ -179,19 +186,19 @@ export const sampleLightBVHTriangle = Fn( ( [
179
186
  // Probability of choosing left child
180
187
  const pLeft = lImportance.div( totalImportance );
181
188
 
182
- // Sample random value to pick child
183
- const rand = RandomValue( rngState );
184
-
185
- If( rand.lessThan( pLeft ), () => {
189
+ If( u.lessThan( pLeft ), () => {
186
190
 
187
191
  // Choose left child
192
+ u.assign( clamp( u.div( max( pLeft, float( 1e-6 ) ) ), 0.0, 0.999999 ) );
188
193
  selectionPdf.mulAssign( pLeft );
189
194
  nodeIndex.assign( leftChildIdx );
190
195
 
191
196
  } ).Else( () => {
192
197
 
193
198
  // Choose right child
194
- selectionPdf.mulAssign( float( 1.0 ).sub( pLeft ) );
199
+ const pRight = float( 1.0 ).sub( pLeft ).toVar();
200
+ u.assign( clamp( u.sub( pLeft ).div( max( pRight, float( 1e-6 ) ) ), 0.0, 0.999999 ) );
201
+ selectionPdf.mulAssign( pRight );
195
202
  nodeIndex.assign( rightChildIdx );
196
203
 
197
204
  } );
@@ -213,7 +220,7 @@ export const sampleLightBVHTriangle = Fn( ( [
213
220
 
214
221
  // Sample one triangle proportional to power within the leaf
215
222
  // Linear scan: pick random threshold against cumulative power sum
216
- const randLeaf = RandomValue( rngState ).mul( leafTotalPower );
223
+ const randLeaf = u.mul( leafTotalPower );
217
224
  const cumPower = float( 0.0 ).toVar();
218
225
 
219
226
  // Default to last entry as fallback
@@ -253,10 +260,7 @@ export const sampleLightBVHTriangle = Fn( ( [
253
260
  // Fetch triangle geometry
254
261
  const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
255
262
 
256
- // Generate random numbers for point sampling
257
- const xi_r1 = RandomValue( rngState ).toVar();
258
- const xi_r2 = RandomValue( rngState ).toVar();
259
- const xi = vec2( xi_r1, xi_r2 );
263
+ const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
260
264
 
261
265
  const geoNormal = normalize( cross( triData.v1.sub( triData.v0 ), triData.v2.sub( triData.v0 ) ) );
262
266
 
@@ -83,13 +83,14 @@ export const LightSample = struct( {
83
83
  distance: 'float',
84
84
  lightType: 'int',
85
85
  valid: 'bool',
86
+ // Reservoir denominator, carried out so the BSDF-hit MIS partner can rebuild the
87
+ // selection pdf without walking every light buffer a second time.
88
+ selectionTotalWeight: 'float',
86
89
  } );
87
90
 
88
91
  export const IndirectLightingResult = struct( {
89
92
  direction: 'vec3', // Sampled direction for next bounce
90
93
  throughput: 'vec3', // Light throughput along this path
91
- misWeight: 'float', // MIS weight for this sample
92
- pdf: 'float', // PDF of the selected strategy
93
94
  combinedPdf: 'float', // Weighted sum of all strategy PDFs (for NEE↔implicit MIS pairing)
94
95
  } );
95
96
 
@@ -74,7 +74,7 @@ export const traceShadowRay = Fn( ( [
74
74
  const rayOrigin = origin.toVar();
75
75
  const remainingDist = float( maxDist ).toVar();
76
76
 
77
- const MAX_SHADOW_TRANSMISSIONS = 8;
77
+ const MAX_SHADOW_TRANSMISSIONS = 16;
78
78
 
79
79
  Loop( { start: int( 0 ), end: int( MAX_SHADOW_TRANSMISSIONS ) }, () => {
80
80
 
@@ -160,7 +160,7 @@ export const traceShadowRay = Fn( ( [
160
160
  const blendAlpha = clamp( shadowMaterial.color.a.mul( shadowMaterial.opacity ).mul( texAlpha ), 0.0, 1.0 );
161
161
  transmittance.mulAssign( float( 1.0 ).sub( blendAlpha ) );
162
162
 
163
- If( transmittance.lessThan( 0.005 ), () => {
163
+ If( transmittance.lessThan( 1e-4 ), () => {
164
164
 
165
165
  transmittance.assign( 0.0 );
166
166
  Break();
@@ -221,7 +221,7 @@ export const traceShadowRay = Fn( ( [
221
221
  transmittance.mulAssign( matTransmittance );
222
222
 
223
223
  // Early exit if almost no light passes through
224
- If( transmittance.lessThan( 0.005 ), () => {
224
+ If( transmittance.lessThan( 1e-4 ), () => {
225
225
 
226
226
  transmittance.assign( 0.0 );
227
227
  Break();
@@ -237,7 +237,7 @@ export const traceShadowRay = Fn( ( [
237
237
  // Handle transparent materials
238
238
  transmittance.mulAssign( float( 1.0 ).sub( shadowMaterial.opacity ) );
239
239
 
240
- If( transmittance.lessThan( 0.005 ), () => {
240
+ If( transmittance.lessThan( 1e-4 ), () => {
241
241
 
242
242
  transmittance.assign( 0.0 );
243
243
  Break();
@@ -7,398 +7,79 @@
7
7
  * strategy — it is handled via deterministic NEE in LightsSampling.js.
8
8
  *
9
9
  * Contains:
10
- * - calculateTransmissionPDF — transmission PDF for MIS
11
- * - calculateClearcoatPDF — clearcoat PDF for MIS
12
- * - computeSamplingInfo — compute importance weights for material strategies
13
- * - selectSamplingStrategy — CDF-based strategy selection (specular, diffuse, transmission, clearcoat)
14
10
  * - calculateIndirectLighting — material multi-strategy MIS for bounce direction
15
11
  */
16
12
 
17
- import {
18
- Fn,
19
- float,
20
- vec2,
21
- vec3,
22
- int,
23
- bool as tslBool,
24
- max,
25
- abs,
26
- sqrt,
27
- dot,
28
- If,
29
- select,
30
- } from 'three/tsl';
13
+ import { Fn, float, vec3, int, max, dot, If, select } from 'three/tsl';
31
14
 
32
- import {
33
- IndirectLightingResult,
34
- } from './LightsCore.js';
35
- import {
36
- SamplingStrategyWeights,
37
- } from './Struct.js';
38
- import {
39
- PI_INV,
40
- EPSILON,
41
- MIN_PDF,
42
- } from './Common.js';
43
- import { DistributionGGX, calculateVNDFPDF, specularVNDFPDFForDir } from './MaterialProperties.js';
44
- import { evaluateMaterialResponse } from './MaterialEvaluation.js';
45
- import { RandomValue } from './Random.js';
46
- import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
15
+ import { IndirectLightingResult } from './LightsCore.js';
16
+ import { MIN_PDF } from './Common.js';
17
+ import { getRandomSample2D } from './Random.js';
47
18
  import { cosineWeightedSample } from './MaterialSampling.js';
48
19
 
49
- // =============================================================================
50
- // PDF Calculation Helpers
51
- // =============================================================================
52
-
53
- // Transmission PDF (Walter et al. 2007)
54
- export const calculateTransmissionPDF = Fn( ( [ V, L, N, ior, roughness, entering ] ) => {
55
-
56
- // eta is the relative IOR: eta_transmitted / eta_incident
57
- // When entering: air(1.0) -> material(ior), so eta = ior
58
- // When exiting: material(ior) -> air(1.0), so eta = 1.0/ior
59
- const eta = select( entering, ior, float( 1.0 ).div( ior ) ).toVar();
60
-
61
- // Transmission half-vector formula
62
- const H_raw = V.add( L.mul( eta ) ).toVar();
63
- const lenSq = dot( H_raw, H_raw ).toVar();
64
- const H = select( lenSq.greaterThan( EPSILON ), H_raw.div( sqrt( lenSq ) ), N ).toVar();
65
-
66
- // Ensure H points into the correct hemisphere
67
- If( dot( H, N ).lessThan( 0.0 ), () => {
68
-
69
- H.assign( H.negate() );
70
-
71
- } );
72
-
73
- const VoH = abs( dot( V, H ) );
74
- const LoH = abs( dot( L, H ) ).toVar();
75
- const NoH = abs( dot( N, H ) ).toVar();
76
-
77
- // GGX distribution
78
- const D = DistributionGGX( NoH, roughness );
79
-
80
- // Jacobian for transmission
81
- const denom_inner = VoH.add( LoH.mul( eta ) ).toVar();
82
- const denom = denom_inner.mul( denom_inner );
83
- const jacobian = LoH.mul( eta ).mul( eta ).div( max( denom, EPSILON ) );
84
-
85
- return D.mul( NoH ).mul( jacobian );
86
-
87
- } );
88
-
89
- // Clearcoat PDF
90
- export const calculateClearcoatPDF = Fn( ( [ V, L, N, clearcoatRoughness ] ) => {
91
-
92
- const H_raw = V.add( L ).toVar();
93
- const lenSq = dot( H_raw, H_raw ).toVar();
94
- const H = select( lenSq.greaterThan( EPSILON ), H_raw.div( sqrt( lenSq ) ), N );
95
-
96
- const NoH = max( dot( N, H ), 0.0 );
97
- const NoV = max( dot( N, V ), 0.0 );
98
-
99
- return calculateVNDFPDF( NoH, NoV, clearcoatRoughness );
100
-
101
- } );
102
-
103
- // =============================================================================
104
- // Sampling Strategy Computation
105
- // =============================================================================
106
-
107
- // Compute normalized strategy weights based on importance info
108
- export const computeSamplingInfo = Fn( ( [
109
- samplingInfo,
110
- ] ) => {
111
-
112
- // Material-only strategies (env handled via deterministic NEE in direct lighting)
113
- const specularW = samplingInfo.specularImportance.toVar();
114
- const useSpecular = specularW.greaterThan( 0.001 ).toVar();
115
-
116
- const diffuseW = samplingInfo.diffuseImportance.toVar();
117
- const useDiffuse = diffuseW.greaterThan( 0.001 ).toVar();
118
-
119
- const transmissionW = samplingInfo.transmissionImportance.toVar();
120
- const useTransmission = transmissionW.greaterThan( 0.001 ).toVar();
121
-
122
- const clearcoatW = samplingInfo.clearcoatImportance.toVar();
123
- const useClearcoat = clearcoatW.greaterThan( 0.001 ).toVar();
124
-
125
- const totalW = specularW.add( diffuseW ).add( transmissionW ).add( clearcoatW ).toVar();
126
-
127
- If( totalW.lessThan( 0.001 ), () => {
128
-
129
- specularW.assign( 0.0 );
130
- diffuseW.assign( 1.0 );
131
- transmissionW.assign( 0.0 );
132
- clearcoatW.assign( 0.0 );
133
- totalW.assign( 1.0 );
134
- useSpecular.assign( tslBool( false ) );
135
- useDiffuse.assign( tslBool( true ) );
136
- useTransmission.assign( tslBool( false ) );
137
- useClearcoat.assign( tslBool( false ) );
138
-
139
- } ).Else( () => {
140
-
141
- const invTotal = float( 1.0 ).div( totalW ).toVar();
142
- specularW.mulAssign( invTotal );
143
- diffuseW.mulAssign( invTotal );
144
- transmissionW.mulAssign( invTotal );
145
- clearcoatW.mulAssign( invTotal );
146
- totalW.assign( 1.0 );
147
-
148
- } );
149
-
150
- return SamplingStrategyWeights( {
151
- envWeight: float( 0.0 ),
152
- specularWeight: specularW,
153
- diffuseWeight: diffuseW,
154
- transmissionWeight: transmissionW,
155
- clearcoatWeight: clearcoatW,
156
- totalWeight: totalW,
157
- useEnv: tslBool( false ),
158
- useSpecular,
159
- useDiffuse,
160
- useTransmission,
161
- useClearcoat,
162
- } );
163
-
164
- } );
165
-
166
- // =============================================================================
167
- // Strategy Selection via Cumulative Distribution
168
- // =============================================================================
169
-
170
- // Strategy IDs: 1=specular, 2=diffuse, 3=transmission, 4=clearcoat
171
- // (env removed — handled via deterministic NEE in direct lighting)
172
- export const selectSamplingStrategy = Fn( ( [ weights, randomValue ] ) => {
173
-
174
- const selectedStrategy = int( 2 ).toVar(); // Default: diffuse
175
-
176
- const cumulative = float( 0.0 ).toVar();
177
- const found = tslBool( false ).toVar();
178
-
179
- If( weights.useSpecular.and( found.not() ), () => {
180
-
181
- cumulative.addAssign( weights.specularWeight );
182
-
183
- If( randomValue.lessThan( cumulative ), () => {
184
-
185
- selectedStrategy.assign( 1 );
186
- found.assign( tslBool( true ) );
187
-
188
- } );
189
-
190
- } );
191
-
192
- If( weights.useDiffuse.and( found.not() ), () => {
193
20
 
194
- cumulative.addAssign( weights.diffuseWeight );
195
21
 
196
- If( randomValue.lessThan( cumulative ), () => {
197
22
 
198
- selectedStrategy.assign( 2 );
199
- found.assign( tslBool( true ) );
200
23
 
201
- } );
202
-
203
- } );
204
-
205
- If( weights.useTransmission.and( found.not() ), () => {
206
-
207
- cumulative.addAssign( weights.transmissionWeight );
208
-
209
- If( randomValue.lessThan( cumulative ), () => {
210
-
211
- selectedStrategy.assign( 3 );
212
- found.assign( tslBool( true ) );
213
-
214
- } );
215
-
216
- } );
217
-
218
- If( weights.useClearcoat.and( found.not() ), () => {
219
-
220
- selectedStrategy.assign( 4 );
221
-
222
- } );
223
-
224
- return selectedStrategy;
225
-
226
- } );
227
-
228
- // =============================================================================
229
- // Cosine Weighted PDF helper
230
- // =============================================================================
231
-
232
- const cosineWeightedPDF = Fn( ( [ NoL ] ) => {
233
-
234
- return max( NoL, 0.0 ).mul( PI_INV );
235
-
236
- } );
237
24
 
238
25
  // =============================================================================
239
26
  // Indirect Lighting Calculation
240
27
  // =============================================================================
241
28
 
242
29
  export const calculateIndirectLighting = Fn( ( [
243
- V, N, material,
30
+ N, material,
244
31
  // brdfSample fields (DirectionSample)
245
32
  brdfSampleDirection, brdfSamplePdf, brdfSampleValue,
33
+ // DirectionSample carries these so refraction throughput does not need a second sampler run.
34
+ isTransmission, brdfSampleColorWeight,
246
35
  rngState,
247
- samplingInfo,
36
+ pixelCoord, resolution, frame, dimBase,
248
37
  ] ) => {
249
38
 
250
39
  // Initialize result
251
40
  const r_direction = vec3( 0.0 ).toVar();
252
41
  const r_throughput = vec3( 0.0 ).toVar();
253
- const r_misWeight = float( 0.0 ).toVar();
254
- const r_pdf = float( 0.0 ).toVar();
255
42
  const r_combinedPdf = float( 0.0 ).toVar();
256
43
 
257
- // Validate input sampling info
258
- const validInput = samplingInfo.diffuseImportance.greaterThanEqual( 0.0 )
259
- .and( samplingInfo.specularImportance.greaterThanEqual( 0.0 ) )
260
- .and( samplingInfo.transmissionImportance.greaterThanEqual( 0.0 ) )
261
- .and( samplingInfo.clearcoatImportance.greaterThanEqual( 0.0 ) );
44
+ // A non-finite pdf would poison the accumulation buffer permanently, so fall back to a
45
+ // cosine bounce rather than dividing by it.
46
+ If( brdfSamplePdf.greaterThan( 0.0 ).not(), () => {
262
47
 
263
- If( validInput.not(), () => {
264
-
265
- // Fallback to diffuse sampling
266
- const r1_fb = RandomValue( rngState ).toVar();
267
- const r2_fb = RandomValue( rngState ).toVar();
268
- const sampleRand = vec2( r1_fb, r2_fb );
48
+ const sampleRand = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 8 ) ), rngState, resolution, frame );
269
49
  r_direction.assign( cosineWeightedSample( N, sampleRand ) );
270
50
  r_throughput.assign( material.color.xyz );
271
- r_misWeight.assign( 1.0 );
272
- r_pdf.assign( 1.0 );
51
+ r_combinedPdf.assign( 1.0 );
273
52
 
274
53
  } ).Else( () => {
275
54
 
276
- // Use corrected sampling info
277
- const weights = SamplingStrategyWeights.wrap( computeSamplingInfo(
278
- samplingInfo,
279
- ).toVar() );
280
-
281
- const selectionRand = RandomValue( rngState ).toVar();
282
- const r1 = RandomValue( rngState ).toVar();
283
- const r2 = RandomValue( rngState ).toVar();
284
- const sampleRand = vec2( r1, r2 ).toVar();
285
-
286
- // Strategy selection
287
- const selectedStrategy = selectSamplingStrategy( weights, selectionRand ).toVar();
288
-
289
- const sampleDir = vec3( 0.0 ).toVar();
290
- const samplePdf = float( 0.0 ).toVar();
291
- const sampleBrdfValue = vec3( 0.0 ).toVar();
292
- // Transmission lobe uses this weight instead of a reflection-BRDF eval, which is
293
- // invalid for the below-surface refraction direction.
294
- const transColorWeight = vec3( 1.0 ).toVar();
295
-
296
- // Execute selected strategy (chained If/ElseIf/Else for exclusive branches)
297
- // Environment removed — handled via deterministic NEE in direct lighting
55
+ // generateSampledDirection IS the sampler — this used to re-select a strategy on top of
56
+ // it, and its strategies 1 and 4 both just returned brdfSampleDirection. So a sample that
57
+ // the inner selection had drawn from the diffuse or sheen lobe was reported to MIS as a
58
+ // specular-VNDF draw, making combinedPdf something no strategy actually sampled from.
59
+ // Since prevBouncePdf is that value, and the NEE sites evaluate a mixture, the two MIS
60
+ // strategies' weights stopped summing to 1.
61
+ //
62
+ // brdfSamplePdf is now the true mixture density (calculateBSDFSamplingPDF), so the
63
+ // estimator is just f·NoL/pdf and the MIS pairs share one function by construction.
64
+ const NoL = max( dot( N, brdfSampleDirection ), 0.0 ).toVar();
65
+ const pdf = max( brdfSamplePdf, MIN_PDF ).toVar();
66
+
67
+ r_direction.assign( brdfSampleDirection );
68
+ r_combinedPdf.assign( pdf );
69
+
70
+ // The reflection BRDF eval is invalid below the surface, so refraction carries the
71
+ // sampler's own tint (material.color × colorWeight), mirroring handleTransmission.
72
+ r_throughput.assign( select(
73
+ isTransmission,
74
+ material.color.xyz.mul( brdfSampleColorWeight ),
75
+ brdfSampleValue.mul( NoL ).div( pdf ),
76
+ ) );
298
77
 
299
- // Strategy 1: Specular
300
- If( selectedStrategy.equal( int( 1 ) ), () => {
301
-
302
- sampleDir.assign( brdfSampleDirection );
303
- samplePdf.assign( brdfSamplePdf );
304
- sampleBrdfValue.assign( brdfSampleValue );
305
-
306
- } ).ElseIf( selectedStrategy.equal( int( 2 ) ), () => {
307
-
308
- // Strategy 2: Diffuse
309
- sampleDir.assign( cosineWeightedSample( N, sampleRand ) );
310
- samplePdf.assign( cosineWeightedPDF( max( dot( N, sampleDir ), 0.0 ) ) );
311
- sampleBrdfValue.assign( evaluateMaterialResponse( V, sampleDir, N, material ) );
312
-
313
- } ).ElseIf( selectedStrategy.equal( int( 3 ) ), () => {
314
-
315
- // Strategy 3: Transmission
316
- const entering = dot( V, N ).greaterThan( 0.0 );
317
- const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
318
- V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState, float( 0.0 )
319
- ).toVar() );
320
- sampleDir.assign( mtResult.direction );
321
- samplePdf.assign( mtResult.pdf );
322
- // evaluateMaterialResponse is reflection-only and returns a non-physical value for
323
- // the below-surface refraction direction (its dots are floored to 0.001). Carry the
324
- // sampler's spectral tint and apply an energy-consistent transmission throughput
325
- // below (mirrors handleTransmission's material.color × colorWeight convention).
326
- transColorWeight.assign( mtResult.colorWeight );
327
-
328
- } ).Else( () => {
329
-
330
- // Strategy 4: Clearcoat (fallback)
331
- sampleDir.assign( brdfSampleDirection );
332
- samplePdf.assign( brdfSamplePdf );
333
- sampleBrdfValue.assign( brdfSampleValue );
334
-
335
- } );
336
-
337
- const NoL = max( dot( N, sampleDir ), 0.0 ).toVar();
338
-
339
- // Calculate combined PDF for MIS (material strategies only)
340
- const combinedPdf = float( 0.0 ).toVar();
341
-
342
- If( weights.useSpecular, () => {
343
-
344
- // Evaluate specular PDF at sampleDir (not brdfSampleDirection which may differ);
345
- // auto-selects iso/aniso to stay consistent with the BRDF sampler.
346
- const specPdfAtSampleDir = specularVNDFPDFForDir( material, N, V, sampleDir );
347
- combinedPdf.addAssign( weights.specularWeight.mul( specPdfAtSampleDir ) );
348
-
349
- } );
350
-
351
- If( weights.useDiffuse, () => {
352
-
353
- combinedPdf.addAssign( weights.diffuseWeight.mul( cosineWeightedPDF( NoL ) ) );
354
-
355
- } );
356
-
357
- If( weights.useTransmission.and( material.transmission.greaterThan( 0.0 ) ), () => {
358
-
359
- // Calculate transmission PDF for this direction
360
- const entering = dot( V, N ).greaterThan( 0.0 );
361
- combinedPdf.addAssign( weights.transmissionWeight.mul(
362
- calculateTransmissionPDF( V, sampleDir, N, material.ior, material.roughness, entering )
363
- ) );
364
-
365
- } );
366
-
367
- If( weights.useClearcoat.and( material.clearcoat.greaterThan( 0.0 ) ), () => {
368
-
369
- // Calculate clearcoat PDF for this direction
370
- combinedPdf.addAssign( weights.clearcoatWeight.mul(
371
- calculateClearcoatPDF( V, sampleDir, N, material.clearcoatRoughness )
372
- ) );
373
-
374
- } );
375
-
376
- // Ensure valid PDFs
377
- samplePdf.assign( max( samplePdf, MIN_PDF ) );
378
- combinedPdf.assign( max( combinedPdf, MIN_PDF ) );
379
-
380
- // MIS weight calculation
381
- const misWeight = samplePdf.div( combinedPdf ).toVar();
382
-
383
- // Reflection lobes: f·NoL·mis/pdf. Transmission lobe: the sampler's energy-consistent
384
- // weight (material tint × dispersion colorWeight × mis) — the reflection BRDF value is
385
- // invalid below the surface, so it is not used for transmission.
386
- const reflThroughput = sampleBrdfValue.mul( NoL ).mul( misWeight ).div( samplePdf );
387
- const transThroughput = material.color.xyz.mul( transColorWeight ).mul( misWeight );
388
-
389
- r_direction.assign( sampleDir );
390
- r_throughput.assign( select( selectedStrategy.equal( int( 3 ) ), transThroughput, reflThroughput ) );
391
- r_misWeight.assign( misWeight );
392
- r_pdf.assign( samplePdf );
393
- r_combinedPdf.assign( combinedPdf );
394
-
395
- } ); // End validInput check
78
+ } );
396
79
 
397
80
  return IndirectLightingResult( {
398
81
  direction: r_direction,
399
82
  throughput: r_throughput,
400
- misWeight: r_misWeight,
401
- pdf: r_pdf,
402
83
  combinedPdf: r_combinedPdf,
403
84
  } );
404
85