three-realtime-rt 0.6.1 → 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 +263 -12
- package/package.json +1 -1
- package/src/GBufferPass.js +115 -1
- package/src/RTLightingPass.js +69 -0
- package/src/RealtimeRaytracer.js +210 -4
- package/src/SceneCompiler.js +212 -1
- package/src/index.d.ts +100 -1
package/src/RTLightingPass.js
CHANGED
|
@@ -193,6 +193,27 @@ void fetchMaterial(float matIndex, out vec3 albedo, out float roughness,
|
|
|
193
193
|
metalness = t1.a;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// World-space 3D-texture albedo ("volumetric surface albedo") for the traced
|
|
197
|
+
// SECONDARY rays (GI bounces + reflection/refraction), so global illumination and
|
|
198
|
+
// mirror views carry the same field colours the primary G-buffer shows. Compiled
|
|
199
|
+
// in ONLY behind RT_VOLUME_ALBEDO: this megakernel already sits at the WebGL2
|
|
200
|
+
// 16-sampler minimum, so the extra sampler3D is added exclusively when a scene
|
|
201
|
+
// registers a volume AND the GPU exposes >= 17 fragment texture units (the
|
|
202
|
+
// RealtimeRaytracer gates both conditions). Absent, the shader is textually
|
|
203
|
+
// identical to the pre-feature megakernel — same 16 samplers, same program. v1 is
|
|
204
|
+
// single-volume: one texture + one material index; a hit on that material samples
|
|
205
|
+
// the field, every other hit reads its flat table albedo unchanged.
|
|
206
|
+
#ifdef RT_VOLUME_ALBEDO
|
|
207
|
+
uniform highp sampler3D uVolumeTex;
|
|
208
|
+
uniform vec3 uVolumeOrigin;
|
|
209
|
+
uniform vec3 uVolumeSize;
|
|
210
|
+
uniform int uVolumeMatIndex;
|
|
211
|
+
vec3 sampleVolumeAlbedo(vec3 p) {
|
|
212
|
+
vec3 uvw = clamp((p - uVolumeOrigin) / uVolumeSize, 0.0, 1.0);
|
|
213
|
+
return texture(uVolumeTex, uvw).rgb;
|
|
214
|
+
}
|
|
215
|
+
#endif
|
|
216
|
+
|
|
196
217
|
// ---------- PBR specular (Cook-Torrance GGX) ----------
|
|
197
218
|
// A separate specular radiance is accumulated for the primary surface's DIRECT
|
|
198
219
|
// lighting alongside the demodulated diffuse irradiance. Because CompositePass
|
|
@@ -495,6 +516,12 @@ vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
|
495
516
|
vec3 hN = normalize(attr.xyz);
|
|
496
517
|
if (dot(hN, rd) > 0.0) hN = -hN;
|
|
497
518
|
vec3 hP = ro + rd * dist;
|
|
519
|
+
// Volumetric surface albedo: if this hit is the volume material, replace its
|
|
520
|
+
// flat table albedo with the 3D-texture sample at the world hit point, so GI /
|
|
521
|
+
// reflection bounces carry the field colours (matches the primary G-buffer).
|
|
522
|
+
#ifdef RT_VOLUME_ALBEDO
|
|
523
|
+
if (int(round(attr.w)) == uVolumeMatIndex) hAlbedo = sampleVolumeAlbedo(hP);
|
|
524
|
+
#endif
|
|
498
525
|
vec3 Ld = sampleOneAny(hP + hN * uEps, hN);
|
|
499
526
|
vec3 hLe = (!specular && uEmissiveCount > 0) ? vec3(0.0) : hEmissive;
|
|
500
527
|
return hLe + hAlbedo * Ld * (1.0 / PI);
|
|
@@ -1001,6 +1028,11 @@ export class RTLightingPass {
|
|
|
1001
1028
|
// RealtimeRaytracer._passClass -> coreFailure).
|
|
1002
1029
|
name: "rt:lighting",
|
|
1003
1030
|
glslVersion: THREE.GLSL3,
|
|
1031
|
+
// RT_VOLUME_ALBEDO is injected only when a scene uses world-space 3D-texture
|
|
1032
|
+
// albedo AND the GPU has a spare fragment sampler (see setVolumeAlbedo +
|
|
1033
|
+
// RealtimeRaytracer). Absent, this megakernel is textually identical to the
|
|
1034
|
+
// pre-feature build — same 16 samplers, same Metal translation.
|
|
1035
|
+
defines: {},
|
|
1004
1036
|
vertexShader: fullscreenVert,
|
|
1005
1037
|
fragmentShader: specMRT
|
|
1006
1038
|
? rtLightingFrag
|
|
@@ -1054,6 +1086,13 @@ export class RTLightingPass {
|
|
|
1054
1086
|
uSkyZenith: { value: new THREE.Color(0.18, 0.34, 0.62) },
|
|
1055
1087
|
uSkyHorizon: { value: new THREE.Color(0.7, 0.8, 0.9) },
|
|
1056
1088
|
uSkyIntensity: { value: 1.0 },
|
|
1089
|
+
// World-space 3D-texture albedo (present only in the compiled program when
|
|
1090
|
+
// the RT_VOLUME_ALBEDO define is set; harmless otherwise — three uploads
|
|
1091
|
+
// only uniforms the program actually declares).
|
|
1092
|
+
uVolumeTex: { value: null },
|
|
1093
|
+
uVolumeOrigin: { value: new THREE.Vector3() },
|
|
1094
|
+
uVolumeSize: { value: new THREE.Vector3(1, 1, 1) },
|
|
1095
|
+
uVolumeMatIndex: { value: -1 },
|
|
1057
1096
|
},
|
|
1058
1097
|
depthTest: false,
|
|
1059
1098
|
depthWrite: false,
|
|
@@ -1237,6 +1276,36 @@ export class RTLightingPass {
|
|
|
1237
1276
|
u.uEmissiveCount.value = compiled.emissiveTriCount;
|
|
1238
1277
|
}
|
|
1239
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* Enable/disable the world-space 3D-texture albedo path for the traced
|
|
1281
|
+
* SECONDARY rays (GI + reflections). Pass the compiled `volumeAlbedo` descriptor
|
|
1282
|
+
* ({ texture, origin, size, matIndex }) to turn it on, or null to turn it off.
|
|
1283
|
+
* Toggling the RT_VOLUME_ALBEDO define recompiles this megakernel (adds/removes
|
|
1284
|
+
* the single sampler3D), which the caller does at compile time, not per frame.
|
|
1285
|
+
* The caller is responsible for only enabling this when the GPU has a spare
|
|
1286
|
+
* fragment texture unit — this pass is at the 16-sampler minimum, so the 17th
|
|
1287
|
+
* sampler would fail to link on a bare-minimum WebGL2 device.
|
|
1288
|
+
*/
|
|
1289
|
+
setVolumeAlbedo(volume) {
|
|
1290
|
+
const wasOn = this.material.defines.RT_VOLUME_ALBEDO !== undefined;
|
|
1291
|
+
const on = !!volume;
|
|
1292
|
+
const u = this.material.uniforms;
|
|
1293
|
+
if (on) {
|
|
1294
|
+
u.uVolumeTex.value = volume.texture;
|
|
1295
|
+
u.uVolumeOrigin.value.copy(volume.origin);
|
|
1296
|
+
u.uVolumeSize.value.copy(volume.size);
|
|
1297
|
+
u.uVolumeMatIndex.value = volume.matIndex;
|
|
1298
|
+
} else {
|
|
1299
|
+
u.uVolumeTex.value = null;
|
|
1300
|
+
u.uVolumeMatIndex.value = -1;
|
|
1301
|
+
}
|
|
1302
|
+
if (on !== wasOn) {
|
|
1303
|
+
if (on) this.material.defines.RT_VOLUME_ALBEDO = "1";
|
|
1304
|
+
else delete this.material.defines.RT_VOLUME_ALBEDO;
|
|
1305
|
+
this.material.needsUpdate = true; // recompile with/without the sampler3D
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1240
1309
|
/**
|
|
1241
1310
|
* Renders lighting into targetA (reading targetB as irradiance history), then
|
|
1242
1311
|
* accumulates the fresh specular (targetA attachment 1) into specA (reading
|
package/src/RealtimeRaytracer.js
CHANGED
|
@@ -349,6 +349,19 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
349
349
|
static DIAG_STABLE_FRAMES = 4;
|
|
350
350
|
static DIAG_WINDOW_FRAMES = 45;
|
|
351
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
|
+
|
|
352
365
|
constructor(renderer, options = {}) {
|
|
353
366
|
this.renderer = renderer;
|
|
354
367
|
|
|
@@ -370,7 +383,7 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
370
383
|
// the RT pipeline is not operational at all; `supported` is the primary
|
|
371
384
|
// signal, but status is kept consistent so integrators can read one field.
|
|
372
385
|
this.compileError = null;
|
|
373
|
-
this.status = { ok: false, disabled: [], coreFailure: null };
|
|
386
|
+
this.status = { ok: false, disabled: [], coreFailure: null, warnings: [] };
|
|
374
387
|
this._diagDone = true;
|
|
375
388
|
return;
|
|
376
389
|
}
|
|
@@ -422,6 +435,18 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
422
435
|
"(WebKit/iOS) — specular buffer disabled, alpha-blend surfaces render opaque."
|
|
423
436
|
);
|
|
424
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
|
+
|
|
425
450
|
this.gbuffer = new GBufferPass(this._width, this._height, { mixedPrecision });
|
|
426
451
|
this.rtPass = new RTLightingPass(this._scaledW, this._scaledH, {
|
|
427
452
|
specMRT: this.specMRTSupported,
|
|
@@ -728,14 +753,25 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
728
753
|
// off to keep the image lit (e.g. taa, denoise, restir).
|
|
729
754
|
// coreFailure string | null — a core pass (gbuffer/lighting/composite)
|
|
730
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.
|
|
731
762
|
this.compileError = null;
|
|
732
|
-
this.status = { ok: true, disabled: [], coreFailure: null };
|
|
763
|
+
this.status = { ok: true, disabled: [], coreFailure: null, warnings: [] };
|
|
733
764
|
this._diagDone = false; // set once the polling window settles
|
|
734
765
|
this._diagFrames = 0; // rendered frames scanned so far
|
|
735
766
|
this._diagStable = 0; // consecutive scans with an unchanged rt:* program set
|
|
736
767
|
this._diagSig = ""; // signature of the rt:* program-name set last scan
|
|
737
768
|
this._diagHandled = new Set(); // rt:* names already acted on (warn-once)
|
|
738
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;
|
|
739
775
|
}
|
|
740
776
|
|
|
741
777
|
// Classify an rt:* pass program by how a LINK failure degrades. CORE passes
|
|
@@ -890,6 +926,102 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
890
926
|
}
|
|
891
927
|
}
|
|
892
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
|
+
|
|
893
1025
|
_makeColorTarget(width, height) {
|
|
894
1026
|
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
895
1027
|
minFilter: THREE.LinearFilter,
|
|
@@ -935,6 +1067,11 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
935
1067
|
}
|
|
936
1068
|
if (this.compiled) this.compiled.dispose();
|
|
937
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;
|
|
938
1075
|
// Emissive area lights are the noisiest direct-light path: one triangle
|
|
939
1076
|
// sample per pixel per frame, and the 1/dist^2 term spikes near a small
|
|
940
1077
|
// emitter (fireflies). ReSTIR's reservoirs are what tame this — warn when
|
|
@@ -954,10 +1091,40 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
954
1091
|
this.volumetricPass.setCompiledScene(this.compiled);
|
|
955
1092
|
this.restirPass.setCompiledScene(this.compiled);
|
|
956
1093
|
this.giReservoirPass.setCompiledScene(this.compiled);
|
|
1094
|
+
this._syncVolumeAlbedo();
|
|
957
1095
|
this.resetAccumulation();
|
|
958
1096
|
return this.compiled;
|
|
959
1097
|
}
|
|
960
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
|
+
|
|
961
1128
|
/**
|
|
962
1129
|
* Re-bake moving (dynamic) meshes into the dynamic BVH level. Call each frame
|
|
963
1130
|
* after moving them. Only the dynamic level is touched — the static BVH was
|
|
@@ -1113,10 +1280,24 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1113
1280
|
// cap the frame time can't reveal headroom, so upscaling only happens when
|
|
1114
1281
|
// frames are measurably faster than the target — it never thrashes.
|
|
1115
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
|
+
}
|
|
1116
1290
|
const now = performance.now();
|
|
1117
1291
|
const dt = this._qLastT == null ? null : now - this._qLastT;
|
|
1118
1292
|
this._qLastT = now;
|
|
1119
|
-
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;
|
|
1120
1301
|
this._qEma = this._qEma == null ? dt : this._qEma * 0.9 + dt * 0.1;
|
|
1121
1302
|
// Calmness: normally 2s between changes. When the last two steps reversed
|
|
1122
1303
|
// direction the governor is hunting the boundary, so hold for 5s AND widen
|
|
@@ -1131,6 +1312,13 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1131
1312
|
if (ratio < dbHi && ratio > dbLo) return; // comfortable — leave it alone
|
|
1132
1313
|
|
|
1133
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));
|
|
1134
1322
|
s = Math.round(Math.min(1, Math.max(0.2, s)) * 20) / 20; // 0.05 steps
|
|
1135
1323
|
|
|
1136
1324
|
// When we're fast, give back the deepest lever FIRST: restore canvas scale
|
|
@@ -1203,7 +1391,23 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1203
1391
|
}
|
|
1204
1392
|
if (this.adaptiveQuality) this._adaptQuality();
|
|
1205
1393
|
if (this.overloadProtection) this._overloadBrake();
|
|
1206
|
-
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
|
+
}
|
|
1207
1411
|
// Still nothing to trace (empty scene — tracer built before meshes were
|
|
1208
1412
|
// added). Show the user's raster scene rather than crashing or rendering
|
|
1209
1413
|
// black; the pipeline picks up automatically once compileScene() succeeds.
|
|
@@ -1213,6 +1417,8 @@ uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
|
1213
1417
|
}
|
|
1214
1418
|
|
|
1215
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();
|
|
1216
1422
|
camera.updateMatrixWorld();
|
|
1217
1423
|
|
|
1218
1424
|
// --- sub-pixel jitter (TAA): offset the projection a fraction of a pixel
|