rayzee 7.2.1 → 7.3.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 +1 -1
- package/dist/rayzee.es.js +1428 -1306
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +45 -45
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +6 -0
- package/src/PathTracerApp.js +20 -1
- package/src/RenderSettings.js +3 -0
- package/src/Stages/PathTracer.js +3 -0
- package/src/TSL/Common.js +62 -0
- package/src/TSL/Environment.js +22 -1
- package/src/TSL/LightsSampling.js +35 -14
- package/src/TSL/ShadeKernel.js +127 -8
- package/src/TSL/Struct.js +8 -0
- package/src/managers/UniformManager.js +3 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -23,6 +23,12 @@ export const ENGINE_DEFAULTS = {
|
|
|
23
23
|
groundProjectionEnabled: false,
|
|
24
24
|
groundProjectionRadius: 100,
|
|
25
25
|
groundProjectionHeight: 15,
|
|
26
|
+
// World Y of the projected ground plane; auto-seeded to the scene floor (min-Y) on model
|
|
27
|
+
// load so models that aren't authored at y=0 sit ON the ground instead of sinking into it.
|
|
28
|
+
groundProjectionLevel: 0,
|
|
29
|
+
// Analytic ground-plane shadow catcher (primary-ray holdout; no geometry)
|
|
30
|
+
enableGroundCatcher: false,
|
|
31
|
+
groundCatcherHeight: 0,
|
|
26
32
|
globalIlluminationIntensity: 1,
|
|
27
33
|
|
|
28
34
|
// Environment Mode System
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebGPURenderer, RectAreaLightNode, SRGBColorSpace } from 'three/webgpu';
|
|
2
2
|
import { texture as _tslTexture, cubeTexture as _tslCubeTexture } from 'three/tsl';
|
|
3
3
|
import {
|
|
4
|
-
ACESFilmicToneMapping, Scene, EventDispatcher
|
|
4
|
+
ACESFilmicToneMapping, Scene, EventDispatcher, Box3
|
|
5
5
|
} from 'three';
|
|
6
6
|
import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js';
|
|
7
7
|
import { SceneHelpers } from './SceneHelpers.js';
|
|
@@ -777,6 +777,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
777
777
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
+
// Seed the ground-projection plane AND the shadow-catcher plane to the scene floor so models
|
|
781
|
+
// that aren't authored at y=0 sit on the ground (not sunk) — auto-updates on every model change.
|
|
782
|
+
const sceneMinY = this.getSceneMinY();
|
|
783
|
+
this.settings.set( 'groundProjectionLevel', sceneMinY, { reset: false } );
|
|
784
|
+
this.settings.set( 'groundCatcherHeight', sceneMinY, { reset: false } );
|
|
785
|
+
|
|
780
786
|
// Apply all settings to stages in one shot
|
|
781
787
|
timer.start( 'Apply settings' );
|
|
782
788
|
this.settings.applyAll();
|
|
@@ -1178,6 +1184,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1178
1184
|
* @param {string} property
|
|
1179
1185
|
* @param {*} value
|
|
1180
1186
|
*/
|
|
1187
|
+
/**
|
|
1188
|
+
* World-space minimum Y of the loaded scene (the floor). Used to seed the
|
|
1189
|
+
* analytic ground-plane shadow catcher height. Returns 0 if no scene is loaded.
|
|
1190
|
+
* @returns {number}
|
|
1191
|
+
*/
|
|
1192
|
+
getSceneMinY() {
|
|
1193
|
+
|
|
1194
|
+
if ( ! this.meshScene ) return 0;
|
|
1195
|
+
const box = new Box3().setFromObject( this.meshScene );
|
|
1196
|
+
return Number.isFinite( box.min.y ) ? box.min.y : 0;
|
|
1197
|
+
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1181
1200
|
setMaterialProperty( materialIndex, property, value ) {
|
|
1182
1201
|
|
|
1183
1202
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
package/src/RenderSettings.js
CHANGED
|
@@ -26,6 +26,9 @@ const SETTING_ROUTES = {
|
|
|
26
26
|
groundProjectionEnabled: { uniform: 'groundProjectionEnabled', reset: true },
|
|
27
27
|
groundProjectionRadius: { uniform: 'groundProjectionRadius', reset: true },
|
|
28
28
|
groundProjectionHeight: { uniform: 'groundProjectionHeight', reset: true },
|
|
29
|
+
groundProjectionLevel: { uniform: 'groundProjectionLevel', reset: true },
|
|
30
|
+
enableGroundCatcher: { uniform: 'enableGroundCatcher', reset: true },
|
|
31
|
+
groundCatcherHeight: { uniform: 'groundCatcherHeight', reset: true },
|
|
29
32
|
globalIlluminationIntensity: { uniform: 'globalIlluminationIntensity', reset: true },
|
|
30
33
|
enableDOF: { uniform: 'enableDOF', reset: true },
|
|
31
34
|
focusDistance: { uniform: 'focusDistance', reset: false },
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -726,6 +726,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
726
726
|
groundProjectionEnabled: this.groundProjectionEnabled,
|
|
727
727
|
groundProjectionRadius: this.groundProjectionRadius,
|
|
728
728
|
groundProjectionHeight: this.groundProjectionHeight,
|
|
729
|
+
groundProjectionLevel: this.groundProjectionLevel,
|
|
730
|
+
enableGroundCatcher: this.enableGroundCatcher,
|
|
731
|
+
groundCatcherHeight: this.groundCatcherHeight,
|
|
729
732
|
envTotalSum: this.envTotalSum,
|
|
730
733
|
envResolution: this.envResolution,
|
|
731
734
|
directionalLightsBuffer: this.directionalLightsBufferNode,
|
package/src/TSL/Common.js
CHANGED
|
@@ -400,6 +400,68 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
|
|
|
400
400
|
|
|
401
401
|
} );
|
|
402
402
|
|
|
403
|
+
// Synthetic diffuse-white material for the analytic ground-plane shadow catcher.
|
|
404
|
+
// No geometry/material buffer is involved — the plane is shaded as a matte Lambertian
|
|
405
|
+
// so the direct-lighting estimator yields an irradiance-weighted shadow ratio (the
|
|
406
|
+
// diffuse BRDF is constant and cancels in shadowed/unoccluded). All fields are set to
|
|
407
|
+
// inert defaults; only color/roughness/metalness/transmission affect the lighting path.
|
|
408
|
+
export const diffuseGroundMaterial = Fn( () => {
|
|
409
|
+
|
|
410
|
+
const idn = mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 );
|
|
411
|
+
return RayTracingMaterial( {
|
|
412
|
+
color: vec4( 1.0, 1.0, 1.0, 1.0 ),
|
|
413
|
+
emissive: vec3( 0.0 ),
|
|
414
|
+
emissiveIntensity: float( 0.0 ),
|
|
415
|
+
roughness: float( 1.0 ),
|
|
416
|
+
metalness: float( 0.0 ),
|
|
417
|
+
ior: float( 1.5 ),
|
|
418
|
+
transmission: float( 0.0 ),
|
|
419
|
+
thickness: float( 0.0 ),
|
|
420
|
+
clearcoat: float( 0.0 ),
|
|
421
|
+
clearcoatRoughness: float( 0.0 ),
|
|
422
|
+
opacity: float( 1.0 ),
|
|
423
|
+
transparent: float( 0.0 ),
|
|
424
|
+
attenuationColor: vec3( 1.0 ),
|
|
425
|
+
attenuationDistance: float( 0.0 ),
|
|
426
|
+
dispersion: float( 0.0 ),
|
|
427
|
+
sheen: float( 0.0 ),
|
|
428
|
+
sheenRoughness: float( 1.0 ),
|
|
429
|
+
sheenColor: vec3( 0.0 ),
|
|
430
|
+
specularIntensity: float( 1.0 ),
|
|
431
|
+
specularColor: vec3( 1.0 ),
|
|
432
|
+
alphaTest: float( 0.0 ),
|
|
433
|
+
alphaMode: int( 0 ),
|
|
434
|
+
side: int( 0 ),
|
|
435
|
+
depthWrite: int( 1 ),
|
|
436
|
+
albedoMapIndex: int( - 1 ),
|
|
437
|
+
emissiveMapIndex: int( - 1 ),
|
|
438
|
+
normalMapIndex: int( - 1 ),
|
|
439
|
+
bumpMapIndex: int( - 1 ),
|
|
440
|
+
bumpScale: float( 1.0 ),
|
|
441
|
+
displacementScale: float( 0.0 ),
|
|
442
|
+
metalnessMapIndex: int( - 1 ),
|
|
443
|
+
roughnessMapIndex: int( - 1 ),
|
|
444
|
+
displacementMapIndex: int( - 1 ),
|
|
445
|
+
normalScale: vec2( 1.0, 1.0 ),
|
|
446
|
+
albedoTransform: idn,
|
|
447
|
+
emissiveTransform: idn,
|
|
448
|
+
normalTransform: idn,
|
|
449
|
+
bumpTransform: idn,
|
|
450
|
+
metalnessTransform: idn,
|
|
451
|
+
roughnessTransform: idn,
|
|
452
|
+
displacementTransform: idn,
|
|
453
|
+
iridescence: float( 0.0 ),
|
|
454
|
+
iridescenceIOR: float( 1.3 ),
|
|
455
|
+
iridescenceThicknessRange: vec2( 100.0, 400.0 ),
|
|
456
|
+
subsurface: float( 0.0 ),
|
|
457
|
+
subsurfaceColor: vec3( 1.0 ),
|
|
458
|
+
subsurfaceRadius: vec3( 1.0, 0.2, 0.1 ),
|
|
459
|
+
subsurfaceRadiusScale: float( 1.0 ),
|
|
460
|
+
subsurfaceAnisotropy: float( 0.0 ),
|
|
461
|
+
} );
|
|
462
|
+
|
|
463
|
+
} );
|
|
464
|
+
|
|
403
465
|
// ── Shadow material thin reader (7 slot reads instead of 27) ─────────────
|
|
404
466
|
// Only fetches fields needed by traceShadowRay: alpha, transmission, attenuation, albedo transform.
|
|
405
467
|
|
package/src/TSL/Environment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, wgslFn, vec2, vec4, ivec2, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
|
|
1
|
+
import { Fn, wgslFn, vec2, vec3, vec4, ivec2, float, int, If, texture, dot, sin, sqrt, floor, fract, min, max, mix, clamp } from 'three/tsl';
|
|
2
2
|
|
|
3
3
|
import { REC709_LUMINANCE_COEFFICIENTS } from './Common.js';
|
|
4
4
|
|
|
@@ -211,3 +211,24 @@ export const getGroundProjectedDirection = Fn( ( [ rayOrigin, rayDirection, radi
|
|
|
211
211
|
return projected;
|
|
212
212
|
|
|
213
213
|
} );
|
|
214
|
+
|
|
215
|
+
// Primary-ray environment lookup direction: bends onto the ground-projection sphere/disk when
|
|
216
|
+
// ground projection is enabled, else returns the ray direction unchanged. Shared by the background
|
|
217
|
+
// miss branch AND the shadow catcher so they can never disagree on the projection (the source of a
|
|
218
|
+
// past horizon-seam bug when only one path bent the direction).
|
|
219
|
+
export const groundProjectedEnvDir = Fn( ( [ rayOrigin, rayDirection, enabled, radius, height, level ] ) => {
|
|
220
|
+
|
|
221
|
+
const dir = rayDirection.toVar();
|
|
222
|
+
If( enabled, () => {
|
|
223
|
+
|
|
224
|
+
// Relocate the projected ground plane from y=0 to world y=level (the scene floor) so a model
|
|
225
|
+
// authored off the origin still sits ON the ground instead of sinking. Shifting the projection
|
|
226
|
+
// origin down by `level` puts the disk at y=level and the sphere at y=level+height; the internal
|
|
227
|
+
// horizontal radius test stays correct because the shifted disk point lands at y'=0.
|
|
228
|
+
const shiftedOrigin = vec3( rayOrigin.x, rayOrigin.y.sub( level ), rayOrigin.z );
|
|
229
|
+
dir.assign( getGroundProjectedDirection( shiftedOrigin, rayDirection, radius, height ) );
|
|
230
|
+
|
|
231
|
+
} );
|
|
232
|
+
return dir;
|
|
233
|
+
|
|
234
|
+
} );
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
sampleIESProfile,
|
|
59
59
|
} from './LightsCore.js';
|
|
60
60
|
|
|
61
|
-
import { MISStrategy, DotProducts } from './Struct.js';
|
|
61
|
+
import { MISStrategy, DotProducts, DirectLightingDual } from './Struct.js';
|
|
62
62
|
import {
|
|
63
63
|
calculateDirectionalLightImportance,
|
|
64
64
|
estimateLightImportance,
|
|
@@ -707,9 +707,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
707
707
|
envCDFTexture,
|
|
708
708
|
envTotalSum, envCompensationDelta, envResolution,
|
|
709
709
|
enableEnvironmentLight,
|
|
710
|
+
// Shadow catcher: when true, also accumulate the unoccluded (visibility=1) reference
|
|
711
|
+
// at every light/env site so the caller can form a shadow ratio. Dead path otherwise.
|
|
712
|
+
wantUnoccluded,
|
|
710
713
|
] ) => {
|
|
711
714
|
|
|
712
715
|
const totalContribution = vec3( 0.0 ).toVar();
|
|
716
|
+
// Unoccluded reference (visibility forced to 1) — only filled when wantUnoccluded.
|
|
717
|
+
const unoccludedContribution = vec3( 0.0 ).toVar();
|
|
713
718
|
const rayOrigin = hitPoint.add( hitNormal.mul( 0.001 ) ).toVar();
|
|
714
719
|
|
|
715
720
|
// Binds BVH params so shadow-ray sites at varying call depths use a 3-arg call
|
|
@@ -819,7 +824,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
819
824
|
const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) );
|
|
820
825
|
const visibility = shadow( rayOrigin, lightSample.direction, shadowDistance );
|
|
821
826
|
|
|
822
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
827
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
823
828
|
|
|
824
829
|
// Share H + dot products between BRDF eval and PDF — otherwise each
|
|
825
830
|
// would recompute normalize(V+L) + 5 dot products independently.
|
|
@@ -846,9 +851,15 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
846
851
|
|
|
847
852
|
} );
|
|
848
853
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
854
|
+
// Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
|
|
855
|
+
// pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
|
|
856
|
+
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( lightWeight, 1e-10 ) );
|
|
857
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
858
|
+
If( wantUnoccluded, () => {
|
|
859
|
+
|
|
860
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
861
|
+
|
|
862
|
+
} );
|
|
852
863
|
|
|
853
864
|
} );
|
|
854
865
|
|
|
@@ -918,7 +929,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
918
929
|
const shadowDistance = min( hitDistance.sub( 0.001 ), float( 1000.0 ) );
|
|
919
930
|
const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
|
|
920
931
|
|
|
921
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
932
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
922
933
|
|
|
923
934
|
const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
|
|
924
935
|
|
|
@@ -934,9 +945,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
934
945
|
const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
|
|
935
946
|
|
|
936
947
|
const lightEmission = light.color.mul( light.intensity );
|
|
937
|
-
//
|
|
938
|
-
const
|
|
939
|
-
totalContribution.addAssign(
|
|
948
|
+
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
949
|
+
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
|
|
950
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
951
|
+
If( wantUnoccluded, () => {
|
|
952
|
+
|
|
953
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
954
|
+
|
|
955
|
+
} );
|
|
940
956
|
|
|
941
957
|
} );
|
|
942
958
|
|
|
@@ -984,7 +1000,7 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
984
1000
|
|
|
985
1001
|
const visibility = shadow( rayOrigin, envDirection, float( 1000.0 ) );
|
|
986
1002
|
|
|
987
|
-
If( visibility.greaterThan( 0.0 ), () => {
|
|
1003
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
988
1004
|
|
|
989
1005
|
// Share H + dots between env BRDF/PDF — same redundancy fix as the
|
|
990
1006
|
// discrete-light path above.
|
|
@@ -1000,9 +1016,14 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1000
1016
|
float( 1.0 )
|
|
1001
1017
|
).toVar();
|
|
1002
1018
|
|
|
1003
|
-
//
|
|
1004
|
-
const
|
|
1005
|
-
totalContribution.addAssign(
|
|
1019
|
+
// Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
|
|
1020
|
+
const baseContribution = envColor.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( envPdf, 1e-10 ) );
|
|
1021
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
1022
|
+
If( wantUnoccluded, () => {
|
|
1023
|
+
|
|
1024
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
1025
|
+
|
|
1026
|
+
} );
|
|
1006
1027
|
|
|
1007
1028
|
} );
|
|
1008
1029
|
|
|
@@ -1018,6 +1039,6 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1018
1039
|
// NOTE: Emissive triangle sampling is handled separately in pathtracer_core.fs
|
|
1019
1040
|
// to bypass firefly suppression. Do not add it here to avoid double-counting.
|
|
1020
1041
|
|
|
1021
|
-
return totalContribution;
|
|
1042
|
+
return DirectLightingDual( { shadowed: totalContribution, unoccluded: unoccludedContribution } );
|
|
1022
1043
|
|
|
1023
1044
|
} );
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
Fn, float, vec2, vec3, vec4, int, uint,
|
|
9
|
+
bool as tslBool,
|
|
9
10
|
If, normalize, max, exp, log, clamp, dot, length, select,
|
|
10
11
|
instanceIndex,
|
|
11
12
|
sampler,
|
|
@@ -13,8 +14,9 @@ import {
|
|
|
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';
|
|
@@ -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,
|
|
@@ -136,6 +148,111 @@ export function buildShadeKernel( params ) {
|
|
|
136
148
|
const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
|
|
137
149
|
const sampleIndex = int( rayID.div( maxRaysPerSample ) ).toVar();
|
|
138
150
|
|
|
151
|
+
// ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
|
|
152
|
+
// A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
|
|
153
|
+
// closer than any BVH hit (hitDist is MISS_DIST on sky rays, so the plane wins over the
|
|
154
|
+
// bare environment), shade it as a diffuse Lambertian holdout: output a monochrome shadow
|
|
155
|
+
// ratio in alpha (rgb = 0) so it composites as bg·ratio over a transparent background.
|
|
156
|
+
// Shadows are caught by the EXISTING NEE shadow ray into real geometry — the plane itself
|
|
157
|
+
// never enters the BVH. Secondary bounces ignore it entirely.
|
|
158
|
+
If( enableGroundCatcher.and( bounceIndex.equal( 0 ) ), () => {
|
|
159
|
+
|
|
160
|
+
const dirY = direction.y.toVar();
|
|
161
|
+
If( dirY.abs().greaterThan( float( EPSILON ) ), () => {
|
|
162
|
+
|
|
163
|
+
const tPlane = groundCatcherHeight.sub( origin.y ).div( dirY ).toVar();
|
|
164
|
+
If( tPlane.greaterThan( float( CATCHER_T_MIN ) ).and( tPlane.lessThan( hitDist ) ), () => {
|
|
165
|
+
|
|
166
|
+
const planePoint = origin.add( direction.mul( tPlane ) ).toVar();
|
|
167
|
+
const planeN = vec3( 0.0, 1.0, 0.0 );
|
|
168
|
+
const planeV = direction.negate().toVar();
|
|
169
|
+
const planeMat = RayTracingMaterial.wrap( diffuseGroundMaterial() ).toVar();
|
|
170
|
+
|
|
171
|
+
// Cosine-weighted hemisphere BRDF sample about the plane normal (0,1,0); the helper
|
|
172
|
+
// puts cosθ along N, so bDir.y == cosθ. Per-component .toVar() on the two randoms
|
|
173
|
+
// (vec2(RandomValue,RandomValue) would collapse to u==v — TSL pitfall).
|
|
174
|
+
const u1 = RandomValue( rngState ).toVar();
|
|
175
|
+
const u2 = RandomValue( rngState ).toVar();
|
|
176
|
+
const bDir = cosineWeightedSample( planeN, vec2( u1, u2 ) ).toVar();
|
|
177
|
+
const bPdf = bDir.y.mul( PI_INV ).toVar(); // cosθ / π
|
|
178
|
+
const bVal = vec3( PI_INV ); // albedo(1) / π
|
|
179
|
+
|
|
180
|
+
// Reuse the full NEE estimator; the diffuse BRDF is constant and cancels in the
|
|
181
|
+
// ratio, so this yields an irradiance-weighted shadow density across all lights + env.
|
|
182
|
+
const dual = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
183
|
+
planePoint, planeN, planeMat, planeV,
|
|
184
|
+
bDir, bPdf, bVal,
|
|
185
|
+
bounceIndex, rngState,
|
|
186
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
187
|
+
areaLightsBuffer, numAreaLights,
|
|
188
|
+
pointLightsBuffer, numPointLights,
|
|
189
|
+
spotLightsBuffer, numSpotLights,
|
|
190
|
+
bvhBuffer, triangleBuffer, materialBuffer,
|
|
191
|
+
envTexture, environmentIntensity, envMatrix,
|
|
192
|
+
envCDFTexture,
|
|
193
|
+
envTotalSum, envCompensationDelta, envResolution,
|
|
194
|
+
enableEnvironmentLight,
|
|
195
|
+
tslBool( true ), // wantUnoccluded
|
|
196
|
+
) ).toVar();
|
|
197
|
+
|
|
198
|
+
const lumShad = max( dot( dual.shadowed, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) );
|
|
199
|
+
const lumLit = max( dot( dual.unoccluded, REC709_LUMINANCE_COEFFICIENTS ), float( 0.0 ) ).toVar();
|
|
200
|
+
const ratio = clamp( lumShad.div( max( lumLit, float( CATCHER_LUMA_FLOOR ) ) ), 0.0, 1.0 ).toVar();
|
|
201
|
+
// Coverage gate: where no light reaches the plane there is no shadow to catch —
|
|
202
|
+
// force ratio=1 (no darkening) so unlit ground reads as plain background, not spurious black.
|
|
203
|
+
If( lumLit.lessThan( float( CATCHER_COVERAGE_MIN ) ), () => {
|
|
204
|
+
|
|
205
|
+
ratio.assign( 1.0 );
|
|
206
|
+
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
// Adaptive output (matches Arnold's scene_background / Cycles shadow catcher): the catcher
|
|
210
|
+
// respects the current background mode instead of forcing transparency.
|
|
211
|
+
// • Transparent background → emit a matte (rgb=0, alpha=1−ratio); the shadow rides alpha
|
|
212
|
+
// and composites as backplate·ratio over an external plate.
|
|
213
|
+
// • Visible background → composite the shadow into RGB: show the environment this
|
|
214
|
+
// camera ray would otherwise see, darkened by the shadow ratio (alpha=1). The HDRI stays
|
|
215
|
+
// visible; at the horizon ratio→1 so the catcher meets the sky with no seam.
|
|
216
|
+
// Sample the background via the SAME shared helper the miss branch uses, so the
|
|
217
|
+
// catcher seamlessly continues the visible environment (with ground projection on it
|
|
218
|
+
// must bend identically, else a bright horizon seam / mismatched ground appears).
|
|
219
|
+
const catcherEnvDir = groundProjectedEnvDir(
|
|
220
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
221
|
+
).toVar();
|
|
222
|
+
const envBehind = sampleEnvironment( {
|
|
223
|
+
tex: envTexture,
|
|
224
|
+
samp: sampler( envTexture ),
|
|
225
|
+
direction: catcherEnvDir,
|
|
226
|
+
environmentMatrix: envMatrix,
|
|
227
|
+
environmentIntensity,
|
|
228
|
+
enableEnvironmentLight,
|
|
229
|
+
} ).xyz.toVar();
|
|
230
|
+
const bgColor = select( showBackground, envBehind.mul( backgroundIntensity ), vec3( 0.0 ) );
|
|
231
|
+
const outRgb = select( transparentBackground, vec3( 0.0 ), bgColor.mul( ratio ) );
|
|
232
|
+
const outAlpha = select( transparentBackground, float( 1.0 ).sub( ratio ), float( 1.0 ) );
|
|
233
|
+
|
|
234
|
+
// The catcher is a real ground surface for the denoiser — write the plane's normal/depth
|
|
235
|
+
// + a neutral albedo (black albedo would break OIDN demodulation) and mark the pixel a
|
|
236
|
+
// valid surface, so OIDN/ASVGF don't smear the caught shadow as a background miss.
|
|
237
|
+
If( sampleIndex.equal( int( 0 ) ), () => {
|
|
238
|
+
|
|
239
|
+
const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
|
|
240
|
+
writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
|
|
241
|
+
writeGBufferSurfaceID( gBufferRW, pixelIndex, uint( CATCHER_SURFACE_ID ), uint( CATCHER_SURFACE_ID ), 0.5, 0.5, uint( 1 ) );
|
|
242
|
+
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
|
|
246
|
+
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
247
|
+
rngBufferRW.element( rayID ).assign( rngState );
|
|
248
|
+
Return();
|
|
249
|
+
|
|
250
|
+
} );
|
|
251
|
+
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
} );
|
|
255
|
+
|
|
139
256
|
If( hitDist.greaterThan( MISS_DIST ), () => {
|
|
140
257
|
|
|
141
258
|
If( enableEnvironmentLight, () => {
|
|
@@ -143,11 +260,12 @@ export function buildShadeKernel( params ) {
|
|
|
143
260
|
// Ground projection bends the primary ray's background lookup onto a
|
|
144
261
|
// projected sphere+disk so the lower env hemisphere reads as a ground
|
|
145
262
|
// plane. Primary ray only; secondary bounces see the raw envmap as a light.
|
|
263
|
+
// Shared helper (also used by the shadow catcher) keeps the two in lockstep.
|
|
146
264
|
const envDir = direction.toVar();
|
|
147
|
-
If( bounceIndex.equal( 0 )
|
|
265
|
+
If( bounceIndex.equal( 0 ), () => {
|
|
148
266
|
|
|
149
|
-
envDir.assign(
|
|
150
|
-
origin, direction, groundProjectionRadius, groundProjectionHeight,
|
|
267
|
+
envDir.assign( groundProjectedEnvDir(
|
|
268
|
+
origin, direction, groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
|
|
151
269
|
) );
|
|
152
270
|
|
|
153
271
|
} );
|
|
@@ -689,7 +807,7 @@ export function buildShadeKernel( params ) {
|
|
|
689
807
|
|
|
690
808
|
} );
|
|
691
809
|
|
|
692
|
-
const directLight = calculateDirectLightingUnified(
|
|
810
|
+
const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
693
811
|
hitPoint, N, material, V,
|
|
694
812
|
brdfDir, brdfPdf, brdfValue,
|
|
695
813
|
bounceIndex, rngState,
|
|
@@ -702,7 +820,8 @@ export function buildShadeKernel( params ) {
|
|
|
702
820
|
envCDFTexture,
|
|
703
821
|
envTotalSum, envCompensationDelta, envResolution,
|
|
704
822
|
enableEnvironmentLight,
|
|
705
|
-
|
|
823
|
+
tslBool( false ), // wantUnoccluded: false on real surfaces — dead-codes the unoccluded sum
|
|
824
|
+
) ).shadowed.toVar();
|
|
706
825
|
|
|
707
826
|
const giScale = select( bounceIndex.greaterThan( 0 ), globalIlluminationIntensity, float( 1.0 ) );
|
|
708
827
|
// 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',
|
|
@@ -188,6 +188,9 @@ export class UniformManager {
|
|
|
188
188
|
ub( 'groundProjectionEnabled', DEFAULT_STATE.groundProjectionEnabled );
|
|
189
189
|
u( 'groundProjectionRadius', DEFAULT_STATE.groundProjectionRadius, 'float' );
|
|
190
190
|
u( 'groundProjectionHeight', DEFAULT_STATE.groundProjectionHeight, 'float' );
|
|
191
|
+
u( 'groundProjectionLevel', DEFAULT_STATE.groundProjectionLevel, 'float' );
|
|
192
|
+
ub( 'enableGroundCatcher', DEFAULT_STATE.enableGroundCatcher );
|
|
193
|
+
u( 'groundCatcherHeight', DEFAULT_STATE.groundCatcherHeight, 'float' );
|
|
191
194
|
|
|
192
195
|
// Sun parameters
|
|
193
196
|
u( 'sunDirection', new Vector3( 0, 1, 0 ), 'vec3' );
|