より自由にハイトマップを作成する

Substance Designerでは最初にハイトマップを作成するのが基本的なアプローチになります。
しかし、何も考えずにハイトマップを作っていくと数値が飽和してノーマルが残念なことになったりします。

f:id:monsho:20180309001758p:plain

[Blend]ノードでは加算(Add)を使っています。
この場合、[Shape]ノードの段階で数値が 1.0 のピクセル、つまり画像中心のピクセルは2倍されるので2.0になるはずです。
そしてそこからすそ野に向かって徐々に数値を減らしていくはずなので、生成されるノーマルマップは山なりになっているべきなのですが、上の画像のように途中から平べったくなってしまいます。

なぜこのようになるのかというと、基本的なフォーマットでは数値が1.0までで飽和してしまい、それより大きな値は1.0にクランプされてしまうからです。
実際に3DViewで高さとノーマルに上の画像のノードを割り当てるとこのようになります。

f:id:monsho:20180309002803p:plain

高さが途中でクランプされてしまっているのが一目瞭然です。

これを避けるため、通常は高さが1.0を超えない範囲でうまくやるものなのですが、[Blend]ノードによる加算や Add Sub などを使ってしまうと意図せず1.0を超えてしまうこともあります。
1.0を超えてしまった部分はノーマルマップが平坦になってしまうため、大変見苦しい結果となってしまいます。

f:id:monsho:20180309003326p:plain

このような画像になるのは意図していない場合がほとんどですが、ユーザが変更可能なパラメータが多くなってくると、パラメータの組み合わせによってはこのような結果となってしまうことが多々あります。

これを避ける方法の1つが浮動小数点フォーマットを使用する方法です。

f:id:monsho:20180309003942p:plain

浮動小数点フォーマットを利用すると、ノードの数値は 1.0 より大きな値を取り扱うことが可能になります。
そのため、最初の画像は以下のような結果となります。

f:id:monsho:20180309004156p:plain

中心部分の高さがクランプされず、滑らかな形状ができていることが確認できますね。

ただし、どこの段階で浮動小数点マップを使用するか、どの段階で16ビットフォーマットに変換するかは難しい判断となります。

例えば、[Gradient Map]ノードのように、入力されて入ってくる数値が 0.0~1.0 の範囲になっていることを前提としているノードでは、1.0を超えた値は1.0と同じ結果となります。

また、[Slope Blur]などのノードに浮動小数点フォーマットのノードを使うと、意図しないノイズが出てしまったりします。

f:id:monsho:20180309005821p:plain

左の画像は[Slope Blur]を使用する段階で浮動小数点フォーマットに変換した場合ですが、細かなノイズが表面に出てしまっているのがわかります。
それに対して、ハイトをノーマル生成ノードに渡す部分で浮動小数点フォーマットに変換した場合は滑らかな表面になっています。
浮動小数点フォーマットは速度面にも問題が出る可能性が高いので、ハイトをブレンドする場所で主に使うようにし、そこを超えた段階で [Auto Levels] ノードを使って 0.0~1.0 の範囲に収めるようにするのが良いのではないかと思います。

Substance Designerのノード構成覚書

これから少しずつですが、Substance Designerでこういうノード構成するとこういうのが作れます、というのを覚書的に書いていこうと思います。

内臓的な形状

内臓のような有機的な形状を作るノード構成です。
これはTwitterでも呟きましたが、ここにも再掲。
基本は[Perlin Noise]と[Slope Blur Grayscale]を用います。

f:id:monsho:20180226224932p:plain

[Slope Blur Grayscale]の Grayscale 入力と Slope 入力の両方に[Perlin Noise]を繋ぎます。
後者のように Slope に繋ぐ前にブラーをかけても良いですね。
後者の場合はより滑らかな感じになります。

[Slope Blur Grayscale]の Mode パラメータは Blur か Max にしましょう。Min だとうまくいきません。
Samples パラメータは基本最大、Intensity は Slope に繋げているノードによってうまいパラメータは変化しますので調整してみてください。
なお、後者のパラメータはこんな感じ。

パラメータ名 数値
[Blur HQ Grayscale]のIntensity 14
[Slope Blur Grayscale]のIntensity 4

ノイズは他のノイズでも使えるものがありますが、どんなノイズでもうまくいくわけではないので注意してください。
とは言え、いろいろなノイズで試してみると面白いと思います。
一例として、[Grunge Map 002]、[Crystal 2]、[Liquid]、[Moisture Noise]で試したものを掲載します。

f:id:monsho:20180226231416p:plain

物によっては布っぽい形状も作れますね。

直線と曲線を織り交ぜた形状

石畳のようなマテリアルを作る際、スタイライズドなマテリアルの場合は形状が結構難しかったりしませんか?
直線と曲線を織り交ぜたような、単純に歪んでるだけじゃない、手書きにあるような意図的な飛び出し部分などが欲しい場合があるかと思います。

石畳の基本形状は [Tile Random] と [Distance] を使うことが自分は多いですが(クラックの作成とかでも使う)、この方法では形状が直線的になりすぎます。
そこで、そのあとに [Perlin Noise] と [Warp] で歪ませたりするのですが、これだと歪みが曲線的過ぎてイメージが何となく違います。

そこで考えたのが以下の2種類の方法です。

