MD2关键桢动画3D模型加载.

时间:2023-03-09 20:15:20
MD2关键桢动画3D模型加载.

  在看Cg教程中,看到关键桢插值来表示一个动画的物体,例如一个动物拥有站着,奔跑,下跪等动画序列,美工将这些特定的姿态称为一个关键桢。为什么要用关键桢这种来表示了,这个比较容易理解,我们知道我们看的一些卡通动画,都不是每桢来画的,都是准备一些关键的过渡动画,然后,美工人员在根据每二幅之间来补充一些中间的动画,以增加精细的效果。

  MD2模型文件就是存储一些关键桢的动画模型,格式还是很简单的,对比OBJ模型来说,更容易读取,分为几个主要部分,一部分是头文件,里面对相应的数据描述在那,如多个面,多少桢,从那读顶点,读桢都有说明,头文件后就是数据存放位置了。

  我们先来看下头文件的定义,有用的部分我做了注释。

 type Md2Header =
struct
val magic: int //MD2文件标示
val version: int //MD2版本
val skinWidth: int //纹理宽度
val skinHeight: int //纹理长度
val frameSize: int //桢的大小
val numSkins: int //
val numVertices: int //多少个顶点(每桢数量相同,数据不同)
val numTexCoords: int //多少个纹理顶点(所有桢共用)
val numTriangles: int //每桢由多少个三角形组成,所有桢是一样的
val numGlCommands: int //用VBO直接放弃
val numFrames: int //多少桢
val offsetSkins: int //
val offsetTexCoords: int //从那开始读纹理数据
val offsetTriangles: int //从那开始读三角形
val offsetFrames: int //从那开始读桢数据
val offsetGlCommands:int //无用
val offsetEnd: int //可以用来检查
end

MD2 头部格式

  然后就是对MD2模型文件的读取了,对MD2整个解析,不包含着色器代码只有200行,可以说读取与绘制比较容易,需要注意的是,一个MD2模型文件中三角形也就我们要画的面是所有桢共有的,在三角形中包含当前顶点的偏移量。这样在所有桢中,三角形的顶点不一样,但是他的纹理索引与纹理是一样的,每桢要画的三角形的个数也是一样的。所以在模型中,他们可以共有纹理缓冲区与顶点索引缓冲区,而每桢要自己建立顶点缓冲区,因顶点的不同,造成法线也会变,故每桢还需要自己建立法线缓冲区,下面是主要代码。

 type Md2Frame(md2Model: Md2Model,count:int) =
