8.2、深入探讨GLKSkyboxEffect是怎么工作的

例子OpenGLES_Ch8_2中的AGLKSkyboxEffect实现了GLKit的GLKSkyboxEffect,并展示了怎么使用OpenGL ES 2.0 建立一个天空盒效果。虽然例子OpenGLES_Ch8_2 使用的是AGLKSkyboxEffect而不是GLKSkyboxEffect,但是除此之外它与例子OpenGLES_Ch8_1是相同的。

下面是AGLKSkyboxEffect的接口代码,与GLKSkyboxEffect相似。


//
//  AGLKSkyboxEffect.h
//  
//

#import <GLKit/GLKit.h>

@interface AGLKSkyboxEffect : NSObject <GLKNamedEffect>

@property (nonatomic, assign) GLKVector3 center;                
@property (nonatomic, assign) GLfloat xSize;
@property (nonatomic, assign) GLfloat ySize;
@property (nonatomic, assign) GLfloat zSize;
@property (strong, nonatomic, readonly) GLKEffectPropertyTexture 
   *textureCubeMap;
@property (strong, nonatomic, readonly) GLKEffectPropertyTransform         
   *transform;

- (void) prepareToDraw;
- (void) draw;

@end

AGLKSkyboxEffect会使用’下面的顶点所定义的三角形带绘制天空盒立方体。立方体每个面的长度是1.0 (-0.5到0.5)。使用这个尺寸可以方便地把天空盒缩放为任意的大小。


  // The 8 corners of a cube
      const float vertices[AGLKSkyboxNumCoords] = {  
          -0.5, -0.5,  0.5,
           0.5, -0.5,  0.5,
          -0.5,  0.5,  0.5,
           0.5,  0.5,  0.5,
          -0.5, -0.5, -0.5,
           0.5, -0.5, -0.5,
          -0.5,  0.5, -0.5,
           0.5,  0.5, -0.5,
      };

      glGenBuffers(1, &vertexBufferID);
      glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
      glBufferData(GL_ARRAY_BUFFER, 
         sizeof(vertices), 
         vertices, 
         GL_STATIC_DRAW);

      // Indices of triangle strip to draw cube
      // Order is critical to make "front" faces be on inside
      // of cube. 
      const GLubyte indices[AGLKSkyboxNumVertexIndices] = {
         1, 2, 3, 7, 1, 5, 4, 7, 6, 2, 4, 0, 1, 2
      };
      glGenBuffers(1, &indexBufferID);
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferID);
      glBufferData(GL_ELEMENT_ARRAY_BUFFER, 
         sizeof(indices), 
         indices, 
         GL_STATIC_DRAW);

AGLKSkyboxEffect会使用与前面的例子相似的代码来利用一个顶点属性数组缓存和一个元素数组缓存绘制立方体。绘制天空盒的诀窍是确保三角形带中的每个三角形的前面朝向天空盒内部。否则,从天空盒内部渲染场景时就会产生错误的结果。如在第7.3节所讲解的,每个三角形的顶点顺序决定了哪~一面是前面。

绑定天空盒纹理为绘制做的准备。然后平移天空盒的坐标系,直到天空盒的中心处在center属性所指定的位置,最后使用xSize、ySize和zSize属性所指定的尺寸缩放天空盒的坐标系。这些变换会施加到天空盒的modelviewMatrix上,在天空盒准备好绘制时modelviewMatrix矩阵已经包含了视点的变换。


// Translate skybox cube to specified center and scale to 
// specified size
GLKMatrix4 skyboxModelView = GLKMatrix4Translate(
self.transform.modelviewMatrix,
self.center.x, self.center.y, self.center.z);
skyboxModelView = GLKMatrix4Scale(
skyboxModelView,
self.xSize, self.ySize, self.zSize);

接着绑定并配置天空盒的顶点属性数组缓存和元素数组缓存。


glEnableVertexAttribArray(GLKVertexAttribPosition);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glVertexAttribPointer(GLKVertexAttribPosition, 
3, 
GL_FLOAT, 
GL_FALSE, 
0, 
NULL);                               // Step 3

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferID);

AGLKSkyboxEffect的“-draw” 方法会利用glDrawElements函数向GPU发送顶点数据,具体代码如下:


/////////////////////////////////////////////////////////////////
// 
- (void)draw
{
   glDrawElements(GL_TRIANGLE_STRIP, 
      AGLKSkyboxNumVertexIndices, 
      GL_UNSIGNED_BYTE, 
      NULL);                                    // Step 5
}

