一.需要的前置知识
1.什么是GPU Instancing?
这里简单介绍一下,Unity官方文档链接:GPU Instancing
GPU Instancing是一种优化方法,用一个DrawCall去渲染多个物体——他们使用相同的材质,他们的Mesh拷贝自同一份Mesh资源。每一份Mesh的拷贝称为Instancing.这种优化方法,经常用于渲染场景重复多次出现的事物(比如植被,树木等)。
GPU Instancing渲染的每个Instancing可以拥有不同的材质属性比如Color,Scale等。(通过MaterialPropertyBlock)
2.骨骼动画与蒙皮
二.为什么Skeletal Animation不能使用GPU Instancing?
其实是可以的,但是Unity默认的方案SkinnedMeshRenderer不行。
SkinnedMeshRenderer,每个角色的顶点数据是运行时计算好蒙皮(Skinning),再分别传递到GPU的。。不能像MeshRenderer一样,所有物体使用同一份顶点数据。
三.如何让播放Skeletal Animation的物体,能够使用GPU Instancing这个优化策略呢?
我看了一些资料发现有两种方案: 方案一:跳过蒙皮,直接将角色播放动画时的顶点信息储存在贴图中,播放动画时,让Shader在顶点函数获得贴图中的顶点位置。 文章:https://www.cnblogs.com/murongxiaopifu/p/7250772.html 仓库:https://github.com/chenjd/Render-Crowd-Of-Animated-Characters 缺点:贴图内存过大,并且一个动画就是一张贴图。模型顶点数小还行,模型顶点数稍微大一点,就可能超过unity可创建的最大贴图尺寸。
方案二(GPUSkinning):将所有骨骼动画的每帧的骨骼在模型空间的TRS矩阵储存在贴图中,播放动画时,让Shader在顶点函数进行蒙皮计算获得蒙皮后的顶点位置。 文章:https://zhuanlan.zhihu.com/p/36896547 仓库:https://github.com/Unity-Technologies/Animation-Instancing 这个方案因为只记录骨骼的的TRS,所以骨骼数量和动画长度决定了贴图大小,骨骼数量远没有顶点数量这么多,所以贴图数量小很多,甚至所有动画的信息都可以记录在一张贴图中。
四.GPUSkinning过程简述
1.将动画信息和模型信息记录在一个GPUAnimation(ScriptableObject)内
class GPUSkinningAnimation
{
GPUSkinningBone[] bones;
GPUSkinningClip[] clips;
}
//骨骼信息
class GPUSkinningBone
{
string name;
Transform transform;//骨骼对应Gameobject上的Transform节点
Matrix4x4 bindpose;//模型空间到骨骼空间的转换矩阵
int parentBoneIndex = -1;//父骨骼
int[] childrenBonesIndices = null;//子骨骼
}
//动画信息
class GPUSkinningClip
{
string name;
float length;//Clip 动画长度
int fps;
WarpMode warpMode;//Once or Loop
int pixelSegmentation;//像素索引,这段动画信息在贴图中的起点位置
GPUSkinningFrame[] frames = null;//帧信息
}
//帧信息
public GPUSkinningFrame
{
Matrix4x4[] matrices = null;//骨骼在模型空间的TRS矩阵
}
2.将所有Clips的每帧骨骼模型空间下TRS矩阵储存在贴图中
a.首先,计算每一帧动画中所有骨骼模型空间下的TRS矩阵,关键代码
//计算单帧所有骨骼的模型空间TRS
GPUSkinningBone[] bones = samplingAniamtion.bones;
for (int i = 0; i < bones.Length; ++i)
{
GPUSkinningBone currentBone = bones[i];
frame.matrices[i] = currentBone.bindpose;
do
{
Matrix4x4 mat = Matrix4x4.TRS(currentBone.transform.localPosition,
currentBone.transform.localRotation,
currentBone.transform.localScale);
frame.matrices[i] = mat * frame.matrices[i];
if (currentBone.parentBoneIndex == -1)
{
break;
}
else
{
currentBone = bones[currentBone.parentBoneIndex];
}
}
while (true);
}
b.将所有动画每帧中的所有骨骼的TRS矩阵储存在贴图中
//计算贴图长宽
for (int index = 0; index < clips.Length; ++index)
{
GPUSkinningClip clip = clips[index];
clip.pixelSegmentation = pixelCount;
GPUSkinningFrame[] frames = clip.frames;
int frameCount = frames.Length;
pixelCount += animation.bones.Length * 3/* 3 x 4个通道,frame.matrices 中的 3x4 */ * frameCount;
}
public Texture2D CreateAnimationMap(GPUSkinningAnimation animation)
{
Texture2D texture = new Texture2D(animation.textureWidth, animation.textureHeight, TextureFormat.RGBAHalf, false, true);
Color[] pixels = texture.GetPixels();
int pixelIndex = 0;
for (int clipIndex = 0; clipIndex < animation.clips.Length; ++clipIndex)
{
GPUSkinningClip clip = animation.clips[clipIndex];
GPUSkinningFrame[] frames = clip.frames;
int numFrames = frames.Length;
for (int frameIndex = 0; frameIndex < numFrames; ++frameIndex)
{
GPUSkinningFrame frame = frames[frameIndex];
Matrix4x4[] matrices = frame.matrices;
int numMatrices = matrices.Length;
for (int matrixIndex = 0; matrixIndex < numMatrices; ++matrixIndex)
{
Matrix4x4 matrix = matrices[matrixIndex];
pixels[pixelIndex++] = new Color(matrix.m00, matrix.m01, matrix.m02, matrix.m03);
pixels[pixelIndex++] = new Color(matrix.m10, matrix.m11, matrix.m12, matrix.m13);
pixels[pixelIndex++] = new Color(matrix.m20, matrix.m21, matrix.m22, matrix.m23);
}
}
}
texture.anisoLevel = 0;
texture.filterMode = FilterMode.Point;
texture.SetPixels(pixels);
texture.Apply();
return texture;
}
c.播放动画的shader代码
//通过在顶点函数调用这个方法,获取蒙皮计算后的顶点位置
float4 GetProcessedPositionOSFromAnimationMap(float4 positionOS,float4 uv2,float4 uv3)
{
//通过MaterialPropertyBlock,告知Shader播放哪个动画,哪一帧
//蒙皮计算
float frameStartIndex = GetFrameStartIndex();
float4x4 bone1TRS = GetBoneTRSMatrix_OS(frameStartIndex,uv2.x);
float bone1Weight = uv2.y;
float4x4 bone2TRS = GetBoneTRSMatrix_OS(frameStartIndex,uv2.z);
float bone2Weight = uv2.w;
float4x4 bone3TRS = GetBoneTRSMatrix_OS(frameStartIndex,uv3.x);
float bone3Weight = uv3.y;
float4x4 bone4TRS = GetBoneTRSMatrix_OS(frameStartIndex,uv3.z);
float bone4Weight = uv3.w;
float4 processedPositionOS = mul(bone1TRS, positionOS) * bone1Weight + mul(bone2TRS, positionOS) * bone2Weight + mul(bone3TRS, positionOS) * bone3Weight + mul(bone4TRS, positionOS) * bone4Weight;
return processedPositionOS;
}
d.写一些应用层代码,通过MaterialPropertyBlock改变材质属性控制Shader播放哪一个动画的哪一帧。