前面我们分析了静态模型OBJ格式,桢动画模型MD2,这篇主要分析骨骼动画MD5的一些概念并且实现。

  混合桢动画有计算简单,容易实现等优点,但是在需要比较细致的效果时,则需要更多的关键桢,每桢都添加相同的顶点,如果模型再细分一些,则比较恐怖了。在这基础上,则发展出了骨骼动画模型,原理说起来很简单,比如我们人类,做的各种动作具体都是由几个关节点来控制,比如你抬腿,你只把你大腿的骨骼调动起来,而大腿的肌肉跟着骨骼向上。由些我们只需要保存每桢的骨骼变动,然后再上面蒙上表皮。因此大量简单了顶点存储,并且,我们能方便的对骨骼实时改动就能添加不同的动画,但是因为骨骼的改变都是针对父骨骼来的,而蒙皮操作又是针对骷髅节点来做的,这些操作需要大量的运算。

  下面我们来解析MD5骨骼模型中,一些基本的概念与实现,在MD5,除去纹理图片,有二个比较主要的文件,一个是后缀为md5mesh的文件,一个是后缀为md5anim的文件,二个文件如他们的后缀名所表达的意思一样,前者和OBJ模型里的描述比较类似,主要包含每部分的顶点,面,纹理组成,不同于OBJ模型的的,这些元素是变化的,因此在OBJ模型有一些新的元素,如顶点不是单独的顶点,而是由一个或多个权重点构成,每个权重点关联着对应着骨骼节点,这样骨骼节点的改变能引起权重点的改变,而权重点的改变又引起了顶点的改变,至于为什么要用到权重点来连接骨骼和顶点,而不是直接用骷髅和顶点关联,首先拿我们来说,我们身上有些位置并不是只和一个骨骼节点有关,更多是和多个节点有关,这样能让动画更真实,也避免在关节点产生重合和断裂的现象。

  首先我们来解析md5mesh文件里的信息,在这个文件里,主要有二大元素,一个是骨骼节点信息,一个是多个部位蒙皮信息,下面我简化了一个md5mesh,实际肯定不可能这样,主要是用来说明各节点用的。

  

  在文件中我都加了注释,简单来说,第一行是版本信息,下面写的解析也是针对这个10的版本,然后是命令行,骨骼节点数,蒙皮组件数,然后是骨骼节点的具体信息,在这里包含每个骨骼的父索引,顶点位置,四元数(包含旋转信息).在这里特别说下,这个骨骼节点的顺序暗含他们自己的索引,还有特别一点,在md5mesh文件中,骨骼的顶点位置与旋转信息是针对模型空间的,后面我们会看到在md5anim也有骨骼节点下的顶点位置与旋转信息,但是那是针对父骨骼节点坐标来的。然后就是蒙皮各个部分的详细信息,包含纹理坐标位置,顶点数,面的信息,权重信息,如前面所说,一个面包含3个顶点,每个顶点包含多个权重,每个权重关联一个或多个骨骼信息。下面根据上面各个部位来定义我们代码里各个类:

 type ArrayList<'T> = System.Collections.Generic.List<'T>
