Atom Graphで画像処理をしてみる

前回紹介したnPass Element GraphはSubstance 3D Designerで言うところのFX-Mapのようなものでした。
個人的にはFX-Mapよりわかりやすくて好きです。

さて、SDにおける特殊なノードといえば、FX-MapとPixel Processorではないかと思います。
今回はInstaMATの中でPixel Processorに相当するAtom Graphを紹介します。
以前、SDでも実装したSymmetric Nearest NeighborをInstaMATでも実装して、実際の使い方を見ていきます。

まずはグラフ全体を見てみます。

さすがにグラフすべてを表示・解説できないので、作成したものをGitHub上にアップしています。

https://github.com/Monsho/InstaMATLibrary/blob/main/graphs/SymmetricNearestNeighbor.IMP

では、最初から作っていきます。

Atom Graphを新規作成し、テンプレートとしてDefaultを選択します。
すると以下のようなグラフが作られます。

Element Graphはノードごとに出力があり、その出力を繋げていくことで最終的な結果を得るものです。
SDや、UEのマテリアルなんかもこの手法です。
この手法は単純でわかりやすい部分はあるものの、特にループのようなフロー制御を実現するのが難しいという問題があります。

これに対してAtom GraphはUEのBlueprintに近い思想のツールと言えます。
Element Graphとの大きな違いは実行コネクタが有ることです。Blueprintにもありますよね?それと同じです。
上の画像にある[Entry]ノードが処理の開始、[Return]ノードが処理の終了です。
それらに存在する白のコネクタが実行コネクタで、[Entry]から繋がっていく順番で処理されていきます。

また、Atom Graphでは出力として[AtomOutputImage]と[AtomOutputImageGray]のどちらかしか選択できません。
つまり、カラーかグレースケールのイメージとしてのみ出力できます。
しかしAtom Graphで処理するのはカラーそのものであってイメージではありません。
そのため、[Write Pixel]ノードを使って処理中のピクセルに対しての書き込みを行います。
そしてその書き込み結果となるイメージを出力として渡すことになります。

上の図はそれらの一連の流れをデフォルトとして用意しています。
ユーザーはこの[Write Pixel]に入力するカラー情報を計算します。

次に入力イメージを設定し、そのイメージからテクセルを取得する方法についてです。
まず、グラフの入力として[AtomInputImage]を追加します。名前は適当でいいですが、今回は[Input]と名付けました。
そしてテクセルをサンプリングする命令をグラフに追加します。これには[Read Pixel]ノードを利用します。
このノードにはフィルタリング方法などでいくつかの手法が用意されていますが、今回追加するのは[Read Pixel (Nearest)]です。

入力としてはイメージ、テクセル座標、UV方向のリピート設定が用意されています。
リピートはどちらも[Clamp]にして繰り返しをしないようにします。
イメージにはグラフ入力として追加した変数を接続します。
テクセル座標は[Get Texel Coordinate]ノードを利用します。
このノードでは処理中のテクセル座標を取得できますが、取得される値は0.0~1.0のUV値です。[Read Pixel]に設定する座標も同様です。

最終的に[Read Pixel]の出力を[Write Pixel]に入力すると画像をそのまま出力するだけのものとなります。
グラフは以下のようになります。

実行コネクタも接続しましょう。
出力画像の結果を見たい場合、[Set Output]ノードを右クリックし、[画像'Output'を表示]を選択すると2Dペインに表示されます。
Element Graphと違い、Vキーのショートカットは対応していません。

例えば、取得したテクセル座標にVec2(0.5, 0.0)を加算してみましょう。

出力結果は中心から半分ズレた状態になります。ここからテクセル座標が0.0~1.0であることがわかりますね。
そのため、1ピクセルをずらしてサンプリングする場合、"1 / テクスチャサイズ"をずらす必要があります。
この1ピクセル分のテクセル座標を求めるには2つの方法があります。

1つはイメージから[Get Texel Size]を取得する方法です。
このノードは1テクセルのサイズであり、前述の計算で求められる値が得られます。

もう1つは[Get Image Size Vec2]でイメージサイズを取得し、[Vec2/Vec2]でコネクタ[A]を1.0にする方法です。
どちらの方法でも結果は同じなので好きな方を選んでください。

では、本題となるループについてです。
ループは以下のように利用します。

[Loop]ノードは実行コネクタの入出力と別に、[Condition]という入力と[LoopBody]という出力があります。
[Condition]はループを実行する条件で、条件がTrueの場合にループが継続します。
[LoopBody]はループ中に実行する処理です。

ループ回数の制御はローカル変数を利用します。
ローカル変数にint32の[X]を追加し、[LoopBody]の処理内で加算していきます。
[Condition]には[X]の比較処理を行います。上の画像では0~9までの10回ループです。
[X]の値は[Loop]の前で初期化しておきます。
ローカル変数の項目で初期化されていますが、変数を使いまわした場合は汚れているので、初期化のクセをつけておくことをおすすめします。

上の画像では[LoopBody]の中で[Read Pixel]の結果を[TotalColor]というローカル変数に加算しています。
10回ループなので、同じテクセルの値を10回加算しているだけということになります。
この結果は[Loop]の先の[Write Pixel]に入力します。
結果として、入力画像のカラーを10倍した結果が出力されます。

最後に、[LoopBody]の処理の終点として[Continue]ノードを利用しています。
C++などのループを知っていればわかるかと思いますが、[Continue]は1回のループ処理をそこで終了し、次のループ処理を行うフロー制御命令です。
プログラミング言語ではループブロックが明確になっている場合が多く、ブロックの終点処理は特に何もしなくても次のループに進みます。
しかし、InstaMATでは明確な終点ノードを設定する必要があります。
[Continue]は基本的なループ終点処理となります。
他にも、ループ自体を終了する[Break]、グラフ自体を終了する[Return]も選択可能です。

ここまでくればある程度のことが可能になります。
SNNフィルタは少し面倒なので、簡単なボックスブラーを実装して2重ループの使い方を見てみましょう。
まずは[Entry]から多重ループの部分。

外のループは[X]、中のループは[Y]をループカウントとします。
[Kernel]は入力パラメータで、ボックスブラーのカーネルサイズを指定します。
デフォルトの3を指定すると、-3~3の間でループするので、7x7のボックスブラーとなります。
外ループの前で[X]を初期化、中ループの前で[Y]を初期化します。

次に中ループの[LoopBody]処理です。

ローカル変数[X]と[Y]分だけずらした座標からテクセルを取得し、[TotalColor]に加算していきます。
前述したように、[Get Texel Size]で1ピクセル分のUV座標オフセットを取得できますので、[X], [Y]をそれぞれの要素に乗算すれば必要なオフセット値を得られます。
色の加算後に[Y]の値をインクリメントし、[Continue]で終点とします。 また、中ループの実行コネクタの先では[X]の値をインクリメントと[Continue]も必要です。

最後に[TotalColor]をサンプリングした回数で除算すればボックスブラーの結果を得られます。

[Kernel]の値を増やせば出力画像のぼやけが強くなるのがわかるはずです。
SNNフィルタはこれの応用のようなものなので、ループ部分はちょっと複雑に見えますが似たようなものです。