天空盒的Shading Language程序

GLKit会自动生成ShadingLanguage程序,因此通常不需要你自己编写。尽管如此,在前面的例子中已经使用了多个自定义的OpenGLESShadingLanguage程序。第3章的例子OpenGLES_Ch3_6改编了苹果的一个示例程序来渲染和驱动两个相似的立方体:其中一个立方体是使用GLKit的GLKBaseEffect绘制的,另一个是使用一个自定义的Shading Language程序绘制的,这个自定义的Shading Language程序复制了GLKBaseEffect生成的Shading Language程序的部分功能。

第5章的例子OpenGLES_Ch5_5 创建了AGLKTextureTransformBaseEffect类及其相关的Shading Language程序,以此来演示iOS 5的GLKit还不支持的纹理坐标变换功能。

第6章的多个例子扩展了AGLKTextureTransformBaseEffect 类,以此来模拟聚 光灯,这个聚光灯与OpenGLES1.1内置的以及GLKit复制的聚光灯功能相似。实际上,AGLKTextureTransformBaseEffect 类中使用的这个自定义Shading Language程序超过了iOs5中的ShadingLanguage程序的实际复杂度。第7章介绍了AGLKArmatureBaseEffect类以及与之相伴的相对简单的Shading Language程序。

本节提供了一个揭秘自定义Shading Language程序的旋风之旅。这个主题是庞大且复杂的,一些书对这个主题做了详细的讲解,比如由Aaftab Munshi、Dan Ginsburg 和Dave Shreiner所编著的《OpenGL ES 2.0 Programming Guide》一书。除非确实需要使用OpenGLES2.0,否则苹果的GLKit会减少并通常会尽量消除ShadingLanguage代码,但实际上苹果无法提供你所需的所有ShadingLanguage程序。在重新构建GLKSkyboxEffect的过程中,例子OpenGLES_Ch8_2的AGLKSkyboxEffect类会使用一个足够简单的Shading Language程序做一个详细介绍。

OpenGL ES 2.0 Shading Language程序通常包含两个着色器(shader) 子程序。一个是顶点着色器(vertex shader), 在渲染一个场景时,每处理一个顶点这个着色器都会执行一次。另一个着色器是片元着色器(fragment shader), 这个着色器会为每个可能写入像素颜色渲染缓存的片元执行一次。 一般场景通常包含数十万或者数百万个顶点。每次渲染一个场景时,GPU都可以计算一百万个或者更多的片元颜色。回想一下,当几何对象重叠时就会出现片元重复。深度缓存可以决定重叠片元中的哪些片元会决定像素颜色渲染缓存中的最终值。否则的话,为每个位置最后计算的片元颜色会替换掉前面计算的颜色值。

现代GPU是非常快速并且高度并行的。即便如此,简单的ShadingLanguage程序通常仍然比复杂的程序执行得快。当程序每秒执行六千万或者更多次时,简单变得非常重要。据粗略计算,一个包含100个指令并要计算100万个片元颜色的片元着色器要产生每秒60帧的渲染结果的话,就需要GPU在- -秒内执行60亿个指令。

1. 顶点着色器

顶点着色器会为提交到GPU的每个顶点运行一次,并且每次顶点着色器运行时,它必须要设置一个叫做gl_Position的内置变量的值。赋给gl_Position的值指定了像素颜色渲染缓存的坐标系中的一个位置,这个坐标系有时又被称为剪裁空间(clip space),因为渲染缓存坐标系之外的值是不可见的,并且因此会被从渲染结果中“剪裁出去”。顶点着色器可以自由地使用任意逻辑来计算gl_Position的值,但是通常情况下,传入的顶点位置属性是通过model-view矩阵和投影矩阵的变换来计算像素颜色渲染缓存中的对应位置的,就像在第5章中讲解的那样。例子OpenGLES_Ch8_2使用的就是下面 的顶点着色器。粗体代码显示的就是对gl_Position 变量的赋值语句。

//
//  SkyboxShader.fsh
//  
//

/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4      u_mvpMatrix;
uniform samplerCube     u_unitCube[1];

/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying lowp vec3       v_texCoord[1];


void main()
{
   gl_FragColor = textureCube(u_unitCube[0], v_texCoord[0]);
}

