OBJ模型文件的结构、导入与渲染Ⅰ_Lion.Kuo的博客-程序员宅基地

技术标签: 3D模型  

在[3DS文件结构的初步认识]中提及了3DS格式模型文件。固然3DS格式很常用,但OBJ格式的模型也是很常见的,于是咔嚓了一下心,熟悉了一下格式,并写了一个导入OBJ格式模型的类,顺便有此文。——ZwqXin.com

先总体说一下两种格式的不同处。比起二进制文件为主、连每个块的用途也得试探来试探去的3DS,文本文件为主的OBJ对我们更友好。与3DS文件的树状[块结构]不同,OBJ文件只是很单纯的字典状结构,没有块ID来表征名字而是简单地用易懂的表意字符来表示。总之看上去是赏心悦目的样子,而苦处也就只有实际写导入代码的时候才知道了- -。OBJ文件优化了存储但劣化了读写,接下来慢解^^……

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

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/obj-model-format-import-and-render-1.html

1.  OBJ,从格式到读入

背景介绍一下吧,它的创始公司是Wavefront Technologies,最早的母体软件是Advanced Visualizer(8认识~),目前版本好像3.0,不含动画。由于有良好的移植性能,3dsMax、Maya一连串建模软件都可以随便导入和导出(印象记得自己唯一系统学过的建模软件ProE也有类似的选项?不过不太确定了)。反正通用性好网上提供的直接资源多,招人欢喜,不像.max那种深闺女,不像.x那种自宠儿……(我没有别的意思.obj您别误会。)换句话说,只要写好一个导入obj文件的工具类,就不用像之前那样到3dsMax里转成3ds再导入OpenGL了。

作为一种文本文件,什么文本查看器都能看,不像3DS那种乱麻麻的乱码。格式说明网上也有很详细的,这里随便找一篇写得挺好的:【ZT】3D中的OBJ文件格式详解(擦~这链接里的转帖不附原文链接实在是卑鄙无耻的行为!)。

  1. #...(#是注释符)
  2. mtllib pCube.mtl
  3. g default
  4. v -0.500000 -0.500000 0.500000
  5. v 0.500000 -0.500000 0.500000
  6. v 0.500000 -0.500000 -0.500000
  7. ....
  8. vt 0.000000 0.000000
  9. vt -1.000000 1.000000
  10. .....
  11. vn 0.000000 0.000000 1.000000
  12. vn 0.000000 0.000000 -1.000000
  13. .....
  14.  
  15. g pCube1
  16. s off
  17. usemtl initialShadingGroup
  18. f 1/1/1 2/2/2 4/4/3
  19. f 3/3/5 4/4/6 6/6/7
  20. f 5/5/9 6/6/10 8/8/11 7/7/12
  21. f 7/7/13 8/8/14 1/9/16
  22.  
  23. g pCube2
    usemtl DefferShadingGroup
  24. .....
整个OBJ文件可以分成三个部分:第一部分是文件前半部的“顶点数据”部分——指定了模型所用到的全部顶点(v)、顶点纹理坐标(vt)、顶点法线(vn)。(括号里是数据的行头标识)其中顶点(位置)数据是必须的,纹理坐标只在对应的物件处有纹理时才必须,法线也是非必须的但是要注意的,也许你还记得在[一个读取3DS文件的类CLoad3DS浅析Ⅰ/一个读取3DS文件的类CLoad3DS浅析]两篇文章中提及3DS是不包含法线信息需要我们自己去计算,这就是OBJ文件相对3DS第一个大特点——

