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