HDR 톤 매핑

HDR tone mapping

카메라가 담을 수 있는 것보다 넓은 명암 범위를 화면이 표현 가능한 좁은 범위로 눌러 담는 기법.

다른 이름: Tone mappingHDR 압축
···
html
<div class="wrap">
  <div class="stage"><canvas id="c" width="300" height="180"></canvas></div>
  <div class="hud"><span>왼쪽 원본 / 오른쪽 톤 매핑</span><b id="s">0.0</b></div>
</div>
css
.wrap{position:absolute;inset:0;padding:clamp(8px,3vmin,20px);display:flex;flex-direction:column;gap:6px}
.stage{position:relative;flex:1;min-height:0}
canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:10px;border:1px solid var(--line);background:#0a0a0a}
.hud{display:flex;justify-content:space-between;gap:8px;font:700 clamp(9px,2.2vmin,11px)/1 ui-monospace,monospace;color:var(--muted)}
.hud b{color:var(--fg)}
js
var W = 300, H = 180;
var c = document.getElementById('c'), ctx = c.getContext('2d');

function paintPhoto(cx, w, h) {
  var g = cx.createLinearGradient(0, 0, 0, h * 0.62);
  g.addColorStop(0, '#5b86c9'); g.addColorStop(0.55, '#bcd6ea'); g.addColorStop(1, '#f5e0ad');
  cx.fillStyle = g; cx.fillRect(0, 0, w, h);
  var sx = w * 0.74, sy = h * 0.22, sr = h * 0.11;
  var sg = cx.createRadialGradient(sx, sy, 0, sx, sy, sr * 3.2);
  sg.addColorStop(0, 'rgba(255,247,214,0.9)'); sg.addColorStop(1, 'rgba(255,247,214,0)');
  cx.fillStyle = sg; cx.fillRect(0, 0, w, h * 0.62);
  cx.fillStyle = '#fff8df'; cx.beginPath(); cx.arc(sx, sy, sr, 0, 7); cx.fill();
  cx.fillStyle = '#6f9152';
  cx.beginPath(); cx.moveTo(0, h * 0.64);
  for (var x = 0; x <= w; x += 14) cx.lineTo(x, h * 0.64 - Math.sin(x * 0.014) * h * 0.05 - Math.sin(x * 0.032 + 1.2) * h * 0.02);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  cx.fillStyle = '#405f38';
  cx.beginPath(); cx.moveTo(0, h * 0.79);
  for (var x2 = 0; x2 <= w; x2 += 14) cx.lineTo(x2, h * 0.79 - Math.sin(x2 * 0.022 + 0.6) * h * 0.035);
  cx.lineTo(w, h); cx.lineTo(0, h); cx.closePath(); cx.fill();
  var tx = w * 0.2, ty = h * 0.8;
  cx.strokeStyle = '#3a2a1c'; cx.lineWidth = Math.max(1.5, w * 0.008);
  cx.beginPath(); cx.moveTo(tx, ty); cx.lineTo(tx - w * 0.01, ty - h * 0.13); cx.stroke();
  cx.fillStyle = '#2e4a28';
  for (var i = 0; i < 5; i++) { cx.beginPath(); cx.arc(tx + (i - 2) * w * 0.013, ty - h * 0.15 - (i % 2) * h * 0.018, w * 0.028, 0, 7); cx.fill(); }
  cx.fillStyle = '#1e1e1c';
  cx.beginPath(); cx.arc(w * 0.56, h * 0.745, h * 0.018, 0, 7); cx.fill();
  cx.fillRect(w * 0.555, h * 0.76, w * 0.01, h * 0.06);
  var img = cx.getImageData(0, 0, w, h), d = img.data;
  for (var p = 0; p < d.length; p += 4) { var n = (Math.random() - 0.5) * 10; d[p] += n; d[p + 1] += n; d[p + 2] += n; }
  cx.putImageData(img, 0, 0);
}
paintPhoto(ctx, W, H);
var orig = ctx.getImageData(0, 0, W, H);
var t = 0;
function frame() {
  t += 0.015;
  var s = (Math.sin(t) + 1) / 2 * 2.6;
  var lut = new Uint8ClampedArray(256);
  for (var v = 0; v < 256; v++) {
    var x = v / 255;
    var y = x * (1 + s) / (1 + s * x);
    lut[v] = Math.round(Math.min(1, Math.max(0, y)) * 255);
  }
  var out = ctx.createImageData(W, H);
  var od = out.data, id = orig.data;
  for (var i = 0; i < id.length; i += 4) { od[i] = lut[id[i]]; od[i + 1] = lut[id[i + 1]]; od[i + 2] = lut[id[i + 2]]; od[i + 3] = 255; }
  ctx.putImageData(out, 0, 0);
  var frac = (Math.sin(t * 0.5) + 1) / 2 * 0.7 + 0.15;
  var sw = Math.round(W * frac);
  if (sw > 0) ctx.putImageData(orig, 0, 0, 0, 0, sw, H);
  ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(sw, 0); ctx.lineTo(sw, H); ctx.stroke();
  document.getElementById('s').textContent = s.toFixed(1);
  requestAnimationFrame(frame);
}
frame();

실제 풍경의 명암 범위는 모니터가 표현할 수 있는 범위(대략 8~10스톱)보다 훨씬 넓습니다 — 맑은 날 하늘과 그림자는 14스톱 이상 차이 나기도 합니다. 노출을 한 번에 맞추면 하이라이트가 날아가거나 그림자가 뭉개집니다. 톤 매핑은 전역적으로 감마를 누르는 대신, 밝은 영역은 더 강하게 압축하고 중간톤은 상대적으로 그대로 두는 비선형 곡선을 씁니다. 이 데모는 이 곡선의 세기를 바꿔가며 하늘의 디테일이 살아나는 대신 전체 대비가 눌리는 맞교환을 보여줍니다.

노을 지는 하늘과 어두운 전경을 한 장에 담고 싶을 때, 또는 여러 노출로 찍은 브라케팅 사진을 합성한 HDR 이미지를 일반 모니터에 표시할 때 씁니다. 창문 너머 밝은 야외까지 함께 살리고 싶은 부동산·인테리어 사진에서도 흔히 쓰입니다.

압축을 과하게 걸면 사진이 전체적으로 밋밋해지고(대비 소실) 하늘 근처에 부자연스러운 후광이나 "HDR 특유의" 과장된 느낌이 생깁니다. 톤 매핑은 그림자도 함께 끌어올리기 때문에, 원본 노출이 심하게 부족했다면 이 과정에서 그림자 노이즈가 두드러지게 드러납니다.

언제 쓰나

명암 차가 큰 장면(노을+전경, 창문 안팎)을 한 장에 담고 싶을 때 씁니다. 과하면 밋밋해지고 그림자 노이즈가 드러납니다.