a.可能包含顶点法线信息而不需自行计算
当然了如果文件中没有法线信息那还是同样要计算的,不然在OpenGL里渲染起来会悲剧,至于这项数据存在的理由你自己列举吧,但是起码是不再需要相邻面法线取平均这种粗糙的手段,嘛不过储存量大了些;文件的第二部分是后半部的"面数据",与3DS类似的是每个面(f)利用索引指示的顶点属性来表示,但是——
b.索引指向整个数据区,且连同纹理坐标和法线,一个顶点的属性可能需要3个索引
c.一个面至少要3组顶点属性,但3组以上组成一个面(多边形而非三角形)也是可能的
前者引入导入时的一堆麻烦事,后面讲解。后者表示如果我们要用3D渲染系统渲染(目前主流是三角面片化,连四角面片也要慢慢不太溶于显卡了),就要在导入时切割面片人为三角面化。其实除了面还可能有点线曲线曲面之类的,此时只能无视之。另外这里还有几个标识符,组(g)标识一个独立物件(也就是3DS文件里的Object概念),每个组有其自己的材质(usemtl指定),如果几个组想共用一种材质的话,只需要改改顺序好了,因为usemtl也类似状态机会一直作用于后面的组直到下一句usemtl。标识s绝对可以选择性无视因为随便找个OBJ文件看上去就知道是个光滑组的概念了,我们没必要理会;第三部分是上面没显示的,它不属于obj后缀的文件但是OBJ文件的一部分——mtl材质库文件。在前面指定材质usemtl后也就简单的一个名字,它代表的东西可以在obj文件附带的mtl文件中找到。这个mtl文件在obj文件中mtllib指定,上面一段,就是pCube.mtl了,注意要在使用材质前指定。相对来说,mtl文件的格式更加复杂:

  1.  
  2. newmtl InitialShadingGroup
  3. illum 4
  4. Kd 0.50 0.50 0.00
  5. Ka 0.10 0.10 0.10
  6. Tf 1.00 1.00 1.00
  7. map_Kd -s 1 1 1 -o 0 0 0 -mm 0 1 desertHouse_details_color.tga
  8. bump  -bm desertHouse_details_normal.tga
  9. Ni 1.00
  10. Ks 0.00 0.00 0.00
  11. map_Ks desertHouse_details_specular.tga
  12. Ns 18.00
  13.  
  14. newmtl DefferShadingGroup
  15. ....
通常mtl文件和纹理文件要和联系的obj文件放在一起。看上面,这里newmtl指定新材质的名称,以供obj文件中对应查询,后面一列会是材质属性,全部属性实在太多了,详情请看这篇文章,很详细:Alias/WaveFront Material (.mtl) File Format。这里挑几个重点的,也是我后面的导入程序主要关心的部分:环境光反射材质(Ka)、漫反射光材质(Kd)、镜面反射材质(Ks)、镜面反射的Exponent(Ns,即常说的Shinness),OpenGL同学们记得吗,以上可以用glMaterialf(v)可指定,只是其中Ns要做个转换从[0,1000]到OpenGL一般光照模型的[0,128]。d/Tr是指透明度(alpha通道),map_Kd是我们最关心的纹理,map_Ks是镜面纹理(SpecularMap)、bump是法线纹理(Bump Map/Normal Map,见【shader复习与深入:Normal Map(法线贴图)Ⅰ/shader复习与深入:Normal Map(法线贴图)Ⅱ】)。(另外提一下资料中提及的map_Ka是环境光纹理,map_Ns是Shinness纹理,map_d是透度纹理,decal是贴花纹理,disp不清楚[说是指示表面粗糙度],这些都相对在3d程序中少用,如果实在需要再重载我的导入类好了。)

d.Obj模型中各对象都可能包含多种纹理贴图类型

好了,格式解释好,就要开始解析(parsing)了。

首先是要定下导入数据的数据结构。以CLoad3DS类[一个读取3DS文件的类CLoad3DS浅析Ⅰ]中3DS的数据格式为基础,在实际导入代码编写过程中增减数据成员,是比较稳妥的。按照模型对象数据(obj文件)+ 材质数据(mtl)的分法,将模型确定为:

  1. //模型信息结构体
  2. typedef struct tag3DModel 
  3.     {
  4.         bool  bIsTextured;                        //是否使用纹理
  5.         std::vector<tMaterialInfo> tMatInfoVec;   // 材质信息
  6.         std::vector<t3DObject>     t3DObjVec;     // 模型中对象信息
  7.     }t3DModel;

