在OpenGL ES 2.0中渲染任何几何图形到屏幕中,你必须创造叫着色器的两个小程序。
着色器是写用一种叫做GLSL的类C语言。不用担心在这个点学习这个参考,现在你只要看这个例子在这个教程中。
有两种类型的着色器:
- 顶点着色器是一种程序在场景里的每个顶点将调用一次。如果你用一个正方形渲染一个简单的场景,每个角用一个顶点,这个将调用四次。他的工作是执行一些计算像光照,几何变化,等等。计算出最终的顶点的位置,也传输一些数据到片段着色器。
- 片段着色器 是一种程序在场景里的每个像素(顺序)将调用一次。如果你用一个正方形渲染一个相同简单的场景,在正方形覆盖的每个像素它将调用一次,片段着色器也能执行光照计算,等等。他们跟重要的工作是设置像素的最终颜色。
就像我说的,理解这个最简单的方法是看一些例子。让我们保持简单和美好的心情,写个尽可能简单的顶点和片段找色器。
在Xcode中,到
File\New\New File...,选择 iOS\Other\Empty,然后点击next,新文件的名字是 SimpleVertex.glsl, 然后点击保存。
打开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 }
这里的每个都是新的,让我们一行一行的解释!
- 这个 attribute 关键字定义这个着色器将传递进一个叫做Position的输入变量。后面,你将写一些代码传一些数据到输入变量。它将表明这个顶点的位置。注意这个顶点类型是vec4,意味着一个顶点有4个组件。
- 定义第二个输入变量,是顶点的颜色。
- 这个定义其他的变量,但是它没有attribute关键字,他是一个输出变量传递给片段着色器。它也有一个varying关键字,像是说“我将告诉你这个特定顶点的值,但是当你给出一个像素计算出这个值,指出这个平滑的值在顶点附近”。
哈?这个最后一点听起来有些迷惑,但是如果你看下面的图片实际上是很好理解的:基本上,你能为每个像素指定不同的颜色,它将设置所有的值在两个渐变!你将看到一个例子不久。4.每个着色器用一个main开始,就像C一样。5.设置这个目的颜色为这个顶点给原颜色。让这个OpenGL修改这个值上面解释的。6.这个构造输出变量你必须设置在顶点着色器叫gl_Position相等这个最终顶点位置。这里仅仅设置它为原始位置而不改变它最终。 好的,这个是简单的顶点着色器,下面让我们增加简单的片段着色器。 去File\New\New File…, 选择 iOS\Other\Empty,并且点击Next。新文件名字是SimpleFragment.glsl 打开SimpleFragment.glsl并且增加下面的代码:
varying lowp vec4 DestinationColor; // 1 void main(void) { // 2 gl_FragColor = DestinationColor; // 3 }
这个是漂亮和甜美的。不管怎样让我们解释它一行又一行:这个是从顶点着色器的输入变量。他是同顶点着色器完全的相同定义,除了他有一个附加的关键字:lowp。在fragment shader中,你必须指定一个精度。出于性能的考虑,你设置一个最低精度是一个好的规则。在这里我们设置最低精度,你也可以设置为medp和highp就像顶点着色器,片段着色器也是从main开始的就像在顶点着色器中你需要设置gl_Position。你需要在片段着色器中设置g_FragColor。这个是简单的把源颜色设置给目的颜色。好吧,现在需要增加一些代码到我们的程序中。编译顶点和片段着色器我们以glsl文件增加两个着色器到Xcode工程,但是Xcode仅仅把他们拷贝到工程bundle里没有做任何的处理。是在程序运行的时候编译和运行着色器!你或许会感到惊讶,但是这种处理不会依赖任何一种图形芯片。让我们写一个能编译着色器的方法。打开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.得到一个NSString用文件内容,这个一个UIKit编程常用方法。2.调用glCreateShader创建一个OpenGL对象,这个方法需要传递一个shaderType参数表明他是片段合适顶点着色器。我们把这个作为一个参数。3.调用glShaderSource为着色器设置源代码。在这里把源代码从NSString转化为C-String4.最后,调用glCompileShader在运行的时候编译着色器5.这个能失败的。他将被处理如果你的GLSL代码有一些错误。当他失败的时候,你需要输出一些失败的消息。这里使用glGetShaderiv和glGetShaderInfoLog输出错误消息到屏幕上。你能使用这个方法编译顶点着色器和片段着色器,但是有一些更多的步骤-连接他们一起,告诉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.使用你写的编译着色器的方法2.调用glCreateProgram,glAttachShader和glLinkProgram连接顶点好片段着色器到完成的程序3.调用glGetProgramiv和glGetProgramInfoLog经常连接错误,并且显示。4.调用glUseProgram告诉OpenGL得到的顶点信息5.最后,调用glGetAttribLocation得到顶点从顶点着色器输入的值,并设置他们。同样调用glEnableVertexAttribArray开启使用这些数组。两步后,增加下面的方法在调用initWithFrame中[self compileShaders];定义实例变量在头文件中:GLuint _positionSlot;GLuint _colorSlot;现在你能编译和运行了,如果显示绿色屏幕并(没有退出和一些错误),他意味着你的顶点和片段着色器在运行的时候编译成功。当然,没有看起来不同的,因为我们没有给任何顶点几何渲染,所以让我们做下面的。为一个简单的正方形创建顶点数据让我们开始渲染一个正方形的屏幕上,这个正方形像下面这样://image当你用OpenGL渲染几何体的时候,要记住他只能渲染数据线不能渲染正方形。然而你能渲染两个三角形。一个三角形顶点为(0,1,2),一个三角形顶点为(2,3,0)一个好的事情是你能使用你喜欢的方式保存三角形的原始数据,打开OpenGLView.m 创建一个c的结构体和一些数组,保存我们的正方形信息,像下面这样: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 };
·一个结构体保存我们每个顶点的信息(当前只要颜色好位置)·一个数组保存所有的点·一个数组得到一个三角形的列表我们现在有需要的使用信息,仅仅需要传递给OpenGL!创建顶点缓冲对象最好的方法发送数据到OpenGL是通过调用顶点缓冲对象。这个OpenGL存储顶点缓冲,你调用一些函数发送你的数据的OpenGL空间。有两种类型的顶点对象,一种是保持每个顶点的数据,一种是保持每个顶点的索引。增加一个方法上面的initWithFrame来创建这些:- (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来 生成一个新的顶点缓冲对象,glBindBuffer告诉OpenGL,当我使用GL_ARRAY_BUFFER时候,意味着我使用的是vertexBuffer,并且glBufferData发送数据到OpenGL空间。增加initWithFrame在调用渲染前:[self setupVBOs];更新渲染代码最终我们更新渲染方法来画新的顶点数据到屏幕上,使用新的着色器!替换渲染好下面的:- (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设置当前值从顶点着色器这个是一个特别重要的函数,让我们了解他怎么工作的。·第一个参数指定属性名字·第二个参数指定每个顶点有多少个值。·第三个参数指定每个值的类型·第四个参数总是false·第五个参数是跨过的大小·最后一个参数是偏移在结构体中找到这个数据。3.调用glDrawElements使魔法发生!实际结束调用顶点着色器为每一个顶点,片段着色器每一个显示的像素·第一个参数是指定画顶点的类型。有不同的选择像 GL_LINE_STRIP 和GL_TRIANGLE_FAN,但是GL_TRIANGLES是最常用的。·第二个参数是渲染顶点的个数。这里使用C的技巧计算元素的个数。·第三个参数是在索引数组中每个元素的类型·从这个文档,最终参数是一个索引的指针。但是我们使用的VBOs是一种特殊的情况,他将使用索引数组我们传进到OpenGL空间的GL_ELEMENT_ARRAY_BUFFER。会发生什么呢,你做到了!编译和运行这个程序你将在屏幕上看到一个漂亮的矩形:你或许会惊讶为什么矩形出现完美的在屏幕上。默认,OpenGL在(0,0,0)有一个照相机,视线朝Z轴。这个左下角是映射到(-1,-1),右上角是映射到(1,1),所以它把正方形扩展到整个屏幕。