1つだけ解説しておくとすると、SNNでは中心ピクセルと対称ピクセルの色の距離を求める必要があります。
これをAtom Graph内で対応するのはちょっと面倒なのでFunction Graphを利用しています。
[ColorDistance]というグラフを作成し、中心カラーと対称カラーの2つの色を入力し、色の距離が近い方を選択するようにしています。
選択には[Branch]ノードを利用していますので、簡単な分岐のサンプルとして見てもいいと思います。
なお、色の距離としては輝度計算を行って、輝度が近いものを選んでいます。

[Test]グラフでは環境マップの画像に適用した結果を見ることができます。

絵画調っぽくなっていますね。
なお、環境マップはデフォルトでは線形となっていて、SNNフィルタに設定すると"色空間が違います"というような警告が出ます。
この場合、環境マップをsRGBに変更することで警告なく、色も元画像と同じようになります。

また、グラフノードはグラフ自体のサイズを継承することしかできず、入力イメージのサイズを継承できません。
この場合、ノードのパラメータで[要素の形式]内にある[絶対サイズ]を有効にしてサイズを指定することで対応できます。
もしくはグラフ自体のサイズを画像サイズに合わせましょう。

と、このようにInstaMATでは比較的わかりやすくループが利用可能です。
SDはループを考慮した初期実装になっていなかったため、ちょっとわかりにくい実装になってしまっているという印象です。
この辺り、後発ソフトの強みが出ているなと感じます。

ただし、注意が必要です。
SDのループには最大ループ回数が設定されていて、無限ループにならないような工夫がされていました。
これに対してInstaMATでは特にそのような機能が存在しません。
ただ、1重ループの場合は故意に無限ループにしても結果が正常にならないだけで停止したりはしませんでした。 しかし、2重ループで両方無限ループにするとOSを巻き込んでフリーズしました。
PCリセット案件となりますので注意しましょう。

nPassとMesh Rendererでメッシュをばらまいてみる

InstaMATは後発とはいえ、かなりいろいろな機能があって可能性を感じるアプリです。
今回はその中でも面白そうなnPass ElementとMesh Rendererを使った簡単なテストを行ってみます。

まず、新しいプロジェクトとしてnPass Element Graphを選択します。

テンプレートとしてScatterサンプルがありますが、今回はこちらを参考にしています。
ただ、作成するプロジェクトはテンプレートを選択しないようにしました。

作成したグラフにはこの2つのノードが最初から存在します。
nPass Element Graphはグラフを指定回数ループすることが可能なグラフで、この2つのノードはループ処理の初期化部分とループ結果の出力部分です。

[On Execute Assignment]はループの初期化部分で、nPass変数というパラメータ群を初期化することができるノードです。
nPass変数はグラフのループ内で使用される、いわゆるローカル変数です。
もちろんパラメータペインで編集もできますが、入力パラメータを利用して初期化することもできます。

[Did Execute Assignment]はループ処理の結果を出力し、次のループへ情報を渡すノードです。
[Pass Index]については自動的に加算されるようなので、コネクタは存在しますが特に何もしなくてもOKです。

次にグラフのパラメータペインで入力パラメータとして[Mesh]を、nPass変数として[Buffer]を追加します。
[Mesh]はElementMeshを選択、[Buffer]はElementImageGrayを選択します。
[Mesh]の初期値はデフォルトアセットにあるBevel Cube.fbxを設定しておきます。

また、今回はメッシュを描画する関係でパフォーマンスが厳しい場面も出てきます。
そのため、ループ回数はあまり大きくしないほうがいいでしょう。
nPass Element Graphのデフォルトループ回数は1ですが、最大値が1024となっていますので、32くらいまで最大値を下げておいたほうがいいです。
パラメータペインのλのボタン(緑枠)を押すと選択中のパラメータの詳細を設定できるようになります。
[Pass Count]パラメータを選択してからボタンを押し、最大値として32を設定しておきましょう。

では、メッシュをレンダリングしてみましょう。
入力パラメータの[Mesh]をグラフ内にD&Dし、これを[Mesh Render]ノードを追加して接続してみます。

すると、このようにメッシュが描画されます。
[Output]コネクタにはライティング結果が提供され、ベースカラーやマスク情報なども取得できます。
今回はテストなので高さ情報のみを取得します。

とりあえず[Mesh Render]ノードのパラメータを操作して回転やスケールなどを試してみてください。
何故かメッシュの回転は存在しないのですが、カメラの回転があるのでこちらで回転は対応できます。
また、今回はばらまいてテクスチャとして利用することを想定していますので、[Camera Type]パラメータを[Orthograpic](Orthographicのtypo?)に設定しておきます。

さて、メッシュをばらまくとして、ばらまいた複数のメッシュがすべて同じ回転だったらおかしいですよね。
ですので[Camera Orientation]パラメータを変更したいのですが、これをループごとに変更したいと思います。

まずnPass変数から[Pass Index]をD&Dしてグラフに追加します。
次に[Random Number Vec3]ノードを検索して追加します。
このノードの最小値を0.0、最大値を360.0にし、[Seed]コネクタに[Pass Index]を接続します。

これで360度以内の乱数値を得られるので、[Mesh Render]の[Camera Orientation]に接続します。
SDと違って、ノードの各種パラメータはコネクタで簡単に接続できます。
初期状態ではコネクタは表示されていないのですが、入力コネクタの[拡大する]ボタンを押すとすべてのコネクタが表示されますので、接続してください。
ただし、拡大したままだとかなりノードが長くなって邪魔ですので、必要なコネクタに接続した段階で[閉じる]ボタンで縮小しましょう。

次に[Wrap Transform]ノードに[Mesh Render]の[Depth]を接続します。
このノードは回転・拡縮・平行移動を行うノードで、Wrapとあるように上下左右でラップします。
ここで平行移動と拡縮を行います。
回転と同様に、ランダム値を生成します。拡縮はユニフォームスケールとしますので、乱数を1つだけ生成してVec2に拡張します。

さて、これだけでは1つのメッシュしか表示されませんので、ループを利用してメッシュを増やしていきます。
まず、[Blend]ノードを利用して[Wrap Transform]の結果と[Buffer]をブレンドします。[Buffer]はパラメータペインからD&Dします。
ブレンド方法で最大値を設定すると、[Buffer]の内容と[Wrap Transform]の結果の最大値が選択されるわけです。
しかし、[Buffer]には初期値しか入っていないので、このブレンド結果を[Did Execute Assignment]の[Buffer]コネクタに接続します。
これによって、次のループでの[Buffer]の値が今回の処理の結果として渡されるわけです。
次のループのそのまた次のループは、今回と次回のループの結果をブレンドしたものになるわけで、そのまた次はさらに…ということになります。 では、[Pass Count]パラメータを適当に増やしてみましょう。

見ての通り、描画されたメッシュの[Pass Count]の分だけ配置されていることがわかります。
最後に出力パラメータを追加し、[Blend]の結果を接続すれば、ループの最後の結果を出力として利用することができるようになります。
最終的なグラフは以下のようになります。

このグラフを別のグラフから呼び出し、高さ情報からノーマルでも出力してみましょう。

コリジョンを取っているわけではないのでめり込みは発生していますが、想定されているメッシュばらまきからの高さ出力、ノーマル計算までできました。
コインを敷き詰めた床、ドクロの山のようなマテリアルはSDでも作っている方がいましたが、かなり高度なテクニックが必要になります。
この手法であればメッシュさえあれば簡単にできますし、メッシュのベースカラーなども設定すれば各種情報も得られるでしょう。
SDではこのようなことはできず、やるのであればHoudiniとか使うところではあるのですが、Houdiniを使うにはこのくらいだと少し大掛かりのようにも感じます。
その点でInstaMATは非常に扱いやすいツールだと思います。

