该教程部分完成的工程状态可见:Custom Render Pipeline

#A new Render Pipeline

早期的 Unity 仅支持 内置渲染管线(Default Render Pipeline, DRP / Built-in Render Pipleline)。自 Unity 2018 后,Unity 引入了 可编程渲染管线(Scriptable Render Piplelines,SRP) ,但在 2018 中该功能是试验预览的状态,在 Unity 2019 中该功能才成为 正式功能。

基于 SRP ,Unity 官方在 2018 的版本中实现了两套管线, Lightweight Render PipelineHigh Definition Render Pipeline 。前者针对于移动端这样的轻量级平台,而后者针对如 PC,主机这样的高性能平台。在 Unity 2019 的版本中, Lightweight Render Pipeline 被拓展为 Universal Render Pipeline

Lightweight Render PipelineUniversal Render Pipeline 实际上是同一套管线,Lightweight Render Pipeline 仅是 Unity 2018 中的早期实现版本的命名。

Universal Render Pipeline 计划最终取代目前的内置渲染管线,成为 Unity 渲染的默认渲染管线。

#Project Setup

该笔记使用的 Unity 版本为 2022.3.12f1

#Color Space

Unity 工程的默认色彩空间 Gamma,而为了保证后续光照等计算的准确性,首先需要将颜色空间切换为线性空间,可通过 Edit -> Project Settings -> Player -> Other Settings -> Rendering -> Color Space 修改。

#Sample Scene

在场景中随意放置一些 Cube 和 Sphere,并附加不同的材质,结果如下图所示:

所使用的材质设置如下图所示:

Red Blue Green/Yellow/White

#Pipeline Asset

SRP 相关的脚本基本都在 UnityEngine.Rendering 命名空间下,且 SRP 已经在引擎内包含,因此此时并不需要额外导入其他的 Package。

当使用 SRP 时,Unity 引擎需要通过 RenderPipe Asset(RP Asset) 来获取渲染管线的实例,同时也会从 RP Asset 中读取关于渲染管线的设置。

为了创建 RP Asset ,首先需要创建对应的 ScriptableObject 。可以通过继承 RenderPipelineAsset 基类创建出可以构建 RP AssetScriptableObject 。如下所示:

1
2
3
4
5
6
7
8
using UnityEngine;
using UnityEngine.Rendering;

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
protected override RenderPipeline CreatePipeline() { return null; }
}

所有派生自 RenderPipelineAsset 的类都必须实现 CreatePipeline 函数,Unity 使用该函数获取渲染管线的实例。

之后可以通过 Assets -> Create -> Rendering -> Custom Render Pipeline 创造出 RP Asset ,结果如下所示:

可以通过 Project Settings -> Graphics -> Scriptable Render Pipeline Settings 将自定义的 RP Asset 设置给 Unity,如下所示:

当替换后了 RP Asset 后,主要有两个变化:

  1. Graphics 面板中的许多设置消失了。
    因为替换的 RP Asset 并没有提供相关的设置选项。

  2. Scene / Game / Material 界面都不再渲染任何东西
    因为替换的 RP Asset 实际上返回的是空,即 Unity 此时没有任何的渲染管线可以用。

#Render Pipeline Instance

为了创建出一个渲染管线,需要通过继承 RenderPipeline 构建自定义的渲染管线类,所有的派生自 RenderPipeline 的类都必须实现 Render 函数,Unity 在每一帧通过触发该函数进行渲染,如下所示:

1
2
3
4
5
6
7
8
9
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){}
}

可以看到上述的实现中,对于 Render 函数有两个重载,其分别有 Camera[]List<Camera> 的形参,在 Unity 2022 之前, 引擎仅支持形参为 Camera[] 的重载版本。而在 Unity 2022 之后,引擎又引入了形参为 List<Camera> 的重载版本。
为了后续的遍历的便捷性,这里使用 List<Camera> 版本的重载,对于 Camera[] 版本的重载,保持空实现即可。

因为形参为 Camera[] 的函数原先被标记为了 abstract,因此必须被定义。

之前的 CustomRenderPipelineAsset.CreatePipeline 函数就可以返回该自定义渲染管线的示例,如下所示:

1
2
3
4
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline();
}

此时 Unity 已经可以使用 CustomRenderPipeline 进行绘制,但此时所有的界面与之前并没有任何的区别,因为定义的 CustomRenderPipeline 中并没有进行任何的实质渲染。

#Rendering

Unity 通过 Render Pipeline Instance 中的 Render 函数进行渲染,Render 函数有两个形参:

  1. ScriptableRenderContext :该形参表示 SRP 渲染的上下文。 RP 使用该形参与 Unity Native 的渲染部分进行通信

  2. Camera[] ,该形参表示所有激活的 Cameras

    RP 使用该形参来控制每个摄像机的渲染与不同摄像机间的渲染顺序

#Camera Renderer

通过 ScriptableRenderContextCamera 就可以控制每个摄像机的渲染,如可以通过自定义的 CameraRenderer 类来负责特定摄像机的渲染:

1
2
3
4
5
6
7
8
9
10
11
12
public class CameraRenderer
{
private ScriptableRenderContext m_RenderContext = default;
private Camera m_Camera = null;

public void Render(ScriptableRenderContext renderContext, Camera camera)
{
m_RenderContext = renderContext;
m_Camera = camera;
// ....
}
}

在之前的 CustomRenderPipeline 中,让每一个相机都调用 CameraRenderer.Render 函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
public class CustomRenderPipeline : RenderPipeline
{
private CameraRenderer m_Renderer = new();

protected override void Render(ScriptableRenderContext context, Camera[] cameras) { }

protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
{
cameras.ForEach(camera => m_Renderer.Render(context, camera));
}
}

#Drawing the Skybox

CameraRenderer.Render 的功能就是渲染所有该摄像机可以看到的物体。如以下的实现,可以让 CameraRender 渲染出天空盒:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Render(ScriptableRenderContext renderContext, Camera camera)
{
m_RenderContext = renderContext;
m_Camera = camera;

Setup();
DrawVisibleGeometry();
Submit();
}

private void Setup() { m_RenderContext.SetupCameraProperties(m_Camera); }
private void DrawVisibleGeometry() { m_RenderContext.DrawSkybox(m_Camera); }
private void Submit() { m_RenderContext.Submit(); }

在实现中,对 ScriptableRenderContext 调用了一系列函数来完成绘制目的:

  • SetupCameraProperties 用于在 Shader 中设置摄像机相关的变量,如 View 矩阵,Projection 矩阵
  • DrawSkybox 将渲染天空盒的命令添加到 Context 的缓冲中
  • Submit 将 Context 缓冲中的命令添加到执行队列中。

仅当 Camera 的 ClearFlags 是 Skybox 时, DrawSkybox 才会真正的将绘制天空盒的命令添加到缓冲中。

结果如下所示:
Draw the Skybox

#Command Buffers

之前的 DrawSkybox 命令向 Context 的缓冲中增加了一条渲染天空盒的命令。除此之外,可以通过 CommandBuffer 类和 context.ExecuteCommandBuffer 函数向 Context 中添加自定义的渲染命令。

通过如下命令创建 CommandBufferCommandBuffername 属性可以在 FrameDebugger 中查看:

1
2
private const string k_BufferName = "Render Camera";
private CommandBuffer m_Buffer = new() {name = k_BufferName};

FrameDebugger 可以通过 Window -> Analysis -> Frame Debugger 打开。
Profiler 可以通过 Window -> Analysis -> Profiler 打开。

而如果想要在 Profiler 中调试, 则可以使用 commandBuffer.BeginSamplecommandBuffer.EndSample API 将开始采样和结束采样的命令添加至 Command Buffer 中,再通过 ScriptableRenderContext.ExecuteCommandBuffer 执行 Command Buffer。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void Setup()
{
m_Buffer.BeginSample(k_BufferName);
ExecuteCommandBuffer();

m_RenderContext.SetupCameraProperties(m_Camera);
}

private void DrawVisibleGeometry() { m_RenderContext.DrawSkybox(m_Camera); }

private void Submit()
{
m_Buffer.EndSample(k_BufferName);
ExecuteCommandBuffer();

m_RenderContext.Submit();
}

