WebGLGraphicsFrontendJavaScript

WebGL guide — 从零理解浏览器里的 GPU 渲染管线

Maxime Euzière··原文链接
收录于 2026/6/13 23:20:54

一句话

WebGL 不是“浏览器里的 3D API”,而是一套让 JavaScript 把数据、矩阵和 shader 送进 GPU 的低层渲染管线;一旦理解这条管线,2D、3D、纹理、相机和光照其实是同一套模型的连续展开。

WebGL 程序从 JavaScript 到 canvas 的整体工作流

这篇教程真正解决的问题

这篇文章名义上是 WebGL 教程,实际更像一份“浏览器图形学最小地图”。作者从一个红色点开始,一路讲到 3D 相机、索引顶点、立方体、光照、模型矩阵、多个物体、层级对象和 WebGL 2.0 差异。

它的价值不在于告诉你某个 API 怎么拼,而在于建立三层心智模型:

  • 数据层:JavaScript 准备顶点、颜色、纹理坐标、法线、索引等数据,并通过 buffer/uniform/attribute 传给 GPU。
  • 计算层:vertex shader 负责处理顶点位置,fragment shader 负责决定每个像素颜色。
  • 空间层:矩阵把模型、相机、投影、旋转、缩放、层级关系统一成可组合的变换。

JavaScript、vertex shader、fragment shader 与 canvas 的分工

WebGL 的底层逻辑:你不是在“画图”,而是在喂管线

WebGL 基于 OpenGL ES,通过 GLSL 写 shader。它不会替你创建相机、绘制多边形、管理场景,也没有类似 Canvas 2D 那样的高级绘制函数。你要明确告诉它:

  • 有哪些顶点;
  • 顶点如何连接成点、线、三角形;
  • 每个顶点携带哪些属性;
  • 如何把顶点变换到裁剪空间;
  • 每个像素最终是什么颜色;
  • 深度、纹理、光照、透明度如何参与计算。

这也是很多前端第一次学 WebGL 时觉得“反人类”的原因:WebGL 并不是一个画布 API,而是一条 GPU 管线的 JavaScript 入口。

数学不是附录,而是 WebGL 的主语

作者把坐标系、三角函数、向量、点积、叉积、法线这些基础数学放在前面,是非常正确的安排。WebGL 里的“效果”大多不是 API 魔法,而是空间计算的结果。

WebGL 坐标轴与 3D 空间方向

几个关键点:

  • 坐标系:2D 中 X/Y 通常在 [-1, 1],3D 中再引入 Z;空间理解错误,后面的相机和法线都会错。
  • 三角函数:旋转、圆周运动、角度与弧度转换都依赖 sin/cos。
  • 向量:位置、方向、速度、法线、光照方向都可以统一表示为向量。
  • 点积:可以衡量两个方向的接近程度,是 diffuse light 的核心。
  • 叉积:可以从两个边向量求出垂直方向,是计算三角形法线的基础。

三角函数与圆周运动的关系

向量、单位向量、加减和长度

叉积用于得到垂直于平面的方向

2D 部分:从一个点到可插值的三角形

教程的 2D 部分很适合作为 WebGL 入门路径:先画一个点,再引入 attribute、uniform、buffer、varying、drawArrays 和绘制模式。

它强调了一个重要事实:WebGL 的世界里,三角形是一切复杂图形的基本单位。点和线只是调试或特殊场景,真正构建面和体,最终都要落到三角形。

WebGL 支持的点、线、三角形绘制模式

这里有几个概念特别值得记住:

  • attribute:每个顶点不同的数据,比如位置、颜色、纹理坐标。
  • uniform:一次 draw call 中对所有顶点/片元相同的数据,比如整体颜色、矩阵、光源位置。
  • varying:vertex shader 传给 fragment shader 的插值数据。
  • buffer:JavaScript 侧把批量顶点数据送进 GPU 的方式。
  • drawArrays:按指定模式解释连续顶点。

