解析UE動畫系統——核心實現

2024年2月6日 20点热度 0人点赞

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


一、想寫的內容有哪些

動畫系統是引擎核心功能之一,之前用Unity開發,隻是用用編輯器,一直沒太深入去看原理。最近看了看UE的功能和源碼,收獲很多,對動畫是怎麼跑起來的,有了更深的理解,同時看的時候也遇到很多問題。

關於動畫的實現,資料並不多,很多隻是大致說一下流程,有些又說的太高級,省略了很多細節,隻提出一個方向,所以又到處查源碼,總算弄清楚了一些問題。

這篇博客,想分享兩方面內容:

一是自己對動畫系統的理解,通過遇到的一些問題,結合源碼,加上用Unity的經驗,有一些解答,不一定對,畢竟沒自己實現過,有一定局限性,歡迎大傢留言討論。

二是UE的實現,熟悉底層邏輯,也能更好地使用和擴展。對於實現來說,不止一種方式,UE的代碼也經過很多次迭代,更重要的是理解背後的設計思路,這也是看代碼的樂趣所在,並不是隻看個流程,重要的是這個思考的過程。

動畫從最底層來說,是骨骼的旋轉、平移、縮放,加上蒙皮,這篇博客更關註整個動畫系統的邏輯,這些數學計算,算法比較成熟,對於我來說,就是一些固定公式,相信偉大的數學傢們說的都是對的,暫時不會細看這部分。

二、動畫系統設計

首先要實現一套動畫系統,需要先分解功能,有個基本的思路。對於動畫系統來說,簡單的可以分為兩層來做,一是實現核心功能,二是基於核心功能,針對特定問題,給出支持的方案。

核心功能分解

首先,最核心的,是讓模型動起來,基於動畫管線實現。這部分在擴展動畫的時候幾乎不用改,是動畫的最底層。

然後,是怎麼組織多個動作,動作的選擇和控制(計算順序、骨骼控制等)。控制的復雜度,源於讓玩傢感覺整體動作是流暢的這樣一個需求。

對遊戲引擎來說,需要一個中間類,去和遊戲邏輯交互,以及驅動動畫系統運行。也就是處理輸入和輸出,加上驅動動畫播放。

這樣,就實現了一個遊戲的基礎動畫系統。但是,僅僅這些還不夠,還需要針對一些問題,做擴展,才是個完整的系統。

解決特定問題

對遊戲引擎來說,能播放動畫,隻是動畫系統的核心功能,而不是個完整的系統。因為動畫和遊戲邏輯是有很多交互的,引擎要針對這些特定問題,或者說是需求,給出解決方案。

  • 要獲取動畫進度,給外部做判斷,或是用於處理動畫之間的融合,通過曲線的形式保存數據。
  • 動畫調用外部接口,比如特定幀播特效音效等,以通知的形式。
  • 有些動畫,比如技能,和遊戲邏輯關聯大,UE提供蒙太奇的功能,和slot結合實現。
  • 性能上的考慮,比如不可見的是否更新,遠距離的是否降低更新頻率等。
  • 一些高級的動畫效果,比如Root Motion、Motion Matching等。
  • 骨骼重定向等功能,簡化工作流。

這樣就大致做了功能模塊分解,到了實現代碼的部分。那寫代碼有一些基本原則,在UE的實現上是怎麼體現的呢,大概想到以下幾點:

代碼結構:高內聚,低耦合的原則。體現為分層設計,加上模塊化功能。

  1. 功能模塊:資源(動畫序列、混合空間),曲線,蒙太奇,通知,插槽,實現特定功能。
  2. 中間層,由節點組成。節點分類型,連接功能模塊和控制層,單向依賴基礎層。
  3. 控制層,核心是藍圖。藍圖依賴節點,將節點組成流程。USkeletalMeshComponent,驅動動畫系統運行,作為動畫系統和外部的中間類。

擴展性:在實際開發中,如果對動畫的要求較高,那擴展是必不可少的,UE主要有兩種方式

  1. 通過接口的形式,表現為節點,通過節點實現不同邏輯,嵌入到動畫流程中。基於依賴倒置原則,節點實現接口,動畫流程依賴這些接口。
  2. 繼承,組件,藍圖實例都可以繼承,自定義運行流程。

