前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础渲染系列(十九)——GPU实例(Instancing)

基础渲染系列(十九)——GPU实例(Instancing)

作者头像
放牛的星星
发布2020-07-10 17:33:57
10K2
发布2020-07-10 17:33:57
举报
文章被收录于专栏:壹种念头壹种念头

本文重点:

1、渲染非常多的球体 2、添加GPU Instancing支持 3、使用material property blocks 4、让instancing和LODgroups共存

这是渲染系列的第19篇教程。上一章节涵盖了 realtime GI, probe volumes, 和LOD groups,这一节我们来试一下另外一种缩减DrawCall的方法,合批。

该教程使用Unity 2017.1.0f3制作。

(数千个球体,只用了极少的批次)

1、合并实例

指示GPU绘制图像需要花费时间。为其提供数据(包括网格和材质属性)也需要时间。我们已经知道有两种方法可以减少绘制调用的数量,即静态和动态批处理。

Unity可以将静态对象的网格合并为更大的静态网格,从而减少draw calls。但只有使用相同材质的对象才能以这种方式组合,它是以存储更多网格数据为代价的。启用动态批处理后,Unity在运行时会对视图中的动态对象执行相同的操作。但仅适用于小型网格,否则会适得其反,开销反而变得非常大。

还有另一种组合绘图调用的方法。被称为GPUinstancing 或几何instancing 。与动态批处理一样,此操作在运行时针对可见对象完成。这个想法是让GPU一次性渲染同一网格多次。因此,它不能组合不同的网格或材质,但不局限于小网格。这里我们将试试这个方法。

1.1 很多的球体

要测试GPU instancing,我们需要渲染同一个网格很多次。首先我们来创建一个简单的球体prefab,这里先设置为白色的材质。

(白色的球体预置)

要实例化此球体,先创建一个测试组件,该组件会多次生成预制件并将其随机放置在球形区域内。让实例化产生的球体放置在它的子层级下,这样编辑器的层次结构窗口就不用显示数千个Instance实例而耗费性能了。

创建一个新场景,并使用此组件将测试对象放入其中。将球预制件分配给它。我将使用它在半径为50的球形范围内创建5000个球实例。

(测试对象)

将测试对象放置在原点处,将相机放置在(0,0,-100)处,可以确保看到整个球体。现在,我们可以使用游戏窗口的统计面板来确定如何绘制所有对象。关闭主光源的阴影,以便仅绘制球体以及背景。再将相机设置为使用forward rendering路径。

(球形范围的大量球体实例)

在刚才的示例中,它需要5002次DC来渲染视图,在统计面板中称为“Batches”。那是5000个球体,外加两个额外的背景和相机效果。请注意,即使启用了动态批处理,也不会批处理这些球。那是因为球体网格太大。如果我们改用立方体的话,它们将会被批处理。

(球形范围的大量立方体实例)

对于立方体,我们只用了8个批次,因此所有的立方体渲染实际上只占用了6个批次。一共减少了4994个批次调用,这个值可以在Saved by batching 下看到。就本示例而言,它还记录了更高的帧率。比如83而不是使用球体的35。fps是对渲染帧时间的度量,而不是实际帧率,但这仍然是性能差异的良好指标。立方体的绘制速度更快,因为它们是批处理的,而且还因为立方体比球体所需的网格数据少得多。因此,这不是一个公平的比较。

由于编辑器自身会产生大量开销,因此构建中的性能差异可能会更大。尤其是场景窗口会使渲染放慢很多,因为这是必须渲染的额外视图。在播放模式下,我将其隐藏以提高性能。

1.2 支持实例化(Instancing)

默认情况下,还无法进行GPU实例化。必须设计着色器来支持它。我们需要给每种材质显式的启用实例化。Unity的标准着色器对此有一个开关。我们也向MyLightingShaderGUI添加实例化的开关。像标准着色器的GUI一样,我们将为其创建“Advanced Options”部分。可以通过调用MaterialEditor.EnableInstancingField方法来添加开关。在一个新的DoAdvanced方法里添加逻辑吧。

把这个部分添加到我们GUI的底部。

选择白色材质。现在,一个Advanced Options标题在其检查器的底部可见。但是,还没有控制实例化的开关。

(现在尚不支持 实例化)

仅当着色器实际支持实例化时,才会显示该开关。我们可以通过将#pragma multi_compile_instancing指令添加到着色器来启用此支持。这将为一些关键字启用着色器变体,在我们的示例中为INSTANCING_ON,但其他关键字也是可以的。为“My First Lighting”的base pass执行此操作。

(支持和开启 实例化)

我们的材质现在具有“Enable Instancing”开关。打开将改变球体的渲染方式。

(每一个批次只有一个位置)

在现在的示例下,批处理数量已减少到42,这意味着现在仅用40个批处理即可渲染所有5000个球体。帧率也高达80 fps,但是只有几个球体可见。

实际上所有5000个球体都在渲染,只是同一批中的所有球体都位于同一位置。它们都使用批次中第一个球的转换矩阵。发生这种情况是因为现在一批中所有球体的矩阵都作为数组发送到GPU。在不告知着色器要使用哪个数组索引的情况下,它始终使用第一个索引。

1.3 实例 Ids

与实例相对应的数组索引称为其实例ID。GPU通过顶点数据将其传递到着色器的顶点程序。在大多数平台上,它是一个无符号整数,名为instanceID,具有SV_InstanceID语义。我们可以简单地使用UNITY_VERTEX_INPUT_INSTANCE_ID宏将其包含在我们的VertexData结构中。它在UnityCG中包含的UnityInstancing中定义。它为我们提供了实例ID的正确定义,或者在未启用实例化时不提供任何内容。将其添加到“My Lighting”中的VertexData结构。

启用实例化后,我们现在可以在顶点程序中访问实例ID。有了它,就可以在变换顶点位置时使用正确的矩阵。但是,UnityObjectToClipPos没有矩阵参数。它始终使用unity_ObjectToWorld。要解决此问题,UnityInstancing包含文件会使用使用矩阵数组的宏覆盖unity_ObjectToWorld。这可以被认为是一种宏的

Dirty Hack,但它无需更改现有着色器代码即可工作,从而确保了向后兼容性。

