建立大量角色的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好項目!
請輸入圖片描述

今天的推薦就到這兒啦,或者它可直接使用,或者它須要您的潤色,或者它啓發了您的思路......

請不要吝嗇您的點贊和轉發,讓咱們知道咱們在作對的事。固然若是您能夠留言給出寶貴的意見,咱們會越作越好。

相關文章
相關標籤/搜索