自定义渲染管线 掌控渲染
创建管线资源和实例
渲染摄像机视图
执行裁剪 ,过滤 ,排序
区分 不透明,半透明,和 无效的 pass
使用多个摄像机工作
这是自定义渲染管线的第一课,它包含了最初创建该管线的内容,我们将会围绕它逐渐展开。
这个系列课程假设你已经学习过 Object Manager 系列 和 Procedural Grid课程。
这个教程使用unity 2019.2.6f1来制作。
注意:作者还有一个基于 untiy 2018 版本的自定义管线教程,所不同的是这个系列教程将会采用更现代化的方法。
该教程最终的效果图
新的渲染管线
为了要渲染所有的物体,Unity 必须 确定 应该绘制什么样的形状,在哪里 应该何时渲染,需要有什么样的设置。这个是很复杂的,具体取决于所涉及的多种效果。灯光,阴影,透明度,图像效果,体积效果等都必须以正确的顺序处理才能得到最终图像。这些就是渲染管线所做的事情。
在过去unity仅支持几种内置的方式来渲染东西。Unity 2018 引入可编程渲染管线 - 简称PRS -使我们可以做任何我们想做的事情,同时仍然能够借助于Unity进行提供的基础方法,例如剔除 。Unity 2018还添加了两个使用两种基于这个思想的管线:LWRP(轻量级RP)和HDRP(高清晰度RP)。在Unity 2019 中LWRP被抛弃并更名为URP。
URP 的目的是为了替换默认的渲染管线。URP很容易定义,除了教你怎么定制渲染管线外,这个系列教程还将从头到尾,交你创建渲染管线。
本教程最开始以最小的渲染管线为基础,使用前向渲染绘制不会被照明的形状物体。一旦工作后,我们将会在后面扩展这个管线
添加光照,阴影,采用不同的渲染方法,以及更多高级功能。
项目设置
使用unity 2019 或者更高的版本创建一个3D 工程。因为我们将使用我们自己的管线,因此不要选择 任何一个管线工程模板来创建工程。(其实就是使用默认的勾选来创建一个项目)。工程打开后,选择PackageManager 选项,移除你不需要用的package 。Unity UI Packag 需要保留,在这个教程中会使用它来绘制UI。
使用线性空间,但是Unity 2019 默认使用Gama空间,所以我们需要去修改相应的设置。
在默认的场景上放置几个物体来填充一下,混合 使用标准的unlit (不能接受光照)的不透明和透明材质。Unlit/Transparent 着色器,仅仅使用在图片上。我们还会使用一个球形贴图。
场景上放置一些cube,他们都使用不透明材质。红色的使用标准的材质,绿色和黄色的使用的Unlit/Color shader.着色器。蓝色的球使用半透明的模式的标准材质。白球使用Unlit/Transparent shader.
管线资源
现在,unity使用默认的渲染管线,为了让自定义管线替换它,我们需要创建一个对应的类型的资源。创建一个文件夹Universal RP,然后在Universal RP文件夹下创建一个子文件夹 Runtime,然后在 Runtime文件夹下创建一个 CustomRenderPipelineAsset类型的Asset。
这个文件必须继承UnityEngine.Rendering空间下的 RenderPipelineAsset。
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipelineAsset : RenderPipelineAsset {}
这个RP Asset的主要目是为Unity提供一种获取负责渲染的管道对象实例的方式。这个Asset的自身的作用仅仅是控制和存放设置。目前我们还没有设置任何东西,因此需要给unity 提供一个(可以设置自定义管线的)实例。通过重写抽象方法CreatePipeline,可以返回一个管线实例。但由于还没有定义自定义管线类型,因此现在运行它会返回null。
CreatePipeline 方法的访问修饰符设置为procted,这意味着只有定义方法的类(即RenderPipelineAsset)及其扩展类可以访问它。
protected override RenderPipeline CreatePipeline () {
return null;
}
现在,我们需要将这种类型的Asset添加到我们的项目中。为此,请将CreateAssetMenu属性添加到CustomRenderPipelineAsset。
[CreateAssetMenu]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
这将在Asset/Create menu中创建一个条目。为了保持整洁我们可以把它放到Rending 的子目录中。可以将属性的menuName属性设置为Rendering / Custom Render Pipeline来实现。这样就可以直接在属性类型后的方括号内设置此属性。
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
使用这种菜单添加方式来将这种类型的Asset添加到工程中后,然后到 Graphics project settings中在 Scriptable Render Pipeline Settings 下选择它。
替换默认的渲染管线改变了几件事。首先,图形设置中很多选项会消失。其次,我们禁用了默认RP而不提供有效的替换,因此什么东西都不会渲染了。游戏窗口,场景窗口和材质预览不再起作用。现在启用 Window / Analysis / Frame Debugger ,您将看到在游戏窗口中确实没有绘制任何内容。
渲染管线实例
创建一个 CustomRenderPipeline
然后和 CustomRenderPipelineAsse 放到同一个文件夹下。这个就是我们的Asset返回的渲染管线实例。同时它也必须继承自RenderPipline。
using UnityEngine;
using UnityEngine.Rendering;
public class CustomRenderPipeline : RenderPipeline {}
由于RenderPipline 定义了一个受保护的抽象Render方法,我们必须重写此方法才能创建具体的管线。它有两个参数:ScriptableRenderContext和Camera数组。现在将该方法留空
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
) {}
使CustomRenderPipelineAsset.CreatePipeline返回CustomRenderPipeline的新实例。这将使我们获得有管线,尽管它还么有渲染任何东西。
protected override RenderPipeline CreatePipeline () {
return new CustomRenderPipeline();
}
渲染
Unity每帧都会RP实例上调用Render。它沿上下文结构传递,该上下文结构提供了到本机引擎的连接,我们可以将其用于渲染。它还可以传递一系列摄像机,场景因此场景中可以有多个摄像处于激活状态的摄像机。 RP负责按照提供的顺序渲染这些摄像机。
摄像机渲染
每一个摄像机都会独立渲染。与其用 CustomRenderPipeline渲染所有的摄像机,不如建立一个类,渲染一个摄像机。把这个类命名为CameraRenderer
,提供一个Render方法,参数为Contex 和 Camera,并把这些参数以属性的形式保存下来。
using UnityEngine;
using UnityEngine.Rendering;
public class CameraRenderer {
ScriptableRenderContext context;
Camera camera;
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
让CustomRenderPipeline在创建渲染器时创建一个渲染器实例,然后使用它来循环渲染所有摄像机
meraRenderer renderer = new CameraRenderer();
protected override void Render (
ScriptableRenderContext context, Camera[] cameras
) {
foreach (Camera camera in cameras) {
renderer.Render(context, camera);
}
}
(相机渲染器大致相当于 传统的 RP的可编程渲染器 吐槽::垃圾翻译),这种渲染方式会很容易的支持每个相机的不同的渲染方式。
例如一个摄像机用于渲染第一人称视图,一个用来渲染场景上的小地图的俯瞰图,一个用于前向渲染一个用于延迟渲染等。但是目前仅采用这一种相同的方式。
绘制天空盒
CameraRenderer.Render 的任务是渲染摄像机视野范围内的所有几何体。用一个独立的方法 DrawVisibleGeometry() 来实现它。
首先需要绘制天空盒,可以通过Contex.DrawSkyBox()方法,把camera 当作参数传递进去即可。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
DrawVisibleGeometry();
}
void DrawVisibleGeometry () {
context.DrawSkybox(camera);
}
目前天空盒还不会绘制出来,那是因为我们发出的绘制命令还在缓冲中(缓冲队列里面)。还得需要
再定义一个方法Submit() ,把它提交执行。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
DrawVisibleGeometry();
Submit();
}
void Submit () {
context.Submit();
}
现在skybox 可以出现在View 和 Game 视图里面了,启用 Frame debugger 可以看到Camera.RenderSkybox,它的下面仅仅只有一个DrawMesh 代表实际的DrawCall。
现在 摄像机的偏移角度并不会对天空盒照成影响,因为调用DrawBox方法只是决定天空盒是否绘制,它是通过控制的是摄像机的Clear flag 来实现的。
为了正确的显示天空盒,需要设置View - Projection matrix,这个变换矩阵包含了摄像机的位置和方向,世界到摄像机,投影矩阵(透视投影 正交投影)。Unity_MatrixVP 在绘制集合体的时候被当作一个共有的属性使用,这个可以在Framedebugger 中的 ShaderProperties部分可以看到。我们通过调用SetupCameraProperties来传递
这个矩阵以及其他的摄像机参数的 。这里单独编写了一个函数DrawVisibleGeemetry来实现它。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
Setup();
DrawVisibleGeometry();
Submit();
}
void Setup () {
context.SetupCameraProperties(camera);
}
Command Buffers
Contex 会延迟实际渲染,直到我们提交它为止。在此之前,我们对其进行配置并向其添加命令以供以后执行。某些任务(例如绘制天空盒)可以通过专用方法发出,而其他命令则必须通过单独的命令缓冲区间接发出。我们需要这样的缓冲区来绘制场景中的其他几何图形。 要获得缓冲区,我们必须创建一个新的CommandBuffer对象实例。我们只需要一个缓冲区,因此默认情况下为CameraRenderer创建一个缓冲区,并将对它的引用存储在字段中。还给缓冲区起一个名字,以便我们在帧调试器中识别它。渲染相机即可。
const string bufferName = "Render Camera";
CommandBuffer buffer = new CommandBuffer {
name = bufferName
};
初始化语法工作原理
上面创建一个对象CommandBuffer 类型对象,把对象的初始化放到了一个中括号块当中,避免了使用一个具有很多参数的构造函数,结构看起来很清晰明了。(原文写的真的很难让人看懂,这里的翻译先按照自己理解,后续再补充)
如果想在Profiler 和 framedebugger 当中观察 command buffer 也是可以的。只需要按照下面这样把 commandBufer 的名字传递进去即可(BegineSample 和 EndSample 分别代表了我们采样的起始点和结束点可以具体参考他们使用方式的有关资料)。
void Setup () {
buffer.BeginSample(bufferName);
context.SetupCameraProperties(camera);
}
void Submit () {
buffer.EndSample(bufferName);
context.Submit();
}
要执行调用 这个buffer ,需要通过Contex.ExcuteCommandBuffer()把buffer 当作参数传递进去。由于这个步骤只是一个拷贝buffer并不会清楚它,(为什么不会自动把它清除掉?),因此如果想重复使用的话,还得需要调用clear()方法。由于他们两个的调用是紧密挨着的,因此可以封装一个方法来实现添加和删除操作。
void Setup () {
buffer.BeginSample(bufferName);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
void Submit () {
buffer.EndSample(bufferName);
ExecuteBuffer();
context.Submit();
}
void ExecuteBuffer () {
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
Camera.RenderSkyBox示例现在嵌套在“渲染相机”内部。
清除渲染目标
论我们绘制什么,最终都会渲染到相机的渲染目标,该目标默Bei认为帧缓冲区,但也可能是渲染纹理。如果这个渲染目标不清除的话就会一直在那里,所以为了保证不对后渲染输出的图像产生影响需要清楚掉。可以通过调用CommandBuffer的 ClearRenderTarget方法实现。这里把它放到了Setup方法当中。
CommandBuffer.ClearRenderTarget 需要至少三个参数。前两个参数表示深度和颜色数据是否需要清除,这里把他们都置为true,第三个参数是用来清除屏幕的颜色,这里使用Color.clear.
void Setup () {
buffer.BeginSample(bufferName);
buffer.ClearRenderTarget(true, true, Color.clear);
ExecuteBuffer();
context.SetupCameraProperties(camera);
}
Clearing, with nested sample
frame debubuffer 现在显示的Draw GL条目是代表进行的清除操作,该条目显示嵌套在附加级别的Render Camera中(影响到了sample 的显示效果)。如果不想让它在里层的RenderCamera里面显示的话,可以把它的调用移到BegineSample 的外面. Noti: 实际上在我使用面那段代码时候,显示的效果是正确的,不知道是不是版本的问题我用的是unity 2019.3.8 f1.而原作者说需要执行下面的代码才能显示出和上面一样的效果。
void Setup () {
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(bufferName);
//buffer.ClearRenderTarget(true, true, Color.clear);
ExecuteBuffer();
context.SetupCameraProperties(camera);
裁剪
当前,我们看到了天空盒,但看不到场景中放置的任何对象,我们仅渲染那些对相机可见的对象。首先从场景中具有渲染器组件的所有对象开始,然后剔除摄像机视锥范围之外的对象。
找出可以剔除的内容需要我们跟踪多个相机设置和矩阵,为此我们可以使用ScriptableCullingParameters结构。不需要自己填充这个结构体数据,我们可以调用Camera.TryGetCullingParameters。它返回值代表着是否成功检索,参数被out 修饰,以便能够传递出在Camera.TryGetCullingParameters函数里面修改后的结果值。这个过程的实现我们把它放大了Cull函数里面。
bool Cull () {
ScriptableCullingParameters p
if (camera.TryGetCullingParameters(out p)) {
return true;
}
return false;
}
当用作输出参数时(被out 修饰),可以将变量声明内联到参数列表中。
bool Cull () {
//ScriptableCullingParameters p
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
return true;
}
return false
}
在Render函数里面 在SetUp函数调用前调用 Cull。
public void Render (ScriptableRenderContext context, Camera camera) {
this.context = context;
this.camera = camera;
if (!Cull()) {
return;
}
Setup();
DrawVisibleGeometry();
Submit();
}
其实真正的裁剪是通过Contect调用Cull来的实现的,它会返回一个结构体 CullingResults
。我们把它通过ref修饰来一个转换为引用传递以获取裁剪后的结构值。
CullingResults cullingResults;
…
bool Cull () {
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
cullingResults = context.Cull(ref p);
return true;
}
return false;
}
绘制几何体
一旦知道了我们要绘制的东西,下一步就是把他们渲染出来 了。通过Context.DrawRenderers 方法,参数为裁剪后的结果来实现。除此之外还必须支持drawing settings and filtering settings. 他们两个都是通过两个结构体来实现—DrawingSettings
and FilteringSettings。
我们都把他们通过ref修饰符来修饰以获取执行后的结果。这个过程我们放到了DrawVisibleGeometry里面在DrawSkeyBox之前来实现。
void DrawVisibleGeometry () {
var drawingSettings = new DrawingSettings();
var filteringSettings = new FilteringSettings();
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
context.DrawSkybox(camera);
}
到现在为止,我们还不能看到任何东西,因为我们还没有指定使用什么样的着色器来渲染。在本教程中仅仅使用unlit 着色器。这里通过设置着色器标签id为SRPDefaultUnlit。
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
传递给 DrawSettings两个参数,一个是上面的unlitShaderTagId,一个结构体SortingSettings,这个结构体将用来设置渲染时候的排序规则。
void DrawVisibleGeometry () {
var sortingSettings = new SortingSettings(camera);
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
);
…
}
除此之外还需要指定一个渲染序列,FilteringSettings 结构体,使用RenderQueueRange.all
来包含所有的渲染序列。
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
Drawing unlit geometry.
仅仅使用 unlit 着色器的物体才被渲染。所有的draw call 都可以在 上面被看到。但是半透明物体的渲染有些奇怪,我们可以在framedebugger中点击一帧一帧查看。(视频地址)https://gfycat.com/bothanyindianskimmer
绘制顺序是随意的。我们可以通过设置排序设置的criterias属性来指定特定的绘制顺序。通过设置SortingCriteria.CommonOpaque实现。
var sortingSettings = new SortingSettings(camera) {
criteria = SortingCriteria.CommonOpaque
};
使用这样的排序只能确保不透明物体的渲染顺序是正确的,但是半透明透明物体的渲染顺序还是不对的。所以为了 我们应该先渲染不透明物体,然后渲染半透明透明物体。同时为了确保天空盒的控制正确,应该在他们中间渲染。由于物体的之间有遮挡,为了性能方面的考虑,
1 在渲染不透明物体的时候按照物体距离摄像机的距离从前到后
2 由于半透明透明物体不能写入深度值,同时为了保证渲染顺序的正确,按照从后到前。(NOTI:半透明透明物体从后到前的绘制顺序其实不能保证渲染的顺序是正确的,因为它只是基于物体的位置来确定的,- 这点很容易理解,由于这个渲染是基于位置的,但是观察的时候其实是基于角度的,如果改变观察角度很容易发现物体的渲染顺序问题)
context.DrawSkybox(camera);
sortingSettings.criteria = SortingCriteria.CommonTransparent;
drawingSettings.sortingSettings = sortingSettings;
filteringSettings.renderQueueRange = RenderQueueRange.transparent;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
编辑器渲染
我们RP 现在可以正确的绘制 不接受光照的物体了,但是现在我们还可以在unity 编辑器里面做一些可以提升经验的的事情。
-
Legacy Shader
由于现在的管线仅仅支持 非光照的pass,回导致使用其他的pass 的物体不能渲染。但是这种情况是正确的,它会把在场景中使用错误shader 的物体的隐藏起来。如果想把这些隐藏物体渲染出来的话,我们需要单独对他们处理。有时候从unity默认设置的工程切换到自定义管线工程,会发现有些物体的渲染会出错。为了渲染出unity所有内置的shader,需要这些tag IDs
Always, ForwardBase, PrepassBase, Vertex, VertexLMRGBM, and VertexLM passes。他我们把他们保存为一个静态数组。
static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};
在绘制完所有几何体之后使用一个单独的方法来渲染目前不支持的着色器。既然这些都是无效的渲染pass,因此不用关心怎么设定它,只要使用默认的FilteringSettings.defaultValue
就可以(DrawUnsupportedShaders 为新添加的方法)。
public void Render (ScriptableRenderContext context, Camera camera) {
…
Setup();
DrawVisibleGeometry();
DrawUnsupportedShaders();
Submit();
}
…
void DrawUnsupportedShaders () {
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);
}
可以通过调用drawing settings的 SetShaderPassName
方法,并传递一个绘制顺序的索引和一个tag来绘制。对数组中的所有tag进行此操作,从第二索引开始,因为在构造函数中我们已经设置了对第一个tag 的处理。
var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
);
for (int i = 1; i < legacyShaderTagIds.Length; i++) {
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}
现在那些不支持的着色器也渲染出来了(默认的standard着色器),但是现在渲染出来的颜色是黑色的,那是因为还没有设置着色器属性。
-
错误的材质渲染
为了很清楚的看到那些物体的着色器不被支持,将使用一个表示渲染错误的着色器来表明。构建一个新的材质,参数调用 Shader.Find("Hidden/InternalErrorShader") 。然后把这个材质标识为静态的防止每帧创建。然后把它赋值给 drawing settings
的overrideMaterial 属性。
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
};
…
}
Rendered with magenta error shader
现在,所有无效对象都是可见的。
-
Partial Class
渲染无效的物体是在开发阶段是有用的,但是在发布后的软件中使用是不可以的。因此需要给CameraRenderer
创建一个Partial 类,并且让其只是在editor模式下工作。只需要复制下CameraRenderer 脚本,然后再命名为 CameraRenderer.Editor.
然后把原来的CameraRenderer 类名前面加上partial,并移除 tag Array 和 error material 和DrawUnsupportedShaders
方法,
public partial class CameraRenderer { … }
What are partial classes?
It's a way to split a class—or struct—definition into multiple parts, stored in different files. The only purpose is to organize code. The typical use case is to keep automatically-generated code separate from manually-written code. As far as the compiler is concerned, it's all part of the same class definition. They were introduced in the Object Management, More Complex Levels tutorial.
移除新创建的 paritial 类中的其他的代码,只保留下面的部分,为了使它仅在编辑器下工作加上编译条件 UNITY_EDITOR。
partial class CameraRenderer {
#if UNITY_EDITOR
static ShaderTagId[] legacyShaderTagIds = { … }
};
static Material errorMaterial;
void DrawUnsupportedShaders () { … }
#endif
}
现在编译的话还是会出问题,原因在于另外一个 paritial类中 在调用 DrawUnsupportedShaders,遇到这种情况 需要在该方法前加上partial
就可以了,这种使用方法类似于使用抽象方法。这种方法的声明也需要在前面加上partial。
partial void DrawUnsupportedShaders ();
#if UNITY_EDITOR
…
partial void DrawUnsupportedShaders () { … }
#endi
现在可以编译成功了,编译器会剔除所有的没有完整声明的partial方法。
Can we make the invalid objects appear in development builds?
Yes, you can base the conditional compilation on
UNITY_EDITOR || DEVELOPMENT_BUILD
instead. ThenDrawUnsupportedShaders
exists in development builds as well and still not in release builds. But I'll consistently limit everything development-related to the editor only in this series.
-
绘制Gizmo
现在不管我们的游戏还是视图窗口都不支持绘制Gizmo。
我们可以通过调用UnityEditor.Handles.ShouldRenderGizmos来检查是否应该绘制Gizmo。如果是这样,我们必须使用相机作为参数在context中调用DrawGizmos,再加上第二个参数来指示应绘制哪个Gizmo子集。有两个子集,分别用于图像效果前后。由于我们目前不支持图像效果,因此我们将同时调用两者。在仅用于编辑器的新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
}
应该在绘制完所有场景物体后再绘制Gizmo。
public void Render (ScriptableRenderContext context, Camera camera) {
…
Setup();
DrawVisibleGeometry();
DrawUnsupportedShaders();
DrawGizmos();
Submit();
}
有Gizmo
没有Gizmo
-
绘制UnityUI
其他需要我们注意的是unity 游戏内用户接口(名字真好听)。例如通过Game/UI/Button(NOTI:如果没有这个选项的话 证明你还没有导入UI package) 添加一个按钮来创建一个简单的UI。但是它现在只能显示在游戏窗口,而不能在视图窗口里面显示出来。
点开frame debuffer 后 会发现 UI的渲染是独立的,它不属于我们自定义管线内容。
至少在将画布组件的“渲染模式”设置为“Screen Space - Overlay(默认设置)的情况下。将其更改为Screen Space - Camera 并将主相机用作其“渲染相机”将使其成为透明几何的一部分.
在场景窗口中中UI默认的渲染模式是World Space,这就是为什么它有时候看起来很大,但是在我们在scene 视图中点击去看发现什么都没有渲染出来。
场景窗口渲染时,我们必须通过使用相机作为参数调用ScriptableRenderContext.EmitWorldGeometryForSceneView,将UI显式添加到世界中。在仅用于编辑器的新PrepareForSceneWindow方法中执行此操作。当场景摄像机的cameraType属性等于CameraType.SceneView时,将使用场景摄像机进行渲染。
partial void PrepareForSceneWindow ();
#if UNITY_EDITOR
…
partial void PrepareForSceneWindow () {
if (camera.cameraType == CameraType.SceneView) {
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
}
}
由于会在场景中添加集合体,所以在剔除前调用它。
PrepareForSceneWindow();
if (!Cull()) {
return;
}
现在可以在scene 视图中看到它了。
多个摄像机
场景中有可能会有多个摄像机,如果这样的话必须保证他们能够协同工作。
-
两个摄像机
每一个摄像机都有一个深度值,主摄像机的默认是-1。它们以越来越高的深度顺序进行渲染。为了能看到这些,复制一下主摄像机然后名字改为Secondary Camera,并把它的深度值设置为0.最好给摄像机命名一个新的tag,这样保证了场景中只有一个主摄像机。
两台摄像机都归入一个样本范围
场景现在被渲染两次,但是最终渲染出的结果却一样这是因为渲染贴图在他们中间被清除,这从frame debugger中就可以看出来。
如果每个摄像机都有自己的渲染范围,那就更清楚了。添加仅在编辑器使用的PrepareBuffer方法,该方法使缓冲区的名称等于摄像机的名称。
partial void PrepareBuffer ();
#if UNITY_EDITOR
…
partial void PrepareBuffer () {
buffer.name = camera.name;
}
#endif
在准备绘制场景窗口前调用它。
PrepareBuffer();
PrepareForSceneWindow();
分开采样每个摄像机
-
更改的缓冲区名称
尽管帧调试器现在为每个摄像机显示了一个单独的渲染层次结构,但是当我们进入播放模式时,Unity的控制台将充满提示,警告我们BeginSample和EndSample计数必须匹配。由于我们为样本及其缓冲区使用了不同的名称,因此编辑器就困惑了。除此之外,每次访问摄像机的name属性时,我们最终都会分配内存,因此我们不想在构建时候这样做。
为了处理这个问题,需要添加一个SampleName 字符串属性,如果在编辑器中,则将它与缓冲区的名称一起设置在PrepareBuffer中,否则,它只是Render Camera字符串的常量别名。
#if UNITY_EDITOR
…
string SampleName { get; set; }
…
partial void PrepareBuffer () {
buffer.name = SampleName = camera.name;
}
#else
const string SampleName = bufferName;
#endif
在Setup 和 Submit 方法中使用 SampleName。
void Setup () {
context.SetupCameraProperties(camera);
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
void Submit () {
buffer.EndSample(SampleName);
ExecuteBuffer();
context.Submit();
}
我们可以通过检查Profiler(通过Window / Analysis / Profiler打开)并首先在编辑器中播放来看到差异。切换到Hierarchy 模式,然后按“ GC Alloc”列进行排序。您将看到一个两次调用GC.Alloc的条目,总共分配了100个字节,这是由检索摄像机名称引起的。在更下方,您会看到这些名称显示为示例:“Main Camera和“Secondary Camera”。
接下来,在启用了Development Build和Autoconnect Profiler的情况下进行构建。播放build ,并确保已连接profiler并进行记录。在这种情况下,我们没有获得100字节的分配,而是获得了一个“渲染相机”样本。
What are the other 48 bytes allocated for?
It's for the cameras array, over which we have no control. Its size depends on how many cameras get rendered
如果仅在编辑器中分配内存,而不在构建中分配内存。在这种情况下,我们需要从UnityEngine.Profiling命名空间调用Profiler.BeginSample和Profiler.EndSample。仅BeginSample需要传递名称.
using UnityEditor;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
partial class CameraRenderer {
…
#if UNITY_EDITOR
…
partial void PrepareBuffer () {
Profiler.BeginSample("Editor Only");
buffer.name = SampleName = camera.name;
Profiler.EndSample();
}
#else
string SampleName => bufferName;
#endif
}
Layers
摄像机也可以配置为仅在某些层上看到事物。这可以通过调整其剔除蒙版来完成。要查看该效果,让我们将所有使用标准着色器的对象移动到“忽略Raycast”层。
从主摄像机的剔除蒙版中排除该层。
Culling the Ignore Raycast layer.
并使其成为“辅助摄像机”看到的唯一图层。
Culling everything but the Ignore Raycast layer.
辅助摄像机最后渲染,所以我们最终只能看到无效的对象。
Only Ignore Raycast layer visible in game window.
-
Clear Flags
我们可以通过调整要渲染的第二个摄像机的清除标志来合并两个摄像机的结果。它们由CameraClearFlags枚举定义,我们可以通过相机的clearFlags属性检索该枚举。清除之前,请在Setup
前执行此操作。
void Setup () {
context.SetupCameraProperties(camera);
CameraClearFlags flags = camera.clearFlags;
buffer.ClearRenderTarget(true, true, Color.clear);
buffer.BeginSample(SampleName);
ExecuteBuffer();
}
The CameraClearFlags
enum defines four values. From 1 to 4 they are Skybox
, Color
, Depth
, and Nothing
. These aren't actually independent flag values but represent a decreasing amount of clearing. The depth buffer has to be cleared in all cases except the last one, so when the flags value is at most Depth
.
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth, true, Color.clear
);
仅在将标志设置为“颜色”时,我们才真正需要清除颜色缓冲区,因为在Skybox中,无论如何我们最终都会替换所有之前的颜色数据。
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
Color.clear
);
如果要清除为纯色,则必须使用相机的背景色。但是,由于我们要在线性颜色空间中进行渲染,因此必须将该颜色转换为线性空间,因此最终需要使用camera.backgroundColor.linear。在所有其他情况下,颜色无关紧要,因此我们可以使用Color.clear就足够了。
buffer.ClearRenderTarget(
flags <= CameraClearFlags.Depth,
flags == CameraClearFlags.Color,
flags == CameraClearFlags.Color ?
camera.backgroundColor.linear : Color.clear
);
由于主摄像机是第一个渲染的摄像机,因此其“清除标志”应设置为“ Skybox”或“color”。启用frame debugger后,我们总是从清除缓冲区开始,但这通常不能保证。
辅助摄像机的清除标志确定如何组合两个摄像机的渲染。对于“skybpox”或者"color",以前的结果将完全替换。如果仅清除深度,则辅助摄影机将正常渲染,但不会绘制天空盒,因此以前的结果显示为背景。当什么都没有清除时,深度缓冲区将保留,因此未照亮的对象最终将遮挡无效对象,就像它们是由同一台摄像机绘制的一样。但是,前一个摄像机绘制的透明对象没有深度信息,因此像 skybox之前所做的那样被绘制.
Clear color, depth-only, and nothing.
通过调整相机的 Viewport Rect ,也可以将渲染区域缩小到整个渲染目标的一小部分。其余渲染目标保持不受影响。在这种情况下,将使用Hidden / InternalClear着色器进行清除。模板缓冲区用于将渲染限制在视口区域。
Reduced viewport of secondary camera, clearing color.
请注意,如果每帧渲染一台以上的摄像机就必须同时进行多次剔除,设置,分类等操作这是很耗的。一种有效的方式是让每一个摄像机都有自己的渲染视角。
下一节课 介绍DrawCall。