建立大量角色的GPU动画系统

【博物纳新】是UWA旨在为开发者推荐新颖、易用、有趣的开源项目,帮助你们在项目研发之余发现世界上的热门项目、前沿技术或者使人惊叹的视觉效果,并探索将其应用到本身项目的可行性。不少时候,咱们并不知道本身想要什么,直到某一天咱们遇到了它。数组

更多精彩内容请关注:lab.uwa4d.com数据结构


导读

Unity中建立的动画角色数量的提高,每每受到DrawCall、IK效果和CPU Skinning等CPU端的性能限制。本文介绍的项目提供了一种使用GPU进行动画渲染的方法,减轻CPU负担,从而可以建立上万的数量级的动画角色。app

开源库连接:https://lab.uwa4d.com/lab/5d0167a272745c25a80ac832函数

数据结构的准备

一、结构体LODData,用来存储不一样细节要求的Mesh。oop

public struct LodData
{
        public Mesh Lod1Mesh;
        public Mesh Lod2Mesh;
        public Mesh Lod3Mesh;

        public float Lod1Distance;
        public float Lod2Distance;
        public float Lod3Distance;
}

二、结构体AnimationTextures,用于储存转换成Texture2D数据的动画片断,每一个动画片断会在顶点处进行三次采样。性能

public struct AnimationTextures : IEquatable<AnimationTextures>
{
        public Texture2D Animation0;
        public Texture2D Animation1;
        public Texture2D Animation2;
}

三、结构体AnimationClipData,存储原始的动画片断和该动画片断在Texture2D中对应的起始和终止像素。测试

public class AnimationClipData
{
        public AnimationClip Clip;
        public int PixelStart;
        public int PixelEnd;
}

四、结构体BakedData,存储转换成Texture2D变量后的动画片断数据和Mesh、LOD、帧率等。动画

public class BakedData
{
        public AnimationTextures AnimationTextures;
        public Mesh NewMesh;
        public LodData lods;
        public float Framerate;
        ...
}

五、结构体BakedAnimationClip,存储Animation Clip数据在材质中的具体位置信息。spa

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;
        ...
}

六、结构体GPUAnimationState,存储动画片断的持续时间和编号。线程

public struct GPUAnimationState : IComponentData
   {
        public float Time;
        public int   AnimationClipIndex;
        ...
    }

七、结构体RenderCharacter,准备好Material、Animation Texture、Mesh以后就能够准备进行绘制了。

struct RenderCharacter : ISharedComponentData, IEquatable<RenderCharacter>
{
        public Material                Material;
        public AnimationTextures        AnimationTexture;
        public Mesh                Mesh;
        ...
}

函数方法的准备

一、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];
...

二、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);

三、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的生成。

四、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);

五、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);

绘制

一、建立绘制的角色列表

private List<RenderCharacter> _Characters = new List<RenderCharacter>();
private Dictionary<RenderCharacter, InstancedSkinningDrawer> _Drawers = new Dictionary<RenderCharacter, InstancedSkinningDrawer>();
 private EntityQuery m_Characters;

二、对要绘制的角色实例化一个Drawer

drawer = new InstancedSkinningDrawer(character.Material, character.Mesh, character.AnimationTexture);

三、传输坐标和LocalToWorld矩阵

var coords = m_Characters.ToComponentDataArray<AnimationTextureCoordinate>(Allocator.TempJob, out jobA);
var localToWorld = m_Characters.ToComponentDataArray<LocalToWorld>(Allocator.TempJob, out jobB);

四、调用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好项目!
请输入图片描述

今天的推荐就到这儿啦,或者它可直接使用,或者它须要您的润色,或者它启发了您的思路......

请不要吝啬您的点赞和转发,让咱们知道咱们在作对的事。固然若是您能够留言给出宝贵的意见,咱们会越作越好。

相关文章
相关标签/搜索