10.5、优化
下面OpenGLES_Ch10_1ViewController 的“-drawTerrainAndModels” 方法的代码会在每次这个方法被调用时不加判断地绘制所有的地形瓦片和模型:
- (void)drawTerrainAndModels
{
TETerrain *terrain = [[self dataSource] terrain];
if(nil == self.tiles)
{ // Cache tiles
self.tiles = terrain.tiles;
}
// The terrain is opaque, so there is no need to blend.
glDisable(GL_BLEND);
[terrain drawTerrainWithinTiles:self.tiles
withCamera:self.camera
terrainEffect:self.terrainEffect];
// Assume subsequent rendering involves translucent objects.
glEnable(GL_BLEND);
// Configure modelEffect for texture and diffuse lighting
self.modelEffect.texture2D =
[self.dataSource.modelManager textureInfo];
self.modelEffect.projectionMatrix =
self.camera.projectionMatrix;
self.modelEffect.modelviewMatrix =
self.camera.modelviewMatrix;
[self.modelEffect prepareToDraw];
[self.dataSource.modelManager prepareToDraw];
[terrain drawModelsWithinTiles:self.tiles
withCamera:self.camera
modelEffect:self.modelEffect
modelManager:self.dataSource.modelManager];
}
例子OpenGLES_Ch10_2相比例子OpenGLES_Ch10_1做了一些温和、简单的改变,尽管如此,例子OpenGLES_Ch10_2仍然近乎加倍了每秒可以产生的帧数。例子OpenGLES_Ch10_2遵循了第9章的尽可能少渲染的忠告。基于平截体的剔除可以避免渲染看不到的瓦片。下面的OpenGLES__Ch10_2ViewController的‘-draw TerrainAndModels”方法的实现可以提供一个更理想的结果,参见下面的粗体代码:
- (void)drawTerrainAndModels
{
TETerrain *terrain = [[self dataSource] terrain];
if(nil == self.tiles)
{ // Cache tiles
self.tiles = terrain.tiles;
}
NSMutableArray *fullDetailTiles = [NSMutableArray array];
NSMutableArray *simplifiedTiles = [NSMutableArray array];
[terrain identifyTilesToDraw:self.tiles
withCamera:self.camera
fullDetail:fullDetailTiles
simplified:simplifiedTiles
simplificationDistanceTiles:
OpenGLES_Ch10_2DefaultSimplifaicationDistanceInTiles];
// The terrain is opaque, so there is no need to blend.
glDisable(GL_BLEND);
[terrain drawTerrainWithinFullDetailTiles:fullDetailTiles
simplifiedTiles:simplifiedTiles
withCamera:self.camera
terrainEffect:self.terrainEffect];
// Assume subsequent rendering involves translucent objects.
glEnable(GL_BLEND);
// Configure modelEffect for texture and diffuse lighting
self.modelEffect.texture2D =
[self.dataSource.modelManager textureInfo];
self.modelEffect.projectionMatrix =
self.camera.projectionMatrix;
self.modelEffect.modelviewMatrix =
self.camera.modelviewMatrix;
[self.modelEffect prepareToDraw];
[self.dataSource.modelManager prepareToDraw];
[terrain drawModelsWithinTiles:fullDetailTiles
withCamera:self.camera
modelEffect:self.modelEffect
modelManager:self.dataSource.modelManager];
}
例子OpenGLES_Ch10_2会添加“-identifyTiles ToDraw:withCamera:fullDetail: sim-plified:simplificationDistanceTiles:"方法到TETerrain+viewAdditions.m文件中。这个方法会遍历所有的地形瓦片并剔除看不到的瓦片,因为它们没有与当前摄像机的位置和方向所对应的视平截体相交。这个方法还会把可视的地形瓦片放在两个不同的数组中。接近摄像机位置的地形瓦片会放人要详细渲染的那个瓦片数组中。远处的瓦片会被放在要以简单的形式渲染的瓦片数组中。
- (void)identifyTilesToDraw:(NSArray *)someTiles
withCamera:(UtilityCamera *)aCamera
fullDetail:(NSMutableArray *)fullDetailTiles
simplified:(NSMutableArray *)simplifiedTiles
simplificationDistanceTiles:(GLfloat)aNumberOfTiles;
{
const AGLKFrustum *frustumForCulling =
[aCamera frustumForCulling];
const GLfloat tileMetersPerUnit =
self.metersPerUnit;
const GLfloat tileWidthMeters = TETerrainTileDefaultWidth *
tileMetersPerUnit;
const GLfloat tileLengthMeters = TETerrainTileDefaultLength *
tileMetersPerUnit;
const GLfloat tileRadiusMeters = hypotf(tileWidthMeters,
tileLengthMeters);
const GLKVector3 cameraPositionMeters = aCamera.position;
const GLfloat simplificationDistance =
aNumberOfTiles * MAX(tileWidthMeters, tileLengthMeters);
const GLfloat simplificationDistanceSquared =
simplificationDistance * simplificationDistance;
NSAssert(NULL != frustumForCulling, @"No valid frustum");
for(TETerrainTile *tile in someTiles)
{
GLfloat originXMeters = tile.originX * tileMetersPerUnit;
GLfloat originYMeters = tile.originY * tileMetersPerUnit;
GLKVector3 centerMeters = GLKVector3Make(
originXMeters + tileWidthMeters/2,
0,
originYMeters + tileLengthMeters/2);
if(AGLKFrustumOut != AGLKFrustumCompareSphere(
frustumForCulling, centerMeters, tileRadiusMeters))
{ // Some part of the tile is visible
if(simplificationDistanceSquared > distanceSquared(
cameraPositionMeters, centerMeters))
{ // Remember to draw the tile with full detail
[fullDetailTiles addObject:tile];
}
else
{ // Remember to draw the tile simplified
[simplifiedTiles addObject:tile];
}
}
}
}
在所有的瓦片中做剔除可以明显地改善渲染的速度,因为大部分的地形可以被剔除掉,并且在剔除的地形瓦片中的模型也会被剔除掉。即使在瓦片不能被整个剔除时,通常仍然可以用简化的几何图形来渲染它们。用户一般察觉不到在远处的瓦片中的任何可视细节的减少。
例子OpenGLES_Ch10_2会以三角形扇的形式绘制简化的瓦片,而不是三角形带。三角形扇是glDrawArrays(和glDrawElements()函数所支持的三个OpenGL ES三角形 模式之一。GL_TRIANGLES模式会让OpenGL ES使用所提供的每三个连续的顶点组来组成三角形。GL_TRIANGLE_STRIP模式是在第6章介绍的,是渲染网格的一个最佳方式,具体是通过让每个三角形与前一个三角形共享两个顶点来组成三角形的。GL_TRIANGLE_FAN模式是让所有的三角形共享一个“中心”点,并且让每个三角形与前一个三角形共享一个边缘顶点来组成三角形的。
图10-7展示了与地形瓦片的几何图形相似的三角形扇,但是每个边只有八个顶点。在图10-7中的箭头标示了OpenGLES处理顶点的顺序。第一个顶点是“中心”点,但它并不是必须要在这个图形的中心。在三角形扇中的所有三角形共享这个“中心”点。在图10-7中的三角形扇包含28个三角形,因为在这个图形中有29个顶点,但是第一个顶点是所有三角形共享的。

使用GL_ TRIANGLE FAN模式来渲染简化的地形可以减少要渲染的三角形的数量。做一个比较,32X32顶点的地形瓦片的网格拥有31行和31列。当以GLTRIANGLE_STRIP模式渲染时,每个地形瓦片包含(31X31X2) +31=1953个三角形,因为在每行每列中的每个矩形都是由两个三角形组成的,并且每行都包含一个“ 退化”的三角形,这个“退化”的三角形没有空间但是保留了顶点的处理顺序。与此相反,由地形瓦片的中心顶点和所有的边缘顶点形成的三角形扇只包含(4X31) =124个三角形。瓦片有四个边,每个边有31行或者31列,所有三角形共享中心点。下面的代码是TETerrain+viewAdditions.m文件中的“-drawSimplifiedTiles:" 方法的实现,这个实现的工作方式与“-drawTiles:"方法的实现方式是相似的,除了其中的一行粗体代码。
- (void)drawSimplifiedTiles:(NSArray *)tiles;
{
for(TETerrainTile *tile in tiles)
{
// Set the pointer to the first vertex position in the
// tile
glVertexAttribPointer(
TETerrainPositionAttrib,
3,
GL_FLOAT,
GL_FALSE,
sizeof(GLKVector3),
((GLKVector3 *)NULL) +
(tile.originY * self.width) +
tile.originX);
[tile drawSimplified];
}
}
在TETerrainTile类中的“ -drawSimplified”方法的实现创建了一个包含在这个瓦片中的中心顶点和所有边缘顶点的顶点索引的元素数组缓存。接着使用GL_TRIANGLE_FAN模式调用glDrawElements()函数来渲染这个瓦片。
- (void)drawSimplified;
{
if(0 == simplifiedIndexBufferID_ &&
0 < [self.simplifiedIndexData length])
{ // Indices haven't been sent to GPU yet
// Create an element array buffer for mesh indices
glGenBuffers(1, &simplifiedIndexBufferID_);
NSAssert(0 != self.simplifiedIndexBufferID,
@"Failed to generate element array buffer");
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
self.simplifiedIndexBufferID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
[self.simplifiedIndexData length],
[self.simplifiedIndexData bytes],
GL_STATIC_DRAW);
// No longer need local index storage
self.simplifiedIndexData = nil;
}
else
{
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,
self.simplifiedIndexBufferID);
}
glDrawElements(GL_TRIANGLE_FAN,
self.numberOfSimplifiedIndices,
GL_UNSIGNED_SHORT,
((GLushort *)NULL));
}
仔细检查在TETerrainTile.m文件中的“generateIndicesForTileOriginX:tileOriginY: tileWidth:tileLength:”和“ -generateSimplifiedIndicesForTileOriginX:tileOriginY:tileWidth:tileLength:”方法,学习一下这两个方法是怎么确定每个地形瓦片的详细的和简化的顶点索引的。这两个方法是相对简单的,仿效的是第6.2.1节中的例子。
OpenGLES_Ch10_2中的优化只会影响两个类,TETerrainTile和TETerrain,并且这个优化会加倍显示刷新率。在第一代iPad上,例子OpenGLESCh101在渲染所包 含的例子地形时可以在每秒内获得大约14帧(14 FPS)。例子OpenGLES_Ch10_2在渲染同一个地形时可以稳定地实现大约30帧。未优化前iPad 2可以实现大约24帧,优化后可以稳定地实现50帧,甚至更多。
其他潜在优化方式
大量学术论文探索了关于实时地形渲染的优化。专注游戏的网站Gamasutra.com为最著名的算法之一实时优化 自适应网格(Real-time Optimally Adapting Mesh, ROAM, 由Mark Duchaineau等人提出)提供了一个说明和示例代码,地址为: http://www.gamasutra.com/view/feature/3188/realtimedynamic_level_of_detail.php ROAM算法可以动态自适应地形网格的细节级别,以用最少的三角形实现高视觉质量。但是渲染少量的不需要的三角形比让CPU去计算真正的最小数量的效率要更高。
ROAM的大部分实现还需要频繁地更新存储在GPU控制的内存中的顶点数据。当优化时,通常必须要考虑CPU、GPU和内存复制之间的平衡。例子OpenGLES_Ch10_2在CPU影响很小并且不用更新顶点数据的条件下提供了可接受的质量。
网页: http://vterrain.org/LOD/Papers/ 上有少量的论文和示例,它们介绍了一些:“超越”ROAM的方法。当前研究探索的是使用运行在GPU上的OpenGLESShading Language程序来动态控制细节级别的问题。这些技术会把高度值编码到纹理中,利用MIP贴图来影响内置的用于选择顶点高度GPU纹理取样逻辑,甚至是在顶点间插补以提供比现存纹理更多的细节。MIP贴图和纹理插补是在第3章讲解的。不幸的是,这个ShadingLanguage方法要依赖于顶点程序的纹理取样能力。所有兼容OpenGLES的GPU都支持片元程序内的纹理取样,但是只有很少支持顶点程序内的纹理取样,虽然OpenGL ES 2.0标准作为一个选项包含了这个能力。在例子OpenGLES_Ch10_1和例子OpenGLES_Ch10_2 中的UtilityTerrainEffect类都是使用下面的语句来确定在顶点程序中可用的纹理取样单元的数量。
glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS,
&maxVertexTextureImageUnits);
不幸的是,在编写本书时没有一个在售的iOs设备支持顶点程序内的任何纹理取样单元。如果苹果公司提供改进的OpenGLES驱动的话,其中一些iOS设备使用的GPU可能会支持顶点程序内的纹理取样。
10.6 小结
地形渲染为大型顶点数据集改进了网格渲染技术。瓦片集合会把地形顶点数据分割成足够小的适合glDrawElements()函数调用的块。这些瓦片还组成了一个简单的场景图,以实施类似第9章介绍的基于视平截体的剔除。
基于编码在帧缓存内的颜色信息的拾取实现了快速简单地确定用户触摸到的地形坐标的方法。MacOSX平台,上的TerrainEditor应用选择放置在地形内的模型时也使用这 种方法。基于颜色的拾取的优势是实现起来相对简单并且相对快速;以用于拾取的伪色彩来做渲染所要耗费的时间量与这个拾取所耗费的时间量是大体相同的,也就是说基于颜色的拾取所做的计算跟普通渲染相当。几乎所有的基于颜色的拾取的运算都是运行在GPU上的,而不是CPU。尽管如此,其他的类似光线投射的更多依靠CPU的拾取方式可能在某些应用中工作得更好。例如,如果一个应用已经使用了一个物理引擎,那么使 用基于光线的拾取可能就不需要额外的性能开销了。
TerrainEditor应用会加载编码到图片文件中的像素亮度作为地形高度值。在例子OpenGLES_Ch10_1和OpenGLES_Ch10_2中的基于颜色的拾取过程中,会多次使用到可以与位置矢量(XYZ)相转换的颜色矢量(RGB)。地形的X和Z坐标被编码在一个FBO的附属像素颜色渲染缓存的R和G分量中。
第11章会回顾3D渲染所需的常见数学运算。以矢量表示的几何图形起初可能看似有悖常理,但是矢量运算几乎完全是由GPU能够快速、并行执行的加法和乘法组成的。第11章将为之前介绍的常见矢量运算提供简明的参考和解释。