Animation Rendering

Animation Rendering

在上一節完成了所有數據的導入,並建立了完整的動畫模型支持系統,現在我們就可以開始渲染一個animation。渲染一個動畫模型的主要基於以下4個步驟:
1、推進當前時間。
2、更新skeleton中每一個bone的tranformations。
3、把更新後的bone transformations發送到skinned model shader中。
4、執行用於繪製skinned model的函數調用。
這幾個步驟中的大部分操作都封裝在一個AnimationPlayer類型的組件中(該類的聲明代碼如列表20.13所示)。

列表20.13 Declaration of the AnimationPlayer Class

#pragma once

#include "GameComponent.h"

namespace Library
{
	class GameTime;
	class Model;
	class SceneNode;
	class AnimationClip;

	class AnimationPlayer : GameComponent
	{
		RTTI_DECLARATIONS(AnimationPlayer, GameComponent)

	public:        
		AnimationPlayer(Game& game, Model& model, bool interpolationEnabled = true);
		
		const Model& GetModel() const;
		const AnimationClip* CurrentClip() const;
		float CurrentTime() const;
		UINT CurrentKeyframe() const;
		const std::vector<XMFLOAT4X4>& BoneTransforms() const;
		
		bool InterpolationEnabled() const;
		bool IsPlayingClip() const;
		bool IsClipLooped() const;

		void SetInterpolationEnabled(bool interpolationEnabled);

		void StartClip(AnimationClip& clip);
		void PauseClip();
		void ResumeClip();
		virtual void Update(const GameTime& gameTime) override;
		void SetCurrentKeyFrame(UINT keyframe);

	private:
		AnimationPlayer();
		AnimationPlayer(const AnimationPlayer& rhs);
		AnimationPlayer& operator=(const AnimationPlayer& rhs);

		void GetBindPoseTopDown(SceneNode& sceneNode);
		void GetBindPoseBottomUp(SceneNode& sceneNode);
		void GetPose(float time, SceneNode& sceneNode);
		void GetPoseAtKeyframe(UINT keyframe, SceneNode& sceneNode);
		void GetInterpolatedPose(float time, SceneNode& sceneNode);		

		Model* mModel;
		AnimationClip* mCurrentClip;
		float mCurrentTime;
		UINT mCurrentKeyframe;
		std::map<SceneNode*, XMFLOAT4X4> mToRootTransforms;
		std::vector<XMFLOAT4X4> mFinalTransforms;
		XMFLOAT4X4 mInverseRootTransform;
		bool mInterpolationEnabled;
		bool mIsPlayingClip;
		bool mIsClipLooped;
	};
}


AnimationPlayer類通過構造函數與一個model對象關聯,並調用StartClip()函數指定一個AnimationClip對象。在StartClip函數中還會重置成員變量mCurrentTime和mCurrentKeyFrame爲0,並設置mIsPlayingClip值爲true。該函數的完成實現代碼如下:

void AnimationPlayer::StartClip(AnimationClip& clip)
{
	mCurrentClip = &clip;
	mCurrentTime = 0.0f;
	mCurrentKeyframe = 0;
	mIsPlayingClip = true;

	XMMATRIX inverseRootTransform = XMMatrixInverse(&XMMatrixDeterminant(mModel->RootNode()->TransformMatrix()), mModel->RootNode()->TransformMatrix());
	XMStoreFloat4x4(&mInverseRootTransform, inverseRootTransform);
	GetBindPoseTopDown(*(mModel->RootNode()));
}


StartClip()函數中的最後幾行代碼用於存儲root transformation矩陣的逆矩陣,並執行GetBindPose()函數。模型根結點的tranformation矩陣的逆矩陣是對每一個bone對象實施的最後一種變換矩陣,因爲只需要計算一次並保存到變量中用於以後使用。GetBindPos()函數是一個遞歸函數,從模型的根結點開始遞歸的把每一個結點初始化爲對應的bind pose(模型骨骼結點的原始座標位置)。bind pose表示模型在執行任何動畫變換之前的原始座標位置。更具體地說,bind pose是一個skinned模型中每一個bone的原始transformations變換結果。列表20.14列出了GetBindPos()函數的一個bottom-up(自底向上遞歸)的實現代碼。

列表20.14 Retrieving a Skinned Model’s Bind Pose (Bottom-up)

void AnimationPlayer::GetBindPoseBottomUp(SceneNode& sceneNode)
{
	XMMATRIX toRootTransform = sceneNode.TransformMatrix();

	SceneNode* parentNode = sceneNode.Parent();
	while (parentNode != nullptr)
	{
		toRootTransform = toRootTransform * parentNode->TransformMatrix();
		parentNode = parentNode->Parent();
	}

	Bone* bone = sceneNode.As<Bone>();
	if (bone != nullptr)
	{
		XMStoreFloat4x4(&(mFinalTransforms[bone->Index()]), bone->OffsetTransformMatrix() *  toRootTransform * XMLoadFloat4x4(&mInverseRootTransform));
	}

	for (SceneNode* childNode : sceneNode.Children())
	{
		GetBindPoseBottomUp(*childNode);
	}
}


