欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

OpenGL ES2.0 – Iphone开发指引

程序员文章站 2022-04-30 19:58:02
opengl es2.0 – iphone开发指引,   opengl es是可以在iphone上实现2d和3d图形的低级api。   如果你之前接触过cocos2d,sparrow,c...

opengl es2.0 – iphone开发指引,

  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 ES2.0 – Iphone开发指引

opengl es1.0:

  针对固定管线硬件(fixed pipeline),通过它内建的functions来设置诸如灯光、,vertexes(图形的顶点数),颜色、camera等等的东西。

opengl es2.0:

  针对可编程管线硬件(programmable pipeline),基于这个设计可以让内建函数见鬼去吧,但同时,你得自己动手编写任何功能。

  “tmd”,你可能会这么想。这样子我还可能想用2.0么?

  但2.0确实能做一些很cool而1.0不能做的事情,譬如:toon shader(贴材质).

OpenGL ES2.0 – Iphone开发指引

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

OpenGL ES2.0 – Iphone开发指引

  opengl es2.0只能够在iphone 3gs+、ipod touch 3g+和所有版本的ipad上运行。庆幸现在大多数用户都在这个范围。

开始吧

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

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

  启动xcode,新建项目-选择window-based application,让我们从零开始。

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

  cmd+r,build and run。你会看到一个空白的屏幕。

OpenGL ES2.0 – Iphone开发指引

  如你所见的,window-based模板创建了一个没有view、没有view controller或者其它东西的项目。它只包含了一个必须的uiwindow。

  file/new file,新建文件:选择ios\cocoa touch\objective-c class,点击下一步。

  选择subclass uiview,点击下一步,命名为openglview.m.,点击保存。

  接下来,你要在这个openglview.m文件下加入很多代码。

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

OpenGL ES2.0 – Iphone开发指引

  加入:opengles.frameworks和quartzcore.framework

  在项目的groups&files目录下,选择target“helloopengl”,展开link binary with libraries部分。这里是项目用到的框架。

  “+”添加,选择opengles.framework,重复一次把quartzcore.framework也添加进来。

2)修改openglview.h

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

#import 
#import 
#include 
#include 
 
@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的世界。

OpenGL ES2.0 – Iphone开发指引

添加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的点。所以通过过渡,能确定这个像素的颜色。

OpenGL ES2.0 – Iphone开发指引

  4每个shader都从main开始–跟c一样嘛。

  5设置目标颜色=传入变量:sourcecolor

  6gl_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。如果编译失败了,我们必须一些信息来找出问题原因。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调用了glcreateprogramglattachshadergllinkprogram连接vertex和fragment shader成一个完整的program。

  3调用glgetprogramivlglgetprograminfolog来检查是否有error,并输出信息。

  4调用gluseprogram让opengl真正执行你的program

  5最后,调用glgetattriblocation来获取指向vertex shader传入变量的指针。以后就可以通过这写指针来使用了。还有调用glenablevertexattribarray来启用这些数据。(因为默认是disabled的。)

  最后还有两步:

  1在initwithframe方法里,在调用render之前要加入这个:

  2在@interface in openglview.h中添加两个变量:

gluint _positionslot;
gluint _colorslot;

  编译!运行!

  如果你仍能正常地看到之前那个绿色的屏幕,就证明你前面写的代码都很好地工作了。

为这个简单的长方形创建vertex data!

  在这里,我们打算在屏幕上渲染一个正方形,如下图:

OpenGL ES2.0 – Iphone开发指引

  在你用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一个用于表示三角形顶点的数组。