Skip to content

Commit 1d71e7a

Browse files
committed
Optimize rgb shader and replace background grid with starfield
1 parent 0526509 commit 1d71e7a

File tree

3 files changed

+183
-99
lines changed

3 files changed

+183
-99
lines changed

src/components/Guide.astro

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ const currentTime = new Date();
136136
flex-direction: column;
137137
align-items: center;
138138
justify-content: center;
139-
background: linear-gradient(to bottom, black, blue);
140139
min-height: 0;
141140
z-index: 1;
142141
}

src/components/RotatingLogo.astro

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
1515
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
1616
import { RGBTrailPass } from "../shaders/RGBTrailPass";
17-
import { GridHelper } from "three";
1817
import { DragControls } from "three/examples/jsm/Addons.js";
1918

2019
const svg = `<svg width="199" height="71" viewBox="0 0 199 71" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -28,7 +27,9 @@
2827
// Three.js setup
2928
// ----------------------
3029

31-
const scene = new THREE.Scene();
30+
const scene = new THREE.Scene(); // Exponential fog for depth effect
31+
scene.fog = new THREE.FogExp2(0x000000, 0.0015);
32+
3233
const camera = new THREE.PerspectiveCamera(
3334
100,
3435
canvas.clientWidth / canvas.clientHeight,
@@ -50,6 +51,60 @@
5051
renderer.toneMappingExposure = 1.2;
5152
canvas.appendChild(renderer.domElement);
5253

54+
// ----------------------
55+
// Starfield
56+
// ----------------------
57+
58+
// Create a group for all stars
59+
const starField = new THREE.Group();
60+
scene.add(starField);
61+
62+
// Star parameters
63+
const starCount = 500;
64+
const starSpeed = 12;
65+
const minStarSize = 0.5;
66+
const maxStarSize = 2;
67+
const starDepth = 2400; // How far the stars travel
68+
const centerAvoidanceRadius = 100; // Radius around center to avoid stars
69+
70+
// Create star material
71+
const starMaterial = new THREE.MeshPhysicalMaterial({
72+
color: "white",
73+
emissive: "white",
74+
emissiveIntensity: 10,
75+
transparent: true,
76+
});
77+
78+
// Create stars
79+
const stars: THREE.Mesh[] = [];
80+
for (let i = 0; i < starCount; i++) {
81+
// Random star size between min and max
82+
const randomSize =
83+
minStarSize + Math.random() * (maxStarSize - minStarSize);
84+
85+
// Create a small sphere for each star
86+
const geometry = new THREE.SphereGeometry(randomSize, 8, 8);
87+
const star = new THREE.Mesh(geometry, starMaterial);
88+
89+
// Random position within a wide area, avoiding center
90+
let x, y, z;
91+
let distanceFromCenter;
92+
93+
// Keep generating positions until we find one outside the center avoidance radius
94+
do {
95+
x = (Math.random() - 0.5) * 1000;
96+
y = (Math.random() - 0.5) * 1000;
97+
z = -Math.random() * starDepth; // Start at random distances
98+
99+
// Calculate distance from center in x-y plane
100+
distanceFromCenter = Math.sqrt(x * x + y * y);
101+
} while (distanceFromCenter < centerAvoidanceRadius);
102+
103+
star.position.set(x, y, z);
104+
starField.add(star);
105+
stars.push(star);
106+
}
107+
53108
const composer = new EffectComposer(renderer);
54109

55110
const resize = () => {
@@ -64,22 +119,6 @@
64119
window.addEventListener("resize", resize);
65120

66121
// ----------------------
67-
// Grid background
68-
// ----------------------
69-
70-
// Create a more customized grid
71-
const size = 3000;
72-
const divisions = 69; // Lol
73-
const mainColor = "white"; // Color of main grid lines
74-
const secondaryColor = "white"; // Color of secondary grid lines
75-
76-
const grid = new GridHelper(size, divisions, mainColor, secondaryColor);
77-
grid.position.set(0, -40, 0);
78-
grid.material.transparent = true;
79-
grid.material.opacity = 0.003; // Make grid semi-transparent
80-
81-
scene.add(grid);
82-
83122
// SVG Logo
84123
// ----------------------
85124

@@ -94,6 +133,8 @@
94133

95134
const material = new THREE.MeshPhysicalMaterial({
96135
color: "white",
136+
emissive: "white",
137+
emissiveIntensity: 2,
97138
reflectivity: 1,
98139
metalness: 1,
99140
roughness: 0.15,
@@ -199,6 +240,46 @@
199240
svgGroup.rotation.y = Math.PI / 7;
200241

201242
function animate() {
243+
// Move stars toward camera
244+
stars.forEach((star) => {
245+
// Calculate current distance from center in x-y plane
246+
const distanceFromCenter = Math.sqrt(
247+
star.position.x * star.position.x + star.position.y * star.position.y,
248+
);
249+
250+
// Apply slight outward drift for stars near the center
251+
if (distanceFromCenter < centerAvoidanceRadius * 2) {
252+
// Calculate direction vector from center to star
253+
const dirX = star.position.x / distanceFromCenter;
254+
const dirY = star.position.y / distanceFromCenter;
255+
256+
// Apply outward drift (stronger when closer to center)
257+
const driftStrength =
258+
(1 - distanceFromCenter / (centerAvoidanceRadius * 2)) * 0.5;
259+
star.position.x += dirX * driftStrength;
260+
star.position.y += dirY * driftStrength;
261+
}
262+
263+
star.position.z += starSpeed;
264+
265+
// Reset stars that have passed the camera
266+
if (star.position.z > 400) {
267+
// Reset position, avoiding center
268+
let newX, newY;
269+
let distanceFromCenter;
270+
271+
do {
272+
newX = (Math.random() - 0.5) * 1000;
273+
newY = (Math.random() - 0.5) * 1000;
274+
distanceFromCenter = Math.sqrt(newX * newX + newY * newY);
275+
} while (distanceFromCenter < centerAvoidanceRadius);
276+
277+
star.position.z = -starDepth;
278+
star.position.x = newX;
279+
star.position.y = newY;
280+
}
281+
});
282+
202283
// Normalize rotation to 0-2π range
203284
const normalizedRotation = svgGroup.rotation.y % (Math.PI * 2);
204285

@@ -227,6 +308,12 @@
227308
.rotating-logo {
228309
width: 100%;
229310
height: 100%;
311+
background: radial-gradient(
312+
ellipse 80% 300% at 50% 120%,
313+
blue,
314+
black,
315+
black
316+
);
230317
}
231318

232319
#canvas {

src/shaders/RGBTrailPass.ts

Lines changed: 78 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,89 @@
11
import {
22
GLSL3,
33
HalfFloatType,
4-
MeshBasicMaterial,
54
type Texture,
65
type TextureDataType,
76
WebGLRenderTarget,
87
type WebGLRenderer,
98
} from "three";
10-
import { FullScreenQuad } from "three/addons/postprocessing/Pass.js";
119
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
1210
import vertexShader from "./rgbtrail.vert?raw";
1311

1412
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;
2315

2416
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-
3817
const fragmentShader = `
3918
#include <common>
4019
41-
uniform sampler2D frames[${echoes.length}];
20+
uniform sampler2D tOld;
21+
uniform sampler2D tNew;
22+
uniform float damp;
23+
4224
in vec2 xy;
4325
out vec4 outputPixel;
4426
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);
4934
}
5035
5136
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+
}
6679
}
6780
`;
6881

6982
const shader = {
7083
uniforms: {
71-
frames: { value: null },
84+
tOld: { value: null },
85+
tNew: { value: null },
86+
damp: { value: 0.96 }, // Higher value = longer trail
7287
},
7388
vertexShader,
7489
fragmentShader,
@@ -84,39 +99,25 @@ export class RGBTrailPass extends ShaderPass {
8499
{ type: HalfFloatType },
85100
];
86101

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);
98105
}
99106

100107
render(
101108
renderer: WebGLRenderer,
102109
writeBuffer: WebGLRenderTarget | null,
103110
readBuffer: { texture: Texture | null },
104111
) {
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-
114112
// 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
120121
if (this.renderToScreen) {
121122
renderer.setRenderTarget(null);
122123
this.fsQuad.render(renderer);
@@ -125,24 +126,21 @@ export class RGBTrailPass extends ShaderPass {
125126
if (this.clear) renderer.clear();
126127
this.fsQuad.render(renderer);
127128
}
129+
130+
// Swap buffers for next frame
131+
const temp = this.oldTarget;
132+
this.oldTarget = this.newTarget;
133+
this.newTarget = temp;
128134
}
129135

130136
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);
135139
}
136140

137141
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();
146144
super.dispose();
147145
}
148146
}

0 commit comments

Comments
 (0)