如果你仍能正常地看到之前那个绿色的屏幕,就证明你前面写的代码都很好地工作了。
在这里,我们打算在屏幕上渲染一个正方形,如下图:
在你用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
传数据到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
想起哪里用过这个模式吗?要不再回去看看framebuffer那一段?
万事俱备,我们可以通过新的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来为vertexshader的两个输入参数配置两个合适的值。
第二段这里,是一个很重要的方法,让我们来认真地看看它是如何工作的:
·第一个参数,声明这个属性的名称,之前我们称之为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 main uniform mat4 Projection; // Modify gl_Position line as follows gl_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’sfolder (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 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
// Add right after the Projection uniform uniform mat4 Modelview; // Modify the gl_Position line gl_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 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.
编译运行,就算我们把z设回0,也可以看到这个位于中间的正方形了。
什么?一动不动的?
当然了,我们只是调用了一次render方法。
接下来,我们在每一帧都调用一次看看。
理想状态下,我们希望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效果。
之前的只能算是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 };
编译运行,你会看到一个方块了。
但这个方块有时候让人觉得假,因为你可以看到方块里面。
这里还有一个叫做 depthtesting(深度测试)的功能,启动它,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 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的一篇引导教程,希望能让你轻松地入门。
对了,我写这篇教程的原因是它在过去的数周中得票最高。谢谢大家的关注,并继续在今后为自己感兴趣的题目投上一票 ---- 我们每周都有一个新教程的。
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!