10.4、拾取
例子OpenGLES_Ch10_1使用专门的伪色彩渲染来实现拾取。当识别到一个用户触摸时,会使用UtilityPickTerrainEffect类来把场景渲染到一个 像素颜色渲染缓存。地形的X和Z位置坐标被编码到片元的红和绿颜色分量中。例如,基于地形网格的{150,45,78}位置生成的片元的颜色为浮点RGB颜色{150.0 / terrainWidth, 78.0 /terrainLength, 0.0}。然后从像素颜色渲染缓存中读取触摸位置之下的像素颜色。检测这个像素颜色的红和绿分量以确定触摸到的地形的位置。
用户是看不到伪色彩渲染的,因为用于拾取的像素颜色渲染缓存永远不会显示到屏幕上。无论摄像机的位置和方向怎么变化,这个方法都有效。在前景中渲染的地形会自动地挡住背景中的地形,因此返回的总是触摸位置处的与视点最近的地形位置。图10-6用于帮助形象化伪色彩场景。UtilityPickTerrainEffect会渲染到一个 OpenGL ES帧缓存对象( frame buffer object- FBO)中。帧缓存是在第1章介绍的。帧缓存会接收渲染结果。截止到现在的例子使用的都是用于显示到屏幕上的帧缓存。CocoaTouch的GLKView类创建了一个默认的帧缓存和一个与视图的Cocoa Touch CALayer共享的附加像素颜色渲染缓存。UtilityPick TerrainEffect类

