找回密码
 立即注册
查看: 823|回复: 0

[笔记] Unity自定义RenderPipeline(SRP)

[复制链接]
发表于 2021-11-23 12:23 | 显示全部楼层 |阅读模式
Unity默认的颜色空间为Gamma空间,所以需要转为线性空间:


新建一些场景物体,物体材质使用 Unlit/Transparent 的shader,纹理采用如下纹理:


场景效果如下:


Unity默认使用的是内置的渲染管线,我们需要自定义管线资源,以便对它进行替换。


CustumRenderPipelineAsset的定义如下:
using UnityEngine;
using UnityEngine.Rendering;
// [CreateAssetMenu] 可通过右击Asset面板弹出创建CustomRenderPipelineAsset按钮
// 右击Asset面板,可点击Rendering/Custom Render Pipeline创建管线资源
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline()
    {
        return null;
    }
}
CustomRenderPipelineAsset的主要目的是给Unity一种获取管线对象实例的方法,负责渲染数据。资源本身只是一个句柄和存储设置的地方。我们还没有任何设置,所以我们要做的就是给Unity一个获取管道对象实例的方法。这是通过重写抽象的CreatePipeline方法完成的,该方法应该返回一个RenderPipeline实例。但是我们还没有定义一个定制的管线类型,所以先默认返回null。
创建自定义的管线资源后可在Graphics面板中设置该资源为内置管线:


但是替换管线资源出现了一些变化。首先,很多选项已经从Graphics设置中消失了。另外,我们在没有提供有效替换的情况下禁用了默认的管线流程,因此不再呈现任何内容。游戏窗口、场景窗口和材质预览不再起作用。如果我们通过Window/Analysis/frame debugger打开帧调试器并启用它,你会看到游戏窗口中确实没有绘制任何内容。
创建一个CustomRenderPipeline类,并将它的脚本文件放在与CustomRenderPipelineAsset相同的文件夹中。这将是我们的资源返回的RP实例所使用的类型,因此它必须继承RenderPipeline。


打开CustomRenderPipeline后,修改代码:
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline
{
    protected override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        
    }
}
RenderPipeline定义了一个受保护的抽象Render方法,所以必须重写它来创建一个具体操作的Pipeline对象。它有两个参数:ScriptableRenderContext和Camera数组。暂时先不实现该方法。
有了CustomRenderPipeline后,可在CustomRenderPipelineAsset的CreatePipeline函数返回该类型对象:
using UnityEngine;
using UnityEngine.Rendering;

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline()
    {
        return new CustomRenderPipeline();
    }
}
Unity会在每一帧调用RenderPipeline实例的Render函数,它传递了一个上下文结构,该结构提供了到Native Engine的连接,我们可以用它来做渲染。它还传递了相机数组,因为场景中可能有多个相机。管线的职责就是按照提供的顺序渲染所有这些相机。
我们知道每个相机都是独立渲染的。因此,我们可以新建一个相机渲染的新类。将其命名为CameraRenderer,并给它一个带有Context和相机参数的公共Render方法。为了方便起见,可以将这些参数存储在类字段中。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
    public ScriptableRenderContext context;
    public Camera camera;
    public void Render(ScriptableRenderContext context, Camera cam)
    {
        this.context = context;
        this.camera = cam;
    }
}
可以在CustomRenderPipeline中实例化CameraRenderer对象,并对每一个Camera调用其Render函数:
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline
{
    CameraRenderer renderer = new CameraRenderer();
    protected override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        foreach(var cam in cameras)
        {
            renderer.Render(context, cam);
        }
    }
}
我们的相机渲染器相当于URP的脚本化渲染器。这种方法将使它在未来为每个相机更容易支持不同的渲染方法,例如一个第一人称相机和一个3D地图相机,或Forward和Deferred渲染。但是现在我们将以相同的方式渲染所有的相机。
CameraRenderer的渲染任务是绘制相机能看到的所有几何图形。为了使代码更加清晰,我们会在单独的DrawVisibleGeometry方法中分离特定任务。我们将开始让它绘制默认的skybox,这可以通过在context上调用DrawSkybox并将camera对象作为参数来完成。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
    public ScriptableRenderContext context;
    public Camera camera;
    public void Render(ScriptableRenderContext context, Camera cam)
    {
        this.context = context;
        this.camera = cam;
        DrawVisibleGeometry();
    }

    void DrawVisibleGeometry()
    {
        context.DrawSkybox(camera);
    }
}
回到编辑器后,天空盒还是没有渲染出来。这是因为我们向Context发出的绘制命令还没提交。我们必须通过在Context中调用submit来提交队列的数据缓冲以供绘制执行。所以需要定义一个单独的Submit方法来做这件事,并在DrawVisibleGeometry函数之后调用。


