SRPCore GenerateHLSL解析及其扩展

序言

在之前的项目开发HDRP的时候,就觉得绑定RT的管理就像一坨屎一样难受,改了管线RT的绑定顺序,原来的Shader输出又会有问题(主要是SV_Target顺序问题)
没办法只能够在管线原来的绑定上面,重新写了一份文件,让Shader的共用Pass引用,这样就能规范不同Shader pass的输出了。

但是这只是解决了一个pass的问题,因为要理清pass的绑定顺序还是要一定时间的
为了解决所有pass的RT绑定顺序的问题,后面就想着用代码生成的办法自动生成Include文件,规范Pass输出struct,顺带也把这个GenerateHLSL Attribute解析一下,于是乎就有了这一篇文章。

Attribute

Attribute是C#的语言特性。
平时在写Mono或者是ScriptableObject或者是其他的脚本的是时候相信大家都用过,
Attribute主要就是用来传递程序中各种元素(Class,Struct,Method,Enum,Field)的行为信息的标签

自定义一个Attribute也很简单,不需要什么别的操作,只需要继承一下System.Attribute就行了。
例如:


class GeneratePassOutput : Attribute
{

}

[GeneratePassOutput]
class PassData
{
    
}

这就是一个最简单的Attribute创建,那怎么体现出他的作用呢?
一般来说,我们会结合C#的反射机制获取Attribute的信息。

    [AttributeUsage(AttributeTargets.Field)]
    public class PassOutputAttribute : Attribute
    {
        public bool isFixed;
        public int outputRTIndex;
        public string marcoName;
        public string marcoKeyWord;

        public bool IsDepthBuffer => outputRTIndex == -1;

        public PassOutputAttribute(int outputRTIndex, bool isFixed, string marcoName, string marcoKeyWord)
        {
            this.outputRTIndex = outputRTIndex;
            this.isFixed = isFixed;
            this.marcoName = marcoName;
            this.marcoKeyWord = marcoKeyWord;
        }
    }


    [GeneratePassOutput]
    class PassData
    {
        public RendererListHandle rendererList;

        [PassOutputAttribute(0, true, "MOTION_VEC", "WRITE_MOTION_VEC")]
        public TextureHandle motionVectorBuffer;
        
        ....
    }


    public void GeneratePassOutputFile()
    {
        //TypeCache是Unity提供的反射接口,快速从Cache中获取加载到Unity的程序集中的类型
        //这里是用来获取带有GeneratePassOutput Attribute的类型。(即PassData)
        //https://docs.unity3d.com/cn/2021.1/ScriptReference/TypeCache.html
        TypeCache.TypeCollection typeCollection = TypeCache.GetTypesWithAttribute<GeneratePassOutput>();

        //遍历TypeCollection
        List<PassOutputAttribute> outputAttributes = new List<PassOutputAttribute>();
        for (int i = 0; i < typeCollection.Count; i++)
        {
            if (typeCollection[i].IsAbstract || typeCollection[i].IsGenericType)
                continue;

            outputAttributes.Clear();

            //获取类型中的所有字段
            var fields = typeCollection[i].GetFields();

            //遍历并过滤字段
            foreach (var field in fields)
            {
                //获取字段的Attribute(PassOutputAttribute)
                var outputAttributeObj = field.GetCustomAttribute(typeof(PassOutputAttribute), false);
                if (outputAttributeObj is PassOutputAttribute attribute)
                    outputAttributes.Add(attribute);
            }

            //...Do Something
        }

        ...
    }

在上面的PassOutputAttribute的Attribute一共有四个字段,在字段上方的定义Attribute的时候,我们就已经把相关参数通过Attribute构造函数传入进去了。
这样就能够在后续根据Attribute的字段做不同的操作了

GenerateHLSL Attribute

在SRP中,我们一般是需要自定义ConstantBuffer,或者定义Shader要用到StructureBuffer数据成员对应的结构体类型
例如,LightData,ShaderGlobalVariables等等。
这时候管线这边需要对应结构体的大小要跟HLSL文件中的结构体一致对齐,不然渲染上就会有bug
为了方便对齐,SRP core提供了GenerateHLSL Tag方便自动生成HLSL文件

工作机制

HLSL生成是通过MenuItem上面的菜单触发的。
注意这里用的是async异步的方法,这样一旦生成的文件比较多的时候也可以进行别的操作,不影响主线程的操作

Invoke GenerateAll


