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,创建一些后面会用到的实例变量。
3)设置layer class 为 CAEAGLLayer#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
想要显示OpenGL的内容,你需要把它缺省的layer设置为一个特殊的layer。(CAEAGLLayer)。这里通过直接复写layerClass的方法。+ (Class)layerClass { return [CAEAGLLayer class]; }
4) 设置layer为不透明(Opaque)
因为缺省的话,CALayer是透明的。而透明的层对性能负荷很大,特别是OpenGL的层。- (void)setupLayer { _eaglLayer = (CAEAGLLayer*) self.layer; _eaglLayer.opaque = YES; }
(如果可能,尽量都把层设置为不透明。另一个比较明显的例子是自定义tableview cell)
5)创建OpenGL context
无论你要OpenGL帮你实现什么,总需要这个 EAGLContext。- (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); } }
EAGLContext管理所有通过OpenGL进行draw的信息。这个与Core Graphics context类似。
当你创建一个context,你要声明你要用哪个version的API。这里,我们选择OpenGL ES 2.0.
(容错处理,如果创建失败了,我们的程序会退出)
6)创建render buffer (渲染缓冲区)
Render buffer 是OpenGL的一个对象,用于存放渲染过的图像。- (void)setupRenderBuffer { glGenRenderbuffers(1, &_colorRenderBuffer); glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer); [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer]; }
有时候你会发现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。
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的世界。
添加shaders: 顶点着色器 和 片段着色器 在OpenGL ES2.0 的世界,在场景中渲染任何一种几何图形,你都需要创建两个称之为“着色器”的小程序。 着色器由一个类似C的语言编写- GLSL。知道就好了,我们不深究。 这个世界有两种着色器(Shader):
打开你的xcode,File\New\New File… 选择 iOS\Other\Empty, 点击下一步。命名为: SimpleVertex.glsl 点击保存。 打开这个文件,加入下面的代码: 我们一行一行解析:
|
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 }
- 这是从vertex shader中传入的变量,这里和vertex shader定义的一致。而额外加了一个关键字: lowp。在fragment shader中,必须给出一个计算的精度。出于性能考虑,总使用最低精度是一个好习惯。这里就是设置成最低的精度。如果你需要,也可以设置成medp或者highp.
- 也是从main开始嘛
- 正如你在vertex shader中必须设置gl_Position, 在fragment shader中必须设置gl_FragColor.这里也是直接从 vertex shader中取值,先不做任何改变。
编译 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 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; }
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); }
- 用来调用你刚刚写的动态编译方法,分别编译了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之前要加入这个:
2 在 @interface in OpenGLView.h 中添加两个变量:[self compileShaders];
编译!运行!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。
- 一个用于表示三角形顶点的数组。
创建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]; }
- 调用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。
增加一个投影
为了在2D屏幕上显示3D画面,我们需要在图形上做一些投影变换,所谓投影就是下图这个意思:
基本上,为了模仿人类的眼球原理。我们设置一个远平面和一个近平面,在两个平面之前,离近平面近的图像,会因为被缩小了而显得变小;而离远平面近的图像,也会因此而变大。
打开SimpleVertex.glsl,做一下修改:
这里我们增加了一个叫做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}} };
- 通过调用 glGetUniformLocation 来获取在vertex shader中的Projection输入变量
- 然后,使用math library来创建投影矩阵。通过这个让你指定坐标,以及远近屏位置的方式,来创建矩阵,会让事情比较简单。
- 你用来把数据传入到vertex shader的方式,叫做 glUniformMatrix4fv. 这个 CC3GLMatrix类有一个很方便的方法 glMatrix,来把矩阵转换成OpenGL的array格式。
- 最后,把之前的vertices数据修改一下,让z坐标为-7。
尝试移动和旋转吧
如果总是要修改那个vertex array才能改变图形,这就太烦人了。
而这正是变换矩阵该做的事(又来了,线性代数)
在前面,我们修改了应用到投影矩阵的vertex array来达到移动图形的目的。何不试一下,做一个 变形、放大缩小、旋转的矩阵来应用?我们称之为 “model-view”变换。
再回到 SimpleVertex.glsl
就是又加了一个 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}} };
- 获取那个model view uniform的传入变量
- 使用cocos3d math库来创建一个新的矩阵,在变换中装入矩阵。
- 变换是在z轴上移动-7,而为什么sin(当前时间) 呢?哈哈,如果你还记得高中时候的三角函数。sin()是一个从-1到1的函数。已PI(3.14)为一个周期。这样做的话,约每3.14秒,这个函数会从-1到1循环一次。
- 把vertex 结构改回去,把z坐标设回0.
什么?一动不动的?
当然了,我们只是调用了一次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];
不费功夫地旋转
让图形旋转起来,才算得上有型。
再到OpenGLView.h 中,添加成员变量。
在OpenGLView.m的render中,在 populateFromTranslation的调用后面加上:float _currentRotation;
_currentRotation += displayLink.duration * 90; [modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)];
- 添加了一个叫_currentRotation的float,每秒会增加90度。
- 通过修改那个model view矩阵(这里相当于一个用于变型的矩阵),增加旋转。
- 旋转在x、y轴上作用,没有在z轴的。
不费功夫地变成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中,添加成员变量。
在OpenGLView.m:GLuint _depthRenderBuffer;
// 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。
何去何从?
这里有本教程的完整源代码。
这只是OpenGL的一篇引导教程,希望能让你轻松地入门。
在这个系列教程中,我们的目标是帮助大家揭开OpenGL ES 2.0的神秘面纱,同时给大家提供一个手把手的例子,能带领大家步入OpenGL ES 2.0的开发世界。
在第一部分教程中,我们介绍了基本的初始化OpenGL,创建简单的顶点和片断shaders,并且在屏幕上面画了一个简单的旋转立方体。
在这部分教程中,我们将步入更高的级别,给立方体进行纹理贴图!
声明:我并不是一个Open GL的专家,所有这些知识都是我自学来的,这篇教程是我在学习的过程中写的。如果我不小心犯了一些很煞笔的错误,欢迎大家可以给我指正!(译者:当然,其实我也是刚学了一点OpenGL的知识,如果翻译有问题,也肯请大牛帮助指正)
好了,让我们一起进入纹理贴图的世界吧!
如果你还没有上一篇教程的工程的话,你可以从这里先下载样例工程。
下载完后,编译并运行工程,你将会看到一个旋转的立方体:
现在,我们的立方体看起来是红绿相间的,因为我们指定顶点的颜色就是这么做的---还没有使用任何纹理贴图。
但是,不用担心---这正是本教程接下来要做的事!
首先,下载本教程所需要使用的纹理图片,下载完后解压之。然后把它们拖到Resource分组中,同时确保 “Copy items into destination group’s folder” 被选中,然后点击Finish。
你会看到,新添加了两张图片---一张看起来像地板砖,另一张看起来像条鱼。我们将把立方体的每一个面都贴上这个地板砖。
读取像素数据
我们的第一步就是把图片数据读取到OpenGL中来。
这里有个问题,就是OpenGL不能直接使用png图片数据,而是,你首先获得png图片的像素数据buffer,并且你需要为这些数据指定格式。
幸运的是,你可以轻松地使用Quartz2D的函数来得到图片的像素数据buffer。如果你看过Core Graphics 101系列教程的话,你肯定对下面的调用很熟悉了。
要完全读取像素buffer的工作,你需要以下4步:
- 获得Core Graphics 图片引用. 因为我们将使用Core Graphics来读取原始的像素数据,所以我们首先要取得图片的引用(reference)。这其实很简单,UIImage有一个CGImageRef属性。
- 创建Core Graphics位图context. 接下来就是创建Core Graphics位图context,context是内存中的缓冲区,是一种非常方便的方式来保存原始像素。
- 在context中绘制Image. 我们可以使用Core Graphics的一个简单的函数调用来搞定这个事----然后context里面会包含原始图片数据。
- 把原始像素数据发送给OpenGL. 为了实现这个,我们首先要创建一个OpenGL 纹理对象,然后获得它的唯一的ID(叫做“name”),然后使用一个函数调用把像素数据发送给OpenGL。
这里有许多代码,让我们一段一段来看:- (GLuint)setupTexture:(NSString *)fileName { // 1 CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage; if (!spriteImage) { NSLog(@"Failed to load image %@", fileName); exit(1); } // 2 size_t width = CGImageGetWidth(spriteImage); size_t height = CGImageGetHeight(spriteImage); GLubyte * spriteData = (GLubyte *) calloc(width*height*4, sizeof(GLubyte)); CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast); // 3 CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage); CGContextRelease(spriteContext); // 4 GLuint texName; glGenTextures(1, &texName); glBindTexture(GL_TEXTURE_2D, texName); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, spriteData); free(spriteData); return texName; }
- 获得Core Graphics 图片引用. 从上面的代码可以看出,其实灰常简单。我们使用UIImage imageNamed:方法初始化一个UIImage对象,然后获得它的CGImage属性即可。
- 创建Core Graphics位图上下文. 为了创建位图上下文,你将不得不手动分配内存空间。这里我们使用image的函数来得到宽度和高度,然后分配 width*height*4个字节的数据空间。
”为什么要乘以4?“你可能会问。当我们调用方法来绘制图片数据的时候,我们要为red,green,blue和alpha通道,每一个通道准备一个字节,所以就要乘以4.
“为什么要为每一个通道准备一个字节?”你可能又会问。好吧,因为我们将使用Core Graphics来建立绘图上下文。而CGBitmapContextCreate函数里面的第4个参数指定的就是每一个通道要采用几位来表现,我们使用8位,所以是一个字节。 - 把图片数据绘制到context中. 这也是一个非常简单的操作---我们只需要告诉Core Graphics在一个指定的矩形区域内来绘制这些图像即可。因为我们完成了绘制图片的工作,所以用完要release。
- 把像素数据发送给OpenGL. 我们首先调用 glGenTextures来创建一个纹理对象,并且得到一个唯一的ID,由“name”保存着。然后,我们调用glBindTexture来把我们新建的纹理名字加载到当前的纹理单元中。接下来的步骤是,为我们的纹理设置纹理参数,使用glTexParameteri函数。这里我们设置函数参数为GL_TEXTURE_MIN_FILTER(这个参数的意思是,当我们绘制远距离的对象的时候,我们会把纹理缩小)和GL_NEAREST(这个参数的作用是,当绘制顶点的时候,选择最邻近的纹理像素)。
注意:虽然本例子中没有使用mipmaps,但是,还是需要设置GL_TEXTURE_MIN_FILTER。我刚开始不知道要这样做,而且并没有设置这个参数,结果屏幕上什么也看不到!后来我在 OpenGL 常见错误 中发现在这个问题--走运啊!
最后一步就是把像素buffer中的数据发送给OpenGL,通过调用函数glTexImage2D。当你调用这个函数的时候,你需要指定像素格式。这里我们指定的是GL_RGBA和GL_UNSIGNED_BYTE。它的意思是说,红绿蓝alpha道具都有,并且他们占用的空间是1个字节,也就是每一个通道8位。
OpenGL还支持其它的像素格式(你也可以查一查 cocos2d支持的像素格式 有哪些)。但是,对于本教程来说,我们就只使用这种RGBA了,其它的留给读者自己去探究。
一旦我们把图片数据发送给OpenGL之后,我们就可以释放掉像素buffer了---我们不再需要它了,因为Opengl已经把纹理存储到GPU中去了。最后,我们返回纹理的名字,这个名字我们之后的程序要使用到。
使用纹理数据
现在,我们拥有一个辅助方法来加载图片并能够把它发送给OpenGL了。接下来,让我们使用这个给立方体做件“嫁衣”吧!
我们将从顶点和片断着色器开始。打开 SimpleVertex.glsl,然后替换成下面的内容:
这里,我们声明了一个新的属性,叫做TexCoordIn。记住,属性就是一个值,你可以把它赋值给每一个顶点。因此,对于每一个顶点,我们为它指定需要映射的纹理坐标。attribute vec4 Position; attribute vec4 SourceColor; varying vec4 DestinationColor; uniform mat4 Projection; uniform mat4 Modelview; attribute vec2 TexCoordIn; // New varying vec2 TexCoordOut; // New void main(void) { DestinationColor = SourceColor; gl_Position = Projection * Modelview * Position; TexCoordOut = TexCoordIn; // New }
纹理坐标看起来有点奇怪,它们的取值范围总是0-1.因此(0,0)就代表纹理的左下角,而(1,1)则代表纹理的右上角。
但是,CoreGraphics在加载图片的时候会垂直翻转图片。所以,在代码中(0,1)是左下角,而(0,0)是左上角,够奇怪了吧!
我们也创建一个新的varying,叫做TexCoordOut,并且把TexCoordIn赋值给它。记住,一个varying也是一个值,OpenGL在进行片断着色的时候会为我们自动进行运算,得到正确的坐标点。因此,打个比方,如果我们把一个正方形的左下角映射纹理坐标为(0,0),而把右上角映射为(1,0)。如果我们在渲染左下角和右上角中间的某个像素的时候,片断着色器会自动计算得到(0.5,0)。
接下来,替换掉SimpleFragment.glsl:
之前我们老是把目标色直接赋值给输出色--现在,我们把这个目标色乘以纹理图片中相应的坐标点处的颜色。texture2D是Opengl内置的一个函数,它可以得到一个纹理。varying lowp vec4 DestinationColor; varying lowp vec2 TexCoordOut; // New uniform sampler2D Texture; // New void main(void) { gl_FragColor = DestinationColor * texture2D(Texture, TexCoordOut); // New }
现在,我们新的shaders准备就绪了,让我们来使用它们吧!打开OpenGLView.h,然后添加下面几个实例变量:
这些变量来用保存我们之前添加进来的两张图片的纹理名字,同时声明了新的输入属性槽(input attribute slot)和一个新的纹理统一槽(new texture uniform slot)。GLuint _floorTexture; GLuint _fishTexture; GLuint _texCoordSlot; GLuint _textureUniform;
然后打开OpenGLView.m,并作如下修改:
基于我们前面教程中所介绍的内容,这里的大部分内容应该非常容易理解。// Add texture coordinates to Vertex structure as follows typedef struct { float Position[3]; float Color[4]; float TexCoord[2]; // New } Vertex; // Add texture coordinates to Vertices as follows const Vertex Vertices[] = { {{1, -1, 0}, {1, 0, 0, 1}, {1, 0}}, {{1, 1, 0}, {1, 0, 0, 1}, {1, 1}}, {{-1, 1, 0}, {0, 1, 0, 1}, {0, 1}}, {{-1, -1, 0}, {0, 1, 0, 1}, {0, 0}}, {{1, -1, -1}, {1, 0, 0, 1}, {1, 0}}, {{1, 1, -1}, {1, 0, 0, 1}, {1, 1}}, {{-1, 1, -1}, {0, 1, 0, 1}, {0, 1}}, {{-1, -1, -1}, {0, 1, 0, 1}, {0, 0}} }; // Add to end of compileShaders _texCoordSlot = glGetAttribLocation(programHandle, "TexCoordIn"); glEnableVertexAttribArray(_texCoordSlot); _textureUniform = glGetUniformLocation(programHandle, "Texture"); // Add to end of initWithFrame _floorTexture = [self setupTexture:@"tile_floor.png"]; _fishTexture = [self setupTexture:@"item_powerup_fish.png"]; // Add inside render:, right before glDrawElements glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*) (sizeof(float) * 7)); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _floorTexture); glUniform1i(_textureUniform, 0);
这里唯一值得详细指出来的就是最后3行代码。这也是我们如何把之前在片断着色器中定义的Texture uiniform映射到我们代码中的texture中来的。
首先,我们激活我们想要加载进入的纹理单元。在IOS上面,我们可以拥有至少2个纹理单元,最多是8个。当我们一次需要对不止一个纹理进行计算的时候,这个就发挥作用了。然后,在本教程中,我们并不需要多于一个的纹理单元,所以我们只需要第一个纹理单元(GL_TEXTURE0)。
然后,我们把纹理绑定到当前的纹理单元中(GL_TEXTURE0)。最后,把纹理单元0的索引设置为_textureUniform。
注意:其实第1和3句调用其实并不是必须的,有时候,你可能会看见别人的代码里面可能并没有包含这几行代码。这是因为我们假设GL_TEXTURE0就是当前激活的纹理单元了,我们也不需要设置uniform,因为它默认就是0.我在这篇教程中添加这3行代码的意思是方便初学者更好地理解代码。
编译并运行代码,这时你可以看到一个拥有纹理贴图的立方体啦!
恩。。。这个立方体的正面看起来还算ok,但是其它面看起来有点被拉伸了---这是怎么回事呢?
修复拉伸效果
举个例子,我们把第一面的左下角映射到(0,0)。但是,在左边那一面,同样的顶点数据却变成了右上角,所以这时候如果使用(0,0)纹理坐标去贴图的话就没有意义了,这时候应该要使用(1,0)。(为什么不是(1,1),因为图片垂直翻转了!!!)
在OpenGL里面,你不能简单的把一个顶点当成是一个顶点坐标---而应该是把坐标、颜色和纹理坐标绑定到一起,统一属于某一个顶点。
继续并把你的顶点和索引数组替换成下面的内容,它为每一面都定义了顶点坐标、颜色和纹理坐标数据。
就和上一篇教程一样,我首先在纸上把这些数据先用笔标记出来,然后再写代码记录下来---读者最好亲自动手实践一下,这是一个很好的机会!!!#define TEX_COORD_MAX 1 const Vertex Vertices[] = { // Front {{1, -1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{1, 1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{-1, 1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{-1, -1, 0}, {0, 0, 0, 1}, {0, 0}}, // Back {{1, 1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{-1, -1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{1, -1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{-1, 1, -2}, {0, 0, 0, 1}, {0, 0}}, // Left {{-1, -1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{-1, 1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{-1, 1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{-1, -1, -2}, {0, 0, 0, 1}, {0, 0}}, // Right {{1, -1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{1, 1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{1, 1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{1, -1, 0}, {0, 0, 0, 1}, {0, 0}}, // Top {{1, 1, 0}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{1, 1, -2}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{-1, 1, -2}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{-1, 1, 0}, {0, 0, 0, 1}, {0, 0}}, // Bottom {{1, -1, -2}, {1, 0, 0, 1}, {TEX_COORD_MAX, 0}}, {{1, -1, 0}, {0, 1, 0, 1}, {TEX_COORD_MAX, TEX_COORD_MAX}}, {{-1, -1, 0}, {0, 0, 1, 1}, {0, TEX_COORD_MAX}}, {{-1, -1, -2}, {0, 0, 0, 1}, {0, 0}} }; const GLubyte Indices[] = { // Front 0, 1, 2, 2, 3, 0, // Back 4, 5, 6, 4, 5, 7, // Left 8, 9, 10, 10, 11, 8, // Right 12, 13, 14, 14, 15, 12, // Top 16, 17, 18, 18, 19, 16, // Bottom 20, 21, 22, 22, 23, 20 };
注意,我们这一次重复了很多数据。我现在还没有找到一种更好的方式来做这件事,希望有牛人可以指出来,大家一起学习一下:)
编译并运行,这时我们有一个看起来更漂亮的立方体啦!
重复纹理
在OpenGL里面,如果你喜欢的话,你可以把一张图片重复地贴在某一个表面上。但是,保证重复纹理时能够拼接得很好,我们需要一些无缝纹理。
有了纹理之后,我们在OpenGLView.m里面定义下面的宏:
因此,现在,我们给立方体每一个表面从左下角(0,0)到右上角(4,4)都映射了纹理贴图。#define TEX_COORD_MAX 4
当映射纹理坐标的时候,它的行为看起来好像是1的模---比如,如果你的纹理坐标是1.5,那么映射的纹理坐标就会是0.5.
编译并运行,现在你可以看到立方体上有非常好看的重复纹理啦。
注意: 这个能够工作的原因是因为GL_TEXTURE_WRAP_S 和GL_TEXTURE_WRAP_T 默认值是GL_REPEAT 。如果你不想让纹理这样子重复的知,你可以调用函数glTexParameteri来覆盖默认的行为。
添加花样
我们将以在立方体的某一个面添加一个鱼骨头的纹理作为教程的结束。为什么呢?因为Grand Cat Dispatch 要上场啦!
实现这个功能的步骤可能和本教程前面部分差不了太多。所以,让我们直接开始吧。
打开OpenGLView.h,然后添加下面几个实例变量:
之前,我们只有一个顶点buffer和一个索引buffer,因此,我们需要再创建它们。现在我们需要2个顶点/索引buffer,就如同上面所定义的一样。GLuint _vertexBuffer; GLuint _indexBuffer; GLuint _vertexBuffer2; GLuint _indexBuffer2;
打开OpenGLView.m,然后做下面的修改:
在第一部分中,我们为矩形定义了一组顶点,我们将在这些顶点所在的范围内绘制鱼骨的纹理。注意,我把它弄得比前表面要稍微小一点,同时,我也把z坐标弄得稍微大一点。这样子我们就会有深度视觉效果了。// 1) Add to top of file const Vertex Vertices2[] = { {{0.5, -0.5, 0.01}, {1, 1, 1, 1}, {1, 1}}, {{0.5, 0.5, 0.01}, {1, 1, 1, 1}, {1, 0}}, {{-0.5, 0.5, 0.01}, {1, 1, 1, 1}, {0, 0}}, {{-0.5, -0.5, 0.01}, {1, 1, 1, 1}, {0, 1}}, }; const GLubyte Indices2[] = { 1, 0, 2, 3 }; // 2) Replace setupVBOs with the following - (void)setupVBOs { glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW); glGenBuffers(1, &_indexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW); glGenBuffers(1, &_vertexBuffer2); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2); glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices2), Vertices2, GL_STATIC_DRAW); glGenBuffers(1, &_indexBuffer2); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer2); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices2), Indices2, GL_STATIC_DRAW); } // 3) Add inside render:, right after call to glViewport glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer); // 4) Add to bottom of render:, right before [_context presentRenderbuffer:...] glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer2); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBuffer2); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, _fishTexture); glUniform1i(_textureUniform, 0); glUniformMatrix4fv(_modelViewUniform, 1, 0, modelView.glMatrix); glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0); glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (GLvoid*) (sizeof(float) * 3)); glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (GLvoid*) (sizeof(float) * 7)); glDrawElements(GL_TRIANGLE_STRIP, sizeof(Indices2)/sizeof(Indices2[0]),
GL_UNSIGNED_BYTE, 0);
在第二部分中,我们保存顶点/索引 buffer。我们也以鱼骨矩形区域创建第一个顶点/索引buffer。
在第三部分中,我们在绘制纹理之前先绑定顶点/索引buffer。
在第四部分中,我们绑定鱼矩形区域的顶点/索引buffer,加载鱼的纹理,然后设置所有的属性。注意,这里我们绘制三角形使用一种新的模式--GL_TRIANGLE_STRIP。
GL_TRIANGLE_STRIP会生成三角形带,具体的解释可以参考《如何使用cocos2d制作一个tiny wings游戏》。使用这个参数可以减少索引buffer的大小。
编译并运行,然后你会看到下面的输出。
这里绘制了鱼的图片,但是,渲染的效果并不是很好。为了使之效果更好看,我们需要激活blend,在渲染函数里面添加下面2行:
第一行使用glBlendFunc 来设置混合算法。设置源为GL_ONE的意思是渲染所有的源图像的像素,而GL_ONE_MINS_SRC_ALPHA意思是渲染所有的目标图像数据,但是源图像存在的时候,就不渲染。得到的效果就是,源图片覆盖在目的图像上面。为了更好地理解混合及其参数的含义,请参考这篇文章。glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND);
当然,我们在之前的教程中也有提到过,详情请查看 这篇教程 。
第2行代码是激活混合模式。就这么多!编译并运行,现在,你可以看到一个更加真实的渲染结果啦。
何去何从?
这里有本教程的完整源代码。
到目前为止,通过这2篇教程,相信大家对于OpenGL ES 2.0已经有一个基本的了解了,你应该学会如何添加顶点,如何创建顶点缓冲对象,如何建仓shaders,如何创建纹理对象等等。
其实,在OpenGL领域,你需要学习的还很多很多,但是,我希望这里是你非常好的一个开端。
如果你想学习更多有关Ipengles的内容的话,推荐你看iPhone 3D Programming。我也是从阅读这本书来学习的。