在看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對應放頂點也是可以的,這樣想想才是對的,都已經脫離固定管線了,本來傳上來的數據各式各樣,系統根據定義名稱來對應本就死板,給我們自己聯系就好.改好后,看這代碼再也沒別扭的地方了.