let filtLine (line:string) = not (String.IsNullOrEmpty(line)) && line <> "(" && line <> ")"
let getLineData (line:string) = line.Split(' ','\t','\"') |> Array.filter filtLine
let getFloat str = snd (System.Single.TryParse(str))
let getInt str = snd (System.Int32.TryParse(str))
let getw x y z = 1.0f - x*x - y*y - z*z |> fun p -> if p < .f then .f else float32 (-Math.Sqrt(float p)) //--------mesh里用的结构
type Md5Joint() =
member val Name = "" with get,set
member val Index = - with get,set
//位移
member val Position = Vector3.Zero with get,set
//旋转
member val Quat = Quaternion.Identity with get,set
member val ParentIndex = - with get,set
member this.SetValue(line:string,ind:int) =
let ls = getLineData line// line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p)))//&& p <> @"\t" && p.Length > 0)
let pos,quat =
let gf str = snd (System.Single.TryParse(str))
let x,y,z,a,b,c = (gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[])
let d = getw a b c
Vector3(x,y,z),Quaternion(a,b,c,d)
this.Name <- ls.[]
this.ParentIndex <- snd (System.Int32.TryParse(ls.[]))
this.Position <- pos
this.Quat <- quat
this //Vert包含纹理坐标,以及关联的权重,根据权重求顶点
type Md5Vert() =
member val Index = - with get,set
member val Texcoord = Vector2.Zero with get,set
member val WeightStart = with get,set
member val WeightCount = with get,set
//经过权重求顶点实际位置
member val Position = Vector3.Zero with get,set
member val Normal = Vector3.Zero with get,set
member this.SetValue(line:string) =
let ls =getLineData line// line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p)))
let gf str = snd (System.Single.TryParse(str))
this.Index <- snd (System.Int32.TryParse(ls.[]))
this.Texcoord <- Vector2(gf ls.[],gf ls.[])
this.WeightStart <- snd (System.Int32.TryParse(ls.[]))
this.WeightCount <- snd (System.Int32.TryParse(ls.[]))
this
member this.DataArray with get() =[| this.Texcoord.X;this.Texcoord.Y;this.Position.X;this.Position.Y;this.Position.Z|] //三角形(包含Vert的索引)
type Md5Tri() =
member val Index = - with get,set
member val VertorIndexs = Array.create with get,set
member this.SetValue(line:string) =
let ls = getLineData line // line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p)))
let gi str = snd (System.Int32.TryParse(str))
this.Index <- gi ls.[]
this.VertorIndexs <- [|gi ls.[];gi ls.[];gi ls.[]|]
this //权重,(权重顶点用于计算Md5Vert,权重的JointIndex用于得到对应joint的四元数)
type Md5Weight() =
member val Index = - with get,set
member val JointIndex = - with get,set
member val Bias = .f with get,set
member val Position = Vector3.Zero with get,set
member this.SetValue(line:string) =
let ls = line.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p)))
let gf str = snd (System.Single.TryParse(str))
this.Index <- snd (System.Int32.TryParse(ls.[]))
this.JointIndex <- snd (System.Int32.TryParse(ls.[]))
this.Bias <- gf ls.[]
this.Position <- Vector3(gf ls.[],gf ls.[],gf ls.[])
this type Md5Mesh() =
let mutable vbo,ebo = ,
member val TexID = with get,set
member val ShaderPath = "" with get,set
member val Verts = ArrayList<Md5Vert>() with get,set
member val Faces = ArrayList<Md5Tri>() with get,set
member val Weights = ArrayList<Md5Weight>() with get,set
member this.ElementCount with get() = this.Faces.Count *

蒙皮节点描述类

  这里的类与文件里各个描述部分差不多都是一一对应,很好理解,因元组在F#编译器级别的默认支持,使我们不用想尽办法组织结构,让结构和原始文件保持一致就行,然后要用到的时候因函数式操作相关便利性,很少的代码就能拿到需要组合的数据。

  在下面,我们具体处理如何加载md5mesh文件。

         let file = new StreamReader(fileName)
while not file.EndOfStream do
let str = file.ReadLine()
match str with
| StartsWith "joints" true ->
let mutable isJoint = true
while isJoint do
let joint = file.ReadLine()
isJoint <- not (joint.Contains("}"))
if isJoint then this.Joints.Add(Md5Joint().SetValue(joint,this.Joints.Count))
| StartsWith "mesh" true ->
let mutable isMesh = true
let md5mesh = Md5Mesh()
this.Meshs.Add(md5mesh)
while isMesh do
let mesh = file.ReadLine()
match mesh with
| StartsWith "shader" true ->
let dict = Path.GetDirectoryName(fileName)
let fileName = (getLineData mesh).[]// mesh.Split(' ','\t') |> Array.filter (fun p -> not (String.IsNullOrEmpty(p))) |> fun p -> p.[1].Trim('\"')
md5mesh.ShaderPath <- Path.Combine(dict,fileName)
| StartsWith "vert" true ->
md5mesh.Verts.Add(Md5Vert().SetValue(mesh))
| StartsWith "tri" true ->
md5mesh.Faces.Add(Md5Tri().SetValue(mesh))
| StartsWith "weight" true ->
md5mesh.Weights.Add(Md5Weight().SetValue(mesh))
| StartsWith "}" true ->
isMesh <- false
| _ -> printfn "%s" ("---------"+str)
| _ -> printfn "%s" str
file.Close()

读取蒙皮文件信息

  在这里,差不多就把蒙皮文件里的所有信息处理完毕。其实如果只是md5mesh,他就相当于一个复杂了些,包含了权重的OBJ模型,组织方式都大同小异,不信请看下面。我们记的在md5mesh前面骨骼也包含了顶点位置与四元数信息,根据这个,可以求得默认的权重点具体位置,然后就能得到顶点的具体位置,然后得到面,然后绘制,下面这段代码可以在没有md5anim文件里,绘制一个静态的,相当于OBJ模型一样功能的模型。  

 //先求得顶点的实际数据
this.Meshs.ForEach(fun mesh ->
mesh.Verts.ForEach(fun vert ->
for i in [vert.WeightStart .. vert.WeightStart + vert.WeightCount - ] do
let weigth = mesh.Weights.[i]
let joint = this.Joints.[weigth.JointIndex]
vert.Position <- vert.Position + (joint.Position + Vector3.Transform(weigth.Position,joint.Quat)) * weigth.Bias
)
)
this.Meshs.ForEach(fun mesh -> mesh.CreateVBO())

根据蒙皮文件绘制模型

  这段代码比较简单,就是上面所说,求面中的顶点,顶点根据权重求,权重根据骨骼当前状态来得到,还是和上面一样说明下,md5mesh里的骨骼节点是模型坐标系下的,所以骨骼节点不需要做转化。

  这里说下四元数,在3D中,我们表示旋转一般有矩阵,欧拉角,四元数,平常我们所用都是矩阵与欧拉角,四元数用到复数,理解起来比较麻烦,我现在也只是记着一些四元数的特性,能实现平滑插值,点p用四元数旋转后得到点p1=ap(a的逆).四元数和矩阵一样,满足结合律,但是不满足交换律。四元数的有向量部分v(x,y,z)和一个分量w,几何意义可以描述为对于一个向量n,旋转@角,四元数就是[w=cos(@/2),sin(@/2)*n]=[w=cos(@/2),sin(@/2)*nx,sin(@/2)*ny,sin(@/2)*nz],根据这个定义,可以推导出一些四元数的特性,如四元数的共轭和四元数代表相反的角位移,上面的p1=ap(a的逆).

  如果没有md5anim文件,MD5文件也就和OBJ文件一样,只是一个静态的模型,下面让我们来分析md5anim的相关格式。下面一样给出一个简化了的样式。

  各信息我给出了基本标注,比较重要的每秒多少桢,桢的具体信息,这个顺序与前面md5mesh是对应的,父索引也是一样的,不同的是,后面二个整数,一个表示应该读frame的那些数据,一个表示读的位置的起点。给出对应的代码格式。

 type Md5JointInfo() =
member val Name = "" with get,set
member val Index = - with get,set
member val ParentIndex = - with get,set
member val Flags = with get,set
member val StartIndex = with get,set
member this.SetValue(line:string,ind:int) =
let ls = getLineData line
this.Name <- ls.[]
this.ParentIndex <- getInt ls.[]
this.Index <- ind
this.Flags <- getInt ls.[]
this.StartIndex <- getInt ls.[]
this type Md5BaseFrame() =
member val Index = - with get,set
//位移
member val Positions = ArrayList<Vector3>() with get,set
//旋转
member val Quats = ArrayList<Quaternion>() with get,set
member this.SetValue(line:string) =
let ls = getLineData line
let pos,quat =
let gf str = snd (System.Single.TryParse(str))
let x,y,z,a,b,c = (gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[]),(gf ls.[])
let d = getw a b c
Vector3(x,y,z),Quaternion(a,b,c,d)
this.Positions.Add(pos)
this.Quats.Add(quat) type Md5Frame() =
member val Index = - with get,set
member val Points = ArrayList<float32>() with get,set
member this.SetValue(line:string) =
let ls = getLineData line
let ds = ls |> Array.map (fun p -> getFloat p)
this.Points.AddRange(ds) //桢动画计算得出如下内容
type Md5SkeletonJoin() =
member val ParentIndex = - with get,set
//位移
member val Position = Vector3.Zero with get,set
//旋转
member val Quat = Quaternion.Identity with get,set
type Md5FrameSkeleton = ArrayList<Md5SkeletonJoin>