let mutable points = Array2D.create .f
let mutable vbo =
member val Vectexs = Array.create count Vector3.Zero
member val Name = ""
member this.VBO with get() = vbo
member this.Faces with get() : ArrayList<int[]*int[]> = md2Model.Faces
member this.TexCoords with get():ArrayList<float32*float32> = md2Model.TexCoords
member this.ElementCount with get() = md2Model.ElementCount
member this.DataArray
with get() =
if points.Length = then this.CreateData()
points
//MD2中不变的是面的面数.面里的顶点根据桢里保存的不同而不同,而面用的纹理是用的同一数据
member this.CreateData() =
let normals = Array.create this.Vectexs.Length (.f,Vector3.Zero)
//遍历第一次,生成面法线,记录对应点的共面,共法线信息
this.Faces.ForEach(fun p ->
let vi = fst p
let p1 = this.Vectexs.[vi.[]] - this.Vectexs.[vi.[]]
let p2 = this.Vectexs.[vi.[]] - this.Vectexs.[vi.[]]
let normal = -Vector3.Cross(p1,p2)
vi |> Array.iter(fun v ->
let mutable ind,n = normals.[v]
n <- n + normal
normals.[v] <- (ind+.f,n)
)
)
//平均点的法线信息并且组装N3fV3f
points <- Array2D.init this.ElementCount (fun i j ->
//当前面,当前面的第几个点
let m,n = i/,i%
let vi = fst this.Faces.[m]
match j with
| || ->
let vn = snd normals.[vi.[n]]/fst normals.[vi.[n]]
if j = then vn.X elif j = then vn.Y else vn.Z
| || ->
let p = this.Vectexs.[vi.[n]]
if j = then p.X elif j = then p.Y else p.Z
)
member this.CreateVBO() =
if vbo = then
vbo <- GL.GenBuffers()
GL.BindBuffer(BufferTarget.ArrayBuffer,vbo)
GL.BufferData(BufferTarget.ArrayBuffer,IntPtr ( * this.ElementCount * ),this.DataArray,BufferUsageHint.StaticDraw) and Md2Model(fileName:string,?texureName:string) =
inherit ModelCommon()
let mutable vbo,ebo = ,
member val Header = Md2Header() with get,set
member val Faces = new ArrayList<int[]*int[]>()
member val TexCoords = new ArrayList<float32*float32>()
member val Frames = new ArrayList<Md2Frame>()
member val texID = with get,set
member val CurrentFrame = .f with get,set
member this.ElementCount with get() = this.Faces.Count *
member this.TotalFrames with get() = this.Frames.Count
member this.LoadModel() =
//加载纹理
if texureName.IsSome then
let dict = Path.GetDirectoryName(fileName)
this.texID <- TexTure.Load(Path.Combine(dict,texureName.Value))
//加载MD2程序
let file = new FileStream(fileName,FileMode.Open, FileAccess.Read)
let binary = new BinaryReader(file)
let size = Marshal.SizeOf(this.Header)
let mutable bytes = Array.create size 0uy
file.Read(bytes, , bytes.Length) |> ignore
let allocIntPtr = Marshal.AllocHGlobal(size)
Marshal.Copy(bytes,,allocIntPtr,size)
this.Header <- Marshal.PtrToStructure(allocIntPtr,typeof<Md2Header>) :?> Md2Header
//读取纹理数据
file.Seek(int64 this.Header.offsetTexCoords,SeekOrigin.Begin)|> ignore
let mTexCoords = Array.init this.Header.numTexCoords (fun p ->
float32 (binary.ReadInt16())/float32 this.Header.skinWidth,
float32 (binary.ReadInt16())/float32 this.Header.skinWidth
)
this.TexCoords.AddRange(mTexCoords)
//读取面数(顶点索引与纹理索引)
file.Seek(int64 this.Header.offsetTriangles,SeekOrigin.Begin)|> ignore
let mtriangles = Array.init this.Header.numTriangles (fun p ->
[|int (binary.ReadInt16());int (binary.ReadInt16());int (binary.ReadInt16())|],
[|int (binary.ReadInt16());int (binary.ReadInt16());int (binary.ReadInt16())|]
)
this.Faces.AddRange(mtriangles)
//读取所有桢
file.Seek(int64 this.Header.offsetFrames,SeekOrigin.Begin)|> ignore
let frames = Array.init this.Header.numFrames (fun p ->
let frame = Md2Frame(this,this.Header.numVertices)
let scale = Vector3(binary.ReadSingle(),binary.ReadSingle(),binary.ReadSingle())
let translate = Vector3(binary.ReadSingle(),binary.ReadSingle(),binary.ReadSingle())
let name = binary.ReadChars()
//这桢的所有点
let vectexs = Array.init this.Header.numVertices (fun t ->
let mvertex = [|binary.ReadByte();binary.ReadByte();binary.ReadByte()|]
let mlightNormalIndex = binary.ReadByte()
mvertex,mlightNormalIndex
)
//桢上的点精确化
vectexs |> Array.iteri(fun i v ->
frame.Vectexs.[i].X <- float32 (fst v).[] * scale.X + translate.X
frame.Vectexs.[i].Y <- float32 (fst v).[] * scale.Z + translate.Z
frame.Vectexs.[i].Z <- float32 (fst v).[] * -scale.Y - translate.Y
)
frame
)
this.Frames.AddRange(frames)
//生成正确的数据
binary.Close()
file.Close()
member this.FrameStep
with get() =
let currentFrame = int (Math.Floor(float this.CurrentFrame))
let step = this.CurrentFrame - float32 currentFrame
currentFrame,step
member this.CreateEBO() =
let len = this.ElementCount -
let eboData = [|..len|]
ebo <- GL.GenBuffers()
GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo)
GL.BufferData(BufferTarget.ElementArrayBuffer,IntPtr ( * this.ElementCount),eboData,BufferUsageHint.StaticDraw)
member this.CreateVBO() =
let texCoords = Array2D.init this.ElementCount (fun i j ->
//当前面,当前面的第几个点
let m,n = i/,i%
let ti = snd this.Faces.[m]
let u,v = this.TexCoords.[ti.[n]]
if j = then u else v
)
vbo <- GL.GenBuffers()
GL.BindBuffer(BufferTarget.ArrayBuffer,vbo)
GL.BufferData(BufferTarget.ArrayBuffer,IntPtr ( * * this.ElementCount),texCoords,BufferUsageHint.StaticDraw)
member this.Render() =
if vbo = then this.CreateVBO()
if ebo = then this.CreateEBO()
if this.CurrentFrame >= float32 this.TotalFrames - .f then this.CurrentFrame <- .f
let currentFrame = this.Frames.[fst this.FrameStep]
let nextFrame = this.Frames.[fst this.FrameStep + ]
currentFrame.CreateVBO()
nextFrame.CreateVBO()
//当前桢的法线与顶点
GL.BindBuffer(BufferTarget.ArrayBuffer,currentFrame.VBO)
GL.InterleavedArrays(InterleavedArrayFormat.N3fV3f,,IntPtr.Zero)
//如果有纹理
if this.texID > && vbo > then
GL.BindBuffer(BufferTarget.ArrayBuffer,vbo)
GL.ClientActiveTexture(TextureUnit.Texture0)
GL.EnableClientState(ArrayCap.TextureCoordArray)
GL.TexCoordPointer(,TexCoordPointerType.Float,,IntPtr.Zero)
//下一桢的法线与顶点存放在Texture1与Texture2
GL.BindBuffer(BufferTarget.ArrayBuffer,nextFrame.VBO)
//下一桢顶点
GL.ClientActiveTexture(TextureUnit.Texture1)
GL.EnableClientState(ArrayCap.TextureCoordArray)
GL.TexCoordPointer(,TexCoordPointerType.Float,,IntPtr )
//下一桢法线
GL.ClientActiveTexture(TextureUnit.Texture2)
GL.EnableClientState(ArrayCap.TextureCoordArray)
GL.TexCoordPointer(,TexCoordPointerType.Float,,IntPtr.Zero)
//绘画
GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo)
GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,IntPtr.Zero)
//一定要按顺序执行这几行,不行,会影响后面的代码
GL.ClientActiveTexture(TextureUnit.Texture0)
GL.DisableClientState(ArrayCap.TextureCoordArray)
GL.ClientActiveTexture(TextureUnit.Texture1)
GL.DisableClientState(ArrayCap.TextureCoordArray)
GL.ClientActiveTexture(TextureUnit.Texture2)
GL.DisableClientState(ArrayCap.TextureCoordArray)

