ポストエフェクトを自作してみる

今回はUE4のマテリアル機能を利用してポストエフェクトを自作してみます。

多分、誰もがとりあえずは実装してみそうなアウトラインを表示するための深度を用いたSobelフィルタと、ちょっとしたおまけのポストエフェクトを作ってみます。

Sobelフィルタは画像フィルタの一種で、今回のようなアウトラインを表示するのによく使われます。

カラーに対して行うことも可能ですが、今回は深度に対してフィルタリングを行っています。

Sobelフィルタは自身のピクセルを中心とした3*3ピクセルに対して以下のような係数を掛け算し、それを合算します。

ue054.png

左側は上下方向のフィルタリング、右側は左右方向へのフィルタリングで用いられる係数です。

今回はこの2種類を適用してみました。

では、適当にプロジェクトを作成し、コンテンツブラウザのGameフォルダ直下にPostProcessフォルダを作成しましょう。

ポストエフェクトを作成する場合にこのフォルダでなければいけない、ということはありませんので、Materialの下に置きたい!という人はそちらに置いてもらってもかまいません。

さて、まずはMaterial Functionから作成します。

マテリアルはBlueprintと同様にノードベースで作成していくことができます。

Blueprintでは接続済みのノード群を関数として切り分けることが可能でしたが、マテリアルではそのようなことができません。

その代わり、Material Functionというコンテンツとして関数化した処理を利用することが可能です。

Blueprintの関数は各Blueprintに紐づけられているのが基本ですが、マテリアルの関数はそうでもない、という理由からでしょう。

格納フォルダを右クリックし、[New Asset] -> [Materials & Textures] -> [Material Function] で新規作成します。

ue055.png

この関数はオフセット付きでピクセルの深度情報を取得する命令としますので、"DepthFetchWithOffset"という名前にしておきます。

この関数は以下のように実装しましょう。(クリックで拡大します)

ue056.png 

軽く見ていきましょう。

まず、"TexCoord"はPalette中ではTextureCoordinateとして存在しています。ポストエフェクトを作成する場合、これは各ピクセルに対応する0~1のUV座標となります。

"Input PixelOffset (Vector2)"は関数の入力値です。Palette中にはFunctionInputとして存在しています。

PixelOffsetは入力値の名前で、これはユーザが任意に指定することができます。()内は入力値の型でこれも変更可能です。

次のSceneTextureですが、これはカラーバッファや深度バッファ、各種GBufferのパラメータなどを取得する命令です。

このノードの出力にあるSizeはこのバッファの幅と高さ、InvSizeはその逆数です。

UV値は0~1の範囲なので、1ピクセルのUV値はInvSizeとなりますので、入力のPixelOffsetとInvSizeを積算、これにTexCoordを加算してオフセットを考慮したUV座標を求めています。

バッファの範囲を超えないように0~1でクランプし、このUV座標を用いて深度値をSceneDepthから取得しています。

なお、SceneDepthで取得できる深度値はビュー空間の深度値のようです。つまり、カメラから見たときの深度ってことですね。

次にSobelフィルタの結果を取得するMaterial Functionを作成します。

前回と同様にMaterial Functionを新規作成したら、名前をDepthSobelとします。

ノードは以下のように実装します。(クリックで拡大)

ue057.png 

大きめの関数ですが、やってることは先に説明したSobelフィルタそのまんまです。

関数の入力としてはSobelWidthとMaxDepthの2種類が存在します。

SobelWidthは近傍ピクセルをフェッチする際のオフセット値を指定します。

オフセットが大きいほどアウトラインは太くなりますが、その分エラーも発生しやすくなるので注意してください。

コメントの"8 Neighbor Depth Fetch"が8近傍の深度情報を取得する命令です。ここで先ほどの関数"DepthFetchWithOffset"を利用しています。

"Horizontal"と"Vertical"は先に提示した係数を積算し、加算している処理です。

上下方向、もしくは左右方向への深度の変化が一定であれば、この結果はほぼ0になるはずです。

しかし、大きな変化があった場合、正か負の値に大きく振れることになります。

最終的には絶対値をとって、正負どちらでも大きな変化があったかどうかをチェックする数値としています。

"Composition"では最終結果を求めています。

"Horizontal"と"Vertical"の結果を加算し、これをMaxDepthで除算しています。

この値を0~1にクランプしてSobelフィルタの結果とします。

変化が0に近ければアウトラインは表示されないので結果は0となり、閾値であるMaxDepth以上になると結果は1となり、フィルタの効果が最大になります。

なお、最後に1倍してるんですが、これは調整パラメータとして適当に入れていたものの名残です。気にしないでください。

ここでやっとポストエフェクトのマテリアルを作成する段階となります。

格納フォルダを右クリックし、[New Asset] -> [Material] を選択してください。

ue058.png

マテリアルを作成したら、"PostProcessDepthSobel"とでも名前を付けて、ダブルクリックして編集を開始します。

開くと最初はこのノードだけが存在しているはずです。

ue059.png

このノードはUnreal Engineのマテリアルすべての最終出力結果です。

実はこの状態ではライティングが行われるマテリアル、つまりオブジェクト用のマテリアルです。

これをポストエフェクト用に変更するには[Details]タブの[Material] -> [Material Domain]を"Post Process"に変更してやります。

ue060.png

こうすると先のノードはEmissive Colorだけがハイライトされている状態になるはずです。

実装は以下のようにします。(クリックで拡大)

ue061.png

Sobelフィルタの結果はアウトラインがもっとも強くて1.0、0.0に近づくと弱くなっていく仕様です。

なので、1-x (Palette中でOneMinus)を利用して0と1を逆転させています。

これを最終結果に積算すれば、アウトラインが強く出る部分が黒くなります。

しかし、これだけだと以下のような結果になってしまいます。

ue062.png

天球の部位によって深度差が大きくなってしまっているようで、このような同心円状のフィルタがかかってしまいました。

なので、下の方のノードでは、FadeStartDepthからFadeEndDepthまでの範囲でフィルタ結果をフェードしていき、FadeEndDepthの段階で完全に消えるようにしています。

このFadeStartDepthとFadeEndDepth、加えてMaxDepthとSobelWidthはPalette中ではScalarParameterとして存在しているのですが、このノードはパラメータを外に出す役割を果たしています。

つまり、ここで指定したパラメータは外部から変更が可能なのです。

シェーダコードは定数を内部に書いてしまった方が高速に動作はしてくれますが、こうしてしまうと定数を変更した際にシェーダの再コンパイルが走ってしまい、トライ&エラーがしにくくなってしまいます。

開発中はトライ&エラーのためにパラメータを外部に出し、パラメータが決定したら内部にもっていって再コンパイル、というのが理想でしょう。

UE4ではこのパラメータをシェーダ内部以外で固定化させる方法があります。

それがMaterial Instanceです。作成方法は、格納フォルダを右クリックし、[New Asset] -> [Materials & Textures] -> [Material Instance]です。

ue063.png 

名前はちょっと長いですが、"PostProcessDepthSobelInstance"としました。

これをダブルクリックし、[Details]タブの[Parent]に"PostProcessDepthSobel"マテリアルをドラッグ&ドロップしてみましょう。

ue064.png

[Parameter Groups]にパラメータが存在しているのがわかりますね。

デフォルトではチェックが入っていませんが、チェックを入れれば変更した数値が有効になります。

長くなりましたが、最後にこのポストエフェクトを有効にする方法です。

[Scene Outliner]から"GlobalPostProcessVolume"を検索し、クリックしてください。

このVolumeはLevel全体を覆っているPostProcess用のVolumeです。

このVolumeで有効にされたPostProcessは、他のVolumeで上書きされない限りは常に適用されます。

選択後、[Details]タブの[Misc] -> [Blendables]の+ボタンをクリックし、項目を追加してください。

追加された項目に"PostProcessDepthSobelInstance"を設定すれば有効になります。

というわけで、今回はこれで終了…ちょっと待て?

なぜかアウトラインがブルブル震えていませんか?

こんなに不安定ではとてもじゃないですが使用できません。

なぜこのようになるのかというと、UE4ではデフォルトのアンチエイリアシング処理としてTemporal AAが使われているからです。

このAAは1フレームでスーパーサンプリングするのは重くてできない…それなら複数フレームで描画した内容からスーパーサンプリングっぽくすればいいじゃない!という割と無茶なAA技術です。

しかし、このAAは1フレームで大きく変化する画面には弱いものの、普通にカメラが動く程度ならかなり綺麗にエイリアシングを消してくれます。

しかし、画面停止時もスーパーサンプリングを有効化させるため、GBufferの描画をジッター、つまり小刻みに揺らしているのです。

デフォルトでは、自作ポストエフェクトはトーンマップやTemporal AAをかけた後に処理されるのですが、SceneDepthで取得した深度値はジッターされたGBufferの情報です。

ジッターしている、つまり小刻みにGBufferが動いているため、アウトラインがブルブル震えてしまっているのです。

ではどうすればよいか?

答えは、ポストエフェクトをトーンマップ前に実行する、です。

"PostProcessDepthSobel"を開いて、最終結果ノードを選択してください。

そして、[Details]タブの[Post Process Material] -> [Blendable Location]を”Before Tonemapping"に変更します。

Applyボタンで変更を有効にするとブルブルとした震えが止まってくれます。

というわけで、今回は終了ですが、おまけとして今回作成したポストエフェクトのアセットを公開します。

https://dl.dropboxusercontent.com/u/39588440/UE4_PostProcess.zip

Contentsフォルダの下に直接おいてもらえればそのまま使えます。

Sobelフィルタと一緒に7x7のSNNフィルタも入れておきました。

SNNというフィルタについては手前みそですがもんしょの巣穴に簡単な解説が存在します。

https://sites.google.com/site/monshonosuana/directxno-hanashi-1/directx-111

最後に、SobelとSNNを有効にした場合のスクリーンショットです。

ue066.jpg