[jimmyzhouj 翻译] Nehe iOS OpenGL ES 2.0教程 --Lesson 02

          [jimmyzhouj 翻译]  Nehe iOS OpenGL ES 2.0教程

 
免责声明:本教程翻译原稿均来自互联网,仅供学习交流之用,请勿进行商业传播。同时,转载时不要移除本声明。如产生任何纠纷,均与本博客所有人和发表该翻译稿之人无任何关系。谢谢合作!
 
IOS Lesson 02 – 第一个三角形
原文链接地址: http://nehe.gamedev.net/tutorial/ios_lesson_02__first_triangle/50001/
 
上节课我们重点介绍了程序的基本结构,本次课程我们要真正开始绘画了。下图就是程序运行时显示内容的截屏:
 

 
 
 

使用OpenGL将几何图形(geometry)绘制到屏幕上需要完成几个步骤。首先我们需要告诉OpenGL绘制什么几何图形。通常是一系列的三角形,每个三角形用三个顶点来指定。OpenGL为每个顶点调用顶点着色器(vertex shader),它可以通过旋转和移动来执行几何变换,或者是简单的光照(lighting)。得到的平面被光栅化(rasterized),意思是OpenGL会计算出被各平面覆盖的所有像素点。对每一个被覆盖的像素点,OpenGL会调用片段着色器(fragment shader,如果我们使用多重抽样,每个像素会运行不止一次,所以总的来说这是片段而不是像素)。片段着色器的任务是根据颜色,光照来决定片段的颜色,或者是将图像映射到几何图形上。

 
因此本课程包括两块内容:怎么样绘制一个三角形,和一个简单的着色器是什么样的。
你最好先下载源代码,然后我们进入下一步。
 
上传几何图形

几何图形可以被指定为几种几何图元:GL_POINT, GL_LINES或者GL_TRIANGLES,其中直线和三角形都有一些变体,可以绘制chain的或者strip的。所有这些几何图元都是由一组顶点组成的。一个顶点是三维空间的一个点,参照坐标系是x轴指向正右方,y轴指向正上方,z轴垂直于屏幕指向读者。

当我们之后讲到移动,旋转和投影课程的时候我们会好好讲讲深度(depth)。现在你只要知道我们需要三个浮点数来表示x,y,z的位置,还需要额外的第四个元素w来实现线性变换(由于矩阵乘法的原因)。
在顶点着色器的最后,屏幕上的每一个顶点都必须在一个立方体内,立方体在x,y,z上的坐标范围都是从-1.0到1.0。我们不做复杂的变换,所以我们选择的顶点位置符合这个规则。
 
在Lesson02::init方法中,我们将顶点存为浮点数的连续数组
  
  
  
  
  1. //create a triangle 
  2. std::vector<float> geometryData; 
  3.   
  4. //4 floats define one vertex (x, y, z and w), first one is lower left 
  5. geometryData.push_back(-0.5); geometryData.push_back(-0.5); geometryData.push_back(0.0); geometryData.push_back(1.0); 
  6. //we go counter clockwise, so lower right vertex next 
  7. geometryData.push_back( 0.5); geometryData.push_back(-0.5); geometryData.push_back(0.0); geometryData.push_back(1.0); 
  8. //top vertex is last 
  9. geometryData.push_back( 0.0); geometryData.push_back( 0.5); geometryData.push_back(0.0); geometryData.push_back(1.0); 
 

