Unity可编程渲染管线系列教程(1):自定义渲染管线

前言

  Jasper Flick《Unity可编程渲染管线》系列教程之:自定义渲染管线。该教程分享了用户如何在Unity引擎从头构建简易的渲染管线。原文链接可见该博客末尾。



创建渲染管线

在这里插入图片描述

在渲染任何物体之前,Unity引擎都需要事先确定物体的形状,物体的位置,当前所处的时间以及物体自身的属性。当越多类似的要素需要纳入考虑,渲染就变得越复杂。事实上,我们所看到的渲染最终效果,是光照,阴影,透明度,图像特效,体积效应以及更多要素经过正确的顺序处理后得到的效果。而处理这些要素的过程,就是 “渲染管线 (Render Pipeline)”

Unity2017支持两种内置的渲染管线,一个适用于 前向渲染 (Forward Rendering),另一个则适用于 延迟渲染 (Deferred Rendering)。由于这两种渲染管线已经被预设好,因此用户仅能够直接使用,或修改管线的部分功能,无法做出与其原生设计差异过大的修改。

从Unity2018起,Unity开始支持可编程渲染管线,从此用户可从头开始设计自己的渲染管线,但一些功能的底层实现仍需要依赖Unity提供的库。此外,Unity2018还加入了 轻量渲染管线 (Lightweight Pipeline)高清渲染管线 (High-Definition Pipeline)


1.1 建立项目

打开UnityHub,点击右上角的 “新建” 弹窗,选择 “3D” 进行创建即可。

创建完成后,在导航栏中选择 Window / Package Manager 并移除引擎默认添加上去的组件。由于 Package Manager UI 这一个组件无法被删除,因此我们保留这一个组件 (笔者自己操作后认为,这些默认组件不删除也可以,只是删除了之后能使项目更加轻量化)。

在这里插入图片描述

建立项目

我们后续需要使用 线性颜色空间 (Linear Color Space),但Unity默认使用的是 伽马颜色空间 (Gamma Space),因此我们需要在导航栏中的 Edit / Project Settings / Player 中找到 Other Settings 选项,并勾选为线性。

在这里插入图片描述

颜色空间

完成这步后,我们需要创建材质,并把材质应用在物体上来测试我们的管线。我们创建了四中材质。第一种采用标准着色器下的不透明材质,颜色设为了红色;第二种材质是标准着色器下的透明材质,颜色为蓝色,并设置为 Decrease Aplha;第三种采用了 Unlit / Color 着色器,颜色设为黄色;最后一种采用了 Unlit / Transparent 着色器,颜色为白色。

在这里插入图片描述

场景示例


1.2 管线资源 (Pipeline Asset)

在默认情况下,Unity会使用前向渲染管线。如果要使用我们自定义的渲染管线,就需要在 “Edit / Project Settings / Graphics”Scriptable Render Pipeline Settings 中用自定义渲染管线把默认的渲染管线替换掉。

在这里插入图片描述

我们首先需要新建一个 C# 脚本。若我们要把管线命名为 MyPipeline,那么管线资源的类名必须为 “MyPipelineAsset” 。此外,任何一个渲染管线资源都必须继承Unity内置的管线资源 “RenderPipelineAsset”,该资源被定义在 “UnityEngine.Experimental.Rendering” 库中。因此我们还需要使用该库的命名空间。

using UnityEngine;
using UnityEngine.Experimental.Rendering;

public class MyPipelineAsset : RenderPipelineAsset {}


通过把渲染管线抽象成 资源 (Asset),渲染管线就能以对象的形式受Unity管理,资源本身只是一种用于存放并管理各项设置的抽象概念。为了能让引擎真正的获得我们自定义渲染管线的管理权,我们需要实例化我们的自定义渲染管线,我们可通过调用 “InternalCreatePipeline()” 实现这一目的。由于我们后期需要在管线中实现自定义功能,不完全遵循 “InternalCreatePipeline()” 的既定功能,因此我们需要以 重写(override) 的形式调用该功能。不过目前我们还未增添自定义功能,因此直接令该方法返回 “null” 即可

public class MyPipelineAsset : RenderPipelineAsset {
    protected override IRenderPipeline InternalCreatePipeline() {
        return null;
    }
}


