« OBJ模型文件的结构、导入与渲染ⅡOpenGL/GLSL数据传递小记(2.x) »

用Indexed-VBO渲染3DS模型

 以前做DEMO的时候,总是导入几个模型进场景就让FPS哭泣了,当然也知道从OpenGL1.0流传下来的glVertex3f打点渲染法是个务实的伙子,但VBO什么的还是在向我招手,后来整理框架的时候便干脆转向VBO了,另外也好好修理了一下原来的3DS导入类,于是也就有了ZWModel3DS了。于是效率就士别了三日了。——ZwqXin.com

  用Indexed-VBO渲染3DS模型

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/idexed-vbo-for-model-3ds-rendering.html

确实,这个连固定管线也要说拜拜的年头,glVertex3f的效率真是太悲催了,先不说glBegin/glEnd之间的颇大的函数调用消费多么让CPU心伤了,GPU也更喜欢VBO这种随用随拿的新潮货。在两年前的[一个读取3DS文件的类CLoad3DS浅析Ⅰ]/[一个读取3DS文件的类CLoad3DS浅析Ⅱ]中提到的CLoad3DS类是以glBegin(GL_TRIANGLES)/glEnd()夹杂glVertex3f来完成渲染的,渲染效率有时候确实让我有点郁闷,现在便改成VBO吧。

3DS文件结构对于生成VBO是有其天性般的便利的——在【3DS文件结构的初步认识】中提到3DS内部树状构造是一个一个的chunk,其中Object也是一个chunk,其子节支包含了该Object网格渲染所需的基本信息(包括纹理坐标),面信息的索引也是单单针对该Object的,因此可以认为Object与Object(网格对象)之间是独立的,这样每个Object形成单独的VBO就毫无压力了。而与[OBJ模型文件的结构、导入与渲染Ⅰ]类似,材质信息作为另外的chunk导入,形成ID供Object在渲染的时候查询,这样就省去顶点颜色之类的VBO。另外,3DS文件内没有法线信息(从目前已解读格式来看),自己计算吧:面都是三角面,面的两个边向量(按序连接)的叉积获得面法向量,顶点法线由拥有该点的面的面法线的均值取得(比较粗糙的获取法)。3DS是可以包含关健帧信息的,因此3DS模型也可能是帧动画模型,但是现在一般都推崇骨骼动画,而且3DS里面的动画信息也是2进制表示(估计是矩阵量之类),比较难解读,网上资料也比较少。(本类不支持3DS动画。)

与OBJ不同的还有,3DS文件每个chunk的header指示了chunk的大小,所以在制定导入数据结构的时候,可以不用vector而用固定的缓存区保存数据(new出来)——免去vecor的2阶内存分配带来的性能耗损:

  1. // 对象信息结构体
  2. typedef struct tag3DObject 
  3. {
  4.     int             numOfVerts;         // 模型中顶点的数目
  5.     int             numOfTexcoords;     // 模型中纹理坐标的数目
  6.     int             numOfIndexes;        // 模型中顶点索引数目
  7.     int             numOfFaces;         // 模型中面的数目[numOfIndexes/3]
  8.     int             nMaterialID;        // 纹理ID
  9.     bool            bHasTexture;        // 是否具有纹理映射
  10.     char            strName[MAX_NAME];  // 对象的名称
  11.     Vector3        *pVerts;             // 对象的顶点
  12.     Vector3        *pNormals;           // 对象的法向量
  13.     TexCoord       *pTexcoords;         // 纹理UV坐标
  14.     unsigned short *pIndexes;            // 对象的顶点索引
  15.     GLuint          nPosVBO;
  16.     GLuint          nNormVBO;
  17.     GLuint          nTexcoordVBO;
  18.     GLuint          nIndexVBO;
  19. }t3DObject;

生成(绑定)VBO:

  1. for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++) 
  2. {
  3.     if(m_Model3DS.t3DObjVec[i].pVerts)
  4.     {
  5.         glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nPosVBO);
  6.         glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nPosVBO);
  7.         glBufferData(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].numOfVerts * sizeof(Vector3), 
  8.             (GLvoid*)m_Model3DS.t3DObjVec[i].pVerts, usage);
  9.     }
  10.  
  11.     if((GLvoid*)m_Model3DS.t3DObjVec[i].pNormals)
  12.     {
  13.         glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nNormVBO);
  14.         .......
  15.                }
  16.  
  17.     if(m_Model3DS.t3DObjVec[i].pTexcoords)
  18.     {
  19.         glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nTexcoordVBO);
  20.         .......
  21.     }
  22.  
  23.     if(m_Model3DS.t3DObjVec[i].pIndexes)
  24.     {
  25.         glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nIndexVBO);
  26.         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nIndexVBO);
  27.         glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].numOfIndexes * sizeof(unsigned short), 
  28.             (GLvoid*)m_Model3DS.t3DObjVec[i].pIndexes, usage);
  29.     }
  30. }