会创建一个附加的帧缓存,这个帧缓存是FBO类型的,而不是CALayers。下面的UtilityPickTerrainInfo数据类型和UtilityPickTerrainEffect类接口展示了拾取有多么简单。在“-initWithTerrain:”方法中使用地形初始化了一个Utility-PickTerrainEffect实例后,调用这个实例用于确定一个触摸事件位置的“ -terrainInfoForProjectionPosition:”方法,这个调用会返回包含这个位置所对应的地形X和Z坐标的一个UtilityPickTerrainInfo结构。
//
// TEPickTerrainEffect.h
//
//
#import "UtilityEffect.h"
#import <GLKit/GLKit.h>
@class TETerrain;
@class TEModelManager;
typedef struct
{
GLKVector2 position;
unsigned char modelIndex;
}
TEPickTerrainInfo;
@interface UtilityPickTerrainEffect : UtilityEffect
@property(assign, nonatomic, readwrite)
GLKMatrix4 projectionMatrix;
@property(assign, nonatomic, readwrite)
GLKMatrix4 modelviewMatrix;
@property(assign, nonatomic, readwrite)
unsigned char modelIndex;
// Designated initializer
- (id)initWithTerrain:(TETerrain *)aTerrain;
- (TEPickTerrainInfo)terrainInfoForProjectionPosition:
(GLKVector2)aPosition;
@end
传递给,“terrainInfoForProjectionPosition:"的位置是处在相对于被触摸的GLKView的位置和大小的Cocoa Touch坐标系中的。返回的UtilityPickTerrainInfo提供 了在3D虚拟世界中的对应坐标。
UtilityPickTerrainE£fect的FBO没有必要与用于在屏幕上显示的帧缓存的大小相匹配。实际上,一个FBO的最佳大小是让尺寸为2的幂。无论用于在屏幕上显示的帧缓存的大小是多少,UtilityPickTerrainEffect 的FBO的大小都是512X512的。2的幂的尺寸是在第3章讲解的,这是非常重要的,因为每个FBO通常都会与一个OpenGL ES纹理缓存共享像素颜色渲染缓存存储空间。渲染到一个FBO通常叫做“渲染到纹理”,并且产生的纹理可以用于接下来的渲染。例子OpenGLES_Ch10_1没有重用与FBO相;关联的纹理缓存,不过苹果提供了GLEssentials示例程序,这个例子会使用一个FBO来渲染在一个场景自身内的场景倒影。这个例子的下载地址为: http://developer.apple.com/library/ios/samplecode/GLEssentials/Introduction/Intro.html
UtilityPick TerrainEffect类使用下面的方法创建并配置了它的用于拾取的FBO。这段代码与例子GLEssentials和用来介绍GLKView的实现方式的例子OpenGLES_Ch2_2中的代码相似。用来创建作为FBO的像素颜色渲染缓存的纹理的代码与用于介绍GLKTextureLoader的工作方式的例子OpenGLES_Ch3_2中的代码相似。
/////////////////////////////////////////////////////////////////
// Build a Frame Buffer Object with attached Pixel Color Render
// Buffer and Depth Buffer to receive the results of rendering
// in false color for picking.
-(GLuint)buildFBOWithWidth:(GLuint)fboWidth
height:(GLuint)fboHeight
{
GLuint fboName;
GLuint colorTexture;
// Create a texture object to apply to model
glGenTextures(1, &colorTexture);
glBindTexture(GL_TEXTURE_2D, colorTexture);
// Set up filter and wrap modes for this texture object
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,
GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
// Allocate a texture image we can render into
// Pass NULL for the data parameter since we don't need to
// load image data. We will be generating the image by
// rendering to this texture.
glTexImage2D(GL_TEXTURE_2D,
0,
GL_RGBA,
fboWidth,
fboHeight,
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
NULL);
GLuint depthRenderbuffer;
glGenRenderbuffers(1, &depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16,
fboWidth, fboHeight);
glGenFramebuffers(1, &fboName);
glBindFramebuffer(GL_FRAMEBUFFER, fboName);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, colorTexture, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) !=
GL_FRAMEBUFFER_COMPLETE)
{
NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
UtilityPickTerrainEffectDestroyFBO(fboName);
return 0;
}
#ifdef DEBUG
{ // Report any errors
GLenum error = glGetError();
if(GL_NO_ERROR != error)
{
NSLog(@"GL Error: 0x%x", error);
}
}
#endif
return fboName;
}
UtilityPickTerrainEffect会使用下面的两个方法销毁它的FBO并把资源返回给;OpenGL ES,这两个方法与苹果公司提供的例子GLEssentials中的代码是相似的。
static void UtilityPickTerrainEffectDestroyFBO(GLuint fboName)
{
if(0 == fboName)
{
return;
}
glBindFramebuffer(GL_FRAMEBUFFER, fboName);
// Delete the attachment
UtilityPickTerrainEffectDeleteFBOAttachment(
GL_COLOR_ATTACHMENT0);
// Delete any depth or stencil buffer attached
UtilityPickTerrainEffectDeleteFBOAttachment(
GL_DEPTH_ATTACHMENT);
glDeleteFramebuffers(1, &fboName);
}
/////////////////////////////////////////////////////////////////
// This function deletes the specified attachment and returns
// resources to OpenGL
static void UtilityPickTerrainEffectDeleteFBOAttachment(
GLenum attachment)
{
GLint param;
GLuint objName;
glGetFramebufferAttachmentParameteriv(
GL_FRAMEBUFFER,
attachment,
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
¶m);
if(GL_RENDERBUFFER == param)
{
glGetFramebufferAttachmentParameteriv(
GL_FRAMEBUFFER,
attachment,
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
¶m);
objName = ((GLuint*)(¶m))[0];
glDeleteRenderbuffers(1, &objName);
}
else if(GL_TEXTURE == param)
{
glGetFramebufferAttachmentParameteriv(
GL_FRAMEBUFFER,
attachment,
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
¶m);
objName = ((GLuint*)(¶m))[0];
glDeleteTextures(1, &objName);
}
}
UtilityPickTerrainEffect的“ -prepareToDraw”方法会设置让OpenGL ES渲染到 FBO中,参见下面的代码。
- (void)prepareToDraw;
{
[super prepareToDraw];
glBindFramebuffer(GL_FRAMEBUFFER, pickFBO);
glViewport(0, 0, TEPickTerrainFBOWidth,
TEPickTerrainFBOHeight);
}
在使用UtilityPickTerrainEffect 渲染了这个场景后,使用“-terrainInfoForProjection Position:”方法来 读取在0.0到1.0范围内的指定位置的像素颜色,然后把这个像素颜色 还原为3D的X和Z坐标:
- (TEPickTerrainInfo)terrainInfoForProjectionPosition:
(GLKVector2)aPosition
{
GLubyte pixelColor[4]; // Red, Green, Blue, Alpha color
GLint readLocationX = MIN((TEPickTerrainFBOWidth - 1),
(TEPickTerrainFBOWidth - 1) * aPosition.x);
GLint readLocationY = MIN((TEPickTerrainFBOHeight - 1),
(TEPickTerrainFBOHeight - 1) * aPosition.y);
glReadPixels(readLocationX,
readLocationY,
1,
1,
GL_RGBA,
GL_UNSIGNED_BYTE,
pixelColor);
#ifdef DEBUG
{ // Report any errors
GLenum error = glGetError();
if(GL_NO_ERROR != error)
{
NSLog(@"GL Error: 0x%x", error);
}
}
#endif
TEPickTerrainInfo result;
GLKVector2 position = {
self.width * (GLfloat)pixelColor[0] / // red component
TEPickTerrainMaxIndex,
self.length * (GLfloat)pixelColor[1] / // green component
TEPickTerrainMaxIndex
};
result.position = position;
result.modelIndex = pixelColor[2]; // blue component
return result;
}
注意
把渲染值从FBO读回CPU控制的内存的过程可能需要执行费时的同步操作。CPU必须要等待GPU渲染完场景,而且在GPU能够接着渲染其他的东西到FBO之前,GPU必须要等待CPU读取完成。OpenGLES会自动管理所有必要的同步,但是永远无法完全避免同步延迟。拾取在每秒中至多发生几次,而理想情况下普通渲染每秒可以发生30或者60次。例子OpenGLES_Ch10_1可以接受一个基于颜色拾取的折中方案,但是其他的应用可能需要使用不同的折中 方案。
最后一点,就像UtilityPickTerrainEffect会把地形顶点坐标编码到每个渲染像素的红色和绿色分量中,可选的modelIndex是编码到蓝色分量中的。如果从FBO读取的这 个像素的蓝色分量的值不是0,那么这个蓝色分量会被缩放到1到255的范围内,并会对应于在拾取位置的模型的索引。根据编码方案,在任何一个场景中,任意的一个时刻 至多能有255个可用于拾取的模型,但这已经足以满足例子OpenGLES_Ch_10_1所需的模型的数量。 联合使用UtilityPickTerrainEffect 和下面的OpenGL ES 2.0 Shading Language程序来使用用于拾取的伪色彩去渲染地形和模型:
//
// UtilityPickTerrainShader.vsh
//
//
/////////////////////////////////////////////////////////////////
// VERTEX ATTRIBUTES
/////////////////////////////////////////////////////////////////
attribute vec3 a_position;
/////////////////////////////////////////////////////////////////
// TEXTURE
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4 u_mvpMatrix;
uniform highp vec2 u_dimensionFactors;
uniform lowp float u_modelIndex;
/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying lowp vec4 v_color;
void main()
{
float r = a_position.x * u_dimensionFactors.x;
float g = a_position.z * u_dimensionFactors.y;
v_color = vec4(r, g, u_modelIndex, 1.0);
gl_Position = u_mvpMatrix * vec4(a_position, 1.0);
}
备选拾取方式
从历史上说,OpenGL的桌面版支持使用glRenderMode(GL SELECT函数来为拾取设定一个专门的渲染模式。现在不提倡使用GL_SELECT 拾取模式,因为现在很少有GPU能够从硬件上支持这个功能。使用GLSELECT渲染模式通常会迫使OpenGL退化到基于软件的渲染,即运行在CPU上而不是GPU上,同时在这个过程中GPU会处于闲置状态。
纯粹的几何拾取方式可以完全避免任何额外渲染。最常见的几何方式叫做光线投射法(ray casting)。设想一个光线从平截体近面上的一个触摸位置投射向与这个触摸位置对 应的平截体远面上的一个点。被这个光线穿过的离视点最近的对象就是要拾取的对象。
不幸的是,完全使用类似光线投射法的几何方式通常会带来很多的软件复杂性并且相交对象的检索可能需要高昂的计算代价。除非用于检测相交的计算执行得非常快速,并且要测试的对象相对较少,要不然几何方式可能要比颜色拾取方式带来更多的延迟。物理引擎Bullet3DGameMultiphysics提供了最先进的光线和3D对象之间的碰撞检测方法。如果你已经在你的应用中使用过Bullet3D来实现类似重力加速和对象碰撞检测的物理效果,那么你应该知道这个基于光线投射法的拾取几乎是没有性能损失的。Bullet Collision Detection可以独立于Bullet 3D库来单独使用。很多的iOs应用会使用Bullet 3D,它可以免费用于商业用途,并且遵循开源ZLib许可条款: http://code.google.com/p/bullet/