以前做DEMO的时候,总是导入几个模型进场景就让FPS哭泣了,当然也知道从OpenGL1.0流传下来的glVertex3f打点渲染法是个务实的伙子,但VBO什么的还是在向我招手,后来整理框架的时候便干脆转向VBO了,另外也好好修理了一下原来的3DS导入类,于是也就有了ZWModel3DS了。于是效率就士别了三日了。——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/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阶内存分配带来的性能耗损:
- // 对象信息结构体
- typedef struct tag3DObject
- {
- int numOfVerts; // 模型中顶点的数目
- int numOfTexcoords; // 模型中纹理坐标的数目
- int numOfIndexes; // 模型中顶点索引数目
- int numOfFaces; // 模型中面的数目[numOfIndexes/3]
- int nMaterialID; // 纹理ID
- bool bHasTexture; // 是否具有纹理映射
- char strName[MAX_NAME]; // 对象的名称
- Vector3 *pVerts; // 对象的顶点
- Vector3 *pNormals; // 对象的法向量
- TexCoord *pTexcoords; // 纹理UV坐标
- unsigned short *pIndexes; // 对象的顶点索引
- GLuint nPosVBO;
- GLuint nNormVBO;
- GLuint nTexcoordVBO;
- GLuint nIndexVBO;
- }t3DObject;
生成(绑定)VBO:
- for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++)
- {
- if(m_Model3DS.t3DObjVec[i].pVerts)
- {
- glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nPosVBO);
- glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nPosVBO);
- glBufferData(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].numOfVerts * sizeof(Vector3),
- (GLvoid*)m_Model3DS.t3DObjVec[i].pVerts, usage);
- }
- if((GLvoid*)m_Model3DS.t3DObjVec[i].pNormals)
- {
- glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nNormVBO);
- .......
- }
- if(m_Model3DS.t3DObjVec[i].pTexcoords)
- {
- glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nTexcoordVBO);
- .......
- }
- if(m_Model3DS.t3DObjVec[i].pIndexes)
- {
- glGenBuffers(1, &m_Model3DS.t3DObjVec[i].nIndexVBO);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nIndexVBO);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].numOfIndexes * sizeof(unsigned short),
- (GLvoid*)m_Model3DS.t3DObjVec[i].pIndexes, usage);
- }
- }
Indexed-VBO的介绍见[索引顶点的VBO与多重纹理下的VBO]。这里遍历网格对象(Object),生成VBO并传输数据,是第一步(导入数据在传输给VBO后应该销毁);
- ///////////////////////////////////////////////////绘制模型
- void ZWModel3DS::DrawModel()
- {
- // 遍历模型中所有的对象
- for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++)
- {
- // 获得当前显示的对象
- t3DObject *t3DObj = &m_Model3DS.t3DObjVec[i];
- // 判断该对象是否有纹理映射
- if(t3DObj->bHasTexture && m_Model3DS.bIsTextured && t3DObj->nMaterialID >= 0)
- {
- glColor3ub(255, 255, 255);
- // 打开纹理映射
- if(NULL != m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap)
- {
- glActiveTexture(m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap);
- glEnable(GL_TEXTURE_2D);
- glBindTexture(GL_TEXTURE_2D, m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].nDiffuseMap);
- }
- }
- else
- {
- glDisable(GL_TEXTURE_2D);
- if(-1 != t3DObj->nMaterialID)
- {
- BYTE *pColor = m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].color;
- glColor3ub(pColor[0], pColor[1], pColor[2]);
- }
- }
- glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nPosVBO);
- glEnableClientState(GL_VERTEX_ARRAY);
- glVertexPointer(3, GL_FLOAT, 0, NULL);
- glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nNormVBO);
- glEnableClientState(GL_NORMAL_ARRAY);
- glNormalPointer(GL_FLOAT, 0, NULL);
- if(m_Model3DS.t3DObjVec[i].nTexcoordVBO)
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nTexcoordVBO);
- glEnableClientState(GL_TEXTURE_COORD_ARRAY);
- glTexCoordPointer(2, GL_FLOAT, 0, NULL);
- }
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Model3DS.t3DObjVec[i].nIndexVBO);
- glDrawElements(GL_TRIANGLES, m_Model3DS.t3DObjVec[i].numOfIndexes, GL_UNSIGNED_SHORT, NULL);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
- if(m_Model3DS.t3DObjVec[i].nTexcoordVBO)
- {
- glDisableClientState(GL_TEXTURE_COORD_ARRAY);
- }
- glDisableClientState(GL_NORMAL_ARRAY);
- glDisableClientState(GL_VERTEX_ARRAY);
- glBindBuffer(GL_ARRAY_BUFFER, NULL);
- if(t3DObj->bHasTexture && m_Model3DS.bIsTextured && t3DObj->nMaterialID >= 0)
- {
- if(NULL != m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap)
- {
- glActiveTexture(m_Model3DS.tMatInfoVec[t3DObj->nMaterialID].TexObjDiffuseMap);
- glDisable(GL_TEXTURE_2D);
- }
- }
- }
- }
以上为渲染部分,也是遍历网格对象,分别绑定和启用该对象的各个顶点属性VBO,指定数据格式等,以glDrawElements进行Indexed-VBO的数据绘制。类的索引类型必须统一,一般是unsigned short或unsigned int。纹理部分与OBJ一样根据textureObject绑定[OBJ模型文件的结构、导入与渲染Ⅱ],3DS一般只有单一类型的纹理(diffuseTex)。以上为第2步。
- ZWModel3DS::~ZWModel3DS()
- {
- //释放VBO
- for(unsigned int i = 0; i < m_Model3DS.t3DObjVec.size(); i++)
- {
- glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nPosVBO);
- glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nNormVBO);
- glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nTexcoordVBO);
- glDeleteBuffers(1, &m_Model3DS.t3DObjVec[i].nIndexVBO);
- }
- m_Model3DS.t3DObjVec.clear();
- m_Model3DS.tMatInfoVec.clear();
- }
注销模型的时候才释放VBO,和Vector里的其他非堆上分配的数据。为第三步。以上完成索引VBO的结合。
3DS的chunk块读入基本不变,去除面信息、面法线信息等一般不需要用到的东西,读入索引信息以代替面信息。ZWModel3DS类继承于ZWModelBase类:
- //模型基类
- class ZWModelBase
- {
- public:
- ZWModelBase();
- virtual ~ZWModelBase() = 0;
- virtual bool ImportModel(wchar_t *strFileName, GLuint usage = GL_STREAM_DRAW) = 0{ return true; }
- void RenderModel();
- inline void SetModelResourceDirectory(wchar_t *szDirectory){ m_szResourceDirectory = szDirectory; }
- .........//Set / Get 函数
- virtual GLuint GetMaterialMapHandle(GLuint nMapType, char *szMaterialName) = 0{ return 0; }
- virtual bool SetEnableMaterialMap(GLuint nMapType, GLuint nTextureObject = GL_TEXTURE0) = 0{ return false; }
- virtual void SetEnableModelTexture(bool bEnable) = 0{}
- void SetStaticGeometry(bool bStatic = true);
- protected:
- //绘制模型
- virtual void DrawModel() = 0{}
- GLuint LoadModelTexture(wchar_t *szTexPathName, GLint nTexWrapMode = GL_REPEAT);
- wchar_t *m_szResourceDirectory;
- GLuint m_nStaticDisplayList;
- Vector3 m_vModelPosition;
- Vector3 m_vModelRotation;
- Vector3 m_vModelSize;
- };
一般我们会建立一个全局静态的模型管理类(单件),把各个模型抽象成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(),也是由子类具体实现:
- void ZWModelBase::RenderModel()
- {
- glPushAttrib(GL_CURRENT_BIT);//保存现有属性
- glPushMatrix();
- glTranslatef(m_vModelPosition.x, m_vModelPosition.y, m_vModelPosition.z);
- glScalef(m_vModelSize.x, m_vModelSize.y, m_vModelSize.z);
- glRotatef(m_vModelRotation.x,1,0,0);
- glRotatef(m_vModelRotation.z,0,0,1);
- glRotatef(m_vModelRotation.y,0,1,0);
- if(m_nStaticDisplayList)
- {
- glCallList(m_nStaticDisplayList);
- }
- else
- {
- DrawModel();
- }
- glPopMatrix();
- glPopAttrib(); //恢复前一属性
- }
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留个问。
- void ZWModelBase::SetStaticGeometry(bool bStatic)
- {
- if(!bStatic && m_nStaticDisplayList)
- {
- glDeleteLists(m_nStaticDisplayList, 1);
- m_nStaticDisplayList = 0;
- }
- else if(bStatic && !m_nStaticDisplayList)
- {
- m_nStaticDisplayList = glGenLists(1);
- glNewList(m_nStaticDisplayList, GL_COMPILE);
- DrawModel();
- glEndList();
- }
- }
阁下发现有什么BUG或者有什么建议,一定请不吝赐教。在下博客http://www.ZwqXin.com。附上ZWModel类的使用123:
- 在ZWModelBase - LoadModelTexture中用自己的纹理导入函数代替;
- 在ZWModel[Derived]中更改两个typedef为自己的Vector3类(x,y,z)和Texcoord类(u,v);
- 更改代码中的Vector3操作函数为自己的;
- 去除ZWTextureMgr.h和ZWVector3.h等
最后展示一下3DS模型,用传统方法和VBO在渲染效率上的区别:
测试中使用了4个简单的车模和一个象征性的teaport模型,3ds文件的平均大小是35k左右。在场景中循环绘制20次(相当于绘制100个平均30多k的模型):
- for(int i = 0; i < 20; ++i)
- {
- RenderAll();
- }
以下为其他条件相近下,传统的面索引glVertex3f打点绘制与使用Indexed-VBO绘制的FPS比照:
按照我在[SwapBuffers的等待,虚伪的FPS]说的,这种实时演算的FPS用于标示真实值是不可靠的,但是作为比较用途则很少多时候能说明问题。当然500+的FPS或许有点虚高了(FPS过高过低都会有点“虚”),但至少你也能看出这里头起码有20倍的关系。(这比起[Bmp文件的结构与基本操作(逐像素印屏版)]和 [认识HBITMAP与Bmp操作(整内存拷贝版)]的差别还要揪心。)
要说怎么不多渲染几个pass来看看?以下是VBO方式,循环由20改为100:
而此时对应的传统方法,真的只显示4FPS了。事实上循环为50左右的时候FPS也没超过10FPS。不忍卒读……
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/idexed-vbo-for-model-3ds-rendering.html