除此以外,我们还希望能够直接在Unity编辑器页面中管理我们的渲染管线,例如可以通过下拉菜单来创建我们的渲染管线资源。这一步可通过添加 “CreateAssetMenu” 属性实现。此外我们还可以手动地指定管线资源在菜单中的位置,例如我们希望如下的菜单结构,我们可以通过下方的代码实现:

Asset
  |__ Create
  |      |__ Rendering
  |      |       |___ My Pipeline
  |      |       |___ Forward Pipeline
  |      |       |___ Deferred Pipeline
  |      |       |___ ...
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset {}


做完这些之后,我们就可以在 Scriptable Render Pipeline Settings 中替换掉默认渲染管线。由于我们的渲染管线目前仍是一个空壳,因此项目中包括场景,材质在内的所有都无法渲染。

在这里插入图片描述

替换完成后


1.3 管线实例 (Pipeline Instance)

在上一节中我们试图通过调用 “InternalCreatePipeline()” 来实例化管线对象。在具体的代码中,我们只是调用了这个方法的空壳,但没有具体实例化渲染管线。要实例化渲染管线,我们首先需要为管线创建实例对象:

using UnityEngine;
using UnityEngine.Experimental.Rendering;

public class MyPipeline : RenderPipeline {}

public class MyPipelineAsset : RenderPipelineAsset {
	...
}


接着,我们就可以在 “MyPipelineAsse” 中,调用的 “InternalCreatePipeline()” 里实例化我们的渲染管线了:

	protected override IRenderPipeline InternalCreatePipeline () {
		return new MyPipeline();
	}


到目前整体的代码如下:

using UnityEngine;
using UnityEngine.Experimental.Rendering;

/* Pipeline object field */
public class MyPipeline : RenderPipeline {

}

/* Pipeline asset field */
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]                       // Add pipeline asset to editor menu
public class MyPipelineAsset : RenderPipelineAsset {
	protected override IRenderPipeline InternalCreatePipeline() {
   		return new MyPipeline();                                            // Instantiate pipeline object
    }
}




渲染

上一部分中,我们创建的渲染管线对象会负责为每一帧执行渲染操作,而Unity引擎则负责在每一帧调用渲染管线的 “Rander()” 方法,这样接到渲染指令的渲染管线才会去渲染当前帧。


2.1 渲染内容(Render Context)

“RenderPipeline” 已包含了 “Rander()” 方法的实现。该方法需要传入两个参数,第一个是渲染内容,是一个 “ScriptableRenderContext” 结构体;第二个是一个摄像机列表,包含了所有需要执行渲染操作的摄像机。

“RenderPipeline.Render()” 方法本身不会渲染任何东西,但会检查所有的渲染管线对象,判定其是否能够合法地执行渲染操作。若存在不适合者,则直接抛出异常。在我们的自定义渲染管线中,我们会重写这个方法,但保留其基础功能。

public class MyPipeline : RenderPipeline {
    public override void Render (
        ScriptableRenderContext renderContext, Camera[] cameras
    ) {
        base.Render(renderContext, cameras);
    }
}


Unity引擎正是通过渲染内容来渲染场景以及控制渲染状态。一个最常见的用例就是渲染场景中的天空盒(Skybox),这一步可通过调用 “DrawSkybox()” 实现。该方法需要传入具体的摄像机作为参数,在此我们把 0 0 0 号摄像机传入:

    base.Render(renderContext, cameras);

    renderContext.DrawSkybox(cameras[0]);


完成这一步后我们并不能在编辑器窗口中看到天空被渲染出来。这是因为上述代码只是让Unity引擎缓存天空盒的渲染,我们还需要指示Unity引擎执行渲染,才能看到最终的效果:

    base.Render(renderContext, cameras);

    renderContext.DrawSkybox(cameras[0]);

    renderContext.Submit();


完成这一步后,我们可以看到天空出现在了编辑器窗口中。此外我们也可以在帧调试器中看到天空盒选项。


2.2 摄像机(Cameras)

在游戏中常常会有需要使用多个摄像机的场景,例如分屏多人游戏,小地图,后视镜等机制,渲染管线需要逐一的处理每个摄像机的渲染,因此在调用 “Render()” 方法时我们传入的是摄像机的列表。

