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

本文已有最新Swift版html

https://www.raywenderlich.com/5146-glkit-tutorial-for-ios-getting-started-with-opengl-esios


教程截图:程序员

OpenGL ES 是能够在iphone上实现2D和3D图形编程的低级API。编程

若是你以前接触过 cocos2d,sparrow,corona,unity 这些框架,你会发现其实它们都是基于OpenGL上建立的。小程序

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

而这篇教程,就是为了让你们更好地入门而写的。数组

在这个系列的文章中,你能够经过一些实用又容易上手的实验,建立相似hello world的APP。例如显示一些简单的立体图形。xcode

流程大体以下:缓存

·建立一个简单的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];
}

4) 设置layer为不透明(Opaque)

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

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
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。若是编译失败了,咱们必须一些信息来找出问题缘由。 glGetShaderivglGetShaderInfoLog 会把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。

何去何从?

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

相关文章
相关标签/搜索