【博物纳新】是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好项目!
今天的推荐就到这儿啦,或者它可直接使用,或者它须要您的润色,或者它启发了您的思路......
请不要吝啬您的点赞和转发,让咱们知道咱们在作对的事。固然若是您能够留言给出宝贵的意见,咱们会越作越好。