rayzee 7.2.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,12 +34,13 @@ export function buildFinalWriteKernel( params ) {
34
34
  transparentBackground,
35
35
  prevAccumTexture, prevNormalDepthTexture, prevAlbedoTexture,
36
36
  renderWidth, renderHeight,
37
- // Multi-sample: average S sample-slots per pixel (slot = pixel + k*w*h, w*h from the resolution uniform).
38
- samplesPerPass = 1,
39
37
  visMode,
38
+ // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
39
+ // on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
40
+ auxGBufferEnabled,
40
41
  } = params;
41
42
 
42
- const S = samplesPerPass | 0;
43
+ const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
43
44
 
44
45
  const computeFn = Fn( () => {
45
46
 
@@ -51,31 +52,21 @@ export function buildFinalWriteKernel( params ) {
51
52
  const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
52
53
  const rayID = uint( pixelIndex );
53
54
 
54
- // Average the S sub-samples; MRT (normal/depth/albedo) from sub-sample 0.
55
- const sampleColor = ( () => {
56
-
57
- if ( S <= 1 ) return readRayRadiance( rayBufferRO, rayID );
58
- const acc = readRayRadiance( rayBufferRO, rayID ).toVar();
59
- const mrps = uint( resolution.x ).mul( uint( resolution.y ) ).toVar(); // w*h from the resolution uniform, not baked
60
- for ( let k = 1; k < S; k ++ ) {
61
-
62
- acc.addAssign( readRayRadiance( rayBufferRO, rayID.add( uint( k ).mul( mrps ) ) ) );
63
-
64
- }
55
+ const sampleColor = readRayRadiance( rayBufferRO, rayID );
56
+ const finalColor = sampleColor.xyz.toVar();
57
+ const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
65
58
 
66
- acc.assign( acc.div( float( S ) ) );
67
- return acc;
59
+ // MRT comes from the per-pixel G-buffer (rayID == pixelIndex). Half-packed: decode.
60
+ // auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
61
+ const finalNormalDepth = vec4( 0.0 ).toVar();
62
+ const finalAlbedo = vec4( 0.0 ).xyz.toVar();
63
+ If( auxOn, () => {
68
64
 
69
- } )();
70
- // MRT comes from the per-pixel G-buffer (rayID == pixelIndex here, i.e. sub-sample 0). Half-packed: decode.
71
- const gbuf = readGBuffer( gBufferRO, rayID );
72
- const normalDepth = gbDecodeNormalDepth( gbuf );
73
- const albedoID = vec4( gbDecodeAlbedo( gbuf ), 0.0 );
65
+ const gbuf = readGBuffer( gBufferRO, rayID );
66
+ finalNormalDepth.assign( gbDecodeNormalDepth( gbuf ) );
67
+ finalAlbedo.assign( vec4( gbDecodeAlbedo( gbuf ), 0.0 ).xyz );
74
68
 
75
- const finalColor = sampleColor.xyz.toVar();
76
- const finalNormalDepth = normalDepth.toVar();
77
- const finalAlbedo = albedoID.xyz.toVar();
78
- const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
69
+ } );
79
70
 
80
71
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
81
72
  const prevUV = pixelCoord.div( resolution );
@@ -87,8 +78,12 @@ export function buildFinalWriteKernel( params ) {
87
78
  const prevAccumSample = texture( prevAccumTexture, prevUV, 0 ).toVar();
88
79
 
89
80
  finalColor.assign( mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) );
90
- finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
91
- finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
81
+ If( auxOn, () => {
82
+
83
+ finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
84
+ finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
85
+
86
+ } );
92
87
 
93
88
  If( transparentBackground, () => {
94
89
 
@@ -107,8 +102,12 @@ export function buildFinalWriteKernel( params ) {
107
102
 
108
103
  const uintCoord = uvec2( uint( gx ), uint( gy ) );
109
104
  textureStore( writeColorTex, uintCoord, vec4( finalColor, outputAlpha ) ).toWriteOnly();
110
- textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
111
- textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
105
+ If( auxOn, () => {
106
+
107
+ textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
108
+ textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
109
+
110
+ } );
112
111
 
113
112
  } );
114
113
 
@@ -33,39 +33,32 @@ export function buildGenerateKernel( params ) {
33
33
  cameraWorldMatrix, cameraProjectionMatrixInverse,
34
34
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
35
35
  renderWidth, renderHeight,
36
- // Multi-sample: S primary rays/pixel/frame; S>1 dispatch covers h*S rows, ray lands in slot subSample*(w*h) + pixelIndex.
37
- samplesPerPass = 1,
38
36
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
39
37
  transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
38
+ auxGBufferEnabled, // live uniform: 1 = init the per-pixel G-buffer (denoiser on), 0 = skip it
40
39
  } = params;
41
40
 
42
- const S = samplesPerPass | 0;
41
+ const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
43
42
 
44
43
  const computeFn = Fn( () => {
45
44
 
46
45
  const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
47
- const gyRaw = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
46
+ const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
48
47
 
49
- const subSample = S > 1 ? gyRaw.div( renderHeight ).toVar() : int( 0 );
50
- const gy = S > 1 ? gyRaw.sub( subSample.mul( renderHeight ) ).toVar() : gyRaw;
51
- const yBound = S > 1 ? renderHeight.mul( int( S ) ) : renderHeight;
52
-
53
- If( gx.lessThan( renderWidth ).and( gyRaw.lessThan( yBound ) ), () => {
48
+ If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
54
49
 
55
50
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
56
51
  const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
57
- // maxRaysPerSample = w*h, derived from the resolution uniform (NOT baked) so resize never changes the WGSL.
58
- const rayID = S > 1
59
- ? uint( pixelIndex ).add( uint( subSample ).mul( uint( resolution.x ).mul( uint( resolution.y ) ) ) )
60
- : uint( pixelIndex );
52
+ // One ray per pixel: rayID is the pixel index.
53
+ const rayID = uint( pixelIndex );
61
54
 
62
55
  const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
63
56
  screenPosition.y.assign( screenPosition.y.negate() );
64
57
 
65
- const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: subSample, frame } ).toVar();
58
+ const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
66
59
  const seed = pcgHash( { state: baseSeed } ).toVar();
67
60
 
68
- const stratifiedJitter = getStratifiedSample( pixelCoord, subSample, int( S ), seed, resolution, frame ).toVar();
61
+ const stratifiedJitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame ).toVar();
69
62
 
70
63
  const jitterScale = vec2( 2.0 ).div( resolution );
71
64
  const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
@@ -78,6 +71,8 @@ export function buildGenerateKernel( params ) {
78
71
  ) );
79
72
 
80
73
  writeRayOriginMeta( rayBufferRW, rayID, ray.origin, int( 0 ), int( 0 ) );
74
+ // A fresh camera ray is NOT redirected — REDIRECTED stays clear so it sees the direct backdrop;
75
+ // ShadeKernel sets REDIRECTED on the first direction-changing interaction (see RAY_FLAG.REDIRECTED).
81
76
  writeRayDirFlags( rayBufferRW, rayID, ray.direction, uint( RAY_FLAG.ACTIVE ) );
82
77
  // pdf inits to 0 = prevBouncePdf (megakernel parity PathTracerCore.js:556). The bounce>0 env/emissive
83
78
  // MIS gate skips until an opaque scatter writes a real combinedPdf; free bounces preserve it.
@@ -87,7 +82,7 @@ export function buildGenerateKernel( params ) {
87
82
  // keeps alpha 1 → solid. Non-transparent mode is inert (FinalWrite forces alpha 1).
88
83
  writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), select( transparentBackground, float( 1.0 ), float( 0.0 ) ) ) );
89
84
 
90
- If( subSample.equal( int( 0 ) ), () => {
85
+ If( auxOn, () => {
91
86
 
92
87
  // default: normal +Z, depth 1 (far), black albedo (background/miss)
93
88
  writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
@@ -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
- // Guard division
850
- const lightContribution = lightSample.emission.mul( brdfValue ).mul( NoL ).mul( visibility ).mul( misW ).div( max( lightSample.pdf, 1e-10 ) );
851
- totalContribution.addAssign( lightContribution.mul( totalSamplingWeight ).div( max( lightWeight, 1e-10 ) ) );
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
- // Guard division
938
- const brdfContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( visibility ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) );
939
- totalContribution.addAssign( brdfContribution.mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) ) );
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
- // Guard division no stochastic scaling needed (deterministic estimator)
1004
- const envContribution = envColor.mul( brdfValue ).mul( NoL ).mul( visibility ).mul( misW ).div( max( envPdf, 1e-10 ) );
1005
- totalContribution.addAssign( envContribution );
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
  } );