创建大量角色的GPU动画系统
【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目、前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性。很多时候,我们并不知道自己想要什么,直到某一天我们遇到了它。
更多精彩内容请关注:lab.uwa4d.com
导读
Unity中创建的动画角色数量的提升,往往受到DrawCall、IK效果和CPU Skinning等CPU端的性能限制。本文介绍的项目提供了一种使用GPU进行动画渲染的方法,减轻CPU负担,从而能够创建上万的数量级的动画角色。
开源库链接:https://lab.uwa4d.com/lab/5d0167a272745c25a80ac832
数据结构的准备
1、结构体LODData,用来存储不同细节要求的Mesh。
public struct LodData
{
public Mesh Lod1Mesh;
public Mesh Lod2Mesh;
public Mesh Lod3Mesh;
public float Lod1Distance;
public float Lod2Distance;
public float Lod3Distance;
}
2、结构体AnimationTextures,用于储存转换成Texture2D数据的动画片段,每个动画片段会在顶点处进行三次采样。
public struct AnimationTextures : IEquatable<AnimationTextures>
{
public Texture2D Animation0;
public Texture2D Animation1;
public Texture2D Animation2;
}
3、结构体AnimationClipData,存储原始的动画片段和该动画片段在Texture2D中对应的起始和终止像素。
public class AnimationClipData
{
public AnimationClip Clip;
public int PixelStart;
public int PixelEnd;
}
4、结构体BakedData,存储转换成Texture2D变量后的动画片段数据和Mesh、LOD、帧率等。
public class BakedData
{
public AnimationTextures AnimationTextures;
public Mesh NewMesh;
public LodData lods;
public float Framerate;
...
}
5、结构体BakedAnimationClip,存储Animation Clip数据在材质中的具体位置信息。
public struct BakedAnimationClip
{
internal float TextureOffset;
internal float TextureRange;
internal float OnePixelOffset;
internal float TextureWidth;
internal float OneOverTextureWidth;
internal float OneOverPixelOffset;
public float AnimationLength;
public bool Looping;
...
}
6、结构体GPUAnimationState,存储动画片段的持续时间和编号。
public struct GPUAnimationState : IComponentData
{
public float Time;
public int AnimationClipIndex;
...
}
7、结构体RenderCharacter,准备好Material、Animation Texture、Mesh之后就可以准备进行绘制了。
struct RenderCharacter : ISharedComponentData, IEquatable<RenderCharacter>
{
public Material Material;
public AnimationTextures AnimationTexture;
public Mesh Mesh;
...
}
函数方法的准备
1、CreateMesh()
从已有的SkinnedMeshRenderer和一个Mesh创建一个新的Mesh。如果第二个参数Mesh为空,则生成的新的newMesh是原来Renderer的sharedMesh的复制。
private static Mesh CreateMesh(SkinnedMeshRenderer originalRenderer, Mesh mesh = null)
通过boneWeights的boneIndex0和boneIndex1生成boneIDs,通过weight0和weight1生成boneInfluences,作为newMesh的UV2和UV3存储起来。
boneIds[i] = new Vector2((boneIndex0 + 0.5f) / bones.Length, (boneIndex1 + 0.5f) / bones.Length);
float mostInfluentialBonesWeight = boneWeights[i].weight0 + boneWeights[i].weight1;
boneInfluences[i] = new Vector2(boneWeights[i].weight0 / mostInfluentialBonesWeight, boneWeights[i].weight1 / mostInfluentialBonesWeight);
...
newMesh.uv2 = boneIds;
newMesh.uv3 = boneInfluences;
如果第二个参数Mesh非空,找到Mesh在sharedMesh中对应的bindPoses,把boneIndex0和boneIndex1映射到给定的Mesh上。
...
boneRemapping[i] = Array.FindIndex(originalBindPoseMatrices, x => x == newBindPoseMatrices[i]);
boneIndex0 = boneRemapping[boneIndex0];
boneIndex1 = boneRemapping[boneIndex1];
...
2、SampleAnimationClip()
SampleAnimationClip方法接收动画对象,单个动画片段,SkinnedMeshRenderer,帧率作为输入,输出动画片段采样过后生成的boneMatrices
private static Matrix4x4[,] SampleAnimationClip(GameObject root, AnimationClip clip, SkinnedMeshRenderer renderer, float framerate)
...
//选取当前所在帧的clip数据作为一段时间的采样
float t = (float)(i - 1) / (boneMatrices.GetLength(0) - 3);
clip.SampleAnimation(root, t * clip.length);
3、BakedClips()
BakedClips方法,接收动画根对象,动画片段数组,帧率,LOD数据作为输入,输出BakedData。
public static BakedData BakeClips(GameObject animationRoot, AnimationClip[] animationClips, float framerate, LodData lods)
//该方法首先获取动画根对象子对象的SkinMeshRenderer
var skinRenderer = instance.GetComponentInChildren<SkinnedMeshRenderer>();
//利用这个skinRenderer作为CreateMesh方法的参数生成 BakedData的NewMesh
bakedData.NewMesh = CreateMesh(skinRenderer);
//BakedData的LodData结构体中的mesh成员也使用CreateMesh方法生成,只不过需要的第二个参数是输入lod的mesh成员
var lod1Mesh = CreateMesh(skinRenderer, lods.Lod1Mesh);
...
bakedData.lods = new LodData(lod1Mesh, lod2Mesh, lod3Mesh, lods.Lod1Distance, lods.Lod2Distance, lods.Lod3Distance);
//BakedData的framerate直接使用输入的framerate
bakedData.Framerate = framerate;
//使用SampleAnimationClip方法对每个动画片段采样得到sampledMatrix,然后添加到list中
var sampledMatrix = SampleAnimationClip(instance, animationClips[i], skinRenderer, bakedData.Framerate);
sampledBoneMatrices.Add(sampledMatrix);
//使用sampledBoneMatrices的维数参数作为关键帧和骨骼的数目统计
numberOfKeyFrames += sampledMatrix.GetLength(0);
int numberOfBones = sampledBoneMatrices[0].GetLength(1);
//使用骨骼数和关键帧数作为大小创建材质
var tex0 = bakedData.AnimationTextures.Animation0 = new Texture2D(numberOfKeyFrames, numberOfBones, TextureFormat.RGBAFloat, false);
//将sampledBoneMatrices的数据全部存入到材质颜色中
texture0Color[index] = sampledBoneMatrices[i][keyframeIndex, boneIndex].GetRow(0);
//创建Dictionary字段
bakedData.AnimationsDictionary = new Dictionary<string, AnimationClipData>();
//生成AnimationClipData需要的开始结束位置
PixelStart = runningTotalNumberOfKeyframes + 1,
PixelEnd = runningTotalNumberOfKeyframes + sampledBoneMatrices[i].GetLength(0) - 1
至此完成BakedData的生成。
4、AddCharacterComponents()
//Add方法是把角色转换成可以使用GPU渲染的关键
public static void AddCharacterComponents(EntityManager manager, Entity entity, GameObject characterRig, AnimationClip[] clips, float framerate)
//利用manager在entity中依次添加animation state,texturecoordinate,rendercharacter
var animState = default(GPUAnimationState);
animState.AnimationClipSet = CreateClipSet(bakedData);
manager.AddComponentData(entity, animState);
manager.AddComponentData(entity, default(AnimationTextureCoordinate));
manager.AddSharedComponentData(entity, renderCharacter);
5、InstancedSkinningDrawer()
public unsafe InstancedSkinningDrawer(Material srcMaterial, Mesh meshToDraw, AnimationTextures animTexture)
//需要的ComputeBuffer只有76个字节,这也是CPU占用低的主要原因,传递的数据是顶点的转移矩阵和它在材质中的坐标
objectToWorldBuffer = new ComputeBuffer(PreallocatedBufferSize, 16 * sizeof(float));
textureCoordinatesBuffer = new ComputeBuffer(PreallocatedBufferSize, 3 * sizeof(float));
调用DrawMeshInstancedIndirect方法实现在场景中绘制指定数量的角色。
Graphics.DrawMeshInstancedIndirect(mesh, 0, material, new Bounds(Vector3.zero, 1000000 * Vector3.one), argsBuffer, 0, new MaterialPropertyBlock(), shadowCastingMode, receiveShadows);
绘制
1、创建绘制的角色列表
private List<RenderCharacter> _Characters = new List<RenderCharacter>();
private Dictionary<RenderCharacter, InstancedSkinningDrawer> _Drawers = new Dictionary<RenderCharacter, InstancedSkinningDrawer>();
private EntityQuery m_Characters;
2、对要绘制的角色实例化一个Drawer
drawer = new InstancedSkinningDrawer(character.Material, character.Mesh, character.AnimationTexture);
3、传输坐标和LocalToWorld矩阵
var coords = m_Characters.ToComponentDataArray<AnimationTextureCoordinate>(Allocator.TempJob, out jobA);
var localToWorld = m_Characters.ToComponentDataArray<LocalToWorld>(Allocator.TempJob, out jobB);
4、调用Draw()方法
即是DrawMeshInstancedIndirect()方法。
drawer.Draw(coords.Reinterpret_Temp<AnimationTextureCoordinate, float3>(), localToWorld.Reinterpret_Temp<LocalToWorld, float4x4>(), character.CastShadows, character.ReceiveShadows);
效果展示
(角色数量400) (角色数量10000)性能分析
考虑到Android端GPU性能的不足,适当减少了生成角色的数量并且采用了较少细节的LOD模型。角色数量减少为100个,LOD面片数量约180个,动画片段保持不变。
测试机型为红米4X、红米Note2和小米8:
同时由于该项目使用了Unity的Jobs系统,大量的计算工作被迁移到Worker线程中,大大节省了CPU主线程的耗时。
快用UWA Lab合辑Mark好项目!
今天的推荐就到这儿啦,或者它可直接使用,或者它需要您的润色,或者它启发了您的思路......
请不要吝啬您的点赞和转发,让我们知道我们在做对的事。当然如果您可以留言给出宝贵的意见,我们会越做越好。