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.
@@ -0,0 +1,402 @@
1
+ import * as THREE from "three";
2
+ import { MAX_LIGHTS } from "./SceneCompiler.js";
3
+
4
+ const fullscreenVert = /* glsl */ `
5
+ out vec2 vUv;
6
+ void main() {
7
+ vUv = uv;
8
+ gl_Position = vec4(position.xy, 0.0, 1.0);
9
+ }
10
+ `;
11
+
12
+ // GLSL shared by the temporal and spatial reservoir shaders: RNG (blue noise
13
+ // first, PCG after) and the candidate contribution — which MUST stay in
14
+ // agreement with RTLightingPass.shadeReservoir (minus visibility).
15
+ const RESTIR_COMMON = /* glsl */ `
16
+ #define MAX_LIGHTS ${MAX_LIGHTS}
17
+ #define PI 3.14159265358979
18
+
19
+ uniform sampler2D uGWorldPos;
20
+ uniform sampler2D uGNormalMetal;
21
+ uniform sampler2D uMaterialsTex; // row 1: emissive tris, rows 2..65: blue noise
22
+ uniform vec4 uLightPosType[MAX_LIGHTS];
23
+ uniform vec4 uLightColorRadius[MAX_LIGHTS];
24
+ uniform int uLightCount;
25
+ uniform int uEmissiveCount;
26
+ uniform float uFrame;
27
+ uniform vec3 uCameraPos;
28
+ uniform float uEps;
29
+
30
+ uint gSeed;
31
+ int gBnDim;
32
+ vec4 gBlueNoise;
33
+ uint pcgHash(uint s) {
34
+ uint state = s * 747796405u + 2891336453u;
35
+ uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
36
+ return (word >> 22u) ^ word;
37
+ }
38
+ float rand() {
39
+ if (gBnDim < 4) {
40
+ float v = gBlueNoise[gBnDim];
41
+ gBnDim++;
42
+ return v;
43
+ }
44
+ gSeed = pcgHash(gSeed);
45
+ return float(gSeed) * (1.0 / 4294967296.0);
46
+ }
47
+ vec4 fetchBlueNoise() {
48
+ ivec2 p = ivec2(gl_FragCoord.xy) & 63;
49
+ vec4 bn = texelFetch(uMaterialsTex, ivec2(p.x, 2 + p.y), 0);
50
+ vec4 shift = fract(float(uFrame) * vec4(0.6180340, 0.7548777, 0.5698403, 0.8191725));
51
+ return fract(bn + shift);
52
+ }
53
+
54
+ float luminance(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }
55
+
56
+ // Unshadowed contribution of candidate (id, uv) at surface (P, N).
57
+ vec3 candidateContribution(float id, vec2 uv, vec3 P, vec3 N) {
58
+ if (id < float(MAX_LIGHTS)) {
59
+ int i = int(id);
60
+ vec4 posType = uLightPosType[i];
61
+ vec4 colRad = uLightColorRadius[i];
62
+ if (posType.w < 0.5) {
63
+ vec3 d = posType.xyz - P; // light CENTER: soft-radius jitter re-drawn at shading
64
+ float dl = length(d);
65
+ if (dl < 1e-5) return vec3(0.0);
66
+ float NdotL = dot(N, d / dl);
67
+ if (NdotL <= 0.0) return vec3(0.0);
68
+ return colRad.rgb * (NdotL / (dl * dl));
69
+ }
70
+ float NdotL = dot(N, -posType.xyz);
71
+ if (NdotL <= 0.0) return vec3(0.0);
72
+ return colRad.rgb * NdotL;
73
+ }
74
+ int t = (int(id) - MAX_LIGHTS) * 4;
75
+ vec4 t0 = texelFetch(uMaterialsTex, ivec2(t, 1), 0);
76
+ vec4 t1 = texelFetch(uMaterialsTex, ivec2(t + 1, 1), 0);
77
+ vec4 t2 = texelFetch(uMaterialsTex, ivec2(t + 2, 1), 0);
78
+ vec4 t3 = texelFetch(uMaterialsTex, ivec2(t + 3, 1), 0);
79
+ vec3 lp = t0.xyz + t1.xyz * uv.x + t2.xyz * uv.y;
80
+ vec3 d = lp - P;
81
+ float d2 = dot(d, d);
82
+ float dist = sqrt(d2);
83
+ if (dist < 1e-4) return vec3(0.0);
84
+ vec3 wi = d / dist;
85
+ float cosS = dot(N, wi);
86
+ float cosL = abs(dot(t3.xyz, wi));
87
+ if (cosS <= 0.0 || cosL < 1e-4) return vec3(0.0);
88
+ // Uniform pick within the emissive set happens at candidate level, so the
89
+ // per-triangle contribution uses area only (count folds into pick pdf).
90
+ return vec3(t1.w, t2.w, t3.w) * (cosS * cosL * t0.w / max(d2, 1e-6));
91
+ }
92
+
93
+ // v3: reservoirs select TRIANGLES, not points. The selection target is the
94
+ // candidate's contribution at a FIXED proxy point (the centroid) — any fixed
95
+ // score keeps RIS consistent as long as shading divides by the same one. The
96
+ // actual surface point is re-drawn fresh every frame at shading, so soft
97
+ // area lighting keeps averaging instead of freezing onto one winning point.
98
+ // (Known approximation: a triangle whose centroid contributes zero but whose
99
+ // far corner doesn't can be under-selected at grazing setups.)
100
+ float phatOf(float id, vec3 P, vec3 N) {
101
+ return luminance(candidateContribution(id, vec2(1.0 / 3.0), P, N));
102
+ }
103
+ `;
104
+
105
+ // TEMPORAL stage. Streams K fresh candidates through a weighted reservoir,
106
+ // merges with the reprojected previous frame's reservoir. Output encoding
107
+ // (fed back as history next frame):
108
+ // r = candidateId * 64 + min(M, 63), g,b = unused, a = wSum
109
+ const temporalFrag = /* glsl */ `
110
+ precision highp float;
111
+
112
+ ${RESTIR_COMMON}
113
+
114
+ #define CANDIDATES 8
115
+
116
+ layout(location = 0) out vec4 outReservoir;
117
+
118
+ in vec2 vUv;
119
+
120
+ uniform sampler2D uPrevReservoir;
121
+ uniform sampler2D uPrevGWorldPos;
122
+ uniform mat4 uPrevViewProj;
123
+
124
+ void main() {
125
+ vec4 wp = texture(uGWorldPos, vUv);
126
+ if (wp.w < 0.5) {
127
+ outReservoir = vec4(0.0);
128
+ return;
129
+ }
130
+ vec3 P = wp.xyz;
131
+ vec3 N = normalize(texture(uGNormalMetal, vUv).xyz);
132
+
133
+ ivec2 px = ivec2(gl_FragCoord.xy);
134
+ gSeed = uint(px.x) * 3079u + uint(px.y) * 9277u + uint(uFrame) * 26699u;
135
+ gSeed = pcgHash(gSeed);
136
+ gBlueNoise = fetchBlueNoise();
137
+ gBnDim = 0;
138
+
139
+ int S = uLightCount + uEmissiveCount; // uniform source pool
140
+ if (S == 0) {
141
+ outReservoir = vec4(0.0);
142
+ return;
143
+ }
144
+
145
+ float rId = 0.0;
146
+ float wSum = 0.0;
147
+ float M = 0.0;
148
+ for (int k = 0; k < CANDIDATES; k++) {
149
+ int pick = min(int(rand() * float(S)), S - 1);
150
+ float id = pick < uLightCount
151
+ ? float(pick)
152
+ : float(MAX_LIGHTS + (pick - uLightCount));
153
+ // source pdf = 1/S -> RIS weight = p̂ * S
154
+ float w = phatOf(id, P, N) * float(S);
155
+ wSum += w;
156
+ M += 1.0;
157
+ if (w > 0.0 && rand() * wSum < w) { rId = id; }
158
+ }
159
+
160
+ // temporal reuse: previous reservoir as ONE candidate carrying its history
161
+ vec4 clip = uPrevViewProj * vec4(P, 1.0);
162
+ if (clip.w > 0.0) {
163
+ vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5;
164
+ if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
165
+ vec4 prevPos = texture(uPrevGWorldPos, prevUv);
166
+ float tol = 0.005 * distance(P, uCameraPos) + 20.0 * uEps;
167
+ if (prevPos.w > 0.5 && abs(dot(P - prevPos.xyz, N)) < tol) {
168
+ vec4 h = texture(uPrevReservoir, prevUv);
169
+ // Staleness cap; ALSO keeps total M within the 6 bits the encoding
170
+ // stores (8 fresh + 40 history < 64).
171
+ float hM = min(mod(h.r, 64.0), 40.0);
172
+ float hId = floor(h.r / 64.0);
173
+ if (hM > 0.0 && h.a > 0.0) {
174
+ // RIS weight = p̂_now · W_prev · M_prev; with p̂_prev ≈ p̂_now on a
175
+ // validated surface this reduces to (wSum/M)·M.
176
+ float hPhat = phatOf(hId, P, N);
177
+ float w = hPhat > 0.0 ? (h.a / max(mod(h.r, 64.0), 1.0)) * hM : 0.0;
178
+ wSum += w;
179
+ M += hM;
180
+ if (w > 0.0 && rand() * wSum < w) { rId = hId; }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ outReservoir = vec4(rId * 64.0 + min(M, 63.0), 0.0, 0.0, wSum);
187
+ }
188
+ `;
189
+
190
+ // SPATIAL stage (the "v2" of ReSTIR): merge a few validated neighbor
191
+ // reservoirs so pixels share each other's discoveries — the big win for
192
+ // convergence speed and area-light (emissive) noise. Output is consumed by
193
+ // the lighting pass THIS frame only (never fed back), so it trades the
194
+ // history encoding for a precomputed weight:
195
+ // r = candidateId, g,b = unused, a = W = wSum / (M · p̂centroid)
196
+ const spatialFrag = /* glsl */ `
197
+ precision highp float;
198
+
199
+ ${RESTIR_COMMON}
200
+
201
+ #define NEIGHBORS 4
202
+
203
+ layout(location = 0) out vec4 outReservoir;
204
+
205
+ in vec2 vUv;
206
+
207
+ uniform sampler2D uReservoirIn;
208
+ uniform vec2 uTexelSize;
209
+
210
+ void main() {
211
+ vec4 wp = texture(uGWorldPos, vUv);
212
+ if (wp.w < 0.5) {
213
+ outReservoir = vec4(0.0);
214
+ return;
215
+ }
216
+ vec3 P = wp.xyz;
217
+ vec3 N = normalize(texture(uGNormalMetal, vUv).xyz);
218
+
219
+ ivec2 px = ivec2(gl_FragCoord.xy);
220
+ gSeed = uint(px.x) * 5417u + uint(px.y) * 7907u + uint(uFrame) * 15731u;
221
+ gSeed = pcgHash(gSeed);
222
+ gBlueNoise = fetchBlueNoise();
223
+ gBnDim = 3; // decorrelate from the temporal stage's blue-noise dims
224
+
225
+ vec4 c = texture(uReservoirIn, vUv);
226
+ float rId = floor(c.r / 64.0);
227
+ float M = mod(c.r, 64.0);
228
+ float wSum = c.a;
229
+
230
+ float tol = 0.005 * distance(P, uCameraPos) + 20.0 * uEps;
231
+ for (int k = 0; k < NEIGHBORS; k++) {
232
+ float a = (float(k) + rand()) * (2.0 * PI / float(NEIGHBORS));
233
+ float rad = 2.0 + rand() * 8.0; // taps within ~10 lighting-res texels
234
+ vec2 nUv = vUv + vec2(cos(a), sin(a)) * rad * uTexelSize;
235
+ if (nUv.x < 0.0 || nUv.x > 1.0 || nUv.y < 0.0 || nUv.y > 1.0) continue;
236
+
237
+ // geometric validation: same plane + similar orientation, or the
238
+ // neighbor's chosen light is meaningless here
239
+ vec4 nwp = texture(uGWorldPos, nUv);
240
+ if (nwp.w < 0.5) continue;
241
+ if (abs(dot(nwp.xyz - P, N)) > tol) continue;
242
+ vec3 nN = normalize(texture(uGNormalMetal, nUv).xyz);
243
+ if (dot(N, nN) < 0.9) continue;
244
+
245
+ vec4 h = texture(uReservoirIn, nUv);
246
+ float hM = mod(h.r, 64.0);
247
+ if (hM < 1.0 || h.a <= 0.0) continue;
248
+ float hId = floor(h.r / 64.0);
249
+ // neighbor reservoir as one candidate, re-weighted at THIS surface
250
+ float hPhat = phatOf(hId, P, N);
251
+ float w = hPhat > 0.0 ? (h.a / hM) * min(hM, 40.0) : 0.0;
252
+ wSum += w;
253
+ M += min(hM, 40.0);
254
+ if (w > 0.0 && rand() * wSum < w) { rId = hId; }
255
+ }
256
+
257
+ float phat = phatOf(rId, P, N);
258
+ float W = (M > 0.0 && phat > 0.0) ? wSum / (M * phat) : 0.0;
259
+ outReservoir = vec4(rId, 0.0, 0.0, W);
260
+ }
261
+ `;
262
+
263
+ /**
264
+ * ReSTIR DI reservoirs at lighting resolution: temporal reuse (ping-ponged
265
+ * history) followed by one spatial-reuse iteration. render() returns the
266
+ * spatial output — {id, uv, W} per pixel — for the lighting pass to shade
267
+ * with a single visibility ray.
268
+ */
269
+ export class RestirPass {
270
+ constructor(width, height) {
271
+ this.targetA = this._makeTarget(width, height);
272
+ this.targetB = this._makeTarget(width, height);
273
+ this.spatialTarget = this._makeTarget(width, height);
274
+
275
+ const mkMaterial = (frag) =>
276
+ new THREE.ShaderMaterial({
277
+ glslVersion: THREE.GLSL3,
278
+ vertexShader: fullscreenVert,
279
+ fragmentShader: frag,
280
+ uniforms: {
281
+ uGWorldPos: { value: null },
282
+ uGNormalMetal: { value: null },
283
+ uMaterialsTex: { value: null },
284
+ uLightPosType: { value: [] },
285
+ uLightColorRadius: { value: [] },
286
+ uLightCount: { value: 0 },
287
+ uEmissiveCount: { value: 0 },
288
+ uFrame: { value: 0 },
289
+ uCameraPos: { value: new THREE.Vector3() },
290
+ uEps: { value: 1e-3 },
291
+ ...(frag === temporalFrag
292
+ ? {
293
+ uPrevReservoir: { value: null },
294
+ uPrevGWorldPos: { value: null },
295
+ uPrevViewProj: { value: new THREE.Matrix4() },
296
+ }
297
+ : {
298
+ uReservoirIn: { value: null },
299
+ uTexelSize: { value: new THREE.Vector2(1 / width, 1 / height) },
300
+ }),
301
+ },
302
+ depthTest: false,
303
+ depthWrite: false,
304
+ });
305
+
306
+ this.material = mkMaterial(temporalFrag);
307
+ this.spatialMaterial = mkMaterial(spatialFrag);
308
+
309
+ this.scene = new THREE.Scene();
310
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
311
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
312
+ this.quad.frustumCulled = false;
313
+ this.scene.add(this.quad);
314
+ }
315
+
316
+ _makeTarget(width, height) {
317
+ // Full float + nearest: reservoirs must never be interpolated (a blend of
318
+ // two candidate ids is meaningless).
319
+ const t = new THREE.WebGLRenderTarget(width, height, {
320
+ minFilter: THREE.NearestFilter,
321
+ magFilter: THREE.NearestFilter,
322
+ format: THREE.RGBAFormat,
323
+ type: THREE.FloatType,
324
+ depthBuffer: false,
325
+ stencilBuffer: false,
326
+ });
327
+ t.texture.generateMipmaps = false;
328
+ return t;
329
+ }
330
+
331
+ setCompiledScene(compiled) {
332
+ for (const m of [this.material, this.spatialMaterial]) {
333
+ const u = m.uniforms;
334
+ u.uMaterialsTex.value = compiled.materialsTex;
335
+ u.uLightPosType.value = compiled.lightPosType;
336
+ u.uLightColorRadius.value = compiled.lightColorRadius;
337
+ u.uLightCount.value = compiled.lightCount;
338
+ u.uEmissiveCount.value = compiled.emissiveTriCount;
339
+ }
340
+ }
341
+
342
+ /** Emissive candidates follow the emissiveNEE toggle (set per frame). */
343
+ setEmissiveCount(count) {
344
+ this.material.uniforms.uEmissiveCount.value = count;
345
+ this.spatialMaterial.uniforms.uEmissiveCount.value = count;
346
+ }
347
+
348
+ clearHistory(renderer) {
349
+ const prev = renderer.getRenderTarget();
350
+ renderer.setClearColor(0x000000, 0);
351
+ for (const t of [this.targetA, this.targetB, this.spatialTarget]) {
352
+ renderer.setRenderTarget(t);
353
+ renderer.clear(true, false, false);
354
+ }
355
+ renderer.setRenderTarget(prev);
356
+ }
357
+
358
+ setSize(width, height) {
359
+ this.targetA.setSize(width, height);
360
+ this.targetB.setSize(width, height);
361
+ this.spatialTarget.setSize(width, height);
362
+ this.spatialMaterial.uniforms.uTexelSize.value.set(1 / width, 1 / height);
363
+ }
364
+
365
+ /** Temporal → spatial; history feeds back from the TEMPORAL stage only. */
366
+ render(renderer, gbuffer, prevViewProj, cameraPos, frame, eps) {
367
+ for (const m of [this.material, this.spatialMaterial]) {
368
+ const u = m.uniforms;
369
+ u.uGWorldPos.value = gbuffer.worldPos;
370
+ u.uGNormalMetal.value = gbuffer.normalMetal;
371
+ u.uFrame.value = frame;
372
+ u.uCameraPos.value.copy(cameraPos);
373
+ u.uEps.value = eps;
374
+ }
375
+ const tu = this.material.uniforms;
376
+ tu.uPrevReservoir.value = this.targetB.texture;
377
+ tu.uPrevGWorldPos.value = gbuffer.prevWorldPos;
378
+ tu.uPrevViewProj.value.copy(prevViewProj);
379
+
380
+ this.quad.material = this.material;
381
+ renderer.setRenderTarget(this.targetA);
382
+ renderer.render(this.scene, this.camera);
383
+
384
+ this.spatialMaterial.uniforms.uReservoirIn.value = this.targetA.texture;
385
+ this.quad.material = this.spatialMaterial;
386
+ renderer.setRenderTarget(this.spatialTarget);
387
+ renderer.render(this.scene, this.camera);
388
+ renderer.setRenderTarget(null);
389
+
390
+ [this.targetA, this.targetB] = [this.targetB, this.targetA];
391
+ return this.spatialTarget.texture;
392
+ }
393
+
394
+ dispose() {
395
+ this.targetA.dispose();
396
+ this.targetB.dispose();
397
+ this.spatialTarget.dispose();
398
+ this.material.dispose();
399
+ this.spatialMaterial.dispose();
400
+ this.quad.geometry.dispose();
401
+ }
402
+ }