UV 언랩핑

UV unwrapping

3D 표면을 이음매(심)를 따라 가위질해서 평평한 2D 평면 위에 펼치는 과정. 텍스처가 표면 어디에 붙을지를 정합니다.

다른 이름: UV mappingSeamsUV layout
···
html
<div class="uv-scene">
  <div class="uv-cube" id="uv-cube">
    <div class="uv-face f0"></div><div class="uv-face f1"></div><div class="uv-face f2"></div>
    <div class="uv-face f3"></div><div class="uv-face f4"></div><div class="uv-face f5"></div>
  </div>
  <span class="uv-cap">seams → flat UV layout</span>
</div>
css
.uv-scene{position:relative;width:100%;height:100%;display:grid;place-items:center;perspective:900px}
.uv-cube{position:relative;width:1px;height:1px;transform-style:preserve-3d}
.uv-face{position:absolute;top:0;left:0;display:block;border:1px solid #ffffff66;background-image:linear-gradient(#ffffff2a 1px,transparent 1px),linear-gradient(90deg,#ffffff2a 1px,transparent 1px);background-size:25% 25%;border-radius:2px}
.f0{background-color:#ff6b6b}.f1{background-color:#ffa94d}.f2{background-color:#51cf66}.f3{background-color:#339af0}.f4{background-color:#cc5de8}.f5{background-color:#20c997}
.uv-cap{position:absolute;left:0;right:0;bottom:5%;text-align:center;font-size:clamp(9px,2.6vmin,13px);color:var(--muted)}
js
const stage = document.querySelector('.uv-scene');
const cube = document.getElementById('uv-cube');
const faces = [...cube.children];
const FOLD = [
  { rx: 0, ry: 0 }, { rx: 0, ry: 180 }, { rx: 0, ry: 90 },
  { rx: 0, ry: -90 }, { rx: 90, ry: 0 }, { rx: -90, ry: 0 },
];
const NET = [
  { x: 0, y: 0 }, { x: 4, y: 0 }, { x: 2, y: 0 },
  { x: -2, y: 0 }, { x: 0, y: -2 }, { x: 0, y: 2 },
];
let S = 30;
function layout() {
  const box = stage.getBoundingClientRect();
  S = Math.max(16, Math.min(box.width, box.height) * 0.095);
  faces.forEach((f) => { f.style.width = S * 2 + 'px'; f.style.height = S * 2 + 'px'; });
}
addEventListener('resize', layout); layout();
function lerp(a, b, p) { return a + (b - a) * p; }
function ease(p) { return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2; }
function frame(t) {
  const cycle = 9000;
  const local = (t % cycle) / cycle;
  let p;
  if (local < 0.22) p = 0;
  else if (local < 0.42) p = ease((local - 0.22) / 0.2);
  else if (local < 0.62) p = 1;
  else if (local < 0.82) p = 1 - ease((local - 0.62) / 0.2);
  else p = 0;
  faces.forEach((f, i) => {
    const fold = FOLD[i], net = NET[i];
    const rx = lerp(fold.rx, 0, p);
    const ry = lerp(fold.ry, 0, p);
    const tz = lerp(S, 0, p);
    const tx = lerp(0, net.x * S, p);
    const ty = lerp(0, net.y * S, p);
    f.style.transform = `translate(-50%,-50%) translate3d(${tx}px, ${ty}px, 0) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${tz}px)`;
  });
  cube.style.transform = `rotateX(${-18 * (1 - p)}deg) rotateY(${((t * 0.012) % 360) * (1 - p)}deg)`;
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

텍스처 이미지는 원래 평평한 사각형(0~1 범위의 U, V 좌표)인데, 그걸 울퉁불퉁한 3D 표면에 왜곡 없이 입히려면 먼저 그 표면을 "어떻게 자르면 평평하게 펴지는가"를 정해야 합니다. 그 자르는 선을 심(seam)이라 부르고, 심을 따라 펼친 결과가 UV 레이아웃입니다.

데모는 정육면체 하나로 그 원리를 보여줍니다 — 여섯 면을 심을 따라 펼치면, 상자였던 것이 십자 모양의 평평한 배치(넷, net)가 됩니다. 각 면 위의 격자는 텍스처의 픽셀이 표면 위 어디에 대응하는지를 나타냅니다.

구체나 유기적인 형태는 정육면체보다 훨씬 더 조심스럽게 심을 배치해야 늘어남(스트레칭)과 겹침을 피할 수 있습니다. Blender의 UV Editor, Maya의 UV Editor, Cinema 4D의 BodyPaint는 모두 심을 표시하고 자동/수동으로 펼친 뒤, 체커보드 텍스처를 입혀 칸이 찌그러진 곳 — 즉 늘어난 부분 — 을 눈으로 확인하게 해줍니다.

언제 쓰나

텍스처가 표면 위에서 늘어나 보이거나 이음매가 눈에 띄게 갈라져 보일 때 UV를 다시 확인하세요.