在該函數中,首先獲取指定結點的transformation矩陣,該矩陣是一個相對的變換矩陣(相對於父結點),用於初始化toRootTransform變量值。其中toRootTransform變量用於改變結點的座標空間,把該結點從bone space變換到根結點的model space。如果GetBindPoseBottomUp()函數參數指定的scene結點是根結點(即沒有父結點),那麼toRootTransform就已經是一個相對於父結點的變換矩陣了。否則,就要把該結點所在的hierarchy(結點層次)中每一個祖先結點的transform矩陣相乘,以及得到相對的toRootTransform矩陣。最終的transform矩陣(也就是要發送到skinned model shader的矩陣)由offset transform矩陣,toRootTransform以及root transform的逆矩陣進行矩陣乘法計算得到。回顧一下前面所講的,與bone關聯的vertices最開始都位於model space中,offset transform矩陣是用於把這些vertices變換到bone space中,以使得skeleton可以對這些vertices執行變換操作。

GetBindPoseBottomUp()函數使用了一種自底向上遞歸的方法,因此可以在結點層次中向上遞歸遍歷當前結點的所有祖先結點。列表20.15中列出了使用一種top-down(自頂向下遞歸)方法的GetBindPosTopDown()函數,這種方法通過增加內存的消耗來減少重複的矩陣乘法運算。

列表20.15 Retrieving a Skinned Model’s Bind Pose (Top-down)

void AnimationPlayer::GetBindPoseTopDown(SceneNode& sceneNode)
{
	XMMATRIX toParentTransform = sceneNode.TransformMatrix();
	XMMATRIX toRootTransform = (sceneNode.Parent() != nullptr ? toParentTransform * XMLoadFloat4x4(&(mToRootTransforms.at(sceneNode.Parent()))) : toParentTransform);
	XMStoreFloat4x4(&(mToRootTransforms[&sceneNode]), toRootTransform);

	Bone* bone = sceneNode.As<Bone>();
	if (bone != nullptr)
	{
		XMStoreFloat4x4(&(mFinalTransforms[bone->Index()]), bone->OffsetTransformMatrix() * toRootTransform * XMLoadFloat4x4(&mInverseRootTransform));
	}

	for (SceneNode* childNode : sceneNode.Children())
	{
		GetBindPoseTopDown(*childNode);
	}
}


完成了animation clip對象的初始化之後,animation就會隨着時間的推移自動向前推進。該操作過程由AnimationPlayer::Update()函數完成,該函數是對GameComponent類派生的Update函數的重寫。列表20.16中列出該函數的實現代碼。

列表20.16 Advancing an Animation Automatically

void AnimationPlayer::Update(const GameTime& gameTime)
{
	if (mIsPlayingClip)
	{
		assert(mCurrentClip != nullptr);

		mCurrentTime += static_cast<float>(gameTime.ElapsedGameTime()) * mCurrentClip->TicksPerSecond();
		if (mCurrentTime >= mCurrentClip->Duration())
		{
			if (mIsClipLooped)
			{
				mCurrentTime = 0.0f;
			}
			else
			{
				mIsPlayingClip = false;
				return;
			}
		}

		if (mInterpolationEnabled)
		{
			GetInterpolatedPose(mCurrentTime, *(mModel->RootNode()));
		}
		else
		{
			GetPose(mCurrentTime, *(mModel->RootNode()));
		}
	}
}


在Update()函數中,首先根據elapsed frame time和animation clip對象的ticks-per-second值(回顧一下,animation clip對象的duration值以ticks爲單位,而不是秒)推進當前時間mCurrentTime。如果允許animation clip對象執行循環,就會在animation完成後重置當前時間變量;否則就會停止animation clip對象的推進。如果當前時間處於animation clip對象的循環播放時間之間,就調用GetPose()或GetInterpolatedPose()函數更新最終的bone transformations。列表20.17中列出了GetInterpolatedPose()函數的代碼。GetPose()函數與些類似,只不過在GetPose()函數中是調用Animation::GetTransform()函數而不是AnimationClip::GetInterpolatedTransform()更新toParentTransform矩陣變量。

列表20.17 Retrieving the Current Pose

