Temporal Anti-Aliasingの実装

Temporal Anti-Aliasing

Temporal Anti-Aliasing(TAA)とは,アンチエイリアスの一種です。
前に組み込んだ,FXAAとかSMAAは1フレームの情報から, スクリーンスペースで画像処理的に行うアンチエイリアスでした。TAAは過去のフレームも使ってサンプリング数を稼いで行うアンチエイリアスです。

http://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING
https://github.com/Unity-Technologies/PostProcessing/blob/v2/PostProcessing/Shaders/Builtins/TemporalAntialiasing.shader

過去のフレームほど,重みを小さくして,1ピクセル未満の範囲でサンプリング位置を変えて,それらを合成するのが基本的なアイディアです。

f:id:hikita12312:20180722095207p:plain:w600

ただし,過去のフレームすべてを保持しておくことは行いません。 指数的に減少するような重みを採用して,指数移動平均とすることで,過去のフレームの積分である履歴バッファのみを保持します。
https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87#%E6%8C%87%E6%95%B0%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87

{ \displaystyle
\begin{eqnarray}
[\mathrm{Output}] &=& \alpha [\mathrm{Input}] + (1-\alpha) [\mathrm{History}] \\ 
[\mathrm{History}] &\leftarrow& [\mathrm{Output}]
\end{eqnarray}
}

Jitter

カメラの透視投影行列を上下左右に1ピクセル未満のオフセットを加えます。

{ \displaystyle
\begin{eqnarray}
a &=& \mathrm{aspect} \\
\theta &=& \mathrm{Fov Y} \\
f &=&  \mathrm{far Z}\\
n &=&  \mathrm{near Z}
\end{eqnarray}
}
{ \displaystyle
\begin{eqnarray}
\left(  
\begin{array}{cccc}
\frac{1}{a \tan\theta } & 0 & -2\mathrm{Offset_x} & 0 \\
0 & \frac{1}{\tan\theta } & -2\mathrm{Offset_y} & 0 \\
0 & 0 & \frac{f}{f-n} & -\frac{f n}{f-n} \\
0 & 0 & 1 & 0
\end{array}
\right)
\end{eqnarray}
}

DX系でのオフセットの掛け方の例です。透視投影行列の計算のnear平面の四角形をずらせば導出できます。
この微妙なオフセットがジッターとして各フレームで時間的な遅れを持ちながらもマルチサンプリング的な効果を与えます。

ずらすオフセットの量ですが,Halton列を利用しました。
https://en.wikipedia.org/wiki/Halton_sequence

VelocityMap

履歴バッファを単にサンプリングしているだけだと,以前実装した残像表現の効果と同様にカメラやオブジェクトが動くと残像として尾を引くアーティファクトが生じてしまいます。 これを補正するために速度バッファを利用して,1フレーム前に参照されていたピクセルと同じ位置の履歴バッファを参照します。

ただし,速度が大きくなってしまうと,やはり誤差が大きくなって残像が出てきてしまうので, 速度ベクトルの長さを利用して動きの速いピクセルほど指数移動平均の重みを小さくすることで残像を消していきます。

また,単に速度ベクトルを使わないで,近傍のテクセルの中から,depthを参照して最も手前にある速度ベクトルを採用すると,うまくいくようです。

Tonemap

入力されたピクセルがあまりに高輝度だと,高輝度成分の緩和に時間がかかって, フリッカーとしてチラついたり,まったくアンチエイリアスがかからなくなってしまったりと不都合が生じます。 理論上は無限時間かけて沢山サンプリングすれば軽減されるのですが, 高輝度成分をサチらせるようなトーンマップをかけてSDR空間上でTAAを行うことでも軽減できます。

{ \displaystyle
\begin{eqnarray}
k &=& \frac{a}{(1-a)L_{\mathrm{mean}}}
\end{eqnarray}
}
float3 TonemapTAA(float3 color)
{
    color *= k;
    return color / (1.0f + max(color.r,max(color.g, color.b)) );
}

float3 InvTonemapTAA(float3 color)
{
    color = color / (1.0f - max(color.r, max(color.g, color.b)) );
    return color / k;
}

このトーンマップ関数は,輝度に対するReinhard関数となっています。 逆トーンマッピング時に発散しないように,輝度は内積を使うものではなく,maxを取るものとしました。
kは露光に対応する係数ですが,平均輝度L_{\mathrm{mean}}がトーンマップによってKeyValueであるa=0.18に移るように選んでいます。

Colorのクランプ,輝度差のある履歴の不採用

TAAはアンチエイリアスなので,原理的には入力となるピクセルの周辺3x3範囲からテクスチャをサンプリングされるはずなので, 少なくとも3x3範囲の色には無い高輝度な色や暗すぎる色はサンプルされないはずです。 周辺の色のmin,maxを取ってクランプすることで,残像を除去します。

また,履歴バッファと入力カラーの輝度差があまりにも大きいと,フリッカーの原因となるので,これを除去するような係数も入れます。

以上の処理を適用したシェーダーが次のものです。

結果

f:id:hikita12312:20180722192934p:plain:w300
TAA OFF
f:id:hikita12312:20180722192956p:plain:w300
TAA ON
f:id:hikita12312:20180722193041p:plain:w300
TAA OFF
f:id:hikita12312:20180722193058p:plain:w300
TAA ON

なんだかアンチエイリアスが掛かっている気がします。 画面を動かしても不自然なゴーストはできません。 しかし,動画としてみると,高周波な部分がジッターで若干画面がザラつくような効果があるので,もっと調整が必要かもしれません。

RICHO THETAでHDRIの作成

RICHO THETA

360度カメラである,THETA SCを買いました。
解像度は静止画で5376x2688で,だいたい4K相当です。 マルチブラケット撮影もできるので,Photomatix ProみたいなHDRIの合成ソフトを使ってみたら,すぐHDRIができました。

http://cgcompo.blog134.fc2.com/blog-entry-67.html
https://www.hdrsoft.com/jp/

ただ,便利なソフトを使ってみただけでは面白くないので自分でHDRIの合成をしてみたいと思います。

HDRIの作成

まずは,以下のように複数の露光でマルチブラケット撮影をしておきます。

f:id:hikita12312:20180612103843p:plain:w600

THETAは絞りが弄れない関係上,どうしても明るく撮れてしまうので,木陰など暗めの場所で撮ったほうがよいと思いました。 また,撮影時間も30~40秒ぐらいはかかるので,三脚とシータ棒は必須です。

次に,各画像を取り込んでシェーダーでRGBA16Fフォーマットのテクスチャへ合成します。 THETAは残念ながらRAW画像は取得できないので,JPEGとして読み込みます。 画像を取り込むときには,EXIF情報も同時に読んで,露光時間とF値,ISO感度からEV値を計算しておきます。

{ \displaystyle
\begin{eqnarray}
\mathrm{EV} = \log_2{\Big( \{\mathrm{Fnumber}\}^2 * \{\mathrm{ExposureTime}\} * \frac{100}{\{\mathrm{ISO}\}} \Big)}
\end{eqnarray}
}

画像の読み込みはDirectXTexを利用しました。EXIF情報はIWICMetadataQueryReaderから読んでいます

https://github.com/Microsoft/DirectXTex
https://msdn.microsoft.com/ja-jp/library/windows/desktop/ee719904(v=vs.85).aspx

入力画像を1枚のHDRIとして合成するときには,EV値が高い画像ほど,値としては暗い部分になるようにスケールします。 いろいろ合成のやり方はあると思いますが,今回は三角形型の重み関数で合成を行っています。

f:id:hikita12312:20180612104234p:plain:w600

合成を行う時に,ソースとなる画像のあまりに暗すぎる部分や明るすぎる部分を計算から省くような処理を行っています。 matlabのドキュメントでも似たようなことをしているようです。

https://jp.mathworks.com/help/images/ref/makehdr.html

実際の合成を行うシェーダーは以下。

結果

出来上がったHDRIを使って,レンダリングをしてみました。

f:id:hikita12312:20180612105925p:plain:w600
f:id:hikita12312:20180612105925p:plain:w300 f:id:hikita12312:20180612105946p:plain:w300

中央の球体のレンダリングはUnrealEngineのPBRを実装しました。

http://blog.selfshadow.com/publications/s2013-shading-course/

ちゃんと高輝度成分が入っているので,ブルームエフェクトも機能しています。 HDRI合成の重みを工夫したらもっとキレイにできそうです。

モーションブラーの実装 その2

前にも,モーションブラーの実装を行いました。
このときは「A Reconstruction Filter for Plausible Motion Blur」という論文をそのまま実装したのですが, 余りにもそのまま実装しただけだったので,もう少しモーションブラーを考えてみます。

カメラモーションブラー

前回は,シーンレンダリング時に1フレーム前のオブジェクトのModelViewProjection行列を保存しておいて, 現在のフレームのModelViewProjection行列の結果との差分を取る方法で速度マップを作成していました。
この方法で間違いは無いのですが,今回は現在のシーンのDepth値とカメラのViewProjection行列のみを利用する方法を試してみます。

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch27.html

const float2 uv = In.aUV[0].xy;
const float depth = SampleTextureLevel0(g_DepthTexture, g_LinearSampler, uv).r;

// ScreenSpace空間 -1 ~ 1に変換
const float2 screenPos = uv * 2.0f - 1.0f;
// depth値から1フレーム前のスクリーン位置を計算
float4 prevPos = float4(screenPos, depth, 1.0f);
prevPos = Transform(CURRENT_INV_VIEWPROJECTION, prevPos);
prevPos /= prevPos.w;
prevPos = Transform(PREV_VIEWPROJECTION, prevPos);
const float2 prevScreenPos = prevPos.xy / prevPos.w;
// 現在のスクリーン位置
const float2 currentScreenPos = screenPos;
// -1~1のスクリーンスペースでの速度ベクトル
// cameraVelocityはUV空間での速度ベクトルなので,0.5倍
float2 cameraVelocity = (currentScreenPos - prevScreenPos) * 0.5f;

シェーダーにはDepthバッファと,CURRENT_INV_VIEWPROJECTIONで表される現在のViewProjection行列の逆行列, PREV_VIEWPROJECTIONで表される1フレーム前のViewProjection行列を渡しています。
スクリーン空間からDepth値を使ってWorld空間座標値を復元し,そこから1フレーム前のViewProjectionを適用して1フレーム前のスクリーン空間座標を求めています。

この方法の利点としては,シーンのレンダリングに関係なく速度マップを計算できることです。ポストエフェクト的に完全に後処理だけで速度マップを計算できます。 また,シーン自体の解像度よりも速度マップを縮小することもできるので高速化も可能です。
欠点としては,シーン内部のオブジェクト自体の速度を判別することが出来ないことです。カメラが固定でオブジェクトが高速に動くようなシーンのモーションブラーには不向きです。 ただ,オブジェクトがそんなに動かないゲームなら気にならない気もします。

ピンポンブラー

モーションブラーの計算時に,光芒で利用したような,ピンポンブラーを使ってみました。
https://game.watch.impress.co.jp/docs/20080310/3dcry.htm

f:id:hikita12312:20180505163952p:plain:w600

前回の「A Reconstruction Filter for Plausible Motion Blur」の実装や, UnityのPostProcessingStackのモーションブラーの実装だと,長さ64ピクセルのブラーを行う場合には,64回のテクスチャフェッチが必要でした。 しかし,再帰的にブラーを繰り返すことで,8回のサンプリング2回,合計16回のテクスチャフェッチで,同等のブラーが実現できます。

以下がピンポンブラーのシェーダーの一部抜粋です。

// このシェーダーを数回繰り返す
const float2 uv0 = In.aUV[0].xy;
const float3 c0 = SampleTextureLevel0(g_ColorTexture, g_LinearSampler, uv0).rgb;
// g_VelocityDepthTextureは,RGに速度マップ,BにDepth値が格納されている
const float4 vd0 = SampleTextureLevel0(g_VelocityDepthTexture, g_LinearSampler, uv0);
const float2 v0 = vd0.xy;
const float d0 = vd0.z;