可能你已经注意到,我们是按照逆时针的次序指定顶点的。这有助于帮助OpenGL丢弃掉我们看不见的几何图形。根据定义,只有逆时针方向的三角形才是面向(facing towards)我们的(所以表面只有一侧)。假设你要翻转这个三角形,只要你翻转超过90度了,顶点的次序也翻转了。
我们现在已经有一个浮点数数组了,怎么把它传递给OpenGL呢?
我们使用了一个概念叫做Vertex Buffer Objects(VBO)。这就是在图像内存中分配一块缓冲区(buffer),然后将几何数据移到那里。当我们之后绘制几何图形时,数据已经在芯片上了,这样就减少了每一帧传递数据的带宽要求。对那些在程序运行时不发生变形(deform)的几何图形,这会工作的很好。一旦几何图形的拓扑发生了变化,必须将新的数据拷贝到缓冲区中。其他各种变换,比如移动,旋转,伸缩都能很容易在顶点着色器中实现,不需要改变缓冲区的内容。
 
 
  
  
  
  
  1. //generate an ID for our geometry buffer in the video memory and make it the active one 
  2. glGenBuffers(1, &m_geometryBuffer); 
  3. glBindBuffer(GL_ARRAY_BUFFER, m_geometryBuffer); 
  4.   
  5. //send the data to the video memory 
  6. glBufferData(GL_ARRAY_BUFFER, geometryData.size() * sizeof(float), &geometryData[0], GL_STATIC_DRAW); 

第一步是在图像内存中生成一个缓冲区。可以调用函数glGenBuffers,第一个参数是要分配的缓冲区的数目,第二个参数是指针,指向保存缓冲区的 id的变量。在头文件中,m_geometryBuffer定义为unsigned int。
接下来绑定缓冲区,之后的所有缓冲区操作都是在我们的geometry buffer上执行的。GL_ARRAY_BUFFER指我们在这里保存简单数据,我们一直使用这个缓冲区直到重新索引到别的缓冲区中去。
最后,我们调用函数glBufferData把几何数据送到VBO中。几个参数分别为:类型,要拷贝的字节长度,指向数据的指针,和是否允许显示芯片做内存优化。对于静态图形,最后一个参数用GL_STATIC_DRAW,如果我们想定期上传新数据,这里要用GL_DYNAMIC_DRAW。
 
添加颜色
到现在为止,我们传递了几何数据,已经可以用纯色画一个三角形了。不过我们还可以
用前面的方法来实现顶点的其他属性(attributes),比如我们可以给每个顶点指定颜色。
在大部分图形应用中,颜色被指定为红,绿,蓝通道的强度( intensities)。在大部分图片格式中强度取值范围为0到255,不过我们在图形芯片上使用的是浮点数,强度范围指定为0到1。
颜色数据的概念和之前几何数据一样:

  
  
  
  
  1. //create a color buffer, to make our triangle look pretty 
  2. std::vector<float> colorData; 
  3.   
  4. //3 floats define one color value (red, green and blue) with 0 no intensity and 1 full intensity 
  5. //each color triplet is assigned to the vertex at the same position in the buffer, so first color -> first vertex 
  6.   
  7. //first vertex is red 
  8. colorData.push_back(1.0); colorData.push_back(0.0); colorData.push_back(0.0); 
  9. //lower right vertex is green 
  10. colorData.push_back(0.0); colorData.push_back(1.0); colorData.push_back(0.0); 
  11. //top vertex is blue 
  12. colorData.push_back(0.0); colorData.push_back(0.0); colorData.push_back(1.0); 
  13.   
  14. //generate an ID for the color buffer in the video memory and make it the active one 
  15. glGenBuffers(1, &m_colorBuffer); 
  16. glBindBuffer(GL_ARRAY_BUFFER, m_colorBuffer); 
  17.   
  18. //send the data to the video memory 
  19. glBufferData(GL_ARRAY_BUFFER, colorData.size() * sizeof(float), &colorData[0], GL_STATIC_DRAW); 
 
 
绘图(drawing)
 
现在所有的数据已经保存在图像内存中了,我们只需要把它显示出来。这通过告诉OpenGL我们要使用的着色器程序来实现。
我们不会详细解释着色器的加载工作。有兴趣了解细节的人可以去看Shader类已有的丰富文档。如果有人需要更细致的解释,请在论坛里告诉我。
 
  
  
  
  
  1. //load our shader 
  2. m_shader = new Shader("shader.vert""shader.frag"); 
  3.   
  4. if(!m_shader->compileAndLink()) 
  5. NSLog(@"Encountered problems when loading shader, application will crash..."); 
  6.   
  7. //tell OpenGL to use this shader for all coming rendering 
  8. glUseProgram(m_shader->getProgram()); 
 

