OpenGL OBJ模型加載.


  在我們前面繪制一個屋,我們可以看到,需要每個立方體一個一個的自己來推並且還要處理位置信息.代碼量大並且要時間.現在我們通過加載模型文件的方法來生成模型文件,比較流行的3D模型文件有OBJ,FBX,dae等,其中OBJ模式只包含靜態的模型,相對FBX這種來說,比較簡單,剛好給我們用來學習之用.

  對比我們之前用代碼來一個一個建模型,用模型文件OBJ的不同就是在OBJ里包含了我們需要的頂點,法線,以及紋理坐標以及頂點組成面索引.去掉了我們用代碼建模最要時的過程.用模型文件我們要做的僅僅是讀出里面的信息,然后組織供OpenGL調用.

  不同的模型文件有不同的信息組織格式,相對於FBX這種二進制並且沒公布格式的文件來說,OBJ模型文本結構對於我們來說更易讀並且容易理解,網上也有不少大神對OBJ模型中出現的文本做了詳細的解說並提供相應的加載模型方法.

OBJ模型文件的結構、導入與渲染Ⅰ OBJ模型文件的結構、導入與渲染Ⅱ

  在上面二篇文章中以及文章中的鏈接,有對OBJ模型比較詳細的解說以及加載,與原文章加載稍有不同的是,我們解析相應數據按照OBJ模型的定義來定義結構.

  在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())
View Code

  其中ObjMode主要是加載文件,主要方法在LoadObjModel里,這個方法主要有二個主要作用.

  一是在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個.

  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,二是為了兼容性.

  二是把對應的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時,我們已知其中這個二維數組的長度,和各個對應元素值.

下面給出效果圖:

image

下面給出附件:源代碼 可執行文件

和前面一樣,其中EDSF上下左右移動,鼠標右鍵加移動鼠標控制方向,空格上升,空格在SHIFT下降。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM