在看Cg教程中,看到关键桢插值来表示一个动画的物体,例如一个动物拥有站着,奔跑,下跪等动画序列,美工将这些特定的姿态称为一个关键桢。为什么要用关键桢这种来表示了,这个比较容易理解,我们知道我们看的一些卡通动画,都不是每桢来画的,都是准备一些关键的过渡动画,然后,美工人员在根据每二幅之间来补充一些中间的动画,以增加精细的效果。
MD2模型文件就是存储一些关键桢的动画模型,格式还是很简单的,对比OBJ模型来说,更容易读取,分为几个主要部分,一部分是头文件,里面对相应的数据描述在那,如多个面,多少桢,从那读顶点,读桢都有说明,头文件后就是数据存放位置了。
我们先来看下头文件的定义,有用的部分我做了注释。
1 type Md2Header = 2 struct 3 val magic: int //MD2文件标示 4 val version: int //MD2版本 5 val skinWidth: int //纹理宽度 6 val skinHeight: int //纹理长度 7 val frameSize: int //桢的大小 8 val numSkins: int // 9 val numVertices: int //多少个顶点(每桢数量相同,数据不同) 10 val numTexCoords: int //多少个纹理顶点(所有桢共用) 11 val numTriangles: int //每桢由多少个三角形组成,所有桢是一样的 12 val numGlCommands: int //用VBO直接放弃 13 val numFrames: int //多少桢 14 val offsetSkins: int // 15 val offsetTexCoords: int //从那开始读纹理数据 16 val offsetTriangles: int //从那开始读三角形 17 val offsetFrames: int //从那开始读桢数据 18 val offsetGlCommands:int //无用 19 val offsetEnd: int //可以用来检查 20 end
然后就是对MD2模型文件的读取了,对MD2整个解析,不包含着色器代码只有200行,可以说读取与绘制比较容易,需要注意的是,一个MD2模型文件中三角形也就我们要画的面是所有桢共有的,在三角形中包含当前顶点的偏移量。这样在所有桢中,三角形的顶点不一样,但是他的纹理索引与纹理是一样的,每桢要画的三角形的个数也是一样的。所以在模型中,他们可以共有纹理缓冲区与顶点索引缓冲区,而每桢要自己建立顶点缓冲区,因顶点的不同,造成法线也会变,故每桢还需要自己建立法线缓冲区,下面是主要代码。
1 type Md2Frame(md2Model: Md2Model,count:int) = 2 let mutable points = Array2D.create 0 0 0.f 3 let mutable vbo = 0 4 member val Vectexs = Array.create count Vector3.Zero 5 member val Name = "" 6 member this.VBO with get() = vbo 7 member this.Faces with get() : ArrayList<int[]*int[]> = md2Model.Faces 8 member this.TexCoords with get():ArrayList<float32*float32> = md2Model.TexCoords 9 member this.ElementCount with get() = md2Model.ElementCount 10 member this.DataArray 11 with get() = 12 if points.Length = 0 then this.CreateData() 13 points 14 //MD2中不变的是面的面数.面里的顶点根据桢里保存的不同而不同,而面用的纹理是用的同一数据 15 member this.CreateData() = 16 let normals = Array.create this.Vectexs.Length (0.f,Vector3.Zero) 17 //遍历第一次,生成面法线,记录对应点的共面,共法线信息 18 this.Faces.ForEach(fun p -> 19 let vi = fst p 20 let p1 = this.Vectexs.[vi.[1]] - this.Vectexs.[vi.[0]] 21 let p2 = this.Vectexs.[vi.[2]] - this.Vectexs.[vi.[0]] 22 let normal = -Vector3.Cross(p1,p2) 23 vi |> Array.iter(fun v -> 24 let mutable ind,n = normals.[v] 25 n <- n + normal 26 normals.[v] <- (ind+1.f,n) 27 ) 28 ) 29 //平均点的法线信息并且组装N3fV3f 30 points <- Array2D.init this.ElementCount 6 (fun i j -> 31 //当前面,当前面的第几个点 32 let m,n = i/3,i%3 33 let vi = fst this.Faces.[m] 34 match j with 35 | 0|1|2 -> 36 let vn = snd normals.[vi.[n]]/fst normals.[vi.[n]] 37 if j = 0 then vn.X elif j = 1 then vn.Y else vn.Z 38 | 3|4|5 -> 39 let p = this.Vectexs.[vi.[n]] 40 if j = 3 then p.X elif j = 4 then p.Y else p.Z 41 ) 42 member this.CreateVBO() = 43 if vbo = 0 then 44 vbo <- GL.GenBuffers(1) 45 GL.BindBuffer(BufferTarget.ArrayBuffer,vbo) 46 GL.BufferData(BufferTarget.ArrayBuffer,IntPtr (4 * this.ElementCount * 6),this.DataArray,BufferUsageHint.StaticDraw) 47 48 and Md2Model(fileName:string,?texureName:string) = 49 inherit ModelCommon() 50 let mutable vbo,ebo = 0,0 51 member val Header = Md2Header() with get,set 52 member val Faces = new ArrayList<int[]*int[]>() 53 member val TexCoords = new ArrayList<float32*float32>() 54 member val Frames = new ArrayList<Md2Frame>() 55 member val texID = 0 with get,set 56 member val CurrentFrame = 0.f with get,set 57 member this.ElementCount with get() = this.Faces.Count * 3 58 member this.TotalFrames with get() = this.Frames.Count 59 member this.LoadModel() = 60 //加载纹理 61 if texureName.IsSome then 62 let dict = Path.GetDirectoryName(fileName) 63 this.texID <- TexTure.Load(Path.Combine(dict,texureName.Value)) 64 //加载MD2程序 65 let file = new FileStream(fileName,FileMode.Open, FileAccess.Read) 66 let binary = new BinaryReader(file) 67 let size = Marshal.SizeOf(this.Header) 68 let mutable bytes = Array.create size 0uy 69 file.Read(bytes, 0, bytes.Length) |> ignore 70 let allocIntPtr = Marshal.AllocHGlobal(size) 71 Marshal.Copy(bytes,0,allocIntPtr,size) 72 this.Header <- Marshal.PtrToStructure(allocIntPtr,typeof<Md2Header>) :?> Md2Header 73 //读取纹理数据 74 file.Seek(int64 this.Header.offsetTexCoords,SeekOrigin.Begin)|> ignore 75 let mTexCoords = Array.init this.Header.numTexCoords (fun p -> 76 float32 (binary.ReadInt16())/float32 this.Header.skinWidth, 77 float32 (binary.ReadInt16())/float32 this.Header.skinWidth 78 ) 79 this.TexCoords.AddRange(mTexCoords) 80 //读取面数(顶点索引与纹理索引) 81 file.Seek(int64 this.Header.offsetTriangles,SeekOrigin.Begin)|> ignore 82 let mtriangles = Array.init this.Header.numTriangles (fun p -> 83 [|int (binary.ReadInt16());int (binary.ReadInt16());int (binary.ReadInt16())|], 84 [|int (binary.ReadInt16());int (binary.ReadInt16());int (binary.ReadInt16())|] 85 ) 86 this.Faces.AddRange(mtriangles) 87 //读取所有桢 88 file.Seek(int64 this.Header.offsetFrames,SeekOrigin.Begin)|> ignore 89 let frames = Array.init this.Header.numFrames (fun p -> 90 let frame = Md2Frame(this,this.Header.numVertices) 91 let scale = Vector3(binary.ReadSingle(),binary.ReadSingle(),binary.ReadSingle()) 92 let translate = Vector3(binary.ReadSingle(),binary.ReadSingle(),binary.ReadSingle()) 93 let name = binary.ReadChars(16) 94 //这桢的所有点 95 let vectexs = Array.init this.Header.numVertices (fun t -> 96 let mvertex = [|binary.ReadByte();binary.ReadByte();binary.ReadByte()|] 97 let mlightNormalIndex = binary.ReadByte() 98 mvertex,mlightNormalIndex 99 ) 100 //桢上的点精确化 101 vectexs |> Array.iteri(fun i v -> 102 frame.Vectexs.[i].X <- float32 (fst v).[0] * scale.X + translate.X 103 frame.Vectexs.[i].Y <- float32 (fst v).[2] * scale.Z + translate.Z 104 frame.Vectexs.[i].Z <- float32 (fst v).[1] * -scale.Y - translate.Y 105 ) 106 frame 107 ) 108 this.Frames.AddRange(frames) 109 //生成正确的数据 110 binary.Close() 111 file.Close() 112 member this.FrameStep 113 with get() = 114 let currentFrame = int (Math.Floor(float this.CurrentFrame)) 115 let step = this.CurrentFrame - float32 currentFrame 116 currentFrame,step 117 member this.CreateEBO() = 118 let len = this.ElementCount - 1 119 let eboData = [|0..len|] 120 ebo <- GL.GenBuffers(1) 121 GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo) 122 GL.BufferData(BufferTarget.ElementArrayBuffer,IntPtr (4 * this.ElementCount),eboData,BufferUsageHint.StaticDraw) 123 member this.CreateVBO() = 124 let texCoords = Array2D.init this.ElementCount 2 (fun i j -> 125 //当前面,当前面的第几个点 126 let m,n = i/3,i%3 127 let ti = snd this.Faces.[m] 128 let u,v = this.TexCoords.[ti.[n]] 129 if j = 0 then u else v 130 ) 131 vbo <- GL.GenBuffers(1) 132 GL.BindBuffer(BufferTarget.ArrayBuffer,vbo) 133 GL.BufferData(BufferTarget.ArrayBuffer,IntPtr (4 * 2 * this.ElementCount),texCoords,BufferUsageHint.StaticDraw) 134 member this.Render() = 135 if vbo = 0 then this.CreateVBO() 136 if ebo = 0 then this.CreateEBO() 137 if this.CurrentFrame >= float32 this.TotalFrames - 1.f then this.CurrentFrame <- 0.f 138 let currentFrame = this.Frames.[fst this.FrameStep] 139 let nextFrame = this.Frames.[fst this.FrameStep + 1] 140 currentFrame.CreateVBO() 141 nextFrame.CreateVBO() 142 //当前桢的法线与顶点 143 GL.BindBuffer(BufferTarget.ArrayBuffer,currentFrame.VBO) 144 GL.InterleavedArrays(InterleavedArrayFormat.N3fV3f,0,IntPtr.Zero) 145 //如果有纹理 146 if this.texID > 0 && vbo > 0 then 147 GL.BindBuffer(BufferTarget.ArrayBuffer,vbo) 148 GL.ClientActiveTexture(TextureUnit.Texture0) 149 GL.EnableClientState(ArrayCap.TextureCoordArray) 150 GL.TexCoordPointer(2,TexCoordPointerType.Float,0,IntPtr.Zero) 151 //下一桢的法线与顶点存放在Texture1与Texture2 152 GL.BindBuffer(BufferTarget.ArrayBuffer,nextFrame.VBO) 153 //下一桢顶点 154 GL.ClientActiveTexture(TextureUnit.Texture1) 155 GL.EnableClientState(ArrayCap.TextureCoordArray) 156 GL.TexCoordPointer(3,TexCoordPointerType.Float,24,IntPtr 12) 157 //下一桢法线 158 GL.ClientActiveTexture(TextureUnit.Texture2) 159 GL.EnableClientState(ArrayCap.TextureCoordArray) 160 GL.TexCoordPointer(3,TexCoordPointerType.Float,24,IntPtr.Zero) 161 //绘画 162 GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo) 163 GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,IntPtr.Zero) 164 //一定要按顺序执行这几行,不行,会影响后面的代码 165 GL.ClientActiveTexture(TextureUnit.Texture0) 166 GL.DisableClientState(ArrayCap.TextureCoordArray) 167 GL.ClientActiveTexture(TextureUnit.Texture1) 168 GL.DisableClientState(ArrayCap.TextureCoordArray) 169 GL.ClientActiveTexture(TextureUnit.Texture2) 170 GL.DisableClientState(ArrayCap.TextureCoordArray)
一些部分我做了注释,相信看懂不难。这段代码有些长,因为读取与存取缓冲区,绘画全在这里了,介绍一下主要方法实现,为了免去桢与模型中的数据交换,故让他们互相引用,其中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环境,请看上篇文章。
1 void v_main(float3 positionA : POSITION, 2 float3 normalA : NORMAL, 3 float2 texCoord : TEXCOORD0, 4 float3 positionB : TEXCOORD1, 5 float3 normalB : TEXCOORD2, 6 out float4 oPosition : POSITION, 7 out float3 objectPos : TEXCOORD0, 8 out float3 oNormal : TEXCOORD1, 9 out float2 oTexCoord : TEXCOORD2, 10 uniform float framstep, 11 uniform float4x4 mvp) 12 { 13 float3 position = lerp(positionA, positionB,framstep);//positionA; 14 oPosition = mul(mvp,float4(position,1.0)); 15 oNormal = lerp(normalA, normalB,framstep);//normalA; 16 oTexCoord = texCoord; 17 objectPos = position.xyz; 18 }
整个过程很简单,对当前桢与下一桢做线性插值,传入的不带前缀的参数中,对应的后缀指向当前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.
根据上面描述,把原来里面绘制二桢数据传值部分改为:
1 member this.Render() = 2 if vbo = 0 then this.CreateVBO() 3 if ebo = 0 then this.CreateEBO() 4 if this.CurrentFrame >= float32 this.TotalFrames - 1.f then this.CurrentFrame <- 0.f 5 let currentFrame = this.Frames.[fst this.FrameStep] 6 let nextFrame = this.Frames.[fst this.FrameStep + 1] 7 currentFrame.CreateVBO() 8 nextFrame.CreateVBO() 9 //当前桢的法线与顶点 10 GL.BindBuffer(BufferTarget.ArrayBuffer,currentFrame.VBO) 11 GL.VertexAttribPointer(0,3,VertexAttribPointerType.Float,false,24,IntPtr 12) 12 GL.EnableVertexAttribArray(0) 13 GL.VertexAttribPointer(2,3,VertexAttribPointerType.Float,false,24,IntPtr.Zero) 14 GL.EnableVertexAttribArray(2) 15 //如果有纹理 16 if this.texID > 0 && vbo > 0 then 17 GL.BindBuffer(BufferTarget.ArrayBuffer,vbo) 18 GL.VertexAttribPointer(8,2,VertexAttribPointerType.Float,false,0,IntPtr.Zero) 19 GL.EnableVertexAttribArray(8) 20 //下一桢的法线与顶点存放在Texture1与Texture2 21 GL.BindBuffer(BufferTarget.ArrayBuffer,nextFrame.VBO) 22 GL.VertexAttribPointer(9,3,VertexAttribPointerType.Float,false,24,IntPtr 12) 23 GL.EnableVertexAttribArray(9) 24 GL.VertexAttribPointer(10,3,VertexAttribPointerType.Float,false,24,IntPtr.Zero) 25 GL.EnableVertexAttribArray(10) 26 //绘画 27 GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo) 28 GL.DrawElements(BeginMode.Triangles,this.ElementCount,DrawElementsType.UnsignedInt,IntPtr.Zero)
着色器部分改为:
1 void v_main(float3 positionA : ATTR0, 2 float3 normalA : ATTR3, 3 float2 texCoord : ATTR8, 4 float3 positionB : ATTR9, 5 float3 normalB : ATTR10, 6 out float4 oPosition : POSITION, 7 out float3 objectPos : TEXCOORD0, 8 out float3 oNormal : TEXCOORD1, 9 out float2 oTexCoord : TEXCOORD2, 10 uniform float framstep, 11 uniform float4x4 mvp) 12 { 13 float3 position = lerp(positionA, positionB,framstep);//positionA; 14 oPosition = mul(mvp,float4(position,1.0)); 15 oNormal = lerp(normalA, normalB,framstep);//normalA; 16 oTexCoord = texCoord; 17 objectPos = position.xyz; 18 }
可以看到,完美运行,这部分附件就不放了,大家直接复制到原来的代码上就好了,其中,代码里的glvertexattribpointer给的序号与Opengl脱离顶点,法线等对应关系上,上面写的好像0对应顶点一样,实际我的代码开始也是根据对应关系来写的,但是根据实际测试,1放顶点,只要着色器ATTR1对应放顶点也是可以的,这样想想才是对的,都已经脱离固定管线了,本来传上来的数据各式各样,系统根据定义名称来对应本就死板,给我们自己联系就好.改好后,看这代码再也没别扭的地方了.