three-realtime-rt 0.6.0 → 0.7.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 +309 -10
- 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 +121 -2
- package/src/GIReservoirPass.js +19 -12
- package/src/RTLightingPass.js +81 -1
- package/src/RealtimeRaytracer.js +408 -4
- package/src/RestirPass.js +12 -5
- package/src/SceneCompiler.js +212 -1
- package/src/TAAPass.js +6 -0
- package/src/VolumetricPass.js +3 -0
- package/src/index.d.ts +154 -0
- package/src/mrtCompat.js +37 -0
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,30 @@ 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
|
+
|
|
352
|
+
// Staleness scan (see _checkStale): a static mesh edited after compileScene()
|
|
353
|
+
// keeps tracing its compile-time shape/place. Checked every Nth frame only —
|
|
354
|
+
// the comparison is an int and 16 floats per static source, and each source
|
|
355
|
+
// stops being checked as soon as it has reported — and capped so a scene that
|
|
356
|
+
// moves everything can never flood the console.
|
|
357
|
+
static STALE_CHECK_FRAMES = 30;
|
|
358
|
+
static MAX_STALE_WARNINGS = 8;
|
|
359
|
+
|
|
360
|
+
// Largest renderScale change the adaptive governor may commit in one step
|
|
361
|
+
// (0.25 = five 0.05 ladder steps). Bounds the reaction to a single very slow
|
|
362
|
+
// frame now that 100ms-2s frames feed the EMA; see _adaptQuality.
|
|
363
|
+
static MAX_SCALE_STEP = 0.25;
|
|
364
|
+
|
|
340
365
|
constructor(renderer, options = {}) {
|
|
341
366
|
this.renderer = renderer;
|
|
342
367
|
|
|
@@ -354,6 +379,12 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
354
379
|
);
|
|
355
380
|
this.compiled = null;
|
|
356
381
|
this.frame = 0;
|
|
382
|
+
// Status surface (see the supported path for the shapes). Unsupported =
|
|
383
|
+
// the RT pipeline is not operational at all; `supported` is the primary
|
|
384
|
+
// signal, but status is kept consistent so integrators can read one field.
|
|
385
|
+
this.compileError = null;
|
|
386
|
+
this.status = { ok: false, disabled: [], coreFailure: null, warnings: [] };
|
|
387
|
+
this._diagDone = true;
|
|
357
388
|
return;
|
|
358
389
|
}
|
|
359
390
|
|
|
@@ -404,6 +435,18 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
404
435
|
"(WebKit/iOS) — specular buffer disabled, alpha-blend surfaces render opaque."
|
|
405
436
|
);
|
|
406
437
|
}
|
|
438
|
+
// Fragment-shader texture-unit budget. The lighting megakernel already binds
|
|
439
|
+
// exactly the WebGL2-guaranteed minimum of 16 fragment samplers, so the
|
|
440
|
+
// world-space 3D-texture-albedo feature's SECONDARY-ray path (an extra
|
|
441
|
+
// sampler3D) can only be compiled in on a GPU that exposes >= 17. Primary
|
|
442
|
+
// visibility (the G-buffer, which has ample headroom) samples the volume
|
|
443
|
+
// regardless; only the traced GI/reflection bounce is gated on this. Most
|
|
444
|
+
// desktop GPUs report 32.
|
|
445
|
+
this._maxFragTexUnits = renderer.getContext().getParameter(
|
|
446
|
+
renderer.getContext().MAX_TEXTURE_IMAGE_UNITS
|
|
447
|
+
);
|
|
448
|
+
this._volumeUnitWarned = false;
|
|
449
|
+
|
|
407
450
|
this.gbuffer = new GBufferPass(this._width, this._height, { mixedPrecision });
|
|
408
451
|
this.rtPass = new RTLightingPass(this._scaledW, this._scaledH, {
|
|
409
452
|
specMRT: this.specMRTSupported,
|
|
@@ -690,6 +733,161 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
690
733
|
);
|
|
691
734
|
this._renderScale = 0.375;
|
|
692
735
|
}
|
|
736
|
+
|
|
737
|
+
// ---- compile-failure status surface -----------------------------------
|
|
738
|
+
// The pipeline is a stack of ShaderMaterial passes; a program that fails to
|
|
739
|
+
// LINK renders black with no exception (three logs to the console and sets
|
|
740
|
+
// program.diagnostics.runnable=false, but rendering proceeds). Before this,
|
|
741
|
+
// a broken pass looked identical to `supported:false` from the outside — the
|
|
742
|
+
// failure that shipped the r166+ black image. These two fields let an
|
|
743
|
+
// integrator render honestly ("raster (reason)") instead of guessing:
|
|
744
|
+
//
|
|
745
|
+
// compileError : string | null
|
|
746
|
+
// First/most-severe failure summary ("rt:lighting: <driver log>"), or
|
|
747
|
+
// null while every rt:* pass is compiling clean.
|
|
748
|
+
// status : { ok, disabled, coreFailure }
|
|
749
|
+
// ok false once ANY rt:* pass failed to link (a core pass, or
|
|
750
|
+
// a feature that was auto-disabled). true = pipeline is
|
|
751
|
+
// running as intended.
|
|
752
|
+
// disabled [{ pass, feature, reason }] — optional features turned
|
|
753
|
+
// off to keep the image lit (e.g. taa, denoise, restir).
|
|
754
|
+
// coreFailure string | null — a core pass (gbuffer/lighting/composite)
|
|
755
|
+
// failed and has no fallback; the image is black-but-diagnosed.
|
|
756
|
+
// warnings [{ code, message }] — USAGE diagnostics (a flag that is
|
|
757
|
+
// being ignored, an object type that cannot be traced, a
|
|
758
|
+
// static mesh edited after compileScene()). Each is also
|
|
759
|
+
// console.warn'd once. These never affect `ok`: the
|
|
760
|
+
// pipeline is healthy, the SCENE SETUP is not what the app
|
|
761
|
+
// probably intended.
|
|
762
|
+
this.compileError = null;
|
|
763
|
+
this.status = { ok: true, disabled: [], coreFailure: null, warnings: [] };
|
|
764
|
+
this._diagDone = false; // set once the polling window settles
|
|
765
|
+
this._diagFrames = 0; // rendered frames scanned so far
|
|
766
|
+
this._diagStable = 0; // consecutive scans with an unchanged rt:* program set
|
|
767
|
+
this._diagSig = ""; // signature of the rt:* program-name set last scan
|
|
768
|
+
this._diagHandled = new Set(); // rt:* names already acted on (warn-once)
|
|
769
|
+
this._compileErrSev = -1; // severity behind the current compileError (2/1/0)
|
|
770
|
+
|
|
771
|
+
// Usage-diagnostic state (see _checkStale / _warn).
|
|
772
|
+
this._staleDone = false; // nothing left worth scanning
|
|
773
|
+
this._staleWarnings = 0; // stale reports emitted (capped)
|
|
774
|
+
this._implicitCompileWarned = false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Classify an rt:* pass program by how a LINK failure degrades. CORE passes
|
|
778
|
+
// have no fallback (record and keep rendering the black result so it is
|
|
779
|
+
// DIAGNOSED, not silent). Optional passes map to the EXACT runtime toggle that
|
|
780
|
+
// already gates them, so disabling one keeps the image lit. Unknown rt:* names
|
|
781
|
+
// (history-carry blits) are auxiliary: non-fatal, warn only. Returns one of
|
|
782
|
+
// { core:true } | { feature, disable } | { aux:true }.
|
|
783
|
+
_passClass(name) {
|
|
784
|
+
switch (name) {
|
|
785
|
+
case "rt:gbuffer":
|
|
786
|
+
case "rt:lighting":
|
|
787
|
+
case "rt:composite":
|
|
788
|
+
return { core: true };
|
|
789
|
+
case "rt:restir-temporal":
|
|
790
|
+
case "rt:restir-spatial":
|
|
791
|
+
return { feature: "restir", disable: () => { this.restir = false; } };
|
|
792
|
+
case "rt:gi-reservoir":
|
|
793
|
+
return { feature: "restirGI", disable: () => { this.restirGI = false; } };
|
|
794
|
+
case "rt:denoise":
|
|
795
|
+
return { feature: "denoise", disable: () => { this.denoise = false; } };
|
|
796
|
+
case "rt:volumetric":
|
|
797
|
+
return { feature: "volumetric", disable: () => { this.volumetric.enabled = false; } };
|
|
798
|
+
case "rt:taa":
|
|
799
|
+
case "rt:taa-copy":
|
|
800
|
+
return { feature: "taa", disable: () => { this.taa = false; } };
|
|
801
|
+
case "rt:specular":
|
|
802
|
+
return { feature: "specular", disable: () => { this.specular = false; } };
|
|
803
|
+
default:
|
|
804
|
+
return { aux: true };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Compact one-line driver message from three's program.diagnostics. The
|
|
809
|
+
// GLSL-frontend error lives in the fragment (or vertex) shader log; programLog
|
|
810
|
+
// is the linker fallback. First line, capped, so it fits a console.warn / a UI.
|
|
811
|
+
_diagLog(diag) {
|
|
812
|
+
const pick = [
|
|
813
|
+
diag && diag.fragmentShader && diag.fragmentShader.log,
|
|
814
|
+
diag && diag.vertexShader && diag.vertexShader.log,
|
|
815
|
+
diag && diag.programLog,
|
|
816
|
+
].find((s) => s && s.trim());
|
|
817
|
+
return (pick || "(no driver log)").trim().split("\n")[0].slice(0, 200);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Keep compileError at the FIRST failure of the HIGHEST severity seen
|
|
821
|
+
// (core 2 > feature 1 > aux 0): a later core failure overrides an earlier
|
|
822
|
+
// feature summary, but two failures of equal severity keep the first.
|
|
823
|
+
_noteCompileError(summary, severity) {
|
|
824
|
+
if (severity > this._compileErrSev) {
|
|
825
|
+
this.compileError = summary;
|
|
826
|
+
this._compileErrSev = severity;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Act on one failed rt:* program (called at most once per pass name).
|
|
831
|
+
_handleFailedProgram(name, diag) {
|
|
832
|
+
const log = this._diagLog(diag);
|
|
833
|
+
const cls = this._passClass(name);
|
|
834
|
+
const summary = `${name}: ${log}`;
|
|
835
|
+
this.status.ok = false;
|
|
836
|
+
if (cls.core) {
|
|
837
|
+
if (!this.status.coreFailure) this.status.coreFailure = summary;
|
|
838
|
+
this._noteCompileError(summary, 2);
|
|
839
|
+
console.warn(
|
|
840
|
+
`three-realtime-rt: core pass ${name} failed to link — the image will ` +
|
|
841
|
+
`be black (no fallback for a core pass). Driver log: ${log}`
|
|
842
|
+
);
|
|
843
|
+
} else if (cls.feature) {
|
|
844
|
+
cls.disable();
|
|
845
|
+
this.status.disabled.push({ pass: name, feature: cls.feature, reason: log });
|
|
846
|
+
this._noteCompileError(summary, 1);
|
|
847
|
+
console.warn(
|
|
848
|
+
`three-realtime-rt: pass ${name} failed to link — auto-disabled ` +
|
|
849
|
+
`"${cls.feature}" to keep the image lit. Driver log: ${log}`
|
|
850
|
+
);
|
|
851
|
+
} else {
|
|
852
|
+
this._noteCompileError(summary, 0);
|
|
853
|
+
console.warn(
|
|
854
|
+
`three-realtime-rt: auxiliary pass ${name} failed to link (non-fatal — ` +
|
|
855
|
+
`resize history is not carried). Driver log: ${log}`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Scan renderer.info.programs for failed rt:* pass programs. Called each frame
|
|
861
|
+
// until the window settles (see the DIAG_* constants). Cheap: ~a dozen entries,
|
|
862
|
+
// string prefix check, warn-once via _diagHandled.
|
|
863
|
+
_scanPrograms() {
|
|
864
|
+
if (this._diagDone) return;
|
|
865
|
+
const programs = this.renderer.info && this.renderer.info.programs;
|
|
866
|
+
if (!programs) { this._diagDone = true; return; }
|
|
867
|
+
this._diagFrames++;
|
|
868
|
+
let names = "";
|
|
869
|
+
for (const p of programs) {
|
|
870
|
+
const name = p && p.name;
|
|
871
|
+
if (!name || name.slice(0, 3) !== "rt:") continue;
|
|
872
|
+
names += name + "|";
|
|
873
|
+
const diag = p.diagnostics; // set at first USE; runnable:false = link failed
|
|
874
|
+
if (diag && diag.runnable === false && !this._diagHandled.has(name)) {
|
|
875
|
+
this._diagHandled.add(name);
|
|
876
|
+
this._handleFailedProgram(name, diag);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Early-out: the rt:* program set has stopped growing and nothing new failed
|
|
880
|
+
// for DIAG_STABLE_FRAMES past the warmup floor → every active pass has been
|
|
881
|
+
// seen and validated. Otherwise stop at the hard window.
|
|
882
|
+
if (names === this._diagSig) this._diagStable++;
|
|
883
|
+
else { this._diagStable = 0; this._diagSig = names; }
|
|
884
|
+
if (
|
|
885
|
+
(this._diagFrames >= RealtimeRaytracer.DIAG_MIN_FRAMES &&
|
|
886
|
+
this._diagStable >= RealtimeRaytracer.DIAG_STABLE_FRAMES) ||
|
|
887
|
+
this._diagFrames >= RealtimeRaytracer.DIAG_WINDOW_FRAMES
|
|
888
|
+
) {
|
|
889
|
+
this._diagDone = true;
|
|
890
|
+
}
|
|
693
891
|
}
|
|
694
892
|
|
|
695
893
|
// Consecutive catastrophic frames mean the GPU is drowning — cut quality
|
|
@@ -728,6 +926,102 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
728
926
|
}
|
|
729
927
|
}
|
|
730
928
|
|
|
929
|
+
// ---- usage diagnostics (status.warnings) ---------------------------------
|
|
930
|
+
// A usage warning is console.warn'd ONCE and recorded on status.warnings so a
|
|
931
|
+
// UI (or an automated check) can read the same signal. `ok` is untouched: the
|
|
932
|
+
// pipeline is fine, the scene setup is what looks wrong. Duplicate
|
|
933
|
+
// code+message pairs are collapsed so a recompile can't grow the array.
|
|
934
|
+
_warn(code, message) {
|
|
935
|
+
console.warn(message);
|
|
936
|
+
this._recordWarning(code, message);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
_recordWarning(code, message) {
|
|
940
|
+
const list = this.status && this.status.warnings;
|
|
941
|
+
if (!list) return;
|
|
942
|
+
for (let i = 0; i < list.length; i++) {
|
|
943
|
+
if (list[i].code === code && list[i].message === message) return;
|
|
944
|
+
}
|
|
945
|
+
list.push({ code, message });
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Mirror the compiler's usage diagnostics onto the status surface. The
|
|
949
|
+
// compiler already wrote them to the console (once per offending object);
|
|
950
|
+
// this only makes them readable.
|
|
951
|
+
_absorbCompilerWarnings(compiled) {
|
|
952
|
+
const w = compiled && compiled.warnings;
|
|
953
|
+
if (!w || w.length === 0) return;
|
|
954
|
+
for (let i = 0; i < w.length; i++) this._recordWarning(w[i].code, w[i].message);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Detect a STATIC mesh that was edited after compileScene(): its vertices were
|
|
958
|
+
// deformed, or it was moved. Either way the traced lighting still uses the
|
|
959
|
+
// shape/place baked into the static BVH, which reads as "the shadow doesn't
|
|
960
|
+
// move" or "rays still hit the original shape" — the single most common
|
|
961
|
+
// integration mistake, and completely silent before this.
|
|
962
|
+
//
|
|
963
|
+
// Cost: runs on every STALE_CHECK_FRAMES-th frame only, over a plain array of
|
|
964
|
+
// fingerprints, with no allocation; each source is checked until it warns once
|
|
965
|
+
// (or its mesh is collected), and the whole scan switches off when nothing is
|
|
966
|
+
// left to report or the warning cap is reached.
|
|
967
|
+
_checkStale() {
|
|
968
|
+
if (this._staleDone) return;
|
|
969
|
+
const srcs = this.compiled && this.compiled.staticSources;
|
|
970
|
+
if (!srcs || srcs.length === 0) { this._staleDone = true; return; }
|
|
971
|
+
let live = 0;
|
|
972
|
+
for (let i = 0; i < srcs.length; i++) {
|
|
973
|
+
const s = srcs[i];
|
|
974
|
+
if (s.warned) continue;
|
|
975
|
+
const mesh = s.ref.deref();
|
|
976
|
+
if (!mesh) { s.warned = true; continue; } // collected — nothing to report
|
|
977
|
+
let stale = null;
|
|
978
|
+
const geo = mesh.geometry;
|
|
979
|
+
const pos = geo ? geo.getAttribute("position") : null;
|
|
980
|
+
if (!pos || pos.version !== s.version) {
|
|
981
|
+
stale = "geometry";
|
|
982
|
+
} else {
|
|
983
|
+
// Relative tolerance rather than an exact compare: an app that rebuilds
|
|
984
|
+
// an unchanged transform each frame (setFromEuler, a physics engine
|
|
985
|
+
// writing back the same pose) can land a few ULPs away, and a sub-micron
|
|
986
|
+
// "move" is not what this diagnostic is about.
|
|
987
|
+
const e = mesh.matrixWorld.elements;
|
|
988
|
+
const m = s.matrix;
|
|
989
|
+
for (let k = 0; k < 16; k++) {
|
|
990
|
+
const d = e[k] - m[k];
|
|
991
|
+
if ((d < 0 ? -d : d) > 1e-6 * (1 + (m[k] < 0 ? -m[k] : m[k]))) {
|
|
992
|
+
stale = "transform";
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!stale) { live++; continue; }
|
|
998
|
+
s.warned = true;
|
|
999
|
+
this._staleWarnings++;
|
|
1000
|
+
if (stale === "geometry") {
|
|
1001
|
+
this._warn(
|
|
1002
|
+
"stale-geometry",
|
|
1003
|
+
`three-realtime-rt: position buffer of ${s.name} changed after compileScene() ` +
|
|
1004
|
+
`but it is not a dynamic mesh — traced lighting still uses the ORIGINAL shape. ` +
|
|
1005
|
+
`Add it to compileScene(scene, {dynamicMeshes:[...]}) and set ` +
|
|
1006
|
+
`mesh.userData.rtDeforming = true, then call updateDynamic() each frame.`
|
|
1007
|
+
);
|
|
1008
|
+
} else {
|
|
1009
|
+
this._warn(
|
|
1010
|
+
"stale-transform",
|
|
1011
|
+
`three-realtime-rt: ${s.name} was moved after compileScene() but it is not a ` +
|
|
1012
|
+
`dynamic mesh — traced lighting still uses the ORIGINAL transform (its shadow ` +
|
|
1013
|
+
`stays behind). Recompile with compileScene(scene), or declare it in ` +
|
|
1014
|
+
`compileScene(scene, {dynamicMeshes:[...]}) and call updateDynamic() each frame.`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
if (this._staleWarnings >= RealtimeRaytracer.MAX_STALE_WARNINGS) {
|
|
1018
|
+
this._staleDone = true; // enough — do not flood the console
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (live === 0) this._staleDone = true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
731
1025
|
_makeColorTarget(width, height) {
|
|
732
1026
|
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
733
1027
|
minFilter: THREE.LinearFilter,
|
|
@@ -748,8 +1042,36 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
748
1042
|
*/
|
|
749
1043
|
compileScene(scene, options) {
|
|
750
1044
|
if (!this.supported) return null;
|
|
1045
|
+
// "Construct the tracer, then add meshes" is a natural call order, so a
|
|
1046
|
+
// scene with no traceable meshes is a NO-OP, not a throw: warn once and keep
|
|
1047
|
+
// any previously compiled scene. Compile the new scene BEFORE disposing the
|
|
1048
|
+
// old one so an empty-scene call never destroys a good scene. Only the
|
|
1049
|
+
// SceneCompiler's specific "no meshes" signal is swallowed here; every other
|
|
1050
|
+
// compile error (bad geometry, oversized attribute) still propagates.
|
|
1051
|
+
let compiled;
|
|
1052
|
+
try {
|
|
1053
|
+
compiled = compileScene(scene, options);
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
if (/no meshes found/.test(String(err && err.message))) {
|
|
1056
|
+
if (!this._emptyWarned) {
|
|
1057
|
+
console.warn(
|
|
1058
|
+
"three-realtime-rt: compileScene() called on a scene with no traceable " +
|
|
1059
|
+
"meshes — keeping the current scene. Until meshes are added and " +
|
|
1060
|
+
"recompiled, render() falls back to plain rasterization (no crash, no black)."
|
|
1061
|
+
);
|
|
1062
|
+
this._emptyWarned = true;
|
|
1063
|
+
}
|
|
1064
|
+
return this.compiled; // unchanged (may still be null)
|
|
1065
|
+
}
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
751
1068
|
if (this.compiled) this.compiled.dispose();
|
|
752
|
-
this.compiled =
|
|
1069
|
+
this.compiled = compiled;
|
|
1070
|
+
// Usage diagnostics raised while compiling (already console.warn'd once per
|
|
1071
|
+
// offending object) become readable on status.warnings. A fresh compile also
|
|
1072
|
+
// re-arms the staleness scan against the NEW fingerprints.
|
|
1073
|
+
this._absorbCompilerWarnings(compiled);
|
|
1074
|
+
this._staleDone = false;
|
|
753
1075
|
// Emissive area lights are the noisiest direct-light path: one triangle
|
|
754
1076
|
// sample per pixel per frame, and the 1/dist^2 term spikes near a small
|
|
755
1077
|
// emitter (fireflies). ReSTIR's reservoirs are what tame this — warn when
|
|
@@ -769,10 +1091,40 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
769
1091
|
this.volumetricPass.setCompiledScene(this.compiled);
|
|
770
1092
|
this.restirPass.setCompiledScene(this.compiled);
|
|
771
1093
|
this.giReservoirPass.setCompiledScene(this.compiled);
|
|
1094
|
+
this._syncVolumeAlbedo();
|
|
772
1095
|
this.resetAccumulation();
|
|
773
1096
|
return this.compiled;
|
|
774
1097
|
}
|
|
775
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Push the compiled scene's world-space 3D-texture albedo (if any) into the
|
|
1101
|
+
* passes. Primary visibility (G-buffer) always gets it — it has spare samplers.
|
|
1102
|
+
* The traced SECONDARY-ray path (GI/reflection colour) needs a 17th fragment
|
|
1103
|
+
* sampler, so it is only enabled on a GPU that exposes one; on a bare-minimum
|
|
1104
|
+
* 16-unit device the bounces fall back to the material's flat table albedo
|
|
1105
|
+
* (primary visibility still shows the full field), logged once.
|
|
1106
|
+
*/
|
|
1107
|
+
_syncVolumeAlbedo() {
|
|
1108
|
+
const vol = this.compiled ? this.compiled.volumeAlbedo : null;
|
|
1109
|
+
this.gbuffer.setVolume(!!vol);
|
|
1110
|
+
const secondaryOk = !!vol && this._maxFragTexUnits >= 17;
|
|
1111
|
+
// NOTE: only the inline-GI lighting pass (rtPass.traceRadiance) samples the
|
|
1112
|
+
// volume for the traced bounce. The experimental ReSTIR GI pass
|
|
1113
|
+
// (giReservoirPass, off by default) has its own GI kernel and is NOT wired
|
|
1114
|
+
// for volume albedo in v1 — a volume surface's indirect bounce falls back to
|
|
1115
|
+
// its flat table colour while restirGI is on (documented limitation).
|
|
1116
|
+
this.rtPass.setVolumeAlbedo(secondaryOk ? vol : null);
|
|
1117
|
+
if (vol && !secondaryOk && !this._volumeUnitWarned) {
|
|
1118
|
+
this._volumeUnitWarned = true;
|
|
1119
|
+
console.info(
|
|
1120
|
+
"[three-realtime-rt] volume albedo: this GPU exposes only " +
|
|
1121
|
+
`${this._maxFragTexUnits} fragment texture units (< 17 needed for the ` +
|
|
1122
|
+
"traced-bounce sampler), so GI / reflection bounces use the material's flat " +
|
|
1123
|
+
"base colour. Primary visibility still shows the full 3D-texture field."
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
776
1128
|
/**
|
|
777
1129
|
* Re-bake moving (dynamic) meshes into the dynamic BVH level. Call each frame
|
|
778
1130
|
* after moving them. Only the dynamic level is touched — the static BVH was
|
|
@@ -928,10 +1280,24 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
928
1280
|
// cap the frame time can't reveal headroom, so upscaling only happens when
|
|
929
1281
|
// frames are measurably faster than the target — it never thrashes.
|
|
930
1282
|
_adaptQuality() {
|
|
1283
|
+
// Hidden tabs are exempt: browser throttling makes every frame look
|
|
1284
|
+
// catastrophic, and adapting on that would drop quality for a tab nobody is
|
|
1285
|
+
// watching (same rule as _overloadBrake).
|
|
1286
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1287
|
+
this._qLastT = null;
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
931
1290
|
const now = performance.now();
|
|
932
1291
|
const dt = this._qLastT == null ? null : now - this._qLastT;
|
|
933
1292
|
this._qLastT = now;
|
|
934
|
-
if (dt == null
|
|
1293
|
+
if (dt == null) return; // first frame — no interval yet
|
|
1294
|
+
// Only a genuine stall/resume (a blocked main thread, a tab coming back, a
|
|
1295
|
+
// debugger pause) is discarded. The old guard bailed above 100ms, which left
|
|
1296
|
+
// every device running slower than 10fps unable to adapt AT ALL: the
|
|
1297
|
+
// governor never saw a sample, and _overloadBrake only reacts past 400ms
|
|
1298
|
+
// with three consecutive strikes. Frames from 100ms to 2s now feed the EMA,
|
|
1299
|
+
// so a 3fps device walks down the ladder like any other.
|
|
1300
|
+
if (dt > 2000) return;
|
|
935
1301
|
this._qEma = this._qEma == null ? dt : this._qEma * 0.9 + dt * 0.1;
|
|
936
1302
|
// Calmness: normally 2s between changes. When the last two steps reversed
|
|
937
1303
|
// direction the governor is hunting the boundary, so hold for 5s AND widen
|
|
@@ -946,6 +1312,13 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
946
1312
|
if (ratio < dbHi && ratio > dbLo) return; // comfortable — leave it alone
|
|
947
1313
|
|
|
948
1314
|
let s = this._renderScale * Math.pow(1 / ratio, 0.35);
|
|
1315
|
+
// Per-step clamp. Now that multi-hundred-millisecond frames feed the EMA, a
|
|
1316
|
+
// single very slow measurement (ratio can reach ~100 at dt 2s) would
|
|
1317
|
+
// otherwise slam the scale from 1.0 to the 0.2 floor in ONE step and throw
|
|
1318
|
+
// away the image on a transient. Move at most MAX_SCALE_STEP per adaptation
|
|
1319
|
+
// (5 ladder steps) and let the cooldown take the next one if it is still slow.
|
|
1320
|
+
const step = RealtimeRaytracer.MAX_SCALE_STEP;
|
|
1321
|
+
s = Math.min(this._renderScale + step, Math.max(this._renderScale - step, s));
|
|
949
1322
|
s = Math.round(Math.min(1, Math.max(0.2, s)) * 20) / 20; // 0.05 steps
|
|
950
1323
|
|
|
951
1324
|
// When we're fast, give back the deepest lever FIRST: restore canvas scale
|
|
@@ -1018,9 +1391,34 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1018
1391
|
}
|
|
1019
1392
|
if (this.adaptiveQuality) this._adaptQuality();
|
|
1020
1393
|
if (this.overloadProtection) this._overloadBrake();
|
|
1021
|
-
if (!this.compiled)
|
|
1394
|
+
if (!this.compiled) {
|
|
1395
|
+
this.compileScene(scene);
|
|
1396
|
+
// An implicit compile is the zero-config path: it works, but it takes NO
|
|
1397
|
+
// options, so every mesh is static forever. Anything that moves needs the
|
|
1398
|
+
// explicit call — say so once, and only once the compile actually produced
|
|
1399
|
+
// a scene (an empty scene has its own warning).
|
|
1400
|
+
if (this.compiled && !this._implicitCompileWarned) {
|
|
1401
|
+
this._implicitCompileWarned = true;
|
|
1402
|
+
this._warn(
|
|
1403
|
+
"implicit-compile",
|
|
1404
|
+
"three-realtime-rt: render() compiled the scene implicitly (no compileScene() " +
|
|
1405
|
+
"call), so it was compiled with NO options — every mesh is static and " +
|
|
1406
|
+
"updateDynamic() has nothing to update. Call compileScene(scene, options) " +
|
|
1407
|
+
"yourself (e.g. {dynamicMeshes:[...]}) before the first render() if anything moves."
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// Still nothing to trace (empty scene — tracer built before meshes were
|
|
1412
|
+
// added). Show the user's raster scene rather than crashing or rendering
|
|
1413
|
+
// black; the pipeline picks up automatically once compileScene() succeeds.
|
|
1414
|
+
if (!this.compiled) {
|
|
1415
|
+
this.renderer.render(scene, camera);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1022
1418
|
|
|
1023
1419
|
this.frame += 1;
|
|
1420
|
+
// Cheap periodic check that no STATIC mesh was edited behind the BVH's back.
|
|
1421
|
+
if (this.frame % RealtimeRaytracer.STALE_CHECK_FRAMES === 0) this._checkStale();
|
|
1024
1422
|
camera.updateMatrixWorld();
|
|
1025
1423
|
|
|
1026
1424
|
// --- sub-pixel jitter (TAA): offset the projection a fraction of a pixel
|
|
@@ -1303,6 +1701,12 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1303
1701
|
// Record this frame's (jittered) view-projection + jitter for next frame.
|
|
1304
1702
|
this._prevViewProj.copy(this._jitteredViewProj);
|
|
1305
1703
|
this._prevJitterUv.copy(this._jitterUv);
|
|
1704
|
+
|
|
1705
|
+
// Compile-failure diagnosis: every pass program used this frame has now had
|
|
1706
|
+
// its link status checked by three (diagnostics populated on first use), so
|
|
1707
|
+
// scan for failures. Runs at frame END (downstream of the passes) and only
|
|
1708
|
+
// until the polling window settles — a no-op on the healthy steady state.
|
|
1709
|
+
if (!this._diagDone) this._scanPrograms();
|
|
1306
1710
|
}
|
|
1307
1711
|
|
|
1308
1712
|
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);
|