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/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
|
|
@@ -85,6 +86,20 @@ uniform bool uHasMetalnessMap;
|
|
|
85
86
|
uniform bool uBlend;
|
|
86
87
|
uniform float uOpacity;
|
|
87
88
|
|
|
89
|
+
// World-space 3D-texture albedo ("volumetric surface albedo"), compiled in ONLY
|
|
90
|
+
// when a scene registers a material with userData.rtVolumeAlbedo (the whole block
|
|
91
|
+
// is behind the RT_VOLUME_ALBEDO define, so a scene without the feature builds a
|
|
92
|
+
// byte-identical G-buffer program with no extra sampler). Primary visibility can
|
|
93
|
+
// afford this sampler3D — the raster pass has ample sampler headroom, unlike the
|
|
94
|
+
// lighting megakernel. The value is gated per-mesh by uHasVolume, so non-volume
|
|
95
|
+
// meshes sharing this program write exactly the same albedo they did before.
|
|
96
|
+
#ifdef RT_VOLUME_ALBEDO
|
|
97
|
+
uniform highp sampler3D uVolumeTex;
|
|
98
|
+
uniform vec3 uVolumeOrigin;
|
|
99
|
+
uniform vec3 uVolumeSize;
|
|
100
|
+
uniform bool uHasVolume;
|
|
101
|
+
#endif
|
|
102
|
+
|
|
88
103
|
// Screen-space cotangent frame (Mikkelsen 2010): reconstruct a tangent basis
|
|
89
104
|
// from derivatives of world position and uv, so tangent-space normal maps work
|
|
90
105
|
// without a per-vertex tangent attribute (none is uploaded to the BVH/G-buffer).
|
|
@@ -105,6 +120,17 @@ void main() {
|
|
|
105
120
|
albedo *= texture(uMap, vUvCoord).rgb;
|
|
106
121
|
}
|
|
107
122
|
albedo *= vColor; // vertex colours (white when the mesh has no color attribute)
|
|
123
|
+
// Volumetric surface albedo: sample a world-space 3D texture at this fragment's
|
|
124
|
+
// world position and use it as the base colour, replacing color x map x vColor.
|
|
125
|
+
// uvw = clamp((p - origin) / size, 0, 1); the ClampToEdge sampler + this clamp
|
|
126
|
+
// keep hits just outside the volume reading the boundary colour instead of
|
|
127
|
+
// wrapping. Gated so only rtVolumeAlbedo meshes are affected.
|
|
128
|
+
#ifdef RT_VOLUME_ALBEDO
|
|
129
|
+
if (uHasVolume) {
|
|
130
|
+
vec3 uvw = clamp((vWorldPos - uVolumeOrigin) / uVolumeSize, 0.0, 1.0);
|
|
131
|
+
albedo = texture(uVolumeTex, uvw).rgb;
|
|
132
|
+
}
|
|
133
|
+
#endif
|
|
108
134
|
vec3 emissive = uEmissive;
|
|
109
135
|
if (uHasEmissiveMap) {
|
|
110
136
|
emissive *= texture(uEmissiveMap, vUvCoord).rgb;
|
|
@@ -186,11 +212,48 @@ export class GBufferPass {
|
|
|
186
212
|
|
|
187
213
|
this._materialCache = new WeakMap(); // mesh -> gbuffer ShaderMaterial
|
|
188
214
|
this._swapped = []; // [mesh, originalMaterial] pairs during render
|
|
215
|
+
this._hidden = []; // objects temporarily hidden for the G-buffer draw
|
|
189
216
|
this._normalMat3 = new THREE.Matrix3();
|
|
217
|
+
// World-space 3D-texture albedo: off unless a scene registers a material with
|
|
218
|
+
// userData.rtVolumeAlbedo (see setVolume). When off, the gbuffer program is
|
|
219
|
+
// compiled WITHOUT the RT_VOLUME_ALBEDO define — no sampler3D, byte-identical.
|
|
220
|
+
this._volumeEnabled = false;
|
|
221
|
+
this._dummyVolumeTex = null; // 1x1x1 fallback bound to non-volume meshes when on
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// A valid 1x1x1 3D texture to bind on gbuffer materials whose mesh is NOT a
|
|
225
|
+
// volume material while the feature is compiled in — keeps every declared
|
|
226
|
+
// sampler3D pointing at a complete texture (its branch is never taken, so the
|
|
227
|
+
// value is irrelevant; it just must not be an unbound/incomplete sampler).
|
|
228
|
+
_dummyVolume() {
|
|
229
|
+
if (!this._dummyVolumeTex) {
|
|
230
|
+
const t = new THREE.Data3DTexture(new Uint8Array([255, 255, 255, 255]), 1, 1, 1);
|
|
231
|
+
t.format = THREE.RGBAFormat;
|
|
232
|
+
t.type = THREE.UnsignedByteType;
|
|
233
|
+
t.minFilter = THREE.LinearFilter;
|
|
234
|
+
t.magFilter = THREE.LinearFilter;
|
|
235
|
+
t.needsUpdate = true;
|
|
236
|
+
this._dummyVolumeTex = t;
|
|
237
|
+
}
|
|
238
|
+
return this._dummyVolumeTex;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Enable/disable the world-space 3D-texture albedo path for the primary
|
|
243
|
+
* (G-buffer) visibility. Compiles the RT_VOLUME_ALBEDO define into the per-mesh
|
|
244
|
+
* gbuffer program when on. Toggling clears the material cache so the programs
|
|
245
|
+
* recompile with/without the define. Called by RealtimeRaytracer after each
|
|
246
|
+
* compile with whether the scene registered any rtVolumeAlbedo material.
|
|
247
|
+
*/
|
|
248
|
+
setVolume(enabled) {
|
|
249
|
+
const on = !!enabled;
|
|
250
|
+
if (on === this._volumeEnabled) return;
|
|
251
|
+
this._volumeEnabled = on;
|
|
252
|
+
this._materialCache = new WeakMap(); // force recompile with the new define
|
|
190
253
|
}
|
|
191
254
|
|
|
192
255
|
_makeTarget(width, height) {
|
|
193
|
-
const t =
|
|
256
|
+
const t = makeMRT(width, height, 4, {
|
|
194
257
|
minFilter: THREE.NearestFilter,
|
|
195
258
|
magFilter: THREE.NearestFilter,
|
|
196
259
|
type: THREE.FloatType,
|
|
@@ -240,7 +303,15 @@ export class GBufferPass {
|
|
|
240
303
|
|
|
241
304
|
_makeGbufferMaterial(mesh) {
|
|
242
305
|
const material = new THREE.ShaderMaterial({
|
|
306
|
+
// Stable program name for the compile-failure self-diagnosis (see
|
|
307
|
+
// RealtimeRaytracer._scanPrograms). Per-mesh materials share one program
|
|
308
|
+
// cache key, so they all surface as the single core pass "rt:gbuffer".
|
|
309
|
+
name: "rt:gbuffer",
|
|
243
310
|
glslVersion: THREE.GLSL3,
|
|
311
|
+
// RT_VOLUME_ALBEDO is present only while a scene uses the volumetric-albedo
|
|
312
|
+
// feature (see setVolume); absent, the compiled program is identical to the
|
|
313
|
+
// pre-feature G-buffer (no sampler3D, no volume branch).
|
|
314
|
+
defines: this._volumeEnabled ? { RT_VOLUME_ALBEDO: "1" } : {},
|
|
244
315
|
vertexShader: gbufferVert,
|
|
245
316
|
fragmentShader: gbufferFrag,
|
|
246
317
|
uniforms: {
|
|
@@ -264,6 +335,16 @@ export class GBufferPass {
|
|
|
264
335
|
uHasMetalnessMap: { value: false },
|
|
265
336
|
uBlend: { value: false },
|
|
266
337
|
uOpacity: { value: 1.0 },
|
|
338
|
+
// Volume-albedo uniforms are always present in the JS uniform object
|
|
339
|
+
// (harmless when the define is off — three uploads only uniforms that
|
|
340
|
+
// exist in the compiled program, so a non-volume scene never touches
|
|
341
|
+
// these). While the define is on, _syncGbufferMaterial binds either the
|
|
342
|
+
// material's own volume texture or the shared dummy, so a non-volume mesh
|
|
343
|
+
// never leaves an incomplete sampler3D bound.
|
|
344
|
+
uVolumeTex: { value: null },
|
|
345
|
+
uVolumeOrigin: { value: new THREE.Vector3() },
|
|
346
|
+
uVolumeSize: { value: new THREE.Vector3(1, 1, 1) },
|
|
347
|
+
uHasVolume: { value: false },
|
|
267
348
|
},
|
|
268
349
|
side: THREE.FrontSide,
|
|
269
350
|
});
|
|
@@ -309,6 +390,23 @@ export class GBufferPass {
|
|
|
309
390
|
// lighting pass. opacity 1 renders opaque, matching the old force-opaque path.
|
|
310
391
|
u.uBlend.value = !!src.transparent;
|
|
311
392
|
u.uOpacity.value = src.opacity ?? 1.0;
|
|
393
|
+
// World-space 3D-texture albedo. Only meshes whose material opted in via
|
|
394
|
+
// userData.rtVolumeAlbedo get uHasVolume=true; every other mesh keeps the
|
|
395
|
+
// dummy sampler (branch never taken) so its albedo is byte-identical. This is
|
|
396
|
+
// per-mesh, so DISTINCT volumes each render correctly in primary visibility.
|
|
397
|
+
if (this._volumeEnabled) {
|
|
398
|
+
const vol = src.userData && src.userData.rtVolumeAlbedo;
|
|
399
|
+
if (vol && vol.texture) {
|
|
400
|
+
u.uHasVolume.value = true;
|
|
401
|
+
u.uVolumeTex.value = vol.texture;
|
|
402
|
+
u.uVolumeOrigin.value.copy(vol.origin ?? { x: 0, y: 0, z: 0 });
|
|
403
|
+
const s = vol.size ?? { x: 1, y: 1, z: 1 };
|
|
404
|
+
u.uVolumeSize.value.set(s.x || 1, s.y || 1, s.z || 1);
|
|
405
|
+
} else {
|
|
406
|
+
u.uHasVolume.value = false;
|
|
407
|
+
u.uVolumeTex.value = this._dummyVolume();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
312
410
|
u.uNormalMatrixWorld.value.getNormalMatrix(mesh.matrixWorld);
|
|
313
411
|
material.side = src.side ?? THREE.FrontSide;
|
|
314
412
|
}
|
|
@@ -348,11 +446,27 @@ export class GBufferPass {
|
|
|
348
446
|
// material word + gEmissive.a, and the lighting pass blends them against the
|
|
349
447
|
// geometry behind. opacity 1 writes fully opaque, so alpha-textured cases
|
|
350
448
|
// (LittlestTokyo's glass) look exactly as before.
|
|
449
|
+
//
|
|
450
|
+
// Objects that are NOT meshes (Sprite / Line / Points) keep their OWN
|
|
451
|
+
// material here, and those materials write a single gl_FragColor. Rendering
|
|
452
|
+
// one into this 4-attachment MRT framebuffer is a GL_INVALID_OPERATION (an
|
|
453
|
+
// ESSL1 fragment shader cannot feed multiple draw buffers) and the object is
|
|
454
|
+
// not traceable geometry either — the BVH compiler skips it. So they are
|
|
455
|
+
// HIDDEN for the duration of the G-buffer draw and restored right after;
|
|
456
|
+
// draw them yourself in an overlay pass on top of rt.render() if you need
|
|
457
|
+
// them on screen. compileScene() warns once naming them.
|
|
351
458
|
this._swapped.length = 0;
|
|
459
|
+
this._hidden.length = 0;
|
|
352
460
|
scene.traverse((obj) => {
|
|
353
|
-
if (obj.
|
|
461
|
+
if (!obj.visible) return;
|
|
462
|
+
if (obj.isMesh && obj.geometry) {
|
|
354
463
|
this._swapped.push([obj, obj.material]);
|
|
355
464
|
obj.material = this._gbufferMaterialFor(obj);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (obj.isSprite || obj.isLine || obj.isPoints) {
|
|
468
|
+
obj.visible = false;
|
|
469
|
+
this._hidden.push(obj);
|
|
356
470
|
}
|
|
357
471
|
});
|
|
358
472
|
|
|
@@ -368,9 +482,14 @@ export class GBufferPass {
|
|
|
368
482
|
scene.background = prevBackground;
|
|
369
483
|
for (const [mesh, mat] of this._swapped) mesh.material = mat;
|
|
370
484
|
this._swapped.length = 0;
|
|
485
|
+
// Restore visibility exactly once per object (each was pushed once, and only
|
|
486
|
+
// if it was visible when we hid it).
|
|
487
|
+
for (const obj of this._hidden) obj.visible = true;
|
|
488
|
+
this._hidden.length = 0;
|
|
371
489
|
}
|
|
372
490
|
|
|
373
491
|
dispose() {
|
|
374
492
|
for (const t of this._targets) t.dispose();
|
|
493
|
+
if (this._dummyVolumeTex) this._dummyVolumeTex.dispose();
|
|
375
494
|
}
|
|
376
495
|
}
|
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;
|
|
@@ -192,6 +193,27 @@ void fetchMaterial(float matIndex, out vec3 albedo, out float roughness,
|
|
|
192
193
|
metalness = t1.a;
|
|
193
194
|
}
|
|
194
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
|
+
|
|
195
217
|
// ---------- PBR specular (Cook-Torrance GGX) ----------
|
|
196
218
|
// A separate specular radiance is accumulated for the primary surface's DIRECT
|
|
197
219
|
// lighting alongside the demodulated diffuse irradiance. Because CompositePass
|
|
@@ -494,6 +516,12 @@ vec3 traceRadiance(vec3 ro, vec3 rd, bool specular) {
|
|
|
494
516
|
vec3 hN = normalize(attr.xyz);
|
|
495
517
|
if (dot(hN, rd) > 0.0) hN = -hN;
|
|
496
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
|
|
497
525
|
vec3 Ld = sampleOneAny(hP + hN * uEps, hN);
|
|
498
526
|
vec3 hLe = (!specular && uEmissiveCount > 0) ? vec3(0.0) : hEmissive;
|
|
499
527
|
return hLe + hAlbedo * Ld * (1.0 / PI);
|
|
@@ -995,7 +1023,16 @@ export class RTLightingPass {
|
|
|
995
1023
|
this.specB = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
996
1024
|
|
|
997
1025
|
this.material = new THREE.ShaderMaterial({
|
|
1026
|
+
// Stable program name for compile-failure self-diagnosis: this is the
|
|
1027
|
+
// CORE lighting megakernel — a link failure here has no fallback (see
|
|
1028
|
+
// RealtimeRaytracer._passClass -> coreFailure).
|
|
1029
|
+
name: "rt:lighting",
|
|
998
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: {},
|
|
999
1036
|
vertexShader: fullscreenVert,
|
|
1000
1037
|
fragmentShader: specMRT
|
|
1001
1038
|
? rtLightingFrag
|
|
@@ -1049,6 +1086,13 @@ export class RTLightingPass {
|
|
|
1049
1086
|
uSkyZenith: { value: new THREE.Color(0.18, 0.34, 0.62) },
|
|
1050
1087
|
uSkyHorizon: { value: new THREE.Color(0.7, 0.8, 0.9) },
|
|
1051
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 },
|
|
1052
1096
|
},
|
|
1053
1097
|
depthTest: false,
|
|
1054
1098
|
depthWrite: false,
|
|
@@ -1057,6 +1101,9 @@ export class RTLightingPass {
|
|
|
1057
1101
|
// Specular temporal accumulation program (its own sampler budget — well
|
|
1058
1102
|
// clear of the lighting pass's 16-sampler ceiling).
|
|
1059
1103
|
this.specMaterial = new THREE.ShaderMaterial({
|
|
1104
|
+
// Optional additive specular buffer — a link failure degrades to the
|
|
1105
|
+
// Lambert-only look (RealtimeRaytracer disables `specular`), image stays lit.
|
|
1106
|
+
name: "rt:specular",
|
|
1060
1107
|
glslVersion: THREE.GLSL3,
|
|
1061
1108
|
vertexShader: fullscreenVert,
|
|
1062
1109
|
fragmentShader: specAccumFrag,
|
|
@@ -1081,6 +1128,9 @@ export class RTLightingPass {
|
|
|
1081
1128
|
// buffers. In single-target fallback the second output collapses the same
|
|
1082
1129
|
// way as the lighting shader's.
|
|
1083
1130
|
this.carryMaterial = new THREE.ShaderMaterial({
|
|
1131
|
+
// Resize-only history-carry blit; a link failure is non-fatal (history is
|
|
1132
|
+
// not carried across a resolution step) so it classifies as auxiliary.
|
|
1133
|
+
name: "rt:history-carry",
|
|
1084
1134
|
glslVersion: THREE.GLSL3,
|
|
1085
1135
|
vertexShader: fullscreenVert,
|
|
1086
1136
|
fragmentShader: specMRT
|
|
@@ -1120,7 +1170,7 @@ export class RTLightingPass {
|
|
|
1120
1170
|
t.texture.generateMipmaps = false;
|
|
1121
1171
|
return t;
|
|
1122
1172
|
}
|
|
1123
|
-
const t =
|
|
1173
|
+
const t = makeMRT(width, height, 2, opts);
|
|
1124
1174
|
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
1125
1175
|
return t;
|
|
1126
1176
|
}
|
|
@@ -1226,6 +1276,36 @@ export class RTLightingPass {
|
|
|
1226
1276
|
u.uEmissiveCount.value = compiled.emissiveTriCount;
|
|
1227
1277
|
}
|
|
1228
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
|
+
|
|
1229
1309
|
/**
|
|
1230
1310
|
* Renders lighting into targetA (reading targetB as irradiance history), then
|
|
1231
1311
|
* accumulates the fresh specular (targetA attachment 1) into specA (reading
|