MD5动画格式

  分别定义了,Md5JointInfo,Md5BaseFrame,Md5Frame,大家可以看出多了Md5SkeletonJoin与Md5FrameSkeleton,没有与文件里的信息对应,这里就是要大家前面老注意的一个地方,在md5mesh文件,给的骨骼节点坐标已经是模型坐标系下的,而md5anim给出的骨骼节点坐标只是针对父骨骼节点里的,Md5SkeletonJoin与Md5FrameSkeleton就是Md5Frame根据父骨骼节点求出的在模型坐标系下的坐标。

  下面首先是加载md5anim信息的代码:

             let path = Path.Combine(Path.GetDirectoryName(fileName),animName.Value)
let animFile = new StreamReader(path)
while not animFile.EndOfStream do
let str = animFile.ReadLine()
match str with
| StartsWith "frameRate" true ->
this.Animation.FrameRate <- getFloat (getLineData str).[]
| StartsWith "hierarchy" true ->
let mutable isJoinHierarchy = true
while isJoinHierarchy do
let joint = animFile.ReadLine()
isJoinHierarchy <- not (joint.Contains("}"))
if isJoinHierarchy then this.Animation.JointInfos.Add(Md5JointInfo().SetValue(joint,this.Animation.JointInfos.Count))
| StartsWith "bounds" true ->
let mutable isbound = true
while isbound do
let bound = animFile.ReadLine()
isbound <- not (bound.Contains("}"))
if isbound then
let data = getLineData bound
let a,b,c,x,y,z = getFloat data.[],getFloat data.[],getFloat data.[],getFloat data.[],getFloat data.[],getFloat data.[]
this.Animation.Bounds.Add(Vector3(a,b,c),Vector3(x,y,z))
| StartsWith "baseframe" true ->
let mutable isFrame = true
let mf = this.Animation.BaseFrame
while isFrame do
let frameLine = animFile.ReadLine()
isFrame <- not (frameLine.Contains("}"))
if isFrame then mf.SetValue(frameLine)
| StartsWith "frame" true ->
let mutable isFrame = true
let mf = Md5Frame()
mf.Index <- getInt (getLineData str).[]
this.Animation.Frames.Add(mf)
while isFrame do
let frameLine = animFile.ReadLine()
isFrame <- not (frameLine.Contains("}"))
if isFrame then mf.SetValue(frameLine)
| _ -> printfn "%s" str
animFile.Close()
//把骨骼动画中,各节点由父骨骼节点坐标转化成模型坐标
this.Animation.CreateFrameSkeleton()
//生成纹理
this.Meshs.ForEach(fun mesh -> if mesh.TexID = && File.Exists mesh.ShaderPath then mesh.TexID <- TexTure.Load(mesh.ShaderPath))

加载MD5动画模型

  这部分代码也是一些IO操作,把读到的信息都放入Md5Animation里去,这个类主要做二件事,一是得到正确的Md5SkeletonJoin与Md5FrameSkeleton,就是得到Md5Frame根据父骨骼节点求出的在模型坐标系下的坐标。然后一些,就是根据当前时间,当前桢率得到正确的插值,这部分和MD2插值差不多。请看主要代码:

 type Md5Animation() =
