`

(译)OpenGL ES2.0 – Iphone开发指引

 
阅读更多

 

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

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

  

  ps:非常感谢skingTree为我们提供的这篇翻译教程,感谢yy、小狼、北方加入我的教程翻译团队,谢谢你们!

教程截图:

  OpenGL ES 是可以在iphone上实现2D3D图形编程的低级API

  如果你之前接触过 cocos2dsparrowcoronaunity 这些框架,你会发现其实它们都是基于OpenGL上创建的。

  多数程序员选择使用这些框架,而不是直接调用OpenGL,因为OpenGL实在是太难用了。

  而这篇教程,就是为了让大家更好地入门而写的。 

  在这个系列的文章中,你可以通过一些实用又容易上手的实验,创建类似hello worldAPP。例如显示一些简单的立体图形。

  流程大致如下:

    ·创建一个简单的OpenGL app

    ·编译并运行 vertex & fragment shaders

    ·通过vertex buffer,在屏幕上渲染一个简单矩形

    ·使用投影 model-view 变形。

    ·渲染一个可以 depth testing3D对象。

  说明:

    我并非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确实能做一些很cool1.0不能做的事情,譬如:toon shader(贴材质).

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

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

开始吧

  尽管Xcode自带了OpenGL ES的项目模板,但这个模板自行创建了大量的代码,这样会让初学者感到迷惘。

  因此我们通过自行编写的方式来进行,通过一步一步编写,你能更清楚它的工作机制。

  启动Xcode,新建项目-选择Window-based Application, 让我们从零开始。

  点击下一步,把这个项目命名为HelloOpenGL,点击下一步,选择存放目录,点击“创建”。

  CMD+Rbuild 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

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

复制代码
#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,你要声明你要用哪个versionAPI。这里,我们选择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 bufferstencil buffer  accumulation buffer

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

  而最后一步  glFramebufferRenderbuffer 这个才有点新意。它让你把前面创建的buffer render依附在frame bufferGL_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];
}

  为了尽快在屏幕上显示一些什么,在我们和那些 vertexesshaders打交道之前,把屏幕清理一下,显示另一个颜色吧。(RGB 0, 104, 55绿色吧)

  这里每个RGB色的范围是0~1,所以每个要除一下255.

  下面解析一下每一步动作:

  1.      调用glClearColor ,设置一个RGB颜色和透明度,接下来会用这个颜色涂满全屏。

  2.      调用glClear来进行这个“填色”的动作(大概就是photoshop那个油桶嘛)。还记得前面说过有很多buffer的话,这里我们要用到GL_COLOR_BUFFER_BIT来声明要清理哪一个缓冲区。

  3.      调用OpenGL contextpresentRenderbuffer方法,把缓冲区(render buffercolor 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];
}
复制代码

10App DelegateOpenGLView 连接起来

  在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。片段着色器的责任是计算灯光,以及更重要的是计算出每个像素的最终颜色。

  下面我们通过简单的例子来说明。

  打开你的xcodeFile\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仅仅会把这两个文件copyapplication 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
constchar* 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是由3floatx,y,z)组成,而颜色是4floatr,g,b,a

  ·第三个,声明每一个值是什么类型。(这例子中无论是位置还是颜色,我们都用了GL_FLOAT

  ·第四个,嗯……它总是false就好了。

  ·第五个,指 stride 的大小。这是一个种描述每个 vertex数据大小的方式。所以我们可以简单地传入 sizeofVertex),让编译器计算出来就好。

  ·最好一个,是这个数据结构的偏移量。表示在这个结构中,从哪里开始获取我们的值。Position的值在前面,所以传0进去就可以了。而颜色是紧接着位置的数据,而position的大小是3float的大小,所以是从 3 * sizeof(float) 开始的。

  回来继续说代码,第三点:

 3       调用glDrawElements ,它最后会在每个vertex上调用我们的vertex shader,以及每个像素调用fragment shader,最终画出我们的矩形。

  它也是一个重要的方法,我们来仔细研究一下:

  ·第一个参数,声明用哪种特性来渲染图形。有GL_LINE_STRIP  GL_TRIANGLE_FAN然而GL_TRIANGLE是最常用的,特别是与VBO 关联的时候。

  ·第二个,告诉渲染器有多少个图形要渲染。我们用到C的代码来计算出有多少个。这里是通过个 arraybyte大小除以一个Indice类型的大小得到的。

  ·第三个,指每个indices中的index类型

  ·最后一个,在官方文档中说,它是一个指向index的指针。但在这里,我们用的是VBO,所以通过indexarray就可以访问到了(在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 Hollingscocos3d的作者。他编写了一个完整的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,来把矩阵转换成OpenGLarray格式。

  ·最后,把之前的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()是一个从-11的函数。已PI3.14)为一个周期。这样做的话,约每3.14秒,这个函数会从-11循环一次。

  ·把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.mrender中,在populateFromTranslation的调用后面加上:

 

 

 

_currentRotation += displayLink.duration *90;
[modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)];

  ·添加了一个叫_currentRotationfloat每秒会增加90度。

  ·通过修改那个model view矩阵(这里相当于一个用于变型的矩阵),增加旋转。

  ·旋转在xy轴上作用,没有在z轴的。

  编译运行,你会看到一个很有型的翻转的3D效果。

 

 

 

 

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

  之前的只能算是2.5D,因为它还只是一个会旋转的面而已。现在我们把它改造成3D的。

  把之前的verticesindices数组注释掉吧。

  然后加上新的:

复制代码
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, 然不是contextrenderBufferStorage(这个是在OpenGLview中特别为color render buffer而设的)。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  ·接着,我们调用glFramebufferRenderbuffer,来关联depth bufferrender buffer。还记得,我说过frame buffer中储存着很多种不同的buffer这正是一个新的buffer

  ·在render方法中,我们在每次update时都清除深度buffer,并启用depth  testing

  编译运行,看看这个教程最后的效果。

  一个选择的立方块,用到了OpenGL ES2.0

何去何从?

  这里有本教程的完整源代码

  这只是OpenGL的一篇引导教程,希望能让你轻松地入门。

  对了,我写这篇教程的原因是它在过去的数周中得票最高。谢谢大家的关注,并继续在今后为自己感兴趣的题目投上一票 ---- 我们每周都有一个新教程的。

 

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

分享到:
评论

相关推荐

    OpenGL ES2.0开发库

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

    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 ...

    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 开发库

    openglES2.0 iPhone版

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

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

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

    《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放弃了固定...

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

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

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

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

    opengl es 2.0 API

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

    OpenGL ES 2.0编程指南中文版

    在编程中,OpenGLES2.0目录通常包含了头文件和库文件,为开发者提供了构建OpenGL ES 2.0应用程序所需的环境。在实现OpenGL ES 2.0应用时,需要包含相应的头文件并链接到OpenGL ES 2.0库。 OpenGL ES 2.0编程主要...

    OpenGL ES 2.0游戏与图形编程:适用于iOS和Android 完整版 pdf

    作者: (美) 马鲁基-弗伊诺(Marucchi-Foino, R.) 著 ...原作名: Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 译者: 王净 译. 出版年: 2014-2 页数: 288 装帧: 平装 ISBN: 9787302352303

    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 数字图像...

Global site tag (gtag.js) - Google Analytics