在代码里,我们创建了Shader类的一个对象,告诉它输入文件的名字。就像之前说过的,有顶点着色器和片段着色器两个阶段。它们分别加载,编译自己的输入文件,最后链接到着色器程序,它是一个vertex shader和fragment shader对。这在compileAndLink()方法中实现,如果出错的话,会返回false。
最后,我们调用glUseProgram来告诉OpenGL使用我们刚刚产生的着色器程序。
真简单。现在怎么样把几何数据和颜色数据送给着色器呢?
对每一个顶点,顶点着色器可以处理几个输入值,如之前添加颜色缓冲区时所说的。这些值被称为属性(attributes)。只要是全局定义了属性,就可以在着色器里访问它们。比如attribute vec4 position,表示有一个属性,名字是position,被指定为一个有4个浮点值的向量。
从程序的观点来看,我们需要把geometry buffer里面的数据分配给名字是position的属性,将color buffer的数据分配给名字是 color的属性。也可以用别的名字,但在这节课里还是用这个吧。
 
 
  
  
  
  
  1. //get the attachment points for the attributes position and color 
  2. m_positionLocation = glGetAttribLocation(m_shader->getProgram(), "position"); 
  3. m_colorLocation = glGetAttribLocation(m_shader->getProgram(), "color"); 
  4.   
  5. //check that the locations are valid, negative value means invalid 
  6. if(m_positionLocation < 0 || m_colorLocation < 0) 
  7. NSLog(@"Could not query attribute locations"); 
  8.   
  9. //enable these attributes 
  10. glEnableVertexAttribArray(m_positionLocation); 
  11. glEnableVertexAttribArray(m_colorLocation); 

OpenGL会索引着色器程序里的所有属性,通过调用函数glGetAttribLocation并且传给它着色器程序和程序源码中属性的名字,我们可以得到并存储索引值。m_positionLocation和m_colorLocation是int型的,所有无效的属性名都会得到-1的返回值。
最后我们调用glEnableVertexAttribArray来允许传送数据给这些属性。
到现在为止我们已经完成了初始化方法的所有工作了。但实际的绘制在每一帧都要发生。所以在Lesson02::draw 方法中,我们需要将缓存的数据映射到属性,然后开始绘制。
 
  
  
  
  
  1. //bind the geometry VBO 
  2. glBindBuffer(GL_ARRAY_BUFFER, m_geometryBuffer); 
  3. //point the position attribute to this buffer, being tuples of 4 floats for each vertex 
  4. glVertexAttribPointer(m_positionLocation, 4, GL_FLOAT, GL_FALSE, 0, NULL); 
 

为了映射数据,首先我们要将geometry buffer绑定以激活它,就像之前传送数据时做的那样。然后我们告诉OpenGL这就是position属性要获得数据的缓冲区。glVertexAttribPointer可以实现这个功能,参数分别是:属性的位置,多少个数组成一个元素(这里1个顶点有4个浮点数 ),数据类型(float),数据是否需要向量化(通常不用),如果交错数据( interleave data)的话需要一个跨距(stride)(这里没有,0),最后一个参数是指向一个数组的指针,每一帧该数组将被拷贝到图形芯片中。我们这里传递NULL,告诉OpenGL使用当前绑定的缓冲区内容。
 
 
 
  
  
  
  
  1. //bint the color VBO 
  2. glBindBuffer(GL_ARRAY_BUFFER, m_colorBuffer); 
  3. //this attribute is only 3 floats per vertex 
  4. glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, NULL); 

绑定color buffer的过程和前面的一样。
现在已经万事俱备,可以画三角形了。
 
 
  
  
  
  
  1. //initiate the drawing process, we want a triangle, start at index 0 and draw 3 vertices 
  2. glDrawArrays(GL_TRIANGLES, 0, 3); 

函数glDrawArrays需要三个参数:我们要画的图元(可以是GL_POINTS,GL_LINE_STRIP,GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES之一),离缓冲区首个元素的偏移量,和需要画多少个顶点。