class ShaderGeneratorMenu
{
    [MenuItem("Edit/Rendering/Generate Shader Includes", priority = CoreUtils.Sections.section3 + CoreUtils.Priorities.editMenuPriority + 1)]
    async static Task GenerateShaderIncludes()
    {
        await CSharpToHLSL.GenerateAll();
        AssetDatabase.Refresh();
    }
}

    //CSharpToHLSL.cs

    /// <summary>
    ///     Generate all shader code from <see cref="GenerateHLSL" /> attribute.
    /// </summary>
    /// <returns>An awaitable task.</returns>
    public static async Task GenerateAll()
    {
        Dictionary<string, List<ShaderTypeGenerator>> sourceGenerators = null;
        try
        {
            //不同的GenerateHLSL Tag的sourcePath,有不同的ShaderTypeGenerator List
            //sourcePath=>GenerateHLSL构造函数中使用CallerFilePath Attribute修饰参数。即sourcePath为C# type对应的文件路径

            // Store per source file path the generator definitions
            sourceGenerators = DictionaryPool<string, List<ShaderTypeGenerator>>.Get();

            //从TypeCache中获取所有带有GenerateHLSL Attribute Tag的类型
            // Extract all types with the GenerateHLSL tag
            foreach (var type in TypeCache.GetTypesWithAttribute<GenerateHLSL>())
            {
                var attr = type.GetCustomAttributes(typeof(GenerateHLSL), false).First() as GenerateHLSL;
               
                if (!sourceGenerators.TryGetValue(attr.sourcePath, out var generators))
                {
                    generators = ListPool<ShaderTypeGenerator>.Get();
                    sourceGenerators.Add(attr.sourcePath, generators);
                }
                //给对应路径的List添加上type对应的ShaderTypeGenerator
                //ShaderTypeGenerator负责生成
                generators.Add(new ShaderTypeGenerator(type, attr));
            }

            //通过sourceGenerators.Select返回(不同根目录路径)所有的sourceGenerator对应的GenerateAsync Task
            // Generate all files
            await Task.WhenAll(sourceGenerators.Select(async it =>
                await GenerateAsync($"{it.Key}.hlsl", $"{Path.ChangeExtension(it.Key, "custom")}.hlsl", it.Value)));
        }
        finally
        {
            // Make sure we always release pooled resources
            if (sourceGenerators != null)
            {
                foreach (var pair in sourceGenerators)
                    ListPool<ShaderTypeGenerator>.Release(pair.Value);
                DictionaryPool<string, List<ShaderTypeGenerator>>.Release(sourceGenerators);
            }
        }
    }

GenerateHLSL sourcePath

可以看到这个sourcePath被CallerFilePath Attribute修饰。
这个CallerFilePath是.Net4.5引入的Attribute。用于获取调用方的源文件的完整路径(编译时的文件路径)
https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.compilerservices.callerfilepathattribute?view=net-6.0
sourceGenerators就根据这个sourcePath进行建立字典,将写入同一个文件generator加入到同一个List中集中处理(生成HLSL文件)。

//ShaderGeneratorAttributes.cs

public GenerateHLSL(PackingRules rules = PackingRules.Exact, bool needAccessors = true, bool needSetters = false, bool needParamDebug = false, int paramDefinesStart = 1,
                    bool omitStructDeclaration = false, bool containsPackedFields = false, bool generateCBuffer = false, int constantRegister = -1,
                    [CallerFilePath] string sourcePath = null)
{
    this.sourcePath = sourcePath;
    packingRules = rules;
    this.needAccessors = needAccessors;
    this.needSetters = needSetters;
    this.needParamDebug = needParamDebug;
    this.paramDefinesStart = paramDefinesStart;
    this.omitStructDeclaration = omitStructDeclaration;
    this.containsPackedFields = containsPackedFields;
    this.generateCBuffer = generateCBuffer;
    this.constantRegister = constantRegister;
}

Generate Async

HLSL文件的生成逻辑主要集中在GenerateAsync中,这里集中处理单个HLSL文件生成
分段式生成
Enum Field
Constant Buffer Struct Field
Accessors/Setters Field
Debug Function Method
PackInfo Field Unpack(Pack) Method


