three-realtime-rt 0.6.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 +55 -7
- package/package.json +2 -1
- package/src/CompositePass.js +3 -0
- package/src/CopyPass.js +3 -0
- package/src/DenoisePass.js +11 -5
- package/src/GBufferPass.js +6 -1
- package/src/GIReservoirPass.js +19 -12
- package/src/RTLightingPass.js +12 -1
- package/src/RealtimeRaytracer.js +200 -2
- package/src/RestirPass.js +12 -5
- package/src/TAAPass.js +6 -0
- package/src/VolumetricPass.js +3 -0
- package/src/index.d.ts +55 -0
- package/src/mrtCompat.js +37 -0
package/README.md
CHANGED
|
@@ -134,7 +134,11 @@ A checklist for dropping the tracer into a scene you already have:
|
|
|
134
134
|
2. **Compile once; recompile after structural changes** — `rt.compileScene(scene)`
|
|
135
135
|
bakes geometry into a static BVH and snapshots materials + emissive area
|
|
136
136
|
lights. Call it again after you add/remove meshes, swap geometry, or change a
|
|
137
|
-
material's `emissive` / `color` / `roughness` / `metalness`.
|
|
137
|
+
material's `emissive` / `color` / `roughness` / `metalness`. <a id="empty-scene"></a>Calling it on a
|
|
138
|
+
scene with **no meshes yet** is a **no-op** (it warns once and keeps any
|
|
139
|
+
previously compiled scene), so "construct the tracer, then add meshes" is a
|
|
140
|
+
valid order — until meshes are added and recompiled, `rt.render()` falls back
|
|
141
|
+
to plain rasterization (no crash, no black screen).
|
|
138
142
|
3. **Declare movers** — pass moving meshes to
|
|
139
143
|
`rt.compileScene(scene, { dynamicMeshes: [...] })`, then call
|
|
140
144
|
`rt.updateDynamic()` each frame after you move them (e.g. after a physics
|
|
@@ -610,8 +614,37 @@ is **feature-detected** — it appears only when the loaded build exposes an
|
|
|
610
614
|
The renderer can pass every compile and framebuffer check and still draw a black
|
|
611
615
|
screen — that is exactly what shipped in 0.4.0 on iOS (WebKit's GLSL-to-Metal
|
|
612
616
|
translation silently broke at a 4th `traceRadiance` call site; clean compile, no
|
|
613
|
-
console error, black output)
|
|
614
|
-
|
|
617
|
+
console error, black output), and again on three r166+ when three's injected
|
|
618
|
+
`luminance` helper collided with the library's own and the affected pass programs
|
|
619
|
+
failed to link. The only defence against that class of failure is to **look at
|
|
620
|
+
the pixels**, so the demo has a headless-friendly self-test.
|
|
621
|
+
|
|
622
|
+
**Programmatic signal (`rt.compileError` / `rt.status`).** A pass whose program
|
|
623
|
+
fails to *link* renders black without throwing — three logs to the console and
|
|
624
|
+
sets `program.diagnostics.runnable = false`, but rendering proceeds. So over the
|
|
625
|
+
first several rendered frames the renderer inspects `renderer.info.programs` for
|
|
626
|
+
its own (stably named `rt:*`) pass programs and reports what it finds:
|
|
627
|
+
|
|
628
|
+
```js
|
|
629
|
+
rt.render(scene, camera); // ...for a few frames
|
|
630
|
+
if (!rt.status.ok) {
|
|
631
|
+
// rt.compileError → "rt:lighting: 'luminance' : function already has a body"
|
|
632
|
+
if (rt.status.coreFailure) showRaster(`raster (${rt.compileError})`);
|
|
633
|
+
else console.warn("degraded:", rt.status.disabled); // e.g. [{pass, feature, reason}]
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
- **`rt.compileError`** (`string | null`) — first / most-severe failure summary
|
|
638
|
+
(`"rt:<pass>: <driver log>"`), or `null` while every pass compiles clean.
|
|
639
|
+
- **`rt.status`** (`{ ok, disabled, coreFailure }`) — `ok` is `true` on the healthy
|
|
640
|
+
path and `false` once any `rt:*` pass fails to link. **Optional** features whose
|
|
641
|
+
pass failed are **auto-disabled** so the image stays lit (`restir`, `restirGI`,
|
|
642
|
+
`denoise`, `volumetric`, `taa`, `specular`), each listed in `disabled` as
|
|
643
|
+
`{ pass, feature, reason }` with a one-line driver log. A **core** pass
|
|
644
|
+
(`gbuffer` / `lighting` / `composite`) has no fallback: `coreFailure` names it
|
|
645
|
+
and the image is black-but-diagnosed. This lets an integrator render an honest
|
|
646
|
+
`raster (reason)` fallback instead of guessing. (When `supported` is `false` the
|
|
647
|
+
RT pipeline never runs, so `status.ok` is `false` too — check `supported` first.)
|
|
615
648
|
|
|
616
649
|
**In the browser:** load [`/?selftest=1`](examples/selftest.js). It forces the
|
|
617
650
|
full lighting stack on (GI + emissive NEE + reflections + refraction, lighting at
|
|
@@ -621,12 +654,16 @@ and into a hidden `#selftest-verdict` DOM node:
|
|
|
621
654
|
|
|
622
655
|
```json
|
|
623
656
|
{ "pass": true, "meanLum": 139.08, "irrLum": 169.73, "glErrors": 0,
|
|
624
|
-
"specMRT": true, "supported": true, "
|
|
657
|
+
"specMRT": true, "supported": true, "statusOk": true, "rtPrograms": 15,
|
|
658
|
+
"compileError": null, "disabled": [], "frames": 91, "ua": "…" }
|
|
625
659
|
```
|
|
626
660
|
|
|
627
661
|
The pass gate wants `meanLum` in `[12, 230]` (calibrated: a healthy composite of
|
|
628
662
|
the gallery centre reads ~140 on desktop; a black screen reads ~0), `irrLum > 6`,
|
|
629
|
-
`glErrors == 0` and `
|
|
663
|
+
`glErrors == 0`, `supported == true`, and `statusOk == true` — the last asserts
|
|
664
|
+
the compile-failure surface above stayed clean (`rt.status.ok`, `compileError`
|
|
665
|
+
null) *and* that the named pass programs were actually discoverable
|
|
666
|
+
(`rtPrograms > 0`), so a broken diagnosis can't read as a false pass.
|
|
630
667
|
|
|
631
668
|
- `meanLum` / `irrLum` — mean Rec.709 luma (0–255) of the **centre 25%** of the
|
|
632
669
|
composite, and of the raw irradiance buffer (`outputMode 3`) for one frame. The
|
|
@@ -641,12 +678,23 @@ the renderer with `preserveDrawingBuffer: true` so the canvas can be read back;
|
|
|
641
678
|
normal runs keep the cheaper default.
|
|
642
679
|
|
|
643
680
|
**In CI:** `npm run test:render` ([`scripts/selftest.mjs`](scripts/selftest.mjs))
|
|
644
|
-
starts vite on
|
|
681
|
+
starts vite on free ports and drives `?selftest=1` through Playwright across
|
|
645
682
|
**chromium, firefox and webkit**, printing a pass/fail/skip table and exiting
|
|
646
683
|
nonzero on any real failure (a documented environmental *skip* does not fail the
|
|
647
684
|
suite). Playwright is loaded from a sibling checkout — see the top of the script.
|
|
648
685
|
|
|
649
|
-
|
|
686
|
+
**Three-version matrix.** The r166+ `luminance` break shipped because nothing
|
|
687
|
+
tested a newer three, so **chromium runs twice**: once against the pinned three
|
|
688
|
+
(`0.160.1`) and once against **`three@latest`** (a second vite with
|
|
689
|
+
`RT_THREE=latest`, which [`vite.config.js`](vite.config.js) aliases `three` to the
|
|
690
|
+
`three-latest` devDependency). The `chromium@3latest` row is the guard for that
|
|
691
|
+
class of regression, and **the pass gate requires both chromium legs**. A fourth
|
|
692
|
+
chromium load of `?selftest=empty` asserts the [empty-scene no-op](#empty-scene)
|
|
693
|
+
(`compileScene` on a scene with no meshes is a no-op and `render()` falls back to
|
|
694
|
+
plain raster instead of crashing). firefox/webkit stay single-leg against the
|
|
695
|
+
default three (both are environmental skips here — see below).
|
|
696
|
+
|
|
697
|
+
Machine-specific note (Windows + NVIDIA, the current dev box): each chromium leg
|
|
650
698
|
runs **headed** with **`--use-angle=gl`**. ANGLE's default D3D11/FXC backend
|
|
651
699
|
never finishes compiling the BVH megakernel here — headless chromium,
|
|
652
700
|
headed+`--use-angle=d3d11` and system Chrome all freeze at ~3 frames with silent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-realtime-rt",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Turn-on ray traced lighting for three.js: a hybrid deferred renderer with BVH-traced soft shadows, one-bounce global illumination, temporal reprojection, an a-trous denoiser and TAA.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"@dimforge/rapier3d-compat": "^0.14.0",
|
|
48
48
|
"gh-pages": "^6.3.0",
|
|
49
49
|
"three": "^0.160.1",
|
|
50
|
+
"three-latest": "npm:three@latest",
|
|
50
51
|
"three-mesh-bvh": "^0.7.8",
|
|
51
52
|
"vite": "^5.0.11"
|
|
52
53
|
}
|
package/src/CompositePass.js
CHANGED
|
@@ -195,6 +195,9 @@ void main() {
|
|
|
195
195
|
export class CompositePass {
|
|
196
196
|
constructor() {
|
|
197
197
|
this.material = new THREE.ShaderMaterial({
|
|
198
|
+
// Stable program name for compile-failure self-diagnosis: this is a CORE
|
|
199
|
+
// pass (the final tonemap/upsample) — a link failure has no fallback.
|
|
200
|
+
name: "rt:composite",
|
|
198
201
|
glslVersion: THREE.GLSL3,
|
|
199
202
|
vertexShader: fullscreenVert,
|
|
200
203
|
fragmentShader: compositeFrag,
|
package/src/CopyPass.js
CHANGED
|
@@ -37,6 +37,9 @@ void main() {
|
|
|
37
37
|
export class CopyPass {
|
|
38
38
|
constructor() {
|
|
39
39
|
this.material = new THREE.ShaderMaterial({
|
|
40
|
+
// Generic fullscreen history-carry blit (resize only); a link failure is
|
|
41
|
+
// non-fatal, so it classifies as auxiliary in the self-diagnosis.
|
|
42
|
+
name: "rt:history-carry",
|
|
40
43
|
glslVersion: THREE.GLSL3,
|
|
41
44
|
vertexShader: fullscreenVert,
|
|
42
45
|
fragmentShader: copyFrag,
|
package/src/DenoisePass.js
CHANGED
|
@@ -35,7 +35,10 @@ uniform bool uBlendIsSpec; // this instance filters the specular buffer
|
|
|
35
35
|
uniform sampler2D uAddTex;
|
|
36
36
|
uniform bool uHasAdd;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Named rtLum, NOT luminance: three r166+ prepends its own luminance(vec3)
|
|
39
|
+
// to every non-raw ShaderMaterial fragment shader, and GLSL treats a second
|
|
40
|
+
// (vec3) body as a redefinition — the whole program fails to compile.
|
|
41
|
+
float rtLum(vec3 c) {
|
|
39
42
|
return dot(c, vec3(0.299, 0.587, 0.114));
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -123,16 +126,16 @@ void main() {
|
|
|
123
126
|
vec2 tuv = vUv + vec2(float(dx), float(dy)) * uTexelSize;
|
|
124
127
|
if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) continue;
|
|
125
128
|
if (texture(uGWorldPos, tuv).w < 0.5) continue;
|
|
126
|
-
maxL = max(maxL,
|
|
129
|
+
maxL = max(maxL, rtLum(sampleIrr(tuv).rgb));
|
|
127
130
|
found = 1.0;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
float cap = maxL * 1.25 + 1e-4;
|
|
131
|
-
float l =
|
|
134
|
+
float l = rtLum(center.rgb);
|
|
132
135
|
if (found > 0.5 && l > cap) center.rgb *= cap / l;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
float lumC =
|
|
138
|
+
float lumC = rtLum(center.rgb);
|
|
136
139
|
|
|
137
140
|
// 3x3 B-spline-ish kernel, edge-avoiding weights.
|
|
138
141
|
vec3 sum = center.rgb * 4.0;
|
|
@@ -155,7 +158,7 @@ void main() {
|
|
|
155
158
|
// flat floor has no geometric edge to protect it, so at high iteration
|
|
156
159
|
// counts the wide passes would average it away ("floating" objects with
|
|
157
160
|
// no contact shadow). Wide steps only get to blend near-equal luminance.
|
|
158
|
-
float wL = exp(-abs(
|
|
161
|
+
float wL = exp(-abs(rtLum(s.rgb) - lumC) / (sigmaL * inversesqrt(uStep)));
|
|
159
162
|
float w = k * wN * wZ * wL * (1.0 - specKeep);
|
|
160
163
|
sum += s.rgb * w;
|
|
161
164
|
wsum += w;
|
|
@@ -182,6 +185,9 @@ export class DenoisePass {
|
|
|
182
185
|
this.targetB = this._makeTarget(width, height);
|
|
183
186
|
|
|
184
187
|
this.material = new THREE.ShaderMaterial({
|
|
188
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
189
|
+
// disables the optional `denoise` feature (image stays lit, just noisier).
|
|
190
|
+
name: "rt:denoise",
|
|
185
191
|
glslVersion: THREE.GLSL3,
|
|
186
192
|
vertexShader: fullscreenVert,
|
|
187
193
|
fragmentShader: atrousFrag,
|
package/src/GBufferPass.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
+
import { makeMRT } from "./mrtCompat.js";
|
|
2
3
|
|
|
3
4
|
// three defines USE_SKINNING and supplies the bindMatrix / bindMatrixInverse /
|
|
4
5
|
// boneTexture uniforms + skinIndex / skinWeight attributes automatically when the
|
|
@@ -190,7 +191,7 @@ export class GBufferPass {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
_makeTarget(width, height) {
|
|
193
|
-
const t =
|
|
194
|
+
const t = makeMRT(width, height, 4, {
|
|
194
195
|
minFilter: THREE.NearestFilter,
|
|
195
196
|
magFilter: THREE.NearestFilter,
|
|
196
197
|
type: THREE.FloatType,
|
|
@@ -240,6 +241,10 @@ export class GBufferPass {
|
|
|
240
241
|
|
|
241
242
|
_makeGbufferMaterial(mesh) {
|
|
242
243
|
const material = new THREE.ShaderMaterial({
|
|
244
|
+
// Stable program name for the compile-failure self-diagnosis (see
|
|
245
|
+
// RealtimeRaytracer._scanPrograms). Per-mesh materials share one program
|
|
246
|
+
// cache key, so they all surface as the single core pass "rt:gbuffer".
|
|
247
|
+
name: "rt:gbuffer",
|
|
243
248
|
glslVersion: THREE.GLSL3,
|
|
244
249
|
vertexShader: gbufferVert,
|
|
245
250
|
fragmentShader: gbufferFrag,
|
package/src/GIReservoirPass.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;
|
|
@@ -133,7 +134,10 @@ vec4 fetchBlueNoise() {
|
|
|
133
134
|
return fract(bn + shift);
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
// Named rtLum, NOT luminance: three r166+ prepends its own rtLum(vec3)
|
|
138
|
+
// to every non-raw ShaderMaterial fragment shader, and GLSL treats a second
|
|
139
|
+
// (vec3) body as a redefinition — the whole program fails to compile.
|
|
140
|
+
float rtLum(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
|
|
137
141
|
|
|
138
142
|
// ---------- reservoir .w bit-packing: M (8 bit) + oct-normal (12+12 bit) ------
|
|
139
143
|
// The RGBA32F reservoir-position attachment is at the pass's hard 16-sampler
|
|
@@ -483,7 +487,7 @@ void main() {
|
|
|
483
487
|
vec3 rad = traceRadianceGI(P + N * uEps, wi, nLight, hitPos, hitNormal);
|
|
484
488
|
// Match the inline firefly clamp, which is applied to indirect (= L_i) so
|
|
485
489
|
// the biased mean of the two paths agrees.
|
|
486
|
-
float rl =
|
|
490
|
+
float rl = rtLum(rad);
|
|
487
491
|
if (rl > uFireflyClamp) rad *= uFireflyClamp / rl;
|
|
488
492
|
|
|
489
493
|
float cosT = max(dot(N, wi), 0.0);
|
|
@@ -532,8 +536,8 @@ void main() {
|
|
|
532
536
|
float valTol = max(0.02 * expectDist, 4.0 * uEps);
|
|
533
537
|
float hitDist = length(hitPos - P);
|
|
534
538
|
bool geomChanged = abs(hitDist - expectDist) > valTol;
|
|
535
|
-
float pHatOld =
|
|
536
|
-
float pHatNew =
|
|
539
|
+
float pHatOld = rtLum(radPrev) * cosT; // stored target at this pixel
|
|
540
|
+
float pHatNew = rtLum(rad) * cosT; // re-shaded target (current light)
|
|
537
541
|
bool wentDark = pHatNew < VAL_DARK_FRAC * pHatOld;
|
|
538
542
|
// KILL (drop the stale temporal term so this pixel's fresh candidates rebuild
|
|
539
543
|
// from the current scene) on geometry change OR a collapse to near-black; leave
|
|
@@ -543,7 +547,7 @@ void main() {
|
|
|
543
547
|
killStore = geomChanged || wentDark;
|
|
544
548
|
} else {
|
|
545
549
|
// --- normal fresh candidate: one cosine-hemisphere GI bounce, shaded inline.
|
|
546
|
-
float pHatFresh =
|
|
550
|
+
float pHatFresh = rtLum(rad) * cosT;
|
|
547
551
|
// w = p_hat / p_source = p_hat / (cos/PI). cosT cancels; guard cosT==0.
|
|
548
552
|
float wFresh = cosT > 0.0 ? pHatFresh * PI / cosT : 0.0;
|
|
549
553
|
wSum = wFresh;
|
|
@@ -571,7 +575,7 @@ void main() {
|
|
|
571
575
|
vec3 dp = hitPrev - P;
|
|
572
576
|
float dl = length(dp);
|
|
573
577
|
float cosPrev = dl > 1e-5 ? max(dot(N, dp / dl), 0.0) : 0.0;
|
|
574
|
-
float pHatPrev =
|
|
578
|
+
float pHatPrev = rtLum(radPrev) * cosPrev;
|
|
575
579
|
float Mc = min(Mprev, uMCap);
|
|
576
580
|
// Combine reservoirs: w = p_hat_current(sample) * W_prev * M_prev.
|
|
577
581
|
float w = pHatPrev * Wprev * Mc;
|
|
@@ -585,7 +589,7 @@ void main() {
|
|
|
585
589
|
// Reconstruct last frame's resolve from this same sample (same W cap
|
|
586
590
|
// and clamp as the live resolve, for a like-for-like EMA partner).
|
|
587
591
|
vec3 pg = radPrev * (cosPrev / PI) * min(Wprev, 32.0);
|
|
588
|
-
float pgl =
|
|
592
|
+
float pgl = rtLum(pg);
|
|
589
593
|
if (pgl > uFireflyClamp) pg *= uFireflyClamp / pgl;
|
|
590
594
|
if (!any(isnan(pg)) && !any(isinf(pg))) {
|
|
591
595
|
emaPrevGi = pg;
|
|
@@ -650,7 +654,7 @@ void main() {
|
|
|
650
654
|
|
|
651
655
|
// Target function at q (same shape the pass already uses).
|
|
652
656
|
float cosQ = max(dot(N, normalize(xS - P)), 0.0);
|
|
653
|
-
float pHatQ =
|
|
657
|
+
float pHatQ = rtLum(Ls) * cosQ;
|
|
654
658
|
// Invalid-shift reject: if the neighbour's hit x_s lies below q's shading
|
|
655
659
|
// hemisphere (cosQ == 0) or carries no radiance, the reconnected target is
|
|
656
660
|
// zero — the shift could never have produced this sample at q, so it must
|
|
@@ -676,7 +680,7 @@ void main() {
|
|
|
676
680
|
vec3 sd = selPos - P;
|
|
677
681
|
float sl = length(sd);
|
|
678
682
|
float selCos = sl > 1e-5 ? max(dot(N, sd / sl), 0.0) : 0.0;
|
|
679
|
-
float pHatSel =
|
|
683
|
+
float pHatSel = rtLum(selRad) * selCos;
|
|
680
684
|
float W = (M > 0.0 && pHatSel > 0.0) ? wSum / (M * pHatSel) : 0.0;
|
|
681
685
|
|
|
682
686
|
// --- final visibility (mandatory): ONE any-hit occlusion ray from x_q toward
|
|
@@ -715,7 +719,7 @@ void main() {
|
|
|
715
719
|
// slightly dim GI on freshly revealed surfaces for a steady image in motion.
|
|
716
720
|
float conf = clamp(M / uMCap, 0.0, 1.0);
|
|
717
721
|
float cap = uFireflyClamp * mix(0.3, 1.0, conf);
|
|
718
|
-
float gil =
|
|
722
|
+
float gil = rtLum(gi);
|
|
719
723
|
if (gil > cap) gi *= cap / gil;
|
|
720
724
|
if (any(isnan(gi)) || any(isinf(gi))) gi = vec3(0.0);
|
|
721
725
|
// Resolve EMA (see the emaPrevGi note above): ~5-frame effective average.
|
|
@@ -729,7 +733,7 @@ void main() {
|
|
|
729
733
|
vec3 sdT = selPosT - P;
|
|
730
734
|
float slT = length(sdT);
|
|
731
735
|
float selCosT = slT > 1e-5 ? max(dot(N, sdT / slT), 0.0) : 0.0;
|
|
732
|
-
float pHatSelT =
|
|
736
|
+
float pHatSelT = rtLum(selRadT) * selCosT;
|
|
733
737
|
float WT = (MT > 0.0 && pHatSelT > 0.0) ? wSumT / (MT * pHatSelT) : 0.0;
|
|
734
738
|
if (any(isnan(selRadT)) || any(isinf(selRadT))) { selRadT = vec3(0.0); WT = 0.0; }
|
|
735
739
|
|
|
@@ -797,6 +801,9 @@ export class GIReservoirPass {
|
|
|
797
801
|
this.targetB = this._makeTarget(width, height);
|
|
798
802
|
|
|
799
803
|
this.material = new THREE.ShaderMaterial({
|
|
804
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
805
|
+
// disables the experimental `restirGI` feature (falls back to inline GI).
|
|
806
|
+
name: "rt:gi-reservoir",
|
|
800
807
|
glslVersion: THREE.GLSL3,
|
|
801
808
|
vertexShader: fullscreenVert,
|
|
802
809
|
fragmentShader: giFrag,
|
|
@@ -851,7 +858,7 @@ export class GIReservoirPass {
|
|
|
851
858
|
// (needs fp32) + M + radiance + W, which must never be interpolated. All fp32
|
|
852
859
|
// (rather than a mixed fp32/fp16 layout) sidesteps drivers that reject mixed
|
|
853
860
|
// MRT precision; the resolved GI (attachment 2) is read 1:1 by the denoise.
|
|
854
|
-
const t =
|
|
861
|
+
const t = makeMRT(width, height, 3, {
|
|
855
862
|
minFilter: THREE.NearestFilter,
|
|
856
863
|
magFilter: THREE.NearestFilter,
|
|
857
864
|
format: THREE.RGBAFormat,
|
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;
|
|
@@ -995,6 +996,10 @@ export class RTLightingPass {
|
|
|
995
996
|
this.specB = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
996
997
|
|
|
997
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",
|
|
998
1003
|
glslVersion: THREE.GLSL3,
|
|
999
1004
|
vertexShader: fullscreenVert,
|
|
1000
1005
|
fragmentShader: specMRT
|
|
@@ -1057,6 +1062,9 @@ export class RTLightingPass {
|
|
|
1057
1062
|
// Specular temporal accumulation program (its own sampler budget — well
|
|
1058
1063
|
// clear of the lighting pass's 16-sampler ceiling).
|
|
1059
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",
|
|
1060
1068
|
glslVersion: THREE.GLSL3,
|
|
1061
1069
|
vertexShader: fullscreenVert,
|
|
1062
1070
|
fragmentShader: specAccumFrag,
|
|
@@ -1081,6 +1089,9 @@ export class RTLightingPass {
|
|
|
1081
1089
|
// buffers. In single-target fallback the second output collapses the same
|
|
1082
1090
|
// way as the lighting shader's.
|
|
1083
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",
|
|
1084
1095
|
glslVersion: THREE.GLSL3,
|
|
1085
1096
|
vertexShader: fullscreenVert,
|
|
1086
1097
|
fragmentShader: specMRT
|
|
@@ -1120,7 +1131,7 @@ export class RTLightingPass {
|
|
|
1120
1131
|
t.texture.generateMipmaps = false;
|
|
1121
1132
|
return t;
|
|
1122
1133
|
}
|
|
1123
|
-
const t =
|
|
1134
|
+
const t = makeMRT(width, height, 2, opts);
|
|
1124
1135
|
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
1125
1136
|
return t;
|
|
1126
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
|
|
|
@@ -690,6 +708,150 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
690
708
|
);
|
|
691
709
|
this._renderScale = 0.375;
|
|
692
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
|
+
}
|
|
693
855
|
}
|
|
694
856
|
|
|
695
857
|
// Consecutive catastrophic frames mean the GPU is drowning — cut quality
|
|
@@ -748,8 +910,31 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
748
910
|
*/
|
|
749
911
|
compileScene(scene, options) {
|
|
750
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
|
+
}
|
|
751
936
|
if (this.compiled) this.compiled.dispose();
|
|
752
|
-
this.compiled =
|
|
937
|
+
this.compiled = compiled;
|
|
753
938
|
// Emissive area lights are the noisiest direct-light path: one triangle
|
|
754
939
|
// sample per pixel per frame, and the 1/dist^2 term spikes near a small
|
|
755
940
|
// emitter (fireflies). ReSTIR's reservoirs are what tame this — warn when
|
|
@@ -1019,6 +1204,13 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1019
1204
|
if (this.adaptiveQuality) this._adaptQuality();
|
|
1020
1205
|
if (this.overloadProtection) this._overloadBrake();
|
|
1021
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
|
+
}
|
|
1022
1214
|
|
|
1023
1215
|
this.frame += 1;
|
|
1024
1216
|
camera.updateMatrixWorld();
|
|
@@ -1303,6 +1495,12 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1303
1495
|
// Record this frame's (jittered) view-projection + jitter for next frame.
|
|
1304
1496
|
this._prevViewProj.copy(this._jitteredViewProj);
|
|
1305
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();
|
|
1306
1504
|
}
|
|
1307
1505
|
|
|
1308
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);
|
package/src/TAAPass.js
CHANGED
|
@@ -128,6 +128,9 @@ export class TAAPass {
|
|
|
128
128
|
this._reset = true;
|
|
129
129
|
|
|
130
130
|
this.material = new THREE.ShaderMaterial({
|
|
131
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
132
|
+
// disables the optional `taa` feature (image stays lit, just aliased).
|
|
133
|
+
name: "rt:taa",
|
|
131
134
|
glslVersion: THREE.GLSL3,
|
|
132
135
|
vertexShader: fullscreenVert,
|
|
133
136
|
fragmentShader: taaFrag,
|
|
@@ -147,6 +150,9 @@ export class TAAPass {
|
|
|
147
150
|
});
|
|
148
151
|
|
|
149
152
|
this.copyMaterial = new THREE.ShaderMaterial({
|
|
153
|
+
// The TAA resolve-to-screen / history-carry blit; folded into the `taa`
|
|
154
|
+
// feature classification (a failure makes TAA unusable → disable taa).
|
|
155
|
+
name: "rt:taa-copy",
|
|
150
156
|
glslVersion: THREE.GLSL3,
|
|
151
157
|
vertexShader: fullscreenVert,
|
|
152
158
|
fragmentShader: copyFrag,
|
package/src/VolumetricPass.js
CHANGED
|
@@ -277,6 +277,9 @@ export class VolumetricPass {
|
|
|
277
277
|
this.targetB = this._makeTarget(width, height);
|
|
278
278
|
|
|
279
279
|
this.material = new THREE.ShaderMaterial({
|
|
280
|
+
// Stable program name for compile-failure self-diagnosis; a link failure
|
|
281
|
+
// disables the optional `volumetric` feature (image stays lit, no god rays).
|
|
282
|
+
name: "rt:volumetric",
|
|
280
283
|
glslVersion: THREE.GLSL3,
|
|
281
284
|
vertexShader: fullscreenVert,
|
|
282
285
|
fragmentShader: volumetricFrag,
|
package/src/index.d.ts
CHANGED
|
@@ -277,6 +277,45 @@ export interface VolumetricState {
|
|
|
277
277
|
zones: FogZone[];
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/** One optional feature auto-disabled after its pass program failed to link. */
|
|
281
|
+
export interface DisabledPass {
|
|
282
|
+
/** The failed pass program's stable name (e.g. `"rt:taa"`, `"rt:denoise"`). */
|
|
283
|
+
pass: string;
|
|
284
|
+
/**
|
|
285
|
+
* The instance toggle that was set false to keep the image lit: one of
|
|
286
|
+
* `"restir"`, `"restirGI"`, `"denoise"`, `"volumetric"`, `"taa"`, `"specular"`.
|
|
287
|
+
*/
|
|
288
|
+
feature: string;
|
|
289
|
+
/** First line of the driver's shader/link info log for the failure. */
|
|
290
|
+
reason: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compile-health summary for the ray tracing pipeline, on
|
|
295
|
+
* {@link RealtimeRaytracer.status}. A pass whose program fails to LINK renders
|
|
296
|
+
* black without throwing (three logs and sets `program.diagnostics.runnable`
|
|
297
|
+
* false, but rendering proceeds), so this is how an integrator distinguishes a
|
|
298
|
+
* working RT image from a broken one — and renders an honest raster fallback
|
|
299
|
+
* with a reason instead of guessing. Populated over the first several rendered
|
|
300
|
+
* frames (link status is checked lazily / can be deferred by the driver).
|
|
301
|
+
*/
|
|
302
|
+
export interface RaytracerStatus {
|
|
303
|
+
/**
|
|
304
|
+
* True while every ray tracing pass is running as intended. Flips to false the
|
|
305
|
+
* moment ANY `rt:*` pass program fails to link — whether a core pass (see
|
|
306
|
+
* `coreFailure`) or an optional feature that was auto-disabled (see `disabled`).
|
|
307
|
+
*/
|
|
308
|
+
ok: boolean;
|
|
309
|
+
/** Optional features turned off to keep the image lit after their pass failed to link. */
|
|
310
|
+
disabled: DisabledPass[];
|
|
311
|
+
/**
|
|
312
|
+
* Set when a CORE pass (gbuffer / lighting / composite) failed to link. Such a
|
|
313
|
+
* pass has no fallback, so the image is black — but now diagnosed. The string
|
|
314
|
+
* is a `"rt:<pass>: <driver log>"` summary; null when no core pass has failed.
|
|
315
|
+
*/
|
|
316
|
+
coreFailure: string | null;
|
|
317
|
+
}
|
|
318
|
+
|
|
280
319
|
/** Options accepted by {@link RealtimeRaytracer.compileScene} and {@link compileScene}. */
|
|
281
320
|
export interface CompileSceneOptions {
|
|
282
321
|
/**
|
|
@@ -370,6 +409,22 @@ export class RealtimeRaytracer {
|
|
|
370
409
|
specMRTSupported: boolean;
|
|
371
410
|
/** The current compiled scene, or null before the first compile / when unsupported. */
|
|
372
411
|
compiled: CompiledScene | null;
|
|
412
|
+
/**
|
|
413
|
+
* First/most-severe pass compile-failure summary (`"rt:<pass>: <driver log>"`),
|
|
414
|
+
* or null while every ray tracing pass compiles clean. A quick honest signal
|
|
415
|
+
* for "should I show the RT image?"; see {@link status} for the structured form.
|
|
416
|
+
* Core failures (gbuffer/lighting/composite) outrank auto-disabled features here.
|
|
417
|
+
*/
|
|
418
|
+
compileError: string | null;
|
|
419
|
+
/**
|
|
420
|
+
* Structured compile-health of the pipeline (see {@link RaytracerStatus}).
|
|
421
|
+
* `status.ok` is true on the healthy path and false once any pass fails to
|
|
422
|
+
* link; `status.disabled` lists auto-disabled optional features; and
|
|
423
|
+
* `status.coreFailure` names an unrecoverable core-pass failure. Populated over
|
|
424
|
+
* the first several rendered frames. When `supported` is false, `status.ok` is
|
|
425
|
+
* false too (the RT pipeline is not operational).
|
|
426
|
+
*/
|
|
427
|
+
status: RaytracerStatus;
|
|
373
428
|
/** Accumulated frame counter. */
|
|
374
429
|
frame: number;
|
|
375
430
|
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive, 6 specular, 7 bvh cost. */
|
package/src/mrtCompat.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiple-render-target constructor that works across the three versions in
|
|
5
|
+
* this library's peer range (three >=0.155.0).
|
|
6
|
+
*
|
|
7
|
+
* three r172 removed `WebGLMultipleRenderTargets`. Its replacement is the base
|
|
8
|
+
* `WebGLRenderTarget` with a `count` option (added r162) for N color
|
|
9
|
+
* attachments, which it exposes as the ARRAY `.textures` — while `.texture`
|
|
10
|
+
* became a single texture (an alias for `textures[0]`). This library predates
|
|
11
|
+
* that split and indexes the attachment array as `.texture[i]` throughout the
|
|
12
|
+
* passes, so on new three we shadow the instance `.texture` accessor with the
|
|
13
|
+
* `.textures` array. That keeps every existing `.texture[i]` / `for (... of
|
|
14
|
+
* .texture)` call site correct with no per-site changes, and does not disturb
|
|
15
|
+
* three's WebGL backend (which binds/reads/reads-back via `.textures`, and only
|
|
16
|
+
* touches the single `.texture` on cube/PMREM targets, never on these 2D MRTs).
|
|
17
|
+
*
|
|
18
|
+
* On old three (<= r171) `WebGLMultipleRenderTargets` already IS an
|
|
19
|
+
* array-`.texture` MRT, so it is used unchanged and the result is byte-identical
|
|
20
|
+
* to before this shim existed.
|
|
21
|
+
*
|
|
22
|
+
* Signature matches the old constructor: (width, height, count, options).
|
|
23
|
+
*/
|
|
24
|
+
export function makeMRT(width, height, count, options = {}) {
|
|
25
|
+
if (THREE.WebGLMultipleRenderTargets) {
|
|
26
|
+
return new THREE.WebGLMultipleRenderTargets(width, height, count, options);
|
|
27
|
+
}
|
|
28
|
+
const rt = new THREE.WebGLRenderTarget(width, height, { ...options, count });
|
|
29
|
+
// Re-point `.texture` at the attachment array (old-three semantics). Shadows
|
|
30
|
+
// the prototype get/set texture() accessor on this instance only.
|
|
31
|
+
Object.defineProperty(rt, "texture", {
|
|
32
|
+
value: rt.textures,
|
|
33
|
+
writable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
|
+
return rt;
|
|
37
|
+
}
|