3、第一个三角形:创建窗口与上下文、顶点着色器与片段着色器、编译与链接着色器程序、绘制三角形
好,我们终于要画第一个三角形了。
说实话,每次带新人入门OpenGL ES,我最期待的就是这一刻。你想想看,从一堆枯燥的初始化代码,到屏幕上真的出现一个彩色三角形——那种成就感,我到现在还记得。
这一章,我们一步步来。别急,代码我会拆得很细。
3.1 创建窗口与上下文
OpenGL ES本身是个状态机,它不负责创建窗口。你得借助平台相关的API,比如EGL(在Android上)或GLFW(在桌面端)。我个人习惯用GLFW做原型验证,因为它跨平台,省心。
创建窗口的流程,说白了就三步:
- 初始化GLFW:调用
glfwInit(),告诉系统我要用OpenGL ES。 - 设置上下文版本:指定OpenGL ES 3.0(或更高)。
- 创建窗口:
glfwCreateWindow()返回一个窗口句柄。
嗯,这里要注意:上下文必须和窗口绑定。没有上下文,你调任何OpenGL函数都会崩溃。我在项目中遇到过有人忘了调用glfwMakeContextCurrent(),结果查了半天bug,最后发现是上下文没激活。
核心代码片段:
// 初始化GLFW
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "第一个三角形", NULL, NULL);
if (!window) {
// 处理失败
glfwTerminate();
return -1;
}
// 绑定上下文
glfwMakeContextCurrent(window);
小技巧:调试阶段,窗口尺寸别设太大。800x600足够了,跑起来快,也方便看日志。
3.2 顶点着色器与片段着色器
OpenGL ES的渲染管线,核心就是两个可编程阶段:顶点着色器和片段着色器。
- 顶点着色器:处理每个顶点。比如你把三角形的三个顶点坐标传进去,它负责变换位置。
- 片段着色器:处理每个像素。它决定三角形内部每个像素最终显示什么颜色。
你想想看,这两个着色器就像流水线上的工人。顶点着色器负责“把零件摆好位置”,片段着色器负责“给零件上色”。
下面是最简单的顶点着色器代码:
#version 300 es
layout (location = 0) in vec3 aPos;
void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
片段着色器呢?更简单:
#version 300 es
precision mediump float;
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 橙色
}
注意:OpenGL ES 3.0要求着色器第一行必须是#version 300 es。我见过有人从桌面OpenGL移植代码,忘了改版本号,结果编译死活过不去。
3.3 编译与链接着色器程序
着色器写好了,但GPU不认识字符串。你得把它编译成二进制,再链接成一个着色器程序。
流程是这样的:
- 创建着色器对象:
glCreateShader() - 上传源码:
glShaderSource() - 编译:
glCompileShader() - 检查编译状态:
glGetShaderiv() - 创建程序对象:
glCreateProgram() - 附加着色器:
glAttachShader() - 链接:
glLinkProgram() - 检查链接状态:
glGetProgramiv()
听起来步骤多,但其实每次都是模板代码。我习惯封装一个工具函数,叫createShaderProgram(),传入顶点和片段着色器源码,返回程序ID。
编译检查的坑:
// 检查编译是否成功
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, NULL, infoLog);
// 打印日志,方便调试
}
我曾经...有一次怎么都编译不过,日志提示“syntax error”。我盯着代码看了半小时,最后发现是中文分号。嗯,从那以后我写着色器只用英文输入法。
3.4 绘制三角形
万事俱备,只欠东风。现在我们要把顶点数据传给GPU,然后告诉它“画!”
步骤:
- 准备顶点数据:三个顶点的坐标。
- 创建VBO和VAO:把数据上传到显存。
- 绑定着色器程序:
glUseProgram() - 绘制:
glDrawArrays()
顶点数据长这样:
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f, // 右下
0.0f, 0.5f, 0.0f // 顶部
};
创建VAO和VBO:
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 告诉OpenGL如何解析顶点数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
最后,在主循环里绘制:
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
为什么用VAO?VAO记录了所有顶点属性的配置。你想想看,如果没有VAO,每次绘制前都要重新设置glVertexAttribPointer,多麻烦。VAO就是帮你记住这些配置的“快捷方式”。
常见错误:忘记调用glUseProgram()。如果你没绑定着色器程序,OpenGL会使用默认的(通常是空的),结果就是屏幕上啥也没有。我曾经debug了一下午,最后发现是程序ID传错了。
3.5 完整流程回顾
来,我们捋一遍:
| 步骤 | 做什么 | 关键函数 |
|---|---|---|
| 1 | 创建窗口和上下文 | glfwInit, glfwCreateWindow, glfwMakeContextCurrent |
| 2 | 编写着色器源码 | 顶点着色器 + 片段着色器 |
| 3 | 编译并链接程序 | glCompileShader, glLinkProgram |
| 4 | 准备顶点数据 | glGenBuffers, glBufferData |
| 5 | 设置顶点属性 | glVertexAttribPointer, glEnableVertexAttribArray |
| 6 | 绘制 | glUseProgram, glDrawArrays |
看到屏幕上出现那个橙色三角形的时候,你可能会觉得:就这么简单?
嗯,其实背后GPU做了大量工作。顶点着色器处理了三个顶点,光栅化器把三角形拆成无数个片段,片段着色器给每个像素上色。你看到的每一帧,都是成千上万次计算的结果。
我个人觉得,第一个三角形是OpenGL ES学习的分水岭。跨过这一步,后面的纹理、光照、模型加载,都是在这个基础上加东西。所以,好好享受这一刻吧。
延伸思考:试试修改片段着色器的颜色值,或者把GL_TRIANGLES改成GL_LINE_LOOP,看看效果有什么不同。动手实验,比看十遍文档都管用。