GRAPHICS PROGRAMMING

PBR Neutral Tone Mapping 코드 해설.

jplee 2025. 5. 28. 00:57
// Input color is non-negative and resides in the Linear Rec. 709 color space.
// Output color is also Linear Rec. 709, but in the [0, 1] range.
vec3 PBRNeutralToneMapping( vec3 color ) {
  const float startCompression = 0.8 - 0.04;
  const float desaturation = 0.15;
  float x = min(color.r, min(color.g, color.b));
  float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
  color -= offset;
  float peak = max(color.r, max(color.g, color.b));
  if (peak < startCompression) return color;
  const float d = 1. - startCompression;
  float newPeak = 1. - d * d / (peak + d - startCompression);
  color *= newPeak / peak;
  float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.);
  return mix(color, newPeak * vec3(1, 1, 1), g);
}

주석을 보면...
// 입력 색상은 비음영이며 리니어 Rec. 709 색 공간에 있습니다.
// 출력 색상도 리니어 Rec. 709이지만 [0, 1] 범위입니다.
이렇게 말하고 있다.
PBR Neutral Tone Mapping 알고리즘을 구현한 GLSL 함수이며 HDR(High Dynamic Range) 렌더링에서 광범위한 밝기값을 디스플레이가 표현할 수 있는 [0, 1] 범위로 압축하는 역할을 수행.

const float startCompression = 0.8 - 0.04;  // 0.76
const float desaturation = 0.15;

 
startCompression: 압축이 시작되는 임계값 (0.76)
desaturation: 채도 감소 강도 (0.15)

float x = min(color.r, min(color.g, color.b));
float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;
color -= offset;

 
RGB 세 채널 중 가장 어두운 값을 찾는다. 이는 해당 픽셀에서 "순수한 어둠의 정도"를 나타낸다. 만약 이 값이 0에 가깝다면 진짜 검은색이고, 값이 클수록 전체적으로 밝은 색상임을 의미.
만약 가장 어두운 채널이 0.08보다 작다면, x - 6.25 * x²라는 식을 사용합니다. 이는 부드러운 S-커브를 만들어내는데, 매우 어두운 값(x≈0)에서는 거의 변화가 없지만, 0.08에 가까워질수록 더 큰 오프셋을 적용.
6.25라는 계수는 x=0.08일 때 선형 구간과 매끄럽게 연결되도록 계산된 값. (0.08 - 6.25 × 0.08² = 0.04)
계산된 오프셋을 모든 채널에서 뺌. 이는 블랙 포인트를 살짝 올려주는 효과를 만들어, 완전히 검은 부분도 약간의 디테일을 보이게 함. 실제 사진에서 완전한 검은색은 거의 없고, 약간의 반사광이나 산란광이 있는 것과 같은 효과.
 

float peak = max(color.r, max(color.g, color.b));
if (peak < startCompression) return color;

const float d = 1. - startCompression;  // 0.24
float newPeak = 1. - d * d / (peak + d - startCompression);
color *= newPeak / peak;

반대로 가장 밝은 채널값을 찾음. 만약 이 값이 압축 시작점(0.76)보다 작다면, 압축이 필요 없으므로 그대로 반환. 이는 중간 톤 영역을 보존하는 핵심.
가장 중요한 압축 공식이 등장하는데 이는 하이퍼볼라 함수(쌍곡선 함수)의 형태로, 수학적으로는 y = 1 - k²/(x + k - c) 형태. 
d = 0.24는 압축 가능한 범위.
peak + d - startCompression는 압축 대상 범위를 조정.
특징은 점근선을 가진다는 것이다. 즉, peak가 무한대로 커져도 newPeak는 1.0에 수렴하며, 압축이 시작되는 지점에서는 기울기가 완만하게 시작됨.
 

float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.);
return mix(color, newPeak * vec3(1, 1, 1), g);

 
모든 채널에 동일한 비율 newPeak/peak를 곱함. 색상의 비율관계를 유지하면서 전체 밝기만 조정하는 방식이다. 예를 들어 (2.0, 1.5, 1.0)인 색상이 있다면, 각 채널의 상대적 비율은 그대로 유지하면서 전체 스케일만 줄인다.
얼마나 많이 압축되었는가에 따라 채도 감소 정도를 계산.
peak - newPeak는 압축량을 나타내고, 이에 desaturation 계수(0.15)를 곱한 후 하이퍼볼라(쌍곡선) 형태의 함수를 적용. 압축량이 클수록 g값이 1에 가까워져서 더 많은 채도 감소 발생.
압축된 색상과 같은 밝기의 그레이스케일을 선형 보간합니다. newPeak * vec3(1, 1, 1)는 완전한 회색을 만들고, g값에 따라 원본 색상과 이 그레이스케일 사이를 블렌딩.


알고리즘의 특징

장점

  • 자연스러운 압축: 하이퍼볼라 함수로 부드러운 톤 변화
  • 색상 보존: 어두운~중간 밝기 영역은 거의 그대로 유지
  • 과포화 방지: 밝은 영역의 채도를 점진적으로 감소
  • 연산 효율성: 간단한 수학 연산으로 구성

적용 분야

  • 게임 엔진: Unreal Engine, Unity 등에서 사용
  • 영화/VFX: 실시간 또는 오프라인 렌더링
  • 사진 처리: HDR 이미지 처리 소프트웨어