ShaderGraph自定义Master Node解析-URP(LWRP)

ShaderGraph自定义Master Node解析-URP(LWRP)

前言

ShaderGraph在Unity2019脱离了Preview,目前功能已经非常丰富了,预设结点数量够多,也提供了自定结点的接口,配合URP后可大大简化工作流,提高效率。

注:Unity在2019.3中已将LWRP升级并重命名为为Universal Render Pipeline,简称URP。见官方的说明,在未来有URP将会逐渐代替传统管线成为默认管线。

以后或许就是图程搭建渲染框架,编写好自定义Master Node,TA通过Shader Graph实现具体的功能了。

自定义Node可以通过官方的接口实现,也很简单。但显然在实际项目中我们还会有自定义Master Node的需要。对此,Unity提供了一种方法,即创建Unlit Graph,用CustomFunction Node实现自定义的光照,然后保存为Sub Graph使用。在这一篇博客中讲解了这种方法:

Custom lighting in shader graphblogs.unity3d.com

但是我觉得这种方法还是不够好,如果说要以ShaderGraph为中心开展工作,我们还需要能够指定更多的东西,比如Cull,ZWrite,ZTest。而这些选项在默认提供的Master Node下都是没有的。



而Unity确实也没有提供任何自定MasterNode的手段,好在ShaderGraph和URP的代码都是开源的,因此我们就来修改一下源代码让其支持更多设置。如图:


本文以实现Lambert+Blinn Phong光照为例,但限于代码全放出来篇幅会非常长,因此只会列举一小部分核心代码,其他部分参考核心代码的实现集可。


准备

修改尽量遵守以下原则:添加新的代码文件,而不修改已有的代码(或者尽量很少很简单),这样还能继续从版本更新中收益。

首先我习惯先将ShaderGraph和URP及其依赖的Core这三个包从项目的“Library/PackageCache"中移动到”Package”中,这样可以在工程中查看和引用这部分的代码。

新建类/接口需要按照源代码中同样的命名空间命名,而且需要放在源代码对应的目录下,因为源代码中类的访问级别都是缺省的internal级别。

开始前还需要先理清一下代码的结构,ShaderGraph这个包中只定义了Node的UI等。而在URP这个包中才实际上定义了将Master Node转化为代码的行为。因此我们先自定UI,再自定生成逻辑。


自定Node UI

在ShaderGraph包中,新建一个接口:

interface IMyLightingSubShader : ISubShader{}

接着新建一个类:

[Serializable,Title("Custom", "Master", "MyLighting")]
class MyLightingMasterNode : MasterNode<IMyLightingSubShader>, IMayRequirePosition,IMayRequireNormal,IMayRequireTangent{}

Title属性指定创建时在菜单中的标题和级别,至于后面的3个接口,在该Node需要这几个属性的时候加上,我们实现Lambert光照,肯定是需要考虑Normal和Tangent的。这3个接口的实现,可以和UnlitMasterNode.cs中一致,这里不放代码。

接下来,我们要创建在MasterNode UI上显示的属性的ID和名称:

public const string AlbedoSlotName = "Albedo";
public const string PositionName = "Vertex Position";

public const int AlbedoSlotId = 0;
public const int PositionSlotId = 1;

每个属性的ID需要不同,名字则是在UI上显示的内容。接着在UpdateNodeAfterDeserialization中用AddSlot指定端口的ID,名称,坐标系等,以及在构造函数中自动调用这个函数:


public MyLightingMasterNode()
{
    UpdateNodeAfterDeserialization();
}        

public sealed override void UpdateNodeAfterDeserialization()
{
    base.UpdateNodeAfterDeserialization();
    name = "My Lighting";

    //Vertex
    AddSlot(new PositionMaterialSlot(PositionSlotId, PositionName, PositionName, CoordinateSpace.Object,ShaderStageCapability.Vertex));

    //Fragment
    AddSlot(new ColorRGBMaterialSlot(AlbedoSlotId,AlbedoSlotName,AlbedoSlotName,SlotType.Input, Color.white, ColorMode.Default,ShaderStageCapability.Fragment));
            
    RemoveSlotsNameNotMatching(new[] {PositionSlotId, AlbedoSlotId,NormalSlotId});
}


最后还需要新建一个创建MyLighting结点的菜单:

[MenuItem("Assets/Create/Shader/Mylighting Graph", false, 208)]
public static void CreateToonShaderGraph()
{
      GraphUtil.CreateNewGraph(new MyLightingMasterNode());
}

按照相同的方法, 创建Vertex Normal,Vertex Tangent,Normal等属性:


返回Unity,即可创建MyLighting Master Graph:



自定右键菜单

这里以加入Cull Mode选项为例,首先在MyLightingMasterNode中,定义Cull Mode属性:

[SerializeField] private SurfaceMaterialOptions.CullMode m_CullMode;
public SurfaceMaterialOptions.CullMode cullMode{
    get { return m_CullMode; }
    set
    {
        if (m_CullMode == value) return;
        m_CullMode = value;
        Dirty(ModificationScope.Graph);
        }
     }
}


接着重载VisualElement函数:

protected override VisualElement CreateCommonSettingsElement()
{
    return new MyLightingSettingView(this);
}


MyLightingSettingView是我们创建的另外一个新的类,其中定义点击设置按钮后的UI:

class MyLightingSettingView : VisualElement
{
    private MyLightingMasterNode m_Node;

    public MyLightingSettingView(MyLightingMasterNode node)
    {
        m_Node = node;

        PropertySheet ps = new PropertySheet();

        ps.Add(new PropertyRow(new Label("Cull")), row =>
        {
            row.Add(new EnumField(SurfaceMaterialOptions.CullMode.Back), field =>
            {
                field.value = m_Node.cullMode;
                field.RegisterValueChangedCallback(ChangeCullMode);
            });
        });
        Add(ps);
    }

    void ChangeCullMode(ChangeEvent<Enum> evt)
    {
        if (Equals(m_Node.cullMode, evt.newValue)) return;

        m_Node.owner.owner.RegisterCompleteObjectUndo("CullMode Change");
        m_Node.cullMode = (SurfaceMaterialOptions.CullMode) evt.newValue;
    }
}

Cull Mode改变时,我们可以在回调中注册Undo允许撤销,再通知Node类修改cullmode属性。


同理,我们可以添加SurfaceType,ZWrite,ZTest的设置,最终结果如下:



生成Shader代码

现在有了可以看的UI了,接下来我们需要修改Universal RP这个包,改变生成Shader代码的逻辑。

首先创建一个UniversalMyLightingSubShader类:

[Serializable]
class UniversalMyLightingSubShader : IMyLightingSubShader{
    public bool IsPipelineCompatible(RenderPipelineAsset renderPipelineAsset)
    {
        return renderPipelineAsset is UniversalRenderPipelineAsset;
    }

    public int GetPreviewPassIndex()
    {
        return 0;
    }
}

IsPipelineCompatible用于判断当前管线是否是URP,GetPreviewPassIndex用于返回在ShaderGraph预览窗口中使用的Pass:


Preview窗口


接着我们要先定义一个Lighting的Pass,这里指定的内容都比较好理解,最后会被生成到Pass的代码中:

ShaderPass m_LambertPass = new ShaderPass(){
    displayName = "Pass",
    referenceName = "SHADERPASS_MyLighting",
    passInclude = "Assets/MyLightingPass.hlsl",
    varyingsInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/Varyings.hlsl",
    useInPreview = true,
    vertexPorts = new List<int>(){
         MyLightingMasterNode.PositionSlotId,
         //...省略
    },
    pixelPorts = new List<int>(){
         MyLightingMasterNode.AlbedoSlotId,
         //...省略
    },
    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/ShaderGraphFunctions.hlsl",
     },
     pragmas = new List<string>()
     {
          "target 3.0",
          "multi_compile_instancing",
     },
     keywords = new KeywordDescriptor[]{},
     requiredAttributes = new List<string>()
     {
        "Varyings.normalWS",  
        "Varyings.tangentWS",
        "Varyings.bitangentWS",
        "Varyings.viewDirectionWS"
     },
}

可以定义多个Pass,比如Depth Only,Meta Pass。要解释一下passInclude指定了一个新的hlsl文件,这是我们编写顶点着色器和片段着色器的地方,我们最后再创建。

定义好Pass后,在GetSubshader这个接口中正式开始创建Sub Shader代码:

public string GetSubshader(IMasterNode masterNode, GenerationMode mode, List<string> sourceAssetDependencyPaths = null)
{
    var lambertMasterNode = masterNode as MyLightingMasterNode;
    var subShader = new ShaderGenerator();

    subShader.AddShaderChunk("SubShader", true);
    subShader.AddShaderChunk("{", true);

    subShader.Indent();
    {
        var surfaceTags = ShaderGenerator.BuildMaterialTags(lambertMasterNode.surfaceType);
        var tagsBuilder = new ShaderStringBuilder(0);
        surfaceTags.GetTags(tagsBuilder, "UniversalPipeline");
        subShader.AddShaderChunk(tagsBuilder.ToString());

        GenerateShaderPass(lambertMasterNode, m_LambertPass, mode, subShader, sourceAssetDependencyPaths);
     }
        subShader.Deindent();
        subShader.AddShaderChunk("}", true);

        return subShader.GetShaderString(0);
}

核心部分是GenerateShaderPass()这一行用于创建Pass内的代码:

private static bool GenerateShaderPass(MyLightingMasterNode masterNode, ShaderPass pass, GenerationMode mode, ShaderGenerator result, List<string> sourceAssetDependencyPaths)
{
    CustomUniversalShaderGraphUtilities.SetRenderState(masterNode.surfaceType, masterNode.alphaMode, masterNode.cullMode, masterNode.zWrite, masterNode.zTest, ref pass);

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

    // use standard shader pass generation
    return GenerationUtils.GenerateShaderPass(masterNode, pass, mode, activeFields, result, sourceAssetDependencyPaths,UniversalShaderGraphResources.s_Dependencies, UniversalShaderGraphResources.s_ResourceClassName, UniversalShaderGraphResources.s_AssemblyName);
}

总共是3行代码,第一行和第二行需要解释一下,第一行CustomUniversalShaderGraphUtilities.SetRenderState这里是完全重写了源代码中的UniversalShaderGraphUtilities.SetRenderState,在源代码的实现中只能通过Two Sided指定Cull Off或Cull Back。修改后让Cull,ZTest,ZWrite都能自己指定即可。这个函数的实现其实很简单,但是代码很长这里就不放了,可以参考源代码进行修改。


第二行代码调用的函数实际上是这个:

private static ActiveFields GetActiveFieldsFromMasterNode(MyLightingMasterNode masterNode, ShaderPass pass)
{
    var activeFields = new ActiveFields();
    var baseActiveFields = activeFields.baseInstance;

    // Graph Vertex
    if (masterNode.IsSlotConnected(MyLightingMasterNode.PositionSlotId))
        baseActiveFields.Add("features.graphVertex");

    // Graph Pixel (always enabled)
    baseActiveFields.Add("features.graphPixel");

    // NormalMap
    if (masterNode.IsSlotConnected(MyLightingMasterNode.NormalSlotId))
         baseActiveFields.Add("Normal");

    return activeFields;
}

在这里我们根据Node的属性状态添加一些内置的字符串,在最后生成代码的时候,这些字符串会最终转化成对应的代码语句,我们可以在”Packages\com.unity.shadergraph\Editor\Templates\PassMesh.template“中查看这些字符串对应的代码:


比如说字符串"Normal”,其实最终会转化为#define _NORMALMAP 1。


最后一步,我们开始编写自己的光照代码了,在定义pass的时候我们指定了一个passInclude这个属性为一个新的文件"Assets/MyLightingPass.hlsl",在这里编写我们自己的Vertex 和 Fragment Shader:

PackedVaryings vert(Attributes input)
{
    Varyings output = (Varyings)0;
    output = BuildVaryings(input);
    PackedVaryings packedOutput = PackVaryings(output);
    return packedOutput;
}
half4 frag(PackedVaryings packedInput): SV_TARGET
{
    Varyings unpacked = UnpackVaryings(packedInput);
    UNITY_SETUP_INSTANCE_ID(unpacked);
    
    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);
    
    //Alpha Clip等,省略
    
    //lambert : albedo * ndl * light.color
    float3 albedo = surfaceDescription.Albedo;
    float3 normalWS = GetNormal(unpacked,surfaceDescription.Normal);
    Light mainLight = GetMainLight();
    float ndl = max(0,dot(normalWS, mainLight.direction));
    float3 diffuse = albedo * ndl * mainLight.color;
    
    //Blinn phong : specular * pow(ndh,shineiness) * light.color 
    float3 specular = surfaceDescription.Specular;
    float shininess = surfaceDescription.Shininess;
    float3 viewDirWS = GetViewDirWS(unpacked);
    float3 h = normalize(viewDirWS + mainLight.direction);
    float ndh = max(0,dot(normalWS,h));
    float3 spec = specular * pow(ndh,shininess) * mainLight.color;
    
    return half4(diffuse + spec, surfaceDescription.Alpha);
}

我们自己实现一个Lambert和Blinn Phong作为Master Node的shader逻辑,最后如果一切都没错的话,就能得到一个正确的Master Node了:



后续问题

通过自行增加新的文件成功创建了自定义MasterNode,但是在实践的过程中还是有一些小问题比较恼人,这些问题我将他们都放出来,有一些我已经找到了解决办法,但是有一些暂时还没有更好的方法。

  • 如何在更新MasterNode后,让所有ShaderGraph重新生成代码?我只知道单个Shader可以点击左上角Save Asset,所有的话或许需要ReImport,我猜测有一个版本号可以控制,目前还没试验。
  • 着色器改正确后但ShaderGraph文件上仍然显示编译错误?关掉Unity后将“Library/ShaderCache"删除后重开。
  • 修改着色器后,但是ShaderGraph的Preview面板不刷新?需要重新打开窗口。

编辑于 2019-10-09

文章被以下专栏收录

    本专栏也算是我的学习渲染的过程中对重点知识的笔记。参考书籍有:《LearnOpenGL》和其中文版、《Mathematics for 3D Game Programming and Computer Graphics》、Unity官方教程和其他互联网资料。