OpenGL ES 3.0 学习 —— 绘制一个三角形
在 上篇 我们使用 OpenGL ES 3.0 给 GLSurfaceView
绘制了一个背景色,现在是时候更加一步了,我们来画一个三角形。
首先,我们来了解一下什么是图形管线(graphics pipeline)。GPU 把绘图的过程分成了几步,每一步执行一些特定的任务。使用 OpenGL ES,可以让我们指定其中的某些步骤,从而获得极大的灵活性:
1. 首先是顶点着色器(vertex shader)。他用于接收 CPU 的数据,然后输出图元(primitives)的顶点。在 OpenGL 的世界里,我们只能绘制三角形、直线和点。这三个形状就是所谓的图元。
2. 顶点着色器输出顶点数据后,经由形状装配、光栅化等,得到一个个的小片段(可以理解为一个个像素点)。
3. 接下来,这些小片段会交由片段着色器(fragment shader)处理。片段着色器的任务是确定像素点的颜色。
在这些步骤里,我们关心的只是顶点着色器和片段着色器。他们由程序员通过 OpenGL ES SL 语言(一种类 C 语言)编写。使用 OpenGL ES 3 的时候,如果没有同时指定顶点着色器和片段着色器,我们将无法绘制任何东西。
有了这两个基本概念后,我们就可以开始动手写程序。完整的代码可以在 Github 上找到。
首先,我们来编写顶点着色器:
#version 300 es
layout (location = 0) in vec2 vPosition;
void main() {
gl_Position = vec4(vPosition, 0.0f, 1.0f);
}
编写一个着色器程序,我们首先要声明 GLSL 语言的版本,这里是 300 es
指 OpenGL ES SL 3.0 版本。第二行的in
表示这是一个(顶点)输入变量,也叫顶点属性;location = 0
把这个属性的位置设为 0,在下面我们还将遇到它。数据的输入在下面我们也会介绍。
和 C 语言一样,main
函数是程序的入口。
vec4
的 GLSL 内置的数据类型,表示长度为 4 的向量。类似的,还有 vec2, vec3
。顶点坐标由 4 维的向量表示,分别是 xyzw
。因为我们只是在平面上画三角形,所以只在程序里设置了 xy
。z
坐标为 0;w
坐标成为其次坐标,在对物体进行平移的时候非常有用,这里我们暂时忽略,将它设置为 1。
gl_Position
是 GLSL 预定于的变量,用于输出顶点的坐标(我们只要赋值给它就可以了)。
接下来是片段着色器:
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(0.64f, 0.77f, 0.22f, 1.0f);
}
precision mediump float
指定默认精度为 mediump
,表示中等精度。类似的还有 lowp, highp
,分别是低、高精度。对于片段着色器,float
必须指定精度;而顶点着色器如果没有指定精度,默认为 highp
。
由于详细地写太耗时间,我又没有把握写得比别人好。所以,从这里开始,不再详细描述所有过程,而是假定读者具有相关的背景知识。文章的目的是复习,而不是学习。
out vec4 fragColor
用于输出该片段的颜色。由于这里只需要一个颜色值,所以不需要指定 location
。
下面,我们编译着色器:
GLuint createShader(const char *sourceCode, GLenum shaderType) {
auto shader = glCreateShader(shaderType);
if (!shader) {
checkGlError("glShaderSource");
return 0;
}
glShaderSource(shader, 1, &sourceCode, nullptr);
auto compiled = GL_FALSE;
glCompileShader(shader);
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
LOGE(kTag, "fail to compile shader");
GLint infoLen = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 0) {
GLchar *info = new GLchar[infoLen];
glGetShaderInfoLog(shader, infoLen, nullptr, info);
LOGE(kTag, "error msg = %s, source = %s", info, sourceCode);
delete[] info;
glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
类似于 C 用于,编译后,还需要链接步奏,才是一个完整的程序:
GLuint createProgram(const char *vertexShaderSource, const char *fragmentShaderSource) {
auto vertexShader = createShader(vertexShaderSource, GL_VERTEX_SHADER);
if (!vertexShader) return 0;
auto fragmentShader = createShader(fragmentShaderSource, GL_FRAGMENT_SHADER);
if (!fragmentShader) return 0;
auto program = glCreateProgram();
if (!program) {
checkGlError("glCreateProgram");
return 0;
}
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
auto compiled = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &compiled);
if (!compiled) {
LOGE(kTag, "fail to link program");
GLint infoLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 0) {
GLchar *info = new GLchar[infoLen];
glGetProgramInfoLog(program, infoLen, nullptr, info);
LOGE(kTag, "error msg = %s", info);
delete[] info;
glDeleteProgram(program);
program = 0;
}
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
这里值得注意的是,链接完成后,shader
对象就可以删除了。
下面我们开始画三角形。首先是顶点的数据,一共三个点:
const GLfloat kVertex[] = {
-0.5f, -0.5f, // bottom left
0.5f, -0.5f, // bottom right
0.0f, 0.5f, // top
};
接下来我们配置顶点着色器的输入:
glGenVertexArrays(1, &mVAO);
glGenBuffers(0, &mVBO);
glBindVertexArray(mVAO);
glBindBuffer(GL_ARRAY_BUFFER, mVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertex), kVertex, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glEnableVertexAttribArray(0);
注意后两行的第一个参数 0
。这里的 0
就对应上文我们提到的 location = 0
。glVertexAttribPointer
的第2个参数表示一共顶点的数据量,由于我们只指定了 x, y
,所以这里是 2
。
最后,我们绘制三角形:
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(mProgram);
glBindVertexArray(mVAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
出来的效果是这样的:
上一篇: GLFW绘制两个三角形拼接为一个矩形