文章

定制ShaderGraph(二)---添加自定义属性

定制ShaderGraph(二)---添加自定义属性

定制ShaderGraph(二)—添加自定义属性

00 前置知识

  • 首先, 自定义的属性应该有一个自己的Foldout来折叠起来
  • 然后, 自定义的属性应该可以点击后增加输入节点

01 实施

将自定义属性写到面板上

  • UniversalVehiclePaintSubTarget中添加代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    
    sealed class UniversalVehiclePaintSubTarget : UniversalSubTarget
    {
    	...
      	
    	[SerializeField] bool m_BakedGI = false;	// 面板bool值, 为true代表激活输入节点, false则取消激活输入节点
              
        [SerializeField] bool m_ExternalFoldoutOn = false; // Foldout折叠用的bool值
      
    	...
              
        // 对应的属性声明
        public bool bakedGI
        {
            get => m_BakedGI;
            set => m_BakedGI = value;
        }
              
        public bool externalFoldoutOn
        {
            get => m_ExternalFoldoutOn;
            set => m_ExternalFoldoutOn = value;
        }
          
        ...
          
        // ShaderGraph输出框的参数口, 及出现条件
        public override void GetActiveBlocks(ref TargetActiveBlockContext context)
        {
            ...
            // 当面板上的bakedGI为true的时候, 这个属性就出现, 要注意, 此时为false的时候, 只会变灰, 而不会去掉
            // 为了避免污染原来的类, 所以新建了一个ExternalBlockFields的类用来存储自定义的节点
            context.AddBlock(ExternalBlockFields.SurfaceDescription.BakedGI, bakedGI);
        }
          
        ...
              
        // UI绘制
        public override void GetPropertiesGUI(ref TargetPropertyGUIContext context, Action onChange,
            Action<String> registerUndo)
        {
            ...
            // 自定义 External Control 块, 用函数嵌入, 最小的破坏原有结构
            DrawExternalControlGUI(ref context, onChange, registerUndo);
    	}
          
         private void DrawExternalControlGUI(ref TargetPropertyGUIContext context, Action onChange, Action<string> registerUndo)
        {
            // 折叠面板
            var external = new Foldout { value = externalFoldoutOn };
            context.AddFoldout(
                "External Control", 
                external, 
                indentLevel: 0, 	// 缩进定义i
                labelColor: CustomStyles.FoldoutColor,	// 颜色在外部类CustomStyles定义
                callback: evt => {
                externalFoldoutOn = evt.newValue;
                onChange();
            });
      
            // 折叠打开时展示 Baked GI 开关
            if (externalFoldoutOn)
            {
                context.AddProperty(
                    "Baked GI",
                    1,	// 折叠内部的缩进通常要+1
                    new Toggle { value = bakedGI },
                    evt =>
                    {
                        if (bakedGI == evt.newValue) return;
                        registerUndo("Change Baked GI");
                        bakedGI = evt.newValue;
                        onChange();
                    });
            }
        }
          
        // ─── 样式与常量 ─────────────────────────────────
        static class CustomStyles
        {
            public static readonly Color FoldoutColor = new Color(0.3294f, 0.7255f, 0.8196f);
            public const string ExternalFoldoutName = "ExternalControlFoldout";
        }
    }
      
    /// <summary>
    /// 用于声明自定义的ShaderGraph输出的属性节点 <br/>
    /// Unity源文件在com.unity.shadergraph@12.1.10/Editor/Generation/TargetResources/BlockFields.cs
    /// </summary>
    ///
    static class ExternalBlockFields
    {
        /// <summary>
        /// 自定义的表面参数
        /// </summary>
        [GenerateBlocks("External")]	// "External"为在ShaderGraph里面手动添加时的菜单路径
        public struct SurfaceDescription
        {
            public static string name = "SurfaceDescription";
            // 此时的ColorControl是为了确定默认初始值, 后面的True表示是HDR颜色, 其中, 第二个参"BakedGI", 
            // 就是可以通过SurfaceDescription.BakedGI在hlsl中访问到的值
            // 千万千万注意, 这个值要保证全项目唯一, 因为实际文件构建的时候就是靠这个值来确定Block的.
            public static BlockFieldDescriptor BakedGI = new(name, "BakedGI", "Baked GI",
                "SURFACEDESCRIPTION_BAKEDGI", new ColorControl(new Color(1,1,1,0),true), ShaderStage.Fragment); 
        }
    }
    
  • 如果要让自定义的字段能够自动”出现”和”去掉”, 那么就需要在LitBlockMasks类中添加BlockFieldDescriptor[], 当然, 我们为了最小侵入, 新建一个类ExternalBlockMasks, 然后声明一个BlockFieldDescriptor[], 将ExternalBlockFields.SurfaceDescription.BakedGI加入. 同时, 要在构建的SubShader的时候, 调用这个Block, 此时, 才可以触发重排, 也就是视觉上的”出现”和”去掉”.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    static class LitBlockMasks
    {
    	...
    }
      
     static class ExternalBlockMasks
     {
         ...
         public static readonly BlockFieldDescriptor[] FragmentVehiclePaint = new BlockFieldDescriptor[]
        {
            BlockFields.SurfaceDescription.BaseColor,
            BlockFields.SurfaceDescription.NormalOS,
            BlockFields.SurfaceDescription.NormalTS,
            BlockFields.SurfaceDescription.NormalWS,
            BlockFields.SurfaceDescription.Emission,
            BlockFields.SurfaceDescription.Metallic,
            BlockFields.SurfaceDescription.Specular,
            BlockFields.SurfaceDescription.Smoothness,
            BlockFields.SurfaceDescription.Occlusion,
            BlockFields.SurfaceDescription.Alpha,
            BlockFields.SurfaceDescription.AlphaClipThreshold,
            BlockFields.SurfaceDescription.CoatMask,
            BlockFields.SurfaceDescription.CoatSmoothness,
      
            #region CustomAddCode
      
            ExternalBlockFields.SurfaceDescription.BakedGI
      
            #endregion
        };
     }
      
    // 在创建SubShader的时候, 将ExternalBlockMasks.FragmentVehiclePaint作为参数传入, 注意, 与LitGLESSubShader对应的
    // LitComputeDotsSubShader中也有类似的代码, 如果你要考虑写Dot适配的, 那么在LitComputeDotsSubShader函数中也要进行对应
    // 的修改
    public static SubShaderDescriptor LitGLESSubShader(UniversalTarget target, WorkflowMode workflowMode,
                string renderType, string renderQueue, bool complexLit)
    {
        ...
        if (complexLit)
            result.passes.Add(ExternalPasses.ForwardOnly(target, workflowMode, complexLit, CoreBlockMasks.Vertex,
                // CustomAddCode
                // ExternalBlockMasks.FragmentComplexLit, CorePragmas.Forward));
                ExternalBlockMasks.FragmentVehiclePaint, CorePragmas.Forward));
    	else
        	result.passes.Add(ExternalPasses.Forward(target, workflowMode));
    }
    
  • 至此, 我们完成了面板和输入节点的自定义 image-20250804164306677

应用自定义属性到最终着色器中

  • 首先, 一些前置准备工作, 在我们拷贝出来的UniversalVehiclePaintSubTarget.cs文件中的Setup()函数中

    注释掉context.AddSubShader(PostProcessSubShader(SubShader.LitComputeDotsSubShader(target, workflowMode, // target.renderType, target.renderQueue, complexLit)));这一行, 原因是因为目前我对于Dot不太了解, 并且前期并不会对Dot进行适配

    1
    2
    3
    4
    5
    6
    7
    
    // Process SubShaders
    // Modify by yumiao 由于没有做Dot相关的适配, 所以暂时不加入Dot的相关SubShader
    // context.AddSubShader(PostProcessSubShader(SubShader.LitComputeDotsSubShader(target, workflowMode,
    //     target.renderType, target.renderQueue, complexLit)));
    // End Add
    context.AddSubShader(PostProcessSubShader(SubShader.LitGLESSubShader(target, workflowMode,
        target.renderType, target.renderQueue, complexLit)));
    

    注释的部分对应的调用代码是在分离出的SubShader.cs

    函数public static SubShaderDescriptor LitComputeDotsSubShader(UniversalTarget target, WorkflowMode workflowMode,string renderType, string renderQueue, bool complexLit)

    如果, 你想兼容Dot相关的SubShader, 那么可以不注释, 但需要将函数中的这部分代码换成你自定义的代码

    image-20250819134247399

02 代码生成

探寻是如何生成代码的

  • Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Importers/ShaderGraphImporterEditor.cs
    • 文件中 image-20250804175706409

Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/Generator.cs

  • 文件中有个BuildShader()函数, 貌似是总调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    
    void BuildShader()
    {
        var activeNodeList = Pool.ListPool<AbstractMaterialNode>.Get();
        bool ignoreActiveState = (m_Mode == GenerationMode.Preview);  // for previews, we ignore node active state
        if (m_OutputNode == null)
        {
            foreach (var block in m_ActiveBlocks)
            {
                // IsActive is equal to if any active implementation has set active blocks
                // This avoids another call to SetActiveBlocks on each TargetImplementation
                if (!block.isActive)
                    continue;
      
                NodeUtils.DepthFirstCollectNodesFromNode(activeNodeList, block, NodeUtils.IncludeSelf.Include, ignoreActiveState: ignoreActiveState);
            }
        }
        else
        {
            NodeUtils.DepthFirstCollectNodesFromNode(activeNodeList, m_OutputNode, ignoreActiveState: ignoreActiveState);
        }
      
        var shaderProperties = new PropertyCollector();
        var shaderKeywords = new KeywordCollector();
        m_GraphData.CollectShaderProperties(shaderProperties, m_Mode);
        m_GraphData.CollectShaderKeywords(shaderKeywords, m_Mode);
      
        var graphInputOrderData = new List<GraphInputData>();
        foreach (var cat in m_GraphData.categories)
        {
            foreach (var input in cat.Children)
            {
                graphInputOrderData.Add(new GraphInputData()
                {
                    isKeyword = input is ShaderKeyword,
                    referenceName = input.referenceName
                });
            }
        }
        string path = AssetDatabase.GUIDToAssetPath(m_GraphData.assetGuid);
      
        // Send an action about our current variant usage. This will either add or clear a warning if it exists
        var action = new ShaderVariantLimitAction(shaderKeywords.permutations.Count, ShaderGraphPreferences.variantLimit);
        m_GraphData.owner?.graphDataStore?.Dispatch(action);
      
        if (shaderKeywords.permutations.Count > ShaderGraphPreferences.variantLimit)
        {
            string graphName = "";
            if (m_GraphData.owner != null)
            {
                if (path != null)
                {
                    graphName = Path.GetFileNameWithoutExtension(path);
                }
            }
            Debug.LogError($"Error in Shader Graph {graphName}:{ShaderKeyword.kVariantLimitWarning}");
      
            m_ConfiguredTextures = shaderProperties.GetConfiguredTextures();
            m_Builder.AppendLines(ShaderGraphImporter.k_ErrorShader.Replace("Hidden/GraphErrorShader2", graphName));
            // Don't continue building the shader, we've already built an error shader.
            return;
        }
      
        foreach (var activeNode in activeNodeList.OfType<AbstractMaterialNode>())
        {
            activeNode.SetUsedByGenerator();
            activeNode.CollectShaderProperties(shaderProperties, m_Mode);
        }
      
        // Collect excess shader properties from the TargetImplementation
        foreach (var target in m_Targets)
        {
            // TODO: Setup is required to ensure all Targets are initialized
            // TODO: Find a way to only require this once
            TargetSetupContext context = new TargetSetupContext();
            target.Setup(ref context);
      
            target.CollectShaderProperties(shaderProperties, m_Mode);
        }
      
        // set the property collector to read only
        // (to ensure no rogue target or pass starts adding more properties later..)
        shaderProperties.SetReadOnly();
      
        m_Builder.AppendLine(@"Shader ""{0}""", m_Name);
        using (m_Builder.BlockScope())
        {
            GenerationUtils.GeneratePropertiesBlock(m_Builder, shaderProperties, shaderKeywords, m_Mode, graphInputOrderData);
            for (int i = 0; i < m_Targets.Length; i++)
            {
                TargetSetupContext context = new TargetSetupContext(m_assetCollection);
      
                // Instead of setup target, we can also just do get context
                m_Targets[i].Setup(ref context);
      
                var subShaderProperties = GetSubShaderPropertiesForTarget(m_Targets[i], m_GraphData, m_Mode, m_OutputNode, m_TemporaryBlocks);
                foreach (var subShader in context.subShaders)
                {
                    GenerateSubShader(i, subShader, subShaderProperties);
                }
      
                var customEditor = context.defaultShaderGUI;
                if (customEditor != null && m_Targets[i].WorksWithSRP(GraphicsSettings.currentRenderPipeline))
                {
                    m_Builder.AppendLine("CustomEditor \"" + customEditor + "\"");
                }
      
                foreach (var rpCustomEditor in context.customEditorForRenderPipelines)
                {
                    m_Builder.AppendLine($"CustomEditorForRenderPipeline \"{rpCustomEditor.shaderGUI}\" \"{rpCustomEditor.renderPipelineAssetType}\"");
                }
      
                m_Builder.AppendLine("CustomEditor \"" + typeof(GenericShaderGraphMaterialGUI).FullName + "\"");
            }
      
            m_Builder.AppendLine(@"FallBack ""Hidden/Shader Graph/FallbackError""");
        }
      
        m_ConfiguredTextures = shaderProperties.GetConfiguredTextures();
    }
    
  • 其中的关键数据结构是GraphData : JsonObject

    • 路径是Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Data/Graphs/GraphData.cs
    • 值得注意的是, 这是个partial class, 由五个部分组成 image-20250804180559252
  • 由于我们不关心节点部分的代码, 实际上模板部分的代码是
    1
    2
    3
    4
    5
    6
    7
    8
    
     // Process Template
    Profiler.BeginSample("ProcessTemplate");
    var templatePreprocessor = new ShaderSpliceUtil.TemplatePreprocessor(activeFields, spliceCommands,
        isDebug, sharedTemplateDirectories, m_assetCollection, m_humanReadable);
    templatePreprocessor.ProcessTemplateFile(passTemplatePath);
    m_Builder.Concat(templatePreprocessor.GetShaderCode());
      
    Profiler.EndSample();
    

Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/ShaderSpliceUtil.cs

  • 通过templatePreprocessor.GetShaderCode()这个方法, 我们找到了ShaderSpliceUtil.cs
    1
    2
    3
    4
    
    public ShaderStringBuilder GetShaderCode()
    {
        return result;
    }
    
  • 其中result
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    public TemplatePreprocessor(ActiveFields activeFields, Dictionary<string, string> namedFragments, bool isDebug, string[] templatePaths, AssetCollection assetCollection, bool humanReadable, ShaderStringBuilder outShaderCodeResult = null)
    {
        this.activeFields = activeFields;
        this.namedFragments = namedFragments;
        this.isDebug = isDebug;
        this.templatePaths = templatePaths;
        this.assetCollection = assetCollection;
        this.result = outShaderCodeResult ?? new ShaderStringBuilder(humanReadable: humanReadable);
        includedFiles = new HashSet<string>();
    }
    
  • ShaderStringBuilder这个类看起来就很像我们要找的目标了 路径: Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/ShaderStringBuilder.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public ShaderStringBuilder(int indentationLevel = 0, int stringBuilderSize = 8192, bool humanReadable = false)
    {
        IncreaseIndent(indentationLevel);
        m_StringBuilder = new StringBuilder(stringBuilderSize);
        m_ScopeStack = new Stack<ScopeType>();
        m_Mappings = new List<ShaderStringMapping>();
        m_CurrentMapping = new ShaderStringMapping();
        m_HumanReadable = humanReadable;
    }
    
  • 回到Generator.cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    // Render State
    Profiler.BeginSample("RenderState");
    using (var renderStateBuilder = new ShaderStringBuilder(humanReadable: m_humanReadable))
    {
        // Render states need to be separated by RenderState.Type
        // The first passing ConditionalRenderState of each type is inserted
        foreach (RenderStateType type in Enum.GetValues(typeof(RenderStateType)))
        {
            var renderStates = pass.renderStates?.Where(x => x.descriptor.type == type);
            if (renderStates != null)
            {
                foreach (RenderStateCollection.Item renderState in renderStates)
                {
                    if (renderState.TestActive(activeFields))
                    {
                        // 这一行应该就是把代码加入
                        renderStateBuilder.AppendLine(renderState.value);
      
                        // Cull is the only render state type that causes a compilation error
                        // when there are multiple Cull directive with different values in a pass.
                        if (type == RenderStateType.Cull)
                            break;
                    }
                }
            }
        }
      
        // 这一行是把对应的部分换成加入后的代码
        string command = GenerationUtils.GetSpliceCommand(renderStateBuilder.ToCodeBlock(), "RenderState");
        // 这一行应该是应用
        spliceCommands.Add("RenderState", command);
    }
    Profiler.EndSample();
    

    要替换的模板路径是: Library/PackageCache/com.unity.render-pipelines.universal@12.1.10/Editor/ShaderGraph/Templates/ShaderPass.template

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // 这三个函数应该就是生成SubShader
      
    // 此函数被BuildShader调用, 又调用了GenerateShaderPass
    void GenerateSubShader(int targetIndex, SubShaderDescriptor descriptor, PropertyCollector subShaderProperties)
      
    // 此函数被BuildShader调用
    static PropertyCollector GetSubShaderPropertiesForTarget(Target target, GraphData graph, GenerationMode generationMode, AbstractMaterialNode outputNode, List<BlockNode> outTemporaryBlockNodes)
      
    void GenerateShaderPass(int targetIndex, PassDescriptor pass, ActiveFields activeFields, List<BlockFieldDescriptor> currentBlockDescriptors, PropertyCollector subShaderProperties)
    
    • GenerateSubShaderTags()函数用来生成SubShader的Tags 路径: Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/GenerationUtils.cs

      1
      
      internal static void GenerateSubShaderTags(Target target, SubShaderDescriptor descriptor, ShaderStringBuilder builder)
      
    • Pass的模板在Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Templates/PassMesh.template

关于PackedVarings的来历

  • 首先, 在Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/GenerationUtils.cs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    
    internal static void GeneratePackedStruct(StructDescriptor shaderStruct, ActiveFields activeFields, out StructDescriptor packStruct)
    {
        packStruct = new StructDescriptor()
        {
            // 这里提到了将"Packed"与"shaderStruct.name", 结合起来作为Struct的名字
            name = "Packed" + shaderStruct.name,
            packFields = true,
            fields = new FieldDescriptor[] { }
        };
        List<FieldDescriptor> packedSubscripts = new List<FieldDescriptor>();
        List<FieldDescriptor> postUnpackedSubscripts = new List<FieldDescriptor>();
        List<int> packedCounts = new List<int>();
        foreach (FieldDescriptor subscript in shaderStruct.fields)
        {
            var fieldIsActive = false;
            var keywordIfDefs = string.Empty;
      
            if (activeFields.permutationCount > 0)
            {
                //find all active fields per permutation
                var instances = activeFields.allPermutations.instances
                    .Where(i => IsFieldActive(subscript, i, subscript.subscriptOptions.HasFlag(StructFieldOptions.Optional))).ToList();
                fieldIsActive = instances.Count > 0;
                if (fieldIsActive)
                    keywordIfDefs = KeywordUtil.GetKeywordPermutationSetConditional(instances.Select(i => i.permutationIndex).ToList());
            }
            else
                fieldIsActive = IsFieldActive(subscript, activeFields.baseInstance, subscript.subscriptOptions.HasFlag(StructFieldOptions.Optional));
            //else just find active fields
      
            if (fieldIsActive)
            {
                // special case, "UNITY_STEREO_INSTANCING_ENABLED" fields must be packed at the end of the struct because they are system generated semantics
                //
                if (subscript.HasPreprocessor() && (subscript.preprocessor.Contains("INSTANCING")))
                    postUnpackedSubscripts.Add(subscript);
                // special case, "SHADER_STAGE_FRAGMENT" fields must be packed at the end of the struct,
                // otherwise the vertex output struct will have different semantic ordering than the fragment input struct.
                //
                else if (subscript.HasPreprocessor() && (subscript.preprocessor.Contains("SHADER_STAGE_FRAGMENT")))
                    postUnpackedSubscripts.Add(subscript);
                else if (subscript.HasSemantic() || subscript.vectorCount == 0)
                    packedSubscripts.Add(subscript);
                else
                {
                    // pack float field
                    int vectorCount = subscript.vectorCount;
                    // super simple packing: use the first interpolator that has room for the whole value
                    int interpIndex = packedCounts.FindIndex(x => (x + vectorCount <= 4));
                    int firstChannel;
                    if (interpIndex < 0 || subscript.HasPreprocessor())
                    {
                        // allocate a new interpolator
                        interpIndex = packedCounts.Count;
                        firstChannel = 0;
                        packedCounts.Add(vectorCount);
                    }
                    else
                    {
                        // pack into existing interpolator
                        firstChannel = packedCounts[interpIndex];
                        packedCounts[interpIndex] += vectorCount;
                    }
                }
            }
        }
        for (int i = 0; i < packedCounts.Count(); ++i)
        {
            // todo: ensure this packing adjustment doesn't waste interpolators when many preprocessors are in use.
            var packedSubscript = new FieldDescriptor(packStruct.name, "interp" + i, "", "float" + packedCounts[i], "INTERP" + i, "", StructFieldOptions.Static);
            packedSubscripts.Add(packedSubscript);
        }
        packStruct.fields = packedSubscripts.Concat(postUnpackedSubscripts).ToArray();
    }
    
  • 然后, 在Library/PackageCache/com.unity.render-pipelines.universal@12.1.10/Editor/ShaderGraph/UniversalStructs.cs中又有, 其中nameVarings, 所以, 合在一起就是PackedVarings
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    static class UniversalStructs
    {
        public static StructDescriptor Varyings = new StructDescriptor()
        {
            name = "Varyings",
            packFields = true,
            populateWithCustomInterpolators = true,
            fields = new FieldDescriptor[]
            {
                StructFields.Varyings.positionCS,
                StructFields.Varyings.positionWS,
                StructFields.Varyings.normalWS,
                StructFields.Varyings.tangentWS,
                StructFields.Varyings.texCoord0,
                StructFields.Varyings.texCoord1,
                StructFields.Varyings.texCoord2,
                StructFields.Varyings.texCoord3,
                StructFields.Varyings.color,
                StructFields.Varyings.viewDirectionWS,
                StructFields.Varyings.screenPosition,
                UniversalStructFields.Varyings.staticLightmapUV,
                UniversalStructFields.Varyings.dynamicLightmapUV,
                UniversalStructFields.Varyings.sh,
                UniversalStructFields.Varyings.fogFactorAndVertexLight,
                UniversalStructFields.Varyings.shadowCoord,
                StructFields.Varyings.instanceID,
                UniversalStructFields.Varyings.stereoTargetEyeIndexAsBlendIdx0,
                UniversalStructFields.Varyings.stereoTargetEyeIndexAsRTArrayIdx,
                StructFields.Varyings.cullFace,
            }
        };
    }
    

关于SurfaceDescription的来历

目前能查到的是Library/PackageCache/com.unity.render-pipelines.universal@12.1.10/Editor/ShaderGraph/Includes/Varyings.hlsl文件中的生成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
SurfaceDescription BuildSurfaceDescription(Varyings varyings)
{
    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(varyings);
#if defined(HAVE_VFX_MODIFICATION)
    GraphProperties properties;
    ZERO_INITIALIZE(GraphProperties, properties);
    GetElementPixelProperties(surfaceDescriptionInputs, properties);
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs, properties);
#else
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);
#endif
    return surfaceDescription;
}

然后在文件Library/PackageCache/com.unity.shadergraph@12.1.10/Editor/Generation/Processors/Generator.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Setup
string pixelGraphInputName = "SurfaceDescriptionInputs";
string pixelGraphOutputName = "SurfaceDescription";
string pixelGraphFunctionName = "SurfaceDescriptionFunction";
var pixelGraphOutputBuilder = new ShaderStringBuilder(humanReadable: m_humanReadable);
var pixelGraphFunctionBuilder = new ShaderStringBuilder(humanReadable: m_humanReadable);

// Build pixel graph outputs
// Add struct fields to active fields
GenerationUtils.GenerateSurfaceDescriptionStruct(pixelGraphOutputBuilder, pixelSlots, pixelGraphOutputName, activeFields.baseInstance, m_OutputNode is SubGraphOutputNode, pass.virtualTextureFeedback);

// Build pixel graph functions from ShaderPass pixel port mask
GenerationUtils.GenerateSurfaceDescriptionFunction(
    pixelNodes,
    pixelNodePermutations,
    m_OutputNode,
    m_GraphData,
    pixelGraphFunctionBuilder,
    functionRegistry,
    propertyCollector,
    keywordCollector,
    m_Mode,
    pixelGraphFunctionName,
    pixelGraphOutputName,
    null,
    pixelSlots,
    pixelGraphInputName,
    pass.virtualTextureFeedback);
  • 生成代码时, 大概是input.shaderOutputName
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    static void GenerateSurfaceDescriptionRemap(
                GraphData graph,
                AbstractMaterialNode rootNode,
                IEnumerable<MaterialSlot> slots,
                ShaderStringBuilder surfaceDescriptionFunction,
                GenerationMode mode)
    {
        if (rootNode == null)
        {
            foreach (var input in slots)
            {
                if (input != null)
                {
                    var node = input.owner;
                    var foundEdges = graph.GetEdges(input.slotReference).ToArray();
                    // 这里的shaderOutputName
                    var hlslName = NodeUtils.GetHLSLSafeName(input.shaderOutputName);
                    if (foundEdges.Any())
                        surfaceDescriptionFunction.AppendLine($"surface.{hlslName} = {node.GetSlotValue(input.id, mode, node.concretePrecision)};");
                    else
                        surfaceDescriptionFunction.AppendLine($"surface.{hlslName} = {input.GetDefaultValue(mode, node.concretePrecision)};");
                }
            }
        }
    
  • 对应的是声明面板时的第二个参, 即”BakedGI”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    /// <summary>
    /// 用于声明自定义的ShaderGraph输出的属性节点 <br/>
    /// Unity源文件在com.unity.shadergraph@12.1.10/Editor/Generation/TargetResources/BlockFields.cs
    /// </summary>
    ///
    static class ExternalBlockFields
    {
        /// <summary>
        /// 自定义的表面参数
        /// </summary>
        [GenerateBlocks("External")]	// "External"为在ShaderGraph里面手动添加时的菜单路径
        public struct SurfaceDescription
        {
            public static string name = "SurfaceDescription";
            // 此时的ColorControl是为了确定默认初始值, 后面的True表示是HDR颜色, 第二个参是hlsl中使用的名称, 与name
            // 合并起来就是通过SurfaceDescription.BakedGI来调用
            public static BlockFieldDescriptor BakedGI = new(name, "BakedGI", "Baked GI",
                "SURFACEDESCRIPTION_BAKEDGI", new ColorControl(new Color(1,1,1,0),true), ShaderStage.Fragment); 
        }
    }
    

03 注意

再次强调, 当你定义BlockFieldDescriptor时后, 第二个参, 即referenceName, 在下面的代码示例中即"BakedGI", 一定要全项目唯一, 否则会导致Block构建异常, 且无法正确传递值到Shader中.

1
2
public static BlockFieldDescriptor BakedGI = new(name, "BakedGI", "Baked GI",
            "SURFACEDESCRIPTION_BAKEDGI", new ColorControl(new Color(1,1,1,0),true), ShaderStage.Fragment);

原因可能需要查一下FieldDescriptorname字段的调用链

1
2
3
4
5
6
public FieldDescriptor(string tag, string name, string define)
{
    this.tag = tag;
    this.name = name;
    this.define = define;
}
参考网页
本文由作者按照 CC BY 4.0 进行授权