let mutable currentTime = .f
member val FrameRate = .f with get,set
//JointInfos集合对象的索引就是本身在文件中的位置,他们本身的顺序就是正序加1
member val JointInfos = ArrayList<Md5JointInfo>() with get,set
member val Bounds = ArrayList<Vector3*Vector3>() with get,set
member val Frames = ArrayList<Md5Frame>() with get,set
member val BaseFrame = Md5BaseFrame() with get,set
member val FrameSkeletonList = ArrayList<Md5FrameSkeleton>() with get,set
//二个目标,一是转化Frames里的数据成对应一桢的所有骨骼节点信息
//二是把所有骨骼节点在父骨骼坐标系中的位置转化成模型坐标系
member this.CreateFrameSkeleton() =
for frame in this.Frames do
let md5FrameSkeleton = Md5FrameSkeleton()
for jointInfo in this.JointInfos do
//skeleton的顺序因为JointInfos的特殊性,也是正序加1
let skeleton = Md5SkeletonJoin()
md5FrameSkeleton.Add(skeleton)
skeleton.ParentIndex <- jointInfo.ParentIndex
let position = this.BaseFrame.Positions.[jointInfo.Index]
let quat = this.BaseFrame.Quats.[jointInfo.Index]
let setFlags index =
let flag = int (Math.Pow(float ,float index))
if jointInfo.Flags &&& flag = flag then frame.Points.[jointInfo.StartIndex + index]
else
match index with
| -> position.X
| -> position.Y
| -> position.Z
| -> quat.X
| -> quat.Y
| -> quat.Z
| _ -> quat.W
let x,y,z,a,b,c = setFlags ,setFlags ,setFlags ,setFlags ,setFlags ,setFlags
let w = getw a b c
//currentPos,currentQuat都是针对父骨骼来的坐标,要转化得到模型坐标
let currentPos,currentQuat=Vector3(x,y,z),Quaternion(a,b,c,w)
skeleton.Position <- currentPos
skeleton.Quat <- currentQuat
if skeleton.ParentIndex >= then
let parentSkeleton = md5FrameSkeleton.[skeleton.ParentIndex]
//先得到currentPos经过父骨骼四元数旋转后的值
let pos = Vector3.Transform(currentPos,parentSkeleton.Quat)
//模型坐标示下的点
skeleton.Position <- parentSkeleton.Position + pos
//模型坐标系下的四元数
skeleton.Quat <- Quaternion.Normalize(parentSkeleton.Quat * skeleton.Quat)
this.FrameSkeletonList.Add(md5FrameSkeleton)
member this.CurrentTime
with get() =
if currentTime > float32 this.Frames.Count/this.FrameRate then currentTime <- .f
currentTime
and set value = currentTime <- value
member this.GetCurrentFrameSkeleton() =
//得到当前的时间所在动画循环的位置
let current = this.CurrentTime * this.FrameRate
//得到所在位置的当前桢索引,与运动到下一桢的位置
let currentFrame,currentStep= int (Math.Floor(float current)),current - float32 (Math.Floor(float current))
//得到下一桢索引,到桢尾就从头开始
let nextFrame = if currentFrame < this.Frames.Count - then currentFrame + else
//得到当前桢,下一桢具体信息
let currentSkeleton,nexSkeleton = this.FrameSkeletonList.[currentFrame],this.FrameSkeletonList.[nextFrame]
let joints = ArrayList<Md5Joint>()
for i in [| .. this.JointInfos.Count - |] do
//根据当前桢位置求得对应四元数与顶点的插值
let lerpPosition = Vector3.Lerp(currentSkeleton.[i].Position,nexSkeleton.[i].Position,currentStep)
let slerpQuat = Quaternion.Slerp(currentSkeleton.[i].Quat,nexSkeleton.[i].Quat,currentStep)
joints.Add(Md5Joint(Index = i,Position = lerpPosition,Quat = slerpQuat))
joints

MD5动画计算模型坐标下骨骼节点与线性插值

  关键部分我都写了注释,应该容易看明白,如上面所说,二件事,一是CreateFrameSkeleton,这个首先根据flag与startIndex读取文件,然后把在父骨骼坐标系中的点转化成模型坐标系下的点。二是GetCurrentFrameSkeleton,分别得到所在时间的当前桢与下一桢,然后根据在这桢之间的位置插值得到各骨骼节点正确的坐标。渲染部分在这里,考虑到因为一个MD5模型本来包含几部分Mesh,然后每部分Mesh又包含各桢的情况,再想用MD2中关键桢顶点信息做VBO不现实,故直接用VA来输出渲染。  

 type Md5Model(fileName:string,?animName:string) =
