OpenGL ES学习 - 着色器
GLSL
前一篇主要是学习OpenGL的基本使用以及完成我们的Hello Triangle项目。在里面我们配置了顶点和片段着色器,对于Hello Triangle项目来说这两个着色器完全够用。但如果需要处理更为复杂的绘制,显然这两个基本着色器是远远不行的,所以这篇文章主要更深入着色器程序,对着色器程序进行更完善的配置。
前面我们已经说过在Xcode中如何编写着色器源码了,在新建空文件的时候我们保存的后缀为.glsl。没错,着色器是使用一种叫GLSL的类C语言完成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。这是前面使用到的顶点着色器源码
attribute vec4 Position;
void main(void) {
gl_Position = Position;
}
第一行定义输入变量也叫顶点属性,这是一个4分量的向量Position。其实我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用。可以使用glGetIntegerv来输出上限数:
int numAttributs;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &numAttributs);
NSLog(@"----numAttributs:%d",numAttributs);
这里输出的是----numAttributs:16,表明能声明的顶点属性上限为16个,大部分情况下是够用了。
数据类型
和其他编程语言一样,GLSL有数据类型可以来指定变量的种类,GLSL中包含C等其它语言大部分的默认基础数据类型:Int、float、double、uint和bool,GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)。
向量(Vector)
GLSL中的向量是一个可以包含有1、2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):
vecn | 包含n个float分量的默认向量 |
---|---|
bvecn | 包含n个bool分量的默认向量 |
ivecn | 包含n个int分量的默认向量 |
uvecn | 包含n个unsigned int分量的默认向量 |
dvecn | 包含n个double分量的默认向量 |
大多数时候我们使用vecn,因为float足够满足大多数要求了。
一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量。 向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组:
vec1 pos1;
vec2 pos2 = pos1.xx // 可以使用pos1的x分量重组一个vec2的pos2
vec4 pos3 = pos2.xyxy // 使用pos2的x和y分量重组一个4分量的pos3
你可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个vec2向量中去获取.z元素,例如上面的pos2里面的没有z元素的。
除了上面的重组,我们还可以这样操作:
vec2 pos4 = vec2(0.5,0.5);
vec3 pos5 = vec3(pos4,1.0);
vec4 pos6 = vec4(pos5.xyz,1.0);
//向量是一种灵活的数据类型,我们可以把用在各种输入和输出上。
Uniform
precision mediump float;
void main(void) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
在前面的程序中,我们是在片段着色器中定义的最终输出颜色为vec4(1.0, 0.0, 0.0, 1.0)。这里就有个问题,如果我们想从应用程序中直接给片段着色器设置颜色该怎么做。
OpenGL显然早已解决了这个问题。在OpenGL中存在一种从CPU中的应用向GPU中的着色器发送数据的方式,但凡定义在着色器程序中的属性前面加上Uniform修饰,这个属性就是一个全局常量,可以存储着色器所需要的各种数据。另外uniform 的空间被顶点着色器和片段着色器分享。也就是说顶点着色器和片段着色器被链接到一起进入项目,它们分享同样的uniform。因此一个在顶点着色器中声明的uniform,相当于在片段着色器中也声明过了。当应用程序装载uniform 时,它的值在顶点着色器和片段着色器都可用。在链接阶段,链接器将分配常量在项目里的实际地址,那个地址是被应用程序使用和加载的标识。
下面我们在片段着色器中声明一个uniform属性,并在着色器主函数中设置最终输出颜色为定义好的uniform:
precision mediump float;
uniform vec4 color;
void main(void) {
gl_FragColor = color;
}
//现在这个uniform现在还是空的;我们还没有给它添加任何数据,这里需要注意如果你声明了一个uniform却在GLSL代码中没用过,
编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误
现在我们需要在应用中找到这个uniform所在的位置然后对其进行更新:
// 获取uniform的位置值,如果glGetUniformLocation返回-1就代表没有找到这个位置值。
int vertexColorLoc = glGetUniformLocation(programHandle, "color");
glUseProgram(programHandle);
// 然后我们可以通过glUniform4f函数设置uniform值。
glUniform4f(vertexColorLoc, 0.5f, 1.0f, 0.5f, 1.0f);
这里有一点需要注意,查询uniform地址不要求之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的,所以在设置uniform之前我们还需要调用glUseProgram函数激活着色器程序
这样运行的话我们就可以看到颜色不一样的图形了。
多个属性
在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
const GLfloat vertices[] = {
//位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f,0.0f,0.0f // 右上角
-0.5f, -0.5f, 0.0f, 0.0f,1.0f,0.0f // 右下角
0.0f, 0.5f, 0.0f , 0.0f,0.0f,1.0f // 左下角
};
定义好顶点数组,我们还需要修改一下顶点着色器源码,由于新增了颜色值,所以在顶点着色器中我们需要定义一个输入属性来接受顶点数组中传进来的颜色数据。并且还需要定义一个输出属性,用来传递颜色值给片段着色器。所以现在的顶点着色器源码如下:
attribute vec4 Position;
attribute vec4 color;
varying vec4 aColor; // 向片段着色器输出一个颜色
void main(void) {
gl_Position = Position;
aColor = color; // aColor设置为我们从顶点数据那里得到的输入颜色
}
再看下片段着色器:
precision mediump float;
varying lowp vec4 aColor; // 接受顶点着色器输出的颜色
void main(void) {
gl_FragColor = aColor; // 设置最终颜色为接受的颜色
}
配置完顶点着色器和片段着色器,接下来需要重新设置顶点数据格式了,因为我们添加了另一个顶点属性,并且更新了VBO的内存,更新后的VBO内存中的数据现在看起来像这样:
相比较只有顶点坐标属性的时候,VBO内存中的每个顶点数据中新增了颜色值,知道了现在的数据布局,我们就可以使用glVertexAttribPointer函数更新顶点格式:
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), 0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
这里需要注意的是第四个参数:由于我们现在有了两个顶点属性,所以不得不重新计算连续顶点属性之间的偏移量,为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
参考《OpenGL ES 3.0编程指南》