Unity Custom SRP
Custom Render Pipeline
A new Render Pipeline
Project Setup
我们需要在线性空间计算光照,所以设置为Linear。
Pipeline Asset
- Unity默认使用默认渲染管线,而在这里,我们通过Pipeline Asset来管理自定义管线。进一步的,我们将资产的文件格式写成默认渲染管线的路径格式。
- 默认的Csharp文件是游戏逻辑的文件格式,我们引入命名空间UnityEngine.Renderring,并且把类实现部分删掉,继承RenderPipelineAsset类来实现我们的自定义管线资产类CustomRenderPipelineAsset。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomRenderPipelineAsset : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipelineAsset: RenderPipelineAsset
{
}
- RenderPipelineAsset渲染管线资产类的作用是,指定一个渲染管线RenderPipeline实例,这个指定是需要重写CreatePipeline来实现的。
protected override RenderPipeline CreatePipeline()
{
return null;
}
- 最终的,我们需要实现能够在界面菜单中选中一个渲染管线资产。这个通过CreateAssetMenu来实现。
[CreateAssetMenu(menuName ="Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
这样一来,通过Create就可以创建渲染管线资产。
创建资产之后,我们就可以修改Scriptable Render Pipeline Settings中的渲染管线资产。这样一来,我们就实现了Unity项目使用我们自己创建的渲染管线资产,接下来就需要去实现我们自己的渲染管线实例。
此时,我们的scene变成黑色,这是因为我们还没有去实现渲染管线实例。
Render Pipeline Instance
- 我们创建一个CustomRenderPipeline脚本,来实现渲染管线实例类。
相似的,我们修改命名空间,此外我们需要继承的是RenderPipeline。此外,需要注意的是,RenderPipeline提供了渲染入口的抽象接口,即Render函数。Render函数有两个参数,一个是上下文,另一个是摄像机。Render函数是抽象函数必须要定义。该函数有两种重载,一个是需要Camera数组,另一个需要List,推荐使用后者,但是作为抽象函数前者也必须重写,因此重写为空函数。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
}
protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
{
}
}
- 在创建了渲染管线实例类之后,我们就可以指定渲染管线资产类的返回实例了。
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline();
}
}
Rendering
Camera Render
渲染管线实例类的Render函数,是对每一个相机分别渲染,因此,我们创建一个相机渲染的类,来处理每一个相机的渲染过程。进一步的,我们需要将Render的上下文和某一个Camera设置在该类内部。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
Camera camera;
ScriptableRenderContext context;
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
}
}
这样一来,我们就可以简单地处理渲染管线类的Render函数。显然的是,CameraRenderer类的Render函数还没有实现渲染效果,这就是接下来的工作。
protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
{
CameraRenderer renderer = new CameraRenderer();
for(int i = 0; i < cameras.Count; i++)
{
renderer.Render(context, cameras[i]);
}
}
Drawing the Skybox
此时我们的Render还没有实现。我们将CameraRender类实例修改两大参数之后,就可以就这两个参数执行渲染了。
- 首先,我们实现一个渲染可见图形的函数,DrawVisiableGeometry,在这里我们简单的渲染一个默认天空盒,这是通过上下文的绘制天空和函数来实现的contex.DrawSkybox(camera)。
- 接着,需要注意的是,我们的渲染函数,实际上是将渲染指令添加到缓冲中去,然后由第三方执行缓冲中的内容;而在此处,我们的操作是将绘制指令添加到了上下文中,因此,我们需要将上下文提交给缓冲。这个类似于UE的渲染队列。因此,在后面我们利用了context.Submit(),并封装成了Submit()。
- 在这里,我们虽然绘制出了天空盒,但是我们的外设操作不能影响观察结果,这是因为我们没有设置camera的View矩阵。这个同样需要使用context的函数context.SetupCameraProperties(camera)来将View矩阵参数设置加入到上下文中,并封装为Setup()。
可见,到目前为止,我们实现了上下文的三个环节:camera信息、渲染指令、提交上下文。
public class CameraRenderer
{
Camera camera;
ScriptableRenderContext context;
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
DrawVisiableGeometry();
}
void DrawVisiableGeometry()
{
Setup();
context.DrawSkybox(camera);
Submit();
}
void Submit()
{
context.Submit();
}
void Setup()
{
context.SetupCameraProperties(camera);
}
}
Command Buffers
- 已知的是,我们的渲染管线的执行,需要在buffer和context之间互动,像是渲染天空盒这类具有既定函数的渲染流程,是已经实现了这些交互的,但是我们的更多的任务需要我们自己来实现交互。首先,我们实现了一个一个CommandBuffer。
const string bufferName = "Renmder Camera";
CommandBuffer buffer = new CommandBuffer { name = bufferName };
- 首先,我们使用buffer.BeginSample(bufferName);与buffer.EndSample(bufferName);,来将这些buffer提交给探查器profiler,以便观察。
- 然后,需要注意的是,我们的buffer与context的互动是这样的:context绑定一个buffer,并从buffer中拷贝命令,这个可以通过context.ExecuteCommandBuffer(buffer)来实现。但是这一个过程并不会删除buffer中的命令,因此会对后续的submit有影响,所以我们需要用buffer.Clear()清理buffer。这个指令在Setup与Submit两个过程都需要执行,目的就是为了从buffer中拷贝命令到context中,并清理buffer,可见在Setup中实现之后便可以写入context,而在Submit中实现之后便可以submit给buffer。
void Submit()
{
buffer.BeginSample(bufferName);
ExecuteBuffer();
context.Submit();
}
void Setup()
{
buffer.EndSample(bufferName);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
void ExecuteBuffer()
{
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
可见,绘制天空盒的指令被显示在了我们的帧缓冲"Render Camera"中了。
Clearing the Render Target
- 使用buffer.ClearRenderTarget(true, true, Color.clear);清理缓冲。将其放在设置camera属性之后可以快速清除,而不是通过绘制来清除。
- 在开始采样之后,如果使用了buffer操作,比如清理缓冲,那么在profier中就会出现嵌套。因此,为了减少嵌套,将其放在sample之前。
void Setup()
{
context.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(bufferName);
ExecuteBuffer();
}
Culling
- 在绘制物体时,我们需要剔除,这里的剔除指的是距离剔除、视锥体粗粒度剔除、遮挡剔除等应用程序阶段的剔除。
- 进行剔除需要用到两个关键函数:首先使用camera.TryGetCullingParameters(out ScriptableCullingParameters p)获得camera剔除相关的参数,这里有可能会因为camera的退化而拿不到提出参数,对于这类情况我们选择退出该camera的渲染。接着,拿到渲染参数之后,我们使用context.Cull(ref p)来获得剔除结果的结构体,这是一个CullingResults类型的结构体。
- 最终,在渲染时,我们需要使用剔除结果,如下一节所述。
CullingResults cullingResults;
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
if (!Cull()) return;
DrawVisiableGeometry();
}
bool Cull()
{
if(camera.TryGetCullingParameters(out ScriptableCullingParameters p))
{
cullingResults = context.Cull(ref p);
return true;
}
return false;
}
Drawing Geometry
- 上面只实现了绘制天空盒,接下来绘制几何。绘制几何需要这样几个关键步骤:
- SortingSettings:该结构体用于描述集合体排序方式,可以按照下面的方式定义,以图按照特定的camera来进行排序(通过传入参数camera实现),并按照某种特定的排序方式来排序(通过构造函数赋值crite来实现,这个可以不实现那么便是未定义的顺序,此处使用了不透明物体的排序方式,即由近及远)。
- ShaderTagId:该结构体用于指定渲染shader,此处创建了一个静态对象作为shaderid,并且选用了unity的Unlit shader,这是通过参数名“SRPDefaultUnllit”来实现的。
- DrawingSettings:该结构体描述了绘制相关的属性,其需要2个参数:ShaderTagId,SortingSettings。
- FilteringSettings:该结构体描述了滤波方式,此处就是指如何按照几何体的Material属性来实现有选择的滤波,这个可通过RenderQueueRange的成员来指定,如此处便是all。
- context.DrawRenderers:该函数最终实现绘制一次渲染循环的几何体,其需要三个参数:cullingResults, ref drawingSettings, ref filteringSettings,都是我们之前的实现内容。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
void DrawVisiableGeometry()
{
var sortingSettings = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
context.DrawSkybox(camera);
}
- 经过调试发现,当几何体不在camera视锥体范围内时,包括距离之外与NDC之外,剔除就会发挥作用,渲染循环中不包含该几何体。但是当机合体被遮挡时,仍然会进行绘制,可见,在此处,剔除只实现了视锥体剔除和距离剔除,而没有遮挡剔除。
Drawing Opaque and Transparent Geometry Separately
如果将天空盒看作不透明物体,那么它便是最远处的不透明物体,所以绘制顺序是由近及远绘制不透明物体,然后绘制天空盒。接下来,由远及近绘制透明物体。这个步骤可以很简单的利用上述的几个结构体来实现,具体来说就是修改sortingSettings的criteria(相应的修改DrawSettings)与filteringSettings的renderQueueRange。
void DrawVisiableGeometry()
{
//Opaque
var sortingSettings = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
//Skybox
context.DrawSkybox(camera);
//Transparent
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
Editor Rendering
Drawing Legacy Shaders
- 从上面可以看出,我们的渲染管线目前只能支持一个ShaderTagId,暂时的,我们选用的是Unity的默认Unlit shader。因此,我们的管线目前没有能力支持其他默认ShaderTagId的渲染。如下,我们修改Material,则我们选中的“DefaultUnlitShader”就不能发挥作用了。
- 为此,我们需要能够支持默认渲染管线,至少是不要忽视这些几何体而发生位置的问题。
- 首先,我们定义一个静态数组,保存我们需要支持的默认管线。
- 然后,我们定义一个函数,该函数中进行完整的渲染流程,但是DrawingSettings需要使用上面数组中的shader,进一步的可以使用drawingSettings.SetShaderPassName将数组中的所有shader都加入到各自的一个Pass中,如此一来,数组中的shader也都会被支持。
- 另外,我们对filter没有要求,使用DefaultValue即可。
static ShaderTagId[] legacyShaderTagIds =
{
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
if (!Cull()) return;
Setup();
DrawVisiableGeometry();
DrawUnsupportedShaders();
Submit();
}
void DrawUnsupportedShaders()
{
var drawingSettings = new DrawingSettings(legacyShaderTagIds[0], new SortingSettings(camera));
var filteringSettings = FilteringSettings.defaultValue;
for(int i=1;i<legacyShaderTagIds.Length;i++) {
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
如下,我们绘制出了旧版管线的几何体。
Error Material
我们期待旧版管线的几何体被以特殊的形式绘制出来。为了实现这个,我们只需要定义一个静态Material对象,并且将其指定为特定的Material(此处通过Shader.Find("Hidden/InternalErrorShader")指定为紫色)。进一步的,我们在drawingSettings的构造函数中,赋值overrideMaterial,即可将旧版管线的几何体绘制为errorMaterial。
static Material erroeMaterial;
void DrawUnsupportedShaders()
{
if(erroeMaterial == null)
{
erroeMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(legacyShaderTagIds[0], new SortingSettings(camera))
{
overrideMaterial = erroeMaterial
};
var filteringSettings = FilteringSettings.defaultValue;
for(int i=1;i<legacyShaderTagIds.Length;i++) {
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
Partial Class
- 我们不希望当程序发布之后还会显示不正常的几何体,为了实现这个需求,我们可以将这一部分使用预处理指令#if UNITY_EDITOR包含起来,进一步的为了更好地处理代码结构,我们使用分布类。
- 需要注意的是,因为我们将DrawUnsupportedShaders()包含在了分布类的条件中,但是该函数始终会被调用,因此我们将其声明为partial,使其分部化。
public partial class CameraRenderer{...}
using UnityEngine;
using UnityEngine.Rendering;
partial class CameraRenderer
{
partial void DrawUnsupportedShaders();
#if UNITY_EDITOR
static ShaderTagId[] legacyShaderTagIds =
{
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
static Material erroeMaterial;
partial void DrawUnsupportedShaders()
{
if (erroeMaterial == null)
{
erroeMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(legacyShaderTagIds[0], new SortingSettings(camera))
{
overrideMaterial = erroeMaterial
};
var filteringSettings = FilteringSettings.defaultValue;
for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
#endif
}
Drawing Gizmos
进一步的,我们绘制出一些可视控件。同样的,这些可视控件只会在编辑时显示,因此与旧版管线一样使用partial。
using UnityEditor;
...
partial void DrawGizmos()
#if UNITY_EDITOR
...
partial void DrawGizmos()
{
if(Handles.ShouldRenderGizmos()) {
context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
}
}
#endif
Drawing Unity UI
Multiple Cameras
当有多个camera时,他们会被放在同一个buffer的sample之下,这是显然的。
为了能够分开显示,我们对CameraRenderer的实例,在传入每个camera时,修改其buffer的name,这样一来我们就获得了多个buffer。
partial void PrepareBuffer();
...
partial void PrepareBuffer()
{
buffer.name = camera.name;
}
public void Render(ScriptableRenderContext context, Camera camera)
{
this.context = context;
this.camera = camera;
if (!Cull()) return;
PrepareBuffer();
Setup();
DrawVisiableGeometry();
DrawUnsupportedShaders();
DrawGizmos();
Submit();
}
但是,问题在于,我们修改了buffer的那么,而在sample中传入的名字仍然是bufferName,这就会使的profier的bufferName和buffer的name不匹配,就会报错。要解决这个问题,我们只需要同时修改sample的name即可。即,在编辑模式下,我们设置string SampleName,并将其与buffer.name赋值为camera.name,那么二者就会同步;而在其他模式下,我们设置string SampleName便是bufferName,也保证了二者一致。
#if UNITY_EDITOR
string SampleName { get; set; }
partial void PrepareBuffer()
{
buffer.name = SampleName = camera.name;
}
#else
const string SampleName = bufferName;
#endif
void Setup()
{
context.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
void Submit()
{
buffer.EndSample(SampleName);
ExecuteBuffer();
context.Submit();
}
然而,我们获取camera的name,需要分配内存。
我们在profier中打上标记,就会发现这两次的malloc只会发生在编辑模式下。
using UnityEngine.Profiling;
...
partial void PrepareBuffer()
{
Profiler.BeginSample("Editor Only");
buffer.name = SampleName = camera.name;
Profiler.EndSample();
}
Layers
我们可以将几何体分层,然后指定每一个camera只去观察某些层。这样,我们就可以用main camera渲染可视几何体,而用second camera渲染旧管线几何体。
Clear Flags
在之前的渲染中,我们设置每次camera渲染都会clear color bufffer与depth buffer,这显然会将main camera的缓冲给清除,而只显示second camera,若为了显示多个camera的情况,可以通过修改camera的clearFlags来实现。clearFlags有四个值:
- 1:skyBox
- 2:color
- 3:depth
- 4:nothing
我们利用这四个值来实现secnond camera的不同clear程度,具体来说是获得控制面板设置的clearFlags参数,然后利用buffer.ClearRenderTarget来设置清除。
void Setup()
{
context.SetupCameraProperties(camera);
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
flags == CameraClearFlags.Color ? camera.backgroundColor : Color.clear
);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
【skybox】
【depth】
【nothing】
Draw Calls
Shaders
Unlit Shader
下面我们开始实现自定义的shader。暂时的,我们只需要实现Unlit的shader,因此我们创建一个UnlitShader,并将其内容清空,保留基本结构。
- 第一行的字符串是创建shader的目录索引。
- 另外的,基本的结构就是Properties与SubShader和Pass。
Shader "Custom RP/Unlit"
{
Properties
{
}
SubShader
{
Pass
{
}
}
}
- 这样一来,Unlit Shader便只有最基本的渲染效果:颜色是白色,并且设定渲染队列编号是2000(不透明物体的默认编号)。
HLSL Programs
shader需要写在HLSLPROGRAM与ENDHLSL关键字之间,并且需要使用#pragma预编译指令来说明vertex与fragment着色器的名字,进一步的应该在后面定义这两个着色器,但是为了代码结构,此处将这两个着色器放在hlsl文件中,然后在此处include进来。注意include的目录是相对目录,所以hlsl文件要与shader放在同一个目录下面。
Shader "Custom RP/Unlit"
{
Properties
{
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "UnlitPass.hlsl"
ENDHLSL
}
}
}
Include Guard
HLSL可以使用include来包含并插入到include节点,为了避免重复包含,类似于c++,需要包含保护。
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#endif
Shader Functions
在hlsl中,shader是以函数的形式定义的,如下。使用SV_TARGET来说明片元着色器的输出是片元着色结果,使用SV__POSITION来说明顶点着色器的输出是顶点坐标。
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
float4 UnlitPassVertex():SV_POSITION
{
return 0.0;
}
float4 UnlitPassFragment():SV_TARGET
{
return 0.0;
}
#endif
Space Transformation
- 对于顶点着色器中的空间坐标变换,首先我们需要MVP矩阵,在此处我们声明了两个矩阵unity_ObjectToWorld与unity_MatrixVP,前者是M矩阵,后者是VP矩阵,二者都是Unity的内部计算结果,因此此处并非定义而仅仅是声明。
#ifndef CUSTOM_UNITY_INPUT_INCLUDED
#define CUSTOM_UNITY_INPUT_INCLUDED
float4x4 unity_ObjectToWorld;
float4x4 unity_MatrixVP;
#endif
- 进一步的,拿到M变换与VP变换的函数。
#ifndef CUSTOM_COMMON_INCLUDED
#define CUSTOM_COMMON_INCLUDED
#include "UnityInput.hlsl"
float3 TransformObjectToWorld(float3 positionOS)
{
return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}
float4 TransformWorldToHClip(float3 positionWS)
{
return mul(unity_MatrixVP, float4(positionWS, 1.0));
}
#endif
- 最终的,我们在顶点着色器中实现MVP变换。
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#include "../ShaderLibrary/Common.hlsl"
float4 UnlitPassVertex(float3 positionOS:POSITION):SV_POSITION
{
float3 positionWS = TransformObjectToWorld(positionOS);
return TransformWorldToHClip(positionWS);
}
float4 UnlitPassFragment():SV_TARGET
{
return 1.0;
}
#endif
Core Library
- 我们在上面实现的空间变换,已经在Core RP Library中实现了。因此,我们可以将该库install到项目中。
- 我们可以把自己实现的空间变换函数删掉,然后包含该库Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl就可以了。进一步的,在这个文件中,各种变换矩阵是跟unity的默认矩阵不同名的,因此,我们需要使用#define预编译指令来处理名称的不一致。同时,我们需要在UnityInput.hlsl中将该文件需要的unity内置矩阵也全部声明。
- 最后,我们需要声明real4 unity_WorldTransformParams;,但是该real4类型不是内置类型,而是在Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl中定义的,因此在包含UnityInput.hlsl之前需要先包含这一个文件。
#ifndef CUSTOM_UNITY_INPUT_INCLUDED
#define CUSTOM_UNITY_INPUT_INCLUDED
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 unity_MatrixInvV;
float4x4 unity_prev_MatrixM;
float4x4 unity_prev_MatrixIM;
float4x4 glstate_matrix_projection;
real4 unity_WorldTransformParams;
#endif
#ifndef CUSTOM_COMMON_INCLUDED
#define CUSTOM_COMMON_INCLUDED
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "UnityInput.hlsl"
#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_I_V unity_MatrixInvV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_PREV_MATRIX_M unity_prev_MatrixM
#define UNITY_PREV_MATRIX_I_M unity_prev_MatrixIM
#define UNITY_MATRIX_P glstate_matrix_projection
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
#endif
Color
- 为了更灵活的修改输出颜色,我们在这里用到了shader与hlsl之间的变量传递。
- 首先,我们在hlsl文件中定义了float4变量_BaseColor,进一步的,我们在shader中利用Properties来实现参数传递,传递方式为_BaseColor("Color",Color)=(1.0,1.0,0.0,1.0),即hlsl的全局变量(面板名称,面板对应的Unity变量的类型)=初始化值。
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#include "../ShaderLibrary/Common.hlsl"
float4 _BaseColor;
float4 UnlitPassVertex(float3 positionOS:POSITION):SV_POSITION
{
float3 positionWS = TransformObjectToWorld(positionOS);
return TransformWorldToHClip(positionWS);
}
float4 UnlitPassFragment():SV_TARGET
{
return _BaseColor;
}
#endif
Shader "Custom RP/Unlit"
{
Properties
{
_BaseColor("Color",Color)=(1.0,1.0,0.0,1.0)
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "UnlitPass.hlsl"
ENDHLSL
}
}
}
Batching
SRP Batcher
- 第一种思路是SRP batcher,其方法是对于同一个shader,其material的结构是一样的,那么该方法将同一sheder的material统一传送到GPU中,而不是每次call draw再去传送,当然了,其仍然会进行多次call。在绘制某一个material的几何体时,会查找该material在GPU中的偏移量,来获取shader所需要的参数。
- 在shader的inspector中可以看出,暂时是不能使用SRP batcher的,这是因为material需要的参数(逐material或者逐object)都是全局变量,需要在drawcall之前分别传送。如下所示,有两个cbuffer需要我们自己来设置,第一个是UnityPerMaterial,用来存放逐marerial的属性,另外一个是UnityPerDraw,用来存放逐object的属性。
#ifndef CUSTOM_UNITY_INPUT_INCLUDED
#define CUSTOM_UNITY_INPUT_INCLUDED
CBUFFER_START(UnityPerDraw)
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4 unity_LODFade;
real4 unity_WorldTransformParams;
CBUFFER_END
float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 unity_MatrixInvV;
float4x4 unity_prev_MatrixM;
float4x4 unity_prev_MatrixIM;
float4x4 glstate_matrix_projection;
#endif
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#include "../ShaderLibrary/Common.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
CBUFFER_END
float4 UnlitPassVertex(float3 positionOS:POSITION):SV_POSITION
{
float3 positionWS = TransformObjectToWorld(positionOS);
return TransformWorldToHClip(positionWS);
}
float4 UnlitPassFragment():SV_TARGET
{
return _BaseColor;
}
#endif
这样一来,我们的shader就可以使用SRP batcher了。
- 但是,我们还没有开启srp batch。
我们需要在我们的渲染管线实例类的构造函数中,开启SRPBatch。
public CustomRenderPipeline()
{
GraphicsSettings.useScriptableRenderPipelineBatching = true;
}
Many Colors
我们可以设置多个该shader的material,这些material会被同批次传到GPU中。但是,如果我们要实现每一个几何体都有一个属性,那么就需要若干个material。为了避免这种情况,我们通过为几何体增加Properties来实现。
首先,我们是为几何体创建c#脚本。
- 我们的类仍然是继承MonoBehaviour,该类可以将脚本绑在几何体上面,他的命名空间是UnityEngine。进一步的,我们设置DisallowMultipleComponent,表示一个几何体只能绑定一个相似类型的MonoBehaviour。
- 我们实现着色器参数绑定的方法是,利用该脚本设置一个类属性,该属性应当是c#(Unity)的类型,例如此处是Color类型,同时我们给他一个初始化值,进一步的,我们使用SerializeField属性来将其显示在面板上。这样一来,我们就可以以几何体为单位,为对象修改值。
- 此时只是能够实现修改几何体属性,但是还没有与shader属性关联。首先,我们需要利用Shader.PropertyToID("")来获得shader中的变量(可以是一般的变量,也可以是全局变量,也可以是cbuffer,事实上没有区别)的id,因为一个shader的变量id是固定的,而我们的脚本也是与该shader绑定的,所以设置成static变量。
- 需要注意的是,SRP batch与Per Object对变量的要求不一样:SRP Batch需要变量在Properties中声明,然后将其放在cbuffer中,因为只有Properities中的属性才会显示在material面板上;而Per Object需要该变量在Properities之外有定义,而对Properities并无要求,这是因为我们并不会用到材质面板属性。
- 目前我们可以通过面板设置脚本属性,也获得了shader中的变量id,但是还需要绑定并修改。我们需要定义一个MaterialPropertyBlock的全局变量,用于实现shader修改。其中,block.SetColor(,)可以通过类变量来修改给定某个id的shader变量值。最后的,我们需要GetComponent
().SetPropertyBlock(block); 来提交修改。 - 进一步的,我们需要将该过程封装在OnValidate()函数中,该函数在面板属性修改时会调用。
- 此外,我们可以在Awake()函数中调用OnValidate函数,以便在对象实例创建时初始化属性值。
using UnityEngine;
[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{
static int baseColorId = Shader.PropertyToID("_BaseColor");
static MaterialPropertyBlock block;
[SerializeField]
Color baseColor = Color.white;
private void OnValidate()
{
if(block == null)
{
block = new MaterialPropertyBlock();
}
block.SetColor(baseColorId, baseColor);
GetComponent<Renderer>().SetPropertyBlock(block);
}
private void Awake()
{
OnValidate();
}
}
- 最终的,我们将脚本绑定在几何体上,然后调整面板,就可以实现shader属性调整。
- 需要注意的是,per-object material properties与SRP batch不兼容,也就是说我们只能逐几何体来传输参数值,此时cbuffer与一般的shader变量没有差异,因此可能会使得性能下降。
GPU Instancing
- GPU实例化是另外一个批处理方法。需要注意的是,前面的SRP batch是通过使用cbuffer来实现material的批传送,并通过Properties属性来实现material面板的参值修改,这样便实现了以material为单位的批处理;per objecy material priorities是通过c#脚本将shader的变量id与面板变量捆绑,来实现面板修改以及实例对象时逐几何体修改参值。但是,instancing有一个重要的区别,便是其只能应用于mesh相同的几何体,其处理方法是将几何体之间的差异信息整理成数组来批量传入GPU,然后在绘制时只需要调用一次Call Draw(因为mesh相同),仅仅是在GPU中按照数组索引不同的参值。
- 首先的,我们加入#pragma multi_compile_instancing,这使得捆绑该shader的材质面板上可选是否使用实例化。
Shader "Custom RP/Unlit"
{
Properties
{
_BaseColor("Color",Color)=(1.0,1.0,0.0,1.0)
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma multi_compile_instancing
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#include "UnlitPass.hlsl"
ENDHLSL
}
}
}
- 在GPU instancing之下编写shader时,是通过已经编写好的core rp库来实现的,因此在define矩阵与几何变换hlsl引入UnityInstancing.hlsl。我们将使用该文件定义的一系列宏来编写shader的实例化内容。
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
- 对于我们需要实例化的数组参数,需要使用UNITY_INSTANCING_BUFFER_START()与UNITY_INSTANCING_BUFFER_END()来替代cbuffer。虽然这个在UnityInstancing中也是用cubffer来实现的。
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
- 进一步的,需要注意的是,在有cbuffer的情况下,即给出了SRP batch的条件,同时没有使用per object方法来覆盖SRP batch,那么优先使用SRP batch的,此时尽管我们将_BaseColor的cbuffer改成了instace buffer,然而我们需要注意的是,我们在前面还用到了UnityPerDraw的cbuffer,其存在也会使得GPU instance失效,因此需要取消。
- 在UnityInstancing.hlsl中,对实例id的访问是通过几struct来实现的,因此,我们需要定义一个struct来保存instanceId。宏UNITY_VERTEX_INPUT_INSTANCE_ID是其define的uint instanceID。实例化的id是需要在顶点着色器和几何着色器中从输入中提取的,因此,我们将这两个着色器的输入的参量与id写入一个struct中。
struct Attributes
{
float3 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
- 进一步的,我们改写顶点着色器,除了将之前的顶点坐标部分的输入输出与结构体相关之外,我么使用了UNITY_SETUP_INSTANCE_ID(input);来从input获得实例化的id并转换为全局id。然后使用UNITY_TRANSFER_INSTANCE_ID(input, output);将id记录在输出结构体中。
Varyings UnlitPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
return output;
}
- 最后的,我们在片元着色器中同样提取了输入(顶点着色器的输出结构体)的id,然后使用UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);获取了实例化数组缓冲UnityPerMaterial中的属性值_BaseColor。
float4 UnlitPassFragment(Varyings input):SV_TARGET
{
UNITY_SETUP_INSTANCE_ID(input);
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#include "../ShaderLibrary/Common.hlsl"
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
struct Attributes
{
float3 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings UnlitPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
return output;
}
float4 UnlitPassFragment(Varyings input):SV_TARGET
{
UNITY_SETUP_INSTANCE_ID(input);
return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}
#endif
Drawing Many Instanced Meshes
在上面,我们使用了同一个material的几何体就会批量绘制,但是这意味着我们需要实例化诸多的对象,此外,如果要绘制材质属性不同的几何体,就需要多个材质。事实上,使用实例化时,我们只需要一个mesh,这是因为实例化只能发生在一个mesh上,那么多个几何体就是浪费,此外,由于材质属性会被按照数组的形式传入GPU,那么我们也只需要一个material,只是需要重新给定数组即可。因此,我们只定义一个几何体,然后使用c#脚本来传输数组。
- 基本的代码结构与per object相似。
- 我们也需要拿到shader的properities的id。
- 我们的绘制需要使用Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 256, block),该方法需要mesh、material、matrices、count、block几个参数,因此我们需要定义mesh与material,并显示在面板上,然后选择合适的mesh与materal,注意的是,此处的这两个属性并不需要与原来的3D object相同,因此,实际上我们可以使用一个空对象。
- 类似的,我们接下来定义matrices与baseColors两个数组,并在Awake()中赋值这两个数组的值。
- 最后的,我们也需要定义MaterialPropertyBlock对象,然后利用block.SetVectorArray(baseColorId, baseColors);来传输数据。
- 最终的,我们使用上面定义的参量,以及方法Graphics.DrawMeshInstanced来绘制实例化物体。
using UnityEngine;
public class MeshBall : MonoBehaviour
{
static int baseColorId = Shader.PropertyToID("_BaseColor");
[SerializeField]
Mesh mesh = default;
[SerializeField]
Material material = default;
Matrix4x4[] matrices = new Matrix4x4[256];
Vector4[] baseColors = new Vector4[256];
MaterialPropertyBlock block;
void Awake()
{
for (int i = 0; i < matrices.Length; i++)
{
matrices[i] = Matrix4x4.TRS(
Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one
);
baseColors[i] =
new Vector4(Random.value, Random.value, Random.value, 1f);
}
}
void Update()
{
if (block == null)
{
block = new MaterialPropertyBlock();
block.SetVectorArray(baseColorId, baseColors);
}
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 256, block);
}
}
Dynamic Batching
此外,还可以用动态批处理,其将诸多的mesh合并为一个mesh。
该方法需要继续使用GPU instance的代码结构,但是该方法的优先级低于instance,更是低于SRP batch。因此可以通过下面两个部分控制开闭。
public CustomRenderPipeline()
{
GraphicsSettings.useScriptableRenderPipelineBatching = false;
}
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
enableDynamicBatching = true,
enableInstancing = false
};
Configuring Batching
我们在上一节通过调节三个bool参数实现了控制三种批处理方法,那么我们就可以不再使用硬编码的方法了。
- 在管线资产类中,创建三个bool成员并放置在面板上。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
[SerializeField]
bool useDynamicBatching = true, useGPUInstancing = true, useSRPBatcher = true;
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(useDynamicBatching, useGPUInstancing, useSRPBatcher);
}
}
- 在管线中,使用这三个参数来构造,其中useSRPBatcher直接在构造函数中设置SRP batch的开关,而另外两个参数通过成员来保存,最终用到render中。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline
{
bool useDynamicBatching, useGPUInstancing;
public CustomRenderPipeline(bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher)
{
this.useDynamicBatching = useDynamicBatching;
this.useGPUInstancing = useGPUInstancing;
GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
}
protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
{
CameraRenderer renderer = new CameraRenderer();
for (int i = 0; i < cameras.Count; i++)
{
renderer.Render(context, cameras[i], useDynamicBatching, useGPUInstancing);
}
}
}
- 在Render中,我们使用这2个参数来进行绘制,即在drawingSetting中实现开关调控。
public void Render(ScriptableRenderContext context, Camera camera, bool useDynamicBatching, bool useGPUInstancing)
{
this.context = context;
this.camera = camera;
if (!Cull()) return;
PrepareBuffer();
Setup();
DrawVisiableGeometry(useDynamicBatching, useGPUInstancing);
DrawUnsupportedShaders();
DrawGizmos();
Submit();
}
void DrawVisiableGeometry(bool useDynamicBatching, bool useGPUInstancing)
{
//Opaque
var sortingSettings = new SortingSettings(camera)
{
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing
};
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
//Skybox
context.DrawSkybox(camera);
//Transparent
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
最终的,我们可以在面板上实现调节:
此时,我们的代码结构仍然是GPU instance的结构,并注意在之前关于变换矩阵的部分也需要放进cbuffer中,通过参数而非cbuffer来控制开关。
Transparency
Blend Modes
本文来自博客园,作者:ETHERovo,转载请注明原文链接:https://www.cnblogs.com/etherovo/p/17644521.html