天空盒终于出现在窗口中了,此时我们可以看下Frame Debug的内容输出:


可以看到RenderSkybox隶属于相机的渲染方式,它下面有一个Draw Mesh项,代表实际的网格绘制调用。
目前相机的朝向不会影响天空盒的渲染。我们将相机对象传递给DrawSkybox,但这只用于确定是否应该绘制天空盒,内部通过相机的ClearFlags确定。
为了正确渲染天空盒和整个场景,我们必须建立视图-投影矩阵。这个变换矩阵将摄像机的位置和方向(视图矩阵)与摄像机的透视或正交投影(投影矩阵)结合起来。它在着色器中被称为unity_MatrixVP,这是绘制几何图形时使用的着色器属性之一。当一个绘制被调用时,可以在Frame Debug的ShaderProperties部分检查这个矩阵的值。目前unity_MatrixVP矩阵值总是相同的。我们必须通过SetupCameraProperties方法将摄像机的属性应用到Context。这样能应用相机矩阵以及其他一些相关属性。在调用DrawVisibleGeometry函数之前,我们定义单独的Setup方法。
public void Render (ScriptableRenderContext context, Camera camera) {
        this.context = context;
        this.camera = camera;
        Setup();
        DrawVisibleGeometry();
        Submit();
}

void Setup () {
        context.SetupCameraProperties(camera);
}


Context需要手动调用Submit函数才能提交并展示实际的渲染结果。在提交前,我们需要对其进行配置并向其添加命令以供之后执行。执行一些绘制任务时:比如绘制天空盒,可以通过指定方法发出,但其他命令必须通过单独的命令缓冲区间接发出。我们需要这样一个缓冲区来绘制场景中的其他几何体。
要获得命令缓冲区,我们必须创建一个新的 CommandBuffer 实例对象。我们只需要一个命令缓冲区,因此默认情况下为 CameraRenderer 创建一个命令缓冲区即可,并把它的实例对象存储在CameraRenderer的字段中。还要为命令缓冲区命名,以便我们可以在Frame Debug中识别它。这里命名为 Render Camera 就可以了。
我们可以使用命令缓冲区来注入Profiler Samples,这会同时显示在Profiler和Frame Debug中。注入的方式通过在代码中调用BeginSample和EndSample来完成,在我们的例子中,它定义在Setup和Submit函数之间。Begin/EndSample两个方法必须具有相同的名称,为此我们使用bufferName属性。
要执行命令缓冲区的话,就需要把CommandBuffer作为参数值在Context中调用ExecuteCommandBuffer。它从命令缓冲区中复制命令,但不会对它清除,如果我们想要重用它,我们必须显式地进行Clear操作。因为Execute和Clear是先后完成的,所以可以定义在同一个函数中来完成这两个任务。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
    public ScriptableRenderContext context;
    public Camera camera;
    const string bufferName = "Render Camera";
    CommandBuffer commandBuffer = new CommandBuffer { name = bufferName };
    public void Render(ScriptableRenderContext context, Camera cam)
    {
        this.context = context;
        this.camera = cam;
        Setup();
        DrawVisibleGeometry();
        Submit();
    }

    void Setup()
    {
        commandBuffer.BeginSample(bufferName);
        ExecuteCommand();
        context.SetupCameraProperties(camera);
    }

    void ExecuteCommand()
    {
        context.ExecuteCommandBuffer(commandBuffer);
        commandBuffer.Clear();
    }

    void DrawVisibleGeometry()
    {
        context.DrawSkybox(camera);
    }

    void Submit()
    {
        commandBuffer.EndSample(bufferName);
        ExecuteCommand();
        context.Submit();
    }
}

