rayzee 7.2.1 → 7.4.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 +2 -3
- package/dist/rayzee.es.js +1517 -1370
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +52 -38
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/EngineDefaults.js +15 -7
- package/src/PathTracerApp.js +24 -2
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/EmissiveTriangleBuilder.js +4 -3
- package/src/Processor/PackedRayBuffer.js +3 -4
- package/src/Processor/QueueManager.js +1 -0
- package/src/RenderSettings.js +16 -2
- package/src/Stages/PathTracer.js +34 -52
- package/src/Stages/PathTracerStage.js +0 -13
- package/src/TSL/Common.js +62 -0
- package/src/TSL/DebugKernel.js +2 -3
- package/src/TSL/Environment.js +22 -1
- package/src/TSL/FinalWriteKernel.js +28 -29
- package/src/TSL/GenerateKernel.js +11 -16
- package/src/TSL/LightsSampling.js +35 -14
- package/src/TSL/ShadeKernel.js +276 -51
- package/src/TSL/Struct.js +8 -0
- package/src/managers/DenoisingManager.js +7 -0
- package/src/managers/UniformManager.js +7 -2
- package/src/managers/VideoRenderManager.js +0 -2
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
Fn, float, vec2, vec3, vec4, int, uint,
|
|
9
|
-
|
|
9
|
+
bool as tslBool,
|
|
10
|
+
If, Loop, normalize, max, exp, log, clamp, dot, length, select,
|
|
10
11
|
instanceIndex,
|
|
11
12
|
sampler,
|
|
12
13
|
atomicAdd, atomicLoad, uintBitsToFloat,
|
|
13
14
|
Return,
|
|
14
15
|
} from 'three/tsl';
|
|
15
16
|
|
|
16
|
-
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect,
|
|
17
|
-
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial } from './Common.js';
|
|
17
|
+
import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
|
|
18
|
+
import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial } from './Common.js';
|
|
19
|
+
import { cosineWeightedSample } from './MaterialSampling.js';
|
|
18
20
|
import { sampleAllMaterialTextures } from './TextureSampling.js';
|
|
19
21
|
import { evaluateMaterialResponse } from './MaterialEvaluation.js';
|
|
20
22
|
import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
|
|
@@ -23,7 +25,7 @@ import { traverseBVHShadow } from './BVHTraversal.js';
|
|
|
23
25
|
import { handleMaterialTransparency, MaterialInteractionResult } from './MaterialTransmission.js';
|
|
24
26
|
import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficients, CollisionSample, MediumCoeffs } from './Subsurface.js';
|
|
25
27
|
import { calculateIndirectLighting } from './LightsIndirect.js';
|
|
26
|
-
import { IndirectLightingResult } from './LightsCore.js';
|
|
28
|
+
import { IndirectLightingResult, sampleCone } from './LightsCore.js';
|
|
27
29
|
import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
|
|
28
30
|
import { getImportanceSamplingInfo } from './MaterialProperties.js';
|
|
29
31
|
import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
MaterialClassification,
|
|
41
43
|
BRDFWeights,
|
|
42
44
|
MaterialCache,
|
|
45
|
+
DirectLightingDual,
|
|
43
46
|
} from './Struct.js';
|
|
44
47
|
import { RandomValue, getRandomSample } from './Random.js';
|
|
45
48
|
import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
@@ -57,6 +60,14 @@ import {
|
|
|
57
60
|
const WG_SIZE = 256;
|
|
58
61
|
const MISS_DIST = 1e19;
|
|
59
62
|
|
|
63
|
+
// Shadow-catcher thresholds (named so the two distinct 1e-4 uses don't read as one value)
|
|
64
|
+
const CATCHER_T_MIN = 1e-4; // min ray-t for the analytic plane hit (skip behind/at the camera)
|
|
65
|
+
const CATCHER_LUMA_FLOOR = 1e-4; // denominator floor for the shadow ratio
|
|
66
|
+
const CATCHER_COVERAGE_MIN = 1e-3; // below this incident luma there is no shadow to catch
|
|
67
|
+
// Reserved G-buffer surface ID for catcher pixels (no real triangle); stable across frames so the
|
|
68
|
+
// denoiser treats the analytic plane as one coherent surface, not a background miss.
|
|
69
|
+
const CATCHER_SURFACE_ID = 0xFFFFFFFE;
|
|
70
|
+
|
|
60
71
|
export function buildShadeKernel( params ) {
|
|
61
72
|
|
|
62
73
|
const {
|
|
@@ -71,7 +82,8 @@ export function buildShadeKernel( params ) {
|
|
|
71
82
|
displacementMaps,
|
|
72
83
|
envTexture, environmentIntensity, envMatrix,
|
|
73
84
|
enableEnvironmentLight, useEnvMapIS,
|
|
74
|
-
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
|
|
85
|
+
groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
86
|
+
enableGroundCatcher, groundCatcherHeight,
|
|
75
87
|
envTotalSum, envCompensationDelta, envResolution,
|
|
76
88
|
directionalLightsBuffer, numDirectionalLights,
|
|
77
89
|
areaLightsBuffer, numAreaLights,
|
|
@@ -79,7 +91,7 @@ export function buildShadeKernel( params ) {
|
|
|
79
91
|
spotLightsBuffer, numSpotLights,
|
|
80
92
|
maxBounceCount, maxSubsurfaceSteps,
|
|
81
93
|
currentBounce, // loop iteration = path length (advances on free bounces); drives RR/firefly/giScale
|
|
82
|
-
transparentBackground, backgroundIntensity, showBackground,
|
|
94
|
+
transparentBackground, backgroundIntensity, backgroundColor, backgroundBlurriness, backgroundBlurSamples, showBackground,
|
|
83
95
|
globalIlluminationIntensity,
|
|
84
96
|
cameraProjectionMatrix, cameraViewMatrix,
|
|
85
97
|
fireflyThreshold, frame, resolution,
|
|
@@ -87,10 +99,45 @@ export function buildShadeKernel( params ) {
|
|
|
87
99
|
emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
|
|
88
100
|
lightBVHNodeCount, reverseMapVec4Offset,
|
|
89
101
|
maxRayCount,
|
|
102
|
+
// Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Gated by a
|
|
103
|
+
// live uniform (1 = denoiser on) so the wavefront skips these writes when nothing consumes them.
|
|
104
|
+
auxGBufferEnabled,
|
|
90
105
|
} = params;
|
|
91
106
|
|
|
107
|
+
const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
|
|
108
|
+
|
|
92
109
|
const useEmissiveNEE = lightBuffer !== undefined;
|
|
93
110
|
|
|
111
|
+
// Stochastic cone-jitter blur of an env backdrop lookup. Plain JS inliner (NOT a Fn — an rng Fn-param
|
|
112
|
+
// would freeze; see TSL pitfalls) so it mutates the caller's rngState .toVar() directly. Shared by the
|
|
113
|
+
// miss branch and the shadow catcher so their blur stays in lockstep (no horizon seam). normalize() the
|
|
114
|
+
// center direction — ground projection can return a non-unit vector, which would skew sampleCone's basis.
|
|
115
|
+
// Clamp the tap count to ≥1 so a 0 forced via the engine API can't produce a 0/0 NaN backdrop.
|
|
116
|
+
const sampleEnvBlurred = ( centerDir, halfAngle, samples, rng ) => {
|
|
117
|
+
|
|
118
|
+
const axis = normalize( centerDir ).toVar();
|
|
119
|
+
const n = max( samples, int( 1 ) ).toVar();
|
|
120
|
+
const acc = vec3( 0.0 ).toVar();
|
|
121
|
+
Loop( { start: int( 0 ), end: n, type: 'int', condition: '<' }, () => {
|
|
122
|
+
|
|
123
|
+
// per-component .toVar(): vec2(RandomValue, RandomValue) would collapse to u==v (TSL pitfall)
|
|
124
|
+
const u1 = RandomValue( rng ).toVar();
|
|
125
|
+
const u2 = RandomValue( rng ).toVar();
|
|
126
|
+
const jDir = sampleCone( axis, halfAngle, vec2( u1, u2 ) ).toVar();
|
|
127
|
+
acc.addAssign( sampleEnvironment( {
|
|
128
|
+
tex: envTexture,
|
|
129
|
+
samp: sampler( envTexture ),
|
|
130
|
+
direction: jDir,
|
|
131
|
+
environmentMatrix: envMatrix,
|
|
132
|
+
environmentIntensity,
|
|
133
|
+
enableEnvironmentLight: float( 1.0 ),
|
|
134
|
+
} ).xyz );
|
|
135
|
+
|
|
136
|
+
} );
|
|
137
|
+
return acc.div( float( n ) );
|
|
138
|
+
|
|
139
|
+
};
|
|
140
|
+
|
|
94
141
|
const computeFn = Fn( () => {
|
|
95
142
|
|
|
96
143
|
const threadIdx = instanceIndex;
|
|
@@ -113,13 +160,19 @@ export function buildShadeKernel( params ) {
|
|
|
113
160
|
|
|
114
161
|
} );
|
|
115
162
|
|
|
163
|
+
// Backdrop-view = the ray still travels the original camera direction (only alpha/transparent passthrough
|
|
164
|
+
// since the camera, REDIRECTED still clear). Captured at ARRIVAL (before the opaque/redirect bitOr below)
|
|
165
|
+
// so it stays valid for both the miss branch and the emissive-hit scale. This — not bounceIndex==0 — is
|
|
166
|
+
// the correct "is the env/emitter here a direct view" test, so env/emitters through alpha-cutout holes
|
|
167
|
+
// are treated like the open backdrop, not a GI bounce.
|
|
168
|
+
const isBackdropView = flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).equal( uint( 0 ) ).toVar();
|
|
169
|
+
|
|
116
170
|
const origin = readRayOrigin( rayBufferRW, rayID ).toVar();
|
|
117
171
|
const direction = readRayDirection( rayBufferRW, rayID ).toVar();
|
|
118
172
|
const throughput = readRayThroughput( rayBufferRW, rayID ).toVar();
|
|
119
173
|
const currentRadiance = readRayRadiance( rayBufferRW, rayID ).toVar();
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
const pixelIndex = rayID.mod( maxRaysPerSample );
|
|
174
|
+
// One ray per pixel: rayID is the pixel index.
|
|
175
|
+
const pixelIndex = rayID;
|
|
123
176
|
const rngState = rngBufferRW.element( rayID ).toVar();
|
|
124
177
|
|
|
125
178
|
const hitDist = readHitDistance( hitBufferRO, rayID ).toVar();
|
|
@@ -134,43 +187,186 @@ export function buildShadeKernel( params ) {
|
|
|
134
187
|
// path length = loop iteration (advances every bounce incl. transmissive/SSS); drives RR/firefly/giScale/MIS. Megakernel: loop counter i.
|
|
135
188
|
const bounceIndex = int( currentBounce ).toVar();
|
|
136
189
|
const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
|
|
137
|
-
const sampleIndex = int( rayID.div( maxRaysPerSample ) ).toVar();
|
|
138
190
|
|
|
139
|
-
|
|
191
|
+
// ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
|
|
192
|
+
// A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
|
|
193
|
+
// closer than any BVH hit (hitDist is MISS_DIST on sky rays, so the plane wins over the
|
|
194
|
+
// bare environment), shade it as a diffuse Lambertian holdout: output a monochrome shadow
|
|
195
|
+
// ratio in alpha (rgb = 0) so it composites as bg·ratio over a transparent background.
|
|
196
|
+
// Shadows are caught by the EXISTING NEE shadow ray into real geometry — the plane itself
|
|
197
|
+
// never enters the BVH. Secondary bounces ignore it entirely.
|
|
198
|
+
If( enableGroundCatcher.and( bounceIndex.equal( 0 ) ), () => {
|
|
199
|
+
|
|
200
|
+
const dirY = direction.y.toVar();
|
|
201
|
+
If( dirY.abs().greaterThan( float( EPSILON ) ), () => {
|
|
202
|
+
|
|
203
|
+
const tPlane = groundCatcherHeight.sub( origin.y ).div( dirY ).toVar();
|
|
204
|
+
If( tPlane.greaterThan( float( CATCHER_T_MIN ) ).and( tPlane.lessThan( hitDist ) ), () => {
|
|
205
|
+
|
|
206
|
+
const planePoint = origin.add( direction.mul( tPlane ) ).toVar();
|
|
207
|
+
const planeN = vec3( 0.0, 1.0, 0.0 );
|
|
208
|
+
const planeV = direction.negate().toVar();
|
|
209
|
+
const planeMat = RayTracingMaterial.wrap( diffuseGroundMaterial() ).toVar();
|
|
210
|
+
|
|
211
|
+
// Cosine-weighted hemisphere BRDF sample about the plane normal (0,1,0); the helper
|
|
212
|
+
// puts cosθ along N, so bDir.y == cosθ. Per-component .toVar() on the two randoms
|
|
213
|
+
// (vec2(RandomValue,RandomValue) would collapse to u==v — TSL pitfall).
|
|
214
|
+
const u1 = RandomValue( rngState ).toVar();
|
|
215
|
+
const u2 = RandomValue( rngState ).toVar();
|
|
216
|
+
const bDir = cosineWeightedSample( planeN, vec2( u1, u2 ) ).toVar();
|
|
217
|
+
const bPdf = bDir.y.mul( PI_INV ).toVar(); // cosθ / π
|
|
218
|
+
const bVal = vec3( PI_INV ); // albedo(1) / π
|
|
219
|
+
|
|
220
|
+
// Reuse the full NEE estimator; the diffuse BRDF is constant and cancels in the
|
|
221
|
+
// ratio, so this yields an irradiance-weighted shadow density across all lights + env.
|
|
222
|
+
const dual = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
223
|
+
planePoint, planeN, planeMat, planeV,
|
|
224
|
+
bDir, bPdf, bVal,
|
|
225
|
+
bounceIndex, rngState,
|
|
226
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
227
|
+
areaLightsBuffer, numAreaLights,
|
|
228
|
+
pointLightsBuffer, numPointLights,
|
|
229
|
+
spotLightsBuffer, numSpotLights,
|
|
230
|
+
bvhBuffer, triangleBuffer, materialBuffer,
|
|
231
|
+
envTexture, environmentIntensity, envMatrix,
|
|
232
|
+
envCDFTexture,
|
|
233
|
+
envTotalSum, envCompensationDelta, envResolution,
|
|
234
|
+
enableEnvironmentLight,
|
|
235
|
+
tslBool( true ), // wantUnoccluded
|
|
236
|
+
) ).toVar();
|
|
237
|
+
|
|
238
|
+
const lumShad = max( dot( dual.shadowed, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) );
|
|
239
|
+
const lumLit = max( dot( dual.unoccluded, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) ).toVar();
|
|
240
|
+
const ratio = clamp( lumShad.div( max( lumLit, float( CATCHER_LUMA_FLOOR ) ) ), 0.0, 1.0 ).toVar();
|
|
241
|
+
// Coverage gate: where no light reaches the plane there is no shadow to catch —
|
|
242
|
+
// force ratio=1 (no darkening) so unlit ground reads as plain background, not spurious black.
|
|
243
|
+
If( lumLit.lessThan( float( CATCHER_COVERAGE_MIN ) ), () => {
|
|
244
|
+
|
|
245
|
+
ratio.assign( 1.0 );
|
|
246
|
+
|
|
247
|
+
} );
|
|
248
|
+
|
|
249
|
+
// Adaptive output (matches Arnold's scene_background / Cycles shadow catcher): the catcher
|
|
250
|
+
// respects the current background mode instead of forcing transparency.
|
|
251
|
+
// • Transparent background → emit a matte (rgb=0, alpha=1−ratio); the shadow rides alpha
|
|
252
|
+
// and composites as backplate·ratio over an external plate.
|
|
253
|
+
// • Visible background → composite the shadow into RGB: show the environment this
|
|
254
|
+
// camera ray would otherwise see, darkened by the shadow ratio (alpha=1). The HDRI stays
|
|
255
|
+
// visible; at the horizon ratio→1 so the catcher meets the sky with no seam.
|
|
256
|
+
// Sample the background via the SAME shared helper the miss branch uses, so the
|
|
257
|
+
// catcher seamlessly continues the visible environment (with ground projection on it
|
|
258
|
+
// must bend identically, else a bright horizon seam / mismatched ground appears).
|
|
259
|
+
const catcherEnvDir = groundProjectedEnvDir(
|
|
260
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
261
|
+
).toVar();
|
|
262
|
+
// force-enable the sampler (pass 1.0): the visible backdrop is decoupled from env-lighting,
|
|
263
|
+
// so the catcher continues the HDRI even when the environment isn't used as a light.
|
|
264
|
+
// Only sample the env where it's actually shown as the catcher backdrop (showBackground); in
|
|
265
|
+
// color/transparent mode the catcher composites over backgroundColor / alpha, so the (up to
|
|
266
|
+
// N-tap) env work would be discarded. Blur matches the miss branch via the shared helper.
|
|
267
|
+
const envBehind = vec3( 0.0 ).toVar();
|
|
268
|
+
If( showBackground, () => {
|
|
269
|
+
|
|
270
|
+
If( backgroundBlurriness.greaterThan( 0.0 ), () => {
|
|
271
|
+
|
|
272
|
+
envBehind.assign( sampleEnvBlurred( catcherEnvDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
|
|
273
|
+
|
|
274
|
+
} ).Else( () => {
|
|
275
|
+
|
|
276
|
+
envBehind.assign( sampleEnvironment( {
|
|
277
|
+
tex: envTexture,
|
|
278
|
+
samp: sampler( envTexture ),
|
|
279
|
+
direction: catcherEnvDir,
|
|
280
|
+
environmentMatrix: envMatrix,
|
|
281
|
+
environmentIntensity,
|
|
282
|
+
enableEnvironmentLight: float( 1.0 ),
|
|
283
|
+
} ).xyz );
|
|
284
|
+
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
} );
|
|
288
|
+
// Background mode: env image (showBackground) or the solid backgroundColor (color mode).
|
|
289
|
+
const bgColor = select( showBackground, envBehind.mul( backgroundIntensity ), backgroundColor );
|
|
290
|
+
const outRgb = select( transparentBackground, vec3( 0.0 ), bgColor.mul( ratio ) );
|
|
291
|
+
const outAlpha = select( transparentBackground, float( 1.0 ).sub( ratio ), float( 1.0 ) );
|
|
292
|
+
|
|
293
|
+
// The catcher is a real ground surface for the denoiser — write the plane's normal/depth
|
|
294
|
+
// + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
|
|
295
|
+
// valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
|
|
296
|
+
If( auxOn, () => {
|
|
297
|
+
|
|
298
|
+
const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
|
|
299
|
+
writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
|
|
300
|
+
writeGBufferSurfaceID( gBufferRW, pixelIndex, uint( CATCHER_SURFACE_ID ), uint( CATCHER_SURFACE_ID ), 0.5, 0.5, uint( 1 ) );
|
|
301
|
+
|
|
302
|
+
} );
|
|
303
|
+
|
|
304
|
+
writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
|
|
305
|
+
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
306
|
+
rngBufferRW.element( rayID ).assign( rngState );
|
|
307
|
+
Return();
|
|
308
|
+
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
} );
|
|
312
|
+
|
|
313
|
+
} );
|
|
140
314
|
|
|
141
|
-
|
|
315
|
+
If( hitDist.greaterThan( MISS_DIST ), () => {
|
|
142
316
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
317
|
+
// Background and environment-lighting are decoupled (independent axes):
|
|
318
|
+
// • Visible backdrop: a PRIMARY ray draws the env image only when showBackground — regardless
|
|
319
|
+
// of enableEnvironmentLight (so you can show the HDRI without it lighting the scene).
|
|
320
|
+
// • Env as a light: SECONDARY bounces add the env (implicit MIS hit) only when enableEnvironmentLight.
|
|
321
|
+
// Backdrop-view = the ray still travels the original camera direction (only alpha/transparent
|
|
322
|
+
// passthrough since the camera). This — NOT bounceIndex==0 — is the correct test for "the env here
|
|
323
|
+
// is the direct backdrop", so env seen through alpha-cutout foliage holes is treated identically to
|
|
324
|
+
// the open sky (blur, intensity, show/hide, color-mode, ground projection all match).
|
|
325
|
+
// isBackdropView was captured at arrival (above) so it survives the REDIRECTED bitOr on opaque hits.
|
|
326
|
+
const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
|
|
327
|
+
const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
|
|
328
|
+
|
|
329
|
+
If( wantBackdrop.or( wantEnvLight ), () => {
|
|
330
|
+
|
|
331
|
+
// Ground projection bends the backdrop-view env lookup onto a projected sphere+disk so the lower
|
|
332
|
+
// env hemisphere reads as a ground plane. Backdrop-view only (incl. through alpha-cutout holes,
|
|
333
|
+
// since they keep the camera direction); redirected bounces see the raw envmap as a light. The
|
|
334
|
+
// shared helper (also used by the shadow catcher) keeps the two in lockstep.
|
|
146
335
|
const envDir = direction.toVar();
|
|
147
|
-
If(
|
|
336
|
+
If( isBackdropView, () => {
|
|
148
337
|
|
|
149
|
-
envDir.assign(
|
|
150
|
-
origin, direction, groundProjectionRadius, groundProjectionHeight,
|
|
338
|
+
envDir.assign( groundProjectedEnvDir(
|
|
339
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
151
340
|
) );
|
|
152
341
|
|
|
153
342
|
} );
|
|
154
343
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
344
|
+
// Backdrop-view rays blur the env (cone jitter, shared helper); redirected env-light bounces take
|
|
345
|
+
// the sharp Else. force-enable the sampler (pass 1.0): the wantBackdrop/wantEnvLight gate above
|
|
346
|
+
// already decided visibility, so the backdrop shows the HDRI even when env-lighting is off.
|
|
347
|
+
// Direction-space jitter keeps the blur free of equirect pole/seam artifacts; accumulation
|
|
348
|
+
// converges the noise. Opt-in — blurriness 0 takes the sharp Else (zero cost).
|
|
349
|
+
const envColor = vec3( 0.0 ).toVar();
|
|
350
|
+
If( isBackdropView.and( backgroundBlurriness.greaterThan( 0.0 ) ), () => {
|
|
351
|
+
|
|
352
|
+
envColor.assign( sampleEnvBlurred( envDir, backgroundBlurriness.mul( 1.3 ), backgroundBlurSamples, rngState ) );
|
|
163
353
|
|
|
164
|
-
|
|
165
|
-
If( bounceIndex.equal( 0 ).and( showBackground.not() ), () => {
|
|
354
|
+
} ).Else( () => {
|
|
166
355
|
|
|
167
|
-
envColor.assign(
|
|
356
|
+
envColor.assign( sampleEnvironment( {
|
|
357
|
+
tex: envTexture,
|
|
358
|
+
samp: sampler( envTexture ),
|
|
359
|
+
direction: envDir,
|
|
360
|
+
environmentMatrix: envMatrix,
|
|
361
|
+
environmentIntensity,
|
|
362
|
+
enableEnvironmentLight: float( 1.0 ),
|
|
363
|
+
} ).xyz );
|
|
168
364
|
|
|
169
365
|
} );
|
|
170
366
|
|
|
171
367
|
// MIS weight for implicit env hit — prevents double-counting with NEE
|
|
172
368
|
const envMisWeight = float( 1.0 ).toVar();
|
|
173
|
-
If(
|
|
369
|
+
If( isBackdropView.not().and( useEnvMapIS ), () => {
|
|
174
370
|
|
|
175
371
|
const prevBouncePdf = readRayPdf( rayBufferRW, rayID );
|
|
176
372
|
If( prevBouncePdf.greaterThan( 0.0 ), () => {
|
|
@@ -189,18 +385,23 @@ export function buildShadeKernel( params ) {
|
|
|
189
385
|
|
|
190
386
|
} );
|
|
191
387
|
|
|
192
|
-
const envGiScale = select(
|
|
193
|
-
const envScale = select(
|
|
388
|
+
const envGiScale = select( isBackdropView.not(), globalIlluminationIntensity, float( 1.0 ) );
|
|
389
|
+
const envScale = select( isBackdropView, backgroundIntensity, envMisWeight.mul( envGiScale ) );
|
|
194
390
|
|
|
195
391
|
// Firefly-suppress the env contribution (megakernel parity: PathTracerCore.js:780). Without
|
|
196
392
|
// this, indirect bounces escaping to a bright environment are unsuppressed spikes that OIDN
|
|
197
393
|
// smears into white blobs. The miss branch Return()s before the hit-branch clamp (~line 712),
|
|
198
394
|
// so it must be applied here.
|
|
395
|
+
// Firefly path length: a backdrop view (incl. through alpha-cutout holes) is a DIRECT view of the
|
|
396
|
+
// sky → use 0 (loosest clamp, same as the open-sky bounce-0 backdrop) so a bright HDRI sun doesn't
|
|
397
|
+
// read dimmer behind foliage cutouts than beside them. Only redirected GI bounces get the tighter
|
|
398
|
+
// path-length threshold.
|
|
399
|
+
const fireflyPathLen = select( isBackdropView, float( 0.0 ), float( bounceIndex ) );
|
|
199
400
|
currentRadiance.assign( vec4(
|
|
200
401
|
currentRadiance.xyz.add(
|
|
201
402
|
regularizePathContribution(
|
|
202
|
-
throughput.mul( envColor
|
|
203
|
-
|
|
403
|
+
throughput.mul( envColor ).mul( envScale ),
|
|
404
|
+
fireflyPathLen, fireflyThreshold, int( frame ),
|
|
204
405
|
),
|
|
205
406
|
),
|
|
206
407
|
currentRadiance.w
|
|
@@ -208,6 +409,18 @@ export function buildShadeKernel( params ) {
|
|
|
208
409
|
|
|
209
410
|
} );
|
|
210
411
|
|
|
412
|
+
// Solid-color backdrop ('color' mode): a primary ray that doesn't show the env image and isn't
|
|
413
|
+
// transparent fills with backgroundColor (default black). Tinted by throughput so it reads
|
|
414
|
+
// correctly behind colored glass, matching the env-backdrop path.
|
|
415
|
+
If( isBackdropView.and( showBackground.not() ).and( transparentBackground.not() ), () => {
|
|
416
|
+
|
|
417
|
+
currentRadiance.assign( vec4(
|
|
418
|
+
currentRadiance.xyz.add( throughput.mul( backgroundColor ) ),
|
|
419
|
+
currentRadiance.w
|
|
420
|
+
) );
|
|
421
|
+
|
|
422
|
+
} );
|
|
423
|
+
|
|
211
424
|
// Transparent-bg alpha: see-through only if the ray escaped WITHOUT ever hitting opaque
|
|
212
425
|
// geometry (megakernel parity: PathTracerCore.js:784). A secondary bounce off an opaque
|
|
213
426
|
// surface that escapes to env keeps alpha 1 (HAS_HIT_OPAQUE set), so glass-in-front-of-an-
|
|
@@ -284,6 +497,8 @@ export function buildShadeKernel( params ) {
|
|
|
284
497
|
throughput.divAssign( rrP );
|
|
285
498
|
|
|
286
499
|
// free-bounce continuation: ray stays in the same medium, so medium stack + coeffs persist
|
|
500
|
+
// SSS scatter changes direction → no longer the direct backdrop view.
|
|
501
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
|
|
287
502
|
writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps );
|
|
288
503
|
writeRayDirFlags( rayBufferRW, rayID, newDir, flags );
|
|
289
504
|
// Free bounce: preserve prevBouncePdf (megakernel leaves it untouched across SSS scatter,
|
|
@@ -359,26 +574,22 @@ export function buildShadeKernel( params ) {
|
|
|
359
574
|
} );
|
|
360
575
|
|
|
361
576
|
// first-hit MRT data (bounce 0 only)
|
|
362
|
-
If( bounceIndex.equal( 0 ), () => {
|
|
577
|
+
If( bounceIndex.equal( 0 ).and( auxOn ), () => {
|
|
363
578
|
|
|
364
579
|
const linearDepth = computeNDCDepth( {
|
|
365
580
|
worldPos: hitPoint,
|
|
366
581
|
cameraProjectionMatrix,
|
|
367
582
|
cameraViewMatrix,
|
|
368
583
|
} );
|
|
369
|
-
// G-buffer is per-pixel
|
|
584
|
+
// G-buffer is per-pixel (rayID == pixelIndex). writeGBuffer half-packs (normal/depth/albedo).
|
|
370
585
|
// Write the primary DEPTH now with the miss-default aux; the real normal/albedo are captured below
|
|
371
586
|
// (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
|
|
372
587
|
// block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
|
|
373
588
|
// (objectNormal/objectColor stay at their init for transmissive-then-miss).
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// branch (misses Return above), so this marks the pixel valid; bary from the bounce-0 hit.
|
|
379
|
-
writeGBufferSurfaceID( gBufferRW, pixelIndex, hitTriIdx, readHitMeshIndex( hitBufferRO, rayID ), hitUV.x, hitUV.y, uint( 1 ) );
|
|
380
|
-
|
|
381
|
-
} );
|
|
589
|
+
writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
|
|
590
|
+
// Persist the primary-hit surface ID (Tier-1 A-SVGF correlated re-projection). Hit-only
|
|
591
|
+
// branch (misses Return above), so this marks the pixel valid; bary from the bounce-0 hit.
|
|
592
|
+
writeGBufferSurfaceID( gBufferRW, pixelIndex, hitTriIdx, readHitMeshIndex( hitBufferRO, rayID ), hitUV.x, hitUV.y, uint( 1 ) );
|
|
382
593
|
|
|
383
594
|
} );
|
|
384
595
|
|
|
@@ -522,6 +733,14 @@ export function buildShadeKernel( params ) {
|
|
|
522
733
|
|
|
523
734
|
// SSS = free bounce (depth unchanged); transmission advances camera-bounce depth.
|
|
524
735
|
// 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).
|
|
736
|
+
// Backdrop-view survives a pure alpha/transparent passthrough (direction unchanged) but is cleared by
|
|
737
|
+
// any redirection (refraction/reflection/SSS boundary), so env through a leaf hole stays the blurred
|
|
738
|
+
// backdrop while env through glass becomes sharp redirected light.
|
|
739
|
+
If( interaction.isAlphaSkip.not(), () => {
|
|
740
|
+
|
|
741
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
|
|
742
|
+
|
|
743
|
+
} );
|
|
525
744
|
writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps );
|
|
526
745
|
writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
|
|
527
746
|
// Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
|
|
@@ -539,12 +758,17 @@ export function buildShadeKernel( params ) {
|
|
|
539
758
|
// PathTracerCore.js:1042). Flag the chain so a later env-escape keeps alpha 1 (the gate in the
|
|
540
759
|
// miss branch). Alpha itself already defaults to 1 from Generate in transparent-bg mode, so there
|
|
541
760
|
// is nothing to set here — a ray dying inside geometry (SSS walk) stays solid without reaching this.
|
|
542
|
-
|
|
761
|
+
// Hit opaque geometry: set HAS_HIT_OPAQUE and mark REDIRECTED (this is a real surface scatter,
|
|
762
|
+
// so any later env-escape is redirected light, not the direct backdrop). Single positive bitOr.
|
|
763
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.HAS_HIT_OPAQUE | RAY_FLAG.REDIRECTED ) ) );
|
|
543
764
|
|
|
544
765
|
const emissive = matSamples.emissive.toVar();
|
|
545
766
|
If( length( emissive ).greaterThan( 0.0 ), () => {
|
|
546
767
|
|
|
547
|
-
|
|
768
|
+
// Key on backdrop-view (not bounceIndex>0) so an emitter seen DIRECTLY through an alpha-cutout hole
|
|
769
|
+
// renders at full intensity (1.0) like a direct view, consistent with env-through-hole — instead of
|
|
770
|
+
// being GI-scaled as if it were an indirect bounce. (MIS below already self-guards via prevBouncePdf.)
|
|
771
|
+
const emissiveGiScale = select( isBackdropView.not(), globalIlluminationIntensity, float( 1.0 ) );
|
|
548
772
|
|
|
549
773
|
// MIS weight vs emissive-triangle NEE (megakernel parity: PathTracerCore.js:1117). On a secondary
|
|
550
774
|
// hit (bounceIndex>0) the prior bounce's NEE also sampled this emitter — power-heuristic balances the
|
|
@@ -616,7 +840,7 @@ export function buildShadeKernel( params ) {
|
|
|
616
840
|
// visible (the surface reflected in a mirror / seen behind glass), not the specular surface. Glass
|
|
617
841
|
// Returns at the transparency block above, so its aux is replaced by the surface behind it. Depth
|
|
618
842
|
// stays at the primary hit (read back + re-packed; the snorm depth re-pack is idempotent — no drift).
|
|
619
|
-
If(
|
|
843
|
+
If( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ).and( auxOn ), () => {
|
|
620
844
|
|
|
621
845
|
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
622
846
|
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
|
|
@@ -636,13 +860,13 @@ export function buildShadeKernel( params ) {
|
|
|
636
860
|
material.clearcoat, material.emissive, material.subsurface,
|
|
637
861
|
) ).toVar();
|
|
638
862
|
|
|
639
|
-
// STBN keyed on (pixel, bounceIndex, frame)
|
|
863
|
+
// STBN keyed on (pixel, bounceIndex, frame).
|
|
640
864
|
const _resX = int( resolution.x ).toVar();
|
|
641
865
|
const _pixelCoord = vec2(
|
|
642
866
|
float( int( pixelIndex ).mod( _resX ) ).add( 0.5 ),
|
|
643
867
|
float( int( pixelIndex ).div( _resX ) ).add( 0.5 ),
|
|
644
868
|
);
|
|
645
|
-
const xi = getRandomSample( _pixelCoord,
|
|
869
|
+
const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
|
|
646
870
|
const emptyWeights = BRDFWeights( {
|
|
647
871
|
specular: float( 0.0 ), diffuse: float( 0.0 ), sheen: float( 0.0 ),
|
|
648
872
|
clearcoat: float( 0.0 ), transmission: float( 0.0 ), iridescence: float( 0.0 ),
|
|
@@ -689,7 +913,7 @@ export function buildShadeKernel( params ) {
|
|
|
689
913
|
|
|
690
914
|
} );
|
|
691
915
|
|
|
692
|
-
const directLight = calculateDirectLightingUnified(
|
|
916
|
+
const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
693
917
|
hitPoint, N, material, V,
|
|
694
918
|
brdfDir, brdfPdf, brdfValue,
|
|
695
919
|
bounceIndex, rngState,
|
|
@@ -702,7 +926,8 @@ export function buildShadeKernel( params ) {
|
|
|
702
926
|
envCDFTexture,
|
|
703
927
|
envTotalSum, envCompensationDelta, envResolution,
|
|
704
928
|
enableEnvironmentLight,
|
|
705
|
-
|
|
929
|
+
tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
|
|
930
|
+
) ).shadowed.toVar();
|
|
706
931
|
|
|
707
932
|
const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
|
|
708
933
|
// Per-term firefly suppression (megakernel parity: PathTracerCore.js:1164) — wrap the direct-light add
|
package/src/TSL/Struct.js
CHANGED
|
@@ -73,6 +73,14 @@ export const ShadowMaterial = struct( {
|
|
|
73
73
|
albedoTransform: 'mat3',
|
|
74
74
|
} );
|
|
75
75
|
|
|
76
|
+
// Dual direct-lighting result: shadowed (the normal NEE estimate) + unoccluded
|
|
77
|
+
// (same estimator with visibility forced to 1). Used by the shadow catcher to
|
|
78
|
+
// derive a shadow ratio = luma(shadowed) / luma(unoccluded).
|
|
79
|
+
export const DirectLightingDual = struct( {
|
|
80
|
+
shadowed: 'vec3',
|
|
81
|
+
unoccluded: 'vec3',
|
|
82
|
+
} );
|
|
83
|
+
|
|
76
84
|
export const Sphere = struct( {
|
|
77
85
|
position: 'vec3',
|
|
78
86
|
radius: 'float',
|
|
@@ -365,6 +365,11 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
365
365
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// PathTracer's aux MRT (normalDepth + albedo) is consumed by the real-time denoisers (ASVGF/
|
|
369
|
+
// BilateralFilter read albedo; ASVGF/SSRC/EdgeFilter read normalDepth) and by OIDN (reads the
|
|
370
|
+
// MRT read-targets directly). When none are active the wavefront skips those writes entirely.
|
|
371
|
+
s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
|
|
372
|
+
|
|
368
373
|
}
|
|
369
374
|
|
|
370
375
|
// ── Render Completion Chain ───────────────────────────────────
|
|
@@ -595,6 +600,8 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
595
600
|
setOIDNEnabled( enabled ) {
|
|
596
601
|
|
|
597
602
|
if ( this.denoiser ) this.denoiser.enabled = enabled;
|
|
603
|
+
// OIDN reads the PathTracer aux MRT; re-sync so the wavefront produces it while OIDN is on.
|
|
604
|
+
this._syncGBufferStages();
|
|
598
605
|
|
|
599
606
|
}
|
|
600
607
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { uniform, uniformArray } from 'three/tsl';
|
|
9
|
-
import { Vector2, Matrix4, Vector3 } from 'three';
|
|
9
|
+
import { Vector2, Matrix4, Vector3, Color } from 'three';
|
|
10
10
|
import { samplingTechniqueUniform } from '../TSL/Random.js';
|
|
11
11
|
import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
|
|
12
12
|
|
|
@@ -161,7 +161,6 @@ export class UniformManager {
|
|
|
161
161
|
// Frame and sampling
|
|
162
162
|
u( 'frame', 0, 'uint' );
|
|
163
163
|
u( 'maxBounces', DEFAULT_STATE.bounces, 'int' );
|
|
164
|
-
u( 'samplesPerPixel', DEFAULT_STATE.samplesPerPixel, 'int' );
|
|
165
164
|
u( 'maxSamples', DEFAULT_STATE.maxSamples, 'int' );
|
|
166
165
|
u( 'transmissiveBounces', DEFAULT_STATE.transmissiveBounces, 'int' );
|
|
167
166
|
u( 'maxSubsurfaceSteps', DEFAULT_STATE.maxSubsurfaceSteps, 'int' );
|
|
@@ -177,6 +176,9 @@ export class UniformManager {
|
|
|
177
176
|
// Environment
|
|
178
177
|
u( 'environmentIntensity', DEFAULT_STATE.environmentIntensity, 'float' );
|
|
179
178
|
u( 'backgroundIntensity', DEFAULT_STATE.backgroundIntensity, 'float' );
|
|
179
|
+
u( 'backgroundColor', new Color( 0, 0, 0 ), 'color' ); // linear; solid backdrop in 'color' mode
|
|
180
|
+
u( 'backgroundBlurriness', DEFAULT_STATE.backgroundBlurriness, 'float' );
|
|
181
|
+
u( 'backgroundBlurSamples', DEFAULT_STATE.backgroundBlurSamples, 'int' );
|
|
180
182
|
ub( 'showBackground', DEFAULT_STATE.showBackground );
|
|
181
183
|
ub( 'transparentBackground', DEFAULT_STATE.transparentBackground );
|
|
182
184
|
ub( 'enableEnvironment', DEFAULT_STATE.enableEnvironment );
|
|
@@ -188,6 +190,9 @@ export class UniformManager {
|
|
|
188
190
|
ub( 'groundProjectionEnabled', DEFAULT_STATE.groundProjectionEnabled );
|
|
189
191
|
u( 'groundProjectionRadius', DEFAULT_STATE.groundProjectionRadius, 'float' );
|
|
190
192
|
u( 'groundProjectionHeight', DEFAULT_STATE.groundProjectionHeight, 'float' );
|
|
193
|
+
u( 'groundProjectionLevel', DEFAULT_STATE.groundProjectionLevel, 'float' );
|
|
194
|
+
ub( 'enableGroundCatcher', DEFAULT_STATE.enableGroundCatcher );
|
|
195
|
+
u( 'groundCatcherHeight', DEFAULT_STATE.groundCatcherHeight, 'float' );
|
|
191
196
|
|
|
192
197
|
// Sun parameters
|
|
193
198
|
u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
|
|
@@ -277,7 +277,6 @@ export class VideoRenderManager {
|
|
|
277
277
|
return {
|
|
278
278
|
maxSamples: app.settings.get( 'maxSamples' ),
|
|
279
279
|
maxBounces: app.settings.get( 'maxBounces' ),
|
|
280
|
-
samplesPerPixel: app.settings.get( 'samplesPerPixel' ),
|
|
281
280
|
transmissiveBounces: app.settings.get( 'transmissiveBounces' ),
|
|
282
281
|
renderMode: app.stages.pathTracer?.renderMode?.value,
|
|
283
282
|
controlsEnabled: app.cameraManager.controls?.enabled,
|
|
@@ -300,7 +299,6 @@ export class VideoRenderManager {
|
|
|
300
299
|
app.settings.setMany( {
|
|
301
300
|
maxSamples: state.maxSamples,
|
|
302
301
|
maxBounces: state.maxBounces,
|
|
303
|
-
samplesPerPixel: state.samplesPerPixel,
|
|
304
302
|
transmissiveBounces: state.transmissiveBounces,
|
|
305
303
|
}, { silent: true } );
|
|
306
304
|
|