同样还是以渲染天空盒作为例子,我们需要重载 “Render()” 方法,使其专职负责对单个摄像机执行天空盒渲染操作。其后我们在原始的 “Render()” 方法中使用循环的方式对所有摄像机调用一次新的 “Render()” 方法,这样就能为每个摄像机渲染天空盒了。事实上,渲染管线常常使用循环的方式来为多个摄像机执行渲染操作。

public override void Render (
    ScriptableRenderContext renderContext, Camera[] cameras
) {
    base.Render(renderContext, cameras);

    //renderContext.DrawSkybox(cameras[0]);
    //renderContext.Submit();

    foreach (var camera in cameras) {
        Render(renderContext, camera);
    }
}

void Render (ScriptableRenderContext context, Camera camera) {
    context.DrawSkybox(camera);

    context.Submit();
}


要注意摄像机的方向并不会影响到天空盒的渲染。我们往 “DrawSkybox()” 传入了摄像机,但这仅仅只是用来决定在该摄像机所看到的场景中是否需要渲染出天空盒。

如果要正确的渲染天空盒–乃至整个场景,我们需要建立起 视图投影矩阵(View-Projection Matrix)。该矩阵能够把摄像机的位置和方向,即视图矩阵,与摄像机的透视/正交投影,即投影矩阵,结合在一起。在帧调试器中,试图投影矩阵被标记为 “unity_MatrixVP”

在当前,“unity_MatrixVP” 都是一样的。我们通过 “SetupCameraProperties()” 来在渲染内容中建立起视图投影矩阵,除了该矩阵以外,这个方法还会设置好摄像机的其他属性:

void Render (ScriptableRenderContext context, Camera camera) {
    context.SetupCameraProperties(camera);

    context.DrawSkybox(camera);

    context.Submit();
}


2.3 命令缓存(Command Buffers)

渲染内容会一直处于缓存状态直到我们把他们提交执行。在提交执行之前,我们需要为渲染内容添加执行命令,这样在后续的渲染过程中,渲染内容才会按照正确的流程来执行渲染。对于一部分渲染内容,例如天空盒,我们可以直接使用一个专用方法来命令其渲染;但对于其他的渲染内容,我们需要通过命令缓存来间接地向渲染内容下达渲染命令。

命令缓存被定义在 “UnityEngine.Rendering” 库中,因此我们需要使用该库的命名空间。命令缓存在我们的自定义渲染管线被创建出来就已经存在,因此我们可以在管线中直接实例化一个命令缓存:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;

public class MyPipeline : RenderPipeline {

    ...

    void Render (ScriptableRenderContext context, Camera camera) {
        context.SetupCameraProperties(camera);

        var buffer = new CommandBuffer();

        context.DrawSkybox(camera);
	
 		context.Submit();
    }
}


我们可以通过调用 “ExecuteCommandBuffer()” 方法来命令渲染内容执行缓存中的命令。再一次需要注意的是,该方法并不会令渲染内容马上执行缓存中的命令,而是把这些命令从命令缓存转移到渲染内容的内部缓存中,相当于提前告知渲染内容有这些命令在一会需要执行。

创建命令缓存需要向系统申请额外的内存资源,因此当我们不需要命令缓存后,我们应该释放掉命令缓存所占用的内存空间。通常我们会在调用 “ExecuteCommandBuffer()” 后马上调用 “Release()” 方法来释放内存。

到目前为止命令缓存都为空,我们以清空渲染对象作为例子,来演示命令缓存的用法。为了确保先前已经渲染好的内容不会对当前的内容产生影响,我们需要在执行新的渲染前先清空渲染对象。这一步难以直接通过调用渲染内容的专用方法,却可以通过命令缓存达成。

可通过调用 “ClearRenderTarget()” 方法来清空渲染对象。该方法需要三个传参,第一个参数用于决定深度信息是否要被清空,第二个参数决定颜色信息是否要被清空,第三个参数是需要清空的颜色信息,若第二个参数为真。在实际代码中,我们仅清空深度信息,对颜色信息不做改动。

	var buffer = new CommandBuffer();
	buffer.ClearRenderTarget(true, false, Color.clear);
	context.ExecuteCommandBuffer(buffer);
	buffer.Release();

在这里插入图片描述

清除深度信息


