원문: Tone Mapping
톤 매핑
소개
이전 포스팅에서는 물리 기반 렌더링에서 생성한 것과 같은 HDR 이미지에서 노출값을 어떻게 계산할 수 있는지 설명했습니다. 노출값을 통해 기본적으로 이미지의 조도값을 톤 커브를 적용할 수 있는 범위로 스케일할 수 있게 “측정”할 수 있었습니다. 이전 포스팅을 읽지 않았다면 워크플로우는 아래 다이어그램과 같습니다:
이 포스팅에서는 세 번째 상자에 있는 톤 매핑 연산에 초점을 맞춥니다. 톤 매핑 연산은 조도값(또는 개별 색상 채널)을 입력으로 받아 디스플레이가 받고자 하는 [0, 1]사이의 값을 출력하는 커브/함수입니다. 그에 더해, 더욱 현실적으로 보이는 이미지를 만들어야 합니다. 그점은 매우 주관적이고 SDR 디스플레이는 현실을 실제로 재현할 수 없기 때문에 어떤 선택이 정답이다 라고 확실히 할 수 없습니다. 그렇기 때문에 시청자에게 대체적으로 같은 인상을 주려는 것에 집중해야 합니다. 완벽한 재현을 구현하기에는 제약이 너무 많기 때문에 (우리가 그걸 정말 원하는지도 의문이지만) 단순히 클램핑하는 것보다 훨씬 좋은 결과를 얻을 수 있습니다!
이런 동작을 구현한 많은 커브들이 있는데 대부분 아래와 같은 기능들이 있습니다:
- 더 많은 조도값을 1에 근접하게 만드는 “어깨”. 대개 이 수렴은 점근적입니다.
- 낮은 조도 범위를 어떻게 보여줄지 제어하는 “발”. 발은 어깨만큼 자주 사용되거나 중요하지는 않지만 이미지의 어두운 부분이 너무 어두워지지 않게 만드는 데 도움이 됩니다.
- 선형 또는 거의 선형인 부분은 “미드 톤” 스케일이 제어합니다.
가장 간단한 예제는 아마 리인하드 커브일 겁니다 (공식 3, 4).
리인하드 커브
사람들이 수년간 사용해 온 리인하드 커브는 보통 두 가지가 있습니다. 더 간단한 첫번째 곡선은 아래와 같습니다:
L_d = L’ / (1 + L’)
여기서 L_d는 최종 디스플레이 조도이며, L’은 노출 보정된 입력 조도입니다:
L’ = L_in / (9.6 * L_avg)
이 방법은 정말 간단하게 어떤 조명이더라도 최대값인 1.0을 넘지 않게 방지합니다. 낮은 조도값은 (L << 1) 선형적이 됩니다 (예: L_d = L). 하지만 이 커브는 실제로 1.0의 조도값을 보여주지 않을 것이기 때문에 수정된 두 번째 공식에서는 더 높은 조도값에 대해 더 많은 것들을 할 수 있게 합니다.
L_d = (L’ * (1 + (L’ / L_white^2))) / (1 + L’)
여기서 L_white는 유저가 saturation point를 설정할 수 있게 하는 변수입니다. L_white값을 바꾸면 어떻게 되는지 desmos 그래프에서 확인해 볼 수 있습니다.
이 톤 연산을 우리 이미지에 적용하기 위해 HDR PBR 출력과 평균 조도(1x1 텍스쳐에 저장됨)를 입력으로 받아 백버퍼에 결과를 기록하는 간단한 프래그먼트 셰이더를 작성할 수 있습니다.
$input v_texcoord0
#include "common.sh"
SAMPLER2D(s_texColor, 0);
// 이전 포스팅에서 봤던 컴퓨팅 셰이더의 출력입니다
SAMPLER2D(s_texAvgLum, 1);
// 여기에 커브에 필요한 인자들을 저장할 것입니다
// (이 경우에는 whitePoint)
uniform vec4 u_params;
void main()
{
vec3 rgb = texture2D(s_texColor, v_texcoord0).rgb;
float avgLum = texture2D(s_texAvgLum, v_texcoord0).r;
// Yxy.x이 Y, 조도
vec3 Yxy = convertRGB2Yxy(rgb);
float whitePoint = u_tonemap.y;
float lp = Yxy.x / (9.6 * avgLum + 0.0001);
// 아래 줄을 다른 톤 매핑 함수로 교체하면 됩니다
// 여기에서 단독으로 조도에 커브를 적용합니다
Yxy.x = reinhard2(lp, whitePoint);
rgb = convertYxy2RGB(Yxy);
gl_FragColor = toGamma(vec4(rgb, 1.0) );
}
코드는 매우 간단하며 절차도 간단합니다.
- RGB값을 Y가 조도인 CIE xyY 색공간으로 변환합니다.
- 노출값을 보정합니다.
- 톤 커브를 사용하여 조도(만)를 스케일합니다.
- 역변환을 수행합니다 (xyY -> RGB)
- 감마 보정을 하고 백버퍼에 결과를 씁니다.
reinhard(Y)
나 reinhard2(Y, whitePoint)
와 같은 각 커브 코드에 대해서는 shader toy를 참고하시기 바랍니다.
리인하드 커브에서는 RGB채널을 각각 처리하면 매우 채도가 낮은 검은색들이 나올 것이기 때문에 각각 처리하려 하지 않을 겁니다. 그래도 한번쯤 만들어서 출력을 비교해 보는 것도 나쁘지 않을 듯 합니다.
참고를 위해 아래 이미지들은 세 가지 다른 커브를 사용한 (감마 보정된) 결과입니다: 선형(연산 없음), 기본 리인하드, 변형 리인하드:
해결해야 할 것이 무엇인지 명확히 하기 위해 우선 선형 예제의 문제를 기억해 보는 것이 좋겠습니다. 토끼에 반사된 하이라이트를 보면, 사실 밝은 파란색의 하늘의 반사입니다. 하지만 하늘의 높은 조도가 토끼에는 그저 하얀색으로 보이게 됩니다. 비슷하게 태양에서 오는 빛이 엄청나기 때문에 창문 부분이 전부 날아가 버리고 동일하게 흰색으로 보입니다. 스케일의 다른 방향에서는 이미지에 검은색이 거의 보이지 않습니다. 검은색으로 보이는 대부분의 것들은 사실 회색입니다. 이 말인즉슨 토끼의 아래쪽 부분이나 수풀의 안쪽과 같이 차폐가 많이 된 부분이 있음에도 [0, 1] 범위를 전부 사용하지 않았다는 것입니다. 이런 것들이 톤 매핑으로 해결하려고 하는 좀 더 미묘한 문제들입니다.
리인하드 커브는 이런 문제들을 전부는 아니지만 어느 정도 해결하려고 합니다. 기본 리인하드는 실제로 토끼에 있는 하이라이트의 대부분의 흰색을 없애는 대신 짙은 푸른색을 보여줍니다. 하지만 왼쪽의 이파리는 물빠진 녹색이어서 (실제로 현실을 보여준다면) 많은 빛을 반사한다는 느낌을 주지 못하고 있습니다. 더욱 주목할 만하게, 태양빛은 대부분 완전 하얀색이지만 해당 창문의 오른쪽 위에 조그만 하늘 조각이 있는 걸 볼 수 있습니다. 선형 예제에서는 태양빛이 너무 강하지만 예상치 못한 저런 색상의 조각이 보이지는 않았습니다.
조정된 리인하드에서는 L_white 값에 따라 더 나은 하이라이트를 보여줍니다. 만약 그 값이 너무 작다면 선형 버전처럼 더 많은 피쳐들을 잃게 될 것이지만 L_white가 커진다면 기본 리인하드와 거의 비슷한 결과가 나올 것입니다. 아래 세 이미지들이 보여줍니다:
필르믹 커브
리인하드가 아무것도 안하는 것보다 나은 수준인 상황에서 업계에서는 좀 더 커스터마이징 가능한, 특히나 극단적으로 가는 방법을 찾고 있었습니다. Romain Guy가 이런 다른 커브들을 보여주는 shader toy를 만들었습니다. 제가 살짝 고치고 두 개의 리인하드 커브를 추가했습니다.
Guy의 shadertoy를 시도해 보기로 했습니다 - ACES 커브, Narkowicz의 구현을 참고했습니다. - ACES 커브에 기반한 언리얼 엔진의 톤 커브. 이 구현에 대해 많은 세부 정보를 찾을 수 없었지만, ACES 커브에 굉장히 가깝습니다. - 하지메 우치무라의 그란 투리스모 커브 - Timothy Lottes가 발표한 커브
각 톤 커브는 살짝 다른 “룩”을 가진 최종 이미지를 만들지만, 필르믹 커브가 다른 것보다 크게 다른 이미지를 만드는 것을 알 수 있습니다. 필르믹 커브는 리인하드 커브보다 덜 “씻겨나간” 이미지를 만듭니다.
또한 알아야 할 것은 RGB의 각 채널마다 적용하는 대신 조도 값에만 커브를 적용하는 것이 선택이라는 점입니다. John Hable이 전자를 지지하지만 Hue가 밀리는 결과를 냅니다. 프로스트바이트 팀의 이 발표에서 hue 보존 vs hue 비보존 톤 매핑의 예를 제공합니다.
두 가지 방법 모두 시도해 본 결과 John Hable이 말이 맞겠다라는 생각이 들었습니다. 톤 커브를 조도에만 적용하면 태양이나 하이라이트 부분에서 이상한 동작을 보이기는 하지만 잘 보면 최종 이미지의 hue를 쉬프트하지 않기 때문입니다. 극단적으로 밝게 푸른 하늘도 클램핑되어 파란색으로 보이게 됩니다. 하지만 픽셀을 꽉 채워 흰색으로 만들어 버리는게 더 나을 수도 있습니다. 아래 그림들은 세 가지 커브들에서의 차이를 보여줍니다: ACES, 우치무라, Lotte
감마 보정에 대한 주석
엄청 자세히 설명하지는 않겠지만, 감마 보정은 톤 매핑뿐만 아니라 일반 컴퓨터 그래픽스에서 색상값을 가지고 일할 때도 필수적인 부분입니다. 디스플레이가 선형적인 값을 기대하지 않기 때문에 심지어 톤 매핑 이후에도 이미지가 올바르게 보이기 위해서는 감마 보정을 적용해야 합니다. 이렇게 되는 가장 큰 이유는 인간이 밝기를 감지할 때 선형으로 인지하지 않기 때문입니다. 이런 감마가 무슨 말인지 더 이해하기 위해서는 여기 몇 가지 좋은 글을 읽어보는 것을 권합니다:
by John Novak
마치면서
이 포스팅에서 여러 가지 커브를 소개해 드렸는데, 어느 것을 사용할 지는 예술적 기준으로 판단해야 합니다. 개인적으로는 그란 투리스모의 룩을 가장 좋아합니다. 하지만 최근 몇년간은 ACES 커브가 많은 인기를 얻고 있습니다.
이 주제에 대해 더 알아야 할 것들은 아래와 같습니다:
- 지역화된 톤 매핑: 이 포스팅에서는 하나의 노출값만을 사용하여 전체 이미지를 매핑했지만 인간의 눈은 그것보다 더 복잡하게 동작합니다. Bart Wronski의 포스팅에서는 글로벌 톤 매핑 연산 하나만을 사용했을 때 만나게 되는 한계들을 자세히 설명합니다.
- 기술적으로 노출을 계산할 때 머테리얼의 알베도에 조도를 곱한 다음에 나오는 조도를 사용합니다. 알베도를 곱하기 전의 조도를 기반으로 계산하는 것이 기술적으로 옳지만 그것을 계산하기 위한 프레임 버퍼가 추가로 필요합니다. 제가 알기로 그런 기법은 대개 선호되지 않습니다.
- 노출값을 렌더링된 선형 HDR 공간에 적용했는데, 일아야 할 것은 이것이 잘 동작하기 위해서는 HDR 버퍼가 극단적인 방사값을 담을 수 있을 정도로 충분한 범위를 가지고 있어야 한다는 점입니다. RGBA16F 버퍼로는 65,504까지만 표현이 가능하기 때문에 힘들 수 있습니다. 잠재적으로 “무한한” 방사값을 HDR 버퍼에 캡쳐하려는 대신 DICE나 다른 곳에서는 빛을 이전 프레임의 조도에 “미리 노출”시키는 방법도 사용합니다.
- 이 포스팅은 SDR 디스플레이에 렌더링하는 것에 초점을 맞췄습니다. HDR 디스플레이는 다른 인코딩을 사용하며 아직까지는 업계 표준이라는 것이 존재하지 않지만 워크플로우가 조금씩만 다르다 뿐입니다. Krzysztof Narkowicz의 블로그 포스팅에서 HDR을 지원하는 기초 단계와 그 외 몇몇 주제들을 설명하고 있습니다.
이전 포스팅을 읽으셨던 분이라면 재미삼아 만들었던 물리 기반 렌더러에서 톤 매핑 없이 만든 이미지를 기억하실 겁니다:
감마 보정이 없는 이미지이기 때문에 더 나빠 보일 수 있지만, 이미지를 망가트린 것은 거의 조도값이 [0, 1] 범위를 넘어섰기 때문입니다. 물리적으로나 기술적으로나 올바른 결과지만 디스플레이가 감당할 수 없을 뿐입니다. 아래 그림은 같은 장면을 수정된 리인하드 톤 매핑과 감마 보정을 적용한 결과입니다:
조명에 실제 물리 값들을 사용하여 PBR 셰이딩을 구현하려는 분들께 이 명확한 차이가 톤 매핑의 필요성을 보여주리라 생각합니다. 위의 토끼 이미지에서 하이라이트 부분의 색깔이 날아간 것과는 다르게 여기서는 물리적으로 의미있는 라이팅을 버리지 않고서는 올바른 이미지를 얻는 것이 불가능합니다.
샘플 코드
여기 나온 토끼 이미지는 여기에서 찾을 수 있는 BGFX 스타일의 예제로 만들었습니다. 여기에 화면 구성과 GPU 리소스, 앞서 보여드린 셰이더 코드도 있습니다.
'강좌번역' 카테고리의 다른 글
[번역] 자동 노출 (0) | 2022.02.11 |
---|---|
(번역) Cassandra와 MongoDB 비교 (0) | 2022.02.02 |
물리 개체의 PID제어 (0) | 2018.10.17 |
그래픽스 API 선택하기 (2) | 2015.09.06 |