5.2、例子OpenGLES_Ch5_1和例子OpenGLES_Ch5_2
例子OpenGLES_Ch5_1 和OpenGLES_Ch5_2都渲染一个代表地球的球体。定向灯光模拟从太阳照向地球的光线。一个映射到球体的纹理显示了熟悉的大陆、海洋和天气。图5-1的左图为来自OpenGLES_Ch5_1的屏幕截图,右图为来自OpenGLES_Ch5_2的截图。

左图显示的地球看起来不像是个球体,它看上去像是被压扁或者拉伸而成为一个鸡蛋的形状。拉伸渲染效果已经在第2章和第3章中的例子中提到过,它的发生是因为默认的OpenGLES坐标系会映射沿着X轴的从-1.0到1.0的值到屏幕的宽度,并 映射沿着Y轴的从-1.0到1.0的值到屏幕的高度。然而,屏幕并不是正方形,它的高要比宽的值大。一个半径为0.5的球体被拉伸显示在屏幕上,这是因为沿着Y轴的0.5的距离要比沿着X轴的0.5的距离覆盖更多的屏幕像素。
例子OpenGLES_Ch5_1和例子OpenGLES_Ch5_2几乎是相同的,但是OpenGLES_Ch5_2使用了一个变换来矫正拉伸的渲染效果。
在OpenGLES_Ch5_1中的OpenGLES_Ch5_1ViewController.m 文件中包含了sphere.h文件,这个文件包含球体的顶点位置、法向量和纹理坐标。在sphere.h中的数据是由一个Perl语言脚本生成的,这个脚本可以解析Wavefront.obj文件并输出对应的C语 言代码。文件格式“ .obj”是由Wavefront Technologies 公司在20世纪80年代末开发和推广的。这个文件格式仍然流行是因为它用软件解析起来很容易。本章例子使用的sphere.obj数据文件是使用免费开源的3D内容创建工具Blender创建的,这个工具可以在https://www.blender.org/上获得。sphere.obj 数据文件和用于解析它的Perl脚本都在本章例子的Resources 目录里,这个例子可以从https://pengles.cosmicthump.com/learning-opengl-es-sample-code/Examples.zip上获得。
注意截止到现在的所有例子都是 硬编码的顶点数据。除了类似-1、0和1的最简单常量之外,硬编码其他的任何东西通常并不是一个好主意。这些例子提供了与数据简单显示使用的代码相似的顶点数据定义代码,不过第4章中只用8个顶点描述 一个三角锥的例子已经达到了硬编码的实际极限。在例子OpenGLES_Ch5_1和OpenGLES_Ch5_2中的球体有1944个顶点。想象一下硬编码那么多的数据,即使你不需要全部键入。一些3D场景由数百万个顶点组成。实际上,类似Blender或 者免费的谷歌SketchUp应用(http://sketchup.google.com/) 的3D编辑器会在嵌入式应用之后读取的文件中生成大量的顶点数据。sphere.h 文件是一-个折中方法,它包含了工具生成的数据,但被格式化为能够被直接编译的C代码。
下面的OpenGLES_Ch5_1ViewController.m 文件结合了迄今为止介绍过的很多OpenGL ES概念。“-viewDidLoad” 方法添加了使用一个深度缓存和多个顶点缓存的代码,参见粗体显示的代码:
// Called when the view controller's view is loaded
// Perform initialization before the view is asked to draw
- (void )viewDidLoad {
[super viewDidLoad];
// Verify the type of view created automatically by the
// Interface Builder storyboard
GLKView *view = (GLKView *)self.view;
NSAssert ([view isKindOfClass:[GLKView class]],@"View controller's view is not a GLKView" ); ,
view.drawableDepthFormat = GLKViewDrawableDepthFormat16;
// Create an 0penGL ES 2.O context and provide it to the
// view
view.context = [[AGLKContext alloc ] initWithAPI:kEAGLRenderingAPI0penGLES2];
// Make the new context current
[AGLKContext setCurrentcontext:view.context];
// Create a base effect that provides standard OpenGL ES 2.O
//shading language programs and set constants to be used for
// all subsequent rendering
self.baseEffect = [[GLKBaseEffect alloc] init];
// Configure a light to simulate the Sun
self.baseEffect.light0.enabled = GL_TRUE;
self.baseEffect.light0.diffuseColor = GLKVector4Make(
0.7f, // Red
0.7f, // Green
0.7f, // Blue
1.0f);// Alpha
self.baseEffect.light0.ambientColor = GLKVector4Make (
0.2f, // Red
O.2f, // Green
0.2f, // Blue
1.0f);// Alpha
self.baseEffect.light0.position = GLKVector4Make(
1.0f, //Off to the right of the Earth
0.0f,
-0.8f, //A bit in front of the Earth
0.0f);
// Setup texture
CGImageRef imageRef = [[UIImage imageNamed:"Earth512x256.jpg"] CGImage];
GLKTextureInfo *textureInfo =
[GLKTextureLoader textureWithCGImage:imageRef
options:[NSDictionary dictionarywithObjectsAndKeys:[NSNumber numberWithBool:YES],
GLKTextureLoaderOriginBottomLeft ,
nil ]
error:NULL];
self.baseEffect.texture2d0.name = textureInfo.name;
self.baseEffect.texture2d0.target = textureInfo.target;
// Set the background color stored in the current context
( (AGLKContext * )view.context).clearColor = GLKVector4Make(
0.0f, // Red
0.0f, // Green
0.0f, // Blue
1.0f);// Alpha
// Create vertex buffers containing vertices to draw
self.vertexPositionBuffer = [[AGLKVertexAttribArrayBuffer al1oc]
initWithAttribStride:(3★sizeof (GLfloat))
numberOfVertices:sizeof(sphereVerts) 1 (3 * sizeof (GLfloat))
data:sphereVerts
usage:GL_STATIC_DRAN] ;
self.vertexNormalBuffer = [ [AGLKVertexAttribArrayBuffer alloc]
initwithAttribStride:(3★sizeof(GLfloat))
numberofVertices:sizeof (sphereNormals) 1 (3 * sizeof (GLfloat))
data:sphereNormals
usage:GL_STATIC_DRAW] ;
se1f.vertexTextureCoordBuffer = [[AGLKVertexAttribArrayBuffer alloc]
initwithAttribstride:(2★sizeof (GLfloat))
numberOfVertices:sizeof (sphereTexCoords) / (2 ★sizeof(GLfloat))
data:sphereTexCoords
usage:GL_STATIC_DRAW] ;
((AGLKContext *)view.context) enable:GL_DEPTH_TEST];
}
与之前把所有的顶点元素交错保存在一个缓存中的例子不同,在例子OpenGLES_Ch51中使用了多个顶点属性数组缓存。当所有的顶点属性靠在一起驻留在内存中时,大部分GPU可以最佳化执行。GPU可能会在一个内存操作中读取所需的所有值。然而,用来生成在sphere.h文件中声明的数据的脚本会保存顶点位置、法向量和纹理坐标到不同的数组中,因此,如在前面的“-viewDidLoad” 实现中以粗体标注的代码显示的一样,保存数据到不同的缓存是最容易的。当一些顶点元素频繁变换而剩下的保持不变时,使用不同的缓存有时可能会产生一个性能优势。例如,如果纹理坐标从来不改变但是顶点位置频繁改变,那么把顶点位置保存到一个使用GL_DYNAMIC_DRAW提示的缓存中,把纹理坐标保存在另一个使用GL_STATIC_DRAW提示的缓存中,这样应该是最佳化的。第2章在介绍glBufferData()函数时,讲解了GL_DYNAMIC_DRAW和GL_STATIC_DRAW.
“-viewDidLoad”的实现最后开启了片元深度测试。如果你好奇想看看在不使用深度缓存的情况下在渲染的过程中会发生什么,请注释掉“[((AGLKContext *)view.context) enable:GL_DEPTH_TEST];"一行并重建。
使用多个顶点属性数组缓存提供的属性所做的绘制与以前的例子会有一点儿不同。为AGLKContext的“ -clear:.”方法指定GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT来在同一时间清除深度缓存和像素颜色渲染缓存。AGLKContext 的“-clear.”方法会调用glClear()函数。在“-viewDidLoad” 中创建的每个AGLKVertexAttribArrayBuffer实例是为绘制准备的。准备好调用glBindBuffer()、glEnableVertexAttribArray() 和gIVertexAttribPointer()函数。最后,一个被添加到本例的AGLK VertexAttribArray Buffer类的类方法“ +drawPreparedArraysWithMode:startVertexIndex:numberOfVertices:"会调用glDrawArrays()函数,这个雨数会绘制顶点,使用从每个开启属性的缓存指针收集来 的数据。
// GLKView delegate method: Called by the view controller's view
// whenever cocoa Touch asks the view controller's view to
// draw itself. (In this case, render into a frame buffer that
// shares memory with a Core Animation Layer)
- (void)glkView: (GLKView *)view drawInRect: (CGRect )rect {
[self.baseEffect prepareToDraw];
// Clear back frame buffer (erase previous drawing)
[(AGLKContext * )view .context
clear:GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT];
[self.vertexPositionBuffer
prepareToDrawwithAttrib:GLKVertexAttribPosition
numberOfCoordinates:3
attriboffset:0
shouldEnable:YES];
[self.vertexNormalBuffer
prepareToDrawWithAttrib:GLKVertexAttribNormal
number0fCoordinates:3
attriboffset:0
shouldEnable:YES];
[self.vertexTextureCoordBuffer
prepareToDrawWithAttrib:GLKVertexAttribTexCoord0
numberOfCoordinates:2
attriboffset:0
shouldEnable:YES];
// Draw triangles using vertices in the prepared vertex
// buffers
[AGLKVertexAttribArrayBuffer
drawPreparedArraysWithMode:GL_TRIANGLES
startVertexIndex:0
numberofVertices:sphereNumVerts];
}
通过把下面的两个粗体显示的代码语句添加到“-glkView:drawInRect:"方法的末尾附近,OpenGLES_Ch5_2修改了OpenGLES_Ch5_1。
// Scale the Y coordinate based on the aspect ratio of the
// view's Layer which matches the screen aspect ratio for
// this example
const GLfloat aspectRatio =
(GLfloat)view.drawableWidth / (GLfloat)view. drawableReight;
self.baseEffect.transform.projectionMatrix =
GLK4atrix4MakeScale(1.0f, aspectRatio, 1.0f);
// Draw triangles using vertices in the prepared vertex
// buffers
[AGLKVertexAttribArrayBuffer
drawPreparedArraysWithMode:GL_TRIANGLES
startVertexIndex:0
numberOfVertices:sphereNumVerts];
GLKit的GLKMatrix4MakeScale()函数创建了一个基础的变换矩阵(transformationmatrix)。这个函数接收三个参数,这三个参数会改变坐标系的三个轴的相对单位长度。指定一个值1.0意味着没有变化。OpenGLESCh5_2会保持沿着X轴和Z轴的单位长度不改变,但是设置沿着Y轴的单位长度为这个表达式(GLfloat)view.drawableWidth /(GLfloat)view.drawableHeight的值,这个表达式会计算屏幕纵横比。drawableWidth属性以像素为单位提供了视图的像素颜色渲染缓存的宽度,这个宽度与视图的全屏Core; Animation层相匹配。drawableHeight 属性以像素为单位提供了高度。屏幕纵横比为宽度除以高度。沿着屏幕的X轴的可用像素的数量与沿着屏幕的Y轴的可用像素的数量是不一样的。通过纵横比缩放Y轴以抵消在OpenGLES_Ch5_1中看到的拉伸效果。
注意 缩放对灯光有一个潜在的影响。缩放会作用到所有的几何图形,包括法向量。在一个法向量被缩放以后,它可能就不再是一个单位向量,因此就不能与OpenGL ES灯光模拟一起正确工作。由GLKit的GLKBaseEffect类生成的Shading Language程序会按需正规化法向量。对于OpenGL ES 1.x 来说,调用glEnable(GL_NORMALIZE)函数会告诉GPU让所有的法向量变回单位长度,在它们被缩放以后。
在5.4节中,当为渲染一个场景而改变视点时,会讲解变换矩阵的作用。顶点属性、矢量和矩阵这三个概念--起构成了理解3D图形的关键。