private void ExecuteCommandBuffer()
{
m_RenderContext.ExecuteCommandBuffer(m_Buffer);
m_Buffer.Clear();
}

BeginSampleEndSample 的命名需要与 Buffer 的名称相同,否则可能会出现 Non matching Profiler.EndSample (BeginSample and EndSample count must match 的错误。

BeginSampleEndSample 这样的 API 每次执行都会向 Command Buffer 中增加一条命令,因此在 ExecuteCommandBuffer 中执行后需要对 Command Buffer 进行 Clear 操作,否则 Command Buffer 中的命令会越来越多。

此时在 Frame Debugger 中既可以看到之前添加的 Buffer Name 信息:
Buffer Name in frame debugger

在 Profiler Window 中也能看到相应的信息:
Buffer Name in Profiler

#Clearing the Render Target

可通过在 Command Buffer 中添加 ClearRenderTarget 命令来清除渲染目标的内容,如下所示:

1
2
3
4
5
6
7
private void Setup()
{
renderContext.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear); // Clear render target
buffer.BeginSample(k_BufferName);
ExecuteCommandBuffer();
}

此时可以在 Frame Debugger 中看到 Clear 的命令,如下所示:
Clear Command

对于增加 Clear Render Target 命令的顺序,必须严格按照上述例子,即先设置 Camera Properties,再进行 Clear,再进行 BeginSample。
如果先进行了 Clear,再进行了 SetupCameraProperties,那么 Frame Debugger 中会显示 Draw GL 命令而非 Clear 命令,即 Unity 通过渲染一张铺满整个渲染目标的 Quad 来达成清除的目的,而这会消费较多的性能。如下代码就会导致渲染 Quad:

1
2
3
4
5
6
7
private void Setup()
{
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(k_BufferName);
ExecuteCommandBuffer();
renderContext.SetupCameraProperties(camera);
}

此时的 Frame Debugger 窗口将显示如下内容:
Draw GL
又因为 CommandBuffer.ClearRenderTarget 的实现会将 Clear 的操作放在一个以 Command Buffer 名称命名的 Sample 中,所以如下的代码将再 Frame Debugger 窗口中引发嵌套的 Sample,如下所示:

1
2
3
4
5
6
7
private void Setup()
{
buffer.BeginSample(k_BufferName);
buffer.ClearRenderTarget(true, true, Color.clear);
ExecuteCommandBuffer();
renderContext.SetupCameraProperties(camera);
}

此时的 Frame Debugger 窗口将显示如下内容:
嵌套 Sample | 300

#Culling

在正式的渲染前,为了保证仅渲染在摄像机的视锥体的内物体,需要让 Unity 进行 Culling 操作。为完成 Culling 操作,首先需要通过函数 TryGetCullingParameters 根据摄像机当前的状态获取到 Culling 相关的参数,再通过函数 context.Cull 将相关参数传递给渲染上下文,并得到 Culling 的结果,结果将以 CullingResults 表示,代码如下所示:

1
2
3
4
5
6
7
8
9
10

private CullingResults m_CullingResults = default;

private bool Cull()
{
if (!m_Camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters)) return false;

m_CullingResults = m_RenderContext.Cull(ref cullingParameters);
return true;
}

上述 Cull 函数,返回 bool 值表示获取 Culling 参数是否成功。在某些情况下,无法通过 TryGetCullingParameters 函数获取到 Culling 的参数,如摄像机的 Viewport 为空,或者近远剪切平面的设置不合法。

对于 Render 函数,应当仅在 Cull 成功的情况下再进行渲染如下所示:

1
2
3
4
5
6
7
8
9
public void Render(ScriptableRenderContext renderContext, Camera camera)
{
m_RenderContext = renderContext;
m_Camera = camera;

if(!Cull()) return;

//...
}

SRP 中许多函数都以 ref 传递数值,如 context.Cull 函数。但这通常是处于性能方面的考虑,避免数据的拷贝,而非是需要修改传入的参数。

#Drawing Geometry

现在可以通过 context.DrawRenderers 方法绘制具体的几何体,其中会用上之前获取到的 CullingResults 如下所示:

1
2
3
4
5
6
7
8
9
10
11
private static ShaderTagId s_UnlitShaderTagId = new("SRPDefaultUnlit");
//...
private void DrawVisibleGeometry()
{
var sortingSettings = new SortingSettings(m_Camera) {criteria = SortingCriteria.CommonOpaque};
var drawingSettings = new DrawingSettings(s_UnlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.all);

m_RenderContext.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
m_RenderContext.DrawSkybox(m_Camera);
}

其中:

  • FilteringSetting 决定了渲染指定 RenderQueue 范围内的物体,这里填的 RenderQueueRange.all 表示无论 RenderQueue 设置的为多少,都将被渲染。
  • DrawingSettings 的第一个形参决定了需要执行的 Shader Pass, 这里传递的 SRPDefaultUnlit 为 Unity 内置的 Tag,因为目前场景中的许多游戏物体选用的是 Unlit 中的 Shader,所以使用该 Tag。

关于 Shader Tag 的内容,查看文档 Built-In Shader TagSRP Shader Tag

DrawingSettings 第二个形参是物体排序相关的设置 SortingSettings,该变量的构造函数依赖 camera 变量,因为其中依赖 camera.transparencySortMode 决定以什么规则来计算排序的数值大小:

  1. Perspective:根据摄像机与物体中心的距离
  2. Orthographic:根据沿着摄像机 View 方向的距离

SortingSettings 中的 criteria 制定了排序的标准,如这里的 CommonOpaque 表示使用通常渲染不透明物体时的排序规则,该规则会综合考虑 RenderQueue,材质,距离等相关信息。

此时在 Frame Debugger 中查看渲染的顺序与结果,如下所示,可以看到基本是先渲染一个特定的材质,然后再渲染下一个:
渲染顺序与结果

如果查看 CommonOpaque 的定义可以看到,它考虑了尽可能的减少渲染上下文的切换,从前至后渲染等因素:

1
2
3
4
5
6
7
8
9
public enum SortingCriteria
{
// ...
/// <summary>
/// <para>Typical sorting for opaque objects.</para>
/// </summary>
CommonOpaque = CanvasOrder | OptimizeStateChanges | QuantizedFrontToBack | RenderQueue | SortingLayer, // 0x0000003B
// ....
}

物体具体的渲染顺序受 Unity 版本 / Unity 实现影响,这里的设置 criteria 更多的是一种“建议”,而具体的排序算法,在 Unity 引擎内部实现,是一个相对黑盒。

如果将 SortingSettings 中的 criteria 去除,即:

1
2
3
4
5
6
7
8
9
private void DrawVisibleGeometry()
{
var sortingSettings = new SortingSettings(m_Camera);
var drawingSettings = new DrawingSettings(s_UnlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.all);

renderContext.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
renderContext.DrawSkybox(camera);
}

则渲染的结果如下所示,几乎是一个无规律的状态在渲染:
Random Rendering

#Drawing Opaque and Transparent Geometry Separately

在之前的最终渲染结果中,天空盒将半透明物体的一部分遮挡掉了,如下所示:
Wrong Effect of Transparent Object

这是因为天空盒在半透明物体的之后进行渲染,而在 Unlit/Transparent 的 Shader 中,设置了 ZWrite Off ,即半透明物体不会写入深度缓冲,因此在绘制了半透明物体的部分,天空盒仍然能通过深度检测,即覆盖半透明物体。

解决这个问题的方式,就是调整渲染顺序为 不透明物体 -> 天空盒 -> 半透明物体 。实现方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void DrawVisibleGeometry()
{
var sortingSettings = new SortingSettings(m_Camera) {criteria = SortingCriteria.CommonOpaque};
var drawingSettings = new DrawingSettings(s_UnlitShaderTagId, sortingSettings);
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
m_RenderContext.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);

m_RenderContext.DrawSkybox(m_Camera);

sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
m_RenderContext.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
}

渲染结果如下:
渲染结果

#Editor Rendering

#Drawing Legacy Shaders

之前通过在初始化 DrawingSettings 时,设置的 Shader Tag 为 SRPDefaultUnlit 的 Shader,因此仅会渲染 Unlit Shader 的物体。而其余的物体,如使用了 Standard Shader 的物体,可以通过以下 Built-in 的 Shader Tag 找到并渲染:

1
2
3
4
5
6
7
8
9
private static ShaderTagId[] s_LegacyShaderTagIds =
{
new("Always"),
new("ForwardBase"),
new("PrepassBase"),
new("Vertex"),
new("VertexLMRGBM"),
new("VertexLM")
};

之后我们新建一个用以渲染这些 Built-in Shader 的物体的函数 DrawUnSupportedShadersGeometry ,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void Render(ScriptableRenderContext renderContext, Camera camera)
{
//...
Setup();
DrawVisibleGeometry();
DrawUnSupportedShadersGeometry();
Submit();
}

private void DrawUnSupportedShadersGeometry()
{
var drawingSettings = new DrawingSettings {sortingSettings = new SortingSettings(m_Camera)};
for (int i = 0; i != s_LegacyShaderTagIds.Length; ++i)
drawingSettings.SetShaderPassName(i, s_LegacyShaderTagIds[i]);

var filteringSettings = FilteringSettings.defaultValue;

m_RenderContext.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
}

其中的 legacyShaderTagIds 中指定了常用的 Built-in 的 Shader Tag,即会尝试渲染 Built-in Shader 的物体。结果如下所示,可以看到使用了 Standard Shader 的物体被渲染了出来:
Rendering Effect of Built-in Shader

在早期的 Unity 版本中,此时 Standard Shader 的物体会被渲染成黑色,这是因为早期 SRP 无法设置 Standard Shader 中的一些参数。

#Error Material

虽然在 Unity 2022 中,一些 Built-in Shader 的物体可以被渲染出来,但这仍然不健壮,且应当有明确的错误提示开发者应当将 Built-in Shader 替换为 SRP Shader,为达到这个目的,可以使用 Unity 内置的表示 Shader 错误的特殊 Shader 来渲染这些物体,只需要修改 DrawingSettings 中的 overrideMaterial 即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Material s_ErrorMaterial = null;

private void DrawUnSupportedShadersGeometry()
{
if (s_ErrorMaterial == null)
s_ErrorMaterial = new Material(Shader.Find("Hidden/InternalErrorShader"));

var drawingSettings = new DrawingSettings();
drawingSettings.sortingSettings = new SortingSettings(m_Camera);
drawingSettings.overrideMaterial = s_ErrorMaterial;
for (int i = 0; i != s_LegacyShaderTagIds.Length; ++i)
drawingSettings.SetShaderPassName(i, s_LegacyShaderTagIds[i]);

var filteringSettings = FilteringSettings.defaultValue;

m_RenderContext.DrawRenderers(m_CullingResults, ref drawingSettings, ref filteringSettings);
}

此时结果如下:
渲染结果

#Partial Class

可以使用 Scripting Symbols 让不支持的 Shader 部分仅在 Editor 和 Development Build 才被显示,即将相关代码定义放到如下的代码块中:

1
2
3
#if UNITY_EDITOR || DEVELOPMENT_BUILD
//...
#endif

同时为了更好的管理代码,可以将 Editor 部分放到 CameraRenderer.Editor 中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// In CameraRenderer.Editor.cs
public partial class CameraRenderer
{

#if UNITY_EDITOR || DEVELOPMENT_BUILD
private static ShaderTagId[] legacyShaderTagIds =
{
// ...
};

private static Material s_ErrorMaterial = null;

partial void DrawUnSupportedShadersGeometry()
{
// ...
}
#endif
}

// in CameraRenderer.cs
partial void DrawUnSupportedShadersGeometry();

这里使用了 Partial Classes 拆分 CameraRenderer 类,方便代码管理。并将 DrawUnSupportedShaderGeometry 函数定义为 Partial Methods,保证在非 Editor 和 Development Build 时,即使 DrawUnSupportedShaderGeometry 未被定义实现,代码仍然能正常编译。

#Drawing Gizmos

目前在 Scene 场景中并没有绘制 Gizmo ,如场景中并没有摄像机的显示,也没有摄像机的视锥体的展示。

