java求质数中 x?y?z:w:q; x,y.z.w,q都是数 这种求结果怎么求

9064人阅读
***************************************************************************
申明:本系列教程原稿来自网络,翻译目的仅供学习与参看,请匆用于商业目的,如果产生商业等纠纷均与翻译人、该译稿发表人无关。转载务必保留此申明。
内容:《iPhone
3D&编程》第二章:数学与抽象
原文地址:
译文地址:
更好的排版下载地址:
***************************************************************************
第二章:数学与抽象
&&&&&&& 计算机图形领域比计算机其它领域对数学的要求都高,如果你想成为一个合格的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_LINE_LOOP
GL_LINE_STRIP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
&&&&&&&& 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中的顶点属性
OpenGL Enumerant
OpenGL Function Call
Dimensionality
GL_VERTEX_ARRAY
glVertexPointer
byte, short, fixed, float
GL_NORMAL_ARRAY
glNormalPointer
byte, short, fixed, float
GL_COLOR_ARRAY
glColorPointer
ubyte, fixed, float
Point Size
GL_POINT_SIZE_ARRAY_OES
glPointSizePointerOES
fixed, float
Texture Coordinate
GL_TEXTURE_COORD_ARRAY
glTexCoordPointer
byte, short, fixed, float
Generic Attribute(ES 2.0)
glVertexAttribPointer
1,2,3,4
byte, ubyte, short, ushort, fixed, float
&&&&&&& OpenGL ES 2.0只有最后一行,它需要你自定义属性。回忆一下HelloArrow中不同rendering engines开启属性的方法:
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
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
signed char
GL_UNSIGNED_BYTE
unsigned char
GL_UNSIGNED_SHORT
unsigned short
&&&&&&& 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方法后缀
8位无符号byte
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();
&&&& 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 * -
&&&& m = m.Transposed();
&&&& m.w = eyeP
&&&&&&& 注意,示例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
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);
&&& m.x.x = m.x.y = 0; m.x.z = 0; m.x.w = 0;
&&& m.y.x = 0; m.y.y = m.y.z = 0; m.y.w = 0;
&&& m.z.x = m.z.y = m.z.z = m.z.w = -1;
&&& m.w.x = 0; m.w.y = 0; m.w.z = m.w.w = 1;
&&& glUniformMatrix4fv(projectionUniform, 1, 0, m.Pointer());
&&&&&&& 一旦设置了设影矩阵, 就设定了视野。视锥表示眼在金字塔顶部的一个锥体(参看图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 = -
&&&& float left = bottom * aspectR
&&&& float right = top * aspectR
&&&& 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) *
&&&&&&& 某些类型的动画,不能用线性补间的方式实现,用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 *
&&& 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 P
&&& vec4 C
&&&&&&& 这正是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) {}
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 /=
&&&&&&& y /=
&&&&&&& z /=
&&& Vector3 Normalized() const
&&&&&&& Vector3 v = *
&&&&&&& v.Normalize();
&&& 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;
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;
struct Vertex {
&&& vec3 P
&&& vec4 C
struct Animation {&&& //[1]
&&& Quaternion S
&&& Quaternion E
&&& Quaternion C
&&& float E
&&& float D
class RenderingEngine1 : public IRenderingEngine {
&&& RenderingEngine1();
&&& void Initialize(int width, int height);
&&& void Render()
&&& void UpdateAnimation(float timeStep);
&&& void OnRotate(DeviceOrientation newOrientation);
&&& vector&Vertex& m_&&&& //[2]
&&& vector&Vertex& m_&&&& //[3]
&&& Animation m_
&&& GLuint m_
&&& GLuint m_colorR
&&& GLuint m_depthR
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 th 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 / coneS
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 =
&&& vertex++;
&&& // Rim vertex
&&& vertex-&Position.x = coneRadius * cos(theta);
&&& vertex-&Position.y = 1 - coneH
&&& vertex-&Position.z = coneRadius * sin(theta);
&&& vertex-&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 - coneH
vertex-&Position.z = 0;
vertex++;
// Initialize the rim vertices of the triangle fan.
const float dtheta = TwoPi / coneS
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 - coneH
&&& 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)
&&& m_animation.Elapsed += timeS
&&& if (m_animation.Elapsed &= AnimationDuration) {
&&&&&&& m_animation.Current = m_animation.E
&&& } else {
&&&&&&& float mu = m_animation.Elapsed / AnimationD
&&&&&&& m_animation.Current = m_animation.Start.Slerp(mu, m_animation.End);
void RenderingEngine1::OnRotate(DeviceOrientation orientation)
&&& switch (orientation) {
&&&&&&& case DeviceOrientationUnknown:
&&&& &&&case DeviceOrientationPortrait:
&&&&&&&&&&& direction = vec3(0, 1, 0);
&&&&&&&&&&&
&&&&&&&&&&&
&&&&&&& case DeviceOrientationPortraitUpsideDown:
&&&&&&&&&&& direction = vec3(0, -1, 0);
&&&&&&&&&&&
&&&&&&&&&&&
&&&&&&& case DeviceOrientationFaceDown:&&&&&&
&&&&&&&&&&& direction = vec3(0, 0, -1);
&&&&&&&&&&&
&&&&&&&&&&&
&&&&&&& case DeviceOrientationFaceUp:
&&&&&&&&&&& direction = vec3(0, 0, 1);
&&&&&&&&&&&
&&&&&&&&&&&
&&&&&&& case DeviceOrientationLandscapeLeft:
&&&&&&&&&&& direction = vec3(+1, 0, 0);
&&&&&&&&&&&
&&&&&&&&&&&
&&&&&&& case DeviceOrientationLandscapeRight:
&&&&&&&&&&& direction = vec3(-1, 0, 0);
&&&&&&&&&&&
&&& m_animation.Elapsed = 0;
&&& m_animation.Start = m_animation.Current = m_animation.E
&&& 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 {
&&& RenderingEngine2();
&&& void Initialize(int width, int height);
&&& void Render()
&&& void UpdateAnimation(float timeStep);
&&& void OnRotate(DeviceOrientation newOrientation);
&&& GLuint BuildShader(const char* source, GLenum shaderType)
&&& GLuint BuildProgram(const char* vShader, const char* fShader)
&&& vector&Vertex& m_
&&& vector&Vertex& m_
&&& Animation m_
&&& GLuint m_simpleP
&&& GLuint m_
&&& GLuint m_colorR
&&& GLuint m_depthR
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 *
&&& 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触摸屏相关知识。
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:998414次
积分:14127
积分:14127
排名:第273名
原创:365篇
转载:45篇
译文:10篇
评论:414条}

我要回帖

更多关于 java求质数 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信