MD2 读取模型。

  一些部分我做了注释,相信看懂不难。这段代码有些长,因为读取与存取缓冲区,绘画全在这里了,介绍一下主要方法实现,为了免去桢与模型中的数据交换,故让他们互相引用,其中F#需要二个类用and来连接,Md2Model的方法LoadModel主要加载纹理,然后根据头文件里的各部分偏移量加载纹理坐标信息,加载三角形面数,加载桢数据,需要注意的量,纹理读取出的是当前像素位置,意思给opengl需要除以对应的长宽,而桢里的数据因为MD2模型生成工具的Z是向上的,Y是从人向屏幕的方向,而Opengl中Z是屏幕向人的方向,Y才是向上的,帮我们需要仔细对应。

  如前面所面,模型自己建立了纹理数组的缓冲区以及顶点索引缓冲区,在Md2Model中用vbo,ebo表示,而在桢里,需要自己建立桢自己的顶点与法线缓冲区,法线生成方法和上遍中OBJ模型中法线生成是一样的,定义一个和顶点一样长的数据,以顶点的下标来表示顶点的法线。

  建立了各个缓冲区,我们需要来画了,根据前面对关键桢的介绍,我想我们需要当前桢与下一桢的数据,在这里面,我们定义一个不断向前走的CurrentFrame,他在等于2.3时,我们知道,他在第二桢与第三桢之间,靠近第二桢多点。在Md2Model里的Renader有具体实现,对当前桢,我们以正常的方式传入,顶点,法线以OpenGL的方式来,但是下一桢的数据如何传了,在这里和上遍中OBJ传入切线的方法比较相似,我们用当前第几份纹理来存取,不用着色器可不容易取来当正确数据用了,分别设点当前纹理,然后存入对应下一桢的顶点与法线到对应的纹理坐标中,这里首先要注意,顶点与法线放在一个数组里,所以设定的时候要注意正确的偏移量,最后注意要执行下面的关闭纹理代码,不然会影响当前与后面执行过程。

  数据传入OpenGL后,我们需要在顶点着色器中执行插值过程,使之看起来连续,一般我们采用线性插值方式,使用的是Cg着色器语言,后面如果没特别指定,默认都是Cg着色器语言,相关如果启用Cg环境,请看上篇文章。  

 void v_main(float3 positionA : POSITION,
float3 normalA : NORMAL,
float2 texCoord : TEXCOORD0,
float3 positionB : TEXCOORD1,
float3 normalB : TEXCOORD2,
out float4 oPosition : POSITION,
out float3 objectPos : TEXCOORD0,
out float3 oNormal : TEXCOORD1,
out float2 oTexCoord : TEXCOORD2,
uniform float framstep,
uniform float4x4 mvp)
{
float3 position = lerp(positionA, positionB,framstep);//positionA;
oPosition = mul(mvp,float4(position,1.0));
oNormal = lerp(normalA, normalB,framstep);//normalA;
oTexCoord = texCoord;
objectPos = position.xyz;
}

