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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章