申明:本系列教程原稿来自网络,翻译目的仅供学习与参看,请匆用于商业目的,如果产生商业等纠纷均与翻译人、该译稿发表人无关。转载务必保留此申明。
内容:《iPhone 3D 编程》第二章:数学与抽象
原文地址:http://ofps.oreilly.com/titles/9780596804824/chmath.html
译文地址:http://blog.csdn.net/favormm/article/details/6920318
更好的排版下载地址:
***************************************************************************
计算机图形领域比计算机其它领域对数学的要求都高,如果你想成为一个合格的OpenGL程序员,那么你得撑握线性代数,并能抽象一些内容。
在本章,我将解释这些抽象内容与回忆线性代数的内容。其中OpenGL涉及到的概念都会得到讲解,于是在HelloArrow示例代码中的神密面纱将一层一层解开。
在本章结束时,我们会运用这些数学知识将HelloArrow这个示例转化为到3D空间中去,完成一个叫”HelloCone”的示例。
你们可以 把包括OpenGL ES在内的任何图形API都看作是集装线的工程流程,将各种原始材料如纹理,顶点做为输入,最终组装成五彩缤纷的图形。
这些传入OpenGL的数据是我们学习OpenGL首先要学的内容,在本章我们重点学习顶点。在图2.1中,用抽象的视角展示了顶点逐渐演变成像素的过程。首先是一系列的顶点变换,接着这些顶点被组装成图元,最后将这些图元光栅化到像素。
图2.1 OpenGL集装线
注意 OpenGL ES 1.1与2.0都能抽象出集装线的概念,但ES 2.0更为明显。图2.1中,最左边的小精灵接手处理vertex shader,最右边的小精灵处理完后交给fragment shader。 |
在本章我们重点介绍集装流程中的变换,但是首先我们概述一下装配图元的流程,因为它很容易理解。
在三维空间中,一个物体的形将可以用几何图形表示。在OpenGL中,这些几何图形是由基础图元,这些基础图元包括三角形,点,线。其础元是由一些顶点通过不同的拓扑规则构建起来的。在OpenGLES中,共有7种拓扑规则,参看图2.2“图形拓扑规则”。
图2.2 “图形拓扑规则”
在第一章Hello Arrow的代码中,有这样一行代码利用OpenGL绘制一个三角形到缓冲区:
glDrawArrays(GL_TRIANGLES, 0, vertexCount); |
第一个参数指明OpenGL绘制的时候拓扑规则为:GL_TRIANGLES,采用这种规则后OpenGL组装基础图形的时候,首先取顶点缓冲区中的前三个顶点出来组成第一个三角形,接着到后面三个顶点组成第二个三角形,以此类推。
大多数情况下,同于顶点都挨着的,所以在顶点组数中会有重复出现的。为了解决这个问题,GL_TRIANGLE_STRIP规则出来了。这样一来,就可以用更小的顶点数组绘制出数量相同的三角形,看表2.1会明了许多,其中v表示顶点数,p表示图元数。这样说吧,如果绘制三个三解形,用GL_TRIANGLES规则,我们需要9个顶点数据(3*p),如果用GL_TRIANGLE_STRIP规则,我们则只需要5个顶点数据(p+2)。
表2.1 图元相关计数
拓扑规则 |
图元数 |
顶点数 |
GL_POINTS |
v |
p |
GL_LINES |
v/2 |
2p |
GL_LINE_LOOP |
v |
p |
GL_LINE_STRIP |
v-1 |
p+1 |
GL_TRIANGLES |
v/3 |
3p |
GL_TRIANGLE_STRIP |
v-2 |
p+2 |
GL_TRIANGLE_FAN |
v-1 |
p+1 |
GL_RTINGLE_FAN这个规则得说一下, 花多边形,圆或锥体的时候这个规则很好用。第一个顶点表示顶点,其它的表示底点。很多种情况下都是用GL_TRINGLE_STRIP,但是在用FAN的时候如果用成了STRIP,那么这个三角形将退化(0区域三角形)。
图2.3 两个三角形组成的四方形
图2.3中用两个三角形绘制了一个方形。(顺便说一下,OpenGL有一种规则GL_QUADS是用来直接绘制方形的,但是OpenGL ES不支持这种规则。)下面的代码分别用三种拓扑规则绘制同一个方形三次。
const int stride = 2 * sizeof(float);
float triangles[][2] = { {0, 0}, {0, 1}, {1, 1}, {1, 1}, {1, 0}, {0, 0} }; glVertexPointer(2, GL_FLOAT, stride, triangles); glDrawArrays(GL_TRIANGLES, 0, sizeof(triangles) / stride);
float triangleStrip[][2] = { {0, 1}, {0, 0}, {1, 1}, {1, 0} }; glVertexPointer(2, GL_FLOAT, stride, triangleStrip); glDrawArrays(GL_TRIANGLE_STRIP, 0, sizeof(triangleStrip) / stride);
float triangleFan[][2] = { {0, 0}, {0, 1}, {1, 1}, {1, 0} }; glVertexPointer(2, GL_FLOAT, stride, triangleFan); glDrawArrays(GL_TRIANGLE_FAN, 0, sizeof(triangleFan) / stride); |
在OpenGL ES中图元并不是只有三角形,GL_POINTS可以用来绘制点。点的大小是可以自定义的, 如果太大看起来就像方形。这样一来,就可以将小的位图与这样的点关联起来,构成所谓的点精灵。在第七章,精灵与字符中会讲到。
OpenGL中关于线的图元拓扑规则有三个,分别是:separatelines, strips与loops。在strips与loops规则中,每一条件的结束点是下一条线的顶点,而loops更特别,第一条线的开始点是最后一条件的结始点。如果你绘制图2.3中方形的边框,下面的代码分别用三种规则实现了。
const int stride = 2 * sizeof(float);
float lines[][2] = { {0, 0}, {0, 1}, {0, 1}, {1, 1}, {1, 1}, {1, 0}, {1, 0}, {0, 0} }; glVertexPointer(2, GL_FLOAT, stride, lines); glDrawArrays(GL_LINES, 0, sizeof(lines) / stride);
float lineStrip[][2] = { {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0} }; glVertexPointer(2, GL_FLOAT, stride, lineStrip); glDrawArrays(GL_LINE_STRIP, 0, sizeof(lineStrip) / stride);
float lineLoop[][2] = { {0, 0}, {0, 1}, {1, 1}, {1, 0} }; glVertexPointer(2, GL_FLOAT, stride, lineLoop); glDrawArrays(GL_LINE_LOOP, 0, sizeof(lineLoop) / stride); |
现在来看看OpenGL中集装线的输入数据。在OpenGL的世界里,每一个顶点至少得有一个属性,其中位置是极为重要的。表2.2罗列了OpenGL ES 1.1的顶点属性。
表2.2 OpenGL ES中的顶点属性
Attribute |
OpenGL Enumerant |
OpenGL Function Call |
Dimensionality |
Types |
Position |
GL_VERTEX_ARRAY
|
glVertexPointer
|
2, 3, 4
|
byte, short, fixed, float
|
Normal |
GL_NORMAL_ARRAY
|
glNormalPointer
|
3
|
byte, short, fixed, float
|
Color |
GL_COLOR_ARRAY
|
glColorPointer
|
4 |
ubyte, fixed, float
|
Point Size |
GL_POINT_SIZE_ARRAY_OES
|
glPointSizePointerOES
|
1 |
fixed, float
|
Texture Coordinate |
GL_TEXTURE_COORD_ARRAY
|
glTexCoordPointer
|
2,3,4 |
byte, short, fixed, float
|
Generic Attribute(ES 2.0) |
N/A
|
glVertexAttribPointer
|
1,2,3,4 |
byte, ubyte, short, ushort, fixed, float
|
OpenGL ES 2.0只有最后一行,它需要你自定义属性。回忆一下HelloArrow中不同rendering engines开启属性的方法:
//ES 1.1 glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY);
//ES 2.0 glEnableVertexAttribArray(positionSlot); glEnableVertexAttribArray(colorSlot); |
在ES 1.1中,是用内置常量来开启顶点属性的,而ES 2.0中则是用从shader中导出的常量来开始(positionSlot与colorSlot)。接着向OpenGL指明要开启顶点属性的类型与维度:
// OpenGL ES 1.1 glVertexPointer(2, GL_FLOAT, ... ); glColorPointer(4, GL_FLOAT, ... );
// OpenGL ES 2.0 glVertexAttribPointer(positionSlot, 2, GL_FLOAT, ...); glVertexAttribPointer(colorSlot, 4, GL_FLOAT, ...); |
顶点数据的数据类型可能是表2.3中的一个。如果是ES 2.0可以使用其中任意一个,而ES 1.1则有限制, 具体要看是什么属性(参看表2.2最右列)。
表2.3 顶点属性数据类型
OpenGL Type |
OpenGL Enumerant |
Typedef Of |
Length in Bits |
GLbyte |
GL_BYTE |
signed char |
8 |
GLubyte |
GL_UNSIGNED_BYTE |
unsigned char |
8 |
GLshort |
GL_SHORT |
short |
16 |
GLushort |
GL_UNSIGNED_SHORT |
unsigned short |
16 |
GLfixed |
GL_FIXED |
int |
32 |
GLfloat |
GL_FLOAT |
float |
32 |
OpenGL ES 1.1中,位置属性有点特殊,因为它是必须的顶点属性。它可以是二维,三维或是四维,但是在OpenGL内部总是把它们转化为四维浮点型进行处理。
四维空间?这可与那些物理学家所说的不一样, 它并不涉及时间与物理,只是一种方法,它可以将所以变换都用矩阵相乘的方式表达。这里的四维坐标就是我们所谓的齐次坐标。当把三维坐标转化为齐次坐标的时候,往往第四个元素是为1(通宵用w表示),为0的情况表示点无限远, 这种情况非常少。(在OpenGL中在设置灯光位置的时候w=0,第四章中会看到。),为负的情况是没有实际意义的。
齐次坐标 齐次坐标是在Möbius于1827年8月发表Der barycentrische Calcul中诞生的。随便说说Möbius发明的barycentrische坐标系,它用于iPhone图形芯片中计算三角形插值颜色。这个术语源于古老的词汇“barycentre”,表示中心的意思。如果你将一个三角的三个角放上不同的权重,那么你就可以通过barycentric坐标系计算平衡点。关于它的推导不在本书讨论的范围,如果你有兴趣可以自行研究。 |
再次回到OpenGL集装线流程,其中所有的点都变为4维,那么它们可能变成2维的点吗?明确的告诉你,会的!特别是苹果发布了触摸屏的iPhone。我们将在下一节介绍顶点是如何变化为2维点,现在我们首先关心如何拆除第四个变量w的,方程式如下:
方程式 2.1 透视变换
这种除以w的计算就叫着透视变换。z也进行同样的处理,紧接着的深度与真实性,你会看到更深入分析。
图2.4, “顶点前期流程。上一排是概念,下一排是OpenGL的视图”与 图2.5,“光珊化前顶点的最后三个流程”描绘了顶点从三维变到二维的过程。在OpenGL的集装线中,它们叫着变换与灯光,或用T&L表示。我们将在第四章,深度与真实性中介绍灯光,现在重点是介绍变换。
每一次变换,顶点就有新的位置。最原传入的顶点是位于对象空间,即叫着对象坐标系。在对象空间中,原点就是对象的中心点,有时候我们把对象空间也叫着模型空间。
通过模型-视图矩阵,对象坐标就被转化为眼坐标空间。在眼坐标空间中,原点是照像机的位置。
接着,通过投影矩阵顶点变转化到裁剪空间。由于OpenGL将位于视图平截面外的顶点会切除掉,所以形像的叫着裁剪空间。在这儿就是w表演的时候了,如果x或y的值大于+w或小于-w,这些点将会被裁剪掉。
图2.4 顶点的先期流程。上一排是概念,下一排是OpenGL的视图
在ES 1.1中,图2.4中的流程是固定的,每一个顶点都必须经过这些流程。在ES2.0中,这取决于你,在进入裁剪空间前,你可以进行任何的变换。但常常你也是进行与此相同的变换而已。
裁剪过后,就进入到透视变换了。我们会把坐标标准化到[-1, +1],术语叫着设备坐标标准化。图2.5描述了其变换过程。与图2.4不同的是,这些流程在ES1.1与ES2.0中都是一样的。
图2.5光珊化前顶点的最后三个流程
光珊化前前最后一步是视口变换,它需要一些该应中当中设定的值。你可以还认得在GLViw.mm中有这样一行代码:
glViewport(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame)); |
glViewport的四个参数分别是:left,bottom,with,height。对于iPhone,往往让width与height的值为320与480,但是为了兼容以后的苹果设备(与其它平台)与避免硬编码,我们需要在运行时获取正确的长度与高度,正如我们在HelloArrow中所写的代码。
glViewport控制x与y变换到窗口空间(也可叫着移动设备,非全屏的情况少之又少)。控制z变换到窗口空间是由另一个方法实现的:
glDepthRangef(near, far); |
实际开发中,这个方法很少用, 它的默认值是near为0,far为1,我们用它的默认值即可。
现在,你明白顶点位置变化的基本流程了吧,但是我们还没有介绍颜色。当灯点禁止(默认禁止)的时候,颜色是不经过变换而直接传递。当开启的时候,颜色就与变换有密切关系。我们将在第四章,深度与真实性介绍。
线装线的抽象让我们明白了OpenGL的后台工作原理,但是对于理解一个3D应用的工作流程,摄影抽象更加有用。当我太太精心准备了印度晚餐,他就会要求我拍摄一些相片用于它的私人博客。我常常会做下面的流程来完成太太的要求:
1. 放置各种餐盘。
2. 放置灯光。
3. 放置相机。
4. 将相机对准食物。
5. 设整焦距。
6. 控快门拍相。
It turns out that each of these actions haveanalogues in OpenGL, although they typically occur in a different order.Setting aside the issue of lighting (which we'll address in a future chapter),an OpenGL program performs the following actions:
你可能已发现,每一步都与OpenGL中有相类之处,尽管有的顺序不同。先把灯光部份放一边(这部份内容在后面章节),OpenGL的的步骤如下:
1. 调整相机的视角, 投影矩阵起作用。
2. 放置相机位置并设置朝向,视图矩阵起作用
3. 对于第一个对象
a. 缩放,旋转,移动,模型矩阵起作用。
b. 渲染对象。
模型矩阵与视图矩阵的合体叫着模型-视图矩阵。在OpenGLES 1.1中,所有的顶点都先经过模型-视图矩阵作用,然后再由投影矩阵作用。而OpenGL ES 2.0中, 你可以任意变换, 但是常常也是按照模形-视图/投影的过程变换,至少得差不多。
在后面我们会详细介绍三种变换,现在来学一些预备识知。无论如何用,OpenGL有一个通用的方法来处理所有的变换。在ES1.1中,当前变换可以用矩阵来表达,如下:
float projection[16] = { ... }; float modelview[16] = { ... };
glMatrixMode(GL_PROJECTION); glLoadMatrixf(projection);
glMatrixMode(GL_MODELVIEW); glLoadMatrixf(modelview); |
在ES2.0中,并没有模形-视图矩阵,也没有glMatrixMode与glLoadMatrixf这样的方法。取而代之的是shaders中的uniform变量。在后面我们会学到,uniforms是一种shader中用到的类型,我们可以简单的把它理解为shader不能改变的常量。加载方式如下:
float projection[16] = { ... }; float modelview[16] = { ... };
GLint projectionUniform = glGetUniformLocation(program, "Projection"); glUniformMatrix4fv(projectionUniform, 1, 0, projection);
GLint modelviewUniform = glGetUniformLocation(program, "Modelview"); glUniformMatrix4fv(modelviewUniform, 1, 0, modelview); |
现在是不是想知道为何OpenGL中的好多方法都由f或fv结尾。许多方法(如glUniform*)可以是浮点-指针参数的方法,可以是整形参数的方法,可是以其它类型参数的方法。OpenGL是C型的API,而C又不支持方法重载,所以每个方法得用不同的名字加以区分。表2.4 “OpenGL ES 方法后缀”,是方法的后缀的解释。随便说一下,v表示是一个指针型参数。 表2.4 OpenGL ES方法后缀
|
ES 1.1提供了另外一个方法,可以使矩阵相乘,而ES2.0中没有这种方法。下面的代码首先加载了一个单位矩阵,然后再与其它两个矩阵相乘。
float view[16] = { ... }; float model[16] = { ... };
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMultMatrixf(view); glMultMatrixf(model); |
模型-视图与投影矩阵默认是单位矩阵。单位矩阵用于恒等变换,即变换不起作用。见方式程2.2 恒等变换。
方程式 2.2 恒等变换
注意 关于矩阵与向量,矩阵与矩阵相乘,请参看附录A,C++向量库 |
本书中一律用行向量进行计算。方程式2.2中,左边的(vx
vy
vz
1
)与右边的(vx*1 vy*1 vz*1 1)
都是四维向量。该方式程可以用列向量表示为:
很多情况下,将4维的行向量想像成1*4的矩阵,或把4维的列向量想像成4*1的矩阵会更容理解。(n*m表示矩阵的维数,其中n表示有多少行,m表示有多少列。)
图2.6 “矩阵相乘”展示了两个矩阵相乘的条件:中间两个维数一定要相等。外面的两个数字决定矩阵相乘的结果维数。利用这条规则,我们来验证方程式2.2中的合法性。*号右边的四维行向量(等价于1*4的矩阵)与右边的4*4的矩阵相乘的结果应是1*4的矩阵(同样的适用于四维列向量)。
图2.6矩阵相乘
从编码的角度来说,我发现行向量比列向理更理想,因行向量更像c语言中的数组。当然,只发你愿 意,你也可以用列向量,但是如果用列向量的话,你的变换顺序将要颠倒顺序才行。由于矩阵相乘不具有交换性,所以顺序很重要。
例如ES 1.1的代码:
glLoadIdentity(); glMultMatrix(A); glMultMatrix(B); glMultMatrix(C); glDrawArrays(...); |
如果用行向量的方式,你可以把每次变换看成当前变换的pre-multiplied。那么上面的代码等效于:
如果用列向量的方式,每次变换是post-multiplied。代码等效于:
无论你用的是行向量还是列向量的方式,我们只需要记住一点,就是代码中最后的变换是最先作用于顶点变换的。为了更明确,那么将上面的列向量变换方式,用加括号的方式显示的展示变换的作用顺度。
由于OpenGL的反向作用的特性,便用行向量会使其展现得更明显,这也是我为何喜欢用它的另一个原因。
关于数学方面的就介绍到此,现在回到摄影抽象,看看它是如何对应到OpenGL中来的。OpenGL ES 1.1提供了方法来生成矩阵,并在其当前矩阵中乘以新的变化矩阵一步完成新的变化。在后面小节中会介绍每一个方法。而ES 2.0没有这些方法,但是我会说明它的背后原理,让你自己实现这方法。
回忆一下OpenGL中用到的三个矩阵
1. 调整视角与视野,由投影矩阵作用。
2. 设置相机位置与朝向,由视图矩阵作用。
3. 缩放,旋转,移动每个对象,由模形矩阵作用。
我们将逐一介绍这三种变换,并完成一个最简单的变换流程。
将一个对象放于场景中通常需要经过缩放,旋转,移动处理。
内置API是glScalef
float scale[16] = { sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0 0, 0, 0, 1 };
// The following two statements are equivalent.下面两种方法等效 glMultMatrixf(scale); glScalef(sx, sy, sz); |
缩放矩阵与数学理论见方程式2.3
方程式2.3 缩放变换
图2.7展示了 sx = sy = 0.5时的缩放变换
图2.7缩放变换
警告 当缩放因子x,y,z三个都不相等的情况,我们称之为非均匀缩放。这种方式的缩放是被完全允许的,但是大多数情况下会影响效率。因为一旦有非均匀缩放,OpenGL就会进行大量的灯光计算。 |
glTranslatef可以轻松实现移动,将对象移动因定长度:
float translation[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1 };
// The following two statements are equivalent.下面两种方法等效 glMultMatrixf(translation); glTranslatef(tx, ty, tz); |
简单的说,移动就是用加法实现的,要记住在齐次坐标中,我们可以用矩阵相乘的方式表达所有的变换,参看方程式2.4
方程式2.4 移动变换
图2.8描绘不当tx = 0.25and ty = 0.5时的移动变换
图2.8移动变换
还记得HelloArrow示例中,固定渲染通道(ES 1.1)下的移动吗?
glRotatef(m_currentAngle, 0, 0, 1); |
这样就会绕着z轴逆时针旋转m_currentAngle度。第一个参数表示旋转角度,后面三个参数表示旋转轴。在ES2.0的实现中,旋转就有点复杂,因为它是手工计算矩阵的:
#include <cmath> ... float radians = m_currentAngle * Pi / 180.0f; float s = std::sin(radians); float c = std::cos(radians); float zRotation[16] = { c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, "Modelview"); glUniformMatrix4fv(modelviewUniform, 1, 0, &zRotation[0]); |
图2.9 描绘了旋转45度的变换
图2.9 旋转变换
绕着z轴旋转是非常简单的,但是如果绕着任意轴旋转就需要一复杂的矩阵。对于ES1.1, glRotatef可以帮我们自动生成矩阵,所以不必过多关心相关的概念。对于ES2.0,参看附录A, C++向量库,窥探其实现。
glRotatef只能通过其原点旋转,如果你想绕任意点旋转,你可以通过下面三步实现:
1. 移动-p。
2. 旋转。
3. 移动+p。
如果想改HelloArrow在(0, 1)点绕z轴旋转,那么可以如下修改:
glTranslatef(0, +1, 0); glRotatef(m_currentAngle, 0, 0, 1); glTranslatef(0, -1, 0); glDrawArrays(...); |
记住,代码中最后的变换,在实现作用的时候是最先起效的!
设置视图矩阵最简单的方法就是用LookAt方法,它并不是OpenGL ES的内置函数,但是可以自已快速实现。它有三个参数:相机位置,目标位置,一个”up”向量表示相机朝向(见图2.10 “LookAt 变换”)。
图2.10 LookAt 变换
通过三个向量的传入,LookAt就可以生成一个变换矩阵,否则就得用基本的变换(缩放,移动,旋转)来生成。示例2.1 是LookAt的实现。
示例2.1 LookAt
mat4 LookAt(const vec3& eye, const vec3& target, const vec3& up) { vec3 z = (eye - target).Normalized(); vec3 x = up.Cross(z).Normalized(); vec3 y = z.Cross(x).Normalized();
mat4 m; m.x = vec4(x, 0); m.y = vec4(y, 0); m.z = vec4(z, 0); m.w = vec4(0, 0, 0, 1);
vec4 eyePrime = m * -eye; m = m.Transposed(); m.w = eyePrime;
return m; } |
注意,示例2.1中用了自定义类型,如vec3,vec4,mat4。关非伪代码,而是用到了附录A,C++向量库中的代码。本章后面内容会详细介绍这个库。
到此为止,我们已能修改模型-视图的变换。对于ES1.1我们可以用glRotatef与glTranslatef来影响当前矩阵,也可以用glMatrixMode在任意时刻来修改矩阵。初始化选中的是GL_MODELVIEW模式。
到底设影矩阵与模形-视图矩阵的区别是什么?对于OpenGL开发新手,会把投影想像为”camera matrix”,这种想法即使不算错,也是过于简单了,因为相机的位置与朝向是由模型-视图矩阵标识的。我更喜欢把投影想像成相机的“聚焦”,因为它可以控制视野。
警告 相机的位置与朝向是由模型-视图矩阵决定的,并非投影矩阵决定。在OpenGL ES 1.1中灯光计算的时候会用到这些数据。 |
在计算机图形学中有两种类型的投影方式:透视投影与正交投影。采用透视投影,物体越远越小,这样更接具真实性。图2.11“投影类型” 中可以看到它们的区别。
图2.11 投影类型
正交投影往往用于2D绘制,所以在Hello Arrow中用了它:
const float maxX = 2; const float maxY = 3; glOrthof(-maxX, +maxX, -maxY, +maxY, -1, 1); |
glOrthof的六个参数表示六面体每一面到原点的矩离,分别是:前,后,左,右,上,上。示例中参数的比例是2:3,这是因为iPhone的屏是320*480。 而ES 2.0 生成正交投影矩阵的方法是:
float a = 1.0f / maxX; float b = 1.0f / maxY; float ortho[16] = { a, 0, 0, 0, 0, b, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1 }; |
当正交投影的中心点位于原点的时候, 生成的投影矩阵类似于缩放矩阵,关于缩放矩阵,前面已介绍过。
sx = 1.0f / maxX sy = 1.0f / maxY sz = -1
float scale[16] = { sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0 0, 0, 0, 1 }; |
由于Hello Cone(本章示例,后面将看到)是绘制的3D图形,于是我们用glFrustumf来设置一个投影矩阵,这样写:
glFrustumf(-1.6f, 1.6, -2.4, 2.4, 5, 10); |
glFrustumf的参数与glOrthof的一样。由于glFrustum在ES 2.0中不存在, 所以Hello Cone的ES2.0的实现就得自己计算矩阵,方法如下:
void ApplyFrustum(float left, float right, float bottom, float top, float near, float far) { float a = 2 * near / (right - left); float b = 2 * near / (top - bottom); float c = (right + left) / (right - left); float d = (top + bottom) / (top - bottom); float e = - (far + near) / (far - near); float f = -2 * far * near / (far - near);
mat4 m; m.x.x = a; m.x.y = 0; m.x.z = 0; m.x.w = 0; m.y.x = 0; m.y.y = b; m.y.z = 0; m.y.w = 0; m.z.x = c; m.z.y = d; m.z.z = e; m.z.w = -1; m.w.x = 0; m.w.y = 0; m.w.z = f; m.w.w = 1;
glUniformMatrix4fv(projectionUniform, 1, 0, m.Pointer()); } |
一旦设置了设影矩阵, 就设定了视野。视锥表示眼在金字塔顶部的一个锥体(参看图2.12 视锥)
2.12 视锥
基于金字塔的顶点(称为视野)的角度,可以计算一个视锥。开发者认为这样比指定六面更加直观。示例2.2中方法有四个参数:视角,金字塔宽高比,远与近裁剪面。
示例 2.2 VerticalFieldOfView
void VerticalFieldOfView(float degrees, float aspectRatio, float near, float far) { float top = near * std::tan(degrees * Pi / 360.0f); float bottom = -top; float left = bottom * aspectRatio; float right = top * aspectRatio;
glFrustum(left, right, bottom, top, near, far); } |
告诫 设置投影的时候,应避免把远近裁剪面设为0或负数。数学上不支持这种工作方式。 |
还记得在用ES1.1实现HelloArrow的时候用glPushMatrix与glPopMatrix来存取变换的状态吗?
void RenderingEngine::Render() { glPushMatrix(); ... glDrawArrays(GL_TRIANGLES, 0, vertexCount); ... glPopMatrix(); } |
用Push/Pop这样的方式来实现Render是非常普遍日,因为这样的好外是可以阻止帧与帧这间变换的累积。
上面的示例,栈没有超过两层,iPhone允许嵌套6层栈。这样使复杂变化变得简单,比如渲染图2.13 “机器人手臂”这种有关节的对象,或者是会层次的模型。在用push/pop写代码的时候,最好有相应的缩进,如示例2.3“分层变换”
示例2.3 分层变换
void DrawRobotArm() { glPushMatrix(); glRotatef(shoulderAngle, 0, 0, 1); glDrawArrays( ... ); // upper arm glTranslatef(upperArmLength, 0, 0); glRotatef(elbowAngle, 0, 0, 1); glDrawArrays( ... ); // forearm glTranslatef(forearmLength, 0, 0); glPushMatrix(); glRotatef(finger0Angle, 0, 0, 1); glDrawArrays( ... ); // finger 0 glPopMatrix(); glPushMatrix(); glRotatef(-finger1Angle, 0, 0, 1); glDrawArrays( ... ); // finger 1 glPopMatrix(); glPopMatrix(); } |
图2.13 机器人手臂
每一个矩阵模式都有自己的栈,如图2.14“矩阵栈”,用得最多的是GL_MODELView。对于GL_TEXTURE模式的栈,我们会在另一章节中介绍。先前说过,OpenGL中的每一个项点位置变换都由当前的模型-视图矩阵与投影矩阵决定,也就是说在它们各自的栈中,它们位于栈顶。用glMatrixMode实现从一个栈模式到另一个模式。
图2.14 矩阵栈
在ES 2.0中不存在矩阵栈,如果你需要,你可以在你自已应用中加代码实现,也可用自己的数学库。这样是不是觉得ES2.0更难呀? 但你得记住ES 2.0 是一种”closerto te metal”的API, 利用shader它可以让你更自由更充分的操控图形。
到现在,我们已看到了OpenGL执行背后的数学支持。由于OpenGL是一个低级别的图形API,并不是动画API。幸运的是,对于动画所需的数学非常简单。
用五个字来总结它:animationis all about interpolation(动画与插值相关)。一个应用程序的动画系统往往需要艺术家,用户或算法设定一些关键帧。然后在运行的时候,计算这些关键帧间的值。被当做关帧的数据可以是任意类型,常规是颜色,位置,角度。
计算两关键帧中间帧的过程叫着补间。如果你将流逝时间除以动画时间,你就可以得到一个[0,1]的权值。如图2.15中所描绘 “缓动方式:线性,二次缓进,二次缓进-缓出”, 我们会讨论三种缓动方程式。对于补间值t,可以用如下方式计算插值:
float LinearTween(float t, float start, float end) { return t * start + (1 - t) * end; } |
某些类型的动画,不能用线性补间的方式实现,用Robert Penner的缓动方程可以让动画更加直实。该缓进的计算是相当简单:
float QuadraticEaseIn(float t, float start, float end) { return LinearTween(t * t, start, end); } |
Penner的 “二次缓进-缓出”方式有点复杂,但是把它分拆分开就变得简单了,见示例2.4。
示例2.4 二次缓进-缓出
float QuadraticEaseInOut(float t, float start, float end) { float middle = (start + end) / 2; t = 2 * t; if (t <= 1) return LinearTween(t * t, start, middle); t -= 1; return LinearTween(t * t, middle, end); } |
图2.15缓动方式:线性,二次缓进,二次缓进-缓出
对于位置与颜色的关键帧,它们很容易插值:对于xyz或rgb分量,分别调用上面的的补间方法求补间值。角度也应一样处理,求角度的补间值而已。但是对于旋转轴不同的情况,如何计算这两个朝向的补间值呢?
在图2.3中,这个例子是在一个平面上(泽注:只能绕Z轴旋转),如果你的需要是每个节点是一个球(泽注:可以360度旋转)。那么每一个节点只存旋转角度是不够的,还要存旋转轴。我们将它标记为轴-角度,于是对于每一个节点需要4个浮点值。
原来有一个更简单的方法来表示一个任意旋转,与轴-角度的一样需要4个分量,这种方法更适合又插值。这个方法就是用四维向量组成的四元数,它于1843年被设想出来的。在现在矢量代数中,四元数的点被忽视,但经历计算机图形的发展,它得于复兴。 Ken Shoemake 是20世纪80年代末著名slerp方程的推广之一,而slerp方程可以计算两个四元数补间值。
知道 Shoemake的方程只是众多四元数插值的方法中的一种,但是它是最出名的,并在我们的向量库中所采用。其它的方法,如normalized quaternion lerp, log-quaternion lerp, 有时候在性能方面更理想。 |
说得差不多了,但你得明确,四元数并不是处理动画的最好的方式。有些时候,只需要简单的计算两个向量的夹角,找出一个旋转轴,并计算角度的插值即可。但是四元数解决了稍微复杂的问题,它不再是两个向时间的插值,而变成两个朝向间的插值。这样看起来更加迂腐,但是它有很重要的区别的。将你的手臂伸直向前,掌心向上。弯曲你的胳膊并旋转你的手,这样你就模仿了两个四元数的插值。
在我们的示例代码中用到了许多“轨迹球”旋转,用四元数来完成再合适不过了。在此我不会涉及大量枯燥的方程式,你可以到附录A,C++向量库去看四元数的实现。在HelloCone示例中与下一章中的wireframe view示例中,将会用到这个向量库。
在Hello Arrow中的顶点数据结构是:
struct Vertex { float Position[2]; float Color[4]; }; |
如果我们继续沿用C数组的方式贯穿全书,你将会发现生活是多么的糟糕! 我们真正想要的应是这样:
struct Vertex { vec2 Position; vec4 Color; }; |
这正是C++运算符重载与类模版强大的地方。运用C++可以让你写一个简单的库(其实,很简单)并使你应用的代码像是基于向量的一种语言开发。其实本书中的示例就是这样的。我们的库只包括了三个头文件,没有一个cpp文件:
Vector.hpp
定义了一套三维,三维,四维向量,可以是浮点也可以是整型。并没有依附任何头文件。
Matrix.hpp
定义了2x2, 3x3, 与 4x4矩阵类,只依附了Vector.hpp。
Quaternion.hpp
定义了四元数的类,并提供了构造与插值的方法,依附Matrix.hpp。
在附录A,C++向量库中包括了这些文件,但是还是向你展示一下本库是如何构成的,示例2.5是Vector.hpp的一部份。
示例 2.5 Vector.hpp
#pragma once #include <cmath>
template <typename T> struct Vector2 { Vector2() {} Vector2(T x, T y) : x(x), y(y) {} T x; T y; ... };
template <typename T> struct Vector3 { Vector3() {} Vector3(T x, T y, T z) : x(x), y(y), z(z) {} void Normalize() { float length = std::sqrt(x * x + y * y + z * z); x /= length; y /= length; z /= length; } Vector3 Normalized() const { Vector3 v = *this; v.Normalize(); return v; } Vector3 Cross(const Vector3& v) const { return Vector3(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x); } T Dot(const Vector3& v) const { return x * v.x + y * v.y + z * v.z; } Vector3 operator-() const { return Vector3(-x, -y, -z); } bool operator==(const Vector3& v) const { return x == v.x && y == v.y && z == v.z; } T x; T y; T z; };
template <typename T> struct Vector4 { ... };
typedef Vector2<int> ivec2; typedef Vector3<int> ivec3; typedef Vector4<int> ivec4;
typedef Vector2<float> vec2; typedef Vector3<float> vec3; typedef Vector4<float> vec4; |
我们把向量类型用C++模版的方式参数化了,这样一来就可以用相同代码成生基于浮点与定义的向量了。
虽然2维向量与3维向量有许多共同点,但是我们还是不能共用一套模版。我不能过过将维数参数化的模版来实现,如下面代码:
template <typename T, int Dimension> struct Vector { ... T components[Dimension]; }; |
当设计一个向量库的时候,在通用性与可读性上一定要有一个适当的平衡点。由于在向量类中逻辑相对较少,并很少需要遍历向量成员,分开定义类看起来是一个不错的选择。比如Position.y就比Position[1]更容易让读者理解。
由于向量这些类型会被常常用到,所以在示例2.5的底部用typedefs定义了一些缩写的类型。这些小写的名字如vec2,ivec4虽然打破了我们建立的命名规则,但是看起来的感觉就更接近语言本身的原生类型。
在我们的向量库中,vec2/ivec2这样的命名是借鉴GLSL中的关键字的。注意区分本书中C++部分与shader部分内容,不要混淆了。
提示 在GLSL着色语言中,vec2与mat4这些类型是语言内置类型。我们的C++向量库是模仿着它写的。 |
现在我们开始修改HelloArrow为Hello Cone。我们要改的不只是把内容从2D变为3D,我们还要支持两个新的朝向,当设备朝上或朝下。
本章示例与上一章的视觉上的变化很大,主要是修改RenderingEngine2.cpp与RenderingEngine2.cpp。由于前面章节中有了良好的接口设计,现在是它发挥作用的时候了。首先来处理ES 1.1 renderer, 即RenderingEngine1.cpp。
表2.5 “HelloArrow与Hello Cone的不同之处” 指出了HelloArrow 与Hello Cone实现的几项差异。
表2.5 Hello Arrow与Hello Cone的不同之处
Hello Arrow |
Hello Cone |
绕着z轴旋转 |
四元数旋转 |
一个绘制方法 |
两个绘制方法,一个绘底,一个绘锥 |
C数组方式表示向量 |
用vec3的对像表示向量 |
三角形的数据小,由代码硬编码 |
运行时生成三角形的数据 |
三角形的数据存于C数级中 |
三角形的数据存于STL 向量中 |
我决定在本书示例中运用C++ STL(标准模版库)。运用它可以简化许多工作量,如它提供了可扩展的数组(std::vector)与双向链表(std::list)。许多的开发者都反对在移动设备如iPhone上写有时实性要求的代码时用STL开发。乱用STL的确会使你应用的内存无法控制,但如今,C++编译器对STL代码做了许多译化。同时我们得注意iPhone SDK也提供了一套Objective-C类(如,NSDictionary),这些类类似于STL的一些类,它们的内存占用率与性能都差不多。 |
它们的区别做到了心中有数 如表2.5, 再来看看RenderingEngine1.cpp的项部, 见示例2.6(注意 在这儿定义了新的顶点数据结构,因此你可以移除旧版本的数据结构)。
注意 如果你想边看边写代码,那么请在Finder中复制一份HelloArrow的工程目录,并改名为HelloCone。然后用Xcode打开,并在Project菜单中选择Rename,将工程名改为HelloCone。接着把附录A,C++向量库中的Vector.app,Matrix.hpp与Quaternion.hpp添加到工程。RenderingEngine1.cpp是区别最大的地方,打开它删掉里面所有内容,并修改为你将要看到的内容。 |
示例 2.6 RenderingEngine1 类定义
#include <OpenGLES/ES1/gl.h> #include <OpenGLES/ES1/glext.h> #include "IRenderingEngine.hpp" #include "Quaternion.hpp" #include <vector>
static const float AnimationDuration = 0.25f;
using namespace std;
struct Vertex { vec3 Position; vec4 Color; };
struct Animation { //[1] Quaternion Start; Quaternion End; Quaternion Current; float Elapsed; float Duration; };
class RenderingEngine1 : public IRenderingEngine { public: RenderingEngine1(); void Initialize(int width, int height); void Render() const; void UpdateAnimation(float timeStep); void OnRotate(DeviceOrientation newOrientation); private: vector<Vertex> m_cone; //[2] vector<Vertex> m_disk; //[3] Animation m_animation; GLuint m_framebuffer; GLuint m_colorRenderbuffer; GLuint m_depthRenderbuffer; }; |
1. 动画结构,用于生成平滑的三维动画。包括三个表示方向的四元数:开始,当前插值,结束;还有两个时间跨度:经过的与持继时间,都是以秒为单位。它们是用来计算[0,1]的。
2. 三角形数据用两个STL容器保存,分别是m_cone与m_disk。向量容器是正确的选择,因为我们知道它有多大,它还能保证空间是连继的。储存顶点的空间必须是连继的,这是OpenGL所要求的。
3. 与Hello Arrow的不同外,这儿需要两个renderbuffers。Hello Arrow是二维的,所以只需要一个颜色renderbuffer。Hello Cone需要一个存深度信息的renderbuffer。在后面的章节会学习深度缓冲,在此只需要简单理角为:它是一个特殊的平面图像,用来存放每一个像素z值的结构。
在Hello Arrow中构造方法非常简单:
IRenderingEngine* CreateRenderer1() { return new RenderingEngine1(); }
RenderingEngine1::RenderingEngine1() { // Create & bind the color buffer so that the caller can allocate its space. glGenRenderbuffersOES(1, &m_renderbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_renderbuffer); } |
示例2.7中的Initialize方法,生成了顶点数据并创建了framebuffer。开始处定义了一些锥体的半径,高度与几何精细度。这儿几何精细度是指在垂直方向上锥体的分片数量。生成顶点数据后,初始化了OpenGL的framebuffer与相关变换矩阵。还开启了深度测试,因为这是一个真3D应用,在第四章会介绍更多的深度测试知识。
示例2.7 RenderingEngine 中的Initialize
void RenderingEngine1::Initialize(int width, int height) { const float coneRadius = 0.5f; //[1] const float coneHeight = 1.866f; const int coneSlices = 40;
{ // Generate vertices for the disk. ... }
{ // Generate vertices for the body of the cone. ... }
// Create the depth buffer. glGenRenderbuffersOES(1, &m_depthRenderbuffer); //[2] glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_depthRenderbuffer); glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, width, height);
// Create the framebuffer object; attach the depth and color buffers. glGenFramebuffersOES(1, &m_framebuffer); //[3] glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, m_colorRenderbuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, m_depthRenderbuffer);
// Bind the color buffer for rendering. glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer); //[4]
glViewport(0, 0, width, height); //[5] glEnable(GL_DEPTH_TEST); //[6]
glMatrixMode(GL_PROJECTION); //[7] glFrustumf(-1.6f, 1.6, -2.4, 2.4, 5, 10);
glMatrixMode(GL_MODELVIEW); glTranslatef(0, 0, -7); } |
示例2.7是处理OpenGL的一个标准流程,在以后的内容中你会慢慢明白这一切。现在,简要说明一下:
1. 定义一些常量,用来生成顶点锥底与锥面的顶点数据。
2. 为深度缓冲生成一个id,绑定它,并为之分配存储空间。在在后面的深度缓冲中详细介绍。
3. 为缓冲对象生成id,绑定之,并把深度与颜色缓冲用glFramebufferRenderbufferOES依附于它。
4. 绑定颜色缓冲,后继的绘制将作用于它。
5. 设置viewport的左下角,长,宽属性。
6. 为3D场景开启深度测试
7. 设置投影与模型-视图矩阵
示例2.7中,两处生成顶点的地方都用省略号表示,是由于这两个地方值得深入分析。将对象拆分为三角形术语叫三角化,但常常也叫关镶嵌,它关系到多边形填充表面的边界问题。任何一个M.CEscher迷都知道,镶嵌是一个有趣的难题; 后面章节也会有介绍。
如图2.16 “HelloCone的镶嵌”,我们将锥面用triangle strip表示,锥底用trianglefan表示。
图2.16 Hello Cone的镶嵌
无论用strip还是fan模式,我们都可以成生锥面,但是fan模式的时候看起来会很奇怪。因为fan模式下,它的中心颜色是不正确的。就算我们为其中心指定一个颜色,在垂直方向上的将有不正确的放射效果,如图2.17 “左:triangle fan模式的锥体,右:triangle strip模式的锥体”
图2.17左:trianglefan模式的锥体,右:triangle strip模式的锥体
用strip的模式并不是生成锥面的最好方法,因为思维的时候三角形有退化过程(如图2.16中。 译注:上面的顶点不断退化为一个点的时候,就成锥体了)。用GL_TRINGLES的模式可以解决这个问题,但是需要两倍空间的顶点数组。由于OpenGL提供了一种基于索引的机制来解决这个顶点数组重复的问题,所以可以解决空间变大的问题,以后面的章节会介绍。现在我们还是用GL_TRIANGLE_STRIP来实现。生成锥体顶点的代码见示例2.8,生成过程原理见图2.18(将代码放在RenderingEngine::Initialize中//Generatevertices for the body of the cone的后面)。每一个切片需要两个顶点(一个顶点,一个底边弧上的点),还需要附加的切片来结束循环(图2.18)。于是总共的顶点数是(n+1)*2,其中n表示切片数。计算底边弧上点,采用绘制圆的经典算法即可, 如果你还记得三角函数,那对此一定觉得面熟的。
图2.18 Hello Cone顶点序列
示例 2.8 成生锥顶点
m_cone.resize((coneSlices + 1) * 2);
// Initialize the vertices of the triangle strip. vector<Vertex>::iterator vertex = m_cone.begin(); const float dtheta = TwoPi / coneSlices; for (float theta = 0; vertex != m_cone.end(); theta += dtheta) {
// Grayscale gradient float brightness = abs(sin(theta)); vec4 color(brightness, brightness, brightness, 1);
// Apex vertex vertex->Position = vec3(0, 1, 0); vertex->Color = color; vertex++;
// Rim vertex vertex->Position.x = coneRadius * cos(theta); vertex->Position.y = 1 - coneHeight; vertex->Position.z = coneRadius * sin(theta); vertex->Color = color; vertex++; } |
在此我们用一种简单的方法创建了一个灰度渐变效果,这样可以模拟灯光:
float brightness = abs(sin(theta)); vec4 color(brightness, brightness, brightness, 1); |
在这儿这个方法生成的颜色是固定的,在改变对象方向的时候是不会改变的,虽然有点遗憾,但是足以满足我们的当前需要。这种技术的术语是baked lighting,在第九章优化中会更会详细的介绍。关于更真实的灯光,在第四章中介绍。
示例2.9是生成锥底顶点的代码(将这代码放在RenderingEngine1::Initizlize中的//Generate vertices for the disk后面)。由于它用了trianglefan模式,所以总共的顶点数为:n+2, 多于的两个顶点,一个是中心点,一个是循环结束点。
示例2.9 生成锥底顶点
// Allocate space for the disk vertices. m_disk.resize(coneSlices + 2);
// Initialize the center vertex of the triangle fan. vector<Vertex>::iterator vertex = m_disk.begin(); vertex->Color = vec4(0.75, 0.75, 0.75, 1); vertex->Position.x = 0; vertex->Position.y = 1 - coneHeight; vertex->Position.z = 0; vertex++;
// Initialize the rim vertices of the triangle fan. const float dtheta = TwoPi / coneSlices; for (float theta = 0; vertex != m_disk.end(); theta += dtheta) { vertex->Color = vec4(0.75, 0.75, 0.75, 1); vertex->Position.x = coneRadius * cos(theta); vertex->Position.y = 1 - coneHeight; vertex->Position.z = coneRadius * sin(theta); vertex++; } |
为了让动画平滑,在UpdateAnimation中用四元数旋转的时候,引入了Slerp(泽注:插值相关)。当设备朝向发生变化的时候,OnRotate方法就开始新的动画序列。具体参看示例2.10,“UpdateAnimation()与OnRotate()”。
示例2.10 UpdateAnimation()与OnRotate()
void RenderingEngine1::UpdateAnimation(float timeStep) { if (m_animation.Current == m_animation.End) return;
m_animation.Elapsed += timeStep; if (m_animation.Elapsed >= AnimationDuration) { m_animation.Current = m_animation.End; } else { float mu = m_animation.Elapsed / AnimationDuration; m_animation.Current = m_animation.Start.Slerp(mu, m_animation.End); } }
void RenderingEngine1::OnRotate(DeviceOrientation orientation) { vec3 direction;
switch (orientation) { case DeviceOrientationUnknown: case DeviceOrientationPortrait: direction = vec3(0, 1, 0); break;
case DeviceOrientationPortraitUpsideDown: direction = vec3(0, -1, 0); break;
case DeviceOrientationFaceDown: direction = vec3(0, 0, -1); break;
case DeviceOrientationFaceUp: direction = vec3(0, 0, 1); break;
case DeviceOrientationLandscapeLeft: direction = vec3(+1, 0, 0); break;
case DeviceOrientationLandscapeRight: direction = vec3(-1, 0, 0); break; }
m_animation.Elapsed = 0; m_animation.Start = m_animation.Current = m_animation.End; m_animation.End = Quaternion::CreateFromVectors(vec3(0, 1, 0), direction); } |
最后非常重要的是HelloCone的Render这个方法。它与Hello Arrow的方法类似,只不过它调用了两上绘制的方法,而且在glClear加入了深度缓冲的标志。
示例 2.11RenderingEngine1::Render()
void RenderingEngine1::Render() const { glClearColor(0.5f, 0.5f, 0.5f, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushMatrix();
glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY);
mat4 rotation(m_animation.Current.ToMatrix()); glMultMatrixf(rotation.Pointer());
// Draw the cone. glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &m_cone[0].Position.x); glColorPointer(4, GL_FLOAT, sizeof(Vertex), &m_cone[0].Color.x); glDrawArrays(GL_TRIANGLE_STRIP, 0, m_cone.size());
// Draw the disk that caps off the base of the cone. glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &m_disk[0].Position.x); glColorPointer(4, GL_FLOAT, sizeof(Vertex), &m_disk[0].Color.x); glDrawArrays(GL_TRIANGLE_FAN, 0, m_disk.size());
glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_COLOR_ARRAY);
glPopMatrix(); } |
注意到rotation.Pointer()这个调用没?在我们的C++向量库中,向量与矩阵都有一个方法Pointer(),用于返回指向指一个元素的指针。 这样将更加方便传递参数到OpenGL。
注意 如果我们将用隐式转换的操作代替Pointer(),那么我们不可能使我们的OpenGL代码更加简洁,同样很容易出错,因为编译器具体做了什么,我们也不知道。出于类似的原因,STL中的string才提供c_str()这样的方法返回char*。 |
由于现在我们只实现了ES1.1的相关部份,所以在GLView.mm中得开启ForceES1。 这样你就可以编译运行你的第一个真3D应用程序。为了看到新加入的两个朝向功能, 你可以将你iPhone放在头顶看,或放在腰间低头看。图2.19 “从左到右依次为:竖屏,上下颠倒,面向上,面向下,home按键在右的横屏,home按键在左的横屏”。
图2.19从左到右依次为:竖屏,上下颠倒,面向上,面向下,home按键在右的横屏,home按键在左的横屏
对于RenderingEngine2.cpp的变化,我们不是将Hello Arrow中的复制过来做一些修改,而是将RenderingEngine1.cpp的内容复制过来,并运用ES2.0的技术来修改,这样会更有学习意义。只需要修改两处, 由于HelloArrow中RenderingEngine2.cpp中的BuildShader与BuildProgram方法仍然需要,于是将它们先保存起来,再修改engine1到engine2。示例2.12 “RenderingEnngine2类声明”是RenderingEngine2.cpp的代码。新加入或是修改的部份用粗体标识。由于一些不需要修改的部分是用…表示的,所以你不能直接复制下面的代码(只需要按粗体进行修改)。
示例2.12 RenderingEnngine2类声明
#include <OpenGLES/ES2/gl.h> #include <OpenGLES/ES2/glext.h> #include "IRenderingEngine.hpp" #include "Quaternion.hpp" #include <vector> #include <iostream>
#define STRINGIFY(A) #A #include "../Shaders/Simple.vert" #include "../Shaders/Simple.frag"
static const float AnimationDuration = 0.25f;
...
class RenderingEngine2 : public IRenderingEngine { public: RenderingEngine2(); void Initialize(int width, int height); void Render() const; void UpdateAnimation(float timeStep); void OnRotate(DeviceOrientation newOrientation); private: GLuint BuildShader(const char* source, GLenum shaderType) const; GLuint BuildProgram(const char* vShader, const char* fShader) const; vector<Vertex> m_cone; vector<Vertex> m_disk; Animation m_animation; GLuint m_simpleProgram; GLuint m_framebuffer; GLuint m_colorRenderbuffer; GLuint m_depthRenderbuffer; }; |
Initialize方法如下,但对于ES2.0不适用。
glMatrixMode(GL_PROJECTION); glFrustumf(-1.6f, 1.6, -2.4, 2.4, 5, 10);
glMatrixMode(GL_MODELVIEW); glTranslatef(0, 0, -7); |
把它们改为:
m_simpleProgram = BuildProgram(SimpleVertexShader, SimpleFragmentShader); glUseProgram(m_simpleProgram);
// Set the projection matrix. GLint projectionUniform = glGetUniformLocation(m_simpleProgram, "Projection"); mat4 projectionMatrix = mat4::Frustum(-1.6f, 1.6, -2.4, 2.4, 5, 10); glUniformMatrix4fv(projectionUniform, 1, 0, projectionMatrix.Pointer()); |
BuildShader与BuildProgram两个方法与Hello Arrow中的一样,于是在这儿不提供出来了。两个shader也一样,由于这儿是bakedlighting,所以只需要简单的传入颜色值即可。
在Render方法中设置模型-视图矩阵,参看示例2.13“RenderingEngine2::Render()”。记住,glUniformMatrix4fv与ES 1.1中的glLoadMatrix扮演的角色是一样的。
示例 2.13RenderingEngine2::Render()
void RenderingEngine2::Render() const { GLuint positionSlot = glGetAttribLocation(m_simpleProgram, "Position"); GLuint colorSlot = glGetAttribLocation(m_simpleProgram, "SourceColor");
glClearColor(0.5f, 0.5f, 0.5f, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnableVertexAttribArray(positionSlot); glEnableVertexAttribArray(colorSlot);
mat4 rotation(m_animation.Current.ToMatrix()); mat4 translation = mat4::Translate(0, 0, -7);
// Set the model-view matrix. GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, "Modelview"); mat4 modelviewMatrix = rotation * translation; glUniformMatrix4fv(modelviewUniform, 1, 0, modelviewMatrix.Pointer());
// Draw the cone. { GLsizei stride = sizeof(Vertex); const GLvoid* pCoords = &m_cone[0].Position.x; const GLvoid* pColors = &m_cone[0].Color.x; glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, pCoords); glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors); glDrawArrays(GL_TRIANGLE_STRIP, 0, m_cone.size()); }
// Draw the disk that caps off the base of the cone. { GLsizei stride = sizeof(Vertex); const GLvoid* pCoords = &m_disk[0].Position.x; const GLvoid* pColors = &m_disk[0].Color.x; glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, pCoords); glVertexAttribPointer(colorSlot, 4, GL_FLOAT, GL_FALSE, stride, pColors); glDrawArrays(GL_TRIANGLE_FAN, 0, m_disk.size()); }
glDisableVertexAttribArray(positionSlot); glDisableVertexAttribArray(colorSlot); } |
示例2.13与示例2.11流程都差不多,只有细节不同。
接着,我们将该文件中所有RenderingEngine1修改为RenderingEngine2,包括工厂方法(修改为CreateRenderer2)。同样要去掉所有的_OES与OES。关闭GLView.mm中的ForceES1,这样基于shader 的Hello Cone就修改完成了。这样ES2.0的支持完成了,并没有添加任何酷的shader效果,让我们学到了两种不同API的区别。
本章是本书术语最多的一章,我们学习了一些基础图形学概念,交澄清了第一章示例代码中掩盖的技术细节。
变换部份可能是最验理解的,也是OpenGL新手最攻克最关键的部份。我希望你能用Hello Cone来做实验,以便你更好的了解其工作原理。比如,硬编码旋转与移动,并观察顺序对渲染结果的影响。
在下一章你会学到用OpenGL绘制更复杂的图形,并初步涉及到iPhone触摸屏相关知识。
原文地址:http://blog.csdn.net/favormm/article/details/6920318