太郎Work

Unityとかで困ったこと等を残しておきます

ShaderGraphのPBRMasterNodeを拡張してみた

UniversalRPには標準でPBRMasterNodeがありますが、Fogの処理を変えたかったりほんの少し処理変えたかったりしたかったので自作のMasterNodeを作ってみました
速攻で仕様変わっていたらすみません...
Unity2019.4+ShaderGraph7.5.0+UniversalRP7.5.0

8/24追記
ShaderGraph9.x.xでは仕様が全く違うためここで作ったものは動きません...

はじめに

ShaderGraphのクラスはprivateになっているため、何かしらの方法でアクセスできるようにする必要があります

  • Graphicsリポジトリをフォークしてその中に直接スクリプトを追加する
  • AssemblyInfo.csに記載されている名前でasmdefを作成する

2個めはかなり無理矢理な手法になってしまうので直接スクリプトを追加する方針にしました

github.com
↑このリポジトリはクソデカなので落とすときは気をつけてください

最低限必要なファイルを複製する

My~とか適当な名前をつけて以下のファイルを複製します

PBRMasterGUI.cs
生成されたシェーダコードに使用されるMaterialEditor

f:id:tarowork:20200818165435p:plain
拡張しなければデフォルトのGUIで描画される

PBRMasterNode.cs
MasterNode実体 Slotの追加や設定で使用するシリアライズパラメータもここでもたせる

f:id:tarowork:20200818165403p:plain
Graphの終点に配置するコード生成を行うノード

PBRSettingsView.cs

f:id:tarowork:20200818165130p:plain
MasterNodeの右上のアイコンをクリックしたときのウィンドウ


PBRSubShader.cs
SubShader生成時の文字列生成ロジック(includeファイルとか)


これだけであとは型を置き換えればMasterNodeが完成します
ここからは機能を追加してみます

Culling設定をMaterial側に持たせる

PBRMasterNodeを使用する場合、Culling設定がSetting内にあるため、両面描画したいだけなのにシェーダを複製する必要があります(多分)

そこでCullパラメータをProperty化してMaterialで設定できるようにしてみます

PBRMasterNodeからTwo Sidedを削除

Settingsに入っていると紛らわしいのでまずはNode設定からは削除します

f:id:tarowork:20200818180620p:plain
TwoSidedの項目が消えます
bool GenerateShaderPass(MyPBRMasterNode masterNode, ShaderPass pass, GenerationMode mode,
    ShaderGenerator result, List<string> sourceAssetDependencyPaths)
{
    // TwoSideはMaterial側で設定する
    UniversalShaderGraphUtilities.SetRenderState(masterNode.surfaceType, masterNode.alphaMode, false, ref pass);

    // apply master node options to active fields
    var activeFields = GetActiveFieldsFromMasterNode(masterNode, pass);

    return GenerationUtils.GenerateShaderPass(masterNode, pass, mode, activeFields, result,
        sourceAssetDependencyPaths,
        UniversalShaderGraphResources.s_Dependencies, UniversalShaderGraphResources.s_ResourceClassName,
        UniversalShaderGraphResources.s_AssemblyName);
}

削除するとPBRSubShader.csのGenerateShaderPassでエラーが出ますが、変わりにfalseを入れておきます(特に使用されないのでなんでもOK)
SetRenderStateメソッド自体は `Cull Off` などの文字列を入れてくれる便利メソッドです

SubShaderにCull設定を追加

前項でTwoSided設定を使わないようにしたので自前で追加します
PBRSubShader.csにForwardPassの設定があるのでここに追記します

ShaderPass m_ForwardPass = new ShaderPass {
    ...
    CullOverride = "Cull[_Cull]",
}

~Overrideという名前のフィールドでShaderLab用の機能が定義されているので埋め込みたい文字列を入れるだけです(かんたん)

f:id:tarowork:20200818182323p:plain
PBRSubShaderで埋め込んだ文字列がShaderGraphの出力にそのまま入る
ShaderPropertiesにCull設定を追加

初期状態ではGraphEditorで追加したProperty項目のみがPropertiesに埋め込まれるので自分で追加する必要があります
追加するのは簡単でPBRMasterNode.csのCollectShaderPropertiesをoverrideして実装します

public override void CollectShaderProperties(PropertyCollector properties, GenerationMode generationMode)
{
    base.CollectShaderProperties(properties, generationMode);

    // 追加のProperty
    properties.AddShaderProperty(new Vector1ShaderProperty
    {
        // Material上の表示名
        displayName = "Cull Mode",
        // Property名
        overrideReferenceName = "_Cull",
        // 数値の扱いをEnumにする
        floatType = FloatType.Enum
        // Enumの扱いをCSharpの型から指定
        enumType = EnumType.CSharpEnum,
        // EnumType.CSharpEnumを指定した場合使用されるEnumの型
        cSharpEnumType = typeof(CullMode),
        // 初期値
        value = (int) CullMode.Back,
    });
}

今回はVector1ShaderPropertyを使用していますが、他のプロパティタイプも全て用意されています

f:id:tarowork:20200818192944p:plain
指定したとおりに文字列が出力される
f:id:tarowork:20200818193113p:plain
Material側にも項目がちゃんと出る

Fogを自前のものに置き換える

標準のFogは機能が微妙なので自前のものに置き換えてみます

fogのmulti_compileを更新

multi_compile系はSubShaderの機能なのでPBRSubShader.cs内を書き換えます

まずmulti_compile_fogはいらないのでpragmasから削除します

pragmas = new List<string>()
{
    "prefer_hlslcc gles",
    "exclude_renderers d3d11_9x",
    "target 3.0",
    "multi_compile_instancing",
},

