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

来源:互联网 发布:做海报的软件 编辑:程序博客网 时间:2024/05/18 18:42

纯的不能再纯的转载,原译者说别人是牛人,不过在我看来,他也挺牛的了。

刚接触OpenGL ES时,这篇文章不知看了多少遍了,只能照本宣科,不知其然与其所以然。

后决定放弃这种不负责任的工作方式,虽然不是我的方式,但我又必须按要求做事!

按自已的想法,找到罗朝辉博客上最全面的那篇,把例子下载下来,准备花长些时间,耐下心来,一点一点地看。

到目前,从几何顶点,到灯光,到着色器,再到模型加载,最后是纹理,中间回到几何顶点几次,轻轻地,仅是轻轻地扒了一小层皮儿而已,现在再回头看这篇,感觉明白了许多。

不过接着往下需要扒的还有很多,需要研究的也有很多。

整个过程中,每次卡得鼻青脸肿的时侯,每次入夜难眠之时,都下了狠心,要花时间,耐下心来,从理论基础入手,每次坚持之后,除了得到时间过长的反复催促的烦恼外,更大的是收获。

做三维这种东西,必须狠下心,花时间去研究理论基础,要不然,卡到哪里,怎么卡死的都不知道,真是成了笑话了。

另外,3dmax必须随手可用,因为它是你理解三维的有力工具,没有它做验证,很多概念,就能把你呼死。


总的来说,我是从理论出发来指导实践的,所以短短两个半月,至少已经能从大面儿上指导自已以如何的思路往下研究了,就像螺丝钉钉进去一点,入门了,再往下,只要轻轻的保持着拧和旋转,就会有很大的力往下钻。


理论来说,我基本都是以罗朝辉的博客为大框,作为提练纲要,然后找官方资料以及网上资料补充,至于这位子龙山人,他更多的资料是cocos2d的,这个一直没兴趣,不过刚看过了其它几篇,才知道cocos2d也是基于OpenGL的,其中很多篇关于优化和做法指导,对于后续做实际项目时,都很有帮助,如png要比jpg好等等。


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

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

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

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

教程截图:

  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 @interfaceOpenGLView* _glView; // After @interface@property (nonatomic, retain) IBOutlet OpenGLView *glView;
复制代码

  接下来修改.m文件:

复制代码
// At top of file@synthesize glView=_glView; // At top of application:didFinishLaunchingWithOptionsCGRect 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; // 1attribute 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);         // 3constchar* 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 mainuniform mat4 Projection; // Modify gl_Position line as followsgl_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 glViewportCC3GLMatrix *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 planesconst 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 uniformuniform mat4 Modelview; // Modify the gl_Position linegl_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 glViewportCC3GLMatrix *modelView = [CC3GLMatrix matrix];[modelView populateFromTranslation:CC3VectorMake(sin(CACurrentMediaTime()), 0, -7)];glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix); // Revert vertices back to z-value 0const 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[] = {    // Front0, 1, 2,    2, 3, 0,    // Back4, 6, 5,    4, 7, 6,    // Left2, 7, 3,    7, 6, 2,    // Right0, 4, 1,    4, 1, 5,    // Top6, 2, 1,     1, 6, 5,    // Bottom0, 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 setupFrameBufferglFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthRenderBuffer); // In the render method, replace the call to glClear with the followingglClear(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翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 电脑被管理员限制登录微信了怎么办 快手绑定的手机号丢了登不上怎么办 快手绑定的手机号丢了登不了怎么办 苹果id有手机号不知道密码怎么办 微信号不是手机号不知道密码怎么办 手机发短信说程序发生错误怎么办 派派游戏崇物等级低怎么办 轻微事故没有报警后面有问题怎么办 苹果6sp手机接电话声音小怎么办 用手机号办的移动宽带到期了怎么办 联通忘了宽带的用户名和密码怎么办 宽带联通移机附近没有端口怎么办 电信卡怎么改服务密码忘记了怎么办 路由器重置宽带账号密码忘了怎么办 重置路由器后不知道宽带密码怎么办 欠费后重新缴费宽带连接不了怎么办 华硕路由器忘记账号密码忘了怎么办 光纤猫光信号闪红灯不能上网怎么办 被传销洗了脑的人怎么办 辞职了评职称年度考核表怎么办 我是饭店饭店欠供货商的钱多怎么办 mac电脑ps卡住了点不了怎么办 高考自愿民族栏要改为民族怎么办 法院拆消裁定后再审有错怎么办 重定向语句前面有文件路劲怎么办 您上传的图片大小超过3M怎么办 两个人打架被拍视频上传了怎么办 小米手机打开后一直出现英文怎么办 百度云分享文件含有违规内容怎么办 百度网盘下载说本地空间不足怎么办 绘声绘影X9将滤镜拖到视频怎么办 苹果手机中间的按钮没用了怎么办? 图片怎么发的在百度里面应该怎么办 学java刚看的视频就忘了怎么办 qq上传照片一直显示排队中怎么办 微信支付不小心重复付款怎么办 学习通上传视频时 文件过大怎么办 电脑死机了怎么办 也不能关机了 还没发货淘宝退款卖家不处理怎么办 还没发货申请退款卖家不处理怎么办 快递写错电话被更改收货信息怎么办