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