InstaMAT事始め

現在のCG業界では物理ベースレンダリング(PBR)が主流となり、そのためのテクスチャを編集するツールとしてSubstance 3D製品が大きなシェアを持つようになりました。
Photoshopのような画像編集ソフトでは複数の関連する画像を編集しづらく、また、実際の3Dオブジェクトをチェックしながら編集することが難しいです。
Substance 3D製品はその点、Substance 3D Painterで3Dメッシュに対するテクスチャリングができますし、Substance 3D DesignerやSamplerを使うことでタイリングテクスチャの生成もできます。
他にも似たようなツールがないわけではないのですが、Substance 3Dほど気軽に、かつ網羅的に使えるツールは他にありませんでした。

そんな中に登場したのがドイツにあるAbstract社がリリースした『InstaMAT』です。
この会社は『InstaLOD』という3Dモデルのリダクションツールもリリースしています。いわゆる『Symplygon』の競合ツールですね。

InstaMATはSubstance 3DのPainter / Designer / Samplerを合わせたようなツールです。
Substance 3Dではこれらの3つのアプリはそれぞれ別々のアプリとなっています。
これに対してInstaMATは同じアプリ内でこれらの機能を利用することができます。
と言ってもやはり役割が違うため、それぞれの機能ごとにUIの差があります。
これは場合によっては混乱するかもしれませんが、1つのパッケージ内にグラフのプロジェクト、ペイントのプロジェクトといったものをまとめておくことも可能です。
最近リリースされたばかりのツールではありますが、かなり機能は充実していて実戦投入も可能だろうと考えます。

というわけで、今回はInstaMATの紹介として、簡単な操作案内とElement Graphを用いたタイルマテリアルの作成をやってみます。

まずはアプリを立ち上げ、新規プロジェクトを作成してみます。
使用するバージョンは1.5で、ちょうど昨日アップデートしたようです。このバージョンで日本語対応もされました。
新規プロジェクト作成時は以下のようなウィンドウが表示されます。

LayeringはPainter、Material LayeringとMaterialize ImageはSampler、そこからしたがDesignerといった感じです。
今回は普通のDesignerと同等のElement Graphを利用します。

テンプレートを選択します。
とりあえずPBRマテリアルを作成する場合はPBRテンプレートを選択したほうがいいです。
今回はテンプレートを使用せず、空の状態からスタートします。

ノードも何も無いシンプルな画面ですが、この状態だとマテリアルの確認ができません。
左上の赤枠で示す3Dキューブをクリックします。これで左側に3Dペインが表示されます。
SDと違って、3Dウィンドウの操作にはALTキーが必要になります。SubstanceでもPainterなんかは必要なのですが、SDばかり使ってる人間としてはちょっと面倒です。

まず適当なノードを配置してみます。
真ん中のグラフペインでスペースキー、もしくはマウス右クリックをします。
検索ウィンドウが表示されますので、[Solid Color]と入力してノードを選択します。SDと同様、スペースキーも名称に含まれるので注意が必要です。
右のノードパラメータペインで適当な色を選択します。[Color]パラメータを右クリックすればカラーパレットが表示されます。
このノードの[Output]コネクタをクリックするか、ノードを選択してVキーを押すと左下の2Dペインに画像が表示されます。
また、ノードを右クリックして[マテリアルチャンネルとしてプレビュー]→[Output]→[BaseColor]と選択して3Dペインへ反映させることができます。

SDと違ってノード自体をダブルクリックしてしまうとノードの実装にジャンプしてしまうので注意しましょう。
手癖でダブルクリックしてしまって開くつもりがなかったノード実装を開いてしまうことがどうしても多いです。
また、グラフエディタの操作も結構SDと違うので、慣れるまでは戸惑うと思います。というか、全然慣れないです。

さて、いちいち右クリックして出力先を選ぶのは面倒なので、出力ノードを作成してしまいましょう。
SDではOutputノードを配置していましたが、InstaMATではグラフのパラメータとして出力を作成することでOutputノードが生まれます。
グラフエディタでノードのない場所をクリックし、右のパラメータペインで出力を追加します。

出力右の+ボタンを押して、型を選択して追加します。
出力の型は[ElementImage]か[ElementImageGray]にします。これはイメージ情報として出力することを意味します。
BaseColorとNormalはElementImage、RoughnessとMetalnessとHeightはElementImageGrayにします。 追加された段階では"Parameter"というパラメータ名になっていますが、これを適切な出力名に変更する必要があります。
名前をダブルクリック、もしくはF2キーでリネームします。このとき、マテリアルパラメータとして登録されているものは検索可能です。
BaseColorであればbと入力した段階でBaseColorが候補に出てきますので、選択すればOKです。
しかもマテリアルパラメータの場合はパラメータ名右側のカテゴリー名も自動的にMaterialに変更されるので便利です。
とりあえず必要そうなパラメータを追加し、グラフエディタでノードを整頓しましょう。
追加した出力ノードをまとめて選択、右クリックからの[コメント]を選択するとSDのフレームと同等のものを追加できます。

それではタイルを作っていきましょう。
まずは以下のようにノードを追加、接続します。

[Tile]ノードは入力イメージをタイル上に配置するノードで、SDでも同様のものがありますね。
入力イメージとしては[Rounded Box]を利用します。サイズやタイルの数は適当に調整します。

この段階ではノードは高さ情報ですので、カラーではなくグレースケールとして設定します。
SDではアトミックノード以外はカラーとグレースケールで別々のノードとなっていましたが、InstaMATはノードに切り替えスイッチが存在しています。
パラメータペインの[インスタンスのプロパティ]カテゴリーの[グレースケール]スイッチを入れることでグレースケールに変更できます。便利。

また、[Tile]ノードは[Flood Fill]スイッチを入れることでタイルのFlood Fill情報を出力してくれます。
これを利用すれば[Flood Fill]ノードを省略することができます。
そして[Flood Fill to Gradient]ノードでランダムな傾斜を求め、[Blend]ノードで乗算して少し傾斜をつけます。
最後に[Height to Normal]ノードでノーマルを生成してノーマルの出力ノードに接続します。
これで3Dペインにノーマルが反映されればOKです。

ここから表面の調整、クラックの作成を行います。

表面のノイズは適当に作成して適用しますが、特に難しいことはしていません。
[Histogram Edit]ノードはSDの[Histogram Scan]とほぼ同等だと思えばOKです。

クラックは[Voronoi]ノードからスタートです。SDのセルノイズ的なもので、[Cracks]コネクタでエッジ部分を取得できます。
[Slope Blur]はSDでも存在しますが、パラメータや挙動がどうもSDと違っていて調整が難しい印象があります。
クラックのズレやマスクは[Flood Fill to Color]と[Directional Warp]を適当に利用しています。

タイルの色はタイルごとのグレースケールを利用して[Gradient Map]ガチャを使います。

SDにあるデスクトップの画面のどこからでもカラーを取得できる機能がこちらにもあります。
ただ、ポイントとして利用できるのは16点までのようで、これを超えるポイント数になると[Gradient Map Dynamic]に自動的にコンバートされます。

後は適当にRoughnessとAOを調整して、できたのがこちら。

短時間で雑に作ったものなのでクオリティは低いですが、SDでできることは結構できるのではないかと思います。
しかし、慣れの問題もあってSDの方が楽に作れるというのは間違いなくあります。