Indexed-VBO的介绍见[索引顶点的VBO与多重纹理下的VBO]。这里遍历网格对象(Object),生成VBO并传输数据,是第一步(导入数据在传输给VBO后应该销毁);

  1. ///////////////////////////////////////////////////绘制模型
  2. void ZWModel3DS::DrawModel()
  3. {
  4.     // 遍历模型中所有的对象
  5.     for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++) 
  6.     {
  7.         // 获得当前显示的对象
  8.         t3DObject *t3DObj = &m_Model3DS.t3DObjVec[i];
  9.  
  10.         // 判断该对象是否有纹理映射
  11.         if(t3DObj->bHasTexture && m_Model3DS.bIsTextured && t3DObj->nMaterialID >= 0) 
  12.         {  
  13.             glColor3ub(255, 255, 255);
  14.  
  15.             // 打开纹理映射
  16.             if(NULL != m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap)
  17.             {
  18.                 glActiveTexture(m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap);
  19.                 glEnable(GL_TEXTURE_2D);
  20.                 glBindTexture(GL_TEXTURE_2D, m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].nDiffuseMap);
  21.             }
  22.         } 
  23.         else 
  24.         {
  25.             glDisable(GL_TEXTURE_2D);
  26.             if(-1 != t3DObj->nMaterialID)
  27.             {
  28.                 BYTE *pColor = m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].color;
  29.                 glColor3ub(pColor[0], pColor[1], pColor[2]);
  30.             }
  31.         }
  32.  
  33.         glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nPosVBO);
  34.         glEnableClientState(GL_VERTEX_ARRAY);
  35.         glVertexPointer(3, GL_FLOAT, 0, NULL);
  36.  
  37.         glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nNormVBO);
  38.         glEnableClientState(GL_NORMAL_ARRAY);
  39.         glNormalPointer(GL_FLOAT, 0, NULL);
  40.  
  41.         if(m_Model3DS.t3DObjVec[i].nTexcoordVBO)
  42.         {
  43.             glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nTexcoordVBO);
  44.             glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  45.             glTexCoordPointer(2, GL_FLOAT, 0, NULL);
  46.         }
  47.  
  48.         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nIndexVBO);
  49.         
  50.         glDrawElements(GL_TRIANGLES, m_Model3DS.t3DObjVec[i].numOfIndexes, GL_UNSIGNED_SHORT, NULL);
  51.  
  52.         glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
  53.  
  54.         if(m_Model3DS.t3DObjVec[i].nTexcoordVBO)
  55.         {
  56.             glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  57.         }
  58.         glDisableClientState(GL_NORMAL_ARRAY);
  59.         glDisableClientState(GL_VERTEX_ARRAY);
  60.         glBindBuffer(GL_ARRAY_BUFFER, NULL);
  61.  
  62.         if(t3DObj->bHasTexture && m_Model3DS.bIsTextured && t3DObj->nMaterialID >= 0) 
  63.         { 
  64.             if(NULL != m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap)
  65.             {
  66.                 glActiveTexture(m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap);
  67.                 glDisable(GL_TEXTURE_2D);
  68.             }
  69.         }
  70.     }
  71. }

以上为渲染部分,也是遍历网格对象,分别绑定和启用该对象的各个顶点属性VBO,指定数据格式等,以glDrawElements进行Indexed-VBO的数据绘制。类的索引类型必须统一,一般是unsigned short或unsigned int。纹理部分与OBJ一样根据textureObject绑定[OBJ模型文件的结构、导入与渲染Ⅱ],3DS一般只有单一类型的纹理(diffuseTex)。以上为第2步。

  1. ZWModel3DS::~ZWModel3DS()
  2. {   
  3.     //释放VBO
  4.     for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++) 
  5.     {
  6.         glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nPosVBO);
  7.         glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nNormVBO);
  8.         glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nTexcoordVBO);
  9.         glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nIndexVBO);
  10.     }
  11.     m_Model3DS.t3DObjVec.clear();
  12.     m_Model3DS.tMatInfoVec.clear();
  13. }

