在我们前面绘制一个屋,我们可以看到,需要每个立方体一个一个的自己来推并且还要处理位置信息.代码量大并且要时间.现在我们通过加载模型文件的方法来生成模型文件,比较流行的3D模型文件有OBJ,FBX,dae等,其中OBJ模式只包含静态的模型,相对FBX这种来说,比较简单,刚好给我们用来学习之用.

  对比我们之前用代码来一个一个建模型,用模型文件OBJ的不同就是在OBJ里包含了我们需要的顶点,法线,以及纹理坐标以及顶点组成面索引.去掉了我们用代码建模最要时的过程.用模型文件我们要做的仅仅是读出里面的信息,然后组织供OpenGL调用.

  不同的模型文件有不同的信息组织格式,相对于FBX这种二进制并且没公布格式的文件来说,OBJ模型文本结构对于我们来说更易读并且容易理解,网上也有不少大神对OBJ模型中出现的文本做了详细的解说并提供相应的加载模型方法.

OBJ模型文件的结构、导入与渲染Ⅰ OBJ模型文件的结构、导入与渲染Ⅱ

  在上面二篇文章中以及文章中的链接,有对OBJ模型比较详细的解说以及加载,与原文章加载稍有不同的是,我们解析相应数据按照OBJ模型的定义来定义结构.

  在OBJ模型中主要分二块,一块是模型组成文件,包含顶点,法线,纹理坐标,面,组的信息,另一块是模型文件所需的材质信息与对应纹理所需图片.

  我们分别定义第一块的数据结构如下:VertexAttribute,ObjFace,ObjGroup.第二块ObjMaterialItem,ObjMaterial.其中模型定义为ObjModel.代码如下:

 type ArrayList<'T> = System.Collections.Generic.List<'T>

 type ObjMaterialItem() =