分解整个顶点着色器,首先是声明每个顶点所需的属性。这个着色器只使用一个属性,就是每个顶点的位置属性。OpenGL ES Shading Language的关键字attribute用于声明一个属性,并且会指定属性的数据类型。在当前情况下,语句“attribute vec3 aposition;”声明-一个命名为a position 的属性,这个属性是由三个浮点元素组成的矢 量。属性的名字是完全随意的。属性可以被顶点程序以任何方式使用。当前的ShadingLanguage程序需要一个什么样的位置属性完全取决于为GPU提供顶点属性数组缓存的程序员。根本没有或者有很少编译器支持或运行时检查。一个不明智的程序员可能会为GPU提供顶点颜色属性而不是位置属性,这样导致GPU渲染垃圾,而不会做任何检测或者错误报告。OpenGL ES Shading Language可能看起来有点像C语言,但它实际上是一种标准化的GPU汇编语言形式,正如一些程序员说的,你正在以非常接近硬件的层次编程。顶点属性只在顶点着色器中可用。片元着色器并不能访问它们。

uniform变量会保存在GPU控制的内存中。在向GPU提供顶点属性之前必须要设置它们的值,在所有提交的顶点被处理之前这些值可能都不会发生变化。uniform关键字会声明统一的变量,并且通常还会设置精度和数据类型。语句“uniform highp mat4 u mvpMatrix;"声明了一个命名为u mvpMatrix的统一变量,这个变量是一个高精度浮点值的4X4矩阵。高精度值相当于C语言中的GLfloat数据类型。除了highp,还有两个其他的可用精度,mediump和lowp。OpenGLES2.0标准并没有指定mediump和lowp所代表的具体精度,除了lowp变量必须能够保存颜色元素值,以及mediump变量必须要支持lowp和highp之间的一些精度。对于iOs设备来说,lowp变量会保存在-1.0到1.0范围内的浮点值,并且这将足以保存标准化矢量和颜色矢量。

在大多数情况下, GPU使用lowp变量进行运算的速度要比使用highp变量的速度更快,因此应该尽量使用最小的精度来满足每个程序的需求。类似这个例子中使用的u_ mvpMatrix 变量的变换矩阵需要保存超出-1.0到1.0范围的值,因此必须要声明为highp类型。在这个例子中使用的其他统-变量会保存-一个纹理取样单元的标识符,纹理取样单 元又被称为纹理单元。

uniform samplerCube     u_unitCube[1];

samplerCube数据类型是另一个内置的魔法位。每个GPU都有两个或者多个通常并行工作的硬件纹理取样单元。u_unitCube变量声明了一个用来在GPU的纹理单元之间做选择的标识符数组。这个变量被声明为了一个统一变量,因此可以在向GPU提交顶点属性之前通过编程改变它的值。这个程序只需要一个纹理取样单元,因此u_unitCube是-一个 只存储了一个标识符的数组。

天空盒使用的是8.1节讲解的立方体纹理取样方式。更常见的2D位置纹理取样方式是使用内置的sampler2D数据类型来指定纹理取样单元的。实际上,对于GLuint来说,samplerCube和sampler2D是相同的。两种数据类型都对应于OpenGL ES的资源标识符。第三个也是最后一种变量是使用varying关键字来声明的。可变变量是可选的附加输出变量,用于顶点着色器和片元着色器间的数据传递。与统一变量类似, 可变变量有指定的精度。片元着色器无法访问顶点属性,因此可变变量的一个常见用途是保存从顶点着色器获得的属性。大部分GPU会在稀缺的GPU寄存器中存储可变变量。varying标识符只能用在float、vec2、 vec3、 vec4、mat2、 mat3 和mat4数据类型中,或者这些类型的数组中。OpenGL ES 2.0标准需要GPU支持至少32个浮点类型的可变变量。一个vec2记作两个浮点值。-个mat4记作16个浮点值。

例子OpenGLES_Ch8_2的顶点程序会使用varying lowp vec3 v_texCoord[1] 数组向片元程序传递纹理坐标。这个变量的精度是lowp,因为一个天空盒的纹理坐标通常在-1.0到1.0的lowp精度范围内。一个更加通用的着色器不应该做这个假设而应该使用highp精度的纹理坐标。这个数组只包含一个纹理坐标集,因为在这个例子中只使用了一个纹理。一个多重纹理片元着色器可能为每个顶点使用多个纹理坐标。

所有的Shading Language着色器都有一个main()函数,GPU会使用这个函数作为着色器的入口点。在这个例子的顶点着色器中的main()函数是尽可能简单的。每个未变换的顶点位置属性还可以用作天空盒立方体内的纹理坐标。这只是本例的特定“单元立方体”顶点位置所引起的一个实现细节。纹理坐标通常是作为顶点属性来存储的,并且是与位置属性分离的。

