【博物納新】是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好項目!
今天的推薦就到這兒啦,或者它可直接使用,或者它須要您的潤色,或者它啓發了您的思路......
請不要吝嗇您的點贊和轉發,讓咱們知道咱們在作對的事。固然若是您能夠留言給出寶貴的意見,咱們會越作越好。