rayzee 7.28.0 → 7.28.1
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 +2328 -2334
- package/dist/rayzee.umd.js +28 -28
- package/package.json +1 -1
- package/src/EngineDefaults.js +1 -1
- package/src/Processor/AssetLoader.js +11 -8
- 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 +17 -10
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const RENDER_PROFILES = Object.freeze( {
|
|
12
12
|
viewer: Object.freeze( {
|
|
13
|
-
areaLightIntensityScale: 0.1, //
|
|
13
|
+
areaLightIntensityScale: 0.1, // scales glTF placeholder area-light power (viewer tuning)
|
|
14
14
|
environmentRotation: 270.0, // degrees
|
|
15
15
|
toneMapping: 4, // ACESFilmicToneMapping
|
|
16
16
|
saturation: 1.2, // compensates for ACES desaturation — see Compositor
|
|
@@ -1759,20 +1759,23 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1759
1759
|
|
|
1760
1760
|
if ( userData.type === 'RectAreaLight' ) {
|
|
1761
1761
|
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1762
|
+
// Authored intensity is three.js radiance (these files also carry power = intensity·w·h·π);
|
|
1763
|
+
// convert to the engine's radiant power through the world area so Normalize reproduces it.
|
|
1764
|
+
const worldScale = object.getWorldScale( new Vector3() );
|
|
1765
|
+
const normalize = userData.normalize ?? true;
|
|
1766
|
+
const shape = userData.shape ?? 'rectangle';
|
|
1767
|
+
const shapeFactor = shape === 'ellipse' || shape === 'disk' ? Math.PI / 4 : 1;
|
|
1768
|
+
const worldArea = shapeFactor * userData.width * worldScale.x * userData.height * worldScale.y;
|
|
1769
|
+
const power = userData.intensity * Math.PI * ( normalize ? worldArea : 1 );
|
|
1766
1770
|
const light = new RectAreaLight(
|
|
1767
1771
|
new Color( ...userData.color ),
|
|
1768
|
-
|
|
1772
|
+
power * this._profile.areaLightIntensityScale,
|
|
1769
1773
|
userData.width,
|
|
1770
1774
|
userData.height
|
|
1771
1775
|
);
|
|
1772
|
-
light.userData.normalize =
|
|
1776
|
+
light.userData.normalize = normalize;
|
|
1773
1777
|
light.userData.spread = Number.isFinite( userData.spread ) ? userData.spread : Math.PI;
|
|
1774
|
-
light.userData.shape =
|
|
1775
|
-
light.position.z = - 2;
|
|
1778
|
+
light.userData.shape = shape;
|
|
1776
1779
|
light.name = userData.name;
|
|
1777
1780
|
object.add( light );
|
|
1778
1781
|
visitedAreaLights.push( light.uuid );
|
|
@@ -527,7 +527,7 @@ export const sampleEmissiveTriangle = Fn( ( [
|
|
|
527
527
|
// products with the PDF call below.
|
|
528
528
|
|
|
529
529
|
export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
530
|
-
hitPoint, normal, viewDir, material,
|
|
530
|
+
hitPoint, normal, geomNormal, viewDir, material,
|
|
531
531
|
bounceIndex, rngState,
|
|
532
532
|
pixelCoord, resolution, frame, dimBase,
|
|
533
533
|
emissiveBoost,
|
|
@@ -565,10 +565,10 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
|
565
565
|
// Check geometric validity
|
|
566
566
|
const NoL = max( float( 0.0 ), dot( normal, emissiveSample.direction ) );
|
|
567
567
|
|
|
568
|
-
If( NoL.greaterThan( 0.0 ), () => {
|
|
568
|
+
If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, geomNormal ).greaterThan( 0.0 ) ), () => {
|
|
569
569
|
|
|
570
570
|
// Calculate ray offset for shadow ray
|
|
571
|
-
const rayOffset = calculateRayOffsetFn( hitPoint,
|
|
571
|
+
const rayOffset = calculateRayOffsetFn( hitPoint, geomNormal, material );
|
|
572
572
|
const rayOrigin = hitPoint.add( rayOffset );
|
|
573
573
|
|
|
574
574
|
// Trace shadow ray
|
|
@@ -609,7 +609,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
|
|
|
609
609
|
|
|
610
610
|
// Wrapper that returns just the contribution vec3
|
|
611
611
|
export const calculateEmissiveTriangleContribution = Fn( ( [
|
|
612
|
-
hitPoint, normal, viewDir, material,
|
|
612
|
+
hitPoint, normal, geomNormal, viewDir, material,
|
|
613
613
|
bounceIndex, rngState,
|
|
614
614
|
pixelCoord, resolution, frame, dimBase,
|
|
615
615
|
emissiveBoost,
|
|
@@ -620,7 +620,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
|
|
|
620
620
|
] ) => {
|
|
621
621
|
|
|
622
622
|
const result = EmissiveContributionResult.wrap( calculateEmissiveTriangleContributionDebug(
|
|
623
|
-
hitPoint, normal, viewDir, material,
|
|
623
|
+
hitPoint, normal, geomNormal, viewDir, material,
|
|
624
624
|
bounceIndex, rngState,
|
|
625
625
|
pixelCoord, resolution, frame, dimBase,
|
|
626
626
|
emissiveBoost,
|
package/src/TSL/LightsDirect.js
CHANGED
|
@@ -341,8 +341,10 @@ export const estimateLightImportance = Fn( ( [ light, hitPoint, normal, material
|
|
|
341
341
|
|
|
342
342
|
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
343
343
|
|
|
344
|
-
|
|
345
|
-
const
|
|
344
|
+
// Diffuse irradiance ∝ L·Ω·cosθ·cosθ′, Ω bounded by the hemisphere. Unit-free.
|
|
345
|
+
const solidAngle = min( light.area.div( max( distSq, 1e-12 ) ), float( 2.0 * Math.PI ) );
|
|
346
|
+
const invArea = select( light.normalize.greaterThan( 0.5 ), float( 1.0 ).div( max( light.area, 1e-10 ) ), float( 1.0 ) );
|
|
347
|
+
const radiance = light.intensity.mul( dot( light.color, REC709_LUMINANCE_COEFFICIENTS ) ).mul( invArea );
|
|
346
348
|
|
|
347
349
|
// Material-aware weighting
|
|
348
350
|
const materialFactor = float( 1.0 ).toVar();
|
|
@@ -358,20 +360,13 @@ export const estimateLightImportance = Fn( ( [ light, hitPoint, normal, material
|
|
|
358
360
|
|
|
359
361
|
} );
|
|
360
362
|
|
|
361
|
-
If( material.roughness.greaterThan( 0.6 ).and( material.metalness.lessThan( 0.3 ) ), () => {
|
|
362
|
-
|
|
363
|
-
const sizeBoost = min( light.area.mul( 2.0 ), float( 2.0 ) );
|
|
364
|
-
materialFactor.mulAssign( sizeBoost );
|
|
365
|
-
|
|
366
|
-
} );
|
|
367
|
-
|
|
368
363
|
If( material.transmission.greaterThan( 0.5 ), () => {
|
|
369
364
|
|
|
370
365
|
materialFactor.mulAssign( float( 1.0 ).add( material.transmission.mul( 0.3 ) ) );
|
|
371
366
|
|
|
372
367
|
} );
|
|
373
368
|
|
|
374
|
-
result.assign(
|
|
369
|
+
result.assign( radiance.mul( solidAngle ).mul( NoL ).mul( lightFacing ).mul( materialFactor ) );
|
|
375
370
|
|
|
376
371
|
} );
|
|
377
372
|
|
|
@@ -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
|
@@ -255,7 +255,7 @@ export function buildShadeKernel( params ) {
|
|
|
255
255
|
// Reuse the full NEE estimator; the diffuse BRDF is constant and cancels in the
|
|
256
256
|
// ratio, so this yields an irradiance-weighted shadow density across all lights + env.
|
|
257
257
|
const dual = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
258
|
-
planePoint, planeN, planeMat, planeV,
|
|
258
|
+
planePoint, planeN, planeN, planeMat, planeV,
|
|
259
259
|
bDir, bPdf, bVal,
|
|
260
260
|
bounceIndex, rngState,
|
|
261
261
|
_pixelCoord, resolution, frame, dimBase,
|
|
@@ -1044,16 +1044,24 @@ export function buildShadeKernel( params ) {
|
|
|
1044
1044
|
|
|
1045
1045
|
// BRDF sample (needed by both direct + indirect)
|
|
1046
1046
|
const V = direction.negate().toVar();
|
|
1047
|
+
// face-forwarded geometric normal: horizon guard for NEE and the bounce continuation
|
|
1048
|
+
const Ngeo = normalize( hitNormal );
|
|
1049
|
+
const NgeoFF = select( dot( Ngeo, V ).lessThan( 0.0 ), Ngeo.negate(), Ngeo ).toVar();
|
|
1047
1050
|
|
|
1048
|
-
// Two-sided shading: opaque path only (transmissive/SSS already continued)
|
|
1049
|
-
//
|
|
1050
|
-
// normal
|
|
1051
|
-
If( dot(
|
|
1051
|
+
// Two-sided shading: opaque path only (transmissive/SSS already continued). Decide the flip on the
|
|
1052
|
+
// GEOMETRIC normal — an inward-normal / double-sided mesh (GLB/PBRT) faces away as a whole — so a
|
|
1053
|
+
// normal-map overshoot is not mistaken for a back face and folded into the surface.
|
|
1054
|
+
If( dot( Ngeo, V ).lessThan( 0.0 ), () => {
|
|
1052
1055
|
|
|
1053
1056
|
N.assign( N.negate() );
|
|
1054
1057
|
|
|
1055
1058
|
} );
|
|
1056
1059
|
|
|
1060
|
+
// A normal-mapped N may still face away from the viewer at grazing angles. Like Cycles' diffuse
|
|
1061
|
+
// closure it is used as is: NEE and the bounce continuation already reject directions below the
|
|
1062
|
+
// geometric horizon. (Cycles raises N for glossy closures only — ensure_valid_specular_reflection;
|
|
1063
|
+
// with one shared N that would lift the diffuse term on every away-facing flank, measured +17.6 %.)
|
|
1064
|
+
|
|
1057
1065
|
// ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
|
|
1058
1066
|
// mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
|
|
1059
1067
|
// viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
|
|
@@ -1139,7 +1147,7 @@ export function buildShadeKernel( params ) {
|
|
|
1139
1147
|
} );
|
|
1140
1148
|
|
|
1141
1149
|
const directLight = DirectLightingDual.wrap( calculateDirectLightingUnified(
|
|
1142
|
-
hitPoint, N, material, V,
|
|
1150
|
+
hitPoint, N, NgeoFF, material, V,
|
|
1143
1151
|
brdfDir, brdfPdf, brdfValue,
|
|
1144
1152
|
bounceIndex, rngState,
|
|
1145
1153
|
_pixelCoord, resolution, frame, dimBase,
|
|
@@ -1205,9 +1213,9 @@ export function buildShadeKernel( params ) {
|
|
|
1205
1213
|
|
|
1206
1214
|
const NoL = max( float( 0.0 ), dot( N, emissiveSample.direction ) );
|
|
1207
1215
|
|
|
1208
|
-
If( NoL.greaterThan( 0.0 ), () => {
|
|
1216
|
+
If( NoL.greaterThan( 0.0 ).and( dot( emissiveSample.direction, NgeoFF ).greaterThan( 0.0 ) ), () => {
|
|
1209
1217
|
|
|
1210
|
-
const rayOffset = calculateRayOffset( hitPoint,
|
|
1218
|
+
const rayOffset = calculateRayOffset( hitPoint, NgeoFF, material );
|
|
1211
1219
|
const rayOrigin = hitPoint.add( rayOffset );
|
|
1212
1220
|
const shadowDist = emissiveSample.distance.sub( 0.001 );
|
|
1213
1221
|
const visibility = traceShadowRayWrapped(
|
|
@@ -1248,7 +1256,7 @@ export function buildShadeKernel( params ) {
|
|
|
1248
1256
|
} ).Else( () => {
|
|
1249
1257
|
|
|
1250
1258
|
const emissiveLight = calculateEmissiveTriangleContribution(
|
|
1251
|
-
hitPoint, N, V, material,
|
|
1259
|
+
hitPoint, N, NgeoFF, V, material,
|
|
1252
1260
|
bounceIndex, rngState,
|
|
1253
1261
|
_pixelCoord, resolution, frame, dimBase,
|
|
1254
1262
|
emissiveBoost,
|
|
@@ -1291,7 +1299,6 @@ export function buildShadeKernel( params ) {
|
|
|
1291
1299
|
|
|
1292
1300
|
// Shading-normal leak guard: a normal-mapped lobe can sample below the geometric surface;
|
|
1293
1301
|
// 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
1302
|
If( brdfIsTransmission.not().and( dot( bounceDir, NgeoFF ).lessThanEqual( 0.0 ) ), () => {
|
|
1296
1303
|
|
|
1297
1304
|
commitDeferredAux( N );
|