可以通过 Handles.ShouldRenderGizmos 判断当前帧是否需要渲染 Gizmos ,如需要的话可通过函数 context.DrawGizmos 进行绘制。

Editor Scene 下的 Gizmos Toggle 会影响 Handles.ShouldRenderGizmos 的返回值。
Gizmos Toggle

Unity 的 Handles 存在许多关于 Gizmos 的帮助函数

Gizmos 的绘制应当在整个流程的最后,最终绘制 Gizmos 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// In CameraRenderer
partial void DrawGizmos();

public void Render(ScriptableRenderContext renderContext, Camera camera)
{
// ...
Setup();
DrawVisibleGeometry();
DrawUnSupportedShadersGeometry();
DrawGizmos();
Submit();
}

// In CameraRenderer.Editor
partial void DrawGizmos()
{
if (!Handles.ShouldRenderGizmos()) return;
m_RenderContext.DrawGizmos(m_Camera,GizmoSubset.PreImageEffects);
m_RenderContext.DrawGizmos(m_Camera,GizmoSubset.PostImageEffects);
}

其中 context.DrawGizmos 需要两个参数,第一个是表示当前 View 的 Camera, 第二个表示哪种 Gizmos 需要被绘制, GizmoSubset.PreImageEffects 表示受后处理影响的 GizmosGizmoSubset.PostImageEffects 表示不受后处理影响的部分。这里选择渲染所有种类的 Gizmos 。渲染的结果如下:
渲染结果

#Drawing Unity UI

在场景中添加了一个 UGUI 的 Button 后,可以看到按钮在 Game 界面中被正常的渲染了出来,如下所示:
Button in Game View

但通过 Frame Debugger 可以发现此时 UI 的渲染并没有经过自定义的 SRP 如下所示:
Frame Debugger for UI

而当将 Canvas 中的 Render Mode 修改为 Screen Space - CameraWorld Space 后,UI 的渲染被放到了渲染半透明物体的部分中,如下所示,且此时因为在半透明的队列中先渲染了 UI,所以 UI 几乎被其他物体遮挡住了:

但无论 Render Mode 是什么格式,在 Scene 界面中,UI 都没有被正常的渲染出来,能看到的只有 UI 的 Gizmo ,如下:

这是因为 UI 在 Scene 界面下,都是以 World Space 模式被渲染出来,而且用了不同的几何信息,且 UI 在 Scene 下的几何信息默认并没有被添加到 SRP 中。

对于在 Scene 中显示的 UI 的几何信息,需要通过函数 ScriptabEmitWorldGeometryForSceneView 添加到 SRP 中。且需要在调用 Cull 函数前被添加,保证这些几何信息同样会被进行正常裁剪。

整体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// In CameraRenderer
partial void PrepareForSceneWindow();

public void Render(ScriptableRenderContext renderContext, Camera camera)
{
// ...
PrepareForSceneWindow();
if (!Cull()) // Get Culling parameters failed
return;
// ...
}