void main()
{
   v_texCoord[0] = a_position; 
   gl_Position = u_mvpMatrix * vec4(a_position, 1.0); 
}

依照所有顶点着色器所需要的,设置内置变量gl_Position。 统一变量u_mvpMatrix 保存了定义了视点的model-view矩阵和投影矩阵的结合。使用u_mvpMatrix变化a_position属性以得出像素颜色渲染缓存坐标系内的对应位置。表达式vec4(a_position,1.0)会把三维位置矢量扩展成四维矢量。设置第四个坐标为1.0会让接下来的变换可以包含在u_mvpMatrix 中编码的任意平移。如果第四个坐标被设置成0.0,那么这个位置可以旋转和缩放,但是不能平移。

2.片元着色器

理解片元着色器的关键点之一是要意识到片元与顶点着色器输出的位置并不 是一对应的。转换几何图形数据为帧缓存内的颜色像素的这个渲染步骤叫做栅格化(rasterizing)。为了栅格化几何图形,GPU会跟踪要渲染的点、线段,或者三角形是由哪些顶点定义的。然后GPU会使用片元着色器计算由每个点、线段,或者三角形所产生的每个片元的颜色。会计算出顶点本身位置以及顶点之间的所有位置的片元颜色。GPU会为顶点着色器所设置的所有可变变量插值。例如,顶点着色器会为定义了一条线段的两个顶点各执行一次,并且每次顶点着色器运行时都会在gl_Position中存储一个不同的像素颜色渲染缓存位置。GPU必须要计算分别赋值给gl_Position的 两个顶点位置之间的线段上的片元颜色。通过使用线性内插法来做到这一点,以使在两个顶点之间的一个位置近似等于每个末端顶点位置的平均值,{ (x0 0.5 + x1 0.5), (y0 0.5 + y1 0.5) }。在第一个顶点和第二个顶点之间的3/4位置处的值是这样计算的:{(x0 0.25+x1 0.75), (y0 0.25+y1 0.75)}。实际上,这个计算并不总是那么简单,因为透视所产生的透视收缩会影响顶点间的片元的内插位置。

片元着色器会为可变变量值的每个内插集合执行一次。并不是只有位置是内插值的,纹理坐标和颜色也是内插值的。当在一个片元程序中计算每个片元的灯光时,即使是法向量也是在顶点间内插值的。

下面的代码实现了例子OpenGLES_Ch8_2的片元着色器。就像顶点着色器会设置特定的内置的gl_Position 变量一样,片元着色器必须要设置内置的gl_FragColor 变量为每个片元计算出来的最终颜伍。如下代码中的粗体代码执行的就是这个赋值。

//
//  SkyboxShader.fsh
//  
//

/////////////////////////////////////////////////////////////////
// UNIFORMS
/////////////////////////////////////////////////////////////////
uniform highp mat4      u_mvpMatrix;
uniform samplerCube     u_unitCube[1];

/////////////////////////////////////////////////////////////////
// Varyings
/////////////////////////////////////////////////////////////////
varying lowp vec3       v_texCoord[1];


void main()
{
   **gl_FragColor = textureCube(u_unitCube[0], v_texCoord[0]);**
}

片元着色器是尽可能简单的。最终片元颜色是通过调用内置的textureCube()函数计算出来的,这个函数首先会指定要使用的纹理取样单元的标识符,然后会指定片元的纹理坐标。请记住,这个纹理坐标就是GPU插补的实际值。编写OpenGL ES 2.0 Shading Language程序有很多的细微差别、局限和古怪。OpenGL ES 2.0 Shading Language是桌面版OpenGL Shading Language的个子集,因此要谨慎对待适用于桌面系统的例子。在网址: http://www.khronos.org/opengles/sdk/docs/reference_cards/OpenGL-ES-2_0-Reference-card.pdf 上有一个方便的快速参考卡, 如果你是一个新手的话,这个快速参考卡可以帮助你找到内置函数、类型、精度和内置变量的正确名字

3.在你的代码中使用OpenGL ES 2.0 Shading Language

苹果提供的示例代码会包含下面的四个方法,本书中的每个自定义的Shading Language 例子都重用了这四个方法

- (BOOL)loadShaders;
- (BOOL)compileShader:(GLuint *)shader  
    type:(GLenum)type 
    file:(NSString *)file;
- (BOOL)linkProgram:(GLuint)prog;
- (BOOL)validateProgram:(GLuint)prog;
图 8-3

完整地介绍创建OpenGL ES 2.0 Shading Language程序对象的步骤会超出本书的范围。但是其实并没有看起来那么难。借助苹果的例子方法,只要包含下面的代码到类似AGLKSkyboxEffect、AGLKTextureTransformBaseEffect, 或者AGLKArmatureBaseEffect的自定义类的“prepareToDraw” 方法中就可以了,就是这么简单。

if(0 == program)
{
    [self loadShaders ] ;
}

本书每个例子中的“-loadShaders”方法都是相同的,除了包含顶点和片元着色器源代码的文件的名字、特定属性,以及着色器使用的统一变量。检查例子OpenGLES_Ch8_2中的AGLKSkyboxEffect类的“ . loadShaders”方法,这是“-loadShaders”方法的一个具有代表性的实现。在程序编译、连接,并验证之后,图8-4列出了使用一个自定义Shading Language程序的步骤。

图 8-4

AGLKSkyboxEffect、AGLKTextureTransformBaseEffect 和AGLKArmatureBaseEffect 以及GLKit的GLKBaseEffect、 GLKSkyboxEffect和GLKReflectionMapEffect类的“ -prepareToDraw”和“ -draw”方法联合执行了图8-4中列出的所有步骤。下面代码显示了在简单的AGLKSkyboxEffect类中的“ -prepareToDraw”和“ -draw”方法的完整实现。 粗体代码突出显示了每个步骤。


- (void)prepareToDraw
{
   if(0 == program)
   {
      [self loadShaders];
   }

   if(0 != program)
   {
     glUseProgram(program);                    // Step 1

      // Translate skybox cube to specified center and scale to 
      // specified size
      GLKMatrix4 skyboxModelView = GLKMatrix4Translate(
         self.transform.modelviewMatrix,
         self.center.x, self.center.y, self.center.z);
      skyboxModelView = GLKMatrix4Scale(
         skyboxModelView,
         self.xSize, self.ySize, self.zSize);

      // Pre-calculate the combined mvpMatrix
      GLKMatrix4 modelViewProjectionMatrix = GLKMatrix4Multiply(
         self.transform.projectionMatrix, 
         skyboxModelView);

      // Set the mvp matrix uniform variable
glUniformMatrix4fv(uniforms[AGLKMVPMatrix], 1, 0, 
         modelViewProjectionMatrix.m);           // Step 2

      // One texture sampler uniform variable
      glUniform1i(uniforms[AGLKSamplersCube], 0);// Step 2

      if(0 == vertexArrayID)
      {  // Set vertex attribute pointers
         glGenVertexArraysOES(1, &vertexArrayID);
         glBindVertexArrayOES(vertexArrayID);      

         glEnableVertexAttribArray(GLKVertexAttribPosition);
         glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
         glVertexAttribPointer(GLKVertexAttribPosition, 
            3, 
            GL_FLOAT, 
            GL_FALSE, 
            0, 
            NULL);                               // Step 3
      }
      else
      {  // The following function call restores all of the
         // vertex attribute pointers previously prepared and 
         // associated with vertexArrayID
         glBindVertexArrayOES(vertexArrayID);      
      }

      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferID);

      // Bind the texture to be used
      if(self.textureCubeMap.enabled)
      {
         glBindTexture(GL_TEXTURE_CUBE_MAP, 
            self.textureCubeMap.name);           // Step 4
      }
      else
      {
         glBindTexture(GL_TEXTURE_CUBE_MAP, 0);  // Step 4
      }
   }
}

- (void)draw
{
   glDrawElements(GL_TRIANGLE_STRIP, 
      AGLKSkyboxNumVertexIndices, 
      GL_UNSIGNED_BYTE, 
      NULL);                                    // Step 5
}

如果本节对于AGLKSkyboxEffect类的实现的深人探讨让你迷惑,请不要担心。GLKit以及类似GLKBaseEffect和GLKSkyboxEffect类的存在就是为了让你不必了解OpenGL ES 2.0 Shading Language的错综复杂,也不必了解使用Shading Language程序的所有相关步骤。重要的是要学习吸收图形的概念,当需要的时候再回来了解实现细节。自定义着色语言程序与在其他编程环境中的自定义汇编语言几乎完全一致。并不是每个人都需要了解如此底层的实现,但是有些控制只有在最底层才可以实行。下一节包含一个用于管理另一个特效(粒子)的自定义Shading Language程序。试 着找出粒子程序与AGLKSkyboxEffect Shading Language程序的相似处。

results matching ""

    No results matching ""