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);
避坑指南: 每次编译后,记得检查编译状态。用 glGetShaderiv 查 GL_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 默认用双缓冲——一个缓冲区在后台画,画好了交换到前台显示。这样不会出现画面撕裂。
核心流程总结:
- 创建窗口 + 绑定上下文
- 定义顶点数据 → 存入 VBO
- 设置顶点属性布局 → 存入 VAO
- 编写并编译着色器 → 链接成程序
- 循环中:清屏 → 绑定程序 → 绑定 VAO → 调用绘制
3.5 知识体系总览
下面这张图,是我自己画的一个流程结构。你看一眼,就能把整个「第一个三角形」的脉络串起来:
你看,整个流程其实就四个大块。窗口是基础,数据是原料,着色器是加工逻辑,绘制循环是最终输出。缺一个都不行。
3.6 常见问题与排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 窗口黑屏 | 未绑定上下文 / 未清屏 | 检查 glfwMakeContextCurrent 和 glClear |
| 三角形不显示 | VAO 未绑定 / 着色器未使用 | 检查 glBindVertexArray 和 glUseProgram |
| 三角形变形 | 视口未设置 | 调用 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);
好了,第一个三角形就讲到这里。代码量不大,但每一步都有它的道理。你照着敲一遍,看到屏幕上那个橙色三角形亮起来的时候——嗯,那种感觉,我到现在还记得。