トーンマップいろいろ

はじめに

今回はトーンマップをいろいろ作って遊んでみます。
前回の続きです。 前提として,

などを行っています

トーンマップ

線形マップ

{ \displaystyle
f(x) = x
}
f:id:hikita12312:20170826234130p:plain ただ入力をそのまま出力しただけです。もはやトーンマップではないです。
一応ガンマ補正やらブルーム処理の結果のリファレンスとして。

Reinhard

{ \displaystyle
f(x) = \frac{x}{1+x} \Bigl( 1+ \frac{x}{L_w^2}\Bigr)
}


f:id:hikita12312:20170826235136p:plain
{L_w=2.0}
f:id:hikita12312:20170826235152p:plain
{L_w=\infty}

前回も登場したReinhard関数。
http://www.cs.utah.edu/~reinhard/cdrom/
前との違いは,白色点(1,1,1)にマップする輝度{L_w}を指定するような形になっているところ。 {L_w=\infty}のときが前回と同じ関数です。{L_w=\infty}のときの関数系と,線形トーンマップを合成したような形となっています。

exp

{ \displaystyle
k = \log{(1/255)} / L_w \\
f(x) = 1 - \exp{(kx)}
}


f:id:hikita12312:20170826235827p:plain
{L_w=2.0}
f:id:hikita12312:20170826235839p:plain
{L_w=8.0}

1.0でサチるような関数を考えた結果,信頼と実績の指数関数を使ってみました。
これも,白色点にマップする輝度{L_w}を考えて設計しています。 8bitのSDRテクスチャにマッピングすると考えて,輝度が254/255以上となるような値を白色と定義しています。

log

{ \displaystyle
k = (e-1) / L_w \\
f(x) = \log{(1 + kx)}
}


f:id:hikita12312:20170827000257p:plain
{L_w=2.0}
f:id:hikita12312:20170827000306p:plain
{L_w=8.0}

指数関数があるなら,対数関数でトーンマップしてもいいだろうと作ってみたやつです。

ACES Filmic Tonemapping Curve

{ \displaystyle
a = 2.51 \\
b = 0.03 \\
c = 2.43 \\
d = 0.59 \\
e = 0.14 \\
f(x) = \mathrm{saturate}\Bigl( \frac{x(ax+b)}{x(cx+d)+e} \Bigl)
}


f:id:hikita12312:20170827000754p:plain https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
フィルム調なトーンマップです。 Ucharted2で使われていたらしいACESトーンマップを使いやすくしたための近似らしいです。
指数関数とかReinhardと違って,上に凸というわけではなく,輝度の小さい領域では立ち下がっていて, 暗い部分はより暗く,明るい部分はより明るくといったような,微妙にS字型となっているトーンマップです。

RGB or 輝度

今まで例に出していたトーンマップの適用結果の画像は,RGBの各チャンネル毎にトーンマップ関数を適用するRGBベースのものです。 ですが,少しだけ修正してあげれば輝度ベースで適用することも可能です。

// トーンマップの適用関数
// @param color 入力値
// @param exposure 露光値
// @param whiteLuminance 白色(1,1,1)にマッピングされる輝度
float3 Tonemap(int funcType, float3 color, float exposure, float whiteLuminance)
{
#ifdef LUMBASE
    // 輝度ベースのトーンマップのとき
    const float luminance = GetLuminance(color);
    return color / luminance * TonemapFunction(funcType, luminance*exposure, whiteLuminance);
#else
    // RGBベースのトーンマップのとき
    return TonemapFunction(funcType, color*exposure, whiteLuminance);
#endif
}

輝度に対して,トーンマップをかけると各チャンネル間の差を変えることなく彩度を保ってマッピングができます。

Reinhard

f:id:hikita12312:20170826235152p:plain:w300
RGB
f:id:hikita12312:20170827001544p:plain:w300
Luminance

ACES

f:id:hikita12312:20170827000754p:plain:w300
RGB
f:id:hikita12312:20170827001618p:plain:w300
Luminance

異なる露光の合成

いわゆるHDR写真みたいな,異なる露光でトーンマッピングを行って 白飛びした領域のみ,低い露光値を適用するトーンマッピングを試しにやってみたいと思います。

まずは,ACESをつかって,露光の高い白飛びしたシーンを用意。

f:id:hikita12312:20170827002216p:plain:w300

このシーンの0.8以上の白飛びしかけのピクセルをピンク色で図示するとこんな感じ。

f:id:hikita12312:20170827002317p:plain:w300

で、白飛びしかけのシーンを露光1/4倍で暗くマッピングしたものと補間しながら合成。

const float3 mappedColor1 = Tonemap(TONEMAP_FUNC_TYPE, outputColor.rgb, EXPOSURE, WHITE_LUMINANCE);
const float3 mappedColor2 = Tonemap(TONEMAP_FUNC_TYPE, outputColor.rgb, EXPOSURE / 4.0f, WHITE_LUMINANCE);
#ifdef LUMBASE
const float lum = GetLuminance(mappedColor1);
#else
const float lum = max(mappedColor1.r, max(mappedColor1.g, mappedColor1.b));
#endif
const float x = smoothstep(0.8f, 1.0f, lum);
outputColor.rgb = lerp(mappedColor1.rgb, mappedColor2.rgb, x);

f:id:hikita12312:20170827002434p:plain

左が合成前,右が合成後

f:id:hikita12312:20170827002216p:plain f:id:hikita12312:20170827002434p:plain

確かに白飛びしているウサギの顔や,空の青色がはっきり見える。
ただ,不自然に明暗が変わっていて微妙な気もします。調整の余地あり。