性能:體現在資源和計算量上,動畫在這兩方面消耗都不低。

  • 計算上:將邏輯和顯示分離。對於服務器,以及客戶端不可見的模型,可以隻做遊戲邏輯相關計算。計算骨骼數據,隻是為了顯示,不影響邏輯,分到多線程去做。
  • 資源上:支持單獨控制部分骨骼,將動畫組合使用,減少資源量。支持骨骼重定向等操作。

想清楚這些問題,看代碼的時候更容易理解現有的代碼結構。當然還有很多我想不到的問題,UE的代碼也是一個一個版本迭代上來的,中間很多設計很難從結果上看出來,盡量理解就好。

三、動畫系統實現結構

功能模塊結構圖

UE提供的功能分四部分,但實際項目,數據層一般是自己寫C 代碼,性能好些,所以重點是前面的三個部分。

從使用上,可以分為這幾個模塊,但是代碼實現上,實際比這更復雜。下面主要關註代碼的實現。

核心層

目的是實現動畫管線,動畫管線本身是個抽象的概念。UE通過節點,根據數據和骨骼操作,調用核心層提供的接口,實現管線。

  1. 采樣:通過資源的封裝實現。
  2. 混合:都是通過幾種模式對應的函數實現,一般是FAnimationRuntime實現。
  3. 後處理:對現有姿勢做調整,按一定算法和條件,一般節點自己實現,用於擴展自定義效果。

混合和後處理的區別,混合的目的是處理動畫間的過渡,算法相對固定。後處理是對動作做調整,為了和場景更好的匹配,一般是IK。

控制層:分編輯和邏輯兩部分

編輯:通過節點,提供數據和骨骼操作,給核心層,驅動動畫管線運行。

  • 在使用上可以理解為就是動畫藍圖。組合節點,實現一個流程,達到控制動畫輸出的目的。
  • 節點可以理解為策略模式,可動態替換,每種節點類型都是一組策略,方便編輯和擴展。
  • 節點之間,是組合的關系,特定節點有順序關聯,大部分可互相關聯。這樣操作起來更靈活,可以互相關聯嵌套。

邏輯:用於執行節點。

  • 對應UAnimInstance類,隻負責動畫表現相關的邏輯。
  • 主線程計算邏輯,主要是處理蒙太奇。
  • 其他線程執行藍圖,反向執行節點。可以先思考一下為什麼要反向執行,後邊會寫我的理解。

遊戲邏輯交互層

以上模塊,隻能實現一個靜態的動畫,而不是一個可交互的系統,這一層是接收玩傢的輸入,驅動動畫系統運行,並將結果展示給玩傢。

對內,驅動系統運行,調用UAnimInstance,處理邏輯相關數據、URO等。
對外,觸發動畫相關事件,提供各種獲取數據的接口。

控制層和交互層,實現了遊戲邏輯和顯示效果解耦,遊戲邏輯隻關心發生了什麼,提供數據,具體表現效果由動畫系統決定。

動畫執行流程

畫了一個大致的流程,算是動畫執行的主線流程吧,一些細節和分支沒畫,避免結構太復雜。對照著代碼實現看,會方便一些。

四、動畫管線實現

什麼是動畫管線

動畫管線指一系列運算,把輸入(動畫資源、混合設置),變換成輸出(局部及全局姿勢、渲染用的矩陣調色板)。

定義有些抽象,簡單理解,就是把生成一個動作的處理,分三個邏輯階段,輸入一些數據,得到一個姿勢。

邏輯上分三部分,采樣、融合、後處理。大致流程是,采樣需要的多個動畫,加上各種參數、條件做融合,之後後處理,輸出的一個姿勢。

UE實現機制
不同於渲染管線,動畫管線是個抽象概念,UE裡通過節點實現。對應管線的三部分邏輯,UE實現上也是分別實現這三個邏輯。

采樣

分兩部分,一是采樣數據,由資源提供接口。二是外部驅動流程。