(Dirty Hack:以不符合设计原理 不易维护 不易调整 不够健壮 不够美观的方式解决问题,https://www.zhihu.com/question/20372589)

要使Hack工作,实例的数组索引必须对所有着色器代码全局可用。我们通过UNITY_SETUP_INSTANCE_ID宏进行手动设置,该宏必须在顶点程序中完成,然后再执行任何可能需要它的代码。

(实例化的球体)

着色器现在可以访问所有实例的变换矩阵,因此球体将在其实际位置进行渲染。

矩阵数组替换是怎么起作用的?

在最简单的情况下,启用实例化可以总结为这一点。

UnityInstinging中的实际代码要复杂得多。它处理平台的差异,其他使用实例的方式,以及立体渲染的特殊代码,这导致了间接定义的多个步骤。它还必须重新定义UnityObjectToClipPos,因为UnityCG首先包含UnityShaderUtilities 。

稍后将解释缓冲区宏。

1.4 合批大小

你最终得到的批次数量可能与我得到的数量不同。在我的情况下,以40批渲染5000个球体实例,这意味着每批125个球体。

每个批次都需要自己的矩阵数组,此数据发送到GPU并存储在内存缓冲区中,在Direct3D中称为常量缓冲区,在OpenGL中称为统一(uniform)缓冲区。这些缓冲区具有最大容量限制,它限制了一个批次中可以容纳多少个实例。假设台式机GPU每个缓冲区的限制为64KB。

一个矩阵由16个浮点数组成,每个浮点数均为4个字节。因此,每个矩阵64个字节。每个实例都需要一个对象到世界的转换矩阵。但是,我们还需要一个世界到对象的矩阵来转换法线向量。因此,最终每个实例有128个字节。这导致最大批处理大小为 64000/128 = 500,能在10个批处理中渲染5000个球体。

最大值不是512吗?

内存的计量是2进制,不是10进制所以1KB代表1024个bytes。所以64*1024/128=512。

默认情况下,UNITY_INSTANCED_ARRAY_SIZE定义为500,但是你可以使用编译器指令覆盖它。例如,#pragma instancing_options maxcount:512将最大值设置为512。但是,这会将导致断言失败的错误,因此实际限制为511。其实500和512之间没有太大差异。

尽管台式机的最大容量为64KB,但假定大多数移动设备的最大容量仅为16KB。Unity通过在针对OpenGL ES 3,OpenGL Core或Metal时将最大值除以四来解决此问题。因为我在编辑器中使用的是OpenGL Core,所以最终的最大批处理大小为 500/4 = 125。

你可以通过添加编译器指令#pragma instancing_options force_same_maxcount_for_gl来禁用该自动减少功能。多个实例化选项组合在同一指令中。但是,这可能会导致在部署到移动设备上时发生问题,因此需要小心使用。

那assumeuniformscaling选项呢?

你可以使用#pragma instancing_options假定统一缩放来指示所有实例对象具有统一的缩放比例。这消除了将世界到对象矩阵用于法线转换的需要。设置此选项后,虽然UnityObjectToWorldNormal函数确实会更改其行为,但它不会消除第二个矩阵数组。因此,在Unity 2017.1.0以前,此选项实际上没有任何作用。

1.5 实例化阴影

到目前为止,我们还没有阴影。重新打开主阴影的柔和阴影,并确保阴影距离足以包含所有球体。当相机位于-100且球体的半径为50时,阴影距离150对我来说足够了。

(很多的阴影)

为5000个球体渲染阴影会给GPU造成巨大损失。但是我们也可以在渲染球体阴影时使用GPU实例化。将所需指令添加到阴影caster pass中。

再将UNITY_VERTEX_INPUT_INSTANCE_ID和UNITY_SETUP_INSTANCE_ID添加到“My Shadows”中。

(实例化阴影)

现在批次有了大幅度的降低。

1.6 多灯光

我们仅在base pass和shadow caster pass中添加了实例化支持。因此,批处理不适用于其他光源。要验证这一点,请停用主光源并添加一些会影响多个球体的聚光灯或点光源。但不要为它们打开阴影,因为那样会降低帧率。

(多灯光会导致渲染性能急速下降)

事实证明,不受额外光照影响的球体仍与阴影一起进行批处理。但是其他区域甚至没有在其base pass中分批处理。对于这些情况,Unity完全不支持批处理。要将实例化与多个光源结合使用,现在别无选择,只能切换到deferred rendering 路径。为此,请将所需的编译器指令添加到着色器的deferred pass中。

(延迟光照下的多灯光表现)

在确认它可以用于延迟渲染后,切换回正向渲染模式。

2 混合材质属性

所有批处理形式的限制之一是它们仅限于具有相同材质的对象。当我们希望渲染的对象具有多样性时,此限制就会成为阻碍。

2.1 随机颜色

例如,当我们改变球体的颜色。创建每个实例的材质后,为其分配随机颜色。这将隐式创建共享的材质副本,因此最终在内存中有5000个材质实例。

(随机颜色的球体,没有阴影和合批)

即使我们为材质启用了批处理,它也不再起作用。关闭阴影可以更清楚地看到这一点。我们回到每个球体一次抽DC。而且由于每个球体现在都有自己的材质,因此每个球体的着色器状态也必须更改。这在统计面板中显示为SetPass Calls。它曾经是所有的球体共用一个,但是现在是5000。结果,我的帧率下降到了10fps。

2.2 材质属性块

除了使用每个球体创建新的材质实例外,我们还可以使用材质属性块。这些是小的对象,其中包含着色器属性的重写。设置属性块的颜色并将其传递给球体的渲染器,而不是直接分配材质的颜色。

MeshRenderer.SetPropertyBlock方法复制该块的数据,因此不依赖于我们在本地创建的块。这使我们可以重用一个块来配置所有实例。

进行此更改后,我们将返回所有球体的SetPassCall。但它们又是白色的。这是因为GPU尚不知道该属性的重写。

2.3 Property Buffers

渲染实例对象时,Unity通过将数组上传到其内存来使转换矩阵可用于GPU。Unity对存储在材料属性块中的属性执行相同的操作。但这要起作用的话,必须在“My Lighting”中定义一个适当的缓冲区。

声明实例化缓冲区的工作类似于创建诸如插值器之类的结构,但是确切的语法因平台而异。我们可以使用UNITY_INSTANCING_CBUFFER_START和UNITY_INSTANCING_CBUFFER_END宏来解决差异。启用实例化后,它们还不会做任何操作。

将_Color变量的定义放在实例缓冲区中。UNITY_INSTANCING_CBUFFER_START宏需要一个名称参数。实际名称无关紧要。宏以UnityInstancing_为其前缀,以防止名称冲突。

像变换矩阵一样,启用实例化后,颜色数据将作为数组上传到GPU。UNITY_DEFINE_INSTANCED_PROP宏会为我们处理正确的声明语法。

要访问片段程序中的数组,我们还需要在其中知道实例ID。因此,将其添加到interpolator 结构中。

在顶点程序中,将ID从顶点数据复制到interpolators。启用实例化时,UNITY_TRANSFER_INSTANCE_ID宏定义此简单操作,否则不执行任何操作。

在片段程序的开头,使ID全局可用,就像在顶点程序中一样。

现在,我们必须在不使用实例化时以_Color的形式访问颜色,而在启用实例化时以_Color [unity_InstanceID]的形式访问颜色。我们可以为此使用UNITY_ACCESS_INSTANCED_PROP宏。

它为什么不编译,或者为什么Unity更改我的代码?

自Unity 2017.3起,UNITY_ACCESS_INSTANCED_PROP宏已更改。现在,它要求您提供缓冲区名称作为第一个参数。代替使用UNITY_ACCESS_INSTANCED_PROP(_Color),请使用UNITY_ACCESS_INSTANCED_PROP(InstanceProperties,_Color)

(合批的带颜色的球体)

现在,我们的颜色随机的球再次被批处理。我们可以用相同的方式使其他属性可变。对于颜色,浮点数,矩阵和四分量浮点向量,这是可以的。如果要改变纹理,可以使用单独的纹理数组,并将索引添加到实例化缓冲区。

可以在同一个缓冲区中组合多个属性,但要牢记大小限制。还应注意,缓冲区被划分为32位块,因此单个浮点数需要与向量相同的空间。您也可以使用多个缓冲区,但是也有一个限制,它们不是免费提供的。启用实例化后,每个要缓冲的属性都将成为一个数组,因此仅对需要根据实例变化的属性执行此操作。

2.4 阴影

我们的阴影也取决于颜色。调整“My Shadows”,以便每个实例也可以支持唯一的颜色。

2.5 LOD Instancing

上次,我们增加了对LOD组的支持。让我们看看它们是否与GPU实例兼容。使用LOD组创建一个新的预制件,该LOD组仅包含一个包含白色材质的球体。将其设置为Cross Fade并进行配置,以使LOD 0在过渡宽度0.25时被剔除为3%。这为我们明显的小球体提供了一个不错的过渡范围。

(LOD 球体预置)

将此预制件关联到我们的测试对象,而不是常规球体。由于此对象本身没有网格渲染器,因此此时进入播放模式时会出现错误。我们必须调整GPUInstancingTest.Start,以便在根对象本身没有渲染器的情况下访问子对象的渲染器。在进行此操作时,请确保它适用于具有任意级别的简单对象和LOD组。

(没有实例化的LOD渐隐,带有阴影)

不幸的是,如果没有有效的批处理,我们现在将获得Fade范围。Unity能够对以相同的LOD褪色因子结束的球进行批处理,但是如果可以像往常一样对它们进行批处理会更好。我们可以通过用缓冲数组替换unity_LODFade来实现。为支持实例化的每个Pass添加lod fade实例化选项来指示Unity的着色器代码执行此操作。

(实例LOD融合)

现在,我们的着色器同时支持最佳实例化和LOD渐变。

下一个教程是 视差([Parallax])。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、合并实例
    • 1.1 很多的球体
      • 1.2 支持实例化(Instancing)
        • 1.3 实例 Ids
          • 1.4 合批大小
            • 1.5 实例化阴影
              • 1.6 多灯光
              • 2 混合材质属性
                • 2.1 随机颜色
                  • 2.2 材质属性块
                    • 2.3 Property Buffers
                      • 2.4 阴影
                        • 2.5 LOD Instancing
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档