/// <summary>
///     Generate all shader code from <paramref name="generators" /> into <paramref name="targetFilename" />.
/// </summary>
/// <param name="targetFilename">Path of the file to generate.</param>
/// <param name="targetCustomFilename">Path of the custom file to include. (If it exists)</param>
/// <param name="generators">Generators to execute.</param>
/// <returns>Awaitable task.</returns>
private static async Task GenerateAsync(string targetFilename, string targetCustomFilename,
    List<ShaderTypeGenerator> generators)
{
    var skipFile = false;

    //遍历所有的generator
    //generator收集对应type里要生成的字段
    //如果Generate返回False说明对应HLSL生成出错,则跳过当前HLSL文件
    // Emit atomic element for all generators
    foreach (var gen in generators.Where(gen => !gen.Generate()))
    {
        // Error reporting will be done by the generator.  Skip this file.
        gen.PrintErrors();
        skipFile = true;
        break;
    }

    // If an error occured during generation, we abort this file
    if (skipFile)
        return;

    // 检测对应File的状态
    // Check access to the file
    if (File.Exists(targetFilename))
    {
        FileInfo info = null;
        try
        {
            info = new FileInfo(targetFilename);
        }
        catch (UnauthorizedAccessException)
        {
            Debug.Log("Access to " + targetFilename + " is denied. Skipping it.");
            return;
        }
        catch (SecurityException)
        {
            Debug.Log("You do not have permission to access " + targetFilename + ". Skipping it.");
            return;
        }

        if (info?.IsReadOnly ?? false)
        {
            Debug.Log(targetFilename + " is ReadOnly. Skipping it.");
            return;
        }
    }

    // Generate content
    using var writer = File.CreateText(targetFilename);
    writer.NewLine = Environment.NewLine;

    //防止重复引用的宏: xxxxx(文件名大写)_CS_HLSL
    // Include guard name
    var guard = Path.GetFileName(targetFilename).Replace(".", "_").ToUpper();
    if (!char.IsLetter(guard[0]))
        guard = "_" + guard;

    await writer.WriteLineAsync("//");
    await writer.WriteLineAsync("// This file was automatically generated. Please don't edit by hand. Execute Editor command [ Edit > Rendering > Generate Shader Includes ] instead");
    await writer.WriteLineAsync("//");
    await writer.WriteLineAsync();
    await writer.WriteLineAsync("#ifndef " + guard);
    await writer.WriteLineAsync("#define " + guard);

    //Enum Field写入
    foreach (var gen in generators.Where(gen => gen.hasStatics))
        await writer.WriteLineAsync(gen.EmitDefines().Replace("\n", writer.NewLine));

    //Constant Buffer Struct Field写入
    foreach (var gen in generators.Where(gen => gen.hasFields))
        await writer.WriteLineAsync(gen.EmitTypeDecl().Replace("\n", writer.NewLine));

    //CBuffer每一个字段的Accessors/Setters函数写入
    foreach (var gen in generators.Where(gen => gen.hasFields && gen.needAccessors && !gen.hasPackedInfo))
    {
        await writer.WriteAsync(gen.EmitAccessors().Replace("\n", writer.NewLine));
        await writer.WriteAsync(gen.EmitSetters().Replace("\n", writer.NewLine));
        const bool emitInitters = true;
        await writer.WriteAsync(gen.EmitSetters(emitInitters).Replace("\n", writer.NewLine));
    }

    //写入Debug Function
    foreach (var gen in generators.Where(gen =>
        gen.hasStatics && gen.hasFields && gen.needParamDebug && !gen.hasPackedInfo))
        await writer.WriteLineAsync(gen.EmitFunctions().Replace("\n", writer.NewLine));

    //最后写入PackInfo Field相关的Unpack/Pack函数
    foreach (var gen in generators.Where(gen => gen.hasPackedInfo))
        await writer.WriteLineAsync(gen.EmitPackedInfo().Replace("\n", writer.NewLine));

    await writer.WriteLineAsync();

    await writer.WriteLineAsync("#endif");

    if (File.Exists(targetCustomFilename))
        await writer.WriteAsync($"#include \"{Path.GetFileName(targetCustomFilename)}\"");
}

收集Attribute相关信息

Generate函数主要是计算m_Statics,m_ShaderFields,m_PackedFields,m_DebugFields,m_PackedFieldsInfos
若当前Type是Enum就直接当做Static Field生成

public bool Generate()
{
    m_Statics = new Dictionary<string, string>();

    FieldInfo[] fields = type.GetFields();
    m_ShaderFields = new List<ShaderFieldInfo>();
    m_DebugFields = new List<DebugFieldInfo>();
    m_PackedFieldsInfos = new List<PackedFieldInfo>();

    //Type是Enum,直接把Type当静态字段进行处理,然后就结束当前Type的处理。
    if (type.IsEnum)
    {
        foreach (var field in fields)
        {
            if (!field.IsSpecialName)
            {
                string name = field.Name;
                name = InsertUnderscore(name);
                m_Statics[(type.Name + "_" + name).ToUpper()] = field.GetRawConstantValue().ToString();
            }
        }
        errors = null;
        return true;
    }

    ...
}

例如:

[GenerateHLSL]
enum GPULightType
{
    Directional,
    Point,
    Spot,
    ProjectorPyramid,
    ProjectorBox,
    Rectangle,
    Tube,
    Disc
}
//
// UnityEngine.Rendering.CustomRenderPipeline.GPULightType:  static fields
//
#define GPULIGHTTYPE_DIRECTIONAL (0)
#define GPULIGHTTYPE_POINT (1)
#define GPULIGHTTYPE_SPOT (2)
#define GPULIGHTTYPE_PROJECTOR_PYRAMID (3)
#define GPULIGHTTYPE_PROJECTOR_BOX (4)
#define GPULIGHTTYPE_RECTANGLE (5)
#define GPULIGHTTYPE_TUBE (6)
#define GPULIGHTTYPE_DISC (7)

Generate遍历所有的字段,判断字段应该用什么逻辑生成hlsl代码。
注:fieldType.IsPrimitive表示字段为基本类型(float,int,uint,bool,其他类型不支持)