[Crystal 1] と [Directional Warp] を使う方法

f:id:monsho:20180227000702p:plain

[Distance] の後に [Directional Warp] と [Crystal 1] を使って、尖った歪みを作成する方法です。
[Perlin Noise] で歪ませるよりは直線的な歪みができます。
しかし、あまり曲線的な部分は出てこないので、[Perlin Noise] の歪みと併用の方がいいかもしれません。
結果は以下のようになりました。

f:id:monsho:20180227001754p:plain

[Distance] の前に歪ませる方法

つい最近、適当に接続したノードが割といい感じになっていたので、それをもとに作ってみたものです。
[Tile Random] から [Distance] に繋げる前に [Warp] と [Perlin Noise] で歪ませます。

f:id:monsho:20180227001931p:plain

歪ませた後に [Edge Detect] でエッジ抽出し、それを [Blend] の Subtract で減算します。
これは [Warp] 後にエッジ部分にアンチエイリアス的に幾分滑らかになってしまうためです。
この状態で [Distance] に通すとうまく形状に合わせて広がってくれません。
後半部は前述の方法と同じなので割愛。
そして結果は以下。

f:id:monsho:20180227002243p:plain

どちらがいいかと言われると好みの問題かな、とも思います。

UE4の描画パスについて Ver 4.18.1

こちらは Unreal Engine 4 Advent Calender 2017 16日目の記事です。

以前書いたUE4描画パスの順序をまとめたものがだいぶ古くなってしまっていたので最新版へのアップデート。
以前の記事はこちらです。

monsho.hatenablog.com

動作を完全に確認しているわけではないので、ミスがあるかもしれません。
見かけたらお伝えいただければ。
なお、今回もDeferred Renderingパスに関してのみで、モバイルやForward Renderingパスについては言及しません。
RenderDocを見ながらチェックする方が読む側は楽しいと思いますが、描画パスを詳しく知りたい、あわよくば改造したいという人はRenderDocよりソースコードの行数やコード片の方がうれしいかな、と思いまして。

Deferred Renderingの開始は

Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp (532)
FDeferredShadingSceneRenderer::Render()

ここから開始です。

行数 コード片 内容
539 GRenderTargetPool.TransitionTargetsWritable() ターゲットプールのターゲットを描画可能な状態に遷移
542 SceneContext.ReleaseSceneColor() シーンカラーバッファを解放
546~552 FRHICommandListExecutor::WaitOnRHIThreadFence() RHIが別スレッドで動作している場合、ここで OcculusionSubmittedFence を待つ
前段のOcculusionCulling処理の終了待ち?
568 GSystemTextures.InitializeTextures() システムテクスチャの初期化
初期化済みならなにもしない
571 SceneContext.Allocate() ViewFamilyに見合ったレンダーターゲット用のバッファを確保する
ViewFamilyはFeatureLevelやSceneCaptureがあるかなどの情報
573 SceneContext.AllocDummyGBufferTargets() GBufferにダミーの黒テクスチャを割り当て
579 InitViews() Viewに関する様々な初期化
メッシュのカリング、半透明のソート、シャドウやライトキャッシュの初期化など多岐にわたる
詳しくはソースを追ってほしい
581~588 PostInitViewFamily_RenderThread()など 未実装っぽい
599~606 GetEyeAdaptation() const関数を呼び出してるが取得したものを格納してないので意味がない?
608~631 if (ShouldPrepareDistanceFieldScene()) DistanceField系(AO、Shadowなど)が使用されているとこのブロックが有効になる
ただし、IntelHD4000シリーズは3Dテクスチャ生成に失敗するらしく、強制的に無効化される
処理内容はDF系技術で必要になるView空間に整列したVolumeテクスチャの作成、更新など
720~727 FGlobalDynamicVertexBuffer::Get().Commit() 動的な頂点バッファとインデックスバッファのコミット処理
主にパーティクル用の頂点、インデックスバッファをアンロック(アンマップ)
732~737 Scene->FXSystem->PreRender() エフェクトの描画事前処理
GPUパーティクルの更新を主に行う
平面リフレクションが有効だったりレンダースレッドが有効な場合はここでは行わない
767~788 RenderPrePass() Z Pre-Pass レンダリングを行う
807 SceneContext.ResolveSceneDepthTexture() Z Pre-Passで描画した深度バッファのリゾル
圧縮深度の展開やMSAAのリゾルブ処理
811 ComputeLightGrid() ライトグリッドの計算
シェーダコードから察するに、Clusterdライティングのためのライトリンクリストを生成している
主に半透明のForward Rendering用
821~826 SceneContext.AllocGBufferTargets() GBufferためのメモリ確保
830~841 RenderOcclusion() オクルージョンに関するレンダリング
低解像度バッファに対するオクルージョンクエリなど
階層型深度バッファもここで作る
Z Pre-Passですべてが深度バッファに描画されている場合に有効になる
SSAOの並列処理を行う場合はここで処理を開始
845~849 RenderShadowDepthMaps() シャドウマップの描画
昔と違ってアトラス化したシャドウマップにレンダリングする
852~857 ClearLPVs() Light Propagation Volumeで使用するバッファをクリアする
859~863 RenderCustomDepthPassAtLocation() CustomDepthをベースパス前に描画する
r.CustomDepth.Orderで0を設定するとこの段階で描画する
DBufferパスで使用することができる
865~868 ComputeVolumetricFog() ボリュームフォグを適用するため、空間分割した3Dテクスチャにライティングを行う
平行光源1灯のみ(多分)、ライトファンクションに対応している
870~878 RenderForwardShadingShadowProjections() Forward Renderingの場合にシーンにシャドウを投影する
Forwardでは複数のシャドウをマテリアル計算時に処理しづらいので、ホワイトバッファという真っ白なバッファに影による減衰を予め計算しておく
885~900 if (bDBuffer) DBufferが有効な場合にDeferred Decalを描画する
902~934 AllocateDeferredShadingPathRenderTargets() Deferred Shading用のレンダーターゲットを確保
条件によってはGBufferや半透明用のボリュームライトバッファもクリアする
936~946 BeginRenderingGBuffer() レンダーターゲットの設定
ワイヤーフレーム表示の場合はMSAAバッファを使用する
951~958 RenderBasePass() ベースパスの描画
複数スレッドで並列にコマンドを積むようになってるっぽい
ベースパス終了後に深度バッファのリゾル
976~985 ClearGBufferAtMaxZ() 描画されなかった部分のGBufferのクリア
通常は必要ないはずだが、GBuffer表示の対応のためか?
987 VisualizeVolumetricLightmap() ボリュームライトマップの可視化
989 ResolveSceneDepthToAuxiliaryTexture() 深度バッファを別バッファにリゾルブする
深度テストしながらフェッチできないハード向け
991~1002 RenderOcclusion() ベースパス後のオクルージョン描画
830行目でレンダリングを行ってない場合はここでレンダリング
1012~1017 RenderShadowDepthMaps() シャドウマップやボリュームフォグの描画
オクルージョン描画がベースパス後の場合はここで行う
1019~1023 RenderCustomDepthPassAtLocation() ベースパス後のカスタムデプス描画
通常はここで描画される
1028~1038 FXSystem->PostRenderOpaque() ベースパス後のエフェクト描画
主に衝突判定をとるGPUパーティクルの更新を行っている
1042~1054 RenderVelocities() 速度バッファを描画する
1057 CopyStencilToLightingChannelTexture() ステンシルバッファに書きこまれたライトチャンネル情報をコピー
1059~1061 GfxWaitForAsyncSSAO() 並列処理しているSSAOのレンダリングを待つ
1065~1081 ProcessAfterBasePass() ベースパス後のポストプロセス
まだ実行されていない場合、Deferred DecalやSSAOの処理を行う
1084~1101 SetRenderTargetsAndClear() ステンシルバッファのみクリア
1111~1114 RenderIndirectCapsuleShadows() カプセルシャドウによる間接照明計算
SceneColorとSSAOに乗算合成される
この段階ではSceneColorはベースパスのエミッシブが書き込まれているはず
1118 RenderDFAOAsIndirectShadowing() DistanceFieldAOの描画
ついでにベントノーマルも計算し、この後数段で使用する
1121~1124 ClearTranslucentVolumeLighting() 半透明用のボリュームライトをクリアする
1127 RenderLights() ライトのレンダリング

