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下降。
Mikel

