前言
Scriptable Render Pipeline定制流水线控制渲染创建管道资产和实例。剔除,过滤,排序,渲染。保持记忆清洁。提供良好的编辑体验。这是涵盖Unity可编写脚本的渲染系列教程的第一部分管道。本教程假设您首先完成了基础知识系列,并且已经完成了程序网格教程。渲染系列的前几部分也很有用。本教程使用Unity 2018.3.0f2。新渲染管道的诞生。
1. 创建管道
要渲染任何东西,Unity必须确定必须绘制哪些形状,其中,什么时候和什么设置。这可能变得非常复杂,具体取决于用到的各种特效。灯光,阴影,透明度,图像特效,体积效果(volumetric effects),所有这些都必须以正确的顺序处理才能得出最终的图像。此过程称为渲染管道。
Unity 2017支持两个预定义的渲染管道,一个用于前向渲染和一个用于延迟渲染。它还支持较旧的延迟渲染(Unity 5中引入的方法)。这些管道是固定的。你可以启用,禁用或覆盖重写管道的某些部分,但不可能大幅度偏离原本的设计。
Unity 2018增加了对可编写脚本(scriptable render pipeline)的渲染管道的支持,使从头开始设计管道变为可能,但许多单独的具体操作仍然需要依赖Unity,如剔除(culling)。Unity 2018引入了两条新的管道新方法,轻量级渲染管道(LWRP)和高清渲染管道(HDRP)。这两个渲染管道技术仍处于预览阶段,其技术文档(API)也为实验技术。但现在它足够稳定,足够我们创建我们自己的管道。
这部分内容有剧透
请点击后再查看
在本教程中,我们将设置一个绘制未点亮形状(unlit shapes)的最小渲染管道
这部分内容有剧透
请点击后再查看
一旦我们设置完这个基础渲染管线,我们可以在以后的教程中扩展我们的管道,添加照明,阴影和更高级的功能。
1.1 项目设置
打开Unity 2018并创建一个新项目。我正在使用Unity 2018.2.9f1,但是任何2018.2或更高版本也应该有效。关闭unity analytics。我们将创建自己的管道,因此不要选择其中一个管道选项。项目打开后,通过Window / Package Manager转到包管理器并删除默认包含的所有包,因为我们不需要它们。仅保留无法删除的程序包管理器UI。
我们将在线性色彩空间中工作,但Unity 2018仍然使用伽马空间默认。(译者注:unity 的发展趋势是从Gamma 空间发展到Linear space,并保留对Gamma空间的支持,以便旧满足项目升级的可持续性,但未来Gamma空间将会被淘汰。 详情请查阅unity 上海大会关于对关照技术的演讲说明。)。所以通过Edit / Project Settings / Player 转到player 并切换“Other Settings”部分中的Color space为“linear”。
我们需要一些简单的材料来测试我们的管道。我创造了四种材料。首先,带有红色Albedo的默认标准不透明材质。第二,同样的材质,但渲染模式设置为透明和蓝色的albedo减少alpha值。第三,使用Unlit / Color着色器并将其颜色设置为的材质黄色。最后使用Unlit / Transparent着色器的材质没有任何变化,所以看起来是纯白色。
使用所有四种材料填充场景中的一些物体。场景显示四种材料。
1.2 管道资产(pipeline asset)
目前,Unity使用默认的正向渲染管道(forward rendering pipeline)。要使用自定义管道,我们必须在graphics settings中选择一个,可以通过Edit / Project Settings / Graphics找到。
要设置我们自己的管道,我们必须将pipeline asset分配给Scriptable Render Pipeline settings。这些Asset 必须扩展(extend) RenderPipelineAsset,这是一个 ScriptableObject类型。为我们的自定义管道资产创建一个新脚本。我们只需将我们的管道命名为MyPipeline。因此,它的asset类型将是MyPipelineAsset,并且必须对定义在 UnityEngine.Experimental.Rendering 命名空间的 RenderPipelineAsset.进行扩展。
using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipelineAsset : RenderPipelineAsset {}
它总是在实验名称空间中(experimental namespace)吗?
这部分内容有剧透
请点击后再查看
管道资产的主要目的是为Unity提供一种方法来获取一个负责渲染的管道对象实例, zhe个资产本身只是一个handle和存储管道设置的位置。我们还没有进行任何设置,所以全部我们要做的是给Unity一个获取我们的管道对象实例的方法。这是通过覆盖InternalCreatePipeline方法。但我们还没有定义我们的管道对象类型,所以我们暂时先只返回null。
InternalCreatePipeline的返回类型是IRenderPipeline。该类型的前缀表示它是一个接口类型。
public class MyPipelineAsset : RenderPipelineAsset {
protected override IRenderPipeline InternalCreatePipeline () {
return null; }
}
什么是界面?
接口就像一个类,除了它定义了一个功能性协议但不提供具体实现。它只定义属性,事件,索引器和方法签名,且都是公开(public)的。任何扩展接口的类型需要包含接口定义的实现。惯例是使用I为接口类型名称添加前缀。
因为接口不包含具体实现,所以类可能甚至结构扩展了多个接口。如果发生多个接口定义相同的东西,他们只是同意该功能应该存在。这不肯能在类中发生 ,即使是抽象类。因为这可能导致冲突实现(conflicting implementations)。
现在我们需要将这种类型的资产添加到我们的项目中。为了实现这一点,请添加一个CreateAssetMenu属性给MyPipelineAsset。
[CreateAssetMenu]
public class MyPipelineAsset : RenderPipelineAsset {}
这会在“Asset / Create 菜单中建立一个新的条目(入口)。看在整洁的份上,我们把它放在一个叫Rendering的子菜单里。通过将属性的menuName属性设置为Rendering/My Pipeline来完成此操作。可以在属性类型后直接设置属性,在圆括号内。
[CreateAssetMenu(menuName = "Rendering/My Pipeline")]
public class MyPipelineAsset : RenderPipelineAsset {}
使用新菜单项将资产添加到项目中,并将其命名为My Pipeline。管道资产及其脚本。
然后将其分配给Scriptable Render Pipeline Settings。(译者注:若读者使用的是更高版本或使用的不是实验命名空间的Rendering,请观阅完第二节,具体实现 RenderPipelineAsset 和 RenderPipeline。)
我们现在已经替换了默认管道,有些事情也随之变换了。首先,很多选项已从graphics settings,消失,在unity的信息面板也可以看到。其次,因为我们绕过了默认管道而没有提供有效的替换,没有任何东西得到渲染。游戏窗口,场景窗口和材质预览不再起作用,尽管是场景窗口仍然显示天空盒。如果打开Frame Debugger—— 通过Window / Analysis /Frame Debugger - 启用它,您将看到确实没有任何东西被绘制出来游戏窗口。
1.3 管道实例
要创建有效的管道,我们必须提供一个对象实来实现IRenderPipeline并让其负责渲染过程。所以创建一个类,命名为MyPipeline。
using UnityEngine;
using UnityEngine.Experimental.Rendering;
public class MyPipeline : IRenderPipeline {}
虽然我们可以自己实现IRenderPipeline,但让它扩展抽象RenderPipeline类更加方便。因为我们可以在实现RenderPipeline的基础上来实现我们的功能。
public class MyPipeline : RenderPipeline {}
现在我们可以在InternalCreatePipeline中返回MyPipeline的新实例。这意味着我们已经有一个有效的管道,虽然它仍然没有渲染任何东西
protected override IRenderPipeline InternalCreatePipeline () {
return new MyPipeline();
}
2 渲染
管道对象负责渲染每个帧。Unity所做的是调用管道的Render方法,这个方法的参数为context和camera。这个方法不仅适用于游戏窗口,也在场景窗口和材质预览编辑中使用。让我们通过适当的配置,找出需要的东西来渲染,并以正确的顺序执行所有操作。
2.1 Context
RenderPipeline包含一个实现IRenderPipeline 接口的 Render 方法。它的第一个参数是render contex,它是一个ScriptableRenderContext结构体,充当内在代码的门面(acting as a facade for native code,俺意译的)。它的第二个参数是一个包含所有需要渲染的相机的数组。
RenderPipeline.Render 不绘制任何东西,但检查管道对象能否渲染。如果不能渲染,它将引发异常。我们将覆盖这一方法并调用基本实现,以保持此检查。
public class MyPipeline : RenderPipeline {
public override void Render ( ScriptableRenderContext renderContext, Camera[] cameras) {
base.Render(renderContext, cameras);}
}
我们通过render context向 Unity引擎发出命令,渲染事物并控制渲染状态。其中一个最简单的例子是绘制天空盒,可以通过调用DrawSkyBox方法来完成。
base.Render(renderContext, cameras);
renderContext.DrawSkybox();
DrawSkybox 需要一个相机作为参数。我们将简单地使用相机数组的第一个元素。
renderContext.DrawSkybox(cameras[0]);
我们仍然没有看到天空盒出现在游戏窗口中。那是因为我们向上下文发出的命令是缓冲的。实际工作发生在我们通过Submit方法提交它以供执行。
renderContext.DrawSkybox(cameras[0]);
renderContext.Submit();
现在你终于能看到天空盒出现在游戏窗口里,你也能从frame debugger 里看到:
2.2 相机
我们提供了一系列相机,因为可以存在需要多个相机渲染的场景。多摄像机设置的示例用途是分屏多人游戏,迷你地图和后视镜。每个相机都需要单独处理。
此时我们先不担心多相机支持我们的管道。只需创建一个替代单个相机的Render方法。由它画天空盒然后提交。所以我们按照相机提交
void Render (ScriptableRenderContext context, Camera camera) {
context.DrawSkybox(camera);
context.Submit();
}
为相机阵列的每个元素调用新方法。我使用foreach循环遍历,就像Unity的管道也使用这种方法来遍历摄像头。
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);
}
}
foreach如何运作?
这部分内容有剧透
请点击后再查看
foreach (var e in a) { … } 就像 for (int i = 0 ; i <a.Length; a ++){ var e = a [i]; ...} 假设a 是一个数组。唯一的功能区别是我们无需访问迭代器变量 i。
当a不是数组而是其它可枚举的其他东西时(如 List, HashSet),迭代器就会进入,你最终可能会创建临时对象,最好避免。但使用带数组的foreach是安全的。使用var来定义元素变量很常见,所以我也使用它。它的类型是的元素类型a的元素类型。
请注意,相机的方向当前不会影响天空盒怎样渲染。我们将相机传递给DrawSkybox,但这仅用于确定是否应该绘制天空盒,这是通过相机的clear flags决定的。
要正确渲染天空盒 -——以及整个场景 —— 我们必须设置视图 -投影矩阵(VP matrix)。这个变换矩阵结合了相机的位置和方向 - 视图矩阵 - 使用相机的透视或正交投影 - 投影矩阵。您可以在帧调试器中看到此矩阵。它是unity_MatrixVP,是绘制某些东西时使用的着色器属性之一。
目前,unity_MatrixVP矩阵始终是相同的。我们必须通过SetupCameraProperties方法将相机的属性添加到context中, 这就像设置其它设置一样设置我们的矩阵。
void Render (ScriptableRenderContext context, Camera camera) {
context.SetupCameraProperties(camera);
context.DrawSkybox(camera);
context.Submit();
}
现在天空盒被正确渲染,将游戏窗口和场景窗口中的相机属性也考虑在内。
2.3 命令缓冲区
context 会延迟实际的渲染直到我们提交它。 在此之前, 我们通过配置和向其添加命令以供以后执行。一些任务 —— 比如绘制天空盒—— 可以通过专用方法来执行,但其他命令的分发需通过单独的命令缓冲区。
可以通过实例化一个在UnityEngine.Rendering 命名空间中定义的 CommandBuffer。 CommandBuffer 已存在在添加脚本化渲染管道之前,它们不是实验性的。 在绘制天空盒之前先创建这样一个缓冲区。
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方法指示context 执行 这个buffer。 同样的,这不会立即执行命令,而是将它们复制到context的内部缓冲区。
var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
Command buffers将把资源命令存储在unity 引擎的底层。所以如果我们不再需要这些资源,最好立即释放它们。这可以通过直接调用缓冲区的Release方法来完成调用
var buffer = new CommandBuffer();
context.ExecuteCommandBuffer(buffer);
buffer.Release();
执行空命令缓冲区什么都不会做。我们添加它以便我们可以清除渲染目标,以确保渲染不受之前绘制的内容的影响。这可以通过command buffer实现,但不能直接通过context实现。
可以通过调用ClearRenderTarget将清除命令添加到缓冲区。它需要三个参数:两个布尔值和一个颜色。第一个参数控制是否清除了深度信息,第二个是否清除了颜色,如果使用的话,第三种是清晰的颜色。例如,让我们清除深度数据,忽略颜色数据,并使用 color.clear作为清除的色彩。
var buffer = new CommandBuffer();
buffer.ClearRenderTarget(true, false, Color.clear);
context.ExecuteCommandBuffer(buffer);
buffer.Release();
frame debugger现在向我们显示一个清除渲染目标的commandbuffer已被执行。在这种情况下,它表明Z和模板(stencil)被清除。ž指深度缓冲区,模板缓冲区总是被清除。
每个摄像机通过其clear flags 和背景颜色来清除内容。我们可以使用那些而不是硬编码我们如何清除渲染目标。
CameraClearFlags clearFlags = camera.clearFlags;
buffer.ClearRenderTarget(
(clearFlags & CameraClearFlags.Depth) != 0,
(clearFlags & CameraClearFlags.Color) != 0,
camera.backgroundColor
);
清除标志如何工作?
这部分内容有剧透
请点击后再查看
CameraClearFlags是一个枚举,可以用作一组位标志。每一点该值用于指示是否启用某个功能。要从整个值中提取位标志,请使用将值与所需标志组合按位AND运算符&。如果结果不为零,则设置标志。
因为我们没有给命令缓冲区命名,所以frame debugger 会显示默认名称,即未命名的命令缓冲区。我们用相机的名字吧相反,通过将其分配给缓冲区的name属性。我们将使用对象初始化器语法来做到这一点。
var buffer = new CommandBuffer {
name = camera.name
};
对象初始化器语法如何工作?
这部分内容有剧透 我们也可以编写buffer.name = camera.name; 作为一个单独的声明之后调用构造函数。但是在创建新对象时,您可以附加代码块到构造函数的调用。然后,您可以在中设置对象的字段和属性阻止,而不必显式引用对象实例。此外,它明确表示只有在设置了这些字段和属性后才能使用实例。除此之外,它允许只允许一个语句的初始化,不需要具有许多参数变体的构造函数。注意,我们省略了构造函数调用的空参数列表,即允许使用对象初始化程序语法时。
请点击后再查看
2.4 剔除(Culling)
我们能够渲染天空盒,但我们放入场景的物体仍不能渲染。我们只会渲染那些相机可以看到的物体而不是所有。我们先从可以渲染的物体开始,然后将超出相机平截头体(frustum)的部分剔除掉。
找出可以剔除的内容需要我们跟踪多个相机的设置和矩阵,我们可以使用 ScriptableCullingParameters结构。我们可以将该工作委托给静态方法CullResults.GetCullingParameters 。它需要一个摄像头作为输入并产生剔除参数作为输出。但是,它不返回参数struct。相反,我们必须提供它作为第二个输出参数,在它前面写出来。
void Render (ScriptableRenderContext context, Camera camera) {
ScriptableCullingParameters cullingParameters;
CullResults.GetCullingParameters(camera, out cullingParameters);
…
}
为什么我们要用 out?
这部分内容有剧透 结构是值类型,因此它们被视为简单值。它们不是具有的对象身份,变量和字段仅保存对其在内存中的位置的引用。所以将struct作为参数传递提供了一个带有该值副本的方法。该方法可以更改副本,但这对复制的值没有影响。当struct参数被定义为输出参数时,它就像一个对象引用,但指向参数所在的内存堆栈上的位置。当方法更改该参数时,它会影响该值,而不是副本。out关键字告诉我们,方法负责正确设置参数,替换以前的值。
请点击后再查看
除了输出参数,GetCullingParameters还返回它是否能够创建有效参数。并非所有相机设置都有效,导致返回结果不能用于剔除。所以如果它失败了,我们就没有任何东西可以渲染了可以退出渲染。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) { return; }
一旦我们有了剔除参数,我们就可以用它们来剔除。这是通过调用静态方法 CullResults.Cull方法,这个方法参数用剔除参数和上下文作为参数。结果是CullResults结构,包含关于什么是可见的信息。
在这种情况下,我们必须提供剔除参数作为参考参数,在它前面写ref。
if (!CullResults.GetCullingParameters(camera, out cullingParameters)) {
return;
}
CullResults cull = CullResults.Cull(ref cullingParameters, context);
为什么我们要写ref?
【spoilerblock]
它的工作原理就像out,但它不要求赋值,而是调用该方法的人负责先初始化。因此它可以用于输入,也可以用于输出。
【/spoilerblock]
为什么ScriptableCullingParameters是一个结构?
【spoilerblock]
这可能是一次优化尝试,这个想法是你可以创建多个参数结构,而不必担心内存分配。然而,ScriptableCullingParameters对于一个struct来说非常大,这就是为什么一个引用参数在此使用,而且出于性能原因。也许它开始很小但却在增长随着时间的推移变成一个巨大的物体, 可重用的对象实例现在可能是更好的方法,但是我们必须使用Unity Technologies决定使用的任何东西。
【/spoilerblock]
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(照相机
第15页
为什么 FilterRenderersSettings而不是FilterRendererSettings?不知道。也许这是一个错字。我们还没有看到任何对象,因为默认的过滤器设置不包含任何内容。我们可以通过提供真实作为参数来包含所有内容FilterRenderersSettings构造函数。这告诉它初始化自己,所以它包括一切。var filterSettings = new FilterRenderersSettings(真);此外,我们必须通过为其构造函数提供来配置绘图设置相机和着色器作为参数传递。相机用于设置排序和剔除图层,而着色器传递的传递控件用于渲染。着色器传递通过字符串标识,该字符串必须包装在ShaderPassName中结构。由于我们只支持管道中未点亮的材料,我们将使用Unity默认的unlit pass,用SRPDefaultUnlit标识。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“SRPDefaultUnlit”));不透明的球体是可见的。我们看到不透明的不透明形状出现,但不是透明的。然而帧调试器表示也会绘制不亮的形状。所有未点亮的渲染器都被绘制出来。
第16页
它们确实被绘制,但因为透明着色器传递不会写入深度缓冲区最终被天空盒吸引。解决方案是推迟绘制透明渲染器,直到天空盒之后。首先,将天空盒之前的绘制限制为仅使用不透明的渲染器。这是通过将过滤器设置的renderQueueRange设置为RenderQueueRange .opaque,其中覆盖从0到2500(包括2500)的渲染队列。var filterSettings = new FilterRenderersSettings(真){renderQueueRange = RenderQueueRange .opaque};仅绘制不透明的渲染器。接下来,将队列范围更改为RenderQueueRange.transparent - 从2501到和渲染天空盒后包括5000-再渲染。var filterSettings = new FilterRenderersSettings(真){renderQueueRange = RenderQueueRange .opaque};context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);context.DrawSkybox(照相机);filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);
第17页
不透明,天空盒,然后透明。我们在天空盒之前绘制不透明的渲染器以防止透支。作为形状将永远在天空盒前面,我们首先通过绘制它们来避免工作。那是因为不透明着色器传递写入深度缓冲区,用于跳过以后绘制的任何东西都会越走越远。除了覆盖天空的一部分,不透明的渲染器也可能最终遮挡每个其他。理想情况下,只为每个片段绘制最靠近相机的那个帧缓冲区。因此,为了尽可能减少透支,我们应该画出最近的形状。这可以通过在绘制之前对渲染器进行排序来完成,这是通过排序标志控制的。绘图设置包含类型的排序结构DrawRendererSortSettings,其中包含排序标志。在绘制不透明之前将其设置为SortFlags .CommonOpaque形状。这指示Unity按照从前到后的距离对渲染器进行排序,加上一些其他标准。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“SRPDefaultUnlit”));drawSettings.sorting.flags = SortFlags .CommonOpaque;但是,透明渲染的工作方式不同 它结合了什么的颜色用以前绘制的东西绘制,结果看起来是透明的。这需要从后到前的反向绘制顺序。我们可以用SortFlags.CommonTransparent 为此。
第18页
context.DrawSkybox(照相机);drawSettings.sorting.flags = SortFlags .CommonTransparent;filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);我们的管道现在能够渲染不透明和透明的不亮物体正确。
第19页
3 抛光能够正确渲染只是拥有功能管道的一部分。有其他要考虑的事情,比如它是否足够快,不分配不需要的临时对象,并与Unity编辑器很好地集成。3.1 内存分配让我们检查一下我们的管道在内存管理方面是否表现良好,或者是否它每帧都分配内存,这将触发频繁的内存垃圾集合运行。这可以通过Window / Analysis / Profiler打开探查器来完成并在层次结构模式下检查CPU使用率数据。虽然你可以在游戏中做到这一点在编辑器中,通过确保创建来分析构建也是一个好主意开发构建并将其自动附加到分析器,尽管很深在这种情况下,不可能进行分析。按GC Alloc排序,你会发现每帧都确实分配了内存。一些它不受我们控制,但在我们的管道中分配了相当多的字节渲染方法。事实证明,剔除分配的内存最多。原因是这样的虽然 CullResults是一个结构,它包含三个列表,它们是对象。每一次我们要求一个新的剔除结果,我们最终为新列表分配内存。所以没有好处 CullResults是一个结构。幸运的是,CullResults有一个替代的Cull方法,它接受一个结构作为引用参数,而不是返回一个新参数。这使得重用成为可能清单。我们所要做的就是将剔除变成一个字段并将其作为附加信息提供CullResults的参数.Cull,而不是分配给它。CullResults剔除;...void 渲染(ScriptableRenderContext context, Camera camera){...// CullResults cull = CullResults.Cull(ref cullingParameters,context);CullResults .Cull(ref cullingParameters,context,ref cull);...}
第20页
连续内存分配的另一个来源是我们使用相机的名称属性。每次我们得到它的值,它从本机代码中获取名称数据,这需要创建一个新的字符串,这是一个对象。所以,让我们永远将命令缓冲区命名为Render Camera。var buffer = new CommandBuffer(){name = “渲染相机”};使用常量缓冲区名称。最后,命令缓冲区本身也是一个对象。幸运的是,我们可以创建一个命令缓冲区一次并重用它。用cameraBuffer替换局部变量领域。由于对象初始化语法,我们可以创建一个命名命令缓冲区作为其默认值。唯一的另一个变化是我们必须清除命令缓冲区而不是释放它,我们可以使用它的Clear方法。CommandBuffer cameraBuffer = new CommandBuffer {name = “渲染相机”};...void 渲染(ScriptableRenderContext context, Camera camera){...// var buffer = new CommandBuffer(){// name =“渲染相机”//};cameraBuffer.ClearRenderTarget(true,false,Color .clear);context.ExecuteCommandBuffer(cameraBuffer);//buffer.Release();cameraBuffer.Clear();...}在这些更改之后,我们的管道不再每帧都创建临时对象。3.2 帧调试器采样
第21页
我们可以做的另一件事是改进帧调试器显示的数据。Unity的管道显示了事件的嵌套层次结构,但我们的管道都是根水平。我们可以使用命令缓冲区来开始和结束来构建层次结构分析器样本。让我们从立即调用ClearRenderTarget之前的BeginSample开始然后调用EndSample。每个样本必须同时包含一个开头和一个结束,两者必须提供完全相同的名称。除此之外,我有发现最好使用与定义的命令缓冲区相同的名称采样。无论如何,命令缓冲区的名称经常被使用。cameraBuffer.BeginSample(“渲染相机”);cameraBuffer.ClearRenderTarget(true,false,Color .clear);cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();采样创建了一个层次结构。我们现在看到一个渲染相机级别嵌套在原始的渲染相机中命令缓冲区,它又包含清除操作。但是有可能去更进一步,将与相机相关的所有其他动作嵌套在其中。这个要求我们延迟样品的结束,直到我们提交样品上下文。所以我们只需要在那一点插入一个额外的ExecuteCommandBuffer包含结束样本的指令。为此使用相同的命令缓冲区,我们完成后再次清除它。cameraBuffer.BeginSample(“渲染相机”);cameraBuffer.ClearRenderTarget(true,false,Color .clear);//cameraBuffer.EndSample("Render Camera“);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();...cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();context.Submit();
第22页
嵌套采样。除了clear动作嵌套在冗余Render中之外,这看起来还不错摄像机级别,而所有其他操作都直接位于根级别之下。我不确定为什么会发生这种情况,但可以通过清除后开始采样来避免。//cameraBuffer.BeginSample("Render Camera“);cameraBuffer.ClearRenderTarget(true,false,Color .clear);cameraBuffer.BeginSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();没有多余的嵌套。3.3 渲染默认管道因为我们的管道只支持不亮的着色器,使用不同着色器的对象没有渲染,使它们看不见。虽然这是正确的,但它隐藏了这样一个事实某些对象使用错误的着色器。如果我们想象这些对象会很好使用Unity的错误着色器,因此它们显示为明显不正确的洋红色形状。让我们为此添加一个专用的DrawDefaultPipeline方法,带有一个上下文和一个相机参数。在绘制透明形状后,我们将在最后调用它。
第23页
void 渲染(ScriptableRenderContext context, Camera camera){...drawSettings.sorting.flags = SortFlags .CommonTransparent;filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);DrawDefaultPipeline(上下文,相机);cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();context.Submit();}void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){}Unity的默认表面着色器有一个用作第一个的ForwardBase传递前向渲染通道。我们可以使用它来识别具有材料的对象适用于默认管道。通过新的绘制设置选择该传递并使用它用于渲染,以及新的默认过滤器设置。我们不关心排序或者分离不透明和透明的渲染器,因为它们无论如何都是无效的。void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“ForwardBase”));var filterSettings = new FilterRenderersSettings(真);context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);}渲染前向基础传球。现在显示使用默认着色器的对象。他们也可以看到帧调试器。
第24页
一切都被吸引了。因为我们的管道不支持前向基础传递,所以它们不会被渲染正确。没有设置必要的数据,所以依赖于照明的一切都结束了像黑色一样。相反,我们应该使用错误着色器渲染它们。要做到这一点,我们需要一个错误材料。为此添加字段。然后,在DrawDefaultPipeline的开头,如果错误材料尚不存在,请创建错误材料。这是通过检索来完成的通过隐藏/ InternalErrorShader 着色 .Find,然后创建与新材料着色器。另外,将材质的隐藏标志设置为HideFlags.HideAndDontSave所以它没有显示在项目窗口中,不会与所有其他资产一起保存。材料错误材料 ;...void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){if (errorMaterial == null ){着色器 errorShader =着色器.Find(“Hidden / InternalErrorShader” );errorMaterial = 新材料(errorShader){hideFlags = HideFlags.HideAndDontSave};}...}绘图设置的一个选项是覆盖渲染时使用的材质调用SetOverrideMaterial。它的第一个参数是要使用的材料。它的第二个parameter是要用于渲染的材质着色器的过程的索引。由于错误着色器只有一次传递,因此使用零。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“ForwardBase”));drawSettings.SetOverrideMaterial(errorMaterial,0);
第25页
使用错误着色器。使用不受支持的材料的对象现在清楚地显示为不正确。但是这个仅适用于Unity的默认管道的材质,其着色器具有ForwardBase通过。还有其他内置着色器,我们可以识别不同的通道,特别是PrepassBase,Always,Vertex,VertexLMRGBM和VertexLM。幸运的是,可以通过调用向绘图设置添加多个传递SetShaderPassName 。名称是此方法的第二个参数。它的第一个parameter是一个索引,用于控制绘制通道的顺序。我们不关心那个,所以任何订单都没问题。通过构造函数提供的传递始终具有索引零,只需增加附加传递的索引。var drawSettings = new DrawRendererSettings(相机,新的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);这涵盖了Unity提供的所有着色器到目前为止,这应该足够了帮助指出在创建场景时使用不正确的材料。但我们只在开发期间需要打扰这样做,而不是在构建中。所以我们只调用它编辑器中的 DrawDefaultPipeline 。一种方法是添加条件属性为方法。
第26页
3.4 条件代码执行该 条件属性在System.Diagnostics名称空间中定义。我们可以用那个命名空间,但不幸的是它还包含一个与之冲突的Debug类型UnityEngine。调试。由于我们只需要属性,我们可以通过使用来避免冲突别名。我们使用特定类型并分配它,而不是使用整个命名空间到有效的类型名称。在这种情况下,我们将Conditional定义为别名System.Diagnostics.ConditionalAttribute 。使用UnityEngine;使用UnityEngine.Rendering;使用UnityEngine.Experimental.Rendering;使用Conditional = System.Diagnostics.ConditionalAttribute;将属性添加到我们的方法。它需要一个指定a的字符串参数符号。如果在编译期间定义了符号,则获取方法调用正常包括在内。但是如果没有定义符号,那么调用它方法 - 包括其所有参数 - 被省略。就好像DrawDefaultPipeline(上下文,相机); 编译期间代码不存在。要仅在编译Unity编辑器时包含调用,我们必须依赖在UNITY_EDITOR符号上。[ 有条件的(“UNITY_EDITOR”)]void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){...}我们可以更进一步,也只包括在开发构建中的调用从发布版本中排除它。为此,请添加一个附加条件DEVELOPMENT_BUILD符号。[ 有条件的(“DEVELOPMENT_BUILD”),有条件的(“UNITY_EDITOR”)]void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){...}3.5 场景窗口中的UI到目前为止我们还没有考虑过的一件事是Unity的游戏内UI。去测试它,通过GameObject / UI /向场景添加UI元素,例如单个按钮按钮。这创建了一个带有按钮的画布,还有一个事件系统。
第27页
事实证明,UI在游戏窗口中呈现,而我们不必这样做任何东西。Unity为我们照顾它。框架调试器显示UI获取单独渲染,作为叠加层。UI在屏幕空间中绘制。至少,当画布设置为在屏幕空间中渲染时就是这种情况。设置为时在世界空间中渲染,UI与其他透明对象一起渲染。在世界空间的UI。尽管UI在游戏窗口中起作用,但它不会显示场景窗口。UI始终存在于场景窗口的世界空间中,但我们必须手动完成将它注入场景。通过调用静态来添加UIScriptableRenderContext.EmitWorldGeometryForSceneView 方法,与当前相机作为一个论点。这必须在剔除之前完成。
第28页
if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}ScriptableRenderContext.EmitWorldGeometryForSceneView(照相机);CullResults .Cull(ref cullingParameters,context,ref cull);但这也在游戏窗口中第二次添加了UI。为了防止这种情况,我们必须仅在渲染场景窗口时发出UI几何体。情况就是这样当摄像机的cameraType等于CameraType .SceneView。if (camera.cameraType ==CameraType .SceneView){ScriptableRenderContext .EmitWorldGeometryForSceneView(camera);}这有效,但仅限于编辑器。条件编译确保了编译构建时, EmitWorldGeometryForSceneView 不存在,这意味着我们现在在尝试构建时遇到编译器错误。为了让它再次发挥作用,我们必须使调用EmitWorldGeometryForSceneView条件的代码也是有条件的。这是通过将代码放在#if和#endif语句之间来完成的。该#如果声明需要一个符号,就像 条件属性。通过使用UNITY_EDITOR,仅在编辑编辑器时才包含的代码。void 渲染(ScriptableRenderContext context, Camera camera){ScriptableCullingParameters cullingParameters;if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}#if UNITY_EDITORif (camera.cameraType ==CameraType .SceneView){ScriptableRenderContext .EmitWorldGeometryForSceneView(camera);}#万一CullResults .Cull(ref cullingParameters,context,ref cull);...}下一个教程是Custom Shaders。知识库
第29页
享受教程?它们有用吗?想要更多?请在Patreon上支持我!或者直接捐款!由Jasper Flick制作
第1页
Catlike Coding > Unity > Tutorials > Scriptable Render Pipeline定制流水线控制渲染创建管道资产和实例。剔除,过滤,排序,渲染。保持记忆清洁。提供良好的编辑体验。这是涵盖Unity可编写脚本的渲染系列教程的第一部分管道。本教程假设您首先完成了基础知识系列,并且已经完成了程序网格教程。渲染系列的前几部分也很有用。本教程使用Unity 2018.3.0f2。新渲染管道的诞生。
第2页
1 创建管道要渲染任何东西,Unity必须确定必须绘制哪些形状,其中,什么时候和什么设置。这可能变得非常复杂,具体取决于数量影响涉及到。灯光,阴影,透明度,图像效果,体积效果,所有这些都必须以正确的顺序处理才能得出最终的图像。此过程称为渲染管道。Unity 2017支持两个预定义的渲染管道,一个用于前向渲染和一个用于延迟渲染。它还支持较旧的延迟渲染Unity 5中引入的方法。这些管道是固定的。你可以启用,禁用或覆盖管道的某些部分,但不可能大幅度偏离他们的设计。Unity 2018增加了对可编写脚本的渲染管道的支持,使其成为可能从头开始设计管道,尽管你仍然需要依赖Unity个别步骤,如剔除。Unity 2018引入了两条新的管道新方法,轻量级管道和高清管道。都管道仍处于预览阶段,脚本化渲染管道API仍然存在标记为实验技术。但在这一点上它足够稳定我们去领先并创建我们自己的管道。在本教程中,我们将设置一个绘制未点亮形状的最小渲染管道。一旦这是有效的,我们可以在以后的教程中扩展我们的管道,添加照明,阴影和更高级的功能。1.1 项目设置打开Unity 2018并创建一个新项目。我正在使用Unity 2018.2.9f1,但是任何2018.2或更高版本也应该有效。使用创建标准3D项目分析已禁用。我们将创建自己的管道,因此不要选择其中一个管道选项。项目打开后,通过Window / Package Manager转到包管理器并删除默认包含的所有包,因为我们不需要它们。仅保留无法删除的程序包管理器UI。
第3页
开始项目。我们将在线性色彩空间中工作,但Unity 2018仍然使用伽马空间默认。所以通过编辑/项目设置/播放器转到播放器设置并切换“其他设置”部分中的颜色空间为“线性”。线性色彩空间。我们需要一些简单的材料来测试我们的管道。我创造了四种材料。首先,带有红色反照率的默认标准不透明材质。第二,同样的材质,但渲染模式设置为透明和蓝色反照率减少阿尔法。第三,使用Unlit / Color着色器并将其颜色设置为的材质黄色。最后使用Unlit / Transparent着色器的材质没有任何变化,所以看起来是纯白色。测试材料。使用所有四种材料填充场景中的一些物体。场景显示四种材料。
第4页
1.2 管道资产目前,Unity使用默认的正向渲染管道。要使用自定义管道,我们必须在图形设置中选择一个,可以通过编辑/找到项目设置/图形。使用默认管道。要设置我们自己的管道,我们必须将管道资产分配给Scriptable渲染管道设置字段。这些资产必须延伸RenderPipelineAsset,其中是一个 ScriptableObject类型。为我们的自定义管道资产创建一个新脚本。我们只需将我们的管道命名为My管道。因此,它的资产类型将是MyPipelineAsset,并且必须进行扩展RenderPipelineAsset,在UnityEngine.Experimental.Rendering中定义命名空间。使用UnityEngine;使用UnityEngine.Experimental.Rendering;公共类MyPipelineAsset:RenderPipelineAsset {}它总是在实验名称空间中吗?它会在某个时刻移出实验命名空间,或者转移到UnityEngine.Rendering 或另一个命名空间。当发生这种情况时,这只是一个问题更新using语句,除非API也被更改。管道资产的主要目的是为Unity提供一种方法来获取a管道对象实例,负责呈现。资产本身只是一个句柄和存储管道设置的位置。我们还没有任何设置,所以全部我们要做的是给Unity一个获取我们的管道对象实例的方法。这是通过覆盖InternalCreatePipeline方法。但我们还没有定义我们的管道对象类型,所以在这一点上我们只返回null。InternalCreatePipeline的返回类型是IRenderPipeline。该类型的前缀name表示它是一个接口类型。
第5页
公共类MyPipelineAsset:RenderPipelineAsset {protected override IRenderPipeline InternalCreatePipeline(){return null ;}}什么是界面?接口就像一个类,除了它定义了一个没有的功能合同提供它的实现。它只定义属性,事件,索引器和方法签名,根据定义都是公开的。任何扩展接口的类型需要包含接口定义的实现。惯例是使用I为接口类型名称添加前缀因为接口不包含具体实现,所以类可能甚至结构扩展了多个接口。如果发生多个接口定义相同的东西,他们只是同意功能应该存在。这不是可能与类 - 即使是抽象 - 因为这可能导致冲突实现。现在我们需要将这种类型的资产添加到我们的项目中。为了实现这一点,请添加一个CreateAssetMenu属性MyPipelineAsset。[CreateAssetMenu]公共类MyPipelineAsset:RenderPipelineAsset {}这会在“资产/创建”菜单中输入一个条目。让我们整洁一点,把它放在渲染中子菜单。我们通过将属性的menuName属性设置为来完成此操作渲染/我的管道。可以在属性类型后直接设置属性,在圆括号内。[CreateAssetMenu(menuName = “渲染/我的管道”)]公共类MyPipelineAsset:RenderPipelineAsset {}使用新菜单项将资产添加到项目中,并将其命名为My Pipeline。管道资产及其脚本。
第6页
然后将其分配给Scriptable Render Pipeline Settings。资产在使用中。我们现在已经替换了默认管道,这改变了一些事情。首先,很多选项已从图形设置中消失,Unity也提到了这些设置信息面板。其次,因为我们绕过了默认管道而没有提供有效的替换,没有任何东西得到渲染。游戏窗口,场景窗口和材质预览不再起作用,尽管是场景窗口仍然显示天空盒。如果打开框架调试器 - 通过Window / Analysis /帧调试器 - 启用它,您将看到确实没有任何东西被绘制出来游戏窗口。1.3 管道实例要创建有效的管道,我们必须提供一个实现的对象实例IRenderPipeline并负责渲染过程。所以创建一个类那,命名为MyPipeline。使用UnityEngine;使用UnityEngine.Experimental.Rendering;公共类MyPipeline:IRenderPipeline {}虽然我们可以自己实现IRenderPipeline,但它更方便改为扩展抽象RenderPipeline类。那种类型已经提供了基本的我们可以构建的IRenderPipeline的实现。公共类MyPipeline:RenderPipeline {}现在我们可以在InternalCreatePipeline中返回MyPipeline的新实例。这意味着我们在技术上有一个有效的管道,虽然它仍然没有渲染任何东西。protected override IRenderPipeline InternalCreatePipeline(){返回新的MyPipeline();}
第7页
2 渲染管道对象负责渲染每个帧。所有Unity都会调用管道的渲染方法,带有上下文和活动的摄像机。这个完成了对于游戏窗口,还可以进行场景窗口和材质预览编辑。由我们来配置适当的东西,找出需要的东西渲染,并以正确的顺序执行所有操作。2.1 背景RenderPipeline包含在中定义的Render 方法的实现IRenderPipeline界面。它的第一个参数是渲染上下文,它是一个ScriptableRenderContext结构,充当本机代码的外观。它的第二个argument是一个包含所有需要渲染的相机的数组。RenderPipeline .Render 不绘制任何东西,但检查是否为管道对象有效用于渲染。如果没有,它将引发异常。我们将覆盖这一点方法并调用基本实现,以保持此检查。公共类MyPipeline:RenderPipeline {public override void Render(ScriptableRenderContext renderContext,相机[]相机){base .Render(renderContext,cameras);}}我们通过渲染上下文向Unity引擎发出命令渲染事物并控制渲染状态。其中一个最简单的例子是绘制skybox,可以通过调用DrawSkyBox方法来完成。base .Render(renderContext,cameras);renderContext.DrawSkybox();DrawSkybox 需要相机作为参数。我们将简单地使用第一个元素相机。renderContext.DrawSkybox(cameras [ 0 ]);我们仍然没有看到天空盒出现在游戏窗口中。那是因为我们向上下文发出的命令是缓冲的。实际工作发生在我们通过Submit方法提交它以供执行。
第8页
renderContext.DrawSkybox(cameras [ 0 ]);renderContext.Submit();天空盒最终出现在游戏窗口中,你也可以看到它出现在帧调试器。显示天空框的框架调试器被绘制。2.2 相机我们提供了一系列相机,因为可以存在多个相机所有必须渲染的场景。多摄像机设置的示例用途是分屏多人游戏,迷你地图和后视镜。每个相机都需要单独处理。此时我们不会担心多相机支持我们的管道。好只需创建一个替代单个相机的Render方法。有它画天空盒然后提交。所以我们按照相机提交。void 渲染(ScriptableRenderContext context, Camera camera){context.DrawSkybox(照相机);context.Submit();}为相机阵列的每个元素调用新方法。我使用foreach循环在这种情况下,Unity的管道也使用这种方法来循环通过摄像头。public override void Render(ScriptableRenderContext renderContext,相机[]相机){base .Render(renderContext,cameras);//renderContext.DrawSkybox(cameras[0]);//renderContext.Submit();的foreach (VAR 相机在相机){渲染(renderContext,camera);}}
第9页
foreach如何运作?的foreach (VAR Ë 在一){...} 就像for (int i = 0 ; i <a.Length; a ++){ var e = a [i]; ...} 假设a 是一个阵列。唯一的功能区别是我们无法访问迭代器变量我。当a不是数组而是可枚举的其他东西时,迭代器就会进入玩,你最终可能会创建临时对象,最好避免。但使用带数组的foreach是安全的。使用var来定义元素变量很常见,所以我也使用它。它的类型是的元素类型一个。请注意,相机的方向当前不会影响天空盒的获取方式渲染。我们将相机传递给DrawSkybox,但这仅用于确定是否应该绘制天空盒,这是通过相机清除控制的标志。要正确渲染天空盒 - 以及整个场景 - 我们必须设置视图 -投影矩阵。这个变换矩阵结合了相机的位置和方向 - 视图矩阵 - 使用相机的透视或正交投影 - 投影矩阵。您可以在帧调试器中看到此矩阵。它是unity_MatrixVP,是绘制某些东西时使用的着色器属性之一。目前,unity_MatrixVP矩阵始终是相同的。我们必须申请通过SetupCameraProperties方法将相机的属性添加到上下文中。这一切矩阵以及其他一些属性。void 渲染(ScriptableRenderContext context, Camera camera){context.SetupCameraProperties(照相机);context.DrawSkybox(照相机);context.Submit();}现在天空盒被正确渲染,考虑到相机属性,在游戏窗口和场景窗口中。2.3 命令缓冲区
第10页
在我们提交之前,上下文会延迟实际呈现。在此之前,我们配置它并向其添加命令以供以后执行。一些任务 - 比如绘制天空盒- 可以通过专用方法发布,但必须发布其他命令间接地,通过单独的命令缓冲区。可以通过实例化new来创建命令缓冲区 CommandBuffer对象,其中在UnityEngine.Rendering命名空间中定义。命令缓冲区已存在在添加脚本化渲染管道之前,它们不是实验性的。在绘制天空盒之前创建这样的缓冲区。使用UnityEngine;使用UnityEngine.Rendering;使用UnityEngine.Experimental.Rendering;公共类MyPipeline:RenderPipeline {...void 渲染(ScriptableRenderContext context, Camera camera){context.SetupCameraProperties(照相机);var buffer = new CommandBuffer();context.DrawSkybox(照相机);context.Submit();}}我们可以通过其ExecuteCommandBuffer方法指示上下文执行缓冲区。再一次,这不会立即执行命令,而是将它们复制到上下文的内部缓冲区。var buffer = new CommandBuffer();context.ExecuteCommandBuffer(缓冲液);命令缓冲区声称资源将其命令存储在本机级别Unity引擎。如果我们不再需要这些资源,最好发布它们立即。这可以通过直接调用缓冲区的Release方法来完成调用ExecuteCommandBuffer。var buffer = new CommandBuffer();context.ExecuteCommandBuffer(缓冲液);buffer.Release();执行空命令缓冲区什么都不做。我们添加它以便我们可以清除渲染目标,以确保渲染不受绘制的内容的影响早。这可以通过命令缓冲区实现,但不能直接通过上下文实现。
第11页
可以通过调用ClearRenderTarget将清除命令添加到缓冲区。它需要三个参数:两个布尔值和一个颜色。第一个参数控制是否清除了深度信息,第二个是否清除了颜色,如果使用的话,第三种是清晰的颜色。例如,让我们清除深度数据,忽略颜色数据,并使用 颜色.clear作为鲜明的色彩。var buffer = new CommandBuffer();buffer.ClearRenderTarget(true,false,颜色。明确);context.ExecuteCommandBuffer(缓冲液);buffer.Release();框架调试器现在将向我们显示执行命令缓冲区清除渲染目标。在这种情况下,它表明Z和模板被清除。ž指深度缓冲区,模板缓冲区总是被清除。清除深度和模板缓冲区。每个摄像机通过其清晰的标志和背景颜色配置清除的内容。我们可以使用那些而不是硬编码我们如何清除渲染目标。CameraClearFlags clearFlags = camera.clearFlags;buffer.ClearRenderTarget((clearFlags&CameraClearFlags.Depth)!= 0,(clearFlags&CameraClearFlags。颜色)!= 0,camera.backgroundColor);清除标志如何工作?CameraClearFlags是一个枚举,可以用作一组位标志。每一点该值用于指示是否启用某个功能。要从整个值中提取位标志,请使用将值与所需标志组合按位AND运算符&。如果结果不为零,则设置标志。因为我们没有给命令缓冲区命名,所以调试器会显示默认名称,即未命名的命令缓冲区。我们用相机的名字吧相反,通过将其分配给缓冲区的name属性。我们将使用对象初始化器语法来做到这一点。
第12页
var buffer = new CommandBuffer {name = camera.name};使用摄像头名称作为命令缓冲区。对象初始化器语法如何工作?我们也可以编写buffer.name = camera.name; 作为一个单独的声明之后调用构造函数。但是在创建新对象时,您可以附加代码块到构造函数的调用。然后,您可以在中设置对象的字段和属性阻止,而不必显式引用对象实例。此外,它明确表示只有在设置了这些字段和属性后才能使用实例。除此之外,它允许只允许一个语句的初始化,不需要具有许多参数变体的构造函数。注意,我们省略了构造函数调用的空参数列表,即允许使用对象初始化程序语法时。2.4 剔除我们能够渲染天空盒,但还没有我们放入的任何物体现场。而不是渲染每个对象,我们只会渲染那些对象相机可以看到。我们通过从场景中的所有渲染器开始然后再这样做剔除那些落在相机视锥外的那些。什么是渲染器?它是附加到游戏对象的组件,可以将它们变成可以的东西渲染。通常,MeshRenderer组件。
第13页
找出可以剔除的内容需要我们跟踪多个相机设置和矩阵,我们可以使用 ScriptableCullingParameters结构。代替我们自己填写,我们可以将该工作委托给静态CullResults .GetCullingParameters 方法。它需要一个摄像头作为输入并产生剔除参数作为输出。但是,它不返回参数struct。相反,我们必须提供它作为第二个输出参数,在它前面写出来。void 渲染(ScriptableRenderContext context, Camera camera){ScriptableCullingParameters cullingParameters;CullResults .GetCullingParameters(camera,out cullingParameters);...}为什么我们要写出来?结构是值类型,因此它们被视为简单值。它们不是具有的对象身份,变量和字段仅保存对其在内存中的位置的引用。所以将struct作为参数传递提供了一个带有该值副本的方法。该方法可以更改副本,但这对复制的值没有影响。当struct参数被定义为输出参数时,它就像一个对象引用,但指向参数所在的内存堆栈上的位置。当方法更改该参数时,它会影响该值,而不是副本。该出关键字告诉我们,方法负责正确设置参数,替换以前的值。除了输出参数,GetCullingParameters还返回它是否能够创建有效参数。并非所有相机设置都有效,导致退化结果不能用于剔除。所以如果它失败了,我们就没有任何东西可以渲染了可以退出渲染。if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}一旦我们有了剔除参数,我们就可以用它们来剔除。这是通过调用静态 CullResults.Cull方法同时具有剔除参数和上下文作为参数。结果是CullResults结构,包含信息关于什么是可见的。
第14页
在这种情况下,我们必须提供剔除参数作为参考参数在它前面写ref。if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}CullResults剔除=CullResults .Cull(ref cullingParameters,context);为什么我们要写ref?它的工作原理就像出,除了在这种情况下分配的东西是不需要的方法价值。并且调用该方法的人负责正确初始化价值第一。因此它可以用于输入,也可以用于输出。为什么ScriptableCullingParameters是一个结构?这可能是一次优化尝试,这个想法是你可以创建多个参数结构,而不必担心内存分配。然而,ScriptableCullingParameters对于一个struct来说非常大,这就是为什么一个引用此处使用参数,再次出于性能原因。也许它开始很小但却在增长随着时间的推移变成一个巨大的 可重用的对象实例现在可能是更好的方法,但是我们必须使用Unity Technologies决定使用的任何东西。2.5 绘图一旦我们知道什么是可见的,我们就可以继续渲染这些形状。这是通过在上下文中调用DrawRenderers来完成,使用cull.visibleRenderers作为参数,告诉它使用哪些渲染器。除此之外,我们必须提供平局设置和过滤器设置。两者都是结构 - DrawRendererSettings和FilterRenderersSettings - 我们最初会使用它们的默认值。平局设置必须作为参考传递。buffer.Release();var drawSettings = new DrawRendererSettings();var filterSettings = new FilterRenderersSettings();context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);context.DrawSkybox(照相机);
第15页
为什么 FilterRenderersSettings而不是FilterRendererSettings?不知道。也许这是一个错字。我们还没有看到任何对象,因为默认的过滤器设置不包含任何内容。我们可以通过提供真实作为参数来包含所有内容FilterRenderersSettings构造函数。这告诉它初始化自己,所以它包括一切。var filterSettings = new FilterRenderersSettings(真);此外,我们必须通过为其构造函数提供来配置绘图设置相机和着色器作为参数传递。相机用于设置排序和剔除图层,而着色器传递的传递控件用于渲染。着色器传递通过字符串标识,该字符串必须包装在ShaderPassName中结构。由于我们只支持管道中未点亮的材料,我们将使用Unity默认的unlit pass,用SRPDefaultUnlit标识。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“SRPDefaultUnlit”));不透明的球体是可见的。我们看到不透明的不透明形状出现,但不是透明的。然而帧调试器表示也会绘制不亮的形状。所有未点亮的渲染器都被绘制出来。
第16页
它们确实被绘制,但因为透明着色器传递不会写入深度缓冲区最终被天空盒吸引。解决方案是推迟绘制透明渲染器,直到天空盒之后。首先,将天空盒之前的绘制限制为仅使用不透明的渲染器。这是通过将过滤器设置的renderQueueRange设置为RenderQueueRange .opaque,其中覆盖从0到2500(包括2500)的渲染队列。var filterSettings = new FilterRenderersSettings(真){renderQueueRange = RenderQueueRange .opaque};仅绘制不透明的渲染器。接下来,将队列范围更改为RenderQueueRange.transparent - 从2501到和渲染天空盒后包括5000-再渲染。var filterSettings = new FilterRenderersSettings(真){renderQueueRange = RenderQueueRange .opaque};context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);context.DrawSkybox(照相机);filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);
第17页
不透明,天空盒,然后透明。我们在天空盒之前绘制不透明的渲染器以防止透支。作为形状将永远在天空盒前面,我们首先通过绘制它们来避免工作。那是因为不透明着色器传递写入深度缓冲区,用于跳过以后绘制的任何东西都会越走越远。除了覆盖天空的一部分,不透明的渲染器也可能最终遮挡每个其他。理想情况下,只为每个片段绘制最靠近相机的那个帧缓冲区。因此,为了尽可能减少透支,我们应该画出最近的形状。这可以通过在绘制之前对渲染器进行排序来完成,这是通过排序标志控制的。绘图设置包含类型的排序结构DrawRendererSortSettings,其中包含排序标志。在绘制不透明之前将其设置为SortFlags .CommonOpaque形状。这指示Unity按照从前到后的距离对渲染器进行排序,加上一些其他标准。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“SRPDefaultUnlit”));drawSettings.sorting.flags = SortFlags .CommonOpaque;但是,透明渲染的工作方式不同 它结合了什么的颜色用以前绘制的东西绘制,结果看起来是透明的。这需要从后到前的反向绘制顺序。我们可以用SortFlags.CommonTransparent 为此。
第18页
context.DrawSkybox(照相机);drawSettings.sorting.flags = SortFlags .CommonTransparent;filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);我们的管道现在能够渲染不透明和透明的不亮物体正确。
第19页
3 抛光能够正确渲染只是拥有功能管道的一部分。有其他要考虑的事情,比如它是否足够快,不分配不需要的临时对象,并与Unity编辑器很好地集成。3.1 内存分配让我们检查一下我们的管道在内存管理方面是否表现良好,或者是否它每帧都分配内存,这将触发频繁的内存垃圾集合运行。这可以通过Window / Analysis / Profiler打开探查器来完成并在层次结构模式下检查CPU使用率数据。虽然你可以在游戏中做到这一点在编辑器中,通过确保创建来分析构建也是一个好主意开发构建并将其自动附加到分析器,尽管很深在这种情况下,不可能进行分析。按GC Alloc排序,你会发现每帧都确实分配了内存。一些它不受我们控制,但在我们的管道中分配了相当多的字节渲染方法。事实证明,剔除分配的内存最多。原因是这样的虽然 CullResults是一个结构,它包含三个列表,它们是对象。每一次我们要求一个新的剔除结果,我们最终为新列表分配内存。所以没有好处 CullResults是一个结构。幸运的是,CullResults有一个替代的Cull方法,它接受一个结构作为引用参数,而不是返回一个新参数。这使得重用成为可能清单。我们所要做的就是将剔除变成一个字段并将其作为附加信息提供CullResults的参数.Cull,而不是分配给它。CullResults剔除;...void 渲染(ScriptableRenderContext context, Camera camera){...// CullResults cull = CullResults.Cull(ref cullingParameters,context);CullResults .Cull(ref cullingParameters,context,ref cull);...}
第20页
连续内存分配的另一个来源是我们使用相机的名称属性。每次我们得到它的值,它从本机代码中获取名称数据,这需要创建一个新的字符串,这是一个对象。所以,让我们永远将命令缓冲区命名为Render Camera。var buffer = new CommandBuffer(){name = “渲染相机”};使用常量缓冲区名称。最后,命令缓冲区本身也是一个对象。幸运的是,我们可以创建一个命令缓冲区一次并重用它。用cameraBuffer替换局部变量领域。由于对象初始化语法,我们可以创建一个命名命令缓冲区作为其默认值。唯一的另一个变化是我们必须清除命令缓冲区而不是释放它,我们可以使用它的Clear方法。CommandBuffer cameraBuffer = new CommandBuffer {name = “渲染相机”};...void 渲染(ScriptableRenderContext context, Camera camera){...// var buffer = new CommandBuffer(){// name =“渲染相机”//};cameraBuffer.ClearRenderTarget(true,false,Color .clear);context.ExecuteCommandBuffer(cameraBuffer);//buffer.Release();cameraBuffer.Clear();...}在这些更改之后,我们的管道不再每帧都创建临时对象。3.2 帧调试器采样
第21页
我们可以做的另一件事是改进帧调试器显示的数据。Unity的管道显示了事件的嵌套层次结构,但我们的管道都是根水平。我们可以使用命令缓冲区来开始和结束来构建层次结构分析器样本。让我们从立即调用ClearRenderTarget之前的BeginSample开始然后调用EndSample。每个样本必须同时包含一个开头和一个结束,两者必须提供完全相同的名称。除此之外,我有发现最好使用与定义的命令缓冲区相同的名称采样。无论如何,命令缓冲区的名称经常被使用。cameraBuffer.BeginSample(“渲染相机”);cameraBuffer.ClearRenderTarget(true,false,Color .clear);cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();采样创建了一个层次结构。我们现在看到一个渲染相机级别嵌套在原始的渲染相机中命令缓冲区,它又包含清除操作。但是有可能去更进一步,将与相机相关的所有其他动作嵌套在其中。这个要求我们延迟样品的结束,直到我们提交样品上下文。所以我们只需要在那一点插入一个额外的ExecuteCommandBuffer包含结束样本的指令。为此使用相同的命令缓冲区,我们完成后再次清除它。cameraBuffer.BeginSample(“渲染相机”);cameraBuffer.ClearRenderTarget(true,false,Color .clear);//cameraBuffer.EndSample("Render Camera“);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();...cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();context.Submit();
第22页
嵌套采样。除了clear动作嵌套在冗余Render中之外,这看起来还不错摄像机级别,而所有其他操作都直接位于根级别之下。我不确定为什么会发生这种情况,但可以通过清除后开始采样来避免。//cameraBuffer.BeginSample("Render Camera“);cameraBuffer.ClearRenderTarget(true,false,Color .clear);cameraBuffer.BeginSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();没有多余的嵌套。3.3 渲染默认管道因为我们的管道只支持不亮的着色器,使用不同着色器的对象没有渲染,使它们看不见。虽然这是正确的,但它隐藏了这样一个事实某些对象使用错误的着色器。如果我们想象这些对象会很好使用Unity的错误着色器,因此它们显示为明显不正确的洋红色形状。让我们为此添加一个专用的DrawDefaultPipeline方法,带有一个上下文和一个相机参数。在绘制透明形状后,我们将在最后调用它。
第23页
void 渲染(ScriptableRenderContext context, Camera camera){...drawSettings.sorting.flags = SortFlags .CommonTransparent;filterSettings.renderQueueRange = RenderQueueRange。透明;context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);DrawDefaultPipeline(上下文,相机);cameraBuffer.EndSample(“渲染相机”);context.ExecuteCommandBuffer(cameraBuffer);cameraBuffer.Clear();context.Submit();}void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){}Unity的默认表面着色器有一个用作第一个的ForwardBase传递前向渲染通道。我们可以使用它来识别具有材料的对象适用于默认管道。通过新的绘制设置选择该传递并使用它用于渲染,以及新的默认过滤器设置。我们不关心排序或者分离不透明和透明的渲染器,因为它们无论如何都是无效的。void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“ForwardBase”));var filterSettings = new FilterRenderersSettings(真);context.DrawRenderers(cull.visibleRenderers,ref drawSettings,filterSettings);}渲染前向基础传球。现在显示使用默认着色器的对象。他们也可以看到帧调试器。
第24页
一切都被吸引了。因为我们的管道不支持前向基础传递,所以它们不会被渲染正确。没有设置必要的数据,所以依赖于照明的一切都结束了像黑色一样。相反,我们应该使用错误着色器渲染它们。要做到这一点,我们需要一个错误材料。为此添加字段。然后,在DrawDefaultPipeline的开头,如果错误材料尚不存在,请创建错误材料。这是通过检索来完成的通过隐藏/ InternalErrorShader 着色 .Find,然后创建与新材料着色器。另外,将材质的隐藏标志设置为HideFlags.HideAndDontSave所以它没有显示在项目窗口中,不会与所有其他资产一起保存。材料错误材料 ;...void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){if (errorMaterial == null ){着色器 errorShader =着色器.Find(“Hidden / InternalErrorShader” );errorMaterial = 新材料(errorShader){hideFlags = HideFlags.HideAndDontSave};}...}绘图设置的一个选项是覆盖渲染时使用的材质调用SetOverrideMaterial。它的第一个参数是要使用的材料。它的第二个parameter是要用于渲染的材质着色器的过程的索引。由于错误着色器只有一次传递,因此使用零。var drawSettings = new DrawRendererSettings(相机,新的ShaderPassName(“ForwardBase”));drawSettings.SetOverrideMaterial(errorMaterial,0);
第25页
使用错误着色器。使用不受支持的材料的对象现在清楚地显示为不正确。但是这个仅适用于Unity的默认管道的材质,其着色器具有ForwardBase通过。还有其他内置着色器,我们可以识别不同的通道,特别是PrepassBase,Always,Vertex,VertexLMRGBM和VertexLM。幸运的是,可以通过调用向绘图设置添加多个传递SetShaderPassName 。名称是此方法的第二个参数。它的第一个parameter是一个索引,用于控制绘制通道的顺序。我们不关心那个,所以任何订单都没问题。通过构造函数提供的传递始终具有索引零,只需增加附加传递的索引。var drawSettings = new DrawRendererSettings(相机,新的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);这涵盖了Unity提供的所有着色器到目前为止,这应该足够了帮助指出在创建场景时使用不正确的材料。但我们只在开发期间需要打扰这样做,而不是在构建中。所以我们只调用它编辑器中的 DrawDefaultPipeline 。一种方法是添加条件属性为方法。
第26页
3.4 条件代码执行该 条件属性在System.Diagnostics名称空间中定义。我们可以用那个命名空间,但不幸的是它还包含一个与之冲突的Debug类型UnityEngine。调试。由于我们只需要属性,我们可以通过使用来避免冲突别名。我们使用特定类型并分配它,而不是使用整个命名空间到有效的类型名称。在这种情况下,我们将Conditional定义为别名System.Diagnostics.ConditionalAttribute 。使用UnityEngine;使用UnityEngine.Rendering;使用UnityEngine.Experimental.Rendering;使用Conditional = System.Diagnostics.ConditionalAttribute;将属性添加到我们的方法。它需要一个指定a的字符串参数符号。如果在编译期间定义了符号,则获取方法调用正常包括在内。但是如果没有定义符号,那么调用它方法 - 包括其所有参数 - 被省略。就好像DrawDefaultPipeline(上下文,相机); 编译期间代码不存在。要仅在编译Unity编辑器时包含调用,我们必须依赖在UNITY_EDITOR符号上。[ 有条件的(“UNITY_EDITOR”)]void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){...}我们可以更进一步,也只包括在开发构建中的调用从发布版本中排除它。为此,请添加一个附加条件DEVELOPMENT_BUILD符号。[ 有条件的(“DEVELOPMENT_BUILD”),有条件的(“UNITY_EDITOR”)]void DrawDefaultPipeline(ScriptableRenderContext context,相机相机){...}3.5 场景窗口中的UI到目前为止我们还没有考虑过的一件事是Unity的游戏内UI。去测试它,通过GameObject / UI /向场景添加UI元素,例如单个按钮按钮。这创建了一个带有按钮的画布,还有一个事件系统。
第27页
事实证明,UI在游戏窗口中呈现,而我们不必这样做任何东西。Unity为我们照顾它。框架调试器显示UI获取单独渲染,作为叠加层。UI在屏幕空间中绘制。至少,当画布设置为在屏幕空间中渲染时就是这种情况。设置为时在世界空间中渲染,UI与其他透明对象一起渲染。在世界空间的UI。尽管UI在游戏窗口中起作用,但它不会显示场景窗口。UI始终存在于场景窗口的世界空间中,但我们必须手动完成将它注入场景。通过调用静态来添加UIScriptableRenderContext.EmitWorldGeometryForSceneView 方法,与当前相机作为一个论点。这必须在剔除之前完成。
第28页
if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}ScriptableRenderContext.EmitWorldGeometryForSceneView(照相机);CullResults .Cull(ref cullingParameters,context,ref cull);但这也在游戏窗口中第二次添加了UI。为了防止这种情况,我们必须仅在渲染场景窗口时发出UI几何体。情况就是这样当摄像机的cameraType等于CameraType .SceneView。if (camera.cameraType ==CameraType .SceneView){ScriptableRenderContext .EmitWorldGeometryForSceneView(camera);}这有效,但仅限于编辑器。条件编译确保了编译构建时, EmitWorldGeometryForSceneView 不存在,这意味着我们现在在尝试构建时遇到编译器错误。为了让它再次发挥作用,我们必须使调用EmitWorldGeometryForSceneView条件的代码也是有条件的。这是通过将代码放在#if和#endif语句之间来完成的。该#如果声明需要一个符号,就像 条件属性。通过使用UNITY_EDITOR,仅在编辑编辑器时才包含的代码。void 渲染(ScriptableRenderContext context, Camera camera){ScriptableCullingParameters cullingParameters;if (!CullResults.GetCullingParameters(camera,out cullingParameters)){回归;}#if UNITY_EDITORif (camera.cameraType ==CameraType .SceneView){ScriptableRenderContext .EmitWorldGeometryForSceneView(camera);}#万一CullResults .Cull(ref cullingParameters,context,ref cull);...}下一个教程是Custom Shaders。知识库
第29页
享受教程?它们有用吗?想要更多?请在Patreon上支持我!或者直接捐款!由Jasper Flick制作
暂无关于此日志的评论。