Unity 场景优化策略
GPU instancing
使用GPU Instancing可以将多个网格相同、材质相同、材质属性可以不同的物体合并为一个批次,从而减少Draw Calls的次数。这可以提高性能和渲染效率。
GPU instancing可用于绘制在场景中多次出现的几何体,例如树木或灌木丛。
渲染管线兼容性
特征 | 内置渲染管线 | 通用渲染管线 (URP) | 高清渲染管线 (HDRP) | 自定义可编程渲染管线 (SRP) |
---|---|---|---|---|
GPU instancing | 是的 | 是 (1) | 是 (1) | 是 (1) |
注意:
- 仅当着色器与 SRP Batcher 不兼容时。
** 设置GPU instancing**
设置很简单只需一步
在默认的材质球下找到Enable GPU Instancing,勾选就可以。
对比效果
没有勾选时:Batches是230左右,Saved By Batching是0
勾选后:Batches是8,Saved By Batching是222左右
使用下文的shader给材质添加随机色后:Batches是4,Saved By Batching是63(这里没有添加阴影Batch会少)
补充:
MaterialPropertyBlock
批处理一般作用与相同的材质。当需要对shader相同,材质属性不同的模型批处理时可以用MaterialPropertyBlock。
MaterialPropertyBlock除了在 Renderer.SetPropertyBlock 被使用,还可以在 Graphics.DrawMesh使用。
void Start()
{
MaterialPropertyBlock material = new MaterialPropertyBlock();
material.SetColor("_Color", new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f)));
GetComponent<MeshRenderer>().SetPropertyBlock(material);
}
这里可以发现,虽然颜色不一样,但是材质球指向的是同一个
** 创建支持 GPU instancing的着色器**
渲染管线兼容性
特征 | 内置渲染管线 | 通用渲染管线 (URP) | 高清渲染管线 (HDRP) | 自定义可编程渲染管线 (SRP) |
---|---|---|---|---|
自定义 GPU instancing着色器 | 是的 | 不 | 不 | 不 |
顶点和片元着色器示例
Shader "Custom/SimplestInstancedShader"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//生成实例化变体。它是可选的。
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
//在顶点着色器输入/输出结构中定义INSTANCE_ID。
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
//使INSTANCE_ID访问片元着色器中的实例化属性。
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;
//允许顶点着色器函数访问INSTANCE_ID
UNITY_SETUP_INSTANCE_ID(v);
//将INSTANCE_ID从输入结构复制到顶点着色器中的输出结构。
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//允许片元着色器函数访问INSTANCE_ID
UNITY_SETUP_INSTANCE_ID(i);
//访问实例化常量缓冲区中的每个实例着色器属性
return UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
}
ENDCG
}
}
}
静态批处理
它通过将多个静态物体合并成一个批次来减少渲染调用,从而减少CPU和GPU的负载,可以显著减少渲染过程中的Draw Call数量,从而提高性能。
静态批处理适用于那些不会在运行时改变位置、旋转或缩放的物体,例如地形、建筑物等。
渲染管线兼容性
特征 | 内置渲染管线 | 通用渲染管线 (URP) | 高清渲染管线 (HDRP) | 自定义可编程渲染管线 (SRP) |
---|---|---|---|---|
静态批处理 | 是的 | 是的 | 是的 | 是的 |
静态批处理的设置
设置也很简单只需两步
-
在ProjectSettings→player→OtherSetting→StaticBatching,勾选
-
在默认的材质球下找到Enable GPU Instancing,勾选就可以。
静态批处理的限制
- 游戏对象处于活动状态且禁止(非禁止对象的会强制禁止)。
- 游戏对象具有MeshFilter组件,并且该组件已启用。
- MeshFilter 组件引用了Mesh
- 网格的顶点数大于 0。
- 该网格尚未与另一个网格合并。
- 游戏对象具有MeshRenderer组件,并且该组件已启用。
- 要批处理在一起的网格,使用相同的顶点属性。例如,Unity 可以对使用顶点位置、顶点法线和一个 UV 的网格进行批处理,但不能对使用顶点位置、顶点法线、UV0、UV1 和顶点切线的网格进行批处理。
效果
Batches是8,Saved By Batching是211
静态批处理的控制
使用StaticBatchingUtility
类来控制静态批处理。
using UnityEngine;
public class StaticBatchingController : MonoBehaviour
{
void Start()
{
StartStaticBatch();
}
GameObject[] GetGameObjectsToBatch()
{
// 返回需要静态批处理的游戏对象数组
// 例如:可以通过标签、层级或者其他方式来获取需要静态批处理的游戏对象
// 这里只是一个简单示例,实际情况可能需要根据具体需求来获取游戏对象
return GameObject.FindGameObjectsWithTag("StaticBatchingObject");
}
void StartStaticBatch()
{
// 获取所有需要静态批处理的游戏对象
GameObject[] gameObjectsToBatch = GetGameObjectsToBatch();
// 执行静态批处理
StaticBatchingUtility.Combine(gameObjectsToBatch, this.gameObject);
}
void StopStaticBatch()
{
// 禁用静态批处理
StaticBatchingUtility.Combine(null, this.gameObject);
}
}
动态批处理
通过将运行时动态地将多个静态或动态的游戏对象合并成一个批次,减少渲染调用的数量,提高帧率和性能。
渲染管线兼容性
特征 | 内置渲染管线 | 通用渲染管线 (URP) | 高清渲染管线 (HDRP) | 自定义可编程渲染管线 (SRP) |
---|---|---|---|---|
动态批处理 | 是的 | 是的 | 不 | 是的 |
静态批处理的设置
设置也很简单只需一步
在ProjectSettings→player→OtherSetting→DynamicBatching,勾选
动态批处理的限制
- Unity 无法对包含超过 225 个顶点的网格应用动态批处理。这是因为网格的动态批处理每个顶点都有开销。
使用顶点位置、顶点法线和单个 UV,则 Unity 最多可以批处理 225 个顶点。但是,如果着色器使用顶点位置、顶点法线、UV0、UV1 和顶点正切,则 Unity 只能批处理 180 个顶点。 - 如果对象使用不同的材质实例,则 Unity 无法将它们批处理在一起,即使它们本质是使用一个shader。唯一的例外是阴影投射器渲染。
- 带有光照贴图的游戏对象具有额外的渲染器参数。这意味着,如果要对光照映射的游戏对象进行批处理,它们必须指向相同的光照贴图位置。
- Unity 无法将动态批处理完全应用于使用多个pass的shader对象。
- 几乎所有 Unity 着色器都支持多个灯光前向渲染为了实现这一点,他们为每个光源处理一个额外的渲染通道。Unity 仅对第一个渲染通道进行批处理。它无法对额外的每像素光源的绘制调用进行批处理。
效果
Batches是8,Saved By Batching是220左右
手动合并网格
手动将多个网格合并为一个网格,在网格靠得很近且彼此不相对移动的情况下,可以很好地替代,静态批处理和动态批处理。
警告:Unity 无法单独剔除您组合的网格。这意味着,如果组合网格的一部分出现在屏幕上,Unity 会绘制整个组合网格。如果网格是静态的,并且希望 Unity 单独剔除它们,使用静态批处理。
合并网格的设置方法
- 在创作网格时在资源生成工具中。即模型制作阶段将其合并到一起,同时这样处理相同的模型会照成模型体量的变大。
- 在 Unity 中使用 Mesh.CombineMeshes
CombineMeshes的用法
//强制添加组件
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class ExampleClass : MonoBehaviour
{
void Start()
{
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
// CombineInstance 结构体 用于描述要使用 Mesh.CombineMeshes 组合的网格。
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
int i = 0;
while (i < meshFilters.Length)
{
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
meshFilters[i].gameObject.SetActive(false);
i++;
}
Mesh mesh = new Mesh();
mesh.CombineMeshes(combine);
transform.GetComponent<MeshFilter>().sharedMesh = mesh;
transform.gameObject.SetActive(true);
}
}
CombineInstance 字段 | 说明 |
---|---|
lightmapScaleOffset | 应用于网格的烘焙光照贴图UV比例和偏移量。 |
mesh | 要组合的网格。 |
realtimeLightmapScaleOffset | 应用于网格的实时光照贴图UV比例和偏移。 |
subMeshIndex | 网格的子网格索引。 |
transform | 在组合之前要变换网格的矩阵。有关示例,请参阅 Mesh.CombineMeshes。 |
效果
Batches是8,Saved By Batching是0
遮挡剔除
遮挡剔除不说了
频繁使用遮挡剔除可能会导致一些性能问题,特别是在复杂的场景中。因为每次相机移动或场景发生变化时,都需要重新计算遮挡剔除信息,这可能会消耗一定的计算资源。
LOD
Unity中使用LOD(Level of Detail)时,可以根据相机与物体的距离,自动切换不同级别的模型,以提高性能和减少渲染开销。
unityLOD设置方法
-
创建多个不同级别的模型,分别代表远、中、近距离的模型。
-
将这些模型作为同一个游戏对象的子对象,并添加LOD Group组件。
-
在LOD Group组件中设置每个级别的距离和对应的模型。
-
Unity会根据相机与物体的距离自动切换不同级别的模型。
效果
可以与静态批处理和动态批处理结合
Batches是11,Saved By Batching是310左右
层剔除 layerCullDistances
用于指定摄像机在渲染不同图层时的剔除距离。通过设置layerCullDistances,可以控制摄像机在渲染不同图层时的剔除距离,从而提高渲染性能。层剔除和lod类似,也是按距离剔除,不同的是层剔除不会替换
float[] distances = new float[32];//设定32个默认图层
distances[11] = 300;//为第11层设定距离
Camera.mian.layerCullDistances = distances;//将剔除层传递给相机