ただ、InstaMATはメッシュをノードとして利用することができるというのが面白いです。
FBXファイルをD&Dでノードとして取り込むことができ、そこから[Mesh Bake]ノードでメッシュノーマルやAOといった情報を作成できます。

SDのグラフでは、ベイク自体は外(Painterなど)で行い、その結果を入力として受け取るという方式ですが、InstaMATではメッシュそのものを入力とすることができ、そこからベイク→ベイクされたマップの利用というところまで可能です。
ベイクの精度がどの程度かは不明ですが、InstaLODで培われた技術が流用されているものと考えられるのでそれなりのクオリティは出るのではないでしょうか?
メッシュを利用した処理は他にもたくさんあるので、少しずつでも検証していこうかと思っています。

これからも検証進めて、面白い機能やらがあったら記事にしていこうと思います。

While Loopを利用したSymmetric Nearest Neighborフィルタ (Substance 3D Designer 13.0)

久しぶりのブログ更新

FunctionグラフのWhile Loop

つい先日のバージョンアップで待望の[While Loop]ノードが追加されました。
今はFunctionグラフでしか使用できませんが、個人的にはかなり大きな追加要素だと思っています。
今回の更新ではこのノードの使い方を解説し、ついでに以前作成したSymmetric Nearest Neighbor (SNN) フィルタを[While Loop]ノードを使って実装してみます。

While Loop解説

利用法

[While Loop]は前述の通り、Functionグラフノードとして提供されています。

入力コネクタは3つ、出力コネクタは1つです。
[Init]入力コネクタは[While Loop]に入る前の処理です。ループを行う前の初期化などを接続します。
[Exit Cond.]入力コネクタは[While Loop]を抜ける条件を指定します。C++のwhile命令はループを継続する条件を指定しますが、こちらは逆を指定する点に注意が必要です。
[Loop Body]は実際のループ処理そのものです。ここに接続した処理が指定条件を満たすまでループされます。
また、パラメータとして[Max Iterations]が存在します。これは無限ループを抑制するためのパラメータで、[Exit Cond.]が必ずfalseになるような条件であっても、このパラメータで指定された回数以上はループが回らないようになっています。

では実際に使ってみましょう。
わかりやすさ重視のため、[Value Processor]をグラフに配置し、[Edit]ボタンを押してFunctionグラフ内に入ります。
そして以下のようにノードを組んでみましょう。

[While Loop]を利用する際、一時変数を設定する[Set]ノードは非常に重要となります。
[Exit Cond.]の条件式内で一時変数を取得し、条件を調べたりする必要が出てくるためです。

まず、[Init]ノードには[Set]ノードで変数iを0で初期化します。
終了条件の[Exit Cond.]は[Get Float]ノードでiを取得し、この値が10以上になったら終了するようにしています。 [Loop Body]はiを取得して1を加算、それをiに再度設定します。つまりインクリメントしているだけです。
[While Loop]の唯一の出力は[Loop Body]の出力となりますので、これをグラフの出力として設定すると結果は最後にiに設定された値、つまりループ終了条件の10という数値になります。
実際に[Value Processor]の結果は"10"となっているはずです。

さて、次はもう少し複雑な処理を行いましょう。1~10までの整数を加算した合計を求めるだけのグラフです。
C/C++Pythonなどを使えば簡単に作れる処理ですが、グラフで再現しようとするとやや面倒です。
答えは以下のようになります。

ここで大切なのは[Sequence]ノードです。
このノードは複数の処理を別個に行うためのノードで、入力コネクタの[In]に最初の処理、[Last]に次の処理を接続します。出力は[Last]の入力結果となります。
Substance 3D DesignerではUEのBlueprintのような、実行コネクタは存在しません。
[Set]ノードを見るとわかりやすいですが、入力は設定する数値、出力は入力に接続した結果をそのまま返します。
ここにさらに[Set]ノードを配置しても、別の変数に同じ数値を入れることしかできず、別の数値を設定することができません。
そのため、[Init]コネクタに接続しているように、2つの[Set]ノードを別々に用意し、[Sequence]ノードで接続する必要があります。

この処理は[Loop Body]でも同様です。
ループで実行する処理は変数viを加算する処理(つまり v += i)と、ループカウントであるiをインクリメントする処理の2つです。
どちらも最終的に[Set]ノードで変数を設定するため、[Init]と同様に[Sequence]を利用する必要があります。

また、[While Loop]の先でも[Sequence]を利用します。
[While Loop]の出力は[Loop Body]への入力で、これはその前の[Sequence]の[Last]ですので、当然変数iの値となります。
最終的に欲しいのはvの値ですので、[Sequence]の[In]に[While Loop]の出力を接続し、[Last]で変数vを取得して接続します。

この結果は"55"となります。[Value Processor]の結果を確認してみてください。

注意

[While Loop]を使用する場合、[Loop Body]に接続したノードとその他の処理で利用するノードの共有に注意が必要です。
どうやら[Loop Body]と[Exit Cond.]は独立したブロックとして処理されなければならないようで、別の部分で使用しているノードを利用すると正常に処理されないようなのです。

以下の画像は先程の処理ですが、①と②と書かれたノードに注目してください。

①は定数である1を設定しているノードで、②は変数vを取得するノードです。
パッと見ではどちらも共有することが可能に見えますので、どちらか一方を削除してもう一方に接続してみてください。
①と②のどちらかだけでも共有してしまうと、[Value Processor]の結果は"0"となってしまいます。
どうやら処理が正常に実行されず、必ず"0"が入ってしまうようです。

ただし、[Loop Body]と[Exit Cond.]は同じブロック扱いのようで、[Exit Cond.]に接続している変数iを取得するノードを[Loop Body]で共有しても問題なく動作しました。 しかしながら、混乱の元になるのできちんとブロック分けはしておくことをおすすめします。

do whileを実現する

[Loop Body]と[Exit Cond.]が同一ブロック扱いなのであれば、[Loop Body]の内容を[Exit Cond.]で処理しても問題ないのでは?
と思ったのでやってみました。

これでも正常動作しました。
[While Loop]ノードは[Init]→[Exit Cond.]→[Loop Body]→[Exit Cond.]→[Loop Body]→...→ループ後処理という順序で処理をしているようです。
ですので、このような接続方法にすることで、C/C++のdo while的な処理も可能ということになります。
ただし、[Loop Body]に何も接続していないとループが回らないようですので、害のないノードを接続しておく必要はあります。

しかし、現在の仕様では可能な処理というだけで、今後も問題なく利用できるかどうかは未知数です。
通常であれば[Loop Body]内で繰り返し処理を行うべきなので、どうしてもという場合以外は使用しないことをおすすめします。

実例 Symmetric Nearest Neighbor

というわけで本題です。
Symmetric Nearest Neighbor (SNN) は絵画っぽい画像フィルタです。
解説とHLSL実装についてはもんしょの巣穴を参照してください。

sites.google.com

このフィルタは以前もSDで作成したのですが、ループ処理がなかったため5x5、7x7、9x9のカーネルサイズで頑張って1つ1つ丁寧に実装していました。
当時作成した9x9の[Pixel Processor]は以下です。

ノード数はそんなに多くないように見えますが、定数を変更して接続して…を繰り返して発狂しそうになりましたね。
しかもカーネルサイズが変更できないですし、これ以上のカーネルサイズは心が折れて実装できませんでした。

