rayzee 7.4.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/rayzee.es.js +1028 -746
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +112 -21
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/Passes/OIDNDenoiser.js +27 -3
- package/src/Processor/AssetLoader.js +7 -1
- package/src/Processor/LightSerializer.js +56 -12
- package/src/Processor/PackedRayBuffer.js +9 -29
- package/src/Processor/StorageTexturePool.js +11 -3
- package/src/Processor/VRAMTracker.js +50 -8
- package/src/Processor/blackbody.js +74 -0
- package/src/Stages/ASVGF.js +27 -0
- package/src/Stages/BilateralFilter.js +12 -0
- package/src/Stages/EdgeFilter.js +12 -0
- package/src/Stages/MotionVector.js +10 -0
- package/src/Stages/NormalDepth.js +10 -0
- package/src/Stages/PathTracer.js +4 -2
- package/src/Stages/PathTracerStage.js +2 -2
- package/src/Stages/SSRC.js +16 -0
- package/src/Stages/Variance.js +13 -0
- package/src/TSL/GenerateKernel.js +1 -3
- package/src/TSL/LightsCore.js +139 -12
- package/src/TSL/LightsSampling.js +210 -17
- package/src/TSL/ShadeKernel.js +2 -9
- package/src/managers/DenoisingManager.js +10 -0
- package/src/managers/LightManager.js +26 -4
- package/src/managers/UniformManager.js +1 -1
package/src/Stages/Variance.js
CHANGED
|
@@ -363,6 +363,19 @@ export class Variance extends RenderStage {
|
|
|
363
363
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
// Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
|
|
367
|
+
// after re-enable, and reset() re-anchors the EMA. See ASVGF.releaseGPUMemory.
|
|
368
|
+
releaseGPUMemory() {
|
|
369
|
+
|
|
370
|
+
this._storageTexA?.dispose();
|
|
371
|
+
this._storageTexB?.dispose();
|
|
372
|
+
// Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
|
|
373
|
+
this.context?.removeTexture( 'variance:output' );
|
|
374
|
+
this._outputTarget?.texture?.dispose();
|
|
375
|
+
this.reset();
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
366
379
|
reset() {
|
|
367
380
|
|
|
368
381
|
this.currentMoments = 0;
|
|
@@ -19,7 +19,7 @@ import { Ray } from './Struct.js';
|
|
|
19
19
|
import { RAY_FLAG } from '../Processor/QueueManager.js';
|
|
20
20
|
import {
|
|
21
21
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
|
|
22
|
-
writeRayRadiance, writeGBuffer,
|
|
22
|
+
writeRayRadiance, writeGBuffer,
|
|
23
23
|
writeMediumStack,
|
|
24
24
|
} from '../Processor/PackedRayBuffer.js';
|
|
25
25
|
|
|
@@ -86,8 +86,6 @@ export function buildGenerateKernel( params ) {
|
|
|
86
86
|
|
|
87
87
|
// default: normal +Z, depth 1 (far), black albedo (background/miss)
|
|
88
88
|
writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
|
|
89
|
-
// surface-ID lane defaults to invalid (valid=0); Shade overwrites it at the bounce-0 hit.
|
|
90
|
-
writeGBufferSurfaceID( gBufferRW, uint( pixelIndex ), uint( 0 ), uint( 0 ), float( 0.0 ), float( 0.0 ), uint( 0 ) );
|
|
91
89
|
|
|
92
90
|
} );
|
|
93
91
|
|
package/src/TSL/LightsCore.js
CHANGED
|
@@ -42,12 +42,15 @@ export const DirectionalLight = struct( {
|
|
|
42
42
|
|
|
43
43
|
export const AreaLight = struct( {
|
|
44
44
|
position: 'vec3',
|
|
45
|
-
u: 'vec3', // First axis of the rectangular light
|
|
46
|
-
v: 'vec3', // Second axis of the rectangular light
|
|
45
|
+
u: 'vec3', // First axis half-vector of the rectangular light
|
|
46
|
+
v: 'vec3', // Second axis half-vector of the rectangular light
|
|
47
47
|
color: 'vec3',
|
|
48
|
-
intensity: 'float',
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
intensity: 'float', // radiant power (Watts), Blender-style
|
|
49
|
+
normalize: 'float', // 1 = power normalized over area (radiance ∝ 1/area), 0 = constant radiance
|
|
50
|
+
spread: 'float', // emission cone half-fan in radians, π = full Lambertian hemisphere
|
|
51
|
+
shape: 'float', // 0 = rectangle, 1 = disk/ellipse
|
|
52
|
+
normal: 'vec3', // derived
|
|
53
|
+
area: 'float', // derived (true world area: rect = w·h, ellipse = π/4·w·h)
|
|
51
54
|
} );
|
|
52
55
|
|
|
53
56
|
export const PointLight = struct( {
|
|
@@ -125,7 +128,7 @@ export const getDirectionalLight = Fn( ( [ directionalLightsBuffer, index ] ) =>
|
|
|
125
128
|
|
|
126
129
|
export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
127
130
|
|
|
128
|
-
const baseIndex = index.mul(
|
|
131
|
+
const baseIndex = index.mul( 16 );
|
|
129
132
|
const u = vec3(
|
|
130
133
|
areaLightsBuffer.element( baseIndex.add( 3 ) ),
|
|
131
134
|
areaLightsBuffer.element( baseIndex.add( 4 ) ),
|
|
@@ -138,6 +141,10 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
|
138
141
|
).toVar();
|
|
139
142
|
|
|
140
143
|
const crossUV = cross( u, v );
|
|
144
|
+
const shape = areaLightsBuffer.element( baseIndex.add( 15 ) );
|
|
145
|
+
|
|
146
|
+
// u,v are half-vectors → full rectangle area = 4·|u×v|; disk/ellipse = π/4 of that.
|
|
147
|
+
const rectArea = length( crossUV ).mul( 4.0 );
|
|
141
148
|
|
|
142
149
|
return AreaLight( {
|
|
143
150
|
position: vec3(
|
|
@@ -153,8 +160,11 @@ export const getAreaLight = Fn( ( [ areaLightsBuffer, index ] ) => {
|
|
|
153
160
|
areaLightsBuffer.element( baseIndex.add( 11 ) ),
|
|
154
161
|
),
|
|
155
162
|
intensity: areaLightsBuffer.element( baseIndex.add( 12 ) ),
|
|
163
|
+
normalize: areaLightsBuffer.element( baseIndex.add( 13 ) ),
|
|
164
|
+
spread: areaLightsBuffer.element( baseIndex.add( 14 ) ),
|
|
165
|
+
shape: shape,
|
|
156
166
|
normal: normalize( crossUV ),
|
|
157
|
-
area:
|
|
167
|
+
area: select( shape.greaterThan( 0.5 ), rectArea.mul( 0.7853981633974483 ), rectArea ),
|
|
158
168
|
} );
|
|
159
169
|
|
|
160
170
|
} );
|
|
@@ -501,12 +511,18 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
|
|
|
501
511
|
const u_dir = light.u.div( uLen );
|
|
502
512
|
const v_dir = light.v.div( vLen );
|
|
503
513
|
|
|
504
|
-
// Project onto axes
|
|
505
|
-
const u_proj = dot( localPoint, u_dir );
|
|
506
|
-
const v_proj = dot( localPoint, v_dir );
|
|
514
|
+
// Project onto axes, normalized to [-1,1] across the half-extents
|
|
515
|
+
const u_proj = dot( localPoint, u_dir ).div( uLen );
|
|
516
|
+
const v_proj = dot( localPoint, v_dir ).div( vLen );
|
|
507
517
|
|
|
508
|
-
//
|
|
509
|
-
|
|
518
|
+
// Rectangle: |u|≤1 ∧ |v|≤1. Disk/ellipse: u²+v²≤1.
|
|
519
|
+
const inside = select(
|
|
520
|
+
light.shape.greaterThan( 0.5 ),
|
|
521
|
+
u_proj.mul( u_proj ).add( v_proj.mul( v_proj ) ).lessThanEqual( 1.0 ),
|
|
522
|
+
abs( u_proj ).lessThanEqual( 1.0 ).and( abs( v_proj ).lessThanEqual( 1.0 ) ),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
If( inside, () => {
|
|
510
526
|
|
|
511
527
|
result.assign( t );
|
|
512
528
|
|
|
@@ -520,3 +536,114 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
|
|
|
520
536
|
|
|
521
537
|
} );
|
|
522
538
|
|
|
539
|
+
// ================================================================================
|
|
540
|
+
// SPHERICAL RECTANGLE SAMPLING (Ureña et al. 2013, "An Area-Preserving
|
|
541
|
+
// Parametrization for Spherical Rectangles"). Same scheme Cycles uses for rect
|
|
542
|
+
// area lights — samples the light's solid angle directly, giving pdf = 1/S in
|
|
543
|
+
// solid-angle measure (much lower variance than uniform-area sampling for
|
|
544
|
+
// large/near lights). Inputs:
|
|
545
|
+
// o = shading point (ray origin)
|
|
546
|
+
// s = rectangle corner = center - u - v
|
|
547
|
+
// ex = full edge vector along u (= 2·u)
|
|
548
|
+
// ey = full edge vector along v (= 2·v)
|
|
549
|
+
// ================================================================================
|
|
550
|
+
|
|
551
|
+
// Solid angle S subtended by the rectangle from o (used for the BSDF-hit MIS pdf).
|
|
552
|
+
export const sphQuadSolidAngle = /*@__PURE__*/ wgslFn( `
|
|
553
|
+
fn sphQuadSolidAngle( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f ) -> f32 {
|
|
554
|
+
let PI = 3.141592653589793f;
|
|
555
|
+
let exl = length( ex );
|
|
556
|
+
let eyl = length( ey );
|
|
557
|
+
let x = ex / max( exl, 1e-12f );
|
|
558
|
+
let y = ey / max( eyl, 1e-12f );
|
|
559
|
+
var z = cross( x, y );
|
|
560
|
+
let d = s - o;
|
|
561
|
+
var z0 = dot( d, z );
|
|
562
|
+
if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
|
|
563
|
+
let x0 = dot( d, x );
|
|
564
|
+
let y0 = dot( d, y );
|
|
565
|
+
let x1 = x0 + exl;
|
|
566
|
+
let y1 = y0 + eyl;
|
|
567
|
+
let v00 = vec3f( x0, y0, z0 );
|
|
568
|
+
let v01 = vec3f( x0, y1, z0 );
|
|
569
|
+
let v10 = vec3f( x1, y0, z0 );
|
|
570
|
+
let v11 = vec3f( x1, y1, z0 );
|
|
571
|
+
let n0 = normalize( cross( v00, v10 ) );
|
|
572
|
+
let n1 = normalize( cross( v10, v11 ) );
|
|
573
|
+
let n2 = normalize( cross( v11, v01 ) );
|
|
574
|
+
let n3 = normalize( cross( v01, v00 ) );
|
|
575
|
+
let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
|
|
576
|
+
let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
|
|
577
|
+
let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
|
|
578
|
+
let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
|
|
579
|
+
return g0 + g1 + g2 + g3 - 2.0f * PI;
|
|
580
|
+
}
|
|
581
|
+
` );
|
|
582
|
+
|
|
583
|
+
// Sample a direction toward the rectangle uniformly in solid angle.
|
|
584
|
+
// Returns vec4( pointOnRect.xyz, pdf ); pdf = 1/S. pdf <= 0 signals the caller
|
|
585
|
+
// to fall back to uniform-area sampling (degenerate / tiny solid angle).
|
|
586
|
+
export const sampleSphQuad = /*@__PURE__*/ wgslFn( `
|
|
587
|
+
fn sampleSphQuad( o: vec3f, s: vec3f, ex: vec3f, ey: vec3f, uv: vec2f ) -> vec4f {
|
|
588
|
+
let PI = 3.141592653589793f;
|
|
589
|
+
let exl = length( ex );
|
|
590
|
+
let eyl = length( ey );
|
|
591
|
+
let x = ex / max( exl, 1e-12f );
|
|
592
|
+
let y = ey / max( eyl, 1e-12f );
|
|
593
|
+
var z = cross( x, y );
|
|
594
|
+
let d = s - o;
|
|
595
|
+
var z0 = dot( d, z );
|
|
596
|
+
if ( z0 > 0.0f ) { z = -z; z0 = -z0; }
|
|
597
|
+
let z0sq = z0 * z0;
|
|
598
|
+
let x0 = dot( d, x );
|
|
599
|
+
let y0 = dot( d, y );
|
|
600
|
+
let x1 = x0 + exl;
|
|
601
|
+
let y1 = y0 + eyl;
|
|
602
|
+
let y0sq = y0 * y0;
|
|
603
|
+
let y1sq = y1 * y1;
|
|
604
|
+
let v00 = vec3f( x0, y0, z0 );
|
|
605
|
+
let v01 = vec3f( x0, y1, z0 );
|
|
606
|
+
let v10 = vec3f( x1, y0, z0 );
|
|
607
|
+
let v11 = vec3f( x1, y1, z0 );
|
|
608
|
+
let n0 = normalize( cross( v00, v10 ) );
|
|
609
|
+
let n1 = normalize( cross( v10, v11 ) );
|
|
610
|
+
let n2 = normalize( cross( v11, v01 ) );
|
|
611
|
+
let n3 = normalize( cross( v01, v00 ) );
|
|
612
|
+
let g0 = acos( clamp( -dot( n0, n1 ), -1.0f, 1.0f ) );
|
|
613
|
+
let g1 = acos( clamp( -dot( n1, n2 ), -1.0f, 1.0f ) );
|
|
614
|
+
let g2 = acos( clamp( -dot( n2, n3 ), -1.0f, 1.0f ) );
|
|
615
|
+
let g3 = acos( clamp( -dot( n3, n0 ), -1.0f, 1.0f ) );
|
|
616
|
+
let b0 = n0.z;
|
|
617
|
+
let b1 = n2.z;
|
|
618
|
+
let b0sq = b0 * b0;
|
|
619
|
+
let k = 2.0f * PI - g2 - g3;
|
|
620
|
+
let S = g0 + g1 + g2 + g3 - 2.0f * PI;
|
|
621
|
+
|
|
622
|
+
if ( S <= 1e-5f ) {
|
|
623
|
+
return vec4f( 0.0f, 0.0f, 0.0f, -1.0f );
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let au = uv.x * S + k;
|
|
627
|
+
let sinAu = sin( au );
|
|
628
|
+
let fu = ( cos( au ) * b0 - b1 ) / max( abs( sinAu ), 1e-7f ) * sign( sinAu );
|
|
629
|
+
var cu = select( -1.0f, 1.0f, fu >= 0.0f ) / sqrt( fu * fu + b0sq );
|
|
630
|
+
cu = clamp( cu, -1.0f, 1.0f );
|
|
631
|
+
|
|
632
|
+
var xu = -( cu * z0 ) / max( sqrt( 1.0f - cu * cu ), 1e-7f );
|
|
633
|
+
xu = clamp( xu, x0, x1 );
|
|
634
|
+
|
|
635
|
+
let dd = sqrt( xu * xu + z0sq );
|
|
636
|
+
let h0 = y0 / sqrt( dd * dd + y0sq );
|
|
637
|
+
let h1 = y1 / sqrt( dd * dd + y1sq );
|
|
638
|
+
let hv = h0 + uv.y * ( h1 - h0 );
|
|
639
|
+
let hv2 = hv * hv;
|
|
640
|
+
var yv = y1;
|
|
641
|
+
if ( hv2 < 1.0f - 1e-6f ) {
|
|
642
|
+
yv = ( hv * dd ) / sqrt( 1.0f - hv2 );
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let p = o + xu * x + yv * y + z0 * z;
|
|
646
|
+
return vec4f( p, 1.0f / S );
|
|
647
|
+
}
|
|
648
|
+
` );
|
|
649
|
+
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
sqrt,
|
|
30
30
|
cos,
|
|
31
31
|
sin,
|
|
32
|
+
tan,
|
|
32
33
|
dot,
|
|
33
34
|
cross,
|
|
34
35
|
normalize,
|
|
@@ -56,6 +57,8 @@ import {
|
|
|
56
57
|
sampleSpotGoboMask,
|
|
57
58
|
sampleDirectionalGoboMask,
|
|
58
59
|
sampleIESProfile,
|
|
60
|
+
sampleSphQuad,
|
|
61
|
+
sphQuadSolidAngle,
|
|
59
62
|
} from './LightsCore.js';
|
|
60
63
|
|
|
61
64
|
import { MISStrategy, DotProducts, DirectLightingDual } from './Struct.js';
|
|
@@ -90,7 +93,49 @@ const TWO_PI = 2.0 * PI;
|
|
|
90
93
|
// Light Sampling Functions
|
|
91
94
|
// =============================================================================
|
|
92
95
|
|
|
93
|
-
//
|
|
96
|
+
// Blender-style area-light spread: linearly attenuates emission as the receiver
|
|
97
|
+
// moves off the light's axis (gridded-softbox look). cosAngle = dot(-dir, normal),
|
|
98
|
+
// the emission cosine. spread = π (default) → full Lambertian hemisphere, no
|
|
99
|
+
// attenuation. Mirrors Cycles' area_light_spread_attenuation.
|
|
100
|
+
export const areaLightSpreadAttenuation = Fn( ( [ cosAngle, spread ] ) => {
|
|
101
|
+
|
|
102
|
+
const att = float( 1.0 ).toVar();
|
|
103
|
+
|
|
104
|
+
// Only attenuate for narrowed cones; at the π default tan(π/2)=∞ → skip.
|
|
105
|
+
If( spread.lessThan( float( PI ).sub( 1e-3 ) ).and( cosAngle.greaterThan( 0.0 ) ), () => {
|
|
106
|
+
|
|
107
|
+
const halfSpread = spread.mul( 0.5 ).toVar();
|
|
108
|
+
const tanHalf = tan( halfSpread ).toVar();
|
|
109
|
+
const sinA = sqrt( max( float( 1.0 ).sub( cosAngle.mul( cosAngle ) ), 0.0 ) );
|
|
110
|
+
const tanA = sinA.div( max( cosAngle, 1e-4 ) );
|
|
111
|
+
// Flux renormalization so narrowing the beam doesn't dim total power.
|
|
112
|
+
const normSpread = select(
|
|
113
|
+
halfSpread.greaterThan( 0.05 ),
|
|
114
|
+
float( 1.0 ).div( max( tanHalf.sub( halfSpread ), 1e-6 ) ),
|
|
115
|
+
float( 3.0 ).div( max( halfSpread.mul( halfSpread ).mul( halfSpread ), 1e-9 ) ),
|
|
116
|
+
);
|
|
117
|
+
att.assign( max( tanHalf.sub( tanA ).mul( normSpread ), 0.0 ) );
|
|
118
|
+
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
return att;
|
|
122
|
+
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
// Emitted radiance for an area light (Blender/Cycles parity):
|
|
126
|
+
// L = color · power · (1/π) · (normalize ? 1/area : 1)
|
|
127
|
+
// The 1/π is the Lambertian emitter normalization; the 1/area (Normalize ON)
|
|
128
|
+
// keeps total radiant power constant as the light is resized.
|
|
129
|
+
export const areaLightRadiance = Fn( ( [ light ] ) => {
|
|
130
|
+
|
|
131
|
+
const invArea = select( light.normalize.greaterThan( 0.5 ), float( 1.0 ).div( max( light.area, 1e-10 ) ), float( 1.0 ) );
|
|
132
|
+
return light.color.mul( light.intensity ).mul( PI_INV ).mul( invArea );
|
|
133
|
+
|
|
134
|
+
} );
|
|
135
|
+
|
|
136
|
+
// Area light sampling — rectangle uses spherical-rectangle (Ureña 2013)
|
|
137
|
+
// solid-angle sampling (pdf = 1/S); disk/ellipse and degenerate rects fall back
|
|
138
|
+
// to uniform-area sampling with the dist²/(area·cos) area→solid-angle pdf.
|
|
94
139
|
export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelectionPdf ] ) => {
|
|
95
140
|
|
|
96
141
|
// Result variables (no early return in TSL)
|
|
@@ -104,12 +149,49 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
104
149
|
// Validate light area to prevent NaN
|
|
105
150
|
If( light.area.greaterThan( 0.0 ), () => {
|
|
106
151
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
152
|
+
const sampledPos = vec3( 0.0 ).toVar();
|
|
153
|
+
// >0 → pdf already in solid-angle measure (spherical-rect); <0 → use area conversion.
|
|
154
|
+
const solidAnglePdf = float( - 1.0 ).toVar();
|
|
155
|
+
|
|
156
|
+
If( light.shape.greaterThan( 0.5 ), () => {
|
|
157
|
+
|
|
158
|
+
// Disk / ellipse: uniform sample on the unit disk, mapped through the
|
|
159
|
+
// half-vectors. PDF handled via the area→solid-angle conversion below.
|
|
160
|
+
const r = sqrt( ruv.x ).toVar();
|
|
161
|
+
const theta = float( TWO_PI ).mul( ruv.y ).toVar();
|
|
162
|
+
sampledPos.assign(
|
|
163
|
+
light.position
|
|
164
|
+
.add( light.u.mul( r.mul( cos( theta ) ) ) )
|
|
165
|
+
.add( light.v.mul( r.mul( sin( theta ) ) ) )
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
} ).Else( () => {
|
|
169
|
+
|
|
170
|
+
// Rectangle: spherical-rectangle solid-angle sampling.
|
|
171
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
172
|
+
const ex = light.u.mul( 2.0 );
|
|
173
|
+
const ey = light.v.mul( 2.0 );
|
|
174
|
+
const q = sampleSphQuad( rayOrigin, corner, ex, ey, ruv ).toVar();
|
|
175
|
+
|
|
176
|
+
If( q.w.greaterThan( 0.0 ), () => {
|
|
177
|
+
|
|
178
|
+
sampledPos.assign( q.xyz );
|
|
179
|
+
solidAnglePdf.assign( q.w );
|
|
180
|
+
|
|
181
|
+
} ).Else( () => {
|
|
182
|
+
|
|
183
|
+
// Degenerate / tiny solid angle → uniform-area fallback.
|
|
184
|
+
sampledPos.assign(
|
|
185
|
+
light.position
|
|
186
|
+
.add( light.u.mul( ruv.x.mul( 2.0 ).sub( 1.0 ) ) )
|
|
187
|
+
.add( light.v.mul( ruv.y.mul( 2.0 ).sub( 1.0 ) ) )
|
|
188
|
+
);
|
|
111
189
|
|
|
112
|
-
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
} );
|
|
193
|
+
|
|
194
|
+
const toLight = sampledPos.sub( rayOrigin ).toVar();
|
|
113
195
|
const lightDistSq = dot( toLight, toLight ).toVar();
|
|
114
196
|
|
|
115
197
|
// Guard against zero distance
|
|
@@ -117,17 +199,17 @@ export const sampleRectAreaLight = Fn( ( [ light, rayOrigin, ruv, lightSelection
|
|
|
117
199
|
|
|
118
200
|
const dist = sqrt( lightDistSq ).toVar();
|
|
119
201
|
const direction = toLight.div( dist ).toVar();
|
|
120
|
-
const
|
|
121
|
-
const cosAngle = dot( direction.negate(), lightNormal ).toVar();
|
|
202
|
+
const cosAngle = dot( direction.negate(), light.normal ).toVar();
|
|
122
203
|
|
|
123
204
|
ls_lightType.assign( int( LIGHT_TYPE_AREA ) );
|
|
124
|
-
ls_emission.assign( light.color.mul( light.intensity ) );
|
|
125
205
|
ls_distance.assign( dist );
|
|
126
206
|
ls_direction.assign( direction );
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
207
|
+
ls_emission.assign( areaLightRadiance( light ).mul( areaLightSpreadAttenuation( cosAngle, light.spread ) ) );
|
|
208
|
+
|
|
209
|
+
// Spherical-rect: pdf = 1/S (solid angle). Else: dist²/(area·cos).
|
|
210
|
+
const areaPdf = lightDistSq.div( max( light.area.mul( max( cosAngle, 0.001 ) ), 1e-10 ) );
|
|
211
|
+
const dirPdf = select( solidAnglePdf.greaterThan( 0.0 ), solidAnglePdf, areaPdf );
|
|
212
|
+
ls_pdf.assign( dirPdf.mul( lightSelectionPdf ) );
|
|
131
213
|
ls_valid.assign( cosAngle.greaterThan( 0.0 ) );
|
|
132
214
|
|
|
133
215
|
} );
|
|
@@ -679,6 +761,91 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
|
|
|
679
761
|
|
|
680
762
|
} );
|
|
681
763
|
|
|
764
|
+
// Total light-selection weight at a shading point, replicating the NEE reservoir's
|
|
765
|
+
// importance accumulation (same per-type importance fns, same 16-light cap + order).
|
|
766
|
+
// Used to reconstruct the exact NEE selection pdf for MIS on the BSDF-hit path —
|
|
767
|
+
// the analogue of Cycles' light_tree_pdf re-walk. Without it, the BSDF-hit MIS
|
|
768
|
+
// partner assumes uniform 1/N selection, which disagrees with the importance-
|
|
769
|
+
// weighted NEE selection and biases the power-heuristic weights.
|
|
770
|
+
export const computeTotalLightImportance = Fn( ( [
|
|
771
|
+
rayOrigin, normal, material, bounceIndex,
|
|
772
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
773
|
+
areaLightsBuffer, numAreaLights,
|
|
774
|
+
pointLightsBuffer, numPointLights,
|
|
775
|
+
spotLightsBuffer, numSpotLights,
|
|
776
|
+
] ) => {
|
|
777
|
+
|
|
778
|
+
const totalWeight = float( 0.0 ).toVar();
|
|
779
|
+
const lightIndex = int( 0 ).toVar();
|
|
780
|
+
|
|
781
|
+
If( numDirectionalLights.greaterThan( int( 0 ) ), () => {
|
|
782
|
+
|
|
783
|
+
Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
784
|
+
|
|
785
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
786
|
+
|
|
787
|
+
const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
|
|
788
|
+
totalWeight.addAssign( calculateDirectionalLightImportance( light, normal, material, bounceIndex ) );
|
|
789
|
+
lightIndex.addAssign( 1 );
|
|
790
|
+
|
|
791
|
+
} );
|
|
792
|
+
|
|
793
|
+
} );
|
|
794
|
+
|
|
795
|
+
} );
|
|
796
|
+
|
|
797
|
+
If( numAreaLights.greaterThan( int( 0 ) ), () => {
|
|
798
|
+
|
|
799
|
+
Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
800
|
+
|
|
801
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
802
|
+
|
|
803
|
+
const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
|
|
804
|
+
totalWeight.addAssign( select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ) );
|
|
805
|
+
lightIndex.addAssign( 1 );
|
|
806
|
+
|
|
807
|
+
} );
|
|
808
|
+
|
|
809
|
+
} );
|
|
810
|
+
|
|
811
|
+
} );
|
|
812
|
+
|
|
813
|
+
If( numPointLights.greaterThan( int( 0 ) ), () => {
|
|
814
|
+
|
|
815
|
+
Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
816
|
+
|
|
817
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
818
|
+
|
|
819
|
+
const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
|
|
820
|
+
totalWeight.addAssign( calculatePointLightImportance( light, rayOrigin, normal, material ) );
|
|
821
|
+
lightIndex.addAssign( 1 );
|
|
822
|
+
|
|
823
|
+
} );
|
|
824
|
+
|
|
825
|
+
} );
|
|
826
|
+
|
|
827
|
+
} );
|
|
828
|
+
|
|
829
|
+
If( numSpotLights.greaterThan( int( 0 ) ), () => {
|
|
830
|
+
|
|
831
|
+
Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
|
|
832
|
+
|
|
833
|
+
If( lightIndex.lessThan( int( 16 ) ), () => {
|
|
834
|
+
|
|
835
|
+
const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
|
|
836
|
+
totalWeight.addAssign( calculateSpotLightImportance( light, rayOrigin, normal, material ) );
|
|
837
|
+
lightIndex.addAssign( 1 );
|
|
838
|
+
|
|
839
|
+
} );
|
|
840
|
+
|
|
841
|
+
} );
|
|
842
|
+
|
|
843
|
+
} );
|
|
844
|
+
|
|
845
|
+
return totalWeight;
|
|
846
|
+
|
|
847
|
+
} );
|
|
848
|
+
|
|
682
849
|
// =============================================================================
|
|
683
850
|
// Unified Direct Lighting System
|
|
684
851
|
// =============================================================================
|
|
@@ -936,15 +1103,41 @@ export const calculateDirectLightingUnified = Fn( ( [
|
|
|
936
1103
|
If( lightFacing.greaterThan( 0.0 ), () => {
|
|
937
1104
|
|
|
938
1105
|
const lightDistSq = hitDistance.mul( hitDistance );
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1106
|
+
|
|
1107
|
+
// Directional pdf must match the NEE sampler: 1/S (spherical-rect)
|
|
1108
|
+
// for rectangles, dist²/(area·cos) for disk/ellipse + degenerate rects.
|
|
1109
|
+
const corner = light.position.sub( light.u ).sub( light.v );
|
|
1110
|
+
const sAngle = sphQuadSolidAngle( rayOrigin, corner, light.u.mul( 2.0 ), light.v.mul( 2.0 ) ).toVar();
|
|
1111
|
+
const areaPdf = lightDistSq.div( max( light.area.mul( lightFacing ), EPSILON ) );
|
|
1112
|
+
const dirPdf = select(
|
|
1113
|
+
light.shape.lessThan( 0.5 ).and( sAngle.greaterThan( 1e-5 ) ),
|
|
1114
|
+
float( 1.0 ).div( max( sAngle, 1e-10 ) ),
|
|
1115
|
+
areaPdf,
|
|
1116
|
+
).toVar();
|
|
1117
|
+
|
|
1118
|
+
// Selection pdf consistent with the importance-weighted NEE reservoir
|
|
1119
|
+
// (Cycles light_tree_pdf re-walk). Falls back to uniform 1/N only when
|
|
1120
|
+
// total importance is zero, matching the NEE fallback.
|
|
1121
|
+
const selImp = estimateLightImportance( light, rayOrigin, hitNormal, material );
|
|
1122
|
+
const totalImp = computeTotalLightImportance(
|
|
1123
|
+
rayOrigin, hitNormal, material, bounceIndex,
|
|
1124
|
+
directionalLightsBuffer, numDirectionalLights,
|
|
1125
|
+
areaLightsBuffer, numAreaLights,
|
|
1126
|
+
pointLightsBuffer, numPointLights,
|
|
1127
|
+
spotLightsBuffer, numSpotLights,
|
|
1128
|
+
).toVar();
|
|
1129
|
+
const selPdf = select(
|
|
1130
|
+
totalImp.greaterThan( 0.0 ),
|
|
1131
|
+
selImp.div( max( totalImp, 1e-10 ) ),
|
|
1132
|
+
float( 1.0 ).div( max( float( totalLights ), 1.0 ) ),
|
|
1133
|
+
);
|
|
1134
|
+
const lightPdf = dirPdf.mul( selPdf ).toVar();
|
|
942
1135
|
|
|
943
1136
|
const brdfPdfWeighted = brdfSamplePdf.mul( brdfWeight );
|
|
944
1137
|
const lightPdfWeighted = lightPdf.mul( lightWeight );
|
|
945
1138
|
const misW = powerHeuristic( { pdf1: brdfPdfWeighted, pdf2: lightPdfWeighted } ).toVar();
|
|
946
1139
|
|
|
947
|
-
const lightEmission = light.
|
|
1140
|
+
const lightEmission = areaLightRadiance( light ).mul( areaLightSpreadAttenuation( lightFacing, light.spread ) );
|
|
948
1141
|
// Base contribution WITHOUT visibility (see discrete-light site).
|
|
949
1142
|
const baseContribution = lightEmission.mul( brdfSampleValue ).mul( NoL ).mul( misW ).div( max( brdfSamplePdf, 1e-10 ) ).mul( totalSamplingWeight ).div( max( brdfWeight, 1e-10 ) );
|
|
950
1143
|
totalContribution.addAssign( baseContribution.mul( visibility ) );
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -51,9 +51,9 @@ import {
|
|
|
51
51
|
readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
|
|
52
52
|
readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
|
|
53
53
|
readHitDistance, readHitBarycentrics, readHitNormal,
|
|
54
|
-
readHitMaterialIndex, readHitTriangleIndex,
|
|
54
|
+
readHitMaterialIndex, readHitTriangleIndex,
|
|
55
55
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
|
|
56
|
-
writeGBuffer,
|
|
56
|
+
writeGBuffer, readGBuffer, gbDecodeNormalDepth,
|
|
57
57
|
readRayRadiance,
|
|
58
58
|
} from '../Processor/PackedRayBuffer.js';
|
|
59
59
|
|
|
@@ -64,9 +64,6 @@ const MISS_DIST = 1e19;
|
|
|
64
64
|
const CATCHER_T_MIN = 1e-4; // min ray-t for the analytic plane hit (skip behind/at the camera)
|
|
65
65
|
const CATCHER_LUMA_FLOOR = 1e-4; // denominator floor for the shadow ratio
|
|
66
66
|
const CATCHER_COVERAGE_MIN = 1e-3; // below this incident luma there is no shadow to catch
|
|
67
|
-
// Reserved G-buffer surface ID for catcher pixels (no real triangle); stable across frames so the
|
|
68
|
-
// denoiser treats the analytic plane as one coherent surface, not a background miss.
|
|
69
|
-
const CATCHER_SURFACE_ID = 0xFFFFFFFE;
|
|
70
67
|
|
|
71
68
|
export function buildShadeKernel( params ) {
|
|
72
69
|
|
|
@@ -297,7 +294,6 @@ export function buildShadeKernel( params ) {
|
|
|
297
294
|
|
|
298
295
|
const planeDepth = computeNDCDepth( { worldPos: planePoint, cameraProjectionMatrix, cameraViewMatrix } );
|
|
299
296
|
writeGBuffer( gBufferRW, pixelIndex, planeN, planeDepth, vec3( 1.0 ) );
|
|
300
|
-
writeGBufferSurfaceID( gBufferRW, pixelIndex, uint( CATCHER_SURFACE_ID ), uint( CATCHER_SURFACE_ID ), 0.5, 0.5, uint( 1 ) );
|
|
301
297
|
|
|
302
298
|
} );
|
|
303
299
|
|
|
@@ -587,9 +583,6 @@ export function buildShadeKernel( params ) {
|
|
|
587
583
|
// block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
|
|
588
584
|
// (objectNormal/objectColor stay at their init for transmissive-then-miss).
|
|
589
585
|
writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
|
|
590
|
-
// Persist the primary-hit surface ID (Tier-1 A-SVGF correlated re-projection). Hit-only
|
|
591
|
-
// branch (misses Return above), so this marks the pixel valid; bary from the bounce-0 hit.
|
|
592
|
-
writeGBufferSurfaceID( gBufferRW, pixelIndex, hitTriIdx, readHitMeshIndex( hitBufferRO, rayID ), hitUV.x, hitUV.y, uint( 1 ) );
|
|
593
586
|
|
|
594
587
|
} );
|
|
595
588
|
|
|
@@ -370,6 +370,16 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
370
370
|
// MRT read-targets directly). When none are active the wavefront skips those writes entirely.
|
|
371
371
|
s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
|
|
372
372
|
|
|
373
|
+
// Reclaim VRAM: free the big 2048² StorageTextures of any denoiser/G-buffer stage that ended up
|
|
374
|
+
// disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
|
|
375
|
+
// toggle funnels through here after the enabled flags above are settled, so this is the one
|
|
376
|
+
// choke point. dispose() is idempotent, so re-running it for an already-released stage is a no-op.
|
|
377
|
+
for ( const stage of [ s.asvgf, s.variance, s.bilateralFilter, s.ssrc, s.edgeFilter, nd, mv ] ) {
|
|
378
|
+
|
|
379
|
+
if ( stage && ! stage.enabled ) stage.releaseGPUMemory?.();
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
// ── Render Completion Chain ───────────────────────────────────
|
|
@@ -38,10 +38,11 @@ export class LightManager extends EventDispatcher {
|
|
|
38
38
|
addLight( type ) {
|
|
39
39
|
|
|
40
40
|
const defaults = {
|
|
41
|
+
// Power in Watts (Blender-style) for point/spot/area; Sun is W/m² strength.
|
|
41
42
|
DirectionalLight: { position: [ 1, 1, 1 ], intensity: 1.0, color: '#ffffff' },
|
|
42
|
-
PointLight: { position: [ 0, 2, 0 ], intensity:
|
|
43
|
-
SpotLight: { position: [ 0, 1, 0 ], intensity:
|
|
44
|
-
RectAreaLight: { position: [ 0, 2, 0 ], intensity:
|
|
43
|
+
PointLight: { position: [ 0, 2, 0 ], intensity: 1000, color: '#ffffff' },
|
|
44
|
+
SpotLight: { position: [ 0, 1, 0 ], intensity: 1000, color: '#ffffff', angle: 15 },
|
|
45
|
+
RectAreaLight: { position: [ 0, 2, 0 ], intensity: 100, color: '#ffffff', width: 2, height: 2 }
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const props = defaults[ type ];
|
|
@@ -73,9 +74,19 @@ export class LightManager extends EventDispatcher {
|
|
|
73
74
|
light = new RectAreaLight( props.color, props.intensity, props.width, props.height );
|
|
74
75
|
light.position.fromArray( props.position );
|
|
75
76
|
light.lookAt( 0, 0, 0 );
|
|
77
|
+
// Blender-style emission defaults: power-normalized, full Lambertian
|
|
78
|
+
// hemisphere (spread = π), rectangular shape.
|
|
79
|
+
light.userData.normalize = true;
|
|
80
|
+
light.userData.spread = Math.PI;
|
|
81
|
+
light.userData.shape = 'rectangle';
|
|
76
82
|
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
// Blender-style emission controls common to every light type.
|
|
86
|
+
light.userData.temperature = 6500;
|
|
87
|
+
light.userData.useTemperature = false;
|
|
88
|
+
light.userData.exposure = 0;
|
|
89
|
+
|
|
79
90
|
const count = this.scene.getObjectsByProperty( 'isLight', true ).length;
|
|
80
91
|
light.name = `${type.replace( 'Light', '' )} ${count + 1}`;
|
|
81
92
|
this.scene.add( light );
|
|
@@ -322,13 +333,24 @@ export class LightManager extends EventDispatcher {
|
|
|
322
333
|
intensity: light.intensity,
|
|
323
334
|
color: `#${light.color.getHexString()}`,
|
|
324
335
|
position: [ light.position.x, light.position.y, light.position.z ],
|
|
325
|
-
angle
|
|
336
|
+
angle,
|
|
337
|
+
// Emission controls common to all light types.
|
|
338
|
+
temperature: light.userData?.temperature ?? 6500,
|
|
339
|
+
useTemperature: light.userData?.useTemperature ?? false,
|
|
340
|
+
exposure: light.userData?.exposure ?? 0
|
|
326
341
|
};
|
|
327
342
|
|
|
328
343
|
if ( light.type === 'RectAreaLight' ) {
|
|
329
344
|
|
|
330
345
|
descriptor.width = light.width;
|
|
331
346
|
descriptor.height = light.height;
|
|
347
|
+
descriptor.normalize = light.userData?.normalize ?? true;
|
|
348
|
+
descriptor.spread = MathUtils.radToDeg( light.userData?.spread ?? Math.PI ); // degrees for UI
|
|
349
|
+
const rawShape = light.userData?.shape;
|
|
350
|
+
descriptor.shape = ( rawShape === 'square' || rawShape === 'rectangle' || rawShape === 'disk' || rawShape === 'ellipse' )
|
|
351
|
+
? rawShape
|
|
352
|
+
: rawShape === 1 ? 'ellipse'
|
|
353
|
+
: 'rectangle'; // 'rect', undefined, 0 → rectangle
|
|
332
354
|
const dir = light.getWorldDirection( light.position.clone() );
|
|
333
355
|
descriptor.target = [ light.position.x + dir.x, light.position.y + dir.y, light.position.z + dir.z ];
|
|
334
356
|
|
|
@@ -212,7 +212,7 @@ export class UniformManager {
|
|
|
212
212
|
// Light buffer nodes - pre-allocate for up to 16 lights per type (shader hard cap)
|
|
213
213
|
this._lightBuffers = {
|
|
214
214
|
directional: uniformArray( new Float32Array( 12 * 16 ), 'float' ),
|
|
215
|
-
area: uniformArray( new Float32Array(
|
|
215
|
+
area: uniformArray( new Float32Array( 16 * 16 ), 'float' ),
|
|
216
216
|
point: uniformArray( new Float32Array( 9 * 16 ), 'float' ),
|
|
217
217
|
spot: uniformArray( new Float32Array( 20 * 16 ), 'float' ),
|
|
218
218
|
};
|