渲染状态管理

文档介绍:  
  提高3D图形性能是个很大课题图形优化大致可以分成两大任务是要有好场景管理能快速剔除不可见多边形并根据对象距相机远近选择合适细节(LOD); 2是要有好渲染能快速渲染送入渲染管线可见多边形   
  我们知道使用OpenGL或Direct3D渲染图形时首先要设置渲染状态渲染状态用于控制渲染器渲染行为应用可以通过改变渲染状态来控制OpenGL或Direct3D渲染行为比如设置Vertex/Fragment Program、绑定纹理、打开深度测试、设置雾效等  
  改变渲染状态对于显卡而言是比较耗时操作而如果能合理管理渲染状态避免多余状态切换将明显提升图形性能这篇文章将讨论渲染状态管理  

文档目录:  
  基本思想  
  实际问题  
  渲染脚本  

文档内容:  

基本思想  
  我们考虑个典型游戏场景包含人、动物、植物、建筑、交通工具、武器等稍微分析下就会发现实际上场景里很多对象渲染状态是比如所有人和动物渲染状态般都所有植物渲染状态也同样建筑、交通工具、武器也是如此我们可以把具有相同渲染状态对象归为然后分组渲染对每组对象只需要在渲染前设置次渲染状态并且还可以保存当前渲染状态设置渲染状态时只需改变和当前状态不状态这样可以大大减少多余状态切换下面代码段演示了这种思路方法:  
   
// 渲染状态组链表由场景管理填充  
RenderStateGroupList groupList;  
// 当前渲染状态  
RenderState curState;  

……  

// 遍历链表中每个组  
RenderStateGroup *group = groupList.GetFirst;  
while ( group != NULL )  
{   
     // 设置该组渲染状态  
     RenderState *state = group->GetRenderState;  
     state->ApplyRenderState( curState );  

     // 该渲染状态组对象链表  
     RenderableObjectList *objList = group->GetRenderableObjectList;  
     // 遍历对象链表每个对象  
     RenderableObject *obj = objList->GetFirst;  
     while ( obj != NULL )  
     {  
         // 渲染对象  
         obj->Render;  

         obj = objList->GetNext;  
     }  

     group = groupList.GetNext;   
}  
   
其中RenderState类ApplyRenderState思路方法形如:   
void RenderState::ApplyRenderState( RenderState &curState )   
{  
     // 深度测试   
      ( depthTest != curState.depthTest )  
     {  
         SetDepthTest( depthTest );  
         curState.depthTest = depthTest;  
     }  

     // Alpha测试  
      ( alphaTest != curState.alphaTest )  
     {  
         SetAlphaTest( alphaTest );  
         curState.alphaTest = alphaTest;  
     }  

     // 其它渲染状态  
     ……  
}      

  这些分组渲染状态般被称为Material或Shader这里Material区别于OpenGL和Direct3D里面用于光照材质Shader也区别于OpenGL里面Vertex/Fragment Program和Direct3D里面Vertex/Pixel Shader而是指封装了显卡渲染图形需要状态(也包括了OpenGL和Direct3D原来Material和Shader)  

  从字面上看Material(材质)更侧重于对象表面外观属性描述而Shader(这个词实在不好用中文表示)则有用控制对象表面外观含义由于显卡可编程管线引入渲染状态中包含了Vertex/Fragment Program这些小可以控制物体渲染所以我觉得将封装渲染状态称为Shader更合适这篇文章也将称的为Shader  

  上面代码段只是简单演示了渲染状态管理基本思路实际上渲染状态管理需要考虑很多问题  
渲染状态管理问题  
   

 消耗时间问题  
  改变渲染状态时区别状态消耗时间并不甚至在区别条件下改变渲染状态消耗时间也不比如绑定纹理是个很耗时操作而当纹理已经在显卡纹理缓存Cache中时速度就会非常快而且随着硬件和软件Software发展些很耗时渲染状态消耗时间可能会有减少因此并没有个准确消耗时间数据  

  虽然消耗时间无法量化情况区别消耗时间也不般来说下面这些状态切换是比较消耗时间:  

Vertex/Fragment Program模式和固定管线模式切换(FFFixed Function Pipeline)   

Vertex/Fragment Program本身切换   

改变Vertex/Fragment Program常量   

纹理切换   

顶点和索引缓存Cache(Vertex & Index Buffers)切换   

  有时需要根据消耗时间多少来做折衷下面将会遇到这种情况   

    

 渲染状态分类  
  实际场景中往往会出现这样情况类对象其它渲染状态都只是纹理和顶点、索引数据区别比如场景中只是身材、长相、服装等区别也就是说只有纹理、顶点、索引数据区别而其它如Vertex/Fragment Program、深度测试等渲染状态都相反般不会存在纹理和顶点、索引数据相同而其他渲染状态区别情况我们可以把纹理、顶点、索引数据不归入到Shader中这样场景中所有人都可以用个Shader来渲染然后在这个Shader下对纹理进行分组排序相同纹理人放在起渲染  
 多道渲染(Multipass Rendering)  
  有些比较复杂图形效果在低档显卡上需要渲染多次每次渲染种效果然后用GL_BLEND合成为最终效果这种思路方法叫多道渲染Multipass Rendering渲染次就是个pass比如做逐像素凹凸光照需要计算环境光、漫射光凹凸效果、高光凹凸效果在NV20显卡上只需要1个pass而在NV10显卡上则需要3个passShader应该支持多道渲染个Shader应该分别包含每个pass渲染状态  

    区别pass往往渲染状态和纹理都区别而顶点、索引数据是这带来个问题:是以对象为单位渲染次渲染个对象所有pass然后渲染下个对象;还是以pass为单位渲染次渲染所有对象个pass第 2次渲染所有对象第 2个pass下面段演示了这两种方式:  

  以对象为单位渲染   
   