其中材质数据体直接按照所需的材质属性据在mtl材质库中找到对应项,并以名称为标识。绝大多数情况下obj文件里要引用所有的材质(当然是不一定但是为了导入方便就这么假定好了),我的策略是当读obj文件读到mtllib标识的时候就马上打开对应的mtl文件把所有材质读入里面tMatInfoVec,再返回obj文件。当后面读到usemtl的时候再按名称查找tMatInfoVec。遇到纹理文件的时候,直接生成纹理ID并保存,另外每种纹理会有一个对应的nTexObjX*数据变量指示该纹理在使用过程中所在的纹理对象(毕竟同一材质的这些纹理基本是要多重贴图[MultiTexture]的),在读入的时候只给DiffuseTex默认GL_TEXTURE0,其他置0,这些值一般是需要的时候再由应用层去设置的,我们的导入类单纯生成所有纹理但只会在渲染的时候使用DiffuseTex(我对是否该在应用层指定的时候才生成其他对应的纹理,留个问号,毕竟实际场合下如果材质中有,我们多半是要用的,而延后生成纹理就很被动了。空间与效率的争夺啊。):

  1. // 材质信息结构体
  2. typedef struct tagMaterialInfo
  3. {
  4.     char      strName[MAX_NAME];   // 纹理名称
  5.     GLfloat   crAmbient[4];
  6.     GLfloat   crDiffuse[4];
  7.     GLfloat   crSpecular[4];
  8.     GLfloat   fShiness;
  9.     GLuint    nDiffuseMap;
  10.     GLuint    nSpecularMap;
  11.     GLuint    nBumpMap;
  12.     GLuint    TexObjDiffuseMap;
  13.     GLuint    TexObjSpecularMap;
  14.     GLuint    TexObjBumpMap;
  15. }tMaterialInfo;
然后就是对象数据了——或者我应该在下篇中再详细结合导入过程讲,先给出数据结构一览:
  1. // 对象信息结构体
  2. typedef struct tag3DObject 
  3. {
  4.     int                         nMaterialID;       // 纹理ID
  5.     bool                        bHasTexture;       // 是否具有纹理映射
  6.     bool                        bHasNormal;        // 是否具有法线
  7.     std::vector<Vector3>        PosVerts;          // 对象的顶点
  8.     std::vector<Vector3>        Normals;           // 对象的法向量
  9.     std::vector<TexCoord>       Texcoords;         // 纹理UV坐标
  10.     std::vector<unsigned short> Indexes;           // 对象的顶点索引
  11.     unsigned int                nNumIndexes;       // 索引数目
  12.     GLuint                      nPosVBO;
  13.     GLuint                      nNormVBO;
  14.     GLuint                      nTexcoordVBO;
  15.     GLuint                      nIndexVBO;
  16. }t3DObject;
纹理ID是用来指涉tMatInfoVec的,对象名字就免去了,减少储存。值得注意的是,我这里没有使用任何跟“面”有关的数据,但是导入过程是读“面”无误,这里头有个转化关系。不然什么是”OBJ文件优化了存储但劣化了读写“呢?——很明显我要动用VBO(顶点缓存对象,见【学一学,VBO】),而且还是Indexed VBO!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/shenshen211/article/details/51729380

智能推荐

asp.net core c# HttpWebRequest 连接特别慢-程序员宅基地

asp.net core c# HttpWebRequest 连接特别慢,查找原因发现 :由 HttpWebRequest. Proxy 代理的原因导致 。 设置对象为null

响应-字节流和字符流输出中文乱码的处理办法_输出流中文乱码-程序员宅基地

1. 响应-字节流输出中文问题public class ResponseDemo1 extends HttpServlet { /** * 演示字节流输出的乱码问题 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /** *_输出流中文乱码

浙大 | PTA 习题7-1 选择法排序 (20分)-程序员宅基地

本题要求将给定的n个整数从大到小排序后输出。输入格式:输入第一行给出一个不超过10的正整数n。第二行给出n个整数,其间以空格分隔。输出格式:在一行中输出从大到小有序的数列,相邻数字间有一个空格,行末不得有多余空格。输入样例:45 1 7 6输出样例:7 6 5 1#include <stdio.h>#define MAXS 10int main(void)...

Maven使用tomcat8-maven-plugin插件-程序员宅基地

在mvnrepository仓库中找到了一个把上面的依赖加入到POM.XML中要么就是提示找不到该依赖,要么就是下载不下来.找了半天找到了一个解决方法,就是使用Maven中的 ,是用来配置插件地址的,因为maven的所有功能都是使用插件来实现功能的,因此需要从特定的地址下载插件包。在POM.XML中加入以下内容 <pluginRepositories> <pluginRepository> <id>alfresco-public</i_tomcat8-maven-plugin

取整的计算机语言符号,word取整符号-程序员宅基地

word取整符号: word中怎么打弧AB?数学符号弧ab的输入方法,缺失:word取整符号11226/11以下是的一些我们精选的word中怎么打弧AB?数学符号弧ab的输入方法word中怎么打弧AB?在输入数学资料的时候遇到了一个符号不知道该怎么输入,就是一个弧下面写ab,找了好久都没找到,该怎么办呢?下面我们就来看看word中数学符号弧ab的输入方法。授权:免费版软件大小:2GB语言:简体中文...

随便推点

一、NLTK工具包使用-程序员宅基地

Natural Language Toolkit,自然语言处理工具包,在NLP领域中,最常使用的一个Python库。先安装NLTkpip install nltk注意你现在安装好一个框架而已,里面没有东西的新建一个ipython,输入import nltk #pip install nltknltk.download()所以要下载里面的包,我觉得下book 和popular下好就可...

SDUT OJ 不敢死队问题-程序员宅基地

不敢死队问题Time Limit: 1000 ms Memory Limit: 65536 KiBSubmit Statistic DiscussProblem Description说到“敢死队”,大家不要以为我来介绍电影了,因为数据结构里真有这么道程序设计题目,原题如下: 有M个敢死队员要炸掉敌人的一个碉堡,谁都不想去,排长决定用轮回数数的办法来决定哪个战士去执行任务。...

h5学习笔记:js find方法_find5.js-程序员宅基地

最近做项目的时候,经常会有这样一个需求,检测数组里面是否存在,存在则返回true,不存在则返回false。于是就会编写一段循环来检测,这个也是很常见的遍历行为,按着顺序去查找是否有对应的匹配值。<script type="text/javascript"> var carlist = [{vin:"ls11",carname:"aaa"},{vin:"ls2..._find5.js

selenium+java环境搭建-程序员宅基地

配置环境:第一步 安装JDK jdk1.8第二步 下载Eclipse官网下载纯净的Java版eclipse第三步 下载Selenium IDE、SeleniumRC、IEDriverServer、SeleniumClient Drivers 1、 Selenium IDE:selenium-ide-2.9.1.0.xpi 用来在Firefox上录制脚本。 2、 Selenium RC:se...

HTML笔记-程序员宅基地

第一部分:HTML属性align 居中排列bgcolor 背景颜色第二部分:HTML标签&lt;hr&gt; 水平线&lt;p&gt; 定义段落&lt;br/&gt; 插入单个折行(换行)&lt;style&gt; 定义样式&lt;link&gt; 定义资源引用&lt;div&gt; 定义文档中的节或区域或区块&lt;span&gt; 定义文档中的行内的小块或

iphone移动端h5拍照上传图片被旋转问题_h5拍照base64上传,在iphone12上图片被裁剪-程序员宅基地

html<input class="xfile" name="file_img" type="file" accept="image/*" multiple type="hidden"><canvas id="myCanvas" style="position: absolute;left:-200%;top:-200%;"></canvas>js//上传图片请求 function preview(input) { _h5拍照base64上传,在iphone12上图片被裁剪