member this.Render()=
//生成骨骼节点的信息
let joints = this.Animation.GetCurrentFrameSkeleton()
//根据骨骼节点生成顶点.也就是蒙皮
this.Meshs.ForEach(fun mesh ->
mesh.Verts.ForEach(fun vert ->
vert.Position <- Vector3.Zero
for i in [vert.WeightStart .. vert.WeightStart + vert.WeightCount - ] do
let weigth = mesh.Weights.[i]
let joint = joints.[weigth.JointIndex]
vert.Position <- vert.Position + (joint.Position + Vector3.Transform(weigth.Position,joint.Quat)) * weigth.Bias
)
)
//顶点绘制
this.Meshs.ForEach(fun mesh -> mesh.Render()) type Md5Mesh() =
member this.Render() =
let vboData = Array2D.init this.ElementCount (fun i j ->
let a,b = i/,i%
this.Verts.[this.Faces.[a].VertorIndexs.[b]].DataArray.[j]
)
GL.InterleavedArrays(InterleavedArrayFormat.T2fV3f,,vboData)
if this.TexID > then
GL.Enable(EnableCap.Texture2D)
GL.BindTexture(TextureTarget.Texture2D,this.TexID)
GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,[|..this.ElementCount - |])

输出MD5动画

  到此,整个过程就差不多了,下面给出效果图:

  

  代码:源码与执行文件 http://files.cnblogs.com/zhouxin/MD5Load.zip 其中\bin\Release\CgTest.exe为可执行文件

  其中EDSF前后左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。再发现,整个工程中,去掉OBJ,MD2模型后,加上DLL一共27M,压缩下才5M,能上传上来,前面每次都分开上传给大家造成不便了其中为了突出MD5的重点,相应的法线没有自动生成,相关方法可以看前面OBJ,MD2里的,计算过程一样。

  CPU和GPU各应该执行的操作让我的理解应该是,一次计算很久变一次应该交给CPU,而在渲染过程快速,大量执行的代码应该交给GPU来算,下一步目标,改进里面关于骨骼位置的计算,以及相应蒙皮的操作应该交给GPU,也就是放到着色器中去处理。

最新文章

  1. spring源码分析之context
  2. iOS_MJRefrash的详解以及使用
  3. elasticsearch rpm 安装
  4. Python 基礎 - 文件的操作
  5. HTML页面跳转的5种方法
  6. Python 操作 mongodb 数据库
  7. oracle表字段为汉字,依据拼音排序
  8. windows中的程序放在linux上因为字符集不同出错
  9. [转]HttpURLConnection的使用
  10. 修改ECSHOP系统红包序列号规律
  11. Completely change MACE timestamps?
  12. MySQL 常用字段类型,介绍及其建表使用方法经验分享
  13. 练习3.20 a 将中缀表达式转换为后缀表达式
  14. PHP上传文件详解
  15. Spring-mvc配置“/”路径过滤问题
  16. 讨论.NET Core 配置对GC 工作模式与内存的影响
  17. 搭建maven
  18. Java并发——线程池原理
  19. html中script标签的使用方法
  20. 使用 trash 避免 rm -rf 悲剧

热门文章

  1. 【Unity】2.6 游戏视图(Game)
  2. Codeforces Round #Pi (Div. 2)(A,B,C,D)
  3. ElasticSearch + xpack 使用
  4. Java 里如何实现线程间通信
  5. OpenCV中图像算术操作与逻辑操作
  6. POJ 1200 Crazy Search(字符串简单的hash)
  7. yum rpm 命令一运行就卡住 只有kill 掉
  8. linux命令(46):批量更改文件后缀,文件名
  9. linux上的语音识别程序
  10. 使用windowAnimations定义Activity及Dialog的进入退出效果