采樣接口

  • 由資源實現,UE提供了幾種資源,基礎的是UAnimSequence,采樣接口在這實現。其他資源,是對資源的另一層封裝,包括混合空間和蒙太奇,蒙太奇是個實用的擴展,後面細說。真正的采樣都是GetBonePose方法。
  • 核心邏輯是,外部傳遞一個時間(保存在FAnimExtractContext結構體中),取到鄰近的兩幀,結合設置的插值算法,返回數據。實現上要比描述的復雜一些,涉及到壓縮相關的處理。
  • 采樣的結果,不隻是骨骼數據,還包括曲線和屬性,這些都是跟動畫幀變化的數據。
  • 分3個方法計算:EvaluateCurveData、DecompressPose、GetCustomAttributes。

流程

  • 首先由狀態機選擇要播放的動畫,對應FAnimNode_AssetPlayerBase節點,觸發采樣需求。
  • 采樣資源之前,先計算時間,因為要加上動畫本身的播放速度。由link節點調用UpdateAssetPlayer,在資源有效的情況下,計算當前動畫時間(判斷有效范圍,加上播放速度)。
  • 然後從link節點調用Update_AnyThread,執行節點的Update邏輯,獲取時間對應的數據,保存在FAnimationPoseData,包括姿勢、曲線、屬性。

融合

融合的效果是將多個動畫,按一定算法,生成一個動畫。

基礎是變換運算

  • 對姿勢做基於權重的運算。
  • 兩種方式,由BlendTransform函數實現。基於權重的覆蓋:目標變換=源變換*權重疊加:目標變換=目標變換 (源變換*權重)

姿勢混合有3種方式

  1. 線性插值:兩個姿勢的中間姿勢,各有權重,用於動畫過渡。權重的算法,可以實現不同的曲線,對應不同融合效果。
  2. 加法混合:用於疊加,一般是基礎動畫,加上一個特殊狀態,比如在基礎的移動上,疊加上受傷、拿武器等。優點是可以用組合的方式減少資源。
  3. 骨骼分部混合:不同骨骼分別播動畫,按部位分離,也是可以減少資源,有些動畫控制特定骨骼就可以了,比如實現上半身攻擊下半身移動的效果。

融合一個作用是動畫過渡

  • 標準過渡:源動畫到目標動畫的過渡。指定時間段,用線性插值實現。這是個基礎的過渡。
  • 慣性過渡:當前姿勢到目標動畫的過渡。有些時候,標準過渡效果不一定好,比如在跑的時候起跳,如果融合跑和跳的動作,看起來就是在空中還在跑,這時用慣性過渡可能更好。UE已經有對慣性過渡的支持,Unity好像還沒有,最近沒關註。

融合觸發方式

  1. 采樣資源時直接做融合,由擴展的資源實現。
  2. 通過指定的節點,輸入多個pose做融合。

核心算法實現,在FAnimationRuntime類,節點和資源會調用這個類的方法。

後處理

作用是對動畫姿勢做校正,主要是各部位的IK,因為做動畫的時候,生成的是和環境無關的姿勢,而實際運行中,動作要和周圍的環境有一定的匹配,這樣才顯得更真實。

具體算法由
FAnimNode_SkeletalControlBase子類實現,UE實現了多種IK算法,之後在細看看每種算法的實現邏輯。

五、節點機制

節點理解

節點可以說是UE實現靈活編輯動畫流程的基礎,在藍圖上自由關聯節點、關聯藍圖,離不開節點的支持。

用樹的方式來理解的話,OutPut Pose是根節點,那些動畫資源播放節點是葉子節點,姿勢混合節點是中間節點。然後通過控制節點關聯到一起。

節點機制用到了策略模式和組合模式。策略模式,體現為節點可以互相替換,這樣也支持了擴展。組合模式體現為節點可以通過PoseLink互相連接,也就實現了自由編輯流程的效果,PoseLink這名字,也說明了節點的最終功能是計算pose。

UE通過節點,將對動作的操作,抽象為對輸入輸出的數據的操作,這樣不管加了什麼邏輯,隻要輸入的數據和輸出的數據結構相同,就可以互相連接,也是基於這樣的原理,支持的自定義擴展。

節點分類

