Unity从2018开始添加了对可编程渲染管线的支持,使得我们可以从头开始设计自定义的管线。我们先从设计一个最小的可编程渲染管线开始,一步一步了解Unity的SRP。这里,我们使用的Unity版本为2019.4.22f1。
要使用SRP,首先要在Project Settings中设置自定义的Render Pipeline Asset
,这个asset是一个ScriptableObject,需要继承自RenderPipelineAsset:
那么我们先自己创建一个空的类:
using UnityEngine.Rendering;
public class MyPipelineAsset : RenderPipelineAsset
{
}
编译发现会有报错,提示需要实现RenderPipelineAsset的抽象方法,通过该方法Unity可以获取到一个自定义管线的对象实例:
同样先实现一个空方法:
using UnityEngine.Rendering;
using UnityEngine;
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset
{
protected override RenderPipeline CreatePipeline () {
return null;
}
}
然后我们就可以通过Create/Rendering/My Pipeline
创建出一个asset出来了,再把它拖到Project Settings里去,可以发现Unity的Scene/Game窗口都变得一片漆黑,打开Frame Debug,里面也是一片空白:
说明我们自定义的管线生效了,这个管线啥也没干,当然就一片漆黑了,一个Draw Call也没有。显然下一步我们需要继承并扩展Unity的RenderPipeline:
using UnityEngine.Rendering;
public class MyPipeline : RenderPipeline
{
}
这时候又有编译报错了,提示我们没有实现抽象方法Render,也就是最核心的渲染接口:
那么,我们先把天空盒显示出来。ScriptableRenderContext提供了一个绘制天空盒的方法,调用之后再提交绘制指令给GPU执行:
using UnityEngine.Rendering;
using UnityEngine;
public class MyPipeline : RenderPipeline
{
protected override void Render (ScriptableRenderContext renderContext, Camera[] cameras)
{
renderContext.DrawSkybox(cameras[0]);
renderContext.Submit();
}
}
可以看到,此时天空盒正常出现了,查看Frame Debug,也是一切正常:
不过context只提供一些简单的绘制命令,对于更为复杂的,我们需要借助command buffer来执行。比如我们模拟内置渲染管线,在渲染之前清一下render target:
var buffer = new CommandBuffer {
name = camera.name
};
CameraClearFlags clearFlags = camera.clearFlags;
buffer.ClearRenderTarget(
(clearFlags & CameraClearFlags.Depth) != 0,
(clearFlags & CameraClearFlags.Color) != 0,
camera.backgroundColor
);
context.ExecuteCommandBuffer(buffer);
buffer.Release();
然后打开Frame Debug,可以看到多了一个Clear的pass:
下一步,我们来考虑如何绘制场景中的objects。首先,我们希望,只有相机可见范围内的objects是需要绘制的,其他不可见的可以提前被剔除掉。SRP中也提供了剔除相关的接口,我们在绘制前先获取剔除的参数,如果获取不到则说明当前相机没有可见的objects需要绘制:
ScriptableCullingParameters cullingParameters;
if(!camera.TryGetCullingParameters(out cullingParameters))
{
return;
}
CullingResults cull = context.Cull(ref cullingParameters);
然后,我们调用DrawRenderers
进行绘制,这个函数除了前面的CullingResults
之外,还需要DrawingSettings
和FilteringSettings
两个参数。DrawingSettings
用来控制场景中objects的绘制顺序,以及需要使用哪个shader pass进行绘制;FilteringSettings
用来进一步筛选场景中的objects是否参与绘制。
var drawSettings = new DrawingSettings(new ShaderTagId("SRPDefaultUnlit"), new SortingSettings(camera));
var filterSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cull, ref drawSettings, ref filterSettings
);
这里,我们使用的是SRPDefaultUnlit
这个shader pass,也就是Unity内置的unlit shader。我们新建两个材质,分别使用Unlit/Color和Unlit/Transparent两个shader,然后创建两个使用该材质的Cube:
场景中只看到了不透明的Cube,虽然在Frame Debug中,显示了绘制两个Cube的pass:
为什么呢?这是因为绘制透明物体是不写深度的,意味着如果先绘制了透明物体,再绘制天空盒时,天空盒会覆盖掉透明物体。因此我们需要对绘制的顺序进行调整,在绘制天空盒之前,只绘制不透明的物体,最后再绘制透明物体。
var drawSettings = new DrawingSettings(new ShaderTagId("SRPDefaultUnlit"), new SortingSettings(camera));
var filterSettings = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(
cull, ref drawSettings, ref filterSettings
);
context.DrawSkybox(camera);
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull, ref drawSettings, ref filterSettings
);
另外,为了尽量避免overdraw,我们希望在绘制不透明的物体时,按照从近到远的顺序进行渲染;在绘制透明的物体时,则要按照从远到近的顺序进行渲染。可以通过设置sortingSettings.criteria
的值来进行控制。
var sortingSettings = new SortingSettings(camera);
sortingSettings.criteria = SortingCriteria.CommonOpaque;
var drawSettings = new DrawingSettings(new ShaderTagId("SRPDefaultUnlit"), sortingSettings);
var filterSettings = new FilteringSettings(RenderQueueRange.opaque);
context.DrawRenderers(
cull, ref drawSettings, ref filterSettings
);
context.DrawSkybox(camera);
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawSettings.sortingSettings = sortingSettings;
filterSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cull, ref drawSettings, ref filterSettings
);
自此,一个简单的自定义管线就算完成了,后面再让我们进一步完善它。
Reference
[1] Custom Pipeline