深入理解 OpenGL WebGL OpenGL ES 全书学习地图
以 Khronos 官方规范为基线,从上下文、状态、着色器到跨平台诊断的十章学习路径
直觉引入
把 GPU 想成一座必须先配置再开工的流水线工厂。顶点缓冲是原料仓,着色器程序是工艺卡,纹理和帧缓冲是输入输出设备;一次绘制调用并不携带全部配置,而是读取上下文里当时绑定的对象和启用的状态。很多“没有报错但屏幕全黑”的问题,正是因为工厂开工时拿到的状态不是程序员以为的状态。
OpenGL、OpenGL ES 和 WebGL 共享这套对象、状态与管线模型,但它们不是可以逐函数替换的三个名字。桌面 OpenGL 由版本与 profile 定义能力;OpenGL ES 面向嵌入式设备收紧接口与资源假设;WebGL 以 OpenGL ES 的语义为基础,又为浏览器安全、可移植性和初始化行为增加限制。真正可迁移的是渲染模型,不能直接假设全部 API、扩展和着色器源码都可迁移。
权威规范基线
本路径不是把三套 API 混成一份“常用函数表”,而是以 Khronos 注册表中的 OpenGL、OpenGL ES、GLSL ES 与 WebGL 规范为共同证据源。阅读时先确定规范版本,再查核心能力、限制值和扩展,最后才写平台分支。
OpenGL 4.6
- 运行位置
- 桌面驱动与窗口系统
- 着色语言
- 桌面 GLSL
- 能力边界
- 能力受版本、profile 与扩展共同约束
OpenGL ES 3.2
- 运行位置
- 移动与嵌入式运行时
- 着色语言
- GLSL ES
- 能力边界
- 面向功耗、带宽与设备差异收紧能力
WebGL 2.0
- 运行位置
- 浏览器安全上下文
- 着色语言
- GLSL ES 3.00
- 能力边界
- 以 ES 3.0 为基线,并增加安全与可移植性限制
- OpenGL 4.6:桌面核心规范的当前终版。实际上下文还要区分 core/compatibility profile,并检查驱动暴露的扩展。
- OpenGL ES 3.2:嵌入式规范主线。它与桌面 OpenGL 共享大量对象和命令语义,但不是桌面规范的机械子集。
- WebGL 1.0:以 OpenGL ES 2.0 为起点,经浏览器规范重新定义可见行为与限制。
- WebGL 2.0:以 OpenGL ES 3.0 为起点,采用 GLSL ES 3.00,并继续施加浏览器安全和可移植性约束。
这里的“以某版本为起点”比“约等于”更准确。实现必须遵守 WebGL 规范列出的差异,浏览器还可能因硬件、驱动或安全策略不暴露某项扩展。因此,跨平台代码应以共同能力集合为基线,而不是以开发机上恰好成功的调用为基线。
共同的执行模型
1. 上下文拥有状态
渲染命令总是在一个上下文中执行。上下文记录当前程序、当前顶点数组对象、各绑定点上的缓冲和纹理、混合/深度/模板开关、视口以及清屏值等状态。OpenGL 被称为,不是说内部只有一个巨大变量,而是强调命令行为依赖隐式的当前状态。
对象名本身不等于对象内容。调用 bind 把对象接到某个目标或绑定点,随后的 bufferData、vertexAttribPointer 或 texImage2D 才会修改当前绑定对象。调试时必须同时问:“对象是否创建成功?”和“命令执行时绑定的是不是这个对象?”
2. 数据经过图形管线
从顶点获取开始,经过顶点着色、图元装配、裁剪、光栅化、片元着色和逐片元测试,最后更新颜色、深度或模板附件。桌面 OpenGL 还能暴露细分与几何等阶段;WebGL 2 的核心可编程阶段仍是顶点和片元。
不能脱离接口契约独立存在。顶点着色器输出必须与片元着色器输入按名称、类型和插值规则匹配;应用侧顶点属性的位置、分量数、类型、步长与偏移也必须与着色器输入一致。链接成功只证明着色器阶段之间能组成程序,不证明顶点布局和绘制状态正确。
3. 绘制写入目标
默认帧缓冲由窗口系统或浏览器提供。WebGL 中它对应 canvas 的;离屏渲染则由应用创建 framebuffer 并挂接纹理或 renderbuffer。drawArrays/drawElements 只是提交边界:它读取当前程序、VAO、缓冲、纹理、测试状态和帧缓冲,把这些状态组合成一次工作。
从规范到可移植实现
跨平台渲染的核心不是维护三个完全独立的渲染器,而是建立:
- 请求上下文:明确需要 WebGL 2、ES 3.x 或桌面 core profile,创建失败要有降级或终止路径。
- 读取身份与限制:记录版本、着色语言版本、纹理尺寸、顶点属性数、统一变量块和颜色附件数量等限制。
- 枚举扩展:扩展必须先探测再使用;名字相似也不能假定不同 API 上语义完全一致。
- 选择资源格式与着色器变体:按能力选择纹理格式、精度限定符、宏和渲染路径。
- 验证结果:检查着色器编译、程序链接、帧缓冲完整性和关键错误;在真实目标设备上运行基准场景。
这套流程把“平台兼容”从编译期猜测变成运行时证据。它也是后续跨平台、调试和性能章节的共同主线。
十章与八个规范单元
当前十章覆盖八个可核查单元,章节数量与规范单元不是一一对应:学习地图负责建立依赖,总复习负责故障定位,中间八章承载实现。
- 上下文、对象与状态:架构与状态机一章解释对象生命周期、绑定点和 draw call 的隐式输入。
- GLSL ES 着色器:着色器语言一章覆盖类型、接口、精度、编译和链接边界。
- WebGL 上下文与 WebGL 1/2:WebGL 基础一章覆盖 canvas、上下文丢失、绘图缓冲和浏览器约束。
- OpenGL ES 移动运行时:移动端适配一章处理生命周期、EGL/平台表面、带宽和精度。
- 帧缓冲与后处理:FBO 一章建立附件完整性、离屏渲染和全屏后处理。
- 绘制与渲染优化:优化一章区分 CPU 提交、GPU 执行、带宽和同步瓶颈。
- 能力探测与扩展:跨平台一章把版本、限制、格式和扩展收敛为能力表。
- 调试与 GPU 分析:调试一章用错误、调试回调、计时查询和帧捕获定位问题。
分步建立第一条绘制链
先预测:如果程序成功链接、VBO 也上传了数据,但没有绑定包含属性格式的 VAO,drawArrays 会稳定画出三角形吗?带着这个问题观察三步中的“当前状态”如何逐层完整。
可运行的 WebGL2 最小程序
先准备一个有稳定尺寸的绘图目标。浏览器会把 drawing buffer 的结果合成到这个 canvas。
<canvas id="gl-canvas" width="640" height="360"></canvas>
<script type="module" src="./triangle.js"></script>triangle.js 包含完整的失败检查。把两个文件放在同一目录并通过本地 HTTP 服务打开,应该看到深色背景上的橙色三角形。
const canvas = document.querySelector("#gl-canvas");
if (!(canvas instanceof HTMLCanvasElement)) {
throw new Error("找不到 #gl-canvas");
}
const gl = canvas.getContext("webgl2");
if (!gl) throw new Error("当前环境不支持 WebGL 2");
function compileShader(type, source) {
const shader = gl.createShader(type);
if (!shader) throw new Error("无法创建着色器");
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader) ?? "未知编译错误";
gl.deleteShader(shader);
throw new Error(log);
}
return shader;
}
function createProgram(vertexSource, fragmentSource) {
const program = gl.createProgram();
if (!program) throw new Error("无法创建程序");
const vertexShader = compileShader(gl.VERTEX_SHADER, vertexSource);
const fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSource);
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program) ?? "未知链接错误";
gl.deleteProgram(program);
throw new Error(log);
}
return program;
}
const program = createProgram(
`#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}`,
`#version 300 es
precision highp float;
out vec4 outColor;
void main() {
outColor = vec4(0.95, 0.45, 0.12, 1.0);
}`,
);
const vertices = new Float32Array([
0.0, 0.75,
-0.75, -0.65,
0.75, -0.65,
]);
const vao = gl.createVertexArray();
const vbo = gl.createBuffer();
if (!vao || !vbo) throw new Error("无法创建 VAO/VBO");
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, "a_position");
if (positionLocation < 0) throw new Error("a_position 未激活");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0.04, 0.05, 0.08, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);这段程序体现了状态依赖:vertexAttribPointer 读取当时绑定的 ARRAY_BUFFER,并把属性格式记录到当前 VAO;drawArrays 又读取当前程序、VAO、视口和绘图目标。交换 bind 的顺序不会总是产生语法错误,却可能改变最终被记录或读取的对象。
能力探测与验收
不要用 UA 字符串猜 GPU 能力。下面的报告可以作为启动日志的一部分,在桌面浏览器、Android WebView 和不同 GPU 上对比。
function capabilityReport(gl) {
const limits = {
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
maxDrawBuffers: gl.getParameter(gl.MAX_DRAW_BUFFERS),
maxColorAttachments: gl.getParameter(gl.MAX_COLOR_ATTACHMENTS),
};
return {
version: gl.getParameter(gl.VERSION),
shadingLanguage: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
renderer: gl.getParameter(gl.RENDERER),
limits,
extensions: gl.getSupportedExtensions() ?? [],
};
}
console.table(capabilityReport(gl).limits);
console.log(capabilityReport(gl));验收时至少保留以下证据:
- canvas 中能看见三角形,调整 canvas 尺寸后同步更新 viewport;
- 故意删掉片元着色器的分号,能够从编译日志看到具体错误;
- 把 a_position 改成未使用输入,程序能在 location 为 -1 时主动失败;
- 输出版本、着色语言、四个限制值与扩展列表,不用“我的机器支持”代替证据;
- 切换到不支持 WebGL2 的环境时走明确失败路径,而不是继续访问 null 上下文。
常见误区
练习
小结
- 三套 API 共享对象、状态和图形管线模型,但规范版本、运行环境和能力边界不同。
- 上下文状态是 draw call 的隐式输入;程序、VAO/VBO、纹理、测试状态和帧缓冲必须在提交时共同正确。
- 跨平台实现应建立能力契约:请求上下文、读取限制、探测扩展、选择变体、验证结果。
- 最小三角形不是玩具,而是隔离上下文、着色器、顶点接口和绘图目标问题的基准场景。
- 后续八个实现专题分别深入状态、GLSL ES、WebGL、OpenGL ES、FBO、优化、跨平台与调试,总复习负责把故障重新映射到这条链路。