主题
WebGL2 概览
WebGL 2.0 是 WebGL 1.0 的增量升级:它把 OpenGL ES 3.0 的核心特性搬进浏览器,同时保留 ES 2.0 的全部能力。也就是说,一段能在 WebGL1 上跑的代码,在 WebGL2 上下文里通常仍然能用——只是着色器语言换了一代,新能力则必须显式启用。
它解决的是这些问题:
- 想让同一批几何体画上万次,但 WebGL1 的实例化要靠扩展;
- 想在 GPU 上迭代粒子状态,不必每帧把数据回读到 CPU;
- 想一次绘制写入多张渲染目标(延迟渲染的 G-Buffer);
- 想用 3D 纹理、纹理数组、UBO 组织资源。
如果你的需求只是「画几个三角形 + 简单纹理」,WebGL1 足够,没必要为迁移而迁移;一旦命中上面任意一条,WebGL2 就是当前最稳妥的选择(WebGPU 更现代,但覆盖率和生态还在追赶)。
与 WebGL 1.0 的关系
| 维度 | 说明 |
|---|---|
| 规范来源 | WebGL2 = OpenGL ES 3.0 的子集;WebGL1 = OpenGL ES 2.0 的子集 |
| 上下文获取 | canvas.getContext('webgl2');拿不到时回退 'webgl' |
| 向后兼容 | ES 2.0 的 API 与状态机基本保留;GLSL 不向后兼容,默认按 1.00 解析 |
| 着色器版本 | #version 300 es 是「加入新语法」的开关,不加就还是 1.00 语义 |
| 扩展 | 一部分旧扩展(VAO、实例化、深度纹理……)被吸收成内建功能 |
| 心智模型 | 仍然是状态机 + 立即模式,只是槽位更多、格式更全 |
一句话理解
WebGL2 ≈ WebGL1 + 一批原本要扩展才能用的能力 + 一门新着色语言。 你现有的渲染管线结构(缓冲区、program、绘制调用)不用推翻。
功能对照表
| 能力 | WebGL 1.0 | WebGL 2.0 |
|---|---|---|
| 着色器语言 | GLSL ES 1.00(attribute / varying / gl_FragColor) | GLSL ES 3.00(in / out / 自定义 out) |
| 顶点数组对象 VAO | 扩展 OES_vertex_array_object | 内建 createVertexArray |
| 实例化渲染 | 扩展 ANGLE_instanced_arrays | 内建 drawArraysInstanced / vertexAttribDivisor |
| 变换反馈 | 不支持(只能 CPU 回读) | 内建 transformFeedbackVaryings + beginTransformFeedback |
| 3D 纹理 / 纹理数组 | 不支持 | TEXTURE_3D、TEXTURE_2D_ARRAY、sampler3D、sampler2DArray |
| MRT 多重渲染目标 | 不支持 | drawBuffers + 片元多个 out |
| Uniform 缓冲对象 | 不支持 | UNIFORM_BUFFER + std140 布局 |
| 深度纹理 | 扩展 WEBGL_depth_texture | 内建 DEPTH_COMPONENT24 / DEPTH24_STENCIL8 |
| 浮点 / 半浮点纹理 | 扩展 OES_texture_float / OES_texture_half_float | 内建格式,渲染需 EXT_color_buffer_float |
| 非 2 次幂纹理 | 需要 CLAMP_TO_EDGE + 不可 mipmap | 内建支持 mipmap 与 REPEAT |
| MSAA 渲染缓冲 | 不支持 | renderbufferStorageMultisample + blitFramebuffer |
| 同步对象 | 不支持 | fenceSync / clientWaitSync / waitSync |
| 查询对象 | 不支持 | createQuery:遮挡查询、写入图元数、GPU 计时 |
| 32 位整数索引 | 扩展 OES_element_index_uint | 内建 UNSIGNED_INT 索引 |
| 缓冲区回读 | 只能 readPixels | getBufferSubData 直接读缓冲区 |
| 压缩纹理 | 各类厂商扩展(PVRTC/ETC1…) | ETC2 / EAC 内建,ASTC、S3TC 走扩展 |
| 着色器编译 | 同步 | 同步 + KHR_parallel_shader_compile 异步查询 |
能力检测与降级策略
检测不要用 UA 判断,直接看能不能拿到上下文:
js
const canvas = document.createElement('canvas')
const gl2 = canvas.getContext('webgl2')
if (!gl2) {
// 三种选择:
// 1) 回退 WebGL1 + 扩展(ANGLE_instanced_arrays / OES_vertex_array_object + 两套 GLSL)
// 2) 回退 Canvas 2D 的简化视图
// 3) 给一块可读的提示,而不是白屏
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
本站示例统一采用第 3 种降级:createGL(container, { version: 2 }) 拿不到上下文会抛错,示例用 try / catch 捕获后调用 notSupported() 渲染提示块。
能力检测:WebGL2 vs WebGL1同时打开两个上下文,把功能与上限参数并排输出
examples/webgl2/webgl2-capabilities.jsjs
/**
* 【WebGL 2.0】能力检测:WebGL2 专属能力 vs WebGL1 的同一批查询
* 演示:createGL({ version: 2 }) 的降级写法、getParameter 探测上限、getExtension 探测功能、DOM 表格输出。
*/
import { createGL, cleanupAll, notSupported } from '../shared/runtime.js'
/** 需要对比的上限参数:[常量名, 说明]。WebGL1 上没有的常量会显示 “—”。 */
const PARAMS = [
['MAX_TEXTURE_SIZE', '纹理最大边长'],
['MAX_TEXTURE_IMAGE_UNITS', '片元着色器可用纹理单元'],
['MAX_VERTEX_ATTRIBS', '顶点属性槽位数'],
['MAX_VERTEX_UNIFORM_VECTORS', '顶点着色器 uniform 向量数'],
['MAX_DRAW_BUFFERS', 'MRT 颜色输出数量'],
['MAX_COLOR_ATTACHMENTS', 'FBO 颜色附件数量'],
['MAX_3D_TEXTURE_SIZE', '3D 纹理最大边长'],
['MAX_ARRAY_TEXTURE_LAYERS', '纹理数组最大层数'],
['MAX_UNIFORM_BLOCK_SIZE', 'UBO 最大字节数'],
['MAX_UNIFORM_BUFFER_BINDINGS', 'UBO 绑定槽位数'],
['MAX_SAMPLES', 'MSAA 最大采样数'],
['MAX_ELEMENT_INDEX', '索引缓冲最大索引值']
]
export default function (container) {
let view
try {
view = createGL(container, { version: 2, width: 720, height: 240, clearColor: [0.04, 0.06, 0.11, 1] })
} catch (err) {
notSupported(container, err.message, 'WebGL2 覆盖率约 97%,Safari 15+ 已支持。')
return
}
const gl2 = view.gl
// 这个示例只做能力查询,不需要显示画布
view.canvas.style.display = 'none'
// 另开一个 WebGL1 上下文作为对照(离开示例时立刻释放)
const probe = document.createElement('canvas')
probe.width = 1
probe.height = 1
const gl1 = probe.getContext('webgl') || probe.getContext('experimental-webgl')
const hasFn = (ctx, name) => !!ctx && typeof ctx[name] === 'function'
const hasExt = (ctx, name) => !!(ctx && ctx.getExtension && ctx.getExtension(name))
const text = (v) => (v === null || v === undefined ? '—' : String(v))
/** 查询一个上限参数;WebGL1 上不存在的常量返回 null。 */
function param(ctx, name) {
if (!ctx) return null
const id = ctx[name]
if (typeof id !== 'number') return null
try {
return text(ctx.getParameter(id))
} catch {
return null
}
}
// 功能对照:同一个能力,在两代 API 里分别是「内建 / 扩展 / 不支持」
const features = [
{ name: '着色器语言', w1: 'GLSL ES 1.00', w2: 'GLSL ES 3.00' },
{
name: '实例化渲染',
w1: hasExt(gl1, 'ANGLE_instanced_arrays') ? '扩展 ANGLE_instanced_arrays' : '不支持',
w2: hasFn(gl2, 'drawArraysInstanced') ? '内建 drawArraysInstanced' : '不支持'
},
{
name: '顶点数组对象 VAO',
w1: hasExt(gl1, 'OES_vertex_array_object') ? '扩展 OES_vertex_array_object' : '不支持',
w2: hasFn(gl2, 'createVertexArray') ? '内建 createVertexArray' : '不支持'
},
{
name: '变换反馈',
w1: '不支持',
w2: hasFn(gl2, 'transformFeedbackVaryings') ? '内建 transformFeedbackVaryings' : '不支持'
},
{
name: '3D 纹理 / 纹理数组',
w1: '不支持',
w2: hasFn(gl2, 'texImage3D') ? '内建 texImage3D' : '不支持'
},
{ name: 'MRT 多重渲染目标', w1: '不支持', w2: hasFn(gl2, 'drawBuffers') ? '内建 drawBuffers' : '不支持' },
{
name: 'Uniform 缓冲对象 UBO',
w1: '不支持',
w2: hasFn(gl2, 'uniformBlockBinding') ? '内建 uniformBlockBinding' : '不支持'
},
{
name: '深度纹理',
w1: hasExt(gl1, 'WEBGL_depth_texture') ? '扩展 WEBGL_depth_texture' : '不支持',
w2: hasFn(gl2, 'texImage3D') ? '内建 DEPTH_COMPONENT24/32F' : '不支持'
},
{
name: '浮点纹理(可渲染)',
w1: hasExt(gl1, 'OES_texture_float') ? '扩展 OES_texture_float' : '不支持',
w2: hasExt(gl2, 'EXT_color_buffer_float') ? '内建 + EXT_color_buffer_float' : '需 EXT_color_buffer_float'
},
{ name: '非 2 次幂纹理 mipmap', w1: '需扩展 / 受限', w2: '内建支持' },
{
name: 'MSAA 渲染缓冲',
w1: '不支持',
w2: hasFn(gl2, 'renderbufferStorageMultisample') ? '内建 renderbufferStorageMultisample' : '不支持'
},
{ name: '同步对象', w1: '不支持', w2: hasFn(gl2, 'fenceSync') ? '内建 fenceSync/clientWaitSync' : '不支持' },
{ name: '查询对象', w1: '不支持', w2: hasFn(gl2, 'createQuery') ? '内建 createQuery' : '不支持' },
{
name: '32 位整数索引',
w1: hasExt(gl1, 'OES_element_index_uint') ? '扩展 OES_element_index_uint' : '不支持',
w2: '内建 UNSIGNED_INT'
},
{ name: '缓冲区回读', w1: '只能 readPixels', w2: hasFn(gl2, 'getBufferSubData') ? '内建 getBufferSubData' : '不支持' }
]
const box = document.createElement('div')
box.className = 'demo-dom'
box.innerHTML = `
<p class="demo-hint">
WebGL2 上下文版本串:<span class="demo-mono">${text(gl2.getParameter(gl2.VERSION))}</span>
${gl1 ? ` · 对照的 WebGL1:<span class="demo-mono">${text(gl1.getParameter(gl1.VERSION))}</span>` : ' · 本环境没有 WebGL1 上下文'}
</p>
<h4>功能对照</h4>
<table>
<thead><tr><th>能力</th><th>WebGL 1.0</th><th>WebGL 2.0</th></tr></thead>
<tbody>${features.map((f) => `<tr><td>${f.name}</td><td>${f.w1}</td><td>${f.w2}</td></tr>`).join('')}</tbody>
</table>
<h4>上限参数(真实查询结果)</h4>
<table>
<thead><tr><th>常量</th><th>含义</th><th>WebGL 1.0</th><th>WebGL 2.0</th></tr></thead>
<tbody>${PARAMS.map(
([name, label]) =>
`<tr><td class="demo-mono">${name}</td><td>${label}</td><td>${text(param(gl1, name))}</td><td>${text(param(gl2, name))}</td></tr>`
).join('')}</tbody>
</table>
<p class="demo-hint">不同显卡/驱动的数值会有差异,但“WebGL1 一列是 —”的位置就代表这是 WebGL2 新增的能力。</p>
`
container.appendChild(box)
/** 探测用的上下文不长期占用 GPU 资源,离开示例时主动释放。 */
function loseContext(ctx) {
const ext = ctx && ctx.getExtension && ctx.getExtension('WEBGL_lose_context')
if (ext) ext.loseContext()
}
return cleanupAll(view, () => {
box.remove()
loseContext(gl1)
loseContext(gl2)
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
关于是否值得写两套 GLSL,经验法则是:
- 面向现代浏览器(Chrome/Edge/Firefox/Safari 15+)的产品:直接要求 WebGL2,省掉一套着色器;
- 必须兼容老 Android WebView / 旧 iOS 的项目:抽出渲染层,准备
shader-100.glsl与shader-300.glsl两份,或者在 WebGL1 下用扩展兜底; - 无论如何都要给用户可读的失败提示:
canvas.getContext('webgl2')返回null只是环境不支持,不是你代码写错了。
最小可运行骨架
下面这个示例是本板块所有示例的模板:#version 300 es、VAO 内建、layout(location = N) 而不是每次 getAttribLocation。
最小 WebGL2 渲染GLSL ES 3.00 + VAO + 索引缓冲,一个旋转的彩色三角形
examples/webgl2/webgl2-hello.jsjs
/**
* 【WebGL 2.0】最小渲染流程:GLSL ES 3.00 + VAO + 索引缓冲
* 这是本板块其余示例的骨架:着色器一律以 #version 300 es 开头,顶点状态用 VAO 打包,切换物体只 bindVertexArray。
*/
import { createGL, rafLoop, cleanupAll, notSupported } from '../shared/runtime.js'
import { createProgram } from '../shared/gl.js'
import { mat4, degToRad } from '../shared/math.js'
const vertexSource = `#version 300 es
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aColor;
uniform mat4 uMatrix;
out vec3 vColor;
void main() {
vColor = aColor;
gl_Position = uMatrix * vec4(aPosition, 1.0);
}
`
const fragmentSource = `#version 300 es
precision highp float;
in vec3 vColor;
out vec4 outColor;
void main() {
outColor = vec4(vColor, 1.0);
}
`
export default function (container) {
let view
try {
view = createGL(container, {
version: 2,
width: 640,
height: 360,
antialias: true,
clearColor: [0.04, 0.06, 0.11, 1]
})
} catch (err) {
notSupported(container, err.message, 'WebGL2 覆盖率约 97%,Safari 15+ 已支持。')
return
}
const { gl } = view
const program = createProgram(gl, vertexSource, fragmentSource)
const uMatrix = gl.getUniformLocation(program, 'uMatrix')
const positions = new Float32Array([-0.6, -0.45, 0, 0.6, -0.45, 0, 0, 0.65, 0])
const colors = new Float32Array([1, 0.42, 0.42, 0.42, 0.85, 1, 0.62, 0.45, 1])
const indices = new Uint16Array([0, 1, 2])
// WebGL2 里 VAO 是内建的:顶点缓冲、属性指针、索引缓冲一次性录进 VAO
const vao = gl.createVertexArray()
gl.bindVertexArray(vao)
const positionBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)
gl.enableVertexAttribArray(0)
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0)
const colorBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)
gl.enableVertexAttribArray(1)
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0)
const indexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)
// 索引缓冲的绑定同样是 VAO 状态的一部分,录完再解绑
gl.bindVertexArray(null)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)
function draw(time) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
const aspect = view.width / view.height || 1
const projection = mat4.perspective(degToRad(45), aspect, 0.1, 100)
const camera = mat4.lookAt([0, 0, 2.4], [0, 0, 0], [0, 1, 0])
const model = mat4.multiply(mat4.fromRotationY(time * 0.6), mat4.fromRotationZ(Math.sin(time * 0.8) * 0.25))
gl.useProgram(program)
gl.uniformMatrix4fv(uMatrix, false, mat4.multiplyAll(projection, camera, model))
// 有了 VAO,绘制前只需要一次 bindVertexArray
gl.bindVertexArray(vao)
gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_SHORT, 0)
gl.bindVertexArray(null)
}
const loop = rafLoop(draw)
return cleanupAll(loop, view, () => {
gl.deleteVertexArray(vao)
gl.deleteBuffer(positionBuffer)
gl.deleteBuffer(colorBuffer)
gl.deleteBuffer(indexBuffer)
gl.deleteProgram(program)
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
浏览器支持现状
| 浏览器 | 起始版本 | 备注 |
|---|---|---|
| Chrome / Edge | 56 / 79 | 桌面与 Android 均已默认开启 |
| Firefox | 51 | 桌面与 Android 稳定支持 |
| Safari | 15(macOS Monterey / iOS 15) | 15 之前完全没有 WebGL2 |
| 覆盖率 | 约 97%(caniuse) | 已是事实上的默认选择 |
WebGL2 也有“例外环境”
软件渲染被禁用、隐私模式、远程桌面、部分虚拟机的驱动白名单,都可能让 getContext('webgl2') 返回 null,而同一台机器上 WebGL1 仍然可用。不要假设「有 WebGL1 就一定有 WebGL2」。
从 WebGL1 迁移检查清单
- 着色器全部改写:加
#version 300 es,attribute→in,varying→in/out,gl_FragColor→自声明out vec4 outColor;,texture2D→texture; - 片元着色器必须声明精度:
precision highp float;(ES 3.00 没有默认精度); - 检查被删掉的语法:
#extension GL_EXT_*、gl_FragData、textureCube、varying关键字都会直接编译失败; - 索引类型:
UNSIGNED_INT现在可以直接用,drawElements的type别忘同步改; - VAO 内建:把「每帧重设属性指针」改成一物一 VAO,绘制前
bindVertexArray; - 实例化:扩展调用换成
drawArraysInstanced/drawElementsInstanced; - 非 2 次幂纹理:可以放开
REPEAT与 mipmap 的限制了; - 浮点渲染:需要
gl.getExtension('EXT_color_buffer_float')才能把RGBA32F当渲染目标; - 能力探测:
gl.getParameter(gl.MAX_3D_TEXTURE_SIZE)之类的数值在低端设备上很小,别硬编码; - 资源释放:新增的 VAO、FBO、查询对象、同步对象、UBO 都要显式
delete*。
API 速查
| API | 说明 |
|---|---|
canvas.getContext('webgl2', attrs) | 获取 WebGL2 上下文,失败返回 null |
gl.createVertexArray() / bindVertexArray(vao) | 内建 VAO,打包顶点属性与索引绑定 |
gl.drawArraysInstanced(mode, first, count, n) | 一次调用绘制 n 个实例 |
gl.vertexAttribDivisor(index, d) | 让某个属性按实例(d=1)而不是按顶点前进 |
gl.transformFeedbackVaryings(p, names, mode) | 声明要写回缓冲区的顶点输出 |
gl.texImage3D(target, level, ifmt, w, h, d, ..., data) | 上传 3D 纹理 / 纹理数组的层数据 |
gl.drawBuffers([...]) | 指定 MRT 的颜色附件列表 |
gl.renderbufferStorageMultisample(...) | MSAA 渲染缓冲 |
gl.blitFramebuffer(...) | 解析 MSAA、在 FBO 间拷贝 |
gl.uniformBlockBinding(program, index, binding) | 把 UBO 绑定点接到 program 的 uniform block |
gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0) | 创建同步对象,配合 clientWaitSync 等 GPU |
gl.getBufferSubData(target, offset, dst) | 把缓冲区内容读回 TypedArray |
gl.getParameter(gl.MAX_*) | 查询设备上限,决定分辨率、层数、采样数 |
常见坑与排查
把 #version 300 es 写在第二行
#version 必须是源码的第一个字符(允许前面有空白与注释,但模板字符串里最常见的错误是反引号后先换了一行)。报错通常是 '#version' must be the first line 或莫名其妙的 syntax error。写代码时保持 const vs = `#version 300 es\n...` 这样的形态。
片元着色器没写精度
ES 3.00 取消了 mediump float 的默认精度。缺少 precision highp float; 时,编译日志是 No precision specified for (float)。顶点着色器有默认 highp,片元着色器没有。
用 WebGL1 的语法去编译
attribute、varying、gl_FragColor、texture2D 在 #version 300 es 下全部是保留字或未定义标识符,会一次性刷出一屏编译错误。迁移时建议先机械替换,再逐个修剩余报错。
调试建议
用 shared/gl.js 的 compileShader()——它在失败时会抛出带行号的日志与源码,比看 gl.getShaderInfoLog() 的裸字符串快得多。
本板块怎么读
| 页面 | 你会得到什么 |
|---|---|
| GLSL ES 3.00 语法 | 新旧语言差异、内置函数、精度与性能 |
| 实例化渲染 | 万级物体的正确打开方式 |
| 变换反馈 | GPU 端写回顶点数据 |
| 3D 纹理与纹理数组 | 体积数据、材质图集 |
| FBO、MRT 与多重采样 | 后处理链、G-Buffer、MSAA |
| Uniform 缓冲对象 | std140 对齐与跨 program 共享 |
| GPGPU 通用计算 | 用片元着色器做通用计算 |
| 其他现代特性 | 查询对象、同步对象、纹理视图 |