在咱們前面繪製一個屋,咱們能夠看到,須要每一個立方體一個一個的本身來推而且還要處理位置信息.代碼量大而且要時間.如今咱們經過加載模型文件的方法來生成模型文件,比較流行的3D模型文件有OBJ,FBX,dae等,其中OBJ模式只包含靜態的模型,相對FBX這種來講,比較簡單,恰好給咱們用來學習之用.html
對比咱們以前用代碼來一個一個建模型,用模型文件OBJ的不一樣就是在OBJ裏包含了咱們須要的頂點,法線,以及紋理座標以及頂點組成面索引.去掉了咱們用代碼建模最要時的過程.用模型文件咱們要作的僅僅是讀出裏面的信息,而後組織供OpenGL調用.數組
不一樣的模型文件有不一樣的信息組織格式,相對於FBX這種二進制而且沒公佈格式的文件來講,OBJ模型文本結構對於咱們來講更易讀而且容易理解,網上也有很多大神對OBJ模型中出現的文本作了詳細的解說並提供相應的加載模型方法.數據結構
OBJ模型文件的結構、導入與渲染Ⅰ OBJ模型文件的結構、導入與渲染Ⅱapp
在上面二篇文章中以及文章中的連接,有對OBJ模型比較詳細的解說以及加載,與原文章加載稍有不一樣的是,咱們解析相應數據按照OBJ模型的定義來定義結構.ide
在OBJ模型中主要分二塊,一塊是模型組成文件,包含頂點,法線,紋理座標,面,組的信息,另外一塊是模型文件所需的材質信息與對應紋理所需圖片.函數
咱們分別定義第一塊的數據結構以下:VertexAttribute,ObjFace,ObjGroup.第二塊ObjMaterialItem,ObjMaterial.其中模型定義爲ObjModel.代碼以下:學習
1 type ArrayList<'T> = System.Collections.Generic.List<'T> 2 3 type ObjMaterialItem() = 4 member val Name = "" with get,set 5 member val Ambient = [|0.f;0.f;0.f;0.f|] with get,set 6 member val Diffuse = [|0.f;0.f;0.f;0.f|] with get,set 7 member val Specular = [|0.f;0.f;0.f;0.f|] with get,set 8 member val Shiness = 0.f with get,set 9 member val DiffuseMap = "" with get,set 10 member val SpecularMap = "" with get,set 11 member val BumpMap = "" with get,set 12 member val DiffuseID = 0 with get,set 13 member val SpecularID = 0 with get,set 14 member val BumpID = 0 with get,set 15 16 type ObjMaterial() = 17 member val Name = "" with get,set 18 member val Items = new ArrayList<ObjMaterialItem>() with get,set 19 member val currentItem = new ObjMaterialItem() with get,set 20 21 type VertexAttribute() = 22 let strToInt str = 23 let (ok,f) = System.Int32.TryParse(str) 24 if ok then f else -1 25 member val Position= Vector3.Zero with get,set 26 member val Texcoord=Vector2.Zero with get,set 27 member val Normal= Vector3.Zero with get,set 28 member val PositionIndex = -1 with get,set 29 member val TexcoordIndex = -1 with get,set 30 member val NormalIndex = -1 with get,set 31 //各個值的索引信息 32 member this.SetValue(line:string) = 33 let ls = line.Split('/') 34 match ls.Length with 35 | 1 -> 36 this.PositionIndex <- strToInt ls.[0] 37 | 2 -> 38 this.PositionIndex <- strToInt ls.[0] 39 this.TexcoordIndex <- strToInt ls.[1] 40 | 3 -> 41 this.PositionIndex <- strToInt ls.[0] 42 this.NormalIndex <- strToInt ls.[2] 43 if not (ls.[1] = "" || ls.[1] = null) then 44 this.TexcoordIndex <- strToInt ls.[1] 45 | _ -> () 46 //組織格式用T2fV3f/N3fV3f/T2fN3fV3f/V3f成float32[] 47 member this.PointArray 48 with get() = 49 let mutable ps = Array.create 0 0.0f 50 if this.TexcoordIndex > 0 then ps <- Array.append ps [|this.Texcoord.X;1.0f - this.Texcoord.Y|] 51 if this.NormalIndex > 0 then ps <- Array.append ps [|this.Normal.X;this.Normal.Y;this.Normal.Z|] 52 if this.PositionIndex > 0 then ps <- Array.append ps [|this.Position.X;this.Position.Y;this.Position.Z|] 53 ps 54 55 type ObjFace() = 56 let mutable vectexs = [||] : VertexAttribute array 57 //每一個面的頂點,一個是三角形,若是是矩形,爲了兼容性,應該化爲成二個三角形. 58 member this.Vectexs 59 with get() = 60 let mutable result = vectexs.[0..] 61 if vectexs.Length = 4 then 62 let newvxs = [|vectexs.[0];vectexs.[2]|] 63 result <- Array.append result newvxs 64 result 65 //在讀取文件時,獲得當前麪包含的頂點索引信息.(此時對應頂點只有索引,沒有真實數據) 66 member this.AddVectex (line:string) = 67 let ls = line.TrimEnd(' ').Split(' ') 68 let vs = 69 ls |> Array.map(fun p -> 70 let va = new VertexAttribute() 71 va.SetValue(p) 72 va) 73 vectexs <- vs 74 member this.VertexCount with get() = this.Vectexs.Length 75 76 type ObjGroup() = 77 //獲得數組裏全部面的對應全部頂點屬性 78 let mutable vectexs = new ArrayList<VertexAttribute>() 79 let mutable points = Array2D.create 0 0 0.f 80 let mutable vbo,ebo = 0,0 81 member val Faces = new ArrayList<ObjFace>() with get,set 82 member val Mtllib = "" with get,set 83 member val Usemtl = "" with get,set 84 member val Name = "" with get,set 85 member val Material = new ObjMaterialItem() with get,set 86 member val IsHaveMaterial = false with get,set 87 member val Path = "" with get,set 88 member this.VBO with get() = vbo 89 member this.EBO with get() = ebo 90 //讀取文件,讀取當前group裏的面的信息,而且會在讀面信息時讀取到這個面全部頂點索引 91 member this.AddFace (line:string) = 92 let face = new ObjFace() 93 face.AddVectex(line) 94 this.Faces.Add(face) 95 vectexs.AddRange(face.Vectexs) 96 //組織一個規則二維數組,一維表示每面上的每一個頂點,二維表示每一個頂點是如何組織,包含法向量,紋理座標不 97 member this.DataArray 98 with get() = 99 if points.Length < 1 then 100 let length1 = vectexs.Count 101 if length1 > 0 then 102 let length2 = vectexs.[0].PointArray.Length 103 if length2 > 0 then 104 points <- Array2D.init length1 length2 (fun i j -> vectexs.[i].PointArray.[j]) 105 points 106 member this.CreateVBO() = 107 if this.ElementLength > 0 then 108 vbo <- GL.GenBuffers(1) 109 GL.BindBuffer(BufferTarget.ArrayBuffer,vbo) 110 GL.BufferData(BufferTarget.ArrayBuffer,IntPtr (4 *this.ElementLength*this.VectorLength ),this.DataArray,BufferUsageHint.StaticDraw) 111 let len = this.ElementLength - 1 112 let eboData = [|0..len|] 113 ebo <- GL.GenBuffers(1) 114 GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo) 115 GL.BufferData(BufferTarget.ElementArrayBuffer,IntPtr (4 * this.ElementLength),eboData,BufferUsageHint.StaticDraw) 116 if this.IsHaveMaterial then 117 let kdPath = Path.Combine(this.Path,this.Material.DiffuseMap) 118 if File.Exists kdPath then 119 this.Material.DiffuseID <- TexTure.Load(kdPath) 120 member this.DrawVBO() = 121 if this.VBO >0 && this.EBO >0 then 122 GL.BindBuffer(BufferTarget.ArrayBuffer,this.VBO) 123 GL.BindBuffer(BufferTarget.ElementArrayBuffer,this.EBO) 124 if this.IsHaveMaterial then 125 GL.Enable(EnableCap.Texture2D) 126 GL.BindTexture(TextureTarget.Texture2D,this.Material.DiffuseID) 127 GL.InterleavedArrays(this.InterFormat,0,IntPtr.Zero) 128 GL.DrawElements(BeginMode.Triangles,this.ElementLength,DrawElementsType.UnsignedInt,IntPtr.Zero) 129 GL.Disable(EnableCap.Texture2D) 130 //多少個頂點 131 member this.ElementLength with get() = Array2D.length1 this.DataArray 132 //頂點組織形式長度T2fV3f/N3fV3f/T2fN3fV3f/V3f 133 member this.VectorLength with get() = Array2D.length2 this.DataArray 134 //頂點組織形式 135 member this.InterFormat 136 with get()= 137 let mutable result = InterleavedArrayFormat.T2fN3fV3f 138 if this.VectorLength = 3 then result <- InterleavedArrayFormat.V3f 139 if this.VectorLength = 5 then result <- InterleavedArrayFormat.T2fV3f 140 if this.VectorLength = 6 then result <- InterleavedArrayFormat.N3fV3f 141 result 142 143 type ObjModel(fileName:string) = 144 let mutable groupName = "default" 145 let mutable groups = [] : ObjGroup list 146 let addGroup group = groups <- (group :: groups) 147 //獲得每行數組去掉標識符後的數據如 v 1.0 2.0 3.0 -> 1.0 2.0 3.0 148 let getLineValue (line:string) = 149 let fs = line.Split(' ') 150 let len = fs.Length - 1 151 if fs.Length > 1 then (fs.[1..len] |> Array.filter (fun p -> p <> null && p<> " " && p <> "")) 152 else [|line|] 153 //數組轉化成float32 154 let strToFloat str = 155 let (ok,f) = System.Single.TryParse(str) 156 if ok then f else System.Single.NaN 157 let mutable group = ObjGroup() 158 let mutable mtllib = "" 159 member val Positions = new ArrayList<Vector3>() with get,set 160 member val Normals = new ArrayList<Vector3>() with get,set 161 member val Texcoords = new ArrayList<Vector2>() with get,set 162 member val Materials = new ArrayList<ObjMaterial>() with get,set 163 member this.Path 164 with get() = System.IO.Path.GetDirectoryName(fileName) 165 member this.GetLineFloatArray (line:string) = 166 let fs = getLineValue(line) 167 fs |> Array.map (fun p -> strToFloat p) 168 member this.GetLineValue (line:string,?sep) = 169 let dsep = defaultArg sep " " 170 let fs = getLineValue(line) 171 String.concat dsep fs 172 member this.CurrentGroup 173 with get() = 174 let bExist = groups |> List.exists(fun p -> p.Name = groupName) 175 if not bExist then 176 let objGroup = new ObjGroup() 177 objGroup.Name <- groupName 178 objGroup.Mtllib <- mtllib 179 addGroup objGroup 180 group <- groups |> List.find(fun p -> p.Name = groupName) 181 group 182 member this.Groups 183 with get() = 184 groups 185 //主要有二步,首先讀取文件信息,而後把頂點,法線,紋理座標根據索引來賦值 186 member this.LoadObjModel(?bCreateVBO) = 187 let bCreate = defaultArg bCreateVBO false 188 let file = new StreamReader(fileName) 189 let mutable beforeFace = false 190 let (|StartsWith|) suffix (s:string) = s.TrimStart(' ','\t').StartsWith(suffix,StringComparison.OrdinalIgnoreCase) 191 //首先讀取文件信息,此時頂點只有索引信息. 192 while not file.EndOfStream do 193 let str = file.ReadLine() 194 match str with 195 | StartsWith "mtllib " true -> 196 mtllib <- this.GetLineValue(str) 197 //#region 讀紋理 198 let material = new ObjMaterial() 199 material.Name <- mtllib 200 let mtlFile = new StreamReader(Path.Combine(this.Path,mtllib)) 201 while not mtlFile.EndOfStream do 202 let str = mtlFile.ReadLine() 203 match str with 204 | null -> () 205 | StartsWith "newmtl " true -> 206 material.currentItem <- new ObjMaterialItem() 207 material.currentItem.Name <- this.GetLineValue(str) 208 material.Items.Add(material.currentItem) 209 | StartsWith "ka " true -> material.currentItem.Ambient <- this.GetLineFloatArray(str) 210 | StartsWith "kd " true -> material.currentItem.Diffuse <- this.GetLineFloatArray(str) 211 | StartsWith "ks " true -> material.currentItem.Specular <- this.GetLineFloatArray(str) 212 | StartsWith "map_Kd " true -> material.currentItem.DiffuseMap <- this.GetLineValue(str) 213 | StartsWith "map_Ks " true -> material.currentItem.SpecularMap <- this.GetLineValue(str) 214 | StartsWith "map_bump " true -> material.currentItem.BumpMap <- this.GetLineValue(str) 215 | StartsWith "Ns " true -> 216 let ns = this.GetLineFloatArray(str).[0] 217 material.currentItem.Shiness <- ns * 0.128f 218 | _ -> () 219 mtlFile.Close() 220 this.Materials.Add(material) 221 //#endregion 222 | null -> () 223 | StartsWith "usemtl " true -> this.CurrentGroup.Usemtl <- this.GetLineValue(str) 224 | StartsWith "g " true -> 225 groupName <- this.GetLineValue(str) 226 beforeFace <- false 227 | StartsWith "vn " true -> 228 let fs = this.GetLineFloatArray(str) 229 this.Normals.Add(Vector3(fs.[0],fs.[1],fs.[2])) 230 | StartsWith "vt " true -> 231 let fs = this.GetLineFloatArray(str) 232 this.Texcoords.Add(Vector2(fs.[0],fs.[1])) 233 | StartsWith "v " true -> 234 let fs = this.GetLineFloatArray(str) 235 this.Positions.Add(Vector3(fs.[0],fs.[1],fs.[2])) 236 | StartsWith "f " true -> 237 if beforeFace then 238 group.AddFace(this.GetLineValue(str)) 239 else 240 this.CurrentGroup.AddFace(this.GetLineValue(str)) 241 beforeFace <- true 242 | _ -> printfn "%s" ("---------"+str) 243 file.Close() 244 //根據索引信息來給對應的頂點,法線,紋理座標賦值 245 groups |>List.iter (fun p -> 246 p.Faces.ForEach(fun face -> 247 face.Vectexs |> Array.iter(fun vect -> 248 if vect.PositionIndex > 0 then vect.Position <-this.Positions.[vect.PositionIndex-1] 249 if vect.TexcoordIndex > 0 then vect.Texcoord <- this.Texcoords.[vect.TexcoordIndex-1] 250 if vect.NormalIndex > 0 then vect.Normal <- this.Normals.[vect.NormalIndex-1] 251 ) 252 ) 253 let mater = this.Materials.Find(fun m -> m.Name = p.Mtllib) 254 if box(mater) <> null then 255 let mitem = mater.Items.Find(fun i -> i.Name = p.Usemtl) 256 if box(mitem) <> null then 257 p.Material <- mitem 258 p.Path <- this.Path 259 p.IsHaveMaterial <- true 260 ) 261 //釋放空間 262 this.Positions.Clear() 263 this.Normals.Clear() 264 this.Texcoords.Clear() 265 if bCreate then this.CreateVbo() 266 //生成VBO信息 267 member this.CreateVbo() = 268 this.Groups |> List.iter (fun p -> p.CreateVBO()) 269 member this.DrawVbo() = 270 this.Groups |> List.iter (fun p -> p.DrawVBO())
其中ObjMode主要是加載文件,主要方法在LoadObjModel裏,這個方法主要有二個主要做用.this
一是在file與file.close這節,主要是讀取OBJ文件裏全部的信息,當讀到mtllib時,會嘗試打開關聯的材質文件,而後讀取材質裏的信息,根據每讀一個newmtl,來添加一個ObjMaterialItem.而後就是讀到g就會生成一個group,而後讀到usemtl與f(面)時,分別爲前面生成的group,來分別對應group當前所用材質以及添加f(面)信息到group中,f(面)通常包含3個頂點(三角形)與四個頂點(方形)的v/vt/vn(可能只包含v,也可能全包含)的頂點索引信息.而f中vn(法向量),v(頂點),vt(紋理向量)中索引指向全局的對應值,就是說,當f中索引v可能已經到100了,而這時,咱們讀到的頂點數據可能只有10個.spa
Face中經過讀到的以下結構,v,v/vt,v//vn,v/vt/vn這四種結構,而後經過AddVectex裏分別解析成對應的VertexAttribute結構.在VertexAttribute中,記住屬性PointArray,這個把上面的v,v/vt,v//vn,v/vt/vn這四種結構按照順序會組裝成一個float[],裏的數據分別對應Opengl中的InterleavedArrayFormat中的V3f,T2fV3f,N3fV3f,T2N3fV3f.與後面在Group裏組裝VBO要用到.(前面Opengl繪製咱們的小屋(一)球體,立方體繪製有講解)其類還有一個做用,若是檢查到4個頂點,則分紅六個頂點,索引若是爲1,2,3,4,分紅1,2,3,4,1,3,意思就是一個方形分紅二個三角形,保持逆時針順序不變,一是爲了只生成一個VBO,二是爲了兼容性.code
二是把對應的VertexAttribute裏的v/vt/vn的索引,變成ObjMode裏所讀到的對應v/vt/vn裏的真實數據.爲何分紅二步作,上面其實有說,f中的v/vt/vn的索引值是全局的.這個索引可能大於你讀到的相關索引數據.而且把對應group裏用到的材質關聯上去.
上面的完成後,下面的才能開始,VertexAttribute中的PointArray就能組裝到對應值.Group裏的DataArray根據其中的Face中的VertexAttribute中的PointArray來組裝數據生成VBO,PointArray的組裝是一個規則二維數組[x,y],x等於Group裏的頂點個數,y就是V3f/T2fV3f/N3fV3f/T2fN3fV3f所對應的數據長度,分別是3,5,6,8.建立VBO與顯示VBO也是group來完成的,在OBJ裏,就是根據每組數據來繪製顯示的數據.
建立VBO與繪製的代碼由於有了上面數據的組裝,因此顯示的很簡單,其中仍是注意GL.InterleavedArrays(this.InterFormat,0,IntPtr.Zero)這句使用,這句能幫咱們節省不少代碼,會自動根據InterleavedArrayFormat來給咱們關閉打開相應狀態,自動給對應頂點結構如VectorPointer,TexcoordPointer,NormalPointer賦值.
在材質方面,我只對咱們最日常的貼圖map_Kd作了處理,還有對應的是法線紋理會在後面說明.
在網上下載了一些OBJ模型,而後用這個來加載,開始會發現紋理是上下反的,在網上查找了下,有種說法,紋理是用窗口座標系,而Opengl是用的笛卡爾座標系.對這種說法我表示懷疑,可是又不知從何解釋,不過把紋理座標通過y通過變換1-y後表示確實顯示正常.
經過此次OBJ模型的加載,也解決了長久以來我心中的一個疑問,我之前總是在想,若是一個頂點,有幾個紋理座標或者幾個法向量,那是如何用VBO的,原來就是經過最簡單,最粗暴的方法複製幾分數據來處理的.
代碼全是經過F#寫的,之前也沒說F#的東東,由於我本身也是在摸索,經過這個模型加載,我發現有些東東能夠說下.你們能夠發現,在F#裏,ObjGroup裏的頂點數組,法線數組,面數組相關數據量大的全是用的ArrayList<'T>這個結構,這個咱們能夠看到定義type ArrayList<'T> = System.Collections.Generic.List<'T>,就是C#的List<T>,你們可能會問這和F#中的List,Array有什麼不一樣?以及爲何不用這二個數據結構,下面是個人實踐.
從此次來看,F#的array爲了函數式不變性,在須要一點一點添加上萬元素時,很坑爹.由於每次添加一個元素,就至關於從新生成一個數組.而F#中的List也不一樣於C#中的List(本質是個數組).當時打開一個3M的文件,加載須要我20S,主要是由於ReadObjFile裏讀ObjGroup裏.我用表示多面元素用的F#中的array,致使每添加一個元素就須要從新生成.而後根據元素對應索引找到對應的值,這個都須要十秒左右,主要是由於我在ReadObjFile後,讀到的點,法線等數據全是用F#的List保存,而在後面根據下標來獲得對應的數據是,這就是個大杯具.
若是要求又能快速添加,又能快速根據下標找元素,應該仍是用到C#中包裝數組的List結構.上面提到的一些操做換成C#中的list,總共原來30S的時間到如今不到2S的時間,不能不說,坑爹啊.
不過我能確定的是,在objgroup中的DataArray,這個是用的F#的Array2D,裏面數據是超大量的.可是這個不會有前面說的問題,由於在組織這個Array2D時,咱們已知其中這個二維數組的長度,和各個對應元素值.
下面給出效果圖:
和前面同樣,其中EDSF上下左右移動,鼠標右鍵加移動鼠標控制方向,空格上升,空格在SHIFT降低。