天天看點

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

這篇文章主要是深入到代碼裡面,來研究UE裡的動畫狀态機裡的權重計算問題,還有使用BlendProfile時各個Bone的權重計算,結尾順便學習了一下遇到的Conduit動畫節點

此篇文章是為了解決這麼幾個具體問題:

  • 多個Transition鍊條的條件同時觸發時,狀态機應該如何處理
  • 動畫狀态機如何配置設定State的權重
  • State、Pose和Bone之間的差别與權重的計算方法

多個Transition同時觸發

假設有這麼個狀态機,當我觸發條件時,這兩條紅色箭頭指向的Transition的條件會同時被觸發:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

那麼此時的PositionOne和PositionTwo的權重應該怎麼變化?難道是PositionOne的權重變化完了才變化PositionTwo的?

Unity裡不知道怎麼處理這個情況的,我看了下UE5,PositionOne和PositionTwo的權重是同時變化的。這個過程類似于一個水流的走向,如下圖所示,是我截取到的一個中間狀态,可以看到,這裡的權重總和為1,我發現在這個轉态過程中,PositionTwo的權重往往都比PositionOne要高一些:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

圖上有個Active for…s的标志,它代表了State Machine目前的動畫flow的方向的終點(類似于水流的終點),在StateMachineNode裡有個相關的資料:

// State machine node
USTRUCT()
struct ENGINE_API FAnimNode_StateMachine : public FAnimNode_Base
{
...
protected:
	// The current state within the state machine
	// 很重要的資料, 其實就是代表了狀态機要播放的State(或者說最終動畫流入的End), 當CurrentState
	// 對應出口的TransitionRule傳回true時, 動畫流向下一個State, 此CurrentState值開始記錄下一個State的id
	// 此值隻會在FAnimNode_StateMachine的Update_AnyThread節點在do while循環裡被SetState函數修改
	// 在動畫狀态機唯一展示的Active For ...s是State應該就是此CurrentState對應的State
	int32 CurrentState;
	...
}
           

至于具體State的權重怎麼算的,後面會再提到

關于FAnimationActiveTransitionEntry.Alpha

在UE源碼裡,這裡的每個轉态對應一個

FAnimationActiveTransitionEntry

對象,裡面有這麼個資料,代表動畫過渡的Blend權重值:

// 此對象會在FAnimNode_StateMachine::Update_AnyThread函數裡被建立, 應該是在動畫狀态機裡發生動畫轉态時
// 會基于FAnimationTransitionBetweenStates, 使用placement new執行個體化一個此對象, 替代原本的ActiveTransitionArray
// Information about an active transition on the transition stack
USTRUCT()
struct FAnimationActiveTransitionEntry
{
	GENERATED_USTRUCT_BODY()

	// Elapsed time for this transition
	float ElapsedTime;

	// The transition alpha between next and previous states
	float Alpha;
	...
}
           

當動畫狀态機節點裡的圖如下所示時,此時的StateID順序是0->2->1:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

出現了奇怪的情況,我了解的是這倆Transition,其Alpha應該是0.04和0.96才對,但是代碼裡的值卻是這樣的:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

由于是一起觸發Transition條件的,這裡的ElapsedTime的值一樣我是能了解的,但是這裡的Alpha值為啥是一樣的?

仔細分析了一下,我發現上圖所示的進度0%、4%、96%,代表的是Slate對應動畫的權重,而這裡的Alpha,代表的是State根據時間擷取到的播放進度,這個播放進度是由以下資料得到的:

  • Transition開始之後過去的時間:ElapsedTime
  • Transition設定是CrossFade的時間:CrossfadeDuration
  • Blend權重的方式:BlendOption

預設的BlendOption是Cubic的方式,比如我如果選擇線性的,那麼這個PositionTwo的進度條會跟時間的值一樣,如下圖所示:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

這裡的Alpha的具體計算代碼如下所示:

// If non-zero, calculate the query alpha
float QueryAlpha = 0.0f;
if (CrossfadeDuration > 0.0f)
{
	// QueryAlpha是目前播放進度的x值
	QueryAlpha = ElapsedTime / CrossfadeDuration;
}

// 基于QueryAlpha, 計算得到實際Blend的weight值(是以整體動畫的weight值跟Blend這個FAlphaBlend對象是沒關系的)
Alpha = FAlphaBlend::AlphaToBlendOption(QueryAlpha, Blend.GetBlendOption(), Blend.GetCustomCurve());
           

是以說,這裡的

FAnimationActiveTransitionEntry.Alpha

