HDR tone mapping

HDR 톤 매핑

Compresses a dynamic range wider than a screen can show down into the narrower range a display can actually reproduce.

Also known as: 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();

A real scene's dynamic range is often far wider than a monitor can reproduce (roughly 8-10 stops) — a bright sky against deep shadow can span 14 stops or more. Compress that with a single global exposure and highlights blow out or shadows crush. Tone mapping uses a non-linear curve instead of a flat gamma push, compressing bright areas hard while leaving midtones comparatively untouched. This demo sweeps that curve's strength, trading overall contrast for recovered sky detail.

Reach for it when a sunset sky and a dark foreground need to fit in one frame, or when several bracketed exposures have been merged into a true HDR image that now needs displaying on an ordinary screen. It's also common in real estate and interior photography, where a bright view through a window needs to survive alongside a dim interior.

Compress too aggressively and the photo goes flat overall (contrast disappears), with an unnatural glow around the sky — the exaggerated "HDR look." Because tone mapping lifts shadows along with everything else, an originally underexposed shot has its shadow noise dragged up and made obvious in the process.

When to use

Use it for scenes with a huge brightness spread — sunset plus foreground, or a window's inside and outside. Overdo it and the photo goes flat with visible shadow noise.