레이 트레이싱

Ray tracing

카메라에서 화면의 각 픽셀로 가상의 광선을 거꾸로 쏘아, 그 광선이 무엇에 부딪히는지 계산해서 색을 정하는 렌더링 방식.

다른 이름: Ray castingRecursive ray tracing
···
html
<canvas id="rt-canvas"></canvas><span class="rt-cap">camera → pixel → object → light</span>
css
#rt-canvas{width:92%;height:82%;display:block}
.rt-cap{position:absolute;left:0;right:0;bottom:4%;text-align:center;font-size:clamp(9px,2.4vmin,12px);color:var(--muted)}
js
const canvas = document.getElementById('rt-canvas');
const ctx = canvas.getContext('2d');
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 cam = { x: 0.08, y: 0.5 };
const screenX = 0.3;
const sphere = { x: 0.64, y: 0.52, r: 0.15 };
const light = { x: 0.92, y: 0.14 };
const N = 6;

function draw(t) {
  const w = canvas.width, h = canvas.height;
  ctx.clearRect(0, 0, w, h);
  const P = (nx, ny) => [nx * w, ny * h];
  ctx.strokeStyle = '#5f5f70';
  ctx.lineWidth = Math.max(1, w * 0.002);
  ctx.beginPath();
  const [x0, y0] = P(screenX, 0.16), [x1, y1] = P(screenX, 0.84);
  ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();

  const [cx, cy] = P(cam.x, cam.y);
  const [lx, ly] = P(light.x, light.y);
  const [scx, scy] = P(sphere.x, sphere.y);
  const rad = sphere.r * Math.min(w, h);

  ctx.fillStyle = '#ffd27a'; ctx.beginPath(); ctx.arc(lx, ly, Math.max(4, w * 0.01), 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#3a4a72'; ctx.beginPath(); ctx.arc(scx, scy, rad, 0, Math.PI * 2); ctx.fill();
  ctx.fillStyle = '#e7ebff'; ctx.beginPath(); ctx.arc(cx, cy, Math.max(3, w * 0.008), 0, Math.PI * 2); ctx.fill();

  const active = Math.floor(t / 700) % N;
  for (let i = 0; i < N; i++) {
    const py = 0.2 + (i / (N - 1)) * 0.6;
    const [px, pyp] = P(screenX, py);
    let dx = px - cx, dy = pyp - cy;
    const dl = Math.hypot(dx, dy) || 1; dx /= dl; dy /= dl;
    const on = i === active;
    ctx.globalAlpha = on ? 1 : 0.3;
    ctx.lineWidth = on ? Math.max(2, w * 0.003) : Math.max(1, w * 0.0015);
    ctx.strokeStyle = '#7c8cff';

    const ox = cx - scx, oy = cy - scy;
    const b = 2 * (dx * ox + dy * oy);
    const c = ox * ox + oy * oy - rad * rad;
    const disc = b * b - 4 * c;
    let hx = px, hy = pyp, reflected = null;
    if (disc >= 0) {
      const th = (-b - Math.sqrt(disc)) / 2;
      if (th > 0) {
        hx = cx + dx * th; hy = cy + dy * th;
        const nx = (hx - scx) / rad, ny = (hy - scy) / rad;
        const dDot = dx * nx + dy * ny;
        reflected = [dx - 2 * dDot * nx, dy - 2 * dDot * ny];
      }
    }
    ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(hx, hy); ctx.stroke();
    if (reflected) {
      ctx.setLineDash(on ? [] : [4, 4]);
      ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(hx + reflected[0] * w * 0.32, hy + reflected[1] * w * 0.32); ctx.stroke();
      ctx.setLineDash([]);
      if (on) { ctx.fillStyle = '#ff8fb0'; ctx.beginPath(); ctx.arc(hx, hy, Math.max(2, w * 0.006), 0, Math.PI * 2); ctx.fill(); }
    }
  }
  ctx.globalAlpha = 1;
  requestAnimationFrame(draw);
}
requestAnimationFrame(draw);

래스터화(실시간 3D의 기본 방식)는 "이 삼각형이 화면 어디에 찍히나"를 묻는 반면, 레이 트레이싱은 거꾸로 "이 픽셀에서 장면 쪽으로 광선을 쏘면 뭐에 맞나"를 묻습니다. 광선이 물체에 맞으면 그 지점의 색을 계산하고, 반사·굴절 재질이면 새 광선을 쏘아 재귀적으로 반복합니다.

이 방식은 빛의 실제 경로를 거의 그대로 따라가기 때문에 정확한 반사·그림자·굴절이 "공짜로" 나옵니다 — 래스터화에서는 각각을 별도의 트릭(환경 맵, 그림자 맵)으로 흉내 내야 하는 것과 대조적입니다. 대신 장면이 복잡해질수록 광선-물체 교차 계산이 기하급수적으로 늘어나 무겁습니다.

Blender의 Cycles, Maya의 Arnold, C4D의 Redshift는 모두 레이 트레이싱(또는 다음 항목인 패스 트레이싱) 기반 렌더러입니다. 최근 GPU의 RT 코어는 이 교차 계산을 하드웨어로 가속해서 실시간 반사·그림자를 게임에도 들여왔습니다. 데모는 실제 3D 렌더 대신, 카메라에서 뻗어나간 광선이 구에 맞고 반사되어 광원 쪽으로 꺾이는 과정을 위에서 내려다본 다이어그램으로 보여줍니다.

언제 쓰나

정확한 반사·굴절·그림자가 필요한 최종 렌더나, GPU RT를 지원하는 실시간 반사 효과를 논의할 때.