본문 바로가기

Graphics , Rendering

Piecewise Power Curve를 이용한 Filmic Tonemapping

원문 :  http://filmicworlds.com/blog/filmic-tonemapping-with-piecewise-power-curves/ (by John Hable)


(개인공부를 위한 번역)


Flimic Tonemapping을 포스팅한 지 꽤 오래되어, 이제 업데이트 할 때가 되었다. 신입들을 위헤 말해두자면, Filmic Tonemapping의 기본 전제는 우리의 이미지애서 Shoulder와 Toe을 이용해 필름이 가진 톤 커브를 시뮬레이션하는 것이다.

위의 이미지에서 왼쪽은 순수 선형의 톤 맵이고 오른쪽은 필름 커브를 사용한다.(이미지가 없는데용)

간단하게 역사를 짚고 넘어가자. 원본 커브는 Haarm-Pieter Duiker아조씨가 코닥의 반응 곡선에 대한 근사치로서 고안한 것이다. 그의 웹사이트에 이에 관한 프레젠테이션이 올라와 있다. (흥미로운데 어려운 ppt) 그는 선형(Linear) 을 지수곡선(Log) 로 변환하기 위해 Cineon Linear를 노드를 로그하는 데 사용했고(?), 최종적인 룩을 위해 LUT를 적용했다. 그리고 그걸 HLSL 셰이더에 포팅했다. 그와 내가 EA의 LMNO 프로젝트에 있을때, 짐과 리처드가 ALU들을 사용하는 더 빠른 근사치를 발견했는데, 이것은 PS3와 xBox360에서의 프로세스를 더욱 실용적으로 만들었다. 너티독으로 이직한 뒤 나는 Filmic Tonemapping의 엄청난 신봉자였고, 그것을 게임에 적용하기에 이르렀다. 그러나 언차티드 프랜차이즈는 극사실적인 룩이었기 때문에 약간의 조작을 위해 짐과 리처드의 방정식을 좀 고쳐서 사용했다.


Waylon Brinck의 Siggraph 2016 Talk (Uncharted 4 Technical Arts)와 같이 룩을 고안하고 LUT를 만들어내는 방법은 수많이 있지만,  아티스트가 오프라인 편집 도구 작업에 익숙하고, 룩을 고안하는 데 사용되는 툴로 스크린샷을 쉽게 왕복할 수 있는 인프라가 있는 경우 이 선택지는 훌륭한 옵션이다. 그러나 나는 많은 사람들이 아직까지도 intergrate가 단순하다는 이유로 언차티드2의 커브를 사용한다는 사실을 알았다.


이 포스트는 그것을 바로잡고자 하는 시도이다. 지난 몇 년간 계속해서 나는 엔진에서 Filmic Tonemapping을 고안하는 더욱 더 간단한 방법을 반복해서 시도해왔다. Uncharted2 커브에서 해결하고자 하는 몇 가지 특정한 문제들은 다음과 같다.


  • 심플하고 직관적인 컨트롤 : 컨트롤은 아티스트들이 이해하기 쉽도록 간단하고 쉬워야 한다.

  • 다이내믹 레인지의 직접적인 컨트롤 : 이것에 나에게 가장 큰 문제였다. 언차티드2 커브를 사용하는 것은 "모 아니면 도" 였다. 이 컨트롤들을 사용해서는 도저히 평평한 선형 커브를 만들 수가 없었다. 아주 약간 Shoulder를 가진 평평한 직선 커브가 필요할 때가 있을 것이다(안개낀 날처럼) 그리고 직사광선처럼 하이라이트와 그림자를 압축하고 싶은 때도 있을 것이다.

  • 잘 작동하는 커브들 : 언차티드2 커브는 파라미터들을 무리하게 넣을 경우 이상하게 작동한다. 새로운 방정식은 이런 이상한 작동이나 오목한 형태의 변화(Concavity changes) 없이 선형과 곡선을 왔다갔다 할 수 있다.

  • 엔진에서의 컨트롤 : 새로운 커브는 심플한 선형 파라미터들만을 사용해야 하며, 커브 에디터로 왔다갔다 할 필요가 없어야 한다.

  • 빠르고, 닫힌 형태 : 커브는 심플하고 측정이 빨라야 한다. 정확한 코스트는 별로 중요하지 않다 - 왜냐하면 요새는 이걸 LUT로 구워내기 때문에.. 하지만 LUT로 구워내는 코스트는 합리적이어야 한다.

  • Lerp 가능한 파라미터들 : 게임에서는 흔히 여러 개의 커브와 그들 간의 깔끔한 블렌드가 필요하다. 우리는 입력된 파라미터들을 보간할 수 있어야 하고 두 커브 간의 적절한 트랜지션을 가져야 한다.

  • 간단한 반전 : 엔진의 한계점들 때문에 우리는 가끔씩 포스트 이펙트를 계산해야 할 때가 있다.

  • Output Gamma으로 컨버그 : 출력 감마를 커브에 컨벌루션 할 수 있는 옵션이 있다면 아주 좋을 것이다. 우리가 그렇게 할 수 있다는 것이 밝혀졌다.

  • Github에 올려진 코드 : Intergration을 간단하게 한다. 소스는 CC0에 따라 사용 가능하다. github.com/johnhable/fw-public. 저 주소로 가면 받을 수 있음~


