7.3、OpenGLES_Ch7_1示例
例子OpenGLES_Ch7_1在运行时加载了一个modelplist文件,这个modelplist文件包含了一个网格,这个网格带有这个例子需要的所有模型顶点属性和索引,同时还包含了一个所有模型公用的纹理图片。图7-4显示了例子OpenGLES_Ch7_1的输出。

从概念上说,模型是由一个或者多个网格组成的。模型可能会使用不同的坐标系变换和材质属性来绘制每个网格组件。设想一个人物模型的头部转动,但是肩部不转。那么头部和肩部应该是同一个模型内的两个不同的网格。
但是,使用OpenGLES进行绘图最有效的方式是使用驻存在GPU控制的内存中的一个不变的大网格。一个比较好的折中方法是,通过指定模型使用部分所对应的顶点索引的范围,来为每个模型绘制一个单独的大网格中的不同子集。这正是UtilityModel和UtilityMesh类在OpenGLES_Ch7_1中的交互方式。所有的UtilityModel实例共享同一个UtilityMesh实例。模型会引用由网格保存的一个绘图命令数组。不同的命令使用不同的OpenGL ES模型绘制不同的网格子集,例如GL_TRIANGLES或者GL_TRIANGLE_STRIP。三角形带是在第6章讲解的。
在OpenGLES_Ch7_1中的模拟碰碰车使用与OpenGLES_Ch6_1相同的基本模拟逻辑。两个例子中的SceneCar类几乎是相同的。在第6章的例子使用编译到应用中的模型数据的地方,OpenGLES_Ch7_1添加了UtilityModelManager 类来在运行时加载模型数据。
self.modelManager = [ [UtilityModelManager alloc] initwithModelPath:modelsPath];
UtilityModelManager类的initWithModelPath:
方法使用指定路径中的文件的内容初始化了一个NSDataobject实例,接着使用在7.2节讲到的“-readFrom-
Data:ofType:error:”方法来从数据中提取模型、网格和纹理对象。
NSData *data = [ NSData datawithContentsOfFile:aPath options:0 error:&modelLoadingError];
if(nil != data) {
[self readFromData:data ofType:[aPath pathExtension] error: &modelLoadingError];
}
例子OpenGLES_Ch7_1中的UtilityMesh类是第6章的SceneMesh类的一个更通用版本。UtilityMesh 添加了一个用于保存命令数组的属性。UtilityModel 类有用来确定 适用于每个模型的网格绘图命令的范围的属性。
@property (assign, nonatomic, readwrite) NSUInteger indexOfF irstCommand;
@property (assign, nonatomic, readwrite) NSUInteger number0fCommands;
@property (assign, nonatomic, readon1y) AGLKAxisAllignedBoundingBox axisAlignedBoundingBox;
UtilityModels还有一个由COLLADAViewer预计算的axisAlignedBoundingBox属性,并且会与模型一起保存在modelplist文件中。一个坐标轴对齐的边界框在空间上包 含了模型的每个顶点。它会保存在模型网格中发现的最大和最小X、Y、Z值。坐标轴对齐的边界框定义了一个模型所占用的3D空间的体积,并且方便于多种用途。在本例和第6章的OpenGLES_Ch6_1例子中的SceneCar类都是用轴对齐的边界框属性来检测碰碰车之间的碰撞。
UtilityModel的“-draw” 方法会向网格发送一个消息,来让其使用模型命令范围内的命令绘图。
- (void)draw {
[self.mesh drawCommandsInRange :NSMakeRange(indexOfFirstCommand , numberOfCommands_ )];
}
UtilityMesh的“-drawCommandsInRange:" 方法会使用模型命令范围内的命令循环调用glDrawElements() 函数,参见下面的代码。
- (void)drawCommandsInRange: (NSRange)aRange {
if(0 < aRange.length) {
const NSUInteger lastCommandIndex = (aRange.location + aRange.length) - 1;
NSParameterAssert(aRange . location < [ self .commands count]);
NSParameterAssert(lastCommandIndex < [ self. commands count]);
for (NSUInteger i = aRange.location;
i <= lastCommandIndex; i++)
NSDictionary *currentCommand =
[self.commands objectAtIndex:i];
const GLsizei numberofIndices = (GLsizei)[ [currentCommand
objectForKey:@"numberofIndices" ]
unsignedIntegerValue];
const GLsizei firstIndex = (GLsizei){ [ currentCommand}
}
objectForKey:@" firstIndex" ] unsignedIntegerValue];
GLenum mode = (GLenum)[ [ currentCommand
objectForKey:@" command" ] unsignedIntegerValue];
glDrawEl ements (mode,
(GLsizei) number0fIndices ,
GL_UNSIGNED SHORT,
((GLushort *)NULL + firstIndex) );
对比例子OpenGLES_Ch7_1和例子OpenGLES_Ch6_1,归纳一下网格和模型所代表两种核心概念的不同实现方式。即使是两种实现方式,在两个工程中的网格和模型类的能力也是相当相似的,以至于SceneCar类可以同时使用能够相互切换的两种实现方式。最终置于GPU控制内存中的是相同的顶点属性,即使它们来自不同的源。这个概念让使用任意的工具来实现任意的资源管道成为可能,只要网格与模型之间存在这个关键关系。
背面剔除
例子OpenGLES_Ch7_1在OpenGL ES状态机中使用了一个新模式: GL_CULL_FACE。如在第6章提到的,每个三角形都有一个前面和一个后面,并且这个前后是由OpenGLES接收三角形顶点的顺序决定的。剔除指的是一个可选的优化,是OpenGLES用来在执行过多的处理之前及早丟弃顶点属性的。背面剔除是指丢弃那些背离观察者的三角形。当观察一个模拟的实心物体时,背面通常朝向物体内部,因此丢弃背面三 角形并不会影响场景的渲染结果;即使没有剔除背面,也只有从一个实心物体的内部看时,才能看到背面。
OpenGLES_Ch7_1ViewController 开启GL_CULL_FACE并不只是为了优化;从SketchUp加载的模型通常是‘双面” 的。在SketchUp中,这个术语的意思是三角形的前面和后面有不同的材质、不同的颜色、不同的纹理。不设定背面剔除的话,OpenGLES就会试图把“双面”三角形的两面都渲染了,但是这个双面实际上是会产生Z冲突的共面图元。
第5章讲到过Z冲突。在例子OpenGLES_Ch7_1中,Z冲突的意思是帧缓存的像素颜色渲染缓存中的最终片元的颜色是随机的,可能来自‘ 双面”三角形的前面,也可能来自后面。实际上,每个面决定一半的片元,并且三角形会因为片元颜色来回翻转而闪烁。
背面剔除通过只渲染前面而避免了Z冲突。下面来自OpenGLES_Ch7_1ViewController的粗体代码开启了背面剔除。
- (void)glkView: (GLKView *)view drawInRect: (CGRect)rect {
// Clear back frame buffer (erase previous drawing )
// and depth buffer
[((AGLKContext * )view. context) clear :GL_COLOR_ BUFFER_ BIT | GL_DEPTH_BUFFER_BIT];
// Cull back faces: Important! many Sketchup models have back
// faces that cause Z fighting if back faces are not culled.
[( (AGLKContext *)view. context) enable:GL_CUL_FACE];
// Calculate the aspect ratio for the scene and setup a
// perspective projection
const GLfloat aspectRatio = (GLfloat)view.drawableWidth / (GLfloat)view.drawableHeight;
self.baseEffect.transform.projectionMatrix = GLKMatrix4MakePerspective( GLKMathDegreesToRadians(35.0f),
aspectRatio,4.0f, // Don't make near plane too close
20.0f); // Far arbitrarily far enough to contain scene
[self.modelManager prepareToDraw];
[self.baseEffect prepareToDraw];
// Draw the rink
[self.rinkModelFloor draw];
[self.rinkModelWalls draw];
// Draw the cars
[cars makeObjectsPer formSelector: @selector (drawWithBaseBffect: ) with0bject:self.baseBffect];
}