GPU 加速动画渲染方案
GPU Animator 解决的问题
同屏动画太多导致的 CPU 蒙皮计算压力太大。比如 MOBA 类游戏的几十个小兵,或者竞技场周围的吃瓜群众等等,
这些动画一般不需要很好的效果,可以尝试使用多种优化手段来降低效果(也可以保持效果基本不变)、占用内存、消耗GPU 来降低 CPU 的压力。
GPU Animator 原理
有两种 GPU 加速模式:
- 缓存每一帧顶点坐标,顶点着色器根据帧数和顶点编号获取顶点数据进行渲染,此为方案 A;
- 缓存每一帧骨骼变换矩阵,顶点着色器计算蒙皮,此为方案 B;
方案A
Step1 编辑器每帧运行 AnimationClip,获取转换后的顶点坐标保存到数组中,根据数组长度计算需要的 Texture 最小尺寸,(Texture用来保存顶点数据)。
private void CalculateTextureSize(ref int textureSize, int totalCount)
{
textureSize = 2;
while (textureSize * textureSize < totalCount)
{
textureSize = textureSize << 1;
}
}
然后把顶点数据 xyz 当成 rgb,保存到 Texture 中,保存成引擎可以支持的任意资源格式都可以。
Step2 运行时,设置我们想要播放动画的速度,Update 驱动帧 CurrentFrame,计算顶点的开始 Index,设置给 Shader,
//因为每一帧都要记录所有定点的位置,所以顶点的开始Index就是totalVerts * currentFrame
currentPixelIndex = totalVerts * currentFrame;
Step3 Shader 渲染部分,
SV_VertexID,我们用他来区分每一个顶点,加上我们上一步计算的出来的 PixelIndex,就是我们 Vertex 的坐标,如何转成 UV 呢?
根据我们顶点保存的规则,来计算,很简单,如下
inline float4 getUV(float startIndex)
{
float y = (int)(startIndex / _SkinningTexSize);
float u = (startIndex - y * _SkinningTexSize) / _SkinningTexSize;
float v = y / _SkinningTexSize;
return float4(u, v, 0, 0);
}
获取 UV 之后,根据 UV 坐标取我们计算好缓存起来的 vertex 坐标输出到片段着色器中。
VertexOutput vert(VertexInput input, uint vid : SV_VertexID)
{
VertexOutput output;
UNITY_SETUP_INSTANCE_ID(input);
float startPixelIndex = _StartPixelIndex + vid;
float4 uv = getUV(startPixelIndex);
float4 vertex = tex2Dlod(_SkinningTex, uv);
output.vertex = UnityObjectToClipPos(vertex);
output.uv = input.uv;
return output;
}
这个方案:
- 优点几乎不占用cpu和gpu消耗;
- 缺点动画文件体积较大(30帧左右,大概3M大小);
方案B
Step1 类似方案A,记录每一帧的关节 matrix,使用三个 rgb 来保存 matrix 的三行。
meshTexturePixels[pixelIndex] = new Color(matrix.m00, matrix.m01, matrix.m02, matrix.m03);
pixelIndex++;
meshTexturePixels[pixelIndex] = new Color(matrix.m10, matrix.m11, matrix.m12, matrix.m13);
pixelIndex++;
meshTexturePixels[pixelIndex] = new Color(matrix.m20, matrix.m21, matrix.m22, matrix.m23);
pixelIndex++;
最后保存到 Texture 里,不一样的一点是,需要保存一个新的 Mesh 文件,因为骨骼的顺序必须和我们保存的 Texture 对应上。
Step2
perFramePixelsCount,上文我们讲了需要三个像素。totalJoints,是我们Mesh的总关节数。同样的,Update 驱动帧 CurrentFrame。
currentPixelIndex = pixelsStartIndex + totalJoints * currentFrame * perFramePixelsCount;
Step3 Shader 渲染部分,
根据设置的 StartPixelIndex,取Matrix,然后计算顶点、法线等
inline float4x4 getMatrix(float startIndex)
{
float4 row0 = tex2Dlod(_SkinningTex, getUV(startIndex));
float4 row1 = tex2Dlod(_SkinningTex, getUV(startIndex + 1));
float4 row2 = tex2Dlod(_SkinningTex, getUV(startIndex + 2));
return float4x4(row0, row1, row2, float4(0, 0, 0, 1));
}
VertexOutput vert(VertexInput input)
{
VertexOutput output;
UNITY_SETUP_INSTANCE_ID(input);
float startPixelIndex = _StartPixelIndex;
float4 index = input.uv1;
float4 weight = input.uv2;
float4x4 matrix1 = getMatrix(startPixelIndex + index.x * 3);
float4x4 matrix2 = getMatrix(startPixelIndex + index.y * 3);
float4x4 matrix3 = getMatrix(startPixelIndex + index.z * 3);
float4x4 matrix4 = getMatrix(startPixelIndex + index.w * 3);
float4 vertex = mul(matrix1, input.vertex) * weight.x;
vertex = vertex + mul(matrix2, input.vertex) * weight.y;
vertex = vertex + mul(matrix3, input.vertex) * weight.z;
vertex = vertex + mul(matrix4, input.vertex) * weight.w;
float4 normal = mul(matrix1, input.normal) * weight.x;
normal = normal + mul(matrix2, input.normal) * weight.y;
normal = normal + mul(matrix3, input.normal) * weight.z;
normal = normal + mul(matrix4, input.normal) * weight.w;
output.vertex = UnityObjectToClipPos(vertex);
output.normal = UnityObjectToWorldNormal(normal);
output.uv = input.uv;
return output;
}
这个方案:
- 优点动画文件体积比原生动画文件还小,不占用CPU,GPU计算蒙皮;
- 缺点GPU压力;
总结
处理大规模角色,同时不要求动画效果很精细,内存够用的情况,可以采用方案A。
处理逻辑压力大、CPU消耗多并且渲染压力小的情况,可以选择方案B。