Animation Instancing是Unity在几年前开源的一个插件,用于让人物动画能在GPU中进行渲染,从而能够利用GPU Instancing来提升性能。这个插件能够大幅优化场景出现大量带有动画的人物时的性能表现,Unity官方的论坛中也有一篇关于该插件的介绍文章,有兴趣的同学可以看看。
Animation Instancing:高性能大规模动画解决方案 - 技术专栏 - Unity官方开发者社区
我也自己测试了下该插件的实际性能表现,方法是在场景中生成1250个带动画的角色。
直接在编辑器中的表现如下:
未使用插件
|
使用插件
|
---|
在oppo find x实机测试中的表现如下:
未使用插件 截屏
|
使用插件 截屏
|
---|---|
未使用插件 帧数
|
使用插件 帧数
|
可以看到,在使用插件下,运行的帧数有巨大的提升。
这个插件要求运行平台支持OpenGL ES 3.0及以上,如果你的最低目标平台无法达到这个要求,可能就得放弃这个插件了。不过如果你的游戏是发布安卓平台,安卓早在Android 4.3也就是API等级18的时候已经加入了对OpenGL ES 3.0的支持,所以这个要求对于现在来说应该一点都不过分。
展示了这个插件的强大之处后,就开始正式讲解一下插件的基础使用步骤和我在使用过程中踩的坑吧。
一、使用流程
1、下载
在Unity的github账号上下载:https://github.com/Unity-Technologies/Animation-Instancing
2、导入模型设置
将模型的Read/Write打开,不然脚本获取不了模型数据。
将Rig设置为Humanoid。(其它的类型暂时没有试过)
3、Prefab制作
给prefab设置AnimatorController并添加AnimationInstancing脚本。
在菜单栏中打开Animation Generator,将刚才制作的Prefab托进去,然后点击Generate生成动画数据。
完成后工程会出现一个AnimationTexture文件夹,生成的动画数据就在里面。同时Prefab上的AnimationInstancing脚本里的Prototype也会附上值。
注意!场景里使用的该Prefab不能改名,否则无法正常执行动画,我猜测应该是和生成的数据文件名字对不上导致的。所以最好是给Prefab外再套一层空父物体,作为最终的Prefab使用。
最后,将物体材质修改为插件提供的材质,并且将材质球的Enable GPU Instancing勾选上(如果Enable GPU Instancing没有勾选上,在安卓平台模型会无法显示)
4、安卓加载处理
因为需要加载动画数据,插件会在Android平台下默认要求加载动画数据所在的AB包,否则会抛出错误。所以在加载Prefab前,需要先调用插件脚本的AB包加载方法指定AB包路径。
yield return AnimationManager.Instance.LoadAnimationAssetBundle(Application.streamingAssetsPath + "/AssetBundle/animationtexture");
脚本自身也在菜单栏中提供了一个将动画数据打包的方法。打包之前,需要我们自己手动创建StreamingAssets文件夹并在其中建一个AssetBundle文件夹,不然打包的时候会报错找不到这个路径。
打包完成后会自动建一个AssetBundle文件夹,并且在其中和StreamingAssets/AssetBundle文件夹里生成出动画数据专用的AB包。
二、自己编写shader
插件自身提供了3个基础shader可以使用,在插件目录下的Shader文件夹中。但是这恐怕并不能满足制作需求,并且无法在URP渲染管线中正常运行。所以有必要自己写个shader。
1、基础原理
首先,了解一下插件中shader的核心文件AnimationInstancingBase.cginc。这个文件就在插件目录下的Shader文件夹中。AnimationInstancingBase.cginc文件实现了GPU蒙皮动画的核心逻辑,并且通过vert方法可以很方便的进行调用。
void vert(inout appdata_full v)
{
#ifdef UNITY_PASS_SHADOWCASTER
v.vertex = skinningShadow(v);
#else
v.vertex = skinning(v);
#endif
}
整个蒙皮动画的处理都在顶点着色器内进行,毕竟其原理就是根据动画数据对顶点做一系列的变换操作。所以在shader的顶点着色器中调用vert就能将蒙皮动画的处理逻辑整合进自己的shader中。
2、编写shader
先贴一个自定义shader的完整代码。这个shader只实现了最基础的纹理渲染、投射阴影和GPU Instance。
Shader "My/UnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vertn
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#pragma multi_compile_instancing
#include "UnityCG.cginc"
#include "AnimationInstancingBase.cginc"
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vertn (appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
Pass
{
Tags{"LightMode" = "ShadowCaster"}
CGPROGRAM
#pragma vertex vertn
#pragma fragment frag
#pragma multi_compile_instancing
#pragma multi_compile_shadowcaster
#include "UnityCG.cginc"
#include "AnimationInstancingBase.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vertn(appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i);
}
ENDCG
}
}
}
自定义的shader中有两个pass块,第一个用于渲染物体,第二个用于投射阴影。
首先,每个pass块中需要加入对AnimationInstancingBase.cginc的引用,并且shader文件需要和AnimationInstancingBase.cginc文件在统一目录下,这样才能使用AnimationInstancingBase.cginc中提供的功能。
#include "AnimationInstancingBase.cginc"
然后就是在自己的顶点着色器中调用AnimationInstancingBase.cginc中提供的vert方法。
v2f vertn (appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
v2f vertn(appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
return o;
}
shader中也需要加入gpu instance相关引用,否则shader无法支持gpu instance,材质球的面板里就不会有Enable GPU Instancing选项。
首先添加编译指示,表明shader要使用gpu instance。
#pragma multi_compile_instancing
然后由于GPU蒙皮动画的逻辑中需要在顶点着色器里使用多例化属性变量,所以需要在顶点着色器中添加对UNITY_SETUP_INSTANCE_ID的调用。
v2f vertn (appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
v2f vertn(appdata_full v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
vert(v);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
return o;
}
本来顶点着色器使用的数据结构中也需要添加对UNITY_VERTEX_INPUT_INSTANCE_ID的引用,但我们顶点着色器使用了Unity内置的appdata_full数据结构,所以就不用管了。
以上,就是我粗浅尝试了下Animation Instancing插件后整理的基础使用方法了。更多的操作可以自行查看插件的示例代码和插件的源码,比如在AnimationInstancing脚本中可以找到PlayAnimation和CrossFade之类的动画控制方法,也可以在AnimationManager脚本中找到动画数据的加载逻辑。