Unity通用渲染管线(URP)系列(一)——自定义渲染管线(Taking Control of Rendering)

2020-12-11 15:26:00 浏览数 (1)

· 1、新的渲染管线

· 1.1 建立工程

· 1.2 管线资产

· 1.3 渲染管线实例

· 2 渲染呈现

· 2.1 相机渲染

· 2.2 呈现天空盒

· 2.3 Command Buffers

· 2.4 清除渲染目标

· 2.5 剔除

· 2.6 画几何

· 2.7 分开绘制不透明和透明物体

· 3 编辑器渲染

· 3.1 使用旧的Shaders

· 3.2 错误的材质

· 3.3 局部类

· 3.4 绘制Gizmos

· 3.5 绘制Unity UI

· 4 多摄像机

· 4.1 两个摄像机

· 4.2 处理更改的缓冲区名称

· 4.3 Layers

· 4.4 清除标志

本文重点: 创建渲染管线资产和实例 渲染摄像机的视角 剔除,过滤和排序 分离不透明、透明和无效通道 多摄像机下工作

这篇是自定义可编程管线的教程的第一部分,它创建一个基础的渲染管线资源,为后面的教程提供基础。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。

本系列,假设你已经完成了 对象管理 以及 网格生成 的章节部分。

该示例使用Unity2019.2.6f1的版本制作。

有没有其他关于SRP系列的教程? 还有另一个教程系列,介绍了脚本化呈现管线,但这个系列使用的是仅适用于Unity2018的实验性SRPAPI。本系列需要Unity2019及更高版本。 本系列采用了一种不同的、更现代的方法,但将涵盖许多相同的主题。如果你不在乎Unty2018版本的系列追上了这个系列,那这个系列还是很有用的。

(使用自定义渲染管线)

1 新的渲染管线

当进行渲染的时候,Unity需要决定把它画成什么形状,以及画在哪里、什么时候画、用什么样的设定去画等等。它的复杂程度取决于涉及到多少的效果。灯光、阴影、透明度、图像效应(后处理)、体积效应等等。所有的效果都需要按照正确的顺序叠加到最后的图像上,这就是我们说的渲染管线所做的事情。

在以前,Unity只支持一些内置的方式来渲染物体。Unity2018引入了脚本化的渲染管线scriptable render pipelines(简称RPS),让我们可以做任何我们想做的事情,同时仍然能够依靠Unity来执行基本的步骤,比如剔除。

Unity2018年还增加了两个实验性的RPs来支持这个特性:轻量级RP和高清晰度RP。在Unity2019,轻量级RP不再是实验性的,并在Unity2019.3被重新命名为Universal RP。

Universal RP注定要取代当前遗留的RP作为默认的渲染管线。之所以这么说,是因为这是一个适合大多数RP的尺度,也相当容易定制。除了自定义RP之外,这个系列还将从零开始创建一个完整的RP。

这个教程会使用最基础的Unlit的前向渲染来画一个基础形状,用来做RP演示的基础。完成之后,会在后面的教程里拓展光照、阴影、不同的渲染方法以及更多的高级特性。

1.1 建立工程

在Unity 2019.2.6或更高版本中创建新的3D项目。因为我们将创建自己的管线,因此不要选择任意的RP项目模板。打开项目后,你可以转到package manager并删除所有不需要的package 。在本教程中,将仅使用Unity UI包来绘制UI,因此可以保留该UI。

该示例会在linear 色彩空间中工作,但Unity 2019.2仍将gamma空间用作默认值。通过“Edit / Project Settings ”进入Player设置,然后选择“Player”,然后将“Other Settings”部分下的“Color Space”切换为“Linear”。

(色彩空间设置为Linear)

使用标准的, standard, unlit opaque 和transparent 的材质进行混合,然后用一些对象填充默认场景。因为“Unlit/Transparent”着色器仅适用于纹理,因此这里看到的是该球体的UV贴图。

(黑色背景上的球体alpha的UV map)

我在测试场景中放了几个立方体,所有这些都是不透明的。红色的使用Standard 着色器的材质,绿色和黄色的使用Unlit/Color着色器的材质。蓝色球体使用Standard 着色器,Rendering Mode 设置为Transparent,而白色球体使用Unlit/Transparent着色器。