由于各摄像机的设定不同,因此更为稳妥的方式是用摄像机自身的设定来决定哪些信息需要被清空,因此我们可以改为:

	var buffer = new CommandBuffer();
	CameraClearFlags clearFlags = camera.clearFlags;
	buffer.ClearRenderTarget(
	    (clearFlags & CameraClearFlags.Depth) != 0,
	    (clearFlags & CameraClearFlags.Color) != 0,
	    camera.backgroundColor
	);
	context.ExecuteCommandBuffer(buffer);
	buffer.Release();


此外,为了更好地管理每一个命令缓存。我们可以为每个命令缓存命名:

	var buffer = new CommandBuffer {
	    name = camera.name
	};

在这里插入图片描述

重命名命令缓存


2.4 剔除(Culling)

到目前为止,我们可以渲染天空盒,但仍无法渲染场景中的其他物体。事实上,我们不需要渲染场景中的所有物体,而只需要渲染摄像机所能观察到的那一部分物体就可以了,因此在渲染过程中,我们需要剔除那些摄像机无法观察到的物体。

要计算出哪些物体需要被剔除依赖于摄像机自身的参数。在Unity中,我们使用 “ScriptableCullingParameters” 结构体来储存需剔除数据,有了这一步后,我们使用 “CullResults.GetCullingParameters()” 来计算出相对于当前摄像机所需要的剔除的物体:

void Render (ScriptableRenderContext context, Camera camera) {
    ScriptableCullingParameters cullingParameters;
    CullResults.GetCullingParameters(camera, out cullingParameters);

    ...
}


但有时候摄像机的参数可能会是无效的,从而使 “GetCullingParameters()” 无法获取到合法的剔除数据,在后续的渲染中使用非法的剔除数据会导致程序崩溃。因此为了程序的稳定性,我们应该检测 “GetCullingParameters()” 所获取到的数据是否合法,若不合法则直接退出 “Render()” 方法:

    // CullResults.GetCullingParameters(camera, out cullingParameters);

    if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
        return;
    }    


得到了剔除数据后,我们可以通过调用 “CullResults.Cull()” 来剔除不需要渲染的物体。该方法需要传入两个参数,第一个是我们先前得到的剔除数据,第二个是渲染内容。该方法的返回是一个 “CullResults” 结构体:

	if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
        return;
    }

	CullResults cull = CullResults.Cull(ref cullingParameters, context);


2.5 描绘(Drawing)

完成了上一步后,下一步要做的就是描绘出物体的形状了。我们可以通过调用 “DrawRenderers()” 方法进行描绘。该方法的第一个参数是 “cull.visibleRenderers()”,用以告诉方法哪些物体是需要描绘的。此外我们还需要传入 “DrawRendererSettings”“FilterRenderersSettings” 作为该方法的第二和第三个参数。在当前这一步我们只传入这两个参数的默认值即可:

	buffer.Release();
	
	var drawSettings = new DrawRendererSettings();
	var filterSettings = new FilterRenderersSettings();
	
	context.DrawRenderers(
	    cull.visibleRenderers, ref drawSettings, filterSettings
	);
	
	context.DrawSkybox(camera);


由于 “FilterRenderersSettings” 的初始默认值是 false,即会把所有物体都过滤掉,因此就不会有任何物体被渲染,所以在窗口中我们什么也看不到。因此我们需要调整 “FilterRenderersSettings” 为不过滤任何物体:

	var filterSettings = new FilterRenderersSettings(true);


此外,“DrawRendererSettings” 也需要配置,我们需要向其中传入摄像机和 “Shader Pass” 作为参数。摄像机用以设置排序和剔除层,而 “Shader Pass” 用以告诉着色器我们使用的是哪一个 “Shader Pass”

由于我们的渲染管线目前仅支持无光照材质,因此我们直接使用Unity内置的无光照着色器(Unlit Shader)

	var drawSettings = new DrawRendererSettings(
	    camera, new ShaderPassName("SRPDefaultUnlit")
	);


经过这一步后,我们可以看到先前设置的不透明球体已经出现在编辑器的窗口中了,但却无法看到透明球体。对此解决方法是先描绘不透明物体,接着描绘天空盒,最后再描绘透明物体。

在这里插入图片描述

不透明物体已被描绘出

