rayzee 7.20.3 → 7.22.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 +75 -4
- package/dist/rayzee.es.js +2821 -2854
- package/dist/rayzee.umd.js +60 -58
- package/package.json +1 -1
- package/src/EngineDefaults.js +9 -2
- package/src/PathTracerApp.js +83 -0
- package/src/Pipeline/BindingAudit.js +178 -0
- package/src/Pipeline/RenderPipeline.js +5 -2
- package/src/Processor/BVHBuilder.js +1 -4
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/KernelManager.js +17 -70
- package/src/Processor/PackedRayBuffer.js +33 -8
- package/src/Processor/QueueManager.js +4 -3
- package/src/Processor/SceneProcessor.js +10 -4
- package/src/Processor/TreeletOptimizer.js +0 -7
- package/src/RenderSettings.js +41 -6
- package/src/Stages/ASVGF.js +10 -10
- package/src/Stages/BilateralFilter.js +70 -11
- package/src/Stages/EdgeFilter.js +44 -6
- package/src/Stages/NormalDepth.js +26 -15
- package/src/Stages/PathTracer.js +36 -34
- package/src/Stages/PathTracerStage.js +15 -46
- package/src/TSL/BVHTraversal.js +1 -67
- package/src/TSL/CameraRay.js +141 -0
- package/src/TSL/Common.js +1 -47
- package/src/TSL/DebugKernel.js +4 -5
- package/src/TSL/EmissiveSampling.js +40 -44
- package/src/TSL/ExtendKernel.js +11 -3
- package/src/TSL/FinalWriteKernel.js +1 -14
- package/src/TSL/GenerateKernel.js +11 -8
- package/src/TSL/LightsCore.js +3 -0
- package/src/TSL/LightsDirect.js +4 -4
- package/src/TSL/LightsSampling.js +138 -327
- package/src/TSL/PathTracerCore.js +2 -2
- package/src/TSL/Random.js +2 -2
- package/src/TSL/ShadeKernel.js +39 -17
- package/src/TSL/SortGlobalKernels.js +52 -7
- package/src/TSL/Struct.js +0 -10
- package/src/index.js +5 -0
- package/src/managers/DenoisingManager.js +13 -0
- package/src/managers/EnvironmentManager.js +6 -8
- package/src/managers/UniformManager.js +12 -2
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Pure TSL: Fn(), If(), Loop(), .toVar(), .assign() — NO wgslFn()
|
|
5
5
|
*
|
|
6
6
|
* Direct lighting combines:
|
|
7
|
-
* -
|
|
7
|
+
* - Discrete light NEE + BSDF-hit, both evaluated per vertex (area, point, spot, directional)
|
|
8
8
|
* - Deterministic environment NEE (always runs, two-strategy Veach MIS with implicit miss)
|
|
9
9
|
*
|
|
10
10
|
* Contains:
|
|
11
11
|
* - sampleRectAreaLight — rectangle area light sampling
|
|
12
12
|
* - sampleSpotLightWithRadius — spot light sampling with radius
|
|
13
13
|
* - samplePointLightWithAttenuation — point light sampling with attenuation
|
|
14
|
-
* - sampleLightWithImportance — importance-weighted light selection (
|
|
14
|
+
* - sampleLightWithImportance — importance-weighted light selection (reservoir)
|
|
15
15
|
* - calculateMaterialPDF — material PDF for MIS
|
|
16
16
|
* - calculateDirectLightingUnified — unified direct lighting (main entry)
|
|
17
17
|
*/
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
sphQuadSolidAngle,
|
|
62
62
|
} from './LightsCore.js';
|
|
63
63
|
|
|
64
|
-
import {
|
|
64
|
+
import { DotProducts, DirectLightingDual } from './Struct.js';
|
|
65
65
|
import {
|
|
66
66
|
calculateDirectionalLightImportance,
|
|
67
67
|
estimateLightImportance,
|
|
@@ -75,7 +75,6 @@ import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
|
|
|
75
75
|
import { calculateBSDFSamplingPDFFromMaterial } from './MaterialProperties.js';
|
|
76
76
|
import { getRandomSample1D, getRandomSample2D, SAMPLER_DIM_AUX_BASE } from './Random.js';
|
|
77
77
|
import {
|
|
78
|
-
selectOptimalMISStrategy,
|
|
79
78
|
PI,
|
|
80
79
|
PI_INV,
|
|
81
80
|
EPSILON,
|
|
@@ -223,6 +222,7 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
223
222
|
distance: ls_distance,
|
|
224
223
|
pdf: ls_pdf,
|
|
225
224
|
lightType: ls_lightType,
|
|
225
|
+
selectionTotalWeight: float( 0.0 ),
|
|
226
226
|
} );
|
|
227
227
|
|
|
228
228
|
} );
|
|
@@ -281,6 +281,7 @@ export const sampleSpotLightWithRadius = Fn( ( [ light, rayOrigin, lightSelectio
|
|
|
281
281
|
distance: ls_distance,
|
|
282
282
|
pdf: ls_pdf,
|
|
283
283
|
lightType: ls_lightType,
|
|
284
|
+
selectionTotalWeight: float( 0.0 ),
|
|
284
285
|
} );
|
|
285
286
|
|
|
286
287
|
} );
|
|
@@ -324,6 +325,7 @@ export const samplePointLightWithAttenuation = Fn( ( [ light, rayOrigin, lightSe
|
|
|
324
325
|
distance: ls_distance,
|
|
325
326
|
pdf: ls_pdf,
|
|
326
327
|
lightType: ls_lightType,
|
|
328
|
+
selectionTotalWeight: float( 0.0 ),
|
|
327
329
|
} );
|
|
328
330
|
|
|
329
331
|
} );
|
|
@@ -355,12 +357,19 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
355
357
|
const r_distance = float( 0.0 ).toVar();
|
|
356
358
|
const r_pdf = float( 0.0 ).toVar();
|
|
357
359
|
const r_lightType = int( LIGHT_TYPE_POINT ).toVar();
|
|
360
|
+
const totalWeight = float( 0.0 ).toVar();
|
|
358
361
|
|
|
359
362
|
const totalLights = numDirectionalLights.add( numAreaLights ).add( numPointLights ).add( numSpotLights ).toVar();
|
|
360
363
|
|
|
361
364
|
If( totalLights.greaterThan( int( 0 ) ), () => {
|
|
362
365
|
|
|
363
|
-
|
|
366
|
+
// One sampler dimension per light in the scene: the reservoir's replacement tests must
|
|
367
|
+
// be mutually independent, and a per-type block hands light 16 of one type the same
|
|
368
|
+
// variate as light 0 of the next.
|
|
369
|
+
const dimDir = int( SAMPLER_DIM_AUX_BASE + 64 ).toVar();
|
|
370
|
+
const dimArea = dimDir.add( numDirectionalLights ).toVar();
|
|
371
|
+
const dimPoint = dimArea.add( numAreaLights ).toVar();
|
|
372
|
+
const dimSpot = dimPoint.add( numPointLights ).toVar();
|
|
364
373
|
|
|
365
374
|
// Reservoir state: winning light's type/index/importance.
|
|
366
375
|
const selectedType = int( - 1 ).toVar(); // 0=dir, 1=area, 2=point, 3=spot
|
|
@@ -375,22 +384,18 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
375
384
|
|
|
376
385
|
Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
377
386
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
selectedIdx.assign( i );
|
|
391
|
-
selectedImportance.assign( importance );
|
|
392
|
-
|
|
393
|
-
} );
|
|
387
|
+
const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
|
|
388
|
+
const importance = calculateDirectionalLightImportance( light, normal, material, bounceIndex ).toVar();
|
|
389
|
+
totalWeight.addAssign( importance );
|
|
390
|
+
If( importance.greaterThan( 0.0 ).and(
|
|
391
|
+
getRandomSample1D( pixelCoord, int( 0 ),
|
|
392
|
+
dimDir.add( i ), rngState, resolution, frame
|
|
393
|
+
).mul( totalWeight ).lessThan( importance )
|
|
394
|
+
), () => {
|
|
395
|
+
|
|
396
|
+
selectedType.assign( 0 );
|
|
397
|
+
selectedIdx.assign( i );
|
|
398
|
+
selectedImportance.assign( importance );
|
|
394
399
|
|
|
395
400
|
} );
|
|
396
401
|
|
|
@@ -402,22 +407,18 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
402
407
|
|
|
403
408
|
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
404
409
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
selectedIdx.assign( i );
|
|
418
|
-
selectedImportance.assign( importance );
|
|
419
|
-
|
|
420
|
-
} );
|
|
410
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
411
|
+
const importance = select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ).toVar();
|
|
412
|
+
totalWeight.addAssign( importance );
|
|
413
|
+
If( importance.greaterThan( 0.0 ).and(
|
|
414
|
+
getRandomSample1D( pixelCoord, int( 0 ),
|
|
415
|
+
dimArea.add( i ), rngState, resolution, frame
|
|
416
|
+
).mul( totalWeight ).lessThan( importance )
|
|
417
|
+
), () => {
|
|
418
|
+
|
|
419
|
+
selectedType.assign( 1 );
|
|
420
|
+
selectedIdx.assign( i );
|
|
421
|
+
selectedImportance.assign( importance );
|
|
421
422
|
|
|
422
423
|
} );
|
|
423
424
|
|
|
@@ -429,22 +430,18 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
429
430
|
|
|
430
431
|
Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
431
432
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
selectedIdx.assign( i );
|
|
445
|
-
selectedImportance.assign( importance );
|
|
446
|
-
|
|
447
|
-
} );
|
|
433
|
+
const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
|
|
434
|
+
const importance = calculatePointLightImportance( light, rayOrigin, normal, material ).toVar();
|
|
435
|
+
totalWeight.addAssign( importance );
|
|
436
|
+
If( importance.greaterThan( 0.0 ).and(
|
|
437
|
+
getRandomSample1D( pixelCoord, int( 0 ),
|
|
438
|
+
dimPoint.add( i ), rngState, resolution, frame
|
|
439
|
+
).mul( totalWeight ).lessThan( importance )
|
|
440
|
+
), () => {
|
|
441
|
+
|
|
442
|
+
selectedType.assign( 2 );
|
|
443
|
+
selectedIdx.assign( i );
|
|
444
|
+
selectedImportance.assign( importance );
|
|
448
445
|
|
|
449
446
|
} );
|
|
450
447
|
|
|
@@ -456,22 +453,18 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
456
453
|
|
|
457
454
|
Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
458
455
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
selectedIdx.assign( i );
|
|
472
|
-
selectedImportance.assign( importance );
|
|
473
|
-
|
|
474
|
-
} );
|
|
456
|
+
const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
|
|
457
|
+
const importance = calculateSpotLightImportance( light, rayOrigin, normal, material ).toVar();
|
|
458
|
+
totalWeight.addAssign( importance );
|
|
459
|
+
If( importance.greaterThan( 0.0 ).and(
|
|
460
|
+
getRandomSample1D( pixelCoord, int( 0 ),
|
|
461
|
+
dimSpot.add( i ), rngState, resolution, frame
|
|
462
|
+
).mul( totalWeight ).lessThan( importance )
|
|
463
|
+
), () => {
|
|
464
|
+
|
|
465
|
+
selectedType.assign( 3 );
|
|
466
|
+
selectedIdx.assign( i );
|
|
467
|
+
selectedImportance.assign( importance );
|
|
475
468
|
|
|
476
469
|
} );
|
|
477
470
|
|
|
@@ -698,6 +691,7 @@ export const sampleLightWithImportance = Fn( ( [
|
|
|
698
691
|
distance: r_distance,
|
|
699
692
|
pdf: r_pdf,
|
|
700
693
|
lightType: r_lightType,
|
|
694
|
+
selectionTotalWeight: totalWeight,
|
|
701
695
|
} );
|
|
702
696
|
|
|
703
697
|
} );
|
|
@@ -724,86 +718,6 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
|
|
|
724
718
|
|
|
725
719
|
} );
|
|
726
720
|
|
|
727
|
-
// Total light-selection weight at a shading point, replicating the NEE reservoir's
|
|
728
|
-
// importance accumulation (same per-type importance fns, same per-type 16-light cap + order).
|
|
729
|
-
// Used to reconstruct the exact NEE selection pdf for MIS on the BSDF-hit path —
|
|
730
|
-
// the analogue of Cycles' light_tree_pdf re-walk. Without it, the BSDF-hit MIS
|
|
731
|
-
// partner assumes uniform 1/N selection, which disagrees with the importance-
|
|
732
|
-
// weighted NEE selection and biases the power-heuristic weights.
|
|
733
|
-
export const computeTotalLightImportance = Fn( ( [
|
|
734
|
-
rayOrigin, normal, material, bounceIndex,
|
|
735
|
-
directionalLightsBuffer, numDirectionalLights,
|
|
736
|
-
areaLightsBuffer, numAreaLights,
|
|
737
|
-
pointLightsBuffer, numPointLights,
|
|
738
|
-
spotLightsBuffer, numSpotLights,
|
|
739
|
-
] ) => {
|
|
740
|
-
|
|
741
|
-
const totalWeight = float( 0.0 ).toVar();
|
|
742
|
-
|
|
743
|
-
If( numDirectionalLights.greaterThan( int( 0 ) ), () => {
|
|
744
|
-
|
|
745
|
-
Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
746
|
-
|
|
747
|
-
If( i.lessThan( int( 16 ) ), () => {
|
|
748
|
-
|
|
749
|
-
const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
|
|
750
|
-
totalWeight.addAssign( calculateDirectionalLightImportance( light, normal, material, bounceIndex ) );
|
|
751
|
-
|
|
752
|
-
} );
|
|
753
|
-
|
|
754
|
-
} );
|
|
755
|
-
|
|
756
|
-
} );
|
|
757
|
-
|
|
758
|
-
If( numAreaLights.greaterThan( int( 0 ) ), () => {
|
|
759
|
-
|
|
760
|
-
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
761
|
-
|
|
762
|
-
If( i.lessThan( int( 16 ) ), () => {
|
|
763
|
-
|
|
764
|
-
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
765
|
-
totalWeight.addAssign( select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ) );
|
|
766
|
-
|
|
767
|
-
} );
|
|
768
|
-
|
|
769
|
-
} );
|
|
770
|
-
|
|
771
|
-
} );
|
|
772
|
-
|
|
773
|
-
If( numPointLights.greaterThan( int( 0 ) ), () => {
|
|
774
|
-
|
|
775
|
-
Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
776
|
-
|
|
777
|
-
If( i.lessThan( int( 16 ) ), () => {
|
|
778
|
-
|
|
779
|
-
const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
|
|
780
|
-
totalWeight.addAssign( calculatePointLightImportance( light, rayOrigin, normal, material ) );
|
|
781
|
-
|
|
782
|
-
} );
|
|
783
|
-
|
|
784
|
-
} );
|
|
785
|
-
|
|
786
|
-
} );
|
|
787
|
-
|
|
788
|
-
If( numSpotLights.greaterThan( int( 0 ) ), () => {
|
|
789
|
-
|
|
790
|
-
Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
791
|
-
|
|
792
|
-
If( i.lessThan( int( 16 ) ), () => {
|
|
793
|
-
|
|
794
|
-
const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
|
|
795
|
-
totalWeight.addAssign( calculateSpotLightImportance( light, rayOrigin, normal, material ) );
|
|
796
|
-
|
|
797
|
-
} );
|
|
798
|
-
|
|
799
|
-
} );
|
|
800
|
-
|
|
801
|
-
} );
|
|
802
|
-
|
|
803
|
-
return totalWeight;
|
|
804
|
-
|
|
805
|
-
} );
|
|
806
|
-
|
|
807
721
|
// =============================================================================
|
|
808
722
|
// Unified Direct Lighting System
|
|
809
723
|
// =============================================================================
|
|
@@ -853,81 +767,21 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
853
767
|
// Early exit for highly emissive surfaces
|
|
854
768
|
If( material.emissiveIntensity.lessThanEqual( 10.0 ), () => {
|
|
855
769
|
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
material.roughness, material.metalness, bounceIndex, currentThroughput
|
|
860
|
-
) );
|
|
861
|
-
|
|
862
|
-
// Extract MIS fields to mutable variables
|
|
863
|
-
// (env is handled deterministically below, not part of stochastic selection)
|
|
864
|
-
const useBRDFSampling = misResult.useBRDFSampling.toVar();
|
|
865
|
-
const useLightSampling = misResult.useLightSampling.toVar();
|
|
866
|
-
const brdfWeight = misResult.brdfWeight.toVar();
|
|
867
|
-
const lightWeight = misResult.lightWeight.toVar();
|
|
868
|
-
|
|
869
|
-
// Adaptive light processing
|
|
770
|
+
// NEE and BSDF-hit both run at every vertex, one sample each, so the MIS weights are the
|
|
771
|
+
// plain power heuristic over the raw densities. Picking one strategy stochastically and
|
|
772
|
+
// dividing by the selection probability was unbiased but cost 3× RMSE on an area light.
|
|
870
773
|
const totalLights = numDirectionalLights.add( numAreaLights ).add( numPointLights ).add( numSpotLights ).toVar();
|
|
871
|
-
|
|
872
|
-
const importanceThreshold = float( 0.001 ).mul( float( 1.0 ).add( float( bounceIndex ).mul( 0.5 ) ) ).toVar();
|
|
873
|
-
|
|
874
|
-
// Check if discrete lights exist
|
|
875
774
|
const hasDiscreteLights = totalLights.greaterThan( int( 0 ) ).toVar();
|
|
876
775
|
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
If( useLightSampling.and( hasDiscreteLights ), () => {
|
|
881
|
-
|
|
882
|
-
totalSamplingWeight.addAssign( lightWeight );
|
|
883
|
-
|
|
884
|
-
} );
|
|
885
|
-
|
|
886
|
-
If( useBRDFSampling, () => {
|
|
887
|
-
|
|
888
|
-
totalSamplingWeight.addAssign( brdfWeight );
|
|
889
|
-
|
|
890
|
-
} );
|
|
891
|
-
|
|
892
|
-
If( totalSamplingWeight.lessThanEqual( 0.0 ), () => {
|
|
893
|
-
|
|
894
|
-
totalSamplingWeight.assign( 1.0 );
|
|
895
|
-
useBRDFSampling.assign( tslBool( true ) );
|
|
896
|
-
brdfWeight.assign( 1.0 );
|
|
897
|
-
|
|
898
|
-
} );
|
|
899
|
-
|
|
900
|
-
// Determine sampling technique: stochastic {lights, BRDF}
|
|
901
|
-
const rand = getRandomSample1D( pixelCoord, int( 0 ), dimBase, rngState, resolution, frame ).toVar();
|
|
902
|
-
const sampleLights = tslBool( false ).toVar();
|
|
903
|
-
const sampleBRDF = tslBool( false ).toVar();
|
|
904
|
-
|
|
905
|
-
// Calculate effective weights for probability (only include light weight if lights exist)
|
|
906
|
-
const effectiveLightWeight = select( hasDiscreteLights, lightWeight, float( 0.0 ) );
|
|
907
|
-
// Guard division
|
|
908
|
-
const invTotalSamplingWeight = float( 1.0 ).div( max( totalSamplingWeight, 1e-10 ) );
|
|
909
|
-
const cumulativeLight = effectiveLightWeight.mul( invTotalSamplingWeight );
|
|
910
|
-
|
|
911
|
-
If( rand.lessThan( cumulativeLight ).and( useLightSampling ).and( hasDiscreteLights ), () => {
|
|
912
|
-
|
|
913
|
-
sampleLights.assign( tslBool( true ) );
|
|
914
|
-
|
|
915
|
-
} ).ElseIf( useBRDFSampling, () => {
|
|
916
|
-
|
|
917
|
-
sampleBRDF.assign( tslBool( true ) );
|
|
918
|
-
|
|
919
|
-
} ).ElseIf( hasDiscreteLights, () => {
|
|
920
|
-
|
|
921
|
-
// Fallback to light sampling only if lights exist
|
|
922
|
-
sampleLights.assign( tslBool( true ) );
|
|
923
|
-
|
|
924
|
-
} );
|
|
776
|
+
// Reservoir denominator from the NEE walk above; the BSDF-hit branch below reuses it
|
|
777
|
+
// rather than re-walking every light buffer to rebuild the same sum.
|
|
778
|
+
const neeTotalWeight = float( 0.0 ).toVar();
|
|
925
779
|
|
|
926
780
|
// =====================================================================
|
|
927
781
|
// LIGHT SAMPLING PATH
|
|
928
782
|
// =====================================================================
|
|
929
783
|
|
|
930
|
-
If(
|
|
784
|
+
If( hasDiscreteLights, () => {
|
|
931
785
|
|
|
932
786
|
// Importance-weighted light sampling
|
|
933
787
|
const lightRandom = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 2 ) ), rngState, resolution, frame ).toVar();
|
|
@@ -940,12 +794,13 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
940
794
|
spotLightsBuffer, numSpotLights,
|
|
941
795
|
) );
|
|
942
796
|
|
|
797
|
+
neeTotalWeight.assign( lightSample.selectionTotalWeight );
|
|
798
|
+
|
|
943
799
|
If( lightSample.valid.and( lightSample.pdf.greaterThan( 0.0 ) ), () => {
|
|
944
800
|
|
|
945
801
|
const NoL = max( float( 0.0 ), dot( hitNormal, lightSample.direction ) ).toVar();
|
|
946
|
-
const lightImportance = lightSample.emission.x.add( lightSample.emission.y ).add( lightSample.emission.z );
|
|
947
802
|
|
|
948
|
-
If( NoL.greaterThan( 0.0 ).and(
|
|
803
|
+
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: hitNormal } ) ), () => {
|
|
949
804
|
|
|
950
805
|
const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) );
|
|
951
806
|
const visibility = shadow( rayOrigin, lightSample.direction, shadowDistance );
|
|
@@ -958,28 +813,19 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
958
813
|
const brdfValue = evaluateMaterialResponseFromDots( material, sharedDots );
|
|
959
814
|
const bPdf = calculateMaterialPDFFromDots( material, sharedDots ).toVar();
|
|
960
815
|
|
|
816
|
+
// Power heuristic only for area lights — they are the only type the BRDF
|
|
817
|
+
// path can intersect, so elsewhere MIS would just delete energy.
|
|
961
818
|
const misW = float( 1.0 ).toVar();
|
|
962
819
|
|
|
963
|
-
If( bPdf.greaterThan( 0.0 ).and(
|
|
964
|
-
|
|
965
|
-
const lightPdfWeighted = lightSample.pdf.mul( lightWeight );
|
|
966
|
-
const brdfPdfWeighted = bPdf.mul( brdfWeight );
|
|
820
|
+
If( bPdf.greaterThan( 0.0 ).and( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ) ), () => {
|
|
967
821
|
|
|
968
|
-
|
|
969
|
-
// intersect area lights, so both strategies contribute and MIS is valid.
|
|
970
|
-
// Point/spot/directional lights are delta or non-intersectable by the
|
|
971
|
-
// BRDF path, so MIS would only reduce energy without compensation.
|
|
972
|
-
If( lightSample.lightType.equal( int( LIGHT_TYPE_AREA ) ), () => {
|
|
973
|
-
|
|
974
|
-
misW.assign( powerHeuristic( { pdf1: lightPdfWeighted, pdf2: brdfPdfWeighted } ) );
|
|
975
|
-
|
|
976
|
-
} );
|
|
822
|
+
misW.assign( powerHeuristic( { pdf1: lightSample.pdf, pdf2: bPdf } ) );
|
|
977
823
|
|
|
978
824
|
} );
|
|
979
825
|
|
|
980
826
|
// Base contribution WITHOUT visibility; shadowed = base × visibility (identical to the
|
|
981
827
|
// pre-dual-sum math), unoccluded = base × 1 (shadow-catcher reference only).
|
|
982
|
-
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) )
|
|
828
|
+
const baseContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
|
|
983
829
|
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
984
830
|
If( wantUnoccluded, () => {
|
|
985
831
|
|
|
@@ -999,129 +845,94 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
999
845
|
// BRDF SAMPLING PATH
|
|
1000
846
|
// =====================================================================
|
|
1001
847
|
|
|
1002
|
-
If(
|
|
1003
|
-
|
|
1004
|
-
If( brdfSamplePdf.greaterThan( 0.0 ).and( useBRDFSampling ), () => {
|
|
1005
|
-
|
|
1006
|
-
const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
|
|
1007
|
-
|
|
1008
|
-
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: hitNormal } ) ), () => {
|
|
848
|
+
If( brdfSamplePdf.greaterThan( 0.0 ).and( numAreaLights.greaterThan( int( 0 ) ) ), () => {
|
|
1009
849
|
|
|
1010
|
-
|
|
1011
|
-
If( numAreaLights.greaterThan( int( 0 ) ), () => {
|
|
850
|
+
const NoL = max( float( 0.0 ), dot( hitNormal, brdfSampleDirection ) ).toVar();
|
|
1012
851
|
|
|
1013
|
-
|
|
1014
|
-
const maxImportance = float( 0.0 ).toVar();
|
|
1015
|
-
const maxImportanceLight = int( - 1 ).toVar();
|
|
852
|
+
If( NoL.greaterThan( 0.0 ).and( isDirectionValid( { direction: brdfSampleDirection, surfaceNormal: hitNormal } ) ), () => {
|
|
1016
853
|
|
|
1017
|
-
|
|
1018
|
-
|
|
854
|
+
// Nearest hit — the ray stops at the first emitter, so a brighter one behind it is
|
|
855
|
+
// the wrong emitter and the wrong pdf. No importance gate: skipping a light drops
|
|
856
|
+
// energy the NEE partner's MIS weight already counted on, and estimateLightImportance
|
|
857
|
+
// reads zero across the terminator band of a large light (its CENTRE is below the
|
|
858
|
+
// horizon while half of it is still visible).
|
|
859
|
+
const nearestT = float( 1e30 ).toVar();
|
|
860
|
+
const nearestLight = int( - 1 ).toVar();
|
|
1019
861
|
|
|
1020
|
-
|
|
862
|
+
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
1021
863
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const lightImp = estimateLightImportance( light, hitPoint, hitNormal, material ).toVar();
|
|
1025
|
-
|
|
1026
|
-
If( lightImp.greaterThanEqual( importanceThreshold ), () => {
|
|
1027
|
-
|
|
1028
|
-
const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
|
|
1029
|
-
|
|
1030
|
-
If( hitDistance.greaterThan( 0.0 ), () => {
|
|
1031
|
-
|
|
1032
|
-
If( lightImp.greaterThan( maxImportance ), () => {
|
|
864
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
1033
865
|
|
|
1034
|
-
|
|
1035
|
-
maxImportanceLight.assign( i );
|
|
866
|
+
If( light.intensity.greaterThan( 0.0 ), () => {
|
|
1036
867
|
|
|
1037
|
-
|
|
1038
|
-
foundIntersection.assign( tslBool( true ) );
|
|
868
|
+
const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
|
|
1039
869
|
|
|
1040
|
-
|
|
870
|
+
If( hitDistance.greaterThan( 0.0 ).and( hitDistance.lessThan( nearestT ) ), () => {
|
|
1041
871
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
} );
|
|
872
|
+
nearestT.assign( hitDistance );
|
|
873
|
+
nearestLight.assign( i );
|
|
1045
874
|
|
|
1046
875
|
} );
|
|
1047
876
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, maxImportanceLight ) );
|
|
1051
|
-
const hitDistance = intersectAreaLight( light, rayOrigin, brdfSampleDirection ).toVar();
|
|
1052
|
-
|
|
1053
|
-
If( hitDistance.greaterThan( 0.0 ), () => {
|
|
1054
|
-
|
|
1055
|
-
const shadowDistance = min( hitDistance.sub( 0.001 ), float( 1000.0 ) );
|
|
1056
|
-
const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
|
|
1057
|
-
|
|
1058
|
-
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
1059
|
-
|
|
1060
|
-
const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
|
|
1061
|
-
|
|
1062
|
-
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
1063
|
-
|
|
1064
|
-
const lightDistSq = hitDistance.mul( hitDistance );
|
|
1065
|
-
|
|
1066
|
-
// Directional pdf must match the NEE sampler: 1/S (spherical-rect)
|
|
1067
|
-
// for rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
1068
|
-
const corner = light.position.sub( light.u ).sub( light.v );
|
|
1069
|
-
const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
1070
|
-
const areaPdf = lightDistSq.div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
1071
|
-
const dirPdf = select(
|
|
1072
|
-
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
1073
|
-
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
1074
|
-
areaPdf,
|
|
1075
|
-
).toVar();
|
|
1076
|
-
|
|
1077
|
-
// Selection pdf consistent with the importance-weighted NEE reservoir
|
|
1078
|
-
// (Cycles light_tree_pdf re-walk). Falls back to uniform 1/N only when
|
|
1079
|
-
// total importance is zero, matching the NEE fallback.
|
|
1080
|
-
const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
|
|
1081
|
-
const totalImp = computeTotalLightImportance(
|
|
1082
|
-
rayOrigin, hitNormal, material, bounceIndex,
|
|
1083
|
-
directionalLightsBuffer, numDirectionalLights,
|
|
1084
|
-
areaLightsBuffer, numAreaLights,
|
|
1085
|
-
pointLightsBuffer, numPointLights,
|
|
1086
|
-
spotLightsBuffer, numSpotLights,
|
|
1087
|
-
).toVar();
|
|
1088
|
-
const selPdf = select(
|
|
1089
|
-
totalImp.greaterThan( 0.0 ),
|
|
1090
|
-
selImp.div( max( totalImp, 1e-10 ) ),
|
|
1091
|
-
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
1092
|
-
);
|
|
1093
|
-
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
1094
|
-
|
|
1095
|
-
const brdfPdfWeighted = brdfSamplePdf.mul( brdfWeight );
|
|
1096
|
-
const lightPdfWeighted = lightPdf.mul( lightWeight );
|
|
1097
|
-
const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
|
|
877
|
+
} );
|
|
1098
878
|
|
|
1099
|
-
|
|
1100
|
-
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
1101
|
-
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
|
|
1102
|
-
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
1103
|
-
If( wantUnoccluded, () => {
|
|
879
|
+
} );
|
|
1104
880
|
|
|
1105
|
-
|
|
881
|
+
If( nearestLight.greaterThanEqual( int( 0 ) ), () => {
|
|
1106
882
|
|
|
1107
|
-
|
|
883
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, nearestLight ) );
|
|
884
|
+
const shadowDistance = min( nearestT.sub( 0.001 ), float( 1000.0 ) );
|
|
885
|
+
const visibility = shadow( rayOrigin, brdfSampleDirection, shadowDistance );
|
|
1108
886
|
|
|
1109
|
-
|
|
887
|
+
If( visibility.greaterThan( 0.0 ).or( wantUnoccluded ), () => {
|
|
1110
888
|
|
|
1111
|
-
|
|
889
|
+
const lightFacing = max( float( 0.0 ), dot( brdfSampleDirection, light.normal ).negate() ).toVar();
|
|
890
|
+
|
|
891
|
+
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
892
|
+
|
|
893
|
+
// Directional pdf must match the NEE sampler: 1/S (spherical-rect) for
|
|
894
|
+
// rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
895
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
896
|
+
const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
897
|
+
const areaPdf = nearestT.mul( nearestT ).div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
898
|
+
const dirPdf = select(
|
|
899
|
+
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
900
|
+
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
901
|
+
areaPdf,
|
|
902
|
+
).toVar();
|
|
903
|
+
|
|
904
|
+
// The NEE partner's selection pdf, rebuilt from the reservoir denominator it
|
|
905
|
+
// already accumulated. Uniform 1/N only when total importance is zero, matching
|
|
906
|
+
// the reservoir's own fallback; 0 for a light NEE can never pick, which hands
|
|
907
|
+
// this strategy the full MIS weight — as it must.
|
|
908
|
+
const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
|
|
909
|
+
const selPdf = select(
|
|
910
|
+
neeTotalWeight.greaterThan( 0.0 ),
|
|
911
|
+
selImp.div( max( neeTotalWeight, 1e-10 ) ),
|
|
912
|
+
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
913
|
+
);
|
|
914
|
+
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
915
|
+
const misW = powerHeuristic( { pdf1: brdfSamplePdf, pdf2: lightPdf } ).toVar();
|
|
916
|
+
|
|
917
|
+
const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
|
|
918
|
+
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
919
|
+
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) );
|
|
920
|
+
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
|
921
|
+
If( wantUnoccluded, () => {
|
|
922
|
+
|
|
923
|
+
unoccludedContribution.addAssign( baseContribution );
|
|
1112
924
|
|
|
1113
925
|
} );
|
|
1114
926
|
|
|
1115
927
|
} );
|
|
1116
928
|
|
|
1117
|
-
} );
|
|
929
|
+
} );
|
|
1118
930
|
|
|
1119
931
|
} );
|
|
1120
932
|
|
|
1121
933
|
} );
|
|
1122
934
|
|
|
1123
935
|
} );
|
|
1124
|
-
|
|
1125
936
|
// =====================================================================
|
|
1126
937
|
// DETERMINISTIC ENVIRONMENT NEE
|
|
1127
938
|
// Always runs (not stochastic) — forms a two-strategy Veach MIS
|
|
@@ -243,7 +243,7 @@ export const computeNDCDepth = /*@__PURE__*/ wgslFn( `
|
|
|
243
243
|
export const handleRussianRoulette = Fn( ( [
|
|
244
244
|
depth, throughput, mc, rayDirection, rngState,
|
|
245
245
|
pixelCoord, resolution, frame, dimBase,
|
|
246
|
-
enableEnvironmentLight,
|
|
246
|
+
enableEnvironmentLight,
|
|
247
247
|
] ) => {
|
|
248
248
|
|
|
249
249
|
const result = float( 1.0 ).toVar();
|
|
@@ -317,7 +317,7 @@ export const handleRussianRoulette = Fn( ( [
|
|
|
317
317
|
estMaterialImportance.assign( clamp( estMaterialImportance, 0.0, 1.0 ) );
|
|
318
318
|
|
|
319
319
|
const directionImportance = float( 0.5 ).toVar();
|
|
320
|
-
If( enableEnvironmentLight.and(
|
|
320
|
+
If( enableEnvironmentLight.and( throughputStrength.greaterThan( 0.01 ) ), () => {
|
|
321
321
|
|
|
322
322
|
const cosTheta = clamp( rayDirection.y, 0.0, 1.0 );
|
|
323
323
|
directionImportance.assign( mix( float( 0.3 ), float( 0.8 ), cosTheta.mul( cosTheta ) ) );
|