(测试场景)

1.2 管线资产

目前,Unity使用默认还都是默认渲染管线。如果要用自定义渲染管线替换它的话,首必须为它创建一个资产类型。我们将使用与Universal RP大致相同的文件夹结构,在Run time子文件夹创建自定义RP资产文件夹。放置一个新的C#脚本命名为CustomRenderPiineAsset作为它的类型。

(目录结构)

资产类型必须继承自RenderPipelineAsset,该类在UnityEngine.Rendering命名空间下。

RP资产的主要目的是提供一种方法来获取负责渲染的管线的对象实例。资产本身只是一个句柄和存储设置的地方。我们还没有进行任何设置,所以所要做的就是给Unity一个获得管线对象实例的方法。通过重写抽象方法CreatePipeline方法来完成,该方法需要返回RenderPipeline实例。但是因为我们还没有定义自定义RP类型,所以先返回NULL。

CreatePipeline管线方法是用protected 的访问修饰符定义的,这意味着只有定义方法的类(即RenderPipelineAsset)和从它继承的类才能访问它。

现在,需要在我们的项目中添加这个类型的资产。要做到这一点,需要向CustomRenderPipelineAsset添加一个CreateAssetMenu属性。

这会在Asset/Create菜单中添加一个菜单条目。如果需要保持菜单整洁,并将其放在呈现的子菜单中的话,我可以将属性的menuName属性设置为Rendering/Custom Render Pipeline。在属性类型之后,圆括号内直接设置此属性。

使用新菜单项将资产添加到项目中,然后转到Graphics设置并在Scriptable Render Pipeline Settings下选择它。

(选择了自定义RP)

替换默认RP会改变一些事。首先是信息面板中提到了许多图形相关的设置选项。其次因为禁用了默认RP,并且还没有提供有效的替换,因此它不再呈现任何内容。游戏窗口,场景窗口和材质预览都不再起作用。

如果你通过“Window/ Analysis / Frame Debugger”打开调试器并启动的话,你将看到在游戏窗口中确实没有绘制任何内容。

1.3 渲染管线实例

创建CustomRenderPipeline类,并将其脚本文件放入与CustomRenderPipelineAsset相同的文件夹中。这将是我们的资产返回的RP实例所使用的类型,因此它必须从RenderPipeline继承。

RenderPipeline定义了一个受保护的抽象的Render方法,我们必须重写这个方法来创建一个具体的管线。它有两个参数:一个ScriptableRenderContext和一个Camera数组。暂时保持该方法为空。

CustomRenderPipelineAsset .CreatePipeline返回一个__CustomRenderPipeline__ 新实例。他会给我们一个有效的,附带功能的管线实例,尽管它现在还没有提供任何功能。

2 渲染呈现

每一帧Unity都会调用RP实例的Render方法。它传递一个上下文结构,该结构会提供到当前引擎的连接,我们可以使用它来进行渲染。它也需要传递一个相机的数组,因为可以有多个活动相机在当前场景。按照提供的摄像机顺序进行渲染是RP的责任。

2.1 相机渲染

每个相机的渲染都是独立的。因此,与其让__CustomRenderPipeline__ 渲染所有摄像机,倒不如把这个责任转发给一个专门用于渲染单个摄像机的新类。将其命名为CameraRenderer,并给它一个带有上下文和照相机参数的公开的Public方法。为了方便起见,让我们将这些参数存储在字段中。

让CustomRenderPipeline在初始化的时候,创建一个CameraRenderer实例,然后使用它在一个循环中渲染所有相机。

camera renderer 大致相当于通用RP的scriptable renderer。 这种方法能让每个相机在未来更容易支持不同的渲染方法。例如一个渲染第一人称视图,一个渲染三维地图,或前向和延迟渲染的区别。但现在我们会用同样的方式渲染所有的摄像机。

2.2 呈现天空盒

CameraRenderer.Render的工作是绘制相机所能看到的所有几何图形。为了清晰起见,在一个单独的DrawVisibleGeometry方法中隔离这个特定的工作任务。第一步先让它绘制默认的Skybox,这可以通过使用摄像机作为参数在上下文中调用DrawSkybox来完成。