首先,我们需要需要命令渲染管线在绘制天空盒前只绘制不透明物体。我们可以通过把 “renderQueueRange” 设为 “ RenderQueueRange.opaque” 达到这一目的,此时渲染队列的范围为 0 至 2500:

	var filterSettings = new FilterRenderersSettings(true) {
	    renderQueueRange = RenderQueueRange.opaque
	};


随后,在完成天空盒的绘制后,我们更改渲染队列的范围到 2501 至 5000,使其适用于透明物体的渲染:

	var filterSettings = new FilterRenderersSettings(true) {
	    renderQueueRange = RenderQueueRange.opaque
	};
	
	context.DrawRenderers(
	    cull.visibleRenderers, ref drawSettings, filterSettings
	);
	
	context.DrawSkybox(camera);
	
	filterSettings.renderQueueRange = RenderQueueRange.transparent;
	context.DrawRenderers(
	    cull.visibleRenderers, ref drawSettings, filterSettings
	);

在这里插入图片描述

透明物体也被描绘出


在上文我们提到,向 “DrawRendererSettings” 传入的摄像机参数将会用以设置排序层。在一个场景中,所有物体都具有各自的物理位置,从摄像机所看到的场景中,一些物体可能会被另外一些物体所遮挡,被遮挡的这些物体是不需要被渲染的。为了避免不必要的渲染,对于不透明的物体渲染管线会从里摄像机最近的物体开始从近到远渲染。因此需要对场景中的物体进行排序:

	var drawSettings = new DrawRendererSettings(
	    camera, new ShaderPassName("SRPDefaultUnlit")
	);
	drawSettings.sorting.flags = SortFlags.CommonOpaque;


对于透明物体来说排序顺序却是相反的。因为透明物体的颜色是由其后方物体的颜色所组成,因此需要由远到近进行渲染:

	context.DrawSkybox(camera);
	
	drawSettings.sorting.flags = SortFlags.CommonTransparent;
	filterSettings.renderQueueRange = RenderQueueRange.transparent;
	context.DrawRenderers(
	    cull.visibleRenderers, ref drawSettings, filterSettings
	);


完成这些后,我们的渲染管线就可以正确的渲染透明和不透明物体了。

当前整体的代码如下:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;

/* Pipeline object field */
public class MyPipeline : RenderPipeline {
    public override void Render ( ScriptableRenderContext renderContext, Camera[] cameras) {
        base.Render(renderContext, cameras);

        foreach (var camera in cameras) {                                   // Do render process for all cameras
            Render(renderContext, camera);
        }
    }

    void Render (ScriptableRenderContext context, Camera camera) {
        /* Culling */
        ScriptableCullingParameters cullingParameters;                      // Declare culling paremeter struct
        if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
            return;                                                         // Calculate cull data
        }
        CullResults cull = CullResults.Cull(ref cullingParameters, context);    // Cull unnecessary render object          


        /* Command Buffer */
        var buffer = new CommandBuffer {name = camera.name};                // Declare command buffer
        CameraClearFlags clearFlags = camera.clearFlags;
        buffer.ClearRenderTarget(                                           // Clear render targets
            (clearFlags & CameraClearFlags.Depth) != 0,
            (clearFlags & CameraClearFlags.Color) != 0,
            camera.backgroundColor
        );
        context.ExecuteCommandBuffer(buffer);                               // Execute command buffer
        buffer.Release();                                                   // Release command buffer


        /* Drawing */
        var drawSettings = new DrawRendererSettings(                        // Set pipeline to use default unlit shader pass
            camera, new ShaderPassName("SRPDefaultUnlit"));

        drawSettings.sorting.flags = SortFlags.CommonOpaque;                // Sort opaque object render order 
        var filterSettings = new FilterRenderersSettings(true) {            // Limit pipeline to render opaque object frist 
            renderQueueRange = RenderQueueRange.opaque};                    // Render queue range: 0 ~ 2500
        context.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);

        context.SetupCameraProperties(camera);                              // Set up view-projection matrix and other camera properties
        context.DrawSkybox(camera);                                         // Draw skybox

        drawSettings.sorting.flags = SortFlags.CommonTransparent;           // Sort transparent object render order
        filterSettings.renderQueueRange = RenderQueueRange.transparent;     // Limit pipeline to render transparent object last, render queue range: 2501 ~ 5000
        context.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);

        context.Submit();
    }
}


