three-realtime-rt 0.3.0 → 0.3.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/src/TAAPass.js CHANGED
@@ -1,230 +1,255 @@
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
- // Temporal anti-aliasing resolve. Runs at full resolution on the final tonemapped
12
- // color. Reprojects the previous resolved frame through world position (same trick
13
- // the lighting/denoise passes use — no separate motion-vector buffer needed),
14
- // clamps that history to the current 3x3 neighborhood colour box (kills ghosting
15
- // AND the bright disocclusion speckles), then blends. Combined with sub-pixel
16
- // camera jitter in RealtimeRaytracer, silhouettes get supersampled over time —
17
- // this is the analytic (FSR2 / TAAU) approach, not a learned upscaler.
18
- const taaFrag = /* glsl */ `
19
- precision highp float;
20
-
21
- layout(location = 0) out vec4 outColor;
22
-
23
- in vec2 vUv;
24
-
25
- uniform sampler2D uCurrent; // this frame's composited LDR colour
26
- uniform sampler2D uHistory; // previous resolved colour
27
- uniform sampler2D uGWorldPos; // current full-res G-buffer
28
- uniform mat4 uPrevViewProj;
29
- uniform vec2 uTexelSize;
30
- uniform vec2 uJitter; // this frame's projection jitter (UV space)
31
- uniform vec2 uPrevJitter; // last frame's projection jitter (UV space)
32
- uniform float uBlend; // fresh-sample weight when history is valid (~0.1)
33
- uniform bool uReset; // first frame after a scene/size change
34
-
35
- // The raster (and the whole lighting chain guided by it) wobbles with the
36
- // sub-pixel jitter. Resolving on that wobbling grid drags history along with
37
- // it — the image shimmers. So the resolve UNJITTERS its input: content that
38
- // unjittered would sit at u is rasterized at u + jitter, hence sample there.
39
- // Output then lives on a stable grid and jitter only contributes sub-pixel
40
- // coverage information over time (which is what gives the anti-aliasing).
41
- vec3 sampleCurrent(vec2 uv) {
42
- return texture(uCurrent, uv + uJitter).rgb;
43
- }
44
-
45
- void main() {
46
- vec3 current = sampleCurrent(vUv);
47
- // World position of the (unjittered) content at this pixel — same offset.
48
- vec4 wp = texture(uGWorldPos, vUv + uJitter);
49
-
50
- // Background (no geometry): no useful reprojection, show current directly.
51
- if (wp.w < 0.5 || uReset) {
52
- outColor = vec4(current, 1.0);
53
- return;
54
- }
55
-
56
- vec3 P = wp.xyz;
57
-
58
- // Neighbourhood colour AABB (used to clamp history — the core anti-ghost /
59
- // anti-speckle step). Also the min corner tells us the local floor, so a
60
- // single bright fireflight can't survive the clamp.
61
- vec3 nmin = current, nmax = current;
62
- for (int dy = -1; dy <= 1; dy++) {
63
- for (int dx = -1; dx <= 1; dx++) {
64
- if (dx == 0 && dy == 0) continue;
65
- vec3 c = sampleCurrent(vUv + vec2(float(dx), float(dy)) * uTexelSize);
66
- nmin = min(nmin, c);
67
- nmax = max(nmax, c);
68
- }
69
- }
70
-
71
- // Reproject this pixel's world point into the previous frame. The history is
72
- // a STABILIZED (unjittered-grid) image, so remove last frame's jitter from
73
- // the projected position before sampling it.
74
- vec4 clip = uPrevViewProj * vec4(P, 1.0);
75
- if (clip.w <= 0.0) { outColor = vec4(current, 1.0); return; }
76
- vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5 - uPrevJitter;
77
- if (prevUv.x < 0.0 || prevUv.x > 1.0 || prevUv.y < 0.0 || prevUv.y > 1.0) {
78
- outColor = vec4(current, 1.0);
79
- return;
80
- }
81
-
82
- // NOTE: no geometric (depth/normal) history validation here, on purpose.
83
- // Under sub-pixel jitter such tests fail on every hard edge each frame,
84
- // dropping those pixels to the raw jittered current — the whole image
85
- // shimmers. The neighbourhood clamp below already bounds any stale history
86
- // (disocclusions resolve within a frame or two), which is exactly how
87
- // production TAA implementations handle rejection.
88
- vec3 history = texture(uHistory, prevUv).rgb;
89
- // Guard against a stray non-finite history value poisoning the buffer (it
90
- // would otherwise re-blend with itself every frame and stick as black).
91
- if (any(isnan(history)) || any(isinf(history))) {
92
- outColor = vec4(current, 1.0);
93
- return;
94
- }
95
- // Clamp history into the current neighbourhood box: removes ghosting on
96
- // motion and rejects bright edge speckles that history would otherwise keep.
97
- history = clamp(history, nmin, nmax);
98
-
99
- vec3 resolved = mix(history, current, uBlend);
100
- outColor = vec4(resolved, 1.0);
101
- }
102
- `;
103
-
104
- const copyFrag = /* glsl */ `
105
- precision highp float;
106
- layout(location = 0) out vec4 outColor;
107
- in vec2 vUv;
108
- uniform sampler2D uTex;
109
- void main() { outColor = vec4(texture(uTex, vUv).rgb, 1.0); }
110
- `;
111
-
112
- /**
113
- * Full-res temporal anti-aliasing resolve with a ping-pong history buffer.
114
- * `render()` resolves current+history into the write buffer, copies that to the
115
- * screen, and swaps. Pair with sub-pixel projection jitter for supersampled AA.
116
- */
117
- export class TAAPass {
118
- constructor(width, height) {
119
- this._width = width;
120
- this._height = height;
121
- this.targetA = this._makeTarget(width, height);
122
- this.targetB = this._makeTarget(width, height);
123
- this._reset = true;
124
-
125
- this.material = new THREE.ShaderMaterial({
126
- glslVersion: THREE.GLSL3,
127
- vertexShader: fullscreenVert,
128
- fragmentShader: taaFrag,
129
- uniforms: {
130
- uCurrent: { value: null },
131
- uHistory: { value: null },
132
- uGWorldPos: { value: null },
133
- uPrevViewProj: { value: new THREE.Matrix4() },
134
- uTexelSize: { value: new THREE.Vector2(1 / width, 1 / height) },
135
- uJitter: { value: new THREE.Vector2() },
136
- uPrevJitter: { value: new THREE.Vector2() },
137
- uBlend: { value: 0.1 },
138
- uReset: { value: true },
139
- },
140
- depthTest: false,
141
- depthWrite: false,
142
- });
143
-
144
- this.copyMaterial = new THREE.ShaderMaterial({
145
- glslVersion: THREE.GLSL3,
146
- vertexShader: fullscreenVert,
147
- fragmentShader: copyFrag,
148
- uniforms: { uTex: { value: null } },
149
- depthTest: false,
150
- depthWrite: false,
151
- });
152
-
153
- this.scene = new THREE.Scene();
154
- this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
155
- this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
156
- this.quad.frustumCulled = false;
157
- this.scene.add(this.quad);
158
- }
159
-
160
- _makeTarget(width, height) {
161
- const t = new THREE.WebGLRenderTarget(width, height, {
162
- minFilter: THREE.LinearFilter,
163
- magFilter: THREE.LinearFilter,
164
- format: THREE.RGBAFormat,
165
- type: THREE.HalfFloatType,
166
- depthBuffer: false,
167
- stencilBuffer: false,
168
- });
169
- t.texture.generateMipmaps = false;
170
- return t;
171
- }
172
-
173
- setSize(width, height) {
174
- this._width = width;
175
- this._height = height;
176
- this.targetA.setSize(width, height);
177
- this.targetB.setSize(width, height);
178
- this.material.uniforms.uTexelSize.value.set(1 / width, 1 / height);
179
- this._reset = true;
180
- }
181
-
182
- /** Force the next frame to show the current sample with no history (scene changed). */
183
- reset() {
184
- this._reset = true;
185
- }
186
-
187
- /**
188
- * @param currentColor tonemapped LDR colour texture for this frame
189
- * @param gbuffer current GBufferPass (provides the world-pos guide)
190
- * @param prevViewProj the *jittered* view-projection used last frame
191
- * @param jitterUv this frame's projection jitter in UV space
192
- * @param prevJitterUv last frame's projection jitter in UV space
193
- * @param outputTarget where the resolved frame goes (null = screen); lets a
194
- * post pass (god rays) run after the resolve
195
- */
196
- render(renderer, currentColor, gbuffer, prevViewProj, jitterUv, prevJitterUv, blend, outputTarget = null) {
197
- const u = this.material.uniforms;
198
- u.uCurrent.value = currentColor;
199
- u.uHistory.value = this.targetB.texture; // previous resolved
200
- u.uGWorldPos.value = gbuffer.worldPos;
201
- u.uPrevViewProj.value.copy(prevViewProj);
202
- u.uJitter.value.copy(jitterUv);
203
- u.uPrevJitter.value.copy(prevJitterUv);
204
- u.uBlend.value = blend;
205
- u.uReset.value = this._reset;
206
-
207
- // Resolve into targetA.
208
- this.quad.material = this.material;
209
- renderer.setRenderTarget(this.targetA);
210
- renderer.render(this.scene, this.camera);
211
-
212
- // Copy the resolved frame out (screen, or a post-pass input target).
213
- this.quad.material = this.copyMaterial;
214
- this.copyMaterial.uniforms.uTex.value = this.targetA.texture;
215
- renderer.setRenderTarget(outputTarget);
216
- renderer.render(this.scene, this.camera);
217
-
218
- // Swap: this frame's resolve becomes next frame's history.
219
- [this.targetA, this.targetB] = [this.targetB, this.targetA];
220
- this._reset = false;
221
- }
222
-
223
- dispose() {
224
- this.targetA.dispose();
225
- this.targetB.dispose();
226
- this.material.dispose();
227
- this.copyMaterial.dispose();
228
- this.quad.geometry.dispose();
229
- }
230
- }
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
+ // Temporal anti-aliasing resolve. Runs at full resolution on the final tonemapped
12
+ // color. Reprojects the previous resolved frame through world position (same trick
13
+ // the lighting/denoise passes use — no separate motion-vector buffer needed),
14
+ // clamps that history to the current 3x3 neighborhood colour box (kills ghosting
15
+ // AND the bright disocclusion speckles), then blends. Combined with sub-pixel
16
+ // camera jitter in RealtimeRaytracer, silhouettes get supersampled over time —
17
+ // this is the analytic (FSR2 / TAAU) approach, not a learned upscaler.
18
+ const taaFrag = /* glsl */ `
19
+ precision highp float;
20
+
21
+ layout(location = 0) out vec4 outColor;
22
+
23
+ in vec2 vUv;
24
+
25
+ uniform sampler2D uCurrent; // this frame's composited LDR colour
26
+ uniform sampler2D uHistory; // previous resolved colour
27
+ uniform sampler2D uGWorldPos; // current full-res G-buffer
28
+ uniform mat4 uPrevViewProj;
29
+ uniform vec2 uTexelSize;
30
+ uniform vec2 uJitter; // this frame's projection jitter (UV space)
31
+ uniform vec2 uPrevJitter; // last frame's projection jitter (UV space)
32
+ uniform float uBlend; // fresh-sample weight when history is valid (~0.1)
33
+ uniform bool uReset; // first frame after a scene/size change
34
+
35
+ // The raster (and the whole lighting chain guided by it) wobbles with the
36
+ // sub-pixel jitter. Resolving on that wobbling grid drags history along with
37
+ // it — the image shimmers. So the resolve UNJITTERS its input: content that
38
+ // unjittered would sit at u is rasterized at u + jitter, hence sample there.
39
+ // Output then lives on a stable grid and jitter only contributes sub-pixel
40
+ // coverage information over time (which is what gives the anti-aliasing).
41
+ vec3 sampleCurrent(vec2 uv) {
42
+ return texture(uCurrent, uv + uJitter).rgb;
43
+ }
44
+
45
+ void main() {
46
+ vec3 current = sampleCurrent(vUv);
47
+ // World position of the (unjittered) content at this pixel — same offset.
48
+ vec4 wp = texture(uGWorldPos, vUv + uJitter);
49
+
50
+ // Background (no geometry): no useful reprojection, show current directly.
51
+ if (wp.w < 0.5 || uReset) {
52
+ outColor = vec4(current, 1.0);
53
+ return;
54
+ }
55
+
56
+ vec3 P = wp.xyz;
57
+
58
+ // Neighbourhood colour AABB (used to clamp history — the core anti-ghost /
59
+ // anti-speckle step). Also the min corner tells us the local floor, so a
60
+ // single bright fireflight can't survive the clamp.
61
+ vec3 nmin = current, nmax = current;
62
+ for (int dy = -1; dy <= 1; dy++) {
63
+ for (int dx = -1; dx <= 1; dx++) {
64
+ if (dx == 0 && dy == 0) continue;
65
+ vec3 c = sampleCurrent(vUv + vec2(float(dx), float(dy)) * uTexelSize);
66
+ nmin = min(nmin, c);
67
+ nmax = max(nmax, c);
68
+ }
69
+ }
70
+
71
+ // Reproject this pixel's world point into the previous frame. The history is
72
+ // a STABILIZED (unjittered-grid) image, so remove last frame's jitter from
73
+ // the projected position before sampling it.
74
+ vec4 clip = uPrevViewProj * vec4(P, 1.0);
75
+ if (clip.w <= 0.0) { outColor = vec4(current, 1.0); return; }
76
+ vec2 prevUv = (clip.xy / clip.w) * 0.5 + 0.5 - uPrevJitter;
77
+ if (prevUv.x < 0.0 || prevUv.x > 1.0 || prevUv.y < 0.0 || prevUv.y > 1.0) {
78
+ outColor = vec4(current, 1.0);
79
+ return;
80
+ }
81
+
82
+ // NOTE: no geometric (depth/normal) history validation here, on purpose.
83
+ // Under sub-pixel jitter such tests fail on every hard edge each frame,
84
+ // dropping those pixels to the raw jittered current — the whole image
85
+ // shimmers. The neighbourhood clamp below already bounds any stale history
86
+ // (disocclusions resolve within a frame or two), which is exactly how
87
+ // production TAA implementations handle rejection.
88
+ vec3 history = texture(uHistory, prevUv).rgb;
89
+ // Guard against a stray non-finite history value poisoning the buffer (it
90
+ // would otherwise re-blend with itself every frame and stick as black).
91
+ if (any(isnan(history)) || any(isinf(history))) {
92
+ outColor = vec4(current, 1.0);
93
+ return;
94
+ }
95
+ // Clamp history into the current neighbourhood box: removes ghosting on
96
+ // motion and rejects bright edge speckles that history would otherwise keep.
97
+ history = clamp(history, nmin, nmax);
98
+
99
+ vec3 resolved = mix(history, current, uBlend);
100
+ outColor = vec4(resolved, 1.0);
101
+ }
102
+ `;
103
+
104
+ const copyFrag = /* glsl */ `
105
+ precision highp float;
106
+ layout(location = 0) out vec4 outColor;
107
+ in vec2 vUv;
108
+ uniform sampler2D uTex;
109
+ void main() { outColor = vec4(texture(uTex, vUv).rgb, 1.0); }
110
+ `;
111
+
112
+ /**
113
+ * Full-res temporal anti-aliasing resolve with a ping-pong history buffer.
114
+ * `render()` resolves current+history into the write buffer, copies that to the
115
+ * screen, and swaps. Pair with sub-pixel projection jitter for supersampled AA.
116
+ */
117
+ export class TAAPass {
118
+ constructor(width, height) {
119
+ this._width = width;
120
+ this._height = height;
121
+ this.targetA = this._makeTarget(width, height);
122
+ this.targetB = this._makeTarget(width, height);
123
+ this._reset = true;
124
+
125
+ this.material = new THREE.ShaderMaterial({
126
+ glslVersion: THREE.GLSL3,
127
+ vertexShader: fullscreenVert,
128
+ fragmentShader: taaFrag,
129
+ uniforms: {
130
+ uCurrent: { value: null },
131
+ uHistory: { value: null },
132
+ uGWorldPos: { value: null },
133
+ uPrevViewProj: { value: new THREE.Matrix4() },
134
+ uTexelSize: { value: new THREE.Vector2(1 / width, 1 / height) },
135
+ uJitter: { value: new THREE.Vector2() },
136
+ uPrevJitter: { value: new THREE.Vector2() },
137
+ uBlend: { value: 0.1 },
138
+ uReset: { value: true },
139
+ },
140
+ depthTest: false,
141
+ depthWrite: false,
142
+ });
143
+
144
+ this.copyMaterial = new THREE.ShaderMaterial({
145
+ glslVersion: THREE.GLSL3,
146
+ vertexShader: fullscreenVert,
147
+ fragmentShader: copyFrag,
148
+ uniforms: { uTex: { value: null } },
149
+ depthTest: false,
150
+ depthWrite: false,
151
+ });
152
+
153
+ this.scene = new THREE.Scene();
154
+ this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
155
+ this.quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), this.material);
156
+ this.quad.frustumCulled = false;
157
+ this.scene.add(this.quad);
158
+ }
159
+
160
+ _makeTarget(width, height) {
161
+ const t = new THREE.WebGLRenderTarget(width, height, {
162
+ minFilter: THREE.LinearFilter,
163
+ magFilter: THREE.LinearFilter,
164
+ format: THREE.RGBAFormat,
165
+ type: THREE.HalfFloatType,
166
+ depthBuffer: false,
167
+ stencilBuffer: false,
168
+ });
169
+ t.texture.generateMipmaps = false;
170
+ return t;
171
+ }
172
+
173
+ setSize(width, height) {
174
+ // No-op when the dimensions are unchanged (e.g. a renderScale-only step,
175
+ // which leaves the full-res TAA buffers alone): reallocating + resetting
176
+ // history here would drop a frame of resolved AA for nothing.
177
+ if (width === this._width && height === this._height) return;
178
+ this._width = width;
179
+ this._height = height;
180
+ this.targetA.setSize(width, height);
181
+ this.targetB.setSize(width, height);
182
+ this.material.uniforms.uTexelSize.value.set(1 / width, 1 / height);
183
+ this._reset = true;
184
+ }
185
+
186
+ /**
187
+ * Reallocate to a new size while carrying the resolved history forward (linear
188
+ * resample), so a canvas-ladder resize doesn't force a one-frame TAA reset and
189
+ * the shimmer that comes with it. The freshest resolved image is targetB (see
190
+ * the swap in render()); the neighbourhood clamp bounds any resample error
191
+ * within a frame, so no _reset is needed.
192
+ */
193
+ resizeCarry(renderer, copyPass, width, height) {
194
+ if (width === this._width && height === this._height) return;
195
+ this._width = width;
196
+ this._height = height;
197
+ const newA = this._makeTarget(width, height);
198
+ const newB = this._makeTarget(width, height);
199
+ copyPass.blit(renderer, this.targetB.texture, newB, -1);
200
+ this.targetA.dispose();
201
+ this.targetB.dispose();
202
+ this.targetA = newA;
203
+ this.targetB = newB;
204
+ this.material.uniforms.uTexelSize.value.set(1 / width, 1 / height);
205
+ }
206
+
207
+ /** Force the next frame to show the current sample with no history (scene changed). */
208
+ reset() {
209
+ this._reset = true;
210
+ }
211
+
212
+ /**
213
+ * @param currentColor tonemapped LDR colour texture for this frame
214
+ * @param gbuffer current GBufferPass (provides the world-pos guide)
215
+ * @param prevViewProj the *jittered* view-projection used last frame
216
+ * @param jitterUv this frame's projection jitter in UV space
217
+ * @param prevJitterUv last frame's projection jitter in UV space
218
+ * @param outputTarget where the resolved frame goes (null = screen); lets a
219
+ * post pass (god rays) run after the resolve
220
+ */
221
+ render(renderer, currentColor, gbuffer, prevViewProj, jitterUv, prevJitterUv, blend, outputTarget = null) {
222
+ const u = this.material.uniforms;
223
+ u.uCurrent.value = currentColor;
224
+ u.uHistory.value = this.targetB.texture; // previous resolved
225
+ u.uGWorldPos.value = gbuffer.worldPos;
226
+ u.uPrevViewProj.value.copy(prevViewProj);
227
+ u.uJitter.value.copy(jitterUv);
228
+ u.uPrevJitter.value.copy(prevJitterUv);
229
+ u.uBlend.value = blend;
230
+ u.uReset.value = this._reset;
231
+
232
+ // Resolve into targetA.
233
+ this.quad.material = this.material;
234
+ renderer.setRenderTarget(this.targetA);
235
+ renderer.render(this.scene, this.camera);
236
+
237
+ // Copy the resolved frame out (screen, or a post-pass input target).
238
+ this.quad.material = this.copyMaterial;
239
+ this.copyMaterial.uniforms.uTex.value = this.targetA.texture;
240
+ renderer.setRenderTarget(outputTarget);
241
+ renderer.render(this.scene, this.camera);
242
+
243
+ // Swap: this frame's resolve becomes next frame's history.
244
+ [this.targetA, this.targetB] = [this.targetB, this.targetA];
245
+ this._reset = false;
246
+ }
247
+
248
+ dispose() {
249
+ this.targetA.dispose();
250
+ this.targetB.dispose();
251
+ this.material.dispose();
252
+ this.copyMaterial.dispose();
253
+ this.quad.geometry.dispose();
254
+ }
255
+ }