仅仅这样并没有使天空盒渲染出来。这是因为我们向上下文发出的命令都是缓冲的。必须通过在上下文上调用Submit来提交排队的工作才会执行。再写一个单独的Submit方法,该方法在DrawVisibleGeometry学之后调用。

天空盒现在出现在游戏窗口和场景窗口中了。启用提交之后时,还可以在frame debugger中看到它的相关信息。在调试器中它被列为Camera.RenderSkybox条目,它下面有一个Draw Mesh项,表示实际的Draw Call。它是与游戏窗口的呈现相对应(frame debugger不会报告在其他窗口中绘制)。

(画出的天空盒)

注意,相机的方向目前并不会影响天窗盒的渲染方式。虽然已经将相机传递给了DrawSkybox,但这只用于确定是否应该绘制天空盒,这是通过摄像机的clear标志来控制的。

为了正确渲染天空盒以及整个场景,我们必须设置视图投影矩阵。此转换矩阵将摄像机的位置和方向(视图矩阵)与摄像机的透视或正投影(投影矩阵)结合在一起。在着色器中称为unity_MatrixVP,这是绘制几何图形时使用的着色器属性之一。选择一个Draw Call后,可以在帧调试器的ShaderProperties部分中检查此矩阵。

目前来说,unity_MatrixVP矩阵始终相同。我们必须通过SetupCameraProperties方法将摄像机的属性应用于上下文。这会设置矩阵以及其他一些属性。在DrawVisibleGeometry之前,创建单独的Setup方法并调用,需要执行此操作。

(天空盒正确呈现)

2.3 Command Buffers

上下文会延迟实际的渲染,直到我们提交它为止。在此之前,我们对其进行配置并向其添加命令以供后续的执行。某些任务(例如绘制天空盒)提供了专属方法,但其他命令则必须通过单独的命令缓冲区(command buffer)间接执行。我们需要用这样的缓冲区来绘制场景中的其他几何图形。

为了获得缓冲区,我们必须创建一个新的CommandBuffer对象实例。一般只需要一个缓冲区,因此默认情况下为CameraRenderer创建一个缓冲区,并将对它的引用存储在字段中。给缓冲区起一个名字,以便我们在frame debugger中识别它。就叫Render Camera好了。

对象初始化器语法是如何工作的? 这个写法就好像我们已经在调用构造函数之后将Buffer.name=BufferName编写为一个单独的语句。但是,在创建新对象时,可以将代码块附加到构造函数的调用中。然后,可以在块中设置对象的字段和属性,而不必显式引用对象实例。它明确指出,只有在设置了这些字段和属性之后,才应该使用实例。

我们可以使用命令缓冲区注入给Profiler注入样本,这些样本将同时显示在Profiler和帧调试器中。通过在适当的位置插入BeginSample和EndSample就可以完成。在本例中,在Setup和Submit的开头添加。注意两个方法必须提供相同的样本名称,为此我们直接使用缓冲区的名称。

要执行缓冲区,需以缓冲区为参数在上下文上调用ExecuteCommandBuffer。这会从缓冲区复制命令但并不会清除它,如果要重用它的话,就必须在之后明确地执行该操作。因为执行和清除总是一起完成的,所以添加同时执行这两种方法的方法很方便。

现在Camera.RenderSkyBox的样本将会出现在Render Camera下面。

(Render camera 样本)

2.4 清除渲染目标

无论我们画了什么,最终都会被渲染到摄像机的渲染目标上,默认情况下,是帧缓冲区,但也可能是渲染纹理。但是所有之前已经画过的东西仍然存在,这可能会干扰现在渲染的图像。为了保证正确的渲染,我们必须清除渲染目标,以消除其旧的内容。通过调用命令缓冲区上的ClearRenderTarget来完成的,它应该属于Setup方法。

ClearRenderTarget至少需要三个参数。前两个指示是否应该清除深度和颜色数据,这对两者都应该是true。第三个参数是用于清除的颜色,我们将对其使用Color.clear。

(清除 样本)

帧调试器现在显示一个绘制了GL条目,用于清除操作,它显示嵌套在另一级别的Render Camera中。这是因为ClearRenderTarget用命令缓冲区的名称将清除封装在一个同一个样本中。我们在开始自己的样本之前清除多余的嵌套。这会导致两个相邻的渲染相机示例范围被合并。

