10.1、地形的实现
第6章介绍了用于有效表示复杂几何图形的网格。网格存储了共享顶点或者边的三角形集合。除了可以表示类似第6章中的碰碰车的模型,网格还能定义连绵起伏的丘陵和山谷以提供跨越千里的地形效果。例子OpenGLES_Ch10_1演示了地形网格的加载和显示。图10-1显示的是一个简化的地形网格。

10.1.1 高度图
地形网格会形成在坐标系的X、Z平面上的矩形网格。网格中每个顶点的Y位置对应于一个高度值(也称为“海拔”)。图10-2显示的是图10-1中的相同地形的网格线。

硬编码一个复杂网格中的每个顶点的位置是不现实的。获得高度值的最简单方式是在运行时从文件中加载它们。很多3D建模工具都具有生成地形高度数据的功能。类似美国地质调查局(U.S. Geological Survey,网址: http://earthexplorer.usgs.gov) 的网站免费提供了包含真实世界的数字高程模型(DigitalElevationModel,DEM)数据的文件。DEM数据存在很多格式。当前的标准是多分辨率地形高程数据2010(Multi-resolution Terrain Elevation Data 2010,GMTED2010),可以使用美国地质调查局的基于Web的GMTED_viewer来把这种标准格式转换成普通的图片文件。
例子OpenGLES_Ch10_1会从一个紧凑二进制文件中加载高度值,这个二进制文件是由Mac OS X平台.上的TerrainEditor应用生成的,TerrainEditor 应用的源码位于:http://opengles.cosmicthump.com/earning-opengl-es-sample-code/.TerrainEditor.app,这 个应用可以读取普通的图片文件,然后把这个图片中每个像素的亮度值解析为在0.0.(黑)到1.0 (白)范围内的标准化值。例子OpenGLES_Ch10_1会加载并缩放标准化的高度值以产生想要的地形。就实现细节来说,TerrainEditor.app 和例子OpenGLES_Ch10_1都是使用苹果公司的Core Data框架来实现紧凑的二进制表示形式的。
注意 CoreData使用数据库概念简化了MacOSX和iOs应用的模型数据的存储、恢 复和处理。Core Data自动化了用来管理相关联对象的常见任务,比如变更验 证和内置的撤销/重做任务。把Core Data与GLKit和OpenGL ES结合起来可 以提供图形较少的应用具有的相同优势。参考网页: http://developer.apple.com/technologies/ios/data-management.html。
图10-3显示的是用于为例子OpenGLES_Ch10_1提供高度值的图像。这个图像有300像素高,300 像素宽,代表了一个边长为3千米的正方形。这个图像的90000个像素(300X 300)中: 的每一个像素都对应于矩形网格中的一个X、z位置,同时每个像素的亮度值决定了每个位置的Y值。在这个网格中的每个矩形都是由两个三角形组成的。因此,要绘制整个地形必须要渲染180000个三角形。

10.1.2 地形瓦片
在前面的例子中,整个网格是在对gIDrraw-Elements()函数的一次调用中绘制的。一个网格包含90000个顶点,180000个三角形,这远远超过了OpenGL ES的glDrawElements()函数的极限。因此,例子OpenGLES Ch10 1会以多个叫做瓦片(tiles) 的较小的组,向glDrawElement()函数提交网格数据。整个地形网格是用 TETerrain类存储的,TETerrain类是CoreData自动生成的,具体生成自一个在Xcode中图形化创建的模型,参见图10-4。
让Xcode生成与一个Core Data模型对应的Objective-C类并不总是必要的。但是,在例子OpenGLES_Ch10_1中,这样做可以方便地生成所要使用的Objective-C类,可以方便地添加与数据存储不相关的方法。有一个技巧:不要修改生成的代码,因为如果哪天你想修改数据模型,你可能需要重新生成它们。重新生成会清除你对前面生成的代码所做的所有编辑。不过,你可以在单独的文件中使用Objective-C类别向这些生成的类添加方法。

注意
Objective-C 类别用于向现存类添加方法,即使现存类的源码不可用或者需要避免对现存/生成的类做编辑。类别让使用多个文件来实现一个类成为可能。使用类别添加的方法与在主类实现中定义的方法在运行时是没有区别的。要了解更 多信息,请访问:http://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Category.html
下面的代码定义了TETerrain 类的类别。类别通常是基于被扩展的类的名字来命名的。这个例子包含了命名为TETerrain+viewAdditions和TETerrain+ modelAdditions的类别。类别添加的方法会获取关于地形的信息,把整个地形分割成瓦片,并绘制瓦片。
//
// TETerrain+viewAdditions.h
// TerrainEditor
//
#import "TETerrain+modelAdditions.h"
#import <GLKit/GLKit.h>
@class GLKTextureInfo;
@class UtilityCamera;
@class UtilityTerrainEffect;
@class UtilityPickTerrainEffect;
@class UtilityModelEffect;
@class UtilityModelManager;
/////////////////////////////////////////////////////////////////
// The only vertex attribute needed for terrain rendering is
// "position".
typedef enum
{
TETerrainPositionAttrib,
TETerrainNumberOfAttributes
} TETerrainAttribute;
@interface TETerrain (viewAdditions)
- (NSArray *)tiles;
- (void)prepareTerrainAttributes;
- (void)drawTerrainWithinTiles:(NSArray *)tiles
withCamera:(UtilityCamera *)aCamera
terrainEffect:(UtilityTerrainEffect *)aTerrainEffect;
- (void)drawModelsWithinTiles:(NSArray *)tiles
withCamera:(UtilityCamera *)aCamera
modelEffect:(UtilityModelEffect *)aModelEffect
modelManager:(UtilityModelManager *)modelManager;
- (void)prepareToPickTerrain:(NSArray *)tiles
withCamera:(UtilityCamera *)aCamera
pickEffect:(UtilityPickTerrainEffect *)aPickEffect;
@end
//
// TETerrain+modelAdditions.h
// OpenGLES_Ch12_1
//
#import "TETerrain.h"
#import <GLKit/GLKit.h>
@interface TETerrain (modelAdditions)
- (GLfloat)calculatedHeightAtXPosMeters:(GLfloat)x
zPosMeters:(GLfloat)z
surfaceNormal:(GLKVector3 *)aNormal;
- (GLfloat)calculatedHeightAtXPos:(GLfloat)x
zPos:(GLfloat)z
surfaceNormal:(GLKVector3 *)aNormal;
- (GLfloat)heightAtXPos:(NSInteger)x zPos:(NSInteger)z;
- (GLfloat)heightAtXPosMeters:(GLfloat)x zPosMeters:(GLfloat)z;
- (GLfloat)maxHeightNearXPosMeters:(NSInteger)x
zPosMeters:(NSInteger)z;
- (GLfloat)regionalHeightAtXPosMeters:(NSInteger)x
zPosMeters:(NSInteger)z;
- (BOOL)isHeightValidAtXPos:(NSInteger)x zPos:(NSInteger)z;
- (BOOL)isHeightValidAtXPosMeters:(NSInteger)x
zPosMeters:(NSInteger)z;
- (GLfloat)widthMeters;
- (GLfloat)heightMeters;
- (GLfloat)lengthMeters;
@end
类型TETerrainAttribute定义了用于渲染地形网格的OpenGLES顶点属性。“-tiles” 方法会返回一个瓦片数组,里面的每个瓦片引用了总地形的一个子集。这些 瓦片是在“-tiles”方法第一次被调用时按需创建的。‘-prepare TerrainAttributes' ”方法会配置用来渲染地形的OpenGLES状态,包括顶点属性缓存的创建。在调用了-prepareTerrainAttributes” 方法后,接着调用“-drawTiles:(NSArray *)tiles” 方法来渲染在tiles数组中的指定tiles所引用的地形网格部分。在类别中剩下的方法会检索这个地形的尺寸以及地形中各个位置的高度。
例子OpenGLES_Ch10_1通过TETerrainTile类封装了瓦片。TETerrainTile 的每个实例都会管理一个OpenGL ES元素数组缓存,这个缓存包含了整个地形的顶点子集的索引。元素数组缓存是在第6章讲解的。比较有意义的工作是在TETerrainTile类的-draw”方法中执行的,这些工作包括绑定一个元素数组缓存并调用glDrawElements()函数以提交需要处理的顶点。
//
// TETerrainTile.h
// TerrainViewer
//
#import <Foundation/Foundation.h>
@class TETerrain;
@interface TETerrainTile : NSObject
@property (assign, nonatomic, readonly) NSInteger originX;
@property (assign, nonatomic, readonly) NSInteger originY;
@property (strong, nonatomic, readonly) NSSet *
containedModelPlacements;
- (id)initWithTerrain:(TETerrain *)aTerrain
tileOriginX:(NSInteger)x
tileOriginY:(NSInteger)y
tileWidth:(NSInteger)aWidth
tileLength:(NSInteger)aLength;
- (void)draw;
- (void)drawSimplified;
- (void)manageContainedModelPlacements:(NSSet *)somePlacements;
- (NSSet *)containedModelPlacements;
@end
static const NSInteger TETerrainTileDefaultWidth = 32;
static const NSInteger TETerrainTileDefaultLength = 32;
浏览在例子OpenGLES_Ch10_1中的TETerrain+viewAdditions类别和TETerrainTile 类的实现。每个方法的实现都很简短。大部分代码与前面章节中管理和渲染网格的代码相似或者相同。
10.1.3 地形效果
这里的自定义OpenGL ES Shading Language程序是通过结合四个“细节”纹理来渲染地形的,“细节”纹理是根据存储在被称为lightAndWeightsTextureInfo的第五个纹理中的混合权重来混合的。当渲染网格时,可以把这些纹理联合起来产生类似隆起、涟漪、树叶、泥土,或者沙子的效果。lightAndWeightsTextureInfo 纹理还包含一个定向光的预计算烘焙灯光效果。第4章简短地介绍了把灯光烘焙进纹理的方法。下面的UtilityTerrainEffect类会管理用于地形渲染的OpenGLES Shading Language程序
//
// UtilityTerrainEffect.h
//
//
#import "UtilityEffect.h"
#import <GLKit/GLKit.h>
@class TETerrain;
@class UtilityTextureInfo;
@interface UtilityTerrainEffect : UtilityEffect
@property (assign, nonatomic, readwrite)
GLKVector4 globalAmbientLightColor;
@property (assign, nonatomic, readwrite)
GLKMatrix4 projectionMatrix;
@property (assign, nonatomic, readwrite)
GLKMatrix4 modelviewMatrix;
@property (assign, nonatomic, readwrite)
GLKMatrix3 textureMatrix0;
@property (assign, nonatomic, readwrite)
GLKMatrix3 textureMatrix1;
@property (assign, nonatomic, readwrite)
GLKMatrix3 textureMatrix2;
@property (assign, nonatomic, readwrite)
GLKMatrix3 textureMatrix3;
@property (strong, nonatomic, readwrite)
UtilityTextureInfo *lightAndWeightsTextureInfo;
@property (strong, nonatomic, readwrite)
UtilityTextureInfo *detailTextureInfo0;
@property (strong, nonatomic, readwrite)
UtilityTextureInfo *detailTextureInfo1;
@property (strong, nonatomic, readwrite)
UtilityTextureInfo *detailTextureInfo2;
@property (strong, nonatomic, readwrite)
UtilityTextureInfo *detailTextureInfo3;
// Designated initializer
- (id)initWithTerrain:(TETerrain *)aTerrain;
- (void)prepareToDraw;
@end
在UtilityTerrainEffect类中Shading Language程序会使用被称为textureMatrix0、textureMatrix1、textureMatrix2 和textureMatrix3的纹理矩阵来分别变换每个纹理,纹理矩阵textureMatrix0、textureMatrix1、 textureMatrix2 和textureMatrix3会分别影响detailTextureInfo0到detailTexturelnfo3。在使用TETerrain的“ -drawTiles:”方法提交地形顶点属性之前,会调用UtilityTerrainEffect的“-prepareToDraw" 方法。
注意
例子 OpenGLES_Ch10_1实现了多个“Effect” 类,这些类封装了分别用于地形渲染、模型渲染和拾取的ShadingLanguage程序。为了避免不必要的代码复制, 例子OpenGLES_Ch10_1会使用抽象基类UtilityEffect来加载、编译、链接和验证Shading Language程序。UtilityEffect 遵守GLKit的GLKNamedEffect协议。 在Objective-C语言环境中,UtilityEffect是 “抽象的”,因为UtilityEffect 实现 了“-bindAttribLocations" 和“- configureUniformLocations”方法,以便当它们 被直接调用时可以生成异常。UtilityEffect 的子类必须要重写这两个方法。
//
// UtilityTerrainShader.vsh
//
//
/////////////////////////////////////////////////////////////////
// VERTEX ATTRIBUTES
/////////////////////////////////////////////////////////////////
attribute vec3 a_position;
/////////////////////////////////////////////////////////////////
// TEXTURE
/////////////////////////////////////////////////////////////////
#define MAX_TEXTURES 5
#define MAX_TEX_COORDS 5
/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4 u_mvpMatrix;
uniform highp mat3 u_texMatrices[MAX_TEXTURES];
uniform sampler2D u_units[MAX_TEXTURES];
uniform lowp vec4 u_globalAmbientColor;
/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying highp vec2 v_texCoords[MAX_TEX_COORDS];
void main()
{
vec3 coords = u_texMatrices[0] * a_position;
v_texCoords[0] = vec2(coords.x, coords.z);
coords = u_texMatrices[1] * a_position;
v_texCoords[1] = vec2(coords.x, coords.z);
coords = u_texMatrices[2] * a_position;
v_texCoords[2] = vec2(coords.x, coords.z);
coords = u_texMatrices[3] * a_position;
v_texCoords[3] = vec2(coords.x, coords.z);
coords = u_texMatrices[4] * a_position;
v_texCoords[4] = vec2(coords.x, coords.z);
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
}
使用权重因数混合纹理颜色与在第7章中介绍的使用权重来影响顶点位置的方式大致相同。UtilityTerrainEffect 和Utility TerrainShader会使用detailTextureInfo3来提供默认的地形片元颜色。换句话说,如果所有的纹理混合权重都是零,那么地形的每个片元颜色都100%来自detailTextureInfo3内的纹素。基于权重混合的工作方式就像是在第3章讲解的GL_ONE_MINUS_SRC_ALPHA混合函数。OpenGL ES Shading Language甚至提供了一个内置的叫做mix(first_color,second_color,weight)的函数来返回表达式((1.0- weight) first_color) + (weight second color) 的值。在下面的片元着色器 中,由detailTextureInfo3提供的默认textureColor会基于weights.x来与textureColor0混合,weights.x 对应于lightAndWeightsTextureInfo纹素的红色分量。接着,使用weights.y把textureColor1混合进来,weights.y 对应于lightAndWeightsTextureInfo纹 素的绿色分量。最后,使用weights.z把textureColor2混合进来,weights.z 对应于lightAndWeightsTextureInfo纹素的蓝色分量。
在lightAndWeightsTextureInfo中的纹素的最后一个颜色分量通常被解析为透明度或者在一个(X, Y, Z,W)坐标系中的W,不过在这里,最后一个分量定义了每个渲 染片元的灯光强度。灯光强度是在Mac OS X平台上的TerrainEditor应用中预计算的,存储在由例子OpenGLES_Ch10_1所加载的lightAndWeightsTextureInfo中。
//
// UtilityTerrainShader.fsh
//
//
/////////////////////////////////////////////////////////////////
// TEXTURE
/////////////////////////////////////////////////////////////////
#define MAX_TEXTURES 5
#define MAX_TEX_COORDS 5
/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4 u_mvpMatrix;
uniform highp mat3 u_texMatrices[MAX_TEXTURES];
uniform sampler2D u_units[MAX_TEXTURES];
uniform lowp vec4 u_globalAmbientColor;
/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying highp vec2 v_texCoords[MAX_TEX_COORDS];
void main()
{
// Extract light color from w component of light and weight
// texture
lowp vec4 lightAndWeights =
texture2D(u_units[0], v_texCoords[0]);
lowp vec4 lightColor = u_globalAmbientColor + lowp vec4(
lightAndWeights.w,
lightAndWeights.w,
lightAndWeights.w,
1.0);
// Extract texture mixing weights from light and weight
// texture
lowp vec3 weights = lowp vec3(
lightAndWeights.x,
lightAndWeights.y,
lightAndWeights.z);
// Blend the terrain textures using weights
lowp vec4 textureColor0 = texture2D(u_units[1], v_texCoords[1]);
lowp vec4 textureColor1 = texture2D(u_units[2], v_texCoords[2]);
lowp vec4 textureColor2 = texture2D(u_units[3], v_texCoords[3]);
lowp vec4 textureColor3 = texture2D(u_units[4], v_texCoords[4]);
lowp vec4 textureColor = textureColor3;
textureColor = mix(textureColor, textureColor0, weights.x);
textureColor = mix(textureColor, textureColor1, weights.y);
textureColor = mix(textureColor, textureColor2, weights.z);
// Scale by light color
lowp vec4 color = lightColor * textureColor;
// Final terrain color is always opaque
color.a = 1.0;
gl_FragColor = color;
}
截止到现在这个例子的TETerrain、TETerrainTile 和UtilityTerrainEffect类会共同努力来有效地渲染复杂的地形网格。10.5节会讨论怎么使用在第9章介绍的剔除和场景 图技术来改善地形渲染性能的问题。