rayzee 7.28.0 → 7.28.2
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 -0
- package/dist/rayzee.es.js +2398 -2389
- package/dist/rayzee.umd.js +32 -32
- package/package.json +1 -1
- package/src/EngineDefaults.js +1 -1
- package/src/PathTracerApp.js +3 -0
- package/src/Processor/AssetLoader.js +24 -12
- package/src/Processor/QueueManager.js +9 -3
- package/src/Stages/PathTracer.js +20 -18
- package/src/TSL/CompactKernel.js +16 -3
- package/src/TSL/EmissiveSampling.js +5 -5
- package/src/TSL/LightsDirect.js +5 -10
- package/src/TSL/LightsSampling.js +154 -150
- package/src/TSL/ShadeKernel.js +18 -10
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
int,
|
|
25
25
|
bool as tslBool,
|
|
26
26
|
max,
|
|
27
|
-
min,
|
|
28
27
|
abs,
|
|
29
28
|
sqrt,
|
|
30
29
|
cos,
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
dot,
|
|
34
33
|
cross,
|
|
35
34
|
normalize,
|
|
35
|
+
length,
|
|
36
36
|
mix,
|
|
37
37
|
select,
|
|
38
38
|
If,
|
|
@@ -87,6 +87,9 @@ import {
|
|
|
87
87
|
} from './Environment.js';
|
|
88
88
|
|
|
89
89
|
const TWO_PI = 2.0 * PI;
|
|
90
|
+
// Visibility rays stop this fraction short of the sampled light point: unit-free, and ≫ float
|
|
91
|
+
// error, so an emitter coplanar with its fixture is never occluded by it.
|
|
92
|
+
const SHADOW_END = 1.0 - 1e-4;
|
|
90
93
|
|
|
91
94
|
// =============================================================================
|
|
92
95
|
// Light Sampling Functions
|
|
@@ -768,7 +771,7 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
|
|
|
768
771
|
// Optimized direct lighting function with importance-based sampling and better MIS
|
|
769
772
|
export const calculateDirectLightingUnified = Fn( ( [
|
|
770
773
|
// Surface hit data
|
|
771
|
-
hitPoint, hitNormal, material,
|
|
774
|
+
hitPoint, hitNormal, geomNormal, material,
|
|
772
775
|
// View direction
|
|
773
776
|
viewDir,
|
|
774
777
|
// BRDF sample (DirectionSample fields)
|
|
@@ -800,81 +803,80 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
800
803
|
const totalContribution = vec3( 0.0 ).toVar();
|
|
801
804
|
// Unoccluded reference (visibility forced to 1) — only filled when wantUnoccluded.
|
|
802
805
|
const unoccludedContribution = vec3( 0.0 ).toVar();
|
|
803
|
-
const rayOrigin = hitPoint.add(
|
|
806
|
+
const rayOrigin = hitPoint.add( geomNormal.mul( 0.001 ) ).toVar();
|
|
804
807
|
|
|
805
808
|
// Binds BVH params so shadow-ray sites at varying call depths use a 3-arg call
|
|
806
809
|
const shadow = Fn( ( [ origin, dir, maxDist ] ) =>
|
|
807
810
|
traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer )
|
|
808
811
|
);
|
|
809
812
|
|
|
810
|
-
// Early exit for highly emissive surfaces
|
|
811
|
-
If( material.emissiveIntensity.lessThanEqual( 10.0 ), () => {
|
|
812
813
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
814
|
+
// NEE and BSDF-hit both run at every vertex, one sample each, so the MIS weights are the
|
|
815
|
+
// plain power heuristic over the raw densities. Picking one strategy stochastically and
|
|
816
|
+
// dividing by the selection probability was unbiased but cost 3× RMSE on an area light.
|
|
817
|
+
const totalLights = numDirectionalLights.add( numAreaLights ).add( numPointLights ).add( numSpotLights ).toVar();
|
|
818
|
+
const hasDiscreteLights = totalLights.greaterThan( int( 0 ) ).toVar();
|
|
818
819
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
820
|
+
// Reservoir denominator from the NEE walk above; the BSDF-hit branch below reuses it
|
|
821
|
+
// rather than re-walking every light buffer to rebuild the same sum.
|
|
822
|
+
const neeTotalWeight = float( 0.0 ).toVar();
|
|
822
823
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
824
|
+
// =====================================================================
|
|
825
|
+
// LIGHT SAMPLING PATH
|
|
826
|
+
// =====================================================================
|
|
826
827
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// Importance-weighted light sampling
|
|
830
|
-
const lightRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
|
|
831
|
-
const lightSample = LightSample.wrap( sampleLightWithImportance(
|
|
832
|
-
rayOrigin, hitNormal, material, lightRandom, bounceIndex, rngState,
|
|
833
|
-
pixelCoord, resolution, frame, dimBase,
|
|
834
|
-
directionalLightsBuffer, numDirectionalLights,
|
|
835
|
-
areaLightsBuffer, numAreaLights,
|
|
836
|
-
pointLightsBuffer, numPointLights,
|
|
837
|
-
spotLightsBuffer, numSpotLights,
|
|
838
|
-
) );
|
|
828
|
+
If( hasDiscreteLights, () => {
|
|
839
829
|
|
|
840
|
-
|
|
830
|
+
// Importance-weighted light sampling
|
|
831
|
+
const lightRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
|
|
832
|
+
const lightSample = LightSample.wrap( sampleLightWithImportance(
|
|
833
|
+
hitPoint, hitNormal, material, lightRandom, bounceIndex, rngState,
|
|
834
|
+
pixelCoord, resolution, frame, dimBase,
|
|
835
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
836
|
+
areaLightsBuffer, numAreaLights,
|
|
837
|
+
pointLightsBuffer, numPointLights,
|
|
838
|
+
spotLightsBuffer, numSpotLights,
|
|
839
|
+
) );
|
|
841
840
|
|
|
842
|
-
|
|
841
|
+
neeTotalWeight.assign( lightSample.selectionTotalWeight );
|
|
843
842
|
|
|
844
|
-
|
|
843
|
+
If( lightSample.valid.and( lightSample.pdf.greaterThan( 0.0 ) ), () => {
|
|
845
844
|
|
|
846
|
-
|
|
845
|
+
const NoL = max( float( 0.0 ), dot( hitNormal, lightSample.direction ) ).toVar();
|
|
847
846
|
|
|
848
|
-
|
|
849
|
-
const visibility = shadow( rayOrigin, lightSample.direction, shadowDistance );
|
|
847
|
+
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: geomNormal } ) ), () => {
|
|
850
848
|
|
|
851
|
-
|
|
849
|
+
// Light geometry is measured from the hit point; only the visibility ray starts on the lifted
|
|
850
|
+
// origin, aimed at the sampled point and stopped a relative hair short of it.
|
|
851
|
+
const toSample = hitPoint.add( lightSample.direction.mul( lightSample.distance ) ).sub( rayOrigin ).toVar();
|
|
852
|
+
const shadowDistance = length( toSample ).toVar();
|
|
853
|
+
const visibility = shadow( rayOrigin, toSample.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
|
|
852
854
|
|
|
853
|
-
|
|
854
|
-
// would recompute normalize(V+L) + 5 dot products independently.
|
|
855
|
-
const sharedDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, lightSample.direction, material ) );
|
|
856
|
-
const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
|
|
857
|
-
const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
|
|
855
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
858
856
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
857
|
+
// Share H + dot products between BRDF eval and PDF — otherwise each
|
|
858
|
+
// would recompute normalize(V+L) + 5 dot products independently.
|
|
859
|
+
const sharedDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, lightSample.direction, material ) );
|
|
860
|
+
const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
|
|
861
|
+
const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
|
|
862
862
|
|
|
863
|
-
|
|
863
|
+
// Power heuristic only for area lights — they are the only type the BRDF
|
|
864
|
+
// path can intersect, so elsewhere MIS would just delete energy.
|
|
865
|
+
const misW = float( 1.0 ).toVar();
|
|
864
866
|
|
|
865
|
-
|
|
867
|
+
If( bPdf.greaterThan( 0.0 ).and( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ) ), () => {
|
|
866
868
|
|
|
867
|
-
} );
|
|
869
|
+
misW.assign( powerHeuristic( { pdf1: lightSample.pdf, pdf2: bPdf } ) );
|
|
868
870
|
|
|
869
|
-
|
|
870
|
-
// pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
|
|
871
|
-
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
|
|
872
|
-
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
873
|
-
If( wantUnoccluded, () => {
|
|
871
|
+
} );
|
|
874
872
|
|
|
875
|
-
|
|
873
|
+
// Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
|
|
874
|
+
// pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
|
|
875
|
+
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
|
|
876
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
877
|
+
If( wantUnoccluded, () => {
|
|
876
878
|
|
|
877
|
-
|
|
879
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
878
880
|
|
|
879
881
|
} );
|
|
880
882
|
|
|
@@ -884,88 +886,89 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
884
886
|
|
|
885
887
|
} );
|
|
886
888
|
|
|
887
|
-
|
|
888
|
-
// BRDF SAMPLING PATH
|
|
889
|
-
// =====================================================================
|
|
889
|
+
} );
|
|
890
890
|
|
|
891
|
-
|
|
891
|
+
// =====================================================================
|
|
892
|
+
// BRDF SAMPLING PATH
|
|
893
|
+
// =====================================================================
|
|
892
894
|
|
|
893
|
-
|
|
895
|
+
If( brdfSamplePdf.greaterThan( 0.0 ).and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
|
|
894
896
|
|
|
895
|
-
|
|
897
|
+
const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
|
|
896
898
|
|
|
897
|
-
|
|
898
|
-
// the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
|
|
899
|
-
// energy the NEE partner's MIS weight already counted on, and estimateLightImportance
|
|
900
|
-
// reads zero across the terminator band of a large light (its CENTRE is below the
|
|
901
|
-
// horizon while half of it is still visible).
|
|
902
|
-
const nearestT = float( 1e30 ).toVar();
|
|
903
|
-
const nearestLight = int( - 1 ).toVar();
|
|
899
|
+
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: geomNormal } ) ), () => {
|
|
904
900
|
|
|
905
|
-
|
|
901
|
+
// Nearest hit — the ray stops at the first emitter, so a brighter one behind it is
|
|
902
|
+
// the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
|
|
903
|
+
// energy the NEE partner's MIS weight already counted on, and estimateLightImportance
|
|
904
|
+
// reads zero across the terminator band of a large light (its CENTRE is below the
|
|
905
|
+
// horizon while half of it is still visible).
|
|
906
|
+
const nearestT = float( 1e30 ).toVar();
|
|
907
|
+
const nearestLight = int( - 1 ).toVar();
|
|
906
908
|
|
|
907
|
-
|
|
909
|
+
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
908
910
|
|
|
909
|
-
|
|
911
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
910
912
|
|
|
911
|
-
|
|
913
|
+
If( light.intensity.greaterThan( 0.0 ), () => {
|
|
912
914
|
|
|
913
|
-
|
|
915
|
+
const hitDistance = intersectAreaLight( light, hitPoint, brdfSampleDirection ).toVar();
|
|
914
916
|
|
|
915
|
-
|
|
916
|
-
nearestLight.assign( i );
|
|
917
|
+
If( hitDistance.greaterThan( 0.0 ).and( hitDistance.lessThan( nearestT ) ), () => {
|
|
917
918
|
|
|
918
|
-
|
|
919
|
+
nearestT.assign( hitDistance );
|
|
920
|
+
nearestLight.assign( i );
|
|
919
921
|
|
|
920
922
|
} );
|
|
921
923
|
|
|
922
924
|
} );
|
|
923
925
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
|
|
927
|
-
const shadowDistance = min( nearestT.sub( 0.001 ), float( 1000.0 ) );
|
|
928
|
-
const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
|
|
926
|
+
} );
|
|
929
927
|
|
|
930
|
-
|
|
928
|
+
If( nearestLight.greaterThanEqual( int( 0 ) ), () => {
|
|
931
929
|
|
|
932
|
-
|
|
930
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
|
|
931
|
+
const toLight = hitPoint.add( brdfSampleDirection.mul( nearestT ) ).sub( rayOrigin ).toVar();
|
|
932
|
+
const shadowDistance = length( toLight ).toVar();
|
|
933
|
+
const visibility = shadow( rayOrigin, toLight.div( shadowDistance ), shadowDistance.mul( SHADOW_END ) );
|
|
933
934
|
|
|
934
|
-
|
|
935
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
935
936
|
|
|
936
|
-
|
|
937
|
-
// rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
938
|
-
const corner = light.position.sub( light.u ).sub( light.v );
|
|
939
|
-
const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
940
|
-
const areaPdf = nearestT.mul( nearestT ).div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
941
|
-
const dirPdf = select(
|
|
942
|
-
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
943
|
-
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
944
|
-
areaPdf,
|
|
945
|
-
).toVar();
|
|
937
|
+
const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
|
|
946
938
|
|
|
947
|
-
|
|
948
|
-
// already accumulated. Uniform 1/N only when total importance is zero, matching
|
|
949
|
-
// the reservoir's own fallback; 0 for a light NEE can never pick, which hands
|
|
950
|
-
// this strategy the full MIS weight — as it must.
|
|
951
|
-
const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
|
|
952
|
-
const selPdf = select(
|
|
953
|
-
neeTotalWeight.greaterThan( 0.0 ),
|
|
954
|
-
selImp.div( max( neeTotalWeight, 1e-10 ) ),
|
|
955
|
-
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
956
|
-
);
|
|
957
|
-
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
958
|
-
const misW = powerHeuristic( { pdf1: brdfSamplePdf, pdf2: lightPdf } ).toVar();
|
|
939
|
+
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
959
940
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
941
|
+
// Directional pdf must match the NEE sampler: 1/S (spherical-rect) for
|
|
942
|
+
// rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
943
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
944
|
+
const sAngle = sphQuadSolidAngle( hitPoint, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
945
|
+
const areaPdf = nearestT.mul( nearestT ).div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
946
|
+
const dirPdf = select(
|
|
947
|
+
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
948
|
+
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
949
|
+
areaPdf,
|
|
950
|
+
).toVar();
|
|
965
951
|
|
|
966
|
-
|
|
952
|
+
// The NEE partner's selection pdf, rebuilt from the reservoir denominator it
|
|
953
|
+
// already accumulated. Uniform 1/N only when total importance is zero, matching
|
|
954
|
+
// the reservoir's own fallback; 0 for a light NEE can never pick, which hands
|
|
955
|
+
// this strategy the full MIS weight — as it must.
|
|
956
|
+
const selImp = estimateLightImportance( light, hitPoint, hitNormal, material );
|
|
957
|
+
const selPdf = select(
|
|
958
|
+
neeTotalWeight.greaterThan( 0.0 ),
|
|
959
|
+
selImp.div( max( neeTotalWeight, 1e-10 ) ),
|
|
960
|
+
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
961
|
+
);
|
|
962
|
+
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
963
|
+
const misW = powerHeuristic( { pdf1: brdfSamplePdf, pdf2: lightPdf } ).toVar();
|
|
964
|
+
|
|
965
|
+
const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
|
|
966
|
+
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
967
|
+
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) );
|
|
968
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
969
|
+
If( wantUnoccluded, () => {
|
|
967
970
|
|
|
968
|
-
|
|
971
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
969
972
|
|
|
970
973
|
} );
|
|
971
974
|
|
|
@@ -976,58 +979,58 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
976
979
|
} );
|
|
977
980
|
|
|
978
981
|
} );
|
|
979
|
-
// =====================================================================
|
|
980
|
-
// DETERMINISTIC ENVIRONMENT NEE
|
|
981
|
-
// Always runs (not stochastic) — forms a two-strategy Veach MIS
|
|
982
|
-
// estimator together with the implicit miss check in the main loop.
|
|
983
|
-
// =====================================================================
|
|
984
982
|
|
|
985
|
-
|
|
983
|
+
} );
|
|
984
|
+
// =====================================================================
|
|
985
|
+
// DETERMINISTIC ENVIRONMENT NEE
|
|
986
|
+
// Always runs (not stochastic) — forms a two-strategy Veach MIS
|
|
987
|
+
// estimator together with the implicit miss check in the main loop.
|
|
988
|
+
// =====================================================================
|
|
986
989
|
|
|
987
|
-
|
|
988
|
-
const envColor = vec3( 0.0 ).toVar();
|
|
990
|
+
If( enableEnvironmentLight, () => {
|
|
989
991
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
envTexture, envCDFTexture,
|
|
993
|
-
envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
|
|
994
|
-
).toVar();
|
|
992
|
+
const envRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 1 ) ), rngState, resolution, frame ).toVar();
|
|
993
|
+
const envColor = vec3( 0.0 ).toVar();
|
|
995
994
|
|
|
996
|
-
|
|
997
|
-
|
|
995
|
+
// Sample direction + PDF + color from importance-sampled environment
|
|
996
|
+
const envSampleResult = sampleEquirectProbability(
|
|
997
|
+
envTexture, envCDFTexture,
|
|
998
|
+
envMatrix, environmentIntensity, envTotalSum, envCompensationDelta, envResolution, envRandom, envColor
|
|
999
|
+
).toVar();
|
|
998
1000
|
|
|
999
|
-
|
|
1001
|
+
const envDirection = envSampleResult.xyz.toVar();
|
|
1002
|
+
const envPdf = envSampleResult.w.toVar();
|
|
1000
1003
|
|
|
1001
|
-
|
|
1004
|
+
If( envPdf.greaterThan( 0.0 ), () => {
|
|
1002
1005
|
|
|
1003
|
-
|
|
1006
|
+
const NoL = max( float( 0.0 ), dot( hitNormal, envDirection ) ).toVar();
|
|
1004
1007
|
|
|
1005
|
-
|
|
1008
|
+
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: envDirection, surfaceNormal: geomNormal } ) ), () => {
|
|
1006
1009
|
|
|
1007
|
-
|
|
1010
|
+
const visibility = shadow( rayOrigin, envDirection, float( 1e20 ) );
|
|
1008
1011
|
|
|
1009
|
-
|
|
1010
|
-
// discrete-light path above.
|
|
1011
|
-
const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, envDirection, material ) );
|
|
1012
|
-
const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
|
|
1013
|
-
const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
|
|
1012
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
1014
1013
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
float( 1.0 )
|
|
1021
|
-
).toVar();
|
|
1014
|
+
// Share H + dots between env BRDF/PDF — same redundancy fix as the
|
|
1015
|
+
// discrete-light path above.
|
|
1016
|
+
const envDots = DotProducts.wrap( computeDotProductsAniso( hitNormal, viewDir, envDirection, material ) );
|
|
1017
|
+
const brdfValue = evaluateMaterialResponseFromDots( material, envDots );
|
|
1018
|
+
const bPdf = calculateMaterialPDFFromDots( material, envDots ).toVar();
|
|
1022
1019
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1020
|
+
// Balance heuristic for env MIS — optimal for MIS-compensated PDFs (Karlík et al. 2019).
|
|
1021
|
+
// The implicit path uses material combinedPdf as prevBouncePdf at the miss check.
|
|
1022
|
+
const misW = select(
|
|
1023
|
+
bPdf.greaterThan( 0.0 ),
|
|
1024
|
+
balanceHeuristic( { pdf1: envPdf, pdf2: bPdf } ),
|
|
1025
|
+
float( 1.0 )
|
|
1026
|
+
).toVar();
|
|
1027
1027
|
|
|
1028
|
-
|
|
1028
|
+
// Base contribution WITHOUT visibility (deterministic estimator; no stochastic scaling).
|
|
1029
|
+
const baseContribution = envColor.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( envPdf, 1e-10 ) );
|
|
1030
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
1031
|
+
If( wantUnoccluded, () => {
|
|
1029
1032
|
|
|
1030
|
-
|
|
1033
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
1031
1034
|
|
|
1032
1035
|
} );
|
|
1033
1036
|
|
|
@@ -1037,7 +1040,8 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
1037
1040
|
|
|
1038
1041
|
} );
|
|
1039
1042
|
|
|
1040
|
-
} );
|
|
1043
|
+
} );
|
|
1044
|
+
|
|
1041
1045
|
|
|
1042
1046
|
// EMISSIVE TRIANGLE DIRECT LIGHTING
|
|
1043
1047
|
// NOTE: Emissive triangle sampling is handled separately in pathtracer_core.fs
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -155,6 +155,7 @@ export function buildShadeKernel( params ) {
|
|
|
155
155
|
If( threadIdx.equal( uint( 0 ) ), () => {
|
|
156
156
|
|
|
157
157
|
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
158
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_ENERGY ) ), uint( 0 ) );
|
|
158
159
|
|
|
159
160
|
} );
|
|
160
161
|
|
|
@@ -255,7 +256,7 @@ export function buildShadeKernel( params ) {
|
|
|
255
256
|
// Reuse the full NEE estimator; the diffuse BRDF is constant and cancels in the
|
|
256
257
|
// ratio, so this yields an irradiance-weighted shadow density across all lights + env.
|
|
257
258
|
const dual = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
258
|
-
planePoint, planeN, planeMat, planeV,
|
|
259
|
+
planePoint, planeN, planeN, planeMat, planeV,
|
|
259
260
|
bDir, bPdf, bVal,
|
|
260
261
|
bounceIndex, rngState,
|
|
261
262
|
_pixelCoord, resolution, frame, dimBase,
|
|
@@ -1044,16 +1045,24 @@ export function buildShadeKernel( params ) {
|
|
|
1044
1045
|
|
|
1045
1046
|
// BRDF sample (needed by both direct + indirect)
|
|
1046
1047
|
const V = direction.negate().toVar();
|
|
1048
|
+
// face-forwarded geometric normal: horizon guard for NEE and the bounce continuation
|
|
1049
|
+
const Ngeo = normalize( hitNormal );
|
|
1050
|
+
const NgeoFF = select( dot( Ngeo, V ).lessThan( 0.0 ), Ngeo.negate(), Ngeo ).toVar();
|
|
1047
1051
|
|
|
1048
|
-
// Two-sided shading: opaque path only (transmissive/SSS already continued)
|
|
1049
|
-
//
|
|
1050
|
-
// normal
|
|
1051
|
-
If( dot(
|
|
1052
|
+
// Two-sided shading: opaque path only (transmissive/SSS already continued). Decide the flip on the
|
|
1053
|
+
// GEOMETRIC normal — an inward-normal / double-sided mesh (GLB/PBRT) faces away as a whole — so a
|
|
1054
|
+
// normal-map overshoot is not mistaken for a back face and folded into the surface.
|
|
1055
|
+
If( dot( Ngeo, V ).lessThan( 0.0 ), () => {
|
|
1052
1056
|
|
|
1053
1057
|
N.assign( N.negate() );
|
|
1054
1058
|
|
|
1055
1059
|
} );
|
|
1056
1060
|
|
|
1061
|
+
// A normal-mapped N may still face away from the viewer at grazing angles. Like Cycles' diffuse
|
|
1062
|
+
// closure it is used as is: NEE and the bounce continuation already reject directions below the
|
|
1063
|
+
// geometric horizon. (Cycles raises N for glossy closures only — ensure_valid_specular_reflection;
|
|
1064
|
+
// with one shared N that would lift the diffuse term on every away-facing flank, measured +17.6 %.)
|
|
1065
|
+
|
|
1057
1066
|
// ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
|
|
1058
1067
|
// mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
|
|
1059
1068
|
// viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
|
|
@@ -1139,7 +1148,7 @@ export function buildShadeKernel( params ) {
|
|
|
1139
1148
|
} );
|
|
1140
1149
|
|
|
1141
1150
|
const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
1142
|
-
hitPoint, N, material, V,
|
|
1151
|
+
hitPoint, N, NgeoFF, material, V,
|
|
1143
1152
|
brdfDir, brdfPdf, brdfValue,
|
|
1144
1153
|
bounceIndex, rngState,
|
|
1145
1154
|
_pixelCoord, resolution, frame, dimBase,
|
|
@@ -1205,9 +1214,9 @@ export function buildShadeKernel( params ) {
|
|
|
1205
1214
|
|
|
1206
1215
|
const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
|
|
1207
1216
|
|
|
1208
|
-
If( NoL.greaterThan( 0.0 ), () => {
|
|
1217
|
+
If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) ), () => {
|
|
1209
1218
|
|
|
1210
|
-
const rayOffset = calculateRayOffset( hitPoint,
|
|
1219
|
+
const rayOffset = calculateRayOffset( hitPoint, NgeoFF, material );
|
|
1211
1220
|
const rayOrigin = hitPoint.add( rayOffset );
|
|
1212
1221
|
const shadowDist = emissiveSample.distance.sub( 0.001 );
|
|
1213
1222
|
const visibility = traceShadowRayWrapped(
|
|
@@ -1248,7 +1257,7 @@ export function buildShadeKernel( params ) {
|
|
|
1248
1257
|
} ).Else( () => {
|
|
1249
1258
|
|
|
1250
1259
|
const emissiveLight = calculateEmissiveTriangleContribution(
|
|
1251
|
-
hitPoint, N, V, material,
|
|
1260
|
+
hitPoint, N, NgeoFF, V, material,
|
|
1252
1261
|
bounceIndex, rngState,
|
|
1253
1262
|
_pixelCoord, resolution, frame, dimBase,
|
|
1254
1263
|
emissiveBoost,
|
|
@@ -1291,7 +1300,6 @@ export function buildShadeKernel( params ) {
|
|
|
1291
1300
|
|
|
1292
1301
|
// Shading-normal leak guard: a normal-mapped lobe can sample below the geometric surface;
|
|
1293
1302
|
// tracing that ray tunnels through single-sided shells onto whatever sits behind them.
|
|
1294
|
-
const NgeoFF = select( dot( hitNormal, V ).lessThan( 0.0 ), hitNormal.negate(), hitNormal );
|
|
1295
1303
|
If( brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ), () => {
|
|
1296
1304
|
|
|
1297
1305
|
commitDeferredAux( N );
|