(正确的 clearing)

现在我们看到清除(颜色 Z 模板),这表明颜色和深度缓冲区都被清除。z表示深度缓冲区,模板数据是同一缓冲区的一部分。

2.5 剔除

我们现在看到的是天空盒,但没有看到我们在场景中放置的任何其他物体。并不是需要把每一个物体都画出来,我们只会渲染那些相机能看见的物体。所以从场景中所有有renderer组件的物体开始,然后剔除掉那些落在摄像机视野以外的物体。

找出什么可以被剔除需要我们跟踪多个相机设置和矩阵,可以使用ScriptableCullingParameters结构。这个结构可以在摄像机上调用TryGetCullingParameters,而不是自己去填充它。它返回是否可以成功检索该参数,因为它可能会获取失败。要获得参数数据,我们必须将其作为输出(out)参数提供,方法是在它前面写一个out。在返回成功或失败的单独的Cull方法中执行此操作。

为什么要写out? 当struct参数被定义为输出参数时,它的作用就像一个对象引用,指向参数所在的内存堆栈上的位置。

Out关键字告诉我们,该方法负责正确设置参数,替换以前的值。 Try-get方法是表示成功或失败并产生结果的常见方法。

当用作输出参数时,可以在参数列表中内联变量声明,看看写法。

在“Render”中的“Setup”之前调用Cull,如果失败则中止。

实际的裁剪是通过调用上下文上的Cull来完成的,这会产生一个CullingResults结构。如果成功的话,可以在清除中执行此操作,并将结果存储在字段中。在这种情况下,我们必须将剔除参数作为引用参数传递,方法是在前面写ref。

为什么需要用ref? ref关键字的工作方式与out一样,只不过该方法不需要为其分配新的东西。调用该方法的人首先要负责正确初始化该值。因此,它可以用于输入,也可以选择用于输出。 在本例中,ref用作优化项,以防止传递ScriptableCullingParameters结构的副本,因为该结构相当大。

2.6 画几何

一旦我们知道什么是可见的,我们就可以继续渲染它们。这是通过调用上下文中的DrawRenderers作为参数来实现的,并告诉它要使用哪个renderers 。此外,我们还必须提供绘图设置和筛选设置。这两种都是结构体DrawingSettings和FilteringSettings 我们将首先使用它们的默认构造函数。两者都必须以引用的方式传递。在绘制天空盒之前,调用DrawVisibleGeometry。

我们还是没有看到任何东西,因为我们还必须指出使用哪种阴影pass。因为在本教程中我们只支持unlit 的着色器,所以我们必须获取SRPDefaultUnlitPass的着色器标签ID,可以新建一次,并将它缓存在一个静态字段中。

提供它作为DrawinSettings构造函数的第一个参数,以及一个新的SortingSettings结构值。将相机传递给SortingSettings的构造函数,它用于确定基于正焦还是基于透视的应用排序。

此外,还必须指出哪些 render 队列是允许的。将RenderQueueRange.all传递给FilteringSettings构造函数,这样就能包含所有内容。

(绘制不受光照的几何图形)

只绘制使用不受光着色器的可见对象。所有的Draw Call都列在帧调试器中,RenderLoop.Draw分组之下。透明对象显然有一些奇怪,我们可以先看看对象的绘制顺序。帧调试器会显示这个顺序,你只要逐个选择或使用箭头键来查看DrawCall就可以了。

绘制顺序是杂乱无章的。我们可以通过设置排序设置的条件属性来强制特定的绘制顺序。用SortingCriteria.CommonOpaque试试。

(不透明物体的排序)

对象现在按照前后顺序进行绘制,但这只是理想的不透明的对象.如果某物最终被画出来的时候,在其他东西后面,则可以跳过隐藏的片段,从而加快渲染速度。常见的不透明排序选项还需要考虑了其他一些标准,包括渲染队列和材质。

2.7 分开绘制不透明和透明物体