varying 的插值尤其关键。你给三角形三个顶点不同颜色,片元着色器会自动拿到中间像素的插值结果。这就是 WebGL 很多“平滑过渡”效果的基础。

顶点属性经过 varying 在三角形内部自动插值

变换:矩阵把移动、旋转、缩放统一了

WebGL 中移动、旋转、缩放顶点,最朴素的做法是逐个改坐标。但这很快会失控。教程引入了更通用的方式:用 4x4 矩阵处理齐次坐标。

这一步是从“画几个图形”进入“做一个图形系统”的分水岭。

矩阵的价值在于:

  • 平移、旋转、缩放可以统一表达;
  • 多个变换可以按顺序组合;
  • 同一套模型数据可以复用,只换 model matrix;
  • 相机和投影也可以继续放进同一套乘法链条;
  • 层级对象可以通过父子矩阵继承实现。

围绕不同 pivot 旋转会得到完全不同的结果

纹理:把图片坐标变成像素采样问题

纹理部分的核心是 UV 坐标。无论图片真实尺寸是多少,WebGL 都把纹理坐标抽象成 [0, 1] 范围内的 U/V 坐标。顶点携带纹理坐标,fragment shader 根据插值后的 UV 去 sampler2D 里采样。

纹理坐标和三角形顶点之间的映射关系

这一节对前端工程师很有启发:图片不是“贴上去”的,而是每个片元根据插值得到的坐标去查一张图。于是 wrap、clamp、mirror、filter、mipmap 这些行为都变成了“越界和缩放时如何采样”的策略问题。

3D 部分:所谓相机,本质还是矩阵

文章对 3D 的解释很克制,也很准确:WebGL 不会天然理解“3D 场景”。你给它 3D 顶点,它也只是执行 shader。所谓透视、相机位置、视野角、近远裁剪面,最终都要变成矩阵,乘到顶点上。

透视相机的 frustum / clipping volume

透视投影和正交投影的区别也很直观:

  • 透视投影:远处物体更小,符合人眼/相机直觉。
  • 正交投影:远近不影响大小,更适合工程图、编辑器、等距视角或 UI 式 3D。

正交投影不产生近大远小效果

索引顶点:减少重复,也让模型更接近真实资产格式

进入 3D 后,重复声明三角形顶点会很快膨胀。索引顶点的思路是:顶点数据只存一份,再用 index buffer 描述三角形如何引用这些顶点。

这不只是性能优化,也更接近真实 3D 模型文件的组织方式。一个 cube、sphere 或复杂模型,通常都会拆成顶点、法线、UV、索引等结构。

不同基础 3D 形体都可以拆成顶点和三角形索引

光照:不要把 lighting 和 shading 混在一起

教程对 lighting 和 shading 的区分很值得保留:

  • Lighting 是物理世界里光如何影响物体。
  • Shading 是计算机图形中如何根据光照把像素画出来。

一个没有 shading 的 3D 物体会非常扁平。哪怕只是给不同面不同亮度,大脑也会立刻把它解释成立体形状。

不同 shading 方式会显著改变立体感

文章依次讲了 diffuse light、ambient light、point light、spot light、specular light、soft shading 和 emissive light。最关键的是 diffuse 和 specular:

  • Diffuse:看表面法线和光线方向的夹角,常用 max(dot(lightDirection, normal), 0.0)
  • Specular:看观察方向、反射方向和光线方向,用来模拟高光。
  • Ambient:给全局补一点基础亮度,避免背光面全黑。
  • Point/Spot:从“方向光”升级为有位置、有范围、有衰减或锥形区域的光。

Diffuse light 的强度取决于光线方向和表面法线夹角

Specular light 用来模拟高光反射

Soft shading 让物体表面过渡更自然

模型矩阵、法线矩阵和多物体绘制

