OpenGL ES 2.0 for iPhone Tutorial中文版
2012-04-14 19:57
465 查看
教程截图: 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; } @end3)设置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的三步:调用glGenRenderbuffers来创建一个新的render buffer object。这里返回一个唯一的integer来标记render buffer(这里把这个唯一值赋值到_colorRenderBuffer)。有时候你会发现这个唯一值被用来作为程序内的一个OpenGL 的名称。(反正它唯一嘛)调用glBindRenderbuffer ,告诉这个OpenGL:我在后面引用 GL_RENDERBUFFER的地方,其实是想用_colorRenderBuffer。其实就是告诉OpenGL,我们定义的buffer对象是属于哪一种OpenGL对象最后,为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; // 2varying vec4 DestinationColor; // 3void main(void) { // 4DestinationColor = SourceColor; // 5gl_Position = Position; // 6}我们一行一行解析:“attribute”声明了这个shader会接受一个传入变量,这个变量名为“Position”。在后面的代码中,你会用它来传入顶点的位置数据。这个变量的类型是 “vec4”,表示这是一个由4部分组成的矢量。与上面同理,这里是传入顶点的颜色变量。这个变量没有“attribute”的关键字。表明它是一个传出变量,它就是会传入片段着色器的参数。“varying”关键字表示,依据顶点的颜色,平滑计算出顶点之间每个像素的颜色。文字比较难懂,我们一图胜千言:图中的一个像素,它位于红色和绿色的顶点之间,准确地说,这是一个距离上面顶点55/100,距离下面顶点45/100的点。所以通过过渡,能确定这个像素的颜色。每个shader都从main开始 – 跟C一样嘛。设置目标颜色 = 传入变量:SourceColorgl_Position 是一个内建的传出变量。这是一个在 vertex shader中必须设置的变量。这里我们直接把 gl_Position = Position; 没有做任何逻辑运算。一个简单的vertex shader 就是这样了,接下来我们再创建一个简单的fragment shader。 新建一个空白文件: File\New\New File… 选择 iOS\Other\Empty 命名为:SimpleFragment.glsl 保存。 打开这个文件,加入以下代码: varying lowp vec4 DestinationColor; // 1void main(void) { // 2gl_FragColor = DestinationColor; // 3}下面解析:这是从vertex shader中传入的变量,这里和vertex shader定义的一致。而额外加了一个关键字: lowp。在fragment shader中,必须给出一个计算的精度。出于性能考虑,总使用最低精度是一个好习惯。这里就是设置成最低的精度。如果你需要,也可以设置成medp或者highp.也是从main开始嘛正如你在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 {// 1NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderNameofType:@"glsl"];NSError* error;NSString* shaderString = [NSString stringWithContentsOfFile:shaderPathencoding:NSUTF8StringEncoding error:&error];if (!shaderString) {NSLog(@"Error loading shader: %@", error.localizedDescription);exit(1);}// 2GLuint shaderHandle = glCreateShader(shaderType);// 3const char * shaderStringUTF8 = [shaderString UTF8String];int shaderStringLength = [shaderString length];glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);// 4glCompileShader(shaderHandle);// 5GLint 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 {// 1GLuint vertexShader = [self compileShader:@"SimpleVertex"withType:GL_VERTEX_SHADER];GLuint fragmentShader = [self compileShader:@"SimpleFragment"withType:GL_FRAGMENT_SHADER];// 2GLuint programHandle = glCreateProgram();glAttachShader(programHandle, vertexShader);glAttachShader(programHandle, fragmentShader);glLinkProgram(programHandle);// 3GLint 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);}// 4glUseProgram(programHandle);// 5_positionSlot = glGetAttribLocation(programHandle, "Position");_colorSlot = glGetAttribLocation(programHandle, "SourceColor");glEnableVertexAttribArray(_positionSlot);glEnableVertexAttribArray(_colorSlot);}下面是解析:用来调用你刚刚写的动态编译方法,分别编译了vertex shader 和 fragment shader调用了glCreateProgram glAttachShader glLinkProgram 连接 vertex 和 fragment shader成一个完整的program。调用 glGetProgramiv lglGetProgramInfoLog 来检查是否有error,并输出信息。调用 glUseProgram 让OpenGL真正执行你的program最后,调用 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};这段代码的作用是:一个用于跟踪所有顶点信息的结构Vertex (目前只包含位置和颜色。)定义了以上面这个Vertex结构为类型的array。一个用于表示三角形顶点的数组。 数据准备好了,我们来开始把数据传入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);// 1glViewport(0, 0, self.frame.size.width, self.frame.size.height);// 2glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE,sizeof(Vertex), 0);glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*) (sizeof(float) * 3));// 3glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]),GL_UNSIGNED_BYTE, 0);[_context presentRenderbuffer:GL_RENDERBUFFER];}调用glViewport 设置UIView中用于渲染的部分。这个例子中指定了整个屏幕。但如果你希望用更小的部分,你可以更变这些参数。调用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) 开始的。回来继续说代码,第三点:调用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。
|
相关文章推荐
- OpenGL ES 2.0 for iPhone Tutorial 1
- OpenGL ES 2.0 for iPhone Tutorial Part 2: Textures
- [译] --- OpenGL ES 2.0 for iPhone Tutorial Part 2: Textures
- OpenGL ES 2.0 for iPhone Tutorial
- OpenGL ES 2.0 for iPhone Tutorial
- OpenGL ES 2.0 for iPhone Tutorial
- OpenGL ES 2.0 for iPhone Tutorial Part 2: Textures
- OpenGL ES 2.0 for iPhone Tutorial
- (译)OpenGL ES2.0 – Iphone开发指引
- iPhone Tutorial for Creating a Splash Screen
- OpenGL ES for iPhone: Drawing a Circle 2
- OpenGL ES2.0 – Iphone开发指引
- 求大神解答Opengl ES2.0 for android的文档应该上哪里查
- OpenGL ES for iPhone: Drawing a Circle 3
- How to use iPhone with File I/O Functions: A Tutorial for Software Developers
- OpenGL ES2.0 – Iphone开发指引
- iPhone OpenGL ES – 8 Great Resources For Learning
- OpenGL ES2.0 – Iphone开发指引
- (译)OpenGL ES2.0 – Iphone开发指引
- OpenGL ES2.0 – Iphone开发指引