注销模型的时候才释放VBO,和Vector里的其他非堆上分配的数据。为第三步。以上完成索引VBO的结合。

3DS的chunk块读入基本不变,去除面信息、面法线信息等一般不需要用到的东西,读入索引信息以代替面信息。ZWModel3DS类继承于ZWModelBase类:

  1. //模型基类
  2. class ZWModelBase
  3. {
  4. public:
  5.     ZWModelBase();              
  6.     virtual ~ZWModelBase() = 0;
  7.  
  8.     virtual bool ImportModel(wchar_t *strFileName, GLuint usage = GL_STREAM_DRAW) = 0{ return true; }
  9.  
  10.     void RenderModel();
  11.  
  12.     inline void SetModelResourceDirectory(wchar_t *szDirectory){ m_szResourceDirectory = szDirectory; }
  13.  
  14.         .........//Set  /  Get  函数
  15.   
  16.     virtual GLuint GetMaterialMapHandle(GLuint nMapType, char *szMaterialName) = 0{ return 0; }
  17.  
  18.     virtual bool   SetEnableMaterialMap(GLuint nMapType, GLuint nTextureObject = GL_TEXTURE0) = 0{ return false; }
  19.  
  20.     virtual void   SetEnableModelTexture(bool bEnable) = 0{}
  21.  
  22.     void SetStaticGeometry(bool bStatic = true);
  23.     
  24. protected:
  25.     //绘制模型
  26.     virtual void DrawModel() = 0{}
  27.  
  28.     GLuint LoadModelTexture(wchar_t *szTexPathName, GLint nTexWrapMode = GL_REPEAT);
  29.  
  30.     wchar_t *m_szResourceDirectory;
  31.  
  32.     GLuint  m_nStaticDisplayList;
  33.  
  34.     Vector3 m_vModelPosition;
  35.     Vector3 m_vModelRotation;
  36.     Vector3 m_vModelSize;
  37. };

一般我们会建立一个全局静态的模型管理类(单件),把各个模型抽象成ModelBase供其按需调用。所以这个类是纯虚基类,3DS模型、MD2模型、OBJ模型……等等乃至之后可能会支持的模型格式的导入-渲染类都继承于它,动态定型。这个类中熟悉的是bool ImportModel(wchar_t *strFileName, GLuint usage)和void RenderModel(),应用层必须至少间接地调用它们来进行模型数据的导入和渲染。前者直接由继承类具体实现(usage参数指示VBO的用途以便OpenGL对之优化,对于这种模型绘制,也就是GL_STATIC_DRAW、GL_STREAM_DRAW、GL_DYNAMIC_DRAW[少用],见[索引顶点的VBO与多重纹理下的VBO]);后者其实就是在模型矩阵变换函数后调用具体的渲染执行函数DrawModel(),也是由子类具体实现:

  1. void ZWModelBase::RenderModel()
  2. {
  3.     glPushAttrib(GL_CURRENT_BIT);//保存现有属性
  4.     glPushMatrix();
  5.  
  6.     glTranslatef(m_vModelPosition.x, m_vModelPosition.y, m_vModelPosition.z);
  7.     glScalef(m_vModelSize.x,  m_vModelSize.y,  m_vModelSize.z);
  8.     glRotatef(m_vModelRotation.x,1,0,0);
  9.     glRotatef(m_vModelRotation.z,0,0,1);
  10.     glRotatef(m_vModelRotation.y,0,1,0);
  11.  
  12.     if(m_nStaticDisplayList)
  13.     {
  14.         glCallList(m_nStaticDisplayList);
  15.     }
  16.     else
  17.     {
  18.         DrawModel();
  19.     }
  20.  
  21.     glPopMatrix();
  22.     glPopAttrib();          //恢复前一属性
  23.  
  24. }

LoadModelTexture实现纹理载入功能;SetModelResourceDirectory指定模型的存放目录;SetStaticGeometry实现把渲染改为静态(显示列表模式);除了载入和渲染执行函数外,还有3个函数也是继承类必须要具体实现的:GetMaterialMapHandle其实就是获得对应MapType的纹理ID,供应用层使用;etEnableMaterialMap就是启用某个MapType,并把对应的纹理绑定到指定的TextureObject中,这样方便shader等的调用。MapType应该由模型管理类或者继承类定义,譬如DiffuseMap、BumpMap等等。最后SetEnableModelTexture指定模型是否显示纹理。