Filmic Curve에는 선형 구간, shoulder, toe 세 구간이 있다. 짧게 말하자면 toe 는 더 날카로운 블랙을 주고, shoulder는 과노출된 화이트로의 더 부드러운 트랜지션을 준다. 그리고 선형 구간(Linear Section)은 비교적 거의 변하지 않는 것처럼 보여야 한다. 더욱 완전한 설명으로는 아래 포스트를 참고하길 추천한다 : Film Contrast Characteristics.



우리는 커브를 4개의 점으로 나누어진 3개의 구간으로 생각할 수 있다. 첫 번째 점은 (0, 0) 원점에 있다. 두 번째 점은 Toe에서 Linear Section으로의 중간단계를 표시하고, 다음 포인트는 Linear Section에서부터 Shoulder로의 중간단계를 표시한다. 마지막 점은 커브의 화이트포인트(White point)이다.


그림을 보았을 때, 나는 5개의 파라미터를 효과적으로 사용할 수 있다고 생각했다. 첫 번째 점은 파라미터가 없다 : 왜냐하면 그 점은 항상 원점에 있기 때문이다. 그리고 마지막 점은 항상 y=1이므로 x에 대한 파라미터만 있으면 된다. Linear Section의 첫 시작점으로 (x0, y0) 쌍을 쓸 것이다. 그리고 (x1, y1) 쌍은 Linear Section의 끝점이다. 그리고 화이트포인트(W) 가 있다. 그러면 이제 각값들을 적절하게 보간할 커브를 찾으면 된다.


Toe를 묘사하기 위한 파라미터가 없는 것이 의아하게 여겨질 것으로 생각하겠지만, (x0, y0) 파라미터들과 Linear section의 경사로 암묵적으로 정의되어 있다.Toe 함수가 (x0, y0)의 위치와 경사와 매치되는 한, Toe는 우리가 기대하는 대로 동작해야 한다. Shoulder에도 마찬가지 효과가 적용된다.


우리는 여기에서 약간의 꼼수를 쓸 것이다. 첫째로, 파라미터는 각각 분리되어 움직였을 때 제대로 동작하지 않는다. 예를 들어, 우리가 만약 더 길거나 더 짧은 Toe를 원한다면, 우리는 보통 (x0, y0)을 함께 움직여아 한다. 그러므로 우리는 먼저 파라미터들이 (x0, y0, x1, y1, W)인 직행 커브(Direct Curve)를 먼저 만들 것이다. 그리고나서 유저 파라미터(User params)를 이용한 꼼수를 만들어 아티스트들이 직관적인 파라미터들을 지정할 수 있게끔 하고 직행 파라미터(Direct Params)를 도출해 낼 것이다.




파트 1: 제곱 곡선를 이용한 직행 파라미터

점들이 주어졌을 때, 그들 사이를 잇는 연속되는 곡선들을 어떻게 그릴 수 있을까? 한가지 방법은 다항식(Polynomials)이지만, 잘 되지 않는다. 이 변경사항들은 선형 공간에서 꽤 날카롭고, 방정식에 맞추는 것은 때때로 원치않는 곡률 변화를 야기할 것이다.

대신, 우리는 제곱 곡선(Power curves) 를 사용할 것이다.

 

우리가 쓸 기본 곡선는 아래와 같다 :


y = Ax^B