/* Pipeline asset field */
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]                       // Add pipeline asset to editor menu
public class MyPipelineAsset : RenderPipelineAsset {
        protected override IRenderPipeline InternalCreatePipeline() {
        return new MyPipeline();                                            // Instantiate pipeline object
    }
}




优化

到目前为止,我们的渲染管线已经能正确的渲染场景中的物体了。但仅达到这一点是不够的。我们还需要考虑渲染管线的性能,算力负载,与Unity引擎的集成等等,因此我们还需要对渲染管线进行优化。


3.1 内存分配(Memory Allocations)

如果我们的渲染管线在每一帧的渲染中,都要重复地申请内存空间的话,那么将会产生大量的内存碎片,系统的内存回收模块就得不停的去处理这些内存碎片,从而极大地降低了系统的性能。我们可以在编辑器菜单栏中的 Window / Analysis / Profiler 来查看 CUP 占用数据。

我们可以看到在每一帧中,系统都要进行多次内存分配。其中一些是在执行我们渲染管线的 “Render()” 方法时分配的。

事实上,剔除占用了最多的内存。原因是我们用于记录剔除结果数据的 “CullResults” 变量是用三个列表组成。相当于每次我们声明一个 “CullResults” 变量,都需要为其中的三个列表分配内存。对此的优化方案是:我们在外部 “Render()” 事先声明好一个 “CullResults” 变量,在每一次渲染中,我们可以清空 “CullResults” 中原有的数据,再把新的数据写入其中,从而达到复用的效果。

CullResults cull;

...

void Render (ScriptableRenderContext context, Camera camera) {
    ...

    // CullResults cull = CullResults.Cull(ref cullingParameters, context);
    CullResults.Cull(ref cullingParameters, context, ref cull);
    
    ...
}


另一个主要的内存占用大户是命令缓存,但原理和剔除相似,都是因为重复地申请了命令缓存导致了不必要的内存消耗。所以对应的优化策略也是设法重用命令缓存:

CommandBuffer cameraBuffer = new CommandBuffer {
    name = camera.name
};

...

void Render (ScriptableRenderContext context, Camera camera) {
    ...

    //var buffer = new CommandBuffer() {
    //	name = camera.name
    //};
    cameraBuffer.ClearRenderTarget(true, false, Color.clear);
    context.ExecuteCommandBuffer(cameraBuffer);
    //buffer.Release();
    cameraBuffer.Clear();

    ...
}


还有一个内存消耗的原因是我们在申请命令缓存时,需要提取摄像机各自的名字。要这么做的话,系统需要新建一个字符串对象来储存我们从摄像机对象中提取出的数据,从而占用了内存。为此我们直接令命令缓存的名字为 “Render Camera” 即可:

    var buffer = new CommandBuffer() {
        // name = camera.name
        name = "Render Camera"
    };


3.2 渲染默认管线(Rendering the Default Pipeline)

由于我们的自定义渲染管线只配置了无光照着色器,因此无法渲染使用了着色器的物体。这些使用了不支持的着色器的物体将无法显示在场景中,虽然不影响场景的正常显示,但我们仍希望能够有特殊的手段显示这些物体,以告诉开发人员场景中存在着非法物体,方便后续的调试。

我们声明一个 “DrawDefaultPipeline()” 方法,以渲染内容和摄像机作为该方法的参数。。并在渲染管线描绘完透明物体之后调用该方法。

void Render (ScriptableRenderContext context, Camera camera) {
    ...

    context.DrawRenderers(
        cull.visibleRenderers, ref drawSettings, filterSettings
    );

    DrawDefaultPipeline(context, camera);

    context.ExecuteCommandBuffer(cameraBuffer);
    cameraBuffer.Clear();
    context.Submit();
}

void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {}


Unity的默认表面着色器配备了 “ForwardBase Pass”。我们可以使用它来区分出那些适用于默认着色器的物体。接下来的步骤与第2.5节《描绘》中的步骤相似,我们需要声明新的 “DrawRendererSettings”“FilterRenderersSettings”,并把新的 “Shader Pass” 设为 “ForwardBase”,最后重新调用 “context.DrawRenderers()” 方法:

void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
    var drawSettings = new DrawRendererSettings(
        camera, new ShaderPassName("ForwardBase")
    );
    
    var filterSettings = new FilterRenderersSettings(true);
    
    context.DrawRenderers(
        cull.visibleRenderers, ref drawSettings, filterSettings
    );
}


至此,使用了默认着色器的物体也能够显示在场景中了,此外我们也能够在帧调试器中看到这些物体。

在这里插入图片描述

由于我们所声明的 “DrawDefaultPipeline()” 为设定好必要的数据,因此所有依赖于光照渲染的物体都只能呈现出黑色。因此我们需要利用Unity内置的 “Error Shader” 来为他们着色。在此之前我们需要先为 “Error Shader” 配置专用材质。通常,Unity在 “Hidden/InternalErrorShader” 中已经预设了对应的材质,我们直接从中获取即可。但该材质并非实际用于开发的材质,我们无需让其作为我们项目的一部分保存,所以我们把材质的 “Hide Flags” 设为 “HideAndDontSave”

Material errorMaterial;

...

void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
    if (errorMaterial == null) {
        Shader errorShader = Shader.Find("Hidden/InternalErrorShader");
        errorMaterial = new Material(errorShader) {
            hideFlags = HideFlags.HideAndDontSave
        };
    }
    
    ...
}


我们可以通过用新获取的材质来重写掉 “DrawRenderSettings” 中原有的设定。这一步可通过调用 “SetOverrideMaterial()” 实现。该方法的第一个参数是我们新获取的材质,第二个参数是所需被覆盖的 “Shader Pass” 的索引,由于 “Error Shader” 只有单个 “Pass”,所以是用0作为传参。

	var drawSettings = new DrawRendererSettings(
	    camera, new ShaderPassName("ForwardBase")
	);
	drawSettings.SetOverrideMaterial(errorMaterial, 0);

在这里插入图片描述

应用Error Material后的描绘效果


除了 “ForwardBase” 以外,Unity默认着色器还具备其他 “Shader Pass”,例如 “PrepassBase”, “Always”, “Vertex”, “VertexLMRGBM”, “VertexLM”。我们也需要为渲染管线添加对这些 “Shader Pass” 的支持:

	var drawSettings = new DrawRendererSettings(
	    camera, new ShaderPassName("ForwardBase")
	);
	drawSettings.SetShaderPassName(1, new ShaderPassName("PrepassBase"));
	drawSettings.SetShaderPassName(2, new ShaderPassName("Always"));
	drawSettings.SetShaderPassName(3, new ShaderPassName("Vertex"));
	drawSettings.SetShaderPassName(4, new ShaderPassName("VertexLMRGBM"));
	drawSettings.SetShaderPassName(5, new ShaderPassName("VertexLM"));
	drawSettings.SetOverrideMaterial(errorMaterial, 0);


3.3 条件代码执行(Conditional Code Execution)

在上一节中,我们通过配置默认渲染管线,使Untiy引擎能够识别到场景中的非法物体的存在。但我们仅在开发的时候需要用到这一功能,在其他时候,我们希望该功能能够静默。为此,我们需要为管线添加条件 (Conditional) 属性。

条件属性被定义在 “System.Diagnostics” 库中。该库的其他一些属性是我们不需要用到的。因此我们仅提取库中的条件属性即可:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using Conditional = System.Diagnostics.ConditionalAttribute;


我们希望 “DrawDefaultPipeline()” 这一功能仅在我们开发或开发构建时有效,在进行发布构建时,我们不希望Unity引擎编译该功能。为此我们需要在 “DrawDefaultPipeline()” 前添加条件属性,并制定为 “DEVELOPMENT_BUILD”“UNITY_EDITOR”

[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
    ...
}


最终整体的代码如下:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Experimental.Rendering;
using Conditional = System.Diagnostics.ConditionalAttribute;

CullResults cull;                                                           // Declare cull result object
CommandBuffer cameraBuffer = new CommandBuffer {name = "Render Camera"};    // Declare command buffer
Material errorMaterial;                                                     // Declare error material

/* Pipeline object field */
public class MyPipeline : RenderPipeline {
    public override void Render ( ScriptableRenderContext renderContext, Camera[] cameras) {
        base.Render(renderContext, cameras);

        foreach (var camera in cameras) {                                   // Do render process for all cameras
            Render(renderContext, camera);
        }
    }

