深入理解 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 规范为共同证据源。阅读时先确定规范版本,再查核心能力、限制值和扩展,最后才写平台分支。

三套 API 共享管线模型,但运行环境、着色器版本和能力边界不能互相等同
  • 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、缓冲、纹理、测试状态和帧缓冲,把这些状态组合成一次工作。

从规范到可移植实现

跨平台渲染的核心不是维护三个完全独立的渲染器,而是建立:

  1. 请求上下文:明确需要 WebGL 2、ES 3.x 或桌面 core profile,创建失败要有降级或终止路径。
  2. 读取身份与限制:记录版本、着色语言版本、纹理尺寸、顶点属性数、统一变量块和颜色附件数量等限制。
  3. 枚举扩展:扩展必须先探测再使用;名字相似也不能假定不同 API 上语义完全一致。
  4. 选择资源格式与着色器变体:按能力选择纹理格式、精度限定符、宏和渲染路径。
  5. 验证结果:检查着色器编译、程序链接、帧缓冲完整性和关键错误;在真实目标设备上运行基准场景。

这套流程把“平台兼容”从编译期猜测变成运行时证据。它也是后续跨平台、调试和性能章节的共同主线。

十章与八个规范单元

当前十章覆盖八个可核查单元,章节数量与规范单元不是一一对应:学习地图负责建立依赖,总复习负责故障定位,中间八章承载实现。

  1. 上下文、对象与状态:架构与状态机一章解释对象生命周期、绑定点和 draw call 的隐式输入。
  2. GLSL ES 着色器:着色器语言一章覆盖类型、接口、精度、编译和链接边界。
  3. WebGL 上下文与 WebGL 1/2:WebGL 基础一章覆盖 canvas、上下文丢失、绘图缓冲和浏览器约束。
  4. OpenGL ES 移动运行时:移动端适配一章处理生命周期、EGL/平台表面、带宽和精度。
  5. 帧缓冲与后处理:FBO 一章建立附件完整性、离屏渲染和全屏后处理。
  6. 绘制与渲染优化:优化一章区分 CPU 提交、GPU 执行、带宽和同步瓶颈。
  7. 能力探测与扩展:跨平台一章把版本、限制、格式和扩展收敛为能力表。
  8. 调试与 GPU 分析:调试一章用错误、调试回调、计时查询和帧捕获定位问题。
深入理解 OpenGL/WebGL/OpenGL ES · 学习地图OpenGL(桌面全功能)OpenGL ES(移动精简)WebGL(浏览器)图形管线阶段顶点输入顶点着色器图元装配光栅化片元着色器逐片元测试帧缓冲橙色为可编程着色器阶段十章脉络1 学习地图2 架构状态机3 GLSL ES4 WebGL 基础5 OpenGL ES6 渲染优化7 FBO 后处理8 跨平台9 调试性能10 总复习核心思想:状态机 + 图形管线 + 可编程着色器设状态 → 喂数据 → 发绘制;三 API 同源,差异在子集与扩展
三 API 同源关系、图形管线七阶段与十章脉络总览

分步建立第一条绘制链

先预测:如果程序成功链接、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、优化、跨平台与调试,总复习负责把故障重新映射到这条链路。

讨论

评论区加载中…