rayzee 6.5.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.
- package/README.md +24 -5
- package/dist/rayzee.es.js +7554 -7014
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +157 -236
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +12 -9
- package/src/PathTracerApp.js +118 -26
- package/src/Pipeline/PipelineContext.js +1 -2
- package/src/Pipeline/RenderPipeline.js +1 -1
- package/src/Pipeline/RenderStage.js +1 -1
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/GeometryExtractor.js +6 -0
- package/src/Processor/KernelManager.js +277 -0
- package/src/Processor/PackedRayBuffer.js +265 -0
- package/src/Processor/QueueManager.js +173 -0
- package/src/Processor/SceneProcessor.js +1 -0
- package/src/Processor/ShaderBuilder.js +11 -317
- package/src/Processor/StorageTexturePool.js +29 -15
- package/src/Processor/VRAMTracker.js +169 -0
- package/src/Processor/utils.js +11 -110
- package/src/RenderSettings.js +0 -3
- package/src/Stages/ASVGF.js +76 -20
- package/src/Stages/BilateralFilter.js +34 -10
- package/src/Stages/EdgeFilter.js +2 -3
- package/src/Stages/MotionVector.js +16 -9
- package/src/Stages/NormalDepth.js +17 -5
- package/src/Stages/PathTracer.js +671 -1456
- package/src/Stages/PathTracerStage.js +1451 -0
- package/src/Stages/SSRC.js +32 -15
- package/src/Stages/Variance.js +35 -12
- package/src/TSL/CompactKernel.js +110 -0
- package/src/TSL/DebugKernel.js +98 -0
- package/src/TSL/Environment.js +13 -11
- package/src/TSL/ExtendKernel.js +75 -0
- package/src/TSL/FinalWriteKernel.js +121 -0
- package/src/TSL/GenerateKernel.js +109 -0
- package/src/TSL/LightsSampling.js +2 -2
- package/src/TSL/PathTracerCore.js +43 -1039
- package/src/TSL/ShadeKernel.js +873 -0
- package/src/TSL/patches.js +81 -4
- package/src/index.js +3 -0
- package/src/managers/CameraManager.js +1 -1
- package/src/managers/DenoisingManager.js +40 -75
- package/src/managers/EnvironmentManager.js +30 -39
- package/src/managers/OverlayManager.js +7 -22
- package/src/managers/UniformManager.js +0 -3
- package/src/managers/helpers/TileHelper.js +2 -2
- package/src/Stages/AdaptiveSampling.js +0 -483
- package/src/TSL/PathTracer.js +0 -384
- 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 };
|