    void Render (ScriptableRenderContext context, Camera camera) {
        /* Culling */
        ScriptableCullingParameters cullingParameters;                      // Declare culling paremeter struct
        if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
            return;                                                         // Calculate cull data
        }
        CullResults.Cull(ref cullingParameters, context, ref cull);         // Cull unnecessary render object          


        /* Command Buffer */
        CameraClearFlags clearFlags = camera.clearFlags;
        cameraBuffer.ClearRenderTarget(                                     // Clear render targets
            (clearFlags & CameraClearFlags.Depth) != 0,
            (clearFlags & CameraClearFlags.Color) != 0,
            camera.backgroundColor
        );
        cameraBuffer.BeginSample("Render Camera");                          // Begin frame debugger sampling 
        context.ExecuteCommandBuffer(cameraBuffer);                         // Execute command buffer
        cameraBuffer.Clear();                                               // Release command buffer


        /* Drawing */
        var drawSettings = new DrawRendererSettings(                        // Set pipeline to use default unlit shader pass
            camera, new ShaderPassName("SRPDefaultUnlit"));

        drawSettings.sorting.flags = SortFlags.CommonOpaque;                // Sort opaque object render order 
        var filterSettings = new FilterRenderersSettings(true) {            // Limit pipeline to render opaque object frist 
            renderQueueRange = RenderQueueRange.opaque};                    // Render queue range: 0 ~ 2500
        context.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);

        context.SetupCameraProperties(camera);                              // Set up view-projection matrix and other camera properties
        context.DrawSkybox(camera);                                         // Draw skybox

        drawSettings.sorting.flags = SortFlags.CommonTransparent;           // Sort transparent object render order
        filterSettings.renderQueueRange = RenderQueueRange.transparent;     // Limit pipeline to render transparent object last, render queue range: 2501 ~ 5000
        context.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);

        DrawDefaultPipeline(context, camera);                               // Draw object that use default shaders

        cameraBuffer.EndSample("Render Camera");                            // End frame debugger sampling
		context.ExecuteCommandBuffer(cameraBuffer);
		cameraBuffer.Clear();

        context.Submit();
    }

    [Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]         // This method only compile in case "Development build" and "Unity editor"
    void DrawDefaultPipeline(ScriptableRenderContext context, Camera camera) {
        if (errorMaterial == null) {                                        // If error material is not initialized, retrieve it from default library
            Shader errorShader = Shader.Find("Hidden/InternalErrorShader"); 
            errorMaterial = new Material(errorShader) {
                hideFlags = HideFlags.HideAndDontSave                       // Prevent error material saved to project and appear in editor window
            };
        }

        var drawSettings = new DrawRendererSettings(
            camera, new ShaderPassName("ForwardBase"));                     // Add support of "Forward Base" shader pass
        var filterSettings = new FilterRenderersSettings(true);

        drawSettings.SetShaderPassName(1, new ShaderPassName("PrepassBase"));   // Add support of "Prepass Base" shader pass
        drawSettings.SetShaderPassName(2, new ShaderPassName("Always"));        // Add support of "Always" shader pass
        drawSettings.SetShaderPassName(3, new ShaderPassName("Vertex"));        // Add support of "Vertex" shader pass
        drawSettings.SetShaderPassName(4, new ShaderPassName("VertexLMRGBM"));  // Add support of "Vertex LMRGBM" shader pass
        drawSettings.SetShaderPassName(5, new ShaderPassName("VertexLM"));      // Add support of "Vertex LM" shader pass
        drawSettings.SetOverrideMaterial(errorMaterial, 0);                     // Override default material with error material
        
        context.DrawRenderers(cull.visibleRenderers, ref drawSettings, filterSettings);
    }

}

/* Pipeline asset field */
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]                       // Add pipeline asset to editor menu
public class MyPipelineAsset : RenderPipelineAsset {
        protected override IRenderPipeline InternalCreatePipeline() {
        return new MyPipeline();                                            // Instantiate pipeline object
    }
}




原文链接

Jasper Flick. (2019). Custom Pipeline. Retrieved from https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/custom-pipeline/

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值