帧调试器向我们展示透明对象会被绘制,但是Skybox会被绘制到不透明对象前面的所有东西前面。如果让Skybox在不透明几何图形之后绘制,就可以跳过所有隐藏的片段,但是它又会覆盖透明的几何图形。这是因为透明着色器不会写入深度缓冲区。他们不会隐藏他们身后的任何东西,因为我们需要看穿它们。解决方案是首先绘制不透明对象,然后是Skybox,然后才是透明对象。

我们可以将透明对象从最初的DrawRenderers调用中删除,方法是切换到RenderQueueRange.opaque。

然后,在绘制Skybox之后,再次调用DrawRenderers。但在此之前,需要将渲染队列范围更改为RenderQueueRange.transparent。还将排序条件更改为 SortingCriteria.CommonTransparent,并再次设置绘图设置的排序。这将倒置透明对象的绘制顺序。

(不透明物体,天空盒,透明物体)

为什么Drawcall顺序倒置了? 由于透明对象不写入深度缓冲区,因此对它们进行前后排序没有任何性能上的好处。但是,当透明的物体在视觉上互相影响时,它们必须被画成正面,才能正确地融合在一起。

3 编辑器渲染

自定义的RP正确地绘制了Unlit对象,但我们还可以做一些事情来改进在Unity编辑器中的使用。

3.1 使用旧的Shaders

因为我们的管线只支持Unlit的着色器通过,所以使用不同Pass渲染的对象便不会呈现,因此它们是不可见的。虽然结果正确的,但它也帮助隐瞒了问题,如果物体在场景里使用错误的着色器的话。我们应该把它们呈现出来,但要分开处理。

假如有人开始一个默认的Unity项目,然后切换到我们的RP,那么他们可能有对象就使用了“错误”的着色器。要覆盖所有的Unity默认着色器,我们必须让带有着色器标签ID的Always,ForwardBase,PrepassBase,Vertex,VertexLMRGBM,和VertexLM通过。可以在一个静态数组中跟踪这些数据。

在可见的几何绘制之后,在一个单独的方法中绘制所有不受支持的着色器,从第一个通道开始。由于这些是无效通道,结果无论如何都是错误的,所以我们不用关心其他设置。可以通过FilteringSettings.defaultValue属性获得默认筛选设置。

我们可以通过调用drawing settings上的SetShaderPassName来绘制多个通道,并使用一个绘制顺序索引和标记作为参数。对数组中的所有通道执行此操作,要从第二次开始,因为我们在构造绘图设置时已经设置了第一次通道。

(标准着色器渲染为黑色)

用标准着色器渲染的对象就会显示出来了,但它们现在是纯黑的,因为我们的RP还没有为它们设置所需的着色属性。

3.2 错误的材质

为了清楚地指出哪些对象使用了不支持的着色器,我们将使用UnityError着色器绘制它们。用这个着色器作为参数构造一个新材质,我们可以通过调用Shader.Find找到一个带有Hidden/InternalErrorShader字符串作为参数的材质。通过静态字段缓存材质,这样我们就不会每帧创建一个新的了。然后将其分配给绘图设置的overrideMaterial属性

(错误的shader 用洋红色渲染)

现在所有不支持的物体都可见,并且展示为错误的了。

3.3 局部类

绘制无效的对象对于开发是有用的,但并不适用于发布的应用程序。因此,我们将CameraRenderer的所有的仅编辑器使用的代码放在一个单独的部分类文件中。首先复制原始CameraRenderer脚本资产并将其重命名为CameraRenderer.Editor。

(一个类有2个资产文件)

然后将原始CameraRenderer转换为一个局部类类,并从其中移除标记数组、错误材料和DrawUnSupporttedShaders方法。

什么是局部类? 这是一种将类或结构定义拆分为多个部分的方法,分别存储在不同的文件中,它唯一的目的就是组织代码。典型的用例是将自动生成的代码与手工编写的代码分开。就编译器而言,它都是同一个类定义的一部分。它们在 对象管理更复杂的关卡 教程中引入的。

清理另一个局部类文件,以便它只包含我们从另一个类中删除的内容。

编辑器部分的内容只需要存在于编辑器中,因此以UnityEditor为条件。