// ブラー処理
const float jitter = (rand(uv0) - 0.5f) * 0.5f; // jitter -0.25f ~ 0.25f
float4 color = float4(c0, 1.0f); // 0番目のcolorは必ずサンプル
[unroll]
for (uint i = 1; i < 4; ++i)
{
    // BLUR_DELTA = powf(4.0f, passIteration) / blurMaxSampleNum;
    const float2 delta = v0 * (BLUR_DELTA * (i + jitter) );
    for (uint j = 0; j < 2; ++j)
    {
        const float2 offset = ((j == 0) ? (1.0f) : (-1.0f)) * delta;
        const float2 uv = uv0 + offset;
        const float3 c = SampleTextureLevel0(g_ColorTexture, g_LinearSampler, uv).rgb;
        const float d = SampleTextureLevel0(g_VelocityDepthTexture, g_LinearSampler, uv).z;
        // 手前にあるピクセルをサンプルしない為の重み
        const float w = step(d0, d + DEPTH_BIAS);
        color += float4(c, 1.0f) * w;
    }
}
color.rgb /= color.a;

Depth値とVelocity値を同じバッファに格納しています。また,変なゴーストが出ないように弱くjitterを掛けます。 また,モーションブラーは動いている物体より止まっている手前の物体の色はサンプルされないはずなので,マスクを掛けています。

結果

カメラモーションブラーとピンポンブラーを実装した結果です。 画面奥方向にカメラが動いている場合のモーションブラーのON/OFFを比較しています。

f:id:hikita12312:20180505165229p:plain:w600
f:id:hikita12312:20180505165200p:plain:w300
モーションブラーOFF
f:id:hikita12312:20180505165229p:plain:w300
モーションブラーON

まあまあ綺麗にそれっぽくなっています。Depthから速度マップを作成しているので,画面の奥の方の地面ほど,ちゃんとブラーの長さが小さくなっています。
ただし,結果を見ると,かなりエッジが汚いです。エッジ処理を何もしていないからなのですが,この辺が改善の余地ありです。Reconstruction Filterの方法と組み合わせてみるとか...

トーンマップのLUT化

トーンマップパスの修正

以前,トーンマップカラーコレクションを実装しましたが, 思うところあって,パスを次のように修正しました。

f:id:hikita12312:20180426103024p:plain:w600

前は,パス内部で直接トーンマップ関数の計算をしたり,色行列を乗算していました。 それだと,組み合わせが膨大になり,用意するシェーダー数が膨大になることと,複雑な計算をする場合に遅くなる懸念があったので,思い切ってLUT化しています。

また,細かいところですが,アンチエイリアスをトーンマップ後のSDR空間にて適用することにしました。 ノイズやディザリングを付与する処理も,アンチエイリアス後に適用しています。これは、アンチエイリアスで細かいノイズが消されないようにするためです。

これらの実装は,UnityのPostProcessingStackと似た実装になっています。

LUTの作成

LUTは3DテクスチャのXYZの軸をRGBと読み替えてマッピングするテーブルのことですが,単純に作ってしまうと0~1の範囲の色のテーブルしか作れません。 今回は無限大の範囲まで持つHDRカラーから,0~1までのSDRカラーにマッピングする必要があります。 HDRカラーの全範囲はLUTとして記録できないので,適当な範囲を定めて圧縮することで対応します。

UnityのPostProcessingStackの場合は.Alexa LogC Curveという対数カーブでHDRカラーを圧縮しているようです。

今回は,Alexa LogCではなく,S-Log3を使って圧縮をしたいと思います。 S-Log3を選んだ理由は特にありません。強いて言えば日本語の文献が沢山あったからでしょうか。Alexa LogCでもS-Log3でも,結局はlogの積和計算で表されて係数が違うくらいなので,大きな違いは無いんじゃないかと思います。

LUTを作成するCompute Shaderは以下のようなものになりました。

LUTを作る段階で,カラーコレクションやガンマの調整を含んでいます。 HDRからSDRへのトーンマップを行うときは,入力カラーにlinear2log()関数を適用したRGB値でLUTを引くことで実現します。

結果

