three-realtime-rt 0.3.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/LICENSE +21 -0
- package/README.md +394 -0
- package/package.json +52 -0
- package/src/CompositePass.js +214 -0
- package/src/DenoisePass.js +202 -0
- package/src/GBufferPass.js +228 -0
- package/src/RTLightingPass.js +680 -0
- package/src/RealtimeRaytracer.js +730 -0
- package/src/RestirPass.js +402 -0
- package/src/SceneCompiler.js +424 -0
- package/src/TAAPass.js +230 -0
- package/src/VolumetricPass.js +329 -0
- package/src/blueNoise.js +17 -0
- package/src/bvhAnyHit.glsl.js +114 -0
- package/src/index.d.ts +289 -0
- package/src/index.js +2 -0
- package/src/sky.glsl.js +23 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const fullscreenVert = /* glsl */ `
|
|
4
|
+
out vec2 vUv;
|
|
5
|
+
void main() {
|
|
6
|
+
vUv = uv;
|
|
7
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const atrousFrag = /* glsl */ `
|
|
12
|
+
precision highp float;
|
|
13
|
+
|
|
14
|
+
layout(location = 0) out vec4 outColor;
|
|
15
|
+
|
|
16
|
+
in vec2 vUv;
|
|
17
|
+
|
|
18
|
+
uniform sampler2D uIrradiance; // rgb = irradiance, a = history count
|
|
19
|
+
uniform sampler2D uGWorldPos; // full-res guides
|
|
20
|
+
uniform sampler2D uGNormalMetal;
|
|
21
|
+
uniform vec2 uTexelSize; // of the irradiance target
|
|
22
|
+
uniform float uStep; // à-trous step: 1, 2, 4, ...
|
|
23
|
+
uniform vec3 uCameraPos;
|
|
24
|
+
uniform float uEps;
|
|
25
|
+
uniform float uLumSigma;
|
|
26
|
+
|
|
27
|
+
float luminance(vec3 c) {
|
|
28
|
+
return dot(c, vec3(0.299, 0.587, 0.114));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
void main() {
|
|
32
|
+
vec4 center = texture(uIrradiance, vUv);
|
|
33
|
+
vec4 wp = texture(uGWorldPos, vUv);
|
|
34
|
+
if (wp.w < 0.5) {
|
|
35
|
+
outColor = center;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
vec3 P = wp.xyz;
|
|
39
|
+
vec4 nm = texture(uGNormalMetal, vUv);
|
|
40
|
+
vec3 N = normalize(nm.xyz);
|
|
41
|
+
// Specular surfaces (mirror metals, glass) carry traced reflections whose
|
|
42
|
+
// detail is NOT in the G-buffer guides — filtering would smear them, and
|
|
43
|
+
// their signal is nearly deterministic anyway. Scale the filter down as the
|
|
44
|
+
// surface gets more mirror-like.
|
|
45
|
+
float matW = nm.w;
|
|
46
|
+
float specAmount = matW >= 2.0 ? clamp(matW - 2.0, 0.0, 1.0) : matW;
|
|
47
|
+
float specKeep = specAmount * (1.0 - clamp(wp.w - 1.0, 0.0, 1.0));
|
|
48
|
+
|
|
49
|
+
// Fewer accumulated samples -> noisier pixel -> wider luminance tolerance.
|
|
50
|
+
// A converged pixel (high count) is barely touched, preserving detail.
|
|
51
|
+
float count = max(center.a, 1.0);
|
|
52
|
+
float sigmaL = uLumSigma * clamp(8.0 / sqrt(count), 0.75, 8.0);
|
|
53
|
+
|
|
54
|
+
float distToCam = distance(P, uCameraPos);
|
|
55
|
+
float planeTol = 0.01 * distToCam + 20.0 * uEps;
|
|
56
|
+
|
|
57
|
+
// Despeckle (first iteration, short history only): a freshly disoccluded
|
|
58
|
+
// pixel can carry one huge sample that the center-weighted filter would
|
|
59
|
+
// preserve as a bright "rain drop" at silhouettes. Such a pixel has no
|
|
60
|
+
// business being brighter than its entire neighbourhood — clamp its
|
|
61
|
+
// luminance to the brightest neighbour. Converged pixels are exempt, so
|
|
62
|
+
// real small highlights survive.
|
|
63
|
+
if (uStep < 1.5 && count < 8.0) {
|
|
64
|
+
float maxL = 0.0;
|
|
65
|
+
float found = 0.0;
|
|
66
|
+
for (int dy = -1; dy <= 1; dy++) {
|
|
67
|
+
for (int dx = -1; dx <= 1; dx++) {
|
|
68
|
+
if (dx == 0 && dy == 0) continue;
|
|
69
|
+
vec2 tuv = vUv + vec2(float(dx), float(dy)) * uTexelSize;
|
|
70
|
+
if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) continue;
|
|
71
|
+
if (texture(uGWorldPos, tuv).w < 0.5) continue;
|
|
72
|
+
maxL = max(maxL, luminance(texture(uIrradiance, tuv).rgb));
|
|
73
|
+
found = 1.0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
float cap = maxL * 1.25 + 1e-4;
|
|
77
|
+
float l = luminance(center.rgb);
|
|
78
|
+
if (found > 0.5 && l > cap) center.rgb *= cap / l;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
float lumC = luminance(center.rgb);
|
|
82
|
+
|
|
83
|
+
// 3x3 B-spline-ish kernel, edge-avoiding weights.
|
|
84
|
+
vec3 sum = center.rgb * 4.0;
|
|
85
|
+
float wsum = 4.0;
|
|
86
|
+
for (int dy = -1; dy <= 1; dy++) {
|
|
87
|
+
for (int dx = -1; dx <= 1; dx++) {
|
|
88
|
+
if (dx == 0 && dy == 0) continue;
|
|
89
|
+
vec2 tuv = vUv + vec2(float(dx), float(dy)) * uStep * uTexelSize;
|
|
90
|
+
if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) continue;
|
|
91
|
+
|
|
92
|
+
vec4 g = texture(uGWorldPos, tuv);
|
|
93
|
+
if (g.w < 0.5) continue;
|
|
94
|
+
vec4 s = texture(uIrradiance, tuv);
|
|
95
|
+
vec3 Nt = normalize(texture(uGNormalMetal, tuv).xyz);
|
|
96
|
+
|
|
97
|
+
float k = (dx == 0 || dy == 0) ? 2.0 : 1.0;
|
|
98
|
+
float wN = pow(max(dot(N, Nt), 0.0), 32.0);
|
|
99
|
+
float wZ = exp(-abs(dot(g.xyz - P, N)) / planeTol);
|
|
100
|
+
// Tighten the luminance gate as the à-trous step widens: a shadow on a
|
|
101
|
+
// flat floor has no geometric edge to protect it, so at high iteration
|
|
102
|
+
// counts the wide passes would average it away ("floating" objects with
|
|
103
|
+
// no contact shadow). Wide steps only get to blend near-equal luminance.
|
|
104
|
+
float wL = exp(-abs(luminance(s.rgb) - lumC) / (sigmaL * inversesqrt(uStep)));
|
|
105
|
+
float w = k * wN * wZ * wL * (1.0 - specKeep);
|
|
106
|
+
sum += s.rgb * w;
|
|
107
|
+
wsum += w;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
outColor = vec4(sum / wsum, center.a);
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Edge-avoiding à-trous wavelet filter (SVGF-lite) on the demodulated
|
|
116
|
+
* irradiance buffer, guided by the full-res G-buffer (plane distance +
|
|
117
|
+
* normals) and modulated by per-pixel history count: freshly disoccluded
|
|
118
|
+
* pixels get blurred hard, converged pixels stay crisp.
|
|
119
|
+
*/
|
|
120
|
+
export class DenoisePass {
|
|
121
|
+
constructor(width, height) {
|
|
122
|
+
this.targetA = this._makeTarget(width, height);
|
|
123
|
+
this.targetB = this._makeTarget(width, height);
|
|
124
|
+
|
|
125
|
+
this.material = new THREE.ShaderMaterial({
|
|
126
|
+
glslVersion: THREE.GLSL3,
|
|
127
|
+
vertexShader: fullscreenVert,
|
|
128
|
+
fragmentShader: atrousFrag,
|
|
129
|
+
uniforms: {
|
|
130
|
+
uIrradiance: { value: null },
|
|
131
|
+
uGWorldPos: { value: null },
|
|
132
|
+
uGNormalMetal: { value: null },
|
|
133
|
+
uTexelSize: { value: new THREE.Vector2() },
|
|
134
|
+
uStep: { value: 1 },
|
|
135
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
136
|
+
uEps: { value: 1e-3 },
|
|
137
|
+
uLumSigma: { value: 0.25 },
|
|
138
|
+
},
|
|
139
|
+
depthTest: false,
|
|
140
|
+
depthWrite: false,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.scene = new THREE.Scene();
|
|
144
|
+
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
145
|
+
this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
|
|
146
|
+
this.quad.frustumCulled = false;
|
|
147
|
+
this.scene.add(this.quad);
|
|
148
|
+
|
|
149
|
+
this._width = width;
|
|
150
|
+
this._height = height;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_makeTarget(width, height) {
|
|
154
|
+
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
155
|
+
minFilter: THREE.LinearFilter,
|
|
156
|
+
magFilter: THREE.LinearFilter,
|
|
157
|
+
format: THREE.RGBAFormat,
|
|
158
|
+
type: THREE.HalfFloatType,
|
|
159
|
+
depthBuffer: false,
|
|
160
|
+
stencilBuffer: false,
|
|
161
|
+
});
|
|
162
|
+
t.texture.generateMipmaps = false;
|
|
163
|
+
return t;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setSize(width, height) {
|
|
167
|
+
this._width = width;
|
|
168
|
+
this._height = height;
|
|
169
|
+
this.targetA.setSize(width, height);
|
|
170
|
+
this.targetB.setSize(width, height);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Runs `iterations` à-trous passes; returns the final filtered texture. */
|
|
174
|
+
render(renderer, inputTexture, gbuffer, cameraPos, eps, iterations = 3) {
|
|
175
|
+
const u = this.material.uniforms;
|
|
176
|
+
u.uGWorldPos.value = gbuffer.worldPos;
|
|
177
|
+
u.uGNormalMetal.value = gbuffer.normalMetal;
|
|
178
|
+
u.uTexelSize.value.set(1 / this._width, 1 / this._height);
|
|
179
|
+
u.uCameraPos.value.copy(cameraPos);
|
|
180
|
+
u.uEps.value = eps;
|
|
181
|
+
|
|
182
|
+
let read = inputTexture;
|
|
183
|
+
let write = this.targetA;
|
|
184
|
+
for (let i = 0; i < iterations; i++) {
|
|
185
|
+
u.uIrradiance.value = read;
|
|
186
|
+
u.uStep.value = 1 << i;
|
|
187
|
+
renderer.setRenderTarget(write);
|
|
188
|
+
renderer.render(this.scene, this.camera);
|
|
189
|
+
read = write.texture;
|
|
190
|
+
write = write === this.targetA ? this.targetB : this.targetA;
|
|
191
|
+
}
|
|
192
|
+
renderer.setRenderTarget(null);
|
|
193
|
+
return read;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
dispose() {
|
|
197
|
+
this.targetA.dispose();
|
|
198
|
+
this.targetB.dispose();
|
|
199
|
+
this.material.dispose();
|
|
200
|
+
this.quad.geometry.dispose();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
const gbufferVert = /* glsl */ `
|
|
4
|
+
out vec3 vWorldPos;
|
|
5
|
+
out vec3 vWorldNormal;
|
|
6
|
+
out vec2 vUvCoord;
|
|
7
|
+
|
|
8
|
+
uniform mat3 uNormalMatrixWorld;
|
|
9
|
+
|
|
10
|
+
void main() {
|
|
11
|
+
vec4 wp = modelMatrix * vec4(position, 1.0);
|
|
12
|
+
vWorldPos = wp.xyz;
|
|
13
|
+
vWorldNormal = normalize(uNormalMatrixWorld * normal);
|
|
14
|
+
vUvCoord = uv;
|
|
15
|
+
gl_Position = projectionMatrix * viewMatrix * wp;
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const gbufferFrag = /* glsl */ `
|
|
20
|
+
precision highp float;
|
|
21
|
+
|
|
22
|
+
layout(location = 0) out vec4 gAlbedoRough;
|
|
23
|
+
layout(location = 1) out vec4 gNormalMetal;
|
|
24
|
+
layout(location = 2) out vec4 gWorldPos;
|
|
25
|
+
layout(location = 3) out vec4 gEmissive;
|
|
26
|
+
|
|
27
|
+
in vec3 vWorldPos;
|
|
28
|
+
in vec3 vWorldNormal;
|
|
29
|
+
in vec2 vUvCoord;
|
|
30
|
+
|
|
31
|
+
uniform vec3 uColor;
|
|
32
|
+
uniform float uRoughness;
|
|
33
|
+
uniform float uMetalness;
|
|
34
|
+
uniform float uTransmission;
|
|
35
|
+
uniform vec3 uEmissive;
|
|
36
|
+
uniform sampler2D uMap;
|
|
37
|
+
uniform bool uHasMap;
|
|
38
|
+
uniform sampler2D uEmissiveMap;
|
|
39
|
+
uniform bool uHasEmissiveMap;
|
|
40
|
+
|
|
41
|
+
void main() {
|
|
42
|
+
vec3 albedo = uColor;
|
|
43
|
+
if (uHasMap) {
|
|
44
|
+
albedo *= texture(uMap, vUvCoord).rgb;
|
|
45
|
+
}
|
|
46
|
+
vec3 emissive = uEmissive;
|
|
47
|
+
if (uHasEmissiveMap) {
|
|
48
|
+
emissive *= texture(uEmissiveMap, vUvCoord).rgb;
|
|
49
|
+
}
|
|
50
|
+
vec3 n = normalize(vWorldNormal) * (gl_FrontFacing ? 1.0 : -1.0);
|
|
51
|
+
gAlbedoRough = vec4(albedo, uRoughness);
|
|
52
|
+
// .w is a packed material word: >= 2 means transmissive glass (w - 2 =
|
|
53
|
+
// transmission amount), else it is plain metalness. Lets the lighting pass
|
|
54
|
+
// read specular/glass properties without an extra G-buffer sampler (it
|
|
55
|
+
// already sits at the WebGL2 16-sampler minimum).
|
|
56
|
+
gNormalMetal = vec4(n, uTransmission > 0.0 ? 2.0 + uTransmission : uMetalness);
|
|
57
|
+
// .w packs the valid flag AND roughness: 0 = background, 1 + roughness
|
|
58
|
+
// otherwise. Every consumer only tests w < 0.5, so this stays compatible.
|
|
59
|
+
gWorldPos = vec4(vWorldPos, 1.0 + uRoughness);
|
|
60
|
+
gEmissive = vec4(emissive, 1.0);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rasterizes the scene into a 4-target G-buffer (all RGBA32F):
|
|
66
|
+
* [0] albedo.rgb + roughness [1] worldNormal.xyz + metalness
|
|
67
|
+
* [2] worldPos.xyz + validFlag [3] emissive.rgb
|
|
68
|
+
*
|
|
69
|
+
* Uses a per-mesh material swap (cached) so each mesh's Standard/Basic/Lambert/Phong
|
|
70
|
+
* material properties flow into the buffer without touching user materials.
|
|
71
|
+
*/
|
|
72
|
+
export class GBufferPass {
|
|
73
|
+
constructor(width, height, { mixedPrecision = true } = {}) {
|
|
74
|
+
// Mixed fp16/fp32 attachments are legal WebGL2 but some implementations
|
|
75
|
+
// (notably Apple's Metal backend) may reject the framebuffer — the caller
|
|
76
|
+
// probes support and passes the verdict here.
|
|
77
|
+
this._mixedPrecision = mixedPrecision;
|
|
78
|
+
// Two G-buffers, ping-ponged each frame: the previous frame's worldPos +
|
|
79
|
+
// normals are needed to validate reprojected history (stage 2).
|
|
80
|
+
this._targets = [
|
|
81
|
+
this._makeTarget(width, height),
|
|
82
|
+
this._makeTarget(width, height),
|
|
83
|
+
];
|
|
84
|
+
this._current = 0;
|
|
85
|
+
|
|
86
|
+
this._materialCache = new WeakMap(); // mesh -> gbuffer ShaderMaterial
|
|
87
|
+
this._swapped = []; // [mesh, originalMaterial] pairs during render
|
|
88
|
+
this._normalMat3 = new THREE.Matrix3();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_makeTarget(width, height) {
|
|
92
|
+
const t = new THREE.WebGLMultipleRenderTargets(width, height, 4, {
|
|
93
|
+
minFilter: THREE.NearestFilter,
|
|
94
|
+
magFilter: THREE.NearestFilter,
|
|
95
|
+
type: THREE.FloatType,
|
|
96
|
+
depthBuffer: true,
|
|
97
|
+
});
|
|
98
|
+
for (const tex of t.texture) tex.generateMipmaps = false;
|
|
99
|
+
// Mixed precision: only world position (reprojection + plane-distance
|
|
100
|
+
// tests) needs fp32. Albedo/normal/emissive are fine in fp16, which halves
|
|
101
|
+
// G-buffer bandwidth on 3 of the 4 targets — a large win on mobile GPUs.
|
|
102
|
+
if (this._mixedPrecision) {
|
|
103
|
+
t.texture[0].type = THREE.HalfFloatType; // albedo + roughness
|
|
104
|
+
t.texture[1].type = THREE.HalfFloatType; // normal + packed material word
|
|
105
|
+
t.texture[3].type = THREE.HalfFloatType; // emissive
|
|
106
|
+
}
|
|
107
|
+
return t;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get target() {
|
|
111
|
+
return this._targets[this._current];
|
|
112
|
+
}
|
|
113
|
+
get _prev() {
|
|
114
|
+
return this._targets[1 - this._current];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
get albedoRough() {
|
|
118
|
+
return this.target.texture[0];
|
|
119
|
+
}
|
|
120
|
+
get normalMetal() {
|
|
121
|
+
return this.target.texture[1];
|
|
122
|
+
}
|
|
123
|
+
get worldPos() {
|
|
124
|
+
return this.target.texture[2];
|
|
125
|
+
}
|
|
126
|
+
get emissive() {
|
|
127
|
+
return this.target.texture[3];
|
|
128
|
+
}
|
|
129
|
+
get prevNormalMetal() {
|
|
130
|
+
return this._prev.texture[1];
|
|
131
|
+
}
|
|
132
|
+
get prevWorldPos() {
|
|
133
|
+
return this._prev.texture[2];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setSize(width, height) {
|
|
137
|
+
for (const t of this._targets) t.setSize(width, height);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_gbufferMaterialFor(mesh) {
|
|
141
|
+
let material = this._materialCache.get(mesh);
|
|
142
|
+
if (!material) {
|
|
143
|
+
material = new THREE.ShaderMaterial({
|
|
144
|
+
glslVersion: THREE.GLSL3,
|
|
145
|
+
vertexShader: gbufferVert,
|
|
146
|
+
fragmentShader: gbufferFrag,
|
|
147
|
+
uniforms: {
|
|
148
|
+
uNormalMatrixWorld: { value: new THREE.Matrix3() },
|
|
149
|
+
uColor: { value: new THREE.Color(1, 1, 1) },
|
|
150
|
+
uRoughness: { value: 1.0 },
|
|
151
|
+
uMetalness: { value: 0.0 },
|
|
152
|
+
uTransmission: { value: 0.0 },
|
|
153
|
+
uEmissive: { value: new THREE.Color(0, 0, 0) },
|
|
154
|
+
uMap: { value: null },
|
|
155
|
+
uHasMap: { value: false },
|
|
156
|
+
uEmissiveMap: { value: null },
|
|
157
|
+
uHasEmissiveMap: { value: false },
|
|
158
|
+
},
|
|
159
|
+
side: THREE.FrontSide,
|
|
160
|
+
});
|
|
161
|
+
this._materialCache.set(mesh, material);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sync properties from the user's material every frame (cheap).
|
|
165
|
+
const src = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
166
|
+
const u = material.uniforms;
|
|
167
|
+
if (src.color) u.uColor.value.copy(src.color);
|
|
168
|
+
u.uRoughness.value = src.roughness ?? 1.0;
|
|
169
|
+
u.uMetalness.value = src.metalness ?? 0.0;
|
|
170
|
+
u.uTransmission.value = src.transmission ?? 0.0; // MeshPhysicalMaterial
|
|
171
|
+
|
|
172
|
+
if (src.emissive) {
|
|
173
|
+
u.uEmissive.value
|
|
174
|
+
.copy(src.emissive)
|
|
175
|
+
.multiplyScalar(src.emissiveIntensity ?? 1);
|
|
176
|
+
}
|
|
177
|
+
u.uMap.value = src.map ?? null;
|
|
178
|
+
u.uHasMap.value = !!src.map;
|
|
179
|
+
u.uEmissiveMap.value = src.emissiveMap ?? null;
|
|
180
|
+
u.uHasEmissiveMap.value = !!src.emissiveMap;
|
|
181
|
+
u.uNormalMatrixWorld.value.getNormalMatrix(mesh.matrixWorld);
|
|
182
|
+
material.side = src.side ?? THREE.FrontSide;
|
|
183
|
+
return material;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
render(renderer, scene, camera) {
|
|
187
|
+
// Ping-pong: what was "current" becomes "previous".
|
|
188
|
+
this._current = 1 - this._current;
|
|
189
|
+
|
|
190
|
+
// Swap in G-buffer materials.
|
|
191
|
+
this._swapped.length = 0;
|
|
192
|
+
scene.traverse((obj) => {
|
|
193
|
+
if (obj.isMesh && obj.geometry && obj.visible) {
|
|
194
|
+
// The G-buffer has no alpha blending — a nearly invisible surface
|
|
195
|
+
// (glass display case, ghost meshes) would write itself opaque over
|
|
196
|
+
// everything behind it. Hide it from the raster pass instead.
|
|
197
|
+
const m = Array.isArray(obj.material) ? obj.material[0] : obj.material;
|
|
198
|
+
if (m.transparent && (m.opacity ?? 1) < 0.5) {
|
|
199
|
+
obj.visible = false;
|
|
200
|
+
this._swapped.push([obj, null]); // restore visibility after render
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this._swapped.push([obj, obj.material]);
|
|
204
|
+
obj.material = this._gbufferMaterialFor(obj);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const prevBackground = scene.background;
|
|
209
|
+
scene.background = null; // background writes nothing; worldPos.w stays 0
|
|
210
|
+
|
|
211
|
+
renderer.setRenderTarget(this.target);
|
|
212
|
+
renderer.setClearColor(0x000000, 0);
|
|
213
|
+
renderer.clear(true, true, false);
|
|
214
|
+
renderer.render(scene, camera);
|
|
215
|
+
renderer.setRenderTarget(null);
|
|
216
|
+
|
|
217
|
+
scene.background = prevBackground;
|
|
218
|
+
for (const [mesh, mat] of this._swapped) {
|
|
219
|
+
if (mat === null) mesh.visible = true; // was hidden, not material-swapped
|
|
220
|
+
else mesh.material = mat;
|
|
221
|
+
}
|
|
222
|
+
this._swapped.length = 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
dispose() {
|
|
226
|
+
for (const t of this._targets) t.dispose();
|
|
227
|
+
}
|
|
228
|
+
}
|