델라우네이 삼각분할

Delaunay triangulation

점들을 "가장 뭉툭하지 않은" 삼각형들로 이어붙이는 방법. 보로노이 다이어그램과 수학적으로 쌍을 이룹니다.

다른 이름: Delaunay mesh
···
js
const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT = cs.getPropertyValue('--accent').trim() || '#5b5bf7';
const FG = cs.getPropertyValue('--fg').trim() || '#f1f0ec';
let w, h;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  c.width = w * dpr; c.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();

function circumcircle(a, b, cpt) {
  const d = 2 * (a[0] * (b[1] - cpt[1]) + b[0] * (cpt[1] - a[1]) + cpt[0] * (a[1] - b[1]));
  if (Math.abs(d) < 1e-6) return null;
  const A2 = a[0] * a[0] + a[1] * a[1], B2 = b[0] * b[0] + b[1] * b[1], C2 = cpt[0] * cpt[0] + cpt[1] * cpt[1];
  const ux = (A2 * (b[1] - cpt[1]) + B2 * (cpt[1] - a[1]) + C2 * (a[1] - b[1])) / d;
  const uy = (A2 * (cpt[0] - b[0]) + B2 * (a[0] - cpt[0]) + C2 * (b[0] - a[0])) / d;
  return { x: ux, y: uy, r: Math.hypot(ux - a[0], uy - a[1]) };
}
function triangulate(points) {
  const M = 6000;
  const pts = points.concat([[-M, -M], [M * 2, -M], [-M, M * 2]]);
  const s0 = points.length, s1 = points.length + 1, s2 = points.length + 2;
  let tris = [[s0, s1, s2]];
  for (let i = 0; i < points.length; i++) {
    const p = points[i], bad = [];
    for (const t of tris) { const cc = circumcircle(pts[t[0]], pts[t[1]], pts[t[2]]); if (cc && Math.hypot(p[0] - cc.x, p[1] - cc.y) < cc.r) bad.push(t); }
    const edgeCount = new Map();
    for (const t of bad) for (const e of [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]]) {
      const key = [...e].sort((a, b) => a - b).join(',');
      edgeCount.set(key, (edgeCount.get(key) || 0) + 1);
    }
    tris = tris.filter((t) => !bad.includes(t));
    for (const t of bad) for (const e of [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]]) {
      const key = [...e].sort((a, b) => a - b).join(',');
      if (edgeCount.get(key) === 1) tris.push([e[0], e[1], i]);
    }
  }
  return { pts, tris: tris.filter((t) => t.every((idx) => idx < points.length)) };
}

const N = 26;
const nodes = Array.from({ length: N }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10 }));
function draw() {
  ctx.clearRect(0, 0, w, h);
  const points = nodes.map((n) => [n.x, n.y]);
  const { pts, tris } = triangulate(points);
  ctx.strokeStyle = ACCENT; ctx.globalAlpha = 0.5; ctx.lineWidth = 1;
  for (const t of tris) {
    ctx.beginPath();
    ctx.moveTo(pts[t[0]][0], pts[t[0]][1]); ctx.lineTo(pts[t[1]][0], pts[t[1]][1]); ctx.lineTo(pts[t[2]][0], pts[t[2]][1]);
    ctx.closePath(); ctx.stroke();
  }
  ctx.globalAlpha = 1; ctx.fillStyle = FG;
  for (const n of nodes) { ctx.beginPath(); ctx.arc(n.x, n.y, 2.6, 0, 7); ctx.fill(); }
}
function step(dt) {
  for (const n of nodes) {
    n.x += n.vx * dt; n.y += n.vy * dt;
    if (n.x < 10 || n.x > w - 10) n.vx *= -1;
    if (n.y < 10 || n.y > h - 10) n.vy *= -1;
  }
}
draw();
(function loop() { step(0.03); draw(); requestAnimationFrame(loop); })();

같은 점들을 삼각형으로 이을 방법은 무수히 많지만, 델라우네이 삼각분할은 그중 "어떤 삼각형의 외접원 안에도 다른 점이 들어가지 않는" 것을 고릅니다. 이 조건 하나만으로 결과가 유일하게 정해지고, 길고 뾰족한 삼각형이 최대한 적게 나옵니다. 러시아 수학자 보리스 델로네가 1934년 정식화했습니다.

이 데모는 보이어-왓슨(Bowyer–Watson) 알고리즘을 씁니다. 점을 하나씩 추가하면서, 새 점이 외접원 안에 들어오는 기존 삼각형들을 모두 지우고 생긴 구멍을 새 점과 이어 다시 채우는 방식입니다. 점을 계속 움직이면서 매 프레임 다시 삼각분할하면, 메시가 실시간으로 재배열되는 모습을 볼 수 있습니다.

지형의 폴리곤 메시, 로우폴리 아트, 자연스러운 그물망 배경을 만들 때 씁니다. 같은 점 집합의 보로노이 다이어그램은 이 삼각분할의 각 삼각형 외심을 이어서 바로 얻을 수 있어서, 둘은 늘 쌍으로 다룹니다.

언제 쓰나

로우폴리 배경, 지형 메시, 산점도 위에 관계망을 그릴 때. 점이 수천 개를 넘으면 더 빠른 라이브러리(d3-delaunay 등)로 옮기세요.