但是,此时进行构建将失败,因为另一部分总是包含对DrawUnsupportedShaders的调用,该调用现在只应该存在于编辑器中。为了解决这一问题,对该方法也进行局部定义。为此,我们总是在方法的前面声明部分加partial,类似于抽象的方法声明。其实可以在类定义的任何部分这样做,所以让我们把它放在编辑器部分。完整的方法声明也必须标记为partial。

构建的编译现在可以成功了。编译器将剔除所有未以完整声明结束的分部方法的调用。

我们能让无效的对象出现在development 构建中吗? 当然,你可以将条件编译建立在 UNITY_EDITOR||DEVELOPMENT_BUILD基础上。那么DrawUnsupportedShaders也存在于development 构建中,不存在于release 版构建中。但本系列教程会始终限制与编辑器相关的所有开发。

3.4 绘制Gizmos

目前,我们的RP没有绘制Gizmos,无论是在场景窗口或是游戏窗口都没有。

(场景没有gizmos)

我们可以通过调用UnityEditor.Handles.ShouldRenderGizmos来检查是否应该绘制gizmos。如果是这样的话,就必须在上下文中调用DrawGizmos作为参数,再加上第二个参数来指示应该绘制哪个gizmo子集。有两个子集,用于图像效果的前和后。由于此时我们不支持图像效果,所以我们将同时调用这两种效果。在一个只使用DrawGizmos编辑器的新方法中写逻辑。

这些gizmos应该在所有其他东西之后画出来。

(场景带有gizmos)

3.5 绘制Unity UI

另一个需要我们关注的事情是Unity的游戏中的用户界面。例如,通过GameObject/UI/Button添加一个按钮来创建一个简单的UI。它会出现在游戏窗口中,但不会出现在场景窗口中。

(game 窗口下的UI按钮)

帧调试器向我们显示UI是单独呈现的,而不是由RP呈现的。

(帧调试器上的UI)

当Canvas组件的Render Mode 被设置为Screen Space - Overlay就是这种情况,这是默认的。改成Screen Space - Camera和使用主相机作为其渲染相机将使其成为透明几何的一部分。

UI在场景窗口中呈现时总是使用World Space模式,这就是为什么它通常会变得非常大的原因。但是,尽管我们可以通过场景窗口编辑UI,但它并不会被绘制。

(Scene窗口下 UI不可见)

在渲染场景窗口时,我们必须显式地将UI添加到世界几何中,方法是以相机作为参数调用ScriptableRenderContext.EmitWorldGeometryForSceneView。在新的只在编辑器下运行的PrepareForSceneWindow方法中调用。当它的CameraType属性等于CameraTypes.SceneView时,我们便能使用场景摄像机渲染。

因为这可能会给场景添加几何体,所以必须在裁剪之前完成。

(UI在场景窗口上可见)

4 多摄像机

场景上有可能同时存在多个激活的摄像机,我们需要保证它们之间都能正常渲染。

4.1 两个摄像机

每个摄像机都有一个深度值,默认主摄像机的深度值为−1。它们是按深度递增的顺序渲染的。要验证这个的话,可以复制主摄像机,将其重命名为Secondary Camera,并将其深度设置为0。最好给它设置另外一个tag,因为MainCamera标签应该只标记一台相机。

(两个相机都集中在一个样本范围内)

这个场景现在被渲染了两次。而生成的图像仍然是相同的,因为渲染的目标在中间被清除过。帧调试器显示了这个记录,但是由于具有相同名称的相邻Sample作用域会被合并,所以我们最终只使用一个Render Camera作用域。

如果每个相机都有自己的镜头,那就更清楚了。为此,添加一个仅编辑器能用的PrepareBuffer方法,使缓冲区的名称与摄像机的名称相等。

在准备场景窗口之前调用它。

(分离每个摄像机的样本)

4.2 处理更改的缓冲区名称

虽然帧调试器现在显示了每个摄像机的一个单独的样本层次结构,但当我们进入Play模式时,Unity的控制台将收到警告,BeginSample和EndSample计数必须匹配的消息。它被混淆弄糊涂了,因为我们对样本和它们的缓冲区之间使用了不同的名称。此外,每次访问相机的Name属性时,都会分配内存,这样会造成性能问题。

为了解决这两个问题,需要添加一个SampleName字符串属性。如果在编辑器中,就在PrepareBuffer中设置它以及缓冲区的名称,否则它只是赋值给相机字符串的常量别名。

