Unity性能调优手册7:渲染优化,DrawCall,剔除,Shader,LOD,TextureStreaming

2023-10-26 15:48:20 浏览数 (3)

翻译自https://github.com/CyberAgentGameEntertainment/UnityPerformanceTuningBible/ 本章介绍围绕Unity图形功能的调整实践。

分辨率调优

在渲染管道中,片段着色器的成本与它们渲染的分辨率成比例增加。特别是随着当今移动设备的高显示分辨率,有必要将渲染分辨率调整到合适的值。

DPI设置

如果分辨率缩放模式,该模式包含在 对于移动平台的播放器设置,被设置为固定的DPI,特定的DPI(点每英寸),分辨率可以降低到目标特定的DPI(点每英寸)。

最终分辨率是通过将目标DPI值乘以质量设置中的分辨率缩放DPI比例因子值来确定的。

脚本缩放分辨率

若要动态更改脚本的绘图分辨率,请调用Screen.SetResolution 当前的分辨率可以在屏幕上获得Screen.width 或者Screen.height,DPI可以在Screen.dpi获得。

代码语言:javascript复制
public void SetupResolution()
{
var factor = 0.8f;

// Get current resolution with Screen.width, Screen.height
var width = (int)(Screen.width * factor);
var height = (int)(Screen.height * factor);

// Set Resolution
 Screen.SetResolution(width, height, true);
 }

tips 屏幕上的分辨率设置。SetResolution仅反映在实际设备上。 注意,更改不会反映在Editor中 译者增加部分 在安卓机中存在小窗功能,可以通过native监听分辨率变化处理 【腾讯文档】Android分屏小窗还原分辨率触摸异常 https://docs.qq.com/doc/DWllJQlpucWVsSU95

半透明overdraw

半透明材料的使用由overdraw控制。Overdraw是指在屏幕上每像素多次绘制片段,它影响的性能与片段着色器的负载成比例。 特别是当生成大量半透明粒子时,例如在粒子系统中,通常会产生大量的overdraw。 以下方法可用于减少因Overdraw而增加的draw负荷。 1.减少不必要的绘图面积 尽可能减少纹理完全透明的区域,因为它们也会受到渲染的影响。 2.对可能导致透支的对象使用轻量级着色器 3.尽量避免使用半透明材料。 使用不透明的材料来模拟半透明的外观,抖动是另一种需要考虑的技术 在Built-in内置渲染管道的编辑器中,设置场景Scene view视图模式为Overdraw,这是调整透支的有用基础。

Tips 自Unity 2021.2以来,URP通用渲染管道支持场景调试视图模式Scene Debug View Modes

减少DrawCall

增加draw调用的次数通常会影响CPU负载。Unity有几个功能可以减少绘制调用的数量。

Dynamic batching

动态批处理是在运行时对动态对象进行批处理的特性。使用同样材质的动态对象上的绘制调用可用于合并和减少DrawCall 要使用它,请转到播放器设置并在播放器中选择Dynamic Batching项设置。

由于动态批处理是一个cpu密集型的过程,因此在将其应用于对象之前必须满足许多条件。主要条件如下。 1.相同材质 2.物体使用MeshRenderer或Particle System进行渲染。其他组件如SkinnedMeshRenderer不受动态批处理的影响 3.网格顶点数小于300 4.没有使用多Pass的shader 5.不受实时阴影影响 Tips 动态批处理可能不推荐,因为它对稳定的影响CPU负载。见下文。下面描述的SRP Batcher可以用来实现类似于动态批处理的效果

Static batching

静态批处理是对场景中不移动的对象进行批处理的函数。此功能可用于减少使用相同材质的静态对象的绘制调用。 与动态批处理类似,在播放器设置中,单击播放器设置中的Static Batching 。

要使一个对象符合静态批处理的条件,设置对象的静态标志该对象的标志必须启用。具体来说,静态标志中的批处理静态子标志必须启用。

要使对象符合静态批处理的条件,请设置对象的静态标志,该对象的标志必须启用。具体来说,静态标志中的批处理静态子标志必须启用。

静态批处理与动态批处理的不同之处在于,它不涉及运行时的顶点转换处理,因此可以在较低的负载下执行。但是,需要注意的是,存储批处理组合的网格信息会消耗大量内存。 译者增加部分 手游中并没有开启,而是使用GPUInstancing渲染草树

GPU Instancing

GPU实例化是一个有效绘制相同网格和材质对象的功能。当多次绘制相同的网格(如草或树)时,期望减少绘制调用。 要使用GPU实例化,请转到材质的检查器,并在材质的检查器中单击启用实例化。

