Synthwave grid

신스웨이브 그리드

The signature 80s retrofuturist scene: a neon grid floor and a striped sun receding into the horizon.

Also known as: Retro gridOutrun grid80s wireframe horizon
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0620);
scene.fog = new THREE.Fog(0x0a0620, 3, 11);
const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 60);
camera.position.set(0, 0.9, 3.6);
camera.rotation.x = -0.18;

const sunVertex = [
  'varying vec2 vUv;',
  'void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',
].join('\n');
const sunFragment = [
  'varying vec2 vUv;',
  'uniform float uTime;',
  'void main() {',
  '  vec2 p = vUv - 0.5;',
  '  float d = length(p) * 2.0;',
  '  if (d > 1.0) discard;',
  '  float stripe = step(0.5, fract((vUv.y - uTime * 0.05) * 10.0));',
  '  vec3 top = vec3(1.0, 0.78, 0.35);',
  '  vec3 bottom = vec3(1.0, 0.25, 0.55);',
  '  vec3 color = mix(bottom, top, vUv.y);',
  '  float mask = vUv.y > 0.46 ? 1.0 : stripe;',
  '  gl_FragColor = vec4(color, mask);',
  '}',
].join('\n');
const sunMat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: sunVertex,
  fragmentShader: sunFragment,
  transparent: true,
  side: THREE.DoubleSide,
});
const sun = new THREE.Mesh(new THREE.PlaneGeometry(3.2, 3.2), sunMat);
sun.position.set(0, 0.9, -8);
scene.add(sun);

const gridVertex = [
  'varying vec2 vUv;',
  'void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }',
].join('\n');
const gridFragment = [
  'varying vec2 vUv;',
  'uniform float uOffset;',
  'void main() {',
  '  vec2 p = vUv * vec2(24.0, 40.0);',
  '  p.y += uOffset;',
  '  vec2 g = abs(fract(p - 0.5) - 0.5) / fwidth(p);',
  '  float line = min(g.x, g.y);',
  '  float fade = smoothstep(1.0, 0.05, vUv.y);',
  '  vec3 color = mix(vec3(1.0, 0.25, 0.85), vec3(0.25, 0.85, 1.0), vUv.x);',
  '  float alpha = (1.0 - min(line, 1.0)) * fade;',
  '  gl_FragColor = vec4(color, alpha);',
  '}',
].join('\n');
const gridMat = new THREE.ShaderMaterial({
  uniforms: { uOffset: { value: 0 } },
  vertexShader: gridVertex,
  fragmentShader: gridFragment,
  transparent: true,
  side: THREE.DoubleSide,
});
const grid = new THREE.Mesh(new THREE.PlaneGeometry(14, 20, 1, 1), gridMat);
grid.rotation.x = -Math.PI / 2;
grid.position.y = -0.5;
scene.add(grid);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  gridMat.uniforms.uOffset.value = time * 1.4;
  sunMat.uniforms.uTime.value = time;
  renderer.render(scene, camera);
});

Synthwave grid isn’t one dedicated three.js API — it’s a style built from a few combined techniques. The grid floor is drawn in a fragment shader by repeating UV coordinates (fract) to draw thin lines at regular intervals; using fwidth() to measure the screen-space rate of change antialiases those lines so they naturally thin out toward the distance. Incrementing a uOffset uniform every frame slides the UVs, so the grid appears to endlessly approach.

The sun is just a plain disc with horizontal stripes cut into it by a fragment shader, using step() to break the stripes only below its center so it reads as partly "occluded." Adding a THREE.Fog matching the background color fades the grid lines out naturally near the horizon.

This style is commonly finished off with bloom so the neon lines actually glow — this demo keeps things light using just the shader’s own color and emissive feel, but layering the bloom entry’s UnrealBloomPass on top makes it far more dramatic in production.

When to use

Retrofuturist/vaporwave mood hero backgrounds, game title screens.