国产av一二三区|日本不卡动作网站|黄色天天久久影片|99草成人免费在线视频|AV三级片成人电影在线|成年人aV不卡免费播放|日韩无码成人一级片视频|人人看人人玩开心色AV|人妻系列在线观看|亚洲av无码一区二区三区在线播放

網(wǎng)易首頁 > 網(wǎng)易號 > 正文 申請入駐

基于DOTS的大批量骨骼動畫方案及實現(xiàn)

0
分享至

【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!

這是侑虎科技第1815篇文章,感謝作者zd304供稿。歡迎轉(zhuǎn)發(fā)分享,未經(jīng)作者授權(quán)請勿轉(zhuǎn)載。如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群:793972859)

作者主頁:

https://www.zhihu.com/people/zhang-dong-13-77

眾所周知,Unity的骨骼動畫是基于SkinnedMeshRenderer實現(xiàn)的。SkinnedMeshRenderer的問題在于,如果要繪制大批量角色時,GPU的繪制效率不高。通常情況下,其瓶頸主要在于CPU將繪制數(shù)據(jù)提交給GPU,而SkinnedMeshRenderer不支持靜態(tài)合批、動態(tài)合批、GPU Instancing,導(dǎo)致以上問題無法解決。

目前已經(jīng)有一些方案來提高大批量繪制骨骼蒙皮動畫的繪制效率,比如將動畫烘焙成紋理,通過GPU Instancing來繪制骨骼蒙皮動畫。但是這種方法有很明顯的局限性,比如這種方法要實現(xiàn)動畫的淡入淡出(Cross Fade)就比較麻煩,也不那么高效。

實際上,Unity的DOTS已經(jīng)提供了一套方案來實現(xiàn)大批量骨骼動畫的繪制,但是目前僅僅是一個名為EntityComponentSystemSamples/Deformation[1]的GitHub示例,其功能離項目可用還有一段距離。

雖然現(xiàn)在還沒有DOTS版本的Animator,不過Unity的Roadmap已經(jīng)預(yù)告,正在開發(fā)中(正式發(fā)布就不知道是什么時候了)。在這之前我們可以根據(jù)需要,自己實現(xiàn)一套骨骼動畫系統(tǒng),詳情可以關(guān)注Unity的Roadmap動態(tài)[2]。

本文基于官方Sample搭建上層功能,實現(xiàn)一個最小可用的骨骼動畫播放系統(tǒng),并在項目里面應(yīng)用。以下是本系統(tǒng)源碼,Entities版本為1.0.16,推薦使用Unity 2022.3.5f1c1打開,源碼已添加使用范例:


https://github.com/zd304/DOTS_Animation_Sample

一、骨骼動畫基本原理

文章開始先簡單回顧一下骨骼動畫的基本原理。

首先從程序的角度來思考,如果要實現(xiàn)骨骼動畫,最簡單的辦法就是所有骨骼初始都處于模型坐標(biāo)系的原點,這樣計算會很簡單。因為AnimationClip的每一幀數(shù)據(jù),保存的就是對應(yīng)骨骼的變換矩陣,我們標(biāo)記為Mbone。動畫播放到某一幀,直接讀取這一幀對應(yīng)骨骼變換矩陣,乘以綁定到這個骨骼的所有頂點的初始坐標(biāo)Pvertex,就可以獲得頂點的最終坐標(biāo)Pfinal。即:

如果程序是這么實現(xiàn)骨骼蒙皮動畫的話,那美術(shù)人員就該反對了,因為把骨骼全部放到模型坐標(biāo)系的原點,這個模型就沒法看了,如下圖所示。

為了降低骨骼模型的制作難度,美術(shù)人員在模型制作軟件里面就需要可以正常擺放骨骼,美術(shù)人員擺放的這個初始姿勢我們命名為BindPose。對于人型模型,其姿勢形似字母“T”,因此也叫T-Pose。

這樣模型制作軟件在導(dǎo)出模型的時候,就需要連同模型骨骼的“從BindPose轉(zhuǎn)換回局部空間的變換矩陣”MbindPose一起導(dǎo)出來。

即美術(shù)不做的事情改由增加程序計算來做,這樣才能在游戲運行時播放動畫。也就是說,在動畫的某一幀,計算頂點位置的時候,需要先把頂點位置從“BindPose位置”變換到“相對于模型局部空間原點的位置”,再將“相對于模型局部空間原點的位置”變換到“動畫當(dāng)前幀的位置”。

以上變換過程用公式描述為:

下圖以角色手部的空間變換為例,展示骨骼蒙皮到動畫播放的完整變換過程。

二、搭建Deformation底層

本文是基于Unity官方的EntityComponentSystemSamples/Deformation[1]示例進行開發(fā)的,實現(xiàn)系統(tǒng)的第一步就是將示例代碼移植過來。

2.1 Deformation數(shù)據(jù)

在Unity項目里,通過Package Manager安裝com.unity.entities.graphics包后,運行時會自動為所有SkinnedMeshRenderer生成Entity,詳細(xì)代碼可以看com.unity.entities.graphics包里面的源碼SkinnedMeshRendererBaking.cs。

根據(jù)SkinnedMeshRenderer自動生成的組件里,最重要的一個就是SkinMatrix。