创建可以使用GPU实例化的着色器需要一些特殊的处理。 下面是一个shader代码示例,其中包含了在内置渲染管道中使用GPU实例化的最小实现。

代码语言:javascript复制
Shader "SimpleInstancing"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    CGINCLUDE
    #include "UnityCG.cginc"
    struct appdata
    {
        float4 vertex : POSITION;
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    struct v2f
    {
        float4 vertex : SV_POSITION;
        // Required only when accessing INSTANCED_PROP in fragment shaders
        UNITY_VERTEX_INPUT_INSTANCE_ID
    };
    UNITY_INSTANCING_BUFFER_START(Props)
    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
    UNITY_INSTANCING_BUFFER_END(Props)
    v2f vert(appdata v)
    {
        v2f o;
        UNITY_SETUP_INSTANCE_ID(v);
        // Required only when accessing INSTANCED_PROP in fragment shaders
        UNITY_TRANSFER_INSTANCE_ID(v, o);
        o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
    }
    fixed4 frag(v2f i) : SV_Target
    {
        // Only required when accessing INSTANCED_PROP with fragment shaders
        UNITY_SETUP_INSTANCE_ID(i);
        float4 color = UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
        return color;
    }
    ENDCG
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            ENDCG
        }
    }
}

GPU实例化只适用于引用相同材质的对象,但你可以为每个实例设置属性。你可以将目标属性设置为可以单独更改的属性,通过将其与UNITY_INSTANCING_BUFFER_START(Props)和UNITY_INSTANCING_BUFFER_END(Props)一起封装,就像上面的着色器代码一样。 这个属性可以在c#中设置为MaterialPropertyBlock API,以设置单个颜色等属性。只是要小心不要在太多的实例中使用MaterialPropertyBlock,因为访问MaterialPropertyBlock可能会影响CPU性能。 译者增加部分 如何使用MaterialPropertyBlock赋值材质 【腾讯文档】MaterialPropertyBlock https://docs.qq.com/doc/DWkhmYnVvUnpnYWVo 部分机型不支持GPUInstancing导致闪烁 【腾讯文档】GPUInstancing出现闪烁 https://docs.qq.com/doc/DWkJMVXFQUUJqcXJN 手游项目中草,树使用GPUInstancing 不同石头的scale为负数来代表旋转等,会导致打断了GPU Instancing

SRP Batcher

SRP批处理程序是在Scriptable Render Pipeline中使用的一个可脚本渲染管道(SRP),它是一个减少渲染CPU成本的特性,仅在Scriptable Render Pipeline中可用。此功能允许使用相同着色器变体的多个着色器set-pass调用一起处理 要使用SRP批处理程序,您需要从SRP的Inspector中添加SRP资产。

您还可以使用以下c#代码在运行时启用或禁用SRP批处理程序 GraphicsSettings.useScriptableRenderPipelineBatching = true;

要使着色器与SRP兼容,必须满足以下两个条件 1.在单个CBUFFER中定义每个对象的内置属性,称为UnityPerDraw 2.在单个CBUFFER中定义每个材质的属性,称为UnityPerMaterial 对于UnityPerDraw通用渲染管道和其他着色器基本上默认支持它,但你需要为UnityPerMaterial设置自己的CBUFFER 用CBUFFER_START(UnityPerMateria1)和CBUFFER_END包围每个材质的属性,如下所示。

代码语言:javascript复制
Properties
{
_Color1 ("Color 1", Color) = (1,1,1,1)
_Color2 ("Color 2", Color) = (1,1,1,1)
}
CBUFFER_START(UnityPerMaterial)
float4 _Color1;
float4 _Color2;
CBUFFER_END

通过上面的操作,你可以创建一个支持SRP Batcher的着色器,但是你也可以从Inspector中检查这个着色器是否支持SRP Batcher。 在着色器的检查器中,点击着色器的SRP批处理项。如果它是“不兼容”就是不兼容,这意味着它不被支持。

译者增加部分 【腾讯文档】静态、动态合批与GPUInstancing https://docs.qq.com/doc/DWm1Ib25MZEFHQW9y

SpriteAtlas图集

2D游戏和ui通常使用许多精灵来构建屏幕。在这种情况下,避免生成大量绘制调用的函数是SpriteAtlas,以避免在这种情况下产生大量绘制调用。 SpriteAtlas通过将多个精灵组合到单个纹理中来减少绘图调用 要创建SpriteAtlas,首先进入包管理器并点击2D Sprite,必须首先从包管理器安装到项目中

安装完成后,右键单击Project视图,选择“Create -> 2D -> Sprite”创建SpriteAtlas资产。

要指定将被制作成图集的精灵,请转到SpriteAtlas检查器并选择SpriteAtlas检查器的“打包对象”项来指定精灵或包含精灵的文件夹。

使用上述设置,精灵将在构建和播放过程中被打包。集成的SpriteAtlas纹理将在绘制目标精灵时被引用。 精灵也可以直接从SpriteAtlas获得,例如以下代码。

代码语言:javascript复制
[SerializeField]
private SpriteAtlas atlas;
public Sprite LoadSprite(string spriteName)
{
// Obtain a Sprite from SpriteAtlas with the Sprite name as an argument
var sprite = atlas.GetSprite(spriteName);
return sprite;
}

在SpriteAtlas中加载单个Sprite比只加载一个Sprite消耗更多的内存,因为整个图集的纹理都是加载的。因此,SpriteAtlas应该小心使用并适当分割 Tips 本节是针对SpriteAtlas V1编写的。SpriteAtlas V2在操作上可能会有重大的变化,比如不能指定要存档的精灵的文件夹。

Culling剔除

剔除图像中最终不会显示在屏幕上的部分。

视觉剔除

视觉剔除(Visual Culling)是一个从渲染中忽略相机渲染区域之外的物体的过程,即视锥。这可以防止相机范围外的物体被计算渲染。 默认情况下执行视觉锥体剔除,没有任何设置。对于顶点着色密集的对象,可以通过适当划分网格来应用剔除,以减少渲染成本

背面剔除

背面剔除是省略渲染(应该是)不可见的多边形背面的过程。大多数网格是封闭的(只有前面的多边形对相机可见),所以多边形的背面不需要绘制。 在Unity中,如果你没有在着色器中指定这个,多边形的背面就会被剔除,但是你可以通过在着色器中指定它来切换剔除设置。 下面是在SubShader中描述的。

代码语言:javascript复制
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull Back // Front, Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}

有三种设置:后,前,和关闭。每个设置的效果如下。 •Back背面-不要在与观看者视角相反的一侧绘制多边形 •Front正面-不要在视点相同的方向上绘制多边形 •Off关闭-禁用反向剔除并绘制所有面。

Occlusion culling遮挡剔除

遮挡剔除是指从渲染中省略那些因为被物体遮挡而对相机不可见的物体的过程。这个函数使用预焙遮挡数据来确定一个对象是否在运行时被遮挡,并从渲染中移除遮挡的对象。 要使一个对象符合遮挡剔除的条件,将inspector的静态标志设置为Occluder Static 或者 Occludee Static。如果Occluder Static被禁用而Occludee Static 被启用,对象将不再被视为遮挡,而只是被遮挡的对象。在相反的情况下,对象是个遮挡物。

为了预烤遮挡剔除,显示遮挡剔除窗口进行预烤遮挡剔除。在此窗口中,您可以更改每个对象的静态标志,更改烘焙设置等,并按下烘焙按钮,可以通过按下烘焙按钮来执行烘焙。

遮挡剔除降低了渲染成本,但同时,它给CPU带来了更多的负载,因此有必要平衡每个负载并进行适当的设置。 Tips 遮挡剔除只减少了物体渲染过程,而实时阴影渲染等过程保持不变。 译者增加部分 在 Unity 中,可以打开遮挡剔除(Occlusion Culling)窗口来进行遮挡剔除的设置和优化。下面是具体的操作步骤: 1.打开菜单栏的“Window”>“Rendering”>“Occlusion Culling”。 2.在“Occlusion Culling”面板中,可以看到三个选项卡,分别是“Bake Settings”、“Visualize”和“Statistics”。 3.在“Bake Settings”选项卡中,可以进行遮挡剔除的设置。包含静态遮挡剔除的“Environment”,以及动态遮挡剔除的“Occlusion Areas”等。 4.在“Visualize”选项卡中,可以可视化场景中物体的遮挡状态。通过勾选不同的复选框,可以查看不同的可视化效果,例如绿色表示物体可见,红色表示物体被遮挡。 5.在“Statistics”选项卡中,可以查看遮挡剔除的统计信息。该选项卡会显示场景中所有网格的数量、遮挡剔除后的数量、减少的三角形数等信息。 需要注意的是,使用遮挡剔除功能需要先对场景进行烘焙(Bake)。在烘焙时,Unity 会根据场景中的物体和灯光等信息生成遮挡剔除数据,使得场景在运行时可以更快地渲染。因此,在进行遮挡剔除之前,需要先设置好场景的静态属性、灯光、摄像机等,然后才能进行烘焙操作。 某些项目未开遮挡剔除,因为轻功会飞在天上,如果建筑缓慢出现效果不好

