OpenGL 窗口、上下文与渲染循环

区分 OpenGL 规范、GLFW、GLAD 与 context,再把 viewport、输入、清屏和双缓冲接成完整帧循环。

学习目标

  • 能解释 OpenGL specification、驱动实现、GLFW 与 GLAD 各自负责什么
  • 能描述窗口创建、context current、函数加载与 viewport 初始化的严格顺序
  • 能比较桌面 OpenGL 与 WebGL2 的画布、上下文和函数入口差异
  • 能解释渲染循环如何处理 input、clear、draw、swap buffers 与 poll events
  • 能判断窗口 resize、context 创建失败或 loader 失败时应在哪个边界诊断

为什么屏幕上的画面需要一帧一帧重画

想象一位画家面前摆着一张空白画布。他要展示一段动画,靠的不是魔法,而是一张接一张地飞快重画:擦掉旧画、画上新的一张、再擦掉、再画……快到你的眼睛看成了连续的动作。电影院的胶片、电视、游戏画面,背后都是这一套——一秒钟翻过几十张。

网页里要让画面动起来,做的是同一件事:先要有一张「画布」,再要有一支能往画布上涂的「画笔」,然后不停地重画。这一章解决的就是最开头的问题——画布从哪来、画笔怎么拿、怎么让重画这件事一直转下去

没有这套机制会怎样?你连「往哪画、用什么画」都没有,更别说让画面动起来——屏幕只会是一片什么都没发生的空白。所以在画三角形、打光、贴纹理之前,得先把这张画布支起来。

OpenGL、GLFW、GLAD 与 Context 不是同一个东西

描述函数应该产生什么结果,真正实现通常来自 GPU vendor driver。它本身不创建窗口、不读取键盘,也不保证应用已经拿到可调用的函数地址。

负责操作系统边界:初始化、声明 OpenGL 3.3 core profile hints、创建 window、注册 resize/input callbacks,并把某个设为 calling thread 的 current context。

随后通过 glfwGetProcAddress 装载函数入口。顺序不能颠倒:没有 current context,loader 不知道应向哪个实现查询地址。

第一个窗口由四份契约组成

规范、上下文、函数加载、帧生命周期

1. SpecificationOpenGL 3.3 core

定义命令结果,不负责窗口

2. Window + contextGLFW

窗口、输入、current context

3. Function loadingGLAD

按 context 装载函数地址

4. Frame lifecycleviewport · clear · swap

事件、绘制、前后缓冲交换

OpenGL 只规定图形 API 语义;GLFW 创建窗口与 current context,GLAD 在该 context 上加载函数,frame loop 再处理输入、viewport、clear、draw 与 buffer swap。
glfwInit();
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, "LearnOpenGL", nullptr, nullptr);
if (!window) { glfwTerminate(); return -1; }
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
 
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) return -1;
glViewport(0, 0, 800, 600);

窗口 resize callback 收到的是 framebuffer pixel size,不应假定永远等于逻辑窗口尺寸;HiDPI 环境两者可能不同。Callback 中以实际宽高更新 glViewport(0,0,w,h),才把 normalized device coordinates 映射到完整 framebuffer。

每帧还需处理 input、绘制、交换与事件:

while (!glfwWindowShouldClose(window)) {
    processInput(window);
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    // draw calls are added in the next chapter
    glfwSwapBuffers(window);
    glfwPollEvents();
}
glfwTerminate();

让 draw commands 写 back buffer,glfwSwapBuffers 再把完成画面呈现。glfwPollEvents 推进窗口与 input 事件;它不是绘制调用,但缺失后窗口会失去响应。

先预测:若只创建 window 却忘记 glfwMakeContextCurrent,GLAD 加载与后续 OpenGL calls 应在哪一步失败?若 resize 后不更新 viewport,画面会继续按旧 framebuffer 区域映射。把这两个故障分别注入,确认日志能指向 context 与 viewport 边界。

画布:先在网页上钉一块板子

要在网页上画图,第一步是有一块能画的地方。这块地方就是 ——HTML 里一个叫 <canvas> 的元素。它本身就是一块钉在网页上的空白板子,规定了「能画的范围有多大」,但单凭它自己,板子上什么都不会出现。

