rayzee 6.4.0 → 7.0.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 (58) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +4953 -4225
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +29 -13
  8. package/src/PathTracerApp.js +119 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +22 -1
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -316
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/TextureCreator.js +6 -0
  21. package/src/Processor/VRAMTracker.js +169 -0
  22. package/src/Processor/utils.js +11 -110
  23. package/src/RenderSettings.js +1 -3
  24. package/src/Stages/ASVGF.js +76 -20
  25. package/src/Stages/BilateralFilter.js +34 -10
  26. package/src/Stages/EdgeFilter.js +2 -3
  27. package/src/Stages/MotionVector.js +16 -9
  28. package/src/Stages/NormalDepth.js +17 -5
  29. package/src/Stages/PathTracer.js +671 -1456
  30. package/src/Stages/PathTracerStage.js +1451 -0
  31. package/src/Stages/SSRC.js +32 -15
  32. package/src/Stages/Variance.js +35 -12
  33. package/src/TSL/BVHTraversal.js +7 -1
  34. package/src/TSL/Common.js +12 -2
  35. package/src/TSL/CompactKernel.js +110 -0
  36. package/src/TSL/DebugKernel.js +98 -0
  37. package/src/TSL/Environment.js +13 -11
  38. package/src/TSL/ExtendKernel.js +75 -0
  39. package/src/TSL/FinalWriteKernel.js +121 -0
  40. package/src/TSL/GenerateKernel.js +109 -0
  41. package/src/TSL/LightsSampling.js +2 -2
  42. package/src/TSL/MaterialTransmission.js +32 -2
  43. package/src/TSL/PathTracerCore.js +43 -912
  44. package/src/TSL/ShadeKernel.js +873 -0
  45. package/src/TSL/Struct.js +5 -0
  46. package/src/TSL/Subsurface.js +232 -0
  47. package/src/TSL/patches.js +81 -4
  48. package/src/index.js +3 -0
  49. package/src/managers/CameraManager.js +1 -1
  50. package/src/managers/DenoisingManager.js +40 -75
  51. package/src/managers/EnvironmentManager.js +30 -39
  52. package/src/managers/MaterialDataManager.js +60 -1
  53. package/src/managers/OverlayManager.js +7 -22
  54. package/src/managers/UniformManager.js +1 -3
  55. package/src/managers/helpers/TileHelper.js +2 -2
  56. package/src/Stages/AdaptiveSampling.js +0 -483
  57. package/src/TSL/PathTracer.js +0 -384
  58. package/src/managers/TileManager.js +0 -298
@@ -1,16 +1,11 @@
1
1
  /**
2
- * PathTracerCore.js - Path Tracing Core
2
+ * PathTracerCore.js — shared path-tracing sampling helpers (TSL).
3
3
  *
4
- * Exact port of pathtracer_core.fs + helper functions from pathtracer.fs
5
- * Pure TSL: Fn(), If(), Loop(), .toVar(), .assign() — NO wgslFn()
6
- *
7
- * Contains:
8
- * - getOrCreateMaterialClassification cached material classification
9
- * - generateSampledDirection — BRDF direction sampling with multi-lobe CDF
10
- * - handleRussianRoulette — adaptive path termination (inlines path importance)
11
- * - sampleBackgroundLighting — environment background sampling
12
- * - regularizePathContribution — firefly suppression
13
- * - Trace — main path tracing loop
4
+ * Imported by the wavefront kernels (GenerateKernel / ShadeKernel):
5
+ * - generateSampledDirection — BRDF direction sampling with multi-lobe CDF
6
+ * - regularizePathContribution — firefly suppression
7
+ * - computeNDCDepth — world position → NDC depth [0,1]
8
+ * - handleRussianRoulette adaptive path termination
14
9
  */
15
10
 