Shaders

着色器对图像非常有效,但它们经常导致性能问题。

降低浮点类型的精度

gpu(尤其是在移动平台上)处理较小的数据类型比处理较大的数据类型要快。因此,应该将浮点类型替换为float浮点类型(32位)到half半类型(16位)在可以替换浮点类型时是有效的。 当精度要求较高时,如深度计算使用float,但在颜色计算中,即使降低精度,也很难在结果外观上造成较大的差异。 译者增加部分 Q挂机一段时间后,模型材质异常

A在shader中传入时间超过了half范围,模型异常

类似问题 https://answer.uwa4d.com/question/63e0da8b0638540599ff5002 Q主界面上的边框流动效果,在进行较长游戏时间之后,流动效果会变得比较卡顿 A由于Shader中传入的时间太大,精度不够导致的,建议对这个Shader中输入的时间做一下类似于Clamp01或者Frac的操作,防止出现精度问题。

使用顶点着色器执行计算

顶点着色器会根据网格中的顶点数量执行,碎片着色器会根据最终写入的像素数量执行。一般来说,顶点着色器的执行频率通常低于片段着色器,所以最好尽可能在顶点着色器中执行复杂的计算。 顶点着色器计算结果通过着色器语义传递给片段着色器,但应该注意的是,传递的值是插值的,可能看起来与在片段着色器中计算的值不同。 顶点着色器的预计算 CGPROGRAM

代码语言:javascript复制
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};
struct v2f
{
    float2 uv : TEXCOORD0;
    float3 factor : TEXCOORD1;
    float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    // Complex precomputations.
    o.factor = CalculateFactor();
    return o;
}
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    // Values computed in the vertex shader are used in the fragment shader
    col *= i.factor;
    return col;
}
ENDCG

预构建信息到纹理

如果着色器中复杂计算的结果不受外部值的影响,那么将预先计算的结果存储为纹理中的元素是一种有效的方法。 这可以通过在Unity中实现一个专门的纹理生成工具或作为各种DCC工具的扩展来完成。如果一个已经在使用的纹理的alpha通道没有被使用,最好是写入它或准备一个专用的纹理。 例如,用于颜色分级的LUT(颜色对应表)将预先校正纹理,使每个像素的坐标对应于每种颜色。通过在着色器中基于原始颜色对纹理进行采样,结果几乎与对原始颜色进行预校正相同。

ShaderVariantCollection变体收集

ShaderVariantCollection可以在防止着色器被编译时出现性能尖峰。 shadervariantcollection允许你保存游戏中使用的着色器变量列表作为资产。它是通过选择“Create -> Shader -> Shader Variant”创建的集合”。

从已创建的ShaderVariantCollection的Inspector视图中,按Add Shader添加目标着色器,然后选择要为着色器添加的变体。

ShaderVariantCollection被添加到Graphics Settings的Shader preloading中。Shader preloading 模块下的Preloaded Shaders添加需要启动时编译的shader变体

你也可以从脚本中调用ShaderVariantCollection. warmup()来设置ShaderVariantCollection中包含的ShaderVariantCollection中的shader变量。

代码语言:javascript复制
public void PreloadShaderVariants(ShaderVariantCollection collection)
{
// Explicitly precompile shader variants
    if (!collection.isWarmedUp)
    {
        collection.WarmUp();
    }
}

译者增加部分 【腾讯文档】YooAsset shader变体收集 https://docs.qq.com/doc/DWmRUbUFIVWRpcmhI

Lighting灯光

灯光是游戏艺术表现中最重要的元素之一,但它也经常对表现产生重大影响。

实时阴影

生成实时阴影消耗大量的绘制调用Drawcall和填充率。 因此,在使用实时阴影时,应仔细考虑设置。 减少DrawCall 以下策略可用于减少生成阴影的绘制调用。 •减少投射阴影的物体数量 •通过批处理合并DrawCall 有几种方法可以减少物体投射阴影的数量,但一个简单的方法是使用MeshRenderer中的投射阴影设置来关闭。这将从阴影绘制调用中移除对象。这个设置通常在Unity中打开,在使用阴影的项目中应该注意。

减少物体在阴影绘制的最大距离也是有用的。在“Quality Settings质量设置”中的“Shadow Distance 阴影距离”中,将投射阴影的物体数量减少到必要的最小值。调整这个设置也会降低阴影的分辨率,因为阴影将在阴影贴图分辨率的最小范围内绘制。

