接前文: Unity Shader: 一个简单的(规则化)序列帧动画(基础显示)
序列帧有时候会应用在数量特别庞大的场景,如下图所示:
创建了900个方阵,每个方阵内有25个对象,共22500个对象,每个对象使用统一的action,因为有自动合并批次,所以效率看起来似乎还可以,但实际应用中,我们不可能所有的方阵都整齐划一,不同的方阵在不同的时机有不同的action,所以通过交叉action的方式来模拟处理:
此时DC就已经升高到1万+,帧率也很低.
要解决此问题,需要用到GPUInstancing(官方文档为:https://docs.unity3d.com/Manual/GPUInstancing.html),使其满足同一Material具有不同表现的情况.
修改后的shader如下:
// 规则化序列帧播放,每帧大小应该一致
Shader "Test/SimpleMovieClip"
{
Properties
{
_MainTex("Image Sequence", 2D) = "white" { }// 序列帧图片
_RowCount("总行数", Float) = 1 // 行数
_ColumnCount("总列数", Float) = 1 // 列数
_FrameRate("帧率", Range(1, 100)) = 30 // speed
_ActionRowIndex("ActionRowIndex", Range(0, 100)) = 0
_ActionFrames("当前action帧数", Range(0, 100)) = 0
_Color("Color", Color) = (1,1,1,1)
}
SubShader
{
//一般序列帧动画的纹理会带有Alpha通道,因此要按透明效果渲染,需要设置标签,关闭深度写入,使用并设置混合
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//#pragma target 3.0
// make fog work
//#pragma multi_compile_fog
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 uv2 : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
//UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID // necessary only if you want to access instanced properties in fragment Shader.
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _RowCount;
float _ColumnCount;
// float _FrameRate;
// float _ActionRowIndex;
// float _ActionFrames;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float, _FrameRate)
UNITY_DEFINE_INSTANCED_PROP(float, _ActionRowIndex)
UNITY_DEFINE_INSTANCED_PROP(float, _ActionFrames)
UNITY_INSTANCING_BUFFER_END(Props)
fixed4 _Color;
v2f vert(appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o); // necessary only if you want to access instanced properties in the fragment Shader.
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 是用原始uv,不进行平铺和偏移
// o.uv.xy = v.uv.xy;// * _MainTex_ST.xy + _MainTex_ST.zw;
//UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i); // necessary only if any instanced properties are going to be accessed in the fragment Shader.
// 将时间取整(变成以秒为单位)相当于1秒1帧,放大到_FrameRate后,相当于得到帧index,通过index去计算行列索引.
// 必须将纹理的wrap mode设置为Repeat(或类似的设定),因为当time>_ColumnCount*2时,row会大于_RowCount
// uvoff中计算的y值会大于1,需要通过纹理的Repeat机制来重复显示.
// 或者在外部维护一个index变量,并传进来,这样可以在外层将这个index进行重置为0
float index = floor(_Time.y * UNITY_ACCESS_INSTANCED_PROP(Props, _FrameRate));
// 取整得到行索引(播放顺序设计为从左到右,先行后列)
float rowIndex = UNITY_ACCESS_INSTANCED_PROP(Props, _ActionRowIndex); // _ActionRowIndex;//floor(index / _ColumnCount);
// 余数为列索引
//float columnIndex = fmod(index, _ActionFrames); // index - rowIndex * _ColumnCount;
float columnIndex = fmod(index, UNITY_ACCESS_INSTANCED_PROP(Props, _ActionFrames)); // index - rowIndex * _ColumnCount;
half2 iuv = i.uv.xy; // /_MainTex_ST.xy;
// 使用中的行列值作为分割计算的元值(总比值). 相当于一个窗口,通过该窗口的上下左右定位得到每帧图片的uv
half2 rawSplit = half2(_ColumnCount, _RowCount);
// 当前uv通过rawSplit分割后,得到当前uv在总uv中的占比. 相当于(窗口的)固定大小
iuv /= rawSplit;
// 通过当前计算出的行列值与总比值的比例,得到uv的起始偏移量. 相当于(窗口的)起始位置, row是从上到下,取反后转换为uv的从下到上
half2 uvoff = half2(columnIndex, -rowIndex) / rawSplit;
iuv += uvoff;
// iuv*=-1;
fixed4 col = tex2D(_MainTex, iuv) * _Color;
// apply fog
//UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
将需要修改的(特性)变量由直接声明变更为了instanced模式:
UNITY_DEFINE_INSTANCED_PROP(float, _FrameRate)
UNITY_DEFINE_INSTANCED_PROP(float, _ActionRowIndex)
UNITY_DEFINE_INSTANCED_PROP(float, _ActionFrames)
同时勾选Enable GPU Instancing:
测试代码(C#)中,使用
MaterialPropertyBlock
进行属性修改:
// ...
var propBlock = new MaterialPropertyBlock();
propBlock.SetFloat("_ActionRowIndex", 0);
propBlock.SetFloat("_ActionFrames", 5);
propBlock.SetFloat("_FrameRate", 2);
// ...
// 进行设置:
go.GetComponent<MeshRenderer>().SetPropertyBlock(propBlock);
//...
(测试代码比较简单,请自行构建)
修改后效果图如下:
如上图所示DC大幅下降,帧率也有所提高. 此处额外查看下Instance的处理情况.
Instance的额外测试
通过Frame Debug观察这52个DC的具体情况:
默认1个,测试环境中的天空占6个,其它都是来自45个均来自Draw Mesh(instanced),图1
处的红框部分是说明在instance机制下,每次合批中,有511个实例进行了合并(含有了2044个顶点). 预览图中看到的是2个三角形的片,是因为我的测试中是用生成的2个三角形来为显示对象的sharedMesh赋值的.
// ...
var mesh = new Mesh();
var size = 0.1f;
var newVertices = new Vector3[]{
new Vector3(0, 0, 0), new Vector3(0, 0, size), new Vector3(size, 0, size), new Vector3(size, 0, 0),
};
var newUV = new Vector2[]{
new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1),new Vector2(1, 0),
};
var newTriangles = new int[]{
0, 1, 2, 2, 3, 0,
};
mesh.vertices = newVertices;
mesh.uv = newUV;
mesh.triangles = newTriangles;
// ...
go.GetComponent<MeshFilter>().sharedMesh = mesh;
// ...
图2
中,最后一个Draw Mesh(instanced)是64个顶点. 相当于24*2044+64=9万个顶点, 刚好与原始设计(900个方阵,每个方阵内有25个对象,共22500个对象)的9万个顶点匹配.
在Unity Dynamic Batching中,一般要求单个模型的顶点信息数据不超过900个,但通过Instancing就可以超过此限制.
将2个三角形的模型替换为一个2000个顶点的模型来观察:
测试结果如下:
DC不变,合并的实例数量也不变(511个),但单次合并的顶点数变为了100万+. 此举虽然能加大mesh合并但加重的是CPU的负担(要在CPU侧进行合并计算),要达到最优效率,需要找到平衡点.
具体原理参考: https://github.com/vanCopper/Unity-GPU-Instancing
回到主题
虽然在Instancing加持下,多个action性能消耗有所降低,但到此还没结束,实际情况中我们往往会有多个角色多个action的情况,哪怕一个角色的所有action合并为了一张贴图,但总会有多个角色存在于场景中的情况,现在测试2张Material交替显示的情况(第二张material设置了一个不同的颜色以示区分),模拟2个角色各自表现2个action,结果如下图所示:
性能又下降了,说明在一个节点之下放置不同Material时,也会导致Instancing不生效.
解决方式1: 在使用不同Material的对象上挂载一个SortingGroup,即将不同的Material进行分层处理:
// ...
if (ismat2)
go.AddComponent<UnityEngine.Rendering.SortingGroup>().sortingOrder = 1;
else
go.AddComponent<UnityEngine.Rendering.SortingGroup>().sortingOrder = 2;
// ...
解决方式2: 按角色进行分层处理(类似canvas中的分层优化),将同类Material归类到一个节点下,然后在该节点添加
SortingGroup
,但当一个SortingGroup下节点数过多会收到一个错误:
将测试(C#)代码修改为同类Material归类为一个大组节点,其下再按4096为小组节点,小组节点下再挂载显示对象:
测试结果如下:
Frame Debug中也能看到全部都是instanced后的结果:
此方式是在每个显示对象的层级交错时有问题,但也基本满足需求.
除上述方式外,还可以
. 使用Graphics.DrawMeshInstanced()
进行直接绘制(没有显示节点对象,相当于在画布上直接绘制).
具体可以参考: https://gist.github.com/Cyanilux/e7afdc5c65094bfd0827467f8e4c3c54
. 如果业务中用到角色动作数量是可预见的,则可以在一个Material中使用所有贴图
GPUInstancing虽然可以大量减少DC,也不是随便滥用,因为需要在CPU侧计算当次合并中的顶点信息等,所以在移动设备上效率可能反而更低(帧率更低),这就需要根据项目实际业务场景进行反复测试,确定那些业务点中到底增加DC性价比更高还是增加单次合批信息性价比更高.