節點主要有三個功能:

  1. 實現特定功能
  2. 控制流程,連接到節點,以及連接到其他藍圖
  3. 擴展,自定義節點,將邏輯插入動畫流程中

節點基類:FAnimNode_Base,不存儲數據,提供虛函數,在指定的時間點被藍圖調用,子類實現具體邏輯。核心函數,包括Initialize_AnyThread、CacheBones_AnyThread、Update_AnyThread、Evaluate_AnyThread。

根節點:FAnimNode_Root,對應藍圖中最後用於輸出的OutPut節點。賦值到FAnimInstanceProxy,作為藍圖運行的節點的起點。

要處理特殊生命周期的節點,保存在
UAnimBlueprintGeneratedClass。存下來是為了在調用函數的時候更快,而不用真的遍歷所有節點,因為隻有少數幾個幾點,需要在這幾個時間點處理邏輯。

    TArray<FStructProperty*> PreUpdateNodeProperties;
    TArray<FStructProperty*> DynamicResetNodeProperties;
    TArray<FStructProperty*> StateMachineNodeProperties;
    TArray<FStructProperty*> InitializationNodeProperties;

實現特定功能

  • 播放特定資源:基類FAnimNode_AssetPlayerBase,對應幾種物理資源,包括原始動畫序列、混合空間等,但是不包括蒙太奇,蒙太奇本身作為資源存儲,但播放方式比較特殊,之後單獨分析。
  • 混合:FAnimNode_BlendListBase,主要邏輯是計算權重,然後調用FAnimationRuntime的混合接口。
  • 操作骨骼:FAnimNode_SkeletalControlBase,實現IK等,比較復雜。

控制流程

  • FAnimNode_StateMachine:實現狀態機
  • FAnimNode_CustomProperty:可以關聯到其他藍圖。

節點的執行

  • 按執行順序從最後一個節點FAnimNode_Root開始,調用保存的FPoseLink::Evaluate,執行link關聯的節點,Evaluate_AnyThread方法。反序遞歸,通過link連接到依賴的節點,從而依次執行。節點關聯的linkpose,在子節點定義需要一個或多個,沒有定義,表示不依賴其他節點的輸入,是遞歸的終結點。
  • 反序遍歷的好處,是確保執行的節點都對最終結果有效,不會有多餘計算。
  • 節點的核心邏輯,分update和evaluate兩部分。update計算的邏輯,可能會影響其他節點,所以要和evaluate分別遍歷。
  • 數據保存,傳遞comp創建的FParallelEvaluationData結構體,代替返回值,減少臨時內存分配。每個節點,內部創建FPoseContext,然後用右值的方式賦值給comp。

節點的同步

這裡的同步,並不是多線程之間的同步。而是用於確保一些節點邏輯隻執行一次。

  • 需要做同步的原因是,有些類型的節點,在藍圖拖出來多個,實際對應的C 類,隻會創建一個,藍圖上的節點隻是引用,典型的節點是緩存pose,一幀隻緩存一次。

同步的基礎是FGraphTraversalCounter結構體,主要是記錄執行次數和執行時的幀數。

  • 相當於是對當前幀數,和操作是否執行做了一次封裝。
  • 提供兩個實例數據比較的接口,可以判讀是否需要執行。
  • 因為URO的機制,執行的計數可能比總幀數少。

實現方法

  • proxy對每個操作定義一個變量,保存一份全局信息。
    FGraphTraversalCounter InitializationCounter;
    FGraphTraversalCounter CachedBonesCounter;
    FGraphTraversalCounter UpdateCounter;
    FGraphTraversalCounter EvaluationCounter;
    FGraphTraversalCounter SlotNodeInitializationCounter;
  • 其他節點,有同步需求時也會定義一個對應的結構,比如FAnimNode_SaveCachedPose,對init、cachedBones、update、evaluation都需要同步。

同步實現邏輯不復雜,就是剛看名字的時候容易想歪,既不是多線程同步,和URO也沒關系,看代碼的時候在這迷惑了半天。

六、藍圖實現

藍圖邏輯看起來很復雜,實際核心功能就是驅動節點運行,加上處理一些可以在主線程處理的邏輯,以及保存數據。流程理清楚就可以了。