打开Frame Debug,此时名称为Render Camera的节点在根节点中。


无论我们绘制什么,最终都会被渲染到相机的Render Target中,这是默认的帧缓冲对象,另外RenderTarget也可以是RenderTexture资源。由于之前绘制的那个目标对象还在,这可能会干扰我们现在绘制的图像内容。为了保证渲染的准确性,我们必须清除渲染目标,清理它的旧内容。这是通过调用CommandBuffer上的ClearRenderTarget来完成的,把它定义在Setup方法中。
CommandBuffer.ClearRenderTarget至少需要三个参数。前两个参数表示是否应该清除深度和颜色数据,这两个参数我们都设置为true。第三个参数是用于清除的颜色,我们将使用Color.clear(黑色)。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer
{
    public ScriptableRenderContext context;
    public Camera camera;
    const string bufferName = "Render Camera";
    CommandBuffer commandBuffer = new CommandBuffer { name = bufferName };
    public void Render(ScriptableRenderContext context, Camera cam)
    {
        this.context = context;
        this.camera = cam;
        Setup();
        DrawVisibleGeometry();
        Submit();
    }

    void Setup()
    {
        commandBuffer.BeginSample(bufferName);
        commandBuffer.ClearRenderTarget(true, true, Color.clear);
        ExecuteCommand();
        context.SetupCameraProperties(camera);
    }

    void DrawVisibleGeometry()
    {
        context.DrawSkybox(camera);
    }

    void ExecuteCommand()
    {
        context.ExecuteCommandBuffer(commandBuffer);
        commandBuffer.Clear();
    }

    void Submit()
    {
        commandBuffer.EndSample(bufferName);
        ExecuteCommand();
        context.Submit();
    }
}


为了消除两个Render Camera同名的层级节点,可以把commandBuffer.ClearRenderTarget(true,true, Color.clear);移至BeginSample前:
void Setup()
{
   commandBuffer.ClearRenderTarget(true, true, Color.clear);
   commandBuffer.BeginSample(bufferName);
   ExecuteCommand();
   context.SetupCameraProperties(camera);
}


现在回到编辑器,当点击场景内的Camera节点时,会发现SceneView的背景会被清除为黑色,相机小窗口有正确渲染:


为了解决这个问题,需要更改下渲染顺序,我们需要在ClearRenderTarget前就需要Setup当前相机的一系列参数信息:
void Setup()
{   
    context.SetupCameraProperties(camera);
    commandBuffer.ClearRenderTarget(true, true, Color.clear);
    commandBuffer.BeginSample(bufferName);
    ExecuteCommand();
}


可以看到Frame Debug的Draw GL节点变为了带有实际Clear信息的节点,这里对颜色,深度以及模板进行了清除。


