本章不从复杂的 3D 物体开始,而是完成一个最小可运行的 Demo:在浏览器中创建一个 WebGL 画布,让画布自动适配窗口尺寸,每一帧用 GPU 清空画布,并让背景色缓慢变化。
该 Demo 的效果并不复杂,但足以呈现 WebGL 的基本工作方式:JavaScript 不直接修改屏幕像素,而是通过 gl 这个 WebGL 上下文设置 GPU 状态、发出绘制命令,再由浏览器把结果显示在 canvas 上。
从页面结构开始。canvas 是浏览器为 WebGL 提供画面输出的位置,后续使用的 gl 对象会从这个元素上创建出来。
<canvas id="webgl-canvas"></canvas>
为了让 Demo 更接近真实页面,需要让画布铺满窗口:
html,
body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #070b12;
}
#webgl-canvas {
display: block;
width: 100vw;
height: 100vh;
}
接着在 JavaScript 中取得 WebGL 上下文:
const canvas = document.querySelector('#webgl-canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
throw new Error('当前浏览器或设备不支持 WebGL');
}
gl 是 JavaScript 与 GPU 通信的主要对象。后续使用的 gl.viewport、gl.clearColor、gl.clear,都会从该对象调用。
需要注意,getContext('webgl') 得到的是 WebGL 1 上下文。很多浏览器也支持 webgl2,但入门时使用 webgl 更合适,因为它能覆盖 WebGL 的基础数据流和核心概念。
取得 gl 后,可以先与 Canvas 2D 的写法做对比。Canvas 2D 中常见的绘制代码如下:
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
这类 API 更接近“在画布上直接绘制形状”的模型。WebGL 的思路不同:它是一套向 GPU 提交状态和命令的接口。通常需要先准备数据和状态,再调用对应命令,让 GPU 按当前状态生成画面。
绘制完整几何体时,数据会经历更长的路径:JavaScript 会准备更多数据,WebGL 会把这些数据交给 GPU 处理,最后再把结果写入画布。
本章暂不展开 shader,也不准备几何图形数据,只使用其中最小的一段:JavaScript 数值 -> WebGL 渲染状态 -> 清屏命令 -> canvas 画面。先明确 WebGL 的入口、画布尺寸、视口和清屏状态,下一章再把 shader 和顶点数据接入这条路径。
理解 WebGL 的命令方式之后,下一步是正确处理画布本身。canvas 有两套尺寸:一套是 CSS 尺寸,决定它在页面上的显示大小;另一套是绘图缓冲区尺寸,决定 WebGL 实际绘制的像素范围。
如果只写 CSS 的 width: 100vw; height: 100vh;,画布会铺满窗口,但它内部的绘图缓冲区可能仍是默认的 300 x 150。这种情况下,画面容易被拉伸,在高清屏上也会变得模糊。
可以通过一个函数把绘图缓冲区尺寸同步到元素显示尺寸:
function resizeCanvasToDisplaySize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.floor(canvas.clientWidth * dpr);
const height = Math.floor(canvas.clientHeight * dpr);
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
gl.viewport(0, 0, canvas.width, canvas.height);
}
canvas.width 和 canvas.height 改变的是 WebGL 真实绘制的像素区域。gl.viewport(0, 0, canvas.width, canvas.height) 则告诉 GPU:接下来生成的绘制结果,需要映射到画布中从左下角开始、宽高为当前绘图缓冲区的区域。
这一步经常影响 WebGL 画面的空白、变形和模糊问题。只改 CSS 尺寸还不够,WebGL 还需要知道自己应该绘制到多大的像素范围里。
完成画布尺寸和视口设置后,可以通过一次固定颜色的清屏让 GPU 改变画面:
resizeCanvasToDisplaySize();
gl.clearColor(0.03, 0.05, 0.08, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.clearColor 不是立刻绘制,它只是设置“下一次清空颜色缓冲区时使用什么颜色”。WebGL 里的颜色通常使用 0.0 到 1.0 的浮点数,而不是 CSS 里常见的 0 到 255。
gl.clear(gl.COLOR_BUFFER_BIT) 才是真正发出清屏命令。COLOR_BUFFER_BIT 表示清空颜色缓冲区,也就是当前画布最终显示的颜色数据。执行完这句后,整个页面会变成一块深蓝黑色。
这两行代码展示了 WebGL 一个重要特点:很多 API 都是在设置状态,真正改变画面的通常是 clear 或后续绘制命令。
为了观察 WebGL 命令在每一帧重新执行的过程,可以用 requestAnimationFrame 做一个动态背景色。每帧根据时间算出一组颜色,再交给 clearColor:
function render(time) {
resizeCanvasToDisplaySize();
const seconds = time * 0.001;
const red = 0.04 + Math.sin(seconds * 0.8) * 0.02;
const green = 0.06 + Math.sin(seconds * 1.1 + 1.2) * 0.03;
const blue = 0.12 + Math.sin(seconds * 0.7 + 2.4) * 0.04;
gl.clearColor(red, green, blue, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
此时画布会在深色调之间缓慢变化。虽然这里的数据路径很短,但已经能够体现 WebGL 的基本工作方式:
- JavaScript 根据当前时间算出
red、green、blue。 gl.clearColor把这组颜色写入 WebGL 的清屏状态。gl.clear(gl.COLOR_BUFFER_BIT)命令 GPU 使用当前清屏颜色重置颜色缓冲区。- 浏览器把颜色缓冲区显示到
canvas上。
后续绘制三角形、立方体和纹理时,这条路径会变长,但基本节奏不变:先准备数据和状态,再发出命令。本章先通过最小的清屏 Demo 建立这条路径的基本认识。
本章实际用到的 API 较少,可以整理为下表:
| API 类型 | 常见 API | 负责什么 |
|---|---|---|
| 获取上下文 | canvas.getContext('webgl') | 创建 JavaScript 操作 GPU 的入口 |
| 尺寸与视口 | gl.viewport | 告诉 GPU 把结果绘制到画布的哪个像素区域 |
| 清屏颜色 | gl.clearColor | 设置下一次清空颜色缓冲区时使用的颜色 |
| 清屏命令 | gl.clear(gl.COLOR_BUFFER_BIT) | 命令 GPU 用当前清屏颜色重置画布颜色数据 |
同样是让画面出现变化,WebGL 往往比 Canvas 2D 需要更多准备步骤。
原因是 WebGL 要让 GPU 高效工作。GPU 擅长一次处理大量数据,但它需要提前明确画布尺寸、视口范围、当前渲染状态,以及最后由哪条命令触发 GPU 工作。
这些准备工作相对繁琐,但它们换来的是 GPU 的并行计算能力。一个 3D 场景里可能有几万个顶点、几十万甚至上百万个像素,WebGL 的价值就在于把这些工作交给 GPU,而不是让 JavaScript 在主线程里逐个像素计算。
如果这个 Demo 没有按预期显示,可以按下面的顺序排查:
gl是null:浏览器、设备或当前环境不支持 WebGL,需要给出降级提示。- 画面模糊:只设置了 CSS 尺寸,没有同步
canvas.width和canvas.height。 - 画面尺寸不对:窗口变化后没有重新调用
gl.viewport。 - 颜色不符合预期:
clearColor使用0.0到1.0,不是0到255。 - 调用了
clearColor但画面没变:只设置了状态,没有调用gl.clear发出清屏命令。
后续遇到 WebGL 空白页时,也可以先按这个顺序排查:上下文是否创建成功、画布尺寸是否正确、视口是否更新、是否调用了绘制命令。开始编写 shader 之后,再继续补充 shader 相关排查项。
小结
canvas.getContext('webgl')会创建 WebGL 上下文,后续操作都通过gl发给 GPU。canvas.width、canvas.height决定 WebGL 实际绘制的像素尺寸,CSS 尺寸只决定页面上的显示大小。gl.viewport告诉 WebGL 把绘制结果映射到画布的哪个区域,窗口尺寸变化后也要同步更新。gl.clearColor负责设置清屏颜色,gl.clear(gl.COLOR_BUFFER_BIT)才会真正发出清屏命令。
更重要的是,本章已经呈现 WebGL 的基本节奏:JavaScript 准备数据或状态,GPU 按当前状态执行命令,并把结果写回画布。下一章会在这块会变色的画布上加入 shader 和顶点数据,让 GPU 画出第一个真正的三角形。
源码地址:TODO(后续补充 GitHub 链接)