// In CameraRenderer.Editor
partial void PrepareForSceneWindow()
{
if (camera.cameraType == CameraType.SceneView)
{
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}

ScriptableRenderContext.EmitWorldGeometryForSceneView 函数的描述如下:

1
2
3
4
5
6
7
/// <summary>
/// <para>Emits UI geometry into the Scene view for rendering.</para>
/// </summary>
/// <param name="cullingCamera">Camera to emit the geometry for.</param>
[FreeFunction("UI::GetCanvasManager().EmitWorldGeometryForSceneView")]
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void EmitWorldGeometryForSceneView(Camera cullingCamera);

此时在 Scene 界面下就可以看到 UI 被正确的渲染出来。

#Multiply Cameras

#Two Cameras

在场景中可以将 Main Camera 进行拷贝,并将新的 Camera 命名为 Second Camera ,并将 Second CameraDepth 参数设置为 0,即此时会先渲染 Main Camera ,然后再渲染 Second Camera

Hierarchy Main Camera Second Camera

此时在 Frame Debugger 中可以看到两个摄像机的渲染被合并在了一起,如下所示:
Red For Main, Yellow For Second

这是因为此时两个 Camera 对应的 CameraRenderer 中的 Command Buffer 命名相同,因此 Frame Debugger 将两者的信息合并在了一起。

可以通过分别对两个 Command Buffer 进行命令来分开两者的渲染信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// In CameraRenderer
partial void PrepareBuffer();
public void Render(ScriptableRenderContext renderContext, Camera camera)
{
// ...
PrepareBuffer();
PrepareForSceneWindow();
if (!Cull()) // Get Culling parameters failed
return;
// ...
}

// In CameraRenderer.Editor
partial void PrepareBuffer()
{
buffer.name = camera.name;
}

camera.name 会造成内存分配,但因为 PrepareBuffer 是定义在 CameraRender.Editor 中,因此仅在 Editor 模式下运行,不会造成运行时的性能浪费。

此时 Frame Debugger 界面如下:
Main And Second Camera|500

#Layers

可以调整物体的 Layer 以及摄像机的 Culling Mask 来控制摄像机仅渲染特定的游戏物体。

如将所有使用了 Standard 的游戏物体的 Layer 调整为 Ignore Raycast ,并将两个摄像机的 Culling Mask 设置为如下:

Main Camera Second Camera

此时的渲染结果如下,因为 Second Camera 仅渲染 Ignore Raycast Layer 的物体,又 Second Camera 会覆盖 Main Camera 的内容:
Second Camera

#Clear Flags

可以通过修改两个摄像机的 Clear Flags 来合并两个摄像机的渲染内容。并根据摄像机的 Clear Flags 调整 ClearRenderTarget 的逻辑,如下所示:

1
2
3
4
5
6
7
8
private void Setup()
{
// ...
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(flags <= CameraClearFlags.Depth, flags == CameraClearFlags.Color,
flags == CameraClearFlags.Color ? camera.backgroundColor.linear : Color.clear);
// ...
}

其中 CameraClearFlags 是 Unity 定义的一个枚举值,有四个参数,参数数值从 1 到 4,分别为 Skybox , Color , Depth , Nothing

上述代码中,除了 Nothing 的情况,都会将 Depth Buffer 清除,而仅在为 Color 的时候会对 Color Buffer 进行清除。在清除时,仅当为 Color 时使用 camera.backgroundColor 其余时候都用 Color.clear

使用 camera.backgroundColor.linear 是因为项目建立时,将颜色空间设置为了 Linear

理论上,在 CameraClearFlagsSkybox 时也应当清除 Color Buffer ,但因为 Skybox 时擦除了 Depth Buffer,又会在渲染的最后绘制 Skybox,所以上一帧的颜色内容即使不清除,也会被这一帧渲染的 Skybox 覆盖,因此不会造成显示的错误。

Main Camera 作为第一个渲染的摄像机,为了保证渲染的正确性,必须使用 SkyboxColor 作为 Clear Flags。 Second Camera 为了不 Clear 掉 Main Camera 渲染的内容,则必须使用 DepthNothing 保证 Main Camear 渲染的 Color Buffer 被保持。

Second Camera 选择 Depth 时, Main Camera 渲染的 Depth Buffer 会被 Clear,此时 Second Camera 渲染的内容就都会叠加到 Main Camera 的内容上。

Second Camera 选择 Nothing 时, Main Camera 渲染的 Depth Buffer 会被保留,此时 Second Camera 渲染的内容就仍要与 Main Camera 渲染的内容进行深度检测。

Main Camera Clear Flags 为 SkyboxSecond Camera 的 Clear Flags 分别为 Skybox , Color , Depth , Nothing 的结果如下:

Skybox Color
Depth Only Don't Clear

还可以通过调整摄像机的 Viewport 决定摄像机渲染结果的输出范围,如下为 Second Camera 的 Clear Flag 为 Color 且 Viewport 为 (0.75, 0.75, 0.25, 0.25) 时的结果:

Unity 使用 Hidden/InternalClear shader 来进行 Clear 操作。该 Shader 中会通过 Stencil Buffer 来实现 Camera Viewport 的效果。

当有多个摄像机时,每帧每个摄像机都需要进行 Culling, Setup , Sorting 等操作。因此增加摄像机数量会增大对性能的消耗。

#Reference

Custom Render Pipeline (catlikecoding.com)