신스웨이브 그리드

Synthwave grid

네온 격자 바닥과 줄무늬 태양이 지평선으로 사라지는, 80년대 레트로퓨처리즘 특유의 장면 구성.

다른 이름: 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);
});

신스웨이브 그리드는 딱히 전용 three.js API가 있는 게 아니라, 여러 기법을 조합한 스타일입니다. 격자 바닥은 프래그먼트 셰이더에서 UV 좌표를 반복시켜(fract) 일정 간격마다 얇은 선을 그리는 방식으로 만듭니다 — fwidth()로 화면 공간에서의 변화율을 구해 안티에일리어싱된 선을 그리면 멀리 갈수록 선이 자연스럽게 가늘어집니다. uOffset uniform을 매 프레임 증가시켜 UV를 미끄러뜨리면 격자가 끝없이 다가오는 것처럼 보입니다.

태양은 평범한 원반에 프래그먼트 셰이더로 가로 줄무늬를 넣되, 중심보다 아래쪽만 step() 함수로 줄무늬를 끊어 "가려진" 것처럼 보이게 합니다. 배경과 같은 색의 THREE.Fog를 더하면 격자 선이 지평선 근처에서 자연스럽게 옅어집니다.

이 스타일은 흔히 bloom(블룸)과 함께 써서 네온 선이 실제로 빛나 보이게 마무리합니다 — 이 데모는 셰이더 자체의 색과 발광감으로 가볍게 표현했지만, 프로덕션에서는 bloom 항목의 UnrealBloomPass를 얹으면 훨씬 화려해집니다.

언제 쓰나

레트로퓨처리즘·베이퍼웨이브 무드의 히어로 배경, 게임 타이틀 화면에.