Unity SRP基本渲染流程

本篇我们来初步实现一个简单的自定义渲染管线,其目的就是要从头熟悉整体的渲染流程。因为每个环节又会涉及到更多的内容,所以你可以将本篇文章视作一个知识类目列表。其中不乏设计渲染流程,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[]。这里需要两个值得注意的问题。

  1. Camera的排序问题
  2. 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 的默认行为。

要清除可编程渲染管线中的渲染目标,请执行以下操作:

  1. 使用 Clear 命令配置 CommandBuffer。
  2. 将 CommandBuffer 添加到 ScriptableRenderContext 上的命令队列;为此,请调用 ScriptableRenderContext.ExecuteCommandBuffer。
  3. 指示图形 API 执行 ScriptableRenderContext 上的命令队列;为此,请调用 ScriptableRenderContext.Submit。

关于 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面板中查看运行效果。

剔除

所谓剔除,总结一句话是:“将不需要渲染的内容过滤掉”。

要在可编程渲染管线中进行剔除,请执行以下操作:

  1. 使用有关摄像机的数据填充 ScriptableCullingParameters 结构;为此,请调用 Camera.TryGetCullingParameters。
  2. 可选:手动更新 ScriptableCullingParameters 结构的值。
  3. 调用 ScriptableRenderContext.Cull,并将结果存储在一个 CullingResults 结构中。

示例代码如下:

CullingResults cullingResults;
if (camera.TryGetCullingParameters(out var cullingParameters))
{
    cullingResults = context.Cull(ref cullingParameters);
}

绘制

绘制是指示图形 API 使用给定设置绘制一组给定几何体的过程。

要在 SRP 中进行绘制,请执行以下操作:

  1. 如上所述执行剔除操作,并将结果存储在 CullingResults 结构中。
  2. 创建和配置 FilteringSettings 结构,它描述如何过滤剔除结果。
  3. 创建和配置 DrawingSettings 结构,它描述要绘制的几何体以及如何进行绘制。
  4. 可选:默认情况下,Unity 基于 Shader 对象设置渲染状态。如果要覆盖即将绘制的部分或所有几何体的渲染状态,可以使用 RenderStateBlock 结构执行此操作。
  5. 调用 ScriptableRenderContext.DrawRenderers,并将创建的结构作为参数进行传递。Unity 根据设置绘制过滤后的几何体集。

示例代码如下:

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,并创建相应的材质赋予材质才可以正确得到渲染结果。

// 这定义一个与自定义可编程渲染管线兼容的简单无光照 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();
        }
    }
}