ogre渲染优化

来源:互联网 发布:哪里买域名不用备案 编辑:程序博客网 时间:2024/05/17 20:45
  做商业网络游戏的话,效率是一个不可避免的话题,为了留更多的空间给客户端的逻辑,那么渲染模块就应该尽量高效.

小弟只浅谈一下Ogre的渲染优化,以大家熟悉的天龙八部为例.

 

以网上流传版本的天龙代码来看,Ogre和CEGUI部分的渲染都有严重的性能问题,

 

Ogre:

       地形的实现,一个tile,只按材质做了批次优化,并没有按材质做摄像机的裁剪,导致游戏视角下批次增加很多

       静态实体的合并,完全错误的做法,同地形一样,只优化了批次,没做摄像机的裁剪.

CEGUI:

       由于CEGUI本身对字体重绘处理的缺陷,导致缓存的字体没用,每帧都会计算位置排版之类,效率其低,改进的方法当然有,

       比如普通字体可以当图片去缓存,最好的方式还是移植到0.75,效率立马提高N倍.

 

 

CEGUI我就懒得说了,只谈谈Ogre部分的优化.

 

Renderable和渲染批次, MovableObject和摄像机裁剪, SimpleRenderable和地形

优化之前,先理解优化的原理.

 

1).Renderable和渲染批次:

      渲染批次是神马?自己去看教程,简单讲,他极大地影响渲染的效率,所以渲染批次尽量少.比如10个批次,渲染10000个三角形,比10000个批次,渲染10个三角形速度快得多,

     怎么才算一个批次?渲染一个Renderable就算一个批次,Renderable是Ogre中最小的渲染单元,所有需要渲染的对象都继承与此.

一句话:一个renderable等于一个batching(渲染批次),减少场景中的renerable,就减少了批次.

     怎么减少renderable?比如2棵材质一样的树(2个renderable),我可以合成一个棵树(1个renderable),合成办法2种,一种是美工在max里面静态合并,一种是程序动态合并(StaticGeometry)

 

值得一提的是,一个mesh不一定是一个批次,submesh才是,如果一个mesh有2个不同材质的submesh,正常,

如果有2个同材质的submesh,不好,应该立即找美工,骂他一顿.说:为什么不把同材质的2个submesh合成一个?

 

2)MovableObject和摄像机裁剪

     Ogre的OctreeSceneManager的摄像机裁剪,是以包围盒为准,那么就意味着有包围盒的对象才可以被裁剪,Renderable有包围合么?没有!但是MovableObject就有,MovableObject是移动对象的基本,同时继承ShadowCaster,可以投射阴影.

值得一提的是,摄像机的裁剪并不是以MovableObject而单位,而是以SceneNode为单位,只有挂接到SceneNode上的MovableObject才

会被裁剪,

3)要渲染,Renderable,要移动,Moveable,如果都要,最简单的对象应该就是SimpleRenderable : public MovableObject, public Renderable

想起以前自己做地形用mesh去做,很笨,效率不行,应该mesh类太大,包含了太多不需要的东西,用SimpleRenderable是最高效率的.

 

 

地形的实现,一种是用拼格子,每个格子一个四边形,不共用顶点,不能lod,但是一个格子就可以多层纹理,适合2.5D视角游戏,比如魔兽3和天龙

另一种就是主流3D游戏做法,共用顶点,lod,缺点是一个地形page的纹理层数和前面那种地形的一个格子一样多...比如8层,其实已经足够,纹理过渡也很自然.拼格子的纹理过渡始终很丑.

 

 

 优化天龙八部的地形

1)天龙的地形实现原理如下.

 

整个地形分很多tile组合而成,class TerrainTile : public Ogre::MovableObject

 

一个tile是一个MovableObject,用来摄像机裁剪.

每个tile包含一个RenderableList;一个tile里面,同材质的格子会被做成一个Renderable,

tile大小事32,那么有32*32 个格子,假如这些格子用了10种不同的材质,那么这些格子就被做成10个Renderable

 

渲染没问题,按材质分批次渲染,10个批次,

但是摄像机裁剪有问题,摄像机只能按tile去裁剪,因为tile是MovableObject,而那10个Renderable却不能单独裁剪

 

这样的话,比如,摄像机只看到这个tile.32*32格子中最边上的一个格子,也会把整个tile渲染,10个批次....

如果把10个Renderable都做成MovableObject,可以裁剪,那么只看到最边上这个格子的话,只会渲染最边上的那个Renderable,1个批次.

 

实践证明,对于2.5D视角游戏,地形的批次几乎可以降低一半

 

2)tile的大小

tile设想多大,效率最高?这和摄像机有关,最好是游戏视角,摄像机刚好看那么大的范围再大一点点,天龙的32,对于他的视角,是合理的.

 

3)texture atlas

 

天龙的地表的纹理很小,128*256 导致材质很多,批次就很多,如果做texture atlas把小纹理合成大纹理,批次就会减少很多,但是也会有其他一些附加问题,比如mipmap导致的缝隙,解决缝隙,国外其实有不少解决方法, 比如可以写shader,在不同的mipmap下调整纹理坐标,或者预留纹理坐标,在已有纹理上加一圈和边缘相同的像素,这样就不会取到其他纹理像素,导致缝隙.

天龙2增加了天空视角,即使看到很多场景,渲染效率却没有受到多大影响,所以个人认为,他应该做了texture atlas,减少了整个场景的批次,不然实现不了天空视角.

 

正确地合并静态实体

 

天龙的静态实体合并方法是典型的反面教材.把所有物体都合并在成一个静态实体了

正确的做法应该是按材质合并.不然效率可能会适得其反

 

具体原因,和前面讲的地形是一样的,

 