上面只是对天空盒进行渲染,但是场景中的其它物体还没执行渲染操作。因为我们只渲染那些对相机可见的物体,而不是绘制每个游戏对象。所以得先确定场景中所有的物体是否可被渲染(是否带顶点数据),然后剔除掉那些在摄像机视图截锥之外的对象。
要想确定哪些物体需要被剔除,我们需要跟踪相机的参数信息以及它们的矩阵,为此我们可以使用ScriptableCullingParameters结构体。并可以在相机对象上调用TryGetCullingParameters。它返回ScriptableCullingParameters对象参数是否能够成功检索的布尔值数据。如果我们要获取更详细的Culling参数数据,我们必须先定义ScriptableCullingParameters对象。然后可以定义一个单独的Cull方法来执行上述的操作,该方法返回成功或失败。
bool Cull() {
        ScriptableCullingParameters p;
        // 也可以 if (camera.TryGetCullingParameters(out ScriptableCullingParameters p))
        if (camera.TryGetCullingParameters(out p))
        {
            return true;
        }
        return false;
}
需要在Setup函数调用前调用Cull函数:
public void Render(ScriptableRenderContext context, Camera cam)
{
       this.context = context;
       this.camera = cam;
       if(!Cull())
       {
           return;
       }
       Setup();
       DrawVisibleGeometry();
       Submit();
}
为了记录更加详细的Cull信息,可以定义CullingResults类型的相关字段:
CullingResults cullRes;
bool Cull()
{
       ScriptableCullingParameters p;
       // 也可以 if (camera.TryGetCullingParameters(out ScriptableCullingParameters p))
       if (camera.TryGetCullingParameters(out p))
       {
           cullRes = context.Cull(ref p);
           return true;
       }
       return false;
}
我们已经有了可见性判断以及Culling的结果信息,就可以绘制场景物体了:
void DrawVisibleGeometry()
{
     // 绘图设置
     var drawSettings = new DrawingSettings();
     // 过滤设置
     var filteringSettings = new FilteringSettings();
     context.DrawRenderers(cullRes, ref drawSettings, ref filteringSettings);
     context.DrawSkybox(camera);
}
此时在场景中还是看不到任何物体,是因为还需要额外的设置,比如需要指定哪一种着色器通道作为默认渲染通道,这里我们指定SRPDefaultUnlit通道,它将作为DrawingSettings的第一个参数, 所以管线只会对指定Unlit着色器相关的物体进行渲染。除此之外还需要指定绘制的排序规则,比如是Orthographic还是Distance-based的排序方式。最后,我们还必须告诉管线哪些渲染队列是允许的,这里指定RenderQueueRange.all,这样我们就包含了所有渲染队列。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
void DrawVisibleGeometry()
{
     var sortingSettings = new SortingSettings(camera);
     // 绘图设置
     var drawSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
     // 过滤设置
     var filteringSettings = new FilteringSettings(RenderQueueRange.all);
     context.DrawRenderers(cullRes, ref drawSettings, ref filteringSettings);
     context.DrawSkybox(camera);
}


可以看到渲染顺序显得非常杂乱,这与我们在SceneManagement一节中所描述的规则违背,增加了GPU OverDraw的负担。所以我们需要设置排序规则:
var sortingSettings = new SortingSettings(camera) {
    // 不透明对象的典型排序(从前到后)
    criteria = SortingCriteria.CommonOpaque
};


为了正确排序透明物体与不透明物体,所以需要重新编写不同的排序规则,并且发现绘制的最后结果只出现了天空盒与非透明物体,而透明物体却没有绘制到屏幕上。出现这个问题的原因是因为透明物体没有写入深度,导致后绘制的物体会覆盖之前绘制的物体。


解决方法是先绘制不透明物体,然后是天空盒,最后是透明的物体。当然如果仅仅是为了显示全部物体,完全可以简单的把DrawSkybox的操作放在最先绘制的位置。
void DrawVisibleGeometry()
{
       var sortingSettings = new SortingSettings(camera) {
           criteria = SortingCriteria.CommonOpaque
       };
       // 绘图设置
       var drawSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
       // 过滤设置,先绘制不透明物体
       var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
       context.DrawRenderers(cullRes, ref drawSettings, ref filteringSettings);
       // 再绘制skybox
       context.DrawSkybox(camera);
       // 最后绘制透明物体,排序规则改为透明排序
       sortingSettings.criteria = SortingCriteria.CommonTransparent;
       drawSettings.sortingSettings = sortingSettings;
       // 绘制透明队列
       filteringSettings.renderQueueRange = RenderQueueRange.transparent;
       context.DrawRenderers(
          cullRes, ref drawSettings, ref filteringSettings
       );

}
此时Unlit所有相关物体就显示在屏幕上了,但是Standard相关的材质物体还是被隐藏了。


为了能够渲染所有物体,并且兼容旧版本的所有着色器,我们需要定义新的ShaderTagId:
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) {
        …
        Setup();
        DrawVisibleGeometry();
        DrawUnsupportedShaders();
        Submit();
}