では、ループを使って実装したものを見てみましょう。

ノード数はそれなりに多く見えますが、処理の流れも接続もわかりやすいかと思います。
なにより、これだけでカーネルサイズを16まで変更できるようになっています。
なお、カーネルサイズは片側のサイズで指定する形ですので、1を指定すると3x3、16なら33x33で処理します。

[While Loop]は3つ利用しています。これは元のHLSL実装でもforループを3つ使っているためです。
リアルタイム実装でなければ2重ループで全ピクセルに対して処理してもさほど問題ないとは思いますが、慣れるために元実装と似たような処理にしています。
3つの内2つは2重ループで使用しています。2重ループは分かりにくくなりやすいので注意が必要だと思います。
解説を…と思ったのですが、特に解説するところがないので、気になる方は以下からダウンロードして試してみてください。

www.dropbox.com

ご自由にお使いください。
Substance 3D Designerの教科書』で紹介しているKuwaharaフィルタも同様に作成することができるでしょう。
興味のある方は練習がてらチャレンジしてみてもいいのではないでしょうか?

UE4用NVIDIA Image Scalingプラグイン実装について

UE4 Advent Calendar 2021 12/5の記事です。

はじめに

PS4/XboxOne世代は1080p出力が、PS5/XboxSeriesX世代は4K出力が基本になっています。
しかし、大体の場合でレンダリング解像度はそれより低い解像度で行い、ハードウェアのアップスケーラーを利用するか、もしくは何らかのアップスケール技術を使うのが割と基本です。
残念ながら、これからもその状況はあまり変わらないでしょう。

そうなってくると、どのようなアップスケール技術を使うか?が問題になってきます。
現在、アップスケール技術としてよく取り沙汰されるのはNVIDIA社のDeep Learning Super Sampling (DLSS)です。
名前の通り、Deep Learning技術を利用した超解像技術で、Temporal Reprojectionも利用するためアンチエイリアシングの機能も兼ね備えているようです。
UE4でいうならTemporal AA Upsampling (TAAU)の代替手段になります。
ただし、この技術はGeforce RTX以上を必要とするため、ハードウェアが限定されます。
当然、コンソールゲーム機には使用できません。

そのような状況で新しく現れたのがAMD社のFidelityFX Super Resolution (FSR)です。
こちらはDLSSと違い、ハードウェアを限定しません。Shader Model 5.0?くらいが使用できれば使えます。
これは昨今のPCならほぼ全て使えますし、PS4以降のハードでも使用が可能です。
しかもUE4.27.1用ではありますが、すでにプラグインとして公開もされています。

https://gpuopen.com/learn/ue4-fsr/

また、FSRは入力として低解像度の最終レンダリング画像のみを求めます。
アンチエイリアシングは別途行う必要がありますが、FSR自体にゴースティングの心配はありません。

UE5ではTemporal Super Resolution (TSR)が追加されています。
こちらはUE4への移植も可能ではありますが、一応PS5/XboxSeriesX世代以降対応ということになっています。
PS4世代で動作するかは不明です。
この技術はTAAUの代替手段ですので、アンチエイリアシング機能も含まれます。

さて、つい先月、DLSSのバージョンが2.3になりました。
しかしそれと同時に、新しい超解像技術も発表されました。
それがNVIDIA Image Scaling (NIS)です。

https://github.com/NVIDIAGameWorks/NVIDIAImageScaling

このアップスケーラーもFSRと同様に、最終出力のみを受け取りアップスケールします。
アンチエイリアシングは行いませんが、やはりNIS自体にゴースティングは発生しません。
加えてコンピュートシェーダによる実装であり、Shader Model 5.1以上に対応していれば利用可能です。
今どきのPCならどんなGPUでも使えますし、PS4世代でも使えるはずです。

残念なことに、まだNISUE4プラグインは提供されていません。
発表されたばかりだからということもあるかと思いますが、そのうち出てくるんじゃないかとは思います。

プラグインを作った話

が、そのうち出てくるだろうと思っていまテストしてみたい!FSRと比較したい!
そう思ったので作ってみました。

https://github.com/Monsho/NISPluginForUE4

一応ライセンスはMITにしています。NIS SDKがMITライセンスなので。
インストールについてですが、Readmeを読んでください。
残念ですが、ビルド済みバイナリは含めていませんので、エンジンに突っ込んでビルドしてください。
プラグインの追加だけなのでエンジンコード自体は汚しません。

なお、UE4.27.1以上対象のプラグインです。
UE4.26以下では利用できません。UE4.27.0なら使えるかも。
というのも、FSRプラグインを参考にして作ったのですが、そこで使用されている機能がUE4.27で追加された機能なので。

以降ではその辺の機能や、プラグイン作成時の注意点について簡単に解説します。

プラグイン解説

ISpatialUpscaler

実は、FSRUE4.26から対応していたのですが、その頃はエンジンコードを変更するパッチとして提供されていました。
UE4.26ではTAAをサードパーティ製に置き換える手段は用意されているのですが、FSRは最終段のPrimary、もしくはSecondaryのUpscalerを置き換えるものなのでTAAの置換手段は利用できません。
なお、DLSSはこのTAAを置換する手段を用いてプラグインを実装しています。

しかしFSRプラグイン実装のためなのか、UE4.27ではこのPrimary、SecondaryのUpscalerを置換する手段が用意されました。
これを利用するにはISpatialUpscalerを継承したクラスを作成し、FSceneViewFamily::SetPrimarySpatialUpscalerInterface()、もしくはFSceneViewFamily::SetSecondarySpatialUpscalerInterface()で登録する必要があります。
これが登録されているとデフォルトのアップスケーラーの代わりに、プラグインで実装したアップスケーラー、つまりFSRNISを実行できるという仕組みです。

FSceneViewExtensionBase

ここで注意点。
FSceneViewFamilyは基本的に毎フレーム生成・削除が行われるようです。
そのため、ISpatialUpscalerを継承したクラスは毎フレーム登録する必要があります。
そこでFSceneViewExtensionBaseを利用します。

このクラスを継承したクラスは生成時に自動的にエンジンが持っているViewExtensionsに登録されます。
ViewExtensionsに登録されているクラスは、ViewFamilyのセットアップ時や描画の開始・終了時にViewFamilyに対して追加処理を行うことができます。
NISプラグインでは、FNISViewExtensions::BeginRenderViewFamily()関数が描画開始時に実行される関数で、ここでPrimaty UpscalerにFNISUpscalerを登録しています。

登録する場合は常にnewする必要があります。
FSceneViewFamilyが削除されるタイミングで登録したUpscalerも勝手に削除されるためです。
寿命管理はこちらで制限できなので、TSharedPtrなどで作成してポインタだけ登録する、というような手法は使えないと考えてください。

FNISUpscaler

実際にNISを実行しているのはこのクラスです。
FNISUpscaler::AddPasses()関数でNISを実行するパスを追加しています。

ここでもまた注意点があります。
FSRはコンピュートシェーダでもピクセルシェーダでも実装することができるのですが、NISはコンピュートシェーダのみの実装となっています。
つまり、NISの出力テクスチャはUAVでなければならないのですが、最終出力となるPassInputs.OverrideOutputはUAVとして使用できません。
そのため、NISプラグインではUAVとして使用できる2Dテクスチャを作成し、ここにNISレンダリングしてから最終出力にコピーするという手段を用いています。
ピクセルシェーダでも実装できるなら良かったのですが、NISはLDSを使用してるっぽいので無理なんですよね…