void AnimationPlayer::GetInterpolatedPose(float time, SceneNode& sceneNode)
{
	XMFLOAT4X4 toParentTransform;
	Bone* bone = sceneNode.As<Bone>();
	if (bone != nullptr)
	{
		mCurrentClip->GetInteropolatedTransform(time, *bone, toParentTransform);
	}
	else
	{
		toParentTransform = sceneNode.Transform();
	}

	XMMATRIX toRootTransform = (sceneNode.Parent() != nullptr ? XMLoadFloat4x4(&toParentTransform) * XMLoadFloat4x4(&(mToRootTransforms.at(sceneNode.Parent()))) : XMLoadFloat4x4(&toParentTransform));
	XMStoreFloat4x4(&(mToRootTransforms[&sceneNode]), toRootTransform);

	if (bone != nullptr)
	{
		XMStoreFloat4x4(&(mFinalTransforms[bone->Index()]), bone->OffsetTransformMatrix() * toRootTransform * XMLoadFloat4x4(&mInverseRootTransform));
	}

	for (SceneNode* childNode : sceneNode.Children())
	{
		GetInterpolatedPose(time, *childNode);
	}
}


另外,還可以調用GetPoseAtKeyframe()函數進行手動推進每一幀的變化,通過調用SetCurrentKeyFrame()函數可以執行GetPoseAtKeyframe()函數:

void AnimationPlayer::SetCurrentKeyFrame(UINT keyframe)
{
	mCurrentKeyframe = keyframe;
	GetPoseAtKeyframe(mCurrentKeyframe, *(mModel->RootNode()));
}


AnimationPlayer中提供了公有函數AnimationPlayer::BoneTransforms(),用於訪問最終的transforms矩陣(用於發送到skinned model shader中)。另外還包括對應的公有函數接口,用於訪問model對象,current animation clip成員對象,current time和current keyframe,以及用於控制animation clip的暫停和恢複函數。本書的配套網站上提供了完成的實現代碼。

調用AnimationPlayer類,首先需要實例一個類對象,開始一個animation clip循環,並通過手動推進keyframes或通過AnimationPlayer組件的Update()函數自動更新。這意味着在此之前,需要使用含有一個skeletal hierarchy和animations數據的文件初始化一個Model對象實例。在本書的配套網站上提供了一個COLLADA(.dae)格式的示例模型文件。此外,還需要創建一個material類與skinned model shader進行交互。該material類中只有一個成員變量(BoneTransform)是用於處理animation,並創建包含有bone weights和indices的vertex buffers。該material類的input layout結構如下所示:

D3D11_INPUT_ELEMENT_DESC inputElementDescriptions[] =
{
	{ "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "BONEINDICES", 0, DXGI_FORMAT_R32G32B32A32_UINT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "WEIGHTS", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};


其中,BoneIndices和Weights元素格式分別爲,indices是4×32-bit無符號整形數,weights是4×32-bit浮點數。
繪製一個skinned model與繪製任何其他模型的方法一樣,除了一點需要把bone transforms發送到shader中。例如:
mMaterial->BoneTransforms() << mAnimationPlayer->BoneTransforms();
其中,mMaterial對象引用了一個SkinnedModelMatreial類的實例對象。在本書的配套網站上提供了一示例程序,該示例支持動態改變animation player的設置,並手動推進keyframes。圖20.3中顯示了animation示例程序的輸出,該程序中渲染了本章開始時描述的running soldier。


圖20.3 Output of the animation demo. (Animated model provided by Brian Salisbury, Florida Interactive Entertainment Academy.)


總結

本章主要講述了skeletal animation。在這個過程中,我們開發了使用Open Asset Import Library導入animation數據的系統,同時還包括用於獲取keyframes並在兩個keyframes之間進行插值的相關組件。此外,還實現了一個shader用於在GPU中執行skinned model的vertices的變換操作。


Exercises

1. From within the debugger, walk through the code used to import an animated model, to
better understand all the moving parts (pun intended). Import a variety of animated models to
compare the idiosyncrasies between file formats (which the Open Asset Import Library might
not successfully abstract).
2. Explore the animation demo found on the book’s companion website. Vary the options
provided to automatically and manually advance keyframes with and without interpolation, and
observe the results.
3. Integrate the animation shader with additional lighting models (directional lighting and
spotlighting, for example).

1、由調試方法運行示例程序,單步查看導入動畫模型的代碼,加深對所有moving parts(雙關語,表示分析動畫數據的部分)的理解。導入各種各樣的動畫格式,並比較不同文件格式的特性(Open Asset Import Library可能沒有抽象表示全部文件格式)。
2、測試本書配套網站上提供的示例程序。根據示例中提供的設置方法,測試自動和手動推進keyframes,以及使用和不使用插值的情況,並觀察結果。
3、在animation shader中集成更多的光照模型(比如,directional lighting和spotlighting)。


發佈了5 篇原創文章 · 獲贊 27 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章