텍스처 베이킹

Texture baking

복잡한 고해상도 모델의 디테일(굴곡, 가려짐, 색상)을 계산해 평평한 텍스처 이미지로 "구워내고", 훨씬 단순한 모델 위에 붙이는 작업.

다른 이름: BakingBake to texture
···
html
<div class="sub-caps"><span>raw low-poly</span><span>baked textures</span></div>
css
.sub-caps{position:absolute;left:0;right:0;bottom:6%;display:flex;justify-content:space-around;font-size:clamp(8px,2.4vmin,12px);color:#dfe3ffcc;text-align:center;padding:0 3%}
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(0x0d0d16);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.5, 6.2);

const S = 96;
const colorCv = document.createElement('canvas'); colorCv.width = colorCv.height = S;
const cc = colorCv.getContext('2d');
cc.fillStyle = '#caa46a'; cc.fillRect(0, 0, S, S);
const bumps = [];
for (let i = 0; i < 36; i++) bumps.push({ x: Math.random() * S, y: Math.random() * S, r: 3 + Math.random() * 5 });
for (const b of bumps) {
  const g = cc.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r);
  g.addColorStop(0, 'rgba(60,40,20,0.5)'); g.addColorStop(1, 'rgba(60,40,20,0)');
  cc.fillStyle = g; cc.beginPath(); cc.arc(b.x, b.y, b.r, 0, Math.PI * 2); cc.fill();
}
const colorTex = new THREE.CanvasTexture(colorCv);

function heightAt(x, y) {
  let h = 0;
  for (const b of bumps) {
    const d = Math.hypot(x - b.x, y - b.y) / b.r;
    if (d < 1) h += Math.pow(Math.cos(d * Math.PI * 0.5), 2);
  }
  return h;
}
const normalCv = document.createElement('canvas'); normalCv.width = normalCv.height = S;
const nc = normalCv.getContext('2d');
const nimg = nc.createImageData(S, S);
for (let y = 0; y < S; y++) {
  for (let x = 0; x < S; x++) {
    const hl = heightAt(x - 1, y), hr = heightAt(x + 1, y), hu = heightAt(x, y - 1), hd = heightAt(x, y + 1);
    const vx = -(hr - hl) * 5, vy = -(hd - hu) * 5, vz = 1;
    const len = Math.hypot(vx, vy, vz);
    const i = (y * S + x) * 4;
    nimg.data[i] = ((vx / len) * 0.5 + 0.5) * 255;
    nimg.data[i + 1] = ((vy / len) * 0.5 + 0.5) * 255;
    nimg.data[i + 2] = ((vz / len) * 0.5 + 0.5) * 255;
    nimg.data[i + 3] = 255;
  }
}
nc.putImageData(nimg, 0, 0);
const normalTex = new THREE.CanvasTexture(normalCv);

const geo = new THREE.IcosahedronGeometry(1.05, 2);
const raw = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x8a7454, roughness: 0.75 }));
raw.position.x = -1.7;
const baked = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ map: colorTex, normalMap: normalTex, roughness: 0.6 }));
baked.position.x = 1.7;
scene.add(raw, baked);
const key = new THREE.DirectionalLight(0xffffff, 1.9); key.position.set(3, 4, 5); scene.add(key);
scene.add(new THREE.AmbientLight(0xffffff, 0.45));

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const r = t * 0.00026;
  raw.rotation.y = r; baked.rotation.y = r;
  camera.lookAt(0, 0, 0);
  renderer.render(scene, camera);
});

하이폴리 조각(수백만 폴리곤)을 게임이나 웹처럼 실시간 예산이 빠듯한 곳에 그대로 쓸 수는 없습니다. 대신 하이폴리와 로우폴리를 겹쳐 놓고, 둘 사이의 차이 — 표면 굴곡, 가려짐(AO), 색상 — 를 픽셀 단위로 계산해 로우폴리의 UV 위에 이미지로 "구워냅니다"(베이킹). 결과물은 노멀 맵·AO 맵·컬러 맵 같은 여러 장의 텍스처입니다.

이렇게 하면 실제 형태는 단순한 로우폴리 메시 하나인데, 텍스처가 조명 계산을 대신 해줘서 훨씬 디테일해 보입니다. 앞선 노멀 맵·범프 vs 디스플레이스먼트 항목이 보여준 "텍스처로 셰이딩을 속인다"는 원리를, 실제 제작 파이프라인 규모로 적용한 게 베이킹입니다.

Blender는 Cycles의 Bake 패널을, Marmoset Toolbag이나 Substance Painter 같은 전용 툴은 베이킹만을 위해 만들어졌습니다. UV 아일랜드 사이 이음매에서 조명이 어긋나 보이지 않도록 패딩(여백)을 주는 것, 하이폴리·로우폴리 실루엣이 너무 다르면 베이킹이 깨지는 것(케이지 투영 거리로 보완)이 대표적인 함정입니다.

언제 쓰나

하이폴리 스컬프트나 스캔을 실시간 예산에 맞는 로우폴리 애셋으로 옮길 때 거의 항상 필요한 단계입니다.