3、第一个三角形:创建窗口与上下文、顶点着色器与片段着色器、编译与链接着色器程序、绘制三角形

好,我们终于要画第一个三角形了。

说实话,每次带新人入门OpenGL ES,我最期待的就是这一刻。你想想看,从一堆枯燥的初始化代码,到屏幕上真的出现一个彩色三角形——那种成就感,我到现在还记得。

这一章,我们一步步来。别急,代码我会拆得很细。

3.1 创建窗口与上下文

OpenGL ES本身是个状态机,它不负责创建窗口。你得借助平台相关的API,比如EGL(在Android上)或GLFW(在桌面端)。我个人习惯用GLFW做原型验证,因为它跨平台,省心。

创建窗口的流程,说白了就三步:

  1. 初始化GLFW:调用glfwInit(),告诉系统我要用OpenGL ES。
  2. 设置上下文版本:指定OpenGL ES 3.0(或更高)。
  3. 创建窗口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不认识字符串。你得把它编译成二进制,再链接成一个着色器程序

流程是这样的:

  1. 创建着色器对象:glCreateShader()
  2. 上传源码:glShaderSource()
  3. 编译:glCompileShader()
  4. 检查编译状态:glGetShaderiv()
  5. 创建程序对象:glCreateProgram()
  6. 附加着色器:glAttachShader()
  7. 链接:glLinkProgram()
  8. 检查链接状态: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,然后告诉它“画!”

步骤:

  1. 准备顶点数据:三个顶点的坐标。
  2. 创建VBO和VAO:把数据上传到显存。
  3. 绑定着色器程序glUseProgram()
  4. 绘制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,看看效果有什么不同。动手实验,比看十遍文档都管用。