当你只旋转相机时,看起来像物体在动,但这不是模型自身的变换。真正要变换某个物体,需要引入 model matrix。

一个标准的 3D 渲染链条会变成:

最终位置 = projection/view/camera matrix × model matrix × vertex position

但光照里还要处理法线。模型被缩放、旋转后,法线方向也必须更新;常见做法是传入 model matrix 的 inverse transpose,作为 normal matrix 使用。

这部分是很多入门教程会跳过、但真实项目一定会撞上的问题:只让顶点位置动了,法线没动,光照就会错。

多物体绘制则进一步说明:同一份 cube 顶点数据可以复用多次,每次只更新 model matrix、MVP matrix 和 normal matrix。这就是从“画一个 demo”走向“组织场景”的开始。

层级对象:scene graph 的最小形态

层级对象这一节用机械臂解释矩阵继承:手掌继承手臂的矩阵,再叠加自己的旋转和平移;子节点不是从世界坐标重新计算,而是在父节点变换基础上继续变换。

多个 cuboid 通过矩阵继承形成层级对象

这就是 scene graph 的雏形。理解它之后,再看 Three.js、Babylon.js、Unity 或任何 3D 引擎里的父子节点、local transform、world transform,就不会觉得神秘。

调试:WebGL 难,不只是因为 API 低层

文章最后列出的 WebGL 常见错误很实用:

  • GLSL 少分号;
  • float 写成 int,比如 1 而不是 1.0
  • uniform/varying 当作可写变量;
  • shader 里使用递归或非常量循环边界;
  • drawArrays / drawElements 的 count 参数写错;
  • index buffer 的 JS 类型和 WebGL 类型不匹配;
  • attribute/uniform 传入数据长度不对;
  • 相机方向、FOV、光源位置、法线方向、alpha、点大小等导致“什么都看不见”。

WebGL 的调试难点在于:错误可能出现在 JS、GLSL、数据格式、矩阵顺序、GPU 状态机、资源加载或浏览器实现差异里。它不是单点 bug,而是管线 bug。

WebGL 2.0:更现代,但不一定更适合教学

作者没有把 WebGL 2.0 作为主线,理由是当时支持率和语法变化会增加学习成本。WebGL 2.0 的改动包括:

  • canvas.getContext('webgl2')
  • shader 顶部要写 #version 300 es
  • attribute 改为 in
  • varying 在 vertex shader 中是 out,fragment shader 中是 in
  • gl_FragColor 不再存在,要自己声明输出变量;
  • texture2D / textureCube 改成 texture
  • 新增更多类型、矩阵函数、纹理能力和默认扩展。

这部分的启发是:学习图形学时,最好先建立 WebGL 1.0 的底层模型,再迁移到 WebGL 2.0 或高级框架。否则很容易把语法升级误认为理解升级。

我的判断

这篇文章的最大价值,是它没有把 WebGL 包装成“几个 API 就能做 3D”的速成课,而是老老实实把底层渲染管线摊开:顶点、shader、buffer、矩阵、纹理、相机、法线、光照、场景层级,一个都绕不过去。

对前端工程师来说,它的意义不只是“学会写 WebGL”。更重要的是补上浏览器图形栈的底层直觉:

  • 为什么 CSS transform、Canvas、WebGL、Three.js 本质上都绕不开矩阵?
  • 为什么 GPU 擅长批量并行,而不是逐像素 JS 操作?
  • 为什么框架能省代码,但不能替你理解空间、数据和状态?
  • 为什么一个“看不见”的 3D bug,可能来自相机、法线、深度、alpha 或 buffer 任意一环?

如果只是做业务前端,不一定要手写 WebGL;但如果你的工作涉及可视化、低代码画布、图形编辑器、WebGPU、地图、3D 展示、动画性能或复杂交互,这类底层知识会显著提高判断力。

一句可带走的话:WebGL 的门槛不是 shader 语法,而是你是否愿意把“画面”拆回数据、矩阵和管线。