Clayman的专栏

It's all about XNA & GPU Programming

用户操作
[即时聊天] [发私信] [加为好友]
claymanID:soilwork
192781次访问,排名382好友0人,关注者13
soilwork的文章
原创 85 篇
翻译 15 篇
转载 0 篇
评论 330 篇
clayman的公告
嘿嘿 ^o^....
最近评论
ffffk:研究到这的都是高手
jym5596337:我也不知道我怎么就走到了你的路上来了呵呵...
太晕了,我们专业就学的C#... 那我就凑合着用它学习MDX喽,但到了2008你的这个时期,感觉形式有点尴尬,以前的人说 在中国搞软件是 前有微软,后有盗版。
现在是 前有XNA后有C++ ... MDX 学习资料太太太难找了。。太太太少了。 师兄给介绍下你学历路途中的资料目录咯。。。我好找来学习咯。。 感谢哦感谢。。
jym5596337:好象没有继续哇 ...
shapin:ATI的网站有个支持HLSL语法高亮的vs插件,可以支持其他版本,只要修改相应的那个注册表就行
flip:To linxv :
貼圖座標有用投影嗎?
文章分类
收藏
    相册
    blogs
    David Weller
    nVidia Developer blog
    Rico Mariani
    Shawn Hargreaves
    XNA Team blog
    XNA资源
    XNA Creators Club
    ZBuffer
    Ziggyware XNA Resources
    中国XNA开发网
    存档
    软件项目交易
    订阅我的博客
    XML聚合  FeedSky
    订阅到鲜果
    订阅到Google
    订阅到抓虾
    订阅到BlogLines
    订阅到Yahoo
    订阅到GouGou
    订阅到飞鸽
    订阅到Rojo
    订阅到newsgator
    订阅到netvibes

    原创 Managed DirectX(第十三章)收藏

    新一篇: 持续郁闷中~~~~~~~ | 旧一篇: 【翻译】Managed DirectX(第十二章 下)

    第十三章 渲染骨骼动画(Rendering Skeletal Animation

        

     

    本文版权归原作者所有,仅供个人学习使用,请勿转载,勿用于任何商业用途。
    由于本人水平有限,难免出错,不清楚的地方请大家以原著为准。欢迎大家和我多多交流。
    翻译:clayman
    Blog:http://blog.csdn.net/soilwork

       所有的游戏都不可能只有静态物体,而是充满了动态元素,比如带动画的人物。通常这些动画都是通过动作捕捉技术记录下来,然后通过3D模型里的骨骼来实现的。当然,除了这些复杂的动画以外,还包括缩放,旋转,位移等简单操作。这一章,我们将讨论以下内容:

    l          加载帧层次(loading frame hierarchy)

    l          创建骨骼信息(generation any skeletal information)

    l          渲染每一帧(rendering each frame)

    l          控制动画时间(advancing the animation time)

    l          使用索引动画提高性能(using indexed animation for better performance)

    (注:最好先对骨骼动画和计算机动画有所了解,不然这章看起来会比较难懂)

     

    创建帧层次

         大部分mesh(包括不含动画信息的mesh)都包含某些层次信息;比如手臂连接到身体上,而手指又连接到手臂上。通过Direct3D扩展库,我们可加载并控制这些层次。

         为了保存这些信息,需要先创建两个抽象类来控制这些层次:Frame类和MeshContainer类。每一个Frame可以保存0到多个兄弟(sibling)帧,当然,同时也包括可能存在的子帧(child frame)。每个Frame同时也能包含0到多个meshContainer。(注:帧层次是一种树状的数据结构)

         当然在开始之前,创建一个新程序,并且为DirectX程序做好准备。之后,添加如下变量:

    private AnimationRootFrame rootFrame;

    private Vector3 objectCenter;

    private float objectRedius;

    private float elapsedTime;

         AnimationRootFrame结构将保存层次树的根节点帧,同时还包括AnimationController对象。接下来的两个变量保存了所加载模型的中心点,以及围住模型的边界球体半径。设置摄像机位置时需要用到这几个参数,保证整个模型都是可见的。最后一个变量保存了程序流逝的时间,我们将依照这个时间来更新动画。

         由于各动画mesh都是不同的,Direct3D没有强制使用一种特定方法来加载层次结构。所以,需要继承AllocateHierarchy来创建一个新类,在程序中添加代码:

    public class AllocateHierarchyDerived : AllocateHierarchy

    {

         Form1 app = null;

         public AllocateHierarchyDerived(Form1 parent)

         {

              app = parent;

         }

    }

         现在看来,这段代码什么也没干。构造函数保存了一个主窗口的实例而已,这样稍后才能使用它来调用其它方法。但是现在,程序甚至不能通过编译。我们还必须实现两个抽象方法。这两个方法用来创建保存层次信息的framemesh container对象。由于这两个类也是抽象的,因此同样需要先创建派生类。我们还需要在派生类中添加一些额外信息来帮助实现渲染。

    public class FrameDrived  : Frame

    {

         private Matrix combined = Matrix.Identity;

         public Matrix CominedTramsformationMatrix

         {

             get

             {

                  return conbined;

             }

             set

             {

                  combined = value;

             }

         }

    }

    除了保存普通的帧信息以外,每一帧还必须保存它自己以及所有父节点帧的混合变换矩阵。这有利于我们设置世界矩阵。接下来,创建MeshContainer的派生类。

    public class MeshContainerDerived : MeshContainer

    {

         private Texture[] meshTexture = null;

         private int numAttr = 0;

         private int numInfl = 0;

         private BoneCombination[] bones;

         private FrameDerived[] frameMarices;

         private Matirx[] offsetMatrices;

         //public prperties

         public Texture[] GetTexture() { reture meshTexture;}

         public void SetTextures(Texture[] textures) { meshTextures = textures; }

         public BoneCombination[] GetBones() { return bones; }

         public void SetBones(BoneCombination[] b) { bones = b; }      

         public FrameDerived[] GetFrames() { return frameMatrices; }

         public void SetFrames(FrameDerived[] frames) { frameMatrices = frames; } 

         public Matrix[] GetOffsetMatrices() { return offsetMatrices; }

         public void SetOffsetMatrices(Matrix[] matrices) { offsetMatrices = matrices; }      

         public int NumberAttributes { get { return numAttr; } set { numAttr = value; } }

         public int NumberInfluences { get { return numInfl; } set { numInfl = value; } }

    }

         每个mesh容器都包含了一些额外信息,包括骨骼连接表,属性的数量,influence的数量,还有编译矩阵。在渲染和动画mesh时,每一个变量都会用到。

         实现了连个派生类,可以来添加刚才没有实现的两个抽象方法了,在AllocateHierarchyDerived中添加如下代码:

    public override Frame CreateFrame(string name)

    {

         FrameDerived frame = new FrameDerived();

         frame.Name = name;

         frame.TransformationMatrix = Matrix.Identity;

         frame.CominedTramsformationMatrix = Matrix.Identity;            

         return frame;

    }

         对要创建的每帧来说,这段代码会保存这一帧的名字,同时把变换和混合矩阵设置为单位矩阵。在后台,所有同层帧,子帧,以及mesh容器都在运行时填充,所以不必为他担心。对于创建mesh容器来说,还有很多需要完成的工作,添加如下代码:

    public override MeshContainer CreateMeshContainer(string name, MeshData meshData, ExtendedMaterial[] materials,

                  EffectInstance[] effectInstances, GraphicsStream adjacency, SkinInformation skinInfo)

    {

         if(meshData.Mesh == null)

             throw new ArgumentException();

         if(meshData.Mesh.VertexFormat == VertexFormats.None)

                  throw new ArgumentException();

         MeshContainerDerived mesh = new MeshContainerDerived();

         mesh.Name = name;

         int numFaces = meshData.Mesh.NumberFaces;

         Device dev = meshData.Mesh.Device;

         if((meshData.Mesh.VertexFormat & VertexFormats.Normal) == 0)

         {

             Mesh tempMesh = meshData.Mesh.Clone(meshData.Mesh.Options.Value,meshData.Mesh.VertexFormat | VertexFormats.Normal,dev);

             meshData.Mesh = tempMesh;

             meshData.Mesh.ComputeNormals();

         }

         mesh.SetMaterials(materials);

         mesh.SetAdjacency(adjacency);

         Texture[] meshTextures = new Texture[materials.Length];

         for(int i=0;i<materials.Length;i++)

         {

             if(materials[i].TextureFilename != null)

             {

                  meshTextures[i] = TextureLoader.FromFile(dev,@"..\..\" + materials[i].TextureFilename);

             }

         }

         mesh.SetTextures(meshTextures);

         mesh.MeshData = meshData;

         if(skinInfo != null)

         {

             mesh.SkinInformation = skinInfo;

             int numBones = skinInfo.NumberBones;

             Matrix[] offsetMatrices = new Matrix[numBones];

             for(int i = 0;i<numBones;i++)

                  offsetMatrices[i] = skinInfo.GetBoneOffsetMatrix(i);

             mesh.SetOffsetMatrices(offsetMatrices);

             app.GenerateSkinnedMesh(mesh);

         }

         return mesh;

    }

         代码看起来有一点点多。首先要做的就是验证mesh是否为程序所支持的类型。显然,如果mesh数据结构中没有包含mesh,那么会出现一些问题,所以抛出一个异常。另外,如果mesh没有包含正确的顶点格式,则抛出另外一个异常。

         假设mesh通过验证,创建了新的mesh container。就设置mesh contain的名字、所包含的面数以及使用的device。这里虽然app成员包含device对象,但由于它是私有的,所以不能直接使用。

         下面几行代码应该很熟悉了,处理关于法线的信息。

         接下来,保存材质和邻接信息,之后会用到他们。因为材质可能包含了纹理,所以还需要创建一个纹理数组。之后,把正确的纹理放到纹理数组的成员中。

         最后,检查mesh container中是否包含任何骨骼信息。如果有,就保存骨骼,同时创建偏移矩阵数组(每条骨骼对应一个矩阵),同样,把相应的偏移矩阵保存到整个数组中。最后调用现在还没有编写的GenerateSkinnedMesh方法,然后返回这个mesh container

         来看看GenerateSkinnedMesh是怎样的吧:

    public void GenerateSkinnedMesh(MeshContainerDerived mesh)

    {

         if(mesh.SkinInformation == null)

             throw new ArgumentException();

         int numInfl = 0;

         BoneCombination[] bones;

         MeshData m = mesh.MeshData;

         m.Mesh = mesh.SkinInformation.ConvertToBlendedMesh(m.Mesh,MeshFlags.Managed | MeshFlags.OptimizeVertexCache,

                       mesh.GetAdjacencyStream(),out numInfl,out bones);

    mesh.NumberInfluences = numInfl;

         mesh.SetBones(bones);

         mesh.NumberAttributes = bones.Length;

         mesh.MeshData = m;

    }

         如果没有骨骼信息就绝对不需要调用这个方法,所以万一那么做了,我们就抛出一个异常。把mesh数据保存到一个临时变量中,之后,使用ConvertToBlendedMesh方法创建一个包含了每个顶点混合权重以及骨骼连接表(bone combination table)的新meshConvertToBlendedMesh是最根本也是最重要的实现mesh动画的方法。最后,保存meshinfluence的数量,骨骼连接表,以及属性的数量。

     

    加载动画Mesh

         我们已经建立了基本的mesh层次,现在要做的就是编写代码来使用他们。在这之前,还需要对显卡作一些检查:

         这些代码大家应该很熟悉。需要注意,对这个例子来说,至少需要4个顶点混合矩阵(vertex blend Matrices)才能完全实现硬件渲染,所以我们在这里做了检查。创建了设备之后,就可以加载动画了。这里使用了一个还没有编写的方法,稍后我们就来实现它,不过在这之前,先来看看所订阅的OnDeviceReset方法:

    private void OnDeviceReset(object sender, EventArgs e)

    {

         Device dev = (Device)sender;

         Vector3 vEye = new Vector3(0,0,-1.8f*objectRadius);

         Vector3 vUp = new Vector3(0,1,0);

         dev.Transform.View = Matrix.LookAtLH(vEye,objectCenter,vUp);

         float aspectRatio = (float)dev.PresentationParameters.BackBufferWidth / (float)dev.PresentationParameters.BackBufferHeight;

         dev.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,aspectRatio,objectRadius/64.0f,objectRadius * 200.0f);

         dev.Lights[0].Type = LightType.Directional;

         dev.Lights[0].Direction = new Vector3(0.0f,0.0f,1.0f);

         dev.Lights[0].Diffuse = Color.White;

         dev.Lights[0].Update();

         dev.Lights[0].Enabled = true;

    }

         首先,设置了观察矩阵,把摄像机移动到足够远的地方,保证能看到整个模型,同时,我们的观察点指向模型的中心。同样,投影矩阵也创建了一个足够远的后裁剪平面,保证可以看到整个模型。另外,我们还添加了一个方向光来照亮模型。

         接下来就是创建动画的代码:

    private void CreateAnimation(string file ,PresentParameters presentParams)

    {

         AllocateHierarchyDerived alloc = new AllocateHierarchyDerived(this);

         rootFrame = Mesh.LoadHierarchyFromFile(file,MeshFlags.Managed,device,alloc,null);

         objectRadius = Frame.CalculateBoundingSphere(rootFrame.FrameHierarchy,out objectCenter);

         SepupBoneMatrices((FrameDerived)rootFrame.FrameHierarchy);

         DXUtil.Timer(DirectXTimer.Start);

    }

         这里的代码很简单,基本上就是调用其它方法而已。

    首先,实例化AllocateHierarchyDerived对象。接下来,使用这个对象以及其它几个参数调用LoadHierarchyFromFile方法。注意,当调用这个方法时,根据mesh中帧和mesh container的数量,将会多次调用重载的CreateFrameCreateMeshContainer方法。之后,它将返回AnimationRootFrame对象作为帧层次树的根节点,当然,还包括animation controller,用来控制mesh的动画。

    之后,使用CalculateBoundingSphere根据根节点帧计算整个帧的边界球体,同时,返回这个球体的半径和mesh的中心。

    最后,在完成动画前,设置骨骼矩阵。这个方法将遍历(walk)整个帧层次树,同时也是其它操作的基础。具体代码如下:

    private void SetupBoneMatrices(FrameDerived frame)

    {

         if(frame.MeshContainer != null)

             SetupBoneMatrices((MeshContainerDerived)frame.MeshContainer);

         if(frame.FrameSibling != null)

             SetupBoneMatrices((FrameDerived)frame.FrameSibling);

         if(frame.FrameFirstChild != null)

             SetupBoneMatrices((FrameDerived)frame.FrameFirstChild);

    }

    private void SetupBoneMatrices(MeshContainerDerived mesh)

    {

         if(mesh.SkinInformation != null)

         {

             int numBones = mesh.SkinInformation.NumberBones;

             FrameDerived[] frameMatrices = new FrameDerived[numBones];

             for(int i=0;i<numBones;i++)

             {

                  FrameDerived frame = (FrameDerived)Frame.Find(rootFrame.FrameHierarchy,mesh.SkinInformation.GetBoneName(i));

                  if(frame == null)

                       throw new ArgumentException();

                  frameMatrices[i] = frame;

             }

             mesh.SetFrames(frameMatrices);

         }

    }

    你看,遍历整个帧层次并不是很复杂。如果有兄弟帧或者子帧,重复调用当前的方法就可以了。我们只需要为第一个子帧调用方法就可以了,因为第一个子帧会把它之后的子帧作为兄弟帧来处理(the first child will have each subsequent child listed as its sibling)。这里,根据骨骼的名字来保存每帧。

    实际上接受FrameDerived参数的重载并不是很有趣,它只是遍历整个层次树,并使用MeshContainerDerived作为参数,再调用下一个重载而已。这里,创建了一个FrameDerived数组,它的每个成员对应一个骨骼。每帧都通过遍历骨骼列表来寻找名字相符的骨骼。找到之后,就保存它。

    接下来更新main()方法:

    static void Main()

    {

         using (Form1 frm = new Form1())

         {

             frm.Show();

             if (!frm.InitializeGraphics())

             {

                  MessageBox.Show("Your card can not perform skeletal animation on " +"this file in hardware. This application will run in " +

                                "ref mode instead.");

    }

             Application.Run(frm);

    }

     

    渲染Mesh动画

         现在所要做的就是渲染动画角色了。渲染代码实际上是很简单的:

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)

    {

         ProcessNextFrame();

         device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, 1.0f, 0);

         device.BeginScene();

         DrawFrame((FrameDerived)rootFrame.FrameHierarchy);

         device.EndScene();

         device.Present();

         this.Invalidate();

    }

         处理下一帧,clear设备,绘制根帧。看起来很简单,但实际处理了很多步骤。来看看如何处理下一帧:

    private void ProcessNextFrame()

    {

         elapsedTime = DXUtil.Timer(DirectXTimer.GetElapsedTime);

         Matrix worldMatrix = Matrix.Translation(objectCenter);

         device.Transform.World = worldMatrix;

         if(rootFrame.AnimationController != null)

             rootFrame.AnimationController.AdvanceTimed(elapsedTime,null);

         UpdateFrameMatrices((FrameDerived)rootFrame.FrameHierarchy,worldMatrix);

    }

         首先,保存逝去时间,接下来,创建根帧的世界矩阵。变换物体的位置,更新设备。假设这个mesh包含动画,使用保存的时间来改变(advance)动画时间。最后,更新混合的变换矩阵:

    private void UpdateFrameMatrices(FrameDerived frame,Matrix parentMatrix)

    {

         frame.CominedTramsformationMatrix = frame.TransformationMatrix * parentMatrix;

         if(frame.FrameSibling != null)

             UpdateFrameMatrices((FrameDerived)frame.FrameSibling,parentMatrix);

         if(frame.FrameFirstChild != null)

              UpdateFrameMatrices((FrameDerived)frame.FrameFirstChild,frame.CominedTramsformationMatrix);

    }

         这里,当前帧的混合变换矩阵通过帧的变换矩阵与他的父变换矩阵相乘来计算。所有兄弟帧都使用当前帧的父矩阵,而子帧则使用当前帧的混合变换矩阵作为他自己的变换矩阵。这里实际上形成了一个矩阵链,子帧需要把他的变换矩阵和所有父节点帧的矩阵相乘。

         处理完下一帧之后,就可以对它进行渲染了:

    private void DrawFrame(FrameDerived frame)

    {

         MeshContainerDerived mesh = (MeshContainerDerived)frame.MeshContainer;

         while(mesh != null)

         {