three-realtime-rt 0.3.0 → 0.4.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 +559 -394
- package/package.json +52 -52
- package/src/CompositePass.js +261 -214
- package/src/CopyPass.js +75 -0
- package/src/DenoisePass.js +221 -202
- package/src/GBufferPass.js +290 -228
- package/src/RTLightingPass.js +1124 -680
- package/src/RealtimeRaytracer.js +1106 -730
- package/src/RestirPass.js +31 -4
- package/src/SceneCompiler.js +542 -424
- package/src/TAAPass.js +271 -230
- package/src/VolumetricPass.js +398 -329
- package/src/blueNoise.js +17 -17
- package/src/index.d.ts +107 -6
package/src/VolumetricPass.js
CHANGED
|
@@ -1,329 +1,398 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import { shaderStructs, shaderIntersectFunction } from "three-mesh-bvh";
|
|
3
|
-
import { MAX_LIGHTS } from "./SceneCompiler.js";
|
|
4
|
-
import { BVH_ANY_HIT_GLSL } from "./bvhAnyHit.glsl.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
uniform
|
|
40
|
-
uniform
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
uniform
|
|
49
|
-
uniform
|
|
50
|
-
|
|
51
|
-
uniform
|
|
52
|
-
|
|
53
|
-
uniform
|
|
54
|
-
uniform
|
|
55
|
-
uniform
|
|
56
|
-
|
|
57
|
-
uniform
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
float
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
vec4
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
vec3
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
vec3
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { shaderStructs, shaderIntersectFunction } from "three-mesh-bvh";
|
|
3
|
+
import { MAX_LIGHTS } from "./SceneCompiler.js";
|
|
4
|
+
import { BVH_ANY_HIT_GLSL } from "./bvhAnyHit.glsl.js";
|
|
5
|
+
|
|
6
|
+
// Must match MAX_FOG_ZONES in the fragment shader.
|
|
7
|
+
const MAX_FOG_ZONES = 8;
|
|
8
|
+
|
|
9
|
+
const fullscreenVert = /* glsl */ `
|
|
10
|
+
out vec2 vUv;
|
|
11
|
+
void main() {
|
|
12
|
+
vUv = uv;
|
|
13
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
// Physically-based single-scatter volumetric light ("god rays"): for each
|
|
18
|
+
// pixel, pick ONE jittered point along the camera ray, sample ONE light
|
|
19
|
+
// source there, and cast a real BVH shadow ray — shafts are carved by actual
|
|
20
|
+
// occluders, not screen-space tricks, and work for off-screen sources too.
|
|
21
|
+
// One occlusion ray per pixel per frame; the Monte Carlo estimate converges
|
|
22
|
+
// through the same EMA temporal accumulation the surface lighting uses.
|
|
23
|
+
const volumetricFrag = /* glsl */ `
|
|
24
|
+
precision highp float;
|
|
25
|
+
precision highp isampler2D;
|
|
26
|
+
precision highp usampler2D;
|
|
27
|
+
|
|
28
|
+
${shaderStructs}
|
|
29
|
+
${shaderIntersectFunction}
|
|
30
|
+
${BVH_ANY_HIT_GLSL}
|
|
31
|
+
|
|
32
|
+
#define MAX_LIGHTS ${MAX_LIGHTS}
|
|
33
|
+
#define PI 3.14159265358979
|
|
34
|
+
|
|
35
|
+
layout(location = 0) out vec4 outScatter;
|
|
36
|
+
|
|
37
|
+
in vec2 vUv;
|
|
38
|
+
|
|
39
|
+
uniform BVH bvhStatic;
|
|
40
|
+
uniform BVH bvhDynamic;
|
|
41
|
+
uniform bool uHasDynamic;
|
|
42
|
+
uniform sampler2D uMaterialsTex; // row 1: emissive NEE triangles
|
|
43
|
+
uniform sampler2D uGWorldPos;
|
|
44
|
+
|
|
45
|
+
// temporal accumulation (reprojected through the SURFACE point — an
|
|
46
|
+
// approximation for a view-ray quantity, good enough for smooth fog)
|
|
47
|
+
uniform sampler2D uPrevAccum;
|
|
48
|
+
uniform mat4 uPrevViewProj;
|
|
49
|
+
uniform float uMaxHistory;
|
|
50
|
+
|
|
51
|
+
uniform vec4 uLightPosType[MAX_LIGHTS];
|
|
52
|
+
uniform vec4 uLightColorRadius[MAX_LIGHTS];
|
|
53
|
+
uniform vec4 uLightDirCone[MAX_LIGHTS]; // spot: direction.xyz + cos(outer)
|
|
54
|
+
uniform int uLightCount;
|
|
55
|
+
uniform int uEmissiveCount;
|
|
56
|
+
|
|
57
|
+
uniform vec3 uCameraPos;
|
|
58
|
+
uniform float uFrame;
|
|
59
|
+
uniform float uEps;
|
|
60
|
+
uniform float uDensity; // scatter coefficient (global term)
|
|
61
|
+
uniform float uMaxDist; // cap for rays that hit nothing / far surfaces
|
|
62
|
+
|
|
63
|
+
// Localized fog zones: up to 8 AABBs. Two vec4 per zone —
|
|
64
|
+
// [2*i] = (min.xyz, density), [2*i+1] = (max.xyz, unused).
|
|
65
|
+
// Density at a point = uDensity + Σ density of every zone containing it.
|
|
66
|
+
#define MAX_FOG_ZONES 8
|
|
67
|
+
uniform vec4 uFogZones[MAX_FOG_ZONES * 2];
|
|
68
|
+
uniform int uFogZoneCount;
|
|
69
|
+
|
|
70
|
+
float fogDensityAt(vec3 p) {
|
|
71
|
+
float d = uDensity;
|
|
72
|
+
for (int i = 0; i < MAX_FOG_ZONES; i++) {
|
|
73
|
+
if (i >= uFogZoneCount) break;
|
|
74
|
+
vec4 lo = uFogZones[i * 2];
|
|
75
|
+
vec3 mn = lo.xyz;
|
|
76
|
+
vec3 mx = uFogZones[i * 2 + 1].xyz;
|
|
77
|
+
if (all(greaterThanEqual(p, mn)) && all(lessThanEqual(p, mx))) {
|
|
78
|
+
d += lo.w;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return d;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------- RNG ----------
|
|
85
|
+
// First four dims from the shared blue-noise tile (rows 2..65 of the
|
|
86
|
+
// scene-data texture) with an R2 temporal shift; the rest is PCG. Same
|
|
87
|
+
// scheme as RTLightingPass — see the comment there.
|
|
88
|
+
uint gSeed;
|
|
89
|
+
int gBnDim;
|
|
90
|
+
vec4 gBlueNoise;
|
|
91
|
+
uint pcgHash(uint s) {
|
|
92
|
+
uint state = s * 747796405u + 2891336453u;
|
|
93
|
+
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
|
|
94
|
+
return (word >> 22u) ^ word;
|
|
95
|
+
}
|
|
96
|
+
float rand() {
|
|
97
|
+
if (gBnDim < 4) {
|
|
98
|
+
float v = gBlueNoise[gBnDim];
|
|
99
|
+
gBnDim++;
|
|
100
|
+
return v;
|
|
101
|
+
}
|
|
102
|
+
gSeed = pcgHash(gSeed);
|
|
103
|
+
return float(gSeed) * (1.0 / 4294967296.0);
|
|
104
|
+
}
|
|
105
|
+
vec2 rand2() { return vec2(rand(), rand()); }
|
|
106
|
+
|
|
107
|
+
vec4 fetchBlueNoise() {
|
|
108
|
+
ivec2 p = ivec2(gl_FragCoord.xy) & 63;
|
|
109
|
+
vec4 bn = texelFetch(uMaterialsTex, ivec2(p.x, 2 + p.y), 0);
|
|
110
|
+
vec4 shift = fract(float(uFrame) * vec4(0.6180340, 0.7548777, 0.5698403, 0.8191725));
|
|
111
|
+
return fract(bn + shift);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
vec3 randUnitVector() {
|
|
115
|
+
vec2 u = rand2();
|
|
116
|
+
float z = u.x * 2.0 - 1.0;
|
|
117
|
+
float a = u.y * 2.0 * PI;
|
|
118
|
+
float r = sqrt(max(0.0, 1.0 - z * z));
|
|
119
|
+
return vec3(r * cos(a), r * sin(a), z);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Any-hit: first blocker wins, no closest-hit sorting (see bvhAnyHit.glsl.js).
|
|
123
|
+
bool occluded(vec3 ro, vec3 rd, float maxDist) {
|
|
124
|
+
if (bvhIntersectAnyHit(bvhStatic, ro, rd, maxDist - 2.0 * uEps)) return true;
|
|
125
|
+
if (uHasDynamic && bvhIntersectAnyHit(bvhDynamic, ro, rd, maxDist - 2.0 * uEps)) return true;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// In-scattered radiance at a point in the volume from analytic light i.
|
|
130
|
+
// Like the surface version but with no cosine term (isotropic phase, folded
|
|
131
|
+
// into uDensity along with 1/4π).
|
|
132
|
+
vec3 lightAt(int i, vec3 S) {
|
|
133
|
+
vec4 posType = uLightPosType[i];
|
|
134
|
+
vec4 colRad = uLightColorRadius[i];
|
|
135
|
+
if (posType.w < 0.5 || posType.w >= 1.5) {
|
|
136
|
+
vec3 lp = posType.xyz + randUnitVector() * colRad.w;
|
|
137
|
+
vec3 d = lp - S;
|
|
138
|
+
float dist = length(d);
|
|
139
|
+
if (dist < 1e-4) return vec3(0.0);
|
|
140
|
+
float cone = 1.0;
|
|
141
|
+
if (posType.w >= 1.5) {
|
|
142
|
+
// spot: this is what draws visible light CONES in fog
|
|
143
|
+
vec4 dc = uLightDirCone[i];
|
|
144
|
+
cone = smoothstep(dc.w, posType.w - 2.0, dot(dc.xyz, -d / dist));
|
|
145
|
+
if (cone <= 0.0) return vec3(0.0);
|
|
146
|
+
}
|
|
147
|
+
if (occluded(S, d / dist, dist)) return vec3(0.0);
|
|
148
|
+
return colRad.rgb * (cone / (dist * dist));
|
|
149
|
+
}
|
|
150
|
+
vec3 L = normalize(-posType.xyz + randUnitVector() * colRad.w);
|
|
151
|
+
if (occluded(S, L, 1e7)) return vec3(0.0);
|
|
152
|
+
return colRad.rgb;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// In-scattered radiance from one sampled emissive triangle (row 1 of the
|
|
156
|
+
// materials texture — same layout the lighting pass uses).
|
|
157
|
+
vec3 emissiveAt(vec3 S) {
|
|
158
|
+
if (uEmissiveCount == 0) return vec3(0.0);
|
|
159
|
+
int i = min(int(rand() * float(uEmissiveCount)), uEmissiveCount - 1) * 4;
|
|
160
|
+
vec4 t0 = texelFetch(uMaterialsTex, ivec2(i, 1), 0);
|
|
161
|
+
vec4 t1 = texelFetch(uMaterialsTex, ivec2(i + 1, 1), 0);
|
|
162
|
+
vec4 t2 = texelFetch(uMaterialsTex, ivec2(i + 2, 1), 0);
|
|
163
|
+
vec4 t3 = texelFetch(uMaterialsTex, ivec2(i + 3, 1), 0);
|
|
164
|
+
vec2 u = rand2();
|
|
165
|
+
if (u.x + u.y > 1.0) u = 1.0 - u;
|
|
166
|
+
vec3 lp = t0.xyz + t1.xyz * u.x + t2.xyz * u.y;
|
|
167
|
+
vec3 d = lp - S;
|
|
168
|
+
float d2 = dot(d, d);
|
|
169
|
+
float dist = sqrt(d2);
|
|
170
|
+
if (dist < 1e-4) return vec3(0.0);
|
|
171
|
+
vec3 wi = d / dist;
|
|
172
|
+
float cosL = abs(dot(t3.xyz, wi));
|
|
173
|
+
if (cosL < 1e-4) return vec3(0.0);
|
|
174
|
+
if (occluded(S, wi, dist)) return vec3(0.0);
|
|
175
|
+
vec3 e = vec3(t1.w, t2.w, t3.w) * (cosL * float(uEmissiveCount) * t0.w / max(d2, 1e-4));
|
|
176
|
+
// same close-range variance clamp idea as the surface pass
|
|
177
|
+
float l = dot(e, vec3(0.299, 0.587, 0.114));
|
|
178
|
+
if (l > 20.0) e *= 20.0 / l;
|
|
179
|
+
return e;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
void main() {
|
|
183
|
+
vec4 wp = texture(uGWorldPos, vUv);
|
|
184
|
+
|
|
185
|
+
ivec2 px = ivec2(gl_FragCoord.xy);
|
|
186
|
+
gSeed = uint(px.x) * 2153u + uint(px.y) * 9277u + uint(uFrame) * 26699u;
|
|
187
|
+
gSeed = pcgHash(gSeed);
|
|
188
|
+
gBlueNoise = fetchBlueNoise();
|
|
189
|
+
gBnDim = 0;
|
|
190
|
+
|
|
191
|
+
// Segment to integrate: camera → surface (or the fog cap on a miss).
|
|
192
|
+
bool hit = wp.w > 0.5;
|
|
193
|
+
vec3 P = wp.xyz;
|
|
194
|
+
float segLen = hit ? min(distance(P, uCameraPos), uMaxDist) : uMaxDist;
|
|
195
|
+
vec3 rd = hit
|
|
196
|
+
? normalize(P - uCameraPos)
|
|
197
|
+
: vec3(0.0); // background without geometry: skip (no stable ray direction here)
|
|
198
|
+
|
|
199
|
+
// STRATIFIED MARCH: several jittered steps per ray instead of one point.
|
|
200
|
+
// This pass runs at quarter canvas resolution (fog is low-frequency), so
|
|
201
|
+
// the extra steps cost less than the old single-sample full-lighting-res
|
|
202
|
+
// version — and MOVING lights, whose in-scatter field changes every frame
|
|
203
|
+
// and can never converge temporally, get real per-frame averaging.
|
|
204
|
+
// Nothing to scatter: no global fog AND no localized zones → output zeros fast.
|
|
205
|
+
if (uDensity <= 0.0 && uFogZoneCount == 0) {
|
|
206
|
+
outScatter = vec4(0.0, 0.0, 0.0, 1.0);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#define VOL_STEPS 4
|
|
211
|
+
vec3 sample_ = vec3(0.0);
|
|
212
|
+
if (hit && segLen > 1e-3) {
|
|
213
|
+
bool hasL = uLightCount > 0;
|
|
214
|
+
bool hasE = uEmissiveCount > 0;
|
|
215
|
+
float segStep = segLen / float(VOL_STEPS);
|
|
216
|
+
// Piecewise integration: density can vary along the ray (zones), so the
|
|
217
|
+
// transmittance is built up step by step from the LOCAL density at each
|
|
218
|
+
// sample rather than a single closed-form exp(-uDensity * t).
|
|
219
|
+
float opticalDepth = 0.0;
|
|
220
|
+
for (int k = 0; k < VOL_STEPS; k++) {
|
|
221
|
+
float t = (float(k) + rand()) * segStep; // ascending strata
|
|
222
|
+
vec3 S = uCameraPos + rd * t;
|
|
223
|
+
float local = fogDensityAt(S);
|
|
224
|
+
opticalDepth += local * segStep;
|
|
225
|
+
vec3 Lin = vec3(0.0);
|
|
226
|
+
// Stochastically pick analytic lights or the emissive set, weighted 1/p.
|
|
227
|
+
if (hasL && hasE) {
|
|
228
|
+
if (rand() < 0.5) {
|
|
229
|
+
int i = min(int(rand() * float(uLightCount)), uLightCount - 1);
|
|
230
|
+
Lin = lightAt(i, S) * float(uLightCount) * 2.0;
|
|
231
|
+
} else {
|
|
232
|
+
Lin = emissiveAt(S) * 2.0;
|
|
233
|
+
}
|
|
234
|
+
} else if (hasL) {
|
|
235
|
+
int i = min(int(rand() * float(uLightCount)), uLightCount - 1);
|
|
236
|
+
Lin = lightAt(i, S) * float(uLightCount);
|
|
237
|
+
} else if (hasE) {
|
|
238
|
+
Lin = emissiveAt(S);
|
|
239
|
+
}
|
|
240
|
+
vec3 c = Lin * local * segStep * exp(-opticalDepth);
|
|
241
|
+
// per-step spike clamp — outliers decay only as 1/count in the EMA
|
|
242
|
+
float sl = dot(c, vec3(0.299, 0.587, 0.114));
|
|
243
|
+
if (sl > 2.0) c *= 2.0 / sl;
|
|
244
|
+
sample_ += c;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- temporal accumulation, reprojected through the surface point ---
|
|
249
|
+
float count = 1.0;
|
|
250
|
+
vec3 history = vec3(0.0);
|
|
251
|
+
if (hit) {
|
|
252
|
+
vec4 clip = uPrevViewProj * vec4(P, 1.0);
|
|
253
|
+
if (clip.w > 0.0) {
|
|
254
|
+
vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5;
|
|
255
|
+
if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
|
|
256
|
+
vec4 h = texture(uPrevAccum, prevUv);
|
|
257
|
+
count = clamp(h.a, 0.0, uMaxHistory) + 1.0;
|
|
258
|
+
history = h.rgb;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
vec3 blended = mix(history, sample_, 1.0 / count);
|
|
263
|
+
if (any(isnan(blended)) || any(isinf(blended))) blended = vec3(0.0);
|
|
264
|
+
outScatter = vec4(blended, count);
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Optional volumetric lighting pass at lighting resolution. Ping-pongs an
|
|
270
|
+
* accumulation buffer like RTLightingPass; the composite adds the result
|
|
271
|
+
* before fog/tonemap. Cost when enabled ≈ one extra shadow ray per lighting
|
|
272
|
+
* pixel per frame.
|
|
273
|
+
*/
|
|
274
|
+
export class VolumetricPass {
|
|
275
|
+
constructor(width, height) {
|
|
276
|
+
this.targetA = this._makeTarget(width, height);
|
|
277
|
+
this.targetB = this._makeTarget(width, height);
|
|
278
|
+
|
|
279
|
+
this.material = new THREE.ShaderMaterial({
|
|
280
|
+
glslVersion: THREE.GLSL3,
|
|
281
|
+
vertexShader: fullscreenVert,
|
|
282
|
+
fragmentShader: volumetricFrag,
|
|
283
|
+
uniforms: {
|
|
284
|
+
bvhStatic: { value: null },
|
|
285
|
+
bvhDynamic: { value: null },
|
|
286
|
+
uHasDynamic: { value: false },
|
|
287
|
+
uMaterialsTex: { value: null },
|
|
288
|
+
uGWorldPos: { value: null },
|
|
289
|
+
uPrevAccum: { value: null },
|
|
290
|
+
uPrevViewProj: { value: new THREE.Matrix4() },
|
|
291
|
+
uMaxHistory: { value: 48 },
|
|
292
|
+
uLightPosType: { value: [] },
|
|
293
|
+
uLightColorRadius: { value: [] },
|
|
294
|
+
uLightDirCone: { value: [] },
|
|
295
|
+
uLightCount: { value: 0 },
|
|
296
|
+
uEmissiveCount: { value: 0 },
|
|
297
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
298
|
+
uFrame: { value: 0 },
|
|
299
|
+
uEps: { value: 1e-3 },
|
|
300
|
+
uDensity: { value: 0.03 },
|
|
301
|
+
uMaxDist: { value: 40 },
|
|
302
|
+
// Localized fog zones: flat vec4 array, two vec4 per zone (see shader).
|
|
303
|
+
uFogZones: { value: new Array(MAX_FOG_ZONES * 2).fill(0).map(() => new THREE.Vector4()) },
|
|
304
|
+
uFogZoneCount: { value: 0 },
|
|
305
|
+
},
|
|
306
|
+
depthTest: false,
|
|
307
|
+
depthWrite: false,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Reused per-frame scratch so we don't allocate the zone vectors each call.
|
|
311
|
+
this._zoneVecs = this.material.uniforms.uFogZones.value;
|
|
312
|
+
|
|
313
|
+
this.scene = new THREE.Scene();
|
|
314
|
+
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
315
|
+
this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
|
|
316
|
+
this.quad.frustumCulled = false;
|
|
317
|
+
this.scene.add(this.quad);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_makeTarget(width, height) {
|
|
321
|
+
const t = new THREE.WebGLRenderTarget(width, height, {
|
|
322
|
+
minFilter: THREE.LinearFilter,
|
|
323
|
+
magFilter: THREE.LinearFilter,
|
|
324
|
+
format: THREE.RGBAFormat,
|
|
325
|
+
type: THREE.HalfFloatType,
|
|
326
|
+
depthBuffer: false,
|
|
327
|
+
stencilBuffer: false,
|
|
328
|
+
});
|
|
329
|
+
t.texture.generateMipmaps = false;
|
|
330
|
+
return t;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
setCompiledScene(compiled) {
|
|
334
|
+
const u = this.material.uniforms;
|
|
335
|
+
u.bvhStatic.value = compiled.staticBvhUniform;
|
|
336
|
+
u.bvhDynamic.value = compiled.dynamicBvhUniform;
|
|
337
|
+
u.uHasDynamic.value = compiled.hasDynamic;
|
|
338
|
+
u.uMaterialsTex.value = compiled.materialsTex;
|
|
339
|
+
u.uLightPosType.value = compiled.lightPosType;
|
|
340
|
+
u.uLightColorRadius.value = compiled.lightColorRadius;
|
|
341
|
+
u.uLightDirCone.value = compiled.lightDirCone;
|
|
342
|
+
u.uLightCount.value = compiled.lightCount;
|
|
343
|
+
u.uEmissiveCount.value = compiled.emissiveTriCount;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
clearHistory(renderer) {
|
|
347
|
+
const prev = renderer.getRenderTarget();
|
|
348
|
+
renderer.setClearColor(0x000000, 0);
|
|
349
|
+
for (const t of [this.targetA, this.targetB]) {
|
|
350
|
+
renderer.setRenderTarget(t);
|
|
351
|
+
renderer.clear(true, false, false);
|
|
352
|
+
}
|
|
353
|
+
renderer.setRenderTarget(prev);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
setSize(width, height) {
|
|
357
|
+
this.targetA.setSize(width, height);
|
|
358
|
+
this.targetB.setSize(width, height);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Renders into targetA (reading targetB as history), swaps, returns the texture. */
|
|
362
|
+
render(renderer, gbuffer, prevViewProj, cameraPos, frame, eps, density, maxDist, zones) {
|
|
363
|
+
const u = this.material.uniforms;
|
|
364
|
+
u.uGWorldPos.value = gbuffer.worldPos;
|
|
365
|
+
u.uPrevAccum.value = this.targetB.texture;
|
|
366
|
+
u.uPrevViewProj.value.copy(prevViewProj);
|
|
367
|
+
u.uCameraPos.value.copy(cameraPos);
|
|
368
|
+
u.uFrame.value = frame;
|
|
369
|
+
u.uEps.value = eps;
|
|
370
|
+
u.uDensity.value = density;
|
|
371
|
+
u.uMaxDist.value = maxDist;
|
|
372
|
+
|
|
373
|
+
// Pack up to MAX_FOG_ZONES AABBs into the flat vec4 array, two vec4 per
|
|
374
|
+
// zone: [min.xyz, density] then [max.xyz, 0]. Reuses cached Vector4s.
|
|
375
|
+
const zn = zones && zones.length ? Math.min(zones.length, MAX_FOG_ZONES) : 0;
|
|
376
|
+
for (let i = 0; i < zn; i++) {
|
|
377
|
+
const z = zones[i];
|
|
378
|
+
this._zoneVecs[i * 2].set(z.min[0], z.min[1], z.min[2], z.density);
|
|
379
|
+
this._zoneVecs[i * 2 + 1].set(z.max[0], z.max[1], z.max[2], 0);
|
|
380
|
+
}
|
|
381
|
+
u.uFogZoneCount.value = zn;
|
|
382
|
+
|
|
383
|
+
renderer.setRenderTarget(this.targetA);
|
|
384
|
+
renderer.render(this.scene, this.camera);
|
|
385
|
+
renderer.setRenderTarget(null);
|
|
386
|
+
|
|
387
|
+
const out = this.targetA;
|
|
388
|
+
[this.targetA, this.targetB] = [this.targetB, this.targetA];
|
|
389
|
+
return out.texture;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
dispose() {
|
|
393
|
+
this.targetA.dispose();
|
|
394
|
+
this.targetB.dispose();
|
|
395
|
+
this.material.dispose();
|
|
396
|
+
this.quad.geometry.dispose();
|
|
397
|
+
}
|
|
398
|
+
}
|