Reinhard関数のLUTを作成し,トーンマップをした結果です。 LUTのサイズは64x64x64です。128にしても結果に差が出なかったので,64にしています。

f:id:hikita12312:20180426105609p:plain:w600

ついでに,GT-Tonemapを適用してみました。

https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp
https://www.desmos.com/calculator/et1qmcg10s

f:id:hikita12312:20180426105751p:plain:w600

歪曲収差の実装

歪曲収差

レンズを通した光が,像となるときに歪んでしまう収差のことです。 大雑把に言えば魚眼レンズみたいな歪みです。

https://en.wikipedia.org/wiki/Distortion_(optics)
http://www.nikon-instruments.jp/jpn/learn-know/microscope-abc/learn-more-microscope/about-aberration

格子状のグリッドを撮影したときに,樽型に歪んだり(樽型),中央に収縮するように歪んだり(糸巻き型)します。

半径方向歪み

Brownのレンズ歪みモデルなるものがレンズ歪みを表すモデルとして使われることが多いようです。

https://www.asprs.org/wp-content/uploads/pers/1971journal/aug/1971_aug_855-866.pdf
http://www.roboken.iit.tsukuba.ac.jp/~ohya/pdf/Robomech2014-KNS.pdf

Brownのレンズ歪みモデルでは,レンズの光軸中心からの半径方向の歪みと,円周方向の歪みをわけて表現されますが, まずは半径方向歪みについて考えます。

光軸中心を{(c_x,c_y)}として,光軸中心からの像の位置を{{\bf \bar{r}}=(\bar{x},\bar{y}) = (x-c_x,y-y_c)}とします。 この時,歪んだ像の位置{{\bf r}_d = (x_d,y_d)}は,距離の2乗の多項式の展開で表せるとしたものが,多項式モデルと言うそうです。

{ \displaystyle
\begin{eqnarray}
{\bf r}_d = \Big(1 + k_1 r^2 + k_2 r^4 + k_3 r^6+ \cdots \Big) {\bf \bar{r}}
\end{eqnarray}
}

一方,OpenCV等の画像処理ライブラリでは,カメラキャリブレーションに,有理関数モデルを利用しているようです。

http://opencv.jp/opencv-2svn/cpp/calib3d_camera_calibration_and_3d_reconstruction.html
https://www.robots.ox.ac.uk/~vgg/publications/2005/Claus05a/claus05a.pdf

{ \displaystyle
\begin{eqnarray}
{\bf r}_d = \frac{1 + k_1 r^2 + k_2 r^4 + k_3 r^6+ \cdots}{1 + k_4 r^2 + k_5 r^4 + k_6 r^6+ \cdots} {\bf \bar{r}}
\end{eqnarray}
}

他にも半径方向歪みのモデルには,有理関数モデルの特殊化として{k_4}以外0としたDivisionModel )http://www.robots.ox.ac.uk/~vgg/publications/papers/fitzgibbon01b.pdf) とか, 魚眼モデルとかFOVモデルといった多項式で表せないものもあります。
ちなみに,UnityのPostProcessingStackの歪曲収差は,樽型のときにFOVモデルを利用しているようなコードでした。

https://pdfs.semanticscholar.org/260d/bcca2edddeb7f743d186cc99a7d586023234.pdf
http://www.close-range.com/docs/Straight_Lines_Have_To_Be_Straight-Automatic_Calibration_and_Removal_of_Distortion--Devernay-Faugeras2001.pdf

今回はOpenCVのカメラキャリブレーションにも使われている実績や,計算の簡単さから4次までの有理関数モデルをレンズ歪みに利用することにします。

{ \displaystyle
\begin{eqnarray}
{\bf r}_d = \frac{1 + k_1 r^2 + k_2 r^4}{1 + k_3 r^2 + k_4 r^4} {\bf \bar{r}}
\end{eqnarray}
}

円周方向歪み

レンズによる光学的な歪みではなく,レンズとフィルムが完全に並行ではない場合に生じうる幾何学的な歪みが円周方向の歪みの項です。

