버텍스 디스플레이스먼트

Vertex displacement

정점 셰이더 안에서 노이즈 값으로 정점의 위치 자체를 밀어 움직이는 기법. 지형·물결·유기적 변형에 씁니다.

다른 이름: Vertex shader displacementNoise displacementHeight field
···
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(0x070712);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(0, 2.6, 4.4);
camera.lookAt(0, 0, 0);

const vertexShader = [
  'uniform float uTime;',
  'varying float vHeight;',
  'float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }',
  'float noise(vec2 p) {',
  '  vec2 i = floor(p);',
  '  vec2 f = fract(p);',
  '  float a = hash(i);',
  '  float b = hash(i + vec2(1.0, 0.0));',
  '  float c = hash(i + vec2(0.0, 1.0));',
  '  float d = hash(i + vec2(1.0, 1.0));',
  '  vec2 u = f * f * (3.0 - 2.0 * f);',
  '  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);',
  '}',
  'void main() {',
  '  vec3 p = position;',
  '  float n = noise(p.xy * 0.6 + uTime * 0.25) + 0.5 * noise(p.xy * 1.3 - uTime * 0.4);',
  '  p.z += n * 0.7;',
  '  vHeight = n;',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);',
  '}',
].join('\n');

const fragmentShader = [
  'varying float vHeight;',
  'void main() {',
  '  vec3 low = vec3(0.22, 0.24, 0.85);',
  '  vec3 high = vec3(0.55, 0.92, 1.0);',
  '  gl_FragColor = vec4(mix(low, high, clamp(vHeight, 0.0, 1.0)), 1.0);',
  '}',
].join('\n');

const geo = new THREE.PlaneGeometry(5.5, 5.5, 90, 90);
const mat = new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader,
  fragmentShader,
  wireframe: true,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.frustumCulled = false;
mesh.rotation.x = -Math.PI / 2.6;
scene.add(mesh);

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

보통 정점 셰이더는 위치를 화면 좌표로 "변환"만 하지만, main() 안에서 position 값 자체를 먼저 바꿔버리면 지오메트리의 실제 형태를 GPU에서 실시간으로 재구성할 수 있습니다. CPU가 매 프레임 수천 개 정점 배열을 다시 계산하는 것보다 훨씬 빠릅니다.

데모는 격자가 촘촘한 PlaneGeometry(세그먼트 90×90)를 만들고, 정점 셰이더 안에서 해시 기반 value noise 함수로 각 정점의 z값을 시간에 따라 밀어 올립니다. 결과를 wireframe: true로 그려서 변형이 격자선의 굴곡으로 그대로 보이게 했습니다.

실제 제품에서는 THREE.SimplexNoise 같은 애드온이나 3D 텍스처로 노이즈를 미리 구워서 쓰기도 합니다. 정점을 옮기면 원래의 법선(normal)이 더 이상 맞지 않으므로, 조명이 필요한 장면에서는 법선도 다시 계산(인접 정점의 미분으로 근사)해야 자연스럽습니다.

언제 쓰나

지형·물·깃발처럼 유기적으로 변형되는 표면, 애니메이션되는 배경 텍스처에.