显示列表,学OpenGL的同学不会陌生。把数据完全交给OpenGL的server端(显卡/GPU),在该固定区域中驻留的话GPU执行调用就很迅速了。完全超越CPU、DMA、显存的其他部位等等,这样调用显示列表渲染的效率就比glVertex3f打点、VertexArray、VBO要高。代价是不能更改数据。事实上很多场景下的静态模型可以调用下面这个函数,启用静态模式,生成显示列表对象后“一本满足”,渲染时候直接CallList调用该显示列表(默认不是开启的)。注意提供的渲染执行函数(DrawModel)里面不要包含任何矩阵转换的函数。其实这里生成显示列表时候也是用VBO渲染(是否GL_STATIC_DRAW问题不大吧~)对于之后是否撤除VBO留个问。

  1. void ZWModelBase::SetStaticGeometry(bool bStatic)
  2. {
  3.     if(!bStatic && m_nStaticDisplayList)
  4.     {
  5.         glDeleteLists(m_nStaticDisplayList, 1);
  6.  
  7.         m_nStaticDisplayList = 0;
  8.     }
  9.     else if(bStatic && !m_nStaticDisplayList)
  10.     {
  11.         m_nStaticDisplayList = glGenLists(1);
  12.  
  13.         glNewList(m_nStaticDisplayList, GL_COMPILE);
  14.  
  15.         DrawModel();
  16.  
  17.         glEndList();
  18.     }
  19. }

阁下发现有什么BUG或者有什么建议,一定请不吝赐教。在下博客http://www.ZwqXin.com。附上ZWModel类的使用123:

  1. 在ZWModelBase - LoadModelTexture中用自己的纹理导入函数代替;
  2. 在ZWModel[Derived]中更改两个typedef为自己的Vector3类(x,y,z)和Texcoord类(u,v);
  3. 更改代码中的Vector3操作函数为自己的;
  4. 去除ZWTextureMgr.h和ZWVector3.h等

最后展示一下3DS模型,用传统方法和VBO在渲染效率上的区别:

测试中使用了4个简单的车模和一个象征性的teaport模型,3ds文件的平均大小是35k左右。在场景中循环绘制20次(相当于绘制100个平均30多k的模型):

  1. for(int i = 0; i < 20; ++i)
  2. {
  3.     RenderAll();
  4. }

以下为其他条件相近下,传统的面索引glVertex3f打点绘制与使用Indexed-VBO绘制的FPS比照:

  用Indexed-VBO渲染3DS模型

  用Indexed-VBO渲染3DS模型

按照我在[SwapBuffers的等待,虚伪的FPS]说的,这种实时演算的FPS用于标示真实值是不可靠的,但是作为比较用途则很少多时候能说明问题。当然500+的FPS或许有点虚高了(FPS过高过低都会有点“虚”),但至少你也能看出这里头起码有20倍的关系。(这比起[Bmp文件的结构与基本操作(逐像素印屏版)]和 [认识HBITMAP与Bmp操作(整内存拷贝版)]的差别还要揪心。)

要说怎么不多渲染几个pass来看看?以下是VBO方式,循环由20改为100:

  用Indexed-VBO渲染3DS模型

而此时对应的传统方法,真的只显示4FPS了。事实上循环为50左右的时候FPS也没超过10FPS。不忍卒读……

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/idexed-vbo-for-model-3ds-rendering.html

  • quote 1.customer
  • 你好,我想试一下用vbo载入3ds的渲染速度,现在还比较才,能否发一下你的源代码。谢谢!我的邮箱是rizhaohaitao@(yahoo.com)
  • 2011-4-9 20:06:46 回复该留言
  • quote 2.vockey

  • 你好,我想试一下用vbo载入3ds的渲染速度,能否发一下你的源代码。谢谢!我的邮箱是vockey@163.com




  • 2011-5-12 16:35:52 回复该留言
  • quote 3.北国之春
  • 最近拜读了楼主的一些文章,大神呐!能否给发一份ZWModel3DS类的源码,不胜感谢!
  • 2017-2-25 21:03:17 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.