また、NISでは係数を保存したテクスチャを渡す必要があります。
この係数はNISが用意しているもので、入力画像によって変化するものではありません。
そのため最初に生成するだけでよいのですが、FNISUpscalerは毎フレーム削除されてしまうので、コンストラクタで生成・デストラクタで破棄とやるのは大変無駄です。
そこで、コンストラクタで外部から渡すという手法を用いているのですが、ここでちょっと面倒なことが…
この理由も含めて次節で解説します。

2つのモジュール

NISプラグインImageScalingImageScalingExtensionという2つのモジュールから成り立っています。
FSRも同様の仕様なのですが、わざわざ2つのモジュールに分けているのは初期化タイミングの問題のためです。

まず、FNISUpscalerはグローバルシェーダを利用するため、独自のシェーダを利用できるように登録する必要があります。
そのためにモジュール開始時にシェーダのディレクトリを登録することになるのですが、この場合のモジュールはPostConfigInitで実行されなければなりません。
このタイミングはエンジン初期化より前のタイミングであるため、この設定になっているモジュールの初期化段階ではエンジンの機能を利用することができないのです。

エンジン初期化の前にできないこととは何かというと、それはViewExtensionの登録とテクスチャの作成です。
FNISViewExtensionを作成してしまうと自動的に登録されてしまうわけで、その登録先はエンジンなわけです。
テクスチャ生成もRHIなどを利用するため、エンジンが初期化されていなければ実行できません。
グローバルシェーダを使うためにはPostConfigInitでなければならないが、ViewExtensionを生成するのはPostEngineInitでなければならないのです。

そのためにViewExtensionを生成するためのモジュールが必要になります。それがImageScalingモジュールというわけです。
テクスチャ生成もこのモジュール開始時に行い、FNISViewExtensionに登録しています。
FNISUpscalerが利用するテクスチャはさらにFNISViewExtensionから渡されるという形になっています。
面倒ではありますが、こうする以外の方法を見出すことができませんでした。

使い方の注意

NISプラグインを使う場合の注意点として、FSRとの併用はできないという点に注意してください。
プラグインを両方とも有効にすることはできますが、その状態でFSRNISを有効にするとISpatialUpscalerの登録時にチェックで引っかかります。
チェックを無視すれば先に進めることはできますが、多分メモリリークとなります。

FSRはデフォルトで有効になってしまうので、NISはデフォルトで無効にしています。
NISを利用する場合はまずFSRを無効にしてから(r.FidelityFX.FSR.Enabled = 0)、NISを有効にしましょう(r.NVIDIA.NIS.Enabled = 1)。 その逆もまた同じです。無効にしてから有効にする。これを守りましょう。

もう1つ。これはFSRでも同様ですが、テクスチャのミップレベル調整は自動的に行われません。
r.ScreenPercentageで設定したパーセンテージに合わせて、r.MipMapLODBiasの値を調整しましょう。

比較

1440pネイティブ f:id:monsho:20211205022850p:plain

720p->1440p デフォルトアップスケーラー f:id:monsho:20211205022946p:plain

720p->1440p FSR f:id:monsho:20211205023317p:plain

720p->1440p NIS f:id:monsho:20211205023241p:plain

さいごに

このプラグインは現段階ではWindowsPCのD3D12バージョンでしか動作を確認していません。
他の環境で動作するかという点については不明です。
多分動くんじゃないかとは思うのですが、シェーダコンパイルで失敗するとかの可能性はあると思います。
そうなった場合はバグを修正してプルリクでも投げてください。

というわけで今年のAdvent Calendar記事はこれにて終了!
前回の更新も去年のアドカレだったんだなぁ…

NVIDIA RTXブランチについて

Unreal Engine 4 アドベントカレンダーその1 2日目

はじめに

今年、Ray Tracing Night Week 2020にてUE4レイトレのアップデートについて講演を行いました。

UE4.25のレイトレーシングで出来ること/出来ないこと

この中で謎の半導体メーカーであるNVIDIA様のRTXブランチについて言及したところ、これについての質問が寄せられました。
質疑応答の際に答えはしたのですが、動画内で言葉だけで説明したのだとわかりにくいですし機能についてもよくわからないと思いますので、ブログにて簡単ですが紹介させていただこうと思った次第です。

この記事はRTXブランチに興味がある方向けのものですが、エンジンビルドについては言及しません。
エンジンビルドについて知りたい方は株式会社ヒストリア様の以下の記事を参考にしてください。

historia.co.jp

また、あくまでもRTXブランチ紹介記事ですので、以下のような方は対象としていません。

  • RTXブランチをすでに使っている方
  • NVIDIA社の中の人

ソースコードのダウンロード

RTXブランチのソースコードGitHubから行います。URLは以下です。

https://github.com/NvPhysX/UnrealEngine/tree/RTX-4.25

ブランチは他にもいくつかあり、RTX以外のブランチも多く存在していますが、今回はそちらの解説は行いません。
URLにアクセスしても見られないという方はGitHubのアカウントを作成し、Epic Games様のUE4ソースコードにアクセスできるようにしましょう。
ダウンロードからビルドの流れは前述の記事の通りです。

現在はUE4.25までしか対応していませんが、UE4.26が正式版になるとそちらへのマージ、対応が行われる可能性があります。

機能について

このブランチの目的はNVIDIA様がレイトレーシングの様々な機能のテスト、パフォーマンス調整を行うものと思われます。
ここで実装された機能がUE4本流にマージされている例もあります。 機能についての簡単な解説はREADME.mdファイルに記述されているので、とりあえずどんな機能があるかを知るだけならGitHubのWebページでも見ることが出来ます。

以下で各機能について解説を行いますが、一部の機能については少し詳しく解説を加えています。

Direct Optimizations

出力結果に変化がない、特にデメリットのない最適化です。
必ず高速化するわけではありませんが、使用することで映像に変化があるということは基本的にありません。

バウンス回数の固定

UE4のリフレクションではマルチバウンスに対応しており、バウンス回数は変更可能です。
リフレクションシェーダは内部にバウンス回数に対する動的なループを持っていますが、動的なループは基本的に負荷が高くなります。
シェーダコンパイル時にループ回数が固定できると動的ループが展開され、一般的には高速化します。
この修正はそのループ回数を固定してシェーダコンパイルが可能な修正ですが、現在はバウンス回数が1回のときのみ固定されます。

半透明マスキング

UE4のレイトレ半透明は画面全体にレイトレースを行いますが、半透明マテリアルにレイが衝突しないピクセルでもレイトレースします。
これは無駄なので、一旦半透明メッシュをステンシルバッファに描画し、ステンシルでマークされた部分以外はレイトレースをキャンセルします。
画面全体を半透明メッシュが覆うような状況では高速化しない可能性がありそうですが、r.RayTracing.Translucency.MaskのCVarでON/OFFを切り替えることができるので、これを使ってパフォーマンス計測しましょう。

インスタンススタティックメッシュ(ISM)のカリング最適化

FoliageのISMのカリングを最適化しています。
ISMカリングのCPU負荷が高い状況では有効な機能です。

動的メッシュのバッチ作成の並列化

レイトレ界に登録する動的メッシュは静的メッシュとは違った処理を行う必要があります。
そのためのコマンドを発行する部分は通常では並列化されていませんが、大量に動的メッシュがある場合に不利になります。
r.RayTracing.ParallelMeshBatchSetupのCVarをONにすることで動的メッシュの処理を並列化出来ます。

