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.
@@ -0,0 +1,75 @@
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
+ // Copies a texture into the bound target, bilinearly resampling when the sizes
12
+ // differ (history targets use LinearFilter, so a resolution change is handled
13
+ // for free). uCountClamp < 0 copies verbatim; >= 0 caps the alpha channel —
14
+ // used to carry accumulation history across a reallocation while REDUCING (not
15
+ // zeroing) the per-pixel sample count, so the temporal EMA reconverges smoothly
16
+ // instead of strobing back to raw 1-spp noise.
17
+ const copyFrag = /* glsl */ `
18
+ precision highp float;
19
+ layout(location = 0) out vec4 outColor;
20
+ in vec2 vUv;
21
+ uniform sampler2D uTex;
22
+ uniform float uCountClamp;
23
+ void main() {
24
+ vec4 c = texture(uTex, vUv);
25
+ if (uCountClamp >= 0.0) c.a = min(c.a, uCountClamp);
26
+ outColor = c;
27
+ }
28
+ `;
29
+
30
+ /**
31
+ * Minimal fullscreen texture copy on its own program (one sampler — safely
32
+ * separate from the 16-sampler lighting pass). Used to blit history buffers
33
+ * into freshly reallocated targets during a resolution change: the linear
34
+ * resample absorbs the size difference, and the optional alpha clamp turns a
35
+ * hard accumulation reset into a soft "keep a few frames of confidence" carry.
36
+ */
37
+ export class CopyPass {
38
+ constructor() {
39
+ this.material = new THREE.ShaderMaterial({
40
+ glslVersion: THREE.GLSL3,
41
+ vertexShader: fullscreenVert,
42
+ fragmentShader: copyFrag,
43
+ uniforms: {
44
+ uTex: { value: null },
45
+ uCountClamp: { value: -1 },
46
+ },
47
+ depthTest: false,
48
+ depthWrite: false,
49
+ });
50
+
51
+ this.scene = new THREE.Scene();
52
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
53
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
54
+ this.quad.frustumCulled = false;
55
+ this.scene.add(this.quad);
56
+ }
57
+
58
+ /**
59
+ * Blit srcTexture into dstTarget. countClamp < 0 copies rgba verbatim; >= 0
60
+ * caps the alpha (accumulation-count) channel to that value.
61
+ */
62
+ blit(renderer, srcTexture, dstTarget, countClamp = -1) {
63
+ this.material.uniforms.uTex.value = srcTexture;
64
+ this.material.uniforms.uCountClamp.value = countClamp;
65
+ const prev = renderer.getRenderTarget();
66
+ renderer.setRenderTarget(dstTarget);
67
+ renderer.render(this.scene, this.camera);
68
+ renderer.setRenderTarget(prev);
69
+ }
70
+
71
+ dispose() {
72
+ this.material.dispose();
73
+ this.quad.geometry.dispose();
74
+ }
75
+ }
@@ -1,202 +1,221 @@
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
- }
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
+ uniform bool uBlendIsSpec; // this instance filters the specular buffer
27
+
28
+ float luminance(vec3 c) {
29
+ return dot(c, vec3(0.299, 0.587, 0.114));
30
+ }
31
+
32
+ void main() {
33
+ vec4 center = texture(uIrradiance, vUv);
34
+ vec4 wp = texture(uGWorldPos, vUv);
35
+ if (wp.w < 0.5) {
36
+ outColor = center;
37
+ return;
38
+ }
39
+ vec3 P = wp.xyz;
40
+ vec4 nm = texture(uGNormalMetal, vUv);
41
+ vec3 N = normalize(nm.xyz);
42
+ // Specular surfaces (mirror metals, glass) carry traced reflections whose
43
+ // detail is NOT in the G-buffer guides filtering would smear them, and
44
+ // their signal is nearly deterministic anyway. Scale the filter down as the
45
+ // surface gets more mirror-like.
46
+ // Packed word ranges (see GBufferPass): [4,5] alpha blend, [2,4) glass,
47
+ // [0,1] metal. In the IRRADIANCE buffer a blend surface carries diffuse-lit
48
+ // direct + GI that DOES need filtering (specAmount 0); in the SPECULAR buffer
49
+ // (uBlendIsSpec) it carries the traced behind-the-pane image, which must be
50
+ // spared like a mirror the pane is flat, so the G-buffer guides would let
51
+ // the filter smear the see-through content into mush.
52
+ float matW = nm.w;
53
+ float specAmount = matW >= 4.0 ? (uBlendIsSpec ? 1.0 : 0.0)
54
+ : (matW >= 2.0 ? clamp(matW - 2.0, 0.0, 1.0) : matW);
55
+ float specKeep = specAmount * (1.0 - clamp(wp.w - 1.0, 0.0, 1.0));
56
+
57
+ // Fewer accumulated samples -> noisier pixel -> wider luminance tolerance.
58
+ // A converged pixel (high count) is barely touched, preserving detail.
59
+ // The widening is CAPPED at 3x: during camera motion every pixel is fresh,
60
+ // and an 8x-wide gate across five à-trous passes erased small contact
61
+ // shadows entirely objects visibly floated while orbiting ("ghostly
62
+ // apparitions") and only grounded once the camera stopped. Blue-noise
63
+ // sampling + ReSTIR keep fresh pixels clean enough for the tighter gate.
64
+ float count = max(center.a, 1.0);
65
+ float sigmaL = uLumSigma * clamp(8.0 / sqrt(count), 0.75, 3.0);
66
+
67
+ float distToCam = distance(P, uCameraPos);
68
+ float planeTol = 0.01 * distToCam + 20.0 * uEps;
69
+
70
+ // Despeckle (first iteration, short history only): a freshly disoccluded
71
+ // pixel can carry one huge sample that the center-weighted filter would
72
+ // preserve as a bright "rain drop" at silhouettes. Such a pixel has no
73
+ // business being brighter than its entire neighbourhood — clamp its
74
+ // luminance to the brightest neighbour. Converged pixels are exempt, so
75
+ // real small highlights survive.
76
+ if (uStep < 1.5 && count < 8.0) {
77
+ float maxL = 0.0;
78
+ float found = 0.0;
79
+ for (int dy = -1; dy <= 1; dy++) {
80
+ for (int dx = -1; dx <= 1; dx++) {
81
+ if (dx == 0 && dy == 0) continue;
82
+ vec2 tuv = vUv + vec2(float(dx), float(dy)) * uTexelSize;
83
+ if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) continue;
84
+ if (texture(uGWorldPos, tuv).w < 0.5) continue;
85
+ maxL = max(maxL, luminance(texture(uIrradiance, tuv).rgb));
86
+ found = 1.0;
87
+ }
88
+ }
89
+ float cap = maxL * 1.25 + 1e-4;
90
+ float l = luminance(center.rgb);
91
+ if (found > 0.5 && l > cap) center.rgb *= cap / l;
92
+ }
93
+
94
+ float lumC = luminance(center.rgb);
95
+
96
+ // 3x3 B-spline-ish kernel, edge-avoiding weights.
97
+ vec3 sum = center.rgb * 4.0;
98
+ float wsum = 4.0;
99
+ for (int dy = -1; dy <= 1; dy++) {
100
+ for (int dx = -1; dx <= 1; dx++) {
101
+ if (dx == 0 && dy == 0) continue;
102
+ vec2 tuv = vUv + vec2(float(dx), float(dy)) * uStep * uTexelSize;
103
+ if (tuv.x < 0.0 || tuv.x > 1.0 || tuv.y < 0.0 || tuv.y > 1.0) continue;
104
+
105
+ vec4 g = texture(uGWorldPos, tuv);
106
+ if (g.w < 0.5) continue;
107
+ vec4 s = texture(uIrradiance, tuv);
108
+ vec3 Nt = normalize(texture(uGNormalMetal, tuv).xyz);
109
+
110
+ float k = (dx == 0 || dy == 0) ? 2.0 : 1.0;
111
+ float wN = pow(max(dot(N, Nt), 0.0), 32.0);
112
+ float wZ = exp(-abs(dot(g.xyz - P, N)) / planeTol);
113
+ // Tighten the luminance gate as the à-trous step widens: a shadow on a
114
+ // flat floor has no geometric edge to protect it, so at high iteration
115
+ // counts the wide passes would average it away ("floating" objects with
116
+ // no contact shadow). Wide steps only get to blend near-equal luminance.
117
+ float wL = exp(-abs(luminance(s.rgb) - lumC) / (sigmaL * inversesqrt(uStep)));
118
+ float w = k * wN * wZ * wL * (1.0 - specKeep);
119
+ sum += s.rgb * w;
120
+ wsum += w;
121
+ }
122
+ }
123
+ outColor = vec4(sum / wsum, center.a);
124
+ }
125
+ `;
126
+
127
+ /**
128
+ * Edge-avoiding à-trous wavelet filter (SVGF-lite) on the demodulated
129
+ * irradiance buffer, guided by the full-res G-buffer (plane distance +
130
+ * normals) and modulated by per-pixel history count: freshly disoccluded
131
+ * pixels get blurred hard, converged pixels stay crisp.
132
+ */
133
+ export class DenoisePass {
134
+ // `blendIsSpec`: the SPECULAR-buffer instance passes true — for blend pixels
135
+ // that buffer holds the traced behind-the-pane image, whose detail is not in
136
+ // the G-buffer guides (the pane itself is flat), so filtering would smear it
137
+ // into mush. The irradiance instance keeps false: there a blend pixel holds
138
+ // the pane's own diffuse-lit surface, which does want filtering.
139
+ constructor(width, height, { blendIsSpec = false } = {}) {
140
+ this.targetA = this._makeTarget(width, height);
141
+ this.targetB = this._makeTarget(width, height);
142
+
143
+ this.material = new THREE.ShaderMaterial({
144
+ glslVersion: THREE.GLSL3,
145
+ vertexShader: fullscreenVert,
146
+ fragmentShader: atrousFrag,
147
+ uniforms: {
148
+ uIrradiance: { value: null },
149
+ uGWorldPos: { value: null },
150
+ uGNormalMetal: { value: null },
151
+ uTexelSize: { value: new THREE.Vector2() },
152
+ uStep: { value: 1 },
153
+ uCameraPos: { value: new THREE.Vector3() },
154
+ uEps: { value: 1e-3 },
155
+ uLumSigma: { value: 0.25 },
156
+ uBlendIsSpec: { value: blendIsSpec },
157
+ },
158
+ depthTest: false,
159
+ depthWrite: false,
160
+ });
161
+
162
+ this.scene = new THREE.Scene();
163
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
164
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
165
+ this.quad.frustumCulled = false;
166
+ this.scene.add(this.quad);
167
+
168
+ this._width = width;
169
+ this._height = height;
170
+ }
171
+
172
+ _makeTarget(width, height) {
173
+ const t = new THREE.WebGLRenderTarget(width, height, {
174
+ minFilter: THREE.LinearFilter,
175
+ magFilter: THREE.LinearFilter,
176
+ format: THREE.RGBAFormat,
177
+ type: THREE.HalfFloatType,
178
+ depthBuffer: false,
179
+ stencilBuffer: false,
180
+ });
181
+ t.texture.generateMipmaps = false;
182
+ return t;
183
+ }
184
+
185
+ setSize(width, height) {
186
+ this._width = width;
187
+ this._height = height;
188
+ this.targetA.setSize(width, height);
189
+ this.targetB.setSize(width, height);
190
+ }
191
+
192
+ /** Runs `iterations` à-trous passes; returns the final filtered texture. */
193
+ render(renderer, inputTexture, gbuffer, cameraPos, eps, iterations = 3) {
194
+ const u = this.material.uniforms;
195
+ u.uGWorldPos.value = gbuffer.worldPos;
196
+ u.uGNormalMetal.value = gbuffer.normalMetal;
197
+ u.uTexelSize.value.set(1 / this._width, 1 / this._height);
198
+ u.uCameraPos.value.copy(cameraPos);
199
+ u.uEps.value = eps;
200
+
201
+ let read = inputTexture;
202
+ let write = this.targetA;
203
+ for (let i = 0; i < iterations; i++) {
204
+ u.uIrradiance.value = read;
205
+ u.uStep.value = 1 << i;
206
+ renderer.setRenderTarget(write);
207
+ renderer.render(this.scene, this.camera);
208
+ read = write.texture;
209
+ write = write === this.targetA ? this.targetB : this.targetA;
210
+ }
211
+ renderer.setRenderTarget(null);
212
+ return read;
213
+ }
214
+
215
+ dispose() {
216
+ this.targetA.dispose();
217
+ this.targetB.dispose();
218
+ this.material.dispose();
219
+ this.quad.geometry.dispose();
220
+ }
221
+ }