three-realtime-rt 0.4.2 → 0.6.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 +157 -14
- package/package.json +2 -1
- package/src/CompositePass.js +6 -2
- package/src/DenoisePass.js +58 -6
- package/src/GBufferPass.js +132 -46
- package/src/GIReservoirPass.js +946 -0
- package/src/RTLightingPass.js +123 -10
- package/src/RealtimeRaytracer.js +143 -6
- package/src/SceneCompiler.js +386 -51
- package/src/bvhAnyHit.glsl.js +15 -0
- package/src/index.d.ts +105 -5
package/src/RTLightingPass.js
CHANGED
|
@@ -66,6 +66,7 @@ uniform bool uReflEnabled; // traced reflections on metallic surfaces
|
|
|
66
66
|
uniform bool uRefrEnabled; // traced refraction on transmissive surfaces
|
|
67
67
|
uniform bool uBlendEnabled; // straight-through view continuation on blend surfaces
|
|
68
68
|
uniform float uIor; // index of refraction for transmissive materials
|
|
69
|
+
uniform float uDispersion; // chromatic dispersion strength for glass (0 = off)
|
|
69
70
|
uniform bool uLightStochastic; // 1 direct shadow ray/pixel/frame instead of 1/light
|
|
70
71
|
uniform bool uRestirEnabled; // shade the reservoir winner instead of sampling
|
|
71
72
|
uniform bool uGIHalfRate; // GI ray on alternating checkerboard, doubled
|
|
@@ -75,6 +76,18 @@ uniform float uEnvIntensity;
|
|
|
75
76
|
uniform float uFrame;
|
|
76
77
|
uniform float uEps;
|
|
77
78
|
uniform bool uGIEnabled;
|
|
79
|
+
// EXPERIMENTAL: when an external ReSTIR GI pass supplies the 1-bounce indirect
|
|
80
|
+
// (added downstream at the denoise stage), skip the inline GI trace so it isn't
|
|
81
|
+
// counted twice. A uniform, NOT a sampler — the lighting pass is already at the
|
|
82
|
+
// WebGL2 16-sampler minimum and cannot take another.
|
|
83
|
+
uniform bool uExternalGI;
|
|
84
|
+
|
|
85
|
+
// BVH traversal-cost heatmap (outputMode 7). When uCostView is on, main() writes
|
|
86
|
+
// the per-pixel shadow-ray node-visit count (gBvhVisits, from bvhAnyHit.glsl.js)
|
|
87
|
+
// through costPalette() into the irradiance attachment INSTEAD of the accumulated
|
|
88
|
+
// lighting — bypassing temporal blending — so the debug view reads the raw cost.
|
|
89
|
+
uniform bool uCostView;
|
|
90
|
+
uniform float uCostScale; // multiplies the visit count before the palette (default 1/96)
|
|
78
91
|
|
|
79
92
|
// Procedural sky (when enabled, replaces the flat env colour as the "miss" term
|
|
80
93
|
// for GI rays — this is what gives natural outdoor bounce light).
|
|
@@ -464,7 +477,8 @@ vec3 sampleOneAny(vec3 P, vec3 N) {
|
|
|
464
477
|
// Incoming radiance along rd: trace, shade the hit with direct + NEE lighting,
|
|
465
478
|
// sky/env on a miss. Specular rays keep emitter emission on hit (NEE at the ray
|
|
466
479
|
// origin cannot cover a specular path); diffuse GI rays drop it for NEE-listed
|
|
467
|
-
// (static
|
|
480
|
+
// emitters (static AND dynamic — dynamic emitters now join the NEE table, their
|
|
481
|
+
// rows refreshed each frame) so that light isn't counted twice.
|
|
468
482
|
vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
469
483
|
uvec4 fi; vec3 bary; float dist; bool isDyn;
|
|
470
484
|
if (!traceBoth(ro, rd, fi, bary, dist, isDyn)) {
|
|
@@ -481,7 +495,7 @@ vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
|
481
495
|
if (dot(hN, rd) > 0.0) hN = -hN;
|
|
482
496
|
vec3 hP = ro + rd * dist;
|
|
483
497
|
vec3 Ld = sampleOneAny(hP + hN * uEps, hN);
|
|
484
|
-
vec3 hLe = (!specular && uEmissiveCount > 0
|
|
498
|
+
vec3 hLe = (!specular && uEmissiveCount > 0) ? vec3(0.0) : hEmissive;
|
|
485
499
|
return hLe + hAlbedo * Ld * (1.0 / PI);
|
|
486
500
|
}
|
|
487
501
|
|
|
@@ -541,16 +555,66 @@ vec3 analyticGlint(vec3 P, vec3 refl) {
|
|
|
541
555
|
|
|
542
556
|
// Glass: Fresnel-weighted blend of a surface reflection and a two-interface
|
|
543
557
|
// refraction (enter at P, march to the exit surface, refract again).
|
|
544
|
-
|
|
558
|
+
//
|
|
559
|
+
// CHROMATIC DISPERSION (stochastic spectral sampling). Real glass has a
|
|
560
|
+
// wavelength-dependent ior, so white light splits into a spectrum (a diamond
|
|
561
|
+
// throws a rainbow). Tracing one refraction path per colour would cost three
|
|
562
|
+
// traceRadiance calls, but the Metal call-site budget (see the note at the
|
|
563
|
+
// unified secondary-ray site) forbids a fourth traceRadiance anywhere in this
|
|
564
|
+
// shader. Instead, when uDispersion > 0 each frame this pixel picks ONE colour
|
|
565
|
+
// channel c in R,G,B uniformly and traces the SAME single refraction path with
|
|
566
|
+
// a channel-shifted ior. The refracted radiance is then isolated to channel c
|
|
567
|
+
// and multiplied by 3 (to compensate the 1-of-3 pick); the temporal EMA
|
|
568
|
+
// averages the three per-channel estimates into a full-spectrum, dispersed
|
|
569
|
+
// refraction — zero extra rays, zero new call sites, unbiased in the mean. It
|
|
570
|
+
// therefore shimmers slightly while converging.
|
|
571
|
+
//
|
|
572
|
+
// THE MIX SPLIT. The return is mix(refrRad, reflRad, fres) = refrRad*(1-fres)
|
|
573
|
+
// + reflRad*fres. Only the TRANSMITTED half (refrRad) carries the channel
|
|
574
|
+
// mask; the reflection half (reflRad) is NOT dispersed and stays full colour
|
|
575
|
+
// EVERY frame. To keep the reflection deterministic frame-to-frame, the
|
|
576
|
+
// Fresnel weight is taken from the BASE ior (constant), not the channel-shifted
|
|
577
|
+
// ior — only the refracted ray DIRECTION disperses, so the reflection term
|
|
578
|
+
// reflRad*fres is identical every frame while refrRad*mask*3 is the spectral
|
|
579
|
+
// estimator.
|
|
580
|
+
//
|
|
581
|
+
// OFF-PATH IDENTITY. uDispersion == 0 skips the channel pick entirely: it
|
|
582
|
+
// consumes NO rand() (so the RNG stream does not shift), leaves iorC == ior and
|
|
583
|
+
// chanMask == vec3(1), and the whole function reduces byte-for-byte to the
|
|
584
|
+
// pre-dispersion path.
|
|
585
|
+
vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough, float ior) {
|
|
545
586
|
vec3 refl = glossyReflect(V, N, rough);
|
|
546
587
|
vec3 reflRad = dot(refl, N) > 0.0
|
|
547
588
|
? traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl)
|
|
548
589
|
: vec3(0.0);
|
|
549
590
|
|
|
550
|
-
|
|
591
|
+
// Per-frame spectral channel pick for the transmitted term (guarded so the
|
|
592
|
+
// off path consumes no rand()).
|
|
593
|
+
vec3 chanMask = vec3(1.0); // full colour (un-masked) when dispersion is off
|
|
594
|
+
float iorC = ior;
|
|
595
|
+
if (uDispersion > 0.0) {
|
|
596
|
+
int c = min(int(rand() * 3.0), 2); // uniform channel: 0 = R, 1 = G, 2 = B
|
|
597
|
+
// Normal dispersion: BLUE has the higher refractive index and bends most,
|
|
598
|
+
// red least. shift = (-1.0, 0.0, +1.0) * 0.5, indexed by channel:
|
|
599
|
+
// R = -0.5, G = 0, B = +0.5. uDispersion (0..0.5) scales the ior spread.
|
|
600
|
+
// (The original spec vector had the R/B signs reversed — audit-corrected.)
|
|
601
|
+
float shift = c == 0 ? -0.5 : (c == 2 ? 0.5 : 0.0);
|
|
602
|
+
iorC = ior * (1.0 + uDispersion * shift);
|
|
603
|
+
// Isolate channel c and weight x3: vec3(3,0,0) / (0,3,0) / (0,0,3). The
|
|
604
|
+
// mean over the three equally-likely picks is (1/3)(3,0,0)+... = (1,1,1),
|
|
605
|
+
// so E[masked refrRad] == refrRad. The OTHER channels are zero this frame.
|
|
606
|
+
chanMask = c == 0 ? vec3(3.0, 0.0, 0.0)
|
|
607
|
+
: c == 1 ? vec3(0.0, 3.0, 0.0)
|
|
608
|
+
: vec3(0.0, 0.0, 3.0);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
float eta = 1.0 / iorC; // channel-shifted: drives the refraction bend
|
|
551
612
|
vec3 rd = refract(V, N, eta);
|
|
552
|
-
if (rd == vec3(0.0)) return reflRad;
|
|
553
|
-
|
|
613
|
+
if (rd == vec3(0.0)) return reflRad; // total internal reflection at entry
|
|
614
|
+
// Fresnel from the BASE ior so the reflection/refraction split is the same
|
|
615
|
+
// every frame (reflection stays full colour and un-dispersed). Equal to the
|
|
616
|
+
// original schlick(..., eta) when uDispersion == 0 (iorC == ior).
|
|
617
|
+
float fres = schlick(clamp(-dot(V, N), 0.0, 1.0), 1.0 / ior);
|
|
554
618
|
|
|
555
619
|
vec3 ro = P - N * (2.0 * uEps);
|
|
556
620
|
vec3 refrRad;
|
|
@@ -563,7 +627,7 @@ vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough) {
|
|
|
563
627
|
vec3 xN = normalize(attr.xyz);
|
|
564
628
|
if (dot(xN, rd) > 0.0) xN = -xN;
|
|
565
629
|
vec3 xP = ro + rd * dist;
|
|
566
|
-
vec3 rd2 = refract(rd, xN,
|
|
630
|
+
vec3 rd2 = refract(rd, xN, iorC); // same channel-shifted ior on exit
|
|
567
631
|
if (rd2 == vec3(0.0)) rd2 = reflect(rd, xN);
|
|
568
632
|
refrRad = traceRadiance(xP - xN * uEps, rd2, true);
|
|
569
633
|
} else {
|
|
@@ -571,7 +635,29 @@ vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough) {
|
|
|
571
635
|
? skyColor(rd, uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity)
|
|
572
636
|
: uEnvColor * uEnvIntensity;
|
|
573
637
|
}
|
|
574
|
-
|
|
638
|
+
// Mask ONLY the transmitted term to the chosen channel (full colour when
|
|
639
|
+
// dispersion is off); the reflection term is never masked.
|
|
640
|
+
return mix(refrRad * chanMask, reflRad, fres);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Compact cold->hot ramp for the BVH-cost heatmap. Piecewise mix of five
|
|
644
|
+
// anchors (deep blue -> green -> yellow -> red -> white) over four equal
|
|
645
|
+
// segments — cheap, no textures, no extra samplers. t is the normalised cost
|
|
646
|
+
// (visit count * uCostScale), clamped to [0,1]; saturating at white = the most
|
|
647
|
+
// expensive pixels.
|
|
648
|
+
vec3 costPalette(float t) {
|
|
649
|
+
t = clamp(t, 0.0, 1.0);
|
|
650
|
+
const vec3 c0 = vec3(0.02, 0.05, 0.45); // cold: cheap (few boxes)
|
|
651
|
+
const vec3 c1 = vec3(0.05, 0.55, 0.25); // green
|
|
652
|
+
const vec3 c2 = vec3(0.95, 0.85, 0.10); // yellow
|
|
653
|
+
const vec3 c3 = vec3(0.90, 0.10, 0.05); // red
|
|
654
|
+
const vec3 c4 = vec3(1.00, 1.00, 1.00); // hot: expensive (many boxes)
|
|
655
|
+
float s = t * 4.0;
|
|
656
|
+
vec3 col = mix(c0, c1, clamp(s, 0.0, 1.0));
|
|
657
|
+
col = mix(col, c2, clamp(s - 1.0, 0.0, 1.0));
|
|
658
|
+
col = mix(col, c3, clamp(s - 2.0, 0.0, 1.0));
|
|
659
|
+
col = mix(col, c4, clamp(s - 3.0, 0.0, 1.0));
|
|
660
|
+
return col;
|
|
575
661
|
}
|
|
576
662
|
|
|
577
663
|
void main() {
|
|
@@ -599,6 +685,10 @@ void main() {
|
|
|
599
685
|
float transmission = (matW >= 2.0 && matW < 4.0) ? clamp(matW - 2.0, 0.0, 1.0) : 0.0;
|
|
600
686
|
float metal = matW < 2.0 ? matW : 0.0;
|
|
601
687
|
float rough = clamp(wp.w - 1.0, 0.0, 1.0);
|
|
688
|
+
// Per-material IOR rides the [3,4) glass sub-band (full-transmission glass, see
|
|
689
|
+
// GBufferPass). Below 3 (partial glass) or non-glass, fall back to the global
|
|
690
|
+
// rt.ior uniform. material.ior wins whenever it was encoded. (Task 2)
|
|
691
|
+
float ior = (matW >= 3.0 && matW < 4.0) ? (1.0 + (matW - 3.0)) : uIor;
|
|
602
692
|
|
|
603
693
|
// Cook-Torrance specular state for this primary surface. gWantSpec gates the
|
|
604
694
|
// GGX term to PRIMARY direct lighting only (GI-bounce direct light, below,
|
|
@@ -608,6 +698,11 @@ void main() {
|
|
|
608
698
|
gSpecRough = rough;
|
|
609
699
|
gWantSpec = true;
|
|
610
700
|
|
|
701
|
+
// Reset the shadow-ray traversal-cost counter for this pixel. It accumulates
|
|
702
|
+
// across every occluded() call below (direct, GI, reflection, glass) and is
|
|
703
|
+
// read once at the end when uCostView is on (see the cost-heatmap branch).
|
|
704
|
+
gBvhVisits = 0;
|
|
705
|
+
|
|
611
706
|
// --- direct lighting ---
|
|
612
707
|
// ReSTIR: shade the reservoir's winner with one visibility ray (flat cost in
|
|
613
708
|
// light count). Stochastic: one blind random sample. Full: one shadow ray
|
|
@@ -646,7 +741,10 @@ void main() {
|
|
|
646
741
|
vec3 indirect = vec3(0.0);
|
|
647
742
|
vec3 blendBehind = vec3(0.0);
|
|
648
743
|
bool wantBehind = uBlendEnabled && blend;
|
|
649
|
-
|
|
744
|
+
// uExternalGI (experimental ReSTIR GI): the GIReservoirPass supplies the
|
|
745
|
+
// bounce, so the inline GI ray is skipped — but the blend continuation is
|
|
746
|
+
// NOT GI and must keep tracing regardless.
|
|
747
|
+
bool wantGI = uGIEnabled && !uExternalGI && !wantBehind
|
|
650
748
|
&& (!uGIHalfRate || (((px.x + px.y + int(uFrame)) & 1) == 0));
|
|
651
749
|
if (wantBehind || wantGI) {
|
|
652
750
|
vec3 Vv = normalize(P - uCameraPos);
|
|
@@ -685,7 +783,7 @@ void main() {
|
|
|
685
783
|
// --- traced glass: Fresnel reflection + two-interface refraction ---
|
|
686
784
|
if (uRefrEnabled && transmission > 0.001) {
|
|
687
785
|
vec3 V = normalize(P - uCameraPos);
|
|
688
|
-
sampleIrr = mix(sampleIrr, glassRadiance(P, N, V, rough), transmission);
|
|
786
|
+
sampleIrr = mix(sampleIrr, glassRadiance(P, N, V, rough, ior), transmission);
|
|
689
787
|
}
|
|
690
788
|
|
|
691
789
|
// --- alpha blend: straight-through view continuation ---
|
|
@@ -772,6 +870,17 @@ void main() {
|
|
|
772
870
|
// the fresh sample is used as-is.
|
|
773
871
|
vec3 blended = mix(history, sampleIrr, 1.0 / count);
|
|
774
872
|
outIrradiance = vec4(blended, count);
|
|
873
|
+
|
|
874
|
+
// BVH traversal-cost heatmap (outputMode 7). Overwrite the accumulated
|
|
875
|
+
// lighting with the palette-mapped shadow-ray node-visit count for this pixel.
|
|
876
|
+
// Alpha is forced to 1.0 so temporal history never builds on the cost image
|
|
877
|
+
// (each frame is a fresh snapshot), and the specular attachment is cleared so
|
|
878
|
+
// the composite's cost branch shows the palette alone. Uniform branch: when
|
|
879
|
+
// uCostView is off this is skipped and the writes above stand unchanged.
|
|
880
|
+
if (uCostView) {
|
|
881
|
+
outIrradiance = vec4(costPalette(float(gBvhVisits) * uCostScale), 1.0);
|
|
882
|
+
outSpecular = vec4(0.0);
|
|
883
|
+
}
|
|
775
884
|
}
|
|
776
885
|
`;
|
|
777
886
|
|
|
@@ -923,6 +1032,7 @@ export class RTLightingPass {
|
|
|
923
1032
|
uRefrEnabled: { value: true },
|
|
924
1033
|
uBlendEnabled: { value: true },
|
|
925
1034
|
uIor: { value: 1.5 },
|
|
1035
|
+
uDispersion: { value: 0 },
|
|
926
1036
|
uLightStochastic: { value: false },
|
|
927
1037
|
uGIHalfRate: { value: false },
|
|
928
1038
|
uEnvColor: { value: new THREE.Color(0.03, 0.04, 0.06) },
|
|
@@ -930,6 +1040,9 @@ export class RTLightingPass {
|
|
|
930
1040
|
uFrame: { value: 0 },
|
|
931
1041
|
uEps: { value: 1e-3 },
|
|
932
1042
|
uGIEnabled: { value: true },
|
|
1043
|
+
uExternalGI: { value: false },
|
|
1044
|
+
uCostView: { value: false },
|
|
1045
|
+
uCostScale: { value: 1 / 96 },
|
|
933
1046
|
uSkyEnabled: { value: false },
|
|
934
1047
|
uSunDir: { value: new THREE.Vector3(0.4, 0.8, 0.45).normalize() },
|
|
935
1048
|
uSunColor: { value: new THREE.Color(1.0, 0.9, 0.75) },
|
package/src/RealtimeRaytracer.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CompositePass } from "./CompositePass.js";
|
|
|
7
7
|
import { TAAPass } from "./TAAPass.js";
|
|
8
8
|
import { VolumetricPass } from "./VolumetricPass.js";
|
|
9
9
|
import { RestirPass } from "./RestirPass.js";
|
|
10
|
+
import { GIReservoirPass } from "./GIReservoirPass.js";
|
|
10
11
|
import { CopyPass } from "./CopyPass.js";
|
|
11
12
|
|
|
12
13
|
// Van der Corput / Halton radical inverse — deterministic low-discrepancy
|
|
@@ -423,8 +424,15 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
423
424
|
this.compiled = null;
|
|
424
425
|
this.frame = 0;
|
|
425
426
|
|
|
426
|
-
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive, 6 specular */
|
|
427
|
+
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive, 6 specular, 7 bvh cost */
|
|
427
428
|
this.outputMode = 0;
|
|
429
|
+
/**
|
|
430
|
+
* BVH-cost heatmap scale (outputMode 7): the per-pixel shadow-ray node-visit
|
|
431
|
+
* count is multiplied by this before the palette, so 1/costScale visits map
|
|
432
|
+
* to the hot (white) end. Default 1/96 — ~96 visits saturate. Live-tunable
|
|
433
|
+
* (the demo's "cost scale" slider drives it).
|
|
434
|
+
*/
|
|
435
|
+
this.costScale = options.costScale ?? 1 / 96;
|
|
428
436
|
/** Environment (sky) color used for GI rays that miss + composite background. */
|
|
429
437
|
this.envColor = options.envColor ?? new THREE.Color(0.03, 0.04, 0.06);
|
|
430
438
|
this.envIntensity = options.envIntensity ?? 1.0;
|
|
@@ -484,6 +492,17 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
484
492
|
this.transparency = options.transparency ?? true;
|
|
485
493
|
/** Index of refraction used for transmissive surfaces. */
|
|
486
494
|
this.ior = options.ior ?? 1.5;
|
|
495
|
+
/**
|
|
496
|
+
* Chromatic dispersion strength for glass (0..0.5, clamped on upload,
|
|
497
|
+
* default 0 = off). Splits refracted white light into a spectrum: each
|
|
498
|
+
* frame every glass pixel estimates ONE colour channel through a
|
|
499
|
+
* channel-shifted ior and the temporal accumulator blends the three into a
|
|
500
|
+
* rainbow (stochastic spectral sampling — no extra rays). It shimmers
|
|
501
|
+
* slightly while converging, so it needs temporal accumulation to settle.
|
|
502
|
+
* Global control only for now (there is no free G-buffer channel for a
|
|
503
|
+
* per-material MeshPhysicalMaterial.dispersion).
|
|
504
|
+
*/
|
|
505
|
+
this.dispersion = options.dispersion ?? 0;
|
|
487
506
|
/**
|
|
488
507
|
* One stochastic direct shadow ray per pixel per frame (source picked at
|
|
489
508
|
* random) instead of one per light — the biggest ray-count lever for
|
|
@@ -552,6 +571,13 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
552
571
|
this.taa = options.taa ?? true;
|
|
553
572
|
/** Fresh-sample weight in the TAA blend (lower = smoother/more AA, more lag). */
|
|
554
573
|
this.taaBlend = options.taaBlend ?? 0.1;
|
|
574
|
+
/**
|
|
575
|
+
* Scales the TAA sub-pixel jitter amplitude. Set this to your canvas scale
|
|
576
|
+
* when you render a reduced drawing buffer CSS-stretched to the screen
|
|
577
|
+
* (canvasScaleHook), so the jitter stays constant in SCREEN pixels instead
|
|
578
|
+
* of being magnified by the stretch (visible wobble at low quality).
|
|
579
|
+
*/
|
|
580
|
+
this.taaJitterScale = options.taaJitterScale ?? 1;
|
|
555
581
|
|
|
556
582
|
/**
|
|
557
583
|
* Volumetric lighting — real "god rays": single-scatter fog integrated
|
|
@@ -582,6 +608,45 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
582
608
|
this.restir = options.restir ?? true;
|
|
583
609
|
this.restirPass = new RestirPass(this._scaledW, this._scaledH);
|
|
584
610
|
|
|
611
|
+
/**
|
|
612
|
+
* EXPERIMENTAL — ReSTIR GI (v1, temporal-only): per-pixel reservoirs reuse
|
|
613
|
+
* the 1-bounce global-illumination sample across frames (at the reprojected
|
|
614
|
+
* same-surface point, no spatial reuse / no Jacobian). Runs in a standalone
|
|
615
|
+
* pass with its own sampler budget; when on, the lighting pass skips its
|
|
616
|
+
* inline GI trace and this pass's resolved GI is added at the denoise stage.
|
|
617
|
+
* Only meaningful when `gi` is on, and injected via the à-trous denoise, so
|
|
618
|
+
* it requires `denoise` (denoiseIterations >= 1). Default OFF. Its mean
|
|
619
|
+
* matches the inline GI path — see GIReservoirPass. Live-toggleable.
|
|
620
|
+
*/
|
|
621
|
+
this.restirGI = options.restirGI ?? false;
|
|
622
|
+
/** Temporal M-cap for the ReSTIR GI reservoir (staleness limit). */
|
|
623
|
+
this.restirGIMCap = options.restirGIMCap ?? 20;
|
|
624
|
+
/**
|
|
625
|
+
* ReSTIR GI (v2) spatial-reuse taps per frame, taken after the temporal
|
|
626
|
+
* merge from the previous frame's reservoirs (reconnection-Jacobian
|
|
627
|
+
* reweighted, with a final visibility ray). Clamped to 0..4; `0` reproduces
|
|
628
|
+
* the v1 temporal-only behaviour. Default 2.
|
|
629
|
+
*/
|
|
630
|
+
// Default 1 (not 2): each reconnection tap adds its own estimator noise;
|
|
631
|
+
// one tap keeps most of the disocclusion win at half the artifact surface
|
|
632
|
+
// (tuned on-device — raise it for scenes with heavy camera motion).
|
|
633
|
+
this.restirGISpatialTaps = options.restirGISpatialTaps ?? 1;
|
|
634
|
+
/**
|
|
635
|
+
* EXPERIMENTAL — ReSTIR GI reservoir-sample validation period. Every frame a
|
|
636
|
+
* rotating 1-in-N subset of pixels (decorrelated by a per-pixel hash) re-aims
|
|
637
|
+
* its ONE candidate ray at the reservoir's STORED hit instead of a fresh
|
|
638
|
+
* cosine bounce and re-shades it; the reservoir is killed (so fresh candidates
|
|
639
|
+
* rebuild) when the geometry moved or the re-shaded target collapsed to
|
|
640
|
+
* near-black (a light switched off), and left untouched otherwise. This reuses
|
|
641
|
+
* the existing candidate trace (no extra bounce rays) and is the fix for stale
|
|
642
|
+
* bounce light: a switched-off light stops haunting the reservoir instead of
|
|
643
|
+
* fading slowly, while a static scene does not drift. `0` disables it
|
|
644
|
+
* (byte-identical to before the feature); default 8.
|
|
645
|
+
*/
|
|
646
|
+
this.restirGIValidate = options.restirGIValidate ?? 8;
|
|
647
|
+
this.giReservoirPass = new GIReservoirPass(this._scaledW, this._scaledH);
|
|
648
|
+
this._giMissWarned = false;
|
|
649
|
+
|
|
585
650
|
/** Distance fog (composited in linear space before tonemap). */
|
|
586
651
|
this.fog = {
|
|
587
652
|
enabled: options.fog?.enabled ?? false,
|
|
@@ -703,6 +768,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
703
768
|
this.rtPass.setCompiledScene(this.compiled);
|
|
704
769
|
this.volumetricPass.setCompiledScene(this.compiled);
|
|
705
770
|
this.restirPass.setCompiledScene(this.compiled);
|
|
771
|
+
this.giReservoirPass.setCompiledScene(this.compiled);
|
|
706
772
|
this.resetAccumulation();
|
|
707
773
|
return this.compiled;
|
|
708
774
|
}
|
|
@@ -728,6 +794,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
728
794
|
this.rtPass.setCompiledScene(this.compiled);
|
|
729
795
|
this.volumetricPass.setCompiledScene(this.compiled);
|
|
730
796
|
this.restirPass.setCompiledScene(this.compiled);
|
|
797
|
+
this.giReservoirPass.setCompiledScene(this.compiled);
|
|
731
798
|
}
|
|
732
799
|
|
|
733
800
|
resetAccumulation() {
|
|
@@ -831,6 +898,11 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
831
898
|
// clear them.
|
|
832
899
|
this.restirPass.setSize(sw, sh);
|
|
833
900
|
this.restirPass.clearHistory(this.renderer);
|
|
901
|
+
// Reservoir GI history is packed (hit position + M + radiance + W) —
|
|
902
|
+
// invalid to linearly resample — but reconverges in a few frames, so
|
|
903
|
+
// reallocate and clear like the DI reservoirs.
|
|
904
|
+
this.giReservoirPass.setSize(sw, sh);
|
|
905
|
+
this.giReservoirPass.clearHistory(this.renderer);
|
|
834
906
|
}
|
|
835
907
|
|
|
836
908
|
// Full-res / canvas-res targets: only touched on a real canvas resize (a
|
|
@@ -976,8 +1048,15 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
976
1048
|
// too — otherwise the raw buffers visibly shake.
|
|
977
1049
|
if (this.taa && this.outputMode === 0) {
|
|
978
1050
|
this._jitterIndex = (this._jitterIndex + 1) % 16;
|
|
979
|
-
|
|
980
|
-
|
|
1051
|
+
// taaJitterScale: when the app renders a REDUCED drawing buffer and CSS-
|
|
1052
|
+
// stretches it to the screen (the canvas-scale ladder), a half-buffer-pixel
|
|
1053
|
+
// jitter is magnified by the stretch and reads as visible screen wobble.
|
|
1054
|
+
// The app sets this to its canvas scale (see canvasScaleHook) so the
|
|
1055
|
+
// jitter stays constant in SCREEN pixels — slightly less sub-pixel AA
|
|
1056
|
+
// coverage at low canvas scales, in exchange for a steady image.
|
|
1057
|
+
const js = this.taaJitterScale;
|
|
1058
|
+
const jx = (halton(this._jitterIndex + 1, 2) - 0.5) * 2 * js / this._width;
|
|
1059
|
+
const jy = (halton(this._jitterIndex + 1, 3) - 0.5) * 2 * js / this._height;
|
|
981
1060
|
proj.elements[8] += jx;
|
|
982
1061
|
proj.elements[9] += jy;
|
|
983
1062
|
// Where this jitter moves the image, in UV space: elements[8/9] multiply
|
|
@@ -999,6 +1078,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
999
1078
|
this.rtPass.clearHistory(this.renderer);
|
|
1000
1079
|
this.volumetricPass.clearHistory(this.renderer);
|
|
1001
1080
|
this.restirPass.clearHistory(this.renderer);
|
|
1081
|
+
this.giReservoirPass.clearHistory(this.renderer);
|
|
1002
1082
|
this._needsClear = false;
|
|
1003
1083
|
}
|
|
1004
1084
|
|
|
@@ -1010,17 +1090,35 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1010
1090
|
rtU.uEnvColor.value.copy(this.envColor);
|
|
1011
1091
|
rtU.uEnvIntensity.value = this.envIntensity;
|
|
1012
1092
|
rtU.uEps.value = this.eps;
|
|
1093
|
+
rtU.uCostView.value = this.outputMode === 7;
|
|
1094
|
+
rtU.uCostScale.value = this.costScale;
|
|
1013
1095
|
rtU.uTemporalReprojection.value = this.temporalReprojection;
|
|
1014
1096
|
rtU.uMaxHistory.value = this.maxHistory;
|
|
1015
1097
|
rtU.uFireflyClamp.value = this.fireflyClamp > 0 ? this.fireflyClamp : 1e6;
|
|
1016
1098
|
rtU.uGIEnabled.value = this.gi;
|
|
1017
1099
|
rtU.uGIHalfRate.value = this.giHalfRate;
|
|
1100
|
+
// ReSTIR GI (experimental) supplies the 1-bounce indirect externally when
|
|
1101
|
+
// on; the lighting pass then skips its inline GI trace so it isn't double
|
|
1102
|
+
// counted. It's injected at the denoise stage, so it needs denoise on.
|
|
1103
|
+
const giExternal =
|
|
1104
|
+
this.restirGI && this.gi && this.denoise && this.denoiseIterations > 0;
|
|
1105
|
+
rtU.uExternalGI.value = giExternal;
|
|
1106
|
+
if (this.restirGI && this.gi && !giExternal && !this._giMissWarned) {
|
|
1107
|
+
console.info(
|
|
1108
|
+
"[three-realtime-rt] restirGI is on but denoise is off — ReSTIR GI is " +
|
|
1109
|
+
"injected during the à-trous denoise, so enable denoise " +
|
|
1110
|
+
"(denoiseIterations >= 1) to see its contribution."
|
|
1111
|
+
);
|
|
1112
|
+
this._giMissWarned = true;
|
|
1113
|
+
}
|
|
1114
|
+
if (giExternal) this._giMissWarned = false;
|
|
1018
1115
|
rtU.uEmissiveCount.value = this.emissiveNEE ? this.compiled.emissiveTriCount : 0;
|
|
1019
1116
|
rtU.uEmissiveCDF.value = this.emissiveImportance;
|
|
1020
1117
|
rtU.uReflEnabled.value = this.reflections;
|
|
1021
1118
|
rtU.uRefrEnabled.value = this.refraction;
|
|
1022
1119
|
rtU.uBlendEnabled.value = this.transparency;
|
|
1023
1120
|
rtU.uIor.value = this.ior;
|
|
1121
|
+
rtU.uDispersion.value = Math.min(0.5, Math.max(0, this.dispersion));
|
|
1024
1122
|
rtU.uLightStochastic.value = this.stochasticLights;
|
|
1025
1123
|
rtU.uSkyEnabled.value = this.sky.enabled;
|
|
1026
1124
|
rtU.uSunDir.value.copy(this.sky.sunDir);
|
|
@@ -1048,17 +1146,55 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1048
1146
|
this.eps
|
|
1049
1147
|
);
|
|
1050
1148
|
}
|
|
1149
|
+
// 2b. ReSTIR GI reservoirs (experimental). Runs after the lighting pass's
|
|
1150
|
+
// inline GI is skipped (uExternalGI); the resolved GI is added at denoise.
|
|
1151
|
+
let giTex = null;
|
|
1152
|
+
if (giExternal) {
|
|
1153
|
+
this.giReservoirPass.setEmissiveCount(
|
|
1154
|
+
this.emissiveNEE ? this.compiled.emissiveTriCount : 0
|
|
1155
|
+
);
|
|
1156
|
+
giTex = this.giReservoirPass.render(
|
|
1157
|
+
this.renderer,
|
|
1158
|
+
this.gbuffer,
|
|
1159
|
+
this._prevViewProj,
|
|
1160
|
+
this._camWorldPos,
|
|
1161
|
+
this.frame,
|
|
1162
|
+
this.eps,
|
|
1163
|
+
{
|
|
1164
|
+
fireflyClamp: this.fireflyClamp > 0 ? this.fireflyClamp : 1e6,
|
|
1165
|
+
mCap: this.restirGIMCap,
|
|
1166
|
+
spatialTaps: Math.max(0, Math.min(4, this.restirGISpatialTaps | 0)),
|
|
1167
|
+
validateInterval: Math.max(0, this.restirGIValidate | 0),
|
|
1168
|
+
emissiveCDF: this.emissiveImportance,
|
|
1169
|
+
envColor: this.envColor,
|
|
1170
|
+
envIntensity: this.envIntensity,
|
|
1171
|
+
skyEnabled: this.sky.enabled,
|
|
1172
|
+
sunDir: this.sky.sunDir,
|
|
1173
|
+
sunColor: this.sky.sunColor,
|
|
1174
|
+
skyZenith: this.sky.zenith,
|
|
1175
|
+
skyHorizon: this.sky.horizon,
|
|
1176
|
+
skyIntensity: this.sky.intensity,
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1051
1181
|
let { irradiance, specular } = this.rtPass.render(this.renderer, this.gbuffer, this.frame, reservoirTex);
|
|
1052
1182
|
|
|
1053
|
-
// 3. denoise (display-only: history keeps accumulating raw samples)
|
|
1054
|
-
|
|
1183
|
+
// 3. denoise (display-only: history keeps accumulating raw samples). The
|
|
1184
|
+
// experimental ReSTIR GI (giTex) is added on the first à-trous iteration —
|
|
1185
|
+
// downstream of the lighting pass's temporal history, so it never
|
|
1186
|
+
// double-counts through it. The bvh-cost heatmap (mode 7) is a per-pixel
|
|
1187
|
+
// debug signal, not lighting — the edge-aware blur would smear its bands,
|
|
1188
|
+
// so it bypasses the denoiser (which also keeps the GI add out of mode 7).
|
|
1189
|
+
if (this.denoise && this.denoiseIterations > 0 && this.outputMode !== 7) {
|
|
1055
1190
|
irradiance = this.denoisePass.render(
|
|
1056
1191
|
this.renderer,
|
|
1057
1192
|
irradiance,
|
|
1058
1193
|
this.gbuffer,
|
|
1059
1194
|
this._camWorldPos,
|
|
1060
1195
|
this.eps,
|
|
1061
|
-
this.denoiseIterations
|
|
1196
|
+
this.denoiseIterations,
|
|
1197
|
+
giTex
|
|
1062
1198
|
);
|
|
1063
1199
|
}
|
|
1064
1200
|
|
|
@@ -1179,6 +1315,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1179
1315
|
this.taaPass.dispose();
|
|
1180
1316
|
this.volumetricPass.dispose();
|
|
1181
1317
|
this.restirPass.dispose();
|
|
1318
|
+
this.giReservoirPass.dispose();
|
|
1182
1319
|
this._sceneColor.dispose();
|
|
1183
1320
|
this._copyPass.dispose();
|
|
1184
1321
|
if (this.compiled) this.compiled.dispose();
|