第23章 动画模型
你肯定赞同动画模型是游戏中最让人激动的功能,本章会介绍一些创建并加载预定义的动画模型的方法。不幸的是,XNA现在还没有一个能自动处理动画模型的类库,所以你必须自己写一个动画模型加载器。本章我们会提供一个模型加载器可以加载并显示Quake II模型,这些模型的格式是.md2。
当然你可以使用MilkShape创建并导出为.md2格式,但是你使用的是其他格式,也可以在MilkShape中创建并导出为想要的格式。如果你使用的是其他建模工具,你可以将它们导入到MilkShape,添加动画并导出为Quake II格式或其他格式。
无论你使用何种方法,请确保在XNA代码中工作正常。
Quake II格式
本章不会完整地解释Quake II模型动画的源代码的工作原理,但是还是要简要介绍一下.md2格式,如果你想深入学习,可以看一下Quake II模型载入器的源代码。本章解释了如何添加这个MD2类播放动画,改变动画,播放动画序列或暂停/继续动画。
MD2格式是由id Software公司开发的,首先用在了Quake II游戏中。id Software后来开发了Quake II游戏引擎的源代码,自此以后,Quake II模型格式变得很流行,因为它的动画很稳定,容易实现,制作模型的工具花费也不多。
Quake II格式是通过关键帧实现动画的。模型的顶点定位在关键帧上。在动画过程中,顶点被映射到两个关键帧之间的时间线上的对应位置。
不像其他模型格式,在模型创建过程中Quake II模型不使用骨骼层次或蒙皮权重,这些信息的缺失会导致模型关节周围的蒙皮不真实的褶皱。但是你可以通过在设计模型时仔细的规划避免(或使之最小)这种情况的发生。近距离观察Quake II模型时蒙皮会有些缺陷,但在远距离时很难注意到它们。
Quake II模型使用的三角形不能超过4,096个。
深入理解.md2数据
让我们看一下.md2文件的结构和它如何实现动画。
Quake II数据以二进制方式存储,可以对顶点和索引进行压缩。为了帮你解开数据,文件开始包含了一个文件头显示文件类型,纹理属性,顶点属性,顶点数量,帧数量和二进制偏移(处理顶点和动画帧的细节)。下面是标准的.md2文件头:
struct md2 { int fileFormatVersion; // file type which must equal 844121161 int version; // file format version which must be 8 int skinWidth; // texture width int skinHeight; // texture height int frameSize; // bytes per frame int numSkins; // total skins used int numVertices; // total vertices per frame int numUVs; // total texture UV's int numTris; // number of triangle coordinates int numglCommands; // number of glCommands int numFrames; // number of keyframes int ofsSkins; // binary offset to skin data int ofsUV; // offset to texture UV data int ofsTriangle; // offset to triangle list data int ofsFrames; // offset to frame data int ofsglcmds; // offset to OpenGL command data int ofsEnd; // offset to end of file };
每帧中的每个顶点都是被索引化的,索引的顺序就是三角形序列的顺序。当文件加载时,索引用来生成顶点坐标的集合,这个坐标用来创建三角形集合。基于效率考虑,你可以使用glCommands数据重写模型加载和动画代码,以三角形带或三角扇形绘制模型。在Quake II模型中可以存储一个以上的动画,例如,你的模型可以跑、跳、挑衅、点头,蹲下或站立,你还想控制这些动作。要处理这些信息,使用.md2文件头,文件头包含了帧信息的偏移量。帧信息可以使用偏移量读取,所有帧信息从第一帧到最后一帧排列在一起,每个帧信息包含动画名称和帧数量的信息。
要确定每个动画的开始帧和结束帧,你必须解析每个帧信息匹配动画名称。一旦有了匹配动画名称的集合,你就可以将开始帧和结束帧的序号存储在这个集合中了。当你想播放想要的动画时,你可以将帧的序号设置为开始帧的序号,当到达结束帧的序号时,你可以从头播放或切换到另一个动画。
在动画播放过程中,顶点被映射到两个关键帧之间的时间线上,法线向量必须也通过这种方式进行插值。
md2格式的纹理
在Quake II游戏中,Quake II模型使用.pcx文件作为纹理。但是XNA的内容管道不支持.pcx格式。一种解决方法是使用图像编辑器将.pcx文件保存为.tga格式,你可以使用.tga文件作为纹理。一个模型文件还可以包含多张纹理,本章的Quake II模型导入器只支持一张纹理。
MilkShape中的动画模型
这部分内容主要是有关如何在MilkShape中制作动画模型并导出为md2格式,我就不翻译了。
加载Quake II模型
在属性面板中保证lamp.md2使用的Content Importer是MD2Importer,Content Processor 使用的是md2Processor。在Game1.cs中添加MD2类的引用,包含以下命名空间:
using quakeMD2;
并添加MD2类型的变量:
private MD2 md2;
pivot(绕竖直轴旋转)和bow(上下旋转)动画定义在一个枚举中:
public enum meAnim { pivot, bow }
模型设置很简单,首先初始化md2对象并加载模型。Quake II模型的纹理,lamp.bmp,可以通过ContentManager加载。模型加载后要设置动画速度,setAnimSpeed()方法的参数是 5.0f。 你也可以根据需要加快或放慢动画的速度。当游戏开始时会播放动画,setAnimSequence()方法依次播放两个动画,第一个参数设置动画只播放一次,第二个参数设置在第一个动画结束后第二个动画一直播放。将下列代码放置到Initialize()中:
md2 = new MD2(); md2.loadModel(gfx.GraphicsDevice, ".\\Models\\lamp",".\\Images\\lamp", content); md2.setAnimSpeed(5.0f); // 5.0f is the default speed md2.setAnimSequence((int)meAnim.bow, (int)meAnim.pivot);
XNA的BasicEffect用来绘制模型,所以要定义一个它的实例:
BasicEffect mBE;
要在程序运行前准备好mBE并避免初始化BasicEffect对象时的循环,这个对象应该在程序开始时只创建一次,因此应在Initialize()方法中设置mBE对象:
mBE = new BasicEffect(gfx.GraphicsDevice, null);
方向光只需设置一次-除非你的世界有一个以上的太阳。要提供一个可以被所有使用BasicEffect的灯光和纹理的对象使用的方法,可以在game类中添加setBasicEffect()方法:
public void setBasicEffect() { // set up lighting mBE.LightingEnabled = true; mBE.DirectionalLight0.Enabled= true; mBE.AmbientLightColor= new Vector3(0.8f, 0.8f, 0.8f); mBE.DirectionalLight0.DiffuseColor = new Vector3(1.0f, 1.0f, 1.0f); mBE.DirectionalLight0.Direction= Vector3.Normalize(new Vector3(0.0f, -0.3f,1.0f)); mBE.DirectionalLight0.SpecularColor= new Vector3(0.2f, 0.2f, 0.2f); mBE.SpecularPower= 0.01f; mBE.TextureEnabled= true; }
要在程序开始前初始化BasicEffect的属性,可以在Initialize()方法中调用setBasicEffect() :
setBasicEffect();
BasicEffect类中还需要一个VertexDeclaration,将这个变量声明放在代码顶部:
private VertexDeclaration mVertPosNormTex;
在Initialize()方法中设置VertexDeclaration对象:
mVertPosNormTex = new VertexDeclaration(gfx.GraphicsDevice,VertexPositionNormalTexture.VertexElements);
.md2模型的顶点在每帧中都要进行插值,在Update()方法中添加updateModel()方法让MD2类可以处理这个插值以获取平滑的动画:
md2.updateModel(gfx.GraphicsDevice, gameTime);
绘制模型的代码与绘制.x或.fbx格式模型的代码类似,不同之处是当设置数据源时必须引用模型的顶点缓冲,而且这个代码使用三角形列表绘制Quake II模型,所以你还要指定图元类型。三角形的总数可以从模型对象中获得,下面在类中添加drawMD2model()方法,并将这个方法添加到Draw()方法中:
void drawMD2model() { // 1: declare matrices Matrix matScale, matTranslation, matRotateY, matWorld; // 2: initialize matrices matScale= Matrix.CreateScale(0.2f, 0.2f, 0.2f); matTranslation = Matrix.CreateTranslation(0.0f, -0.9f, 4.0f); matRotateY= Matrix.CreateRotationY((float)Math.PI); // 3: build cumulative world matrix using I.S.R.O.T. sequence // identity, scale, rotate, orbit(translate & rotate), translate matWorld = Matrix.Identity * matScale * matRotateY * matTranslation; // 4: set shader matrices, and texture mBE.Begin(); mBE.World = matWorld; mBE.View = mMatView; mBE.Projection = mMatProj; mBE.Texture = md2.getTexture(); mBE.CommitChanges(); // 5: draw object - select vertex type, data source, # of primitives gfx.GraphicsDevice.VertexDeclaration = mVertPosNormTex; foreach (EffectPass pass in mBE.CurrentTechnique.Passes) { pass.Begin(); // get the data and draw it gfx.GraphicsDevice.Vertices[0].SetSource(md2.vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes); gfx.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, md2.getNumTriangles()); pass.End(); } mBE.End(); }
运行代码后,你就可以看到一个台灯先上下转动一次然后不停地左右旋转。
在代码中加载并控制模型
上一个演示想你展示了如何创建一个Quake II模型并用代码实现动画,这是个有用的例子,但是,Quake II模型可以实现更有趣的动画,下一个演示展示了如何使用MD2类根据命令播放动画,切换动画,暂停/继续动画。这个例子加载了一个叫做Zarlag的模型。
Zarlag模型是由Phillip T. Wheeler制作的,它有许多有趣的动画。图23-8显示了Zarlag的一些动作。要控制Zarlag的动画,首先需要定义一个枚举,包含所有动画的名称:
public enum meAnim { stand, run, attack, pain1, pain2, pain3, jump, flip, salute, taunt, wave, point, crstand, crwalk, crattack, crpain, crdeath, death1, death2, death3 }
使用下列代码加载Zarlag模型:
md2.loadModel(gfx.GraphicsDevice, ".\\Models\\tris",".\\Images\\Zarlag", content);
要从第一个站立的动画开始,应在Initialize()方法中将SetAnimSequence()如下设置:
md2.setAnim((int)meAnim.stand);
图23-8 Zarlag的几个动作
在drawMD2model()方法中设置缩放、平移和旋转:
matScale= Matrix.CreateScale(0.02f, 0.02f, 0.02f); matTranslation = Matrix.CreateTranslation(0.0f, -0.4f, 3.0f); matRotateY= Matrix.CreateRotationY((float)Math.PI / 2.0f);
MD2类有几个不同的方法让你可以用命令控制动画,播放动画序列,暂停/继续动画。这些命令都是由按键触发的。要保证在一个按键事件中触发多个动画,这要持续几个帧的时间,下列变量要添加到上一次按下事件的时间中以保证在再次触发动画时经过足够的时间:
private double mdblAnimDelay, mdblRunDelay, mdblJumpDelay = 0;
第一个动画处理器让你可以通过按下空格键或左摇杆循环播放动画。MD2类的advanceAnimation()可以遍历动画集合。将下列代码添加到Update()方法中让用户可以看到所有动画:
if (gameTime.TotalGameTime.TotalMilliseconds - mdblAnimDelay > 200) { mdblAnimDelay = gameTime.TotalGameTime.TotalMilliseconds; if (kbState.IsKeyDown(Keys.Space) ||GamePad.GetState(PlayerIndex.One).Buttons.LeftStick== ButtonState.Pressed) { md2.advanceAnimation(); } }
下一个动画处理器让你可以通过按下A键或右扳机播放奔跑动画。将这个代码添加到Update()方法中:
if (gameTime.TotalGameTime.TotalMilliseconds - mdblRunDelay > 100) { mdblRunDelay = gameTime.TotalGameTime.TotalMilliseconds; // start running animation if it isn't already playing if ((kbState.IsKeyDown(Keys.A)|| GamePad.GetState(PlayerIndex.One).Triggers.Right > 0.0f)&& !md2.isPlaying((int)meAnim.run)) { md2.setAnim((int)meAnim.run); } }
下面的代码让你可以按J键或右摇杆触发一次跳的动作,将这个代码添加到Update()方法中实现这个功能:
if (gameTime.TotalGameTime.TotalMilliseconds - mdblJumpDelay > 100) { mdblJumpDelay = gameTime.TotalGameTime.TotalMilliseconds; // start jump animation if it isn't already playing if ((kbState.IsKeyDown(Keys.J) ||GamePad.GetState(PlayerIndex.One).Buttons.RightStick== ButtonState.Pressed )&& !md2.isPlaying((int)meAnim.jump)) { md2.setAnimSequence((int)meAnim.jump, (int)meAnim.run); } }
你还可以通过按P键或手柄的B键暂停动画,按R键或手柄的A键继续动画,将下列代码添加到Update()方法中实现这个功能:
if (kbState.IsKeyDown(Keys.P) ||GamePad.GetState(PlayerIndex.One).Buttons.B ==ButtonState.Pressed ) { md2.Pause(); } else if (kbState.IsKeyDown(Keys.R) ||GamePad.GetState(PlayerIndex.One).Buttons.A == ButtonState.Pressed) { md2.Resume(); }
现在运行程序,模型应该首先处在站立姿态,你可以按下右扳机或空格键让它处于奔跑状态,继续按左摇杆或空格键会循环播放所有动画。按A键或右扳机可以开始奔跑动画,此时按下J键或右摇杆会跳起。你可以按P或R键或手柄上的B或A键暂停/继续动画。
加载武器
Quake II的武器通常是与角色分开的,分开的武器让你可以切换来复枪、等离子炮、火箭筒等。这些武器的动画要匹配角色的动画,当模型在跑动、匍匐、倒下时,武器也应跟着模型的手一起移动,当角色死亡时,武器还要从手中掉落。
要添加武器,首先要将weapon.md2文件导入到Models文件夹,weaponSkin.tga文件导入到Images文件夹。要对武器对象使用MD2类需要一些改变。定义两个常量区分不同的模型。
const int CHARACTER= 0; const int WEAPON= 1;
在game中还需要一个MD2对象用于武器:
private MD2 md2Weapon;
在Initialize()加载武器的模型和纹理:
md2Weapon = new MD2(); md2Weapon.loadModel(gfx.GraphicsDevice,".\\Models\\weapon", ".\\Images\\weaponSkin", content); md2Weapon.setAnimSpeed(5.0f); md2Weapon.setAnim((int)meAnim.stand);
注意:武器的动画速度设置为5.0f,开始动画被设置到匹配Zarlag的动画速度和开始动画,这可以让武器动画正确地匹配角色。
武器动画的每一帧都要更新,所以在Update()方法中加入以下代码:
md2Weapon.updateModel(gfx.GraphicsDevice, gameTime);
在Update()方法中,实现以上五种情况的处理:从一个动画切换到下一个动画,指定一个动画,设置一个动画序列,暂停/继续动画。要保证武器也能随着这些动画正确的移动,下面五行代码必须包含在对应的判断中:
md2Weapon.advanceAnimation(); md2Weapon.setAnim((int)meAnim.run); md2Weapon.setAnimSequence((int)meAnim.jump, (int)meAnim.run); md2Weapon.Pause(); md2Weapon.Resume();
在drawMD2model()方法中也需进行一些小变动,需要处理播放哪个模型的动画,所以给drawMD2model增加一个参数:
void drawMD2model(int iModel)
在drawMD2model()方法中需要一些额外的变化保证Zarlag模型和武器模型使用哪个纹理,在施加纹理前需要检查绘制的是哪个模型。代码变为:
if(iModel == WEAPON) mBE.Texture = md2Weapon.getTexture(); else mBE.Texture = md2.getTexture();
然后在drawMD2Model()方法中, 当设置数据源和从数据源绘制图元时,需要检查正确的顶点集和三角形数量。代码改变如下:
if (iModel == WEAPON) { gfx.GraphicsDevice.Vertices[0].SetSource(md2Weapon.vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes); gfx.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, md2Weapon.getNumTriangles()); } else { gfx.GraphicsDevice.Vertices[0].SetSource(md2.vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes); gfx.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, md2.getNumTriangles()); }
最后,根据参数绘制角色和武器:
drawMD2model(CHARACTER); drawMD2model(WEAPON);
当你运行代码并改变模型时,Zarlag和他的武器会一起运动。
发布时间:2009/6/9 下午12:22:54 阅读次数:9686