A闪的 BLOG 技术与人文
本篇我们来初步实现一个简单的自定义渲染管线,其目的就是要从头熟悉整体的渲染流程。因为每个环节又会涉及到更多的内容,所以你可以将本篇文章视作一个知识类目列表。其中不乏设计渲染流程,Shader编写以及相关一些其他的知识内容。不过不用担心,我们会将所有扩展内容在后续的文章中逐步进行分析拆解。
在上一篇 初识Unity自定义渲染管线 中,我们已经搭建了一个基本的渲染管线骨架。本篇主要围绕 TeacupRenderingPipeline 中的 Render 函数进行内容的学习与研究。
渲染事件点
在 RenderPipeline 中,Unity为我们准备了4个和渲染行为相关的事件点,你可以在你的渲染管线中使用他们,也可完全不使用。这四个事件点分别为:
通常情况下 BeginFrameRendering 和 EndFrameRendering 在每帧渲染时,只会被调用一次。他们应该位于Render函数实现的开头与结尾。
BeginCameraRendering 和 EndCameraRendering 的调用次数应该成对出现,并且出现的次数与当前帧索要处理的摄像头数量对应。为了学习他们的使用方法,我们来编写最基本的Render函数实现,但不做任何实际渲染操作。
using UnityEngine;
using UnityEngine.Rendering;
namespace Max2D.Rendering.Teacup
{
public class TeacupRenderingPipeline : RenderPipeline
{
public TeacupRenderingPipeline(TeacupRenderingPipelineAssets assets)
{
//构造函数
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
//渲染管线入口
BeginFrameRendering(context, cameras);
foreach (Camera camera in cameras)
{
BeginCameraRendering(context, camera);
//围绕当前 摄像机 做渲染操作
EndCameraRendering(context, camera);
}
EndFrameRendering(context, cameras);
}
}
}
在上层逻辑中,我们如何去监听使用这些事件呢?答案是 RenderPipelineManager。借助该类,我们可以方便的添加四个渲染事件点的监听,当某一个事件发生时,添加我们需要的逻辑。
值得注意的是,尽量不要在四个事件点添加过于耗时的逻辑操作,这样会加长整个Render的执行时间,从而导致FPS下降。
使用实例如下,建立一个 RenderCallbackTest 的 MonoBehaviour,并挂在到当前场景的任意激活对象上,同时添加多个Camera进行测试。
using UnityEngine;
using UnityEngine.Rendering;
public class RenderCallbackTest : MonoBehaviour
{
void Start()
{
RenderPipelineManager.beginFrameRendering += OnBeginFrameRendering;
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
RenderPipelineManager.endFrameRendering += OnEndFrameRendering;
}
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
Debug.Log($"OnBeginCameraRendering - Camera's name: {camera.name}");
}
private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
{
Debug.Log($"OnEndCameraRendering - Camera's name: {camera.name}");
}
private void OnBeginFrameRendering(ScriptableRenderContext context, Camera[] cameras)
{
Debug.Log($"OnBeginFrameRendering - Camera's number: {cameras.Length}");
}
private void OnEndFrameRendering(ScriptableRenderContext context, Camera[] cameras)
{
Debug.Log($"OnEndFrameRendering - Camera's number: {cameras.Length}");
}
private void OnDestroy()
{
RenderPipelineManager.beginFrameRendering -= OnBeginFrameRendering;
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
RenderPipelineManager.endFrameRendering -= OnEndFrameRendering;
}
}
我在场景中一共放置了三个摄像机,运行场景,可以看到输出如下:
OnBeginFrameRendering - Camera's number: 3
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnBeginFrameRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/Scripts/RenderCallbackTest.cs:55)
UnityEngine.Rendering.RenderPipeline:BeginFrameRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[])
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:17)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnBeginCameraRendering - Camera's name: Main Camera
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnBeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:45)
UnityEngine.Rendering.RenderPipeline:BeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:21)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnEndCameraRendering - Camera's name: Main Camera
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnEndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:50)
UnityEngine.Rendering.RenderPipeline:EndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:23)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnBeginCameraRendering - Camera's name: Camera (1)
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnBeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:45)
UnityEngine.Rendering.RenderPipeline:BeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:21)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnEndCameraRendering - Camera's name: Camera (1)
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnEndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:50)
UnityEngine.Rendering.RenderPipeline:EndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:23)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnBeginCameraRendering - Camera's name: Camera
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnBeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:45)
UnityEngine.Rendering.RenderPipeline:BeginCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:21)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnEndCameraRendering - Camera's name: Camera
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnEndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera) (at Assets/Scripts/RenderCallbackTest.cs:50)
UnityEngine.Rendering.RenderPipeline:EndCameraRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera)
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:23)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
OnEndFrameRendering - Camera's number: 3
UnityEngine.Debug:Log (object)
RenderCallbackTest:OnEndFrameRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/Scripts/RenderCallbackTest.cs:60)
UnityEngine.Rendering.RenderPipeline:EndFrameRendering (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[])
Max2D.Rendering.Teacup.TeacupRenderingPipeline:Render (UnityEngine.Rendering.ScriptableRenderContext,UnityEngine.Camera[]) (at Assets/com.max2d.rendering.teacup/Runtime/TeacupRenderingPipeline.cs:26)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
因为Render会不停的执行,所以可以看到不停的输出,上面则是一帧当中输出的内容。
注意:在Unity 2021.1 或更高版本中,BeginFrameRendering 和 EndFrameRendering 的命名发生变化,更改为 BeginContextRendering 和 EndContextRendering。
多Camera的处理
场景中可能会出现多个Camera,在Render函数中我们也会发现第二个参数为Camera[]。这里需要两个值得注意的问题。
排序问题
当前获取的cameras数组其顺序不一定按照我们想要的顺序进行排列,所以先要对所有的Camera进行一步排序操作,以确保多个摄像头可以按照“正确”的顺序以此处理。那么这个所谓的“正确的顺序”其排列规则是什么呢?
答案很简单,就是Camera的 depth 属性值。在官方文档中对depth属性的解释为摄像机在摄像机渲染顺序中的深度。深度较低的摄像机在深度较高的摄像机之前渲染。
根据此规则,我们建立一个名称为 SortCameras 的方法,对摄像机进行排序。
private Comparison<Camera> cameraComparison = (camera1, camera2) =>
{
return (int) camera1.depth - (int) camera2.depth;
};
private void SortCameras(Camera[] cameras)
{
if (cameras.Length <= 1) return;
Array.Sort(cameras, cameraComparison);
}
类型问题
前面我们提及过,自定义渲染管线不仅仅用于游戏中,也会运行于编辑器环境下。那么当获取到一个Camera时,我们要判断,当前Camera类型属于编辑器还是运行时,在不同的情况下,我们可能会需要不同的处理方法。
定义一个IsGameCamera方法,来对Camera类型进行区分。
private bool IsGameCamera(Camera camera)
{
if (camera == null)
throw new ArgumentNullException("camera");
return camera.cameraType == CameraType.Game;
}
具体摄像机存在哪些种类型,可以通过查阅 CameraType枚举 官方文档。
为了缩减文章篇幅,我们暂时只处理Game Camera。
接下来我们就要进行渲染操作,通常情况下,一个渲染包含一下三个环节:
每一个步骤又会有不同的细节操作需要处理,我们以一个最简单的例子来展示如何编写一个渲染循环。
我们会使用原生Camera类型中的一些属性,但不会做到支持所有属性。如果要开发真正符合自定义需求的渲染管线,那么你应该重新包装Camera所支持的功能。同时暂不考虑性能问题。
清除意味着移除在最后一帧期间绘制的内容。渲染目标通常是屏幕。但是,也可以渲染到纹理以创建“画中画”效果。这些示例演示如何渲染到屏幕,这是 Unity 的默认行为。
要清除可编程渲染管线中的渲染目标,请执行以下操作:
关于 CommandBuffer 与 ScriptableRenderContext 后面详细说明。
在Camera组件中,提供名称为Clear Flags的选项,用来表示当前清除行为,共有四种
针对以上四种不同类型,我们编写如下逻辑:
private void RenderGameCamera(ScriptableRenderContext context, Camera camera)
{
//清除渲染目标
if (camera.clearFlags != CameraClearFlags.Nothing ||
(camera.clearFlags == CameraClearFlags.Skybox && RenderSettings.skybox == null))
{
var cmd = new CommandBuffer();
cmd.ClearRenderTarget(camera.clearFlags == CameraClearFlags.Depth,
camera.clearFlags == CameraClearFlags.SolidColor, camera.backgroundColor, camera.depth);
context.ExecuteCommandBuffer(cmd);
cmd.Release();
}
//OTHER
//绘制天空盒
if (camera.clearFlags == CameraClearFlags.Skybox && RenderSettings.skybox != null)
{
context.DrawSkybox(camera);
}
context.Submit();
}
编写后,你可以在Unity中运行,并在Game面板中查看运行效果。
所谓剔除,总结一句话是:“将不需要渲染的内容过滤掉”。
要在可编程渲染管线中进行剔除,请执行以下操作:
示例代码如下:
CullingResults cullingResults;
if (camera.TryGetCullingParameters(out var cullingParameters))
{
cullingResults = context.Cull(ref cullingParameters);
}
绘制是指示图形 API 使用给定设置绘制一组给定几何体的过程。
要在 SRP 中进行绘制,请执行以下操作:
示例代码如下:
if (camera.TryGetCullingParameters(out var cullingParameters))
{
CullingResults cullingResults = context.Cull(ref cullingParameters);
//绘制
// 基于当前摄像机,更新内置着色器变量的值
context.SetupCameraProperties(camera);
// 基于 LightMode 通道标签值,向 Unity 告知要绘制的几何体
ShaderTagId shaderTagId = new ShaderTagId("ExampleLightModeTag");
// 基于当前摄像机,向 Unity 告知如何对几何体进行排序
var sortingSettings = new SortingSettings(camera);
// 创建描述要绘制的几何体以及绘制方式的 DrawingSettings 结构
DrawingSettings drawingSettings = new DrawingSettings(shaderTagId, sortingSettings);
// 告知 Unity 如何过滤剔除结果,以进一步指定要绘制的几何体
// 使用 FilteringSettings.defaultValue 可指定不进行过滤
FilteringSettings filteringSettings = FilteringSettings.defaultValue;
// 基于定义的设置,调度命令绘制几何体
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
我们还需要编写一个对应的Shader,并创建相应的材质赋予材质才可以正确得到渲染结果。
// 这定义一个与自定义可编程渲染管线兼容的简单无光照 Shader 对象。
// 它应用硬编码颜色,并演示 LightMode 通道标签的使用。
// 它不与 SRP Batcher 兼容。
Shader "Examples/SimpleUnlitColor"
{
SubShader
{
Pass
{
// LightMode 通道标签的值必须与 ScriptableRenderContext.DrawRenderers 中的 ShaderTagId 匹配
Tags { "LightMode" = "ExampleLightModeTag"}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
float4x4 unity_MatrixVP;
float4x4 unity_ObjectToWorld;
struct Attributes
{
float4 positionOS : POSITION;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
};
Varyings vert (Attributes IN)
{
Varyings OUT;
float4 worldPos = mul(unity_ObjectToWorld, IN.positionOS);
OUT.positionCS = mul(unity_MatrixVP, worldPos);
return OUT;
}
float4 frag (Varyings IN) : SV_TARGET
{
return float4(0.5,1,0.5,1);
}
ENDHLSL
}
}
}
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace Max2D.Rendering.Teacup
{
public class TeacupRenderingPipeline : RenderPipeline
{
public TeacupRenderingPipeline(TeacupRenderingPipelineAssets assets)
{
//构造函数
}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
//渲染管线入口
BeginFrameRendering(context, cameras);
SortCameras(cameras);
Camera camera;
for (int i = 0; i < cameras.Length; ++i)
{
camera = cameras[i];
BeginCameraRendering(context, camera);
//围绕当前 摄像机 做渲染操作
if (IsGameCamera(camera))
{
//Game Camera渲染操作
RenderGameCamera(context, camera);
}
else
{
//Editor Camera渲染操作
}
EndCameraRendering(context, camera);
}
EndFrameRendering(context, cameras);
}
private Comparison<Camera> cameraComparison = (camera1, camera2) =>
{
return (int) camera1.depth - (int) camera2.depth;
};
private void SortCameras(Camera[] cameras)
{
if (cameras.Length <= 1) return;
Array.Sort(cameras, cameraComparison);
}
private bool IsGameCamera(Camera camera)
{
if (camera == null)
throw new ArgumentNullException("camera");
return camera.cameraType == CameraType.Game;
}
private void RenderGameCamera(ScriptableRenderContext context, Camera camera)
{
//清除渲染目标
if (camera.clearFlags != CameraClearFlags.Nothing ||
(camera.clearFlags == CameraClearFlags.Skybox && RenderSettings.skybox == null))
{
var cmd = new CommandBuffer();
cmd.ClearRenderTarget(camera.clearFlags == CameraClearFlags.Depth,
camera.clearFlags == CameraClearFlags.SolidColor, camera.backgroundColor, camera.depth);
context.ExecuteCommandBuffer(cmd);
cmd.Release();
}
//剔除
if (camera.TryGetCullingParameters(out var cullingParameters))
{
CullingResults cullingResults = context.Cull(ref cullingParameters);
//绘制
// 基于当前摄像机,更新内置着色器变量的值
context.SetupCameraProperties(camera);
// 基于 LightMode 通道标签值,向 Unity 告知要绘制的几何体
ShaderTagId shaderTagId = new ShaderTagId("ExampleLightModeTag");
// 基于当前摄像机,向 Unity 告知如何对几何体进行排序
var sortingSettings = new SortingSettings(camera);
// 创建描述要绘制的几何体以及绘制方式的 DrawingSettings 结构
DrawingSettings drawingSettings = new DrawingSettings(shaderTagId, sortingSettings);
// 告知 Unity 如何过滤剔除结果,以进一步指定要绘制的几何体
// 使用 FilteringSettings.defaultValue 可指定不进行过滤
FilteringSettings filteringSettings = FilteringSettings.defaultValue;
// 基于定义的设置,调度命令绘制几何体
context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
}
//绘制天空盒
if (camera.clearFlags == CameraClearFlags.Skybox && RenderSettings.skybox != null)
{
context.DrawSkybox(camera);
}
context.Submit();
}
}
}