Ogre的一个静态实体,是分region的,一个region类似于一个地形的tile

 

而裁剪都是以region为单位,和前面提到的地形一模一样,

 

如果分材质裁剪多个静态实体,就没问题了

 

附上我简单更改过的天龙的代码,

 

[cpp:showcolumns] view plaincopyprint?
·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150
  1. //-----------------------------------------------------------------------  
  2. void System::bakeStaticGeometries(size_t maxMemoryUsage)  
  3. {  
  4.     // 2011.2.24 LuoYinan 不要学天龙那样把不同材质的物体合并成一个静态实体,  
  5.     // 虽然这样做简单,而且StaticGeometry内部会自动按材质合并,但是包围合却只有一个(一个region一个)  
  6.     // 这样摄像机的裁剪优化会大打折扣,可能你合并了还没有不合并速度快...   
  7.     // 正确的做法应该是按材质(至少也要按mesh)合并静态实体,这里我们按mesh分类  
  8.   
  9.     clearStaticGeometries();  
  10.   
  11.     Ogre::StaticGeometry* sg = 0;  
  12.     int size = getTerrainData()->mTileSize * getTerrainData()->getScale().x;  
  13.   
  14.     // 1.build mesh bucket   
  15.     typedef std::multimap<Ogre::ResourceHandle, WX::ObjectPtr> MeshBucketMap;  
  16.     MeshBucketMap meshBucket;  
  17.     WX::Scene::ObjectsByTypeRange objests = mSceneInfo->findObjectsByType(WX::StaticEntityObject::msType);  
  18.     for (WX::Scene::ObjectsByTypeIterator it = objests.first; it != objests.second; ++ it)  
  19.     {  
  20.         const ObjectPtr& object = *it;  
  21.         EntityList entities;  
  22.         object->queryBakableEntities(entities);  
  23.   
  24.         if (!entities.empty())  
  25.         {  
  26.             Ogre::ResourceHandle handle = entities.front()->getMesh()->getHandle();  
  27.             meshBucket.insert( MeshBucketMap::value_type(handle, object) );  
  28.         }  
  29.     }  
  30.   
  31.     // 2.bake StaticGeometry   
  32.     Ogre::ResourceHandle last = 0;  
  33.     int i = 0;  
  34.     MeshBucketMap::iterator it = meshBucket.begin();  
  35.     for (; it != meshBucket.end(); ++it)  
  36.     {  
  37.         if (it->first != last) // key changed  
  38.         {  
  39.             // build old   
  40.             if (sg)  
  41.             {  
  42.                 sg->build();  
  43.                 //if (isEditable())  
  44.                 //{   
  45.                     //sg->dump(sg->getName() + ".txt");  
  46.                 //}   
  47.             }  
  48.   
  49.             // create new   
  50.             sg = mSceneManager->createStaticGeometry("StaticGeometry" + Ogre::StringConverter::toString(i));  
  51.             sg->setRegionDimensions(Ogre::Vector3(size, size, size));  
  52.             mStaticGeometries.push_back(sg);  
  53.             ++ i;  
  54.         }  
  55.   
  56.         // bake   
  57.         ObjectPtr& object = it->second;  
  58.         EntityList entities;  
  59.         object->queryBakableEntities(entities);  
  60.         object->bakeStaticGeometry(sg, entities);  
  61.   
  62.         last = it->first;  
  63.     }  
  64.   
  65.     // 最后一个,bulid   
  66.     if (sg)  
  67.     {  
  68.         sg->build();  
  69.         //if (isEditable())   
  70.         //{   
  71.             //sg->dump(sg->getName() + ".txt");  
  72.         //}   
  73.     }  
  74. }  

 

 

用硬件蒙皮代替软件蒙皮,用GPU代替CPU

 

骨骼动画的计算,是比较费时的,因为数据量大.

这部分数据,用CPU计算,就是软件蒙皮,用GPU去计算,就硬件蒙皮

 

1.软件蒙皮

 

Ogre本身已经实现了软件蒙皮,默认情况下就是软件蒙皮,计算的代码在void Entity::updateAnimation(void)

 

2.硬件蒙皮

 

需要用shader,把计算放到GPU里面去处理,而shader的代码,也不用自己去写,Ogre本身例子就带有一个支持2个权重的顶点程序,

因为一个顶点最多绑定4个骨头,最多4个权重,所以顶点也稍微麻烦,要4种情况都实现,那么,配置顶点程序就应该在代码里面动态添加,而不是写在.material里面.

更好的是用火炬之光的硬件蒙皮,有一套完整的实现,

 

我们需要硬件蒙皮么?答案是现在的每个3D游戏,都应该有.

天龙和火炬之光都用到了硬件蒙皮,因为他能大大地提高运行速度,

而火炬之光,唯一用到的shader程序,就硬件蒙皮,然后就找不到其他任何shader程序了.

所以,硬件蒙皮应该算是最重要的shader程序,效率啊.

 

我们用Ogre本身的例子来测试速度

 

 

 

 

6个人,132和198的fps,差距还是不小,

 

 

//////////////////////////////////////////////////////////////////////////////////////

另外,如果用了shader程序,那么我前面提到的XRAY的实现方法就要改了

 

因为前面的方法是针对固定渲染管线的,我是禁止渲染状态改变的方法来实现的,如果有shader程序,禁止了就不行

 

不过可以采用另外的方法,就是等pass的属性设置到渲染状态以后,我再更改渲染状态,在哪改?在SubEntity中重写下面的函数,

就可以在渲染前任意更改了

  virtual bool preRender(SceneManager* sm, RenderSystem* rsys)
                { (void)sm; (void)rsys; return true; }

 

原创粉丝点击