rayzee 7.20.2 → 7.20.3

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.
@@ -27,7 +27,7 @@ import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficient
27
27
  import { calculateIndirectLighting } from './LightsIndirect.js';
28
28
  import { IndirectLightingResult, sampleCone } from './LightsCore.js';
29
29
  import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
30
- import { getImportanceSamplingInfo, evaluateDFG } from './MaterialProperties.js';
30
+ import { evaluateDFG } from './MaterialProperties.js';
31
31
  import { dielectricF0 } from './Fresnel.js';
32
32
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
33
33
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
@@ -40,14 +40,13 @@ import {
40
40
  MaterialSamples,
41
41
  ExtMapResult,
42
42
  DirectionSample,
43
- ImportanceSamplingInfo,
44
43
  MaterialClassification,
45
44
  BRDFWeights,
46
45
  MaterialCache,
47
46
  DirectLightingDual,
48
47
  DFGResult,
49
48
  } from './Struct.js';
50
- import { RandomValue, getRandomSample } from './Random.js';
49
+ import { getRandomSample, getRandomSample1D, getRandomSample2D, SAMPLER_DIMS_PER_BOUNCE, SAMPLER_DIM_AUX_BASE } from './Random.js';
51
50
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
52
51
  import {
53
52
  readRayOrigin, readRayDirection, readRayBounceFlags, readRayThroughput, readRayPdf,
@@ -114,17 +113,17 @@ export function buildShadeKernel( params ) {
114
113
  // miss branch and the shadow catcher so their blur stays in lockstep (no horizon seam). normalize() the
115
114
  // center direction — ground projection can return a non-unit vector, which would skew sampleCone's basis.
116
115
  // Clamp the tap count to ≥1 so a 0 forced via the engine API can't produce a 0/0 NaN backdrop.
117
- const sampleEnvBlurred = ( centerDir, halfAngle, samples, rng ) => {
116
+ const sampleEnvBlurred = ( centerDir, halfAngle, samples, rng, pixelCoord ) => {
118
117
 
119
118
  const axis = normalize( centerDir ).toVar();
120
119
  const n = max( samples, int( 1 ) ).toVar();
121
120
  const acc = vec3( 0.0 ).toVar();
122
- Loop( { start: int( 0 ), end: n, type: 'int', condition: '<' }, () => {
121
+ Loop( { start: int( 0 ), end: n, type: 'int', condition: '<' }, ( { i } ) => {
123
122
 
124
- // per-component .toVar(): vec2(RandomValue, RandomValue) would collapse to u==v (TSL pitfall)
125
- const u1 = RandomValue( rng ).toVar();
126
- const u2 = RandomValue( rng ).toVar();
127
- const jDir = sampleCone( axis, halfAngle, vec2( u1, u2 ) ).toVar();
123
+ // Tap count varies per pixel, so these cannot sit in a per-bounce block without
124
+ // running over into the next bounce's dimensions — they get the AUX range instead.
125
+ const jXi = getRandomSample2D( pixelCoord, int( 0 ), int( SAMPLER_DIM_AUX_BASE ).add( i ), rng, resolution, frame ).toVar();
126
+ const jDir = sampleCone( axis, halfAngle, jXi ).toVar();
128
127
  acc.addAssign( sampleEnvironment( {
129
128
  tex: envTexture,
130
129
  samp: sampler( envTexture ),
@@ -189,6 +188,19 @@ export function buildShadeKernel( params ) {
189
188
  const pixelIndex = rayID;
190
189
  const rngState = rngBufferRW.element( rayID ).toVar();
191
190
 
191
+ // STBN keyed on (GLOBAL pixel, dimension, frame). pixelIndex is the LOCAL path slot; the global pixel
192
+ // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
193
+ // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
194
+ // Hoisted here rather than at the BSDF draw because the ground catcher runs NEE far earlier.
195
+ const _resX = int( resolution.x ).toVar();
196
+ const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
197
+ const _pixelCoord = vec2(
198
+ float( _globalPixel.mod( _resX ) ).add( 0.5 ),
199
+ float( _globalPixel.div( _resX ) ).add( 0.5 ),
200
+ );
201
+ // This bounce's block of sampler dimensions — see the budget table in Random.js.
202
+ const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
203
+
192
204
  // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
193
205
  // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
194
206
  // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
@@ -229,11 +241,9 @@ export function buildShadeKernel( params ) {
229
241
  const planeMat = RayTracingMaterial.wrap( diffuseGroundMaterial() ).toVar();
230
242
 
231
243
  // Cosine-weighted hemisphere BRDF sample about the plane normal (0,1,0); the helper
232
- // puts cosθ along N, so bDir.y == cosθ. Per-component .toVar() on the two randoms
233
- // (vec2(RandomValue,RandomValue) would collapse to u==v — TSL pitfall).
234
- const u1 = RandomValue( rngState ).toVar();
235
- const u2 = RandomValue( rngState ).toVar();
236
- const bDir = cosineWeightedSample( planeN, vec2( u1, u2 ) ).toVar();
244
+ // puts cosθ along N, so bDir.y == cosθ.
245
+ const bXi = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 7 ) ), rngState, resolution, frame ).toVar();
246
+ const bDir = cosineWeightedSample( planeN, bXi ).toVar();
237
247
  const bPdf = bDir.y.mul( PI_INV ).toVar(); // cosθ / π
238
248
  const bVal = vec3( PI_INV ); // albedo(1) / π
239
249
 
@@ -243,6 +253,7 @@ export function buildShadeKernel( params ) {
243
253
  planePoint, planeN, planeMat, planeV,
244
254
  bDir, bPdf, bVal,
245
255
  bounceIndex, rngState,
256
+ _pixelCoord, resolution, frame, dimBase,
246
257
  directionalLightsBuffer, numDirectionalLights,
247
258
  areaLightsBuffer, numAreaLights,
248
259
  pointLightsBuffer, numPointLights,
@@ -289,7 +300,7 @@ export function buildShadeKernel( params ) {
289
300
 
290
301
  If( backgroundBlurriness.greaterThan( 0.0 ), () => {
291
302
 
292
- envBehind.assign( sampleEnvBlurred( catcherEnvDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
303
+ envBehind.assign( sampleEnvBlurred( catcherEnvDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState, _pixelCoord ) );
293
304
 
294
305
  } ).Else( () => {
295
306
 
@@ -373,7 +384,7 @@ export function buildShadeKernel( params ) {
373
384
  const envColor = vec3( 0.0 ).toVar();
374
385
  If( isBackdropView.and( backgroundBlurriness.greaterThan( 0.0 ) ), () => {
375
386
 
376
- envColor.assign( sampleEnvBlurred( envDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
387
+ envColor.assign( sampleEnvBlurred( envDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState, _pixelCoord ) );
377
388
 
378
389
  } ).Else( () => {
379
390
 
@@ -519,13 +530,14 @@ export function buildShadeKernel( params ) {
519
530
  const mSigmaT = mSigmaA.add( mSigmaS );
520
531
  const coll = CollisionSample.wrap( sampleChromaticCollision(
521
532
  mSigmaT, mSigmaS, throughput, hitDist, rngState,
533
+ _pixelCoord, resolution, frame, dimBase,
522
534
  ) ).toVar();
523
535
  throughput.mulAssign( coll.weight );
524
536
 
525
537
  If( coll.didScatter, () => {
526
538
 
527
539
  // scatter via Henyey-Greenstein, continue as a free bounce off the sssSteps budget
528
- const xi2 = vec2( RandomValue( rngState ), RandomValue( rngState ) );
540
+ const xi2 = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 6 ) ), rngState, resolution, frame );
529
541
  const scatterPoint = origin.add( direction.mul( coll.t ) );
530
542
  const newDir = sampleHenyeyGreenstein( direction, mG, xi2 ).toVar();
531
543
  sssSteps.addAssign( 1 );
@@ -533,7 +545,7 @@ export function buildShadeKernel( params ) {
533
545
  // terminate walk: step cap or Russian roulette
534
546
  const rrP = clamp( max( max( throughput.x, throughput.y ), throughput.z ), 0.02, 1.0 ).toVar();
535
547
  const terminate = sssSteps.greaterThanEqual( maxSubsurfaceSteps )
536
- .or( RandomValue( rngState ).greaterThan( rrP ) ).toVar();
548
+ .or( getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 12 ) ), rngState, resolution, frame ).greaterThan( rrP ) ).toVar();
537
549
 
538
550
  If( terminate, () => {
539
551
 
@@ -728,6 +740,7 @@ export function buildShadeKernel( params ) {
728
740
  const currentRay = Ray( { origin, direction } );
729
741
  const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
730
742
  currentRay, N, material, rngState,
743
+ _pixelCoord, resolution, frame, dimBase,
731
744
  int( transTraversals ),
732
745
  currentMediumIOR, previousMediumIOR,
733
746
  pathWavelength,
@@ -1047,32 +1060,24 @@ export function buildShadeKernel( params ) {
1047
1060
  material.clearcoat, material.emissive, material.subsurface,
1048
1061
  ) ).toVar();
1049
1062
 
1050
- // STBN keyed on (GLOBAL pixel, bounceIndex, frame). pixelIndex is the LOCAL path slot; the global pixel
1051
- // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
1052
- // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
1053
- const _resX = int( resolution.x ).toVar();
1054
- const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
1055
- const _pixelCoord = vec2(
1056
- float( _globalPixel.mod( _resX ) ).add( 0.5 ),
1057
- float( _globalPixel.div( _resX ) ).add( 0.5 ),
1058
- );
1059
1063
  const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
1064
+ const lobeXi = getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
1060
1065
  const emptyWeights = BRDFWeights( {
1061
1066
  specular: float( 0.0 ), diffuse: float( 0.0 ), sheen: float( 0.0 ),
1062
1067
  clearcoat: float( 0.0 ), transmission: float( 0.0 ), iridescence: float( 0.0 ),
1063
1068
  } );
1064
- // unused (materialCacheCached=false), but must match the 11-field struct shape to construct
1069
+
1070
+ // Unused (weightsComputed / materialCacheCached are false) but must satisfy the signature.
1065
1071
  const emptyCache = MaterialCache( {
1066
- F0: vec3( 0.04 ), NoV: float( 1.0 ),
1067
- diffuseColor: vec3( 0.0 ), isPurelyDiffuse: false,
1068
- alpha: float( 0.0 ), k: float( 0.0 ), alpha2: float( 0.0 ),
1069
- invRoughness: float( 0.5 ), metalFactor: float( 0.5 ),
1072
+ invRoughness: float( 0.0 ), metalFactor: float( 0.5 ),
1070
1073
  iorFactor: float( 0.67 ), maxSheenColor: float( 0.0 ),
1071
1074
  } );
1072
1075
 
1073
1076
  const brdfDir = vec3( 0.0 ).toVar();
1074
1077
  const brdfValue = vec3( 0.0 ).toVar();
1075
1078
  const brdfPdf = float( 0.0 ).toVar();
1079
+ const brdfIsTransmission = tslBool( false ).toVar();
1080
+ const brdfColorWeight = vec3( 1.0 ).toVar();
1076
1081
 
1077
1082
  If( material.clearcoat.greaterThan( 0.0 ), () => {
1078
1083
 
@@ -1084,6 +1089,7 @@ export function buildShadeKernel( params ) {
1084
1089
  } );
1085
1090
  const ccResult = ClearcoatResult.wrap( sampleClearcoat(
1086
1091
  ccRay, ccHit, material, xi, rngState,
1092
+ _pixelCoord, resolution, frame, dimBase,
1087
1093
  ) );
1088
1094
  brdfDir.assign( ccResult.L );
1089
1095
  brdfValue.assign( ccResult.brdf );
@@ -1092,7 +1098,8 @@ export function buildShadeKernel( params ) {
1092
1098
  } ).Else( () => {
1093
1099
 
1094
1100
  const bs = DirectionSample.wrap( generateSampledDirection(
1095
- V, N, material, xi, rngState,
1101
+ V, N, material, xi, lobeXi, rngState,
1102
+ _pixelCoord, resolution, frame, dimBase,
1096
1103
  mc,
1097
1104
  false, emptyWeights,
1098
1105
  false, emptyCache,
@@ -1100,6 +1107,8 @@ export function buildShadeKernel( params ) {
1100
1107
  brdfDir.assign( bs.direction );
1101
1108
  brdfValue.assign( bs.value );
1102
1109
  brdfPdf.assign( bs.pdf );
1110
+ brdfIsTransmission.assign( bs.isTransmission );
1111
+ brdfColorWeight.assign( bs.colorWeight );
1103
1112
 
1104
1113
  } );
1105
1114
 
@@ -1107,6 +1116,7 @@ export function buildShadeKernel( params ) {
1107
1116
  hitPoint, N, material, V,
1108
1117
  brdfDir, brdfPdf, brdfValue,
1109
1118
  bounceIndex, rngState,
1119
+ _pixelCoord, resolution, frame, dimBase,
1110
1120
  directionalLightsBuffer, numDirectionalLights,
1111
1121
  areaLightsBuffer, numAreaLights,
1112
1122
  pointLightsBuffer, numPointLights,
@@ -1156,6 +1166,7 @@ export function buildShadeKernel( params ) {
1156
1166
  const emissiveSample = EmissiveSample.wrap( sampleLightBVHTriangle(
1157
1167
  hitPoint, N,
1158
1168
  rngState,
1169
+ _pixelCoord, resolution, frame, dimBase,
1159
1170
  lightBuffer,
1160
1171
  lightBuffer,
1161
1172
  emissiveVec4Offset,
@@ -1216,6 +1227,7 @@ export function buildShadeKernel( params ) {
1216
1227
  const emissiveLight = calculateEmissiveTriangleContribution(
1217
1228
  hitPoint, N, V, material,
1218
1229
  bounceIndex, rngState,
1230
+ _pixelCoord, resolution, frame, dimBase,
1219
1231
  emissiveBoost,
1220
1232
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1221
1233
  triangleBuffer,
@@ -1244,14 +1256,12 @@ export function buildShadeKernel( params ) {
1244
1256
  // per-term (env / emissive-hit / direct-light / emissive-NEE), matching the megakernel which never
1245
1257
  // re-suppresses the running radiance.
1246
1258
 
1247
- const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
1248
- material, bounceIndex, mc,
1249
- ) ).toVar();
1250
-
1251
1259
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1252
- V, N, material,
1260
+ N, material,
1253
1261
  brdfDir, brdfPdf, brdfValue,
1254
- rngState, samplingInfo,
1262
+ brdfIsTransmission, brdfColorWeight,
1263
+ rngState,
1264
+ _pixelCoord, resolution, frame, dimBase,
1255
1265
  ) ).toVar();
1256
1266
 
1257
1267
  const bounceDir = indirectResult.direction.toVar();
@@ -1265,6 +1275,7 @@ export function buildShadeKernel( params ) {
1265
1275
  // terminate. Subsumes the old compensated low-throughput kill (#12). Unbiased; just terminates smarter.
1266
1276
  const rrSurvival = handleRussianRoulette(
1267
1277
  bounceIndex, throughput, mc, bounceDir, rngState,
1278
+ _pixelCoord, resolution, frame, dimBase,
1268
1279
  enableEnvironmentLight, useEnvMapIS,
1269
1280
  ).toVar();
1270
1281
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
package/src/TSL/Struct.js CHANGED
@@ -151,6 +151,11 @@ export const DirectionSample = struct( {
151
151
  direction: 'vec3',
152
152
  value: 'vec3',
153
153
  pdf: 'float',
154
+ // Refraction went below the surface, so `value` (a reflection-only BRDF eval) is not usable
155
+ // and the consumer must take the transmission throughput path instead.
156
+ isTransmission: 'bool',
157
+ // Spectral tint from the transmission sampler; vec3(1) for every reflection lobe.
158
+ colorWeight: 'vec3',
154
159
  } );
155
160
 
156
161
  export const BRDFWeights = struct( {
@@ -162,12 +167,6 @@ export const BRDFWeights = struct( {
162
167
  iridescence: 'float',
163
168
  } );
164
169
 
165
- export const ImportanceSamplingInfo = struct( {
166
- diffuseImportance: 'float',
167
- specularImportance: 'float',
168
- transmissionImportance: 'float',
169
- clearcoatImportance: 'float',
170
- } );
171
170
 
172
171
  // Anisotropy tangent frame in world space (rotated ONB). Shared by the sampler and
173
172
  // the eval/PDF so both derive the identical frame — see anisoTangentFrame().
@@ -234,35 +233,14 @@ export const UVCache = struct( {
234
233
  // Fields are split into two groups:
235
234
  // 1. BRDF evaluation: F0, NoV, diffuseColor, isPurelyDiffuse, alpha, k, alpha2
236
235
  // 2. BRDF weight calc: invRoughness, metalFactor, iorFactor, maxSheenColor
236
+ // Shared inputs to calculateBRDFWeights, which is the only reader.
237
237
  export const MaterialCache = struct( {
238
- F0: 'vec3', // Base reflectance
239
- NoV: 'float', // Normal dot View
240
- diffuseColor: 'vec3', // Precomputed diffuse color (only for isPurelyDiffuse fast path)
241
- isPurelyDiffuse: 'bool', // Optimized path flag
242
- alpha: 'float', // roughness squared
243
- k: 'float', // Geometry term constant
244
- alpha2: 'float', // roughness to the fourth power
245
- // BRDF weight calculation: precomputed shared values
246
238
  invRoughness: 'float', // 1.0 - roughness
247
239
  metalFactor: 'float', // 0.5 + 0.5 * metalness
248
240
  iorFactor: 'float', // min(2.0 / ior, 1.0)
249
241
  maxSheenColor: 'float', // max component of sheen color
250
242
  } );
251
243
 
252
- export const SamplingStrategyWeights = struct( {
253
- envWeight: 'float',
254
- specularWeight: 'float',
255
- diffuseWeight: 'float',
256
- transmissionWeight: 'float',
257
- clearcoatWeight: 'float',
258
- totalWeight: 'float',
259
-
260
- useEnv: 'bool',
261
- useSpecular: 'bool',
262
- useDiffuse: 'bool',
263
- useTransmission: 'bool',
264
- useClearcoat: 'bool',
265
- } );
266
244
 
267
245
  // IMPROVEMENT: Dynamic MIS strategy based on material properties
268
246
  export const MISStrategy = struct( {
@@ -9,7 +9,7 @@
9
9
  import {
10
10
  Fn,
11
11
  float,
12
- vec2,
12
+ int,
13
13
  vec3,
14
14
  If,
15
15
  select,
@@ -29,7 +29,7 @@ import {
29
29
 
30
30
  import { struct } from './patches.js';
31
31
  import { TWO_PI, EPSILON, constructTBN } from './Common.js';
32
- import { RandomValue } from './Random.js';
32
+ import { getRandomSample1D, getRandomSample2D } from './Random.js';
33
33
  import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
34
34
  import { ImportanceSampleGGX } from './MaterialSampling.js';
35
35
 
@@ -95,13 +95,16 @@ export const sampleHenyeyGreenstein = Fn( ( [ wi, g, xi ] ) => {
95
95
  // Per-channel sigma_t can't be represented by one scalar distance. Pick a channel
96
96
  // ∝ throughput, sample t against it, and weight by the balance-heuristic combined pdf
97
97
  // p̄ = Σ pmf_c·p_c — the shared scalar p̄ is what suppresses color fireflies.
98
- export const sampleChromaticCollision = Fn( ( [ sigmaT, sigmaS, beta, surfaceDist, rngState ] ) => {
98
+ export const sampleChromaticCollision = Fn( ( [
99
+ sigmaT, sigmaS, beta, surfaceDist, rngState,
100
+ pixelCoord, resolution, frame, dimBase,
101
+ ] ) => {
99
102
 
100
103
  const w = max( beta, vec3( 1e-4 ) ); // floor so no channel goes unsampled
101
104
  const pmf = w.div( w.x.add( w.y ).add( w.z ) ).toVar();
102
105
 
103
106
  // .toVar() pins the single RNG draw (else it re-executes per comparison → state drift).
104
- const u = RandomValue( rngState ).toVar();
107
+ const u = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 10 ) ), rngState, resolution, frame ).toVar();
105
108
  const cSigmaT = float( 0.0 ).toVar();
106
109
  If( u.lessThan( pmf.x ), () => {
107
110
 
@@ -117,7 +120,7 @@ export const sampleChromaticCollision = Fn( ( [ sigmaT, sigmaS, beta, surfaceDis
117
120
 
118
121
  } );
119
122
 
120
- const xi = RandomValue( rngState ).toVar();
123
+ const xi = getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 9 ) ), rngState, resolution, frame ).toVar();
121
124
  const t = log( max( float( 1.0 ).sub( xi ), 1e-6 ) ).negate().div( max( cSigmaT, 1e-6 ) ).toVar();
122
125
 
123
126
  const didScatter = t.lessThan( surfaceDist ).toVar();
@@ -167,7 +170,9 @@ export const subsurfaceCoefficients = Fn( ( [ subsurfaceColor, subsurfaceRadius,
167
170
  // Dielectric interface driven by material.ior: reflect (Fresnel/TIR) or refract across
168
171
  // the boundary. No color tint — the scattering color lives in sigma_s.
169
172
  export const handleSubsurfaceEntry = Fn( ( [
170
- rayDir, normal, material, entering, rngState, currentMediumIOR, previousMediumIOR,
173
+ rayDir, normal, material, entering, rngState,
174
+ pixelCoord, resolution, frame, dimBase,
175
+ currentMediumIOR, previousMediumIOR,
171
176
  ] ) => {
172
177
 
173
178
  const result = SubsurfaceEntryResult( {
@@ -188,13 +193,13 @@ export const handleSubsurfaceEntry = Fn( ( [
188
193
  const Fr = select( tir, float( 1.0 ), fresnelSchlickFloat( cosThetaI, F0 ) ).toVar();
189
194
  const reflectProb = clamp( Fr, 0.02, 0.98 ).toVar();
190
195
 
191
- const doReflect = tir.or( RandomValue( rngState ).lessThan( reflectProb ) ).toVar();
196
+ const doReflect = tir.or( getRandomSample1D( pixelCoord, int( 0 ), dimBase.add( int( 11 ) ), rngState, resolution, frame ).lessThan( reflectProb ) ).toVar();
192
197
  result.didReflect.assign( doReflect );
193
198
 
194
199
  If( doReflect, () => {
195
200
 
196
201
  // GGX-sampled reflection: a perfect mirror here makes SSS surfaces read as polished ceramic.
197
- const xiR = vec2( RandomValue( rngState ), RandomValue( rngState ) );
202
+ const xiR = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 5 ) ), rngState, resolution, frame );
198
203
  const H = ImportanceSampleGGX( { N, roughness: material.roughness, Xi: xiR } );
199
204
  const reflDir = reflect( rayDir, H ).toVar();
200
205
  If( dot( reflDir, N ).lessThanEqual( 0.0 ), () => {