UnityのShaderGraphでインクシェーダーを作る

これはUnity Advent Calendar 2022の22日目の記事です。


スプラトゥーン3、けっこう面白いですね。過去作の1,2は未プレイでしたが、3からスプラデビューしました。

スプラ3で遊びながら、インクシェーダーの実装方法に興味が出てきたので、UnityのShaderGraphでそれっぽいものを実装してみました。

ShaderGraphの基本機能だけで構成されており、ノードの量も少なめにしました。

ShaderGraphの基本操作は解説しませんが、なるべく丁寧に説明をしたつもりなので、ShaderGraphの入門記事として参考にしてください!

インクシェーダー

色変更

インクシェーダー 色変更

しきい値の調整

インクシェーダー しきい値の調整

ShaderGraph全体

ShaderGraphの全体です。

ShaderGraph全体

ShaderGraphのスクロール領域を含めてキャプチャするために Cyanilux/ShaderGraphToPNG というUnityのパッケージを利用しました。

基本方針

  • URPのLitグラフに与えるBaseColorやSmoothnessや法線をいい感じに制御してインクっぽくする
    • カスタムなシェーディングはしない
  • インクの高さマップはGradient Noiseからプロシージャル生成
    • インタラクティブなインク制御は未対応
    • RenderTextureを生成してペイントするようなアプローチでインタラクティブにできそう(今後の課題)

チュートリアル

そこまで規模の大きくないShaderGraphですが、理解しやすいように1ステップごと解説します。

ステップ1. PBRテクスチャに対応

まずはPBRテクスチャに対応します。

PBRテクスチャは以下のサイトからお借りしました。とても良い感じのCC0ライセンスの床のタイル素材を利用させていただきました。

これがPBRテクスチャをプロパティにして、ShaderGraphの各種PBRパラメーターを渡すだけのShaderGraphです。

PBRテクスチャに対応

BaseColorNormal はそのままノードを繋げるだけでOKです。

Metallic/Smoothness/Ambient Occlusion だけ少し工夫がいります。

Poly Havenのテクスチャは Ambient Occlusion/Roughness/Metallic(以下、ARMテクスチャ)がRGBに格納されているようなので、RGBの順番をBGRのように並び替える必要があります。

Smoothness = 1 - Roughness の関係があるので One Minus ノードで変換します。

これでPoly Havenから落としてきたARMテクスチャに対応したShaderGraphができました。

ステップ2. UVのタイリング

ここから最終的なインクシェーダーのShaderGraphをステップごとに解説します。

まずUVのタイリングですが、単純にUVに定数を乗算しているだけです。今回は下地のテクスチャ用とインク用で独立してタイリングできるようにしました。

UVのタイリング

ステップ3. インクの高さマップ用のノイズ生成

インクの高さマップはGradient Noiseから生成します。時間でアニメーションさせるために2つのGradient Noiseを線形補間で合成しています。

1つ目のGradient NoiseのUVは固定させておいて、2つ目のGradient NoiseのUVはtimeでスクロールさせています。

非常にシンプルな処理ですが、意外にもそれなりにインクっぽく見えるのではないでしょうか?

余談になりますが、ShaderGraphのGradient Noiseはシェーダーでプロシージャル生成しているのでGPU負荷も高いと思います。実用するなら軽量化のためにテクスチャのサンプリングに置き換えた方がいいかもしれません。

インクの高さマップ用のノイズ生成

ステップ4. 凹凸を考慮したインク判定のしきい値

ステップ3. でインクの高さマップを生成しました。この高さマップがしきい値以上ならインクの領域と見なすようにします。

インク判定のしきい値は定数でも良いのですが、高さマップを考慮してブロックの溝など低い部分の方がインクになりやすくします。

インクの高さマップ用のノイズ生成

高さマップの影響力はプロパティで制御できるようにしました。

高さマップの考慮がないと真っ平らなPlaneにインクが乗っているようで、雑コラ感・馴染まない感があります。

高さマップの考慮なし

高さマップを考慮すると、ブロックの凹凸を考慮してインクが広がるので、リアリティを少し向上できます。

高さマップの考慮あり

ステップ5. インクのマスク生成

「ステップ3のインクの高さマップ」から「ステップ4のしきい値」を引き算することで、インクのマスク画像を生成します。

そのままだとコントラストが薄いので、Powerノードでコントラストを強めに調整します。

インクのマスク生成

インクのマスクマップをLerpの引数にして、各種PBRパラメーターにインク用の値をブレンドします。 元はARMテクスチャの値をそのままPBRパラメーターとして渡していましたが、間にLerpノードを挟み込んで、インク用の Ambient Occlusion/Roughness/Metallic をブレンドできるようにしました。

インク用の設定をブレンド

BaseMapも同じようにLerpします。

インク用の設定をブレンド(BaseMap)

ステップ6. 法線の生成

法線の生成

ステップ3のインクの高さマップから法線を生成します。Normal From Heightノードがあるので利用します。

ステップ5のインクのマスクでは高さマップの影響で高周波成分が現れてしまうので滑らかな法線ができず、法線生成には不適切です。しきい値を引き算する前のGradient Noiseの値をNormal From Heightノードに繋ぎます。

今回もPowerノードでコントラストを調整可能にしました。SaturateノードではなくMaximumノードを利用しているのでは、 Clamp(x, 0, INF) にしたいからです。

マスク画像の結果は [0-1] に正規化する必要がありますが、法線生成のHeightマップであれば最大値の制限は不要だと思ったからです。

以上がインク用のシェーダーの解説でした。

まとめ

ShaderGraphだけでノーコードのインクシェーダーを試作しました。 PBRパラメーターを制御するだけのお手軽な実装ですが、思ったよりも良い見た目になったので満足です。

今回はインクのマスクにGradient Noiseを利用しましたが、RenderTextureをシェーダー外部から与えればインタラクティブにインクを塗ったりもできると思います。

comments powered by Disqus

gam0022.net's Tag Cloud