小弟只浅谈一下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为单位,和前面提到的地形一模一样,
如果分材质裁剪多个静态实体,就没问题了
附上我简单更改过的天龙的代码,
用硬件蒙皮代替软件蒙皮,用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; }