{ \displaystyle
\begin{eqnarray}
\Delta x_t = \Big( 2p_1\bar{x} \bar{y} +p2(r^2 + 2\bar{x}^2)\Big) \Big(1+ p_3r^2 + \cdots\Big) \\
\Delta y_t = \Big( p1(r^2 + 2\bar{y}^2) + 2p_2\bar{x} \bar{y}\Big) \Big(1+ p_3r^2 + \cdots\Big) 
\end{eqnarray}
}

この項の{p_3r^2}以降を無視して,先程のレンズ歪みモデルに組み込みます。

{ \displaystyle
\begin{eqnarray}
{\bf r}_d = \frac{1 + k_1 r^2 + k_2 r^4}{1 + k_3 r^2 + k_4 r^4} {\bf \bar{r}} + 
\left[
\begin{array}{cc}
2p_1\bar{x} \bar{y} +p2(r^2 + 2\bar{x}^2) \\ 
p1(r^2 + 2\bar{y}^2) + 2p_2\bar{x} \bar{y} \\
\end{array}
\right]
\end{eqnarray}
}

結果

歪曲収差の式をUVに適用してテクスチャのフェッチする位置を歪めることで実装します。

半径方向歪みが正の値の場合(UVフェッチ時逆転なので,本来の計算式的には負),歪みが外に膨らんで行くので樽型の歪曲収差を作れます。

f:id:hikita12312:20180407140007p:plain:w300
歪曲収差 OFF
f:id:hikita12312:20180407140049p:plain:w300
歪曲収差 ON (樽型)

ただし,樽型の場合,画面内に元のテクスチャよりも外側のUVもフェッチされてしまうので,画面端の方が不正確なものになります。

f:id:hikita12312:20180407140123p:plain:w300 f:id:hikita12312:20180407140049p:plain:w300

そのため,歪曲収差を適用した後のUVを画面中心から拡大することで,見せたくない画面外側の範囲をクリップしても良いでしょう。

f:id:hikita12312:20180407140654p:plain:w300 f:id:hikita12312:20180407140631p:plain:w300

半径方向歪みが負の値の場合は,糸巻き型となります。

f:id:hikita12312:20180407140007p:plain:w300
歪曲収差 OFF
f:id:hikita12312:20180407140815p:plain:w300
歪曲収差 ON (糸巻き型)
f:id:hikita12312:20180407140854p:plain:w300 f:id:hikita12312:20180407140815p:plain:w300

樽型と糸巻き型の歪みが組み合わさったものは陣笠型と呼びますが, これもパラメータの組み合わせで半径方向の歪みが距離によって正負入れ変えるような関数系をつくって再現できます。

f:id:hikita12312:20180407141119p:plain:w300 f:id:hikita12312:20180407141052p:plain:w300

円周方向の歪みのパラメータを与えると,画面自体が奥に傾いたような歪みを作り出すこともできます。

f:id:hikita12312:20180407141454p:plain:w300 f:id:hikita12312:20180407141433p:plain:w300

実装してみると,思っていたよりも画面端の違和感が強いので, 周辺減光や, 倍率色収差をかけると, よりレンズっぽく画面端をごまかせるかと思います。

f:id:hikita12312:20180407141747p:plain:w600

SMAAの組み込み

SMAA

スクリーンスペースのアンチエイリアスとして,FXAAを組み込みました。 今回は,もう少し綺麗になるらしい,スクリーンスペースのアンチエイリアスであるSMAAを組み込んでみます。

http://www.iryoku.com/smaa/
https://github.com/iryoku/smaa

UnityのPostProcessingStack v2にも組み込まれているようです。

SMAAの組み込み

f:id:hikita12312:20180324122235p:plain:w600

SMAAは3つのパス(エッジ検出パス,ブレンド重み決定パス,ブレンドパス)と,テーブルとして利用されるテクスチャ2つ(AreaTexture,SerchTexture)が必要となります。 上記githubから,次のファイルを持ってきます。

  • smaa/SMAA.hlsl
  • smaa/Textures/SearchTex.h
  • smaa/Textures/AreaTex.h