次に新しい定義を追加します
こちらはEnum定義されているので書き間違いがないですね

keywords = new KeywordDescriptor[]
{
    new KeywordDescriptor()
    {
        displayName = "Custom Fog",
        referenceName = "CUSTOM_FOG",
        type = KeywordType.Boolean,
        definition = KeywordDefinition.MultiCompile,
        scope = KeywordScope.Global,
    },
    ...
}
f:id:tarowork:20200818194947p:plain
Keywordsにちゃんと登録されました
hlsl複製

シェーダファイルにkeywordが追加されたのでそれを参照するためにhlslファイル書き換える必要があります

Editor/ShaderGraph/Includes/PBRForwardPass.hlsl
このhlslファイルはvert関数とfrag関数が用意されておりMasterNodeがシェーダファイルを生成する際にこのファイルを参照します


次に生成時に使用するシェーダの読み込み先を変更します
SubShader単位の設定なのでkeywordsと同じくPBRSubShader.cs内を書き換えます

ShaderPass m_ForwardPass = new ShaderPass
{
    // Definition
    displayName = "My PBR Forward",
    referenceName = "SHADERPASS_FORWARD",
    lightMode = "UniversalForward",
    // vert,frag関数が定義されているファイルを指定
    passInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/MyPBRForwardPass.hlsl",

ここで指定したhlslファイルはvert, fragが定義されたファイルですが、それ以外の関数を使用する場合はincludesの方に追加します

// Pass setup
includes = new List<string>()
{
    "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl",
    "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl",
    "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl",
    "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl",
    "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl",
    "Packages/com.unity.shadergraph/ShaderGraphLibrary/ShaderVariablesFunctions.hlsl",
    // ここに呼び出したい関数が定義されたhlslファイルのパスを追加する
},

ここで指定したhlslファイル内の関数は全て使えるのでvert, frag内から意識せずに呼び出すことができます

シェーダ改修

最後にhlsl内のfragを書き換えます

// Unity標準のFog関数から自前の密度計算をしたFog関数に置き換え
float3 cameraPositionWS = GetCameraPositionWS();
color.rgb = ApplyDensityFog(cameraPositionWS, inputData.positionWS - cameraPositionWS, length(inputData.positionWS - cameraPositionWS), color.rgb);
//    color.rgb = MixFog(color.rgb, inputData.fogCoord);
return color;

Cutoffの数値でAlphaTest処理を分岐させる

ShaderGraphのAlphaTest仕様

PBRMasterNodeはビルド時にAlphaClipThreasholdのSlotを参照して0以上又は何かしらの接続がされている場合常にAlphaTestが実行される仕様になっています
f:id:tarowork:20200818221120p:plain
これはPBRSubShader.csのGetActiveFieldsFromMasterNodeメソッドで以下のように実装されています

if (masterNode.IsSlotConnected(PBRMasterNode.AlphaThresholdSlotId) ||
    masterNode.GetInputSlots<Vector1MaterialSlot>().First(x => x.id == PBRMasterNode.AlphaThresholdSlotId)
        .value > 0.0f)
{
    baseActiveFields.Add("AlphaClip");
}

ここでAlphaClipが定義されるとシェーダ生成時にShaderGraphが参照しているPassMesh.template内の$AlphaClipがアクティブになり以下の1行が埋め込まれる

#define _AlphaClip 1

これによりfrag関数内ではこのようにAlphaTestを実現することができる

#if _AlphaClip
    clip(surfaceDescription.Alpha - surfaceDescription.AlphaClipThreshold);
#endif

なんか微妙な仕様なのでそのうち仕様変更されそうな気がしますが、意図的に0を代入しない限り常にAlphaTestが行われていることが分かると思います

従来の処理に合わせて_ALPHATEST_ONマクロを定義する

Lit.shaderやその他Unity製のシェーダは_ALPHATEST_ONを使用して処理を切り替えているので、ShaderGraphの出力シェーダも同じになるように変更します

まずはFog処理の差し替えと同じようにKeywordDescriptorを追加します
AlphaTestはForwardPass以外のパスでも使用するため、以下の全Passに追加する必要があるので注意です
m_ForwardPass, m_DepthOnlyPass, m_ShadowCasterPass, m_LitMetaPass

new KeywordDescriptor()
{
    displayName = "Cutout",
    referenceName = "_ALPHATEST_ON",
    type = KeywordType.Boolean,
    definition = KeywordDefinition.ShaderFeature,
    scope = KeywordScope.Global,
},

Fog差し替え時に作成したhlslファイルのfrag関数内に_AlphaClipで切り替えている箇所があるので_ALPHATEST_ONに差し替える

#if _ALPHATEST_ON
    clip(surfaceDescription.Alpha - surfaceDescription.AlphaClipThreshold);
#endif


あとはshader_featureを切り替えるだけなのでいつもどおりShaderGUIでKeywordを切り替えるだけです

var propCutout = FindProperty("_Cutoff", properties);
if (propCutout != null)
{
    if (propCutout.floatValue > 0.001f)
    {
        material.EnableKeyword("_ALPHATEST_ON");
    }
    else
    {
        material.DisableKeyword("_ALPHATEST_ON");
    }
}

Universal Render Pipeline/LitシェーダではCutoffパラメータとAlphaTestの2つに分かれていますが、今回は_Cutoffが0なら適用しない実装にしました

おわり

ノリで書いたら説明不足のよくわかんない記事になった気がしますが今ならMasterNodeは割と簡単に作れるよって事が言いたかったです(昔は複雑で訳わかんなかった)