void DrawUnsupportedShaders () {
        var drawingSettings = new DrawingSettings(
                        legacyShaderTagIds[0], new SortingSettings(camera));
        // 可以通过在绘图设置上调用SetShaderPassName来绘制多个通道,并使用绘制顺序索引和标签作为参数
        // 新版本Unity,这里默认只有ForwardBase会起作用
        for (int i = 1; i < legacyShaderTagIds.Length; i++)
        {
            drawingSettings.SetShaderPassName(i, legacyShaderTagIds);
        }
        var filteringSettings = FilteringSettings.defaultValue;
        context.DrawRenderers(
                cullingResults, ref drawingSettings, ref filteringSettings);
}
回到编辑器后,输出结果如下:


用标准着色器渲染的物体出现在屏幕上了,但它们现在是纯黑色的,因为我们的自定义管线还没有为它们设置所需的着色器属性。
有时候自定义管线可能没法兼顾所有的着色器,如果出现不支持的着色器应该告诉用户,对应的着色器是不支持的,这可以使用 Hidden/InternalErrorShader 来设定。
static Material errorMaterial;

void DrawUnsupportedShaders () {
        if (errorMaterial == null) {
                errorMaterial =
                        new Material(Shader.Find("Hidden/InternalErrorShader"));
        }
        var drawingSettings = new DrawingSettings(
                legacyShaderTagIds[0], new SortingSettings(camera)
        ) {
                overrideMaterial = errorMaterial
        };
}


绘制应用了错误Shader的物体显然有用,但是一般发布游戏后,应该没有人还会让这个错误继续存在,所以绘制错误Shader的物体逻辑可以专门放在编辑器宏中进行。我们可以通过partial class进行定义,可以先复制CameraRenderer类文件,并重命名为CameraRenderer.Editor。


CameraRenderer代码:
using UnityEngine;
using UnityEngine.Rendering;
public partial class CameraRenderer
{
    public ScriptableRenderContext context;
    public Camera camera;
    const string bufferName = "Render Camera";
    CommandBuffer commandBuffer = new CommandBuffer { name = bufferName };
    CullingResults cullRes;
    static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
    public void Render(ScriptableRenderContext context, Camera cam)
    {
        this.context = context;
        this.camera = cam;
        if(!Cull())
        {
            return;
        }
        Setup();
        DrawVisibleGeometry();
        DrawUnSupportShader();
        Submit();
    }
   
    bool Cull()
    {
        ScriptableCullingParameters p;
        // 也可以 if (camera.TryGetCullingParameters(out ScriptableCullingParameters p))
        if (camera.TryGetCullingParameters(out p))
        {
            cullRes = context.Cull(ref p);
            return true;
        }
        return false;
    }

    void Setup()
    {   
        context.SetupCameraProperties(camera);
        commandBuffer.ClearRenderTarget(true, true, Color.clear);
        commandBuffer.BeginSample(bufferName);
        ExecuteCommand();
    }

    void DrawVisibleGeometry()
    {
        var sortingSettings = new SortingSettings(camera) {
            criteria = SortingCriteria.CommonOpaque
        };
        // 绘图设置
        var drawSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
        // 过滤设置,先绘制不透明物体
        var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
        context.DrawRenderers(cullRes, ref drawSettings, ref filteringSettings);
        // 再绘制skybox
        context.DrawSkybox(camera);
        // 最后绘制透明物体,排序规则改为透明排序
        sortingSettings.criteria = SortingCriteria.CommonTransparent;
        drawSettings.sortingSettings = sortingSettings;
        // 绘制透明队列
        filteringSettings.renderQueueRange = RenderQueueRange.transparent;
        context.DrawRenderers(
            cullRes, ref drawSettings, ref filteringSettings
        );

    }

    void ExecuteCommand()
    {
        context.ExecuteCommandBuffer(commandBuffer);
        commandBuffer.Clear();
    }

    void Submit()
    {
        commandBuffer.EndSample(bufferName);
        ExecuteCommand();
        context.Submit();
    }
}
CameraRenderer.Editor代码:
using UnityEngine;
using UnityEngine.Rendering;
partial class CameraRenderer
{
    // 为了发布不至于报错
    partial void DrawUnSupportShader();
// 只针对编辑器
#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 _errorMat;
   
