OpenGL学习——数据传输和组织方式与着色器语法

之前的文章初步了解OpenGL的基本运行流程同时在Android和iOS这两个平台上搭建了一套简单的渲染框架让我们可以使用OpenGL引擎进行着色器的脚本渲染。

之前介绍了,我们通过顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)来操控每一个像素点的绘制着色工作。而在OpenGL中编写着色器有一套专门的语法叫GLSL,它的语法跟C语言很像。

这里就不重点学习语法了,可以将GLSL看做是C语言的一种变种。

简单脚本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NSString *const kVertexShaderString = SHADER_STRING
(
attribute vec2 position;
attribute vec2 inputTextureCoordinate;
varying vec2 textureCoordinate;

void main() {
gl_Position = vec4(position.xy, 0, 1);
textureCoordinate = inputTextureCoordinate;
}
);

NSString *const kFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputTexture;

void main() {
gl_FragColor = texture2D(inputTexture, textureCoordinate);
}
);

这里是我在上一篇文章里面用到的脚本,可以看到输入到平台中的脚本是直接包裹成一段字符串输入到OpenGL进行编译的。

让我们来简单看一下这两段脚本,首先则两段脚本里面声明了四个变量

  • attribute vec2 position
  • attribute vec2 inputTextureCoordinate
  • varying vec2 textureCoordinate
  • uniform sampler2D inputTexture;

其中使用到attribute,varying,uniform这三个限定符和vec2,sampler2D这两个变量类型以及一个highp精度指定。它们具体的含义下面详解,在这里我们只需要知道:

  • attribute用于从外部给Vertex Shader传递数值。
  • varying用于Vertex Shader给Fragment Shader传递数值。
  • uniform用于给Fragment Shader传递数值。
  • vec2是一个二元数的类型定义。
  • sampler2D是一个纹理句柄的类型定义。
  • highp用于指定变量为高精度。

数据组织和传递

这里我们要学习一下我们要传递给顶点着色器的是什么数据块,怎么传递给它?在OpenGL中有以下几种组织数据和传递数据的方式:

  • 使用glVertex或者Display List
  • VA(Vertex Array)
  • VBO(Vertex Buffer Object)
  • VAO(Vertex Array Object)

详细参考:OpenGL数据传输方式对比

数据块

数据传递

1.使用VA数组传递

参考上面的文章介绍,可知我们既可以一个个数值传给OpenGL也可以将整个数组传给它。当然我们一般都是通过传递整个数组数据给OpenGL。而传递的数据字段长度是可以定义的,比如我们定义一个3 * 3的VA数组。

1
2
3
4
5
6
7
8
9
GLfloat vertex[3 * 3] = {
-0.5f, 0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f
};

glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertices);//1.1可用
glDrawArray(GL_TRIANGLES, 0, 3);

上面的操作相当于在内存中定义了一份数据然后将它从CPU传递到GPU上。然后使用三角形图元绘制出来。值得注意的是这种方式是GL1的标准,在GL2之后使用编写渲染脚本的方式。

2.使用VBO/VAO/VBO传递数据

VBO出现之前,做OpenGL优化,提高顶点绘制效率的办法一般就两种:

显示列表:把常规的绘制代码放置一个显示列表中(通常在初始化阶段完成,顶点数据还是需要一个个传输的),渲染时直接使用这个显示列表。优化点:减少数据传输次数

顶点数组:把顶点以及顶点属性数据打包成单个数组,渲染时直接传输该数组。优化点:减少了函数调用次数(弃用glVertex)

显然之前两种传输数据的方式是GL2.0之前的方式,在这之后我们可以编写渲染脚本了我们开始使用VAO/VBO/EBO这三种传输数据的方式。

  • 顶点缓冲对象(Vertex Buffer Objects,VBO),VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。

  • 顶点数组对象(Vertex Arrary Object,VAO),VBO保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息,当数据量很大时,重复这样的动作变得非常麻烦。VAO可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个VAO对象就可以了。

  • 索引缓冲对象(Element Buffer Object,EBO),索引缓冲对象EBO相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多洗重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef struct {
GLKVector2 positionCoords;
} TriangleVertex;

// 定义三个顶点的位置
static TriangleVertex vertices[3] = {
{{-0.5f, -0.5f}},
{{ 0.5f, -0.5f}},
{{-0.5f, 0.5f}}
};

// 在初始化的地方生成一个VBO用于存储这三个顶点
glGenBuffers(1, &_vbo);

// 绘制的时候
[self.program use];
glViewport(0, 0, size.width, size.height);

