Retopology

리토폴로지

Rebuilding a clean, quad-based mesh on top of a messy high-density scan or sculpt, following the same shape.

Also known as: RetopoManual retopologyAuto-retopo
···
html
<canvas id="rp-canvas"></canvas><div class="rp-caps"><span id="rp-messy">scan · dense &amp; messy</span><span id="rp-clean">retopo · clean quads</span></div>
css
#rp-canvas{width:100%;height:100%;display:block}
.rp-caps{position:absolute;left:0;right:0;bottom:4%;display:grid;place-items:center;font-size:clamp(9px,2.4vmin,12px);color:var(--muted)}
.rp-caps span{grid-area:1/1;transition:opacity .3s linear}
js
const canvas = document.getElementById('rp-canvas');
const ctx = canvas.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const colFg = cs.getPropertyValue('--fg').trim() || '#222';
const colMuted = cs.getPropertyValue('--muted').trim() || '#888';

function resize() {
  const r = canvas.getBoundingClientRect();
  canvas.width = Math.round(r.width * devicePixelRatio);
  canvas.height = Math.round(r.height * devicePixelRatio);
}
addEventListener('resize', resize); resize();

const N_BLOB = 18;
const blobR = [];
for (let i = 0; i < N_BLOB; i++) blobR.push(0.8 + Math.sin(i * 1.7) * 0.08 + Math.cos(i * 0.6) * 0.06);
function blobPoint(a) {
  const idx = (a / (Math.PI * 2)) * N_BLOB;
  const i0 = Math.floor(idx) % N_BLOB, i1 = (i0 + 1) % N_BLOB;
  const f = idx - Math.floor(idx);
  const r = blobR[i0] * (1 - f) + blobR[i1] * f;
  return [Math.cos(a) * r, Math.sin(a) * r];
}
const messyPts = [];
for (let i = 0; i < 55; i++) {
  const a = Math.random() * Math.PI * 2;
  const rr = 0.12 + Math.random() * 0.82;
  const [bx, by] = blobPoint(a);
  messyPts.push([bx * rr, by * rr]);
}
const messyLines = [];
for (let i = 0; i < messyPts.length; i++) {
  const dists = messyPts
    .map((p, j) => [j, Math.hypot(p[0] - messyPts[i][0], p[1] - messyPts[i][1])])
    .filter(([j]) => j !== i)
    .sort((a, b) => a[1] - b[1]);
  for (let k = 0; k < 3; k++) messyLines.push([i, dists[k][0]]);
}

function ease(x) { return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; }

function draw(t) {
  const w = canvas.width, h = canvas.height;
  ctx.clearRect(0, 0, w, h);
  const cx = w / 2, cy = h / 2;
  const scale = Math.min(w, h) * 0.42;

  const cycle = 6000;
  const local = (t % cycle) / cycle;
  let clean;
  if (local < 0.4) clean = 0;
  else if (local < 0.5) clean = ease((local - 0.4) / 0.1);
  else if (local < 0.9) clean = 1;
  else clean = 1 - ease((local - 0.9) / 0.1);

  ctx.globalAlpha = 0.35;
  ctx.strokeStyle = colFg;
  ctx.lineWidth = Math.max(1, scale * 0.006);
  ctx.beginPath();
  for (let i = 0; i <= 64; i++) {
    const a = (i / 64) * Math.PI * 2;
    const [x, y] = blobPoint(a);
    const px = cx + x * scale, py = cy + y * scale;
    if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
  }
  ctx.closePath(); ctx.stroke();

  ctx.globalAlpha = (1 - clean) * 0.85;
  ctx.strokeStyle = colMuted;
  ctx.lineWidth = Math.max(1, scale * 0.004);
  ctx.beginPath();
  for (const [i, j] of messyLines) {
    ctx.moveTo(cx + messyPts[i][0] * scale, cy + messyPts[i][1] * scale);
    ctx.lineTo(cx + messyPts[j][0] * scale, cy + messyPts[j][1] * scale);
  }
  ctx.stroke();

  ctx.globalAlpha = clean;
  ctx.strokeStyle = colFg;
  ctx.lineWidth = Math.max(1, scale * 0.006);
  const RINGS = 4, SPOKES = 14;
  for (let ring = 1; ring <= RINGS; ring++) {
    const frac = ring / RINGS;
    ctx.beginPath();
    for (let i = 0; i <= 64; i++) {
      const a = (i / 64) * Math.PI * 2;
      const [x, y] = blobPoint(a);
      const px = cx + x * frac * scale, py = cy + y * frac * scale;
      if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
    }
    ctx.stroke();
  }
  for (let s = 0; s < SPOKES; s++) {
    const a = (s / SPOKES) * Math.PI * 2;
    const [x, y] = blobPoint(a);
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.lineTo(cx + x * scale, cy + y * scale);
    ctx.stroke();
  }
  ctx.globalAlpha = 1;

  document.getElementById('rp-messy').style.opacity = String(1 - clean);
  document.getElementById('rp-clean').style.opacity = String(clean);
  requestAnimationFrame(draw);
}
requestAnimationFrame(draw);

Output from a 3D scan or a sculpting tool like ZBrush often has millions of vertices scattered with no flow at all — the shape is accurate, but the topology is often the worst kind. It can’t go straight into animation or a game, because parts that need to bend will deform unpredictably.

Retopology uses that high-density surface as a guide and draws a new, far sparser quad mesh on top that follows the form’s flow. The result’s silhouette and volume match the original almost exactly, but its internal structure — the topology — gets completely redesigned. This low-density mesh can then serve directly as a subdivision surface’s cage.

Blender’s Retopoflow, Maya’s Quad Draw, and ZBrush’s ZRemesher all help with this — some, like ZRemesher, automatically guess a plausible flow, while others require drawing each face by hand. For parts that need refined animation, like a face, manual retopology is still often preferred over automatic.

When to use

An almost mandatory step before a scan or sculpt goes into an animation or game pipeline.