three-realtime-rt 0.4.0 → 0.4.2
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/package.json +1 -1
- package/src/RTLightingPass.js +104 -49
- package/src/RealtimeRaytracer.js +82 -2
- package/src/index.d.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-realtime-rt",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
package/src/RTLightingPass.js
CHANGED
|
@@ -634,11 +634,29 @@ void main() {
|
|
|
634
634
|
// (unbiased) while GI's ray cost halves; accumulation + denoise absorb
|
|
635
635
|
// the alternation.
|
|
636
636
|
gWantSpec = false; // secondary bounces contribute to diffuse GI only
|
|
637
|
+
// BLEND pixels reuse THIS call site as their straight-through view
|
|
638
|
+
// continuation instead of a GI bounce (their behind-image rides the specular
|
|
639
|
+
// attachment; the pane forgoes its own GI bounce — visually negligible, and
|
|
640
|
+
// it saves a ray). CRITICAL CALL-SITE BUDGET: traceRadiance may appear at
|
|
641
|
+
// most THREE times in this shader (glass refraction exit, this unified
|
|
642
|
+
// secondary site, the metal-reflection path). WebKit's GLSL->Metal
|
|
643
|
+
// translation silently emits a broken program at a FOURTH inlined call site
|
|
644
|
+
// (clean compile, black output on every iOS browser) — bisected live on an
|
|
645
|
+
// iPad, 2026-07-22. Never add a call site; extend this one.
|
|
637
646
|
vec3 indirect = vec3(0.0);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
647
|
+
vec3 blendBehind = vec3(0.0);
|
|
648
|
+
bool wantBehind = uBlendEnabled && blend;
|
|
649
|
+
bool wantGI = uGIEnabled && !wantBehind
|
|
650
|
+
&& (!uGIHalfRate || (((px.x + px.y + int(uFrame)) & 1) == 0));
|
|
651
|
+
if (wantBehind || wantGI) {
|
|
652
|
+
vec3 Vv = normalize(P - uCameraPos);
|
|
653
|
+
vec3 dir = wantBehind ? Vv : cosineSampleHemisphere(N, rand2());
|
|
654
|
+
vec3 org = wantBehind ? P + Vv * uEps : P + N * uEps;
|
|
655
|
+
vec3 r = traceRadiance(org, dir, wantBehind);
|
|
656
|
+
if (wantBehind) {
|
|
657
|
+
blendBehind = r;
|
|
658
|
+
} else {
|
|
659
|
+
indirect = r;
|
|
642
660
|
if (uGIHalfRate) indirect *= 2.0;
|
|
643
661
|
}
|
|
644
662
|
}
|
|
@@ -685,11 +703,8 @@ void main() {
|
|
|
685
703
|
// where the pane's albedo is actually available. sampleIrr keeps only the
|
|
686
704
|
// pane's own surface lighting, which is static on the surface and accumulates
|
|
687
705
|
// with normal full-length history.
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
vec3 V = normalize(P - uCameraPos);
|
|
691
|
-
blendBehind = traceRadiance(P + V * uEps, V, true);
|
|
692
|
-
}
|
|
706
|
+
// (The straight-through trace itself happens at the unified secondary-ray
|
|
707
|
+
// call site above — see the Metal call-site-count note there.)
|
|
693
708
|
|
|
694
709
|
// A single NaN/Inf sample would poison the EMA history for good (mix() with
|
|
695
710
|
// NaN stays NaN until a disocclusion resets the pixel) — sanitize first.
|
|
@@ -854,18 +869,31 @@ void main() {
|
|
|
854
869
|
* into a second ping-pong pair, specA/specB).
|
|
855
870
|
*/
|
|
856
871
|
export class RTLightingPass {
|
|
857
|
-
|
|
872
|
+
// `specMRT: false` is the WebKit fallback: iOS Safari (every iOS browser)
|
|
873
|
+
// silently fails the 2-attachment half-float MRT draw — the whole lighting
|
|
874
|
+
// pass renders black. RealtimeRaytracer probes this functionally at
|
|
875
|
+
// construction; in fallback mode this pass allocates a single-attachment
|
|
876
|
+
// target exactly like 0.3.x, the shader's second output collapses to a dead
|
|
877
|
+
// local variable, and render() returns { specular: null } (the caller then
|
|
878
|
+
// runs with specular off; blend surfaces degrade to opaque as documented).
|
|
879
|
+
constructor(width, height, { specMRT = true } = {}) {
|
|
880
|
+
this.specMRT = specMRT;
|
|
858
881
|
this.targetA = this._makeTarget(width, height);
|
|
859
882
|
this.targetB = this._makeTarget(width, height);
|
|
860
883
|
// Accumulated specular history (ping-pong), fed by the fresh specular in
|
|
861
884
|
// targetA/B attachment 1.
|
|
862
|
-
this.specA = this._makeSpecTarget(width, height);
|
|
863
|
-
this.specB = this._makeSpecTarget(width, height);
|
|
885
|
+
this.specA = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
886
|
+
this.specB = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
864
887
|
|
|
865
888
|
this.material = new THREE.ShaderMaterial({
|
|
866
889
|
glslVersion: THREE.GLSL3,
|
|
867
890
|
vertexShader: fullscreenVert,
|
|
868
|
-
fragmentShader:
|
|
891
|
+
fragmentShader: specMRT
|
|
892
|
+
? rtLightingFrag
|
|
893
|
+
: rtLightingFrag.replace(
|
|
894
|
+
"layout(location = 1) out vec4 outSpecular;",
|
|
895
|
+
"vec4 outSpecular; // single-target fallback: dead store"
|
|
896
|
+
),
|
|
869
897
|
uniforms: {
|
|
870
898
|
bvhStatic: { value: null },
|
|
871
899
|
bvhDynamic: { value: null },
|
|
@@ -936,11 +964,18 @@ export class RTLightingPass {
|
|
|
936
964
|
depthWrite: false,
|
|
937
965
|
});
|
|
938
966
|
|
|
939
|
-
// 2-output history carry (see mrtCarryFrag) — matches the MRT's draw
|
|
967
|
+
// 2-output history carry (see mrtCarryFrag) — matches the MRT's draw
|
|
968
|
+
// buffers. In single-target fallback the second output collapses the same
|
|
969
|
+
// way as the lighting shader's.
|
|
940
970
|
this.carryMaterial = new THREE.ShaderMaterial({
|
|
941
971
|
glslVersion: THREE.GLSL3,
|
|
942
972
|
vertexShader: fullscreenVert,
|
|
943
|
-
fragmentShader:
|
|
973
|
+
fragmentShader: specMRT
|
|
974
|
+
? mrtCarryFrag
|
|
975
|
+
: mrtCarryFrag.replace(
|
|
976
|
+
"layout(location = 1) out vec4 o1;",
|
|
977
|
+
"vec4 o1; // single-target fallback: dead store"
|
|
978
|
+
),
|
|
944
979
|
uniforms: { uTex: { value: null }, uCountClamp: { value: -1 } },
|
|
945
980
|
depthTest: false,
|
|
946
981
|
depthWrite: false,
|
|
@@ -957,19 +992,31 @@ export class RTLightingPass {
|
|
|
957
992
|
// Half-float + linear: history is sampled bilinearly at reprojected UVs,
|
|
958
993
|
// and fp16 halves the bandwidth (EMA blending never accumulates a raw sum,
|
|
959
994
|
// so fp16 precision is sufficient). Two attachments: [0] irradiance,
|
|
960
|
-
// [1] fresh dielectric specular
|
|
961
|
-
|
|
995
|
+
// [1] fresh dielectric specular — or a single attachment in the WebKit
|
|
996
|
+
// fallback (see the constructor note).
|
|
997
|
+
const opts = {
|
|
962
998
|
minFilter: THREE.LinearFilter,
|
|
963
999
|
magFilter: THREE.LinearFilter,
|
|
964
1000
|
format: THREE.RGBAFormat,
|
|
965
1001
|
type: THREE.HalfFloatType,
|
|
966
1002
|
depthBuffer: false,
|
|
967
1003
|
stencilBuffer: false,
|
|
968
|
-
}
|
|
1004
|
+
};
|
|
1005
|
+
if (!this.specMRT) {
|
|
1006
|
+
const t = new THREE.WebGLRenderTarget(width, height, opts);
|
|
1007
|
+
t.texture.generateMipmaps = false;
|
|
1008
|
+
return t;
|
|
1009
|
+
}
|
|
1010
|
+
const t = new THREE.WebGLMultipleRenderTargets(width, height, 2, opts);
|
|
969
1011
|
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
970
1012
|
return t;
|
|
971
1013
|
}
|
|
972
1014
|
|
|
1015
|
+
// The accumulated-irradiance texture of a history target, either layout.
|
|
1016
|
+
_irrTex(target) {
|
|
1017
|
+
return this.specMRT ? target.texture[0] : target.texture;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
973
1020
|
_makeSpecTarget(width, height) {
|
|
974
1021
|
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
975
1022
|
minFilter: THREE.LinearFilter,
|
|
@@ -991,6 +1038,7 @@ export class RTLightingPass {
|
|
|
991
1038
|
const prevAlpha = renderer.getClearAlpha();
|
|
992
1039
|
renderer.setClearColor(0x000000, 0);
|
|
993
1040
|
for (const t of [this.targetA, this.targetB, this.specA, this.specB]) {
|
|
1041
|
+
if (!t) continue; // spec pair is absent in the single-target fallback
|
|
994
1042
|
renderer.setRenderTarget(t);
|
|
995
1043
|
renderer.clear(true, false, false);
|
|
996
1044
|
}
|
|
@@ -1001,8 +1049,8 @@ export class RTLightingPass {
|
|
|
1001
1049
|
setSize(width, height) {
|
|
1002
1050
|
this.targetA.setSize(width, height);
|
|
1003
1051
|
this.targetB.setSize(width, height);
|
|
1004
|
-
this.specA.setSize(width, height);
|
|
1005
|
-
this.specB.setSize(width, height);
|
|
1052
|
+
if (this.specA) this.specA.setSize(width, height);
|
|
1053
|
+
if (this.specB) this.specB.setSize(width, height);
|
|
1006
1054
|
}
|
|
1007
1055
|
|
|
1008
1056
|
/**
|
|
@@ -1025,7 +1073,7 @@ export class RTLightingPass {
|
|
|
1025
1073
|
// material so the draw matches the MRT's draw buffers (a 1-output CopyPass
|
|
1026
1074
|
// blit here is INVALID_OPERATION on ANGLE/D3D11). Attachment 1 is fresh-
|
|
1027
1075
|
// written every frame, so it needs no carry.
|
|
1028
|
-
this.carryMaterial.uniforms.uTex.value = this.targetB
|
|
1076
|
+
this.carryMaterial.uniforms.uTex.value = this._irrTex(this.targetB);
|
|
1029
1077
|
this.carryMaterial.uniforms.uCountClamp.value = carryFrames;
|
|
1030
1078
|
this.quad.material = this.carryMaterial;
|
|
1031
1079
|
const prev = renderer.getRenderTarget();
|
|
@@ -1039,13 +1087,15 @@ export class RTLightingPass {
|
|
|
1039
1087
|
this.targetB = newB;
|
|
1040
1088
|
|
|
1041
1089
|
// Specular history carries the same way (freshest is specB — see render()).
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1090
|
+
if (this.specMRT) {
|
|
1091
|
+
const newSpecA = this._makeSpecTarget(width, height);
|
|
1092
|
+
const newSpecB = this._makeSpecTarget(width, height);
|
|
1093
|
+
copyPass.blit(renderer, this.specB.texture, newSpecB, carryFrames);
|
|
1094
|
+
this.specA.dispose();
|
|
1095
|
+
this.specB.dispose();
|
|
1096
|
+
this.specA = newSpecA;
|
|
1097
|
+
this.specB = newSpecB;
|
|
1098
|
+
}
|
|
1049
1099
|
}
|
|
1050
1100
|
|
|
1051
1101
|
setCompiledScene(compiled) {
|
|
@@ -1074,7 +1124,7 @@ export class RTLightingPass {
|
|
|
1074
1124
|
u.uGWorldPos.value = gbuffer.worldPos;
|
|
1075
1125
|
u.uGNormalMetal.value = gbuffer.normalMetal;
|
|
1076
1126
|
u.uPrevGWorldPos.value = gbuffer.prevWorldPos;
|
|
1077
|
-
u.uPrevAccum.value = this.targetB
|
|
1127
|
+
u.uPrevAccum.value = this._irrTex(this.targetB);
|
|
1078
1128
|
u.uReservoir.value = reservoirTexture;
|
|
1079
1129
|
u.uRestirEnabled.value = reservoirTexture !== null;
|
|
1080
1130
|
u.uFrame.value = frame;
|
|
@@ -1085,37 +1135,42 @@ export class RTLightingPass {
|
|
|
1085
1135
|
renderer.render(this.scene, this.camera);
|
|
1086
1136
|
|
|
1087
1137
|
// 2. specular temporal accumulation: fresh (targetA[1]) + history (specB).
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1138
|
+
// Skipped entirely in the single-target fallback — there is no fresh
|
|
1139
|
+
// specular attachment to accumulate.
|
|
1140
|
+
let outSpec = null;
|
|
1141
|
+
if (this.specMRT) {
|
|
1142
|
+
const su = this.specMaterial.uniforms;
|
|
1143
|
+
su.uFreshSpec.value = this.targetA.texture[1];
|
|
1144
|
+
su.uPrevSpec.value = this.specB.texture;
|
|
1145
|
+
su.uGWorldPos.value = gbuffer.worldPos;
|
|
1146
|
+
su.uGNormalMetal.value = gbuffer.normalMetal;
|
|
1147
|
+
su.uPrevGWorldPos.value = gbuffer.prevWorldPos;
|
|
1148
|
+
su.uPrevViewProj.value.copy(u.uPrevViewProj.value);
|
|
1149
|
+
su.uViewProj.value.copy(u.uViewProj.value);
|
|
1150
|
+
su.uCameraPos.value.copy(u.uCameraPos.value);
|
|
1151
|
+
su.uEps.value = u.uEps.value;
|
|
1152
|
+
su.uMaxHistory.value = u.uMaxHistory.value;
|
|
1153
|
+
su.uTemporalReprojection.value = u.uTemporalReprojection.value;
|
|
1154
|
+
this.quad.material = this.specMaterial;
|
|
1155
|
+
renderer.setRenderTarget(this.specA);
|
|
1156
|
+
renderer.render(this.scene, this.camera);
|
|
1157
|
+
outSpec = this.specA.texture;
|
|
1158
|
+
}
|
|
1103
1159
|
|
|
1104
1160
|
this.quad.material = this.material; // restore for the next caller
|
|
1105
1161
|
renderer.setRenderTarget(null);
|
|
1106
1162
|
|
|
1107
|
-
const outIrr = this.targetA
|
|
1108
|
-
const outSpec = this.specA.texture;
|
|
1163
|
+
const outIrr = this._irrTex(this.targetA);
|
|
1109
1164
|
[this.targetA, this.targetB] = [this.targetB, this.targetA];
|
|
1110
|
-
[this.specA, this.specB] = [this.specB, this.specA];
|
|
1165
|
+
if (this.specMRT) [this.specA, this.specB] = [this.specB, this.specA];
|
|
1111
1166
|
return { irradiance: outIrr, specular: outSpec };
|
|
1112
1167
|
}
|
|
1113
1168
|
|
|
1114
1169
|
dispose() {
|
|
1115
1170
|
this.targetA.dispose();
|
|
1116
1171
|
this.targetB.dispose();
|
|
1117
|
-
this.specA.dispose();
|
|
1118
|
-
this.specB.dispose();
|
|
1172
|
+
if (this.specA) this.specA.dispose();
|
|
1173
|
+
if (this.specB) this.specB.dispose();
|
|
1119
1174
|
this.material.dispose();
|
|
1120
1175
|
this.specMaterial.dispose();
|
|
1121
1176
|
this.carryMaterial.dispose();
|
package/src/RealtimeRaytracer.js
CHANGED
|
@@ -242,6 +242,72 @@ export class RealtimeRaytracer {
|
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/**
|
|
246
|
+
* FUNCTIONAL probe: can this context actually DRAW into a 2-attachment
|
|
247
|
+
* half-float MRT? A checkFramebufferStatus probe is not enough — WebKit
|
|
248
|
+
* (every iOS browser) reports the framebuffer complete and then renders
|
|
249
|
+
* black, which killed the whole lighting pass on iPhone/iPad in 0.4.0.
|
|
250
|
+
* So: render one 2-output quad into a tiny fp16 MRT, resolve attachment 0
|
|
251
|
+
* into an RGBA8 target through a sampler, and read the pixel back. Only a
|
|
252
|
+
* round-trip that returns the written value counts as support.
|
|
253
|
+
*/
|
|
254
|
+
static _specMrtSupported(renderer) {
|
|
255
|
+
let mrt, out, mat, copy, quad, scene2, cam;
|
|
256
|
+
const prevTarget = renderer.getRenderTarget();
|
|
257
|
+
try {
|
|
258
|
+
mrt = new THREE.WebGLMultipleRenderTargets(2, 2, 2, {
|
|
259
|
+
format: THREE.RGBAFormat,
|
|
260
|
+
type: THREE.HalfFloatType,
|
|
261
|
+
depthBuffer: false,
|
|
262
|
+
stencilBuffer: false,
|
|
263
|
+
});
|
|
264
|
+
for (const tex of mrt.texture) tex.generateMipmaps = false;
|
|
265
|
+
out = new THREE.WebGLRenderTarget(2, 2, { depthBuffer: false, stencilBuffer: false });
|
|
266
|
+
const vert = `out vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`;
|
|
267
|
+
mat = new THREE.ShaderMaterial({
|
|
268
|
+
glslVersion: THREE.GLSL3,
|
|
269
|
+
vertexShader: vert,
|
|
270
|
+
fragmentShader: `precision highp float;
|
|
271
|
+
layout(location = 0) out vec4 o0; layout(location = 1) out vec4 o1;
|
|
272
|
+
void main(){ o0 = vec4(0.5, 0.25, 0.75, 1.0); o1 = vec4(0.125); }`,
|
|
273
|
+
depthTest: false,
|
|
274
|
+
depthWrite: false,
|
|
275
|
+
});
|
|
276
|
+
copy = new THREE.ShaderMaterial({
|
|
277
|
+
glslVersion: THREE.GLSL3,
|
|
278
|
+
vertexShader: vert,
|
|
279
|
+
fragmentShader: `precision highp float; in vec2 vUv; out vec4 outColor;
|
|
280
|
+
uniform sampler2D uTex; void main(){ outColor = texture(uTex, vUv); }`,
|
|
281
|
+
uniforms: { uTex: { value: mrt.texture[0] } },
|
|
282
|
+
depthTest: false,
|
|
283
|
+
depthWrite: false,
|
|
284
|
+
});
|
|
285
|
+
scene2 = new THREE.Scene();
|
|
286
|
+
cam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
287
|
+
quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat);
|
|
288
|
+
quad.frustumCulled = false;
|
|
289
|
+
scene2.add(quad);
|
|
290
|
+
renderer.setRenderTarget(mrt);
|
|
291
|
+
renderer.render(scene2, cam);
|
|
292
|
+
quad.material = copy;
|
|
293
|
+
renderer.setRenderTarget(out);
|
|
294
|
+
renderer.render(scene2, cam);
|
|
295
|
+
const px = new Uint8Array(4);
|
|
296
|
+
renderer.readRenderTargetPixels(out, 0, 0, 1, 1, px);
|
|
297
|
+
// 0.5 -> ~128; accept a generous tolerance (fp16 + dithering).
|
|
298
|
+
return Math.abs(px[0] - 128) < 24 && Math.abs(px[1] - 64) < 24;
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
} finally {
|
|
302
|
+
renderer.setRenderTarget(prevTarget);
|
|
303
|
+
if (quad) quad.geometry.dispose();
|
|
304
|
+
if (mat) mat.dispose();
|
|
305
|
+
if (copy) copy.dispose();
|
|
306
|
+
if (mrt) mrt.dispose();
|
|
307
|
+
if (out) out.dispose();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
245
311
|
/**
|
|
246
312
|
* Companion settings for a given lighting resolution. LOW resolution wants
|
|
247
313
|
* MORE denoise passes, not fewer — the filter runs at lighting res so extra
|
|
@@ -325,8 +391,22 @@ export class RealtimeRaytracer {
|
|
|
325
391
|
if (!mixedPrecision) {
|
|
326
392
|
console.info("three-realtime-rt: mixed fp16/fp32 G-buffer not supported here — using fp32 for all targets.");
|
|
327
393
|
}
|
|
394
|
+
// Functional probe, not just a status check: WebKit (all iOS browsers)
|
|
395
|
+
// reports the 2-attachment fp16 MRT complete but silently renders black,
|
|
396
|
+
// which blanks the whole lighting pass. On such devices the lighting pass
|
|
397
|
+
// runs single-attachment (0.3.x layout): the specular buffer is disabled
|
|
398
|
+
// and blend surfaces degrade to opaque — everything else keeps working.
|
|
399
|
+
this.specMRTSupported = RealtimeRaytracer._specMrtSupported(renderer);
|
|
400
|
+
if (!this.specMRTSupported) {
|
|
401
|
+
console.info(
|
|
402
|
+
"three-realtime-rt: multi-attachment lighting buffer failed the draw probe here " +
|
|
403
|
+
"(WebKit/iOS) — specular buffer disabled, alpha-blend surfaces render opaque."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
328
406
|
this.gbuffer = new GBufferPass(this._width, this._height, { mixedPrecision });
|
|
329
|
-
this.rtPass = new RTLightingPass(this._scaledW, this._scaledH
|
|
407
|
+
this.rtPass = new RTLightingPass(this._scaledW, this._scaledH, {
|
|
408
|
+
specMRT: this.specMRTSupported,
|
|
409
|
+
});
|
|
330
410
|
this.denoisePass = new DenoisePass(this._scaledW, this._scaledH);
|
|
331
411
|
// Separate à-trous instance for the specular buffer (its own ping-pong
|
|
332
412
|
// targets, so the specular denoise cannot clobber the irradiance result).
|
|
@@ -343,7 +423,7 @@ export class RealtimeRaytracer {
|
|
|
343
423
|
this.compiled = null;
|
|
344
424
|
this.frame = 0;
|
|
345
425
|
|
|
346
|
-
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive */
|
|
426
|
+
/** Debug view: 0 composite, 1 albedo, 2 normal, 3 irradiance, 4 worldPos, 5 emissive, 6 specular */
|
|
347
427
|
this.outputMode = 0;
|
|
348
428
|
/** Environment (sky) color used for GI rays that miss + composite background. */
|
|
349
429
|
this.envColor = options.envColor ?? new THREE.Color(0.03, 0.04, 0.06);
|
package/src/index.d.ts
CHANGED
|
@@ -288,6 +288,12 @@ export class RealtimeRaytracer {
|
|
|
288
288
|
renderer: WebGLRenderer;
|
|
289
289
|
/** False when the platform can't run the tracer; render() then forwards to renderer.render. */
|
|
290
290
|
supported: boolean;
|
|
291
|
+
/**
|
|
292
|
+
* False when the 2-attachment lighting buffer fails the construction-time
|
|
293
|
+
* draw probe (WebKit/iOS): the specular buffer is disabled there and
|
|
294
|
+
* alpha-blend surfaces render opaque; everything else keeps working.
|
|
295
|
+
*/
|
|
296
|
+
specMRTSupported: boolean;
|
|
291
297
|
/** The current compiled scene, or null before the first compile / when unsupported. */
|
|
292
298
|
compiled: CompiledScene | null;
|
|
293
299
|
/** Accumulated frame counter. */
|