第二章:数学与抽象

 

计算机图形领域比计算机其它领域对数学的要求都高,如果你想成为一个合格的OpenGL程序员,那么你得撑握线性代数,并能抽象一些内容。

 

在本章,我将解释这些抽象内容与回忆线性代数的内容。其中OpenGL涉及到的概念都会得到讲解,于是在HelloArrow示例代码中的神密面纱将一层一层解开。

 

在本章结束时,我们会运用这些数学知识将HelloArrow这个示例转化为到3D空间中去,完成一个叫”HelloCone”的示例。

 

集装线的抽象概念

你们可以 把包括OpenGL ES在内的任何图形API都看作是集装线的工程流程,将各种原始材料如纹理,顶点做为输入,*终组装成五彩缤纷的图形。

 

这些传入OpenGL的数据是我们学习OpenGL首先要学的内容,在本章我们重点学习顶点。在图2.1中,用抽象的视角展示了顶点逐渐演变成像素的过程。首先是一系列的顶点变换,接着这些顶点被组装成图元,*后将这些图元光栅化到像素。

 

图2.1 OpenGL集装线

%title插图%num

注意

OpenGL ES 1.1与2.0都能抽象出集装线的概念,但ES 2.0更为明显。图2.1中,*左边的小精灵接手处理vertex shader,*右边的小精灵处理完后交给fragment shader。

 

在本章我们重点介绍集装流程中的变换,但是首先我们概述一下装配图元的流程,因为它很容易理解。

装配顶点为图元

在三维空间中,一个物体的形将可以用几何图形表示。在OpenGL中,这些几何图形是由基础图元,这些基础图元包括三角形,点,线。其础元是由一些顶点通过不同的拓扑规则构建起来的。在OpenGLES中,共有7种拓扑规则,参看图2.2“图形拓扑规则”。

图2.2 “图形拓扑规则”

%title插图%num

 

 

在*章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 两个三角形组成的四方形

%title插图%num

        图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的视图

 %title插图%num

 

在ES 1.1中,图2.4中的流程是固定的,每一个顶点都必须经过这些流程。在ES2.0中,这取决于你,在进入裁剪空间前,你可以进行任何的变换。但常常你也是进行与此相同的变换而已。

 

裁剪过后,就进入到透视变换了。我们会把坐标标准化到[-1, +1],术语叫着设备坐标标准化。图2.5描述了其变换过程。与图2.4不同的是,这些流程在ES1.1与ES2.0中都是一样的。

 

图2.5光珊化前顶点的*后三个流程

 %title插图%num

光珊化前前*后一步是视口变换,它需要一些该应中当中设定的值。你可以还认得在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方法后缀

后缀

类型

i

32位整形

x

16位定点

f

32位浮点

ub

8位无符号byte

ui

32位无符号整形

 

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矩阵相乘

 %title插图%num

从编码的角度来说,我发现行向量比列向理更理想,因行向量更像c语言中的数组。当然,只发你愿 意,你也可以用列向量,但是如果用列向量的话,你的变换顺序将要颠倒顺序才行。由于矩阵相乘不具有交换性,所以顺序很重要。

例如ES 1.1的代码:

      glLoadIdentity();

glMultMatrix(A);

glMultMatrix(B);

glMultMatrix(C);

glDrawArrays(…);

 

如果用行向量的方式,你可以把每次变换看成当前变换的pre-multiplied。那么上面的代码等效于:

%title插图%num

如果用列向量的方式,每次变换是post-multiplied。代码等效于:

%title插图%num

无论你用的是行向量还是列向量的方式,我们只需要记住一点,就是代码中*后的变换是*先作用于顶点变换的。为了更明确,那么将上面的列向量变换方式,用加括号的方式显示的展示变换的作用顺度。

%title插图%num

 

由于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 缩放变换

%title插图%num

 

图2.7展示了 sx = sy = 0.5时的缩放变换

图2.7缩放变换

 %title插图%num

警告

当缩放因子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 移动变换

 %title插图%num

 

图2.8描绘不当tx = 0.25and ty = 0.5时的移动变换

 

图2.8移动变换

 %title插图%num

旋转

 

还记得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 旋转变换

 %title插图%num

        绕着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 变换

 %title插图%num

        通过三个向量的传入,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 投影类型

 %title插图%num

        正交投影往往用于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 视锥

 %title插图%num

 

基于金字塔的顶点(称为视野)的角度,可以计算一个视锥。开发者认为这样比指定六面更加直观。示例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 机器人手臂

 %title插图%num

        每一个矩阵模式都有自己的栈,如图2.14“矩阵栈”,用得*多的是GL_MODELView。对于GL_TEXTURE模式的栈,我们会在另一章节中介绍。先前说过,OpenGL中的每一个项点位置变换都由当前的模型-视图矩阵与投影矩阵决定,也就是说在它们各自的栈中,它们位于栈顶。用glMatrixMode实现从一个栈模式到另一个模式。

 

 

图2.14 矩阵栈

%title插图%num

        在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缓动方式:线性,二次缓进,二次缓进-缓出

 %title插图%num

旋转动画与四元数

 

对于位置与颜色的关键帧,它们很容易插值:对于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示例中,将会用到这个向量库。

 

用C++优化向量

在Hello Arrow中的顶点数据结构是:

struct Vertex {

float Position[2];

float Color[4];

};

 

如果我们继续沿用C数组的方式贯穿全书,你将会发现生活是多么的糟糕! 我们真正想要的应是这样:

 

struct Vertex {

vec2 Position;

vec4 Color;

};

这正是C++运算符重载与类模版强大的地方。运用C++可以让你写一个简单的库(其实,很简单)并使你应用的代码像是基于向量的一种语言开发。其实本书中的示例就是这样的。我们的库只包括了三个头文件,没有一个cpp文件:

Vector.hpp

定义了一套三维,三维,四维向量,可以是浮点也可以是整型。并没有依附任何头文件。

Matrix.hpp

定义了2×2, 3×3, 与 4×4矩阵类,只依附了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++向量库是模仿着它写的。

 

ES1.1实现Hello Cone

现在我们开始修改HelloArrow为Hello Cone。我们要改的不只是把内容从2D变为3D,我们还要支持两个新的朝向,当设备朝上或朝下。

 

本章示例与上一章的视觉上的变化很大,主要是修改RenderingEngine2.cpp与RenderingEngine2.cpp。由于前面章节中有了良好的接口设计,现在是它发挥作用的时候了。首先来处理ES 1.1 renderer, 即RenderingEngine1.cpp。

 

RenderingEngine 声明

 

表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值的结构。

 

OpenGL 初始化与锥体的镶嵌

 

在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的镶嵌

 %title插图%num

 

无论用strip还是fan模式,我们都可以成生锥面,但是fan模式的时候看起来会很奇怪。因为fan模式下,它的中心颜色是不正确的。就算我们为其中心指定一个颜色,在垂直方向上的将有不正确的放射效果,如图2.17 “左:triangle fan模式的锥体,右:triangle strip模式的锥体”

 

图2.17左:trianglefan模式的锥体,右:triangle strip模式的锥体

%title插图%num

        用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顶点序列

 %title插图%num

示例 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++;

}

 

3D中平滑旋转

 

为了让动画平滑,在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);

}

Render 方法

 

*后非常重要的是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按键在左的横屏

 %title插图%num

Shader实现的Hello Cone

对于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触摸屏相关知识。