曾经看到过一个H的3d动画,就是一大堆游戏的主角在**,如果有哪位名人曾经有过厌学好心

这个时代变化的很快,以往我们看某个产品或产业的热度时,往往会综合参考线上、线下等多方面因素。但如今,我们或许只需要看看它的贴吧,就可以对某件事物略知一二了。莆田系医院如此,国产无良盗版游戏如此,如今非常火爆的VR技术也是如此。此前,有趣君就曾经写到过VR贴吧被卖一事。当时正值百度血友病吧被卖事件的舆论高峰时段,那...
这个时代变化的很快,以往我们看某个产品或产业的热度时,往往会综合参考线上、线下等多方面因素。
但如今,我们或许只需要看看它的贴吧,就可以对某件事物略知一二了。
莆田系医院如此,国产无良盗版游戏如此,如今非常火爆的VR技术也是如此。
此前,有趣君就曾经写到过VR贴吧被卖一事。
当时正值百度血友病吧被卖事件的舆论高峰时段,那篇新闻的评论区也给广大愤怒的网友们提供了充足的吐槽空间。
当VR技术一次又一次的占领科技新闻版块的头条时,我们也猛然发现,这个名叫虚拟现实的东西还真就不同寻常。
此外,国内外的资本巨头们也对其展开了一轮又一轮的资本攻势。
从福布斯的全球科技公司排行榜来看,入围该榜单的前十家科技公司中,就有八家公司以不同的形式涉足了VR技术的发展。
这其中,就包含了苹果、微软、三星、谷歌、因特尔等巨头公司。
而国内方面,以暴风墨镜为首的虚拟现实厂家当然也吸引到了非常多的投资,但产品嘛……
对于我们老百姓而言,虚拟现实这东西固然新奇;说到这东西的应用,普通人或许也只能萌生出这样的想法——
有木有羞羞的电影!有木有羞羞的游戏!
对了,还有恐怖电影!恐怖游戏!
额,不过两者相比之下,我还是更想看虚拟现实的羞羞电影和游戏!
早在Oculus还是个昂贵的开发套件时(现在也不是很便宜),不少人就想到了这点,并凭借着这项技术搞出了一些个游戏和电影。
接下来,我们就来聊聊有关VR的那些广播上不让说、电视上不让报的东西 /(/ /o/ω/o/ /)/。
毕竟大家都是成年人,话题稍微污一点也没什么关系嘛。
前阵子,由知名国人女性漫画家Sayori担任原画,日本NEKO WORKs公司制作、Sekai Project公司发行的“《巧克力与香子兰》系列”游戏搞出了一条大新闻——
我们要推出VR版的《巧克力与香子兰》啦!
随后,NEKO WORKs的官方推特账号也发了张HTC Vive的开发机照片,以及游戏VR化的相关截图。
说到《巧克力与香子兰》这款游戏,恐怕广大Steam玩家都不会觉得陌生。
这款本应在日本各大动漫展会中销售的成人级游戏到了Steam平台后摇身一变,就成了大家所能看到的样子。
原作中,主角开了一家蛋糕店,开业当日却从寄来的行李中发现了两“只”人形猫。
是的没错,就是这两个萌萌的猫耳妹子,名字叫“巧克力”和“香子兰”。
然而但凡是只猫,它就会发情……而后主角在工作和生活中没羞没臊的日子……
我们就不在这多说什么了……
值得一提的是,《巧克力与香子兰》中的所有角色都采用了Live 2D技术。
简单来说,这就是一个用3D渲染技术模仿传统漫画手绘风格的一种游戏角色制作引擎。
以往文字冒险游戏中的立绘只要用了这个引擎,就会完完全全地动起来。
是的,就连羞羞的场景都会动起来!
它的原理大概是这样的
其成品大概是这样的
Live 2D的官网中也提到了该引擎即将支持全3D的消息,虽然这个消息从几年前就开始说了吧……
当然,我们也祝愿该游戏可以VR化成功,但我们还是有些好奇——
虚拟现实中的3D马赛克到底怎么打?
同样是日系羞羞类游戏,《3D定制女仆2》就赶在Oculus正式发售之前支持了VR技术。
这款游戏与上文中所介绍的某“艹猫”游戏略显不同,游戏中的妹子采用的是全3D建模。
游戏中,玩家可以在“定制”界面中捏出完全符合自己心意的女仆,然后依照剧情的需要嘿嘿嘿。
游戏正式发售后,广大玩家们也充分发挥了各自的技术手段,茫茫多的MOD以及各种羞羞补丁也是将这款游戏“玩”出了花样。
最有意思的是,国内外不少MMD制作人也将该游戏中的舞蹈模块单独拿了起来,用它制作的舞蹈视频质量也是相当了得。
其易用性也是超越了此前的MikuMikuDance
咳咳,说正经事。
自打《3D定制女仆2》推出了VR试玩版后,网友们瞬间就沸腾了,呼喊着“换电脑”的玩家数不胜数。
本来,Oculus公司还打算通过自家旗下的技术,在观光、电影、医药、建筑、空间探索以及战场等领域上大展手脚。
然而万万没想到,《3D定制女仆 2》试玩版的横空出世彻底却把Oculus推向了“万丈深渊”。
在很早以前的VR技术开发者大会中,Oculus rift的老大曾经默许了成人内容的存在;但谁也没想到,《3D定制女仆 2》就这么突如其然的“降临”了。
而后又过了一段时间,Oculus rift的创始人Palmer Luckey先生便矢口否认此前说过的那段话,大呼“羞羞的东西是万万不能登陆在我家VR平台的”。
对于这突如其来的《3D定制女仆 2》,也不知道一直致力于PS4体感游戏的索尼PlayStation VR团队有着怎样的感想。
平井一夫:明明是我先……
其实早在前两年,全心致力于黑科技的索尼就将自家旗下HMZ系列3D头戴显示器项目组解散,将原有的人才投入到了PlayStation VR研发团队当中。
而后,索尼在极短的时间内就研制出了属于他们的VR设备。
受限于PS4有限的机能,索尼方面只能强行要求游戏厂商将VR游戏帧数提高至60帧/秒,并通过一个外置的黑科技盒子将帧数强行提高至120帧。
就是右边这个不起眼的小黑盒子
为了提升游戏的流畅度、满足玩家的视觉需求,索尼在这方面也是蛮拼的。
当然,索尼旗下的VR游戏也十分争气,首发的大作名单也非常豪华。
这其中,就包括了我们熟知的《死或生:沙滩排球3》。
这款作品中,光荣脱裤魔可谓是使出了全身解数,游戏中女性角色的塑造简直是突破了天际……
柔肤引擎、换装系统……甚至还可以偷看她们换衣服……
当然,太大胆的偷看行为也会她们被骂变态(当然也有人好这口)。
作为即将登陆在PS4主机上的作品,《死或生:沙滩排球3》当然不会突破自己的下限,毕竟光荣脱裤魔还没“堕落”到illusion的那个级别。
如此清凉可口的福利,恐怕每一个买了PlayStation VR的玩家都会跑去买来尝试一下吧。
当然,除了以上几款作品之外,万代旗下的《铁拳》制作团队也开发了一款名为《夏日课堂》的VR游戏。
同样是PlayStation VR独占
游戏中清澈飒爽的风景和身旁的美少女,二者交相辉映,简直是美成了一幅画。
虽然该游戏也不会出现任何的限制级场景,但该作品所能表达出的意境也绝不是“就是干”风格游戏所能企及的。
其实除了文章中所提到的这些游戏之外,我们也见到了不少的虚拟现实“小电影”和游戏演示。
当然,国内的很大一部分玩家也对这些资源非常感兴趣,甚至为了它们跑去购买了VR设备。
谷歌纸板也好,Oculus、PlayStation VR也罢,这项新技术的流行势必需要着一些催化剂的协助。
而这些羞羞的内容,也就担当了这项重任。
最后,老司机们请不要在在评论区里肆意发车(?乛?乛?)
大家都在看
(扫描关注微信订阅号)
CopyRight &
版权所有|京ICP备号-2丨京网文(-838号1425人阅读
技术理论(1005)
3D游戏角色动画
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 作者:迷糊小亚
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
(这篇文章在gameres也有下的,这里的没有图片)
摘要:本文主要描述了3D游戏角色动画的原理及应用,从介绍微软的X文件到最为广泛应用的骨骼蒙皮动画,另外简要的介绍了下渐变动画的原理。
关键词:Role Animation&& Skeletal Animation&& Morphing Animation&& Skinned Mesh&
Abstract:Introduce 3D Game Role Animation, for example Skeletal Animation and Skinned Mesh etc.
一 概述3D角色动画的应用
二 3D游戏动画基础------基于时间的运动
三 3D游戏角色动画
&& 1 介绍微软的X文件
&& 2 骨骼蒙皮动画的原理与实现
&& 3 增加场景数据
&& 4 简介渐变动画
一 概述3D角色动画的应用
3D角色动画是计算机动画技术的一个重要组成部分,也是计算机图形学的一个分支。无论是在离线渲染环境下,还是在实时渲染环境下,3D角色动画都得到了广泛的应用。在离线渲染环境下,主要应用于动画电影制作和各类广告制作。动画电影制作中所使用的3D角色动画技术的一个重要特点是动画数据量大,渲染需要耗费大量时间,因此动画作品必须预先制作,渲染,然后转化成视频文件播放。在实时渲染环境下,主要应用于虚拟现实,视频游戏,甚至是建模软件,动画制作软件。现在,随着计算机硬件技术的发展,特别是带有硬件加速功能的显卡性能的提高,很多曾经只能在离线环境下应用的技术,都转移到实时渲染环境中来。其中,实时渲染的角色动画技术得到了发展且被广泛的应用。目前,实时角色动画技术大体可分为三种类型。
第一类是关节动画(Skeletal Animation)。关节动画中的角色由若干独立的部分组成。每一个部分对应着一个独立的网格模型,不同的部分按照角色的特点组织成一个层次结构。比如说,一个人体模型可以由头,上身,左上臂,左前臂,左手,右上臂,右前臂,右手,左大腿,左小腿,左脚,右大腿,右小腿,右脚等各部分组成。而某个部分,可能是另一个部分的子节点,同时又是另一个部分的父节点。比如上面的人体模型中,右前臂就是右上臂的子节点,同时也是右手的父节点。而右上臂是上身的子节点,后者则是躯体的子节点。通过改变不同部分之间的相对位置,比如夹角,位移等等,就可以实现所需要的各种动画效果。这类动画的优点很多。首先,在动画序列的关键帧中只需要存储节点间的相对变化,因此动画文件占用的空间很小。其次,可以实现很多复杂的动画效果,如果应用程序支持反向动力学还可以动态实现预先存储的动画序列之外的新的动画效果。当然这类动画也有不少缺点。其中之一是由于角色模型是一个层次模型,要获得某一个部分相对于世界坐标的位置,必须从根结点开始遍历该节点所有的祖先节点累计计算模型的世界变换。但最关键的问题是在不同部分的结合处往往会有很明显的接缝,这会严重的影响模型的真实感。
第二类是渐变动画(Morphing Animation)。这种动画中的角色由一系列的渐变网格模型构成。在动画序列的关键帧中记录着组成网格的各个顶点的新位置或者是相对于原位置的改变量。通过在相邻关键帧之间插值来直接改变该网格模型中各个顶点的位置就可以实现动画效果。相对于关节动画,单一网格模型动画的角色看上去更真实,也不会有关节动画所面临的接缝问题。由于没有使用层次模型,获得模型网格顶点在世界坐标中位置的计算量也很小。但是,这类动画的适应性很弱,角色很难通过实时计算来与环境进行良好的互动,以获得预先存储的动画序列之外的动画效果。另一方面,由于关键帧要存储网格模型所有的顶点信息,动画文件占用的空间比较大。
第三类是骨骼蒙皮动画(Skinned Mesh)。骨骼蒙皮动画可以看作是关节动画和渐变动画的结合。他同时兼有关节动画的灵活和渐变动画的逼真。后面将详细介绍骨骼蒙皮动画的技术细节。
3D角色动画技术和其它动画技术相结合,就能创造出绚丽多彩的游戏世界。
二 3D游戏动画基础------基于时间的运动
在一个游戏项目中,计时扮演了一个重要的角色。基于时间的运动,也就是创建计时器来控制运动。它能够产生这样一种动画效果:同样处理10000毫秒的动画,在性能好的计算机上得到平滑完整的动画效果,在性能不好的计算机上显得跳帧,但也能够在10000毫秒的时候完成任务,和性能好的计算机是同步的。
基本思路是事先设置好动画关键帧序列,在主循环中判断出第一个动画关键帧和下一个动画关键帧的编号,利用一个时间计数器去定位相对于第一动画关键帧的位置。随着时间计数器的增长,不断从第一个动画关键帧的位置移动至下一个动画关键帧的位置。主要分为以下几个步骤:
1设置动画关键帧序列。
2计算出每一帧的时间Time,Time是相对于程序开始运行的毫秒数。
3定位出第一个动画关键帧和下一个动画关键帧。
4利用Time计算出相对于第一个动画关键帧的毫秒数,再利用这个偏移毫秒数计算出相对于第一个动画帧的偏移位置。
5设置变换矩阵。
6回到第2步。
上图表示了4帧的关键帧动画,其中第0帧和第3帧变换矩阵相同。下面直接看代码,我将结合代码详细叙述。
typedef struct sKeyframe
& DWORD&&&& T
& D3DMATRIX matT
} sK//关键帧的结构,DWORD Time为执行该帧的时间,D3DMATRIX //matTransformation为在该帧时模型的变换矩阵。
sKeyframe g_Keyframes[4] =
& // Keyframe 0, 0ms
& {&& 0, 1.000000f, 0.000000f, 0.000000f, 0.000000f,
&&&&&&&& 0.000000f, 1.000000f, 0.000000f, 0.000000f,
&&&&&&&& 0.000000f, 0.000000f, 1.000000f, 0.000000f,
&&&&&&&& 0.000000f, 0.000000f, 0.000000f, 1.000000f },
& // Keyframe 1, 40ms
& {& 400, 0.000796f, 1.000000f, 0.000000f, 0.000000f,
&&&&&&&& -1.000000f, 0.000796f, 0.000000f, 0.000000f,
&&&&&&&&& 0.000000f, 0.000000f, 1.000000f, 0.000000f,
&&&&&&&& 50.000000f, 0.000000f, 0.000000f, 1.000000f },
& // Keyframe 2, 80ms
& {& 800, -0.999999f,& 0.001593f, 0.000000f, 0.000000f,
&&&&&&&&& -0.001593f, -0.999999f, 0.000000f, 0.000000f,
&&&&&&&&&& 0.000000f,& 0.000000f, 1.000000f, 0.000000f,
&&&&&&&&& 25.000000f, 25.000000f, 0.000000f, 1.000000f },
& // Keyframe 3, 120ms
& { 000f, 0.000000f, 0.000000f, 0.000000f,
&&&&&&&&& 0.000000f, 1.000000f, 0.000000f, 0.000000f,
&&&&&&&&& 0.000000f, 0.000000f, 1.000000f, 0.000000f,
&&&&&&&&& 0.000000f, 0.000000f, 0.000000f, 1.000000f }
};//定义了4帧的关键动画。其中第3帧和第0帧的变换矩阵一样,为了使动画能进入循环状态。
void DoFrame()&&& //此函数在循环内
& static DWORD StartTime = timeGetTime();
DWORD Time = timeGetTime() - StartT
//用timeGetTime()得到一个操作系统运行的毫秒数,储存到static变量以后将不再改变,//DWORD Time变量不断改变,为本程序运行的毫秒数。
Time %= (g_Keyframes[3].Time+1);//得到一个不断从0到1200变化的毫秒数。
& DWORD Keyframe = 0;& // 从第0帧开始。
& for(DWORD i=0;i&4;i++) {
&&& // 如果Time&= 某一关键帧的时间,将关键帧定位于此帧。
&&& if(Time &= g_Keyframes[i].Time)
&&&&& Keyframe =
& DWORD Keyframe2 = (Keyframe==3) ? Keyframe:Keyframe + 1;//得到接下来的关键帧,如//果Keyframe为第3关键动画帧,Keyframe2也为第3关键动画帧。
//当Keyframe=1200时这种情况才成立,几率很小。一般情况下,Keyframe2=Keyframe+1。
&& DWORD TimeDiff = g_Keyframes[Keyframe2].Time -
&&&&&&&&&&&&&&&&&& g_Keyframes[Keyframe].T
& if(!TimeDiff)
&&& TimeDiff=1;//计算两个sKeyframe的时间差,当Keyframe=Keyframe2=3时,TimeDiff=0,//此时另TimeDiff=1。
&&& float Scalar = (float)(Time - g_Keyframes[Keyframe].Time) / (float)TimeD& // Scalar取 //值为[0,1),利用Time计算出相对于Keyframe的偏移毫秒数,再除以两//帧的时间差。
& D3DXMATRIX matInt = D3DXMATRIX(g_Keyframes[Keyframe2].matTransformation) -
&&&&&&&&&&&&&&&&&&&&& D3DXMATRIX(g_Keyframes[Keyframe].matTransformation);
& matInt *= S //用于计算相对于Keyfrme的偏移位置。
& matInt += D3DXMATRIX(g_Keyframes[Keyframe].matTransformation); // 计算出该帧处相//对于Keyfrme的偏移位置。
& g_pD3DDevice-&SetTransform(D3DTS_WORLD, &matInt);& // 设置 world transformation matrix
&& 设置完变换矩阵,剩下的事情就只是渲染了。创建计时器控制动画的技术是非常简单有效的,这是现代计算机游戏动画的基础,因此,必须深刻理解它的内容。
三 3D游戏角色动画
1 介绍微软的X文件
&&& 制作3D游戏角色动画需要与之相关的&动画容器&。这个&容器&中储存着动画的数据。微软的X文件正是这样的一种容器。由于微软的影响力,它们公司设计的X文件也广为流传。X文件是一套基于模版定义的文件,理论上它能够容纳任何数据。也就意味着不仅仅是3D模型文件,任何用于游戏引擎加载的外部资源都可以被包含于X文件之中。下面我们详细的介绍下X文件。为了得到一个直观的印象,我们首先浏览下它的全貌。
xof 0302txt 0032
template Header {
&3D82AB43-62DA-11cf-AB39-&
template Frame {
&3D82AB46-62DA-11cf-AB39-&
[FrameTransformMatrix]
Frame Scene_Root {
FrameTransformMatrix {
1....000000,
0....000000,
0....000000,
0....000000;;
&&&&&&&&&&&&&&&&&&&& }
Frame Pyramid_Frame {
FrameTransformMatrix {
1....000000,
0....000000,
0....000000,
0....000000;;
Mesh PyramidMesh {
0.00;0.00000;,
-10.00;10.00000;,
10.00;10.00000;,
-10.00;-10.00000;,
10.00;-10.00000;;
MeshMaterialList {
0,0,0,0,0,0;;
Material Material0 {
1....000000;;
0.000000;&
0...050000;;
0...000000;;
&&&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&& }
&&&&&&&&&&&&&&&&&&& }
首先看头文件xof 0302txt 0032。xof表示这是一个真正的X文件。0302txt表示通知程序使用Directx的X文件,版本为3.2的模版,其中txt表示此文件为文本文件,可读,并非是一个2进制文件。0032表示一个浮点数的位数为32,如果想要用64位的浮点数,可以写成0064。
下面我们将按照以下七个步骤进行介绍和说明。
第一,声明一个模版:
假设声明 template ContactEntry ,首先需要用guidgen.exe产生一个GUID。产生的GUID如下:
// {4C9D055B-C64D-4bfe-A7D9-981F507E45FF}
DEFINE_GUID(&&name&&,
0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, /
0x1f, 0x50, 0x7e, 0x45, 0xff);
之后需要在程序代码中加入:
#include "initguid.h"
// At beginning of source code file - add DEFINE_GUIDs
DEFINE_GUID(ContactEntry, /
0x4c9d055b, 0xc64d, 0x4bfe, 0xa7, 0xd9, 0x98, /
0x1f, 0x50, 0x7e, 0x45, 0xff);
还要在X文件中加入:
template ContactEntry {
&4C9D055B-C64D-4bfe-A7D9-981F507E45FF&}
这里介绍下声明模版用到的数据类型:
关键字&&&&&& 描述
WORD&&&&&&&& 16-bit value (short)
DWORD&&&&&&& 32-bit value (32-bit int or long)
FLOAT&&&&&&& IEEE float value (float)
DOUBLE&&&&&& 64-bit floating-point value (double)
CHAR&&&&&&&& 8-bit signed value (signed char)
UCHAR&&&&&&& 8-bit unsigned value (unsigned char)
BYTE&&&&&&&& 8-bit unsigned value (unsigned char)
STRING&&&&&& A NULL-terminated string (char[]))
array&&&&&&& Signifies an array of following data type to follow ([])
使用数据类型的举例:
array STRING Text[20];//定义一个名为Text的数组,类型为STRING,大小为20。
DWORD ArrayS array STRING Names[ArraySize]; //可以将大小设置为变量。
现在,我们声明一个ContactEntry模版:
template ContactEntry {
&4C9D055B-C64D-4bfe-A7D9-981F507E45FF&
STRING N // The contact's name
STRING PhoneN // The contact's phone number
DWORD A // The contact's age
实例化一个模版对象:
ContactEntry JimsEntry {
"Jim Adams";
"(800) 555-1212";
{JimsEntry} 可以用这样的形式引用一个数据对象。例如,在一个animation sequence template中引用一个Frame data object做为其内嵌数据对象。也可以利用引用表示一个数据对象的副本,没有必要重复书写这个数据对象。
第二,内嵌数据对象和模版约束:
首先,我们分别声明了三个不同的模版,请仔细看它们的区别。
template ClosedTemplate {
&4C9D055B-C64D-4bfe-A7D9-981F507E45FF&
DWORD ClosedD
template OpenTemplate {
&4C9D055B-C64D-4bff-A7D9-981F507E45FF&
DWORD OpenD
template RestrictedTemplate {
&4C9D055B-C64D-4c00-A7D9-981F507E45FF&
DWORD RestrictedD
[ClosedTemplate]
[OpenTemplate]
ClosedTemplate看起来没有什么不同,因为它就是标准的模版声明。在OpenTemplate中包含一个[...],表示这是一个开放模版。开放模版允许在[]中内嵌任何数据对象。例如,你可以实例化OpenTemplate,在里面定义一个OpenData变量和内嵌一个ClosedTemplate的实例。最后的RestrictedTemplate为约束模版。约束模版实例化时只允许包含它列出的数据对象,如,不能在RestrictedTemplate包含[ClosedTemplate],[OpenTemplate]以外的数据对象。
第三,充分利用DirectX .X Standard Templates:
正如上面提到的,X文件广泛用于包含一个mesh信息。一个Standard Templates包含了各种信息。
Table 3: DirectX .X Standard Templates
Template Name&&&&&&&&&&&&&& Description
Animation:&&&&&&&&&&&&&&&&&& Defines animation data for a single frame.
AnimationKey:&&&&&&&&&&&&&&& Defines a single key frame for the parent animation template.
AnimationOptions:&&&&&&&&&&&& Contains animation playback information.
AnimationSet:&&&&&&&&&&&&&&&& Contains a collection of animation templates.
Boolean:&&&&&&&&&&&&&&&&&&&& Holds a Boolean value.
Boolean2d:&&&&&&&&&&&&&&&&&& Holds two Boolean values.
ColorRGB:&&&&&&&&&&&&&&&&&& Contains red, green, and blue color values.
ColorRGBA:&&&&&&&&&&&&&&&&& Contains red, green, blue, and alpha color values.
Coords2d:&&&&&&&&&&&&&&&&&&& Defines two coordinate values.
FloatKeys:&&&&&&&&&&&&&&&&&&& Contains an array of floating-point values.
FrameTransformMatrix:&&&&&&&& Holds the transformation matrix for a parent Frame template.
Frame:&&&&&&&&&&&&&&&&&&&&&& A frame-of-reference template that defines a hierarchy.
Header:&&&&&&&&&&&&&&&&&&&&& The .X file header that contains version numbers.
IndexedColor:&&&&&&&&&&&&&&&& Contains an indexed color value.
Material:&&&&&&&&&&&&&&&&&&&& Contains material color values.
Matrix4x4:&&&&&&&&&&&&&&&&&& Holds a 4x4 homogenous matrix container.
Mesh:&&&&&&&&&&&&&&&&&&&&&& Contains a single mesh's data.
MeshFace:&&&&&&&&&&&&&&&&&& Holds a mesh's face data.
MeshFaceWraps:&&&&&&&&&&&&& Contains the texture wrapping for mesh faces.
MeshMaterialList:&&&&&&&&&&&&& Contains the material for face-mapping values.
MeshNormals:&&&&&&&&&&&&&&& Holds normals used for mesh data.
MeshTextureCoords:&&&&&&&&&& Holds texture coordinates used for mesh data.
MeshVertexColors:&&&&&&&&&&&& Holds vertex color information used for mesh vertices.
Patch:&&&&&&&&&&&&&&&&&&&&&& Defines a control patch.
PatchMesh:&&&&&&&&&&&&&&&&& Contains a patch mesh (much like the Mesh template).
Quaternion:&&&&&&&&&&&&&&&&&& Holds a quaternion value.
SkinWeights:&&&&&&&&&&&&&&&& Contains an array of weight values mapped to a mesh's vertices. Used in skinned meshes.
TextureFilename:&&&&&&&&&&&&& Contains the texture file name to use for a material.
TimedFloatKeys:&&&&&&&&&&&&& Contains an array of FloatKeys templates.
Vector:&&&&&&&&&&&&&&&&&&&&& Holds a 3D coordinate value.
VertexDuplicationIndices:&&&&&& Informs you which vertices are duplicates of other vertices.
XSkinMeshHeader:&&&&&&&&&&& Used by skinned meshes to define the number of bones contained in a mesh.
我们可以在DirectX9SDK的安装目录下搜索到&rmxfguid.h&字样的头文件。在rmxfguid.h中定义了各个模版的宏,例如:
/* {3D82AB44-62DA-11cf-AB39-} */
DEFINE_GUID(TID_D3DRMMesh,
0x3d82ab44, 0x62da, 0x11cf, 0xab, 0x39, 0x0, 0x20, 0xaf, 0x71, 0xe4, 0x33);
每个模版名加上前缀TID_D3DRM就是宏定义名。既然微软已经帮助我们定义了那么多的模版,我们可以根据需求,充分的去利用这些模版。另外,这些模版往往会有相关的帮助函数,我们使用它们可以事半功倍。
第四,创建X文件接口:
&&& 我们已经对X文件的模版概念有所了解,现在将使用它们。当然,要想使用首先得访问X文件。访问任何X文件首先要调用DirectXFileCreate函数创建一个IDirectXFile接口,这个接口就代表了一个X文件。
IDirectXFile *pDXFile = NULL;
HRESULT Result = DirectXFileCreate(&pDXFile);//用&pDXFile返回指向接口的指针。用SUCCEEDED或者FAILED宏判断返回值是否有效。
创建完IDirectXFile接口,我们要注册一个定制模版或者标准模版。定制模版是自己定义的模版,标准模版是微软帮我们定义的模版,其实它们之间没有本质的区别,你现在要做的就是告诉IDirectXFile接口,使用二者之中的哪种。
如何注册定制模版呢?下面举个例子更容易理解。你可以把X文件中的模版移除,直接在代码里定义那些模版。IDirectXFile接口支持这样的特性。需要调用IDirectXFile::RegisterTemplates函数。
HRESULT IDirectXFile::RegisterTemplates(
LPVOID pvData, // 一个定义模版数据的缓存,应该精确无误。
DWORD cbSize); // pvData缓存的字节数。
可以如下定义一个模版数据:
char *Templates = "
"xof 0303txt 0032 /& //标准X文件头。
template CustomTemplate { /
&4ce9a-11cf-ab43-& /
array DWORD Values[Length]; /
之后在用RegisterTemplates将其注册:
pFile-&RegisterTemplates(Templates, strlen(Templates));
如何注册标准模版呢?首先需要在代码中包含rmxfguid.h和rmxftmpl.h。rmxfguid.h定义了各个标准模版的GUDI,rmxftmpl.h以2进制数据形式定义了标准模版数据的缓存和其字节数。然后调用RegisterTemplates将其注册:
pFile-&RegisterTemplates(D3DRM_XTEMPLATES, /
D3DRM_XTEMPLATE_BYTES);
第五,打开X文件:
创建完IDirectXFile接口,注册模版之后需要打开X文件,枚举其数据对象。调用IDirectXFile::CreateEnumObject函数。
HRESULT IDirectXfile::CreateEnumObject(LPVOID pvSource, // .X filename
DXFILELOADOPTIONS dwLoadOptions, // Load options
LPDIRECTXFILEENUMOBJECT* ppEnumObj); // Enum interface
当调用CreateEnumObject函数,用pvSource指定一个文件的名字,用ppEnumObj返回一个枚举对象接口指针。用dwLoadOptions指定load操作方式。当指定DXFILELOAD_FROMFILE值,告诉DirectX从磁盘载入一个文件。还有DXFILELOAD_FROMRESOURCE,DXFILELOAD_FROMMEMORY和DXFILELOAD_FROMURL分别表示从一个资源,内存缓冲和Internet上加载X文件。当从Internet加载文件时,需要为其指定完整的网址。
下面代码从磁盘加载X文件:
// Filename = filename to load ("test.x" for example)
IDirectXFileEnumObject *pE
pFile-&CreateEnumObject((LPVOID)Filename, /
DXFILELOAD_FROMFILE, &pEnum);
Filename指向一个有效的文件名,pEnum返回一个枚举对象接口指针。
第六,枚举数据对象:
注册完模版,打开X文件并且得到一个枚举对象接口,下面需要从X文件读出数据。枚举对象接口指针指向文件的第一个数据对象,因为每一个数据对象可能包含内嵌数据对象或者引用的数据对象,所以与第一个数据对象同在一层级的其它数据对象为同层级数据对象。至于包含的子数据对象的类型,需要对其分别的行进询问。可以使用HRESULT IDirectXFileEnumObject::GetNextDataObject (LPDIRECTXFILEDATA* ppDataObj)得到一个IDirectXFileData接口。它只有一个参数:
IDirectXFileData *pD
HRESULT hr = pEnum-&GetNextDataObject(&pData);
利用此函数,可以不断地访问同一层级的数据对象接口,具体代码如下:
while(SUCCEEDED(pEnum-&GetNextDataObject(&pData))) {
// 这里可以对pData数据对象进行操作。
pData-&Release();//释放接口。
当返回值为FAILED,表示已经访问完所有的接口。当访问值为SUCCEEDED,你需要继续判断这个数据对象是否包含子对象。利用接口IDirectXFileObject,和HRESULT IDirectXFileData::GetNextObject( LPDIRECTXFILEOBJECT* ppChildObj)函数,代码如下:
IDirectXFileObject *pO
while(SUCCEEDED(pData-&GetNextObject(&pObject)))
// 如果一个子对象存在,需要继续询问它,判断出它的类型为内嵌数据对象或者引用的数
// 据对象。
pObject-&Release();// 释放接口。
接下来询问接口,看其是否为内嵌数据对象:
IDirectXFileData *pSubD
if(SUCCEEDED(pObject-&QueryInterface( /
IID_IDirectXFileData, (void**)&pSubData))) {
// 如果询问内嵌数据对象成功,可以对pSubData数据对象进行操作
pSubData-&Release();//释放接口。
看其是否为引用数据对象:
IDirectXFileDataReference *pR
IDirectXFileData *pSubD
if(SUCCEEDED(pSubObj-&QueryInterface( IID_IDirectXFileDataReference, /
(void**)&pRef))) {
// 如果询问引用的数据对象成功,解析出引用的原型。
pRef-&Resolve(&pSubData);
//这里可以对pData数据对象进行操作。
pRef-&Release();
pSubData-&Release();//释放接口。
现在整理下思路:大体的思路其实很简单,首先枚举最顶层的数据对象,然后判断其是否有子对象,这个子对象可能是内嵌对象或者引用对象二者之一,分别询问其接口,就可以判断出具体的类型。
下面是完整的Parse模版的函数:
BOOL Parse(char *Filename)
IDirectXFile *pFile = NULL;
IDirectXFileEnumObject *pEnum = NULL;
IDirectXFileData *pData = NULL;
if(FAILED(DirectXFileCreate(&pFile)))
return FALSE;
//注册标准模版。
if(FAILED(pFile-&RegisterTemplates( /
(LPVOID)D3DRM_XTEMPLATES, D3DRM_XTEMPLATE_BYTES)))
return FALSE;
//创建一个枚举对象接口。
if(FAILED(pDXFile-&CreateEnumObject((LPVOID)Filename, /
DXFILELOAD_FROMFILE, /
&pEnum))) {
pFile-&Release();
return FALSE;
// 遍历所有的顶层数据对象。
while(SUCCEEDED(pEnum-&GetNextDataObject(&pData))) {
// 用ParseObject解析其子数据对象。
ParseObject(pData);
pData-&Release();
pEnum-&Release();
pFile-&Release();
return TRUE;
这个函数的主要部分在ParseObject(pData),它负责解析子数据对象:
void ParseObject(IDirectXFileData *pData)
& IDirectXFileObject *pObject = NULL;
& IDirectXFileData *pSubData = NULL;
& IDirectXFileDataReference *pRef = NULL;
& while(SUCCEEDED(pData-&GetNextObject(&pObject))) {
& if(SUCCEEDED(pObject-&QueryInterface( IID_IDirectXFileDataReference, (void**)&pRef))) {
& pRef-&Resolve(&pSubData);
& ParseObject(pSubData);
& pSubData-&Release();
& pRef-&Release();&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
& if(SUCCEEDED(pObject-&QueryInterface( IID_IDirectXFileData, (void**)&pSubData))) {
&&& ParseObject(pSubData);
&&& pSubData-&Release();
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
& pObject-&Release();
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& }
这是个递归函数,调用函数自身。判断子对象的类别,对其继续解析,直到返回值为FAILED,表示已没有子对象。从上面可以看出,这个函数除了枚举所有的对象,并没有做任何事情,下面就要从这些数据对象检索数据。
第七,从数据对象检索数据:
当你用IDirectXFileData接口指针指向一个有效的数据对象,可以调用IDirectXFileData::GetName函数得到该数据对象的名字。函数原型为:
HRESULT IDirectXFileData::GetName(
LPSTR pstrNameBuf, // 名字缓冲
LPDWORD pdwBufLen); // 名字缓冲的大小
可以这样使用这个函数:
pData-&GetName(NULL, &Size);
char *Name = new char[Size];
pData-&GetName(Name, &Size);
首先声明一个DWORD Size变量,调用GetName函数时将第一个参数设为NULL,在Size返回名字缓冲的大小。之后利用这个Size值创建存放名字的缓冲,再调用GetName在Name中返回数据对象的名字。
得到了数据对象的名字,你需要得到这个数据对象的模版GUID,去判断这个数据对象是否为你想使用的那个模版的数据对象。利用IDirectXFileData::GetType函数,其原型为:
HRESULT IDirectXFileData::GetType(const GUID ** ppguid);
可以这样使用这个函数:
const GUID *TemplateGUID = NULL;
pData-&GetType(&TemplateGUID);//在TemplateGUID中返回该数据对象对应模版的GUID。
现在去匹配这个GUID,看它是否为你想使用的模版的数据对象。
if(*TemplateGUID == TID_D3DRMMeshNormals) {
// 如果匹配成功,这里可以继续处理这个模版的数据对象。
最后将介绍GetData函数,用它真正的得到了数据对象的数据。其原型为:
HRESULT IDirectXFileData::GetData(
LPCSTR szMember, // 设置为NULL
DWORD *pcbSize, // 数据的大小
void **ppvData); // 数据指针
下面介绍用GetData得到数据对象结构的大小和数据对象的数据。
假设有这样一个颜色的模版:
template ColorRGBA {
&35FF44E0-6C7C-11cf-8F52-A3&
你想访问基于此模版的数据对象的数据你可以这样做:
DWORD DataS
float *DataP
pData-&GetData(NULL, &DataSize, (void**)&DataPtr);
float red = *DataPtr++;
float green = *DataPtr++;
float blue = *DataPtr++;
float alpha = *DataPtr++;
得到指向数据对象的数据的指针后,就像访问一般的结构一样简单。当然,你可以做得更直接:
typedef struct {
float red, green, blue,
} sColorRGBA;//定一个结构方便访问数据对象的数据。
sColorRGBA *C
DWORD DataS
pData-&GetData(NULL, &DataSize,(void**)&Color);
这样访问数据时更直接:
float red = Color-&
float blue = Color-&
float green = Color-&
float alpha = Color-&
访问单个变量是很简单的,下面继续介绍访问数组或字符串。
访问数组:
DWORD DataS
DWORD *DataP
pData-&GetData(NULL, &DataSize, (void**)&DataPtr);
DWORD NumKeys = *DataPtr++;
for(DWORD i=0;i&NumKi++) {
float fValue = *(FLOAT*)DataPtr++;
访问字符串:
DWORD DataS
DWORD *DataP
pData-&GetData(NULL, &DataSize, (void**)&DataPtr);
char *StringPtr = (char*)DataP
MessageBox(NULL, StringPtr, "Texture Filename", MB_OK);
为了访问数组或字符串,其本质就是把指针转化成匹配的类型,方便指针的定位操作。
至此,我们已经介绍了X文件的使用方法,X文件是动画模型的载体,所以有必要了解它,进而才能更好的操作它。同时也为后面的动画技术做好了准备。
2 骨骼动画的原理与实现
&&& 这里的骨骼蒙皮动画特指skinnd mesh,也叫骨骼动画。无论是合金装备,波斯王子,还是魔兽世界,到处都是骨骼动画技术的运用。用它塑造了各种各样,栩栩如生的生物。其中,人类体格的骨骼动画运用最为广泛。现在让我们一步一步揭开它神秘的面纱。
从本质上来讲,所有的3D角色动画系统都是基于一种逻辑,就是用一定的方法去改变Mesh顶点的位置,只是具体改变的方法不同而已。骨骼动画也是一样的。
骨骼动画的基本原理就是首先控制各个骨骼和关节,再使符在上面的skinned mesh与其匹配。在骨骼蒙皮动画中,一个角色由作为皮肤的单一网格模型和按照一定层次组织起来的骨骼组成。骨骼层次描述了角色的结构,就像关节动画中的不同部分一样,骨骼蒙皮动画中的骨骼按照角色的特点组成一个层次结构。相邻的骨骼通过关节相连,并且可以作相对的运动。通过改变相邻骨骼间的夹角,位移,组成角色的骨骼就可以做出不同的动作,实现不同的动画效果。皮肤则作为一个网格蒙在骨骼之上,规定角色的外观。这里的皮肤不是固定不变的刚性网格,而是可以在骨骼影响下变化的一个可变形网格。组成皮肤的每一个顶点都会受到一个或者多个骨骼的影响。在顶点受到多个骨骼影响的情况下,不同的骨骼按照与顶点的几何,物理关系确定对该顶点的影响权重,这一权重可以通过建模软件计算,也可以手工设置。通过计算影响该顶点的不同骨骼对它影响的加权和就可以得到该顶点在世界坐标系中的正确位置。动画文件中的关键帧一般保存着骨骼的位置,朝向等信息。通过在动画序列中相邻的两个关键帧间插值可以确定某一时刻各个骨骼的新位置和新朝向。然后按照皮肤网格各个顶点中保存的影响它的骨骼索引和相应的权重信息可以计算出该顶点的新位置。这样就实现了在骨骼驱动下的单一皮肤网格变形动画。或者简单地说骨骼蒙皮动画。骨骼蒙皮动画的效果比关节动画和单一网格动画更逼真,更生动。而且,随着3D硬件性能的提高,越来越多的相关计算可以通过硬件来完成,骨骼蒙皮动画已经成为各类实时动画应用中使用最广泛的动画技术。
下面讨论骨骼蒙皮动画实现的技术细节。在一个典型的骨骼蒙皮动画模型文件中,会保存如下信息:网格信息,骨骼信息和动画信息。网格信息是角色的多边形模型。该多边形模型一般由三角形面片组成,每一三角形面片有三个指向模型的顶点表的索引。通过该索引,可以确定该三角形的三个顶点。顶点表中的每一顶点除了带有位置,法向量,材质,纹理等基本信息外,还会指出有哪些骨骼影响了该顶点,影响权重又是多少。影响一个顶点的最大骨骼数一般取决于模型的设计和目标硬件平台的限制。比如,对于一个典型的人体骨架,一般只有在关节附近的顶点才会受到相邻几块骨骼的影响,而同时影响某一顶点的骨骼数,也不会超过四块。骨骼信息包括全部骨骼的数量和每一骨骼的具体信息。所有的骨骼按照父子关系组织成一棵树。树根代表整个骨架,其余每一节点包括叶子节点代表一根骨骼。每一根骨骼包括该骨骼在父骨骼坐标系中的变换矩阵,通过该变化矩阵确定了该骨骼在父骨骼坐标系中的位置。在动画信息中则保存了若干关键帧。每一关键帧指出了每一骨骼在该时刻相对于父骨骼坐标系的变换矩阵,当然也可以是该骨骼相对于父骨骼的位置,朝向等变动。在播放动画序列中的任一时刻:
1)首先确定该时刻之前和之后的两个关键帧,然后按照该时刻与前后两个关键帧时刻的时间值插值计算出该时刻该骨骼相对于父骨骼的新变换矩阵,这个变换矩阵往往代表旋转变换,放缩变换也能接受,如果是平移变换矩阵,很容易将Mesh四分五裂。
2)对于皮肤网格中的每一个顶点,计算它在世界坐标中新的位置和朝向。首先找到影响该顶点的所有骨骼。然后计算每一骨骼对该顶点的影响。也就时说,计算在该骨骼独立作用下顶点的新位置。计算按照如下公式:&&&&&&&&&&&&
顶点的新位置 =& 最初状态顶点的位置* 最初状态骨骼世界变换矩阵的逆矩阵* 骨骼的新变换矩阵(I)
然后将所有这些新位置按照每一骨骼的影响权重加权求和。注意所有权重的和应该恰好为 1。在公式(I)中,最初状态顶点的位置为什么首先要与最初状态骨骼世界变矩阵的逆矩阵相乘呢?前面说过,骨骼的新变换矩阵是相对于父骨骼变换的,另一方面这个新变换矩阵是世界变换矩阵,它的任何变换是相对于世界坐标系原点的。因此需要把最初状态顶点移动到相当于父骨骼节点是原点的位置上,再进行矩阵变换。
3)根据网格模型顶点的新位置和朝向绘制角色网格。
下面我们结合具体的图形API环境(Direct3D)来进一步介绍骨骼动画。
第一,了解骨骼结构(Skeletal Structures)和骨层级(Bone Hierarchies):
骨骼结构就是连续很多的骨头(Bone)相结合,形成的骨层级。第一个骨头叫做根骨(root bone),是形成骨骼结构的关键点。其它所有的骨骼作为孩子骨(child bone)或者兄弟骨(sibling bone)附加在根骨之上。所谓的&骨&用一个帧(frame)对象表示。在Directx中,用一个D3DXFRAME结构或者X文件中的Frame template来表示帧对象。下面看一下Frame template和D3DXFRAME结构的定义:
template Frame{&&&&&&& & 3D82AB46-62DA-11cf-AB39- &&&&&&&& FrameTransformMatrix frameTransformM&&&&& // 骨骼相对于父节点的坐标变换矩阵,就是一个matrix&&&&&&& M&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& // 骨骼的Mesh}
typedef struct _D3DXFRAME{&LPSTR&&&&&&&&&&&&&&&&&& N&&&&&&&&&&&&&&&&&&&&&&&&&&& // 骨骼名称 &D3DXMATRIX&&&&&&&& TransformationM&&&&&&& // 相对与父节点的坐标变换矩阵
&LPD3DXMESHCONTAINER&&&& pMeshC& // LPD3DXMESHCONTAINER对象,
//用来加载MESH,还有一些附加属性,见SDK
&struct _D3DXFRAME&&&&&& *pFrameS&&&&&&&& // 兄弟节点指针,和下面的子节点指针
&// 一块作用构成骨骼的层次结构。&&& &&& struct _D3DXFRAME&&&&&& *pFrameFirstC&&&&& // 子节点指针} D3DXFRAME, *LPD3DXFRAME;
注意D3DXFRAME * pFrameSibling和D3DXFRAME * pFrameFirstChild,主要是利用这两个指针形成骨层级。pFrameSibling把一个骨头连接到兄弟层级,相对的,pFrameFirstChild把一个骨头连接到子层级。通常,你需要用建模软件为你的程序创建那些骨骼结构,输出骨层级到X文件以便使用。Microsoft有3D Studio Max和Maya的输出插件(exporter),可以输出骨骼和动画数据到X文件。很多建模程序也都有这样的功能。
X文件包含了帧数据,用一个帧(frame)模版的数据对象层级代表骨骼层级。如下图:
注意上面的图,利用D3DXFRAME pointers指针形成了一个兄弟帧和孩子帧的链表。
在前面template Frame中已经提及过每个Frame数据对象中存放着一个变换矩阵,这个矩阵描述了该骨骼相对于父骨骼的位置。另外在根Frame数据对象中内嵌了一个标准的Mesh数据对象。Frame定义了骨骼的层级,而Mesh中的SkinWeights数据对象定义了Frame代表的骨头。我们用D3DXFRAME结构容纳从X文件加载进来的Frame数据对象。为了更好的容纳Frame数据对象,我们需要扩展下D3DXFRAME结构:
struct D3DXFRAME_EX : D3DXFRAME
& D3DXMATRIX matC&& // 组合变换矩阵,用于储存变换的骨骼矩阵
& D3DXMATRIX matO&& // 从X文件加载的原始变换矩阵
& D3DXFRAME_EX()
&&& Name = NULL;
&&& pMeshContainer = NULL;
&&& pFrameSibling = pFrameFirstChild = NULL;
&&& D3DXMatrixIdentity(&matCombined);
&&& D3DXMatrixIdentity(&matOriginal);
&&& D3DXMatrixIdentity(&TransformationMatrix);
& ~D3DXFRAME_EX()
&&& delete [] N&&&&&&&&& Name = NULL;
&&& delete pFrameS&&& pFrameSibling = NULL;
&&& delete pFrameFirstC pFrameFirstChild = NULL;
利用我们以前介绍的cXParse类可以遍历X文件的数据对象,从而加载出Frame数据对象。下面的代码都是写在方法ParseObject中,如下:
// 判断当前分析的是不是Frame节点if( objGUID == TID_D3DRMFrame ){
// 引用对象直接返回,不需要做分析。一个数据段实际定义一次后可以被其他模板引用,例
//如后面的Animation动画模板就会引用这里的Frame
// 节点,标识动画关联的骨骼。&&&&&&& if( pDataObj-&IsReference() )
&&&&&&& // D3DXFRAME_EX为D3DXFRAME的扩展结构,增加些数据成员
&&&&&&& D3DXFRAME_EX *pFrame = new D3DXFRAME_EX();
// 得到名称&&&&&&& pFrame-&Name = GetObjectName( pDataObj );
// 注意观察文件就可以发现一个Frame要么是根Frame,父节点不存在, 要么作为某
//个Frame的孩子Frame而存在。&&&&&&& if( NULL == pData )&&&&&&& {&&&&&&&&&&&&&&& // 作为根节点的兄弟节点加入链表。&&&&&&&&&&&&&&& pFrame-&pFrameSibling = m_pRootF&&&&&&&&&&&&&&& m_pRootFrame = pF&&&&&&&&&&&&&&& pFrame = NULL;
&&&&&&&&&&& // 将自定义数据指针指向自己,供子节点引用。&&&&&&&&&&&&&&& pData = ( void** )&m_pRootF&&&&&&&& }&&&&&&&& else&&&&&&&& {&&&&&&&&&&&&&&& // 作为传入节点的子节点&&&&&&&&&&&&&&& D3DXFRAME_EX *pDataFrame = ( D3DXFRAME_EX* )( *pData );&&&&&&&&&&&&&&& pFrame-&pFrameSibling = pDataFrame-&pFrameFirstC&&&&&&&&&&&&&&& pDataFrame-&pFrameFirstChild = pF &&&&&&&&&&&&&&& pFrame = NULL;
&&&&&&&&&&&&&&& pData = ( void** )&pDataFrame-&pFrameFirstC&&&&&&&&& } }
记住我们只需要做一件事情,判断类型,分配匹配的对象然后拷贝数据,下面来分析Frame中的matrix,
// frame的坐标变换矩阵, 因为matrix必然属于某个Frame所以pData必须有效else if( objGUID == TID_D3DRMFrameTransformMatrix && pData ){&&&&&&&&& // 我们可以肯定pData指向某个Frame&&&&&&&&& D3DXFRAME_EX *pDataFrame = ( D3DXFRAME_EX* )( *pData );
&&&&&&&&& // 先取得缓冲区大小,应该是个标准的4x4矩阵 &&&&&&&&& DWORD size = 0;&&&&&&&&& LPCVOID buffer = NULL;
&&&&&&&&& hr = pDataObj-&Lock( &size, &buffer );&&&&&&&&& if( FAILED( hr ) )
&&&&&&&&& // 拷贝数据&&&&&&&&& if( size == sizeof( D3DXMATRIX ) )&&&&&&&&& {&&&&&&&&&&&&&& memcpy( &pDataFrame-&TransformationMatrix, buffer, size );&&&&&&&&&&&&&& pDataObj-&Unlock();
&&&&&&&&&&&&&& pDataFrame-&matOriginal = pDataFrame-&TransformationM&&&&&&&&& }}
第二,修改和更新骨骼层级:
加载完骨骼层级之后,你可以操作它,更改骨骼的方位。你需要创建一个递归函数,按照名字找到相应的Frame数据对象。这个函数如下:
D3DXFRAME_EX *FindFrame(D3DXFRAME_EX *Frame, char *Name)
if(Frame && Frame&&Name && Name) {
// 如果名字找到,返回一个Frame指针
if(!strcmp(Frame&&Name, Name)) // strcmp函数比较两个字符串,如果两个字符串相等,返回0
// 在sibling frames找匹配的名字
if(Frame && Frame&&pFrameSibling) {
D3DXFRAME_EX *FramePtr = /
FindFrame((D3DXFRAME_EX*)Frame&&pFrameSibling, /
if(FramePtr)
return FrameP
// 在child frames找匹配的名字
if(Frame && Frame&&pFrameFirstChild) {
D3DXFRAME_EX *FramePtr = /
FindFrame((D3DXFRAME_EX*)Frame&&pFrameFirstChild, /
if(FramePtr)
return FrameP
// 如果没有找到,返回 NULL
return NULL;
如果你想找到一个叫&Leg&的Frame,可以把&Leg&传入FindFrame函数,并且提供指向RootFrame的指针:
// pRootframe 为D3DXFRAME_EX root frame 指针
D3DXFRAME_EX *Frame = FindFrame(pRootFrame, "Leg");
if(Frame) {
// 可以在这里做一些处理,比如旋转操作
// 你在这里可以稍微的旋转这个骨头
D3DXMatrixRotationY(&Frame&&TransformationMatrix, 1.57f);
一旦你修改变换骨头,你需要更新整个骨骼层级,也就是把变换的组合矩阵存入D3DXFRAME_EX结构的matCombined成员中,用于后面的渲染。下面的函数应该增加到D3DXFRAME_EX结构中,如下:
void UpdateHierarchy(D3DXMATRIX *matTransformation = NULL)
&&& D3DXFRAME_EX *pFrameP
&&& D3DXMATRIX matI
&&& // 如果为空,用一个全同矩阵
&&& if(!matTransformation) {
&&&&& D3DXMatrixIdentity(&matIdentity);
&&&&& matTransformation = &matI
&&& // 把变换矩阵组合到matCombined中
&&& matCombined = TransformationMatrix * (*matTransformation);
&&& // 更新兄弟层级
&&& if((pFramePtr = (D3DXFRAME_EX*)pFrameSibling))
&&&&& pFramePtr-&UpdateHierarchy(matTransformation);
&&& // 更新孩子层级
&&& if((pFramePtr = (D3DXFRAME_EX*)pFrameFirstChild))
&&&&& pFramePtr-&UpdateHierarchy(&matCombined);
&&& 现在matCombined储存着每个骨骼相对于原点的变换矩阵,然后只要把各个顶点附在相应的骨骼上,就能渲染了。
第三,使用蒙皮网格:
& 网格可以分为蒙皮网格(Skin Mesh)和普通网格(Mesh)。蒙皮网格就是具有蒙皮信息的普通网格。为了搞清楚蒙皮网格我们需要介绍相关的三个模版:
template Mesh
&&& &3D82AB44-62DA-11CF-AB39-&
&&& DWORD nV&&&&&&&&&&& //顶点数
&&& array Vector vertices[nVertices];& //顶点坐标数组
&&& DWORD nF&&&&&&&&&&&&& //多边形数
&&& array MeshFace faces[nFaces];&& //多边形顶点引索
}这个模板存储一个表态的网格和网格的材质。在骨骼蒙皮动画中,整个角色只是一个网格,由蒙皮信息确定网格中的每一个部分如何受到骨骼的影响。网格在内部会分成几个子集,每一个子集将受到一些特定骨骼的影响。template XSkinMeshHeader
&&& & 3CF169CE-FF7C-44ab-93C0-F78F62D172E2 &&
&&& WORD nMaxSkinWeightsPerV& // 网格中受到骨骼影响的顶点数
&&& WORD nMaxSkinWeightsPerF&&& // 网格中受到骨骼影响的多边形数
&&& WORD nB&&&&&&&&&&&&&&&&&& // 影响网格顶点的骨骼数量
这个模版包含于Mesh模版中。包含关于蒙皮信息的属性。template SkinWeights
&&& & 6F0D123B-BAD2--80224F25FABB &
&&& STRING transformNodeN&&&&&&& //骨骼的名字
&&& DWORD nW&&&&&&&&&&&&&&&& //附属到该骨骼的顶点数
&&& array DWORD vertexIndices[nWeights]; //附属到该骨骼的顶点引索
&&& array float weights[nWeights];&&&&&&&& //相应引索的顶点权值
&&& Matrix4x4 matrixO&&&&&&&&&&&&& //相对于骨骼位置的偏移矩阵
这个模版也包含于Mesh模版中,真正的蒙皮信息就存储在这里。每一个影响到网格的骨骼在模版中都有实例。例如有12个骨骼影响到网格,Mesh模版里将有12个SkinWeights模版的实例。
蒙皮网格和普通网格的唯一不同点就是看XskinMeshHeader和SkinWeights模版是否存在。如果把这两个模版从任何一个蒙皮网格里面移走的话,就可以得到一个普通网格。在X文件中,我们将会发现一个GUID为TID_D3DRMMesh的模版,这表示模版里面存有一个网格。利用D3D的帮助函数D3DXLoadSkinMeshFromXof将会加载蒙皮网格和其它补充性数据。只需要向它传递一个IDirectXFileData指针,然后它将为你做剩下的事情。现在介绍下D3DXLoadSkinMeshFromXof函数:
HRESULT D3DXLoadSkinMeshFromXof(
& LPD3DXFILEDATA pxofMesh,&&&&&&& //X文件数据接口
& DWORD Options,&&&&&&&&&&&&&&&&&& //加载参数
& LPDIRECT3DDEVICE9 pD3DDevice,& //使用的三维设备
& LPD3DXBUFFER * ppAdjacency,&&&&& //邻接信息缓冲接口
& LPD3DXBUFFER * ppMaterials,&&&&&& //材质缓冲接口
& LPD3DXBUFFER * ppEffectInstances,& //效果实例接口
& DWORD * pMatOut,&&&&&&&&&&&&&&&& //材质数
& LPD3DXSKININFO * ppSkinInfo,&&&&& //蒙皮信息接口
& LPD3DXMESH * ppMesh&&&&&&&&&&&& //加载的网格模型接口
需要特别注意是LPD3DXSKININFO * ppSkinInfo接口,储存着蒙皮信息。
当你加载一个网格,并读取了的这些顶点的权值之后,你可以变换这些顶点去匹配骨骼的方向,使用以下步骤:
1)迭代所有的顶点。为每个顶点进行第2步。
2)对当前顶点连接到的每一个骨头,得到骨头的变换矩阵。
3)对于每个骨头的变换矩阵,用顶点的权值乘以这个变换矩阵然后把这个结果应用到顶点的组合变换矩阵。
4)为每个连接的骨头重复第三步,然后为每个顶点通过第四步重复第二步。当你完成以上步骤,把组合变换矩阵应用到具体的被迭代的顶点(从第一步)。
怎样精确的获得顶点的权值?可以利用ID3DXSkinInfo接口的GetBoneVertexInfluence方法得到这些权值。顶点的权值一般储存在Mesh数据对象的末端。执行完以上步骤后,剩下的仅仅是渲染了。结合前面介绍的计时动画技术,可以把关键帧时间和变换矩阵储存在AnimationSet模版中,利用一些变量不断的修改和更新骨骼层级便能创造出各种动画效果。
3 增加场景数据
&&& 在游戏中,仅仅有活灵活现的角色动画是远远不够的。因为你需要让它在具体的场景中尽情地表演,这就需要场景数据。场景数据中最具代表性的就是角色模型在游戏世界中的位置,包围球半径。显然,还有很多其它数据,这些数据都是因场合而异的。正如前面所说,X文件可以用于储存任何数据,包括场景数据。但是用X文件储存场景信息会碰到很多麻烦。比如你很难找到能够把模型转化成附有场景信息的X文件的建模工具。当然,你可以自己开发建模工具的插件。下面我介绍一个简单有效的方式得到场景数据。
&&& 很多建模软件都支持导出XML文件。在3DSMAX中,就有这样的插件。完全可以在XML文件中储存场景信息。这样,就需要一个XML文件的读取函数。虽然XML文件较为复杂,但是利用一些帮助库,可以很方便的写出XML文件的读取函数。Tinyxml 就是一个小巧稳定的XML帮助库,在互联网上可以找到很多关于它的信息。我们的目的就是把XML文件中的场景数据根据需要加载到自己定义的数据结构中。
&&& 假设定义这样一个简单的数据结构:
struct SMeshSceneInfo& //Mesh场景信息,为了访问的便利,不使用链表动态分配内存
&&& SMeshSceneInfo()
&&&&&& int ID=0;
&&&&&& D3DXVECTOR3 position(0.0f,0.0f,0.0f);
&&&&&& string&&&&&&&&&& //Mesh的名字
&&&&&& int&&&&&&&&& ID;&&&&&&& //MeshID
&&& D3DXVECTOR3&& //Mesh世界坐标
&&& 接着利用帮助库的函数,在程序的初始化阶段加载场景数据到这个数据结构中。之后在程序里就可以利用这些数据进行相关的操作。
4 简介渐变动画
&&& 回到20世纪90年代早期,一种革命性的计算机图形动画技术称之为morphing,就是渐变。它一举成功,成为主流技术,并延用至今。游戏中的渐变技术,最好的例子可能就是ID SOFT的雷神之锤(Quake)。所有这些角色的动画序列由一系列的渐变网格模型构成,一个模型缓慢的改变形状变成第2个模型,第2个模型再改变形状匹配到第3个网络模型上,以此类推。
&&& 其实渐变动画的原理很简单,就是差值两个关键帧模型的顶点。第一个关键帧模型叫做源网格模型,第二个关键帧叫做目标网格模型。当从第二个关键帧向第三个关键帧变化时,第二个关键帧网格又变成源网格模型,第三个关键帧变成目标网格模型。
下图显示了随着时间的变化,各网格顶点的变化:
&&& 每个顶点都共享源网格模型和目标网格模型中相同的引索。在这里,顶点的次序尤为重要,如果次序错误,将产生奇怪的动画效果。
&&& 相比骨骼蒙皮动画,渐变动画的逻辑简单许多,如果掌握了前者,很容易就能实现各种基于渐变动画的效果。
&&& 游戏的世界正因为各种动画技术的灵活运用才显得绚丽多彩,有趣迷人。在不久的未来,更棒的动画技术将会普及。比如,在游戏中你是一个身怀绝技的武士,当对手跃起,咆哮着从上方挥刀砍来,你不能简单地按游戏手柄的&挡格&键,而是真正的手握战刀,看准时机把对方的攻击化险为夷。各个动作的计算准确无误,如果你愿意的话,可以如实记录你的行为,立即产生出相同的动画效果。或者你是一个高尔夫球的爱好者,并不是任何地方都有打高尔夫球的场地。没有问题,你同样可以在家里尽情地挥棒击球,享受高夫球的乐趣。相信你可以想象出更多有趣的事情。这不仅仅是想象,而是不久就能体会到的快乐。
相关参考资料:Introduction to 3D Game Programming with DirectX 9.0&& by Frank Luna
&&&&&&&&&&&&& ISBN:1-& Wordware Publishing &
&&&&&&&&&&&&&
&&&&&&&&&&&&& Advanced Animation with DirectX&&&&&&&&&&&&&&&&&&&& by Jim Adams
&&&&&&&&&&&&& Copyright & 2003 Premier Press, a division of Course Technology.
&&&&&&&&&&&&&
&&&&&&&&&&&&& Advanced 3D Game Programming with DirectX 9.0&&&&&& by Peter Walsh
&&&&&&&&&&&&& ISBN:1- (pbk.)& Copyright & 2003 Wordware Publishing, Inc.
&&&&&&&&&&&&&
&&&&&&&&&&&&& DirectX 9.0 Programmer's Reference&&&&&&&&&&&&&&&&&& by Microsoft
&&&&&&&&&&&&& 2005 Microsoft Corporation. All rights reserved.
&&&&&&&&&&&&& 硬件支持下骨骼蒙皮动画的实现&&&&&&&&&&&&&&&&&&&& by Octane3d
本文来自CSDN博客,转载请标明出处:
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:6239181次
积分:74911
积分:74911
排名:第17名
原创:71篇
转载:4303篇
评论:844条
声明:早期转载的文章未标明转载敬请原谅,以后将陆续改过来,向原创者致敬!
有问题可留言
痞子龙3D编程
QQ技术交流群:
(3)(6)(10)(6)(19)(17)(17)(8)(5)(8)(14)(13)(3)(44)(42)(46)(40)(123)(114)(128)(159)(168)(40)(45)(43)(38)(5)(6)(7)(2)(3)(7)(24)(5)(5)(16)(17)(16)(66)(7)(55)(2)(37)(16)(1)(10)(6)(37)(5)(31)(18)(31)(128)(333)(203)(256)(59)(78)(57)(16)(39)(10)(27)(16)(8)(26)(32)(53)(56)(45)(142)(228)(6)(10)(6)(9)(6)(9)(22)(25)(18)(83)(208)(442)(111)(32)(1)}

我要回帖

更多关于 好心分手歌词 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信