顶点着色器

  整个过程很简单,对当前桢与下一桢做线性插值,传入的不带前缀的参数中,对应的后缀指向当前Opengl传入的数据,如POSITION是当前桢的顶点,Normal是当前桢的法线,而TEXCOORD1与TEXCOORD2分别指定下一桢的顶点与法线。带Out前缀的,除了POSITION后缀有意义,别的后缀都只是用来与片断着色器对应的,没有具体的意义。

  片断着色器和上篇中的一样,就不贴出来了,下面看下效果图。

MD2关键桢动画3D模型加载.

  主要代码 引用DLL Md2模型文件 和前面一样,其中EDSF前后左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。

  大家组织好对应目录应该就可以看到效果了。

PS 2013/12/20 16:20.

  在上面的把数据从OpenGL传入着色器中时,模访的是Cg基础教程16课,但是总感觉别扭,把法线顶点分别放入纹理这种方式,就和前面把切线放入本来颜色位置一样,感觉不爽,虽然功能是实现了,但是代码总感觉阅读时容易出乱子,后查找得这个API(glvertexattribpointer),在GLSL里,着色器根据传入的attribut来对每个顶点附加数据,glsl里有的,没道理cg里没有,查找在http://3dgep.com/?p=2665中如下:

Cg defines the following default semantics and the default generic attribute ID’s that are bound to the semantic.

