MultiAnimation Sample
这个例子展示了使用高级shader语言、蒙皮技术和D3DX动画控制器的多动画集的Mesh动画。动画控制器用来混合动画集,以确保从一个动画到另一个动画的平滑转换。
Path
源代码: (SDK root)/Samples/C++/Direct3D/MultiAnimation
可执行文件: (SDK root)/Samples/C++/Direct3D/Bin/x86 or x64/MultiAnimation.exe
Overview
这个例子展示如何利用D3DX动画支持,在应用程序中渲染3D动画。D3DX的API函数处理动画Mesh的装载,混合多个动画。动画控制器的功能是支持动画轨,允许从一个动画到另一个动画的平滑转换。这个例子分为2部分:动画类库和应用程序。
动画类库是一个多用途库,逻辑上它在应用程序与D3DX之间。为后面的渲染作准备,它压缩从.x文件中装载的Mesh,然后操作它的框架层次。像渲染动画Mesh一样,使用顶点着色器和矩阵调色板索引蒙皮,。它的设计支持复用和定制。
这个应用程序的部分代码仅仅适用于本例。它使用动画类库创建Tiny人物的实例,它的动画依赖于它们执行的动作。人物实例受控于用户或程序。这部分处理Tiny间的碰撞检测,检查是否超出范围,人物实例的行为管理由程序控制。
The Application
应用程序部分由2部分组成。第一部分是CTiny类。在本例中,它通过tiny_4anim.x文件处理动画Mesh的行为。第二部分包含CMyD3DApplication类和创建常规DirectX应用程序的全部代码。
在本例中,Tiny受控于用户或应用程序(默认)。当Tiny被应用程序控制时,将发生下列事件:首先,在地面上选择一个随机位置,确定移动速度。接下来,Tiny在当前位置转向一个新目标位置的方向,并移动。然后,Tiny到达新位置。当Tiny移动时,在另一个位置被选择之前会有一个短暂的停顿时间,然后重复全部的过程。可以有多个Tiny的实例存在。所有实例都执行碰撞检测,这样它们能够阻止彼此间的移动。
CTiny类有一个CAnimInstance成员,它在animation类库中定义。CTiny使用这个成员和它的动画控制器去执行必需的动画。在本例中,Tiny有3个已知的动画设定:Loiter(闲走),Walk(行走),Run(慢跑)。Tiny的动画控制器支持2个动画轨。大部分时间,在一个动画集中它们中的1个是被激活的。当Tiny的动作导致动画转换时(例如,由闲走到行走,或由慢跑到停止),在新的动画集中使用第2个轨播放,设置一个过渡周期来完成从第1轨到第2轨的转换。因此,一旦到了指定的时间(时间前进了),动画控制器将生成正确的框架矩阵,使用2轨之间的插值来表现平滑过渡。在过渡之后,第1个轨被取消,第2个轨播放新的动画。
同样,CTiny利用回调系统,在动画中给适当的人物实例播放脚步声。初始化时,CTiny类设置一个CallbackDataTiny结构,包含传递给回调处理程序的数据。该结构有3个成员:m_dwFoot表示哪只脚触发了声音,m_pvCameraPos表示当回调发生时摄像机的位置,m_pvTinyPos表示Tiny在世界空间中的位置。这个数据结构用来确定声音在哪里播放,和播放多大音量。本例定义了一个叫CBHandlerTiny的类,它继承自ID3DXAnimationCallbackHandler接口,包含被动画控制器调用的回调处理函数。回调处理函数HandleCallback,使用传递过来的CallbackDataTiny结构的数据,根据这个结构的值,用适当大小的声音播放一个DirectSound缓冲区。
在FrameMove方法中,遍例CTiny数组中的每个实例,并分别调用它们的动画。以更新全部实例的行为,如果需要可以替换它们。
应用程序渲染代码相对简单。首先设置基于摄像机位置和方向的视图矩阵和投影矩阵,然后渲染代表地面的Mesh对象。接下来,遍例Tiny数组的每个实例,调用它们的AdvanceTime()和Draw()方法。AdvanceTime()方法获得动画控制器以更新框架层次的矩阵,然后用Draw()使用更新的矩阵来渲染实例,最后的代码渲染必需的文本信息。
The Animation Class Library
库包含下列结构和类:
CMultiAnim
CAnimInstance
CMultiAnimAllocateHierarchy
MultiAnimFrame Structure
MultiAnimMC Structure
CMultiAnim
这个类是库的核心,它的功能是压缩装载自一个.x文件的Mesh层次。它也可以创建类型为CAnimInstance的动画Mesh实例,它共享.x文件中的Mesh层次。这些实例与创建它们的CMultiAnim相关联。
在它的初始化方法CMultiAnim::Setup中,CMultiAnim确定矩阵数组的最大值,这依赖于顶点着色器的版本。然后它从一个给定的.x文件装载Mesh层次,并根据一个给定的.fx文件创建一个effect(效果)对象,它包含渲染Mesh的顶点着色器。Mesh层次的框架和动画控制器在这个过程中创建。
框架由3个结构组成,用来表示骨骼的层次,就象Mesh对象,这个结构被所有关联的实例共享。
动画控制器被Mesh的层次框架关联。CMultiAnim的动画控制器保存没有使用的动画。当新动画实例创建时,动画控制器会被复制一份,新的实例所拥有新的动画控制器。应用程序使用实例自己的动画控制器去控制它的动画。因为每个实例有自己的动画控制器的一份拷贝,它的动画可以不依赖于其他实例。
当动画控制器的AdvanceTime()方法在一个动画实例中被调用时,动画控制器将为每个在Mesh层次中的框架更新变换矩阵。框架在稍后的实例渲染时被用到。
CAnimInstance
这个类描述一个动画实体,或动画实例。每个动画实例有它自己的动画控制器,它允许这个实例的动画不依赖于任何其他实例。每个实例总是关联一个CMultiAnim对象,它保存Mesh层次,并被所有关联的实例共享。下表列出控制动画的大部分有趣的方法,当动画和渲染时,它们在应用程序中被广泛应用。
方法名 描述
GetAnimController 返回这个实例的动画控制器,类型为ID3DXAnimationController。使用动画控制器,应用程序可以设置它需要播放的动画。
SetWorldTransform 为这个实例设置顶级的世界变换矩阵。当动画的时间前进时,在Mesh层次中的每个框架递归这个世界变换,来产生正确的世界空间和方向。
AdvanceTime 使用可选的回调处理程序为这个实例前进本地动画时间,这个前进的时间导致动画控制器去更新这个实例的骨骼位置。如果事件被触发,动画控制器也将调用回调处理程序。
ResetTime 重置这个实例的本地时间。
Draw 用HLSL顶点着色器渲染这个实例。通常在调用AdvanceTime()方法之后被调用,在框架被正确的设置后,这便于渲染
当一个动画实例被渲染时,它使用被它关联的CMultiAnim的效果对象去设置渲染参数,包括顶点着色器。顶点着色器函数负责给Mesh蒙皮,变换屏幕空间中的位置,并计算颜色。蒙皮部分使用VS_Skin函数。这个函数可以在顶点着色器文件Skin.vsh中找到。这个文件包含一个名字为amPalette的float4x3类型数组(描述蒙皮Mesh的矩阵板),和VS_Skin蒙皮函数。VS_Skin的设计可以被另外的顶点着色器调用,因此应用程序可以用自己的顶点着色器调用它去处理蒙皮过程。VS_Skin函数用来取得对象空间位置和法线参数,由3个混合权重决定,和4个矩阵板的索引决定。然后函数为了取得位置和法线,与矩阵数组中对应的矩阵和混合权重进行四次变换,将结果相加形成世界空间中的位置和法线。
CMultiAnimAllocateHierarchy
这个类继承自ID3DXAllocateHierarchy接口。它的用途是:在装载和释放Mesh层次期间,处理和分配存储资源。在本例中,CMultiAnimAllocateHierarchy在CreateFrame()中初始化所有成员和Mesh的容器,在CreateMeshContainer()调用期间创建Mesh层次。在DestroyFrame()和DestroyMeshContainer()中分别释放所有在CreateFrame()和CreateMeshContainer()方法中分配的资源。
CreateMeshContainer 所做的工作只比简单的分配和拷贝多一点。它总是做下列工作:
创建Mesh用到的所有纹理。
初始化Mesh容器的骨骼偏移数组,给予pSkinInfo信息。
获得Mesh工作的调色版权尺寸,调用ConvertToIndexedBlendedMesh创建与调色板尺寸兼容的Mesh。
MultiAnimFrame Structure
这个结构继承自D3DXFRAME。它描述Mesh层次中的一个简单的框架,或骨骼。一个框架可能包含兄弟框架或子框架。完整的框架层次结构就像树。应用程序可以给这个结构增加D3DXFRAME类的成员。在本例中,不需要其它成员。
MultiAnimMC Structure
这个结构继承自D3DXMESHCONTAINER。它包含一个有Mesh层次和关联数据的Mesh对象。 一个Mesh层次可以包含许多Mesh容器。在本例中,它其他成员被定义在下面表格中:
类型 名称 描述
LPDIRECT3DTEXTURE9* m_apTextures Mesh使用的Direct3D纹理数组对象。
LPD3DXMESH m_pWorkingMesh 一个与矩阵数组大小一致的Mesh的一份拷贝。
D3DXMATRIX* m_amxBoneOffsets 一个描述骨骼偏移的矩阵数组。这个信息是从pSkinInfo成员获取,为了方便拷贝到这里。
D3DXMATRIX** m_apmxBonePointers 矩阵指针的数组,它们指向这个Mesh层次中不同框架变换矩阵。这个数组提供一个从骨骼索引到骨骼矩阵的简单映射。
DWORD m_dwNumPaletteEntries 矩阵数组的尺寸,在渲染Mesh时使用。 大小不可能超过我们的顶点着色器允许的数组的最大值,因为注册器的限制, 它永远不能大于Mesh中的骨骼数量。
DWORD m_dwMaxNumFaceInfls 一个单面所影响的最大骨骼数。这个值从ConvertToIndexedBlendedMesh获得。顶点着色器需要这个值,当渲染时为了知道什么时候计算最后一个权重。
DWORD m_dwNumAttrGroups 工作的Mesh中属性组的数量。一个属性组是Mesh的一个子集,它能在简单的绘制调用中被画出来。 如果工作数组的大小不够渲染全部的Mesh,它就需要断开个别的属性组,用ConvertToIndexedBlendedMesh来做。
LPD3DXBUFFER m_pBufBoneCombos 包含骨骼组合表,以D3DXBONECOMBINATION数组的形式。每个属性组都有一个D3DXBONECOMBINATION。它标识Mesh中的子集(顶点,面,骨骼),它能够在一个简单的绘制调用中绘制。
User's Guide
在本例中被定义了下列常规控制。
键 动作
Q Move camera down.
E Move camera up.
W Move camera forward.
A Move camera left.
S Move camera backward.
D Move camera right.
N Next view
P Previous view
R Reset camera
F2 Bring up Direct3D Settings menu.
Alt+Enter Toggle full screen.
Esc Quit
当你在一个特定Tiny视角时,下列控制是有效的。
键 动作
C Take control
W Move forward
A,D Turn
W+Shift Hold for run mode
Pitfalls and Alternatives
Some of what this sample does can be done differently, with different trade-offs.
因为所有的Tiny实例共享同一个框架层次矩阵,例子必须给特定的实例增加时间值,并在移动到另外的实例前渲染它。通常,这不是问题。然而,如果应用程序希望先为所有实例更新矩阵,然后渲染它们,在克隆时它可以创建新动画控制器指向一个不同的矩阵组。这样,每个动画控制器有它自己的矩阵组,将不会发生重写,但这将消耗更多的内存。
这也是值得注意的 - 在蒙皮顶点着色器中的矩阵板(matrix palette)是一个矩阵数组,and naturally takes up a significant number of constant registers.应用程序可能会碰到常量注册器不够给其他对象使用的问题。当在CMultiAnim中创建效果对象时,应用程序能与小型的矩阵调色板工作,用MATRIX_PALETTE_SIZE_DEFAULT设定着色器,定义一个较小的数值。这个方法的缺点是:Mesh可能包含更多子集,需要分别的绘制。
注意,这个例子明确的展示了渲染动画Mesh。它不能处理静态的Mesh,尽管代码是可以扩展的。
另外的任务,也是例子的一部分:当Direct3D设备释放和重建时,保存动画控制器的全部状态。这一般在应用程序的设备从HAL到REF的转换(或做相反的转换)时发生,或从一个Direct3D设备到另外一个双监视器系统。在本例中,当Direct3D设备释放时,所有动画控制器就都被释放了。当例子初始化一个新Direct3D设备时,动画控制器也被重建。这提出一个问题,因为保存动画状态的动画控制器丢失了,程序也就丢失了释放前原设备保存的信息:每个轨播放什么动画,哪些轨可以使用,每个轨的速度和权重。解决这个问题的方案是,应用程序应该在释放Direct3D对象前在它的清除函数中(本例中的DeleteDeviceObjects()函数),重新取得动画控制器状态并保存到一个缓冲区中。然后,在重建动画控制器后(本例中的InitDeviceObjects()函数),再从缓冲区中恢复动画控制器状态。每个动画控制器将保存下列状态:
动画控制器的当前时间,用GetTime()函数获得。
每个轨正在播放的动画集的名字,调用GetTrackAnimationSet()函数获得这个信息,然后调用GetName函数.
所有轨的轨描述,使用GetTrackDesc()函数获得这个描述。
所有轨的当前事件,如果有一个事件是正在运行的。这可以用GetCurrentTrackEvent()函数和GetEventDesc()函数重新取得。
所有键入的轨事件。为了接收每个事件的键入。首先,用NULL的hEvent参数调用GetUpcomingTrackEvent()。会返回下一个要发生的keyed事件的事件句柄。然后,传递这个句柄给GetEventDesc()方法,获得这个事件的描述。然后,用这个从前面的GetUpcomingTrackEvent()函数得到的句柄,再次调用GetUpcomingTrackEvent()。这样你就取得了第二个键入事件的句柄,重复这个过程,就可以取得所有键入事件。
动画控制器当前的前一个混合,调用GetPriorityBlend.()。
动画控制器当前的前一个混合事件,如果一个正在运行,调用GetCurrentPriorityBlend()和GetEventDesc。
所有键入的前一个混合事件。调用GetUpcomingPriorityBlend()和GetEventDesc来做这个。这个方法去处理所有事件与处理轨的键入事件相似。
因为简单,本例没有保存动画控制器所有的状态,只保存了当前轨正在播放动画集。因此,如果在一个动画转换期间,Direct3D设备对象执行了释放和重建,那么转换后的动画可能和之前是不同的;如果转换全部完成,将会显示出来。 另外,在重新初始化之后所有键入事件将不能恢复。