// 渲染状态组链表由场景管理填充  
ShaderGroupList groupList;  

……  

// 遍历链表中每个组  
ShaderGroup *group = groupList.GetFirst;  
while ( group != NULL )  
{   
     Shader *shader = group->GetShader;  
   
     RenderableObjectList *objList = group->GetRenderableObjectList;  

     // 遍历相同Shader每个对象  
     RenderableObject *obj = objList->GetFirst;  
     while ( obj != NULL )  
     {  
         // 获取shaderpass数  
          iNumPasses = shader->GetPassNum;  
         for ( i = 0; i < iNumPasses; i )
{
// 设置shader第i个pass渲染状态
shader->ApplyPass( i );  
             // 渲染对象  
             obj->Render;  
         }  

         obj = objList->GetNext;  
     }  
   
     group = groupList->GetNext;  
}  
     

以pass为单位渲染   
    
// 渲染状态组链表由场景管理填充  
ShaderGroupList groupList;  
   
……  
    
for ( i = 0; i < MAX_PASSES_NUM; i )
{
// 遍历链表中每个组
ShaderGroup *group = groupList.GetFirst;
while ( group != NULL )
{
Shader *shader = group->GetShader;  
          iNumPasses = shader->GetPassNum;  
         // 如果shaderpass数小于循环次数跳过此shader  
         ( i >= iNumPasses )  
         {  
             group = groupList->GetNext;  
             continue;  
         }  

         // 设置shader第i个pass渲染状态  
         shader->ApplyPass( i );  

         RenderableObjectList *objList =   
             group->GetRenderableObjectList;  
   
         // 遍历相同Shader每个对象  
         RenderableObject *obj = objList->GetFirst;  
         while ( obj != NULL )  
         {  
             obj->Render;  

             obj = objList->GetNext;  
         }  

         group = groupList->GetNext;  
     }  
}  
    

      
  这两种方式各有什么优缺点呢?  

  以对象为单位渲染渲染个对象个pass后马上紧接着渲染这个对象第 2个pass而每个pass顶点和索引数据是相同因此第个pass将顶点和索引数据送入显卡后显卡Cache中已经有了这个对象顶点和索引数据后续pass不必重新将顶点和索引数据拷到显卡因此速度会非常快而问题是每个pass渲染状态都区别这使得实际上每次渲染都要设置新渲染状态会产生大量多余渲染状态切换  

  以pass为单位渲染则正好相反以Shader分组相同Shader对象起渲染可以只在这组开始时设置次渲染状态相比以对象为单位大大减少了渲染状态切换可是每次渲染对象区别因此每次都要将对象顶点和索引数据拷贝到显卡会消耗不少时间  
  可见想减少渲染状态切换就要频繁拷贝顶点索引数据而想减少拷贝顶点索引数据又不得不增加渲染状态切换鱼和熊掌不可兼得 :-(  
  由于硬件条件和场景数据情况比较复杂具体哪种思路方法效率较高并没有定式两种思路方法都有人使用具体选用那种思路方法需要在实际环境测试后才能知道  
   

 多光源问题  
待续……  

   

 阴影问题  
待续……  


   

渲染脚本  
  现在很多图形都会自己定义种脚本文件来描述Shader  

  比如较早OGRE(Object-oriented Graphics Rendering Engine面向对象图形渲染引擎)Material脚本Quake3Shader脚本以及刚问世不久Direct3DEffect FilenVIDIACgFX脚本(文件格式和Direct3D Effect File兼容)ATI RenderMonkey使用xml格式脚本OGRE Material和Quake3 Shader这两种脚本比较有历史了不支持可编程渲染管线而后面 3种比较新脚本都支持可编程渲染管线  

   

脚本  特性  范例   
OGRE Material 封装各种渲染状态不支持可编程渲染管线  >>>>   
Quake3 Shader 封装渲染状态支持些特效不支持可编程渲染管线  >>>>   
Direct3D Effect File 封装渲染状态支持multipass支持可编程渲染管线  >>>>   
nVIDIA CgFX脚本 封装渲染状态支持multipass支持可编程渲染管线  >>>>   
ATI RenderMonkey脚本 封装渲染状态支持multipass支持可编程渲染管线  >>>>   

   

  使用脚本来控制渲染有很多好处:  

可以非常方便修改个物体外观而不需重新编写或编译   

可以用外围工具以所见即所得方式来创建、修改脚本文件(类似ATI RenderMonkey工作方式)便于美工、关卡设计人员设定对象外观建立外围工具和图形引擎联系   

可以在渲染时将相同外观属性及渲染状态对象(也就是Shader相同对象)归为然后分组渲染对每组对象只需要在渲染前设置次渲染状态大大减少了多余状态切换  
Tags: 

延伸阅读

最新评论

发表评论