就是動畫轉态的轉态進度,它是随着CrossFade的時間來不斷增長的,當增長到1時,說明動畫完成了轉态過程。前面的例子裡,倆Transition由于是一起觸發Transition條件的,且CrossFade時間和BlenOption也相同,那算得的ElapsedTime的值也是一樣的,是以Alpha值也是一樣的

動畫狀态機裡State權重的計算方法

如下圖所示,還是前面那個狀态機,此時選用的BlendOption為Linear,可以看到,PositionTwo為最終要過度到的State,它的權重為60%,但是其他的24%和16%,我不知道怎麼算出來的:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

算法應該是這樣,一切以最終State的進度為主,比如這裡PositionTwo的進度為6.044/10 = 60.44%,那麼剩下的39.56%的權重就交給PositionOne和PositionZero來分,同樣的,PositionOne此時的權重為39.56% * 0.6044 = 23.91%,那麼最後剩下的就是39.56% - 23.91% = 15.64%

核心函數為

FAnimNode_StateMachine::GetStateWeight

,這個函數被調用很多次,最後得到最終的權重:

// - Callded in FAnimNode_StateMachine::SetState
// - Callded in FAnimNode_StateMachine::Update_AnyThread
// - Callded in FAnimNode_StateMachine::CacheBones_AnyThread
// - Callded in FAnimNode_StateMachine::UpdateTransitionStates
// - Callded in FAnimNode_StateMachine::GatherDebugData
// Returns the blend weight of the specified state, as calculated by the last call to Update()
float FAnimNode_StateMachine::GetStateWeight(int32 StateIndex) const
{
	const int32 NumTransitions = ActiveTransitionArray.Num();
	if (NumTransitions > 0)
	{
		// 周遊所有ActiveTransition
		// Determine the overall weight of the state here.
		float TotalWeight = 0.0f;
		for (int32 Index = 0; Index < NumTransitions; ++Index)
		{
			const FAnimationActiveTransitionEntry& Transition = ActiveTransitionArray[Index];

			// SourceWeight應該指的是Transition裡PreviousState的Weight
			float SourceWeight = (1.0f - Transition.Alpha);

			// After the first transition, so source weight is the fraction of how much all previous transitions contribute to the final weight.
			// So if our second transition is 50% complete, and our target state was 80% of the first transition, then that number will be multiplied by this weight
			if (Index > 0)
			{
				TotalWeight *= SourceWeight;
			}
			// 在第一個Transition裡, 1 - Alpha的值代表其Previous State的權重
			//during the first transition the source weight represents the actual state weight
			else if (Transition.PreviousState == StateIndex)
			{
				TotalWeight += SourceWeight;// 第一次計算權重時, 使用加法
			}

			// The next state weight is the alpha of this transition. We always just add the value, it will be reduced down if there are any newer transitions
			if (Transition.NextState == StateIndex)
			{
				TotalWeight += Transition.Alpha;
			}
		}

		return FMath::Clamp<float>(TotalWeight, 0.0f, 1.0f);
	}
	else
	{
		return (StateIndex == CurrentState) ? 1.0f : 0.0f;
	}
}
           

看了下,在動畫狀态機轉态這個過程中,它先後會被以下地方調用:

  • FAnimNode_StateMachine::Update_AnyThread裡針對每個ActiveTransition對象調用FAnimNode_StateMachine::UpdateTransitionStates
  • FAnimNode_StateMachine::Update_AnyThread裡,在完成transition的update後,周遊StatePoseLinks對應的每個FPoseLink,record相關state weight

StateMachineNode裡State、Pose和Bone之間的權重計算

實在是搞得有點暈,必須在這裡寫下來整理一下

準确的說,這裡的Pose其實是大多數Bone(不設BlendProfile)的那些Bone的權重,是以這裡其實是探究State權重與Bone之間的關系。首先,提出以下問題:

  • AccumulateWithShortestRotation

    這種函數與普通Blend函數有差別麼?
  • StateMachine裡不同state一起帶着權重生效時,是否就是單純按照其權重,再把每個State的輸出Pose進行Blend?
  • State的權重與Bone之間的權重如何一起影響最終的Pose?是相乘的關系嗎,如果是的話,那麼是不是state之間的權重和為1,而且sate乘以BoneWeight的權重總和也要為1?

AccumulateWithShortestRotation與普通Blend函數

Accumalte類的函數其實是增量Blend,其實在Pose進行Blend時,有兩種做法:

  • 直接計算,就是我預先已經把要Blend的所有Pose和權重都計算好了,那麼我直接把它們基于權重累加起來,寫入FinalPose的buffer裡就行了
  • 累加計算,比如我先算最第一個的Pose,此時乘以對應權重,再Overwrite到FinalPose的buffer上,至于後面的Pose,我再慢慢算,算出來再乘以對應的權重,Accumulate到FinalPose的buffer上即可。