實現上分兩個類,AnimInstance和AnimInstanceProxy,目的是讓動畫系統高效運行,將邏輯數據和表現數據分別計算,邏輯數據在主線程,表現數據分到其他線程。

  • inst的作用,是執行動畫藍圖。分為兩部分,一是和遊戲邏輯相關的,比如通知。二是遊戲邏輯無關的,純動畫表現,通過proxy執行,放到額外線程。
  • proxy是對節點的封裝,並將節點分類,按生命周期調用節點函數。也負責動畫邏輯和inst的交互,以動畫通知的形式。

UAnimInstance
藍圖的父類。對內封裝動畫流程,對外和組件交互。可以繼承,實現自定義邏輯。

幾個主要功能,體現為一些被組件調用的函數。

  • 封裝蒙太奇功能
  • 調用一些指定時間點的藍圖實現的函數
  • 提供在各個線程,獲取FAnimInstanceProxy的接口原因是game線程和工作線程,不能同時訪問這個數據。如果是game線程訪問,如果task運行中則等待task完成,否則直接獲取。工作線程訪問,如果當前是game線程,應該會報錯。調用組件的HandleExistingParallelEvaluationTask方法,執行完當前異步操作。
  • 和組件交互,UpdateCurvesToComponents,將曲線信息提供給組件處理,針對材質類型的曲線。
  • RecalcRequiredBones,處理骨骼,初始化、lod變化都會執行。

更新相關接口

核心邏輯有兩個,一個是inst實現的,更新動畫相關邏輯數據。一個是proxy實現的更新動畫相關的顯示數據。

UpdateAnimation處理邏輯數據

  • 將更新分為兩步,preupdate和updatepreupdate主要是做初始化,重置數據等,也會調用proxy的preupdateupdate主要是處理蒙太奇,因為蒙太奇是種特殊的動畫實現,和邏輯相關,放在主線程處理。

ParallelEvaluateAnimation處理顯示數據
多線程下被工作線程調用,可設置為主線程。根據UpdateAnimation計算後產生的控制變量,通過節點計算修改骨骼。

用FParallelEvaluationData保存計算後的骨骼、曲線和屬性數據。

EvaluateAnimation函數,調用保存的根節點,開始執行各個節點。

SkeletalMeshComponent邏輯

用於處理動畫系統和遊戲邏輯的交互,對外,處理遊戲相關邏輯。對內,封裝動畫系統,通過inst驅動系統運行。

處理遊戲相關邏輯,一個是對玩傢操作動畫的影響,一個是從動畫取數據,反饋給遊戲邏輯。

驅動動畫系統更新。分三步,更新邏輯(和遊戲邏輯相關),異步計算骨骼位置,提交渲染。

這部分代碼不少,一些判斷條件較多,執行流程可以結合上邊發的圖來看,具體邏輯就不寫了,打個斷點看一下,基本了解流程也就可以了,核心的邏輯還是依靠AnimInstance和AnimInstanceProxy實現。

七、蒙太奇

實現的功能

表現上,蒙太奇是種動畫資源,但是實際上,隻是引用了資源,本身可以看做一條邏輯線,用於連接動畫和遊戲邏輯。

  • 在使用上來說,提供了一種直接通過藍圖或C 代碼控制動畫資源的方式。
  • 在資源層面,實現對多個動畫序列編輯,生成一個資源從資源的繼承體系,可以看到,蒙太奇是對基本資源做的擴展可以將多個動畫序列合並為單個資產並通過藍圖和C 播放。簡化了動畫資源的管理,將多個相互關聯的動畫當做一個處理。
  • 播放上,做了擴展,提供更多控制接口可以創建多個蒙太奇分段,在運行時,按一定邏輯以任何順序動態播放。可以在蒙太奇分段面板中控制分段之間的過渡,也可以使用藍圖(Blueprints)在分段之間設置更復雜的過渡行為。在順序播放的基礎上支持跳轉,以及正向和反向播放。編輯結果相當於創建一條支持跳轉的邏輯線,這條邏輯線可以驅動關聯的動作的播放,也可以隻用來觸發動畫通知或控制曲線。
  • 通過slot插入到動畫藍圖中可以通過節點,播放蒙太奇後,指定要覆蓋的骨骼,這樣就可以和現有動作很好的結合,又減少了資源數量。關聯到動畫資源,但本身不處理動畫資源,隻提供進度數據。
  • 邏輯交互和遊戲邏輯交互,通過動畫通知的形式。在不需要顯示動畫的地方,可以單獨更新邏輯,比如服務器,或客戶端不可見的模型。也有特定的蒙太奇通知,支持立即觸發,更精確的控制動畫。
  • 遊戲應用適合多段的動畫,比如射擊遊戲的換子彈。rpg遊戲的一些特殊狀態,比如浮空,中間的時間不固定,起始的動作是固定的。
    1)這種情況,如果按Unity的處理,就是增加個子狀態機。
    2)而用蒙太奇的機制,就可以當做一個普通動作,簡化了狀態機,因為動作沒掛在狀態機上,加載時的內存也減少了。加載時也比加載多個動作方便。邏輯相關的動作,技能為主。

