原文链接地址:http://www.raywenderlich.com/3664/opengl-es-2-0-for-iphone-tutorial
免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
教程截图:
OpenGL ES 是可以在iphone上实现2D和3D图形编程的低级API。
如果你之前接触过 cocos2d,sparrow,corona,unity 这些框架,你会发现其实它们都是基于OpenGL上创建的。
多数程序员选择使用这些框架,而不是直接调用OpenGL,因为OpenGL实在是太难用了。
而这篇教程,就是为了让大家更好地入门而写的。
在这个系列的文章中,你可以通过一些实用又容易上手的实验,创建类似hello world的APP。例如显示一些简单的立体图形。
流程大致如下:
说明:
我并非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的三步:
7)创建一个 frame buffer (帧缓冲区)Frame buffer也是OpenGL的对象,它包含了前面提到的render buffer,以及其它后面会讲到的诸如:depth buffer、stencil buffer 和 accumulation buffer。- (void)setupFrameBuffer { GLuint framebuffer; glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderBuffer); } 前两步创建frame buffer的动作跟创建render buffer的动作很类似。(反正也是用一个glBind什么的) 而最后一步 glFramebufferRenderbuffer 这个才有点新意。它让你把前面创建的buffer render依附在frame buffer的GL_COLOR_ATTACHMENT0位置上。 8)清理屏幕 为了尽快在屏幕上显示一些什么,在我们和那些 vertexes、shaders打交道之前,把屏幕清理一下,显示另一个颜色吧。(RGB 0, 104, 55,绿色吧)- (void)render { glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); [_context presentRenderbuffer:GL_RENDERBUFFER]; } 这里每个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 10)把App Delegate和OpenGLView 连接起来// 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]; } 在 HelloOpenGLAppDelegate.h 中修改一下: 接下来修改.m文件:// At top of file #import "OpenGLView.h" // Inside @interface OpenGLView* _glView; // After @interface @property (nonatomic, retain) IBOutlet OpenGLView *glView; 一切顺利的话,你就能看到一个新的view在屏幕上显示。// 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]; 这里是OpenGL的世界。 |
我们一行一行解析:attribute vec4 Position; // 1 attribute vec4 SourceColor; // 2 varying vec4 DestinationColor; // 3 void main(void) { // 4 DestinationColor = SourceColor; // 5 gl_Position = Position; // 6 }
一个简单的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 }
下面解析:- (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 const char * 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; }
下面是解析:- (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); }
2 在 @interface in OpenGLView.h 中添加两个变量:[self compileShaders];
编译!运行!GLuint _positionSlot; GLuint _colorSlot;
为这个简单的长方形创建 Vertex Data! 在这里,我们打算在屏幕上渲染一个正方形,如下图: 在你用OpenGL渲染图形的时候,时刻要记住一点,你只能直接渲染三角形,而不是其它诸如矩形的图形。所以,一个正方形需要分开成两个三角形来渲染。 这段代码的作用是: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 Buffer 对象 传数据到OpenGL的话,最好的方式就是用Vertex Buffer对象。 基本上,它们就是用于缓存顶点数据的OpenGL对象。通过调用一些function来把数据发送到OpenGL-land。(是指OpenGL的画面?) 这里有两种顶点缓存类型 – 一种是用于跟踪每个顶点信息的(正如我们的Vertices array),另一种是用于跟踪组成每个三角形的索引信息(我们的Indices array)。 下面我们在initWithFrame中,加入一些代码: 下面是定义这个setupVBOs:[self setupVBOs]; 如你所见,其实很简单的。这其实是一种之前也用过的模式(pattern)。- (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); } 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]; }
你可能会疑惑,为什么这个长方形刚好占满整个屏幕。在缺省状态下,OpenGL的“camera”位于(0,0,0)位置,朝z轴的正方向。 当然,后面我们会讲到projection(投影)以及如何控制camera。 |
增加一个投影 为了在2D屏幕上显示3D画面,我们需要在图形上做一些投影变换,所谓投影就是下图这个意思: 基本上,为了模仿人类的眼球原理。我们设置一个远平面和一个近平面,在两个平面之前,离近平面近的图像,会因为被缩小了而显得变小;而离远平面近的图像,也会因此而变大。 这里我们增加了一个叫做projection的传入变量。uniform 关键字表示,这会是一个应用于所有顶点的常量,而不是会因为顶点不同而不同的值。// Add right before the main uniform mat4 Projection; // Modify gl_Position line as follows gl_Position = Projection * Position; 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 中加入一个实例变量: 然后到OpenGLView.m文件中加上:GLuint _projectionUniform; // 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}} };
尝试移动和旋转吧 如果总是要修改那个vertex array才能改变图形,这就太烦人了。 就是又加了一个 Uniform的矩阵而已。顺便把它应用到gl_Position当中。// Add right after the Projection uniform uniform mat4 Modelview; // Modify the gl_Position line gl_Position = Projection * Modelview * Position; 然后到 OpenGLView.h中加上一个变量: 到OpenGLView.m中修改:GLuint _modelViewUniform; // 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}} };
什么?一动不动的? 当然了,我们只是调用了一次render方法。 接下来,我们在每一帧都调用一次看看。 渲染 和 CADisplayLink 理想状态下,我们希望OpenGL的渲染频率跟屏幕的刷新频率一致。 幸运的是,Apple为我们提供了一个CADisplayLink的类。这个很好用的,马上就用吧。 在OpenGLView.m文件,修改如下: 这就行了,有CADisplayLink在每一帧都调用你的render方法,我们的图形看起身就好似被sin()周期地变型了。现在这个方块会前前后后地来回移动。// 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];
|