three-realtime-rt 0.3.2 → 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/README.md +187 -19
- package/package.json +1 -1
- package/src/CompositePass.js +59 -21
- package/src/DenoisePass.js +16 -2
- package/src/GBufferPass.js +84 -22
- package/src/RTLightingPass.js +472 -23
- package/src/RealtimeRaytracer.js +370 -18
- package/src/RestirPass.js +20 -3
- package/src/SceneCompiler.js +127 -32
- package/src/TAAPass.js +20 -4
- package/src/index.d.ts +93 -4
package/src/RTLightingPass.js
CHANGED
|
@@ -26,6 +26,7 @@ ${SKY_GLSL}
|
|
|
26
26
|
#define PI 3.14159265358979
|
|
27
27
|
|
|
28
28
|
layout(location = 0) out vec4 outIrradiance;
|
|
29
|
+
layout(location = 1) out vec4 outSpecular; // dielectric direct specular (fresh, this frame)
|
|
29
30
|
|
|
30
31
|
in vec2 vUv;
|
|
31
32
|
|
|
@@ -60,8 +61,10 @@ uniform vec4 uLightColorRadius[MAX_LIGHTS]; // rgb color*intensity, w radius
|
|
|
60
61
|
uniform vec4 uLightDirCone[MAX_LIGHTS]; // spot: direction.xyz + cos(outer angle)
|
|
61
62
|
uniform int uLightCount;
|
|
62
63
|
uniform int uEmissiveCount; // NEE area-light triangles in row 1 of uMaterialsTex
|
|
64
|
+
uniform bool uEmissiveCDF; // importance-sample tris by the power CDF (row 66)
|
|
63
65
|
uniform bool uReflEnabled; // traced reflections on metallic surfaces
|
|
64
66
|
uniform bool uRefrEnabled; // traced refraction on transmissive surfaces
|
|
67
|
+
uniform bool uBlendEnabled; // straight-through view continuation on blend surfaces
|
|
65
68
|
uniform float uIor; // index of refraction for transmissive materials
|
|
66
69
|
uniform bool uLightStochastic; // 1 direct shadow ray/pixel/frame instead of 1/light
|
|
67
70
|
uniform bool uRestirEnabled; // shade the reservoir winner instead of sampling
|
|
@@ -176,6 +179,57 @@ void fetchMaterial(float matIndex, out vec3 albedo, out float roughness,
|
|
|
176
179
|
metalness = t1.a;
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
// ---------- PBR specular (Cook-Torrance GGX) ----------
|
|
183
|
+
// A separate specular radiance is accumulated for the primary surface's DIRECT
|
|
184
|
+
// lighting alongside the demodulated diffuse irradiance. Because CompositePass
|
|
185
|
+
// multiplies the irradiance by albedo, a white dielectric highlight (F0 ~= 0.04)
|
|
186
|
+
// cannot ride in that buffer — it is emitted into gSpec and written to a second
|
|
187
|
+
// MRT attachment (added by the composite WITHOUT the albedo multiply). Metals'
|
|
188
|
+
// specular is albedo-tinted (F0 = albedo), so it stays in the reflection path
|
|
189
|
+
// where the composite's albedo multiply supplies the tint; gSpec is therefore
|
|
190
|
+
// scaled by (1 - metal)(1 - transmission) at output. Net effective Fresnel
|
|
191
|
+
// across both buffers is mix(0.04, albedo, metal) without the lighting pass ever
|
|
192
|
+
// sampling albedo (that would push it past the 16-sampler minimum).
|
|
193
|
+
vec3 gSpec; // accumulated dielectric direct specular radiance
|
|
194
|
+
vec3 gViewDir; // unit vector from the primary surface toward the camera
|
|
195
|
+
float gSpecRough; // primary surface roughness (drives the GGX lobe width)
|
|
196
|
+
bool gWantSpec; // true only while shading the PRIMARY surface's direct light
|
|
197
|
+
|
|
198
|
+
float D_GGX(float NoH, float a) {
|
|
199
|
+
float a2 = a * a;
|
|
200
|
+
float d = NoH * NoH * (a2 - 1.0) + 1.0;
|
|
201
|
+
return a2 / max(PI * d * d, 1e-8);
|
|
202
|
+
}
|
|
203
|
+
// Height-correlated Smith visibility (already folds in the 1/(4 NoL NoV) term).
|
|
204
|
+
float V_SmithGGX(float NoV, float NoL, float a) {
|
|
205
|
+
float a2 = a * a;
|
|
206
|
+
float gv = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
|
|
207
|
+
float gl = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
|
|
208
|
+
return 0.5 / max(gv + gl, 1e-5);
|
|
209
|
+
}
|
|
210
|
+
vec3 F_Schlick(float VoH, vec3 f0) {
|
|
211
|
+
return f0 + (1.0 - f0) * pow(clamp(1.0 - VoH, 0.0, 1.0), 5.0);
|
|
212
|
+
}
|
|
213
|
+
// Specular BRDF value (without the incoming NoL*radiance factor). F0 fixed at
|
|
214
|
+
// the dielectric 0.04 — metals are handled in the reflection path.
|
|
215
|
+
float ggxSpec(vec3 N, vec3 L) {
|
|
216
|
+
vec3 H = normalize(gViewDir + L);
|
|
217
|
+
float NoH = max(dot(N, H), 0.0);
|
|
218
|
+
float NoV = max(dot(N, gViewDir), 1e-4);
|
|
219
|
+
float NoL = max(dot(N, L), 1e-4);
|
|
220
|
+
float VoH = max(dot(gViewDir, H), 0.0);
|
|
221
|
+
// Clamp alpha off zero so a mirror-smooth dielectric does not produce an
|
|
222
|
+
// infinite spike the temporal buffer cannot resolve.
|
|
223
|
+
float a = max(gSpecRough * gSpecRough, 2e-3);
|
|
224
|
+
return D_GGX(NoH, a) * V_SmithGGX(NoV, NoL, a) * F_Schlick(VoH, vec3(0.04)).x;
|
|
225
|
+
}
|
|
226
|
+
// Add the dielectric specular for one light: li is the incoming radiance
|
|
227
|
+
// factor (light colour * cone / dist^2), NoL the geometric cosine.
|
|
228
|
+
void addSpec(vec3 N, vec3 L, vec3 li, float NoL) {
|
|
229
|
+
if (!gWantSpec) return;
|
|
230
|
+
gSpec += li * (NoL * ggxSpec(N, L));
|
|
231
|
+
}
|
|
232
|
+
|
|
179
233
|
// ---------- lighting ----------
|
|
180
234
|
// Direct irradiance (demodulated: no albedo) at point P with normal N,
|
|
181
235
|
// from light i, with one shadow ray. Area-samples point lights for soft shadows.
|
|
@@ -218,7 +272,9 @@ vec3 lightContribution(int i, vec3 P, vec3 N) {
|
|
|
218
272
|
if (NdotL <= 0.0) return vec3(0.0);
|
|
219
273
|
|
|
220
274
|
if (occluded(P + N * uEps, L, maxDist)) return vec3(0.0);
|
|
221
|
-
|
|
275
|
+
vec3 li = colRad.rgb * (cone / dist2);
|
|
276
|
+
addSpec(N, L, li, NdotL); // same shadow ray shadows the highlight
|
|
277
|
+
return li * NdotL;
|
|
222
278
|
}
|
|
223
279
|
|
|
224
280
|
// Direct light at a GI bounce hit: sample ONE random light (weighted by count).
|
|
@@ -232,9 +288,44 @@ vec3 sampleOneLight(vec3 P, vec3 N) {
|
|
|
232
288
|
// pick one triangle, sample a point on it, cast one shadow ray, convert the
|
|
233
289
|
// area pdf to solid angle. Turns emitters into proper soft area lights instead
|
|
234
290
|
// of surfaces a GI ray has to hit by luck.
|
|
291
|
+
//
|
|
292
|
+
// NOISE CAVEAT: emissive NEE is the highest-variance direct-light path in the
|
|
293
|
+
// engine — one triangle sample per pixel per frame, and the area-to-solid-angle
|
|
294
|
+
// conversion carries a 1/dist^2 that spikes into fireflies when a shading point
|
|
295
|
+
// sits close to a small emitter. Two mitigations stack here:
|
|
296
|
+
// 1. uEmissiveCDF (default on): the triangle is IMPORTANCE-SAMPLED by
|
|
297
|
+
// area x emitted luminance via the power CDF in the scene-data texture
|
|
298
|
+
// (row 2 + 64 — see SceneCompiler's layout comment). A big bright panel is
|
|
299
|
+
// picked proportionally more often than a tiny dim strip, and each sample
|
|
300
|
+
// is weighted by its true pick probability — same mean, far less variance
|
|
301
|
+
// than the uniform 1-of-N pick.
|
|
302
|
+
// 2. ReSTIR reservoirs converge each pixel onto the emitter that matters
|
|
303
|
+
// (the demo keeps restir on whenever emissive NEE is on;
|
|
304
|
+
// RealtimeRaytracer.compileScene logs a hint otherwise).
|
|
305
|
+
// fireflyClamp and the denoiser absorb the residual tail. Distance-aware
|
|
306
|
+
// selection and solid-angle triangle sampling remain future work.
|
|
235
307
|
vec3 sampleEmissiveTri(vec3 P, vec3 N) {
|
|
236
308
|
if (uEmissiveCount == 0) return vec3(0.0);
|
|
237
|
-
int
|
|
309
|
+
int idx;
|
|
310
|
+
float invProb; // 1 / P(picked this triangle)
|
|
311
|
+
if (uEmissiveCDF) {
|
|
312
|
+
// Binary search the power CDF: 8 steps covers MAX_EMISSIVE_TRIS = 256.
|
|
313
|
+
float u = rand();
|
|
314
|
+
int lo = 0;
|
|
315
|
+
int hi = uEmissiveCount - 1;
|
|
316
|
+
for (int s = 0; s < 8; s++) {
|
|
317
|
+
if (lo >= hi) break;
|
|
318
|
+
int mid = (lo + hi) >> 1;
|
|
319
|
+
if (u > texelFetch(uMaterialsTex, ivec2(mid, 66), 0).x) lo = mid + 1;
|
|
320
|
+
else hi = mid;
|
|
321
|
+
}
|
|
322
|
+
idx = lo;
|
|
323
|
+
invProb = 1.0 / max(texelFetch(uMaterialsTex, ivec2(idx, 66), 0).y, 1e-8);
|
|
324
|
+
} else {
|
|
325
|
+
idx = min(int(rand() * float(uEmissiveCount)), uEmissiveCount - 1);
|
|
326
|
+
invProb = float(uEmissiveCount);
|
|
327
|
+
}
|
|
328
|
+
int i = idx * 4;
|
|
238
329
|
vec4 t0 = texelFetch(uMaterialsTex, ivec2(i, 1), 0); // v0 | area
|
|
239
330
|
vec4 t1 = texelFetch(uMaterialsTex, ivec2(i + 1, 1), 0); // e1 | emit.r
|
|
240
331
|
vec4 t2 = texelFetch(uMaterialsTex, ivec2(i + 2, 1), 0); // e2 | emit.g
|
|
@@ -256,9 +347,14 @@ vec3 sampleEmissiveTri(vec3 P, vec3 N) {
|
|
|
256
347
|
if (cosS <= 0.0 || cosL < 1e-4) return vec3(0.0);
|
|
257
348
|
if (occluded(P + N * uEps, wi, dist)) return vec3(0.0);
|
|
258
349
|
|
|
259
|
-
//
|
|
260
|
-
// Solid-angle conversion gives irradiance
|
|
261
|
-
|
|
350
|
+
// Pick of one tri (probability 1/invProb) + uniform point on it:
|
|
351
|
+
// pdf_area = 1/(invProb·area). Solid-angle conversion gives irradiance
|
|
352
|
+
// Le · cosS · cosL / (d² · pdf_area).
|
|
353
|
+
vec3 e = vec3(t1.w, t2.w, t3.w) * (cosS * cosL * invProb * t0.w / max(d2, 1e-6));
|
|
354
|
+
|
|
355
|
+
// Dielectric highlight from this emitter: e already folds in cosS, so the
|
|
356
|
+
// specular is e * (GGX BRDF) toward the sampled point (wi).
|
|
357
|
+
if (gWantSpec) gSpec += e * ggxSpec(N, wi);
|
|
262
358
|
|
|
263
359
|
// Uniform-area sampling has huge single-sample variance for receivers close
|
|
264
360
|
// to a big emitter (sampled point can land almost on top of P, d² → 0);
|
|
@@ -336,6 +432,9 @@ vec3 shadeReservoir(vec3 P, vec3 N) {
|
|
|
336
432
|
}
|
|
337
433
|
|
|
338
434
|
if (occluded(P + N * uEps, wi, maxDist)) return vec3(0.0);
|
|
435
|
+
// Dielectric highlight from the reservoir winner (C = li * cos, shared with
|
|
436
|
+
// the diffuse term; W = res.a is applied to both).
|
|
437
|
+
if (gWantSpec) gSpec += C * (ggxSpec(N, wi) * res.a);
|
|
339
438
|
vec3 e = C * res.a;
|
|
340
439
|
// Safety clamp, same budget as the emissive direct clamp elsewhere.
|
|
341
440
|
float l = dot(e, vec3(0.299, 0.587, 0.114));
|
|
@@ -401,11 +500,52 @@ vec3 glossyReflect(vec3 V, vec3 N, float rough) {
|
|
|
401
500
|
return refl;
|
|
402
501
|
}
|
|
403
502
|
|
|
503
|
+
// Analytic lights live in uniform arrays, not the BVH, so a traced reflection
|
|
504
|
+
// ray never sees them — a mirror under a spotlight would show no glint. Evaluate
|
|
505
|
+
// each light as a small area source along the (roughness-jittered) reflection
|
|
506
|
+
// direction: if refl points within the light's angular radius, the light's disc
|
|
507
|
+
// is reflected, so add its radiance. The jitter in refl (from glossyReflect)
|
|
508
|
+
// softens the disc over temporal accumulation, widening the glint with
|
|
509
|
+
// roughness. Shadowed with the same any-hit occluder as direct lighting.
|
|
510
|
+
vec3 analyticGlint(vec3 P, vec3 refl) {
|
|
511
|
+
vec3 sum = vec3(0.0);
|
|
512
|
+
for (int i = 0; i < MAX_LIGHTS; i++) {
|
|
513
|
+
if (i >= uLightCount) break;
|
|
514
|
+
vec4 posType = uLightPosType[i];
|
|
515
|
+
vec4 colRad = uLightColorRadius[i];
|
|
516
|
+
if (posType.w < 0.5 || posType.w >= 1.5) {
|
|
517
|
+
// point / spot
|
|
518
|
+
vec3 d = posType.xyz - P;
|
|
519
|
+
float dl = length(d);
|
|
520
|
+
if (dl < 1e-4) continue;
|
|
521
|
+
vec3 toL = d / dl;
|
|
522
|
+
float cone = spotFalloff(i, -toL);
|
|
523
|
+
if (cone <= 0.0) continue;
|
|
524
|
+
// Angular radius of the sphere light + a small floor so a zero-radius
|
|
525
|
+
// light still shows a pin-point glint.
|
|
526
|
+
float ang = atan(max(colRad.w, 1e-3) / dl) + 0.01;
|
|
527
|
+
if (dot(refl, toL) < cos(ang)) continue;
|
|
528
|
+
if (occluded(P + refl * uEps, refl, dl)) continue;
|
|
529
|
+
sum += colRad.rgb * (cone / (dl * dl));
|
|
530
|
+
} else {
|
|
531
|
+
// directional: fixed small angular size (colRad.w = sun softness)
|
|
532
|
+
vec3 toL = normalize(-posType.xyz);
|
|
533
|
+
float ang = max(colRad.w, 0.02) + 0.01;
|
|
534
|
+
if (dot(refl, toL) < cos(ang)) continue;
|
|
535
|
+
if (occluded(P + refl * uEps, refl, 1e7)) continue;
|
|
536
|
+
sum += colRad.rgb;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return sum;
|
|
540
|
+
}
|
|
541
|
+
|
|
404
542
|
// Glass: Fresnel-weighted blend of a surface reflection and a two-interface
|
|
405
543
|
// refraction (enter at P, march to the exit surface, refract again).
|
|
406
544
|
vec3 glassRadiance(vec3 P, vec3 N, vec3 V, float rough) {
|
|
407
545
|
vec3 refl = glossyReflect(V, N, rough);
|
|
408
|
-
vec3 reflRad = dot(refl, N) > 0.0
|
|
546
|
+
vec3 reflRad = dot(refl, N) > 0.0
|
|
547
|
+
? traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl)
|
|
548
|
+
: vec3(0.0);
|
|
409
549
|
|
|
410
550
|
float eta = 1.0 / uIor;
|
|
411
551
|
vec3 rd = refract(V, N, eta);
|
|
@@ -438,6 +578,7 @@ void main() {
|
|
|
438
578
|
vec4 wp = texture(uGWorldPos, vUv);
|
|
439
579
|
if (wp.w < 0.5) {
|
|
440
580
|
outIrradiance = vec4(0.0);
|
|
581
|
+
outSpecular = vec4(0.0);
|
|
441
582
|
return;
|
|
442
583
|
}
|
|
443
584
|
|
|
@@ -450,11 +591,23 @@ void main() {
|
|
|
450
591
|
vec3 P = wp.xyz;
|
|
451
592
|
vec4 nmSample = texture(uGNormalMetal, vUv);
|
|
452
593
|
vec3 N = normalize(nmSample.xyz);
|
|
453
|
-
// Decode the packed material word (see GBufferPass):
|
|
454
|
-
|
|
455
|
-
float
|
|
594
|
+
// Decode the packed material word (see GBufferPass): [4,5] → alpha blend
|
|
595
|
+
// (w - 4 = opacity), [2,4) → glass (w - 2 = transmission), else metalness.
|
|
596
|
+
float matW = nmSample.w;
|
|
597
|
+
bool blend = matW >= 4.0;
|
|
598
|
+
float opacity = blend ? clamp(matW - 4.0, 0.0, 1.0) : 1.0;
|
|
599
|
+
float transmission = (matW >= 2.0 && matW < 4.0) ? clamp(matW - 2.0, 0.0, 1.0) : 0.0;
|
|
600
|
+
float metal = matW < 2.0 ? matW : 0.0;
|
|
456
601
|
float rough = clamp(wp.w - 1.0, 0.0, 1.0);
|
|
457
602
|
|
|
603
|
+
// Cook-Torrance specular state for this primary surface. gWantSpec gates the
|
|
604
|
+
// GGX term to PRIMARY direct lighting only (GI-bounce direct light, below,
|
|
605
|
+
// reuses the same functions but must not pollute the highlight buffer).
|
|
606
|
+
gSpec = vec3(0.0);
|
|
607
|
+
gViewDir = normalize(uCameraPos - P);
|
|
608
|
+
gSpecRough = rough;
|
|
609
|
+
gWantSpec = true;
|
|
610
|
+
|
|
458
611
|
// --- direct lighting ---
|
|
459
612
|
// ReSTIR: shade the reservoir's winner with one visibility ray (flat cost in
|
|
460
613
|
// light count). Stochastic: one blind random sample. Full: one shadow ray
|
|
@@ -480,11 +633,30 @@ void main() {
|
|
|
480
633
|
// DOUBLED — the temporal average converges to the same brightness
|
|
481
634
|
// (unbiased) while GI's ray cost halves; accumulation + denoise absorb
|
|
482
635
|
// the alternation.
|
|
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.
|
|
483
646
|
vec3 indirect = vec3(0.0);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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;
|
|
488
660
|
if (uGIHalfRate) indirect *= 2.0;
|
|
489
661
|
}
|
|
490
662
|
}
|
|
@@ -503,7 +675,10 @@ void main() {
|
|
|
503
675
|
if (dot(refl, N) > 0.0) {
|
|
504
676
|
// Metals have no diffuse term: replace by metalness. The composite's
|
|
505
677
|
// albedo multiply then tints the reflection (F0 = albedo for metals).
|
|
506
|
-
|
|
678
|
+
// analyticGlint adds the direct lights the reflection ray cannot see, so
|
|
679
|
+
// a metal under a spotlight shows a proper (albedo-tinted) glint.
|
|
680
|
+
vec3 reflRad = traceRadiance(P + N * uEps, refl, true) + analyticGlint(P, refl);
|
|
681
|
+
sampleIrr = mix(sampleIrr, reflRad, metal);
|
|
507
682
|
}
|
|
508
683
|
}
|
|
509
684
|
|
|
@@ -513,10 +688,45 @@ void main() {
|
|
|
513
688
|
sampleIrr = mix(sampleIrr, glassRadiance(P, N, V, rough), transmission);
|
|
514
689
|
}
|
|
515
690
|
|
|
691
|
+
// --- alpha blend: straight-through view continuation ---
|
|
692
|
+
// A transparent surface is primary-visible in the G-buffer but was kept out of
|
|
693
|
+
// the BVH, so a ray along the view direction passes THROUGH it to whatever is
|
|
694
|
+
// behind. Trace that continuation and shade it like a glass/GI hit (emitters
|
|
695
|
+
// keep their emission — this is direct visibility through the pane — sky/env on
|
|
696
|
+
// a miss). The two quantities live at DIFFERENT scales: sampleIrr is the
|
|
697
|
+
// pane's own demodulated surface light (composite re-applies albedo), while
|
|
698
|
+
// the behind trace is final outgoing radiance — mixing them in one slot makes
|
|
699
|
+
// the pane term drown out what shows through. So the behind image rides the
|
|
700
|
+
// SPECULAR attachment instead (composite adds that buffer without the albedo
|
|
701
|
+
// multiply, and its short-history accumulation suits behind-content that
|
|
702
|
+
// parallaxes against the pane), and CompositePass performs the opacity blend
|
|
703
|
+
// where the pane's albedo is actually available. sampleIrr keeps only the
|
|
704
|
+
// pane's own surface lighting, which is static on the surface and accumulates
|
|
705
|
+
// with normal full-length history.
|
|
706
|
+
// (The straight-through trace itself happens at the unified secondary-ray
|
|
707
|
+
// call site above — see the Metal call-site-count note there.)
|
|
708
|
+
|
|
516
709
|
// A single NaN/Inf sample would poison the EMA history for good (mix() with
|
|
517
710
|
// NaN stays NaN until a disocclusion resets the pixel) — sanitize first.
|
|
518
711
|
if (any(isnan(sampleIrr)) || any(isinf(sampleIrr))) sampleIrr = vec3(0.0);
|
|
519
712
|
|
|
713
|
+
// Fresh dielectric direct specular for this frame. Metals/glass carry their
|
|
714
|
+
// (albedo-tinted) specular in the reflection path above, so scale their share
|
|
715
|
+
// out of the white buffer — the effective F0 is mix(0.04, albedo, metal),
|
|
716
|
+
// split across the two buffers. The separate SpecularAccumPass reprojects and
|
|
717
|
+
// temporally accumulates this with a short (near-mirror) history.
|
|
718
|
+
// Blend pixels repurpose this attachment for the straight-through behind
|
|
719
|
+
// radiance (see above) — their dielectric highlight is dropped, a fair trade
|
|
720
|
+
// for a correct-scale see-through image.
|
|
721
|
+
vec3 spec = blend ? blendBehind : gSpec * ((1.0 - metal) * (1.0 - transmission));
|
|
722
|
+
if (any(isnan(spec)) || any(isinf(spec))) spec = vec3(0.0);
|
|
723
|
+
if (!blend) {
|
|
724
|
+
float specLum = dot(spec, vec3(0.299, 0.587, 0.114));
|
|
725
|
+
float specCap = uFireflyClamp * 4.0; // narrow lobes spike; keep the EMA stable
|
|
726
|
+
if (specLum > specCap) spec *= specCap / specLum;
|
|
727
|
+
}
|
|
728
|
+
outSpecular = vec4(spec, 1.0);
|
|
729
|
+
|
|
520
730
|
// --- temporal reprojection: pull validated history from last frame ---
|
|
521
731
|
float count = 1.0;
|
|
522
732
|
vec3 history = vec3(0.0);
|
|
@@ -546,6 +756,10 @@ void main() {
|
|
|
546
756
|
// reflection under camera motion — and specular rays are nearly
|
|
547
757
|
// deterministic, so they don't need the accumulation anyway.
|
|
548
758
|
float specHist = max(metal, transmission) * (1.0 - rough);
|
|
759
|
+
// (Blend pixels need no shortening here: this slot holds only the
|
|
760
|
+
// pane's own surface light, which is static on the surface. The
|
|
761
|
+
// parallaxing behind-image rides the specular attachment, whose
|
|
762
|
+
// accumulation is short-history by design.)
|
|
549
763
|
float histCap = mix(uMaxHistory, min(uMaxHistory, 10.0), specHist);
|
|
550
764
|
count = clamp(h.a, 0.0, histCap) + 1.0;
|
|
551
765
|
history = h.rgb;
|
|
@@ -561,20 +775,125 @@ void main() {
|
|
|
561
775
|
}
|
|
562
776
|
`;
|
|
563
777
|
|
|
778
|
+
// Specular accumulation: the lighting pass emits FRESH dielectric specular in
|
|
779
|
+
// MRT attachment 1 (it has no spare sampler to read its own specular history).
|
|
780
|
+
// This second, cheap program reprojects that fresh sample against the previous
|
|
781
|
+
// accumulated specular and EMA-blends it — the same temporal scheme as the
|
|
782
|
+
// irradiance buffer, but with the SHORT (near-mirror) history a view-dependent
|
|
783
|
+
// highlight needs so it tracks moving lights and the camera without smearing.
|
|
784
|
+
const specAccumFrag = /* glsl */ `
|
|
785
|
+
precision highp float;
|
|
786
|
+
|
|
787
|
+
layout(location = 0) out vec4 outSpec;
|
|
788
|
+
|
|
789
|
+
in vec2 vUv;
|
|
790
|
+
|
|
791
|
+
uniform sampler2D uFreshSpec;
|
|
792
|
+
uniform sampler2D uPrevSpec;
|
|
793
|
+
uniform sampler2D uGWorldPos;
|
|
794
|
+
uniform sampler2D uGNormalMetal;
|
|
795
|
+
uniform sampler2D uPrevGWorldPos;
|
|
796
|
+
uniform mat4 uPrevViewProj;
|
|
797
|
+
uniform mat4 uViewProj;
|
|
798
|
+
uniform vec3 uCameraPos;
|
|
799
|
+
uniform float uEps;
|
|
800
|
+
uniform float uMaxHistory;
|
|
801
|
+
uniform bool uTemporalReprojection;
|
|
802
|
+
|
|
803
|
+
void main() {
|
|
804
|
+
vec4 wp = texture(uGWorldPos, vUv);
|
|
805
|
+
if (wp.w < 0.5) { outSpec = vec4(0.0); return; }
|
|
806
|
+
vec3 P = wp.xyz;
|
|
807
|
+
vec3 N = normalize(texture(uGNormalMetal, vUv).xyz);
|
|
808
|
+
float rough = clamp(wp.w - 1.0, 0.0, 1.0);
|
|
809
|
+
vec3 fresh = texture(uFreshSpec, vUv).rgb;
|
|
810
|
+
|
|
811
|
+
float count = 1.0;
|
|
812
|
+
vec3 history = vec3(0.0);
|
|
813
|
+
if (uTemporalReprojection) {
|
|
814
|
+
vec4 clip = uPrevViewProj * vec4(P, 1.0);
|
|
815
|
+
vec4 clipC = uViewProj * vec4(P, 1.0);
|
|
816
|
+
if (clip.w > 0.0 && clipC.w > 0.0) {
|
|
817
|
+
vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5;
|
|
818
|
+
vec2 currUv = (clipC.xy / clipC.w) * 0.5 + 0.5;
|
|
819
|
+
prevUv -= currUv - vUv; // cancel the G-buffer texel sub-pixel offset
|
|
820
|
+
if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
|
|
821
|
+
vec4 prevPos = texture(uPrevGWorldPos, prevUv);
|
|
822
|
+
float tol = 0.005 * distance(P, uCameraPos) + 20.0 * uEps;
|
|
823
|
+
if (prevPos.w > 0.5 && abs(dot(P - prevPos.xyz, N)) < tol) {
|
|
824
|
+
vec4 h = texture(uPrevSpec, prevUv);
|
|
825
|
+
// Short history: specular is view-dependent, so a long EMA smears the
|
|
826
|
+
// highlight under motion. Smoother (sharper) highlights react fastest.
|
|
827
|
+
float specHist = 1.0 - rough;
|
|
828
|
+
float histCap = mix(min(uMaxHistory, 32.0), min(uMaxHistory, 8.0), specHist);
|
|
829
|
+
count = clamp(h.a, 0.0, histCap) + 1.0;
|
|
830
|
+
history = h.rgb;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
vec3 blended = mix(history, fresh, 1.0 / count);
|
|
837
|
+
if (any(isnan(blended)) || any(isinf(blended))) blended = vec3(0.0);
|
|
838
|
+
outSpec = vec4(blended, count);
|
|
839
|
+
}
|
|
840
|
+
`;
|
|
841
|
+
|
|
842
|
+
// Irradiance-history carry for renderScale/canvas resizes. The shared CopyPass
|
|
843
|
+
// writes ONE output; rendering it into the 2-attachment MRT is a draw-buffer
|
|
844
|
+
// mismatch that some drivers (ANGLE/D3D11) reject with INVALID_OPERATION. This
|
|
845
|
+
// 2-output copy matches the MRT: attachment 0 = resampled history (alpha/count
|
|
846
|
+
// clamped), attachment 1 = 0 (fresh-written next frame anyway).
|
|
847
|
+
const mrtCarryFrag = /* glsl */ `
|
|
848
|
+
precision highp float;
|
|
849
|
+
layout(location = 0) out vec4 o0;
|
|
850
|
+
layout(location = 1) out vec4 o1;
|
|
851
|
+
in vec2 vUv;
|
|
852
|
+
uniform sampler2D uTex;
|
|
853
|
+
uniform float uCountClamp;
|
|
854
|
+
void main() {
|
|
855
|
+
vec4 c = texture(uTex, vUv);
|
|
856
|
+
if (uCountClamp >= 0.0) c.a = min(c.a, uCountClamp);
|
|
857
|
+
o0 = c;
|
|
858
|
+
o1 = vec4(0.0);
|
|
859
|
+
}
|
|
860
|
+
`;
|
|
861
|
+
|
|
564
862
|
/**
|
|
565
863
|
* Fullscreen pass: for every G-buffer pixel, trace shadow rays to every light and
|
|
566
864
|
* one cosine-weighted GI bounce against the BVH. Outputs demodulated irradiance,
|
|
567
865
|
* progressively accumulated into a ping-pong float target while the camera is still.
|
|
866
|
+
*
|
|
867
|
+
* The target is a 2-attachment MRT: [0] demodulated diffuse irradiance,
|
|
868
|
+
* [1] FRESH dielectric direct specular (temporally accumulated by specAccumFrag
|
|
869
|
+
* into a second ping-pong pair, specA/specB).
|
|
568
870
|
*/
|
|
569
871
|
export class RTLightingPass {
|
|
570
|
-
|
|
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;
|
|
571
881
|
this.targetA = this._makeTarget(width, height);
|
|
572
882
|
this.targetB = this._makeTarget(width, height);
|
|
883
|
+
// Accumulated specular history (ping-pong), fed by the fresh specular in
|
|
884
|
+
// targetA/B attachment 1.
|
|
885
|
+
this.specA = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
886
|
+
this.specB = specMRT ? this._makeSpecTarget(width, height) : null;
|
|
573
887
|
|
|
574
888
|
this.material = new THREE.ShaderMaterial({
|
|
575
889
|
glslVersion: THREE.GLSL3,
|
|
576
890
|
vertexShader: fullscreenVert,
|
|
577
|
-
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
|
+
),
|
|
578
897
|
uniforms: {
|
|
579
898
|
bvhStatic: { value: null },
|
|
580
899
|
bvhDynamic: { value: null },
|
|
@@ -599,8 +918,10 @@ export class RTLightingPass {
|
|
|
599
918
|
uLightDirCone: { value: [] },
|
|
600
919
|
uLightCount: { value: 0 },
|
|
601
920
|
uEmissiveCount: { value: 0 },
|
|
921
|
+
uEmissiveCDF: { value: true },
|
|
602
922
|
uReflEnabled: { value: true },
|
|
603
923
|
uRefrEnabled: { value: true },
|
|
924
|
+
uBlendEnabled: { value: true },
|
|
604
925
|
uIor: { value: 1.5 },
|
|
605
926
|
uLightStochastic: { value: false },
|
|
606
927
|
uGIHalfRate: { value: false },
|
|
@@ -620,6 +941,46 @@ export class RTLightingPass {
|
|
|
620
941
|
depthWrite: false,
|
|
621
942
|
});
|
|
622
943
|
|
|
944
|
+
// Specular temporal accumulation program (its own sampler budget — well
|
|
945
|
+
// clear of the lighting pass's 16-sampler ceiling).
|
|
946
|
+
this.specMaterial = new THREE.ShaderMaterial({
|
|
947
|
+
glslVersion: THREE.GLSL3,
|
|
948
|
+
vertexShader: fullscreenVert,
|
|
949
|
+
fragmentShader: specAccumFrag,
|
|
950
|
+
uniforms: {
|
|
951
|
+
uFreshSpec: { value: null },
|
|
952
|
+
uPrevSpec: { value: null },
|
|
953
|
+
uGWorldPos: { value: null },
|
|
954
|
+
uGNormalMetal: { value: null },
|
|
955
|
+
uPrevGWorldPos: { value: null },
|
|
956
|
+
uPrevViewProj: { value: new THREE.Matrix4() },
|
|
957
|
+
uViewProj: { value: new THREE.Matrix4() },
|
|
958
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
959
|
+
uEps: { value: 1e-3 },
|
|
960
|
+
uMaxHistory: { value: 128 },
|
|
961
|
+
uTemporalReprojection: { value: true },
|
|
962
|
+
},
|
|
963
|
+
depthTest: false,
|
|
964
|
+
depthWrite: false,
|
|
965
|
+
});
|
|
966
|
+
|
|
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.
|
|
970
|
+
this.carryMaterial = new THREE.ShaderMaterial({
|
|
971
|
+
glslVersion: THREE.GLSL3,
|
|
972
|
+
vertexShader: fullscreenVert,
|
|
973
|
+
fragmentShader: specMRT
|
|
974
|
+
? mrtCarryFrag
|
|
975
|
+
: mrtCarryFrag.replace(
|
|
976
|
+
"layout(location = 1) out vec4 o1;",
|
|
977
|
+
"vec4 o1; // single-target fallback: dead store"
|
|
978
|
+
),
|
|
979
|
+
uniforms: { uTex: { value: null }, uCountClamp: { value: -1 } },
|
|
980
|
+
depthTest: false,
|
|
981
|
+
depthWrite: false,
|
|
982
|
+
});
|
|
983
|
+
|
|
623
984
|
this.scene = new THREE.Scene();
|
|
624
985
|
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
625
986
|
this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
|
|
@@ -630,7 +991,33 @@ export class RTLightingPass {
|
|
|
630
991
|
_makeTarget(width, height) {
|
|
631
992
|
// Half-float + linear: history is sampled bilinearly at reprojected UVs,
|
|
632
993
|
// and fp16 halves the bandwidth (EMA blending never accumulates a raw sum,
|
|
633
|
-
// so fp16 precision is sufficient).
|
|
994
|
+
// so fp16 precision is sufficient). Two attachments: [0] irradiance,
|
|
995
|
+
// [1] fresh dielectric specular — or a single attachment in the WebKit
|
|
996
|
+
// fallback (see the constructor note).
|
|
997
|
+
const opts = {
|
|
998
|
+
minFilter: THREE.LinearFilter,
|
|
999
|
+
magFilter: THREE.LinearFilter,
|
|
1000
|
+
format: THREE.RGBAFormat,
|
|
1001
|
+
type: THREE.HalfFloatType,
|
|
1002
|
+
depthBuffer: false,
|
|
1003
|
+
stencilBuffer: false,
|
|
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);
|
|
1011
|
+
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
1012
|
+
return t;
|
|
1013
|
+
}
|
|
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
|
+
|
|
1020
|
+
_makeSpecTarget(width, height) {
|
|
634
1021
|
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
635
1022
|
minFilter: THREE.LinearFilter,
|
|
636
1023
|
magFilter: THREE.LinearFilter,
|
|
@@ -650,7 +1037,8 @@ export class RTLightingPass {
|
|
|
650
1037
|
renderer.getClearColor(prevColor);
|
|
651
1038
|
const prevAlpha = renderer.getClearAlpha();
|
|
652
1039
|
renderer.setClearColor(0x000000, 0);
|
|
653
|
-
for (const t of [this.targetA, this.targetB]) {
|
|
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
|
|
654
1042
|
renderer.setRenderTarget(t);
|
|
655
1043
|
renderer.clear(true, false, false);
|
|
656
1044
|
}
|
|
@@ -661,6 +1049,8 @@ export class RTLightingPass {
|
|
|
661
1049
|
setSize(width, height) {
|
|
662
1050
|
this.targetA.setSize(width, height);
|
|
663
1051
|
this.targetB.setSize(width, height);
|
|
1052
|
+
if (this.specA) this.specA.setSize(width, height);
|
|
1053
|
+
if (this.specB) this.specB.setSize(width, height);
|
|
664
1054
|
}
|
|
665
1055
|
|
|
666
1056
|
/**
|
|
@@ -679,11 +1069,33 @@ export class RTLightingPass {
|
|
|
679
1069
|
resizeCarry(renderer, copyPass, width, height, carryFrames) {
|
|
680
1070
|
const newA = this._makeTarget(width, height);
|
|
681
1071
|
const newB = this._makeTarget(width, height);
|
|
682
|
-
|
|
1072
|
+
// Carry the irradiance history (attachment 0) with the 2-output carry
|
|
1073
|
+
// material so the draw matches the MRT's draw buffers (a 1-output CopyPass
|
|
1074
|
+
// blit here is INVALID_OPERATION on ANGLE/D3D11). Attachment 1 is fresh-
|
|
1075
|
+
// written every frame, so it needs no carry.
|
|
1076
|
+
this.carryMaterial.uniforms.uTex.value = this._irrTex(this.targetB);
|
|
1077
|
+
this.carryMaterial.uniforms.uCountClamp.value = carryFrames;
|
|
1078
|
+
this.quad.material = this.carryMaterial;
|
|
1079
|
+
const prev = renderer.getRenderTarget();
|
|
1080
|
+
renderer.setRenderTarget(newB);
|
|
1081
|
+
renderer.render(this.scene, this.camera);
|
|
1082
|
+
renderer.setRenderTarget(prev);
|
|
1083
|
+
this.quad.material = this.material;
|
|
683
1084
|
this.targetA.dispose();
|
|
684
1085
|
this.targetB.dispose();
|
|
685
1086
|
this.targetA = newA;
|
|
686
1087
|
this.targetB = newB;
|
|
1088
|
+
|
|
1089
|
+
// Specular history carries the same way (freshest is specB — see render()).
|
|
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
|
+
}
|
|
687
1099
|
}
|
|
688
1100
|
|
|
689
1101
|
setCompiledScene(compiled) {
|
|
@@ -701,30 +1113,67 @@ export class RTLightingPass {
|
|
|
701
1113
|
u.uEmissiveCount.value = compiled.emissiveTriCount;
|
|
702
1114
|
}
|
|
703
1115
|
|
|
704
|
-
/**
|
|
1116
|
+
/**
|
|
1117
|
+
* Renders lighting into targetA (reading targetB as irradiance history), then
|
|
1118
|
+
* accumulates the fresh specular (targetA attachment 1) into specA (reading
|
|
1119
|
+
* specB as history). Swaps both ping-pong pairs. Returns { irradiance,
|
|
1120
|
+
* specular } textures for this frame.
|
|
1121
|
+
*/
|
|
705
1122
|
render(renderer, gbuffer, frame, reservoirTexture = null) {
|
|
706
1123
|
const u = this.material.uniforms;
|
|
707
1124
|
u.uGWorldPos.value = gbuffer.worldPos;
|
|
708
1125
|
u.uGNormalMetal.value = gbuffer.normalMetal;
|
|
709
1126
|
u.uPrevGWorldPos.value = gbuffer.prevWorldPos;
|
|
710
|
-
u.uPrevAccum.value = this.targetB
|
|
1127
|
+
u.uPrevAccum.value = this._irrTex(this.targetB);
|
|
711
1128
|
u.uReservoir.value = reservoirTexture;
|
|
712
1129
|
u.uRestirEnabled.value = reservoirTexture !== null;
|
|
713
1130
|
u.uFrame.value = frame;
|
|
714
1131
|
|
|
1132
|
+
// 1. lighting (MRT): [0] accumulated irradiance, [1] fresh specular.
|
|
1133
|
+
this.quad.material = this.material;
|
|
715
1134
|
renderer.setRenderTarget(this.targetA);
|
|
716
1135
|
renderer.render(this.scene, this.camera);
|
|
1136
|
+
|
|
1137
|
+
// 2. specular temporal accumulation: fresh (targetA[1]) + history (specB).
|
|
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
|
+
}
|
|
1159
|
+
|
|
1160
|
+
this.quad.material = this.material; // restore for the next caller
|
|
717
1161
|
renderer.setRenderTarget(null);
|
|
718
1162
|
|
|
719
|
-
const
|
|
1163
|
+
const outIrr = this._irrTex(this.targetA);
|
|
720
1164
|
[this.targetA, this.targetB] = [this.targetB, this.targetA];
|
|
721
|
-
|
|
1165
|
+
if (this.specMRT) [this.specA, this.specB] = [this.specB, this.specA];
|
|
1166
|
+
return { irradiance: outIrr, specular: outSpec };
|
|
722
1167
|
}
|
|
723
1168
|
|
|
724
1169
|
dispose() {
|
|
725
1170
|
this.targetA.dispose();
|
|
726
1171
|
this.targetB.dispose();
|
|
1172
|
+
if (this.specA) this.specA.dispose();
|
|
1173
|
+
if (this.specB) this.specB.dispose();
|
|
727
1174
|
this.material.dispose();
|
|
1175
|
+
this.specMaterial.dispose();
|
|
1176
|
+
this.carryMaterial.dispose();
|
|
728
1177
|
this.quad.geometry.dispose();
|
|
729
1178
|
}
|
|
730
1179
|
}
|