これはUnity Advent Calendar 2022の22日目の記事です。
スプラトゥーン3、けっこう面白いですね。過去作の1,2は未プレイでしたが、3からスプラデビューしました。
スプラ3で遊びながら、インクシェーダーの実装方法に興味が出てきたので、UnityのShaderGraphでそれっぽいものを実装してみました。
ShaderGraphの基本機能だけで構成されており、ノードの量も少なめにしました。
ShaderGraphの基本操作は解説しませんが、なるべく丁寧に説明をしたつもりなので、ShaderGraphの入門記事として参考にしてください!
UnityのShaderGraphでインクシェーダーを試作#Unity3D #ShaderGraph pic.twitter.com/PHxIkfnkiQ
— がむ (@gam0022) December 23, 2022
色変更
しきい値の調整
- Unityプロジェクト
- WebGLデモ
- WASDと右クリックのドラッグでカメラ操作
ShaderGraph全体
ShaderGraphの全体です。
ShaderGraphのスクロール領域を含めてキャプチャするために Cyanilux/ShaderGraphToPNG というUnityのパッケージを利用しました。
完璧なソリューションがありました。
— がむ (@gam0022) December 26, 2022
Unity2021.3.15f1 + URP12.1.8 でも完璧に動作!https://t.co/3J2qsrye8s
基本方針
- URPのLitグラフに与えるBaseColorやSmoothnessや法線をいい感じに制御してインクっぽくする
- カスタムなシェーディングはしない
- インクの高さマップはGradient Noiseからプロシージャル生成
- インタラクティブなインク制御は未対応
- RenderTextureを生成してペイントするようなアプローチでインタラクティブにできそう(今後の課題)
チュートリアル
そこまで規模の大きくないShaderGraphですが、理解しやすいように1ステップごと解説します。
ステップ1. PBRテクスチャに対応
まずはPBRテクスチャに対応します。
PBRテクスチャは以下のサイトからお借りしました。とても良い感じのCC0ライセンスの床のタイル素材を利用させていただきました。
これがPBRテクスチャをプロパティにして、ShaderGraphの各種PBRパラメーターを渡すだけのShaderGraphです。
BaseColor
や Normal
はそのままノードを繋げるだけで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に定数を乗算しているだけです。今回は下地のテクスチャ用とインク用で独立してタイリングできるようにしました。
ステップ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します。
ステップ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をシェーダー外部から与えればインタラクティブにインクを塗ったりもできると思います。