`
yangping_Account
  • 浏览: 191730 次
  • 性别: Icon_minigender_1
  • 来自: 无锡
社区版块
存档分类
最新评论

OpenGL ES2.0 – Iphone开发指引

 
阅读更多

原文链接地址:http://www.raywenderlich.com/3664/opengl-es-2-0-for-iphone-tutorial

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

教程截图:

  OpenGL ES 是可以在iphone上实现2D和3D图形编程的低级API。
  如果你之前接触过 cocos2d,sparrow,corona,unity 这些框架,你会发现其实它们都是基于OpenGL上创建的。
  多数程序员选择使用这些框架,而不是直接调用OpenGL,因为OpenGL实在是太难用了。
  而这篇教程,就是为了让大家更好地入门而写的。 
  在这个系列的文章中,你可以通过一些实用又容易上手的实验,创建类似hello world的APP。例如显示一些简单的立体图形。
  流程大致如下:

  • 创建一个简单的OpenGL app
  • 编译并运行 vertex & fragment shaders
  • 通过vertex buffer,在屏幕上渲染一个简单矩形
  • 使用投影 和 model-view 变形。
  • 渲染一个可以 depth testing的3D对象。

  说明:
    我并非OpenGL的专家,这些完全是通过自学得来的。如果大家发现哪些不对的地方,欢迎指出。

OpenGL ES1.0 和 OpenGL ES2.0
  第一件你需要搞清楚的事,是OpenGL ES 1.0 和 2.0的区别。
  他们有多不一样?我只能说他们很不一样。

OpenGL ES1.0:
  针对固定管线硬件(fixed pipeline),通过它内建的functions来设置诸如灯光、,vertexes(图形的顶点数),颜色、camera等等的东西。
OpenGL ES2.0:
  针对可编程管线硬件(programmable pipeline),基于这个设计可以让内建函数见鬼去吧,但同时,你得自己动手编写任何功能。
  “TMD”,你可能会这么想。这样子我还可能想用2.0么?
  但2.0确实能做一些很cool而1.0不能做的事情,譬如:toon shader(贴材质).

  利用opengles2.0,甚至还能创建下面的这种很酷的灯光和阴影效果:

  OpenGL ES2.0只能够在iphone 3GS+、iPod Touch 3G+ 和所有版本的ipad上运行。庆幸现在大多数用户都在这个范围。

开始吧
  尽管Xcode自带了OpenGL ES的项目模板,但这个模板自行创建了大量的代码,这样会让初学者感到迷惘。
  因此我们通过自行编写的方式来进行,通过一步一步编写,你能更清楚它的工作机制。
  启动Xcode,新建项目-选择Window-based Application, 让我们从零开始。
  点击下一步,把这个项目命名为 HelloOpenGL,点击下一步,选择存放目录,点击“创建”。
  CMD+R,build and run。你会看到一个空白的屏幕。

  如你所见的,Window-based 模板创建了一个没有view、没有view controller或者其它东西的项目。它只包含了一个必须的UIWindow。
  File/New File,新建文件:选择iOS\Cocoa Touch\Objective-c Class, 点击下一步。
  选择subclass UIView,点击下一步,命名为 OpenGLView.m., 点击保存。
  接下来,你要在这个OpenGLView.m 文件下加入很多代码。

1)  添加必须的framework (框架)

  加入:OpenGLES.frameworks 和 QuartzCore.framework
  在项目的Groups&Files 目录下,选择target “HelloOpenGL”,展开Link Binary with Libraries部分。这里是项目用到的框架。
  “+”添加,选择OpenGLES.framework, 重复一次把QuartzCore.framework也添加进来。

2)修改OpenGLView.h

  如下: 引入OpenGL的Header,创建一些后面会用到的实例变量。

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
 
@interface OpenGLView : UIView {
    CAEAGLLayer* _eaglLayer;
    EAGLContext* _context;
    GLuint _colorRenderBuffer;
}
 
@end

3)设置layer class 为 CAEAGLLayer

+ (Class)layerClass {
    return [CAEAGLLayer class];
}

  想要显示OpenGL的内容,你需要把它缺省的layer设置为一个特殊的layer。(CAEAGLLayer)。这里通过直接复写layerClass的方法。
4) 设置layer为不透明(Opaque)

- (void)setupLayer {
    _eaglLayer = (CAEAGLLayer*) self.layer;
    _eaglLayer.opaque = YES;
}

  因为缺省的话,CALayer是透明的。而透明的层对性能负荷很大,特别是OpenGL的层。
  (如果可能,尽量都把层设置为不透明。另一个比较明显的例子是自定义tableview cell)
5)创建OpenGL context

- (void)setupContext {   
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    _context = [[EAGLContext alloc] initWithAPI:api];
    if (!_context) {
        NSLog(@"Failed to initialize OpenGLES 2.0 context");
        exit(1);
    }
 
    if (![EAGLContext setCurrentContext:_context]) {
        NSLog(@"Failed to set current OpenGL context");
        exit(1);
    }
}

  无论你要OpenGL帮你实现什么,总需要这个 EAGLContext。
  EAGLContext管理所有通过OpenGL进行draw的信息。这个与Core Graphics context类似。
  当你创建一个context,你要声明你要用哪个version的API。这里,我们选择OpenGL ES 2.0.
  (容错处理,如果创建失败了,我们的程序会退出)
6)创建render buffer (渲染缓冲区)

- (void)setupRenderBuffer {
    glGenRenderbuffers(1, &_colorRenderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);        
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];    
}

  Render buffer 是OpenGL的一个对象,用于存放渲染过的图像。
  有时候你会发现render buffer会作为一个color buffer被引用,因为本质上它就是存放用于显示的颜色。
  创建render buffer的三步:

  1. 调用glGenRenderbuffers来创建一个新的render buffer object。这里返回一个唯一的integer来标记render buffer(这里把这个唯一值赋值到_colorRenderBuffer)。有时候你会发现这个唯一值被用来作为程序内的一个OpenGL 的名称。(反正它唯一嘛)
  2. 调用glBindRenderbuffer ,告诉这个OpenGL:我在后面引用 GL_RENDERBUFFER的地方,其实是想用_colorRenderBuffer。其实就是告诉OpenGL,我们定义的buffer对象是属于哪一种OpenGL对象
  3. 最后,为render buffer分配空间,renderbufferStorage。
    7)创建一个 frame buffer (帧缓冲区)
    - (void)setupFrameBuffer {    
        GLuint framebuffer;
        glGenFramebuffers(1, &framebuffer);
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
            GL_RENDERBUFFER, _colorRenderBuffer);
     }
    Frame buffer也是OpenGL的对象,它包含了前面提到的render buffer,以及其它后面会讲到的诸如:depth buffer、stencil buffer 和 accumulation buffer。

      前两步创建frame buffer的动作跟创建render buffer的动作很类似。(反正也是用一个glBind什么的)

      而最后一步  glFramebufferRenderbuffer 这个才有点新意。它让你把前面创建的buffer render依附在frame buffer的GL_COLOR_ATTACHMENT0位置上。

    8)清理屏幕
    - (void)render {
        glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);
        [_context presentRenderbuffer:GL_RENDERBUFFER];
    }
    为了尽快在屏幕上显示一些什么,在我们和那些 vertexes、shaders打交道之前,把屏幕清理一下,显示另一个颜色吧。(RGB 0, 104, 55,绿色吧)
      这里每个RGB色的范围是0~1,所以每个要除一下255.
      下面解析一下每一步动作:
      1.      调用glClearColor ,设置一个RGB颜色和透明度,接下来会用这个颜色涂满全屏。
      2.      调用glClear来进行这个“填色”的动作(大概就是photoshop那个油桶嘛)。还记得前面说过有很多buffer的话,这里我们要用到 GL_COLOR_BUFFER_BIT来声明要清理哪一个缓冲区。
      3.      调用OpenGL context的presentRenderbuffer方法,把缓冲区(render buffer和color buffer)的颜色呈现到UIView上。

    9)把前面的动作串起来 修改一下OpenGLView.m
    // Replace initWithFrame with this
    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {        
            [self setupLayer];        
            [self setupContext];                
            [self setupRenderBuffer];        
            [self setupFrameBuffer];                
            [self render];        
        }
        return self;
    }
     
    // Replace dealloc method with this
    - (void)dealloc
    {
        [_context release];
        _context = nil;
        [super dealloc];
    }
    10)把App Delegate和OpenGLView 连接起来
      在 HelloOpenGLAppDelegate.h 中修改一下:
    // At top of file
    #import "OpenGLView.h"
     
    // Inside @interface
    OpenGLView* _glView;
     
    // After @interface
    @property (nonatomic, retain) IBOutlet OpenGLView *glView;
    接下来修改.m文件:
    // At top of file
    @synthesize glView=_glView;
     
    // At top of application:didFinishLaunchingWithOptions
    CGRect screenBounds = [[UIScreen mainScreen] bounds];    
    self.glView = [[[OpenGLView alloc] initWithFrame:screenBounds] autorelease];
    [self.window addSubview:_glView];
     
    // In dealloc
    [_glView release];
    一切顺利的话,你就能看到一个新的view在屏幕上显示。
    这里是OpenGL的世界。

    添加shaders: 顶点着色器 和 片段着色器
    在OpenGL ES2.0 的世界,在场景中渲染任何一种几何图形,你都需要创建两个称之为“着色器”的小程序。
    着色器由一个类似C的语言编写- GLSL。知道就好了,我们不深究。
    这个世界有两种着色器(Shader):
    •   Vertex shaders – 在你的场景中,每个顶点都需要调用的程序,称为“顶点着色器”。假如你在渲染一个简单的场景:一个长方形,每个角只有一个顶点。于是vertex shader 会被调用四次。它负责执行:诸如灯光、几何变换等等的计算。得出最终的顶点位置后,为下面的片段着色器提供必须的数据。
    •   Fragment shaders – 在你的场景中,大概每个像素都会调用的程序,称为“片段着色器”。在一个简单的场景,也是刚刚说到的长方形。这个长方形所覆盖到的每一个像素,都会调用一次fragment shader。片段着色器的责任是计算灯光,以及更重要的是计算出每个像素的最终颜色。
    下面我们通过简单的例子来说明。
      打开你的xcode,File\New\New File… 选择 iOS\Other\Empty, 点击下一步。命名为:
      SimpleVertex.glsl 点击保存。
      打开这个文件,加入下面的代码:
    attribute vec4 Position; // 1
    attribute vec4 SourceColor; // 2
     
    varying vec4 DestinationColor; // 3
     
    void main(void) { // 4
        DestinationColor = SourceColor; // 5
        gl_Position = Position; // 6
    }
    我们一行一行解析:
    1. “attribute”声明了这个shader会接受一个传入变量,这个变量名为“Position”。在后面的代码中,你会用它来传入顶点的位置数据。这个变量的类型是 “vec4”,表示这是一个由4部分组成的矢量。
    2. 与上面同理,这里是传入顶点的颜色变量。
    3. 这个变量没有“attribute”的关键字。表明它是一个传出变量,它就是会传入片段着色器的参数。“varying”关键字表示,依据顶点的颜色,平滑计算出顶点之间每个像素的颜色。文字比较难懂,我们一图胜千言:图中的一个像素,它位于红色和绿色的顶点之间,准确地说,这是一个距离上面顶点55/100,距离下面顶点45/100的点。所以通过过渡,能确定这个像素的颜色。

    4. 每个shader都从main开始 – 跟C一样嘛。
    5. 设置目标颜色 = 传入变量:SourceColor
    6. gl_Position 是一个内建的传出变量。这是一个在 vertex shader中必须设置的变量。这里我们直接把 gl_Position = Position; 没有做任何逻辑运算。

    一个简单的vertex shader 就是这样了,接下来我们再创建一个简单的fragment shader。
      新建一个空白文件:
      File\New\New File… 选择 iOS\Other\Empty
      命名为:SimpleFragment.glsl 保存。
      打开这个文件,加入以下代码:

    varying lowp vec4 DestinationColor; // 1
     
    void main(void) { // 2
        gl_FragColor = DestinationColor; // 3
    }
    下面解析:
    1. 这是从vertex shader中传入的变量,这里和vertex shader定义的一致。而额外加了一个关键字: lowp。在fragment shader中,必须给出一个计算的精度。出于性能考虑,总使用最低精度是一个好习惯。这里就是设置成最低的精度。如果你需要,也可以设置成medp或者highp.
    2. 也是从main开始嘛
    3. 正如你在vertex shader中必须设置gl_Position, 在fragment shader中必须设置gl_FragColor.这里也是直接从 vertex shader中取值,先不做任何改变。
      还可以吧?接下来我们开始运用这些shader来创建我们的app。

    编译 Vertex shader 和 Fragment shader
        目前为止,xcode仅仅会把这两个文件copy到application bundle中。我们还需要在运行时编译和运行这些shader。
        你可能会感到诧异。为什么要在app运行时编译代码?
        这样做的好处是,我们的着色器不用依赖于某种图形芯片。(这样才可以跨平台嘛)
        下面开始加入动态编译的代码,打开 OpenGLView.m
        在 initWithFrame: 方法上方加入:
    - (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
     
        // 1
        NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName 
            ofType:@"glsl"];
        NSError* error;
        NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath 
            encoding:NSUTF8StringEncoding error:&error];
        if (!shaderString) {
            NSLog(@"Error loading shader: %@", error.localizedDescription);
            exit(1);
        }
     
        // 2
        GLuint shaderHandle = glCreateShader(shaderType);    
     
        // 3
        const char * shaderStringUTF8 = [shaderString UTF8String];    
        int shaderStringLength = [shaderString length];
        glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
     
        // 4
        glCompileShader(shaderHandle);
     
        // 5
        GLint compileSuccess;
        glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
        if (compileSuccess == GL_FALSE) {
            GLchar messages[256];
            glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"%@", messageString);
            exit(1);
        }
        return shaderHandle;
    }
    下面解析:
      1 这是一个UIKit编程的标准用法,就是在NSBundle中查找某个文件。大家应该熟悉了吧。
      2 调用 glCreateShader来创建一个代表shader 的OpenGL对象。这时你必须告诉OpenGL,你想创建 fragment shader还是vertex shader。所以便有了这个参数:shaderType
      3 调用glShaderSource ,让OpenGL获取到这个shader的源代码。(就是我们写的那个)这里我们还把NSString转换成C-string
      4 最后,调用glCompileShader 在运行时编译shader
      5 大家都是程序员,有程序的地方就会有fail。有程序员的地方必然会有debug。如果编译失败了,我们必须一些信息来找出问题原因。 glGetShaderiv 和 glGetShaderInfoLog  会把error信息输出到屏幕。(然后退出)
      我们还需要一些步骤来编译vertex shader 和frament shader。
    - 把它们俩关联起来
    - 告诉OpenGL来调用这个程序,还需要一些指针什么的。
      在compileShader: 方法下方,加入这些代码
    - (void)compileShaders {
     
        // 1
        GLuint vertexShader = [self compileShader:@"SimpleVertex" 
            withType:GL_VERTEX_SHADER];
        GLuint fragmentShader = [self compileShader:@"SimpleFragment" 
            withType:GL_FRAGMENT_SHADER];
     
        // 2
        GLuint programHandle = glCreateProgram();
        glAttachShader(programHandle, vertexShader);
        glAttachShader(programHandle, fragmentShader);
        glLinkProgram(programHandle);
     
        // 3
        GLint linkSuccess;
        glGetProgramiv(programHandle, GL_LINK_STATUS, &linkSuccess);
        if (linkSuccess == GL_FALSE) {
            GLchar messages[256];
            glGetProgramInfoLog(programHandle, sizeof(messages), 0, &messages[0]);
            NSString *messageString = [NSString stringWithUTF8String:messages];
            NSLog(@"%@", messageString);
            exit(1);
        }
     
        // 4
        glUseProgram(programHandle);
     
        // 5
        _positionSlot = glGetAttribLocation(programHandle, "Position");
        _colorSlot = glGetAttribLocation(programHandle, "SourceColor");
        glEnableVertexAttribArray(_positionSlot);
        glEnableVertexAttribArray(_colorSlot);
    }
    下面是解析:
    1. 用来调用你刚刚写的动态编译方法,分别编译了vertex shader 和 fragment shader
    2. 调用了glCreateProgram glAttachShader  glLinkProgram 连接 vertex 和 fragment shader成一个完整的program。
    3. 调用 glGetProgramiv  lglGetProgramInfoLog 来检查是否有error,并输出信息。
    4. 调用 glUseProgram  让OpenGL真正执行你的program
    5.  最后,调用 glGetAttribLocation 来获取指向 vertex shader传入变量的指针。以后就可以通过这写指针来使用了。还有调用 glEnableVertexAttribArray来启用这些数据。(因为默认是 disabled的。)
      最后还有两步:
      1 在 initWithFrame方法里,在调用render之前要加入这个:
    [self compileShaders];
           2 在 @interface in OpenGLView.h 中添加两个变量:
    GLuint _positionSlot;
    GLuint _colorSlot;
    编译!运行!
    如果你仍能正常地看到之前那个绿色的屏幕,就证明你前面写的代码都很好地工作了。
    为这个简单的长方形创建 Vertex Data!
    在这里,我们打算在屏幕上渲染一个正方形,如下图:

      在你用OpenGL渲染图形的时候,时刻要记住一点,你只能直接渲染三角形,而不是其它诸如矩形的图形。所以,一个正方形需要分开成两个三角形来渲染。
      图中分别是顶点(0,1,2)和顶点(0,2,3)构成的三角形。
      OpenGL ES2.0的一个好处是,你可以按你的风格来管理顶点。
      打开OpenGLView.m文件,创建一个纯粹的C结构以及一些array来跟踪我们的矩形信息,如下:

    typedef struct {
        float Position[3];
        float Color[4];
    } Vertex;
     
    const Vertex Vertices[] = {
        {{1, -1, 0}, {1, 0, 0, 1}},
        {{1, 1, 0}, {0, 1, 0, 1}},
        {{-1, 1, 0}, {0, 0, 1, 1}},
        {{-1, -1, 0}, {0, 0, 0, 1}}
    };
     
    const GLubyte Indices[] = {
         0, 1, 2,
         2, 3, 0
    };
    这段代码的作用是:
    1. 一个用于跟踪所有顶点信息的结构Vertex (目前只包含位置和颜色。)
    2. 定义了以上面这个Vertex结构为类型的array。
    3. 一个用于表示三角形顶点的数组。
      数据准备好了,我们来开始把数据传入OpenGL

    创建Vertex Buffer 对象
      传数据到OpenGL的话,最好的方式就是用Vertex Buffer对象。
      基本上,它们就是用于缓存顶点数据的OpenGL对象。通过调用一些function来把数据发送到OpenGL-land。(是指OpenGL的画面?)
    这里有两种顶点缓存类型 – 一种是用于跟踪每个顶点信息的(正如我们的Vertices array),另一种是用于跟踪组成每个三角形的索引信息(我们的Indices array)。
      下面我们在initWithFrame中,加入一些代码:
    [self setupVBOs];
    下面是定义这个setupVBOs:
    - (void)setupVBOs {
     
        GLuint vertexBuffer;
        glGenBuffers(1, &vertexBuffer);
        glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
     
        GLuint indexBuffer;
        glGenBuffers(1, &indexBuffer);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
     
    }
    如你所见,其实很简单的。这其实是一种之前也用过的模式(pattern)。
      glGenBuffers - 创建一个Vertex Buffer 对象
           glBindBuffer – 告诉OpenGL我们的vertexBuffer 是指GL_ARRAY_BUFFER
      glBufferData – 把数据传到OpenGL-land
      想起哪里用过这个模式吗?要不再回去看看frame buffer那一段? 
      万事俱备,我们可以通过新的shader,用新的渲染方法来把顶点数据画到屏幕上。
      用这段代码替换掉之前的render:
    - (void)render {
        glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);
     
        // 1
        glViewport(0, 0, self.frame.size.width, self.frame.size.height);
     
        // 2
        glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 
            sizeof(Vertex), 0);
        glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 
            sizeof(Vertex), (GLvoid*) (sizeof(float) * 3));
     
        // 3
        glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), 
            GL_UNSIGNED_BYTE, 0);
     
        [_context presentRenderbuffer:GL_RENDERBUFFER];
    }
    1. 调用glViewport 设置UIView中用于渲染的部分。这个例子中指定了整个屏幕。但如果你希望用更小的部分,你可以更变这些参数。
    2. 调用glVertexAttribPointer来为vertex shader的两个输入参数配置两个合适的值。
      第二段这里,是一个很重要的方法,让我们来认真地看看它是如何工作的:
      • 第一个参数,声明这个属性的名称,之前我们称之为glGetAttribLocation
      • 第二个参数,定义这个属性由多少个值组成。譬如说position是由3个float(x,y,z)组成,而颜色是4个float(r,g,b,a)
      • 第三个,声明每一个值是什么类型。(这例子中无论是位置还是颜色,我们都用了GL_FLOAT)
      • 第四个,嗯……它总是false就好了。
      • 第五个,指 stride 的大小。这是一个种描述每个 vertex数据大小的方式。所以我们可以简单地传入 sizeof(Vertex),让编译器计算出来就好。
      • 最好一个,是这个数据结构的偏移量。表示在这个结构中,从哪里开始获取我们的值。Position的值在前面,所以传0进去就可以了。而颜色是紧接着位置的数据,而position的大小是3个float的大小,所以是从 3 * sizeof(float) 开始的。
    3. 回来继续说代码,第三点:
      调用glDrawElements ,它最后会在每个vertex上调用我们的vertex shader,以及每个像素调用fragment shader,最终画出我们的矩形。
        它也是一个重要的方法,我们来仔细研究一下:
      • 第一个参数,声明用哪种特性来渲染图形。有GL_LINE_STRIP 和 GL_TRIANGLE_FAN。 然而GL_TRIANGLE是最常用的,特别是与VBO 关联的时候。
      • 第二个,告诉渲染器有多少个图形要渲染。我们用到C的代码来计算出有多少个。这里是通过个 array的byte大小除以一个Indice类型的大小得到的。
      • 第三个,指每个indices中的index类型
      • 最后一个,在官方文档中说,它是一个指向index的指针。但在这里,我们用的是VBO,所以通过index的array就可以访问到了(在GL_ELEMENT_ARRAY_BUFFER传过了),所以这里不需要。
      编译运行的话,你就可以看到这个画面喇。

    你可能会疑惑,为什么这个长方形刚好占满整个屏幕。在缺省状态下,OpenGL的“camera”位于(0,0,0)位置,朝z轴的正方向。
    当然,后面我们会讲到projection(投影)以及如何控制camera。
    增加一个投影

     为了在2D屏幕上显示3D画面,我们需要在图形上做一些投影变换,所谓投影就是下图这个意思:

      基本上,为了模仿人类的眼球原理。我们设置一个远平面和一个近平面,在两个平面之前,离近平面近的图像,会因为被缩小了而显得变小;而离远平面近的图像,也会因此而变大。
      打开SimpleVertex.glsl,做一下修改:

    // Add right before the main
    uniform mat4 Projection;
    
    // Modify gl_Position line as follows
    gl_Position = Projection * Position;
    这里我们增加了一个叫做projection的传入变量。uniform 关键字表示,这会是一个应用于所有顶点的常量,而不是会因为顶点不同而不同的值。
      mat4 是 4X4矩阵的意思。然而,Matrix math是一个很大的课题,我们不可能在这里解析。所以在这里,你只要认为它是用于放大缩小、旋转、变形就好了。
      Position位置乘以Projection矩阵,我们就得到最终的位置数值。
      无错,这就是一种被称之“线性代数”的东西。我在大学时期后,早就忘大部分了。
      其实数学也只是一种工具,而这种工具已经由前面的才子解决了,我们知道怎么用就好。
      Bill Hollings,cocos3d的作者。他编写了一个完整的3D特性框架,并整合到cocos2d中。(作者:可能有一天我也会弄一个3D的教程)无论任何,Cocos3d包含了Objective-C的向量和矩阵库,所以我们可以很好地应用到这个项目中。
      这里,http://d1xzuxjlafny7l.cloudfront.net/downloads/Cocos3DMathLib.zip
      有一个zip文件,(作者:我移除了一些不必要的依赖)下载并copy到你的项目中。记得选上:“Copy items into destination group’s folder (if needed)” 点击Finish。
      在OpenGLView.h 中加入一个实例变量:
    GLuint _projectionUniform;
    然后到OpenGLView.m文件中加上:
    // Add to top of file
    #import "CC3GLMatrix.h"
     
    // Add to bottom of compileShaders
    _projectionUniform = glGetUniformLocation(programHandle, "Projection");
     
    // Add to render, right before the call to glViewport
    CC3GLMatrix *projection = [CC3GLMatrix matrix];
    float h = 4.0f * self.frame.size.height / self.frame.size.width;
    [projection populateFromFrustumLeft:-2
                                                    andRight:2
                                                 andBottom:-h/2
                                                       andTop:h/2
                                                     andNear:4
                                                        andFar:10];
    glUniformMatrix4fv(_projectionUniform, 1, 0, projection.glMatrix);
     
    // Modify vertices so they are within projection near/far planes
    const Vertex Vertices[] = {
        {{1, -1, -7}, {1, 0, 0, 1}},
        {{1, 1, -7}, {0, 1, 0, 1}},
        {{-1, 1, -7}, {0, 0, 1, 1}},
        {{-1, -1, -7}, {0, 0, 0, 1}}
    };
    • 通过调用  glGetUniformLocation 来获取在vertex shader中的Projection输入变量
    • 然后,使用math library来创建投影矩阵。通过这个让你指定坐标,以及远近屏位置的方式,来创建矩阵,会让事情比较简单。
    • 你用来把数据传入到vertex shader的方式,叫做 glUniformMatrix4fv. 这个 CC3GLMatrix类有一个很方便的方法 glMatrix,来把矩阵转换成OpenGL的array格式。
    • 最后,把之前的vertices数据修改一下,让z坐标为-7。
    编译后运行,你应该可以看到一个稍稍有点距离的正方形了。

    尝试移动和旋转吧

      如果总是要修改那个vertex array才能改变图形,这就太烦人了。
      而这正是变换矩阵该做的事(又来了,线性代数)
      在前面,我们修改了应用到投影矩阵的vertex array来达到移动图形的目的。何不试一下,做一个 变形、放大缩小、旋转的矩阵来应用?我们称之为 “model-view”变换。
      再回到 SimpleVertex.glsl

    // Add right after the Projection uniform
    uniform mat4 Modelview;
     
    // Modify the gl_Position line
    gl_Position = Projection * Modelview * Position;
    就是又加了一个 Uniform的矩阵而已。顺便把它应用到gl_Position当中。
      然后到 OpenGLView.h中加上一个变量:
    GLuint _modelViewUniform;
    到OpenGLView.m中修改:
    // Add to end of compileShaders
    _modelViewUniform = glGetUniformLocation(programHandle, "Modelview");
     
    // Add to render, right before call to glViewport
    CC3GLMatrix *modelView = [CC3GLMatrix matrix];
    [modelView populateFromTranslation:CC3VectorMake(sin(CACurrentMediaTime()), 0, -7)];
    glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix);
     
    // Revert vertices back to z-value 0
    const Vertex Vertices[] = {
        {{1, -1, 0}, {1, 0, 0, 1}},
        {{1, 1, 0}, {0, 1, 0, 1}},
        {{-1, 1, 0}, {0, 0, 1, 1}},
        {{-1, -1, 0}, {0, 0, 0, 1}}
    };
    • 获取那个model view uniform的传入变量
    • 使用cocos3d math库来创建一个新的矩阵,在变换中装入矩阵。
    • 变换是在z轴上移动-7,而为什么sin(当前时间) 呢?哈哈,如果你还记得高中时候的三角函数。sin()是一个从-1到1的函数。已PI(3.14)为一个周期。这样做的话,约每3.14秒,这个函数会从-1到1循环一次。
    • 把vertex 结构改回去,把z坐标设回0.
    编译运行,就算我们把z设回0,也可以看到这个位于中间的正方形了。

    什么?一动不动的?
      当然了,我们只是调用了一次render方法。
      接下来,我们在每一帧都调用一次看看。

    渲染 和 CADisplayLink
         理想状态下,我们希望OpenGL的渲染频率跟屏幕的刷新频率一致。
      幸运的是,Apple为我们提供了一个CADisplayLink的类。这个很好用的,马上就用吧。
      在OpenGLView.m文件,修改如下:
    // Add new method before init
    - (void)setupDisplayLink {
        CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self
                                                                                                             selector:@selector(render:)];
        [displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                                                            forMode:NSDefaultRunLoopMode];    
    }
     
    // Modify render method to take a parameter
    - (void)render:(CADisplayLink*)displayLink {
     
    // Remove call to render in initWithFrame and replace it with the following
    [self setupDisplayLink];
    这就行了,有CADisplayLink在每一帧都调用你的render方法,我们的图形看起身就好似被sin()周期地变型了。现在这个方块会前前后后地来回移动。


    不费功夫地旋转

     
     让图形旋转起来,才算得上有型。
      再到OpenGLView.h 中,添加成员变量。
    float _currentRotation;
    在OpenGLView.m的render中,在 populateFromTranslation的调用后面加上:
    _currentRotation += displayLink.duration * 90;
    [modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)];
    • 添加了一个叫_currentRotation的float,每秒会增加90度。
    • 通过修改那个model view矩阵(这里相当于一个用于变型的矩阵),增加旋转。
    • 旋转在x、y轴上作用,没有在z轴的。
    编译运行,你会看到一个很有型的翻转的3D效果。

     

    不费功夫地变成3D方块?

      之前的只能算是2.5D,因为它还只是一个会旋转的面而已。现在我们把它改造成3D的。
      把之前的vertices、indices数组注释掉吧。
      然后加上新的:

    const Vertex Vertices[] = {
        {{1, -1, 0}, {1, 0, 0, 1}},
        {{1, 1, 0}, {1, 0, 0, 1}},
        {{-1, 1, 0}, {0, 1, 0, 1}},
        {{-1, -1, 0}, {0, 1, 0, 1}},
        {{1, -1, -1}, {1, 0, 0, 1}},
        {{1, 1, -1}, {1, 0, 0, 1}},
        {{-1, 1, -1}, {0, 1, 0, 1}},
        {{-1, -1, -1}, {0, 1, 0, 1}}
    };
     
    const GLubyte Indices[] = {
        // Front
        0, 1, 2,
        2, 3, 0,
        // Back
        4, 6, 5,
        4, 7, 6,
        // Left
        2, 7, 3,
        7, 6, 2,
        // Right
        0, 4, 1,
        4, 1, 5,
        // Top
        6, 2, 1, 
        1, 6, 5,
        // Bottom
        0, 3, 7,
        0, 7, 4    
    };
    编译运行,你会看到一个方块了。

    但这个方块有时候让人觉得假,因为你可以看到方块里面。
      这里还有一个叫做 depth testing(深度测试)的功能,启动它,OpenGL就可以跟踪在z轴上的像素。这样它只会在那个像素前方没有东西时,才会绘画这个像素。
      到OpenGLView.h中,添加成员变量。
    GLuint _depthRenderBuffer;
          在OpenGLView.m:
    // Add new method right after setupRenderBuffer
    - (void)setupDepthBuffer {
        glGenRenderbuffers(1, &_depthRenderBuffer);
        glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 
                               self.frame.size.width, self.frame.size.height);    
    }
     
    // Add to end of setupFrameBuffer
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
                                             _depthRenderBuffer);
     
    // In the render method, replace the call to glClear with the following
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_DEPTH_TEST);
     
    // Add to initWithFrame, right before call to setupRenderBuffer
    [self setupDepthBuffer];
    • setupDepthBuffer方法创建了一个depth buffer。这个与前面的render/color buffer类似,不再重复了。值得注意的是,这里使用了glRenderbufferStorage, 然不是context的renderBufferStorage(这个是在OpenGL的view中特别为color render buffer而设的)。
    • 接着,我们调用glFramebufferRenderbuffer,来关联depth buffer和render buffer。还记得,我说过frame buffer中储存着很多种不同的buffer? 这正是一个新的buffer。
    • 在render方法中,我们在每次update时都清除深度buffer,并启用depth  testing。
    编译运行,看看这个教程最后的效果。
      一个选择的立方块,用到了OpenGL ES2.0。

    何去何从?

      这里有本教程的完整源代码
      这只是OpenGL的一篇引导教程,希望能让你轻松地入门。

    著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!
分享到:
评论

相关推荐

    OpenGL ES 2.0规范(中文版)——本人自己翻译总结的

    - **帧缓存的区别**:OpenGLES命令对window-system-provided framebuffers的影响最终由分配帧缓存资源的窗口系统控制。窗口系统决定了OpenGL ES在任何特定上下文中的行为。 #### 六、OpenGL ES 2.0与OpenGL 2.0的...

    《OpenGL ES 2.0 编程指南 中文版》.pdf

    OpenGL ES 2.0 编程指南中文版详细介绍了OpenGL ES 2.0的编程技术和方法,这是专为移动和嵌入式系统设计的图形API的2.0版本,广泛应用于各种便携式设备和游戏机上。 ### OpenGL ES基础 OpenGL ES(Open Graphics ...

    OpenGL ES2.0开发库

    OpenGL ES2.0开发库和OpenGL ES3.0开发库有差别,保留对OpenGL ES2.0开发库的使用 用RAR5.4解压

    Android OpenGL ES 2.0学习书籍.rar

    最后,了解如何与硬件加速的OpenGLES API交互,以及熟悉GLSL语言(OpenGL Shading Language),是成为OpenGL ES 2.0专家的必经之路。通过编写高效、可读性强的着色器代码,可以创造出各种复杂的视觉效果,实现令人...

    《OpenGL ES 2.0 编程指南 中文版》PDF

    《OpenGL ES 2.0 编程指南 中文版》

    OPENGLES 2.0 开发库

    OPENGLES 2.0 开发库 OPENGLES 2.0 开发库 OPENGLES 2.0 开发库

    OpenGLES 2.0 for android 开发教程 从入门到精通和Demo

    在Android系统中,OpenGLES 2.0是用于图形渲染的重要框架,支持3D图形和复杂的视觉效果。本教程旨在帮助开发者从零基础开始掌握OpenGLES 2.0,并通过实例Demo加深理解。 入门篇: 1. **环境配置**:首先,你需要...

    openglES2.0 iPhone版

    openglES2.0 iPhone版 的教程例子源码,例子源码有注释,是学习iPhone 三维游戏的好教程。

    《OpenGL ES 2.0 编程指南 中文版》

    《OpenGL ES 《OpenGL ES 2.0 编程指南 中文版》2.0 编程指南 中文版》

    OpenGL ES 2.0 Programming Guide中文版.pdf

    OpenGL ES 2.0 Programming Guide中文版!

    opengl es 2.0 spec

    OpenGL ES 2.0是该系列中的一个重要版本,它为开发高质量3D图形应用提供了强大支持。本规格书详细描述了OpenGL ES 2.0的各项特性及其使用方法。 #### 二、OpenGL ES 2.0 特性 ##### 2.1 硬件加速 OpenGL ES 2.0...

    openGLES2.0 游戏开发源码

    在Android系统中,OpenGLES2.0是广泛用于2D和3D图形渲染的标准,它提供了丰富的图形功能,为游戏开发提供了强大的支持。下面我们将深入探讨OpenGLES2.0在Android游戏开发中的关键知识点。 1. **顶点着色器(Vertex ...

    OpenGL ES 2.0

    OpenGL ES 2.0是一种广泛应用于移动设备和嵌入式系统的图形API,是OpenGL(Open Graphics Library)的一个子集,专门用于3D图形和游戏开发。OpenGL ES 2.0支持的特性包括: 1. 着色器程序:OpenGL ES 2.0放弃了固定...

    AMD的OpenGL ES2.0的模拟器 和《OpenGL ES2.0 Programming guide》里的例子代码

    1. AMD的OpenGL ES2.0的模拟器 2. 《OpenGL ES2.0 Programming guide》里的例子代码

    Android 3D游戏开发技术宝典 OpenGL ES2.0.pdf

    第3章~第10章介绍了基于OpenGL ES 2.0进行3D应用开发的一些必知必会的基本知识;第11章~第15章介绍了一些高级特效的实现方法;第16章~第17章介绍了3D游戏开发中相关的一些物理、碰撞检测知识以及常用的3D物理引擎...

    opengl es 2.0 API

    opengl es 2.0 API 网上没提供下载...俺提供! opengl es 2.0全面抛弃了 固定渲染管线.只能说shader的时代越来越近了..先下手为强 哈哈.反正nehe已经老黄历了.shader当道.感觉图形学门槛高了一大截.无论怎么说对很多...

    Android 3D游戏开发技术宝典-OpenGL ES 2.0 (吴亚峰) 源代码

    13.6 opengl es 1.x与opengl es 2.0实现方案的对比 363 13.7 本章小结 364 第14章 片元着色器的妙用 365 14.1 程序纹理技术 365 14.1.1 砖块着色器 365 14.1.2 沙滩球着色器 367 14.2 数字图像...

    OpenGL ES 2.0 编程指南 中文版

    在本书中,作者首先介绍了OpenGL ES 2.0的基本概念,包括它的核心内容、定点着色器和片段着色器,以及它与OpenGLES 1.0的向后兼容性。接着,书中详细讲解了EGL的使用,它是OpenGL ES与本地窗口系统交互的桥梁,涉及...

    OpenGLES2.0 Programming Guide

    OpenGLES2.0 Programming Guide

Global site tag (gtag.js) - Google Analytics