在Setup和Submit中对样本使用SampleName。

我们可以通过分析器(Window / Analysis / Profiler)和首先在编辑器中播放来看出差异。切换到层次结构模式,并按GC ALLOC列进行排序。你会看到GC.alloc两次调用的那一个条目,总共分配100个字节,这是通过检索摄像机名称造成的。再往下看,你会看到这些名字显示为样本:Main Camera和Second Camera。

(分析器里不同的采样和100 B的内存分配)

接下来,在启用了“Development Build”和“Autoconnect Profiler”的情况下进行构建。运行并确保分析器已连接并开始录制。在这种情况下,我们没有看到100字节的分配,得到的是单一渲染相机样本。

(分析构建)

另外48个字节的分配是干什么的? 是我们无法控制的摄像机数组。它的大小取决于有多少摄像机被渲染。 通过将相机名称包装在一个名为Editor的分析器示例中,可以让示例只在编辑器中分配了内存,而不在在构建后分配内存。在本例中,我们需要从UnityEngine.Profiling命名空间调用Profiler.BeginSample和Profiler.EndSample。只有BeginSample需要传递名称。

(只在编辑器下才有的内存分配)

4.3 Layers

通过调整他们的Culling Mask,相机也可以配置成只能看到某些层上的东西。想要实验这点的话,可以将使用标准着色器的所有对象移动到Ignore Raycast层。

(把层切换到 Ignore Raycast)

将这一层排除在主摄像机的culling mask之外。

(剔除Ignore Raycast)

然后让二号位摄像机只看这个层。

(剔除除了该层之外的其他内容)

因为第二摄像机是后渲染的,所以我们暂时只能看到无效的对象。

(game窗口下只能看到无效物体)

4.4 清除标志

通过调整第二个摄像机的清除标志,我们可以结合两个相机的渲染结果。它们是由 CameraClearFlags 枚举定义的,通过相机的“clearFlags”属性来获取和定义它。在Setup函数里,清除渲染目标前执行。

CameraClearFlag枚举定义了四个值。从1到4,它们是Skybox,Color,Depth和Nothing。它实际上不是一个独立的标志值,但表示清除量递减。除最后一种情况外,其他情况都必须清除深度缓冲区,因此,标志值最多的设置是Depth。

当标志设置为Color时,我们只需要清除颜色缓冲区,因为本例中,有Skybox的情况下,无论如何,最终都会替换所有之前的颜色数据。

如果我们要清除一个不透明的颜色,就要使用到相机的背景色。但是因为我们是在线性颜色空间中绘制,所以我们必须把颜色转换到线性空间,所以我们最终需要camera.backgroundColor.linear。在所有其他情况下,颜色都不重要,所以使用Color.clear就足够了。

因为主摄像机是第一个渲染的,它的Clear Flags 应该设置为Skybox或Color。当启用帧调试器的时候,一般是从一个Clear的缓冲区开始,但并不是绝对的。

二号位摄像机的Clear Flags将决定如何组合两个摄像机的渲染。对于skybox 或color 而言,之前的结果将完全替换。如果仅清除深度,则二号位摄影机将正常渲染,但不会绘制天空盒,因此之前的结果会显示为背景。当标志是nothing的时候,深度缓冲区将保留,因此unlit的对象最终将遮挡无效对象,就像它们是由同一台摄像机绘制的一样。但是,前一台摄像机绘制的透明对象因为没有深度信息,因此会像天空盒之前所展示的那样被绘制。

(Clear为 color, depth-only, 和 nothing)

通过调整摄像机的视口,还可以将渲染区域缩小到整个渲染目标的一小部分。呈现目标的其余部分不受影响。在这种情况下,清除发生在Hidden/InternalClear着色器。模板缓冲区用于将渲染限制在视口区域。

请注意,每帧渲染一个以上的相机意味着裁剪、设置、排序等也必须多次完成。一般高效的做法是,只为每个独特的视角分配一台相机。

(减少第二摄像机的视口,clearing color)

下一部分是 Draw Calls。

欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。

本文翻译自 Jasper Flick的系列教程

原文地址:

https://catlikecoding.com/unity/tutorials

0 人点赞