本文重点: 设计常规和附加LOD组 交叉淡化LOD不同级别 应用屏幕空间抖动 使用动画抖动模式 剔除没有使用的着色器变体
这是涵盖Unity的可脚本化渲染管道的教程系列的第十期。它增加了对交叉过渡LOD组和着色器变体剥离的支持。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。
本教程使用Unity 2018.4.4f1制作。
(抖动去除细节)
1 剔除细节
理想情况下,我们想让渲染尽可能的少。渲染的次数越少,GPU上的压力就越小,这意味着我们可以获得更高的帧速率,并且需要更少的精力来渲染场景。如果某些东西在视觉上变得很小,以至于不再可见(小于单个像素),那么我们可以跳过渲染。当它们仍然可见时,也可以跳过某些方面,因为很小,以至于缺失的时候几乎不会被注意到。因此,我们可以控制场景的细节级别。
1.1 LOD组
创建 level-of-detail 对象的典型方法是为每个 level-of-detail 使用带有子对象的根对象。最详细或最完整的可视化级别称为LOD0。作为示例,让我们创建一个具有单个球形子节点的预制件。与往常一样,我们使用自己的材质,并使用InstancedMaterialProperties组件为其赋予明显的颜色,例如红色。
(具有各种大小的LOD球面实例的场景)
可以通过将LOD Group组件添加到游戏对象的根目录来控制对象的视觉LOD。默认情况下,它具有三个LOD级别。显示的百分比对应于对象的估计视觉尺寸,表示为垂直覆盖的视口数量。只要保持在60%以上,就使用LOD 0,否则它将切换到较低的LOD级别,直到对象被完全剔除到10%以下为止。将球体子级拖动到LOD 0框上,以便将其渲染的图像用于LOD 0可视化。
(LOD球体预制,已选择LOD 0 盒子)
可以通过拖动阈值来调整阈值,也可以通过右键单击它们来通过弹出菜单添加或删除级别。由于我们只有一个LOD级别,因此请删除其他两个。这意味着我们总是显示球体,直到其视觉尺寸降到10%以下为止。至少,没有LOD偏差时就是这种情况。有一个全局LOD偏差可用于调整所有LOD阈值。可以通过代码和项目设置的“Quality ”面板进行设置。例如,将Lod Bias设置为1.5意味着对象的视觉尺寸被相同的因素高估,因此,当我们的球体下降到6.7%以下时,才将其球体剔除。LOD组的检查器将指示存在偏差。
(10%的时候剔除,LOD偏差为1.5)
1.2 多LOD级别
通常,一个对象具有多个LOD级别,每个级别使用一个逐渐简化的网格。要清楚地看到正在使用的不同LOD级别,请复制球状子对象两次以创建LOD级别1和2,并为每个颜色赋予不同的颜色。然后将它们添加到LOD组,例如以15%和10%的阈值将完全剔除移到5%。
(3个LOD级别 球体)
现在,你可以通过移动摄像机或调整LOD偏置来查看正在选择的LOD。
(调整LOD偏差)
LOD组可以与光照贴图一起使用吗? 是。将LOD组标记为静态时,它仍会在LOD级别之间切换,因此静态批处理不适用于它。但是,它确实包含在灯光映射中。LOD 0用于预期的灯光映射,此外,所有其他LOD级别也将获得烘焙照明。至少在使用渐进式光照贴图的情况下。Enlighten在其他LOD级别上存在更多麻烦,需要使用光探针。这也意味着只有静态LOD 0与动态全局照明配合使用。如果动态GI很重要,则应确保其他LOD级别不是静态的,以便它们通过光探头接收GI。
1.3 增量LOD
创建LOD的另一种方法是将其添加到基本可视化中。作为示例,我用立方体和球体创建了抽象树。树的核心被添加到所有三个LOD级别。将较小的树枝,树叶和树皮添加到前两个级别。并且最小的叶子和树皮详细信息仅添加到LOD 0。
(LOD级别为0,1,2)
这与每个LOD级别使用单独的子层次结构相同,除了某些对象是多个级别的一部分。
(一篇LOD树组成的森林)
2 LOD混合
当一个对象从一个LOD级别切换到另一个LOD级别时,会突然交换或移除渲染器,这在视觉上是十分明显的。通过在相邻的LOD级别之间进行混合,可以使过渡更加平缓。
2.1 交叉淡化
LOD混合是每个LOD组和单个LOD级别的控制器。首先,将Group的Fade Mode 设置为Cross Fade。这将显示“Animate Cross-fading ”切换选项,使你可以在基于百分比或时间的渐变之间进行选择。启用此选项后,将在发生LOD更改时发生基于时间的过渡,即使对象的视觉大小不再更改,该过渡也只会持续很短的时间。过渡持续时间可以通过LODGroup.crossFadeAnimationDuration全局设置,默认为半秒。禁用cross-fade时,交叉淡化将基于视觉百分比,并且可以通过其“Fade Transition Width ”滑块为每个LOD级别配置确切的范围。设置为1时,衰减将覆盖LOD级别的整个范围。这会使过渡最平缓,但也意味着一直使用过渡。最好避免这种情况,因为在过渡期间,需要同时呈现两个LOD级别。
(跨整个LOD范围的淡入淡出)
SpeedTree 淡入淡出模式选项如何? 该模式专门用于SpeedTree树,该树使用其自己的LOD系统折叠树并在3D模型和广告牌表示之间进行转换。
当使用cross-fading时,Unity将使用LOD_FADE_CROSSFADE关键字选择一个着色器变体,因此将其的多编译指令添加到我们的着色器的常规通道中。
要检查是否确实使用了淡入淡出,可以在Lit.hlsl中将所有淡入淡出片段设为纯黑色。
(黑色球)
当所有淡入淡出范围都设置为1时,这将使每个球体变为纯黑色,但那些最终在视觉上大于视口的球除外。相反,使用附加LOD级别的树在相同设置下仅部分为黑色。属于两个LOD级别的对象不包括在淡入淡出中,而是正常渲染。
(部分黑色的树)
通过unity_LODFade向量的第一个分量(该变量是UnityPerDraw缓冲区的一部分),可以控制物体淡化量。
返回该值而不是纯黑色,尽管由于OverDraw我们只能看到每个片段使用两个混合因子之一,但可以看到正在使用的混合因子。从最低LOD级别到被裁剪的过渡仅涉及单个对象,因此在这种情况下不会发生OverDraw。
(混合因子)
2.2 屏幕空间坐标
在透明几何的时候,可以使用混合因子淡出,但是对于不透明几何则不可能。我们可以做的是根据混合因子裁剪片段的一部分,就像CutOut渲染一样。这适用于不透明和透明几何体。但是对于为对象渲染的所有片段,淡入度因子都是相同的,因此仅将其用作剪切的阈值仍会产生突然的过渡。因此,我们必须为每个片段的裁切阈值添加变化。
为每个片段添加变体的最简单方法是将其基于片段的屏幕空间位置。首先直接使用其LitPassFragment的XY分量。
XY坐标作为片段索引提供,因此将使所有内容变为白色。要获得合理的结果,请对屏幕空间位置取一些模,然后将其除以相同的值。让我们使用64。
(屏幕空间UV坐标)
结果是一个网格填充了红绿色渐变方块,每64个像素重复一次。 由于相对于屏幕,即使球体在视觉上发生变化,图案也始终相同。 我们可以使用这些坐标来执行屏幕空间纹理采样。
2.3 裁剪
让我们创建一个单独的方法来基于LOD交叉淡入淡出进行剪辑。在其中,剪切与alpha剪切一样,只是基于渐变因子减去偏差而不是alpha减去截止值。最初,对偏差使用16像素的垂直渐变。
(基于平铺渐变进行裁剪)
我们最终将单条杠切出了球体。在某些情况下,我们可以看到两个LOD级别的一部分,但即使如此,也缺少某些部分。发生这种情况的原因是,当一个LOD级别进行剪辑时,另一个不应该剪辑,但是现在它们是独立的。我们必须使偏差对称,这可以通过在渐变系数降至0.5以下时将其翻转来实现。
(对称偏差)
消除偏差的不利之处在于,现在在中点出现了明显的视觉变化。当分离但视觉上重叠的对象在不同时间翻转时,这也会导致图案干扰。如果对象过渡到被淘汰,它们的视觉交点可能变得完全不透明。
(由于翻转而导致图案不一致)
在Unity为着色器提供其他数据以允许我们确定要渲染的LOD级别之前,我们无法避免这种情况。然后,我们总是可以翻转一侧,而不是两边都做一半。还一种方法是始终使两个渐变因子之一为负,这可能会在Unity 2019的未来版本中完成。
2.4 抖动
使用偏差模式不是个好主意。相反,让我们使用基本一致的噪声纹理来执行抖动,如下。
(64X64的蓝色噪点)
你从哪儿得到那个纹理? 这是克里斯托夫·彼得斯(Christoph Peters)制作的蓝色噪音图案。有关更多详细信息,请参见他的免费蓝噪声纹理博客文章。(http://momentsingraphics.de/BlueNoise.html)
纹理的所有四个通道都包含相同的数据。将其作为未压缩的单通道纹理导入,设置为alpha。还要将其“Filter Mode”设置为“ Point”,因为我们使用精确的像素值并且不需要任何插值。因此,它也不需要mip映射。
(纹理导入设置)
在MyPipelineAsset中添加一个纹理字段,这样我们就可以将抖动模式添加到资产中。
(带有抖动纹理的管线)
然后将其传递给MyPipeline的构造函数调用。
在MyPipeline中,跟踪纹理。
在渲染摄像机之前配置抖动模式。这意味着设置纹理,我们还将全局设置其缩放变换数据。我们假定它是64×64纹理,因此UV比例变为1除以64。我们可以使用摄影机缓冲区执行此操作。
在着色器端,我们简单地将缩放转换添加到UnityPerFrame缓冲区中。还要定义纹理,并使用转换后的屏幕位置对其进行采样,以确定用于交叉淡化的剪辑偏差。
(抖动后的交叉淡化)
由于抖动模式是以窗口的分辨率采样的,因此在高分辨率的显示器和屏幕截图上可能很难看到。你可以按比例放大游戏视图以更好地查看它。
(抖动放大4倍)
为什么使用纹理而不是LODDitheringTransition? 核心库包含LODDitheringTransition函数,该函数根据3D种子值和淡入淡出因子进行裁剪。它使用种子生成哈希值,然后将其用于剪切。尽管基于散列的方法行之有效,但我发现这种特定的实现方式并不可靠,至少在Metal API中,这种情况表现为像素大小的孔和不稳定的结果。HDRP管道将种子基于视图方向,该问题具有使问题更加严重的精度问题,但是将其更改为使用屏幕空间位置并不能解决所有问题。相反,始终使用屏幕空间纹理。
2.5 交叉淡化阴影
我们可以将相同的技术应用于阴影。在剔除期间选择了LOD,因此对象及其阴影的LOD匹配。首先,还将LOD_FADE_CROSSFADE的多编译指令添加到阴影投射器通道中。
然后将所需的数据添加到ShadowCaster.hlsl。
然后复制LODCrossFadeClip并在适当的时候在ShadowCasterPassFragment中调用它。
(抖动的交叉淡化阴影)
在有阴影的情况下,抖动与阴影摄像机对齐。因此,用于定向阴影的抖动模式的移动方式不同于常规摄像机的抖动模式。仅当聚光灯本身移动或旋转时,聚光灯阴影的图案才会更改。但是由于阴影过滤,图案可能会被弄脏。
2.6 动画抖动模式
由于高收缩和大衰减范围,在我们的示例场景中,抖动模式可能非常明显。通常情况下,场景的对比度要低得多,并且使用较小的淡入范围,这会使抖动不太明显。但是,显而易见的是,这可能会分散注意力,尤其是在部分场景移动时,因为模式在视觉上保持固定。可以通过对抖动模式进行动画处理来模糊处理此事实,并及时对其进行有效加扰,从而使其成为易于忽略的噪声。
制作图案动画的直接方法是每帧使用一个新的图案。但是,当帧速率不稳定时,这可能会产生感知上的闪烁;当不将vsync与非常高的帧速率结合使用时,这也会加剧视觉撕裂。我们可以尝试通过对抖动模式使用固定的动画速度来缓解这种情况。为此,向MyPipelineAsset添加一个滑块选项,范围为0–120,默认值为每秒30帧。这也使放慢动画的速度成为可能,以便我们可以更好地对其进行观察。
(抖动动画速度)
将速度添加到构造函数调用中。
无需直接在MyPipeline中跟踪速度,我们只需要记住帧持续时间即可,它是速度的倒数。在构造函数中进行设置,除非速度为零,否则持续时间也保持为零。这样,如果您不喜欢或想要获得准确的结果,可以完全关闭抖动动画,这对于图像比较很有用。
可以通过添加更多纹理并遍历它们来对抖动模式进行动画处理。但是我们也可以使用单个纹理并改为调整其比例变换。那不会产生高质量的动画,但足以满足我们的目的。
如果速度为正,则填充16个ST向量的数组,这将产生足够的唯一帧。
我们将通过每隔两帧水平翻转图案和每两帧垂直翻转图案来创建唯一的帧。然后,我们每4帧水平偏移一次模式,每8帧垂直偏移一次模式。
尽管这产生了16种独特的配置,但调整是有规律的,并且有很多对称性。我们可以通过使用每帧随机偏移量将其分解。为了始终使用相同的帧,我们首先初始化随机状态。我们只使用零作为种子。之后,我们恢复了旧的随机状态,因此我们的管道不会与游戏的其余随机状态混为一谈。
我们不是必须量化偏移量吗? 不需要使偏移量精确为1/64的倍数,因为我们在采样纹理时使用点过滤(point filtering)。
我们不需要将图案动画与游戏时间同步,因此我们将其基于未缩放的时间。同样,我们不在乎动画的定时精确性,只是在不同的图案帧以大致固定的频率出现。如果一帧花费的时间很长,那么我们只需要转到下一个模式,就无需跳过任何帧来使动画与时间保持同步。因此,我们仅需跟踪自上一次模式更改以来已过去了多少时间。如果时间太长,请转到下一个ST索引。
但是,只有在动画帧时长为正时,才需要这样做。而且我们也只需要初始化一次纹理。我们可以通过将ST索引初始设置为-1并基于这两种情况设置一次来实现。
(动画后的抖动,速度为4)
将动画抖动模式与为LOD组启用动画交叉渐变相结合,应使过渡尽可能平滑,尤其是在视觉对比度不太高的情况下。但是,当在编辑器中而不是在播放模式下工作时,仅当发生更改时才渲染新帧。这意味着当我们什么都不做时,抖动模式保持不变,但是当我们执行一项使人分心的动作时突然改变。因此,我们仅在播放模式下对其进行动画处理。这可以通过在构造器中配置动画之前检查Application.isPlaying来完成。
3 着色器变体裁剪
将所有这些功能添加到着色器的不利之处在于,最终会生成许多着色器变体。当使用shader-feature编译器指令时,这是可管理的,因为构建中仅包含已为废料启用的关键字。但是,多重编译指令并不受此限制。
Unity可以根据构建中包含的场景中使用的内容自动从构建中删除一些关键字。在我们的案例中,受影响的关键字是LIGHTMAP_ON,DYNAMICLIGHTMAP_ON和INSTANCING_ON。仍然留下了很多关键字,在每个版本中可能都不需要其中一些。幸运的是,Unity为我们提供了一种从构建中剥离着色器变体的方法。
3.1 预处理着色器
构建完成后,Unity编辑器将查找实现IPreprocessShaders接口的任何类,该类在UnityEditor.Build名称空间中定义。它将创建该类的实例,然后为其提供着色器变体以进行剥离。在“Editor ”文件夹中为此类创建定义。
该接口要求我们实现两件事。首先,一个callbackOrder getter属性返回一个整数。如果有多个,则用于确定预处理器的调用顺序。我们可以简单地返回零。
其次,传递了一个着色器,一个着色器代码段数据以及一个包含有关一组着色器变体信息的编译器数据列表的OnProcessShader方法。首先让它记录着色器的名称。
现在,当我们构建项目时,会记录很多着色器名称。其中包括我们的着色器,但默认情况下还包含许多着色器,你可以通过项目设置的“Graphics ”面板进行管理。由于着色器编译过程破坏了着色器变体的方式,因此也会有很多重复项,但是我们不必担心确切的顺序和分组。
3.2 仅预处理我们的管道
已定义的所有预处理器将用于每次构建。因此,即使我们的预处理器在项目中,即使项目不使用我们的自定义管道,它也将始终被使用。为了确保我们不与其他管道混在一起,我们需要验证当前的管道确实是我们的。为此,我们可以通过GraphicsSettings.renderPipelineAsset检索当前正在使用的管道的资产,并检查其类型是否为MyPipelineAsset。
稍后可以使用管道资产,因此让我们通过在构造方法中初始化一次的字段来对其进行跟踪。
3.3 计数着色器变体
在开始剥离变体之前,让我们首先找出有几个。着色器编译器数据列表中的每个条目都代表一个变体,因此我们必须在OnProcessShader的所有调用中对它们进行求和。
为了每个构建只记录一次,我们可以从UnityEditor.Callbacks命名空间添加具有PostProcessBuild属性的方法。它有一个参数来设置其回调顺序,为此我们将再次使用零。该方法必须具有UnityEditor.BuildTarget参数以及用于存储构建的路径的字符串。构建过程完成后,Unity将调用所有此类方法。
该方法必须是静态的,因此我们还要跟踪预处理器的静态实例,以便我们可以检索计数。我们可以在记录后摆脱实例。
为什么不静态计数器? 这也是可以的,但是稍后我们需要跟踪更多数据。这样可以将所有内容捆绑在一个对象实例中,而我们可以通过一条语句来销毁它。
现在,我们可以看到构建中包含多少个着色器变体。有多少取决于所包含的场景。就我而言,我得到了一个日志条目,内容为“包含3054着色器变体”。最后是表明构建成功的最终构建日志。
3.4 剔除级联阴影
我们可以安全剥离的着色器变体示例是级联阴影的变体。如果我们将管道资产的阴影级联设置为零,那么它们将永远不会被使用,因此不需要包含在构建中。
首先,我们必须使预处理器能够检查管道是否启用了阴影级联。我们可以通过向MyPipelineAsset添加一个公共布尔型getter属性来实现这一点,该属性返回阴影级联是否不为零。
让预处理器使用该属性来确定是否应去除级联的阴影变体。我们可以在构造函数中执行一次此操作并跟踪决策。
要检查变体是否使用了关键字,我们需要为其创建ShaderKeyword结构。对两个级联的shadows关键字执行一次,然后将它们存储在静态字段中。
接下来,创建一个Strip方法,该方法将简单的着色器编译器数据集作为输入,并返回是否应删除该变体。在应删除级联阴影并启用两个相关关键字之一的情况下就是这种情况。可以通过在数据的着色器关键字集上调用IsEnabled进行检查。
现在,我们可以遍历OnProcessShader中的所有数据集,并删除应删除的数据集。在增加着色器变量数之后执行此操作,以便我们跟踪原始计数。
3.5 报告剔除的变体
现在可以从构建中删除对级联阴影的支持,但是我们对此还没有任何反馈。发生这种情况的唯一线索是构建时间和大小减少了。为了清楚地说明构建中包含了多少个着色器变体,还请跟踪剥离了多少个着色器变体。我们可以通过每次变体被剥离时简单地增加剥离数来做到这一点。
记录日志时,请同时注明最终变量和原始变量。顺带一提,我们还记录了所包含变体的百分比。
就我而言,禁用阴影级联时,我得到了“包含3054(50%)个中的1518个着色器变体”。这是一个显着的减少。请注意,我们决定纯粹是根据资产是否已禁用层叠阴影来剥离它们。这意味着,如果启用了它们但未在任何场景中使用它们,则变体仍会包含在构建中。因此,你必须将管道的配置与构建中真正需要的相匹配。
3.6 剔除交叉淡化
作为另一个示例,让我们可以从构建中剥离LOD交叉渐变。此功能不受我们的管道直接控制。它仅在LOD组需要时使用。但是我们仍然可以在MyPipelineAsset中添加一个切换选项,以指示是否应支持它,并带有一个公共的getter属性。
(LOD交叉渐变的支持选项)
去除交叉渐变的变体的工作方式与去除级联阴影的变体的工作原理完全相同,只是它依赖于LOD_FADE_CROSSFADE关键字和其他属性。将所需的代码添加到我们的预处理器中。当应用阴影剥离或交叉淡入淡出剥离时,Strip方法必须返回true。
以我为例,禁用LOD交叉渐变可将着色器变体减少到1878个(61%)。而且,当也禁用了级联阴影时,该比例进一步降低到1110(36%)。
请注意,禁用对LOD交叉渐变的支持只会影响剥离哪些着色器变体。交叉淡入淡出仍可在编辑器中使用,但无法在构建版本中使用。因此,只有在确定它不会被使用时才将其禁用。
也可以剥离特定的关键字组合,单个通道甚至至整个着色器。根据需要剥离单个关键字是最简单的,但是仅此一项就已经可以大大减少构建中包含的内容。
下一章,介绍后处理。