member val Name = "" with get,set
member val Ambient = [|.f;.f;.f;.f|] with get,set
member val Diffuse = [|.f;.f;.f;.f|] with get,set
member val Specular = [|.f;.f;.f;.f|] with get,set
member val Shiness = .f with get,set
member val DiffuseMap = "" with get,set
member val SpecularMap = "" with get,set
member val BumpMap = "" with get,set
member val DiffuseID = with get,set
member val SpecularID = with get,set
member val BumpID = with get,set type ObjMaterial() =
member val Name = "" with get,set
member val Items = new ArrayList<ObjMaterialItem>() with get,set
member val currentItem = new ObjMaterialItem() with get,set type VertexAttribute() =
let strToInt str =
let (ok,f) = System.Int32.TryParse(str)
if ok then f else -
member val Position= Vector3.Zero with get,set
member val Texcoord=Vector2.Zero with get,set
member val Normal= Vector3.Zero with get,set
member val PositionIndex = - with get,set
member val TexcoordIndex = - with get,set
member val NormalIndex = - with get,set
//各个值的索引信息
member this.SetValue(line:string) =
let ls = line.Split('/')
match ls.Length with
| ->
this.PositionIndex <- strToInt ls.[]
| ->
this.PositionIndex <- strToInt ls.[]
this.TexcoordIndex <- strToInt ls.[]
| ->
this.PositionIndex <- strToInt ls.[]
this.NormalIndex <- strToInt ls.[]
if not (ls.[] = "" || ls.[] = null) then
this.TexcoordIndex <- strToInt ls.[]
| _ -> ()
//组织格式用T2fV3f/N3fV3f/T2fN3fV3f/V3f成float32[]
member this.PointArray
with get() =
let mutable ps = Array.create 0.0f
if this.TexcoordIndex > then ps <- Array.append ps [|this.Texcoord.X;1.0f - this.Texcoord.Y|]
if this.NormalIndex > then ps <- Array.append ps [|this.Normal.X;this.Normal.Y;this.Normal.Z|]
if this.PositionIndex > then ps <- Array.append ps [|this.Position.X;this.Position.Y;this.Position.Z|]
ps type ObjFace() =
let mutable vectexs = [||] : VertexAttribute array
//每个面的顶点,一个是三角形,如果是矩形,为了兼容性,应该化为成二个三角形.
member this.Vectexs
with get() =
let mutable result = vectexs.[..]
if vectexs.Length = then
let newvxs = [|vectexs.[];vectexs.[]|]
result <- Array.append result newvxs
result
//在读取文件时,得到当前面包含的顶点索引信息.(此时对应顶点只有索引,没有真实数据)
member this.AddVectex (line:string) =
let ls = line.TrimEnd(' ').Split(' ')
let vs =
ls |> Array.map(fun p ->
let va = new VertexAttribute()
va.SetValue(p)
va)
vectexs <- vs
member this.VertexCount with get() = this.Vectexs.Length type ObjGroup() =
//得到数组里所有面的对应所有顶点属性
let mutable vectexs = new ArrayList<VertexAttribute>()
let mutable points = Array2D.create .f
let mutable vbo,ebo = ,
member val Faces = new ArrayList<ObjFace>() with get,set
member val Mtllib = "" with get,set
member val Usemtl = "" with get,set
member val Name = "" with get,set
member val Material = new ObjMaterialItem() with get,set
member val IsHaveMaterial = false with get,set
member val Path = "" with get,set
member this.VBO with get() = vbo
member this.EBO with get() = ebo
//读取文件,读取当前group里的面的信息,并且会在读面信息时读取到这个面所有顶点索引
member this.AddFace (line:string) =
let face = new ObjFace()
face.AddVectex(line)
this.Faces.Add(face)
vectexs.AddRange(face.Vectexs)
//组织一个规则二维数组,一维表示每面上的每个顶点,二维表示每个顶点是如何组织,包含法向量,纹理坐标不
member this.DataArray
with get() =
if points.Length < then
let length1 = vectexs.Count
if length1 > then
let length2 = vectexs.[].PointArray.Length
if length2 > then
points <- Array2D.init length1 length2 (fun i j -> vectexs.[i].PointArray.[j])
points
member this.CreateVBO() =
if this.ElementLength > then
vbo <- GL.GenBuffers()
GL.BindBuffer(BufferTarget.ArrayBuffer,vbo)
GL.BufferData(BufferTarget.ArrayBuffer,IntPtr ( *this.ElementLength*this.VectorLength ),this.DataArray,BufferUsageHint.StaticDraw)
let len = this.ElementLength -
let eboData = [|..len|]
ebo <- GL.GenBuffers()
GL.BindBuffer(BufferTarget.ElementArrayBuffer,ebo)
GL.BufferData(BufferTarget.ElementArrayBuffer,IntPtr ( * this.ElementLength),eboData,BufferUsageHint.StaticDraw)
if this.IsHaveMaterial then
let kdPath = Path.Combine(this.Path,this.Material.DiffuseMap)
if File.Exists kdPath then
this.Material.DiffuseID <- TexTure.Load(kdPath)
member this.DrawVBO() =
if this.VBO > && this.EBO > then
GL.BindBuffer(BufferTarget.ArrayBuffer,this.VBO)
GL.BindBuffer(BufferTarget.ElementArrayBuffer,this.EBO)
if this.IsHaveMaterial then
GL.Enable(EnableCap.Texture2D)
GL.BindTexture(TextureTarget.Texture2D,this.Material.DiffuseID)
GL.InterleavedArrays(this.InterFormat,,IntPtr.Zero)
GL.DrawElements(BeginMode.Triangles,this.ElementLength,DrawElementsType.UnsignedInt,IntPtr.Zero)
GL.Disable(EnableCap.Texture2D)
//多少个顶点
member this.ElementLength with get() = Array2D.length1 this.DataArray
//顶点组织形式长度T2fV3f/N3fV3f/T2fN3fV3f/V3f
member this.VectorLength with get() = Array2D.length2 this.DataArray
//顶点组织形式
member this.InterFormat
with get()=
let mutable result = InterleavedArrayFormat.T2fN3fV3f
if this.VectorLength = then result <- InterleavedArrayFormat.V3f
if this.VectorLength = then result <- InterleavedArrayFormat.T2fV3f
if this.VectorLength = then result <- InterleavedArrayFormat.N3fV3f
result type ObjModel(fileName:string) =
let mutable groupName = "default"
let mutable groups = [] : ObjGroup list
let addGroup group = groups <- (group :: groups)
//得到每行数组去掉标识符后的数据如 v 1.0 2.0 3.0 -> 1.0 2.0 3.0
let getLineValue (line:string) =
let fs = line.Split(' ')
let len = fs.Length -
if fs.Length > then (fs.[..len] |> Array.filter (fun p -> p <> null && p<> " " && p <> ""))
else [|line|]
//数组转化成float32
let strToFloat str =
let (ok,f) = System.Single.TryParse(str)
if ok then f else System.Single.NaN
let mutable group = ObjGroup()
let mutable mtllib = ""
member val Positions = new ArrayList<Vector3>() with get,set
member val Normals = new ArrayList<Vector3>() with get,set
member val Texcoords = new ArrayList<Vector2>() with get,set
member val Materials = new ArrayList<ObjMaterial>() with get,set
member this.Path
with get() = System.IO.Path.GetDirectoryName(fileName)
member this.GetLineFloatArray (line:string) =
let fs = getLineValue(line)
fs |> Array.map (fun p -> strToFloat p)
member this.GetLineValue (line:string,?sep) =
let dsep = defaultArg sep " "
let fs = getLineValue(line)
String.concat dsep fs
member this.CurrentGroup
with get() =
let bExist = groups |> List.exists(fun p -> p.Name = groupName)
if not bExist then
let objGroup = new ObjGroup()
objGroup.Name <- groupName
objGroup.Mtllib <- mtllib
addGroup objGroup
group <- groups |> List.find(fun p -> p.Name = groupName)
group
member this.Groups
with get() =
groups
//主要有二步,首先读取文件信息,然后把顶点,法线,纹理坐标根据索引来赋值
member this.LoadObjModel(?bCreateVBO) =
let bCreate = defaultArg bCreateVBO false
let file = new StreamReader(fileName)
let mutable beforeFace = false
let (|StartsWith|) suffix (s:string) = s.TrimStart(' ','\t').StartsWith(suffix,StringComparison.OrdinalIgnoreCase)
//首先读取文件信息,此时顶点只有索引信息.
while not file.EndOfStream do
let str = file.ReadLine()
match str with
| StartsWith "mtllib " true ->
mtllib <- this.GetLineValue(str)
//#region 读纹理
let material = new ObjMaterial()
material.Name <- mtllib
let mtlFile = new StreamReader(Path.Combine(this.Path,mtllib))
while not mtlFile.EndOfStream do
let str = mtlFile.ReadLine()
match str with
| null -> ()
| StartsWith "newmtl " true ->
material.currentItem <- new ObjMaterialItem()
material.currentItem.Name <- this.GetLineValue(str)
material.Items.Add(material.currentItem)
| StartsWith "ka " true -> material.currentItem.Ambient <- this.GetLineFloatArray(str)
| StartsWith "kd " true -> material.currentItem.Diffuse <- this.GetLineFloatArray(str)
| StartsWith "ks " true -> material.currentItem.Specular <- this.GetLineFloatArray(str)
| StartsWith "map_Kd " true -> material.currentItem.DiffuseMap <- this.GetLineValue(str)
| StartsWith "map_Ks " true -> material.currentItem.SpecularMap <- this.GetLineValue(str)
| StartsWith "map_bump " true -> material.currentItem.BumpMap <- this.GetLineValue(str)
| StartsWith "Ns " true ->
let ns = this.GetLineFloatArray(str).[]
material.currentItem.Shiness <- ns * 0.128f
| _ -> ()
mtlFile.Close()
this.Materials.Add(material)
//#endregion
| null -> ()
| StartsWith "usemtl " true -> this.CurrentGroup.Usemtl <- this.GetLineValue(str)
| StartsWith "g " true ->
groupName <- this.GetLineValue(str)
beforeFace <- false
| StartsWith "vn " true ->
let fs = this.GetLineFloatArray(str)
this.Normals.Add(Vector3(fs.[],fs.[],fs.[]))
| StartsWith "vt " true ->
let fs = this.GetLineFloatArray(str)
this.Texcoords.Add(Vector2(fs.[],fs.[]))
| StartsWith "v " true ->
let fs = this.GetLineFloatArray(str)
this.Positions.Add(Vector3(fs.[],fs.[],fs.[]))
| StartsWith "f " true ->
if beforeFace then
group.AddFace(this.GetLineValue(str))
else
this.CurrentGroup.AddFace(this.GetLineValue(str))
beforeFace <- true
| _ -> printfn "%s" ("---------"+str)
file.Close()
//根据索引信息来给对应的顶点,法线,纹理坐标赋值
groups |>List.iter (fun p ->
p.Faces.ForEach(fun face ->
face.Vectexs |> Array.iter(fun vect ->
if vect.PositionIndex > then vect.Position <-this.Positions.[vect.PositionIndex-]
if vect.TexcoordIndex > then vect.Texcoord <- this.Texcoords.[vect.TexcoordIndex-]
if vect.NormalIndex > then vect.Normal <- this.Normals.[vect.NormalIndex-]
)
)
let mater = this.Materials.Find(fun m -> m.Name = p.Mtllib)
if box(mater) <> null then
let mitem = mater.Items.Find(fun i -> i.Name = p.Usemtl)
if box(mitem) <> null then
p.Material <- mitem
p.Path <- this.Path
p.IsHaveMaterial <- true
)
//释放空间
this.Positions.Clear()
this.Normals.Clear()
this.Texcoords.Clear()
if bCreate then this.CreateVbo()
//生成VBO信息
member this.CreateVbo() =
this.Groups |> List.iter (fun p -> p.CreateVBO())
member this.DrawVbo() =
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(本质是个数组).当时打开一个3M的文件,加载需要我20S,主要是因为ReadObjFile里读ObjGroup里.我用表示多面元素用的F#中的array,导致每添加一个元素就需要重新生成.然后根据元素对应索引找到对应的值,这个都需要十秒左右,主要是因为我在ReadObjFile后,读到的点,法线等数据全是用F#的List保存,而在后面根据下标来得到对应的数据是,这就是个大杯具.

  如果要求又能快速添加,又能快速根据下标找元素,应该还是用到C#中包装数组的List结构.上面提到的一些操作换成C#中的list,总共原来30S的时间到现在不到2S的时间,不能不说,坑爹啊.

  不过我能肯定的是,在objgroup中的DataArray,这个是用的F#的Array2D,里面数据是超大量的.但是这个不会有前面说的问题,因为在组织这个Array2D时,我们已知其中这个二维数组的长度,和各个对应元素值.