代碼如下,其實就是

baseTransform.Add(deltaAdditiveTransform, additiveWeight)

struct alignas( TAlignOfTransform<T>::Value ) TTransform
{
	/**
	 * Accumulates another transform with this one, with an optional blending weight
	 *
	 * Rotation is accumulated additively, in the shortest direction (Rotation = Rotation +/- DeltaAtom.Rotation * Weight)
	 * Translation is accumulated additively (Translation += DeltaAtom.Translation * Weight)
	 * Scale3D is accumulated additively (Scale3D += DeltaAtom.Scale * Weight)
	 *
	 * @param DeltaAtom The other transform to accumulate into this one
	 * @param Weight The weight to multiply DeltaAtom by before it is accumulated.
	 */
	FORCEINLINE void AccumulateWithShortestRotation(const TTransform<T>& DeltaAtom, const ScalarRegister& BlendWeightScalar)
	{
		const TransformVectorRegister BlendWeight(BlendWeightScalar.Value);
		// 貌似這裡的transform的rotation是用vector表示的, 應該是表示的四元數吧
		const TransformVectorRegister BlendedRotation = VectorMultiply(DeltaAtom.Rotation, BlendWeight);

		Rotation = VectorAccumulateQuaternionShortestPath(Rotation, BlendedRotation);

		Translation = VectorMultiplyAdd(DeltaAtom.Translation, BlendWeight, Translation);
		Scale3D = VectorMultiplyAdd(DeltaAtom.Scale3D, BlendWeight, Scale3D);

		DiagnosticCheckNaN_All();
	}
	...
}
           

State和Bone之間的權重計算

State的權重其實就是直接代表了絕大多數沒有使用到BlendProfile的那些Bone在參與Blend時的權重,這裡稍微複雜一些的其實是BlendProfile影響的Bone,其實思路很簡單,多個Pose進行Blend時,這裡有兩種weight數組:

  • 普通的不受BlendProfile影響的Bone的weight數組,裡面每個元素對應一個SampleData的TotalWeight
  • 受BlendProfile影響的Bone的weight數組,每個Bone對應一個Weight數組,存在

    SampleDataList[PoseIndex].PerBoneBlendData

解釋到最後,無論是State還是Pose的權重,最終代表的還是Bone的權重,歸一化的核心原理就是,Blend時,所有相同Bone的權重和必須為1,這樣就好說了,它們各自計算自己的權重,然後各自在對應的weight數組裡歸一化即可,核心代碼就是

FBlendSampleData::NormalizeDataWeight(TArray<FBlendSampleData>& SampleDataList)

函數:

// NormalizeDataWeight是FBlendSampleData類提供的靜态函數, 會被以下函數調用(也是所有會使用Blend的地方):
// - UBlendSpace::UpdateBlendSamples_Internal
// - FAnimNode_BlendListBase::Update_AnyThread, FAnimNode_BlendListBase也是多個BlendNode的基類
// - FAnimationActiveTransitionEntry::Update會在動畫狀态機裡發生動畫轉态時被調用
// 核心原理是, 無論是State還是Pose的權重,最終代表的還是Bone的權重,歸一化的邏輯是,Blend時,所有相同Bone的權重和必須為1
// 這裡的Pose Weight, 即TotalWeight , 它代表的是那些沒有被BlendScale影響的Bone的weight
// 而PerBoneTotalSum對應的是那些有BlendScale影響的Bone的weight, 各個Bone在不同BlendSample上的權重值總和應該為1
// 受BlendScale影響的每個Bone各自對應一個權重數組, 數組裡的權重和應該為1
void FBlendSampleData::NormalizeDataWeight(TArray<FBlendSampleData>& SampleDataList)
{
	float TotalSum = 0.f;// TotalSum代表所有Sample對應的權重和

	// 1. 擷取skeleton的Bone的個數, 這裡的Bone是指參與了PerBoneBlend的Bone, 并不是所有Skeleton的Bone
	check(SampleDataList.Num() > 0);
	const int32 NumBones = SampleDataList[0].PerBoneBlendData.Num();

	// 2. 建立float數組, 每個Bone對應一個float, 記錄的各個Sample裡相同Bone的權重值的累加
	TArray<float> PerBoneTotalSums;
	PerBoneTotalSums.AddZeroed(NumBones);

	// 3. 周遊輸入的要進行Blend的SampleData
	for (int32 PoseIndex = 0; PoseIndex < SampleDataList.Num(); PoseIndex++)
	{
		// 3.1 确認骨骼數目相同
		checkf(SampleDataList[PoseIndex].PerBoneBlendData.Num() == NumBones, TEXT("Attempted to normalise a blend sample list, but the samples have differing numbers of bones."));

		// 3.2 TotalSum代表所有Sample對應的權重和
		TotalSum += SampleDataList[PoseIndex].TotalWeight;

		// 3.3 累計每個SampleData裡bone的權重
		if (SampleDataList[PoseIndex].PerBoneBlendData.Num() > 0)// 正常肯定是大于0的
		{
			// now interpolate the per bone weights
			for (int32 BoneIndex = 0; BoneIndex<NumBones; BoneIndex++)
			{
				PerBoneTotalSums[BoneIndex] += SampleDataList[PoseIndex].PerBoneBlendData[BoneIndex];
			}
		}
	}


	// 4. 根據TotalSum把各個Sample的Weight歸一化
	// Re-normalize Pose weight
	if (TotalSum > ZERO_ANIMWEIGHT_THRESH)
	{
		if (FMath::Abs<float>(TotalSum - 1.f) > ZERO_ANIMWEIGHT_THRESH)
		{
			for (int32 PoseIndex = 0; PoseIndex < SampleDataList.Num(); PoseIndex++)
			{
				SampleDataList[PoseIndex].TotalWeight /= TotalSum;
			}
		}
	}
	else
	{
		for (int32 PoseIndex = 0; PoseIndex < SampleDataList.Num(); PoseIndex++)
		{
			SampleDataList[PoseIndex].TotalWeight = 1.0f / SampleDataList.Num();
		}
	}

	// 5. 根據各個PerBoneBlendData數組把各個Sample裡的Bone的Weight歸一化
	// Re-normalize per bone weights.
	for (int32 BoneIndex = 0; BoneIndex < NumBones; BoneIndex++)
	{
		if (PerBoneTotalSums[BoneIndex] > ZERO_ANIMWEIGHT_THRESH)
		{
			if (FMath::Abs<float>(PerBoneTotalSums[BoneIndex] - 1.f) > ZERO_ANIMWEIGHT_THRESH)
			{
				for (int32 PoseIndex = 0; PoseIndex < SampleDataList.Num(); PoseIndex++)
				{
					SampleDataList[PoseIndex].PerBoneBlendData[BoneIndex] /= PerBoneTotalSums[BoneIndex];
				}
			}
		}
		else
		{
			for (int32 PoseIndex = 0; PoseIndex < SampleDataList.Num(); PoseIndex++)
			{
				SampleDataList[PoseIndex].PerBoneBlendData[BoneIndex] = 1.0f / SampleDataList.Num();
			}
		}
	}
}
           

關于Conduit

參考:https://forums.unrealengine.com/t/animbp-what-is-a-conduit-and-why-do-you-need-it/363568

參考:https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/StateMachines/Overview/

動畫狀态機節點裡可以右鍵添加Conduit State,如下圖所示,前面有個拉分支線的圖示:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit
Conduits serve as a more advanced and sharable transition resource. Where a Transition Rule is going to be a simple 1-to-1 relationship form one state to another, a Conduit can have 1-to-many, many-to-1, or many-to-many.
They’re basically similar to a transition rule only they can have multiple inputs and / or outputs so you can branch into different animation states depending on various checks.

Conduit翻譯過來是管道的意思,動畫狀态機裡的Conduit其實就是可以共享的Transition Rule而已,這個節點點進去的界面和點選Transition的圖示進去的界面是一樣的:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

用的時候,隻需要把原本State A到State B的連線,改成State A到Conduit State,再到State B的連線即可,如下圖所示,左邊是原本的,右邊是使用Conduit之後的連線:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

注意這裡的Transition可以點選的圖示從一個變成了兩個,出Conduit的State的Transition圖示和左邊的PositionZero指向PositionOne的Transition的圖示效果是相同的,都可以用來設定Position Zero轉到Position One的轉态時間和BlendOption選項,但是另外一個新增的Icon可以設定的内容就少了,如下圖所示:

UE裡動畫狀态機節點權重研究多個Transition同時觸發關于FAnimationActiveTransitionEntry.Alpha動畫狀态機裡State權重的計算方法StateMachineNode裡State、Pose和Bone之間的權重計算關于Conduit

這裡的Conduit對應在代碼裡還有一個特性,正常的TransitionRule會根據條件判斷傳回True或者False,但是任何連向Conduit State的Transition Rule都永遠傳回True,因為任何State都可以轉态到Conduit State