与正常渲染一样,阴影渲染可以通过批处理来减少绘制调用。 节省填充率FillRate 阴影的填充率取决于阴影贴图的渲染和受阴影影响的物体的渲染。 可以通过在质量设置的阴影部分调整几个设置来保存各自的填充率。

“Shadows”部分允许您更改阴影的格式硬阴影会产生清晰的阴影边界,但负载相对较低,而软阴影更昂贵,但它可以产生模糊的阴影边界。 Shadow Resolution阴影分辨率和Shadow Cascades阴影级联项会影响阴影贴图的分辨率,较大的设置会增加阴影贴图的分辨率并消耗更多的填充率。然而,由于这些设置与阴影的质量有很大关系,因此应该仔细调整它们以在性能和质量之间取得平衡。 一些设置可以使用Light组件的检查器进行调整,因此可以更改单个灯光的设置。

译者增加部分 手游不使用unity自带阴影

使用shader投影阴影方式,但是这种会产生阴影只能照在平面上,不能出现在斜坡与影子部分照射在石头上 【腾讯文档】Unity阴影原理-插件-平面阴影 https://docs.qq.com/doc/DWlFlZ3R5Y0dEdnlN

假阴影

根据游戏类型或美术风格,使用平板多边形或其他材料来模拟物体的阴影可能会很有效。虽然这种方法有很强的使用限制,灵活性也不高,但它比通常的实时阴影渲染方法要轻得多。

译者增加部分 手游项目中非重要角色使用圆盘形面片假阴影,角色在斜坡上,需要发射线计算斜坡角度,设置面片角度。Physics.Raycast会被遍历发射,如果需要100个角色,就会在这帧执行100次, 改用JobSystem优化发射射线。

光照贴图Light Mapping

通过提前将光照效果和阴影烘焙到纹理中,可以以比实时生成低得多的负载实现高质量的光照表达式。 要烘培lightmap,首先将放置在场景中光组件设置Mixed或者Backed模式

另外,激活要烘烤的对象的静态标志。

在此状态下,从菜单中选择“Window -> Rendering -> Lighting”来显示照明视图。 默认设置是照明设置资产没有指定,我们需要改变。通过点击新建灯光设置按钮创建一个新的灯光设置。

lightmaps的主要设置是Lightmapping settings选项卡。

有许多设置可以调整,以改变光图烘焙的速度和质量。因此,应适当调整这些设置以获得所需的速度和质量。 在这些设置中,对性能影响最大的是Lightmap Resolution此设置对性能影响最大。这个设置决定了在Unity中每个单元分配多少光图纹理,并且由于最终的光图大小取决于这个值,它对存储和内存容量,纹理访问速度和其他因素有重大影响。

最后,在检查器视图的底部,在检查器视图底部的生成照明按钮来烘烤光图。烘焙完成后,你会看到烘焙后的光图存储在与场景同名的文件夹中。

Level of Detail细节层次

在高多边形、高清晰度的情况下,对远离相机的物体进行渲染是低效的。细节水平(LOD)方法可用于根据物体与相机的距离来降低物体的细节水平 在Unity中,对象被分配给对象的LOD Group组件。

父对象上放置LODGroup组件 通过插入lod分级,在下方Renderers处拖入模型

使用LOD通常会减少绘图负载,但是应该注意内存和存储压力,因为每个LOD级别的所有网格都是加载的。

Texture Streaming

Unity的纹理流可以用来减少纹理所需的内存占用和加载时间。纹理流是一种通过基于场景中的摄像机位置加载mipmaps来节省GPU内存的功能。 要启用此功能,请到Quality Settings 中Texture Streaming进行设置

此外,必须更改纹理导入设置以允许纹理贴图流。打开纹理检查器,选择Advanced 中Streaming Mipmaps

这些设置为指定的纹理启用流媒体贴图。同样,在在Quality Settings质量设置下的Memory Budget 内存预算限制加载纹理的总内存使用。纹理流系统将加载mipmaps,而不会超过这里设置的内存量。 译者增加部分 在Unity中,纹理串流技术叫做The Mipmap Streaming System,其作用是让Unity根据摄像机的位置只加载对应Mipmap Level的纹理到显存中,而不是把所有Mipmap Level全加载到显存中让GPU根据摄像机位置使用对应的Mipmap Level。 【腾讯文档】Mipmap-内存多三分之一 https://docs.qq.com/doc/DWnlrS2drYWdkaU1N 【腾讯文档】纹理串流Texture Mipmap Streaming https://docs.qq.com/doc/DWlZFcFFqcFZQSHJ3

0 人点赞