在看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后缀有意义,别的后缀都只是用来与片断着色器对应的,没有具体的意义。

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

  主要代码 引用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对应放顶点也是可以的,这样想想才是对的,都已经脱离固定管线了,本来传上来的数据各式各样,系统根据定义名称来对应本就死板,给我们自己联系就好.改好后,看这代码再也没别扭的地方了.

  

 

  

MD2关键桢动画3D模型加载.的更多相关文章

  1. WPF 3D动态加载模型文件

    原文:WPF 3D动态加载模型文件 这篇文章需要读者对WPF 3D有一个基本了解,至少看过官方的MSDN例子. 一般来说关于WPF使用3D的例子,都是下面的流程: 1.美工用3DMAX做好模型,生成一 ...

  2. DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率

    前言 一个模型通常是由三个部分组成:网格.纹理.材质.在一开始的时候,我们是通过Geometry类来生成简单几何体的网格.但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多 ...

  3. OpenGL OBJ模型加载.

    在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的3D模型文件有OBJ,FBX,da ...

  4. 从零开始openGL——三、模型加载及鼠标交互实现

    前言 在上篇文章中,介绍了基本图形的绘制.这篇博客中将介绍模型的加载.绘制以及鼠标交互的实现. 模型加载 模型存储 要实现模型的读取.绘制,我们首先需要知道模型是如何存储在文件中的. 通常模型是由网格 ...

  5. 6_1 持久化模型与再次加载_探讨(1)_三种持久化模型加载方式以及import_meta_graph方式加载持久化模型会存在的变量管理命名混淆的问题

    笔者提交到gitHub上的问题描述地址是:https://github.com/tensorflow/tensorflow/issues/20140 三种持久化模型加载方式的一个小结论 加载持久化模型 ...

  6. cesium模型加载-加载fbx格式模型

    整体思路: fbx格式→dae格式→gltf格式→cesium加载gltf格式模型 具体方法: 1. fbx格式→dae格式 工具:3dsMax, 3dsMax插件:OpenCOLLADA, 下载地址 ...

  7. 使用lua实现Spine动画的预加载

    创建spine动画有两种方法,分别是createwithfile和createwithdata. createWithFile是通过加载动作数据马上进行创建,如果spine动画中的json文件大小超过 ...

  8. Wish3D用户必看!模型加载失败原因汇总

    上传到Wish3D的模型加载不出来,作品显示页面漆黑一片,是什么原因? 很有可能是操作过程中的小失误,不妨从以下几点检查.还是不行的请加QQ群(Wish3D交流群3):635725654,@Wish3 ...

  9. PyTorch模型加载与保存的最佳实践

    一般来说PyTorch有两种保存和读取模型参数的方法.但这篇文章我记录了一种最佳实践,可以在加载模型时避免掉一些问题. 第一种方案是保存整个模型: 1 torch.save(model_object, ...

随机推荐

  1. android 中毛玻璃效果的实现

    最近在做一款叫叽叽的App(男银懂的),其中有一个功能需要对图片处理实现毛玻璃的特效 进过一番预研,找到了3中实现方案,其中各有优缺点: 1.如果系统的api在16以上,可以使用系统提供的方法直接处理 ...

  2. Dynamic Control Flow in ML

    https://arxiv.org/abs/1805.01772 https://www.leiphone.com/news/201702/cb7cPOtzFj1pgRpk.html

  3. 关键词抽取:pagerank,textrank

    摘抄自微信公众号:AI学习与实践 TextRank,它利用图模型来提取文章中的关键词.由 Google 著名的网页排序算法 PageRank 改编而来的算法. PageRank PageRank 是一 ...

  4. 【Acm】八皇后问题

    八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题. 其解决办法和我以前发过的[算法之美—Fire Net:www.cnblogs.com/lcw/p/3159414.html]类似 题目:在8 ...

  5. scp拷贝提示its a directory 错误

    scp拷贝提示its a directory 错误 场景 使用scp的格式是 scp my_file user@ip:/home/directory 之前也一直这么用,没什么错误,莫名其妙 原因定位 ...

  6. 2. K-Means的优化

    1. K-Means原理解析 2. K-Means的优化 3. sklearn的K-Means的使用 4. K-Means和K-Means++实现 1. 前言 上一篇博文K-Means原理解析简单清晰 ...

  7. iis部署wcf服务

    win8的如下 . 打开iis新建一个应用程序MyWcfTest 检查iis中的处理程序映射,含有svc说明激活了. 然后把svc文件和webconfig放入到指定的目录上. 使用地址http://l ...

  8. JDK 5.0 注解的使用

    了解注解 在编写代码时,除了源程序以外,我们还会使用Javadoc标签对类.方法或成员变量进行注解,以便使用Javadoc工具生成和源代码配套的Javadoc文档. /** * 重写toString ...

  9. 【WPF】帐号系统中,用户注册的校验逻辑(正则表达式)

    帐号系统中,用户注册时,在向服务器发送校验请求之前,客户端会先进行用户填写内容的校验(主要靠正则表达式). 由于校验注册的逻辑在多个项目中可以重用,这类通用的代码最好记录下来. 界面内容大致如下.现在 ...

  10. <要做股市赢家:杨百万>读书笔记

    书在这里 和这里: 要注意的是,并不是政府每出台一个政策股价就要变.如果听到各种消息,市场却没有反应,就不要去做这个聪明人.有消息后应该密切关注市场反应,看成交量.价格的变化等等,再作决定.总之,关键 ...