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

这篇教程真正解决的问题
这篇文章名义上是 WebGL 教程,实际更像一份“浏览器图形学最小地图”。作者从一个红色点开始,一路讲到 3D 相机、索引顶点、立方体、光照、模型矩阵、多个物体、层级对象和 WebGL 2.0 差异。
它的价值不在于告诉你某个 API 怎么拼,而在于建立三层心智模型:
- 数据层:JavaScript 准备顶点、颜色、纹理坐标、法线、索引等数据,并通过 buffer/uniform/attribute 传给 GPU。
- 计算层:vertex shader 负责处理顶点位置,fragment shader 负责决定每个像素颜色。
- 空间层:矩阵把模型、相机、投影、旋转、缩放、层级关系统一成可组合的变换。

WebGL 的底层逻辑:你不是在“画图”,而是在喂管线
WebGL 基于 OpenGL ES,通过 GLSL 写 shader。它不会替你创建相机、绘制多边形、管理场景,也没有类似 Canvas 2D 那样的高级绘制函数。你要明确告诉它:
- 有哪些顶点;
- 顶点如何连接成点、线、三角形;
- 每个顶点携带哪些属性;
- 如何把顶点变换到裁剪空间;
- 每个像素最终是什么颜色;
- 深度、纹理、光照、透明度如何参与计算。
这也是很多前端第一次学 WebGL 时觉得“反人类”的原因:WebGL 并不是一个画布 API,而是一条 GPU 管线的 JavaScript 入口。
数学不是附录,而是 WebGL 的主语
作者把坐标系、三角函数、向量、点积、叉积、法线这些基础数学放在前面,是非常正确的安排。WebGL 里的“效果”大多不是 API 魔法,而是空间计算的结果。

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



2D 部分:从一个点到可插值的三角形
教程的 2D 部分很适合作为 WebGL 入门路径:先画一个点,再引入 attribute、uniform、buffer、varying、drawArrays 和绘制模式。
它强调了一个重要事实:WebGL 的世界里,三角形是一切复杂图形的基本单位。点和线只是调试或特殊场景,真正构建面和体,最终都要落到三角形。

这里有几个概念特别值得记住:
attribute:每个顶点不同的数据,比如位置、颜色、纹理坐标。uniform:一次 draw call 中对所有顶点/片元相同的数据,比如整体颜色、矩阵、光源位置。varying:vertex shader 传给 fragment shader 的插值数据。buffer:JavaScript 侧把批量顶点数据送进 GPU 的方式。drawArrays:按指定模式解释连续顶点。
varying 的插值尤其关键。你给三角形三个顶点不同颜色,片元着色器会自动拿到中间像素的插值结果。这就是 WebGL 很多“平滑过渡”效果的基础。

变换:矩阵把移动、旋转、缩放统一了
WebGL 中移动、旋转、缩放顶点,最朴素的做法是逐个改坐标。但这很快会失控。教程引入了更通用的方式:用 4x4 矩阵处理齐次坐标。
这一步是从“画几个图形”进入“做一个图形系统”的分水岭。
矩阵的价值在于:
- 平移、旋转、缩放可以统一表达;
- 多个变换可以按顺序组合;
- 同一套模型数据可以复用,只换 model matrix;
- 相机和投影也可以继续放进同一套乘法链条;
- 层级对象可以通过父子矩阵继承实现。

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

这一节对前端工程师很有启发:图片不是“贴上去”的,而是每个片元根据插值得到的坐标去查一张图。于是 wrap、clamp、mirror、filter、mipmap 这些行为都变成了“越界和缩放时如何采样”的策略问题。
3D 部分:所谓相机,本质还是矩阵
文章对 3D 的解释很克制,也很准确:WebGL 不会天然理解“3D 场景”。你给它 3D 顶点,它也只是执行 shader。所谓透视、相机位置、视野角、近远裁剪面,最终都要变成矩阵,乘到顶点上。

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

索引顶点:减少重复,也让模型更接近真实资产格式
进入 3D 后,重复声明三角形顶点会很快膨胀。索引顶点的思路是:顶点数据只存一份,再用 index buffer 描述三角形如何引用这些顶点。
这不只是性能优化,也更接近真实 3D 模型文件的组织方式。一个 cube、sphere 或复杂模型,通常都会拆成顶点、法线、UV、索引等结构。

光照:不要把 lighting 和 shading 混在一起
教程对 lighting 和 shading 的区分很值得保留:
- Lighting 是物理世界里光如何影响物体。
- Shading 是计算机图形中如何根据光照把像素画出来。
一个没有 shading 的 3D 物体会非常扁平。哪怕只是给不同面不同亮度,大脑也会立刻把它解释成立体形状。

文章依次讲了 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:从“方向光”升级为有位置、有范围、有衰减或锥形区域的光。



模型矩阵、法线矩阵和多物体绘制
当你只旋转相机时,看起来像物体在动,但这不是模型自身的变换。真正要变换某个物体,需要引入 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 的最小形态
层级对象这一节用机械臂解释矩阵继承:手掌继承手臂的矩阵,再叠加自己的旋转和平移;子节点不是从世界坐标重新计算,而是在父节点变换基础上继续变换。

这就是 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 语法,而是你是否愿意把“画面”拆回数据、矩阵和管线。