Vertex displacement

버텍스 디스플레이스먼트

Pushing vertex positions around inside the vertex shader using a noise function — used for terrain, waves, organic deformation.

Also known as: 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);
});

A vertex shader normally just "transforms" a position, but if you rewrite position itself inside main() first, you can reshape geometry on the GPU in real time — far faster than recomputing a JS array of thousands of vertices every frame.

The demo builds a dense PlaneGeometry (90×90 segments), then pushes each vertex’s z upward over time using a compact hash-based value-noise function inside the vertex shader. It’s drawn with wireframe: true so the displacement reads directly as bends in the grid lines.

In production you might precompute noise with an addon like THREE.SimplexNoise or a baked 3D texture instead. Moving vertices invalidates the original normals, so a lit scene needs the normal recomputed too (usually approximated from neighboring-vertex derivatives) to look right.

When to use

Organically deforming surfaces — terrain, water, flags — and animated background textures.