16
11
  import {
@@ -19,135 +14,43 @@ import {
19
14
  float,
20
15
  vec2,
21
16
  vec3,
22
- vec4,
23
17
  int,
24
- bool as tslBool,
25
18
  max,
26
19
  min,
27
- exp,
28
- log,
29
20
  clamp,
30
- mix,
31
21
  dot,
32
- normalize,
33
- length,
34
22
  reflect,
35
23
  If,
36
- Loop,
37
- Break,
38
- Continue,
39
- select,
24
+ mix,
40
25
  smoothstep,
41
- sampler,
26
+ exp,
27
+ select,
42
28
  } from 'three/tsl';
43
29
 
44
- import { struct } from './patches.js';
45
-
46
30
  import {
47
31
  PI_INV,
48
- MIN_ROUGHNESS,
49
32
  MAX_ROUGHNESS,
50
33
  MIN_CLEARCOAT_ROUGHNESS,
51
34
  MIN_PDF,
52
- maxComponent,
53
- classifyMaterial,
54
35
  constructTBN,
55
36
  calculateFireflyThreshold,
56
37
  applySoftSuppressionRGB,
57
- getMaterial,
58
- powerHeuristic,
59
- balanceHeuristic,
60
- computeDotProducts,
61
38
  } from './Common.js';
62
- import {
63
- DirectionSample,
64
- MaterialClassification,
65
- MaterialCache,
66
- BRDFWeights,
67
- Ray,
68
- HitInfo,
69
- MaterialSamples,
70
- RayTracingMaterial,
71
- ImportanceSamplingInfo,
72
- DotProducts,
73
- } from './Struct.js';
74
- import { RandomValue, getRandomSample } from './Random.js';
75
- import { traverseBVH } from './BVHTraversal.js';
76
- import { sampleEnvironment, sampleEquirect, getGroundProjectedDirection } from './Environment.js';
77
- import { sampleAllMaterialTextures } from './TextureSampling.js';
78
- import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
79
- import { handleMaterialTransparency, MaterialInteractionResult, sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
39
+ import { DirectionSample, MaterialCache } from './Struct.js';
40
+ import { RandomValue } from './Random.js';
41
+ import { sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
80
42
  import {
81
43
  SheenDistribution,
82
44
  calculateVNDFPDF,
83
45
  calculateBRDFWeights,
84
- createMaterialCache,
85
- getImportanceSamplingInfo,
86
46
  } from './MaterialProperties.js';
87
- import { evaluateMaterialResponse, evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
47
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
88
48
  import { dielectricF0 } from './Fresnel.js';
89
49
  import {
90
50
  ImportanceSampleCosine,
91
51
  ImportanceSampleGGX,
92
52
  sampleGGXVNDF,
93
53
  } from './MaterialSampling.js';
94
- import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
95
- import { calculateDirectLightingUnified, calculateMaterialPDFFromDots } from './LightsSampling.js';
96
- import { calculateIndirectLighting } from './LightsIndirect.js';
97
- import { IndirectLightingResult } from './LightsCore.js';
98
- import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
99
- import { sampleLightBVHTriangle } from './LightBVHSampling.js';
100
- import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
101
- import { traverseBVHShadow } from './BVHTraversal.js';
102
-
103
- // =============================================================================
104
- // Constants
105
- // =============================================================================
106
-
107
- // Ray type enumeration
108
- const RAY_TYPE_CAMERA = 0;
109
- const RAY_TYPE_REFLECTION = 1;
110
- const RAY_TYPE_TRANSMISSION = 2;
111
- const RAY_TYPE_DIFFUSE = 3;
112
-
113
- // Trace result struct
114
- export const TraceResult = struct( {
115
- radiance: 'vec4',
116
- objectNormal: 'vec3',
117
- objectColor: 'vec3',
118
- objectID: 'float',
119
- firstHitPoint: 'vec3',
120
- firstHitDistance: 'float',
121
- } );
122
-
123
- // =============================================================================
124
- // Material Classification Caching
125
- // =============================================================================
126
-
127
- // OPTIMIZED: Consolidated material classification with material change detection
128
- // Note: In TSL, we cannot use inout on PathState, so we pass individual cache fields
129
- // and return classification. PathState cache management happens in the caller.
130
- export const getOrCreateMaterialClassification = Fn( ( [
131
- material, materialIndex,
132
- classificationCached, lastMaterialIndex,
133
- cachedClassification,
134
- ] ) => {
135
-
136
- const result = cachedClassification.toVar();
137
-
138
- If( classificationCached.not().or( lastMaterialIndex.notEqual( materialIndex ) ), () => {
139
-
140
- result.assign( classifyMaterial(
141
- material.metalness, material.roughness,
142
- material.transmission, material.clearcoat,
143
- material.emissive,
144
- ) );
145
-
146
- } );
147
-
148
- return result;
149
-
150
- } );
151
54
 
152
55
  // =============================================================================
153
56
  // BRDF Direction Sampling
@@ -295,23 +198,45 @@ export const generateSampledDirection = Fn( ( [
295
198
  } );
296
199
 
297
200
  // =============================================================================
298
- // Russian Roulette Path Termination
201
+ // Firefly Suppression
299
202
  // =============================================================================
300
203
 
204
+ export const regularizePathContribution = /*@__PURE__*/ wgslFn( `
205
+ fn regularizePathContribution( contribution: vec3f, pathLength: f32, fireflyThreshold: f32, frame: i32 ) -> vec3f {
206
+ let threshold = calculateFireflyThreshold( fireflyThreshold, i32( pathLength ), frame );
207
+ return applySoftSuppressionRGB( contribution, threshold, 0.5f );
208
+ }
209
+ `, [ calculateFireflyThreshold, applySoftSuppressionRGB ] );
210
+
211
+ // ── Shared sampling helpers (used by the wavefront kernels) ──
212
+
213
+ // World position → NDC depth [0,1] for motion-vector reprojection.
214
+ export const computeNDCDepth = /*@__PURE__*/ wgslFn( `
215
+ fn computeNDCDepth( worldPos: vec3f, cameraProjectionMatrix: mat4x4f, cameraViewMatrix: mat4x4f ) -> f32 {
216
+ let clipPos = cameraProjectionMatrix * cameraViewMatrix * vec4f( worldPos, 1.0f );
217
+ let ndcDepth = clipPos.z / clipPos.w * 0.5f + 0.5f;
218
+ return clamp( ndcDepth, 0.0f, 1.0f );
219
+ }
220
+ ` );
221
+
222
+ // Adaptive Russian roulette (megakernel parity: PathTracerCore.js:302 on `main`, gap #7). Returns the
223
+ // survival probability (≥minProb) when the path continues, or 0.0 when terminated. Material-importance +
224
+ // throughput + env-direction aware, with a dynamic minBounces floor and exponential depth decay — replaces
225
+ // the flat `clamp(maxThroughput,0.05,0.95)` test. Unbiased either way; this just terminates the *right* rays
226
+ // (keeps smooth-metal / transmissive / emissive chains alive longer) → less noise per sample.
227
+ // Takes the already-computed MaterialClassification `mc` directly (the wavefront classifies once per shade).
301
228
  export const handleRussianRoulette = Fn( ( [
302
- depth, throughput, material, materialIndex, rayDirection, rngState,
303
- classificationCached, lastMaterialIndex, cachedClassification,
229
+ depth, throughput, mc, rayDirection, rngState,
304
230
  enableEnvironmentLight, useEnvMapIS,
305
231
  ] ) => {
306
232
 
307
233
  const result = float( 1.0 ).toVar();
308
234
 
309
- // Always continue for first few bounces
310
235
  If( depth.greaterThanEqual( int( 3 ) ), () => {
311
236
 
312
- const throughputStrength = max( maxComponent( { v: throughput } ), 0.0 ).toVar();
237
+ const throughputStrength = max( max( max( throughput.x, throughput.y ), throughput.z ), 0.0 ).toVar();
313
238
 
314
- // Energy-conserving early termination for very low throughput paths
239
+ // Energy-conserving early termination for very low throughput paths (compensated)
315
240
  If( throughputStrength.lessThan( 0.0008 ).and( depth.greaterThan( int( 4 ) ) ), () => {
316
241
 
317
242
  const lowThroughputProb = max( throughputStrength.mul( 125.0 ), 0.01 );
@@ -320,19 +245,8 @@ export const handleRussianRoulette = Fn( ( [
320
245
 
321
246
  } ).Else( () => {
322
247
 
323
- // Get classification
324
- const mc = MaterialClassification.wrap( getOrCreateMaterialClassification(
325
- material, materialIndex,
326
- classificationCached, lastMaterialIndex, cachedClassification,
327
- ) ).toVar();
328
-
248
+ // Importance boosts: deeper budget for transport types that physically carry energy farther.
329
249
  const materialImportance = mc.complexityScore.toVar();
330
-
331
- // Boost importance for special materials — depth hierarchy reflects
332
- // how many bounces each transport type physically needs:
333
- // Specular metals: deepest (mirror chains carry energy efficiently)
334
- // Transmissive: deep (caustics, internal reflections)
335
- // Emissive: shallowest (emission already collected, continuation rarely valuable)
336
250
  If( mc.isMetallic.and( mc.isSmooth ).and( depth.lessThan( int( 7 ) ) ), () => {
337
251
 
338
252
  materialImportance.addAssign( 0.3 );
@@ -368,7 +282,6 @@ export const handleRussianRoulette = Fn( ( [
368
282
 
369
283
  } ).Else( () => {
370
284
 
371
- // Path contribution estimate — reuses throughputStrength + mc from outer scope.
372
285
  const estMaterialImportance = mc.complexityScore.toVar();
373
286
  If( mc.isMetallic.and( mc.isSmooth ), () => {
374
287
 
@@ -400,27 +313,21 @@ export const handleRussianRoulette = Fn( ( [
400
313
  mix( estMaterialImportance.mul( 0.7 ), directionImportance, 0.3 ),
401
314
  ).mul( throughputWeight ).toVar();
402
315
 
403
- // Smooth adaptive continuation probability (no discrete depth brackets)
404
- // Early behavior: throughput + material driven, generous
316
+ // Smooth early→deep continuation probability (no discrete depth brackets)
405
317
  const earlyProb = clamp(
406
318
  materialImportance.mul( 0.4 ).add( throughputStrength.mul( 0.6 ) ).mul( 1.2 ),
407
319
  0.15, 0.95,
408
320
  );
409
- // Deep behavior: aggressive termination, material-aware floor
410
321
  const deepProb = clamp(
411
322
  throughputStrength.mul( 0.4 ).add( materialImportance.mul( 0.1 ) ),
412
323
  0.03, 0.6,
413
324
  );
414
325
 
415
- // Smooth blend from early → deep using depth relative to minBounces
416
- // At minBounces: t=0 (earlyProb), at minBounces+10: t=1 (deepProb)
417
326
  const depthT = clamp( float( depth.sub( minBounces ) ).div( 10.0 ), 0.0, 1.0 );
418
327
  const rrProb = mix( earlyProb, deepProb, depthT ).toVar();
419
328
 
420
- // Mix in path contribution for direction-aware survival
421
329
  rrProb.assign( mix( rrProb, max( rrProb, pathContribution ), 0.4 ) );
422
330
 
423
- // Material-specific boosts
424
331
  If( materialImportance.greaterThan( 0.5 ), () => {
425
332
 
426
333
  const boostFactor = materialImportance.sub( 0.5 ).mul( 0.6 );
@@ -428,12 +335,10 @@ export const handleRussianRoulette = Fn( ( [
428
335
 
429
336
  } );
430
337
 
431
- // Exponential depth decay
432
338
  const depthDecay = float( 0.12 ).add( materialImportance.mul( 0.08 ) );
433
339
  const depthFactor = exp( float( depth.sub( minBounces ) ).negate().mul( depthDecay ) );
434
340
  rrProb.mulAssign( depthFactor );
435
341
 
436
- // Minimum probability floor
437
342
  const minProb = select( mc.isEmissive, float( 0.04 ), float( 0.02 ) );
438
343
  rrProb.assign( max( rrProb, minProb ) );
439
344
 
@@ -449,777 +354,3 @@ export const handleRussianRoulette = Fn( ( [
449
354
  return result;
450
355
 
451
356
  } );
452
-
453
- // =============================================================================
454
- // Background & Environment Sampling
455
- // =============================================================================
456
-
457
- export const sampleBackgroundLighting = Fn( ( [
458
- isPrimaryRay, rayOrigin, direction,
459
- envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
460
- showBackground, backgroundIntensity,
461
- groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
462
- ] ) => {
463
-
464
- // Only hide background for primary camera rays when showBackground is false
465
- const envColor = vec4( 0.0 ).toVar();
466
-
467
- If( isPrimaryRay.and( showBackground.not() ), () => {
468
-
469
- // Return zero
470
- envColor.assign( vec4( 0.0 ) );
471
-
472
- } ).Else( () => {
473
-
474
- // Primary-ray only: indirect bounces must see the raw envmap so shading stays physically correct.
475
- const effectiveDir = direction.toVar();
476
- If( isPrimaryRay.and( groundProjectionEnabled ), () => {
477
-
478
- effectiveDir.assign( getGroundProjectedDirection(
479
- rayOrigin, direction, groundProjectionRadius, groundProjectionHeight,
480
- ) );
481
-
482
- } );
483
-
484
- const sampled = sampleEnvironment( {
485
- tex: envTexture, samp: sampler( envTexture ), direction: effectiveDir, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight,
486
- } );
487
-
488
- If( isPrimaryRay, () => {
489
-
490
- envColor.assign( sampled.mul( backgroundIntensity ) );
491
-
492
- } ).Else( () => {
493
-
494
- envColor.assign( sampled );
495
-
496
- } );
497
-
498
- } );
499
-
500
- return envColor;
501
-
502
- } );
503
-
504
- // =============================================================================
505
- // Firefly Suppression
506
- // =============================================================================
507
-
508
- export const regularizePathContribution = /*@__PURE__*/ wgslFn( `
509
- fn regularizePathContribution( contribution: vec3f, pathLength: f32, fireflyThreshold: f32, frame: i32 ) -> vec3f {
510
- let threshold = calculateFireflyThreshold( fireflyThreshold, i32( pathLength ), frame );
511
- return applySoftSuppressionRGB( contribution, threshold, 0.5f );
512
- }
513
- `, [ calculateFireflyThreshold, applySoftSuppressionRGB ] );
514
-
515
- // =============================================================================
516
- // Main Path Tracing Loop
517
- // =============================================================================
518
-
519
- export const Trace = Fn( ( [
520
- ray, rngState, rayIndex,
521
- // BVH / Scene
522
- bvhBuffer,
523
- triangleBuffer,
524
- materialBuffer,
525
- // Texture arrays for material sampling
526
- albedoMaps, normalMaps, bumpMaps,
527
- metalnessMaps, roughnessMaps, emissiveMaps,
528
- displacementMaps,
529
- // Lights
530
- directionalLightsBuffer, numDirectionalLights,
531
- areaLightsBuffer, numAreaLights,
532
- pointLightsBuffer, numPointLights,
533
- spotLightsBuffer, numSpotLights,
534
- // Environment
535
- envTexture, environmentIntensity, envMatrix,
536
- envCDFBuffer,
537
- envTotalSum, envCompensationDelta, envResolution,
538
- enableEnvironmentLight, useEnvMapIS,
539
- groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
540
- // Rendering parameters
541
- maxBounceCount, transmissiveBounces,
542
- backgroundIntensity, showBackground, transparentBackground,
543
- fireflyThreshold, globalIlluminationIntensity,
544
- enableEmissiveTriangleSampling,
545
- emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
546
- lightBVHBuffer, lightBVHNodeCount,
547
- // Per-pixel info
548
- pixelCoord, resolution, frame,
549
- ] ) => {
550
-
551
- const radiance = vec3( 0.0 ).toVar();
552
- const throughput = vec3( 1.0 ).toVar();
553
- const alpha = float( 1.0 ).toVar();
554
- const hasHitOpaqueSurface = tslBool( false ).toVar(); // Tracks if ray chain has hit non-transmissive geometry
555
- const prevBouncePdf = float( 0.0 ).toVar(); // 0 = camera ray (skip MIS for directly visible emissive)
556
-
557
- // Output data
558
- const objectNormal = vec3( 0.0 ).toVar();
559
- const objectColor = vec3( 0.0 ).toVar();
560
- const objectID = float( - 1000.0 ).toVar();
561
- const firstHitPoint = ray.origin.toVar();
562
- const firstHitDistance = float( 1e10 ).toVar();
563
- // OIDN clean-aux: extend albedo/normal capture through specular/transmissive
564
- // surfaces so aux features describe what's actually visible (e.g. scenery
565
- // behind glass), not the glass surface itself.
566
- const auxLocked = tslBool( false ).toVar();
567
-
568
- // Medium stack for transmission (slots 1-3 for nested media, depth 0 = air).
569
- // Each slot tracks IOR plus KHR_materials_volume attenuation (color + distance)
570
- // so per-bounce in-volume absorption uses the actual ray path length.
571
- const mediumStackDepth = int( 0 ).toVar();
572
- const mediumStack_ior_1 = float( 1.0 ).toVar();
573
- const mediumStack_ior_2 = float( 1.0 ).toVar();
574
- const mediumStack_ior_3 = float( 1.0 ).toVar();
575
- const mediumStack_attColor_1 = vec3( 1.0 ).toVar();
576
- const mediumStack_attColor_2 = vec3( 1.0 ).toVar();
577
- const mediumStack_attColor_3 = vec3( 1.0 ).toVar();
578
- const mediumStack_attDist_1 = float( 0.0 ).toVar();
579
- const mediumStack_attDist_2 = float( 0.0 ).toVar();
580
- const mediumStack_attDist_3 = float( 0.0 ).toVar();
581
- // Precomputed Beer-Lambert absorption coefficient sigma_a = -log(attColor)/attDist.
582
- // Stored at push time so per-bounce absorption inside a medium becomes a single
583
- // exp(-sigma_a * thickness) instead of a log + div + exp every bounce.
584
- const mediumStack_sigmaA_1 = vec3( 0.0 ).toVar();
585
- const mediumStack_sigmaA_2 = vec3( 0.0 ).toVar();
586
- const mediumStack_sigmaA_3 = vec3( 0.0 ).toVar();
587
-
588
- // Locked at the first dispersive transmission; reused for subsequent transmissions on
589
- // the path so multi-bounce dispersion doesn't collapse under repeated colorWeight ×.
590
- const pathWavelength = float( 0.0 ).toVar();
591
-
592
- // Render state
593
- const stateTraversals = maxBounceCount.toVar();
594
- const stateTransmissiveTraversals = transmissiveBounces.toVar();
595
- const stateRayType = int( RAY_TYPE_CAMERA ).toVar();
596
- const stateIsPrimaryRay = tslBool( true ).toVar();
597
-
598
-
599
- // Path state cache fields (managed individually since TSL can't do inout struct)
600
- const psWeightsComputed = tslBool( false ).toVar();
601
- const psClassificationCached = tslBool( false ).toVar();
602
- const psMaterialCacheCached = tslBool( false ).toVar();
603
- const psLastMaterialIndex = int( - 1 ).toVar();
604
-
605
- // Cached classification
606
- const psCachedClassification = MaterialClassification( {
607
- isMetallic: false, isRough: false, isSmooth: false,
608
- isTransmissive: false, hasClearcoat: false, isEmissive: false,
609
- complexityScore: float( 0.0 ),
610
- } ).toVar();
611
-
612
- // Cached BRDF weights
613
- const psCachedBrdfWeights = BRDFWeights( {
614
- specular: float( 0.5 ), diffuse: float( 0.5 ),
615
- sheen: float( 0.0 ), clearcoat: float( 0.0 ),
616
- transmission: float( 0.0 ), iridescence: float( 0.0 ),
617
- } ).toVar();
618
-
619
- // Cached material cache
620
- const psCachedMaterialCache = MaterialCache( {
621
- F0: vec3( 0.04 ), NoV: float( 1.0 ),
622
- diffuseColor: vec3( 0.0 ), isPurelyDiffuse: false,
623
- alpha: float( 0.0 ), k: float( 0.0 ), alpha2: float( 0.0 ),
624
- invRoughness: float( 1.0 ), metalFactor: float( 0.5 ),
625
- iorFactor: float( 1.0 ), maxSheenColor: float( 0.0 ),
626
- } ).toVar();
627
-
628
- // Track effective bounces
629
- const effectiveBounces = int( 0 ).toVar();
630
-
631
- // Mutable ray
632
- const rayOrigin = ray.origin.toVar();
633
- const rayDirection = ray.direction.toVar();
634
-
635
- // Main bounce loop
636
- Loop( { start: int( 0 ), end: maxBounceCount.add( transmissiveBounces ).add( 1 ), type: 'int', condition: '<' }, ( { i: bounceIndex } ) => {
637
-
638
- // Update state
639
- stateTraversals.assign( maxBounceCount.sub( effectiveBounces ) );
640
- stateIsPrimaryRay.assign( bounceIndex.equal( int( 0 ) ) );
641
-
642
-
643
- // Check bounce budget
644
- If( effectiveBounces.greaterThan( maxBounceCount ), () => {
645
-
646
- Break();
647
-
648
- } );
649
-
650
- // Non-compounding GI intensity: applied per-bounce to radiance, not throughput
651
- const giScale = select( bounceIndex.greaterThan( int( 0 ) ), globalIlluminationIntensity, float( 1.0 ) );
652
-
653
- // Traverse BVH
654
- const currentRay = Ray( { origin: rayOrigin, direction: rayDirection } );
655
- const hitInfo = HitInfo.wrap( traverseBVH(
656
- currentRay,
657
- bvhBuffer,
658
- triangleBuffer,
659
- ) ).toVar();
660
-
661
- // KHR_materials_volume: apply Beer's law over the actual distance the ray
662
- // traveled inside the current medium. Top-of-stack holds the medium the ray
663
- // is currently in — depth==0 means air (no absorption). sigma_a was
664
- // precomputed at push time, so this collapses to a single exp().
665
- If( hitInfo.didHit.and( mediumStackDepth.greaterThan( int( 0 ) ) ), () => {
666
-
667
- const mSigmaA = vec3( 0.0 ).toVar();
668
- If( mediumStackDepth.equal( int( 1 ) ), () => {
669
-
670
- mSigmaA.assign( mediumStack_sigmaA_1 );
671
-
672
- } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
673
-
674
- mSigmaA.assign( mediumStack_sigmaA_2 );
675
-
676
- } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
677
-
678
- mSigmaA.assign( mediumStack_sigmaA_3 );
679
-
680
- } );
681
-
682
- throughput.mulAssign( exp( mSigmaA.mul( hitInfo.dst ).negate() ) );
683
-
684
- } );
685
-
686
- If( hitInfo.didHit.not(), () => {
687
-
688
- // ENVIRONMENT LIGHTING
689
- const envColor = sampleBackgroundLighting(
690
- stateIsPrimaryRay, rayOrigin, rayDirection,
691
- envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
692
- showBackground, backgroundIntensity,
693
- groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
694
- );
695
-
696
- // MIS weight for implicit environment hit — prevents double-counting with NEE.
697
- // Primary rays and camera rays (prevBouncePdf == 0) get full weight.
698
- // Secondary rays use power heuristic between the scatter PDF and the
699
- // environment importance-sampling PDF, mirroring the emissive MIS at line ~978.
700
- const envMisWeight = float( 1.0 ).toVar();
701
- If( prevBouncePdf.greaterThan( 0.0 ).and( enableEnvironmentLight ).and( useEnvMapIS ), () => {
702
-
703
- const envEval = sampleEquirect(
704
- envTexture, rayDirection, envMatrix, envTotalSum, envCompensationDelta, envResolution,
705
- );
706
- const envPdf = envEval.w.toVar();
707
- If( envPdf.greaterThan( 0.0 ), () => {
708
-
709
- envMisWeight.assign( balanceHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) );
710
-
711
- } );
712
-
713
- } );
714
-
715
- radiance.addAssign( regularizePathContribution( {
716
- contribution: envColor.xyz.mul( throughput ).mul( giScale ).mul( envMisWeight ), pathLength: float( bounceIndex ), fireflyThreshold, frame: int( frame ),
717
- } ) );
718
-
719
- // Transparent background: only transparent if ray escaped WITHOUT hitting opaque geometry first.
720
- // Secondary bounces from opaque surfaces escaping to env should NOT make the pixel transparent.
721
- If( transparentBackground.and( hasHitOpaqueSurface.not() ), () => {
722
-
723
- alpha.assign( 0.0 );
724
-
725
- } ).ElseIf( transparentBackground.not(), () => {
726
-
727
- alpha.mulAssign( envColor.a );
728
-
729
- } );
730
-
731
- Break();
732
-
733
- } );
734
-
735
- // Get full material (27 reads). Lazy transform loading was tested but regressed
736
- // textured scenes due to identity-construct + conditional-assign overhead.
737
- // Shadow rays use getShadowMaterial() (7 reads) — the real bandwidth win.
738
- const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
739
-
740
- // Tessellation-free displacement — refine intersection with ray-height field marching
741
- const samplingUV = hitInfo.uv.toVar();
742
- const displacedNormal = hitInfo.normal.toVar();
743
-
744
- If( material.displacementMapIndex.greaterThanEqual( int( 0 ) ).and( material.displacementScale.greaterThan( 0.0 ) ), () => {
745
-
746
- const dispResult = DisplacementResult.wrap( refineDisplacedIntersection(
747
- currentRay, hitInfo, triangleBuffer, displacementMaps, material, bounceIndex,
748
- ) ).toVar();
749
- samplingUV.assign( dispResult.uv );
750
- displacedNormal.assign( dispResult.normal );
751
- hitInfo.hitPoint.assign( dispResult.hitPoint );
752
-
753
- } );
754
-
755
- // Sample all textures using displacement-refined UVs
756
- const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
757
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
758
- material, samplingUV, hitInfo.normal,
759
- ) ).toVar();
760
-
761
- // Update material with texture samples
762
- material.color.assign( matSamples.albedo );
763
- material.metalness.assign( clamp( matSamples.metalness, 0.0, 1.0 ) );
764
- material.roughness.assign( clamp( matSamples.roughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
765
-
766
- // Blend displaced normal with texture normal map — displacement provides macro shape, normal map adds micro detail
767
- const N = matSamples.normal.toVar();
768
- If( material.displacementMapIndex.greaterThanEqual( int( 0 ) ).and( material.displacementScale.greaterThan( 0.0 ) ), () => {
769
-
770
- N.assign( normalize( displacedNormal.add( matSamples.normal.sub( hitInfo.normal ) ) ) );
771
-
772
- } );
773
-
774
- // Compute current and previous medium IOR from stack for transmission
775
- const currentMediumIOR = float( 1.0 ).toVar();
776
- const previousMediumIOR = float( 1.0 ).toVar();
777
- If( mediumStackDepth.equal( int( 1 ) ), () => {
778
-
779
- currentMediumIOR.assign( mediumStack_ior_1 );
780
-
781
- } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
782
-
783
- currentMediumIOR.assign( mediumStack_ior_2 );
784
- previousMediumIOR.assign( mediumStack_ior_1 );
785
-
786
- } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
787
-
788
- currentMediumIOR.assign( mediumStack_ior_3 );
789
- previousMediumIOR.assign( mediumStack_ior_2 );
790
-
791
- } );
792
-
793
- // Handle transparent materials
794
- const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
795
- currentRay, N, material, rngState,
796
- stateTransmissiveTraversals,
797
- currentMediumIOR, previousMediumIOR,
798
- pathWavelength,
799
- ) ).toVar();
800
- pathWavelength.assign( interaction.pathWavelength );
801
-
802
- If( interaction.continueRay, () => {
803
-
804
- const isFreeBounce = tslBool( false ).toVar();
805
-
806
- If( interaction.isTransmissive.and( stateTransmissiveTraversals.greaterThan( int( 0 ) ) ), () => {
807
-
808
- stateTransmissiveTraversals.subAssign( 1 );
809
- stateRayType.assign( int( RAY_TYPE_TRANSMISSION ) );
810
- isFreeBounce.assign( tslBool( true ) );
811
-
812
- // Update medium stack only if we actually transmitted (not TIR/reflection)
813
- If( interaction.didReflect.not(), () => {
814
-
815
- If( interaction.entering, () => {
816
-
817
- // Push new medium onto stack (IOR + KHR_materials_volume attenuation)
818
- If( mediumStackDepth.lessThan( int( 3 ) ), () => {
819
-
820
- mediumStackDepth.addAssign( 1 );
821
-
822
- // Precompute sigma_a = -log(attColor)/attDist once at push time.
823
- // attDist==0 means "no absorption" — store sigma_a=0 so exp() returns 1.
824
- const mSigmaA = select(
825
- material.attenuationDistance.greaterThan( 0.0 ),
826
- log( max( material.attenuationColor, vec3( 0.001 ) ) ).negate().div( material.attenuationDistance ),
827
- vec3( 0.0 )
828
- ).toVar();
829
-
830
- If( mediumStackDepth.equal( int( 1 ) ), () => {
831
-
832
- mediumStack_ior_1.assign( material.ior );
833
- mediumStack_attColor_1.assign( material.attenuationColor );
834
- mediumStack_attDist_1.assign( material.attenuationDistance );
835
- mediumStack_sigmaA_1.assign( mSigmaA );
836
-
837
- } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
838
-
839
- mediumStack_ior_2.assign( material.ior );
840
- mediumStack_attColor_2.assign( material.attenuationColor );
841
- mediumStack_attDist_2.assign( material.attenuationDistance );
842
- mediumStack_sigmaA_2.assign( mSigmaA );
843
-
844
- } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
845
-
846
- mediumStack_ior_3.assign( material.ior );
847
- mediumStack_attColor_3.assign( material.attenuationColor );
848
- mediumStack_attDist_3.assign( material.attenuationDistance );
849
- mediumStack_sigmaA_3.assign( mSigmaA );
850
-
851
- } );
852
-
853
- } );
854
-
855
- } ).Else( () => {
856
-
857
- // Pop medium from stack
858
- If( mediumStackDepth.greaterThan( int( 0 ) ), () => {
859
-
860
- mediumStackDepth.subAssign( 1 );
861
-
862
- } );
863
-
864
- } );
865
-
866
- } );
867
-
868
- } ).ElseIf( interaction.isAlphaSkip, () => {
869
-
870
- isFreeBounce.assign( tslBool( true ) );
871
-
872
- } );
873
-
874
- // Update ray and continue
875
- throughput.mulAssign( interaction.throughput );
876
-
877
- // Transparent background: defer alpha decision to final hit/miss
878
- // Normal mode: apply material transparency alpha (blend/mask/transmission)
879
- If( transparentBackground.not(), () => {
880
-
881
- alpha.mulAssign( interaction.alpha );
882
-
883
- } );
884
-
885
- // For reflection (Fresnel/TIR): offset along the geometric normal to stay on the same side
886
- // For transmission: offset along the old ray direction to push through the surface
887
- const reflectOffsetDir = select( interaction.entering, N, N.negate() );
888
- const offsetDir = select( interaction.didReflect, reflectOffsetDir, rayDirection );
889
- rayOrigin.assign( hitInfo.hitPoint.add( offsetDir.mul( 0.001 ) ) );
890
- rayDirection.assign( interaction.direction );
891
-
892
- stateIsPrimaryRay.assign( tslBool( false ) );
893
-
894
- // Reset material-dependent caches
895
- psWeightsComputed.assign( tslBool( false ) );
896
- psMaterialCacheCached.assign( tslBool( false ) );
897
-
898
- If( isFreeBounce.not(), () => {
899
-
900
- effectiveBounces.addAssign( 1 );
901
-
902
- } );
903
-
904
- Continue();
905
-
906
- } );
907
-
908
- // Apply transparency alpha (skip in transparent background mode — alpha is binary hit/miss)
909
- If( transparentBackground.not(), () => {
910
-
911
- alpha.mulAssign( interaction.alpha );
912
-
913
- } );
914
-
915
- // Ray hit non-transmissive geometry — lock alpha at 1.0 for subsequent bounces
916
- hasHitOpaqueSurface.assign( tslBool( true ) );
917
-
918
- const randomSample = getRandomSample( pixelCoord, rayIndex, bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
919
-
920
- const V = rayDirection.negate().toVar();
921
-
922
- // Two-sided shading: flip the shading normal into the viewer's hemisphere.
923
- // This is the opaque reflection path (transmissive rays already Continue'd),
924
- // so it never disturbs dielectric entering/exiting. No-op when N already
925
- // faces V; rescues meshes with inward-facing normals (common in imported
926
- // scenes, e.g. pbrt PLY assets) that would otherwise shade black.
927
- If( dot( N, V ).lessThan( 0.0 ), () => {
928
-
929
- N.assign( N.negate() );
930
-
931
- } );
932
-
933
- material.sheenRoughness.assign( clamp( material.sheenRoughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
934
-
935
- // Sync material classification cache up front — the materialCache, BRDF
936
- // sample, importance sampling, and Russian roulette all consume it.
937
- // getOrCreateMaterialClassification is a cache hit when materialIndex
938
- // matches the previous bounce; otherwise it runs classifyMaterial once.
939
- // Doing this here eliminates a redundant classifyMaterial that previously
940
- // fired after generateSampledDirection to "sync" the caller's variable.
941
- psCachedClassification.assign( MaterialClassification.wrap( getOrCreateMaterialClassification(
942
- material, hitInfo.materialIndex,
943
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
944
- ) ) );
945
- psClassificationCached.assign( tslBool( true ) );
946
- psLastMaterialIndex.assign( hitInfo.materialIndex );
947
-
948
- // Create material cache if needed
949
- If( psMaterialCacheCached.not(), () => {
950
-
951
- psCachedMaterialCache.assign( createMaterialCache( N, V, material, matSamples, psCachedClassification ) );
952
- psMaterialCacheCached.assign( tslBool( true ) );
953
-
954
- } );
955
-
956
- // BRDF sampling
957
- const brdfDir = vec3( 0.0 ).toVar();
958
- const brdfValue = vec3( 0.0 ).toVar();
959
- const brdfPdf = float( 0.0 ).toVar();
960
-
961
- // Handle clearcoat
962
- If( material.clearcoat.greaterThan( 0.0 ), () => {
963
-
964
- const ccResult = ClearcoatResult.wrap( sampleClearcoat(
965
- currentRay, hitInfo, material, randomSample, rngState,
966
- ) );
967
- brdfDir.assign( ccResult.L );
968
- brdfValue.assign( ccResult.brdf );
969
- brdfPdf.assign( ccResult.pdf );
970
-
971
- } ).Else( () => {
972
-
973
- // Classification was already synced at the top of the bounce — pass
974
- // psCachedClassification directly so generateSampledDirection doesn't
975
- // have to call classifyMaterial again internally.
976
- const brdfSample = DirectionSample.wrap( generateSampledDirection(
977
- V, N, material, randomSample, rngState,
978
- psCachedClassification,
979
- psWeightsComputed, psCachedBrdfWeights,
980
- psMaterialCacheCached, psCachedMaterialCache,
981
- ) );
982
- brdfDir.assign( brdfSample.direction );
983
- brdfValue.assign( brdfSample.value );
984
- brdfPdf.assign( brdfSample.pdf );
985
-
986
- psWeightsComputed.assign( tslBool( true ) );
987
-
988
- } );
989
-
990
- // 1. EMISSIVE CONTRIBUTION (with MIS when direct emissive sampling is active)
991
- If( length( matSamples.emissive ).greaterThan( 0.0 ), () => {
992
-
993
- const emissiveMISWeight = float( 1.0 ).toVar();
994
-
995
- // Apply MIS when emissive direct sampling is active and this isn't a camera ray hit
996
- If( enableEmissiveTriangleSampling.equal( int( 1 ) )
997
- .and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
998
- .and( prevBouncePdf.greaterThan( 0.0 ) ), () => {
999
-
1000
- const lightPdf = calculateEmissiveLightPdf(
1001
- hitInfo.triangleIndex, hitInfo.dst, rayDirection, rayOrigin,
1002
- triangleBuffer, materialBuffer, emissiveTotalPower,
1003
- );
1004
-
1005
- emissiveMISWeight.assign(
1006
- powerHeuristic( { pdf1: prevBouncePdf, pdf2: lightPdf } )
1007
- );
1008
-
1009
- } );
1010
-
1011
- radiance.addAssign( regularizePathContribution( {
1012
- contribution: matSamples.emissive.mul( throughput ).mul( giScale ).mul( emissiveMISWeight ),
1013
- pathLength: float( bounceIndex ), fireflyThreshold, frame: int( frame ),
1014
- } ) );
1015
-
1016
- } );
1017
-
1018
- // 2. DIRECT LIGHTING
1019
- const directLight = calculateDirectLightingUnified(
1020
- hitInfo.hitPoint, N, material,
1021
- V,
1022
- brdfDir, brdfPdf, brdfValue,
1023
- bounceIndex, rngState,
1024
- directionalLightsBuffer, numDirectionalLights,
1025
- areaLightsBuffer, numAreaLights,
1026
- pointLightsBuffer, numPointLights,
1027
- spotLightsBuffer, numSpotLights,
1028
- bvhBuffer,
1029
- triangleBuffer,
1030
- materialBuffer,
1031
- envTexture, environmentIntensity, envMatrix,
1032
- envCDFBuffer,
1033
- envTotalSum, envCompensationDelta, envResolution,
1034
- enableEnvironmentLight,
1035
- );
1036
-
1037
- radiance.addAssign( regularizePathContribution( {
1038
- contribution: directLight.mul( throughput ).mul( giScale ), pathLength: float( bounceIndex ), fireflyThreshold, frame: int( frame ),
1039
- } ) );
1040
-
1041
- // 2b. EMISSIVE TRIANGLE DIRECT LIGHTING
1042
- If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1043
-
1044
- const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist ] ) => {
1045
-
1046
- return traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer );
1047
-
1048
- } );
1049
-
1050
- If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
1051
-
1052
- // Use Light BVH for spatially-aware importance sampling
1053
- const emissiveSample = EmissiveSample.wrap( sampleLightBVHTriangle(
1054
- hitInfo.hitPoint, N,
1055
- rngState,
1056
- lightBVHBuffer,
1057
- emissiveTriangleBuffer,
1058
- emissiveVec4Offset,
1059
- triangleBuffer,
1060
- ) );
1061
-
1062
- // Skip for very rough diffuse surfaces on secondary bounces
1063
- const skip = bounceIndex.greaterThan( int( 1 ) )
1064
- .and( material.roughness.greaterThan( 0.9 ) )
1065
- .and( material.metalness.lessThan( 0.1 ) );
1066
-
1067
- If( skip.not().and( emissiveSample.valid ).and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
1068
-
1069
- const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
1070
-
1071
- If( NoL.greaterThan( 0.0 ), () => {
1072
-
1073
- const rayOffset = calculateRayOffset( hitInfo.hitPoint, N, material );
1074
- const rayOrigin = hitInfo.hitPoint.add( rayOffset );
1075
- const shadowDist = emissiveSample.distance.sub( 0.001 );
1076
- const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist );
1077
-
1078
- If( visibility.greaterThan( 0.0 ), () => {
1079
-
1080
- // Share H + dot products between BRDF eval and PDF eval.
1081
- const emisDots = DotProducts.wrap( computeDotProducts( N, V, emissiveSample.direction ) );
1082
- const brdfValue = evaluateMaterialResponseFromDots( material, emisDots );
1083
- const brdfPdf = calculateMaterialPDFFromDots( material, emisDots );
1084
- const misWeight = select(
1085
- brdfPdf.greaterThan( 0.0 ),
1086
- powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: brdfPdf } ),
1087
- float( 1.0 )
1088
- );
1089
-
1090
- const emissiveLight = emissiveSample.emission
1091
- .mul( brdfValue ).mul( NoL )
1092
- .div( emissiveSample.pdf )
1093
- .mul( visibility ).mul( emissiveBoost ).mul( misWeight );
1094
-
1095
- radiance.addAssign( regularizePathContribution( {
1096
- contribution: emissiveLight.mul( throughput ).mul( giScale ), pathLength: float( bounceIndex ), fireflyThreshold, frame: int( frame ),
1097
- } ) );
1098
-
1099
- } );
1100
-
1101
- } );
1102
-
1103
- } );
1104
-
1105
- } ).Else( () => {
1106
-
1107
- // Fallback: flat CDF importance sampling
1108
- const emissiveLight = calculateEmissiveTriangleContribution(
1109
- hitInfo.hitPoint, N, V, material,
1110
- bounceIndex, rngState,
1111
- emissiveBoost,
1112
- emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1113
- triangleBuffer,
1114
- traceShadowRayWrapped,
1115
- calculateRayOffset,
1116
- );
1117
-
1118
- radiance.addAssign( regularizePathContribution( {
1119
- contribution: emissiveLight.mul( throughput ).mul( giScale ), pathLength: float( bounceIndex ), fireflyThreshold, frame: int( frame ),
1120
- } ) );
1121
-
1122
- } );
1123
-
1124
- } );
1125
-
1126
- // Classification was already synced at the top of the bounce loop, so
1127
- // psCachedClassification is current here regardless of which BRDF sample
1128
- // path ran above.
1129
- const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
1130
- material, bounceIndex, psCachedClassification,
1131
- ) );
1132
-
1133
- // 3. INDIRECT LIGHTING
1134
- const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1135
- V, N, material,
1136
- brdfDir, brdfPdf, brdfValue,
1137
- rngState,
1138
- samplingInfo,
1139
- ) );
1140
- throughput.mulAssign( indirectResult.throughput );
1141
-
1142
- // Prepare for next bounce
1143
- rayOrigin.assign( hitInfo.hitPoint.add( N.mul( 0.001 ) ) );
1144
- rayDirection.assign( indirectResult.direction );
1145
- prevBouncePdf.assign( indirectResult.combinedPdf );
1146
-
1147
- stateIsPrimaryRay.assign( tslBool( false ) );
1148
-
1149
- // Determine ray type
1150
- If( material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) ), () => {
1151
-
1152
- stateRayType.assign( int( RAY_TYPE_REFLECTION ) );
1153
-
1154
- } ).ElseIf( material.transmission.greaterThan( 0.5 ), () => {
1155
-
1156
- stateRayType.assign( int( RAY_TYPE_TRANSMISSION ) );
1157
-
1158
- } ).Else( () => {
1159
-
1160
- stateRayType.assign( int( RAY_TYPE_DIFFUSE ) );
1161
-
1162
- } );
1163
-
1164
- // firstHitPoint / firstHitDistance reflect the actual primary-ray hit
1165
- // (used for depth + temporal reprojection — must not skip transparent surfaces)
1166
- If( bounceIndex.equal( int( 0 ) ).and( hitInfo.didHit ), () => {
1167
-
1168
- firstHitPoint.assign( hitInfo.hitPoint );
1169
- firstHitDistance.assign( hitInfo.dst );
1170
-
1171
- } );
1172
-
1173
- // objectNormal / objectColor / objectID feed OIDN's aux inputs. Overwrite
1174
- // at each bounce until we land on a non-specular surface, then lock.
1175
- // Falls back to the last specular hit if the path never hits diffuse.
1176
- If( auxLocked.not().and( hitInfo.didHit ), () => {
1177
-
1178
- objectNormal.assign( N );
1179
- objectColor.assign( material.color.xyz );
1180
- objectID.assign( float( hitInfo.materialIndex ) );
1181
-
1182
- const isMirror = material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) );
1183
- const isTransmissive = material.transmission.greaterThan( 0.5 );
1184
- If( isMirror.or( isTransmissive ).not(), () => {
1185
-
1186
- auxLocked.assign( tslBool( true ) );
1187
-
1188
- } );
1189
-
1190
- } );
1191
-
1192
- // 4. RUSSIAN ROULETTE
1193
- const rrSurvivalProb = handleRussianRoulette(
1194
- bounceIndex, throughput, material, hitInfo.materialIndex,
1195
- rayDirection, rngState,
1196
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
1197
- enableEnvironmentLight, useEnvMapIS,
1198
- );
1199
- If( rrSurvivalProb.lessThanEqual( 0.0 ), () => {
1200
-
1201
- Break();
1202
-
1203
- } );
1204
- // Apply throughput compensation
1205
- throughput.divAssign( rrSurvivalProb );
1206
-
1207
- // Increment effective bounces
1208
- effectiveBounces.addAssign( 1 );
1209
-
1210
- // Reset per-bounce caches so next iteration recomputes for its own material
1211
- psWeightsComputed.assign( tslBool( false ) );
1212
- psMaterialCacheCached.assign( tslBool( false ) );
1213
-
1214
- } );
1215
-
1216
- return TraceResult( {
1217
- radiance: vec4( radiance, alpha ),
1218
- objectNormal,
1219
- objectColor,
1220
- objectID,
1221
- firstHitPoint,
1222
- firstHitDistance,
1223
- } );
1224
-
1225
- } );