// 使用VBO
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(self.positionAttribute);
glVertexAttribPointer(self.positionAttribute, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
glDisableVertexAttribArray(self.positionAttribute);

// 或者在CPU申请一块内存(这种就不是VBO了)直接将内存指针传递过去
const float vertex[8] = {
-0.5, -0.5,
-0.5, 0.5,
0.5, 0.5 };
// 将顶点坐标和program中position这个变量联系到一起
glVertexAttribPointer(self.positionAttribute, 2, GL_FLOAT, GL_FALSE, 0, vertex);

从实践过程来看,我们使用VBO来存储数据,但是由于每次调用VBO的需要给GL_ARRAY_BUFFER单元绑定一下数据或者更新其他绘制状态,所以使用VAO来简化绘制代码。而由于VBO里面可能存有大量的顶点数据而我们绘制所需要的只是某几个顶点,这时候我们可以通过EBO来指定顶点索引。具体示例可以看OpenGL学习实例

限定符

限定符是用于限定类型和类型成员的声明,常见的如访问限定符public,private,protected,类型限定符const等,而在OpenGL中有几个特有的限定符attribute,varying,uniform。

attribute 限定符

attribute只用于顶点着色器(Vertex Shader)中,它用于外部程序向顶点着色器脚本传递数据,attribute通常用来存储位置坐标、法向量、纹理坐标等信息。

例如上面那个脚本中定义了position,inputTextureCoordinate这两个变量是希望这次绘制空间和图像的坐标位置由外部来指定。

1
2
attribute vec2 position;
attribute vec2 inputTextureCoordinate;

接下来假设我们要绘制一个三角形:

1
2
3
4
5
6
7
const float vertex[8] = {
-0.5, -0.5,
-0.5, 0.5,
0.5, 0.5 };

// 将顶点坐标和program中position这个变量联系到一起
glVertexAttribPointer(self.positionAttribute, 2, GL_FLOAT, GL_FALSE, 0, vertex);

我们通过glVertexAttribPointer函数将三角形的三个顶点分别传递到position这个变量中。

与uniform相似,可使用的最大attribute数量也是有上限的,可以使用gl_MaxVertexAttribs来获取,也可以使用内置函数glGetIntegerv来询问GL_MAX_VERTEX_ATTRIBS。OpenGL ES 2.0实现支持的最少attribute个数是8个。

varying 限定符

varying存储的是顶点着色器的输出,同时作为片段着色器的输入,通常顶点着色器都会把需要传递给片段着色器的数据存储在一个或多个varying变量中。(Vertex -> Fragment)

这些变量在片段着色器中需要有相对应的声明且数据类型一致,然后在光栅化过程中进行插值计算。简单的说,这个限定符用于顶点着色器向片段着色器传递数据。

比如上面示例中textureCoordinate这个变量就是通过顶点着色器的inputTextureCoordinate将外部传入的纹理坐标再通过textureCoordinate传递给片段着色器。

1
varying highp vec2 textureCoordinate;

与uniform和attribute相同,varying也有数量的限制,可以使用gl_MaxVaryingVectors获取或使用glGetIntegerv查询GL_MAX_VARYING_VECTORS来获取。OpenGL ES 2.0实现中的varying变量最小支持数为8。

uniform 限定符

uniform用于存储应用程序通过GLSL传递给着色器的只读值。它通常是存储在硬件中的”常量区”,由于这一区域尺寸非常有限,因此着色程序中可以使用的uniform的个数也是有限的。

同时顶点着色器和片段着色器共享了uniform变量的命名空间,意味着两者都可以使用uniform来定义变量,然而一般情况下我们常使用这个限定符给片段着色器定义变量(主要用于传输纹理句柄)。

同样的它也有数量上的限制,可以通过读取内置变量gl_MaxVertexUniformVectors以及gl_MaxFragmentUniformVectors来获得。也可以使用glGetIntegerv查询GL_MAX_VERTEX_UNIFORM_VECTORS或者GL_MAX_FRAGMENT_UNIFORM_VECTORS。OpenGL-ES-2.0的实现必须提供至少128个顶点uniform矢量及16片段uniform矢量。

const 限定符

这个限定符跟C/C++中的含义是一致的,它用于定义常量即改变一个变量的读写属性,让它变成只读属性。

三角形动画

这里想要绘制一个三角形的简单动画,上面的脚本就可以实现这个功能。下面主要介绍如何使用VBO来组织数据块并实时更新它达到动画效果。

最终实现以下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)render:(CGSize)size {
// 当前上下文使用该渲染管线
[self.program use];

// 定义裁剪空间转换到屏幕上的空间大小
glViewport(0, 0, size.width, size.height);

// 更新VBO的数值
[self updateAnimatedVertexPositions];
glBindBuffer(GL_ARRAY_BUFFER, _VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glEnableVertexAttribArray(self.positionAttribute);

/**
* glVertexAttribPointer最后一个参数ptr指针的含义:
* 在不使用VBO的情况下:ptr就是一个指针,指向的是需要上传到顶点数据指针。通常是数组名的偏移量。
* 在使用VBO的情况下:首先要glBindBuffer,以后ptr指向的就不是具体的数据了。这里的ptr指向的是缓冲区数据的偏移量。
**/
glVertexAttribPointer(self.positionAttribute, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);

// 使用三角形图元绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
glDisableVertexAttribArray(self.positionAttribute);
}

这里只是加了一步操作VBO的流程,其他的与之前文章的内容是一致的。

Demo工程:OpenGL学习实例

总结

这篇文章简单学习了我们要传输什么样的数据给OpenGL以及要如何传输数据给它,然后编写了一个简单的渲染脚本进一步感知OpenGL这套框架的一些操作原理。