    partial void DrawUnSupportShader()
    {
        if(!_errorMat)
        {
            _errorMat = new Material(Shader.Find("Hidden/InternalErrorShader"));
        }
        var drawingSettings = new DrawingSettings(
            legacyShaderTagIds[0], new SortingSettings(camera))
        {
            overrideMaterial = _errorMat
        };
        var filterSettings = FilteringSettings.defaultValue;
        for (int i = 1; i < legacyShaderTagIds.Length; i++)
        {
            drawingSettings.SetShaderPassName(i, legacyShaderTagIds);
        }
        context.DrawRenderers(cullRes, ref drawingSettings, ref filterSettings);
    }
#endif
}
接下来可以点击Camera等节点,发现没有任何相关gizmo线框的绘制,我们可以通过调用UnityEditor.Handles . ShouldRenderGizmos来检查gizmos是否应该被绘制,如果需要绘制则调用context的DrawGizmos。并将相机作为第一个参数,第二个参数则用来表示应该绘制Gizmo的哪个子集。有两个子集,PreImageEffects与PostImageEffects,由于我们目前不支持ImageEffects,所以我们会同时调用这两种效果函数,并定义一个新的函数DrawGizmos,并在Render函数中的Submit调用前调用DrawGizmos函数。
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

partial class CameraRenderer {
       
        partial void DrawGizmos ();

        partial void DrawUnsupportedShaders ();

#if UNITY_EDITOR

        …

        partial void DrawGizmos () {
                if (Handles.ShouldRenderGizmos()) {
                        context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
                        context.DrawGizmos(camera, GizmoSubset.PostImageEffects);
                }
        }

        partial void DrawUnsupportedShaders () { … }

#endif
}

public void Render (ScriptableRenderContext context, Camera camera) {
        …
        Setup();
        DrawVisibleGeometry();
        DrawUnsupportedShaders();
        DrawGizmos();
        Submit();
}
此时相机的Gizmo就出现在屏幕上了。


我们还需要绘制游戏的UI界面,它会显示在游戏运行窗口,而不会显示在SceneView屏幕中。新建Button后,打开Frame Debug,可以看到UI在另外的渲染流程中。


可以看到,在渲染UI前,会对FBO clear stencil操作,保留color与depth,并且UI的渲染并不在我们自定义的管线流程当中,这是因为Canvas的默认Render Mode为Screen Space-Overlay。


我们需要更改Render Mode为Screen Space-Camera,并指定Main Camera作为渲染的主相机,这样就能让UI的渲染通过自定义管线进行。


此时可以看到UI的渲染现在处于半透明渲染队列里。


虽然UI有执行绘制,但是只出现在了GameView上并没有出现在SceneView的Canvas中,针对该问题需要做下特殊判断:
partial void PrepareForSceneWindow ();
#if UNITY_EDITOR

partial void PrepareForSceneWindow () {
        // 当相机类型为SceneView类型,显式通过调用ScriptableRenderContext将UI添加到World Geometry中。
        // 使用相机作为参数
        if (camera.cameraType == CameraType.SceneView) {
                ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
        }
}
#endif

PrepareForSceneWindow();
if (!Cull()) {
        return;
}
此时SceneView就显示UI了。


对于多相机的渲染,需要做额外的操作。对于默认的Main Camera,它的Depth属性默认值为-1,如果新建Camera,则新建的Camera的默认Depth为0,所以新建的Camera的渲染优先级会低于Main Camera,即后面渲染。如在Frame Debug中新建的Camera在最后渲染。


现在场景被渲染了两次,并且只会显示最后一次的渲染结果,这是因为之前相机的渲染结果被清除了。我们可以为每个Camera的渲染设置对应的CommandBuffer name:
partial void PrepareBuffer ();

#if UNITY_EDITOR

        …
       
        partial void PrepareBuffer () {
                buffer.name = camera.name;
        }

#endif
PrepareBuffer();
PrepareForSceneWindow();


但是运行游戏后,打开Profiler会出现类似下面的报错:


这是因为CommandBuffer与Begin/EndSample使用了不同的name导致。
#if UNITY_EDITOR
        …
        string SampleName { get; set; }       
        …       
        partial void PrepareBuffer () {
                commandBuffer.name = SampleName = camera.name;
        }
#else
        //非编辑器条件下不需要进行可视化区分,固定SampleName就好
        const string SampleName = bufferName;
#endif
void Setup () {
        context.SetupCameraProperties(camera);
        commandBuffer.ClearRenderTarget(true, true, Color.clear);
        commandBuffer.BeginSample(SampleName);
        ExecuteBuffer();
}

void Submit () {
        commandBuffer.EndSample(SampleName);
        ExecuteBuffer();
        context.Submit();
}


获取Profiler的输出结果后,有时候我们并不知道对应的项是哪些代码输出的,比如这里有三个GC Alloc,为了标识GC.Alloc,我们可以对赋值CommandBuffer name的过程进行Profiler采样:
#if UNITY_EDITOR

        …

        partial void PrepareBuffer () {
                Profiler.BeginSample("Editor Only");
                buffer.name = SampleName = camera.name;
                Profiler.EndSample();
        }

#else
重新运行Profiler后,有两个GC.Alloc被Editor Sample包裹:


每个Camera组件都有其对应的Culling Mask属性,这对应了各个GameObject相应的Layer名称。如:我们设置当前所有无效的Shader对应物体的Layer为Ignore Raycast。


并对MainCamera的Culling Mask取消对Ignore Raycast的勾选:


而对Second Camera只勾选Ignore Raycast:


所以此时Second Camera只对无效Shader的GameObject进行了渲染:


MainCamera只对Unlit相关的GameObject进行了渲染:


此时我们想要混合两个相机的结果就需要借助于两个相机的ClearFlags,ClearFlags是
该枚举类型有4个值,从1~4分别对应了Skybox,Color,Depth与Nothing。所以需要对Setup函数进行更改:
void Setup() {   
        context.SetupCameraProperties(camera);
        // 当前相机的ClearFlags
        CameraClearFlags flags = camera.clearFlags;
        // flags <= CameraClearFlags.Depth:因为除了ClearFlags为Nothing外,其它ClearFlags都需要清除Depth
        // flags == CameraClearFlags.Color:只有ClearFlags为Color时,才需要清理颜色值,Skybox理论上也要清理,但是清不清理它都会覆盖之前的渲染结果,所以不需要判断
        // flags == CameraClearFlags.Color? camera.backgroundColor.linear : Color.clear: 只有Clear Color的情况下才需要当前相机的BackgroundColor
        // 并且由于我们选择的是线性空间,所以这里选择linear的映射关系。如果是gamma空间,则选择gamma的映射关系。
        commandBuffer.ClearRenderTarget(flags <= CameraClearFlags.Depth, flags == CameraClearFlags.Color, flags == CameraClearFlags.Color? camera.backgroundColor.linear : Color.clear);
        commandBuffer.BeginSample(SampleName);
        ExecuteCommand();
}
一般来讲,MainCamera是最先渲染的,所以它的ClearFlags可以选择为Skybox或Color。而SecondCamera由于优先级低于MainCamera,所以SecondCamera决定着与MainCamera如何进行混合。如果SecondCamera的ClearFlags选择的是Skybox或Color,则只显示SecondCamera的渲染结果;当ClearFlags选择的是Depth,那么SecondCamera的渲染画面会与MainCamera的画面混合,但是由于清除了MainCamera的深度信息,所以SecondCamera的模型没法与MainCamera中渲染的模型一起进行深度测试;当ClearFlags选择的是Nothing,由于上一个Camera(也就是MainCamera)的深度以及模板与颜色信息都在,所以该混合方式的结果是两个相机的画面既能够融合,各自所渲染的模型也能够做深度测试。



SecondCamera的ClearFlags为Color



SecondCamera的ClearFlags为Depth



SecondCamera的ClearFlags为Nothing

如果ClearFlags为Color,也可以通过调整SecondCamera的Viewport来看到MainCamera的渲染结果:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 20:02 , Processed in 1.020307 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表