マテリアルバインドの高速化

r.RayTracing.BatchMaterialsでON/OFFできる機能で、マテリアルバインド処理を単純化することで高速化するもののようです。
バインドするマテリアル数が多い状況で高速化するのかな?

Quality Tradeoffs

これらの最適化は映像クオリティとのトレードオフを要求します。
コメントでは違いを見つけるのは困難な程度のしきい値で設定されていると書かれています。

シャドウレイのシザリング

ポイントライトなどの効果範囲が限定されるライトでは画面全体をレイトレースせずに効果範囲のみをレンダリングするほうが効果的です。
どうやらこの機能はレイトレースシャドウを効果範囲でシザリングする機能を提供しているようです。
単純なレイトレースシャドウであればトレードオフはありませんが、デノイザーが絡むとシザーボックス境界付近に問題が出ることがあります。

影を落とすライトの優先度付け

r.RayTracing.Shadow.MaxLightsr.RayTracing.Shadow.MaxDenoisedLightsがCVarに追加されます。
この指定によってシャドウを落とすライトの数を制限することが可能です。
制限されるとライトを距離や範囲で優先度付けして、優先度の高いものから順に制限数までシャドウを落とすようにします。
この設定はMovableライトには適用されず、Static, Stationaryライトに適用されます。

また、r.RayTracing.Shadow.FallbackToSharpを利用すると、エリアライトで本来ならデノイズが必要なレイトレースシャドウをシャープな影にしてデノイズを避けるようです。

ライトの優先度付け

リフレクションやGIではレイトレースしてヒットしたマテリアルに対してライティング計算を行います。
この際に影響するライトの数はエンジン側で256個で制限されていますが、シーン中にはそれを超えるライトの配置が可能です。
配置されているライト数が256を超える場合、デフォルトでは頭から順に256個を利用するようにしますが、この機能を使うことでライトに優先度を付けて上位から選択するようにします。
r.RayTracing.Lighting.MaxLightsは登録するライトの個数を指定します。優先度付けしたライトを指定個数まで登録するようになります。
r.RayTracing.Lighting.MaxShadowLightsはリフレクションなどのライト計算時にシャドウレイを飛ばすライトの個数を制限します。
これらの設定は単純なカリング処理となるので、大量にライトが配置されている場所ではポッピングするので注意してください。

f:id:monsho:20201122102039p:plain

ライトの優先度付けルールはr.RayTracing.Lighting.Priority.~である程度の調整ができますので、ゲームに合わせて対応すると良いでしょう。
例えば屋外の場合はフラスタムを重視するほうが良さそうですが、屋内の狭い部屋ならカメラ周辺やカメラ後方を重要視したほうがいいかもしれません。

ラフネスへの乗算値

ラフネスが高いと反射レイは様々な方向に飛んでノイズが大きくなります。するとデノイズでは間に合わなくなり、もやもやしたノイズが残ることになります。
ラフネス値をマテリアルごとに調整する事もできますが、大変ですしラスタライザでのライティング結果にも影響を及ぼします。
r.RayTracing.Reflections.RoughnessMultiplierを利用すると、レイトレリフレクションのときのみラフネス値に補正をかけることが出来ます。
r.RayTracing.Reflections.MaxRoughnessと違ってレイトレをキャンセルすることはありませんが、ノイズの調整には扱いやすいと思います。

Enhanced Features

UE4本流にはない機能を追加しています。
あなたのアプリケーションに必要な機能が見つかるかもしれません。
見つけてしまった場合、Epicさんにマージ要求するか、自前でマージしましょう。

Hybrid Translucency

Ray Tracing Night Week 2020でも言及した機能で、デフォルトのレイトレ半透明の代替として使用することが出来ます。

デフォルトのレイトレ半透明は半透明に対する反射と屈折を1つのレイトレシェーダ内で別のレイトレースで実現します。
この手法は高品位ではあるものの、処理負荷が高く、その上半透明が多重に重なっている場合に問題が発生することがあります。

Hybrid Translucencyは反射のみをレイトレで対応し、屈折についてはこれまでどおりのラスタライザで対応します。
反射は別バッファにレンダリングすることで対応しますが、複数の重なりに対してはレイヤー数を増やしてカバーします。
レイヤー数が少なければデフォルトのレイトレ半透明より負荷が軽くなる可能性が高く、また、半透明の重なりによるレンダリングエラーにも対応しやすいので映像作品よりゲーム作品向けと言えます。

詳しくはshikihuikuさんの記事をご覧ください。

UE4 RTX-4.23ブランチのHybrid Translucencyは何をしているのかshikihuiku.wordpress.com

ライトファンクション、ライトチャンネル

UE4本流では対応していないライトファンクションとライトチャンネルに対応する修正です。
これらの機能を多用しているアプリケーションではマージを検討したほうが良いでしょう。
特にライトチャンネルはパフォーマンスが上がることもあります。

余談ですが、UE4本流では使用されていないCallable Shaderがライトファンクションでは使われています。
自分も使ったことがなかった機能なのでちょっと感動したけど、PSやCSでもCallable Shader使いたい…

Cast Static Shadows/Cast Dynamic Shadows対応

デフォルトではリフレクションのシャドウキャスト設定はCast Shadowsフラグだけをチェックしていますが、RTXブランチではCast Static Shadows/Cast Dynamic Shadowsにも対応しています。
ただ、フラグに対応しているというだけで、静的/動的なメッシュにきちんと対応するわけではなく、この2つのフラグが落ちていたらシャドウを落とさない、という形になるだけです。

非対称ScreenPercentage

レイトレリフレクション/GIではScreenPercentage設定が存在しますが、この設定は細かく調整されているわけではなく100%(デフォルト)の後は50%、25%という順番で下がっていきます。
つまり、縦横が1/2、1/4という形でしか下がっていきません。

この修正では縦横比が変わる形での調整ができるようになっていて、70%では横だけが1/2になります。
50%だとどうしても汚くて困る、という場合に試してみると良いかもしれません。

Subsurfaceのバックフェースライトカリング

UE4本流では一部のシェーディングモデルにおいて、ライトの方向と法線方向が水平より下(つまり背面)になっている場合にシャドウレイの発射をキャンセルする機能があります。
DefaultLitでは特に問題ないようですが、Subsurfaceでは陰影のエッジが非常に汚くなってしまいます。
r.RayTracing.Shadow.CullTransmissivesを利用すると正しくシャドウレイを飛ばすようになり、陰影のエッジが綺麗になります。

f:id:monsho:20201122114505p:plain

Debugging & Visualization Features

デバッグ用の機能追加です。

BVHの視覚化

ShowFlag.VisualizeBVHComplexity/VisualizeBVHOverlapを利用してBVHの複雑さを視覚化出来ます。
レイトレーシングではオブジェクトやポリゴンが重なり合っている部分では処理負荷が高くなる傾向がありますので、できる限り分散して配置した方が有利です。
この機能を使って負荷が高い場所、つまり重なりが複雑な部分をチェックしてパフォーマンス改善に役立てることが出来ます。

f:id:monsho:20201122120758p:plain

レイトレーシング負荷の視覚化

ShowFlag.VisualizeRayTimingを利用してレイトレーシング自体の負荷を確認することが出来ます。
レイトレは様々な要素がパフォーマンスに複雑に関わってくるため、この視覚化はマテリアルの複雑性のみを視覚化しているわけではないことに注意してください。
また、レイの反射方向などによって結果が大きく変わるためか、かなりノイジーです。
しかし、明確に重い部分はわかりやすいので、最適化の指針としては十分使えるでしょう。

