1
1
import {
2
2
GLSL3 ,
3
3
HalfFloatType ,
4
- MeshBasicMaterial ,
5
4
type Texture ,
6
5
type TextureDataType ,
7
6
WebGLRenderTarget ,
8
7
type WebGLRenderer ,
9
8
} from "three" ;
10
- import { FullScreenQuad } from "three/addons/postprocessing/Pass.js" ;
11
9
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js" ;
12
10
import vertexShader from "./rgbtrail.vert?raw" ;
13
11
14
12
export class RGBTrailPass extends ShaderPass {
15
- frames : {
16
- target : WebGLRenderTarget ;
17
- quad : FullScreenQuad ;
18
- } [ ] ;
19
- comp : {
20
- target : WebGLRenderTarget ;
21
- quad : FullScreenQuad ;
22
- } ;
13
+ private oldTarget : WebGLRenderTarget ;
14
+ private newTarget : WebGLRenderTarget ;
23
15
24
16
constructor ( ) {
25
- const echoes = [
26
- [ "r" , "g" , "b" , "a" ] ,
27
- // Red
28
- [ "2.0" , "0.0" , "0.0" , "0.8 * a" ] ,
29
- // Green
30
- [ "0.0" , "2.0" , "0.0" , "0.6 * a" ] ,
31
- // Blue
32
- [ "0.0" , "0.0" , "2.0" , "0.4 * a" ] ,
33
- [ "0.0" , "0.0" , "2.0" , "0.3 * a" ] ,
34
- [ "0.0" , "0.0" , "2.0" , "0.2 * a" ] ,
35
- [ "0.0" , "0.0" , "2.0" , "0.1 * a" ] ,
36
- ] . map ( ( [ r , g , b , a ] , i ) => ( { r, g, b, a, i } ) ) ;
37
-
38
17
const fragmentShader = `
39
18
#include <common>
40
19
41
- uniform sampler2D frames[${ echoes . length } ];
20
+ uniform sampler2D tOld;
21
+ uniform sampler2D tNew;
22
+ uniform float damp;
23
+
42
24
in vec2 xy;
43
25
out vec4 outputPixel;
44
26
45
- vec4 blend(vec4 bg, vec4 fg) {
46
- vec4 result = mix(bg, fg, fg.a);
47
- result = max(bg, result);
48
- return result;
27
+ // Function to create a tighter falloff, similar to the Shadertoy example
28
+ float tighten(float x) {
29
+ return pow(1.0 - clamp(x, 0.0, 1.0), 8.0);
30
+ }
31
+
32
+ vec4 when_gt(vec4 x, float y) {
33
+ return max(sign(x - y), 0.0);
49
34
}
50
35
51
36
void main() {
52
- outputPixel = vec4(0.0);
53
- ${ echoes
54
- . map (
55
- ( { r, g, b, a, i } ) => `
56
- {
57
- vec4 pixel = texture(frames[${ i } ], xy);
58
- pixel.r = ${ r . replace ( "r" , "pixel.r" ) } ;
59
- pixel.g = ${ g . replace ( "g" , "pixel.g" ) } ;
60
- pixel.b = ${ b . replace ( "b" , "pixel.b" ) } ;
61
- pixel.a = ${ a . replace ( "a" , "pixel.a" ) } ;
62
- outputPixel = blend(outputPixel, pixel);
63
- }` ,
64
- )
65
- . join ( "\n" ) }
37
+ vec4 texelOld = texture(tOld, xy);
38
+ vec4 texelNew = texture(tNew, xy);
39
+
40
+ // Check if there's new content
41
+ bool hasNewContent = texelNew.r > 0.01 || texelNew.g > 0.01 || texelNew.b > 0.01;
42
+
43
+ if (hasNewContent) {
44
+ // For new content, preserve the original color and set alpha to 1.0
45
+ outputPixel = vec4(texelNew.rgb, 1.0);
46
+ } else {
47
+ // For existing trail, apply damping with a tighter falloff
48
+ vec4 damped = texelOld * damp;
49
+
50
+ // Apply visibility threshold with tighter falloff
51
+ float visibility = max(damped.r, max(damped.g, damped.b));
52
+ float tightVisibility = tighten(1.0 - visibility);
53
+ damped *= vec4(tightVisibility);
54
+
55
+ // Check if the pixel is still visible
56
+ if (damped.a > 0.01) {
57
+ // Use the alpha value to determine the color
58
+ // As alpha decreases, we transition from colors to transparent
59
+ float alpha = damped.a;
60
+
61
+ if (alpha > 0.65) {
62
+ // Red phase
63
+ outputPixel = vec4(1.0, 0.0, 0.0, alpha);
64
+ } else if (alpha > 0.5) {
65
+ // Green phase
66
+ outputPixel = vec4(0.0, 1.0, 0.0, alpha);
67
+ } else if (alpha > 0.1) {
68
+ // Blue phase
69
+ outputPixel = vec4(0.0, 0.0, 1.0, alpha);
70
+ } else {
71
+ // Fading out
72
+ outputPixel = vec4(0.0, 0.0, 0.0, alpha);
73
+ }
74
+ } else {
75
+ // Fully faded out
76
+ outputPixel = vec4(0.0, 0.0, 0.0, 0.0);
77
+ }
78
+ }
66
79
}
67
80
` ;
68
81
69
82
const shader = {
70
83
uniforms : {
71
- frames : { value : null } ,
84
+ tOld : { value : null } ,
85
+ tNew : { value : null } ,
86
+ damp : { value : 0.96 } , // Higher value = longer trail
72
87
} ,
73
88
vertexShader,
74
89
fragmentShader,
@@ -84,39 +99,25 @@ export class RGBTrailPass extends ShaderPass {
84
99
{ type : HalfFloatType } ,
85
100
] ;
86
101
87
- this . comp = {
88
- target : new WebGLRenderTarget ( ...params ) ,
89
- quad : new FullScreenQuad ( this . material ) ,
90
- } ;
91
-
92
- this . frames = Array ( echoes . length )
93
- . fill ( null )
94
- . map ( ( ) => ( {
95
- target : new WebGLRenderTarget ( ...params ) ,
96
- quad : new FullScreenQuad ( new MeshBasicMaterial ( { transparent : true } ) ) ,
97
- } ) ) ;
102
+ // Create two render targets for ping-pong buffering
103
+ this . oldTarget = new WebGLRenderTarget ( ...params ) ;
104
+ this . newTarget = new WebGLRenderTarget ( ...params ) ;
98
105
}
99
106
100
107
render (
101
108
renderer : WebGLRenderer ,
102
109
writeBuffer : WebGLRenderTarget | null ,
103
110
readBuffer : { texture : Texture | null } ,
104
111
) {
105
- // Update frame buffers
106
- // biome-ignore lint/style/noNonNullAssertion: Array is always available
107
- this . frames . unshift ( this . frames . pop ( ) ! ) ;
108
-
109
- renderer . setRenderTarget ( this . frames [ 0 ] . target ) ;
110
- ( this . frames [ 0 ] . quad . material as MeshBasicMaterial ) . map =
111
- readBuffer . texture ;
112
- this . frames [ 0 ] . quad . render ( renderer ) ;
113
-
114
112
// Update uniforms
115
- this . material . uniforms . frames . value = this . frames . map (
116
- ( frame ) => frame . target . texture ,
117
- ) ;
118
-
119
- // Render final composition
113
+ this . material . uniforms . tNew . value = readBuffer . texture ;
114
+ this . material . uniforms . tOld . value = this . oldTarget . texture ;
115
+
116
+ // Render to new target
117
+ renderer . setRenderTarget ( this . newTarget ) ;
118
+ this . fsQuad . render ( renderer ) ;
119
+
120
+ // Render to output buffer
120
121
if ( this . renderToScreen ) {
121
122
renderer . setRenderTarget ( null ) ;
122
123
this . fsQuad . render ( renderer ) ;
@@ -125,24 +126,21 @@ export class RGBTrailPass extends ShaderPass {
125
126
if ( this . clear ) renderer . clear ( ) ;
126
127
this . fsQuad . render ( renderer ) ;
127
128
}
129
+
130
+ // Swap buffers for next frame
131
+ const temp = this . oldTarget ;
132
+ this . oldTarget = this . newTarget ;
133
+ this . newTarget = temp ;
128
134
}
129
135
130
136
setSize ( width : number , height : number ) {
131
- this . comp . target . setSize ( width , height ) ;
132
- for ( const frame of this . frames ) {
133
- frame . target . setSize ( width , height ) ;
134
- }
137
+ this . oldTarget . setSize ( width , height ) ;
138
+ this . newTarget . setSize ( width , height ) ;
135
139
}
136
140
137
141
dispose ( ) {
138
- this . comp . target . dispose ( ) ;
139
- this . comp . quad . dispose ( ) ;
140
-
141
- for ( const frame of this . frames ) {
142
- frame . target . dispose ( ) ;
143
- frame . quad . dispose ( ) ;
144
- }
145
-
142
+ this . oldTarget . dispose ( ) ;
143
+ this . newTarget . dispose ( ) ;
146
144
super . dispose ( ) ;
147
145
}
148
146
}
0 commit comments