BINDING SEMANTICS NAME CORRESPONDING DATA
POSITION, ATTR0 Input Vertex, Generic Attribute 0
BLENDWEIGHT, ATTR1 Input vertex weight, Generic Attribute 1
NORMAL, ATTR2 Input normal, Generic Attribute 2
DIFFUSE, COLOR0, ATTR3 Input primary color, Generic Attribute 3
SPECULAR, COLOR1, ATTR4 Input secondary color, Generic Attribute 4
TESSFACTOR, FOGCOORD, ATTR5 Input fog coordinate, Generic Attribute 5
PSIZE, ATTR6 Input point size, Generic Attribute 6
BLENDINDICES, ATTR7 Generic Attribute 7
TEXCOORD0-TEXCOORD7, ATTR8-ATTR15 Input texture coordinates (texcoord0-texcoord7), Generic Attributes 8-15
TANGENT, ATTR14 Generic Attribute 14
BINORMAL, ATTR15 Generic Attribute 15

Don’t worry if this concept of semantics doesn’t make sense yet. I will go into more detail about semantics when I show how we send the vertex data to the shader program. I will just define a few macros that are used to refer to these predefined generic attributes.

  根据上面描述,把原来里面绘制二桢数据传值部分改为:

     member this.Render() =
if vbo = then this.CreateVBO()
if ebo = then this.CreateEBO()
if this.CurrentFrame >= float32 this.TotalFrames - .f then this.CurrentFrame <- .f
let currentFrame = this.Frames.[fst this.FrameStep]
let nextFrame = this.Frames.[fst this.FrameStep + ]
currentFrame.CreateVBO()
nextFrame.CreateVBO()
//当前桢的法线与顶点
GL.BindBuffer(BufferTarget.ArrayBuffer,currentFrame.VBO)
GL.VertexAttribPointer(,,VertexAttribPointerType.Float,false,,IntPtr )
GL.EnableVertexAttribArray()
GL.VertexAttribPointer(,,VertexAttribPointerType.Float,false,,IntPtr.Zero)
GL.EnableVertexAttribArray()
//如果有纹理
if this.texID > && vbo > then
GL.BindBuffer(BufferTarget.ArrayBuffer,vbo)
GL.VertexAttribPointer(,,VertexAttribPointerType.Float,false,,IntPtr.Zero)
GL.EnableVertexAttribArray()
//下一桢的法线与顶点存放在Texture1与Texture2
GL.BindBuffer(BufferTarget.ArrayBuffer,nextFrame.VBO)
GL.VertexAttribPointer(,,VertexAttribPointerType.Float,false,,IntPtr )
GL.EnableVertexAttribArray()
GL.VertexAttribPointer(,,VertexAttribPointerType.Float,false,,IntPtr.Zero)
GL.EnableVertexAttribArray()
//绘画
GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo)
GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,IntPtr.Zero)

新版 绘画动画

  着色器部分改为:

 void v_main(float3 positionA : ATTR0,
float3 normalA : ATTR3,
float2 texCoord : ATTR8,
float3 positionB : ATTR9,
float3 normalB : ATTR10,
out float4 oPosition : POSITION,
out float3 objectPos : TEXCOORD0,
out float3 oNormal : TEXCOORD1,
out float2 oTexCoord : TEXCOORD2,
uniform float framstep,
uniform float4x4 mvp)
{
float3 position = lerp(positionA, positionB,framstep);//positionA;
oPosition = mul(mvp,float4(position,1.0));
oNormal = lerp(normalA, normalB,framstep);//normalA;
oTexCoord = texCoord;
objectPos = position.xyz;
}

新版 着色器

  可以看到,完美运行,这部分附件就不放了,大家直接复制到原来的代码上就好了,其中,代码里的glvertexattribpointer给的序号与Opengl脱离顶点,法线等对应关系上,上面写的好像0对应顶点一样,实际我的代码开始也是根据对应关系来写的,但是根据实际测试,1放顶点,只要着色器ATTR1对应放顶点也是可以的,这样想想才是对的,都已经脱离固定管线了,本来传上来的数据各式各样,系统根据定义名称来对应本就死板,给我们自己联系就好.改好后,看这代码再也没别扭的地方了.