ここから RenderLights() 関数内に一旦入ります。
ソースコードは以下です。

Engine/Source/Runtime/Renderer/Private/LightRendering.cpp (316)

行数 コード片 内容
329~332 GatherSimpleLights() パーティクルライトをSimpleLightとして集める
主にタイルベースライティング用
340~365 LightSceneInfo->ShouldRenderLight() 描画すべきライトのリストを作成する
368~375 SortedLights.Sort() シャドウあり、ライトファンクションありなどでライトをソートする
420~424 WaitComputeFence(TranslucencyLightingVolumeClearEndFence) 非同期実行している半透明用ライトボリュームのクリアを待つ
435~455 RenderTiledDeferredLighting() タイルベースのDeferred Lightingを実行する
457~461 RenderSimpleLightsStandardDeferred() SimpleLightをレンダリングする
タイルベースレンダリングを実行している場合はここでのレンダリングは行わない
463~478 RenderLight() シャドウなし、ライトファンクションなしのライトをレンダリング
480~495 InjectTranslucentVolumeLightingArray() シャドウなしライト、SimpleLightを半透明用ライトボリュームに描画する
ジオメトリシェーダを用いて各スライスに1ライト1DrawCallで描画
499~559 UpdateLPVs() LPVが有効な場合、LPVの更新を行う
Reflective Shadow Mapsと直接光をボリュームに流し込む
569 for (int32 LightIndex = AttenuationLightStart;... ここからシャドウあり or/and ライトファンクションありのライトごとの処理
592~631 RenderShadowProjections() ホワイトバッファへシャドウによる減衰を描画する
シャドウはライティング計算時に行うのではなく、一旦フレームバッファと同サイズのホワイトバッファに描き込む
また、半透明用ボリュームライトやハイトマップライトへの描画も行っている
633~637 HeightfieldLightingViewInfo.ComputeLighting() ハイトフィールドのライティング計算
ハイトフィールドだけライティング結果をアトラステクスチャにレンダリングしてる
640~657 RenderPreviewShadowsIndicator() ライトファンクションによる減衰をホワイトバッファに描き込む
659~662 CopyToResolveTarget() ホワイトバッファのリゾル
664~669 InjectTranslucentVolumeLighting() シャドウが有効でない場合はここで半透明用ライトボリュームへレンダリング
674~677 RenderLight() 直接ライトのレンダリング

ライトレンダリングはここまでです。
以降は再び DeferredShadingRenderer.cpp に戻ります。

行数 コード片 内容
1133 InjectAmbientCubemapTranslucentVolumeLighting() Ambient CubeMapを半透明用ライトボリュームへレンダリング
1137 FilterTranslucentVolumeLighting() 半透明用ライトボリュームのフィルタリング
3x3x3のボックスフィルタ
1142~1153 ProcessLpvIndirect() ライティング後のポストプロセス処理
現在はLPVの適用のみ
1155 RenderDynamicSkyLighting() 動的スカイライトの描画
1159 ResolveSceneColor() ここまで描画したSceneColorのターゲットをリゾルブする
通常はRHIのリゾルブ命令を使用するが、モバイルエミュレートの必要がある場合は特殊処理
1162 RenderDeferredReflections() ポストプロセス的に環境リフレクションを適用する
Screen Space Reflectionの計算もここで
1169~1173 ProcessAfterLighting() リフレクション適用後のポストプロセス
現在はScreen Space Subsurface Scatteringのポストプロセス処理のみ
1180~1185 RenderLightShaftOcclusion() ライトシャフトの遮蔽情報の描画
これがなくてもライトシャフトは有効化できるが、カメラ近くに遮蔽物があった場合の品質に差が出る
1188~1212 RenderAtmosphere() Atomospheric Fogの描画
1217~1222 RenderFog() Height Fogの描画
ボリュームフォグもここで適用される
1224~1236 RenderPostOpaqueExtensions() 不透明描画後の追加処理
ユーザがC++で指定できるレンダリング処理っぽい?
ComputeShaderのDispatchもできるようだ
1252~1261 RenderTranslucency() 半透明オブジェクトの描画
Separate Translucencyの場合は別バッファに描画して合成まで行う
1267~1275 RenderDistortion() Refractionが有効なマテリアルを描画
1280~1286 RenderLightShaftBloom() ライトシャフトを描画する
遮蔽情報が描画されているかどうかで結果が異なる
1288~1293 RenderOverlayExtensions() 多分ユーザが実行可能なレンダリングパス
RenderPostOpaqueExtensionsと同様
1295~1303 RenderDistanceFieldLighting() DistanceField系のライティング処理(GIなど)
1306~1318 RenderMeshDistanceFieldVisualization()
RenderStationaryLightOverlap()
デバッグ用の情報ビジュアライズ
メッシュDFとStationaryLightのオーバーラップ情報
1336~1341 GPostProcessing.Process() ポストプロセスの描画
後述
1361~1366 RenderFinish() 描画の終了処理
デバッグビジュアライズなど

以上です。
ここからはポストプロセスです。
Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp (1262) この場所からです。

行数 コード片 内容
1385~1451 AddPostProcessDepthOfFieldGaussian()
AddPostProcessDepthOfFieldCircle()
AddPostProcessDepthOfFieldBokeh()
被写界深度(DoF)の描画
BokehDoFの場合はSeparate Translucencyバッファとの合成もここで行う
1453~1463 RegisterPass(new(FMemStack::Get()) FRCPassPostProcessBokehDOFRecombine(bIsComputePass)) BokehDoFではない場合はここでSeparate Translucencyを合成する
1465 AddPostProcessMaterial(Context, BL_BeforeTonemapping, SeparateTranslucency) トーンマップ前のポストプロセスマテリアルを適用する
1469~1482 AddTemporalAA() TemporalAAを適用する
1484~1544 FRCPassPostProcessMotionBlur() モーションブラーを適用する
[A Reconstruction Filter for Plausible Motion Blur]を改良したものを利用
スケール値を変更した2パス描画によってより高クオリティのブラーも適用できる
r.MotionBlurSeparable に 1 を指定すれば有効になる
1546~1559 FRCPassPostProcessVisualizeMotionBlur() モーションブラーとブルームのビジュアライズ
1562~1572 FRCPassPostProcessDownsample() SceneColorバッファを半解像度バッファにダウンサンプリング
1574~1609 FRCPassPostProcessHistogram() ダウンサンプルしたSceneColorを利用し、画面のヒストグラムを求める
1612~1625 CreateDownSampleArray() ブルーム用に半解像度SceneColorから複数回のダウンサンプリングを行う
Eye Adaptationが有効な場合はこの中でセットアップを行う
1628~1653 AddPostProcessBasicEyeAdaptation()
AddPostProcessHistogramEyeAdaptation
Eye Adaptationを実行する
ブルームが無効の場合はここでダウンサンプルを行う
1655~1756 AddBloom() ブルームを適用する
レンズフレア、レンズブラーもこの関数内で行う
1760~1790 AddTonemapper() トーンマップを適用する
1792~1795 AddPostProcessAA() FXAAを適用する
1797~1804 FRCPassPostProcessVisualizeDOF() DoFのビジュアライズ
1808~1823 AddGammaOnlyTonemapper() フルポストプロセスでない場合、Separate Translucencyの合成と簡易トーンマッパーを適用する
1826~1906 FRCPassPostProcessVisualizeComplexity()など 各種ビジュアライズなど
1908 AddPostProcessMaterial(Context, BL_AfterTonemapping... トーンマップ後のポストプロセスマテリアルを適用する
1910~1982 FRCPassPostProcessSubsurfaceVisualize()など 各種ビジュアライズなど
1986~2025 FRCPassPostProcessUpscale() スクリーンパーセンテージが有効な場合はここでアップスケール

以上となります。
おつかれさまでした。

通常、UE4を使う場合はこれらのパスの順序を意識する必要もないのですが、どうしても処理を入れ替えたい、無駄な処理を省きたいなどの場合にはソースコードを読まなければなりません。
本記事がそのような方の一助になれば幸いです。

明日は @negimochi さんで、「Sequencer 構造解説とカスタムトラック追加 (UE4.18版)」です。

レイヤーインスタンシングのまとめ

Substance Painter 2017.4 がリリースされました。
SPの大型アップデートとしては多分今年最後です。以降はバグフィックスのみでしょう。

今回のアップデートではプラグインとしてDCC Live Linkが追加されています。
名前の通り、他のDCCツール(MayaやMODOなど)でメッシュを表示しながらSPでペイントをするためのもののようです。
レンダリングしながらペイントとかもできるのでしょうかね?
ゲーム開発の現場の場合、Maya上にゲームと同じレンダリング状態を提供しているチームにとっては便利かもですね。

さて、今回のアップデートの目玉機能はというと、やはりレイヤーインスタンシングでしょう。
これまでのSPでは、テクスチャセット(FBX上のマテリアル)が別のものに同じマテリアルを割り当てたい場合、コピペして対応するしかありませんでした。
ある程度絵面が固まった終盤でならコピペでも行けるかもですが、序盤だと元のマテリアルを何度も修正すると思いますのでコピペが多発します。
このような状態を回避することができるのが今回のインスタンシング機能です。

使い方は簡単。
まず、インスタンス元のレイヤー(塗りつぶしレイヤーやフォルダ)を選択し、マウス右クリック→[Instantiate across texture sets...]を選択します。
f:id:monsho:20171125195032p:plain
すると、以下のようなインスタンシングウィンドウが表示されます。
f:id:monsho:20171125195235p:plain
インスタンシング先レイヤーを選択して[OK]ボタンを押すと、選択していたレイヤーがインスタンス先のテクスチャセットの最上位レイヤーにインスタンシングされます。
f:id:monsho:20171125195532p:plain
インスタンス元/先レイヤーには文書のようなアイコンが設定されます。
このアイコンをクリックするとインスタンスプロパティウィンドウが表示され、このレイヤーのインスタンス元と先がわかりやすいように一覧表示されます。
f:id:monsho:20171125200032p:plain
このプロパティウィンドウから編集中のテクスチャセットを切り替えることも可能になっています。
インスタンスレイヤーの調整はインスタンス元からしかできないので、どこからインスタンスしたのかわからないという状況になったら押してみるとよいでしょう。

なお、インスタンスレイヤーをさらにインスタンスすることもできます。
f:id:monsho:20171125202824p:plain
しかし闇雲にインスタンス機能を使用してもあとで迷ってしまうのがオチです。あまり深くインスタンシングしないようにしましょう。

インスタンス先ではインスタンス元のレイヤーの内容を引き継ぎます。
引き継がれるのはマスクやフィルタなどほとんどすべてです。
インスタンス元がフォルダの場合、フォルダ内の設定も基本的にすべて引き継がれます。
インスタンス元にフィルタを追加したり、フォルダに新しいレイヤーを配置したりすればそれらも反映されます。

そしてインスタンス先で新たにマスクを追加することが可能です。
例えば以下のようにインスタンス元レイヤーにEdge Wearマスクを適用していると、インスタンス先にも適用されます。
f:id:monsho:20171125203928p:plain
ここでBodyには元のマスクに加えて別のマスクも追加したいということもあるでしょう。
このような場合はインスタンス先レイヤーでマスクを設定、まずは塗りつぶしフィルタで真っ白にしておきます。
こうすることで元のマスク情報を取得できますのでこの状態に別のジェネレータなどを適用します。
以下の例ではGlass Edge Wearを減算で適用しています。
f:id:monsho:20171125204227p:plain
このようにインスタンス元の状態を生かしつつマスクの追加が可能になります。
残念ながらインスタンス先でペイント側にはフィルタが適用できないので、適用したい場合はパススルーレイヤーを追加してそちらでフィルタをかけましょう。

インスタンスはほとんどの情報をインスタンスできますが、例外が存在します。
ペイントレイヤーは残念ながらインスタンス対象外です。ペイントフィルタも同様です。
まあ、ペイントレイヤーをインスタンスできてもどう使えばいいのかわかりませんがね…
もちろん、パーティクルブラシもインスタンスできないということになります。

もう1つの例外はアンカーポイントです。
アンカーポイントは特にノーマル情報を元にしてマスクを生成したい場合に用いられます。
アンカーポイントを使わないと、使用できるノーマル情報はベイクしたテクスチャの情報に限られますが、アンカーポイントを使うことでペイントした結果生成されたノーマル情報も使えるようになります。
大変便利な機能ではあるのですが、これは特定条件でのみインスタンス可能となります。
それは、フォルダをインスタンスしていること、そのフォルダ内からアンカーポイントを参照している場合です。
つまり、このような状態です。
f:id:monsho:20171125210043p:plain
この状態であればインスタンス先でも同じ効果を得ることができます。

もしもインスタンス元が普通の塗りつぶしレイヤーで、そこでアンカーポイントが参照されているとどうなるかというと、こうなります。
f:id:monsho:20171125210450p:plain
画像にもあるように、インスタンス元のアンカーポイントが不正扱いされ、結果としてインスタンス元/先ともにアンカーポイントが無視されます。
これらのことから、インスタンスレイヤーはできるだけフォルダで利用しておく方がよいのではないかと思います。

レイヤーインスタンシングについての検証まとめは以上になります。
もし他にも情報がありましたらこっそり教えていただけるとありがたいです。

小ネタ 簡易ワームホール的エフェクト

なんか思いついたので小ネタでエフェクト用テクスチャを簡易で作ってみるテスト。
表題通り、ワームホール的な奴です。
作ったのはこちら。

f:id:monsho:20171116083558g:plain

ブラウザで見ると奥の方が動いてないように見えるけど…まあいいや。

ノードはこちら。

f:id:monsho:20171116083807p:plain

Transform2DのOffsetパラメータ内はこうなってます。

f:id:monsho:20171116083854p:plain

あとはSubstance Playerで再生するだけ。
Frequencyパラメータで周期の変更もできます。
ちゃんとループもするのでエフェクトでも使いやすいでしょう。

以上、小ネタでした。

Substance Automation Toolkit で Pixel Processor を作成する

前回、Substance Automation Toolkit (SAT) を使ってノードを接続する方法について解説しました。
しかしこの手法、普通にマテリアルを作成するには不向きです。
テンプレート的な処理を大量に作る分にはいいのですが、1点もののマテリアルを作るなら普通にSubstance Designerを使った方がよいでしょう。

しかし現在のSATには式グラフを比較的簡単に生成する機能が提供されています。
sbsMathというモジュールがそれです。
SATの標準モジュールだけではプログラムコードを使ってノードを繋げることしかできませんが、sbsMathを利用することでプログラムで数式を実装する感覚で式グラフを作成できます。
が、このモジュールはなぜかSATの標準モジュールとして提供されていません。
いや、正確には提供されているのですが、なぜか "MyDocument/Allegorithmic/Substance Automation Toolkit" 内にインストールされます。

sbsMathを利用しているサンプルは同じフォルダ内にある raytracer.py です。
このサンプルは Pixel Processor を利用して各ピクセルごとにレイトレースして球をライティングしてレンダリングするサンプルです。
複雑に見えますが、sbsMath の機能を一通り使っているのでサンプルとしては十分でしょう。
とは言え、いきなりサンプルを全部解説、というのもあれなので、とりあえずより簡易なサンプルで解説しましょう。

今回作成したサンプルは、2枚のノーマルマップの各ピクセル内積を取るものです。
何かに使えるのか?という話をされると…使い道は全くないんですが、これを Pixel Processor で作成しようとすると意外と面倒だったりします。

内積計算自体は式ノードに存在するのでそのまま使えばいいのですが、ノーマルマップをサンプルした値はそのままベクトルとして使用できません。
カラーマップは負の値を保存できないため、ノーマルマップに格納されている値は -1~1 を 0~1 に圧縮しています。
つまりベクトルとして利用するには 0~1 を -1~1 に伸張しなければなりません。
そのための計算式は以下の通りです。

v = color * 2 - 1

まあ、正直な話、この程度の処理ならやっぱりSDで作った方が早いような気もしますがね…

とにかく、コードを見ていきましょう。
まず、インポートするモジュールですが、sbsMathを追加する必要があります。

import sbsMath.sbsmath as sm
import sbsMath.sbsmath_tools as st

sbsmathとsbsmath_toolsをそれぞれ sm, st という名前空間で取り扱います。

次にノードグラフの作成部分ですが、基本的には前回と同じです。
作成するのはカラーの入力ノードを2つ、それぞれID normal_0, normal_1 とします。
出力ノードはグレイスケールです。
最後の1つは Pixel Processor です。

    # pixel proc
    pix_proc_node = sbsGraph.createCompFilterNode(aFilter = sbsenum.FilterEnum.PIXEL_PROCESSOR,
        aGUIPos      = input_normal_0.getOffsetPosition(xOffset),
        aParameters  = {sbsenum.CompNodeParamEnum.COLOR_MODE:sbsenum.ColorModeEnum.GRAYSCALE})

出力はグレイスケールにしておきます。

問題なのは Pixel Processor への入力です。
このノードは最初の段階では入力ピンが1つしかありませんが、そのピンへノードを繋ぐと次のピンが出現するという仕組みになっています。
そのため、あるノードを Pixel Processor の入力ピンへノードを接続する場合、名前を直接指定する必要があります。

    # connection
    sbsGraph.connectNodes(aLeftNode = input_normal_0, aRightNode = pix_proc_node,
        aRightNodeInput = 'input')
    sbsGraph.connectNodes(aLeftNode = input_normal_1, aRightNode = pix_proc_node,
        aRightNodeInput = 'input:1')

最初のピンは input という名前で接続できます。
1つ目のピンへの接続はピンの名前を指定しなくても勝手に接続されるのでさほど問題はありません。
これに対して2つ目のピンは input:1 という名前で指定する必要があります。
ここから先に接続する場合も input:n という形で順次接続できます。
接続順番も、かならず0番から進めてください。1つ以上飛ばして接続しようとすると失敗します。

では、Pixel Processor の式グラフの作成です。命令は簡単で、以下のようにします。

    # pixel proc function
    st.generate_function(NormalMapDot, sbsDoc, fn_node = pix_proc_node.getPixProcFunction())

sbsmath_tools.generate_function() を使うと、特定の式グラフに対して式ノード生成関数をを走らせます。
第1引数に渡している NormalMapDot という関数がノードの作成を行ってくれます。
実装は以下のようになっています。

def NormalMapDot(fn):
    pixel_pos = fn.variable('$pos', widget_type=sbsenum.WidgetEnum.SLIDER_FLOAT2)

    normal0 = sm.create_color_sampler(pixel_pos, 0)
    normal1 = sm.create_color_sampler(pixel_pos, 1)

    normal0 = sm.swizzle_float3(normal0, [0, 1, 2])
    normal1 = sm.swizzle_float3(normal1, [0, 1, 2])

    normal0 = normal0 * fn.constant([2.0, 2.0, 2.0]) - fn.constant([1.0, 1.0, 1.0])
    normal1 = normal1 * fn.constant([2.0, 2.0, 2.0]) - fn.constant([1.0, 1.0, 1.0])

    dot_p = sm.dot(normal0, normal1)
    return fn.generate(dot_p)

sbsmathモジュールには式グラフで使用可能なノードが登録されています。
ここではベクトル要素のスイズルを行う SwizzleFloat3 や内積演算の Dot を使っています。
ノードや計算式の結果は変数に登録することができ、最終出力をgenerate()関数に引き渡せば自動的にグラフが生成されるというわけです。
ね、簡単でしょ?

作成したグラフは以下のようになります。
f:id:monsho:20171115003858p:plain
そして Pixel Processor の中身は以下です。
f:id:monsho:20171115003952p:plain
割といい感じにノードの配置もしてくれてますね。
ノード数も少ないので、やはりこれくらいならSD上で作成した方が早いかもしれませんが、パッと見でどういう計算しているのかわかりにくいのもノードベースならではかなとも思います。

さらに複雑な計算になるとノードベースではやりたくなくなるかもしれませんので、特にエンジニアの方はPythonでの生成法も覚えておくとあとあと便利かもしれませんよ。

Substance Automation Toolkit の紹介

Substance Designer 2017のライセンスを持っている方はSubstance Automation Toolkit(以下SAT)の使用が可能です。
f:id:monsho:20171103103811p:plain Allegorithmic公式サイトの自身のアカウントからDLできるはずです。

このツールキットはその名の通り、自動化を主な目的としたツール群です。
ドキュメントは以下です。

Automation Toolkit Home - Substance Automation ToolKit - Allegorithmic Documentation

Substance Designer 6以前のライセンスであればSubstance Batch Tools(以下SBT)というものが使えましたが、SATはSBTを引き継いで機能を追加したものと考えてください。

同梱されているコマンドラインツールは以下のものがあります。

sbsbaker

メッシュからAO、Normal等のメッシュ固有のマップを生成します。
ハイポリメッシュの指定もできるので、マップのベイクを一括で行いたい場合に使えます。

sbscooker

.sbsファイルから.sbsarファイルを生成するツールです。
SD作業者がSP作業者にマテリアルやフィルタを提供する場合、.sbsarで提供するのが基本です。
このツールを使えば自動的に更新された.sbsファイルから.sbsarファイルを生成するようにすることが可能です。

sbsmutator

.sbsファイルに修正をかけることができるツールのようです。
例えば.sbsで使用しているイメージデータを入れ替えたり、パラメータを変更したりして別の.sbsを生成することができるようです。

sbsrender

.sbsarからイメージをレンダリングするツールです。
SDからテクスチャイメージをエクスポートするのを自動化することができます。
入力イメージ、パラメータ変更もできるので、テクスチャエクスポートをしたいだけならsbsmutatorは不要です。

sbsupdater

.sbsの対応バージョンを更新するツールです。
SDが更新されると、以前作成した.sbsを読み込む際に更新処理が入ります。
SDで使用する際にいちいちそれらが出力されるのは面倒なので、このツールで一括更新してしまうとよいでしょう。

多分、sbsupdater以外はSBTにもあったツールだと思います。

SATではこれらのコマンドラインツールに加え、Python用のモジュールが追加されています。
PythonAPIによってこれらのツールと同じことができるようで、かなり便利になりました。

また、このモジュールによって.sbsファイルの生成もできるようになっています。
通常であればSDを立ち上げて作成する.sbsですが、PythonAPIによってノード追加、接続などができるわけです。
使い道は…これがなかなか難しい。
簡単な接続ならPythonでも簡単に行えますが、SDを使った方がより簡単です。
複雑なマテリアルを作成するならそれこそSD使った方がいいです。
あるとすると、2つのマテリアルを指定のマスクイメージでブレンドするというノード接続において、マテリアルをいろいろ変化させて様々な組み合わせのマテリアルを作成したい場合でしょうか。
C#などでUIを作り、自身のライブラリ内のマテリアルとマスクイメージを選択できるようにしてとっかえひっかえ組み合わせを試すとか?

まあ、イマイチ使い道が思い浮かばないのですが、簡単なPythonコードを紹介しましょう。

# coding: utf-8
import os
import sys
import pysbs

from pysbs import python_helpers
from pysbs import context
from pysbs import sbsenum
from pysbs import sbsgenerator

def CreateHelloWorld(destFileAbsPath):
  aContext = context.Context()

  sbsDoc = sbsgenerator.createSBSDocument(aContext,
    aFileAbsPath = destFileAbsPath,
    aGraphIdentifier = 'TestMaterial')

  sbsGraph = sbsDoc.getSBSGraph('TestMaterial')

  startPos = [48, 48, 0]
  xOffset = [192, 0, 0]
  yOffset = [0, 192, 0]
  xyOffset = [192, 96, 0]

  # input nodes
  input_0 = sbsGraph.createInputNode(aIdentifier = 'input_0',
    aGUIPos = startPos,
    aColorMode = sbsenum.ColorModeEnum.COLOR,
    aAttributes = {sbsenum.AttributesEnum.Label: 'Input0'})
  input_1 = sbsGraph.createInputNode(aIdentifier = 'input_1',
    aGUIPos = input_0.getOffsetPosition(yOffset),
    aColorMode = sbsenum.ColorModeEnum.COLOR,
    aAttributes = {sbsenum.AttributesEnum.Label: 'Input1'})

  # noise
  noise_node = sbsGraph.createCompInstanceNodeFromPath(aSBSDocument = sbsDoc,
    aPath = 'sbs://crystal_1.sbs/crystal_1',
    aGUIPos = input_1.getOffsetPosition(yOffset))

  # blend
  blend_node = sbsGraph.createCompFilterNode(aFilter = sbsenum.FilterEnum.BLEND,
    aGUIPos = input_1.getOffsetPosition(xOffset),
    aParameters = {sbsenum.CompNodeParamEnum.BLENDING_MODE: sbsenum.BlendBlendingModeEnum.COPY,
    sbsenum.CompNodeParamEnum.OPACITY: 0.8},
    aInheritance = {sbsenum.CompNodeParamEnum.OUTPUT_SIZE: sbsenum.ParamInheritanceEnum.PARENT})

  # output nodes
  output_node = sbsGraph.createOutputNode(aIdentifier = 'BlendResult',
    aGUIPos = blend_node.getOffsetPosition(xOffset),
    aOutputFormat = sbsenum.TextureFormatEnum.DEFAULT_FORMAT,
    aAttributes = {sbsenum.AttributesEnum.Description: 'noise masked blending result.'})

  # connection
  sbsGraph.connectNodes(aLeftNode = input_0, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.SOURCE)
  sbsGraph.connectNodes(aLeftNode = input_1, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.DESTINATION)
  sbsGraph.connectNodes(aLeftNode = noise_node, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.OPACITY)
  sbsGraph.connectNodes(aLeftNode = blend_node, aRightNode = output_node)

  sbsDoc.writeDoc()

  return True

if __name__ == "__main__":
  destFileAbsPath = python_helpers.getAbsPathFromModule(sys.modules[__name__], 'sample/TestMaterial.sbs')
  CreateHelloWorld(destFileAbsPath)

まずpysbsからコンテキストを取得し、これを元にして.sbsファイルを生成します。これはドキュメントと呼ばれます。
ドキュメントには複数のグラフを作成することができます。これはそのままSubstanceのグラフです。
今回は TestMaterial.sbs というドキュメントに TestMaterial というグラフを作成しました。

作成したグラフ内に各ノードを作成していきます。
まずは入力ノードを2つ作成します。createInputNode()関数を用いています。
なお、ノードの位置は直接指定することもできれば、あるノードからの相対位置も指定できます。

次に適当なノイズノードを持ってきます。
createCompInstanceNodeFromPath() 関数を使用することで、特定のパスにある.sbsから特定のノードを取得できます。
SDの標準ノードについては "sbs://" から始まるパスでOKのようです。

次はブレンドノード。
組み込みノードについては createCompFilterNode() 関数で生成できますが、一部のノード(入力や出力など)は特定の関数があります。

ノードの最後は出力ノードです。Descriptionにコメントを残すことも可能です。

コメントと言えば、フレームも作成できるみたいですね。

配置したノードはそれだけでは置かれているだけなので、connectNodes() 命令で接続を行います。
左と右のノードと、それぞれどこのピンに接続するかを指定しますが、ピンが1つだけの場合は省略可能です。
なお、存在しないピンに入出力しようとすると例外を発生させます。入出力のどちらが悪いのかは教えてくれません。うぼぁぁ

最後にドキュメントを writeDoc() すれば.sbsが出力されます。
出力された.sbsがこちらです。

f:id:monsho:20171103125009p:plain

ちゃんと接続されてますね。
まあ、正直な話、これだけコード書くよりノード接続した方が速いです。
なので、こういうことをしたいのであればPythonを使う理由はほとんどありません。
しかし、ノード接続では面倒なものもありまして…
次回はその辺の、ノード接続では面倒なものを簡単に作成する方法について書きます。