public bool Generate()
{
    ...
    //遍历Type内所有字段
    foreach (var field in fields)
    {
        // Support array get getting array type
        Type fieldType = field.FieldType;
        int arraySize = -1;

        if (Attribute.IsDefined(field, typeof(FixedBufferAttribute)))
        {
            ...
        }
        else if (Attribute.IsDefined(field, typeof(HLSLArray)))
        {
            Error("Invalid HLSLArray target: '" + field.FieldType + "'" + ", this attribute can only be used on fixed array");
            return false;
        }

        //静态字段处理
        if (field.IsStatic)
        {
            if (fieldType.IsPrimitive)
            {
            ...
            }
            continue;
        }

        //包含需要DebugView输出的参数
        if (attr.needParamDebug && !attr.containsPackedFields)
        {
            List<string> displayNames = new List<string>();
            displayNames.Add(field.Name);

            bool isDirection = false;
            bool sRGBDisplay = false;
            bool checkIsNormalized = false;
            string preprocessor = "";

            //如果字段带有SurfaceDataAttributes Attribute,且needParamDebug(生成DebugView输出相关宏及Getter函数)
            // Check if the display name have been override by the users
            if (Attribute.IsDefined(field, typeof(SurfaceDataAttributes)))
            {
                ...
            }

            //GenerateHLSL标志为字段不需要Pack(DebugView直接输出)
            //登记相关字段到m_DebugFields(添加Debug输出函数)
            if (!attr.containsPackedFields)
            {
                ...
            }
        }
        //GenerateHLSL标志为存在字段需要Unpack/Pack
        //登记相关字段到m_PackedFieldsInfos(添加Pack/Unpack函数)
        //登记相关字段到m_DebugFields(添加Debug输出函数)
        if (attr.containsPackedFields)
        {
            ...
        }

        // To control variable scope
        {
            PrimitiveType floatPrecision = PrimitiveType.Float;
            string preprocessor = "";
            //如果字段带有SurfaceDataAttributes Attribute,需要处理精度的定义(Half/Real)
            if (Attribute.IsDefined(field, typeof(SurfaceDataAttributes)))
            {
                ...
            }

            //基础类型处理
            if (fieldType.IsPrimitive)
            {
                if (fieldType == typeof(float))
                    EmitPrimitiveType(floatPrecision, 1, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(int))
                    EmitPrimitiveType(PrimitiveType.Int, 1, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(uint))
                    EmitPrimitiveType(PrimitiveType.UInt, 1, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(bool))
                    EmitPrimitiveType(PrimitiveType.Bool, 1, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else
                {
                    Error("unsupported field type '" + fieldType + "'");
                    return false;
                }
            }
            else
            {
                // handle special types, otherwise try parsing the struct
                if (fieldType == typeof(Vector2))
                    EmitPrimitiveType(floatPrecision, 2, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(Vector3))
                    EmitPrimitiveType(floatPrecision, 3, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(Vector4))
                    EmitPrimitiveType(floatPrecision, 4, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(Vector2Int))
                    EmitPrimitiveType(PrimitiveType.Int, 2, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(ShaderGenUInt4))
                    EmitPrimitiveType(PrimitiveType.UInt, 4, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (fieldType == typeof(Matrix4x4))
                    EmitMatrixType(floatPrecision, 4, 4, arraySize, field.Name, "", preprocessor, m_ShaderFields);
                else if (!ExtractComplex(field, preprocessor, m_ShaderFields))
                {
                    // Error reporting done in ExtractComplex()
                    return false;
                }
            }
        }
    }

    //若当前Type的packingRules标志为PackingRules.Aggressive,则需要对遍历Field之后的获得所有的ShaderFieldInfo做进一步处理,
    //尝试让一些还有空余的vector与其他field合并成一个参数(Vector3+float...)

    m_PackedFields = m_ShaderFields;
    if (attr.packingRules == PackingRules.Aggressive)
    {
        m_PackedFields = Pack(m_ShaderFields);

        if (m_PackedFields == null)
        {
            return false;
        }
    }

    errors = null;
    return true;
}

限制作用范围

原生的SRP Core生成HLSL时,所有带有GenerateHLSL类型都重新生成。
文件一多,一旦涉及到关键文件的改动,很容易让Shader集体变粉红。而且重新编译也需要时间,所以GenerateHLSL生成这边我做了点小改动,把作用的范围只局限在当前ProjectBrower选中的文件

在原本的ShaderGeneratorMenu中写一个生成单个文件的函数回调就行。

//ShaderGeneratorMenu.cs

[MenuItem("Assets/Create/Shader/Script Generate Shader Includes", priority = 1)]
async static void GenerateShaderIncludesInOneFolder()
{
    string selectionFileString = AssetDatabase.GetAssetPath(Selection.activeInstanceID);
    int indexDiagonal = selectionFileString.LastIndexOf('/');
    string scriptName = selectionFileString.Substring(indexDiagonal + 1);
    if (scriptName.EndsWith(".cs"))
        await CSharpToHLSL.FileGenerate(scriptName);

    AssetDatabase.Refresh();
}



//CSharpToHLSL.cs

/// <summary>
///     Generate all shader code from selected scirpt <see cref="GenerateHLSL" /> attribute.
/// </summary>
/// <returns>An awaitable task.</returns>
public static async Task FileGenerate(string fileName)
{
    Dictionary<string, List<ShaderTypeGenerator>> sourceGenerators = null;
    try
    {
        // Store per source file path the generator definitions
        sourceGenerators = DictionaryPool<string, List<ShaderTypeGenerator>>.Get();

        // Extract all types with the GenerateHLSL tag
        foreach (var type in TypeCache.GetTypesWithAttribute<GenerateHLSL>())
        {
            var attr = type.GetCustomAttributes(typeof(GenerateHLSL), false).First() as GenerateHLSL;

            if (attr?.sourcePath != null)
            {
                if (attr.sourcePath.EndsWith(fileName))
                {
                    if (!sourceGenerators.TryGetValue(attr.sourcePath, out var generators))
                    {
                        generators = ListPool<ShaderTypeGenerator>.Get();
                        sourceGenerators.Add(attr.sourcePath, generators);
                    }

                    generators.Add(new ShaderTypeGenerator(type, attr));
                }
            }
        }

        // Generate all files
        await Task.WhenAll(sourceGenerators.Select(async it =>
            await GenerateAsync($"{it.Key}.hlsl", $"{Path.ChangeExtension(it.Key, "custom")}.hlsl", it.Value)));
    }
    finally
    {
        // Make sure we always release pooled resources
        if (sourceGenerators != null)
        {
            foreach (var pair in sourceGenerators)
                ListPool<ShaderTypeGenerator>.Release(pair.Value);
            DictionaryPool<string, List<ShaderTypeGenerator>>.Release(sourceGenerators);
        }
    }
}

PassOutputRT生成HLSL文件

文件结构及调用流程

在开发的时候,为了方便(懒)升级版本,通常不会去修改源码。就算是改,也要遵循修改最小原则,做到功能可拔插
而CSharp的partial关键字特别适合这一点,unity的代码划分模块也经常是用这个关键字,
使得在同一个class的范围内,能够划分不同的代码文件
在划分的时候,也同样注意要模块的资源申请及其资源释放。

所以为了实现自动生成Pass Output RT Index的HLSL文件,
在CSharpToHLSL所在文件夹中新建多一个CSharpToHLSL.PassOutputGenerate.cs

//Packages/com.unity.render-pipelines.core@12.1.6/Editor/ShaderGenerator/CSharpToHLSL.PassOutputGenerate.cs
internal partial class CSharpToHLSL
{
    /// <summary>
    /// 对单个文件进行Attribute tag的解析
    /// </summary>
    /// <param name="fileName">目标文件名</param>
    public static async Task PassOutputGenerateFileGenerate(string fileName)
    {
        Dictionary<string, List<PassOutputGeneration>> sourceGenerators = null;
        try
        {
            // Store per source file path the generator definitions
            sourceGenerators = DictionaryPool<string, List<PassOutputGeneration>>.Get();

            // Extract all types with the GenerateHLSL tag
            foreach (var type in TypeCache.GetTypesWithAttribute<GeneratePassOutput>())
            {
                var attr = type.GetCustomAttributes(typeof(GeneratePassOutput), false).First() as GeneratePassOutput;

                if (attr?.sourcePath != null)
                {
                    if (attr.sourcePath.EndsWith(fileName))
                    {
                        if (!sourceGenerators.TryGetValue(attr.sourcePath, out var generators))
                        {
                            generators = ListPool<PassOutputGeneration>.Get();
                            sourceGenerators.Add(attr.sourcePath, generators);
                        }

                        generators.Add(new PassOutputGeneration(type, attr));
                    }
                }
            }

            // Generate all files
            await Task.WhenAll(sourceGenerators.Select(async it =>
                await GeneratePassOutputAsync($"{it.Key}.hlsl", $"{Path.ChangeExtension(it.Key, "custom")}.hlsl", it.Value)));
        }
        finally
        {
            // Make sure we always release pooled resources
            if (sourceGenerators != null)
            {
                foreach (var pair in sourceGenerators)
                {
                    foreach (var generation in pair.Value)
                    {
                        generation.Dispose();
                    }

                    ListPool<PassOutputGeneration>.Release(pair.Value);
                }

                DictionaryPool<string, List<PassOutputGeneration>>.Release(sourceGenerators);
            }
        }
    }


    /// <summary>
    /// 根据解析的结果,进行生成
    /// </summary>
    /// <param name="targetFilename">目标文件名</param>
    /// <param name="targetCustomFilename">假设存在targetFilename.custom.hlsl,将会在生成的目标文件最后自动include</param>
    /// <param name="generators">脚本文件中包含的所有的PassOutputGeneration</param>
    private static async Task GeneratePassOutputAsync(string targetFilename, string targetCustomFilename,
        List<PassOutputGeneration> generators)
    {
        var skipFile = false;

        // Emit atomic element for all generators
        foreach (var gen in generators.Where(gen => !gen.Generate()))
        {
            // Error reporting will be done by the generator.  Skip this file.
            gen.PrintErrors();
            skipFile = true;
            break;
        }

        // If an error occured during generation, we abort this file
        if (skipFile)
            return;

        // Check access to the file
        if (File.Exists(targetFilename))
        {
            FileInfo info = null;
            try
            {
                info = new FileInfo(targetFilename);
            }
            catch (UnauthorizedAccessException)
            {
                Debug.Log("Access to " + targetFilename + " is denied. Skipping it.");
                return;
            }
            catch (SecurityException)
            {
                Debug.Log("You do not have permission to access " + targetFilename + ". Skipping it.");
                return;
            }

            if (info?.IsReadOnly ?? false)
            {
                Debug.Log(targetFilename + " is ReadOnly. Skipping it.");
                return;
            }
        }

        // Generate content
        using var writer = File.CreateText(targetFilename);
        writer.NewLine = Environment.NewLine;

        // Include guard name
        var guard = Path.GetFileName(targetFilename).Replace(".", "_").ToUpper();
        if (!char.IsLetter(guard[0]))
            guard = "_" + guard;

        await writer.WriteLineAsync("//");
        await writer.WriteLineAsync("// This file was automatically generated. Please don't edit by hand. Execute Editor command [ Edit > Rendering > Generate Shader Includes ] instead");
        await writer.WriteLineAsync("//");
        await writer.WriteLineAsync();
        await writer.WriteLineAsync("#ifndef " + guard);
        await writer.WriteLineAsync("#define " + guard);

        await writer.WriteLineAsync();

        foreach (var gen in generators)
            await writer.WriteLineAsync(gen.EmitDefines().Replace("\n", writer.NewLine));


        await writer.WriteLineAsync();

        await writer.WriteLineAsync("#endif");

        if (File.Exists(targetCustomFilename))
            await writer.WriteAsync($"#include \"{Path.GetFileName(targetCustomFilename)}\"");
    }
}

PassOutputGeneration.cs,

//Packages/com.unity.render-pipelines.core@12.1.6/Editor/ShaderGenerator/PassOutputGeneration.cs
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Rendering;


namespace UnityEditor.Rendering
{
    /// <summary>
    /// //生成文本的主要逻辑
    /// </summary>
    internal class PassOutputGeneration : IDisposable
    {
        /// <summary>
        /// 生成绑定文件的结构体类型
        /// </summary>
        public Type type;

        /// <summary>
        /// 目标结构体类型的 attribute obj
        /// </summary>
        public GeneratePassOutput attr;

        /// <summary>
        /// 用于抛出异常
        /// </summary>
        public List<string> errors = null;

        /// <summary>
        /// 目标结构体中的PassOutputAttribute列表
        /// </summary>
        private List<PassOutputAttribute> outputAttributes;

        /// <summary>
        /// PassOutputAttribute对应的宏与关键字列表,
        /// keyWordNames由于递归生成需要翻转列表
        /// </summary>
        private List<string> marcoNames;

        private List<string> keyWordNames;

        /// <summary>
        /// 没有Fixed的Target数量
        /// </summary>
        private int notFixedCount;

        /// <summary>
        /// Fixed的Target数量,由于SV_Target0(从零开始),预计算完毕需要-1
        /// </summary>
        private int baseOutputTargetOffset;

        /// <summary>
        ///  生成HLSL主要逻辑的对象
        /// </summary>
        /// <param name="type">生成绑定文件的结构体类型</param>
        /// <param name="attr">目标结构体类型的 attribute obj</param>
        public PassOutputGeneration(Type type, GeneratePassOutput attr)
        {
            this.type = type;
            this.attr = attr;
            this.outputAttributes = new List<PassOutputAttribute>();

            this.notFixedCount = 0;
            this.baseOutputTargetOffset = 0;
        }

        /// <summary>
        /// 记录异常
        /// </summary>
        /// <param name="error"></param>
        void Error(string error)
        {
            if (errors == null)
            {
                errors = new List<string>();
            }

            errors.Add("Failed to generate shader type for " + type.ToString() + ": " + error);
        }

        /// <summary>
        /// 用于抛出异常
        /// </summary>
        public void PrintErrors()
        {
            if (errors != null)
            {
                foreach (var e in errors)
                {
                    Debug.LogError(e);
                }
            }
        }

        /// <summary>
        /// 正式生成前预计算所需数据
        /// </summary>
        /// <returns></returns>
        public bool Generate()
        {
            if (attr == null || type == null)
                return false;

            var fields = this.type.GetFields();
            foreach (var field in fields)
            {
                var outputAttributeObj = field.GetCustomAttribute(typeof(PassOutputAttribute), false);
                if (outputAttributeObj is PassOutputAttribute attribute)
                    outputAttributes.Add(attribute);
            }

            this.marcoNames = new List<string>(outputAttributes.Count);
            this.keyWordNames = new List<string>(outputAttributes.Count);
            this.notFixedCount = 0;
            this.baseOutputTargetOffset = 0;
            foreach (var output in outputAttributes)
            {
                if (!output.IsDepthBuffer)
                    this.marcoNames.Add(output.marcoName);

                if (!output.isFixed && !output.IsDepthBuffer)
                {
                    notFixedCount++;
                    this.keyWordNames.Add(output.marcoKeyWord);
                }

                if (output.isFixed && !output.IsDepthBuffer)
                    this.baseOutputTargetOffset++;
            }

            //自顶向下递归,需要逆转整个keyword List
            this.keyWordNames.Reverse();
            baseOutputTargetOffset--;
            return true;
        }

        /// <summary>
        /// 正式生成HLSL文件对应的字符
        /// </summary>
        /// <returns></returns>
        public string EmitDefines()
        {
            string OutputSVTargetIndex(List<string> marcoNames, int baseOutputTargetOffset, List<int> finalResult)
            {
                string res = "";

                if (finalResult.Count > 0)
                {
                    int count = 0;

                    for (int channel = 0; channel < (baseOutputTargetOffset + 1); channel++)
                    {
                        res += $"#define {marcoNames[count++]} SV_Target{channel}\n";
                    }

                    int currentChannel = baseOutputTargetOffset;

                    for (int j = 0; j < finalResult.Count; j++)
                    {
                        if (finalResult[j] != 0)
                        {
                            currentChannel += finalResult[j];
                            res += $"#define {marcoNames[count++]} SV_Target{currentChannel}\n";
                        }
                        else
                        {
                            res += $"#define {marcoNames[count++]}\n";
                        }
                    }

                    while (count < marcoNames.Count)
                    {
                        res += $"#define {marcoNames[count++]}\n";
                    }
                }

                return res;
            }

            string OutputChildNodeMarco(
                List<string> keyWordNames, List<string> marcoNames,
                int fixedOutputTargetOffset, int depth, List<int> parentNodeResult)
            {
                string res = "";

                int nextDepth = depth - 1;

                res += ($"#if defined({keyWordNames[nextDepth]})\n");

                using (ListPool<int>.Get(out List<int> marcoOnChildNodeResult))
                {
                    marcoOnChildNodeResult.AddRange(parentNodeResult);
                    marcoOnChildNodeResult.Add(1);
                    if (nextDepth > 0)
                    {
                        res += OutputChildNodeMarco(keyWordNames, marcoNames, fixedOutputTargetOffset, nextDepth, marcoOnChildNodeResult);
                    }
                    else
                    {
                        res += OutputSVTargetIndex(marcoNames, fixedOutputTargetOffset, marcoOnChildNodeResult);
                    }
                }


                res += ("#else\n");

                using (ListPool<int>.Get(out List<int> marcoOffChildNodeResult))
                {
                    marcoOffChildNodeResult.AddRange(parentNodeResult);
                    marcoOffChildNodeResult.Add(0);
                    if (nextDepth > 0)
                    {
                        res += OutputChildNodeMarco(keyWordNames, marcoNames, fixedOutputTargetOffset, nextDepth, marcoOffChildNodeResult);
                    }
                    else
                    {
                        res += OutputSVTargetIndex(marcoNames, fixedOutputTargetOffset, marcoOffChildNodeResult);
                    }
                }

                res += ("#endif\n");

                return res;
            }

            string res = "";

            int nextDepth = notFixedCount - 1;
            if (nextDepth >= 1)
            {
                res += ($"#if defined({keyWordNames[nextDepth]})\n");
                using (ListPool<int>.Get(out List<int> marcoOnChildNodeResult))
                {
                    marcoOnChildNodeResult.Add(1);
                    res += OutputChildNodeMarco(keyWordNames, marcoNames, baseOutputTargetOffset, nextDepth, marcoOnChildNodeResult);
                }


                res += ("#else\n");

                using (ListPool<int>.Get(out List<int> marcoOffChildNodeResult))
                {
                    marcoOffChildNodeResult.Add(0);
                    res += OutputChildNodeMarco(keyWordNames, marcoNames, baseOutputTargetOffset, nextDepth, marcoOffChildNodeResult);
                }

                res += ("#endif\n");
            }
            else
            {
                res += ($"#if defined({keyWordNames[nextDepth]})\n");

                using (ListPool<int>.Get(out List<int> marcoOnChildNodeResult))
                {
                    marcoOnChildNodeResult.Add(1);
                    res += OutputSVTargetIndex(marcoNames, baseOutputTargetOffset, marcoOnChildNodeResult);
                }

                res += ("#else\n");

                using (ListPool<int>.Get(out List<int> marcoOffChildNodeResult))
                {
                    marcoOffChildNodeResult.Add(0);
                    res += OutputSVTargetIndex(marcoNames, baseOutputTargetOffset, marcoOffChildNodeResult);
                }

                res += ("#endif\n");
            }

            return res;
        }

        /// <summary>
        /// 生成之后释放对应的资源
        /// </summary>
        public void Dispose()
        {
            this.outputAttributes.Clear();
            this.outputAttributes = null;
            this.marcoNames.Clear();
            this.marcoNames = null;
            this.keyWordNames.Clear();
            this.keyWordNames = null;
        }
    }
}

ShaderGeneratorMenu中添加新的回调函数

//Packages/com.unity.render-pipelines.core/Editor/ShaderGenerator/ShaderGeneratorMenu.cs

[MenuItem("Assets/Create/Shader/Script Generate Pass Output Includes", priority = 2)]
async static void GeneratePassOutputShaderIncludesInOneFolder()
{
    string selectionFileString = AssetDatabase.GetAssetPath(Selection.activeInstanceID);
    int indexDiagonal = selectionFileString.LastIndexOf('/');
    string scriptName = selectionFileString.Substring(indexDiagonal + 1);
    if (scriptName.EndsWith(".cs"))
        await CSharpToHLSL.PassOutputGenerateFileGenerate(scriptName);

    AssetDatabase.Refresh();
}

值得注意的是GenerateHLSL attribute位于Runtime程序集之中,
所以定义的GeneratePassOutput(标志要生成出HLSL的struct)/PassOutputAttribute(标志TextureHandle的宏以及顺序)也同样放在Runtime程序集之中,
不然打包的程序会来索命了。

//按照GenerateHLSL的ShaderGeneratorAttributes一样,新建多一个folder PassOutputGenerator
//Packages/com.unity.render-pipelines.core/Runtime/PassOutputGenerator/PassOutputGeneratorAttributes.cs

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace UnityEngine.Rendering
{
    [AttributeUsage(AttributeTargets.Field)]
    public class PassOutputAttribute : Attribute
    {
        /// <summary>
        /// 标志当前RT绑定的位置是不是固定不变的
        /// </summary>
        public bool isFixed;

        /// <summary>
        /// 标志RT绑定的顺序
        /// </summary>
        public int outputRTIndex;

        /// <summary>
        /// 指定SV_Target时要用的宏
        /// </summary>
        public string marcoName;
        
        /// <summary>
        /// 控制Pass是否写入RT的关键字
        /// </summary>
        public string marcoKeyWord;

        public bool IsDepthBuffer => outputRTIndex == -1;

        public PassOutputAttribute(int outputRTIndex, bool isFixed, string marcoName, string marcoKeyWord)
        {
            this.outputRTIndex = outputRTIndex;
            this.isFixed = isFixed;
            this.marcoName = marcoName;
            this.marcoKeyWord = marcoKeyWord;
        }
    }

    [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)]
    public class GeneratePassOutput : Attribute
    {
        /// <summary>
        /// Path of the generated file
        /// </summary>
        public string sourcePath;

        public GeneratePassOutput([CallerFilePath] string sourcePath = null)
        {
            this.sourcePath = sourcePath;
        }
    }
}

上述的调用流程就是:
ShaderGeneratorMenu中的MenuItem函数触发解析选择的文件
CSharpToHLSL.PassOutputGenerateFileGenerate解析文件中的Attribute Tag
CSharpToHLSL.GeneratePassOutputAsync根据解析的结果调用PassOutputGeneration.Generate预生成一部分计算用数据
PassOutputGeneration.EmitDefines把最终结果输出,并写入到文件中

使用规则

简单起见,我这里就不涉及Buffer相关的Unpack和Pack函数生成了。
如果有需要就要对PassOutputAttribute按照GenerateHLSL一样进行扩展
定义PassOutputAttribute时,需要
标志当前RT绑定的位置是不是固定不变的(Fixed),
以及RT绑定的顺序
指定SV_Target时要用的宏
以及控制Pass是否写入RT的关键字

生成实例:

[GeneratePassOutput]
class PrepassOutputData
{
    public RendererListHandle rendererList;

    [PassOutput(0, false, "SV_TARGET_NORMAL", "WRITE_NORMAL_BUFFER")]
    public TextureHandle normalBuffer;

    [PassOutput(1, false, "SV_TARGET_DEPTH_COLOR", "WRITE_MSAA_DEPTH")]
    public TextureHandle depthAsColorBuffer;

    [PassOutput(2, false, "SV_TARGET_MOTION_VEC", "WRITE_MOTION_VEC_BUFFER")]
    public TextureHandle motionVectorBuffer;

    [PassOutput(3, false, "SV_TARGET_DECAL", "WRITE_DECAL_BUFFER")]
    public TextureHandle DecalBuffer;
    
    [PassOutput(-1, true, "", "")] public TextureHandle depthBuffer;
}
//
// This file was automatically generated. Please don't edit by hand. Execute Editor command [ Edit > Rendering > Generate Shader Includes ] instead
//

#ifndef PREPASSOUTPUTDATA_CS_HLSL
#define PREPASSOUTPUTDATA_CS_HLSL

#if defined(WRITE_NORMAL_BUFFER)
#if defined(WRITE_MSAA_DEPTH)
#if defined(WRITE_MOTION_VEC_BUFFER)
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR SV_Target1
#define SV_TARGET_MOTION_VEC SV_Target2
#define SV_TARGET_DECAL SV_Target3
#else
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR SV_Target1
#define SV_TARGET_MOTION_VEC SV_Target2
#define SV_TARGET_DECAL
#endif
#else
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR SV_Target1
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL SV_Target2
#else
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR SV_Target1
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL
#endif
#endif
#else
#if defined(WRITE_MOTION_VEC_BUFFER)
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC SV_Target1
#define SV_TARGET_DECAL SV_Target2
#else
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC SV_Target1
#define SV_TARGET_DECAL
#endif
#else
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL SV_Target1
#else
#define SV_TARGET_NORMAL SV_Target0
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL
#endif
#endif
#endif
#else
#if defined(WRITE_MSAA_DEPTH)
#if defined(WRITE_MOTION_VEC_BUFFER)
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR SV_Target0
#define SV_TARGET_MOTION_VEC SV_Target1
#define SV_TARGET_DECAL SV_Target2
#else
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR SV_Target0
#define SV_TARGET_MOTION_VEC SV_Target1
#define SV_TARGET_DECAL
#endif
#else
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR SV_Target0
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL SV_Target1
#else
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR SV_Target0
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL
#endif
#endif
#else
#if defined(WRITE_MOTION_VEC_BUFFER)
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC SV_Target0
#define SV_TARGET_DECAL SV_Target1
#else
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC SV_Target0
#define SV_TARGET_DECAL
#endif
#else
#if defined(WRITE_DECAL_BUFFER)
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL SV_Target0
#else
#define SV_TARGET_NORMAL
#define SV_TARGET_DEPTH_COLOR
#define SV_TARGET_MOTION_VEC
#define SV_TARGET_DECAL
#endif
#endif
#endif
#endif


#endif


posted @ 2024-04-13 18:29  凶恶的真实  阅读(229)  评论(0编辑  收藏  举报