下面给出效果图:

下面给出附件:源代码 可执行文件

和前面一样,其中EDSF上下左右移动,鼠标右键加移动鼠标控制方向,空格上升,空格在SHIFT下降。

OpenGL OBJ模型加载.的更多相关文章

  1. 从零开始openGL——三、模型加载及鼠标交互实现

    前言 在上篇文章中,介绍了基本图形的绘制.这篇博客中将介绍模型的加载.绘制以及鼠标交互的实现. 模型加载 模型存储 要实现模型的读取.绘制,我们首先需要知道模型是如何存储在文件中的. 通常模型是由网格 ...

  2. DirectX11 With Windows SDK--19 模型加载:obj格式的读取及使用二进制文件提升读取效率

    前言 一个模型通常是由三个部分组成:网格.纹理.材质.在一开始的时候,我们是通过Geometry类来生成简单几何体的网格.但现在我们需要寻找合适的方式去表述一个复杂的网格,而且包含网格的文件类型多种多 ...

  3. 软件光栅器实现(四、OBJ文件加载)

    本节介绍软件光栅器的OBJ和MTL文件加载,转载请注明出处. 在管线的应用程序阶段,我们需要设置光栅器所渲染的模型数据.这些模型数据包括模型顶点的坐标.纹理.法线和材质等等,可以由我们手动编写,也可以 ...

  4. Wish3D用户必看!模型加载失败原因汇总

    上传到Wish3D的模型加载不出来,作品显示页面漆黑一片,是什么原因? 很有可能是操作过程中的小失误,不妨从以下几点检查.还是不行的请加QQ群(Wish3D交流群3):635725654,@Wish3 ...

  5. 6_1 持久化模型与再次加载_探讨(1)_三种持久化模型加载方式以及import_meta_graph方式加载持久化模型会存在的变量管理命名混淆的问题

    笔者提交到gitHub上的问题描述地址是:https://github.com/tensorflow/tensorflow/issues/20140 三种持久化模型加载方式的一个小结论 加载持久化模型 ...

  6. cesium模型加载-加载fbx格式模型

    整体思路: fbx格式→dae格式→gltf格式→cesium加载gltf格式模型 具体方法: 1. fbx格式→dae格式 工具:3dsMax, 3dsMax插件:OpenCOLLADA, 下载地址 ...

  7. PyTorch模型加载与保存的最佳实践

    一般来说PyTorch有两种保存和读取模型参数的方法.但这篇文章我记录了一种最佳实践,可以在加载模型时避免掉一些问题. 第一种方案是保存整个模型: 1 torch.save(model_object, ...

  8. CC模型加载太慢?一招破解!

    伴随无人机性能的提升,单个项目涉及到的倾斜摄影数据范围不断扩大,模型的数据量越来越大,在同配置机器上的显示速度也相应的越来越慢,那么如何在不升级配置的情况下提升模型的加载速度呢? 01 百GB倾斜摄影 ...

  9. MD2关键桢动画3D模型加载.

    在看Cg教程中,看到关键桢插值来表示一个动画的物体,例如一个动物拥有站着,奔跑,下跪等动画序列,美工将这些特定的姿态称为一个关键桢.为什么要用关键桢这种来表示了,这个比较容易理解,我们知道我们看的一些 ...

随机推荐

  1. AdminLTE, Color Admin

    AdminLTE, Color Adminhttps://github.com/almasaeed2010/AdminLTE/http://www.seantheme.com/color-admin- ...

  2. 【Unity】7.5 移动设备输入

    分类:Unity.C#.VS2015 创建日期:2016-04-21 一.简介 在iOS和Android系统中,操作都是通过触摸来完成的.Input类中对触摸操作的方法或变量如下图所示: 通过GetT ...

  3. R语言之——字符串处理函数

    nchar 取字符数量的函数 length与nchar不同,length是取向量的长度 # nchar表示字符串中的字符的个数 nchar("abcd") [1] 4 # leng ...

  4. 菜鸟学Java(十五)——Java反射机制(二)

    上一篇博文<菜鸟学编程(九)——Java反射机制(一)>里面,向大家介绍了什么是Java的反射机制,以及Java的反射机制有什么用.上一篇比较偏重理论,理论的东西给人讲出来总感觉虚无缥缈, ...

  5. layer-list:Android中layer-list使用详解

    使用layer-list可以将多个drawable按照顺序层叠在一起显示,默认情况下,所有的item中的drawable都会自动根据它附上view的大小而进行缩放, layer-list中的item是 ...

  6. .net core 调用数字证书 使用X509Certificate2

    .NET下面的 .netfromwork使用和asp.net core下使用方式不一样 配置文件中代码: public const string API_URL = "https://api ...

  7. LeetCode: Linked List Cycle II 解题报告

    Linked List Cycle II Given a linked list, return the node where the cycle begins. If there is no cyc ...

  8. poj1988(判断一个结点下面有多少个结点,推荐)

    题意:有n个元素,开始每个元素自己一栈,有两种操作,将含有元素x的栈放在含有y的栈的顶端,合并为一个栈.第二种操作是询问含有x元素下面有多少个元素. 6 M 1 6 C 1 M 2 4 M 2 6 C ...

  9. java 获取 path

    (1).request.getRealPath("/");//不推荐使用获取工程的根路径 (2).request.getRealPath(request.getRequestURI ...

  10. 手写体识别中用到的Tensorflow函数复习

    tf.truncated_normal(shape, stddev=0.1) 从截断的正态分布中输出随机值. 生成的值服从具有指定平均值和标准偏差的正态分布,如果生成的值大于平均值2个标准偏差的值则丢 ...