f:id:monsho:20201122121941p:plain

Miscellaneous Features & Fixes

その他の機能です。

半透明マテリアルのシャドウ対応

UE4本流では半透明マテリアルもシャドウを落とす設定になっています。
オブジェクト単位、マテリアル単位でシャドウキャストフラグを操作する方法ももちろんありますが、r.RayTracing.ExcludeTranslucentsFromShadowsを利用することで一括で半透明マテリアルのシャドウを削除できます。

ただ、エディタ上でON/OFFした場合、何故か即時反映はされず、メッシュのCast ShadowフラグをON/OFFし直したらそのメッシュだけ適用されるという状態になりました。
不具合なのか仕様なのかは不明ですが、やはり明示的にマテリアル側で指定するほうが良いでしょう。

シャドウの遮蔽面の統一

レイトレシャドウなどの遮蔽はデフォルトで表面、裏面ともにシャドウを落とします。これはr.RayTracing.Shadows.EnableTwoSidedGeometryをOFFにすることで片面のみシャドウを落とすようにすることが出来ます。
しかしこのシャドウを落とす面がシャドウマップとレイトレで逆になっているという問題があります。

f:id:monsho:20201122124629p:plain

r.RayTracing.OcclusionCullDirectionをONにすることで、この逆転現象を解消することが出来ます。

f:id:monsho:20201122124759p:plain

メッシュが閉塞されている場合は意味がない機能ではあるのですが、時折ペラ1枚の書き割りなどを利用することがあったりします。
このような場合で両面シャドウで代用できない場合には有効な機能ですが、かなり使用場面は限定されるのではないかと思います。

さいごに

先日ぷちコンゲームジャムでレイトレ半透明を利用してレンズ越しにのみ表示されるマテリアルという特殊な環境を作ってみました。
この際にRayTracingQualitySwitchReplaceを利用したのですが、OpacityMaskに利用したところ半透明越しじゃなくても表示されてしまうという不具合にぶつかりました。
この理由が普通に半透明部分以外もレイトレして、普通にライティング計算しているからだったわけなのですが、エンジン側のシェーダを修正して対応しました。

その後、この記事を書いていて気づいた。

RTXブランチの半透明マスキング使えばエンジン改造しないで済んだじゃん!

NVIDIA様、さすがでございます。

明日はUE4でアニメーションといえばこの人、”無敵の龍”ほげたつさんのControl Rigの記事です。 お楽しみに!

Substance Designerでシェーダを書いてみた話

ちょっと遅くなりましたが、先々週にあったSubstance Designerゆるゆる会で作成したマテリアルについてです。

ゆるゆる会のテーマはスタイライズドでした。
私が選んだのは『あつまれ どうぶつの森』に出てくる床の1つである”みなものゆか”でした。

f:id:monsho:20201101141808p:plain

この床はゲーム中で動きます。また、カメラを動かすと白っぽい部分(コースティクス的な?)と青い部分に視差があることがわかります。
いわゆるParallax Mappingを施されているテクスチャです。
で、まずは普通にそれっぽく再現するところから始めてみました。出来たのがこれ。

f:id:monsho:20201101143104p:plain

色味とかに差はありますが、まあ似た感じになったかなと。
しかし物足りない。
原因は2つ。動かない。視差がない。
え?再現性が低い?ごめんなさい…

とにかくこの2つをなんとかしたい。
動かない点についてはSubstance Designerの性質上仕方ない部分もありますが、timeパラメータを使って対応すると確認が難しい上に、パラメータが変更されるとテクスチャが生成し直しになるので時間がかかります。
これくらいのマテリアルならそこまで遅くはないものの、なめらかに動いているようには見えません。

もう1つの視差がない点については、Substance DesignerにはParallax Occlusion Mappingが存在しています。
これを使えば再現できるかというと、これは無理です。
POMは高さ方向でOcclusion、つまり遮蔽されてしまうので、例えば白い部分を高くするようにしてしまうとその部分が山になっているように見えてしまいます。

通常、この手の表現はシェーダ側で行います。UE4のマテリアルなんかが最終出力はシェーダですね。
しかしSubstance Designerの最終出力はテクスチャです。シェーダではありません。
テクスチャというのはシェーダで利用するリソースの1つでしかありません。実際、Substance Designerでもシェーダは使われていて、3DビューメニューのMaterialからプリセットのシェーダを選択することが出来ます。
しかしこの中にはみなものゆかを表現できるシェーダが存在しません。

ないなら作れ!

というわけで作ってみました。

f:id:monsho:20201101150925p:plain

似たような感じではありますが、カメラを動かすと水面と水底に視差があるように見えます。
そして水底の影が水面のコースティクスを参照するようになっています。
このように、Substance Designerは(Painterもですが)自作のシェーダを使用することが出来ます。

シェーダの解説は行いませんが、作成されているシェーダとこのマテリアルはゆるゆる会のTrelloにアップロードされていますので、興味ある人は試してみてください。

trello.com

Substance DesignerのシェーダはGLSLで書く必要があります。書けるのはオブジェクト表面用のシェーダだけで、ポストプロセス的なものは出来ません。マルチパスレンダリングも無理。
しかし環境マップやライト情報も取得できるのでライティングも出来ます。カメラ情報も取得できるのでカメラ角度に応じた変化も対応できます。

とはいえ、書き方やパラメータ設定の仕方はGLSLを知ってるだけでは難しいでしょう。
そんなあなたに朗報です。なんと、ビルトインシェーダのコードが読めます!
以下のフォルダを開いてみてください。

$(SubstanceDesignerインストールフォルダ)\resources\view3d\shaders

ここにビルトインされたシェーダが入っています。
このフォルダ直下にある.glslfxファイルはどのシェーダステージにどのシェーダコードを利用するか、パラメータはどんな名前で設定するか、テクスチャはどんなIDで設定するかといった内容を記述します。
実際のシェーダコードはこのフォルダ内の各マテリアル名のフォルダにあります。
頂点シェーダやテッセレーションはあまりいじることはないかもしれませんが、その場合はcommonフォルダ内にデフォルトのシェーダがあるのでそれを使うことが出来ます。
もちろん、頂点シェーダも作成することも可能ですが、多くの場合はそこまでする必要はないでしょう。

シェーダが書けるという点ではSubstance Painterでも書くことが出来ますが、SDとの互換性がなさそうな感じです。
なぜそこに互換性がないのか…謎。

もしシェーダの内容に質問などがあるようでしたらコメントなりいただければ追記しますが、特に難しいことはしてないです。多分。

でまあ、こんな感じでシェーダを書くことに意味があるのかという話をしてしまうと、多分ほとんどの場合では必要ないです。
しかしゲームエンジンを使うにしろインハウスエンジンを使うにしろSubstance DesignerやSubstance Painterと同じ結果にするというのはなかなか難しいです。
ポストプロセスの問題もありますが、ライティングモデルが別だったり、それこそトゥーンシェーダのようなPBRではないライティングを行っているものもあるでしょう。
そういった違いが制作効率を下げるような状況であれば十分使えるはずです。
とはいえ、SPならともかく、SDで使うことはほとんどないと思います。
こんな事もできるよ~という程度のものとして覚えておくくらいで良いでしょう。