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.
@@ -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,20 +40,20 @@ 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,
54
53
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
55
54
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
56
55
  readTransparentCount,
56
+ readMisRayT,
57
57
  readHitDistance, readHitBarycentrics, readHitNormal,
58
58
  readHitMaterialIndex, readHitTriangleIndex,
59
59
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
@@ -80,7 +80,7 @@ export function buildShadeKernel( params ) {
80
80
  counters,
81
81
  activeIndicesRO,
82
82
  envTexture, environmentIntensity, envMatrix,
83
- enableEnvironmentLight, useEnvMapIS,
83
+ enableEnvironmentLight,
84
84
  groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
85
85
  enableGroundCatcher, groundCatcherHeight,
86
86
  envTotalSum, envCompensationDelta, envResolution,
@@ -95,6 +95,10 @@ export function buildShadeKernel( params ) {
95
95
  globalIlluminationIntensity,
96
96
  cameraProjectionMatrix, cameraViewMatrix,
97
97
  fireflyThreshold, frame, resolution,
98
+ // Accumulation index, NOT the free-running seed counter `frame` rides. The firefly clamp
99
+ // relaxes as sqrt(frame+1) so its bias decays as a render converges; keying that off the
100
+ // seed axis let it grow for the whole session and disabled suppression outright.
101
+ accumFrame,
98
102
  chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
99
103
  emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
100
104
  emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
@@ -114,17 +118,17 @@ export function buildShadeKernel( params ) {
114
118
  // miss branch and the shadow catcher so their blur stays in lockstep (no horizon seam). normalize() the
115
119
  // center direction — ground projection can return a non-unit vector, which would skew sampleCone's basis.
116
120
  // 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 ) => {
121
+ const sampleEnvBlurred = ( centerDir, halfAngle, samples, rng, pixelCoord ) => {
118
122
 
119
123
  const axis = normalize( centerDir ).toVar();
120
124
  const n = max( samples, int( 1 ) ).toVar();
121
125
  const acc = vec3( 0.0 ).toVar();
122
- Loop( { start: int( 0 ), end: n, type: 'int', condition: '<' }, () => {
126
+ Loop( { start: int( 0 ), end: n, type: 'int', condition: '<' }, ( { i } ) => {
123
127
 
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();
128
+ // Tap count varies per pixel, so these cannot sit in a per-bounce block without
129
+ // running over into the next bounce's dimensions — they get the AUX range instead.
130
+ const jXi = getRandomSample2D( pixelCoord, int( 0 ), int( SAMPLER_DIM_AUX_BASE ).add( i ), rng, resolution, frame ).toVar();
131
+ const jDir = sampleCone( axis, halfAngle, jXi ).toVar();
128
132
  acc.addAssign( sampleEnvironment( {
129
133
  tex: envTexture,
130
134
  samp: sampler( envTexture ),
@@ -189,6 +193,19 @@ export function buildShadeKernel( params ) {
189
193
  const pixelIndex = rayID;
190
194
  const rngState = rngBufferRW.element( rayID ).toVar();
191
195
 
196
+ // STBN keyed on (GLOBAL pixel, dimension, frame). pixelIndex is the LOCAL path slot; the global pixel
197
+ // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
198
+ // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
199
+ // Hoisted here rather than at the BSDF draw because the ground catcher runs NEE far earlier.
200
+ const _resX = int( resolution.x ).toVar();
201
+ const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
202
+ const _pixelCoord = vec2(
203
+ float( _globalPixel.mod( _resX ) ).add( 0.5 ),
204
+ float( _globalPixel.div( _resX ) ).add( 0.5 ),
205
+ );
206
+ // This bounce's block of sampler dimensions — see the budget table in Random.js.
207
+ const dimBase = int( currentBounce ).mul( int( SAMPLER_DIMS_PER_BOUNCE ) );
208
+
192
209
  // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
193
210
  // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
194
211
  // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
@@ -229,11 +246,9 @@ export function buildShadeKernel( params ) {
229
246
  const planeMat = RayTracingMaterial.wrap( diffuseGroundMaterial() ).toVar();
230
247
 
231
248
  // 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();
249
+ // puts cosθ along N, so bDir.y == cosθ.
250
+ const bXi = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 7 ) ), rngState, resolution, frame ).toVar();
251
+ const bDir = cosineWeightedSample( planeN, bXi ).toVar();
237
252
  const bPdf = bDir.y.mul( PI_INV ).toVar(); // cosθ / π
238
253
  const bVal = vec3( PI_INV ); // albedo(1) / π
239
254
 
@@ -243,6 +258,7 @@ export function buildShadeKernel( params ) {
243
258
  planePoint, planeN, planeMat, planeV,
244
259
  bDir, bPdf, bVal,
245
260
  bounceIndex, rngState,
261
+ _pixelCoord, resolution, frame, dimBase,
246
262
  directionalLightsBuffer, numDirectionalLights,
247
263
  areaLightsBuffer, numAreaLights,
248
264
  pointLightsBuffer, numPointLights,
@@ -289,7 +305,7 @@ export function buildShadeKernel( params ) {
289
305
 
290
306
  If( backgroundBlurriness.greaterThan( 0.0 ), () => {
291
307
 
292
- envBehind.assign( sampleEnvBlurred( catcherEnvDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
308
+ envBehind.assign( sampleEnvBlurred( catcherEnvDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState, _pixelCoord ) );
293
309
 
294
310
  } ).Else( () => {
295
311
 
@@ -373,7 +389,7 @@ export function buildShadeKernel( params ) {
373
389
  const envColor = vec3( 0.0 ).toVar();
374
390
  If( isBackdropView.and( backgroundBlurriness.greaterThan( 0.0 ) ), () => {
375
391
 
376
- envColor.assign( sampleEnvBlurred( envDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
392
+ envColor.assign( sampleEnvBlurred( envDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState, _pixelCoord ) );
377
393
 
378
394
  } ).Else( () => {
379
395
 
@@ -394,7 +410,7 @@ export function buildShadeKernel( params ) {
394
410
 
395
411
  // MIS weight for implicit env hit — prevents double-counting with NEE
396
412
  const envMisWeight = float( 1.0 ).toVar();
397
- If( isBackdropView.not().and( useEnvMapIS ), () => {
413
+ If( isBackdropView.not(), () => {
398
414
 
399
415
  const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
400
416
  If( prevBouncePdf.greaterThan( 0.0 ), () => {
@@ -429,7 +445,7 @@ export function buildShadeKernel( params ) {
429
445
  currentRadiance.xyz.add(
430
446
  regularizePathContribution(
431
447
  throughput.mul( envColor ).mul( envScale ),
432
- fireflyPathLen, fireflyThreshold, int( frame ),
448
+ fireflyPathLen, fireflyThreshold, int( accumFrame ),
433
449
  ),
434
450
  ),
435
451
  currentRadiance.w
@@ -519,13 +535,14 @@ export function buildShadeKernel( params ) {
519
535
  const mSigmaT = mSigmaA.add( mSigmaS );
520
536
  const coll = CollisionSample.wrap( sampleChromaticCollision(
521
537
  mSigmaT, mSigmaS, throughput, hitDist, rngState,
538
+ _pixelCoord, resolution, frame, dimBase,
522
539
  ) ).toVar();
523
540
  throughput.mulAssign( coll.weight );
524
541
 
525
542
  If( coll.didScatter, () => {
526
543
 
527
544
  // scatter via Henyey-Greenstein, continue as a free bounce off the sssSteps budget
528
- const xi2 = vec2( RandomValue( rngState ), RandomValue( rngState ) );
545
+ const xi2 = getRandomSample2D( _pixelCoord, int( 0 ), dimBase.add( int( 6 ) ), rngState, resolution, frame );
529
546
  const scatterPoint = origin.add( direction.mul( coll.t ) );
530
547
  const newDir = sampleHenyeyGreenstein( direction, mG, xi2 ).toVar();
531
548
  sssSteps.addAssign( 1 );
@@ -533,7 +550,7 @@ export function buildShadeKernel( params ) {
533
550
  // terminate walk: step cap or Russian roulette
534
551
  const rrP = clamp( max( max( throughput.x, throughput.y ), throughput.z ), 0.02, 1.0 ).toVar();
535
552
  const terminate = sssSteps.greaterThanEqual( maxSubsurfaceSteps )
536
- .or( RandomValue( rngState ).greaterThan( rrP ) ).toVar();
553
+ .or( getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 12 ) ), rngState, resolution, frame ).greaterThan( rrP ) ).toVar();
537
554
 
538
555
  If( terminate, () => {
539
556
 
@@ -728,6 +745,7 @@ export function buildShadeKernel( params ) {
728
745
  const currentRay = Ray( { origin, direction } );
729
746
  const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
730
747
  currentRay, N, material, rngState,
748
+ _pixelCoord, resolution, frame, dimBase,
731
749
  int( transTraversals ),
732
750
  currentMediumIOR, previousMediumIOR,
733
751
  pathWavelength,
@@ -914,7 +932,10 @@ export function buildShadeKernel( params ) {
914
932
  } );
915
933
 
916
934
  } );
917
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount );
935
+ // Alpha skip keeps the direction, so the MIS origin is recoverable: accumulate the segment.
936
+ // Any redirection (refraction/reflection/SSS) breaks the line — reset.
937
+ const misRayT = select( interaction.isAlphaSkip, readMisRayT( rayBufferRW, rayID ).add( hitDist ), float( 0.0 ) );
938
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount, misRayT );
918
939
  writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
919
940
  // Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
920
941
  // transmission/alpha-skip/SSS-boundary). Writing 1.0 corrupts the next bounce's env/emissive MIS,
@@ -967,18 +988,24 @@ export function buildShadeKernel( params ) {
967
988
  // MIS partner pdf MUST match the actual NEE sampler: re-walk the Light BVH descent
968
989
  // when it is active, else use the flat-CDF pdf. Mismatching them breaks MIS
969
990
  // partition-of-unity → a real bias (see calculateLightBVHPdf).
991
+ // Evaluate it at the scatter point that produced prevBouncePdf — alpha skips moved
992
+ // the ray origin, and the skip-point pdf under-counts dist² so full-Le BSDF hits
993
+ // pass nearly unweighted (fireflies on emitters inside alpha-blend shades).
994
+ const misT = readMisRayT( rayBufferRW, rayID ).toVar();
995
+ const misOrigin = origin.sub( direction.mul( misT ) ).toVar();
996
+ const misDist = hitDist.add( misT ).toVar();
970
997
  const lightPdf = float( 0.0 ).toVar();
971
998
  If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
972
999
 
973
1000
  lightPdf.assign( calculateLightBVHPdf(
974
- int( hitTriIdx ), hitDist, direction, origin,
1001
+ int( hitTriIdx ), misDist, direction, misOrigin,
975
1002
  lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
976
1003
  ) );
977
1004
 
978
1005
  } ).Else( () => {
979
1006
 
980
1007
  lightPdf.assign( calculateEmissiveLightPdf(
981
- int( hitTriIdx ), hitDist, direction, origin,
1008
+ int( hitTriIdx ), misDist, direction, misOrigin,
982
1009
  triangleBuffer, materialBuffer, emissiveTotalPower,
983
1010
  ) );
984
1011
 
@@ -995,7 +1022,7 @@ export function buildShadeKernel( params ) {
995
1022
  currentRadiance.xyz.add(
996
1023
  regularizePathContribution(
997
1024
  emissive.mul( throughput ).mul( emissiveGiScale ).mul( emissiveMISWeight ),
998
- float( bounceIndex ), fireflyThreshold, int( frame ),
1025
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
999
1026
  ),
1000
1027
  ),
1001
1028
  currentRadiance.w
@@ -1047,32 +1074,24 @@ export function buildShadeKernel( params ) {
1047
1074
  material.clearcoat, material.emissive, material.subsurface,
1048
1075
  ) ).toVar();
1049
1076
 
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
1077
  const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
1078
+ const lobeXi = getRandomSample1D( _pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
1060
1079
  const emptyWeights = BRDFWeights( {
1061
1080
  specular: float( 0.0 ), diffuse: float( 0.0 ), sheen: float( 0.0 ),
1062
1081
  clearcoat: float( 0.0 ), transmission: float( 0.0 ), iridescence: float( 0.0 ),
1063
1082
  } );
1064
- // unused (materialCacheCached=false), but must match the 11-field struct shape to construct
1083
+
1084
+ // Unused (weightsComputed / materialCacheCached are false) but must satisfy the signature.
1065
1085
  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 ),
1086
+ invRoughness: float( 0.0 ), metalFactor: float( 0.5 ),
1070
1087
  iorFactor: float( 0.67 ), maxSheenColor: float( 0.0 ),
1071
1088
  } );
1072
1089
 
1073
1090
  const brdfDir = vec3( 0.0 ).toVar();
1074
1091
  const brdfValue = vec3( 0.0 ).toVar();
1075
1092
  const brdfPdf = float( 0.0 ).toVar();
1093
+ const brdfIsTransmission = tslBool( false ).toVar();
1094
+ const brdfColorWeight = vec3( 1.0 ).toVar();
1076
1095
 
1077
1096
  If( material.clearcoat.greaterThan( 0.0 ), () => {
1078
1097
 
@@ -1084,6 +1103,7 @@ export function buildShadeKernel( params ) {
1084
1103
  } );
1085
1104
  const ccResult = ClearcoatResult.wrap( sampleClearcoat(
1086
1105
  ccRay, ccHit, material, xi, rngState,
1106
+ _pixelCoord, resolution, frame, dimBase,
1087
1107
  ) );
1088
1108
  brdfDir.assign( ccResult.L );
1089
1109
  brdfValue.assign( ccResult.brdf );
@@ -1092,7 +1112,8 @@ export function buildShadeKernel( params ) {
1092
1112
  } ).Else( () => {
1093
1113
 
1094
1114
  const bs = DirectionSample.wrap( generateSampledDirection(
1095
- V, N, material, xi, rngState,
1115
+ V, N, material, xi, lobeXi, rngState,
1116
+ _pixelCoord, resolution, frame, dimBase,
1096
1117
  mc,
1097
1118
  false, emptyWeights,
1098
1119
  false, emptyCache,
@@ -1100,6 +1121,8 @@ export function buildShadeKernel( params ) {
1100
1121
  brdfDir.assign( bs.direction );
1101
1122
  brdfValue.assign( bs.value );
1102
1123
  brdfPdf.assign( bs.pdf );
1124
+ brdfIsTransmission.assign( bs.isTransmission );
1125
+ brdfColorWeight.assign( bs.colorWeight );
1103
1126
 
1104
1127
  } );
1105
1128
 
@@ -1107,6 +1130,7 @@ export function buildShadeKernel( params ) {
1107
1130
  hitPoint, N, material, V,
1108
1131
  brdfDir, brdfPdf, brdfValue,
1109
1132
  bounceIndex, rngState,
1133
+ _pixelCoord, resolution, frame, dimBase,
1110
1134
  directionalLightsBuffer, numDirectionalLights,
1111
1135
  areaLightsBuffer, numAreaLights,
1112
1136
  pointLightsBuffer, numPointLights,
@@ -1127,7 +1151,7 @@ export function buildShadeKernel( params ) {
1127
1151
  currentRadiance.xyz.add(
1128
1152
  regularizePathContribution(
1129
1153
  throughput.mul( directLight ).mul( giScale ),
1130
- float( bounceIndex ), fireflyThreshold, int( frame ),
1154
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1131
1155
  ),
1132
1156
  ),
1133
1157
  currentRadiance.w
@@ -1156,18 +1180,16 @@ export function buildShadeKernel( params ) {
1156
1180
  const emissiveSample = EmissiveSample.wrap( sampleLightBVHTriangle(
1157
1181
  hitPoint, N,
1158
1182
  rngState,
1183
+ _pixelCoord, resolution, frame, dimBase,
1159
1184
  lightBuffer,
1160
1185
  lightBuffer,
1161
1186
  emissiveVec4Offset,
1162
1187
  triangleBuffer,
1163
1188
  ) );
1164
1189
 
1165
- // skip rough diffuse surfaces on secondary bounces
1166
- const skip = bounceIndex.greaterThan( int( 1 ) )
1167
- .and( material.roughness.greaterThan( 0.9 ) )
1168
- .and( material.metalness.lessThan( 0.1 ) );
1169
-
1170
- If( skip.not().and( emissiveSample.valid ).and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1190
+ // No rough-diffuse secondary-bounce skip here: dropping NEE while the emissive-hit
1191
+ // MIS still down-weights BSDF hits deletes emitter energy from deep GI.
1192
+ If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1171
1193
 
1172
1194
  const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
1173
1195
 
@@ -1199,7 +1221,7 @@ export function buildShadeKernel( params ) {
1199
1221
  currentRadiance.xyz.add(
1200
1222
  regularizePathContribution(
1201
1223
  emissiveLight.mul( throughput ).mul( giScale ),
1202
- float( bounceIndex ), fireflyThreshold, int( frame ),
1224
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1203
1225
  ),
1204
1226
  ),
1205
1227
  currentRadiance.w,
@@ -1216,6 +1238,7 @@ export function buildShadeKernel( params ) {
1216
1238
  const emissiveLight = calculateEmissiveTriangleContribution(
1217
1239
  hitPoint, N, V, material,
1218
1240
  bounceIndex, rngState,
1241
+ _pixelCoord, resolution, frame, dimBase,
1219
1242
  emissiveBoost,
1220
1243
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1221
1244
  triangleBuffer,
@@ -1227,7 +1250,7 @@ export function buildShadeKernel( params ) {
1227
1250
  currentRadiance.xyz.add(
1228
1251
  regularizePathContribution(
1229
1252
  emissiveLight.mul( throughput ).mul( giScale ),
1230
- float( bounceIndex ), fireflyThreshold, int( frame ),
1253
+ float( bounceIndex ), fireflyThreshold, int( accumFrame ),
1231
1254
  ),
1232
1255
  ),
1233
1256
  currentRadiance.w,
@@ -1244,17 +1267,26 @@ export function buildShadeKernel( params ) {
1244
1267
  // per-term (env / emissive-hit / direct-light / emissive-NEE), matching the megakernel which never
1245
1268
  // re-suppresses the running radiance.
1246
1269
 
1247
- const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
1248
- material, bounceIndex, mc,
1249
- ) ).toVar();
1250
-
1251
1270
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1252
- V, N, material,
1271
+ N, material,
1253
1272
  brdfDir, brdfPdf, brdfValue,
1254
- rngState, samplingInfo,
1273
+ brdfIsTransmission, brdfColorWeight,
1274
+ rngState,
1275
+ _pixelCoord, resolution, frame, dimBase,
1255
1276
  ) ).toVar();
1256
1277
 
1257
1278
  const bounceDir = indirectResult.direction.toVar();
1279
+
1280
+ // Shading-normal leak guard: a normal-mapped lobe can sample below the geometric surface;
1281
+ // tracing that ray tunnels through single-sided shells onto whatever sits behind them.
1282
+ const NgeoFF = select( dot( hitNormal, V ).lessThan( 0.0 ), hitNormal.negate(), hitNormal );
1283
+ If( brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ), () => {
1284
+
1285
+ commitDeferredAux( N );
1286
+ terminatePath();
1287
+ Return();
1288
+
1289
+ } );
1258
1290
  // combinedPdf is stored as next bounce's prevBouncePdf for NEE↔implicit-env MIS
1259
1291
  const bouncePdf = max( indirectResult.combinedPdf, 0.001 ).toVar();
1260
1292
  throughput.mulAssign( indirectResult.throughput );
@@ -1265,7 +1297,8 @@ export function buildShadeKernel( params ) {
1265
1297
  // terminate. Subsumes the old compensated low-throughput kill (#12). Unbiased; just terminates smarter.
1266
1298
  const rrSurvival = handleRussianRoulette(
1267
1299
  bounceIndex, throughput, mc, bounceDir, rngState,
1268
- enableEnvironmentLight, useEnvMapIS,
1300
+ _pixelCoord, resolution, frame, dimBase,
1301
+ enableEnvironmentLight,
1269
1302
  ).toVar();
1270
1303
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
1271
1304
 
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,45 +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
-
267
- // IMPROVEMENT: Dynamic MIS strategy based on material properties
268
- export const MISStrategy = struct( {
269
- brdfWeight: 'float',
270
- lightWeight: 'float',
271
- envWeight: 'float',
272
- useBRDFSampling: 'bool',
273
- useLightSampling: 'bool',
274
- useEnvSampling: 'bool',
275
- } );
276
244
 
277
245
  // General rendering state (used across all rendering paths)
278
246
  export const RenderState = 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 ), () => {
package/src/index.js CHANGED
@@ -54,6 +54,11 @@ export { VRAMTracker, bufferBytes, textureBytes } from './Processor/VRAMTracker.
54
54
  export { RenderPipeline } from './Pipeline/RenderPipeline.js';
55
55
  export { RenderStage, StageExecutionMode } from './Pipeline/RenderStage.js';
56
56
  export { PipelineContext } from './Pipeline/PipelineContext.js';
57
+ export {
58
+ setBindingAudit,
59
+ getBindingAuditFindings,
60
+ clearBindingAuditFindings,
61
+ } from './Pipeline/BindingAudit.js';
57
62
 
58
63
  // Animation (for advanced consumers)
59
64
  export { AnimationManager } from './managers/AnimationManager.js';
@@ -274,7 +274,6 @@ export class EnvironmentManager {
274
274
  this._updateCDFTexture();
275
275
  this.uniforms.set( 'envTotalSum', 0.0 );
276
276
  this.uniforms.set( 'envCompensationDelta', 0.0 );
277
- this.uniforms.set( 'useEnvMapIS', 0 );
278
277
  return;
279
278
 
280
279
  }
@@ -289,7 +288,6 @@ export class EnvironmentManager {
289
288
  this._updateCDFTexture();
290
289
  this.uniforms.set( 'envTotalSum', 0.0 );
291
290
  this.uniforms.set( 'envCompensationDelta', 0.0 );
292
- this.uniforms.set( 'useEnvMapIS', 0 );
293
291
  return;
294
292
 
295
293
  }
@@ -309,7 +307,6 @@ export class EnvironmentManager {
309
307
  this._updateCDFTexture();
310
308
  this.uniforms.set( 'envTotalSum', this.equirectHdrInfo.totalSum );
311
309
  this.uniforms.set( 'envCompensationDelta', this.equirectHdrInfo.compensationDelta );
312
- this.uniforms.set( 'useEnvMapIS', 1 );
313
310
 
314
311
  const { width, height } = this.equirectHdrInfo;
315
312
  if ( width && height ) {
@@ -326,7 +323,6 @@ export class EnvironmentManager {
326
323
  } catch ( error ) {
327
324
 
328
325
  log.error( 'CDF build failed:', error );
329
- this.uniforms.set( 'useEnvMapIS', 0 );
330
326
  this.uniforms.set( 'envTotalSum', 0.0 );
331
327
  this.uniforms.set( 'envCompensationDelta', 0.0 );
332
328
 
@@ -387,7 +383,6 @@ export class EnvironmentManager {
387
383
  this._updateCDFTexture();
388
384
  this.uniforms.set( 'envTotalSum', 0.0 );
389
385
  this.uniforms.set( 'envCompensationDelta', 0.0 );
390
- this.uniforms.set( 'useEnvMapIS', 0 );
391
386
 
392
387
  }
393
388
 
@@ -160,6 +160,10 @@ export class UniformManager {
160
160
 
161
161
  // Frame and sampling
162
162
  u( 'frame', 0, 'uint' );
163
+ // Sampler seed axis, separate from the accumulation index: `frame` is zeroed by every
164
+ // reset, and a camera move resets each frame of a drag, so the RNG redrew the identical
165
+ // sequence and temporal denoising had nothing to average.
166
+ u( 'seedFrame', 0, 'uint' );
163
167
  u( 'maxBounces', DEFAULT_STATE.bounces, 'int' );
164
168
  u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
165
169
  u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
@@ -198,7 +202,6 @@ export class UniformManager {
198
202
  ub( 'transparentBackground', DEFAULT_STATE.transparentBackground );
199
203
  ub( 'enableEnvironment', DEFAULT_STATE.enableEnvironment );
200
204
  u( 'environmentMatrix', new Matrix4(), 'mat4' );
201
- ub( 'useEnvMapIS', DEFAULT_STATE.useImportanceSampledEnvironment );
202
205
  u( 'envTotalSum', 0.0, 'float' );
203
206
  u( 'envCompensationDelta', 0.0, 'float' );
204
207
  u( 'envResolution', new Vector2( 1, 1 ), 'vec2' );