Raymarching

레이마칭

Drawing shapes defined purely as math, by marching a ray forward from every pixel, with no triangle mesh at all.

Also known as: Signed distance fieldSDFSphere tracingMetaballs
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

const vertexShader = ['void main() { gl_Position = vec4(position, 1.0); }'].join('\n');

const fragmentShader = [
  'precision highp float;',
  'uniform vec2 uResolution;',
  'uniform float uTime;',
  'float sphere(vec3 p, vec3 c, float r) { return length(p - c) - r; }',
  'float smin(float a, float b, float k) {',
  '  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);',
  '  return mix(b, a, h) - k * h * (1.0 - h);',
  '}',
  'float map(vec3 p) {',
  '  float t = uTime;',
  '  vec3 c1 = vec3(sin(t * 0.8) * 1.1, cos(t * 0.6) * 0.6, 0.0);',
  '  vec3 c2 = vec3(cos(t * 0.7) * 1.0, sin(t * 0.9) * 0.7, 0.0);',
  '  vec3 c3 = vec3(sin(t * 0.5 + 2.0) * 0.9, cos(t * 0.4 + 1.0) * 0.5, 0.0);',
  '  float d1 = sphere(p, c1, 0.62);',
  '  float d2 = sphere(p, c2, 0.5);',
  '  float d3 = sphere(p, c3, 0.55);',
  '  return smin(smin(d1, d2, 0.4), d3, 0.4);',
  '}',
  'vec3 normalAt(vec3 p) {',
  '  vec2 e = vec2(0.001, 0.0);',
  '  return normalize(vec3(',
  '    map(p + e.xyy) - map(p - e.xyy),',
  '    map(p + e.yxy) - map(p - e.yxy),',
  '    map(p + e.yyx) - map(p - e.yyx)',
  '  ));',
  '}',
  'void main() {',
  '  vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / uResolution.y;',
  '  vec3 ro = vec3(0.0, 0.0, 3.2);',
  '  vec3 rd = normalize(vec3(uv, -1.4));',
  '  float total = 0.0;',
  '  vec3 col = vec3(0.03, 0.02, 0.07);',
  '  for (int i = 0; i < 48; i++) {',
  '    vec3 p = ro + rd * total;',
  '    float d = map(p);',
  '    if (d < 0.001) {',
  '      vec3 n = normalAt(p);',
  '      vec3 lightDir = normalize(vec3(0.6, 0.7, 0.5));',
  '      float diff = max(dot(n, lightDir), 0.0);',
  '      float fresnel = pow(1.0 - max(dot(n, -rd), 0.0), 2.5);',
  '      vec3 base = mix(vec3(0.25, 0.35, 0.95), vec3(0.9, 0.35, 0.65), uv.x + 0.5);',
  '      col = base * (0.25 + diff * 0.85) + fresnel * vec3(0.6, 0.8, 1.0);',
  '      break;',
  '    }',
  '    if (total > 7.0) break;',
  '    total += d;',
  '  }',
  '  gl_FragColor = vec4(col, 1.0);',
  '}',
].join('\n');

const mat = new THREE.ShaderMaterial({
  uniforms: {
    uResolution: { value: new THREE.Vector2(innerWidth, innerHeight) },
    uTime: { value: 0 },
  },
  vertexShader,
  fragmentShader,
});
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat);
quad.frustumCulled = false;
scene.add(quad);

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

Ordinary 3D rendering projects a mesh made of vertices onto the screen. Raymarching flips this around: for each pixel, define a ray through the camera, then repeatedly call a signed distance function (SDF) that returns "the distance to the nearest surface from this point," and step the ray forward by exactly that distance each time. Once the distance is close enough to zero, the ray has hit a surface — stop, and shade that point.

This demo’s SDF blends three spheres (sphere()) together with smin() (smooth minimum) — where an ordinary min() would stitch two shapes together with a hard seam, smin() melts the boundary smoothly, giving a metaball look, like liquid drops pulling into each other. The normal at a hit point is approximated from the SDF’s difference across six nearby directions (finite differences) and used for lighting.

The upside is drawing smooth blobs, fluids, or fractals that would be a pain to model as a mesh, all in one shader. The cost is calling the SDF dozens of times per pixel, which gets expensive fast. So this demo caps the step count and draws to a fullscreen quad (THREE.PlaneGeometry(2,2)) so the whole screen is one draw call.

When to use

Metaballs, liquid, clouds, fractals — organic shapes that are awkward to model as polygons, drawn entirely in one shader.