three-realtime-rt 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -13
- package/package.json +2 -1
- package/src/CompositePass.js +3 -0
- package/src/CopyPass.js +3 -0
- package/src/DenoisePass.js +37 -7
- package/src/GBufferPass.js +6 -1
- package/src/GIReservoirPass.js +443 -62
- package/src/RTLightingPass.js +74 -8
- package/src/RealtimeRaytracer.js +237 -2
- package/src/RestirPass.js +12 -5
- package/src/SceneCompiler.js +219 -33
- package/src/TAAPass.js +6 -0
- package/src/VolumetricPass.js +3 -0
- package/src/index.d.ts +102 -2
- package/src/mrtCompat.js +37 -0
package/src/RTLightingPass.js
CHANGED
|
@@ -3,6 +3,7 @@ import { shaderStructs, shaderIntersectFunction } from "three-mesh-bvh";
|
|
|
3
3
|
import { MAX_LIGHTS } from "./SceneCompiler.js";
|
|
4
4
|
import { SKY_GLSL } from "./sky.glsl.js";
|
|
5
5
|
import { BVH_ANY_HIT_GLSL } from "./bvhAnyHit.glsl.js";
|
|
6
|
+
import { makeMRT } from "./mrtCompat.js";
|
|
6
7
|
|
|
7
8
|
const fullscreenVert = /* glsl */ `
|
|
8
9
|
out vec2 vUv;
|
|
@@ -66,6 +67,7 @@ uniform bool uReflEnabled; // traced reflections on metallic surfaces
|
|
|
66
67
|
uniform bool uRefrEnabled; // traced refraction on transmissive surfaces
|
|
67
68
|
uniform bool uBlendEnabled; // straight-through view continuation on blend surfaces
|
|
68
69
|
uniform float uIor; // index of refraction for transmissive materials
|
|
70
|
+
uniform float uDispersion; // chromatic dispersion strength for glass (0 = off)
|
|
69
71
|
uniform bool uLightStochastic; // 1 direct shadow ray/pixel/frame instead of 1/light
|
|
70
72
|
uniform bool uRestirEnabled; // shade the reservoir winner instead of sampling
|
|
71
73
|
uniform bool uGIHalfRate; // GI ray on alternating checkerboard, doubled
|
|
@@ -476,7 +478,8 @@ vec3 sampleOneAny(vec3 P, vec3 N) {
|
|
|
476
478
|
// Incoming radiance along rd: trace, shade the hit with direct + NEE lighting,
|
|
477
479
|
// sky/env on a miss. Specular rays keep emitter emission on hit (NEE at the ray
|
|
478
480
|
// origin cannot cover a specular path); diffuse GI rays drop it for NEE-listed
|
|
479
|
-
// (static
|
|
481
|
+
// emitters (static AND dynamic — dynamic emitters now join the NEE table, their
|
|
482
|
+
// rows refreshed each frame) so that light isn't counted twice.
|
|
480
483
|
vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
481
484
|
uvec4 fi; vec3 bary; float dist; bool isDyn;
|
|
482
485
|
if (!traceBoth(ro, rd, fi, bary, dist, isDyn)) {
|
|
@@ -493,7 +496,7 @@ vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
|
493
496
|
if (dot(hN, rd) > 0.0) hN = -hN;
|
|
494
497
|
vec3 hP = ro + rd * dist;
|
|
495
498
|
vec3 Ld = sampleOneAny(hP + hN * uEps, hN);
|
|
496
|
-
vec3 hLe = (!specular && uEmissiveCount > 0
|
|
499
|
+
vec3 hLe = (!specular && uEmissiveCount > 0) ? vec3(0.0) : hEmissive;
|
|
497
500
|
return hLe + hAlbedo * Ld * (1.0 / PI);
|
|
498
501
|
}
|
|
499
502
|
|
|
@@ -553,16 +556,66 @@ vec3 analyticGlint(vec3 P, vec3 refl) {
|
|
|
553
556
|
|
|
554
557
|
// Glass: Fresnel-weighted blend of a surface reflection and a two-interface
|
|
555
558
|
// refraction (enter at P, march to the exit surface, refract again).
|
|
559
|
+
//
|
|
560
|
+
// CHROMATIC DISPERSION (stochastic spectral sampling). Real glass has a
|
|
561
|
+
// wavelength-dependent ior, so white light splits into a spectrum (a diamond
|
|
562
|
+
// throws a rainbow). Tracing one refraction path per colour would cost three
|
|
563
|
+
// traceRadiance calls, but the Metal call-site budget (see the note at the
|
|
564
|
+
// unified secondary-ray site) forbids a fourth traceRadiance anywhere in this
|
|
565
|
+
// shader. Instead, when uDispersion > 0 each frame this pixel picks ONE colour
|
|
566
|
+
// channel c in R,G,B uniformly and traces the SAME single refraction path with
|
|
567
|
+
// a channel-shifted ior. The refracted radiance is then isolated to channel c
|
|
568
|
+
// and multiplied by 3 (to compensate the 1-of-3 pick); the temporal EMA
|
|
569
|
+
// averages the three per-channel estimates into a full-spectrum, dispersed
|
|
570
|
+
// refraction — zero extra rays, zero new call sites, unbiased in the mean. It
|
|
571
|
+
// therefore shimmers slightly while converging.
|
|
572
|
+
//
|
|
573
|
+
// THE MIX SPLIT. The return is mix(refrRad, reflRad, fres) = refrRad*(1-fres)
|
|
574
|
+
// + reflRad*fres. Only the TRANSMITTED half (refrRad) carries the channel
|
|
575
|
+
// mask; the reflection half (reflRad) is NOT dispersed and stays full colour
|
|
576
|
+
// EVERY frame. To keep the reflection deterministic frame-to-frame, the
|
|
577
|
+
// Fresnel weight is taken from the BASE ior (constant), not the channel-shifted
|
|
578
|
+
// ior — only the refracted ray DIRECTION disperses, so the reflection term
|
|
579
|
+
// reflRad*fres is identical every frame while refrRad*mask*3 is the spectral
|
|
580
|
+
// estimator.
|
|
581
|
+
//
|
|
582
|
+
// OFF-PATH IDENTITY. uDispersion == 0 skips the channel pick entirely: it
|
|
583
|
+
// consumes NO rand() (so the RNG stream does not shift), leaves iorC == ior and
|
|
584
|
+
// chanMask == vec3(1), and the whole function reduces byte-for-byte to the
|
|
585
|
+
// pre-dispersion path.
|
|
556
586
|
vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough, float ior) {
|
|
557
587
|
vec3 refl = glossyReflect(V, N, rough);
|
|
558
588
|
vec3 reflRad = dot(refl, N) > 0.0
|
|
559
589
|
? traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl)
|
|
560
590
|
: vec3(0.0);
|
|
561
591
|
|
|
562
|
-
|
|
592
|
+
// Per-frame spectral channel pick for the transmitted term (guarded so the
|
|
593
|
+
// off path consumes no rand()).
|
|
594
|
+
vec3 chanMask = vec3(1.0); // full colour (un-masked) when dispersion is off
|
|
595
|
+
float iorC = ior;
|
|
596
|
+
if (uDispersion > 0.0) {
|
|
597
|
+
int c = min(int(rand() * 3.0), 2); // uniform channel: 0 = R, 1 = G, 2 = B
|
|
598
|
+
// Normal dispersion: BLUE has the higher refractive index and bends most,
|
|
599
|
+
// red least. shift = (-1.0, 0.0, +1.0) * 0.5, indexed by channel:
|
|
600
|
+
// R = -0.5, G = 0, B = +0.5. uDispersion (0..0.5) scales the ior spread.
|
|
601
|
+
// (The original spec vector had the R/B signs reversed — audit-corrected.)
|
|
602
|
+
float shift = c == 0 ? -0.5 : (c == 2 ? 0.5 : 0.0);
|
|
603
|
+
iorC = ior * (1.0 + uDispersion * shift);
|
|
604
|
+
// Isolate channel c and weight x3: vec3(3,0,0) / (0,3,0) / (0,0,3). The
|
|
605
|
+
// mean over the three equally-likely picks is (1/3)(3,0,0)+... = (1,1,1),
|
|
606
|
+
// so E[masked refrRad] == refrRad. The OTHER channels are zero this frame.
|
|
607
|
+
chanMask = c == 0 ? vec3(3.0, 0.0, 0.0)
|
|
608
|
+
: c == 1 ? vec3(0.0, 3.0, 0.0)
|
|
609
|
+
: vec3(0.0, 0.0, 3.0);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
float eta = 1.0 / iorC; // channel-shifted: drives the refraction bend
|
|
563
613
|
vec3 rd = refract(V, N, eta);
|
|
564
|
-
if (rd == vec3(0.0)) return reflRad;
|
|
565
|
-
|
|
614
|
+
if (rd == vec3(0.0)) return reflRad; // total internal reflection at entry
|
|
615
|
+
// Fresnel from the BASE ior so the reflection/refraction split is the same
|
|
616
|
+
// every frame (reflection stays full colour and un-dispersed). Equal to the
|
|
617
|
+
// original schlick(..., eta) when uDispersion == 0 (iorC == ior).
|
|
618
|
+
float fres = schlick(clamp(-dot(V, N), 0.0, 1.0), 1.0 / ior);
|
|
566
619
|
|
|
567
620
|
vec3 ro = P - N * (2.0 * uEps);
|
|
568
621
|
vec3 refrRad;
|
|
@@ -575,7 +628,7 @@ vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough, float ior) {
|
|
|
575
628
|
vec3 xN = normalize(attr.xyz);
|
|
576
629
|
if (dot(xN, rd) > 0.0) xN = -xN;
|
|
577
630
|
vec3 xP = ro + rd * dist;
|
|
578
|
-
vec3 rd2 = refract(rd, xN,
|
|
631
|
+
vec3 rd2 = refract(rd, xN, iorC); // same channel-shifted ior on exit
|
|
579
632
|
if (rd2 == vec3(0.0)) rd2 = reflect(rd, xN);
|
|
580
633
|
refrRad = traceRadiance(xP - xN * uEps, rd2, true);
|
|
581
634
|
} else {
|
|
@@ -583,7 +636,9 @@ vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough, float ior) {
|
|
|
583
636
|
? skyColor(rd, uSunDir, uSunColor, uSkyZenith, uSkyHorizon, uSkyIntensity)
|
|
584
637
|
: uEnvColor * uEnvIntensity;
|
|
585
638
|
}
|
|
586
|
-
|
|
639
|
+
// Mask ONLY the transmitted term to the chosen channel (full colour when
|
|
640
|
+
// dispersion is off); the reflection term is never masked.
|
|
641
|
+
return mix(refrRad * chanMask, reflRad, fres);
|
|
587
642
|
}
|
|
588
643
|
|
|
589
644
|
// Compact cold->hot ramp for the BVH-cost heatmap. Piecewise mix of five
|
|
@@ -941,6 +996,10 @@ export class RTLightingPass {
|
|
|
941
996
|
this.specB = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
942
997
|
|
|
943
998
|
this.material = new THREE.ShaderMaterial({
|
|
999
|
+
// Stable program name for compile-failure self-diagnosis: this is the
|
|
1000
|
+
// CORE lighting megakernel — a link failure here has no fallback (see
|
|
1001
|
+
// RealtimeRaytracer._passClass -> coreFailure).
|
|
1002
|
+
name: "rt:lighting",
|
|
944
1003
|
glslVersion: THREE.GLSL3,
|
|
945
1004
|
vertexShader: fullscreenVert,
|
|
946
1005
|
fragmentShader: specMRT
|
|
@@ -978,6 +1037,7 @@ export class RTLightingPass {
|
|
|
978
1037
|
uRefrEnabled: { value: true },
|
|
979
1038
|
uBlendEnabled: { value: true },
|
|
980
1039
|
uIor: { value: 1.5 },
|
|
1040
|
+
uDispersion: { value: 0 },
|
|
981
1041
|
uLightStochastic: { value: false },
|
|
982
1042
|
uGIHalfRate: { value: false },
|
|
983
1043
|
uEnvColor: { value: new THREE.Color(0.03, 0.04, 0.06) },
|
|
@@ -1002,6 +1062,9 @@ export class RTLightingPass {
|
|
|
1002
1062
|
// Specular temporal accumulation program (its own sampler budget — well
|
|
1003
1063
|
// clear of the lighting pass's 16-sampler ceiling).
|
|
1004
1064
|
this.specMaterial = new THREE.ShaderMaterial({
|
|
1065
|
+
// Optional additive specular buffer — a link failure degrades to the
|
|
1066
|
+
// Lambert-only look (RealtimeRaytracer disables `specular`), image stays lit.
|
|
1067
|
+
name: "rt:specular",
|
|
1005
1068
|
glslVersion: THREE.GLSL3,
|
|
1006
1069
|
vertexShader: fullscreenVert,
|
|
1007
1070
|
fragmentShader: specAccumFrag,
|
|
@@ -1026,6 +1089,9 @@ export class RTLightingPass {
|
|
|
1026
1089
|
// buffers. In single-target fallback the second output collapses the same
|
|
1027
1090
|
// way as the lighting shader's.
|
|
1028
1091
|
this.carryMaterial = new THREE.ShaderMaterial({
|
|
1092
|
+
// Resize-only history-carry blit; a link failure is non-fatal (history is
|
|
1093
|
+
// not carried across a resolution step) so it classifies as auxiliary.
|
|
1094
|
+
name: "rt:history-carry",
|
|
1029
1095
|
glslVersion: THREE.GLSL3,
|
|
1030
1096
|
vertexShader: fullscreenVert,
|
|
1031
1097
|
fragmentShader: specMRT
|
|
@@ -1065,7 +1131,7 @@ export class RTLightingPass {
|
|
|
1065
1131
|
t.texture.generateMipmaps = false;
|
|
1066
1132
|
return t;
|
|
1067
1133
|
}
|
|
1068
|
-
const t =
|
|
1134
|
+
const t = makeMRT(width, height, 2, opts);
|
|
1069
1135
|
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
1070
1136
|
return t;
|
|
1071
1137
|
}
|
package/src/RealtimeRaytracer.js
CHANGED
|
@@ -9,6 +9,7 @@ import { VolumetricPass } from "./VolumetricPass.js";
|
|
|
9
9
|
import { RestirPass } from "./RestirPass.js";
|
|
10
10
|
import { GIReservoirPass } from "./GIReservoirPass.js";
|
|
11
11
|
import { CopyPass } from "./CopyPass.js";
|
|
12
|
+
import { makeMRT } from "./mrtCompat.js";
|
|
12
13
|
|
|
13
14
|
// Van der Corput / Halton radical inverse — deterministic low-discrepancy
|
|
14
15
|
// sub-pixel offsets for temporal jitter.
|
|
@@ -256,7 +257,7 @@ export class RealtimeRaytracer {
|
|
|
256
257
|
let mrt, out, mat, copy, quad, scene2, cam;
|
|
257
258
|
const prevTarget = renderer.getRenderTarget();
|
|
258
259
|
try {
|
|
259
|
-
mrt =
|
|
260
|
+
mrt = makeMRT(2, 2, 2, {
|
|
260
261
|
format: THREE.RGBAFormat,
|
|
261
262
|
type: THREE.HalfFloatType,
|
|
262
263
|
depthBuffer: false,
|
|
@@ -337,6 +338,17 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
337
338
|
// frames of confidence". See RTLightingPass.resizeCarry.
|
|
338
339
|
static HISTORY_CARRY_FRAMES = 8;
|
|
339
340
|
|
|
341
|
+
// Compile-failure diagnosis polling window (see _scanPrograms). three checks
|
|
342
|
+
// link status lazily at a program's first USE, and under
|
|
343
|
+
// KHR_parallel_shader_compile it may hold a mesh back for a few frames until
|
|
344
|
+
// its program is ready — so a single frame-1 scan can miss a failure. Poll
|
|
345
|
+
// from frame 1 up to DIAG_WINDOW_FRAMES, and early-out once the set of rt:*
|
|
346
|
+
// programs has been stable (no new programs, no unhandled failures) for
|
|
347
|
+
// DIAG_STABLE_FRAMES past a DIAG_MIN_FRAMES warmup floor.
|
|
348
|
+
static DIAG_MIN_FRAMES = 8;
|
|
349
|
+
static DIAG_STABLE_FRAMES = 4;
|
|
350
|
+
static DIAG_WINDOW_FRAMES = 45;
|
|
351
|
+
|
|
340
352
|
constructor(renderer, options = {}) {
|
|
341
353
|
this.renderer = renderer;
|
|
342
354
|
|
|
@@ -354,6 +366,12 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
354
366
|
);
|
|
355
367
|
this.compiled = null;
|
|
356
368
|
this.frame = 0;
|
|
369
|
+
// Status surface (see the supported path for the shapes). Unsupported =
|
|
370
|
+
// the RT pipeline is not operational at all; `supported` is the primary
|
|
371
|
+
// signal, but status is kept consistent so integrators can read one field.
|
|
372
|
+
this.compileError = null;
|
|
373
|
+
this.status = { ok: false, disabled: [], coreFailure: null };
|
|
374
|
+
this._diagDone = true;
|
|
357
375
|
return;
|
|
358
376
|
}
|
|
359
377
|
|
|
@@ -492,6 +510,17 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
492
510
|
this.transparency = options.transparency ?? true;
|
|
493
511
|
/** Index of refraction used for transmissive surfaces. */
|
|
494
512
|
this.ior = options.ior ?? 1.5;
|
|
513
|
+
/**
|
|
514
|
+
* Chromatic dispersion strength for glass (0..0.5, clamped on upload,
|
|
515
|
+
* default 0 = off). Splits refracted white light into a spectrum: each
|
|
516
|
+
* frame every glass pixel estimates ONE colour channel through a
|
|
517
|
+
* channel-shifted ior and the temporal accumulator blends the three into a
|
|
518
|
+
* rainbow (stochastic spectral sampling — no extra rays). It shimmers
|
|
519
|
+
* slightly while converging, so it needs temporal accumulation to settle.
|
|
520
|
+
* Global control only for now (there is no free G-buffer channel for a
|
|
521
|
+
* per-material MeshPhysicalMaterial.dispersion).
|
|
522
|
+
*/
|
|
523
|
+
this.dispersion = options.dispersion ?? 0;
|
|
495
524
|
/**
|
|
496
525
|
* One stochastic direct shadow ray per pixel per frame (source picked at
|
|
497
526
|
* random) instead of one per light — the biggest ray-count lever for
|
|
@@ -610,6 +639,29 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
610
639
|
this.restirGI = options.restirGI ?? false;
|
|
611
640
|
/** Temporal M-cap for the ReSTIR GI reservoir (staleness limit). */
|
|
612
641
|
this.restirGIMCap = options.restirGIMCap ?? 20;
|
|
642
|
+
/**
|
|
643
|
+
* ReSTIR GI (v2) spatial-reuse taps per frame, taken after the temporal
|
|
644
|
+
* merge from the previous frame's reservoirs (reconnection-Jacobian
|
|
645
|
+
* reweighted, with a final visibility ray). Clamped to 0..4; `0` reproduces
|
|
646
|
+
* the v1 temporal-only behaviour. Default 2.
|
|
647
|
+
*/
|
|
648
|
+
// Default 1 (not 2): each reconnection tap adds its own estimator noise;
|
|
649
|
+
// one tap keeps most of the disocclusion win at half the artifact surface
|
|
650
|
+
// (tuned on-device — raise it for scenes with heavy camera motion).
|
|
651
|
+
this.restirGISpatialTaps = options.restirGISpatialTaps ?? 1;
|
|
652
|
+
/**
|
|
653
|
+
* EXPERIMENTAL — ReSTIR GI reservoir-sample validation period. Every frame a
|
|
654
|
+
* rotating 1-in-N subset of pixels (decorrelated by a per-pixel hash) re-aims
|
|
655
|
+
* its ONE candidate ray at the reservoir's STORED hit instead of a fresh
|
|
656
|
+
* cosine bounce and re-shades it; the reservoir is killed (so fresh candidates
|
|
657
|
+
* rebuild) when the geometry moved or the re-shaded target collapsed to
|
|
658
|
+
* near-black (a light switched off), and left untouched otherwise. This reuses
|
|
659
|
+
* the existing candidate trace (no extra bounce rays) and is the fix for stale
|
|
660
|
+
* bounce light: a switched-off light stops haunting the reservoir instead of
|
|
661
|
+
* fading slowly, while a static scene does not drift. `0` disables it
|
|
662
|
+
* (byte-identical to before the feature); default 8.
|
|
663
|
+
*/
|
|
664
|
+
this.restirGIValidate = options.restirGIValidate ?? 8;
|
|
613
665
|
this.giReservoirPass = new GIReservoirPass(this._scaledW, this._scaledH);
|
|
614
666
|
this._giMissWarned = false;
|
|
615
667
|
|
|
@@ -656,6 +708,150 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
656
708
|
);
|
|
657
709
|
this._renderScale = 0.375;
|
|
658
710
|
}
|
|
711
|
+
|
|
712
|
+
// ---- compile-failure status surface -----------------------------------
|
|
713
|
+
// The pipeline is a stack of ShaderMaterial passes; a program that fails to
|
|
714
|
+
// LINK renders black with no exception (three logs to the console and sets
|
|
715
|
+
// program.diagnostics.runnable=false, but rendering proceeds). Before this,
|
|
716
|
+
// a broken pass looked identical to `supported:false` from the outside — the
|
|
717
|
+
// failure that shipped the r166+ black image. These two fields let an
|
|
718
|
+
// integrator render honestly ("raster (reason)") instead of guessing:
|
|
719
|
+
//
|
|
720
|
+
// compileError : string | null
|
|
721
|
+
// First/most-severe failure summary ("rt:lighting: <driver log>"), or
|
|
722
|
+
// null while every rt:* pass is compiling clean.
|
|
723
|
+
// status : { ok, disabled, coreFailure }
|
|
724
|
+
// ok false once ANY rt:* pass failed to link (a core pass, or
|
|
725
|
+
// a feature that was auto-disabled). true = pipeline is
|
|
726
|
+
// running as intended.
|
|
727
|
+
// disabled [{ pass, feature, reason }] — optional features turned
|
|
728
|
+
// off to keep the image lit (e.g. taa, denoise, restir).
|
|
729
|
+
// coreFailure string | null — a core pass (gbuffer/lighting/composite)
|
|
730
|
+
// failed and has no fallback; the image is black-but-diagnosed.
|
|
731
|
+
this.compileError = null;
|
|
732
|
+
this.status = { ok: true, disabled: [], coreFailure: null };
|
|
733
|
+
this._diagDone = false; // set once the polling window settles
|
|
734
|
+
this._diagFrames = 0; // rendered frames scanned so far
|
|
735
|
+
this._diagStable = 0; // consecutive scans with an unchanged rt:* program set
|
|
736
|
+
this._diagSig = ""; // signature of the rt:* program-name set last scan
|
|
737
|
+
this._diagHandled = new Set(); // rt:* names already acted on (warn-once)
|
|
738
|
+
this._compileErrSev = -1; // severity behind the current compileError (2/1/0)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Classify an rt:* pass program by how a LINK failure degrades. CORE passes
|
|
742
|
+
// have no fallback (record and keep rendering the black result so it is
|
|
743
|
+
// DIAGNOSED, not silent). Optional passes map to the EXACT runtime toggle that
|
|
744
|
+
// already gates them, so disabling one keeps the image lit. Unknown rt:* names
|
|
745
|
+
// (history-carry blits) are auxiliary: non-fatal, warn only. Returns one of
|
|
746
|
+
// { core:true } | { feature, disable } | { aux:true }.
|
|
747
|
+
_passClass(name) {
|
|
748
|
+
switch (name) {
|
|
749
|
+
case "rt:gbuffer":
|
|
750
|
+
case "rt:lighting":
|
|
751
|
+
case "rt:composite":
|
|
752
|
+
return { core: true };
|
|
753
|
+
case "rt:restir-temporal":
|
|
754
|
+
case "rt:restir-spatial":
|
|
755
|
+
return { feature: "restir", disable: () => { this.restir = false; } };
|
|
756
|
+
case "rt:gi-reservoir":
|
|
757
|
+
return { feature: "restirGI", disable: () => { this.restirGI = false; } };
|
|
758
|
+
case "rt:denoise":
|
|
759
|
+
return { feature: "denoise", disable: () => { this.denoise = false; } };
|
|
760
|
+
case "rt:volumetric":
|
|
761
|
+
return { feature: "volumetric", disable: () => { this.volumetric.enabled = false; } };
|
|
762
|
+
case "rt:taa":
|
|
763
|
+
case "rt:taa-copy":
|
|
764
|
+
return { feature: "taa", disable: () => { this.taa = false; } };
|
|
765
|
+
case "rt:specular":
|
|
766
|
+
return { feature: "specular", disable: () => { this.specular = false; } };
|
|
767
|
+
default:
|
|
768
|
+
return { aux: true };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Compact one-line driver message from three's program.diagnostics. The
|
|
773
|
+
// GLSL-frontend error lives in the fragment (or vertex) shader log; programLog
|
|
774
|
+
// is the linker fallback. First line, capped, so it fits a console.warn / a UI.
|
|
775
|
+
_diagLog(diag) {
|
|
776
|
+
const pick = [
|
|
777
|
+
diag && diag.fragmentShader && diag.fragmentShader.log,
|
|
778
|
+
diag && diag.vertexShader && diag.vertexShader.log,
|
|
779
|
+
diag && diag.programLog,
|
|
780
|
+
].find((s) => s && s.trim());
|
|
781
|
+
return (pick || "(no driver log)").trim().split("\n")[0].slice(0, 200);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Keep compileError at the FIRST failure of the HIGHEST severity seen
|
|
785
|
+
// (core 2 > feature 1 > aux 0): a later core failure overrides an earlier
|
|
786
|
+
// feature summary, but two failures of equal severity keep the first.
|
|
787
|
+
_noteCompileError(summary, severity) {
|
|
788
|
+
if (severity > this._compileErrSev) {
|
|
789
|
+
this.compileError = summary;
|
|
790
|
+
this._compileErrSev = severity;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Act on one failed rt:* program (called at most once per pass name).
|
|
795
|
+
_handleFailedProgram(name, diag) {
|
|
796
|
+
const log = this._diagLog(diag);
|
|
797
|
+
const cls = this._passClass(name);
|
|
798
|
+
const summary = `${name}: ${log}`;
|
|
799
|
+
this.status.ok = false;
|
|
800
|
+
if (cls.core) {
|
|
801
|
+
if (!this.status.coreFailure) this.status.coreFailure = summary;
|
|
802
|
+
this._noteCompileError(summary, 2);
|
|
803
|
+
console.warn(
|
|
804
|
+
`three-realtime-rt: core pass ${name} failed to link — the image will ` +
|
|
805
|
+
`be black (no fallback for a core pass). Driver log: ${log}`
|
|
806
|
+
);
|
|
807
|
+
} else if (cls.feature) {
|
|
808
|
+
cls.disable();
|
|
809
|
+
this.status.disabled.push({ pass: name, feature: cls.feature, reason: log });
|
|
810
|
+
this._noteCompileError(summary, 1);
|
|
811
|
+
console.warn(
|
|
812
|
+
`three-realtime-rt: pass ${name} failed to link — auto-disabled ` +
|
|
813
|
+
`"${cls.feature}" to keep the image lit. Driver log: ${log}`
|
|
814
|
+
);
|
|
815
|
+
} else {
|
|
816
|
+
this._noteCompileError(summary, 0);
|
|
817
|
+
console.warn(
|
|
818
|
+
`three-realtime-rt: auxiliary pass ${name} failed to link (non-fatal — ` +
|
|
819
|
+
`resize history is not carried). Driver log: ${log}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Scan renderer.info.programs for failed rt:* pass programs. Called each frame
|
|
825
|
+
// until the window settles (see the DIAG_* constants). Cheap: ~a dozen entries,
|
|
826
|
+
// string prefix check, warn-once via _diagHandled.
|
|
827
|
+
_scanPrograms() {
|
|
828
|
+
if (this._diagDone) return;
|
|
829
|
+
const programs = this.renderer.info && this.renderer.info.programs;
|
|
830
|
+
if (!programs) { this._diagDone = true; return; }
|
|
831
|
+
this._diagFrames++;
|
|
832
|
+
let names = "";
|
|
833
|
+
for (const p of programs) {
|
|
834
|
+
const name = p && p.name;
|
|
835
|
+
if (!name || name.slice(0, 3) !== "rt:") continue;
|
|
836
|
+
names += name + "|";
|
|
837
|
+
const diag = p.diagnostics; // set at first USE; runnable:false = link failed
|
|
838
|
+
if (diag && diag.runnable === false && !this._diagHandled.has(name)) {
|
|
839
|
+
this._diagHandled.add(name);
|
|
840
|
+
this._handleFailedProgram(name, diag);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Early-out: the rt:* program set has stopped growing and nothing new failed
|
|
844
|
+
// for DIAG_STABLE_FRAMES past the warmup floor → every active pass has been
|
|
845
|
+
// seen and validated. Otherwise stop at the hard window.
|
|
846
|
+
if (names === this._diagSig) this._diagStable++;
|
|
847
|
+
else { this._diagStable = 0; this._diagSig = names; }
|
|
848
|
+
if (
|
|
849
|
+
(this._diagFrames >= RealtimeRaytracer.DIAG_MIN_FRAMES &&
|
|
850
|
+
this._diagStable >= RealtimeRaytracer.DIAG_STABLE_FRAMES) ||
|
|
851
|
+
this._diagFrames >= RealtimeRaytracer.DIAG_WINDOW_FRAMES
|
|
852
|
+
) {
|
|
853
|
+
this._diagDone = true;
|
|
854
|
+
}
|
|
659
855
|
}
|
|
660
856
|
|
|
661
857
|
// Consecutive catastrophic frames mean the GPU is drowning — cut quality
|
|
@@ -714,8 +910,31 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
714
910
|
*/
|
|
715
911
|
compileScene(scene, options) {
|
|
716
912
|
if (!this.supported) return null;
|
|
913
|
+
// "Construct the tracer, then add meshes" is a natural call order, so a
|
|
914
|
+
// scene with no traceable meshes is a NO-OP, not a throw: warn once and keep
|
|
915
|
+
// any previously compiled scene. Compile the new scene BEFORE disposing the
|
|
916
|
+
// old one so an empty-scene call never destroys a good scene. Only the
|
|
917
|
+
// SceneCompiler's specific "no meshes" signal is swallowed here; every other
|
|
918
|
+
// compile error (bad geometry, oversized attribute) still propagates.
|
|
919
|
+
let compiled;
|
|
920
|
+
try {
|
|
921
|
+
compiled = compileScene(scene, options);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
if (/no meshes found/.test(String(err && err.message))) {
|
|
924
|
+
if (!this._emptyWarned) {
|
|
925
|
+
console.warn(
|
|
926
|
+
"three-realtime-rt: compileScene() called on a scene with no traceable " +
|
|
927
|
+
"meshes — keeping the current scene. Until meshes are added and " +
|
|
928
|
+
"recompiled, render() falls back to plain rasterization (no crash, no black)."
|
|
929
|
+
);
|
|
930
|
+
this._emptyWarned = true;
|
|
931
|
+
}
|
|
932
|
+
return this.compiled; // unchanged (may still be null)
|
|
933
|
+
}
|
|
934
|
+
throw err;
|
|
935
|
+
}
|
|
717
936
|
if (this.compiled) this.compiled.dispose();
|
|
718
|
-
this.compiled =
|
|
937
|
+
this.compiled = compiled;
|
|
719
938
|
// Emissive area lights are the noisiest direct-light path: one triangle
|
|
720
939
|
// sample per pixel per frame, and the 1/dist^2 term spikes near a small
|
|
721
940
|
// emitter (fireflies). ReSTIR's reservoirs are what tame this — warn when
|
|
@@ -985,6 +1204,13 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
985
1204
|
if (this.adaptiveQuality) this._adaptQuality();
|
|
986
1205
|
if (this.overloadProtection) this._overloadBrake();
|
|
987
1206
|
if (!this.compiled) this.compileScene(scene);
|
|
1207
|
+
// Still nothing to trace (empty scene — tracer built before meshes were
|
|
1208
|
+
// added). Show the user's raster scene rather than crashing or rendering
|
|
1209
|
+
// black; the pipeline picks up automatically once compileScene() succeeds.
|
|
1210
|
+
if (!this.compiled) {
|
|
1211
|
+
this.renderer.render(scene, camera);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
988
1214
|
|
|
989
1215
|
this.frame += 1;
|
|
990
1216
|
camera.updateMatrixWorld();
|
|
@@ -1084,6 +1310,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1084
1310
|
rtU.uRefrEnabled.value = this.refraction;
|
|
1085
1311
|
rtU.uBlendEnabled.value = this.transparency;
|
|
1086
1312
|
rtU.uIor.value = this.ior;
|
|
1313
|
+
rtU.uDispersion.value = Math.min(0.5, Math.max(0, this.dispersion));
|
|
1087
1314
|
rtU.uLightStochastic.value = this.stochasticLights;
|
|
1088
1315
|
rtU.uSkyEnabled.value = this.sky.enabled;
|
|
1089
1316
|
rtU.uSunDir.value.copy(this.sky.sunDir);
|
|
@@ -1128,6 +1355,8 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1128
1355
|
{
|
|
1129
1356
|
fireflyClamp: this.fireflyClamp > 0 ? this.fireflyClamp : 1e6,
|
|
1130
1357
|
mCap: this.restirGIMCap,
|
|
1358
|
+
spatialTaps: Math.max(0, Math.min(4, this.restirGISpatialTaps | 0)),
|
|
1359
|
+
validateInterval: Math.max(0, this.restirGIValidate | 0),
|
|
1131
1360
|
emissiveCDF: this.emissiveImportance,
|
|
1132
1361
|
envColor: this.envColor,
|
|
1133
1362
|
envIntensity: this.envIntensity,
|
|
@@ -1266,6 +1495,12 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1266
1495
|
// Record this frame's (jittered) view-projection + jitter for next frame.
|
|
1267
1496
|
this._prevViewProj.copy(this._jitteredViewProj);
|
|
1268
1497
|
this._prevJitterUv.copy(this._jitterUv);
|
|
1498
|
+
|
|
1499
|
+
// Compile-failure diagnosis: every pass program used this frame has now had
|
|
1500
|
+
// its link status checked by three (diagnostics populated on first use), so
|
|
1501
|
+
// scan for failures. Runs at frame END (downstream of the passes) and only
|
|
1502
|
+
// until the polling window settles — a no-op on the healthy steady state.
|
|
1503
|
+
if (!this._diagDone) this._scanPrograms();
|
|
1269
1504
|
}
|
|
1270
1505
|
|
|
1271
1506
|
dispose() {
|
package/src/RestirPass.js
CHANGED
|
@@ -52,7 +52,10 @@ vec4 fetchBlueNoise() {
|
|
|
52
52
|
return fract(bn + shift);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// Named rtLum, NOT luminance: three r166+ prepends its own luminance(vec3)
|
|
56
|
+
// to every non-raw ShaderMaterial fragment shader, and GLSL treats a second
|
|
57
|
+
// (vec3) body as a redefinition — the whole program fails to compile.
|
|
58
|
+
float rtLum(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
|
|
56
59
|
|
|
57
60
|
// Primary-surface roughness, set per pixel in main(). Drives the cheap specular
|
|
58
61
|
// lobe below so reservoirs favour lights that land on a highlight.
|
|
@@ -121,7 +124,7 @@ vec3 candidateContribution(float id, vec2 uv, vec3 P, vec3 N) {
|
|
|
121
124
|
// (Known approximation: a triangle whose centroid contributes zero but whose
|
|
122
125
|
// far corner doesn't can be under-selected at grazing setups.)
|
|
123
126
|
float phatOf(float id, vec3 P, vec3 N) {
|
|
124
|
-
return
|
|
127
|
+
return rtLum(candidateContribution(id, vec2(1.0 / 3.0), P, N));
|
|
125
128
|
}
|
|
126
129
|
`;
|
|
127
130
|
|
|
@@ -297,8 +300,12 @@ export class RestirPass {
|
|
|
297
300
|
this.targetB = this._makeTarget(width, height);
|
|
298
301
|
this.spatialTarget = this._makeTarget(width, height);
|
|
299
302
|
|
|
300
|
-
const mkMaterial = (frag) =>
|
|
303
|
+
const mkMaterial = (frag, name) =>
|
|
301
304
|
new THREE.ShaderMaterial({
|
|
305
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
306
|
+
// in either reservoir stage disables `restir` (falls back to the
|
|
307
|
+
// non-reservoir per-light direct sampling path — image stays lit).
|
|
308
|
+
name,
|
|
302
309
|
glslVersion: THREE.GLSL3,
|
|
303
310
|
vertexShader: fullscreenVert,
|
|
304
311
|
fragmentShader: frag,
|
|
@@ -329,8 +336,8 @@ export class RestirPass {
|
|
|
329
336
|
depthWrite: false,
|
|
330
337
|
});
|
|
331
338
|
|
|
332
|
-
this.material = mkMaterial(temporalFrag);
|
|
333
|
-
this.spatialMaterial = mkMaterial(spatialFrag);
|
|
339
|
+
this.material = mkMaterial(temporalFrag, "rt:restir-temporal");
|
|
340
|
+
this.spatialMaterial = mkMaterial(spatialFrag, "rt:restir-spatial");
|
|
334
341
|
|
|
335
342
|
this.scene = new THREE.Scene();
|
|
336
343
|
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|