为什么需要它?因为浏览器得知道你的画往哪儿放、画多大。<canvas> 就是这块预留好的地盘。它在整条流程的最前端,是后面一切的落脚点。

下面这张图标出了我们这一章要搭的几个角色,以及它们在整条「从数据到屏幕」的流水线里各自的位置:

画笔:从画布身上拿到 WebGL2 上下文

光有板子还不能画——你得有支「专门画 3D 图形的笔」。在网页里,这支笔就是 :调用 canvas.getContext('webgl2'),浏览器就把一个对象交到你手里,习惯上把它命名为 gl

它是什么?可以把它想成「连通显卡(GPU)的总开关」。之后所有命令——清屏、上传数据、画三角形——全都是对这个 gl 对象发号施令,由它转交给 GPU 去真正执行。

为什么从 canvas 身上拿?因为这支笔天生就和那块板子绑定:你对 gl 下的每条命令,最终都画在它所属的那块 <canvas> 上。在管线里,它紧跟在画布之后——板子有了,握住笔,才能开画。

渲染循环:让重画这件事一直转下去

拿到笔,还差最关键的一步:让画面动起来。这就要靠 ——一段「画一帧 → 等下一次刷新 → 再画一帧」不停重复的代码。

回到电影的类比:电影是一张张静止的胶片飞快放出来的,每一张叫一 。渲染循环就是网页里的「放映机」,它一帧接一帧地驱动你的画笔重画。我们用浏览器提供的 requestAnimationFrame:你把「画一帧」的函数交给它,它会在屏幕下一次刷新之前回头调你一次;你在函数末尾再登记一次,于是就一直转下去了。

为什么要循环、不能画一次就完?因为画面要变——物体在动、颜色在渐变、鼠标在交互。只有反复重画,每次用最新的状态画,眼睛才会看到「动」。它处在管线的最外层:每一帧,循环都会把后面「清屏 → 绘制」整套流程从头跑一遍。下面把「一帧是怎么来的」单步拆开看:

分步1 / 4

① 放映机喊「该画下一帧了」

requestAnimationFrame 在屏幕即将刷新前,回头调用你登记的那个「画一帧」函数。一秒钟通常喊 60 次。

清屏:每帧开头先擦干净画板

上面第 ② 步是这一章的主角。 干的事就是「擦黑板」:在画新内容之前,先用一种颜色把整块画布抹一遍,把上一帧的痕迹彻底盖掉。

它分两步下命令:先用 gl.clearColor(r, g, b, a) 告诉画笔「待会儿要拿哪种颜色来抹」(这只是定好颜料,还没动手);再用 gl.clear(...) 真正动手,把那块存画面的内存——也就是 ——整体填成那个颜色。

为什么每帧都要清?因为画布上一帧的内容不会自己消失。不擦干净就接着画,新旧画面会叠在一起糊成残影。在原书里,这一步对应「把窗口涂成青色」——窗口里还没有任何模型,整个画面就是清屏色铺满。下面的 Demo 就让你亲手调这个清屏色。

动手:给整块画布选个清屏色

下面这块演示区就是一张 <canvas> 画布。它的片段着色器没画任何形状,只是把你选的清屏色铺满整块画布——这正是「清屏」的效果:整面被刷成一种颜色。

猜一猜:把下面的「清屏颜色」调成你喜欢的颜色,整块画布会怎样?是只变一个角,还是整面一起变?动手拖一下颜色块再看。

可交互

实时演示加载中…

默认那抹偏暗的青绿,正是原书里 glClearColor(0.2, 0.3, 0.3, 1.0) 那杯经典的清屏色。你每换一次颜色,相当于改了 gl.clearColor 的参数——整块画布下一帧就被刷成新色。

代码对照:原书的 C++ ↔ 我们的 TypeScript

原书用 C++ 配 GLFW 开一个桌面窗口、用 GLAD 加载函数指针,再开渲染循环清屏。我们在网页里不需要「开窗口」——<canvas> 已经在页面上了,直接从它身上拿上下文即可。先看「拿画笔」这一段:

// 创建窗口 + 拿到 OpenGL 上下文(GLFW 建窗,GLAD 加载函数指针)
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
glfwMakeContextCurrent(window);
gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);

两边目的相同——拿到一个能对 GPU 下命令的上下文对象;只是桌面端要先自己开窗口、加载函数,而网页端浏览器都替你做好了。这里有个第一次就要留意的差异点:

拿到 gl 后,渲染循环里的「清屏」两边几乎一模一样——先定色、再清,区别只在循环靠谁驱动:

// 渲染循环:窗口没被关就一直转
while (!glfwWindowShouldClose(window)) {
  glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 定好清屏色
  glClear(GL_COLOR_BUFFER_BIT);         // 用该色刷满颜色缓冲
  glfwSwapBuffers(window);
  glfwPollEvents();
}

clearColor / clear 两步在两端完全对应。最大的不同是循环:桌面端用 while 自己转、末尾手动交换缓冲;网页端把「画一帧」的函数交给 requestAnimationFrame,由浏览器在每次刷新前回调,节奏跟着屏幕走、切到后台标签页还会自动暂停省电。

容易踩的坑

小结

  • 屏幕动画 = 一帧接一帧飞快重画,像电影放胶片
  • <canvas> 是钉在网页上的画板,规定「往哪画、多大」
  • 从 canvas 拿到的 WebGL2 上下文gl)是连通 GPU 的画笔,所有命令都对它下
  • 渲染循环requestAnimationFrame 驱动,每帧重画,让画面动起来
  • 每帧开头先清屏clearColor 定色 + clear 执行),擦掉上一帧,否则会糊成残影

练习

问题 1: 把上面 Demo 的清屏色控件默认值,从原书的青色 [0.2, 0.3, 0.3] 改成你喜欢的颜色(比如纯紫 [0.49, 0.36, 1.0])。说说改的是哪个 OpenGL 函数的参数。

问题 2: 为什么渲染循环里要「每一帧」都清屏,而不是程序启动时清一次就够了?

问题 3: 用一句话说出 <canvas>、WebGL2 上下文、渲染循环三者在「从数据到屏幕」的管线里各自的位置;再说明桌面端 GLFW/GLAD 分别补上什么职责。

名词解释

本章出现的专业名词,用大白话再讲一遍。

OpenGL 规范

规定图形 API 命令输入、输出与行为的 specification;驱动提供实现,规范本身不负责窗口和输入。

GLFW

跨平台创建窗口、处理输入和建立 OpenGL context 的库。

OpenGL 上下文

保存对象状态和命令环境的实例;桌面端必须先把它设为 calling thread 的 current context。

GLAD

在 current context 建立后,通过平台查询接口加载 OpenGL 驱动函数地址的 loader。

双缓冲

在 back buffer 画完整一帧,再交换到 front buffer 显示,避免用户看到半成品。

画布(canvas)

网页上一个叫 <canvas> 的方块元素,就是一块空白画板,规定了「能画的范围有多大」。光有它还画不出东西,得再配一支「画笔」(上下文)。详见本章「画布」一节。

WebGL2 上下文

从画布身上拿到的一个对象(一般命名 gl),是连通显卡(GPU)的「总开关」兼「画笔」。清屏、上传数据、画三角形等所有命令,都是对它下达、再由它转交 GPU 执行。

渲染循环

一段「画一帧 → 等屏幕下次刷新 → 再画一帧」不停重复的代码,通常用 requestAnimationFrame 驱动。它是网页里的「放映机」,让画面一帧帧动起来。详见本章「渲染循环」一节。

一整张完成的画面。屏幕上的动画,本质是一帧接一帧飞快地播放(一秒通常几十帧),快到眼睛看成了连续的动作,就像电影放胶片。

清屏

在画新一帧之前,用一种颜色把整块画布整体刷一遍,盖掉上一帧留下的内容。两步配套:先 gl.clearColor 定好用哪种颜色,再 gl.clear 真正动手刷。详见本章「清屏」一节。

颜色缓冲

显存里存放「当前这一帧每个像素是什么颜色」的一块内存。屏幕显示的就是它的内容;清屏做的事,就是把这块内存整体重置成一种颜色。

讨论

评论区加载中…