解決什麼問題

提供蒙太奇的功能,是為了簡化使用,即使沒有蒙太奇,動畫功能也完全能實現,隻是麻煩很多。可以想象UE也是不斷遇到類似的需求,然後才抽出這樣一個模塊,和我們平時重構系統,提出公共模塊一樣的道理。

按我的理解,蒙太奇的核心想法,是將動畫系統分為純表現和表現 邏輯兩部分,對應兩種播放方式。每部分職責更明確,簡化遊戲邏輯。

  • 純表現的,比如移動相關的,邏輯層不關心動畫播到哪。對於同步效果來說,可以在看不見的時候不處理,比如進入視野時,左右腳誰在前面不重要,但是技能動作播到哪就很重要了。
  • 蒙太奇,用於實現邏輯相關的動作,服務器處理蒙太奇的通知,減少計算量。
  • 邏輯分離,更好的支持服務器邏輯。對客戶端也是優化,對不可見的隻計算邏輯,減少計算量。
  • Unity沒有這個支持,隻有個不可見時是否更新的選項,要增加一部分邏輯處理。比如視野外的玩傢放技能,為了在進入視野的適合播正確的動作,要不就是完全更新動畫,要不是在邏輯層記錄時間,可見時強制設置動畫時間,而UE可以在底層處理這部分邏輯。

另一方面,可以簡化狀態機,狀態機上的動畫是預先放好的,加載時要占內存。而有些動畫,不經常播,動態加載,對內存比較好,也降低了狀態機的復雜度。這時就可以通過slot 蒙太奇動畫,在狀態機上預留一個位置,運行時替換,這樣新的動畫,就可以結合狀態機原本的狀態,實現IK等效果。蒙太奇本身擴展了動畫的邏輯,相當於也實現了一部分狀態機的功能。

實現方式

邏輯上可以分三部分:

邏輯線,play狀態下每次更新計算一個進度。

  • 蒙太奇本身分為兩個類處理,UAnimMontage代表資源,FAnimMontageInstance封裝運行時邏輯。
  • 計算進度,封裝了一個FMontageSubStepper結構體處理,由FAnimMontageInstance::Advance調用。Advance函數內也處理事件和關聯動畫的通知。

采樣接口,通過藍圖節點FAnimNode_Slot使用。

  • 調用proxy的SlotEvaluatePose方法,傳slot名字,proxy內部,通過保存的FSlotAnimationTrack數組,按名字找到動畫序列

蒙太奇的播放,其實很簡單,蒙太奇本身邏輯隻計算一個進度,然後slot節點通過proxy找到蒙太奇對應的動畫資源,調用資源的采樣方法,給節點提供骨骼數據。

遊戲邏輯交互,包括播放接口,開始和結束的回調,以及特殊的蒙太奇通知。

  • 播放接口很簡單,就是inst提供的Montage_Play。
  • 通過Montage_SetEndDelegate可以設置播放結束的回調,結束方式有兩種,正常播完和打斷,分別保存在QueuedMontageEndedEvents和QueuedMontageBlendingOutEvents數組。另外在所有蒙太奇都播完,會觸發一個OnAllMontageInstancesEnded。事件不會立即觸發,而是在骨骼計算完成後,通過DispatchQueuedAnimEvents函數觸發。
  • UE定義了蒙太奇的特殊動畫通知,UAnimNotify_PlayMontageNotify,支持瞬時的和持續性的通知,會觸發inst定義的OnPlayMontageNotifyEnd回調。

八、URO(Update Rate Optimization)

降低更新頻率,是一種很常見的優化思路,實現邏輯並不是簡單的隔幾幀更新一次,而是在中間插入了一些插值的幀,一定程度上避免動作顯示跳幀的問題。這種優化方式,會影響動畫效果,但性能提升也很明顯,UE還提供了一個預算分配器插件,可以更精細的控制頻率。

頻率的選擇,會根據距離計算一個LOD等級,從這個角度來說,遠處的模型動畫更新頻率低點影響也不大,本身就不會特別關註遠處的目標,一些特殊情況下,比如當前場景角色很少,或是遠處是個大型boss,可以單獨處理,優化總是要和實際情況結合才能有更好的效果。

實現上分三個步驟,首先計算更新頻率,判斷當前幀是否需要更新。然後將更新分為兩步,update和evaluate,其中evaluate頻率一定是update的整數倍,因為evaluate執行時需要update計算的數據,要確保update先執行過。

步驟一:計算更新頻率
更新頻率相關參數,封裝了結構體FAnimUpdateRateParameters,分為兩個模式Trail和LookAhead,LookAhead用於處理Root Motion。分別記錄update和evaluate在當前幀是否需要更新,以及跳過了多少幀等數據。

FAnimUpdateRateManager命名空間用於封裝一些方法,計算更新頻率相關數據。

計算入口在TickUpdateRate,最終調用AnimUpdateRateSetParams函數計算。

步驟二:update動畫
邏輯很簡單,基於上一步計算的數據,如果不更新,整個藍圖節點都不會執行。

判斷的地方有兩個,一個是TickPose函數。一個是DispatchParallelTickPose,用在AlwaysTickPose模式下。

步驟三:骨骼計算

  • 有兩個參數,決定這一幀骨骼的計算方式,bDoEvaluation是否計算骨骼,bDoInterpolation是否插值,組合起來,對一幀的計算,有4種可能性。
  • 需要插值,同時需要evaluation,則執行藍圖。將藍圖計算結果保存在CachedComponentSpaceTransforms。之後和上次骨骼做混合。混合的比例由EOptimizeMode類型計算。
  • 需要插值,不需要evaluation,調用FAnimationRuntime::LerpBoneTransforms,將cache和上次的骨骼位置做混合。
  • 不需要插值,但是計算evaluation,結果保存在ComponentSpaceTransforms。骨骼正常計算,采樣時間疊加上跳過的時間。同時將保存數據到cache。
  • 不需要插值,也不計算,則跳過這一幀。

九、動畫系統總結

以上這些,就是動畫最核心的實現了,是個層層封裝的結構,節點實現動畫管線,藍圖管理節點,組件驅動動畫系統運行。流程上一些細節,看看代碼都好理解。

整個動畫系統,還包括IK、表情、Motion Matching等應用,以後看到了再來分享。

對比Unity,UE可能對和遊戲邏輯的交互支持的更好一些,本身提供的功能也要更多一些,比如Unity一般IK都要通過插件去做,而UE基本實現了常見的IK算法。Unity的狀態機也比較簡單,但是這種簡單帶來了使用上的復雜,一般遊戲如果動作多的話,狀態機連的十分復雜,沒有像UE這樣更清晰的分層。

蒙太奇和URO,不是動畫系統的必要邏輯,但十分實用,這也能看出UE是在開發遊戲的,知道開發的痛點在哪,並能給出很好的方案。

第一次看動畫系統的實現,收獲很多,但也可能有些地方理解的不對,歡迎大傢留言討論,一起探索UE的各個功能。


這是侑虎科技第1532篇文章,感謝作者星辰大海供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群:465082844)

作者主頁:星辰大海 - 知乎

再次感謝星辰大海的分享,如果您有任何獨到的見解或者發現也歡迎聯系我們,一起探討。(QQ群:465082844)