自定义SRP管线(一)
创建RenderPipelineAsset
创建自定义SRP管线,我们首先需要一个RenderPipelineAsset,这可以通过使用脚本继承RenderPipelineAsset这个抽象类来创建自己的RenderPipelineAsset。
具体代码如下:
//拓展编辑器,这样可以使用右键创建一个Asset
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline Asset")]
//RenderPipelineAsset继承自ScriptableObject,是一个Unity的资产文件,类似XML,JSON等可以保存数据,但是我们可以通过Inspector面板看到里面的数据。运行时可以通过引用获取到里面的数据,相比传统Monobehavior的值拷贝传递,节省空间。存储不变数据最好使用ScriptableObject
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
//继承自RenderPipelineAsset这个抽象类的类必须实现这个抽象方法,返回一个自定义的RenderPipeline
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline();
}
}
这个脚本编译之后,可以在Asset下右键依次点击Create/Rendering/Custom Render Pipeline Asset创建一个Asset,之后可以在Edit/Project Settings/Graphics下指定需要使用的RenderPipelineAsset,
指定之后,Scene窗口和Game窗口都会完全变成黑色,这是因为我们只是创建了一个Pipeline,但是还没有实现任何渲染流程。
创建RenderPipeline
上面的这个Asset可以和引擎核心进行交流,确定使用的RenderPipeline,因此必须返回一个RenderPipeline。RenderPipeline里我们可以手动实现每一步的渲染流程,这就很像OpenGL的编程了,区别是Unity给我们封装好了很多函数,比OpenGL方便很多。
创建一个自定义的RenderPipeline,我们需要继承自RenderPipeline这个抽象类。
//自定义RenderPipeline,需要继承自RenderPipeline抽象类
public class CustomRenderPipeline : RenderPipeline
{
//这个RenderPipeline可以持有很多Renderer,首先我们需要一个摄像机Renderer,渲染出所有相机观察到的东西
private readonly CameraRenderer m_CameraRenderer = new();
//引擎每一帧都会调用这个自己实现的抽象方法Render,里面有一个context和所有的摄像机
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
//对每个摄像机,使用摄像机渲染场景
foreach (var camera in cameras)
{
m_CameraRenderer.Render(context,camera);
}
}
}
上面的ScriptableRenderContext是当前使用的图形API的抽象,可使用 ScriptableRenderContext 向 GPU 调度和提交状态更新和绘制命令。
实现CameraRenderer
public class CameraRenderer
{
private ScriptableRenderContext m_Context;
private Camera m_Camera;
private const string BufferName = "Render Camera";
//在 Scriptable Render Pipeline 中,当 Unity 执行剔除操作时,它会将结果存储在 CullingResults 结构中。此数据包括有关可见对象、灯光和反射探测器的信息。Unity 使用此数据来渲染对象和处理灯光。 CullingResults 结构还提供了几个函数来帮助阴影渲染。
private CullingResults m_CullingResults;
//支持的SRP ShaderID
private static readonly ShaderTagId UnlitShaderTagId = new("SRPDefaultUnlit");
private readonly CommandBuffer m_Buffer = new()
{
name = BufferName
};
}
CameraRenderer肯定需要持有当前的ScriptableRenderContext,和所使用的相机。CommandBuffer则包含一组图形命令,比如设置渲染目标,采样渲染命令显示到Frame Debugger中。目前主要是把渲染的所有Draw Call显示出来。
public void Render(ScriptableRenderContext context, Camera camera)
{
m_Context = context;
m_Camera = camera;
if (!Cull())
{
return;
}
SetUp();
DrawVisibleGeometry();
DrawUnsupportedShaders();
Submit();
}
在Render函数里,接收RenderPipeline中传来的context和相机。
private void SetUp()
{
m_Context.SetupCameraProperties(m_Camera);
m_Buffer.ClearRenderTarget(true, true, Color.clear);
//开始采样
m_Buffer.BeginSample(BufferName);
ExecuteBuffer();
}
SetUp主要是设置摄像机的视图和投影矩阵,清屏(类似glClear(),glClearColor())防止上一帧的画面影响当前帧。然后使用CommandBuffer开始采样,这主要是想让FrameDebugger显示渲染的所有DrawCall信息。
private void DrawVisibleGeometry()
{
//渲染不透明物体
var sortingSettings = new SortingSettings(m_Camera)
{
criteria = SortingCriteria.CommonOpaque
};
var drawingSettings = new DrawingSettings(UnlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
//渲染天空盒
m_Context.DrawSkybox(m_Camera);
//渲染透明物体
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
}
Submit则是提交自己在Context中配置的所有DrawCall
private void Submit()
{
//结束采样
m_Buffer.EndSample(BufferName);
ExecuteBuffer();
m_Context.Submit();
}
在上面DrawVisibleGeometry()渲染各种物体的时候,需要一些参数,比如渲染不透明物体的时候,我们一般由近到远渲染物体,因为如果后面的物体被挡到,我们就可以不花费时间去渲染它了。
这就需要我们设置
var sortingSettings = new SortingSettings(m_Camera)
{
criteria = SortingCriteria.CommonOpaque
};
而在渲染透明物体的时候,我们又需要从远到近渲染,这是因为我们最终可能要混合各种不透明物体的颜色
因此设置
sortingSettings.criteria = SortingCriteria.CommonTransparent;
filteringSettings则是设置的过滤条件,Unity对于不透明和透明物体,分别设置了两条渲染队列,把两类物体分在两个队列里,依次渲染。
//只渲染不透明物体队列中的物体
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
//只渲染透明物体队列中的物体
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
在设置好这些参数之后,就可以使用下列代码让context渲染了
ref关键字表示按引用传递,类似C++中的const-to-references,减少拷贝带来的性能消耗。
m_Context.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
这里的m_CullingResults则是相机的剔除结果,获得 CullingResults 结构,调用 ScriptableRenderContext.Cull。
private bool Cull()
{
//获取相机剔除参数
if (!m_Camera.TryGetCullingParameters(out var p)) return false;
//使用参数进行剔除
m_CullingResults = m_Context.Cull(ref p);
return true;
}
渲染Unity UI物体
上面的这些代码并不会在Scene窗口下渲染各种Unity 内部的UI,如果我们想在scene下看到UI,我们需要显式告诉Unity,通过下列代码:
partial void PrepareForSceneWindow()
{
//如果相机是Scene窗口的相机,那么渲染UI物体
if (m_Camera.cameraType == CameraType.SceneView)
{
ScriptableRenderContext.EmitWorldGeometryForSceneView(m_Camera);
}
}
渲染Gizmoz
partial void DrawGizmos()
{
if (!Handles.ShouldRenderGizmos()) return;
//Gizmoz有两种,一起渲染
m_Context.DrawGizmos(m_Camera, GizmoSubset.PreImageEffects);
m_Context.DrawGizmos(m_Camera, GizmoSubset.PostImageEffects);
}
渲染不支持的Shader
在URP中,我们可以看到普通的shader呈现出一种粉色,表示不支持这个shader,给开发者一个提示,换成支持的SRP shader
我们可以把不支持的shader列出来
private static readonly ShaderTagId[] LegacyShaderTagIds =
{
new("Always"),
new("ForwardBase"),
new("PrepassBase"),
new("Vertex"),
new("VertexLMRGBM"),
new("VertexLM")
};
然后渲染这些不支持的shader
private static Material _errorMaterial;
partial void DrawUnsupportedShaders()
{
if (_errorMaterial == null)
{
//把_errorMaterial设置为粉色的shader
_errorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));
}
var drawingSettings = new DrawingSettings(
LegacyShaderTagIds[0], new SortingSettings(m_Camera)
)
{
overrideMaterial = _errorMaterial
};
for (var i = 1; i < LegacyShaderTagIds.Length; i++)
{
//配置所有不支持的shader
drawingSettings.SetShaderPassName(i, LegacyShaderTagIds[i]);
}
var filteringSettings = FilteringSettings.defaultValue;
m_Context.DrawRenderers(
//使用粉色shader渲染
m_CullingResults, ref drawingSettings, ref filteringSettings
);
}
粉色是使用不支持的shader的物体
完整Render代码
public void Render(ScriptableRenderContext context, Camera camera)
{
//设置使用的context和camera
m_Context = context;
m_Camera = camera;
//配置buffer名称,让Frame Debugger显示
PrepareBuffer();
//渲染UI
PrepareForSceneWindow();
//剔除
if (!Cull())
{
return;
}
//设置相机参数,清空屏幕缓存
SetUp();
//渲染可见物体
DrawVisibleGeometry();
//渲染不支持的shader
DrawUnsupportedShaders();
//渲染Gizmoz
DrawGizmos();
//提交上面的所有渲染命令
Submit();
}
ClearFlags
每个相机都有一个ClearFlags参数
从上到下依次为Skybox,Solid Color,Depth Only,Not Clear。
依次表示清空所有,只清空天空盒并保留颜色和深度缓存,清空天空盒和背景颜色并只保留深度缓存,和什么都不清空。
我们创建两个相机,放在相同位置,第一个选择天空盒子。
在SetUp中,设置当CameraClearFlags<=CameraClearFlags.Depth清空颜色缓冲,这表示当我们设置flag为Skybox和Solid Color和Depth Only的时候清空深度缓冲。
然后设置 flags == CameraClearFlags.Color的时候清空颜色缓冲,这表示只有当我们设置相机flag为Solid Color的时候清空颜色缓冲。
private void SetUp()
{
m_Context.SetupCameraProperties(m_Camera);
var flags = m_Camera.clearFlags;
m_Buffer.ClearRenderTarget(flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
flags == CameraClearFlags.Color ? m_Camera.backgroundColor.linear : Color.clear);
//开始采样
m_Buffer.BeginSample(sampleName);
ExecuteBuffer();
}
如果第二个选择天空盒,表示第二个相机全部重新渲染,因此第一个相机的所有画面都看不到了。
如果第二个选择Solid Color,表示清空颜色缓冲,并且使用自己相机的背景颜色重新渲染,因此第一个相机的天空盒和渲染结果,被第二个相机的背景颜色完全覆盖了,表现为
当我们设置第二个相机为Depth Only,表示只清除第一个相机的深度缓冲,因此第二个相机的画面都会在第一个相机画面的前方。
当我们设置第二个相机为Not Clear,我们对于第一个相机的渲染结果不做任何处理,
主要区别在这个地方,紫色方块是在绿色方块后方的,如果我们清空深度缓冲,第二张图的渲染结果会覆盖第一张图,因此粉色方块反而出现在绿色方块的前方了,不清空深度缓冲的话,渲染结果就正确了。