在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的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())
其中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(本质是个数组),用下标找值是O(n).当时打开一个3M的文件,加载需要我20S,主要是因为ReadObjFile里读ObjGroup里.我用表示多面元素用的F#中的array,导致每添加一个元素就需要重新生成.然后根据元素对应索引找到对应的值,这个都需要十秒左右,主要是因为我在ReadObjFile后,读到的点,法线等数据全是用F#的List保存,而在后面根据下标来得到对应的数据是,这就是个大杯具.
如果要求又能快速添加,又能快速根据下标找元素,应该还是用到C#中包装数组的List结构.上面提到的一些操作换成C#中的list,总共原来30S的时间到现在不到2S的时间,不能不说,坑爹啊.
不过我能肯定的是,在objgroup中的DataArray,这个是用的F#的Array2D,里面数据是超大量的.但是这个不会有前面说的问题,因为在组织这个Array2D时,我们已知其中这个二维数组的长度,和各个对应元素值.
下面给出效果图:
和前面一样,其中EDSF上下左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。