Skip to content
获取 WebGL 上下文
WebGL 不依赖第三方库,浏览器原生支持,入口是一个 <canvas> 元素。先拿到 canvas,再调用 getContext('webgl')。这行代码会让浏览器创建一个 WebGLRenderingContext 对象,所有后续操作——清屏、编译着色器、向 GPU 提交数据——都通过这个对象完成。
javascript
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('当前环境不支持 WebGL');
}获取上下文之后通常立即做两件事:设置视口、设置清屏颜色。
gl.viewport(0, 0, canvas.width, canvas.height) 告诉 WebGL 渲染结果要铺满整个 canvas 的像素区域。前两个参数是左下角坐标,后两个是宽高,单位都是屏幕像素。
gl.clearColor(0.0, 0.0, 0.0, 1.0) 设置一个 RGBA 颜色,四个分量取值 0 到 1。这个方法只是“记住”了一个状态,并不会立刻让屏幕变色。真正用颜色填充屏幕的是 gl.clear(gl.COLOR_BUFFER_BIT),它把当前记住的清屏颜色刷到颜色缓冲区。
javascript
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 黑色
gl.clear(gl.COLOR_BUFFER_BIT);这三行跑完,canvas 区域变纯黑。此时还没有任何图形,但渲染环境已经就绪。
裁剪空间
WebGL 不使用像素坐标,它用裁剪空间坐标(clip space)。裁剪空间是一个中心在原点的立方体,x、y、z 三个轴的范围都是 -1 到 1。无论 canvas 实际是 800×600 还是 400×300,在这个空间里,左边缘永远是 -1,右边缘永远是 1,下边缘是 -1,上边缘是 1。这个坐标系也叫 NDC(Normalized Device Coordinates)。
一个顶点要被画出来,它的 x、y、z 分量必须在 -1 到 1 之间,超出范围的部分会被 GPU 裁剪掉,不会出现在屏幕上。最终显示时,GPU 会把 -1 到 1 的坐标自动映射到 viewport 设定的像素区域——这个映射对开发者透明,只需要在裁剪空间里指定顶点位置。
GPU 还要求每个顶点用齐次坐标表示。齐次坐标比普通三维坐标多一个 w 分量。在 WebGL 里,顶点着色器输出的内置变量 gl_Position 类型是 vec4,四个分量依次是 x、y、z、w。GPU 拿到 gl_Position 之后会做一次透视除法——把 x、y、z 分别除以 w。绝大多数时候 w 直接填 1.0 就可以。
例如要画一个三角形,三个顶点分别位于裁剪空间的左下方、右下方和上方正中:
javascript
const positions = new Float32Array([
0.0, 0.5, // 顶部正中
-0.5, -0.5, // 左下
0.5, -0.5 // 右下
]);这里每个顶点只给了两个分量,z 和 w 会在顶点着色器里补全。
着色器
WebGL 的渲染管线要求提供两段跑在 GPU 上的程序,用 GLSL(OpenGL Shading Language)编写:顶点着色器和片元着色器。数据从 JavaScript 传入顶点着色器,经过光栅化后进入片元着色器,最终变成屏幕上的像素。
顶点着色器的最小实现
顶点着色器对每个顶点执行一次,必须给内置变量 gl_Position 赋值。输入数据通过 attribute 变量接收。
glsl
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}a_position 是 vec2 类型,接收 JavaScript 传来的二维坐标。vec4(a_position, 0.0, 1.0) 把它扩展成齐次坐标:z 填 0.0 表示三角形平放在裁剪空间的近平面附近,w 填 1.0 保证透视除法后坐标不变。
片元着色器的最小实现
光栅化阶段把三角形拆成像素,每个像素触发一次片元着色器。片元着色器必须给内置变量 gl_FragColor 赋值,这个值就是屏幕上该像素的最终颜色。
glsl
precision mediump float;
void main() {
gl_FragColor = vec4(0.0, 0.5, 1.0, 1.0);
}precision mediump float 是必需的:片元着色器里使用浮点数必须声明精度,否则编译报错。mediump 是中等精度,在性能和准确度之间取常见折中。
gl_FragColor 也是 vec4,RGBA 四个分量。这里给了一个偏蓝的颜色,alpha 为 1.0 表示完全不透明。每个被三角形覆盖的像素都会变成这个颜色,最终屏幕上出现一个纯色三角形。
编译、链接、激活
GLSL 源码以字符串形式从 JavaScript 侧传入 WebGL。流程分三步:创建着色器对象并编译、创建程序对象并链接着色器、激活程序。
javascript
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}gl.createShader 的参数是 gl.VERTEX_SHADER 或 gl.FRAGMENT_SHADER,区分两种着色器类型。gl.shaderSource 接收 GLSL 字符串,gl.compileShader 触发 GPU 驱动编译。编译错误通过 gl.getShaderParameter(shader, gl.COMPILE_STATUS) 检查,日志用 gl.getShaderInfoLog 取出。
两个着色器分别编译通过后,gl.createProgram 创建一个程序对象,gl.attachShader 把两个着色器挂上去,gl.linkProgram 将它们链接成一个可执行的整体。链接阶段会检查顶点着色器和片元着色器之间的变量传递是否匹配,例如 attribute 和 varying 变量的类型一致性。
最后 gl.useProgram(program) 告诉 WebGL:后续的绘制调用全部使用这个程序。程序被激活后,才能往里面传数据、发绘制指令。
缓冲区
顶点数据在 JavaScript 这边是一个 Float32Array,存在于 CPU 内存中。GPU 读不到这块内存,必须通过缓冲区对象(buffer)把数据拷贝到 GPU 一侧。
操作缓冲区的 API 遵循 OpenGL 的状态机模式:绑定目标 → 写入数据。gl.ARRAY_BUFFER 是存放顶点属性数据的目标槽位。
javascript
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);gl.createBuffer 在 GPU 一侧分配一个缓冲区对象,返回一个句柄。gl.bindBuffer 把这个缓冲区设到 ARRAY_BUFFER 目标上,后续对 ARRAY_BUFFER 的操作全部作用在这个缓冲区上。gl.bufferData 把 positions 的数据拷贝进去。
第三个参数 gl.STATIC_DRAW 是用法提示,告诉 GPU 这个数据不会频繁修改。三个常见值:
STATIC_DRAW:数据写一次,用很多次。DYNAMIC_DRAW:数据会反复修改,但每次修改后用很多次。STREAM_DRAW:数据每次绘制都可能不同。
将缓冲区绑定到着色器的 attribute
数据已经位于 GPU 侧,但顶点着色器还不知道从哪个缓冲区、以什么格式读取 a_position。这需要先获取 a_position 在程序中的位置,再指定缓冲区和内存布局。
javascript
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);gl.getAttribLocation 返回 a_position 在已链接程序中的索引,本质是一个整数。如果程序中没有这个变量,返回 -1。
gl.enableVertexAttribArray 启用这个 attribute——默认是禁用的,不调用这行,数据就传不进去。
gl.vertexAttribPointer 的参数依次是:
- attribute 的位置索引。
- 每个顶点取几个分量,这里是 2(二维坐标)。
- 分量的数据类型,
gl.FLOAT。 - 是否归一化,
FLOAT类型不需要,填false。 - stride,相邻两个顶点之间的字节跨度。0 表示顶点数据紧密排列,GPU 自动按
size * sizeof(type)推算。 - offset,数据在缓冲区中的起始偏移量,单位字节。0 表示从起点开始。
这一串调用做完,WebGL 就知道了:绘制时从当前绑定的 ARRAY_BUFFER 里读数据,按每顶点两个 float 的格式取出,塞给 a_position。
绘制调用
最后发起绘制的是 gl.drawArrays。
javascript
gl.drawArrays(gl.TRIANGLES, 0, 3);gl.TRIANGLES 表示把顶点数据解析为三角形,每三个顶点组成一个独立三角形。0 指从第一个顶点开始读,3 是一共绘制三个顶点。
GPU 的执行路径:顶点着色器执行三次,分别处理 (0.0, 0.5)、(-0.5, -0.5)、(0.5, -0.5),生成三个 gl_Position。光栅化器根据这三个位置算出哪些像素被三角形覆盖。对每个被覆盖的像素,片元着色器执行一次,输出 (0.0, 0.5, 1.0, 1.0),屏幕上的三角形就此出现。
完整示例
把上面的代码片段拼在一起:
javascript
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
// 视口与清屏
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 着色器源码
const vertexShaderSource = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShaderSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(0.0, 0.5, 1.0, 1.0);
}
`;
// 编译与链接
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
// 顶点数据
const positions = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
// 绑定 attribute
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, 3);把这段放到一个 HTML 文件里,用浏览器打开,canvas 区域中央会出现一个蓝色三角形。
协作关系
坐标系统、着色器、缓冲区三者不是松散的配置项,而是严格的生产线:
- 缓冲区提供原材料(顶点坐标数据)。
- 顶点着色器接收原材料,按裁剪空间的规则加工出
gl_Position(齐次坐标形式)。 - GPU 执行裁剪与光栅化,把加工结果映射到
viewport定义的像素区域。 - 片元着色器为每个被光栅化的像素上色。
任何一个环节缺失或配置错误,三角形都不会出现。常见问题包括:忘记 gl.useProgram、忘记 gl.enableVertexAttribArray、gl.vertexAttribPointer 的 size 参数与实际数据不匹配、gl.drawArrays 的第一个参数写成了别的图元类型导致顶点数不够。这些错误 WebGL 大多不抛异常,只是屏幕一片黑,需要逐一排查绑定状态和参数。
注意点
GL 状态残留。 如果在同一个 canvas 上混用 WebGL 与其他渲染器(例如地图 SDK 的 WebGL 叠加视图),渲染完毕后必须把 GL 状态重置到初始值。典型的做法是保存一份初始状态引用,绘制完成后恢复 gl.bindBuffer、gl.useProgram 等关键绑定。未重置会导致状态冲突,后续渲染异常。
Y 轴方向。 WebGL 的裁剪空间里 Y 轴向上为正,而 HTML 的坐标系 Y 轴向下为正。在裁剪空间里写顶点坐标时,正值位于 canvas 上方,与 CSS 的直觉相反。如果后续需要把 DOM 坐标映射到 WebGL,需要手动翻转 Y 轴。
坐标归一化。 裁剪空间的坐标范围固定为 -1 到 1,与 canvas 的像素尺寸无关。如果数据源使用的是像素坐标(例如鼠标点击位置),需要先归一化到 -1 到 1 再传入着色器。
浏览器支持检测。 getContext('webgl') 在某些老旧浏览器或安全策略受限的环境中返回 null,调用前务必判空。
参考链接
- [1] https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL
- [2] https://developers.google.com/maps/documentation/javascript/webgl/webgl-overlay-view?hl=zh-cn
- [4] https://developer.chrome.com/blog/html-in-canvas-origin-trial?hl=zh-cn
- [5] https://learn.microsoft.com/zh-cn/windows/uwp/gaming/draw-to-the-screen
