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
@@ -0,0 +1,873 @@
1
+ /**
2
+ * ShadeKernel.js — wavefront material eval + bounce generation. 256×1 workgroup, 1D dispatch.
3
+ * 10 storage-buffer bindings: bvh, tri, mat, light, ray, rng, hit, gBuffer, counters, activeIndices
4
+ * (at the device per-stage limit of 10; envCDF is a texture, not a storage buffer).
5
+ */
6
+
7
+ import {
8
+ Fn, float, vec2, vec3, vec4, int, uint,
9
+ If, normalize, max, exp, log, clamp, dot, length, select,
10
+ instanceIndex,
11
+ sampler,
12
+ atomicAdd, atomicLoad, uintBitsToFloat,
13
+ Return,
14
+ } from 'three/tsl';
15
+
16
+ import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, getGroundProjectedDirection } from './Environment.js';
17
+ import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial } from './Common.js';
18
+ import { sampleAllMaterialTextures } from './TextureSampling.js';
19
+ import { evaluateMaterialResponse } from './MaterialEvaluation.js';
20
+ import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
21
+ import { traceShadowRay, calculateRayOffset } from './LightsDirect.js';
22
+ import { traverseBVHShadow } from './BVHTraversal.js';
23
+ import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
24
+ import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
25
+ import { calculateIndirectLighting } from './LightsIndirect.js';
26
+ import { IndirectLightingResult } from './LightsCore.js';
27
+ import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
28
+ import { getImportanceSamplingInfo } from './MaterialProperties.js';
29
+ import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
30
+ import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
31
+ import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
32
+ import { sampleLightBVHTriangle } from './LightBVHSampling.js';
33
+ import {
34
+ Ray,
35
+ HitInfo,
36
+ RayTracingMaterial,
37
+ MaterialSamples,
38
+ DirectionSample,
39
+ ImportanceSamplingInfo,
40
+ MaterialClassification,
41
+ BRDFWeights,
42
+ MaterialCache,
43
+ } from './Struct.js';
44
+ import { RandomValue, getRandomSample } from './Random.js';
45
+ import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
46
+ import {
47
+ readRayOrigin, readRayDirection, readRayBounceFlags, readRayThroughput, readRayPdf,
48
+ readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
49
+ readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
50
+ readHitDistance, readHitBarycentrics, readHitNormal,
51
+ readHitMaterialIndex, readHitTriangleIndex,
52
+ writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
53
+ writeGBuffer, readGBuffer, gbDecodeNormalDepth,
54
+ readRayRadiance,
55
+ } from '../Processor/PackedRayBuffer.js';
56
+
57
+ const WG_SIZE = 256;
58
+ const MISS_DIST = 1e19;
59
+
60
+ export function buildShadeKernel( params ) {
61
+
62
+ const {
63
+ bvhBuffer, triangleBuffer, materialBuffer,
64
+ envCDFTexture,
65
+ lightBuffer,
66
+ rayBufferRW, rngBufferRW, hitBufferRO, gBufferRW,
67
+ counters,
68
+ activeIndicesRO,
69
+ albedoMaps, normalMaps, bumpMaps,
70
+ metalnessMaps, roughnessMaps, emissiveMaps,
71
+ displacementMaps,
72
+ envTexture, environmentIntensity, envMatrix,
73
+ enableEnvironmentLight, useEnvMapIS,
74
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
75
+ envTotalSum, envCompensationDelta, envResolution,
76
+ directionalLightsBuffer, numDirectionalLights,
77
+ areaLightsBuffer, numAreaLights,
78
+ pointLightsBuffer, numPointLights,
79
+ spotLightsBuffer, numSpotLights,
80
+ maxBounceCount, maxSubsurfaceSteps,
81
+ currentBounce, // loop iteration = path length (advances on free bounces); drives RR/firefly/giScale
82
+ transparentBackground, backgroundIntensity, showBackground,
83
+ globalIlluminationIntensity,
84
+ cameraProjectionMatrix, cameraViewMatrix,
85
+ fireflyThreshold, frame, resolution,
86
+ emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
87
+ emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
88
+ lightBVHNodeCount,
89
+ maxRayCount,
90
+ } = params;
91
+
92
+ const useEmissiveNEE = lightBuffer !== undefined;
93
+
94
+ const computeFn = Fn( () => {
95
+
96
+ const threadIdx = instanceIndex;
97
+
98
+ // bound on ENTERING_COUNT so an over-sized margin dispatch is safe
99
+ const bound = counters ? atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) : maxRayCount;
100
+ If( threadIdx.greaterThanEqual( bound ), () => {
101
+
102
+ Return();
103
+
104
+ } );
105
+
106
+ const rayID = activeIndicesRO.element( threadIdx );
107
+
108
+ const flags = readRayBounceFlags( rayBufferRW, rayID ).toVar();
109
+
110
+ If( flags.bitAnd( uint( RAY_FLAG.ACTIVE ) ).equal( uint( 0 ) ), () => {
111
+
112
+ Return();
113
+
114
+ } );
115
+
116
+ const origin = readRayOrigin( rayBufferRW, rayID ).toVar();
117
+ const direction = readRayDirection( rayBufferRW, rayID ).toVar();
118
+ const throughput = readRayThroughput( rayBufferRW, rayID ).toVar();
119
+ const currentRadiance = readRayRadiance( rayBufferRW, rayID ).toVar();
120
+ // pixelIndex + sampleIndex are derived from rayID (= subSample*maxRaysPerSample + pixelIndex; GenerateKernel.js:64), not stored.
121
+ const maxRaysPerSample = uint( resolution.x ).mul( uint( resolution.y ) ).toVar();
122
+ const pixelIndex = rayID.mod( maxRaysPerSample );
123
+ const rngState = rngBufferRW.element( rayID ).toVar();
124
+
125
+ const hitDist = readHitDistance( hitBufferRO, rayID ).toVar();
126
+ const hitNormal = readHitNormal( hitBufferRO, rayID ).toVar();
127
+ // hitInfo.uv is the interpolated texture UV (not barycentrics)
128
+ const hitUV = readHitBarycentrics( hitBufferRO, rayID ).toVar();
129
+ const hitMatIdx = readHitMaterialIndex( hitBufferRO, rayID ).toVar();
130
+ const hitTriIdx = readHitTriangleIndex( hitBufferRO, rayID ).toVar();
131
+
132
+ // per-ray camera-bounce depth — advances ONLY on opaque scatter (free bounces don't); drives termination (maxBounces). Megakernel: effectiveBounces.
133
+ const cameraDepth = readPathBounces( rayBufferRW, rayID ).toVar();
134
+ // path length = loop iteration (advances every bounce incl. transmissive/SSS); drives RR/firefly/giScale/MIS. Megakernel: loop counter i.
135
+ const bounceIndex = int( currentBounce ).toVar();
136
+ const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
137
+ const sampleIndex = int( rayID.div( maxRaysPerSample ) ).toVar();
138
+
139
+ If( hitDist.greaterThan( MISS_DIST ), () => {
140
+
141
+ If( enableEnvironmentLight, () => {
142
+
143
+ // Ground projection bends the primary ray's background lookup onto a
144
+ // projected sphere+disk so the lower env hemisphere reads as a ground
145
+ // plane. Primary ray only; secondary bounces see the raw envmap as a light.
146
+ const envDir = direction.toVar();
147
+ If( bounceIndex.equal( 0 ).and( groundProjectionEnabled ), () => {
148
+
149
+ envDir.assign( getGroundProjectedDirection(
150
+ origin, direction, groundProjectionRadius, groundProjectionHeight,
151
+ ) );
152
+
153
+ } );
154
+
155
+ const envColor = sampleEnvironment( {
156
+ tex: envTexture,
157
+ samp: sampler( envTexture ),
158
+ direction: envDir,
159
+ environmentMatrix: envMatrix,
160
+ environmentIntensity,
161
+ enableEnvironmentLight,
162
+ } ).toVar();
163
+
164
+ // Hide the background for primary rays when showBackground is off; secondary bounces still see the envmap as a light.
165
+ If( bounceIndex.equal( 0 ).and( showBackground.not() ), () => {
166
+
167
+ envColor.assign( vec4( 0.0 ) );
168
+
169
+ } );
170
+
171
+ // MIS weight for implicit env hit — prevents double-counting with NEE
172
+ const envMisWeight = float( 1.0 ).toVar();
173
+ If( bounceIndex.greaterThan( 0 ).and( useEnvMapIS ), () => {
174
+
175
+ const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
176
+ If( prevBouncePdf.greaterThan( 0.0 ), () => {
177
+
178
+ const envEval = sampleEquirect(
179
+ envTexture, direction, envMatrix, envTotalSum, envCompensationDelta, envResolution,
180
+ );
181
+ const envPdf = envEval.w;
182
+ If( envPdf.greaterThan( 0.0 ), () => {
183
+
184
+ envMisWeight.assign( balanceHeuristic( { pdf1: prevBouncePdf, pdf2: envPdf } ) ); // megakernel parity (PathTracerCore.js:774): env NEE also uses balance
185
+
186
+ } );
187
+
188
+ } );
189
+
190
+ } );
191
+
192
+ const envGiScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
193
+ const envScale = select( bounceIndex.equal( 0 ), backgroundIntensity, envMisWeight.mul( envGiScale ) );
194
+
195
+ // Firefly-suppress the env contribution (megakernel parity: PathTracerCore.js:780). Without
196
+ // this, indirect bounces escaping to a bright environment are unsuppressed spikes that OIDN
197
+ // smears into white blobs. The miss branch Return()s before the hit-branch clamp (~line 712),
198
+ // so it must be applied here.
199
+ currentRadiance.assign( vec4(
200
+ currentRadiance.xyz.add(
201
+ regularizePathContribution(
202
+ throughput.mul( envColor.xyz ).mul( envScale ),
203
+ float( bounceIndex ), fireflyThreshold, int( frame ),
204
+ ),
205
+ ),
206
+ currentRadiance.w
207
+ ) );
208
+
209
+ } );
210
+
211
+ // Transparent-bg alpha: see-through only if the ray escaped WITHOUT ever hitting opaque
212
+ // geometry (megakernel parity: PathTracerCore.js:784). A secondary bounce off an opaque
213
+ // surface that escapes to env keeps alpha 1 (HAS_HIT_OPAQUE set), so glass-in-front-of-an-
214
+ // object stays opaque while glass-in-front-of-sky exports see-through.
215
+ If( transparentBackground.and( flags.bitAnd( uint( RAY_FLAG.HAS_HIT_OPAQUE ) ).equal( uint( 0 ) ) ), () => {
216
+
217
+ currentRadiance.w.assign( 0.0 );
218
+
219
+ } );
220
+
221
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
222
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
223
+ Return();
224
+
225
+ } );
226
+
227
+ const hitPoint = origin.add( direction.mul( hitDist ) ).toVar();
228
+ const N = normalize( hitNormal ).toVar();
229
+
230
+ // medium stack read once here; reused by the transparency block below
231
+ const medStack = readMediumStack( rayBufferRW, rayID );
232
+ const mediumStackDepth = int( medStack.stackDepth ).toVar();
233
+ const mediumStack_ior_1 = medStack.ior1.toVar();
234
+ const mediumStack_ior_2 = medStack.ior2.toVar();
235
+ const mediumStack_ior_3 = medStack.ior3.toVar();
236
+ const transTraversals = int( medStack.transTraversals ).toVar();
237
+ // per-ray locked dispersion wavelength (nm; 0 = achromatic), in medium-stack bits 16-31
238
+ const pathWavelength = float( medStack.wavelength ).toVar();
239
+
240
+ // in-medium transport: glass (sigmaS==0) absorbs, subsurface (sigmaS>0) random-walk scatters
241
+ If( mediumStackDepth.greaterThan( 0 ), () => {
242
+
243
+ const mSigmaA = readMediumSigmaA( rayBufferRW, rayID ).toVar();
244
+ const sssMed = readSSSMedium( rayBufferRW, rayID );
245
+ const mSigmaS = sssMed.sigmaS.toVar();
246
+ const mG = sssMed.g.toVar();
247
+
248
+ If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), () => {
249
+
250
+ // glass: Beer-Lambert absorption
251
+ throughput.mulAssign( exp( mSigmaA.mul( hitDist ).negate() ) );
252
+
253
+ } ).Else( () => {
254
+
255
+ // subsurface: chromatic collision-distance sampling
256
+ const mSigmaT = mSigmaA.add( mSigmaS );
257
+ const coll = CollisionSample.wrap( sampleChromaticCollision(
258
+ mSigmaT, mSigmaS, throughput, hitDist, rngState,
259
+ ) ).toVar();
260
+ throughput.mulAssign( coll.weight );
261
+
262
+ If( coll.didScatter, () => {
263
+
264
+ // scatter via Henyey-Greenstein, continue as a free bounce off the sssSteps budget
265
+ const xi2 = vec2( RandomValue( rngState ), RandomValue( rngState ) );
266
+ const scatterPoint = origin.add( direction.mul( coll.t ) );
267
+ const newDir = sampleHenyeyGreenstein( direction, mG, xi2 ).toVar();
268
+ sssSteps.addAssign( 1 );
269
+
270
+ // terminate walk: step cap or Russian roulette
271
+ const rrP = clamp( max( max( throughput.x, throughput.y ), throughput.z ), 0.02, 1.0 ).toVar();
272
+ const terminate = sssSteps.greaterThanEqual( maxSubsurfaceSteps )
273
+ .or( RandomValue( rngState ).greaterThan( rrP ) ).toVar();
274
+
275
+ If( terminate, () => {
276
+
277
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
278
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
279
+ rngBufferRW.element( rayID ).assign( rngState );
280
+ Return();
281
+
282
+ } );
283
+
284
+ throughput.divAssign( rrP );
285
+
286
+ // free-bounce continuation: ray stays in the same medium, so medium stack + coeffs persist
287
+ writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps );
288
+ writeRayDirFlags( rayBufferRW, rayID, newDir, flags );
289
+ // Free bounce: preserve prevBouncePdf (megakernel leaves it untouched across SSS scatter,
290
+ // PathTracerCore.js:1272 sets it only after an opaque scatter). Writing 1.0 here spuriously
291
+ // fires the next hit's env/emissive MIS, down-weighting SSS-then-env/emitter views.
292
+ writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
293
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
294
+ rngBufferRW.element( rayID ).assign( rngState );
295
+ Return();
296
+
297
+ } );
298
+
299
+ // no scatter: reached boundary, fall through to surface handling
300
+
301
+ } );
302
+
303
+ } );
304
+
305
+ const material = RayTracingMaterial.wrap(
306
+ getMaterial( int( hitMatIdx ), materialBuffer )
307
+ ).toVar();
308
+
309
+ // displacement: analytical ray-height marching refines hitPoint/UV/normal; no-op without a map
310
+ const samplingUV = hitUV.toVar();
311
+ const displacedNormal = N.toVar();
312
+ If(
313
+ material.displacementMapIndex.greaterThanEqual( int( 0 ) )
314
+ .and( material.displacementScale.greaterThan( 0.0 ) ),
315
+ () => {
316
+
317
+ const dispRay = Ray( { origin, direction } );
318
+ const dispHit = HitInfo( {
319
+ didHit: true, dst: hitDist, hitPoint, normal: N, uv: hitUV,
320
+ materialIndex: int( hitMatIdx ), meshIndex: int( 0 ),
321
+ triangleIndex: int( hitTriIdx ),
322
+ boxTests: int( 0 ), triTests: int( 0 ),
323
+ } );
324
+ const dispResult = DisplacementResult.wrap( refineDisplacedIntersection(
325
+ dispRay, dispHit, triangleBuffer, displacementMaps, material, bounceIndex,
326
+ ) ).toVar();
327
+ samplingUV.assign( dispResult.uv );
328
+ displacedNormal.assign( dispResult.normal );
329
+ hitPoint.assign( dispResult.hitPoint );
330
+
331
+ }
332
+ );
333
+
334
+ const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
335
+ albedoMaps, normalMaps, bumpMaps,
336
+ metalnessMaps, roughnessMaps, emissiveMaps,
337
+ material, samplingUV, N,
338
+ ) ).toVar();
339
+
340
+ // BRDF functions read material.color/metalness/roughness, so apply samples here
341
+ material.color.assign( matSamples.albedo );
342
+ material.metalness.assign( matSamples.metalness.clamp( 0.0, 1.0 ) );
343
+ material.roughness.assign( matSamples.roughness.clamp( 0.05, 1.0 ) );
344
+ material.sheenRoughness.assign( material.sheenRoughness.clamp( 0.05, 1.0 ) ); // megakernel parity (PathTracerCore.js:1060): sample/PDF mismatch at sheenRoughness~0
345
+
346
+ const albedo = matSamples.albedo.toVar();
347
+ If(
348
+ material.displacementMapIndex.greaterThanEqual( int( 0 ) )
349
+ .and( material.displacementScale.greaterThan( 0.0 ) ),
350
+ () => {
351
+
352
+ N.assign( normalize( displacedNormal.add( matSamples.normal.sub( normalize( hitNormal ) ) ) ) );
353
+
354
+ }
355
+ ).Else( () => {
356
+
357
+ N.assign( matSamples.normal );
358
+
359
+ } );
360
+
361
+ // first-hit MRT data (bounce 0 only)
362
+ If( bounceIndex.equal( 0 ), () => {
363
+
364
+ const linearDepth = computeNDCDepth( {
365
+ worldPos: hitPoint,
366
+ cameraProjectionMatrix,
367
+ cameraViewMatrix,
368
+ } );
369
+ // G-buffer is per-pixel — only sub-sample 0 writes it (FinalWrite reads sub-sample 0). writeGBuffer half-packs (normal/depth/albedo).
370
+ // Write the primary DEPTH now with the miss-default aux; the real normal/albedo are captured below
371
+ // (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
372
+ // block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
373
+ // (objectNormal/objectColor stay at their init for transmissive-then-miss).
374
+ If( sampleIndex.equal( int( 0 ) ), () => {
375
+
376
+ writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
377
+
378
+ } );
379
+
380
+ } );
381
+
382
+ // transparency / refraction (medium stack + wavelength read at the hit, above)
383
+ const currentMediumIOR = float( 1.0 ).toVar();
384
+ const previousMediumIOR = float( 1.0 ).toVar();
385
+ If( mediumStackDepth.equal( 1 ), () => {
386
+
387
+ currentMediumIOR.assign( mediumStack_ior_1 );
388
+
389
+ } ).ElseIf( mediumStackDepth.equal( 2 ), () => {
390
+
391
+ currentMediumIOR.assign( mediumStack_ior_2 );
392
+ previousMediumIOR.assign( mediumStack_ior_1 );
393
+
394
+ } ).ElseIf( mediumStackDepth.equal( 3 ), () => {
395
+
396
+ currentMediumIOR.assign( mediumStack_ior_3 );
397
+ previousMediumIOR.assign( mediumStack_ior_2 );
398
+
399
+ } );
400
+
401
+ const currentRay = Ray( { origin, direction } );
402
+ const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
403
+ currentRay, N, material, rngState,
404
+ int( transTraversals ),
405
+ currentMediumIOR, previousMediumIOR,
406
+ pathWavelength,
407
+ ) ).toVar();
408
+
409
+ // persist any wavelength locked on a fresh dispersive transmission; identity write otherwise
410
+ pathWavelength.assign( interaction.pathWavelength );
411
+
412
+ If( interaction.continueRay, () => {
413
+
414
+ // update medium stack for transmission (not reflection/TIR)
415
+ If( interaction.isTransmissive.and( interaction.didReflect.not() ), () => {
416
+
417
+ If( interaction.entering, () => {
418
+
419
+ If( mediumStackDepth.lessThan( 3 ), () => {
420
+
421
+ mediumStackDepth.addAssign( 1 );
422
+ If( mediumStackDepth.equal( 1 ), () => {
423
+
424
+ mediumStack_ior_1.assign( material.ior );
425
+
426
+ } );
427
+ If( mediumStackDepth.equal( 2 ), () => {
428
+
429
+ mediumStack_ior_2.assign( material.ior );
430
+
431
+ } );
432
+ If( mediumStackDepth.equal( 3 ), () => {
433
+
434
+ mediumStack_ior_3.assign( material.ior );
435
+
436
+ } );
437
+
438
+ // precompute Beer-Lambert sigmaA once at enter
439
+ writeMediumSigmaA( rayBufferRW, rayID, select(
440
+ material.attenuationDistance.greaterThan( 0.0 ),
441
+ log( max( material.attenuationColor, vec3( 0.001 ) ) ).negate().div( material.attenuationDistance ),
442
+ vec3( 0.0 ),
443
+ ) );
444
+ // sigmaS==0 marks glass → in-medium block takes the Beer-Lambert path, not SSS walk
445
+ writeSSSMedium( rayBufferRW, rayID, vec3( 0.0 ), float( 0.0 ) );
446
+
447
+ } );
448
+
449
+ } ).Else( () => {
450
+
451
+ If( mediumStackDepth.greaterThan( 0 ), () => {
452
+
453
+ mediumStackDepth.subAssign( 1 );
454
+
455
+ } );
456
+
457
+ } );
458
+
459
+ } );
460
+
461
+ // subsurface boundary: push the scattering medium on enter, pop on exit; free bounce
462
+ If( interaction.isSubsurface.and( interaction.didReflect.not() ), () => {
463
+
464
+ If( interaction.entering, () => {
465
+
466
+ If( mediumStackDepth.lessThan( 3 ), () => {
467
+
468
+ mediumStackDepth.addAssign( 1 );
469
+ If( mediumStackDepth.equal( 1 ), () => {
470
+
471
+ mediumStack_ior_1.assign( material.ior );
472
+
473
+ } );
474
+ If( mediumStackDepth.equal( 2 ), () => {
475
+
476
+ mediumStack_ior_2.assign( material.ior );
477
+
478
+ } );
479
+ If( mediumStackDepth.equal( 3 ), () => {
480
+
481
+ mediumStack_ior_3.assign( material.ior );
482
+
483
+ } );
484
+
485
+ const ssCoeffs = MediumCoeffs.wrap( subsurfaceCoefficients(
486
+ material.subsurfaceColor, material.subsurfaceRadius, material.subsurfaceRadiusScale,
487
+ ) ).toVar();
488
+ // Store extinction−scattering (un-clamped) so the SSS read reconstructs the true sigmaT=1/r
489
+ // (mSigmaA+mSigmaS); the clamped ssCoeffs.sigmaA loses it when subsurfaceColor>1. Equals sigmaA for color≤1.
490
+ writeMediumSigmaA( rayBufferRW, rayID, ssCoeffs.sigmaT.sub( ssCoeffs.sigmaS ) );
491
+ writeSSSMedium( rayBufferRW, rayID, ssCoeffs.sigmaS, clamp( material.subsurfaceAnisotropy, - 0.99, 0.99 ) );
492
+
493
+ } );
494
+
495
+ } ).Else( () => {
496
+
497
+ If( mediumStackDepth.greaterThan( 0 ), () => {
498
+
499
+ mediumStackDepth.subAssign( 1 );
500
+
501
+ } );
502
+
503
+ } );
504
+
505
+ } );
506
+
507
+ If( interaction.isTransmissive.and( transTraversals.greaterThan( 0 ) ), () => {
508
+
509
+ transTraversals.subAssign( 1 );
510
+
511
+ } );
512
+
513
+ throughput.mulAssign( interaction.throughput );
514
+
515
+ // reflection stays on same side, transmission pushes through
516
+ const reflectOffsetDir = select( interaction.entering, N, N.negate() );
517
+ const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
518
+ const newOrigin = hitPoint.add( offsetDir.mul( 0.001 ) );
519
+
520
+ // SSS = free bounce (depth unchanged); transmission advances camera-bounce depth.
521
+ // Transmissive / alpha-skip / SSS-boundary are all FREE bounces — they do NOT advance camera depth (megakernel parity, gap #4). cameraDepth advances only on opaque scatter (below).
522
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps );
523
+ writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
524
+ // Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
525
+ // transmission/alpha-skip/SSS-boundary). Writing 1.0 corrupts the next bounce's env/emissive MIS,
526
+ // down-weighting environment/emitters seen through glass.
527
+ writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
528
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
529
+ writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
530
+ rngBufferRW.element( rayID ).assign( rngState );
531
+ Return();
532
+
533
+ } );
534
+
535
+ // Past the transparency block ⇒ the ray hit non-transmissive geometry (megakernel parity:
536
+ // PathTracerCore.js:1042). Flag the chain so a later env-escape keeps alpha 1 (the gate in the
537
+ // miss branch). Alpha itself already defaults to 1 from Generate in transparent-bg mode, so there
538
+ // is nothing to set here — a ray dying inside geometry (SSS walk) stays solid without reaching this.
539
+ flags.assign( flags.bitOr( uint( RAY_FLAG.HAS_HIT_OPAQUE ) ) );
540
+
541
+ const emissive = matSamples.emissive.toVar();
542
+ If( length( emissive ).greaterThan( 0.0 ), () => {
543
+
544
+ const emissiveGiScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
545
+
546
+ // MIS weight vs emissive-triangle NEE (megakernel parity: PathTracerCore.js:1117). On a secondary
547
+ // hit (bounceIndex>0) the prior bounce's NEE also sampled this emitter — power-heuristic balances the
548
+ // two estimators. Without it emissive geometry / area lights double-count (~2x bright + noisier).
549
+ // Primary hits keep weight 1.0 (the wavefront's bounce-0 stored pdf is the Generate init, not a NEE pdf).
550
+ const emissiveMISWeight = float( 1.0 ).toVar();
551
+ if ( useEmissiveNEE ) {
552
+
553
+ If( enableEmissiveTriangleSampling.equal( int( 1 ) )
554
+ .and( emissiveTriangleCount.greaterThan( int( 0 ) ) )
555
+ .and( bounceIndex.greaterThan( 0 ) ), () => {
556
+
557
+ const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
558
+ If( prevBouncePdf.greaterThan( 0.0 ), () => {
559
+
560
+ const lightPdf = calculateEmissiveLightPdf(
561
+ int( hitTriIdx ), hitDist, direction, origin,
562
+ triangleBuffer, materialBuffer, emissiveTotalPower,
563
+ );
564
+ emissiveMISWeight.assign( powerHeuristic( { pdf1: prevBouncePdf, pdf2: lightPdf } ) );
565
+
566
+ } );
567
+
568
+ } );
569
+
570
+ }
571
+
572
+ currentRadiance.assign( vec4(
573
+ currentRadiance.xyz.add(
574
+ regularizePathContribution(
575
+ emissive.mul( throughput ).mul( emissiveGiScale ).mul( emissiveMISWeight ),
576
+ float( bounceIndex ), fireflyThreshold, int( frame ),
577
+ ),
578
+ ),
579
+ currentRadiance.w
580
+ ) );
581
+
582
+ } );
583
+
584
+ // BRDF sample (needed by both direct + indirect)
585
+ const V = direction.negate().toVar();
586
+
587
+ // Two-sided shading: opaque path only (transmissive/SSS already continued), so this never disturbs
588
+ // dielectric enter/exit. Flip N toward the viewer when back-facing — rescues double-sided / inward-
589
+ // normal imported meshes (GLB/PBRT) that otherwise shade black (NoL collapses). Megakernel: PathTracerCore.js:1054.
590
+ If( dot( N, V ).lessThan( 0.0 ), () => {
591
+
592
+ N.assign( N.negate() );
593
+
594
+ } );
595
+
596
+ // OIDN clean-aux (megakernel parity: PathTracerCore.js:1300): keep overwriting the per-pixel normal/
597
+ // albedo through mirror/glass until the first non-specular hit, so aux describes what's actually
598
+ // visible (the surface reflected in a mirror / seen behind glass), not the specular surface. Glass
599
+ // Returns at the transparency block above, so its aux is replaced by the surface behind it. Depth
600
+ // stays at the primary hit (read back + re-packed; the snorm depth re-pack is idempotent — no drift).
601
+ If( sampleIndex.equal( int( 0 ) ).and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
602
+
603
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
604
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
605
+
606
+ const auxIsMirror = material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) );
607
+ const auxIsTransmissive = material.transmission.greaterThan( 0.5 );
608
+ If( auxIsMirror.or( auxIsTransmissive ).not(), () => {
609
+
610
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
611
+
612
+ } );
613
+
614
+ } );
615
+
616
+ const mc = MaterialClassification.wrap( classifyMaterial(
617
+ material.metalness, material.roughness, material.transmission,
618
+ material.clearcoat, material.emissive, material.subsurface,
619
+ ) ).toVar();
620
+
621
+ // STBN keyed on (pixel, bounceIndex, frame); sampleIndex gives each sub-sample a distinct tap
622
+ const _resX = int( resolution.x ).toVar();
623
+ const _pixelCoord = vec2(
624
+ float( int( pixelIndex ).mod( _resX ) ).add( 0.5 ),
625
+ float( int( pixelIndex ).div( _resX ) ).add( 0.5 ),
626
+ );
627
+ const xi = getRandomSample( _pixelCoord, sampleIndex, bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
628
+ const emptyWeights = BRDFWeights( {
629
+ specular: float( 0.0 ), diffuse: float( 0.0 ), sheen: float( 0.0 ),
630
+ clearcoat: float( 0.0 ), transmission: float( 0.0 ), iridescence: float( 0.0 ),
631
+ } );
632
+ // unused (materialCacheCached=false), but must match the 11-field struct shape to construct
633
+ const emptyCache = 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( 0.5 ), metalFactor: float( 0.5 ),
638
+ iorFactor: float( 0.67 ), maxSheenColor: float( 0.0 ),
639
+ } );
640
+
641
+ const brdfDir = vec3( 0.0 ).toVar();
642
+ const brdfValue = vec3( 0.0 ).toVar();
643
+ const brdfPdf = float( 0.0 ).toVar();
644
+
645
+ If( material.clearcoat.greaterThan( 0.0 ), () => {
646
+
647
+ const ccRay = Ray( { origin, direction } );
648
+ const ccHit = HitInfo( {
649
+ didHit: true, dst: hitDist, hitPoint, normal: N, uv: hitUV,
650
+ materialIndex: int( hitMatIdx ), meshIndex: int( 0 ),
651
+ triangleIndex: int( 0 ), boxTests: int( 0 ), triTests: int( 0 ),
652
+ } );
653
+ const ccResult = ClearcoatResult.wrap( sampleClearcoat(
654
+ ccRay, ccHit, material, xi, rngState,
655
+ ) );
656
+ brdfDir.assign( ccResult.L );
657
+ brdfValue.assign( ccResult.brdf );
658
+ brdfPdf.assign( ccResult.pdf );
659
+
660
+ } ).Else( () => {
661
+
662
+ const bs = DirectionSample.wrap( generateSampledDirection(
663
+ V, N, material, xi, rngState,
664
+ mc,
665
+ false, emptyWeights,
666
+ false, emptyCache,
667
+ ) );
668
+ brdfDir.assign( bs.direction );
669
+ brdfValue.assign( bs.value );
670
+ brdfPdf.assign( bs.pdf );
671
+
672
+ } );
673
+
674
+ const directLight = calculateDirectLightingUnified(
675
+ hitPoint, N, material, V,
676
+ brdfDir, brdfPdf, brdfValue,
677
+ bounceIndex, rngState,
678
+ directionalLightsBuffer, numDirectionalLights,
679
+ areaLightsBuffer, numAreaLights,
680
+ pointLightsBuffer, numPointLights,
681
+ spotLightsBuffer, numSpotLights,
682
+ bvhBuffer, triangleBuffer, materialBuffer,
683
+ envTexture, environmentIntensity, envMatrix,
684
+ envCDFTexture,
685
+ envTotalSum, envCompensationDelta, envResolution,
686
+ enableEnvironmentLight,
687
+ );
688
+
689
+ const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
690
+ // Per-term firefly suppression (megakernel parity: PathTracerCore.js:1164) — wrap the direct-light add
691
+ // like every other contribution (env/emissive-hit/emissive-NEE). This replaces the cumulative catch-all
692
+ // that re-suppressed already-wrapped terms + prior-bounce radiance — suppress(a+b) ≠ suppress(a)+suppress(b) (gap #13).
693
+ currentRadiance.assign( vec4(
694
+ currentRadiance.xyz.add(
695
+ regularizePathContribution(
696
+ throughput.mul( directLight ).mul( giScale ),
697
+ float( bounceIndex ), fireflyThreshold, int( frame ),
698
+ ),
699
+ ),
700
+ currentRadiance.w
701
+ ) );
702
+
703
+ // emissive triangle NEE: light-BVH fast path when available, flat-CDF fallback otherwise
704
+ if ( useEmissiveNEE ) {
705
+
706
+ If(
707
+ enableEmissiveTriangleSampling.equal( int( 1 ) )
708
+ .and( emissiveTriangleCount.greaterThan( int( 0 ) ) ),
709
+ () => {
710
+
711
+ // closes over scene buffers for the inner shadow-trace callback
712
+ const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist ] ) => {
713
+
714
+ return traceShadowRay(
715
+ origin, dir, maxDist,
716
+ traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer,
717
+ );
718
+
719
+ } );
720
+
721
+ If( lightBVHNodeCount.greaterThan( int( 0 ) ), () => {
722
+
723
+ const emissiveSample = EmissiveSample.wrap( sampleLightBVHTriangle(
724
+ hitPoint, N,
725
+ rngState,
726
+ lightBuffer,
727
+ lightBuffer,
728
+ emissiveVec4Offset,
729
+ triangleBuffer,
730
+ ) );
731
+
732
+ // skip rough diffuse surfaces on secondary bounces
733
+ const skip = bounceIndex.greaterThan( int( 1 ) )
734
+ .and( material.roughness.greaterThan( 0.9 ) )
735
+ .and( material.metalness.lessThan( 0.1 ) );
736
+
737
+ If( skip.not().and( emissiveSample.valid ).and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
738
+
739
+ const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
740
+
741
+ If( NoL.greaterThan( 0.0 ), () => {
742
+
743
+ const rayOffset = calculateRayOffset( hitPoint, N, material );
744
+ const rayOrigin = hitPoint.add( rayOffset );
745
+ const shadowDist = emissiveSample.distance.sub( 0.001 );
746
+ const visibility = traceShadowRayWrapped(
747
+ rayOrigin, emissiveSample.direction, shadowDist,
748
+ );
749
+
750
+ If( visibility.greaterThan( 0.0 ), () => {
751
+
752
+ const brdfVal = evaluateMaterialResponse( V, emissiveSample.direction, N, material );
753
+ const bPdf = calculateMaterialPDF( V, emissiveSample.direction, N, material );
754
+ const misW = select(
755
+ bPdf.greaterThan( 0.0 ),
756
+ powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: bPdf } ),
757
+ float( 1.0 ),
758
+ );
759
+
760
+ const emissiveLight = emissiveSample.emission
761
+ .mul( brdfVal ).mul( NoL )
762
+ .div( emissiveSample.pdf )
763
+ .mul( visibility ).mul( emissiveBoost ).mul( misW );
764
+
765
+ currentRadiance.assign( vec4(
766
+ currentRadiance.xyz.add(
767
+ regularizePathContribution(
768
+ emissiveLight.mul( throughput ).mul( giScale ),
769
+ float( bounceIndex ), fireflyThreshold, int( frame ),
770
+ ),
771
+ ),
772
+ currentRadiance.w,
773
+ ) );
774
+
775
+ } );
776
+
777
+ } );
778
+
779
+ } );
780
+
781
+ } ).Else( () => {
782
+
783
+ const emissiveLight = calculateEmissiveTriangleContribution(
784
+ hitPoint, N, V, material,
785
+ bounceIndex, rngState,
786
+ emissiveBoost,
787
+ lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
788
+ triangleBuffer,
789
+ traceShadowRayWrapped,
790
+ calculateRayOffset,
791
+ );
792
+
793
+ currentRadiance.assign( vec4(
794
+ currentRadiance.xyz.add(
795
+ regularizePathContribution(
796
+ emissiveLight.mul( throughput ).mul( giScale ),
797
+ float( bounceIndex ), fireflyThreshold, int( frame ),
798
+ ),
799
+ ),
800
+ currentRadiance.w,
801
+ ) );
802
+
803
+ } );
804
+
805
+ },
806
+ );
807
+
808
+ }
809
+
810
+ // (gap #13) No cumulative catch-all here: every radiance contribution above is now firefly-suppressed
811
+ // per-term (env / emissive-hit / direct-light / emissive-NEE), matching the megakernel which never
812
+ // re-suppresses the running radiance.
813
+
814
+ const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
815
+ material, bounceIndex, mc,
816
+ ) ).toVar();
817
+
818
+ const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
819
+ V, N, material,
820
+ brdfDir, brdfPdf, brdfValue,
821
+ rngState, samplingInfo,
822
+ ) ).toVar();
823
+
824
+ const bounceDir = indirectResult.direction.toVar();
825
+ // combinedPdf is stored as next bounce's prevBouncePdf for NEE↔implicit-env MIS
826
+ const bouncePdf = max( indirectResult.combinedPdf, 0.001 ).toVar();
827
+ throughput.mulAssign( indirectResult.throughput );
828
+
829
+ // Adaptive Russian roulette (gap #7) — material-importance + throughput + env-direction aware, replacing
830
+ // the flat clamp(maxThroughput,0.05,0.95). depth = bounceIndex (path length, per gap #4); rayDirection =
831
+ // the continuation dir (bounceDir) for env-facing importance. Returns survival prob (compensated) or 0 to
832
+ // terminate. Subsumes the old compensated low-throughput kill (#12). Unbiased; just terminates smarter.
833
+ const rrSurvival = handleRussianRoulette(
834
+ bounceIndex, throughput, mc, bounceDir, rngState,
835
+ enableEnvironmentLight, useEnvMapIS,
836
+ ).toVar();
837
+ If( rrSurvival.lessThanEqual( 0.0 ), () => {
838
+
839
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
840
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
841
+ rngBufferRW.element( rayID ).assign( rngState );
842
+ Return();
843
+
844
+ } );
845
+ throughput.divAssign( rrSurvival );
846
+
847
+ // Terminate on CAMERA depth (opaque scatter count), not path length — glass/SSS free bounces no longer burn the maxBounces budget (gap #4).
848
+ If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
849
+
850
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
851
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
852
+ rngBufferRW.element( rayID ).assign( rngState );
853
+ Return();
854
+
855
+ } );
856
+
857
+ const newOrigin = hitPoint.add( N.mul( 0.001 ) );
858
+
859
+ // Opaque scatter: the only bounce that advances camera depth.
860
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps );
861
+ writeRayDirFlags( rayBufferRW, rayID, bounceDir, flags );
862
+ writeRayThroughputPdf( rayBufferRW, rayID, throughput, bouncePdf );
863
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
864
+ writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
865
+ rngBufferRW.element( rayID ).assign( rngState );
866
+
867
+ } );
868
+
869
+ return computeFn;
870
+
871
+ }
872
+
873
+ export { WG_SIZE as SHADE_WG_SIZE };