提示:学习内容来自以下文献
catlikecoding.com
提示:以下是本篇文章正文内容,下面案例可供参考
在 Unity 2019.2.6或更高版本中创建一个新的3 D 项目,我们将创建自己的管道,所以不要选择一个 URP 项目模板。一旦项目打开,您可以转到包管理器并删除所有您不需要的包。我们将只使用本教程中的 Unity UI 包来尝试绘制 UI,因此您可以保留这个 UI。
我们将只使用线性色彩空间,但 Unity 2019.2仍然使用伽玛空间作为默认值。转到播放器设置通过编辑/项目设置,然后播放器,然后切换到其他设置部分的色彩空间线性:
用一些物体填充默认场景,混合使用标准的、不透明的和透明的材质。非照明/透明着色器只对纹理起作用,所以这里有一个 UV 球体贴图:
我在我的测试场景中放了一些立方体,它们都是不透明的。红色的使用Standard材质,而绿色和黄色的使用非着色/颜色着色器材质。蓝色球体使用渲染模式设置为透明的标准着色器,而白色球体使用非亮/透明着色器
目前,Unity 使用默认的渲染管道。要用自定义渲染管道替换它,我们首先必须为它创建一个资产类型。我们将使用大致相同的文件夹结构,目前Unity 使用的通用 RP。使用 Runtime 子文件夹创建自定义 RP 资产文件夹,将 CustomRenderPipelineAsset 放入Runtime 子文件夹。
资产类型必须从 RenderPipelineAsset 命名空间扩展 继承,UnityEngine.Rendering
using UnityEngine; using UnityEngine.Rendering; public class CustomRenderPipelineAsset : RenderPipelineAsset {}
RP 资产的主要目的是为 Unity 提供一种获取负责呈现的管道对象实例的方法。资产本身只是一个句柄和存储设置的位置。我们还没有任何设置,所以我们所要做的就是给 Unity 一个获得管道对象实例的方法。这是通过重写抽象 CreatePipeline 方法实现的,该方法应该返回一个 RenderPipeline 实例。但是我们还没有定义一个自定义的 RP 类型,所以从返回 null 开始。
CreatePipeline 方法使用 protected 访问修饰符定义,这意味着只有定义该方法的类ー RenderPipelineAssetー和扩展该方法的类可以访问该方法。
protected override RenderPipeline CreatePipeline () { return null; }
现在,我们需要向项目中添加这种类型的资产。为了实现这一点,向 CustomRenderPipelineAsset 添加 CreateAssetMenu 属性:
[CreateAssetMenu] public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
这将在资产/创建菜单中放置一个条目。让我们整理一下,把它放在渲染子菜单中。为此,我们将属性的 menuName 属性设置为 Rendering/Custom Render Pipeline。此属性可以在属性类型之后直接设置,在圆括号中
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")] public class CustomRenderPipelineAsset : RenderPipelineAsset { … }
使用新菜单项将资产添加到项目中,然后转到 Graphics 项目设置,并在“脚本渲染管道设置”下选择它:
替换默认的 RP 改变了一些东西。首先,很多选项已经从图形设置中消失,这在信息面板中提到。其次,我们已经禁用了默认的 RP,但是没有提供有效的替换,所以什么都不会被渲染。游戏窗口、场景窗口和材质预览不再有效。如果你通过 Window/Analysis/Frame debugger 打开这个框架调试器并启用它,你会发现在游戏窗口中确实没有绘制任何内容:
创建一个 CustomRenderPipeline 类,并将其脚本文件放在与 CustomRenderPipelineAsset 相同的文件夹中。这将是我们的资产返回的 RP 实例所使用的类型,因此它必须扩展 RenderPipeline
using UnityEngine; using UnityEngine.Rendering; public class CustomRenderPipeline : RenderPipeline {}
RenderPipeline 定义了一个受保护的抽象 Render 方法,我们必须重写这个方法来创建一个具体的管道。它有两个参数: 一个 ScriptableRenderContext和一个 Camera 数组。现在让这个方法为空
protected override void Render ( ScriptableRenderContext context, Camera[] cameras ) {}
创建 CustomRenderPipelineAsset。CreatePipeline 返回一个新的 CustomRenderPipeline 实例。这将为我们提供一个有效的、可用的管道,尽管它还不能呈现任何东西
protected override RenderPipeline CreatePipeline () { return new CustomRenderPipeline(); }
每一帧Unity调用渲染的RP实例。它传递一个上下文结构,提供到本地引擎的连接,我们可以使用它进行渲染。它还会传递一个摄像机阵列,因为场景中可能有多个活动摄像机。它是RP的责任渲染所有这些相机的顺序,他们提供。
每个相机都是独立渲染的。因此,与其让 CustomRenderPipeline 渲染所有的相机,我们不如把这个责任转移到一个新的类来,用来专门渲染的相机。将其命名为 CameraRenderer,并给它一个带有上下文和相机参数的公共 Render 方法。为了方便起见,我们将这些参数存储在字段中,方便后续使用
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 在创建渲染器时创建一个实例,然后使用它在一个循环中渲染所有相机
CameraRenderer renderer = new CameraRenderer(); protected override void Render ( ScriptableRenderContext context, Camera[] cameras ) { foreach (Camera camera in cameras) { renderer.Render(context, camera); } }
我们的相机渲染器大致相当于UniversalRP 的脚本渲染器。这种方法将使未来每个相机支持不同的渲染方法变得简单,例如第一人称视图和3d 地图叠加,或者向前与延迟渲染。但是现在我们将以同样的方式渲染所有的摄像机
CameraRenderer.Render是绘制所有的几何图形。为了清晰起见,将特定任务隔离到单独的 DrawVisibleGeometry 方法中。我们首先让它绘制默认的 skybox,这可以通过在上下文中调用 DrawSkybox 和 camera 作为参数来完成
public void Render (ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera; DrawVisibleGeometry(); } void DrawVisibleGeometry () { context.DrawSkybox(camera); }
这还不能使 skybox 显示出来。这是因为我们向上下文发出的命令是缓冲的。我们必须通过调用上下文上的 Submit 来提交排队等待执行的工作。让我们在一个单独的 Submit 方法中完成这项工作,该方法在 DrawVisibleGeometry 之后调用
public void Render (ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera; DrawVisibleGeometry(); Submit(); } void Submit () { context.Submit(); }
天空盒最终出现在游戏和场景窗口中。当您启用它时,您还可以在框架调试器中看到它的条目。它的名字是 Camera.RenderSkybox,下面有一个 Draw Mesh 项,它表示实际的 Draw 调用。这对应于游戏窗口的渲染。帧调试器不报告其他窗口中的绘图
注意,相机目前的方向并不影响 skybox 渲染的方式。我们把相机传给 DrawSkybox,但这只是用来决定天空盒是否应该绘制,这是通过相机的Clear Flags来控制的.
为了正确地渲染天空盒和整个场景,我们必须建立视图投影矩阵。这个变换矩阵结合了 相机的位置和方向---- 视图矩阵---- 和相机的透视或正投影---- 投影矩阵。它在着色器中被称为 unity _ matrixvp,这是绘制几何图形时使用的着色器属性之一。当选择绘图调用时,可以在框架调试器的 ShaderProperties 部分中查看此矩阵
目前,unity _ matrixvp 矩阵总是相同的。我们必须通过 SetupCameraProperties 方法将相机的属性应用到上下文中。它设置了矩阵以及其他一些属性。在单独的 Setup 方法中调用 ,在DrawVisibleGeometry 之前进行此操作
public void Render (ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera; Setup(); DrawVisibleGeometry(); Submit(); } void Setup () { context.SetupCameraProperties(camera); }
上下文延迟了实际的呈现,直到我们提交它。在此之前,我们对其进行配置,并向其中添加命令,以便以后执行。有些任务(比如绘制 skybox)可以通过专用方法发出,但其他命令必须通过单独的命令缓冲区间接发出。我们需要这样一个缓冲区来绘制场景中的其他几何图形。
要获得缓冲区,我们必须创建一个新的 CommandBuffer 对象实例。我们只需要一个缓冲区,因此默认情况下为 CameraRenderer 创建一个并在字段中存储对它的引用。还要给缓冲区一个名称,这样我们就可以在框架调试器中识别它。渲染相机就可以了。
const string bufferName = "Render Camera"; CommandBuffer buffer = new CommandBuffer { name = bufferName };
对象初始化器语法是如何工作的?
这个写法就好像我们已经在调用构造函数之后将Buffer.name=BufferName编写为一个单独的语句。但是,在创建新对象时,可以将代码块附加到构造函数的调用中。然后,可以在块中设置对象的字段和属性,而不必显式引用对象实例。它明确指出,只有在设置了这些字段和属性之后,才应该使用实例。
我们可以使用命令缓冲区来注入 profiler 样本,这些样本将同时显示在 profiler 和frame debugger中。这是通过在适当的点调用BeginSample和EndSample来完成的,在我们的例子中,这是在Setup和Submit的开始。必须为这两个方法提供相同的示例名称,我们将使用缓冲区的名称。
void Setup () { buffer.BeginSample(bufferName); context.SetupCameraProperties(camera); } void Submit () { buffer.EndSample(bufferName); context.Submit(); }
要执行缓冲区,请使用缓冲区作为参数在上下文中调用ExecuteCommandBuffer。这将从缓冲区中复制命令,但不清除它,如果我们想重用它,我们必须在之后显式地这样做。因为执行和清理总是一起完成的,所以添加一个同时做这两件事的方法很方便:
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 样本被嵌套在 Render Camera 中:
无论我们绘制什么,最终都会渲染到摄像机的渲染目标,这是默认的帧缓冲,但也可以是渲染纹理。早些时候被吸引到目标的东西仍然在那里,这可能会干扰我们现在渲染的图像。为了保证正确的渲染,我们必须清除渲染目标以去除它的旧内容。这是通过在命令缓冲区上调用ClearRenderTarget来完成的,该命令缓冲区属于Setup方法。
CommandBuffer.ClearRenderTarget至少需要三个参数。前两个指示是否应该清除深度和颜色数据,这对两者都是正确的。第三个参数是用于清除的颜色,对此我们将使用color .clear。
void Setup () { buffer.BeginSample(bufferName); buffer.ClearRenderTarget(true, true, Color.clear); ExecuteBuffer(); context.SetupCameraProperties(camera); }
帧调试器现在显示了一个Draw GL条目用于清除动作,它嵌套在渲染相机的额外级别中。发生这种情况是因为ClearRenderTarget用命令缓冲区的名称包装了示例中的清除。在开始我们自己的示例之前,我们可以通过清理来消除冗余嵌套。这导致了两个相邻的渲染相机样本范围,它们被合并:
void Setup () { buffer.ClearRenderTarget(true, true, Color.clear); buffer.BeginSample(bufferName); //buffer.ClearRenderTarget(true, true, Color.clear); ExecuteBuffer(); context.SetupCameraProperties(camera); }
Draw GL条目表示使用Hidden/InternalClear着色器绘制一个全屏四边形,该着色器写入渲染目标,这不是清除它的最有效方式。使用这种方法是因为我们要在设置相机属性之前进行清理。如果我们交换这两个步骤的顺序,我们就得到了快速清除的方法。
void Setup () { context.SetupCameraProperties(camera); buffer.ClearRenderTarget(true, true, Color.clear); buffer.BeginSample(bufferName); ExecuteBuffer(); //context.SetupCameraProperties(camera); }
现在我们看到Clear (color+Z+stencil),这表明颜色缓冲区和深度缓冲区都被清除了。Z表示深度缓冲区,模板数据是相同缓冲区的一部分。
我们现在看到的是天空盒,不是我们放入场景中的任何物体。不会绘制每个对象,我们将只渲染那些对相机可见的。我们从场景中所有带有渲染器组件的对象开始,然后剔除那些落在摄像机视图视锥外的对象。
弄清楚哪些可以剔除需要我们跟踪多个摄像机设置和矩阵,我们可以使用ScriptableCullingParameters结构。我们可以在摄像机上调用TryGetCullingParameters,而不是自己填充它。它返回参数是否可以成功检索,因为它可能会失败的退化相机设置。为了获得参数数据,我们必须把它作为输出参数提供,在前面写出来。在一个单独的Cull方法中执行此操作,该方法返回成功或失败。
bool Cull () { ScriptableCullingParameters p if (camera.TryGetCullingParameters(out p)) { return true; } return false; }
为什么要写out?
当struct参数被定义为输出参数时,它的作用就像一个对象引用,指向参数所在的内存堆栈上的位置
当作为输出参数使用时,可以在参数列表中内联变量声明,所以让我们这样做:
bool Cull () { //ScriptableCullingParameters p if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) { return true; } return false; }
在渲染中的Setup之前调用 Cull,如果失败则中止:
public void Render (ScriptableRenderContext context, Camera camera) { this.context = context; this.camera = camera; if (!Cull()) { return; } Setup(); DrawVisibleGeometry(); Submit(); }
实际的剔除是通过在上下文中调用Cull来完成的,它会产生一个CullingResults结构体。如果成功,在Cull中这样做,并将结果存储在一个字段中。在这种情况下,我们必须将剔除参数作为引用参数传递,方法是在参数前面写入ref:
CullingResults cullingResults; … bool Cull () { if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) { cullingResults = context.Cull(ref p); return true; } return false; }
为什么需要用ref?
ref关键字的工作方式与out一样,只不过该方法不需要为其分配新的东西。调用该方法的人首先要负责正确初始化该值。因此,它可以用于输入,也可以选择用于输出。
在本例中,ref用作优化项,以防止传递ScriptableCullingParameters结构的副本,因为该结构相当大。
一旦我们知道什么是可见的,我们就可以继续渲染这些东西。这是通过在上下文中调用DrawRenderers,将筛选结果作为参数,告诉它使用哪个渲染器来完成的。除此之外,我们还必须提供 绘图设置 和 过滤设置 。这两个都是结构——DrawingSettings and FilteringSettings——我们将首先使用它们的默认构造函数。两者都必须通过引用传递。在绘制天空盒之前,在DrawVisibleGeometry中这样做:
void DrawVisibleGeometry () { var drawingSettings = new DrawingSettings(); var filteringSettings = new FilteringSettings(); context.DrawRenderers( cullingResults, ref drawingSettings, ref filteringSettings ); context.DrawSkybox(camera); }
我们还没有看到任何东西,因为我们还必须指出哪种着色器通道是允许的。由于我们在本教程中只支持unlit着色器,我们必须为SRPDefaultUnlit通道获取着色器标签ID,我们可以这样做一次,并将其缓存到一个静态字段中:
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
提供它作为 DrawingSettings 构造函数的第一个参数,以及一个新的 SortingSettings 结构值。将相机传递给排序设置的构造函数,因为它用于确定应用正投影法还是基于距离的排序:
void DrawVisibleGeometry () { var sortingSettings = new SortingSettings(camera); var drawingSettings = new DrawingSettings( unlitShaderTagId, sortingSettings ); … }
此外,我们还必须指出允许哪些渲染队列。将 RenderQueueRange.all 传递给 FilteringSettings 构造函数,以便包含所有内容:
var filteringSettings = new FilteringSettings(RenderQueueRange.all);
只有使用unlit着色器的可见物体才会被绘制。所有的绘制调用都列在帧调试器中,分组在RenderLoop.Draw下。透明对象有些奇怪,但是让我们先看看对象绘制的顺序。这是由帧调试器显示的,你可以通过选择一个接一个的绘制调用或使用箭头键:
单步执行框架调试器视频
绘图顺序杂乱无章。我们可以通过设置排序设置的 criteria 属性来强制一个特定的绘制顺序。让我们使用SortingCriteria.CommonOpaque
var sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonOpaque };
单步执行框架调试器视频
对象现在或多或少地得到从前到后的绘制,这对于不透明的对象是理想的。如果某样东西最终被绘制在其他东西后面,它隐藏的片段可以被跳过,这将加快渲染速度。常用的不透明排序选项还考虑了其他一些标准,包括渲染队列和材质。
帧调试器告诉我们透明对象会被绘制,但是天空盒会被绘制在所有不透明对象前面的东西上。天空盒被绘制在不透明几何体之后,所以它所有的隐藏碎片可以被跳过,但它覆盖了透明几何体。这是因为透明着色器不写入深度缓冲区。他们不会隐藏他们背后的东西,因为我们可以看穿他们。解决方法是先绘制不透明的物体,然后是天空盒,然后是透明的物体。
通过切换到RenderQueueRange.opaque,我们可以从最初的DrawRenderers调用中消除透明对象:
var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
然后在绘制天空盒之后再次调用DrawRenderers。但在此之前,将渲染队列范围更改为RenderQueueRange.transparent。还要将排序标准更改为SortingCriteria。CommonTransparent和再次设置排序的绘图设置。这就颠倒了透明对象的绘制顺序:
context.DrawSkybox(camera); sortingSettings.criteria = SortingCriteria.CommonTransparent; drawingSettings.sortingSettings = sortingSettings; filteringSettings.renderQueueRange = RenderQueueRange.transparent; context.DrawRenderers( cullingResults, ref drawingSettings, ref filteringSettings );
单步执行框架调试器视频
为什么Drawcall顺序倒置了?
由于透明对象不写入深度缓冲区,因此对它们进行前后排序没有任何性能上的好处。但是,当透明的物体在视觉上互相影响时,它们必须被画成正面,才能正确地融合在一起。
我们的RP能够正确地绘制unlit的物体,但是我们可以做一些事情去改善在Unity编辑器中使用它的体验.
因为我们的管道只支持unlit着色器通道,使用不同通道的对象不被渲染,使它们不可见。虽然这是正确的,但它隐藏了场景中的一些对象使用了错误的着色器的事实。让我们分别渲染它们。
如果有人从一个默认的Unity项目开始,然后切换到我们的RP,那么他们可能会在场景中使用错误的着色器。为了覆盖所有Unity的默认着色器,我们必须为Always, ForwardBase, PrepassBase, Vertex, VertexLMRGBM和VertexLM通道使用着色器标签id。在静态数组中跟踪这些:
static ShaderTagId[] legacyShaderTagIds = { new ShaderTagId("Always"), new ShaderTagId("ForwardBase"), new ShaderTagId("PrepassBase"), new ShaderTagId("Vertex"), new ShaderTagId("VertexLMRGBM"), new ShaderTagId("VertexLM") };
绘制所有不支持的着色器在一个单独的方法,在DrawVisibleGeometry之后,因为这些是无效的传递,结果将是错误的,所以我们不关心其他设置。我们可以通过FilteringSettings.defaultValue属性获得默认的过滤设置:
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 ); }
我们可以通过在绘制设置上调用SetShaderPassName,并以绘制顺序索引和标记作为参数来绘制多个通道。对数组中的所有通道都这样做,从第二个开始,因为我们已经在构造绘图设置时设置了第一个通道:
var drawingSettings = new DrawingSettings( legacyShaderTagIds[0], new SortingSettings(camera) ); for (int i = 1; i < legacyShaderTagIds.Length; i++) { drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]); }
使用标准着色器渲染的对象会显示出来,但是它们现在是纯黑色的,因为我们的 RP 没有为它们设置所需的着色器属性。
为了明确指出哪些对象使用了不支持的着色器,我们将使用Unity的错误着色器绘制它们。用那个着色器作为参数构造一个新的材质,我们可以通过调用着色器来找到它。使用Hidden/InternalErrorShader字符串作为参数进行查找。通过一个静态字段缓存材料,这样我们就不会在每一帧创建一个新的。然后将它分配给绘图设置的overrides属性:
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 }; … }
现在,所有无效对象都是可见的,而且明显是错误的。
绘制无效对象对开发很有用,但对已发布的应用程序并不适用。因此,让我们将CameraRenderer的所有仅编辑器代码放在一个单独的partial类文件中。首先复制原始的CameraRenderer脚本资源,并将其重命名为CameraRenderer.Editor
然后将原始的 CameraRenderer 变成一个部分类,并从中删除标记数组、错误材质和 DrawUnsupportedShaders 方法
public partial class CameraRenderer { … }
什么是局部类?
这是一种将类或结构定义拆分为多个部分的方法,分别存储在不同的文件中,它唯一的目的就是组织代码。典型的用例是将自动生成的代码与手工编写的代码分开。就编译器而言,它都是同一个类定义的一部分。
清理另一个分部类文件,使其仅包含我们从另一个分部类文件中删除的内容
using UnityEngine; using UnityEngine.Rendering; partial class CameraRenderer { static ShaderTagId[] legacyShaderTagIds = { … }; static Material errorMaterial; void DrawUnsupportedShaders () { … } }
编辑器部分的内容只需要存在于编辑器中,所以要使它在 UNITY_EDITOR 上有条件:
partial class CameraRenderer { #if UNITY_EDITOR static ShaderTagId[] legacyShaderTagIds = { … } }; static Material errorMaterial; void DrawUnsupportedShaders () { … } #endif }
然而,在这一点上构建将会失败,因为另一部分总是包含对DrawUnsupportedShaders的调用,它现在只存在于编辑器中。为了解决这个问题,我们让这个方法也是偏的。为此,我们总是在方法签名前面声明partial,类似于抽象方法声明。我们可以在类定义的任何部分这样做,所以让我们把它放在编辑器部分。完整的方法声明也必须用partial标记:
partial void DrawUnsupportedShaders (); #if UNITY_EDITOR … partial void DrawUnsupportedShaders () { … } #endif
编译构建现在成功了。编译器将删除所有没有以完整声明结束的部分方法的调用。
目前我们的RP不能绘制gizmos,无论是在场景窗口还是在游戏窗口,如果它们被启用的话:
我们可以通过调用UnityEditor.Handles.ShouldRenderGizmos来检查gizmos是否应该被绘制。如果是这样,我们必须在上下文中使用摄像机作为参数调用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 }
这个小玩意儿应该排在其他所有东西之后:
public void Render (ScriptableRenderContext context, Camera camera) { … Setup(); DrawVisibleGeometry(); DrawUnsupportedShaders(); DrawGizmos(); Submit(); }
另一件需要我们注意的事情是Unity的游戏内部用户界面。例如,通过GameObject / UI / button添加按钮来创建一个简单的UI。它会出现在游戏窗口中,但不会出现在场景窗口中:
frame debugger向我们展示了 UI 是单独呈现的,而不是由 RP 呈现的:
至少,当画布组件的渲染模式设置为 屏幕空间-覆盖 时,这是默认的情况。将其更改为屏幕空间-相机,并使用主相机作为其渲染相机将使其成为透明几何的一部分:
当在场景窗口中呈现世界时,UI总是使用World Space模式,这也是为什么它最终会变得非常大。但是,尽管我们可以通过场景窗口编辑UI,但它并不会被绘制出来:
在渲染场景窗口时,我们必须通过调用ScriptableRenderContext明确地将UI添加到世界几何图形中。EmitWorldGeometryForSceneView,相机作为参数。在一个新的只有编辑器的PrepareForSceneWindow方法中执行此操作。当它的cameraType属性等于 CameraType.SceneView时,我们使用场景相机进行渲染:
#if UNITY_EDITOR … partial void PrepareForSceneWindow () { if (camera.cameraType == CameraType.SceneView) { ScriptableRenderContext.EmitWorldGeometryForSceneView(camera); } }
因为这可能会给场景添加几何体,所以必须在剔除前完成:
PrepareForSceneWindow(); if (!Cull()) { return; }
在场景中可以有多个活动的摄像机。如果是这样的话,我们必须确保它们一起工作.
每个摄像机有“Depth”参数,默认主摄像机为“−1”。它们按照深度递增的顺序呈现。要看到这一点,复制主摄像机,重命名为次要摄像机,并设置其深度为0。给它另一个标签也是一个好主意,因为MainCamera应该只被一个相机使用。
场景现在被渲染两次。最终的图像仍然是相同的,因为渲染目标在两者之间被清除。帧调试器显示了这一点,但由于名称相同的相邻示例作用域被合并,我们最终得到一个单独的渲染摄像机作用域。
如果每个摄像头都有自己的瞄准镜就更清楚了。要实现这一点,请添加一个仅针对编辑器的PrepareBuffer方法,该方法使缓冲区的名称等于摄像机的名称:
partial void PrepareBuffer (); #if UNITY_EDITOR … partial void PrepareBuffer () { buffer.name = camera.name; } #endif
在我们PrepareForSceneWindow之前调用它:
PrepareBuffer(); PrepareForSceneWindow();
尽管 frame debugge 现在显示了一个独立的样本层次结构每个摄像头,当我们进入游戏模式时,Unity的控制台将充满消息警告我们,BeginSample和EndSample计数必须匹配。因为我们用了不同的名字来表示样本和它们的缓冲,所以很容易混淆。除此之外,我们还会在每次访问相机的name属性时分配内存,所以我们不想在构建中这么做.
为了解决这两个问题,需要添加一个SampleName字符串属性。如果在编辑器中,就在PrepareBuffer中设置它以及缓冲区的名称,否则它只是赋值给相机字符串的常量别名:
#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打开)和首先在编辑器中播放来看到差异。切换到层次模式并按GC Alloc列排序。您将看到两个GC调用的条目。Alloc,共分配100个字节,这是由检索相机名称引起的。再往下,你会看到这些名字显示为样本:主摄像机和副摄像机:
接下来,启用Development build和Autoconnect Profiler进行构建。播放构建并确保profiler已连接并记录。在这种情况下,我们并没有获得100字节的分配,而是获得了单个渲染相机样本:
另外48个字节的分配是干什么的?
是我们无法控制的摄像机数组。它的大小取决于有多少摄像机被渲染。
通过将相机名称包装在一个名为Editor的分析器示例中,可以让示例只在编辑器中分配了内存,而不在在构建后分配内存。在本例中,我们需要从UnityEngine.Profiling命名空间调用Profiler.BeginSample和Profiler.EndSample。只有BeginSample需要传递名称
我们可以清楚地表明,我们只在编辑器中分配内存,而不是在构建中分配内存,方法是将相机名称检索封装在一个名为editor only的分析器示例中。在这种情况下,我们需要调用Profiler。BeginSample和分析器。UnityEngine的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 }
摄像头也可以配置成只能看到特定层上的东西。这是通过调整他们的剔除遮罩来完成的。让我们将所有使用标准着色器的对象移动到Ignore Raycast层:
从主相机的剔除蒙版中排除这个图层:
并使其成为第二相机所能看到的唯一图层:
因为次级相机最后渲染,我们最终只看到无效的对象:
我们可以通过调整第二个被渲染的相机的清除标志来合并两个相机的结果。它们是由CameraClearFlags枚举定义的,我们可以通过相机的clearFlags属性来检索。在清除之前在安装程序中执行此操作:
void Setup () { context.SetupCameraProperties(camera); CameraClearFlags flags = camera.clearFlags; buffer.ClearRenderTarget(true, true, Color.clear); buffer.BeginSample(SampleName); ExecuteBuffer(); }
CameraClearFlags枚举定义了四个值。从1到4分别是Skybox、Color、Depth和Nothing。这些实际上并不是独立的标志值,而是表示清理量的减少。除了最后一个,深度缓冲区必须在所有情况下被清除,所以当flags值与于等于depth时:
buffer.ClearRenderTarget( flags <= CameraClearFlags.Depth, true, Color.clear );
我们只需要在flags设置为color时清除颜色缓冲区,因为在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 );
因为主摄像机是第一个渲染的,它的Clear Flags应该设置为Skybox或Color。当帧调试器被启用时,我们总是以一个清除缓冲区开始,但这在一般情况下是不能保证的.
二级相机的清除标志决定了如何将两个相机的渲染结合起来。在skybox或color的情况下,以前的结果被完全取代。当只有深度被清除时,Secondary Camera渲染正常,除了它没有绘制天空盒,所以之前的结果显示为背景。当没有任何东西被清除时,深度缓冲区将被保留,所以未被照亮的物体最终会遮挡无效的物体,就好像它们是由相同的摄像机绘制的一样。然而,由前一个相机绘制的透明物体没有深度信息,所以被绘制到上方,就像之前的天空盒一样:
通过调整摄像机的Viewport Rect,也可以将渲染区域缩小到整个渲染目标的一小部分。渲染目标的其余部分不受影响。在这种情况下,使用Hidden/InternalClear着色器进行清除。模板缓冲区用于将渲染限制在视口区域:
请注意,渲染一帧多个摄像机意味着剔除、设置、排序等操作也需要进行多次。通常来说,最有效的方法是在一个独特的视角下使用一个摄像机。
!!!第一篇终于学习完了,恭喜恭喜