SMAA.hlslはシェーダー本体で,SearchTex.h,AreaTex.hはテーブルとして利用されるテクスチャのバイナリ定義です。 SearchTextureはRG8フォーマット,AreaTextureはR8フォーマットとして,先に読み込んでおきます。

SMAA.hlslをインクルードして,3つのパスを定義しますが,この時,次のマクロを定義します。

  • SMAA_RT_METRICS :
    zwにスクリーンサイズ,xyにスクリーンサイズの逆数の入ったfloat4
  • SMAA_THRESHOLD :
    エッジ検出の閾値
  • SMAA_MAX_SEARCH_STEPS :
    ブレンド重み計算時のstep数
  • SMAA_MAX_SEARCH_STEPS_DIAG :
    ブレンド重み計算時のstep数。SMAA_DISABLE_DIAG_DETECTION定義で無効
  • SMAA_CORNER_ROUNDING :
    ブレンド重み計算時の角の丸め率? SMAA_DISABLE_CORNER_DETECTION定義で無効

実際の組み込み例は次のような感じです。

エッジ検出パスでは,輝度ベースでエッジ検出を行うSMAALumaEdgeDetectionPSパスか,Color値のそれぞれの要素の差分から検出を行うSMAAColorEdgeDetectionPSパスを選択できます。 また,Depthテクスチャを渡してエッジの誤検出を防ぐような仕組みもあるようです。 今回は利用していませんが,TAA的に履歴バッファを利用してさらにサンプル数を稼ぐようなパスも用意されています。

結果

SMAA.hlslのクォリティ定義SMAA_PRESET_HIGHにあたる設定で適用してみました。

f:id:hikita12312:20180324122424p:plain:w600
f:id:hikita12312:20180324122508p:plain:w300
SMAA OFF
f:id:hikita12312:20180324122526p:plain:w300
SMAA ON
f:id:hikita12312:20180324122633p:plain:w600

以前実装したFXAAは,エッジ部分がボケた形になってしあう弱点があるのですが, SMAAだとそのような事はおきず,綺麗にエッジのギザギザが無くなっています。 FXAAと比べてもかなり高品質な印象です。

ただし,FXAAだと0.14ms程度掛かっていた処理が,SMAAだと0.442ms程度まで増加しています。 やはり処理が複雑であることと,3つのパスを利用していることが効いているのでしょうか。

ImGuiでデバッグツール

以前,ImGuiの導入をしてから, 簡易的なプロファイラを作ったりしていたのですが, 思うところあって,いろいろ作り直しました。

f:id:hikita12312:20180317094721p:plain:w600

いかにも開発中って感じの画面になって良いです。

f:id:hikita12312:20180317094853p:plain:w600

プロファイラはツリー表示を導入したり,再帰構造を縦に並べて見やすくしてみたりと改善してみました。 利用時には,コード中に

BEGIN_GPU_MEASURE(pDeviceContext, "posteffect");
// (処理)
// ...
END_GPU_MEASURE(pDeviceContext);

みたいな感じのマクロで囲むとその範囲の測定を行います。測定範囲のネストも自動的に解決します。

ImGuiのサンプルにもありますが,ImGui::GetWindowDrawList()を利用して描画を行うときには,ImGui::GetCursorScreenPos()で描画位置のオフセットを取得して, 描画が終わったらImGui::Dummy()を使って描画された領域をダミーの矩形で埋めると,うまい感じにレイアウトが決まります。

f:id:hikita12312:20180317095412p:plain:w600

リアルタイムで,作業バッファの内容を画面中に表示する機能も作りました。 並べて比較したり,作業中のバッファをツリーで表示できるのは便利です。
ImTextureIDの実体はvoid*です。ImGui組み込みの実装にもよるのですが,テクスチャの寿命管理がちゃんとしていればImGui::Image()でテクスチャの描画が簡単に出来ます。 作業バッファを見るには,コード中で

// (処理)
// ...
DEBUG_DRAW(pDeviceContext,"pfx/glare/source", m_pGlareSourceTextureView);

のようなマクロから,コピー元のテクスチャとそのキーを指定すると,デバッグ描画が必要となっているならば,表示用のテクスチャにコピーされます。

ImGuiは便利ですね。