public struct SkinMatrix : IBufferElementData {     ///     /// 蒙皮變換的矩陣。     ///     public float3x4 Value; }

這個組件保存了當(dāng)前幀蒙皮變換后的矩陣,也就是前文提到的MbindPose·Mbone。這個矩陣會通過PushSkinMatrixSystem(詳細(xì)代碼可以看com.unity.entities.graphics包里面的源碼PushSkinMatrixSystem.cs)提交到GPU,在Shader里通過矩陣變換來變換頂點位置。

在播放骨骼動畫的時候,為了收集骨骼當(dāng)前的運動Pose來計算Mbone,需要在SkinnedMeshRenderer生成的Entity上綁以下兩個組件,以便獲得骨骼信息。

/// /// 非根骨骼組件,用于獲取骨骼Entity /// internal struct BoneEntity : IBufferElementData {     public Entity entity; } /// /// 根骨骼組件,用于獲取根骨骼Entity /// internal struct RootEntity : IComponentData {     public Entity value; }

同時,為了計算MbindPose,需要在SkinnedMeshRenderer生成的Entity上綁以下DynamicBuffer,來獲得所有骨骼的BindPose逆矩陣。

/// /// BindPose逆矩陣 /// internal struct BindPose : IBufferElementData {     public float4x4 value; }

2.2 Deformation蒙皮數(shù)據(jù)烘焙

這個烘焙過程就是將SkinnedMeshRenderer的數(shù)據(jù)烘焙成Entities可以訪問的組件數(shù)據(jù)。

public classSkinnedMeshAnimationAuthoring : MonoBehaviour {     ///     /// 默認(rèn)動畫名稱     ///     public string defaultAnimation;     ///     /// 默認(rèn)動畫的播放層級     ///     publicintdefaultAnimationLayer=1; } internal classSkinnedMeshAnimationBaker : Baker {     public override voidBake(SkinnedMeshAnimationAuthoring authoring)     {         varskinnedMeshRenderer= GetComponent ();         if (skinnedMeshRenderer == null)         {             return;         }         DependsOn(skinnedMeshRenderer.sharedMesh);         boolhasSkinning= skinnedMeshRenderer.bones.Length > 0 && skinnedMeshRenderer.sharedMesh.bindposes.Length > 0;         if (hasSkinning)         {             Entityentity= GetEntity(TransformUsageFlags.Dynamic);             // 接收動畫請求,決定動畫系統(tǒng)播放指定動畫,以及如何播放             varrequestBuffer= AddBuffer (entity);             if (!string.IsNullOrEmpty(authoring.defaultAnimation) && authoring.defaultAnimationLayer > 0)             {                 // 如果Prefab上配置了默認(rèn)動畫,則播放默認(rèn)動畫                 requestBuffer.Add(newAnimationRequest() { animationName = authoring.defaultAnimation, fadeoutTime = 0.0f, speed = 1.0f, layer = authoring.defaultAnimationLayer });             }             // 添加BoneBakedTag組件,表明該SkinnedMesh已經(jīng)烘焙完成,可以交給ComputeSkinMatricesBakingSystem去初始化了             AddComponent(entity, newBoneBakedTag());             // 獲得根骨骼的引用             TransformrootTransform= skinnedMeshRenderer.rootBone ? skinnedMeshRenderer.rootBone : skinnedMeshRenderer.transform;             EntityrootEntity= GetEntity(rootTransform, TransformUsageFlags.Dynamic);             AddComponent(entity, newRootEntity { value = rootEntity });             // 獲得所有骨骼的引用             DynamicBuffer boneEntities = AddBuffer (entity);             boneEntities.ResizeUninitialized(skinnedMeshRenderer.bones.Length);             for (intboneIndex=0; boneIndex < skinnedMeshRenderer.bones.Length; ++boneIndex)             {                 varbone= skinnedMeshRenderer.bones[boneIndex];                 // 為每根骨骼創(chuàng)建Entity                 varboneEntity= GetEntity(bone, TransformUsageFlags.Dynamic);                 boneEntities[boneIndex] = newBoneEntity { entity = boneEntity };             }             // 獲得每一根骨骼的BindPose逆矩陣             DynamicBuffer bindPoseArray = AddBuffer (entity);             bindPoseArray.ResizeUninitialized(skinnedMeshRenderer.bones.Length);             for (intboneIndex=0; boneIndex != skinnedMeshRenderer.bones.Length; ++boneIndex)             {                 Matrix4x4bindPose= skinnedMeshRenderer.sharedMesh.bindposes[boneIndex];                 bindPoseArray[boneIndex] = newBindPose { value = bindPose };             }         }     } }

以上代碼邏輯很簡單,不再詳細(xì)解釋。最后,將SkinnedMeshRenderer烘焙成Entity后,所有組件數(shù)據(jù)如下圖所示。


這里面多了一個AnimationRequest組件是之前沒提到過的,它是用來接收其他系統(tǒng)發(fā)送來的動畫請求的DynamicBuffer,后續(xù)講到動畫的章節(jié)會詳細(xì)介紹。

2.3 Deformation骨骼數(shù)據(jù)初始化

以上數(shù)據(jù)烘焙的過程,主要是生成SkinnedMeshRenderer對應(yīng)的Entity上面的組件。而每一根骨骼也需要初始化,就需要單獨的一個System在蒙皮數(shù)據(jù)烘焙完成后,初始化每一根骨骼的Entity上面的組件數(shù)據(jù)。

上面2.2小節(jié)會在已經(jīng)完成數(shù)據(jù)烘焙的Entity上綁一個名為BoneBakedTag的組件,有這個組件的Entity才會進入本小節(jié)的流程。

public partial classComputeSkinMatricesBakingSystem : SystemBase {     protected override voidOnUpdate()     {         varecb=newEntityCommandBuffer(Allocator.TempJob);         // 只有蒙皮數(shù)據(jù)被烘焙完成后,這個Job才會被執(zhí)行         Entities             .WithAll ()             .ForEach((Entity entity, in RootEntity rootEntity, in DynamicBuffer bones) =>             {                 // 在骨骼的Entity上綁RootTag,標(biāo)記這個Entity是根骨骼                 ecb.AddComponent (rootEntity.value);                 // 給所有骨骼加上一個Tag,以便當(dāng)計算SkinMatrices的時候可以獲取到                 for (intboneIndex=0; boneIndex < bones.Length; ++boneIndex)                 {                     // 獲取所有骨骼的Entity                     varboneEntity= bones[boneIndex].entity;                     // 調(diào)試用,這個組件可有可無                     ecb.AddComponent(boneEntity, newBoneIndex { value = boneIndex });                     // 在骨骼的Entity上綁BoneTag,標(biāo)記這個Entity是非根骨骼                     ecb.AddComponent(boneEntity, newBoneTag());                     // 在骨骼的Entity上綁SkinnedMeshAnimationController,用來控制骨骼隨著動畫幀運動                     ecb.AddComponent(boneEntity, newSkinnedMeshAnimationController() { enable = false });                     // 骨骼當(dāng)前受影響的動畫曲線(AnimationCurve)                     DynamicBuffer buffer = ecb.AddBuffer (boneEntity);                 }                 // 移除BoneBakedTag,避免這個Entity重復(fù)執(zhí)行本Job                 ecb.RemoveComponent (entity);                 ecb.SetName(rootEntity.value, "RootBone");                 ecb.SetName(entity, "SkinnedMesh"); }).WithEntityQueryOptions(EntityQueryOptions.IncludeDisabledEntities).WithStructuralChanges().Run();         ecb.Playback(EntityManager);         ecb.Dispose();     } }

以上過程主要做了兩個事情:

  • 為每根骨骼打上標(biāo)記(RootTag和BoneTag),后續(xù)運行時會通過EntityQuery查找到這些骨骼,用于計算當(dāng)前動畫的變換矩陣。

  • 為每根骨骼綁上播放動畫相關(guān)的組件,包括SkinnedMeshAnimationController和SkinnedMeshAnimationCurve,后續(xù)會詳細(xì)講解這些組件的用途。

2.4 計算變換矩陣

準(zhǔn)備好了以上數(shù)據(jù),就可以計算SkinMatrix了,也就是最終的變換矩陣:

[RequireMatchingQueriesForUpdate] [WorldSystemFilter(WorldSystemFilterFlags.Default | WorldSystemFilterFlags.Editor)] [UpdateInGroup(typeof(PresentationSystemGroup))] [UpdateBefore(typeof(DeformationsInPresentation))] partial classCalculateSkinMatrixSystemBase : SystemBase {     EntityQuery m_BoneEntityQuery;     EntityQuery m_RootEntityQuery;     protected override voidOnCreate()     {         // 查詢所有非根骨骼         m_BoneEntityQuery = GetEntityQuery(                 ComponentType.ReadOnly (),                 ComponentType.ReadOnly ()             );         // 查詢所有根骨骼         m_RootEntityQuery = GetEntityQuery(                 ComponentType.ReadOnly (),                 ComponentType.ReadOnly ()             );     }     protected override voidOnUpdate()     {         vardependency= Dependency;         // 收集所有非根骨骼的變換矩陣         varboneCount= m_BoneEntityQuery.CalculateEntityCount();         varbonesLocalToWorld=newNativeParallelHashMap (boneCount, Allocator.TempJob);         varbonesLocalToWorldParallel= bonesLocalToWorld.AsParallelWriter();         varbone= Entities             .WithName("GatherBoneTransforms")             .WithAll ()             .ForEach((Entity entity, in LocalToWorld localToWorld) =>             {                 bonesLocalToWorldParallel.TryAdd(entity, localToWorld.Value);             }).ScheduleParallel(dependency);         // 收集所有根骨骼的變換矩陣         varrootCount= m_RootEntityQuery.CalculateEntityCount();         varrootWorldToLocal=newNativeParallelHashMap (rootCount, Allocator.TempJob);         varrootWorldToLocalParallel= rootWorldToLocal.AsParallelWriter();         varroot= Entities             .WithName("GatherRootTransforms")             .WithAll ()             .ForEach((Entity entity, in LocalToWorld localToWorld) =>             {                 rootWorldToLocalParallel.TryAdd(entity, math.inverse(localToWorld.Value));             }).ScheduleParallel(dependency);         // 以上兩個Job執(zhí)行完成才能執(zhí)行下面的Job         dependency = JobHandle.CombineDependencies(bone, root);         // 計算SkinMatrix         dependency = Entities             .WithName("CalculateSkinMatrices")             .WithReadOnly(bonesLocalToWorld)             .WithReadOnly(rootWorldToLocal)             .WithBurst()             .ForEach((ref DynamicBuffer skinMatrices, in DynamicBuffer bindPoses, in DynamicBuffer bones, in RootEntity rootEtt) =>             {                 // 循環(huán)遍歷每一根骨骼                 for (inti=0; i < skinMatrices.Length; ++i)                 {                     // 非根骨骼                     varboneEntity= bones[i].entity;                     // 根骨骼Entity                     varrootEntity= rootEtt.value;                     // #TODO: this is necessary for LiveLink?                     if (!bonesLocalToWorld.ContainsKey(boneEntity) || !rootWorldToLocal.ContainsKey(rootEntity))                         return;                     // 骨骼的世界空間變換矩陣                     varmatrix= bonesLocalToWorld[boneEntity];                     // 將世界矩空間轉(zhuǎn)換到模型局部空間的變換矩陣                     varrootMatrixInv= rootWorldToLocal[rootEntity];                     // 獲得骨骼的模型局部空間的變換矩陣                     matrix = math.mul(rootMatrixInv, matrix);                     // BindPose的逆矩陣                     varbindPose= bindPoses[i].value;                     // 獲得動畫當(dāng)前幀的最終變換矩陣,傳入Shader和頂點Position相乘,獲得最終位置                     matrix = math.mul(matrix, bindPose);                     // 獎最終變換矩陣賦值給SkinMatrix                     skinMatrices[i] = newSkinMatrix                     {                         Value = newfloat3x4(matrix.c0.xyz, matrix.c1.xyz, matrix.c2.xyz, matrix.c3.xyz)                     };                 }             }).ScheduleParallel(dependency);         Dependency = JobHandle.CombineDependencies(bonesLocalToWorld.Dispose(dependency), rootWorldToLocal.Dispose(dependency));     } }

通過CalculateSkinMatrixSystemBase的計算,CPU端的矩陣數(shù)據(jù)已經(jīng)準(zhǔn)備好了,接下來看一下Shader里如何使用這些數(shù)據(jù)。

2.5 Shader

前文提到CPU通過將數(shù)據(jù)組織成由SkinMatrix組成的DynamicBuffer傳遞到GPU,那么Shader里面是如何接收這些數(shù)據(jù)的呢?

Shader Model 5(也就是Shader Target 4.5)引進了一種更為原始的訪問數(shù)據(jù)的方式,Shader里可以直接訪問CPU端傳入的二進制byte數(shù)據(jù),在Shader里面需要自行解析這些數(shù)據(jù)。微軟在HLSL里引進了ByteAddressBuffer類型,在Shader里可以自行解析來訪問這些數(shù)據(jù)。這種自行解析的二進制數(shù)據(jù)類型適合保存所有骨骼的最終變換矩陣數(shù)組,因此本Shader可以使用ByteAddressBuffer來接收SkinMatrix數(shù)據(jù)。

uniform ByteAddressBuffer _SkinMatrices;


注意:這里使用了ByteAddressBuffer,也就是說不兼容Shader Model 5的設(shè)備無法運行本游戲。

下面看一下Shader里如何利用這些數(shù)據(jù),來求出蒙皮骨骼模型最終的頂點位置。

half3x4 LoadSkinMatrix(int index) {     intoffset= index * 48;     half4p1= asfloat(_SkinMatrices.Load4(offset + 0 * 16));     half4p2= asfloat(_SkinMatrices.Load4(offset + 1 * 16));     half4p3= asfloat(_SkinMatrices.Load4(offset + 2 * 16));     return half3x4(p1.x, p1.w, p2.z, p3.y,p1.y, p2.x, p2.w, p3.z,p1.z, p2.y, p3.x, p3.w); } voidUnity_LinearBlendSkinning_float(int4 indices, half4 weights, half3 positionIn, half3 normalIn, out half3 positionOut, out half3 normalOut) {     positionOut = 0;     normalOut = 0;     // 每個頂點最多受四根骨骼影響     for (inti=0; i < 4; ++i)     {         // 通過InstanceID獲得當(dāng)前頂點的蒙皮索引         intskinMatrixIndex= indices[i] + UNITY_ACCESS_HYBRID_INSTANCED_PROP(_SkinMatrixIndex, int);         // 獲取當(dāng)前索引對應(yīng)的最終變換矩陣         half3x4skinMatrix= LoadSkinMatrix(skinMatrixIndex);         // 最終變換矩陣乘以頂點位置,獲取當(dāng)前骨骼影響下當(dāng)前頂點的最終位置         half3vtransformed= mul(skinMatrix, half4(positionIn, 1));         half3ntransformed= mul(skinMatrix, half4(normalIn, 0));         // 當(dāng)前骨骼影響下當(dāng)前頂點的最終位置乘以骨骼影響權(quán)重,求得頂點最終位置的一個分量         positionOut += vtransformed * weights[i];         normalOut += ntransformed * weights[i];     } }

如果熟悉GPU骨骼蒙皮的話,相信從以上過程很容易看出來,Unity_LinearBlendSkinning_float函數(shù)就是經(jīng)典的GPU骨骼蒙皮算法,只需要在Vertex Shader里調(diào)用該函數(shù)就可以實現(xiàn)蒙皮了,這里不再贅述。如果要優(yōu)化低端機效率,可以考慮減少受影響的骨骼數(shù)量。

需要注意的是LoadSkinMatrix函數(shù),通過調(diào)用ByteAddressBuffer的Load4函數(shù),每次取出4個float值,總共取3次組成該骨骼的最終變換矩陣。

Shader的其他部分和Deformation沒有太大關(guān)系,這里不再展開,有興趣可以下載官方例程學(xué)習(xí)。

三、動畫控制

接下來進入本文的重點章節(jié)。本章將會在以上基礎(chǔ)上實現(xiàn)一個動畫播放器,讓骨骼蒙皮動畫在Entities里高效地執(zhí)行。

本章節(jié)將實現(xiàn)一套類似Unity自帶的Animation的功能,包括以下子功能:

  • 基礎(chǔ)功能:根據(jù)動畫路徑播放動畫,并可以控制播放速度、過渡時間等

  • 動畫漸入漸出(Cross Fade)

  • 動畫層之間的動畫混合(Blend)

  • 動畫分層(Layer)播放

  • Avatar Mask

暫未實現(xiàn)的功能:動畫層之間的動畫疊加(Additive)。

3.1 自定義動畫格式

由以上內(nèi)容可知,動畫要控制的目標(biāo)就是每一根骨骼上面的LocalTransform組件的數(shù)據(jù)。只要骨骼的LocalTransform組件的數(shù)據(jù)發(fā)生改變,CalculateSkinMatrixSystemBase就會通過查詢BoneTag,將改變的數(shù)據(jù)提交到GPU,從而表現(xiàn)到圖形上。

而為了控制骨骼的LocalTransform組件,已經(jīng)無法再使用傳統(tǒng)的Aniamtion或者Animator了,需要自己寫一套類似的代碼來實現(xiàn)。因此,需要自定義一種類似于AnimationClip的動畫格式,提供給Entities計算。下面使用ScriptableObject來定義這種動畫格式。

[Serializable] publicclassBakedKeyframe {     ///     /// 幀時間     ///     publicfloat time;     ///     /// 關(guān)鍵幀的值     ///     public Vector4 value;     ///     /// 關(guān)鍵幀進入的曲線切線     ///     public Vector4 inTangent;     ///     /// 關(guān)鍵幀出去的曲線切線     ///     public Vector4 outTangent; } [Serializable] publicclassBakedCurve {     ///     /// 曲線作用的骨骼索引     ///     publicint boneIndex;     ///     /// 曲線的所有關(guān)鍵幀     ///     public List keyframes =  newList (); } publicclassSkinnedMeshAnimationClip : ScriptableObject {     ///     /// 動畫長度     ///     publicfloat length;     ///     /// 動畫包裝模式     ///     public AnimationWrapType wrapType;     ///     /// 所有骨骼的位置和縮放曲線:xyz保存位置信息,w保存縮放信息     ///     public BakedCurve[] posAndSclCurves;     ///     /// 所有骨骼的旋轉(zhuǎn)曲線:xyzw保存四元數(shù)信息     ///     public BakedCurve[] rotCurves; }

根據(jù)以上數(shù)據(jù)的定義可知,新的動畫片段的格式為SkinnedMeshAnimationClip,其成員posAndSclCurves和rotCurves是兩個曲線數(shù)組,這兩個數(shù)組的Length等于骨骼數(shù)量,數(shù)組的索引就是骨骼索引。

前文提到過AnimationRequest,這個類里面就包含了一個字符串用來指定要播放的動畫路徑,也就是SkinnedMeshAnimationClip的路徑。

public struct AnimationRequest : IBufferElementData {     ///     /// 動畫路徑     ///     public FixedString128Bytes animationName;     public int layer;     public float speed;     public float fadeoutTime;     public FixedString128Bytes maskPath; }

當(dāng)本動畫系統(tǒng)接收到動畫播放請求的時候,就會根據(jù)這個路徑去加載SkinnedMeshAnimationClip動畫片段數(shù)據(jù)。

加載完成的動畫數(shù)據(jù)是SkinnedMeshAnimationClip類型的,這是一個class。但是為了利用Burst編譯,需要將其轉(zhuǎn)換成struct提供給Entities使用。

于是需要定義一套對應(yīng)的struct數(shù)據(jù)。

/// /// 運行時關(guān)鍵幀 /// public struct Keyframe {     publicfloat time;     public float4 value;     public float4 inTangent;     public float4 outTangent; } /// /// 運行時曲線 /// public struct AnimationCurve {     ///     /// 關(guān)鍵幀數(shù)據(jù)     ///     public BlobArray keyframes;     publicint boneIndex;     ///     /// 曲線類型:PositionAndScale或者Rotation     ///     public KeyframePropertyType propertyType; } /// /// 運行時動畫曲線組件 /// internal struct SkinnedMeshAnimationCurve : IBufferElementData {     ///     /// 曲線所處的動畫層     ///     publicint layer;     ///     /// 曲線的開始播放時間     ///     publicfloat startTime;     ///     /// 曲線的持續(xù)時間     ///     publicfloat duration;     ///     /// 曲線的播放速度     ///     publicfloat speed;     ///     /// 曲線的包裝類型     ///     public AnimationWrapType wrapType;     ///     /// 曲線的幀數(shù)據(jù)     ///     public BlobAssetReference curveRef;     ///     /// 運行時臨時變量:正在進行Cross Fade的動畫信息     ///     public SkinnedMeshLayerFadeout layerFadeout;     ///     /// 運行時臨時變量:即將取代當(dāng)前曲線的下一條曲線信息     ///     public SkinnedMeshAninationFadeout nextCurve; } /// /// 對應(yīng)動畫片段的緩存數(shù)據(jù) /// internal classAnimationCurveCache {     ///     /// 動畫片段長度     ///     publicfloat length;     ///     /// 動畫包裝類型     ///     public AnimationWrapType wrapType;     ///     /// Entities可以使用的位置和縮放曲線     ///     public SkinnedMeshAnimationCurve[] posAndSclCurves;     ///     /// Entities可以使用的旋轉(zhuǎn)曲線     ///     public SkinnedMeshAnimationCurve[] rotCurves; }

由以上數(shù)據(jù)定義可知,SkinnedMeshAnimationClip加載后將會轉(zhuǎn)化成運行時數(shù)據(jù)AnimationCurveCache。AnimationCurveCache之所以是class而不是struct,是因為這個類也并非直接傳入Entities使用的。真正傳入Entities使用的是動畫片段的每一條動畫曲線SkinnedMeshAnimationCurve。也就是說,骨骼當(dāng)前正在播放的動畫,會拆分到曲線這么細(xì)的粒度作為組件,存在于骨骼Entity上。

BakedCurve對應(yīng)的就是SkinnedMeshAnimationCurve,這個結(jié)構(gòu)體里有兩個臨時對象layerFadeout和nextCurve,后文會介紹,這里先略過,其他成員對象都比較好理解,不一一解釋。需要理解的是,每一根骨骼上,同一時刻,通常情況只會存在“2×layer數(shù)量”個正在播放的SkinnedMeshAnimationCurve對象,也就是一組PositionAndScale曲線和一組Rotation曲線,除非這根骨骼正處于Cross Fade階段。

下面看一下加載代碼。

// 請求的動畫名稱 stringanimationName= currentReq.animationName.ToString(); // 查詢該動畫曲線資源是否已經(jīng)被加載過 if (!animationCurveCache.TryGetValue(animationName, out AnimationCurveCache animCache)) {     // 通過SkinnedMeshAnimationClip類型的Asset來初始化動畫曲線組件     SkinnedMeshAnimationClipclip= Resources.Load (animationName);     if (clip == null)     {         Debug.LogError($"Loading {animationName} failed!");         return;     }     animCache = newAnimationCurveCache()     {         length = clip.length,         wrapType = clip.wrapType,         posAndSclCurves = newSkinnedMeshAnimationCurve[clip.posAndSclCurves.Length],         rotCurves = newSkinnedMeshAnimationCurve[clip.rotCurves.Length],     };     // 加載(初始化)動畫曲線資源     InitAnimationCache(animCache, clip, clip.posAndSclCurves.Length);     animationCurveCache.Add(animationName, animCache); }

其中InitAnimationCache函數(shù)的功能就是將BakedCurve數(shù)據(jù)拷貝到SkinnedMeshAnimationCurve,代碼量較多且邏輯簡單,這里不再贅述,詳細(xì)實現(xiàn)請看源碼。

3.2 播放動畫

當(dāng)動畫系統(tǒng)接收到播放動畫的請求后,如上文所述,動畫系統(tǒng)將會去獲取動畫的所有曲線數(shù)據(jù)。獲得曲線數(shù)據(jù)之后,就需要將曲線數(shù)據(jù)設(shè)置到對應(yīng)的每一根骨骼上作為組件,通過專門的System來執(zhí)行動畫播放邏輯,從而修改骨骼的LocalTransform。

骨骼的Entity綁定了一個DynamicBuffer 類型的組件,來保存當(dāng)前正在播放的動畫曲線。

SkinnedMeshAnimationCurve有兩個重要的成員:

  • Layer:當(dāng)前曲線正在播放的動畫層級;

  • PropertyType:當(dāng)前動畫曲線的數(shù)據(jù)類型。

3.2.1 動畫Cross Fade

將要添加到骨骼上面的動畫曲線會先查看當(dāng)前骨骼正在播放的動畫曲線,是否有和自己相同Layer和PropertyType的動畫曲線。如果有的話,就說明需要進行動畫曲線替換。

動畫曲線的替換如果僅僅是簡單的賦值替換,表現(xiàn)上可能會出現(xiàn)動畫銜接生硬,動畫有“跳幀”的感覺。因此需要引入Cross Fade的概念,來讓動畫的替換呈現(xiàn)平滑過渡的效果。因此,將要替換的動畫,會先保存到將要被替換的SkinnedMeshAnimationCurve里的nextCurve字段里(也就是3.1小節(jié)提到后文會介紹的字段)。此時內(nèi)存里同時存在一老一新兩個SkinnedMeshAnimationCurve,程序就可以根據(jù)播放時間,對兩個動畫進行動畫融合,實現(xiàn)Cross Fade的效果。

當(dāng)過渡時間結(jié)束,將會用SkinnedMeshAnimationCurve.nextCurve替換原來的SkinnedMeshAnimationCurve。

過渡時間內(nèi),曲線Value進行的是簡單的線性插值:

3.2.2 動畫分層

前文2.3小節(jié)提到過,每一根骨骼上會有一個名為SkinnedMeshAnimationController的組件,該組件的功能之一就是用來決定骨骼當(dāng)前播放的動畫層是哪一層。

public struct SkinnedMeshAnimationController : IComponentData {     ///     /// 標(biāo)記當(dāng)前骨骼是否受動畫影響     ///     public bool enable;     ///     /// 當(dāng)前骨骼正在播放的層級     ///     public int currentLayer; }

骨骼上每次發(fā)生動畫曲線的變動,比如動畫曲線替換、動畫曲線播放結(jié)束等,都會重新計算當(dāng)前正在播放的動畫層。計算方法很簡單,就是遍歷所有動畫曲線,取最大值賦值給SkinnedMeshAnimationController.currentLayer即可。

如果曲線的SkinnedMeshAnimationCurve.layer低于SkinnedMeshAnimationController.currentLayer,則該曲線不再執(zhí)行動畫。

總結(jié)下來,對于骨骼來說,如果同時存在多個層級的動畫曲線,只播放層級最高的那條動畫曲線。

3.2.3 動畫層Blend

正常來說動畫層Blend是需要為動畫層設(shè)置權(quán)重,來讓多個層進行動畫混合的。本動畫系統(tǒng)由于項目需要,不需要設(shè)置動畫層權(quán)重,因此動畫層的Blend僅僅用在:當(dāng)某一層動畫即將播放結(jié)束時,漸漸過渡到下一層動畫。也就是說,本動畫系統(tǒng)使用動畫層Blend來實現(xiàn)動畫跨層的Cross Fade。

為了實現(xiàn)動畫層之間的Cross Fade,動畫曲線在添加到骨骼上之前,就需要先做排序。

  • 將不同PropertyType的曲線放到相鄰位置,也就是在DynamicBuffer 內(nèi)部根據(jù)PropertyType進行分組存放;

  • 根據(jù)Layer進行降序排序。

排好序的動畫曲線,在進行for循環(huán)遍歷計算時候,以下計算過程的時間復(fù)雜度將會是O(1)。

排好序的動畫曲線,在同一個PropertyType分組內(nèi),就可以計算當(dāng)前播放層的動畫是否即將結(jié)束,如果動畫即將結(jié)束了,下一層動畫就不要跳過執(zhí)行。下一層動畫執(zhí)行的結(jié)果和當(dāng)前層動畫的執(zhí)行結(jié)果進行線性差值,使層和層之間的動畫過渡也變得平滑:

3.2.4 Avatar Mask

由于無法使用Animation和Animator,因此Avatar Mask的數(shù)據(jù)也需要自定義。

public class SkinnedMeshBoneMask : ScriptableObject {     ///     /// 允許播放動畫的骨骼index     ///     public List

 mask; }

SkinnedMeshBoneMask保存了允許播放動畫的所有骨骼索引。開始播放動畫的時候,如前文所述,需要把動畫的曲線添加到骨骼上,與此同時,加載SkinnedMeshBoneMask并且過濾允許播放動畫的骨骼,在這個索引列表里的骨骼才允許將動畫曲線添加到該骨骼上,通過這種方法實現(xiàn)Avatar Mask。

for (inti=0; i < bonesBuffer.Length; ++i) {     BoneEntitybone= bonesBuffer[i];     // Avatar Mask包含此骨骼才允許更新此骨骼的動畫     if (maskAsset != null && !maskAsset.mask.Contains(i))     {         continue;     }     ...     // 獲取當(dāng)前骨骼上所有正在播放的動畫曲線的數(shù)組     if (!curveLookup.TryGetBuffer(bone.entity, out DynamicBuffer curveBuffer))     {         continue;     }     for (intcIndex=0; cIndex < curveBuffer.Length; ++cIndex)     {         SkinnedMeshAnimationCurvecurve= curveBuffer[cIndex];         ......         curveBuffer.Add(curve);     }     ...... }

四、調(diào)用播放動畫接口

讓指定角色播放某個動畫,需要兩步操作:

  • 獲得角色的DynamicBuffer 組件;

  • 往DynamicBuffer 組件里添加元素,即播放動畫請求。

// 獲得DynamicBuffer if (!animRequestLookup.TryGetBuffer(characterBody.body,     out DynamicBuffer animRequest)) {     return; } // 往DynamicBuffer 里添加播放動畫請求 animRequest.Add(newAnimationRequest() {     animationName = animPath, // 動畫片段的路徑     fadeoutTime = 0.5f, // Cross Fade的持續(xù)時間     speed = 1.0f, // 播放速度,默認(rèn)1.0     layer = 1, // 動畫層,高層數(shù)動畫覆蓋低層數(shù)動畫     maskPath = maskPath // Avatar Mask的路徑 });

下圖為本系統(tǒng)的應(yīng)用范例,其中邊移動邊施放閃電應(yīng)用了動畫分層和Avatar Mask,從移動動畫過渡到站立動畫應(yīng)用了Cross Fade。

通過Frame Debugger可以看到,200+個角色只有1個Batch,也就是達(dá)到了合批的目的。

五、總結(jié)

本文基于Unity的官方例程,實現(xiàn)了一個類似于Unity的Animation的骨骼蒙皮動畫系統(tǒng),滿足項目的特定需求。這套系統(tǒng)的特點是,在滿足基本功能的前提下,能夠高效地渲染出骨骼蒙皮動畫。這套系統(tǒng)在項目上補全了DOTS欠缺的基礎(chǔ)系統(tǒng),為開發(fā)者使用DOTS制作3D游戲提供基礎(chǔ)設(shè)施。

參考:

[1]EntityComponentSystemSamples/Deformation


https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/GraphicsSamples/URPSamples/Assets/SampleScenes/5.%20Deformation

[2]Unity的Roadmap動態(tài)


https://unity.com/cn/roadmap#unity-platform

文末,再次感謝zd304 的分享, 作者主頁: https://www.zhihu.com/people/zhang-dong-13-77, 如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群: 793972859 )。

近期精彩回顧

【學(xué)堂上新】

【厚積薄發(fā)】

【萬象更新】

【學(xué)堂上新】

特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相關(guān)推薦
熱點推薦
李元吉五個兒子全被殺,為何他的后人卻繁衍了三百年?

李元吉五個兒子全被殺,為何他的后人卻繁衍了三百年?

小豫講故事
2026-01-11 06:00:07
官方通報“亞運冠軍遭索要獎金”調(diào)查結(jié)果

官方通報“亞運冠軍遭索要獎金”調(diào)查結(jié)果

第一財經(jīng)資訊
2026-01-10 23:46:34
平分黃海?李在明表態(tài)重大升級,中國尚未簽聲明,6字描述沒出現(xiàn)

平分黃海?李在明表態(tài)重大升級,中國尚未簽聲明,6字描述沒出現(xiàn)

面包夾知識
2026-01-10 13:05:00
為什么最近股市大漲?

為什么最近股市大漲?

橙先生看大盤
2026-01-11 11:38:26
北大才子楊舒春,不顧父母跪求拒進外交部,癡迷種地,后來怎樣了

北大才子楊舒春,不顧父母跪求拒進外交部,癡迷種地,后來怎樣了

以茶帶書
2025-12-19 20:25:26
1972年陳毅追悼會,江青故意無視宋慶齡,毛主席當(dāng)場下一死命令,事后宋慶齡感慨:主席真聰明

1972年陳毅追悼會,江青故意無視宋慶齡,毛主席當(dāng)場下一死命令,事后宋慶齡感慨:主席真聰明

寄史言志
2025-12-17 16:08:14
俄羅斯與法國交換被捕人員

俄羅斯與法國交換被捕人員

參考消息
2026-01-10 18:35:32
2026監(jiān)管利刃出鞘!外賣平臺“燒錢搶存量市場”迎來終局?

2026監(jiān)管利刃出鞘!外賣平臺“燒錢搶存量市場”迎來終局?

野馬財經(jīng)
2026-01-10 22:29:05
伊朗警告特朗普:若遭攻擊,必將還擊

伊朗警告特朗普:若遭攻擊,必將還擊

新京報政事兒
2026-01-11 15:22:48
反擊來了!當(dāng)大貨車變成流動全國的水果店的那一刻,年年漲價的水果店慌了?。?!

反擊來了!當(dāng)大貨車變成流動全國的水果店的那一刻,年年漲價的水果店慌了!?。?/a>

張曉磊
2026-01-11 12:13:43
女子半月逛30次超市買大量桶裝水,店長決定報警,警方破門后震驚

女子半月逛30次超市買大量桶裝水,店長決定報警,警方破門后震驚

青青會講故事
2025-03-19 10:31:38
原來一切都是故意的,閆學(xué)晶的目的達(dá)到了,別提多高興了

原來一切都是故意的,閆學(xué)晶的目的達(dá)到了,別提多高興了

手工制作阿殲
2026-01-08 13:41:25
林強涉案989億被抓!生活奢華超過中東富豪,超5萬百姓血本無歸

林強涉案989億被抓!生活奢華超過中東富豪,超5萬百姓血本無歸

千言娛樂記
2025-12-27 20:07:06
沒進過全明星,卻能1換7!能進絕殺但效率暴跌!這交易后悔了嗎?

沒進過全明星,卻能1換7!能進絕殺但效率暴跌!這交易后悔了嗎?

阿浪的籃球故事
2026-01-10 16:30:50
中國向全球曝光美4400顆衛(wèi)星圍堵中國空間站

中國向全球曝光美4400顆衛(wèi)星圍堵中國空間站

花寒弦絮
2026-01-09 22:03:26
沉默11天后,73歲李顯龍發(fā)聲,臺海和平至關(guān)重要,不許改變現(xiàn)狀

沉默11天后,73歲李顯龍發(fā)聲,臺海和平至關(guān)重要,不許改變現(xiàn)狀

南宮一二
2026-01-09 18:02:34
中國首例五胞胎終于長大了,父親因勞累去世,母親直言后悔生下他們

中國首例五胞胎終于長大了,父親因勞累去世,母親直言后悔生下他們

等風(fēng)來育兒聯(lián)盟
2025-08-01 12:21:35
真正忽悠具俊曄的是大S!大S去世后,具俊曄表現(xiàn)深情也是無奈之舉

真正忽悠具俊曄的是大S!大S去世后,具俊曄表現(xiàn)深情也是無奈之舉

小娛樂悠悠
2025-12-21 10:10:12
調(diào)查:30歲健美冠軍之死

調(diào)查:30歲健美冠軍之死

新民周刊
2026-01-09 21:18:09
誰是開拓者非賣品?波特蘭媒體列出多人名單:楊瀚森阿夫迪亞在列

誰是開拓者非賣品?波特蘭媒體列出多人名單:楊瀚森阿夫迪亞在列

羅說NBA
2026-01-11 07:06:22
2026-01-11 17:44:49
侑虎科技UWA incentive-icons
侑虎科技UWA
游戲/VR性能優(yōu)化平臺
1538文章數(shù) 986關(guān)注度
往期回顧 全部

科技要聞

“我們與美國的差距也許還在拉大”

頭條要聞

網(wǎng)約車送斷指乘客在交警帶路闖紅燈時出車禍 被判全責(zé)

頭條要聞

網(wǎng)約車送斷指乘客在交警帶路闖紅燈時出車禍 被判全責(zé)

體育要聞

詹皇曬照不滿打手沒哨 裁判報告最后兩分鐘無誤判

娛樂要聞

留幾手為閆學(xué)晶叫屈?稱網(wǎng)友自卑敏感

財經(jīng)要聞

外賣平臺"燒錢搶存量市場"迎來終局?

汽車要聞

2026款宋Pro DM-i長續(xù)航補貼后9.98萬起

態(tài)度原創(chuàng)

親子
本地
教育
房產(chǎn)
手機

親子要聞

爸媽總是對外人比對我好

本地新聞

云游內(nèi)蒙|“包”你再來?一座在硬核里釀出詩意的城

教育要聞

監(jiān)考老師怎么抓作弊?很容易抓,同學(xué)們千萬別作弊!

房產(chǎn)要聞

66萬方!4755套!三亞巨量房源正瘋狂砸出!

手機要聞

iQOO Z11 Turbo新機官宣搭載電競信號增強芯片雷霆Z1

無障礙瀏覽 進入關(guān)懷版