이 곡선은 사실 부동소수점 예측(Floating point precision)에서 문제가 있을 것이다(B가 증가할수록, A는 기하급수적으로 크거나 작아지고 FLT_MIN/FLT_MAX 로 다가갈 것이다. 그래서 커브 구간은 실제로 아래와 같을 것이다:




y = e^(ln(A) + B*ln(x))



In(A) 가 상수인 것에 주목하자. 이것은 그저 제곱 곡선을 다시 쓰는 방법 중 하나에 불과하다. 마지막으로, 우리는 오프셋과 스케일을 적용해야 한다. 그래서 실제 곡선은 아래와 같을 것이다:



y = scaleY * e^(ln(A) + B*ln((x - offsetX) * scaleX)) + offsetY


우리는 곡선을 좌우 또는 상하로 반전시키기 위해 scaleX와 scaleY를 분리해야 한다. 각각의 세 구간(Shoulder, Linear Section, 그리고 toe) 는 그 모양의 커브일 것이다. 이제 이 곡선을 셰이더나 CPU에서 실행하는 식은 다음과 같다:



1
2
3
4
5
6
7
8
9
10
11
12
13
float FilmicToneCurve::FullCurve::Eval(float x) const
{
    int index = (x < m_x0) ? 0 : ((x < m_x1) ? 1 : 2);
    CurveSegment segment = m_segments[index];
    return segment.Eval(x);
}
 
float FilmicToneCurve::CurveSegment::Eval(float x) const
{
    float x0 = (x - m_offsetX)*m_scaleX;
    float y0 = expf(m_lnA + m_B*logf(x0));
    return y0*m_scaleY + m_offsetY;
}
cs


이 코드는 몇몇 디테일들을 빠뜨리고 지나갔지만, 핵심 아이디어는 3개의 제곱 곡선 구간으로 filmic curve를 재현할 수 있다는 것이다. 전체 곡선을 구현하기 위해서는 그저 우리가 어느 구간에 있는지를 파악한 다음 값을 계산하기만 하면 된다.


Linear Section 도출해내기

Linear section을 도출해 내는 것은 꽤 쉽다. 그 구간은 B=1.0 일때의 오프셋와 스케일이다. 끝! 그러나 감마를 컨벌전하려고 하면 쪼끔 더 복잡해진다.


Toe 도출해내기

toe를 맞추는 것은 역시나 쉽다. 오프셋와 스케일은 무시하고, 우리는 아래의 조건으로 함수를 찾을 것이다:


1, 원점을 지난다.

2. Linear section과 같은 지점에서 만난다.

3. Linear section의 경사와 같은 지점에서 일치한다.


그래서 우리의 함수와 그것의 첫 도함수는 아래와 같다:



f(x) = Ax^B
f'(x) = ABx^(B-1); // derivative of f w.r.t. x



코드는 다음과 같다 :


1
2
3
4
5
6
7
8
9
10
11
// find a function of the form:
//   f(x) = e^(lnA + Bln(x))
// where
//   f(0)   = 0; not really a constraint
//   f(x0)  = y0
//   f'(x0) = m
static void SolveAB(float & lnA, float & B, float x0, float y0, float m)
{
    B = (m*x0)/y0;
    lnA = logf(y0) - B*logf(x0);
}
cs


우리가 A를 직접 구하는게 아닌 A의 지수를 구하고 있다는 것을 주목하자.



Shoulder 도출해내기

Shoulder를 도출해내기 위해서, 우리는 Toe에서 했던 것을 다시 할 건데, 다만 그걸 가로, 세로 방향으로 반전시킬 것이다. 그러나 한 가지 문제가 있다.

내가 처름 이 함수를 구현했을 때, 나는 내 코드에 버그가 있다는 것을 알았다. 반전된 제곱 곡선은 우리가 원하는 화이트 포인트 W에 이르기 전에는 실제로 1.0에 도달하지 않으면서 1.0에 계속해서 가까워 지는 것을 보장하기 때문에 좋다. 문제는 그것이 1.0에 너무 가깝기 때문에 아무 의미가 없다는 것이다.


아래에 간단한 곡선 그래프가 있다. 이것은 선형 값 4.0에 이르자마자 흰색에 도달해야 한다. 그러나 인식으로는 흰색에 일찍 도달하는 것처럼 보인다. 이것은 3.25 값에서, 그래프가 감마 공간으로 변환된 이후 0.996을 찍기 때문이다. 이것은 우리의 마지막 8비트 값이다. 그래서 3.25부터 4.00범위의 값은 모두 254에서 255값을 가진다. 이것은 쓸모가 없다. 우리는 너무 평평한 커브는 원하지 않는다..


대신에, 우리는 화이트포인트를 더 가게 할 것이다(Overshoot). 우리는 OvershootX, OvershootY 두 파라미터를 더할 것이다. 제곱 곡선을 (W, 1.0)에서 끝나게끔 하는 대신, (W + overshootX, 1.0 + overshootY) 에서 끝나게 하는 것이다. 얼마나 더 overshoot할지를 변화시킴으로써 각도를 증가시키고 shoulder 끝부분의 더 넓은 범위를 얻을 수 있다.




그러나 이 변경은 추가적인 문제를 더해준다. 우리가 overshooting하기 때문에. 우리는 화이트포인트 W에서 우리가 원하는 값 W에 도달할 것이라는 것을 보장받지 못한다. 우리는 overshootX나 OvershootY 중에 하나를 선택해서, 우리가 정한 화이트포인트에서 1.0을 찍도록 다른 파라미터를 도출해내야 한다. 그러나 그렇게 너저분한 함수는  내가 아는 한 닫힌 형태가 아니다. 대신, 우리는 전체 함수에 스케일을 적용해서 문제를 해걸할 것이다.



여기에 같은 곡선이지만 각기 다른 overshooting값들을 가진 곡선들이 있다. 여기에서 우리가 원하는 점 W에서 흰색에 도달하는것을 확실시하기 위해 스케일링을 적용했으므로, 선형 구간이 약간 변경된 것에 주목하자.



함수 스케일링하기

우리가 써볼 수 있는 꼼수들이 몇가지 더 있다. 일을 간단히 하기 위해, 우리는 x 값들을 1.0/W으로 스케일링할 수 있다. 즉, x값을 스케일함으로 우리는 1.0에서 흰색에 도달할 수 있다. 이 연산은 함수를 텍스처로 구워내는 것을 더욱 간단하게 해 준다.


감마 컨벌젼하기

마지막으로, 우리는 이 함수에 감마 파라미터를 컨벌전할 수 있다. 우리의 filmic curve는 선형 값을 인풋으로 받고 선형 값을 아웃풋으로 출력한다. 그러나 많은 경우에 우리는 디스플레이 감마를 여기에 적용하고 싶을 것이다. 인풋 파라미터들을 약간 조정해서 이걸 할 수 있다.

감마 함수를 적용하고 싶은 대부분의 경우에서 명확성을 위해, 위 툴들을 사용해 초기 파라미터들로부터 도출해낸 filmic curve를 간단히 F(x)라고 부르기로 하자. 그리고 감마 함수를 G(x), 합쳐진 함수를 H(x)라고 부르자.


F(x) = // our filmic function
G(x) = pow(x,displayGamma). // gamma correction 

H(x) = G(F(x)) // our filmic function, followed by gamma.


또한, 우리는 우리의 Filmic 함수가 다음과 같은 제약을 갖는 것을 알고있다.


F(x0) = y0

F(x1) = y1
F'(x0) = m
F'(x1) = m


Chain 룰을 적용해서, 우리는 아래와 같이 알 수 있다 :


H(x) = G(F(x))

H'(x) = G'(F(x))*F'(x)



그리고 커브 중간의 제약은 다음과 같을 것이다 :



H(x0) = G(y0)
H(x1) = G(y1)
H'(x0) = G'(y0)*m
H'(x1) = G'(1)*m



그래서 우리가 Filmic 커브를 감마 스텝과 합치고 싶을 경우, 우리는 그냥 시작점에 chain rule을 적용해서 함수를 풀면 된다. 이것에 완벽한 근사치는 아님을 알아두자. 그리고 당신이 LUT를 구워내는 것이라면 이것을 할 이유가 없다는 것도 알아두라. 그러나 어떤 이유에서 셰이더에서 두 단계를 모두 해야하고 LUT를 굽지 않을 것이라면 저 선택지가 있다는 것만 알면 된다.


마지막으로, 감마 커브와 컨벌전한 경우 우리의 Lineanr section은 더이상 선형이 아니다. 그러나 함수가 오프셋과 스케일값이 빌트인되어 있으므로, 아무 문제가 없다.


마지막으로 이 함수가 가지는 또 다른 장점은, 이 함수의 역이 같은 형태를 가진다는 것이다. 곡선구간에서, 역은 원래의 값을 구하는 것과 매우 비슷하다 : 



1
2
3
4
5
6
7
8
9
10
11
12
13
float FilmicToneCurve::CurveSegment::Eval(float x) const
{
    float x0 = (x - m_offsetX)*m_scaleX;
    float y0 = expf(m_lnA + m_B*logf(x0));
    return y0*m_scaleY + m_offsetY;
}
 
float FilmicToneCurve::CurveSegment::EvalInv(float y) const
{
    float y0 = (y-m_offsetY)/m_scaleY;
    float x0 = expf((logf(y0) - m_lnA)/m_B);
    return x = x0/m_scaleX + m_offsetX;
}
cs


요악하자면, 커브를 컨트롤하기 위한 파라미터들은 다음과 같다.


1
2
3
4
5
6
7
8
9
10
float m_x0;
float m_y0;
float m_x1;
float m_y1;
float m_W;
 
float m_overshootX;
float m_overshootY;
 
float m_gamma;
cs



  • (x0, u0)과 (x1, y1) 은 선형 구간을 정의한다.
  • W는 화이트포인트를 정의한다.
  • overshootX와 overshootY는 더 가파른 shoulder를 위한 여분의 공간을 더해준다.
  • gamma는 커브에 컨벌전하기 위한 여분의 감마이다.


파트2 : 사용자 파라미터


위에 기술된 곡선들은 그래프 상의 특정한 점들을 선택해서 그것들로부터 부드러운 filmic tomemapping curve를 도출해내는 방법을 소개한 것이다. 문제는 이 파라미터들이 아티스트들이 컨트롤하기에 직관적이지 않다는 것이다. 커브들이 쓸모 있는 방향으로 다르게 동작하게 하기 위해서는 보통 여러 파라미터들을 함께 한꺼번에 움직이는 것이 필요하다. 예를 들어, 당신이 다이내믹 레인지를 Linear section으로부터 shoulder 쪽으로 옮기고 싶다면, (x1, y1) 을 모두 함께 움직어야 한다. 이것은 당신이 포인트를 x와 y 따로따로 움직이는 게 아닌 Linear section을 따라 움직이고 싶어할 것이기 때문이다.

다른 문제는 당신이 좋지 않은 파라미터들을 입력했을 때이다. 예를 들어, 당신은 toe를 항상 양수 곡률로, 그리고 shoulder를 항상 음수 곡률로 유지하길 원할 것이다. 이상적으로 파라미터들은 그렇게 정의되어야 하고 그래야 이 경우들이 보장된다.

그래서 우리는 직관적으로 곡선을 묘사하는 다른 컨트롤 세트들을 사용할 것이다 (toe가 얼마나 강한지, 얼마나 shoulder가 길지 등등). 그리고 거기에서 직행 파라미터(Direct Parameters)들을 도출해낼 것이다.


아래는 우리가 아티스트들에게 노출시킬 파라미터들이다:


1
2
3
4
5
6
float m_toeStrength; // [0-1]
float m_toeLength; // [0-1]
float m_shoulderStrength; // [0-1]
float m_shoulderLength; // in F stops
float m_shoulderAngle; // [0-1]
float m_gamma;
cs



직행 파라미터들을 도출하기 위해서, 우리는 커브를 몇 가지 방법으로 제한해야 한다. 첫 먼째 단순화는 Linear section 의 경사를 늘 1.0의 경사를 가지도록 강제하는 것이다. 처음에는 이 점이 문제가 있는 제한으로 보이겠지만, 실제로 이는 보편성을 크게 잃지 않는다.이 커브를 적용하기 전에, 노출 조정을 실행할 것임을 암시한다. 즉, 당신은 전체적인 빛 강도를 값으로 곱해줄 것이다. Linear section의 경사를 옮기는 것은 1.0의 경사를 노출 조정과 함께 쓰는 것과 동등하다.


part1에서 나는 linear section의 경사를 별개의 컨트롤을 사용하는 것을 시도해 보았다. 그러나 Linear section과 toe의 컨트롤을 적용하는 것은 항상 서로 충돌이 있었다. 그래서 Linear section의 경사를 고정하는 것이 가장 타당한 해결방법이었다. 더 좋은 방법들이 있겠지만, 실험해본 결과 이것이 최적의 트레이드오프였다.


Toe 파라미터

우리는 toeStrength와 toeLength라는 두 개의 toe 파라미터가 있다.  Length는 얼마나 많은 다이내믹 레인지가 toe 안에 있을지에 영향을 준다. 작은 값이라면, toe는 아주 짧고 linear section으로 재빨리 전환될 것이다. 그리고 더 긴 값이면 더 긴 toe가 될 것이다. 식은 간단하다 :


x0 = toeLength * .5;


0값은 toe가 없다는 뜻이고, 1은 toe가 전체 곡선의 절반을 차지한다는 것을 의마한다. 아래의 그래프에서, 파란 곡선은 그냥 선형이다 : 왜냐하면 toe가 없기 때문이다. 그 밑의 곡선(주황색)은 약간의 toe를 가진다. 보라색 곡선을 향한 모든 곡선은 긴 toe를 갖고 있다. 이 파라미터는 toe가 얼마나 빨리 linear section으로 바뀌어가는지에 영향을 준다.


toe strength 파라미터는 y0 파라미터를 떨어뜨려 더 강한 toe를 갖게 한다. 우리는 이것을 y0을 x0과 0 사이로 보간하기 위해 사용한다. 아래 다섯 개의 곡선상에서 toe에서 linear section으로의 전환이 x좌표의 같은 점(0.11)에서 이루어진다는 점을 주목하자. 극단적인 경우(보라색 곡선) toe는 x축에 대해 완전히 평평하다.


toe를 disable시키는 방법에는 두 가지가 있다는 것을 알아두자. toeLength 나 toeStrength 둘 중 하나를 0으로 만들면 toe는 사라질 것이다. 그러나 대체로 toe length는 상수로 두고, toe strength만을 사용해서 조정하는 것이 가장 좋다.



Shoulder 파라미터

Shoulder 파라미터는 세 가지가 있다 : shoulderStrength, shoulderLength, 그리고 shoulderAngle.

shoulderStrength는 직관적으로 shoulder 커브를 그래프에서 어느 지점에서 시작할지에 영향을 준다. (x0, y0)이 결정된 이후 shouderStrength는 (x1, y1) 모두를 결정한다. 만약 shoulderStrenght가 1이라면, 이것은 shoulder가 toe가 끝난 바로 다음에 시작될 것임을 의미한다. (즉 y1 = y0) 그리고 shoulder는 가능한 한 가장 많은 범위를 가진다. 만약 shoulderStrength가 0이라면, y1=1이고, shoulder는 없다.

다음 그래프는 shoulderLength의 여러 가지 값을 보여준다. 눈치채기 조금 어렵지만, 모든 5개의 커브(선형인 것을 제외하고)는 같은 화이트포인트에서 끝난다. 하지만, 모든 커브들이 linear section에서 shoulder로 넘어가는 전환 지점이 다르다.



shoulderLength 파라미터는 얼마나 많은 F-Stop들을 곡선의 다이내믹 레인지에 더할 것인지 나타낸다. shoulderStrength가 0 인 경우 흰색에 도달하면 어디에서나 F 스톱을 선형 흰색 값 (W)에 더한다.

다음 그래프는 shoulderStrength 파라미터를 보여준다. 세 곡선 모두 linear section으로부터 shoulder로 전환되는 지점이 같다. 각각의 세 커브는 각기 다른 점에서 흰색에 도달한다. 파란색 커브는 주황색 커브보다 훨씬 높은 화이트포인트를 가진다. 그러나 그래프에 overshoot가 적용되지 않았기 때문에, 이들은 다 같이 합쳐진다.

마지막으로, shoulderAngle은 shoulder에 얼마나 많은 overshoot들이 더해질지를 나타낸다. 그리고 많은 overshoot는 더 가파른 각도를 만들어낸다. 이 식에는 많은 원리가 들어 있지 않지만, 그냥 잘 작동하는 것 같다.


m_overshootX = (W * 2.0f) * shoulderAngle * shoulderStrength;

m_overshootY = 0.5f * shoulderAngle * shoulderStrength;


지각에 의한 감마(Perceptual Gamma)


전체 곡선이 선형 공간에 있으므로, 우리는 아티스트들에게 아주 작은 값들을 쉽게 정할 수 있도록 할 필요가 있다. 간단한 트릭으로, 우리는 toeLength를 2.2배 제곱해서 toe에 대해 더 미세한 컨트롤이 가능하게끔 할 수 있다.


컨벌전된 감마(Convolved Gamma)

마지막으로, 우리는 컨벌전된 감마를 지나갈 것이다. 여기에는 더 볼게 없다.


사용자 파라미터들을 직행 파라미터들로


아래는 사용자가 제공한 파라미터들을 직행 파라미터로 변환하는 전체 함수이다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void FilmicToneCurve::CalcDirectParamsFromUser(CurveParamsDirect & dstParams, const CurveParamsUser & srcParams)
{
    dstParams = CurveParamsDirect();
 
    float toeStrength = srcParams.m_toeStrength;
    float toeLength = srcParams.m_toeLength;
    float shoulderStrength = srcParams.m_shoulderStrength;
    float shoulderLength = srcParams.m_shoulderLength;
 
    float shoulderAngle = srcParams.m_shoulderAngle;
    float gamma = srcParams.m_gamma;
 
    // This is not actually the display gamma. It's just a UI space to avoid having to 
    // enter small numbers for the input.
    float perceptualGamma = 2.2f;
 
    // constraints
    {
        toeLength = Saturate(toeLength);
        toeStrength = Saturate(toeStrength);
        shoulderAngle = Saturate(shoulderAngle);
        shoulderLength = Saturate(shoulderLength);
 
        shoulderStrength = MaxFloat(0.0f,shoulderStrength);
    }
 
    // apply base params
    {
        // toe goes from 0 to 0.5
        float x0 = toeLength * .5f;
        float y0 = (1.0f - toeStrength) * x0; // lerp from 0 to x0
 
        float remainingY = 1.0f - y0;
 
        float initialW = x0 + remainingY;
 
        float y1_offset = (1.0f - shoulderLength) * remainingY;
        float x1 = x0 + y1_offset;
        float y1 = y0 + y1_offset;
 
        // filmic shoulder strength is in F stops
        float extraW = exp2f(shoulderStrength)-1.0f;
 
        float W = initialW + extraW;
 
        // to adjust the perceptual gamma space, apply power
        dstParams.m_x0 = powf(x0,perceptualGamma);
        dstParams.m_y0 = powf(y0,perceptualGamma);
        dstParams.m_x1 = powf(x1,perceptualGamma);
        dstParams.m_y1 = powf(y1,perceptualGamma);
        dstParams.m_W = W;
 
        // bake the linear to gamma space conversion
        dstParams.m_gamma = gamma;
    }
 
    dstParams.m_overshootX = (dstParams.m_W * 2.0f) * shoulderAngle * shoulderStrength;
    dstParams.m_overshootY = 0.5f * shoulderAngle * shoulderStrength;
cs



예시 이미지


아래는 몇 가지 이미지의 비교이다. 소스 이미지는 Christian Bloch의 sIBL Archive.에서의 "Wooden Door" 에서 크롭해왔다.

먼저, filmic 커브에서 선형으로의 온오프 비교이다.





아래 이미지에서, Filmic 커브는 왼쪽 이미지에서 shoulder 없이 적용되었고, 오른쪽은 full shoulder로 적용되었다. 이는 하이라이트를 범위 안으로 되돌려놓는다. Shoulder 파라미터는 toe에 아무런 영향을 주지 않으므로, 그림자는 그대로이다.



Shoulder


아래 이미지는 overshoot의 영향을 보여준다. overshoot가 없으면, 왼쪽 이미지는 너무 푸른 기가 돈다(too cyan-ish). 왜냐하면 그린과 레드체널이 클램핑되지 않을 때 1.0에 너무 가까워지기 때문이다. 반만 오른쪽 이미지는 overshoot가 활성화되어 과노출된 부분의 디테일이 보존되었다. 햇볕 쪽에 있는 인도에도 디테일이 살아있다.



Overshoot


그리고 아래는 toe Strength 파라미터 예시이다. 블랙을 낮추면서 하이라이트는 그대로 두는 것에 주목하자. toe 를 변경하는 것은 커브의 다른 부분도 변경하지만, 이 변화는 미미하다.



Toe


선형 파라미터들의 기본값


이 파라미터들이 엔진에서 어떻게 세팅될 것인지는 개발자에게 달려있다. 나는 toeStrength shoulderStrength를 0으로 맞추고 toeLength shoulderLength를 조정하기 쉬운 적당한 값(0.5정도) 으로 세팅하는것을 선호한다. 기본 커브는 선형이고, strength 파라미터들로 얼마나 비선형일지 결정하는 식이다.

어떤 사람들은 이것에 동의하지 않는다. 논쟁은 "만약 사용자가 Filmic Tomemapping을 활성화한다면, 사용자는 즉시 뭔가 일어나는 것을 볼 수 있어야 한다.". 나는 이 논쟁을 이해한다. 그렇지만 게임 팀에 대해서는 이것에 동의하지 않는다. 아티스트들은 filmic 커브가 파이널 씬에 어떻게 영향을 주는지 이해해야 한다. 그리고 그들이 배우는 데 가장 좋은 것은 항상 선형으로부터 시작해서 필요한 만큼 범위를 더해가는 것이다.

선형에서부터 시작하는 것은 전환점을 만드는 것을 더 쉽게 한다는 또 다른 장점이 있다. 만약 당신 게임이 지금 선형이라면(filmic curve를 적용하기 전이라면..이라는 뜻?) 필르믹 커브를 켜도 아무것도 일어나지 않을 것이다. 그러면 toe와 shoulder를 필요한만큼 점진적으로 더해가면 된다. 나에게는, 이 방법이 상황마다 변하는 모든 요소들을 싹 고쳐서 당신 게임의 룩에 아주 큰 변화를 주는 것보다는 더 선호할 만하다고 생각한다. 솔직히, 나는 당신 파라미터들의 기본값이 큰 영향을 미치는지 아니면 아무 영향도 안주는지 상관없지만 나는 선형에서부터 출발하는 게 좋다.

그렇긴 해도, 당신이 파라미터 기본값을 원한다면 아래가 트위킹 시작점으로 좋을 것이다.


toeStrength = .5
toeLength = .5
shoulderStrength = 2.0
shoulderLength = 0.5 shoulderAngle = 1.0



밝기만 조절


마지막으로, 또 다른 일반적인 변화는 필르믹 커브를 조도(Luminance) 에만 적용하는 것이다. 나는 이 관례가 별로 맘에 들지 않지만, 이걸 하기에 좋은 이유들이 있다. filmic 곡선은 그림자에 약간의 채도를 더할 것이고 하이라이트에서 약간 채도를 뺏어갈 것이다. 이것이 원래 기능인지 아니면 버그인지에 대한 질문은 미결 문제이다.

조도만 조절하는 가장 간단한 코드는 아래와 같다:


1
2
3
4
5
 
float3 val = ...
float srcLum = dot(val,float3(1,1,1)/3.0)
float dstLum = Tonemap(srcLum)
return val * (dstLum/srcLum);
cs



다른 말로, 소스 조도(source luminance) 를 계산하고, 톤맵을 계산해서 목표 luminance를 계산하고, 그리고 비율을 곱한다. 어떤 사람들은 그걸 선호하지만, 나는 각각의 채널에 톤맵을 적용하는 것을 선호한다.


이론적인 예시로, 아래와 같은 변형을 만드는 톤커브가 있다고 하자.


F(0) = 0.00
F(2) = 0.80
F(4) = 0.95 
F(5) = 1.00


당신이 (0, 2, 4)의 RGB 값을 가지고 있고 이전의 filmic 커브를 조도에 적용하고싶다고 하자. 평균 조도 2.0은 톤맵된 이후 0.8이 될 것이다.


처음 컬러 (R=0, G=2, B=4)는 스케일 (0.8/2)를 적용할 경우 (R=0, G=8, B=1.6) 이 될 것이다. 입력된 블루 값이 4.0이었고 화이트 포인트가 5.0이었다고 하도, 블루 값은 여전히 클램프된다. 근본적인 문제는 선형 연산이 S자 모양의 곡선을 적용하고 난 뒤에는 제대로 동작하지 않는다는 것이다.

극단적으로 말해서, 같은 커브로 (R=4, G=4, B=4) 의 색을 가공한다고 하자. 평균 조도는 4.0이며, 톤맵된 조도는 0.95이다. 그러면 이 스케일(0.95/4.0) 을 처음의 색 (R=4, G=4, B=4) 에 적용하면 (R=0.95,G=0.95,B=.95).가 된다.


이 테스트 경우에, (R=4,G=4,B=4)의 블루 값은  (R=0,G=2,B=4) 의 블루 값보다 사실 낮다 - 이는 채널 간의 혼선(crosstalk) 때문이다. 우리는 충분한 레드가 없기 때문에 블루 값이 클램프되는 이상한 상황에 다다른다. 이것은 filmic s자 커브를 적용한 이후에 색의 색상과 채도를 보존하는 것이 아직 잘 정해지지 않았기 때문이다.


그러나 만약 filmic curve를 각각의 채널에 적용시킨다면,  (R=0,G=0.8,B=0.95) 의 결과를 얻으면서 하이라이트의 디테일이 보존될 것이다. 흰색에 가까이 다가갈수록, 우리는 채도를 낮추고 싶어한다! 과노출된 하이라이트에서 채도를 점진적으로 지우는 톤 커브는 버그가 아닌 기능이다. (내 소견으로는)


그렇게 말하긴 했지만, 이건 그냥 개인적인 선호이다. 우리는 예술적인 선택을 다루고 있으므로, 정답은 없다. 그러나 분명히 말해서, 나는 각각의 채널에 개별적으로 filmic 커브를 적용하는 것을 선호한다.