《iPhone 3D 编程》第二章:数学与抽象

***************************************************************************html

申明:本系列教程原稿来自网络,翻译目的仅供学习与参看,请匆用于商业目的,若是产生商业等纠纷均与翻译人、该译稿发表人无关。转载务必保留此申明。ios

内容:《iPhone 3D 编程》第二章:数学与抽象程序员

原文地址:http://ofps.oreilly.com/titles/9780596804824/chmath.html 译文地址:http://blog.csdn.net/favormm/article/details/6920318web

***************************************************************************算法

 

 

第二章:数学与抽象

 

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

 

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

 

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

 

集装线的抽象概念

        大家能够 把包括OpenGL ES在内的任何图形API都看做是集装线的工程流程,将各类原始材料如纹理,顶点作为输入,最终组装成五彩缤纷的图形。app

 

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

 

图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方法后缀

后缀

类型

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

 \

        从编码的角度来讲,我发现行向量比列向理更理想,因行向量更像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示例中,将会用到这个向量库。

 

用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

定义了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++向量库是模仿着它写的。

 

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

 \

 

        不管用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++;

}

 

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按键在左的横屏

 

 \

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