如果我们增加一个三角形,包括了3个顶点的geometry buffer和color buffer。当想要同时画这两个三角形时,我们只需要把dlDrawArrays中顶点的数目从3改为6就可以了。
 
Yay!我们确实把第一个三角形画到屏幕上了!
 

OpenGL Shading Language

我们还不知道在图形芯片里发生了什么。可编程图形管道(programmable graphics pipeline)的核心部分就是顶点着色器和片段着色器,它们是用OpenGL ES 着色语言(GLSL)指定的。它看起来和C语言很像,但有一些不同的数据类型,和包含2,3,后者4个该数据类型的元素的向量。对浮点数来说,这些向量就是vec2, vec3, vec4。
我们不准备详细的解释GLSL,而是看一个简单的例子。对于GLSL编程来说有一个又好又全的帮助文档,这就是快速参考卡的第三页和第四页,下载地址如下:
http://www.khronos.org/opengles/sdk/2.0/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf
那我们的顶点着色器使用的shader.vert是什么样的呢,我们已经讨论了属性,它被用作输入:
 
 
  
  
  
  
  1. //the incoming vertex' position 
  2. attribute vec4 position; 
  3.   
  4. //and its color 
  5. attribute vec3 color; 

这里我们看到了两个向量,position对应的向量有4个元素,color对应的有3个元素。其实color是在片段着色器而不是在顶点着色器中才有用。为了把数值从顶点着色器传递到片段着色器去,需要全局定义为varying限定符。
 
 
  
  
  
  
  1. //the varying statement tells the shader pipeline that this variable 
  2. //has to be passed on to the next stage (so the fragment shader) 
  3. varying lowp vec3 colorVarying; 

colorVarying这个名字看起来很奇怪,这里只是给大家做一个示范。记住,顶点着色器只是在每一个顶点才调用一次,这样一个三角形会调用3次。而片段着色器会在每一个覆盖的像素点都调用一次。要注意的是,在顶点之间的每一个像素点,一个varying变量的数值会由线性插值计算得到。这就是为什么显示出来的三角形有这么光滑的颜色过渡。
关键字lowp代表低精度。在GLSL中我们需要给变量指明是高精度,中等精度还是低精度(highp, mediump, lowp)。对颜色值而言,精度不是那么重要,所以用低精度就可以了。如果是顶点的位置,为了防止边缘部分出现奇怪的人工痕迹,我们就需要用高精度。
 
 
  
  
  
  
  1. //the shader entry point is the main method 
  2. void main() 
  3. colorVarying = color; //save the color for the fragment shader 
  4. gl_Position = position; //copy the position 

运行着色器程序的时候,首先调用的是main()方法。我们先传递color值,然后将position属性拷贝给预先定义的输出变量gl_Position,指明将position用于光栅化。
 
最后在片段着色器用到的shader.frag中,将传来的color值存到指定的输出变量gl_FragColor中。因为每一个像素点都要执行一次main方法,给每一个覆盖到的片段都设置上颜色,我们就可以在屏幕上看到一个着色非常棒的图形了。
 
 
  
  
  
  
  1. //incoming values from the vertex shader stage. 
  2. //if the vertices of a primitive have different values, they are interpolated! 
  3. varying lowp vec3 colorVarying; 
  4.   
  5. void main() 
  6. //create a vec4 from the vec3 by padding a 1.0 for alpha 
  7. //and assign that color to be this fragment's color 
  8. gl_FragColor = vec4(colorVarying, 1.0); 

gl_FragColor定义为vec4,所以我们需要在后面加一个alpha值。alpha通道定义了颜色的不透明度。这里我们需要三角形是完全不透明的,将它设为1.0。
注意:GLSL不支持从int到float的隐式类型转换,alpha值不能写为”1”,必须是”1.0”。
 
到这里我就结束这节课了。你可以试着修改颜色,顶点的位置,和增加一些三角形。要确保你已经明白了这些基本原则。
 
Cheers,
Carsten
 
 

你可能感兴趣的:(ios,翻译,教程,OpenGL,es,nehe)