3、第一个三角形:窗口创建、顶点数据定义、着色器编写、绘制三角形

说实话,每次带新人入门图形API,我最怕他们卡在「第一个三角形」这一步。明明代码逻辑都对,屏幕上就是一片漆黑。我自己当年也在这上面栽过跟头——折腾了整整一个下午,最后发现是窗口初始化时少传了一个标志位。

好,咱们今天就把这个坎儿迈过去。我会带着你,从零开始,一步步把那个经典的三角形画到屏幕上。

3.1 窗口创建:你的画布从哪来?

图形API本身不负责创建窗口。它只管画,至于画在哪,那是操作系统的事。所以我们需要一个「中间人」——GLFW 或者 SDL 都行。我个人习惯用 GLFW,轻量、干净。

核心思路: 先创建窗口,再绑定图形上下文。顺序不能乱。

来看一段初始化代码:

// 初始化 GLFW
if (!glfwInit()) {
    // 嗯,这里要处理错误
    return -1;
}

// 设置 OpenGL 版本(3.3 核心模式)
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

// 创建窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "第一个三角形", NULL, NULL);
if (!window) {
    glfwTerminate();
    return -1;
}

// 绑定上下文
glfwMakeContextCurrent(window);

这里有个坑,我曾经踩过:核心模式下,旧版的固定管线函数全废了。你如果习惯用 glBegin/glEnd,那在核心模式下编译都过不去。所以一开始就要用可编程管线——也就是着色器。

小提示: 窗口创建后,记得检查一下版本号。用 glGetString(GL_VERSION) 打印出来,确保你加载的是 3.3 以上。

3.2 顶点数据定义:三角形长什么样?

三角形有三个顶点。每个顶点需要位置信息——说白了就是三维坐标。在 OpenGL 里,坐标范围是 [-1, 1],超出这个范围就看不见了。

我习惯把顶点数据定义成一个浮点数组:

float vertices[] = {
    -0.5f, -0.5f, 0.0f,  // 左下
     0.5f, -0.5f, 0.0f,  // 右下
     0.0f,  0.5f, 0.0f   // 顶部
};

为什么是三个分量?因为 OpenGL 内部用齐次坐标,第四个分量 w 默认是 1.0。你只传 xyz 没问题,它会自动补全。

数据有了,怎么传给 GPU?答案是 VBO(顶点缓冲对象)VAO(顶点数组对象)。VAO 是 OpenGL 3.0 之后引入的,说白了就是个「配置记忆器」——你把顶点属性的布局设置好,存到 VAO 里,以后画的时候直接绑定 VAO 就行。

unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);

// 先绑定 VAO
glBindVertexArray(VAO);

// 再绑定 VBO,把数据传进去
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);

// 解绑(好习惯)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

注意: 顺序很重要!先绑 VAO,再绑 VBO,最后设置属性指针。我曾经因为先绑了 VBO 再绑 VAO,结果属性指针没关联上,屏幕黑了半小时。

3.3 着色器编写:GPU 的「小剧本」

着色器就是用 GLSL 写的小程序。每个顶点执行一次顶点着色器,每个片段(像素)执行一次片段着色器。说白了,就是告诉 GPU:「顶点怎么变换,像素什么颜色」。

先看顶点着色器:

#version 330 core
layout (location = 0) in vec3 aPos;

void main() {
    gl_Position = vec4(aPos, 1.0);
}

这里 layout (location = 0) 对应上面 glVertexAttribPointer(0, ...) 的第一个参数。说白了就是「第 0 号输入通道接收位置数据」。

再看片段着色器:

#version 330 core
out vec4 FragColor;

void main() {
    FragColor = vec4(1.0, 0.5, 0.2, 1.0);  // 橙色
}

颜色是 RGBA,范围 0.0 到 1.0。这里我给了个橙色——你想想看,第一个三角形用橙色,是不是比纯白更有成就感?

着色器写好了,怎么编译?OpenGL 提供了编译函数:

// 编译顶点着色器
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

// 链接成着色器程序
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

// 用完可以删掉中间产物
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

避坑指南: 每次编译后,记得检查编译状态。用 glGetShaderivGL_COMPILE_STATUS。我曾经因为 GLSL 版本号写成了 #version 300(少了个 es),编译失败但没检查,结果找了半天 bug。

3.4 绘制三角形:见证奇迹的时刻

所有准备工作就绪,终于可以画了。绘制循环长这样:

while (!glfwWindowShouldClose(window)) {
    // 清屏
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 使用着色器程序
    glUseProgram(shaderProgram);

    // 绑定 VAO
    glBindVertexArray(VAO);

    // 画!三个顶点,画三角形
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 交换缓冲区
    glfwSwapBuffers(window);
    glfwPollEvents();
}

这里 glDrawArrays(GL_TRIANGLES, 0, 3) 的意思是:从第 0 个顶点开始,取 3 个顶点,画成三角形。如果你有 6 个顶点,可以画两个三角形。

为什么要有 glfwSwapBuffers?因为 OpenGL 默认用双缓冲——一个缓冲区在后台画,画好了交换到前台显示。这样不会出现画面撕裂。

核心流程总结:

  1. 创建窗口 + 绑定上下文
  2. 定义顶点数据 → 存入 VBO
  3. 设置顶点属性布局 → 存入 VAO
  4. 编写并编译着色器 → 链接成程序
  5. 循环中:清屏 → 绑定程序 → 绑定 VAO → 调用绘制

3.5 知识体系总览

下面这张图,是我自己画的一个流程结构。你看一眼,就能把整个「第一个三角形」的脉络串起来:

第一个三角形 · 核心流程 1. 创建窗口 (GLFW) 2. 顶点数据 → VBO → VAO 定义顶点坐标 → 上传到GPU → 设置属性布局 3. 着色器编写 → 编译 → 链接 顶点着色器 + 片段着色器 → 生成着色器程序 4. 绘制循环 → glDrawArrays 关键函数 glGenVertexArrays glGenBuffers glBufferData glVertexAttribPointer glCreateShader glCompileShader glLinkProgram glDrawArrays

你看,整个流程其实就四个大块。窗口是基础,数据是原料,着色器是加工逻辑,绘制循环是最终输出。缺一个都不行。

3.6 常见问题与排查

现象 可能原因 排查方法
窗口黑屏 未绑定上下文 / 未清屏 检查 glfwMakeContextCurrentglClear
三角形不显示 VAO 未绑定 / 着色器未使用 检查 glBindVertexArrayglUseProgram
三角形变形 视口未设置 调用 glViewport(0, 0, width, height)
编译报错 GLSL 版本不匹配 / 语法错误 glGetShaderInfoLog 查看错误信息

嗯,这里要特别说一下:视口设置很多人会忘。窗口大小变了,视口也要跟着变。你可以在窗口回调里更新:

void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

注册回调:glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

好了,第一个三角形就讲到这里。代码量不大,但每一步都有它的道理。你照着敲一遍,看到屏幕上那个橙色三角形亮起来的时候——嗯,那种感觉,我到现在还记得。


专注资料整理