WebGL与WebGPU入门指南

摘要:WebGL 统治了 Web 图形十余年,WebGPU 则在 2023--2025 年间完成了从实验特性到全平台默认可用的跃迁。本文用「一个三角形」把两条技术路线走通:先讲清两者的定位与图形管线心智模型,再分别给出 WebGL 2 与 WebGPU 的完整可运行示例、uniform 动画、WebGPU 独有的计算着色器,最后给出选型决策矩阵与迁移路径。所有代码均可直接复制到本地 HTML 文件运行。

关键词:WebGL、WebGPU、WGSL、GLSL、计算着色器、渲染管线、Three.js


一、先别急着写代码:三个必须搞清楚的定位问题

1.1 WebGL 是什么

WebGL 是 OpenGL ES 的 JavaScript 绑定 。WebGL 1.0 发布于 2011 年,对应 OpenGL ES 2.0;WebGL 2.0 发布于 2017 年,对应 OpenGL ES 3.0。它的核心特征是全局状态机 :你调用 gl.bindBuffer()、gl.useProgram()、gl.enable() 去修改一个隐藏的全局状态,然后 gl.drawArrays() 按照这套"当前状态"去画。

这个模型在 2011 年是合理的,但它有两个绕不开的问题:

  • 驱动负担重:OpenGL 的设计年代早于现代 GPU 架构,浏览器和驱动需要大量猜测与校验才能把状态翻译成 GPU 指令;
  • CPU 开销高:每次 draw call 前的状态切换都是 CPU 端的验证工作,场景复杂时 CPU 先于 GPU 成为瓶颈。

1.2 WebGPU 是什么

WebGPU 是 W3C 制定的全新 Web 图形与计算 API,它的设计目标是"高效映射到 2014 年之后的原生 GPU API"------Windows 上走 Direct3D 12,macOS/iOS 上走 Metal,Linux/Android 上走 Vulkan,底层由 Chrome 的 Dawn、Firefox 的 wgpu(Rust)等实现。

它带来了三个结构性变化:

特性 WebGL WebGPU
状态管理 全局状态机 不可变的管线对象(Pipeline)+ 绑定组(Bind Group)
命令提交 每次调用立即提交 录制到命令缓冲(Command Buffer),批量提交,可在 Worker 中录制
通用计算 只能通过片元着色器"伪装" 一等公民的计算着色器(Compute Shader)

1.3 WebGPU 不是「WebGL 3.0」

这是入门时最重要的澄清,也是最容易踩的认知坑:

  • API 层面毫无继承关系。W3C 规范里明确写着 "WebGPU is not related to WebGL"。着色器语言从 GLSL 换成 WGSL,两者不能混用。
  • WebGL 不会被移除。规范制定者的公开立场是 WebGL 与 WebGPU 长期共存,WebGL 已进入"功能冻结"状态------不再新增特性,但也不会消失。
  • 性能收益有前提 。WebGPU 的优势集中在高 draw call 数量、GPU 计算、实例化渲染等场景;如果你的场景只有几十个 draw call,WebGPU 的接口开销甚至可能略高于 WebGL。

1.4 2026 年的浏览器支持现状

查支持率请认准 caniuse.com/webgpu,以下是截至 2026 年 10 月的情况(数字会持续变化,以 caniuse 为准):

浏览器 默认启用版本 平台覆盖
Chrome / Edge 113+(2023-05) Windows、macOS、ChromeOS;Android 12+ 自 121 起;Linux 自 144 起逐步放开
Safari 26+(2025-09) macOS Tahoe 26、iOS 26、iPadOS 26、visionOS 26
Firefox 141+(Windows,2025-07)、145/147+(macOS Apple Silicon) Linux、Intel Mac、Android 尚在 Nightly / 需手动开启
Samsung Internet 24+ Android

全球覆盖率约 85%--87% 。规范本身目前仍是 W3C 候选推荐草案(CRD),最新草案持续更新中------这意味着核心 API 已稳定(2023 年写的代码今天照样能跑),但尚未走完正式推荐流程。

实践结论 :桌面端与主流移动端可以放心把 WebGPU 作为主路径,但必须保留 WebGL 2 回退,尤其要覆盖 Linux 与 Firefox 用户。


二、图形管线的通用心智模型

先把框架立住。无论 WebGL 还是 WebGPU,一次绘制的本质都是同一件事:

复制代码
CPU 端                                GPU 端
─────                                ─────
准备顶点数据(位置/颜色/法线/UV)
        │
        ▼
  上传到显存(Buffer)
        │
        ▼
编写着色器 ─────────────────►  顶点着色器:决定每个顶点落在屏幕哪里
(GLSL / WGSL)                        │
        │                              ▼
        │                         图元装配 + 光栅化:顶点 → 三角形 → 像素
        │                              │
        │                              ▼
        │                         片元着色器:决定每个像素是什么颜色
        │                              │
        ▼                              ▼
配置渲染状态 ──────────────►  深度/模板测试 + 混合 → 写入画布

记住这四件事,剩下的 API 细节都是"怎么把这几件事告诉 GPU":

  1. 数据怎么进去(Buffer / 顶点布局)
  2. 着色器怎么描述(GLSL / WGSL)
  3. 状态怎么组织(WebGL 的全局状态 vs WebGPU 的管线对象)
  4. 命令怎么发出(立即调用 vs 命令缓冲)

三、环境准备与能力检测

3.1 检测 WebGL 2

js 复制代码
function initWebGL2(canvas) {
  const gl = canvas.getContext('webgl2');
  if (!gl) {
    console.error('当前环境不支持 WebGL 2');
    return null;
  }
  // 顺手打印一下 GPU 信息,调试时很有用
  const info = gl.getExtension('WEBGL_debug_renderer_info');
  if (info) {
    console.log('GPU:', gl.getParameter(info.UNMASKED_RENDERER_WEBGL));
  }
  return gl;
}

3.2 检测 WebGPU ------ 必须分两步

这是新手最常见的错误:只判断 navigator.gpu 是否存在。实际上 navigator.gpu 存在 ≠ 真的能拿到 GPU ,驱动被浏览器列入黑名单时 requestAdapter() 会返回 null。

js 复制代码
async function checkWebGPU() {
  if (!('gpu' in navigator)) {
    return { ok: false, reason: '浏览器未实现 WebGPU API' };
  }
  try {
    const adapter = await navigator.gpu.requestAdapter();
    if (!adapter) {
      return { ok: false, reason: '浏览器支持 API,但拿不到适配器(驱动被黑名单 / 无可用 GPU)' };
    }
    const device = await adapter.requestDevice();
    return { ok: true, device, adapter };
  } catch (e) {
    return { ok: false, reason: e.message };
  }
}

3.3 本地运行方式

两个 API 都要求页面运行在安全上下文 中------http://localhost 或 https://。直接双击打开 .html 文件(file://)在部分浏览器下会失败,建议起个本地服务:

bash 复制代码
# 任选其一
npx serve .
python3 -m http.server 8000

四、WebGL 2 入门:第一个彩色三角形

把下面代码保存为 webgl-triangle.html 直接运行,会看到一个渐变色的三角形。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>WebGL 2 三角形</title>
  <style>body{margin:0;background:#14161c}canvas{display:block;width:100vw;height:100vh}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) throw new Error('当前环境不支持 WebGL 2');

/* ── 1. 适配高分屏 ───────────────────────────── */
function resize() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width  = Math.floor(canvas.clientWidth  * dpr);
  canvas.height = Math.floor(canvas.clientHeight * dpr);
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

/* ── 2. 着色器源码(GLSL ES 3.00)──────────────── */
const vsSource = `#version 300 es
in vec2 a_position;   // 顶点位置
in vec3 a_color;      // 顶点颜色
out vec3 v_color;     // 传给片元着色器

void main() {
  v_color = a_color;
  gl_Position = vec4(a_position, 0.0, 1.0);
}`;

const fsSource = `#version 300 es
precision highp float;
in  vec3 v_color;
out vec4 outColor;    // WebGL 2 用 out 变量替代 gl_FragColor

void main() {
  outColor = vec4(v_color, 1.0);
}`;

/* ── 3. 编译着色器(务必检查错误!)────────────── */
function compileShader(type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const log = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error('着色器编译失败:\n' + log);
  }
  return shader;
}

function createProgram(vsSrc, fsSrc) {
  const program = gl.createProgram();
  gl.attachShader(program, compileShader(gl.VERTEX_SHADER, vsSrc));
  gl.attachShader(program, compileShader(gl.FRAGMENT_SHADER, fsSrc));
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    throw new Error('程序链接失败:\n' + gl.getProgramInfoLog(program));
  }
  return program;
}

const program = createProgram(vsSource, fsSource);

/* ── 4. 顶点数据(交错存储:x, y, r, g, b)──────── */
const vertices = new Float32Array([
   0.0,  0.5,   1.0, 0.2, 0.3,   // 顶部,偏红
  -0.5, -0.5,   0.2, 1.0, 0.6,   // 左下,偏绿
   0.5, -0.5,   0.3, 0.5, 1.0,   // 右下,偏蓝
]);

/* ── 5. VAO + VBO:记录顶点属性的读取方式 ─────── */
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);

const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

const STRIDE = 5 * 4;                                  // 每个顶点 5 个 float = 20 字节
const posLoc   = gl.getAttribLocation(program, 'a_position');
const colorLoc = gl.getAttribLocation(program, 'a_color');

gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, STRIDE, 0);      // 从偏移 0 读 2 个 float
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, STRIDE, 2 * 4); // 从偏移 8 读 3 个 float

/* ── 6. 绘制 ──────────────────────────────────── */
gl.clearColor(0.08, 0.09, 0.11, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>

4.1 关键 API 速查

API 作用
gl.createShader / shaderSource / compileShader 创建并编译着色器
gl.createProgram / attachShader / linkProgram 链接成 GPU 程序
gl.createBuffer / bindBuffer / bufferData 把数据传进显存
gl.createVertexArray / bindVertexArray VAO,缓存顶点属性配置(WebGL 2 才有,务必用)
gl.getAttribLocation / vertexAttribPointer 告诉 GPU 顶点数据怎么切分给各个 in 变量
gl.getUniformLocation / uniformXX 传 uniform 值
gl.drawArrays / drawElements 发起绘制

4.2 WebGL 新手必踩的五个坑

  1. 忘记检查编译错误 。着色器写错时 WebGL 不会抛异常 ,只会静默画黑屏。一定要 getShaderParameter(..., COMPILE_STATUS)。
  2. canvas 的 CSS 尺寸 ≠ 画布像素尺寸 。不处理 devicePixelRatio 会导致画面模糊,不调 gl.viewport() 会导致画面错位。
  3. gl_FragColor 在 WebGL 2 里已废弃 ,改用 out vec4 outColor;。
  4. 忘了 enableVertexAttribArray。属性默认关闭,不启用就读不到数据。
  5. NDC 坐标系 :屏幕坐标范围是 [-1, 1],Y 轴向上为正(与 DOM 相反),原点在中心。

五、WebGPU 入门:同一个三角形

保存为 webgpu-triangle.html。代码比 WebGL 长一些,但结构清晰得多------你在创建对象,而不是在改状态。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>WebGPU 三角形</title>
  <style>body{margin:0;background:#14161c}canvas{display:block;width:100vw;height:100vh}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
const canvas = document.getElementById('canvas');

/* ── 1. 适配器 → 设备 ──────────────────────────── */
if (!navigator.gpu) throw new Error('浏览器不支持 WebGPU,请升级到 Chrome 113+ / Safari 26+ / Firefox 141+');

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('拿不到 GPU 适配器');
const device = await adapter.requestDevice();

// 优雅处理设备丢失(驱动崩溃、显存耗尽、标签页被系统回收)
device.lost.then((info) => {
  console.error('WebGPU 设备丢失:', info.message, '| reason:', info.reason);
  // 生产环境:提示用户并重建设备,注意重建时要重新 requestAdapter()
});

/* ── 2. 配置画布 ───────────────────────────────── */
const context = canvas.getContext('webgpu');
const format  = navigator.gpu.getPreferredCanvasFormat(); // 通常是 bgra8unorm
context.configure({ device, format, alphaMode: 'opaque' });

/* ── 3. WGSL 着色器 ────────────────────────────── */
const code = `
struct VertexOutput {
  @builtin(position) position : vec4f,
  @location(0)       color    : vec3f,
};

@vertex
fn vs(@location(0) pos : vec2f,
      @location(1) col : vec3f) -> VertexOutput {
  var out : VertexOutput;
  out.position = vec4f(pos, 0.0, 1.0);
  out.color    = col;
  return out;
}

@fragment
fn fs(in : VertexOutput) -> @location(0) vec4f {
  return vec4f(in.color, 1.0);
}
`;
const module = device.createShaderModule({ code });

/* ── 4. 顶点缓冲 ───────────────────────────────── */
const vertices = new Float32Array([
   0.0,  0.5,   1.0, 0.2, 0.3,
  -0.5, -0.5,   0.2, 1.0, 0.6,
   0.5, -0.5,   0.3, 0.5, 1.0,
]);
const vertexBuffer = device.createBuffer({
  size:  vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);

/* ── 5. 渲染管线:把"状态"一次性打包成不可变对象 ── */
const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module,
    entryPoint: 'vs',
    buffers: [{
      arrayStride: 5 * 4,
      attributes: [
        { shaderLocation: 0, offset: 0,     format: 'float32x2' }, // pos
        { shaderLocation: 1, offset: 2 * 4, format: 'float32x3' }, // col
      ],
    }],
  },
  fragment: {
    module,
    entryPoint: 'fs',
    targets: [{ format }],
  },
  primitive: { topology: 'triangle-list' },
});

/* ── 6. 录制命令 → 提交 ────────────────────────── */
function frame() {
  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({
    colorAttachments: [{
      view:       context.getCurrentTexture().createView(), // 每帧都要重新取!
      clearValue: { r: 0.08, g: 0.09, b: 0.11, a: 1.0 },
      loadOp:     'clear',
      storeOp:    'store',
    }],
  });
  pass.setPipeline(pipeline);
  pass.setVertexBuffer(0, vertexBuffer);
  pass.draw(3);
  pass.end();

  device.queue.submit([encoder.finish()]);
}
frame();
</script>
</body>
</html>

5.1 WebGPU 的对象模型

复制代码
navigator.gpu                     入口
    │  requestAdapter()
    ▼
GPUAdapter                        物理显卡的抽象
    │  requestDevice()
    ▼
GPUDevice ──────────────────────── 所有资源的工厂
    ├── createBuffer()             GPUBuffer      顶点/uniform/存储/回读
    ├── createTexture()            GPUTexture     纹理、深度缓冲、画布
    ├── createShaderModule()       GPUShaderModule(WGSL)
    ├── createRenderPipeline()     GPURenderPipeline   打包好的渲染状态
    ├── createComputePipeline()    GPUComputePipeline
    ├── createBindGroup()          GPUBindGroup   资源与着色器槽位的映射
    └── createCommandEncoder()     GPUCommandEncoder
             │  beginRenderPass() / beginComputePass()
             ▼
         GPUPassEncoder → 录制 draw/dispatch → end()
             │  finish()
             ▼
         GPUCommandBuffer → device.queue.submit([...])

5.2 WebGPU 新手必踩的五个坑

  1. 初始化是异步的 。requestAdapter() 和 requestDevice() 都返回 Promise,顶层 await 需要 <script type="module">。
  2. 忘记 context.configure() 。不配置画布,getCurrentTexture() 直接报错。
  3. getCurrentTexture() 每帧都要重新调用。画布尺寸变化或页面合成后纹理会失效,缓存下来会导致画面不更新。
  4. WGSL 里 struct 输出不能直接用 @builtin(position) 之外的 builtin 传给片元 ------要用 @location(n) 自定义成员(如上例的 color)。
  5. 缓冲区 usage 必须提前声明 。GPUBufferUsage.VERTEX | COPY_DST 少写一个就会在创建时报错,且不可后期补加。

5.3 捕获 GPU 错误

WebGPU 的默认行为是把错误推到控制台,但生产代码应该主动捕获:

js 复制代码
device.pushErrorScope('validation');
// ... 可能出错的 GPU 调用 ...
const error = await device.popErrorScope();
if (error) console.error('GPU 校验错误:', error.message);

WGSL 编译警告则应监听着色器模块:

js 复制代码
const info = await module.getCompilationInfo();
for (const m of info.messages) {
  console.log(`${m.type} [${m.lineNum}:${m.linePos}] ${m.message}`);
}

六、让三角形转起来:uniform 的两种写法

静态图形没意思,加个时间参数。这是两者差异体现得最直观的地方。

6.1 WebGL:逐个 uniform 设置

js 复制代码
// 顶点着色器里加:
// uniform float u_angle;
// gl_Position = vec4(a_position.x * cos(u_angle) - a_position.y * sin(u_angle),
//                    a_position.x * sin(u_angle) + a_position.y * cos(u_angle), 0.0, 1.0);

const angleLoc = gl.getUniformLocation(program, 'u_angle');

function render(time) {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(program);
  gl.bindVertexArray(vao);
  gl.uniform1f(angleLoc, time * 0.001);   // 每次 draw 前设一次
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

6.2 WebGPU:uniform 缓冲 + 绑定组

WebGPU 没有单独的"设一个 float"这种 API,所有 uniform 都走缓冲:

wgsl 复制代码
struct Uniforms {
  angle : f32,
  pad   : vec3f,   // uniform 缓冲按 16 字节对齐,显式补齐更稳妥
};
@group(0) @binding(0) var<uniform> u : Uniforms;

@vertex
fn vs(@location(0) pos : vec2f, @location(1) col : vec3f) -> VertexOutput {
  let s = sin(u.angle);
  let c = cos(u.angle);
  var out : VertexOutput;
  out.position = vec4f(pos.x * c - pos.y * s, pos.x * s + pos.y * c, 0.0, 1.0);
  out.color    = col;
  return out;
}
js 复制代码
const uniformBuffer = device.createBuffer({
  size: 16,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformData = new Float32Array(4); // [angle, 0, 0, 0]

// 用 layout:'auto' 时,直接从管线反查布局
const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});

function frame(time) {
  uniformData[0] = time * 0.001;
  device.queue.writeBuffer(uniformBuffer, 0, uniformData);

  const encoder = device.createCommandEncoder();
  const pass = encoder.beginRenderPass({ /* ...同上... */ });
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bindGroup);     // 一次调用,整体切换
  pass.setVertexBuffer(0, vertexBuffer);
  pass.draw(3);
  pass.end();
  device.queue.submit([encoder.finish()]);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

差异一目了然 :WebGL 是"每次 draw 前逐个设状态",WebGPU 是"预先把资源打包成绑定组,一次 setBindGroup 整体切换"。后者在资源数量多时 CPU 开销低得多。


七、WebGPU 的独门兵器:计算着色器

这是 WebGPU 真正无法被 WebGL 替代的能力------在 GPU 上跑通用计算,不经过任何渲染环节。粒子模拟、物理求解、图像后处理、浏览器端 AI 推理(Transformers.js、WebLLM、ONNX Runtime Web)都靠它。

下面是个最小可运行例子:把一个数组每个元素翻倍,然后读回 CPU。

js 复制代码
const data = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]);

// 存储缓冲:GPU 可读写 + 可拷贝出去
const storageBuffer = device.createBuffer({
  size:  data.byteLength,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(storageBuffer, 0, data);

// 回读缓冲:CPU 可映射读取
const readbackBuffer = device.createBuffer({
  size:  data.byteLength,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});

const module = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage, read_write> data : array<f32>;

@compute @workgroup_size(8)
fn main(@builtin(global_invocation_id) gid : vec3u) {
  let i = gid.x;
  data[i] = data[i] * 2.0;
}
`});

const computePipeline = device.createComputePipeline({
  layout: 'auto',
  compute: { module, entryPoint: 'main' },
});
const bindGroup = device.createBindGroup({
  layout: computePipeline.getBindGroupLayout(0),
  entries: [{ binding: 0, resource: { buffer: storageBuffer } }],
});

// 录制并提交
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(1);        // 派发 1 个工作组 × 8 个线程 = 8 个元素
pass.end();
encoder.copyBufferToBuffer(storageBuffer, 0, readbackBuffer, 0, data.byteLength);
device.queue.submit([encoder.finish()]);

// 读回(注意:mapAsync 是异步的,且用前必须 unmap)
await readbackBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readbackBuffer.getMappedRange().slice(0));
readbackBuffer.unmap();

console.log(result); // [2, 4, 6, 8, 10, 12, 14, 16]

7.1 三个必须理解的概念

  • 工作组(workgroup) :一组并行线程,@workgroup_size(8) 表示每个工作组 8 个线程;
  • 派发(dispatch) :dispatchWorkgroups(1) 表示派发 1 个工作组,总线程数 = 1 × 8 = 8;
  • 全局 ID :global_invocation_id 是线程在整次派发中的唯一索引,用它定位自己该处理哪个数据。

可以理解为:总线程数 = dispatchWorkgroups 参数 × workgroup_size。


八、核心差异总对照表

维度 WebGL 2 WebGPU
底层映射 OpenGL ES 3.0 Vulkan / Metal / D3D12
首次发布 2017(1.0 为 2011) 2023(Chrome 113)
着色器语言 GLSL ES 3.00 WGSL
状态模型 全局状态机 管线对象 + 绑定组
命令提交 立即模式 命令缓冲,批量提交
多线程 只能在主线程 命令可在 Worker 录制
计算着色器 ❌(只能用片元着色器模拟) ✅ 原生支持
顶点属性配置 vertexAttribPointer 逐个设 顶点缓冲布局在管线里声明
Uniform gl.uniformXX() 逐个设 uniform 缓冲 + 绑定组
错误处理 getError() 轮询 pushErrorScope / device lost Promise
画布格式 固定 getPreferredCanvasFormat()
特性扩展 大量 EXT_* 扩展 可选特性 adapter.features
学习曲线 平缓,资料多 陡峭,样板代码多
浏览器覆盖 ~99% ~85%

九、我该怎么选:决策矩阵与迁移路径

9.1 决策矩阵

场景 建议
新项目、无历史包袱 直接上 WebGPU,保留 WebGL 2 回退
大量 draw call / 实例化渲染 / 粒子系统 尽快迁移,收益明显
需要 GPU 计算(模拟、AI 推理、GPU 剔除) 必须 WebGPU
现有项目运行流畅、无新需求 继续 WebGL,不必折腾
大量自定义 GLSL 着色器 先评估改写成本(WGSL 或 TSL)
面向 Linux / Firefox 为主的受众 以 WebGL 2 为主,WebGPU 作为增强
简易展示型 3D(旋转立方体级别) 两者皆可,WebGL 更快出活

9.2 用 Three.js 迁移:几乎是一行代码

如果你不想手写 WGSL,主流引擎已经把回退逻辑做好了:

js 复制代码
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu';

const renderer = new WebGPURenderer({ antialias: true });
await renderer.init();          // ⚠️ 必须等待异步初始化
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

// 后续 API 与 WebGLRenderer 完全一致
const scene  = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
renderer.setAnimationLoop(() => renderer.render(scene, camera));

关键点:

  • WebGPURenderer 来自 three/webgpu,检测到不支持时自动降级到 WebGL 2 后端,"一套代码两个后端";
  • 用 renderer.backend.isWebGPUBackend 判断当前跑在哪个后端,据此决定是否启用计算着色器相关效果;
  • 自定义的 ShaderMaterial(GLSL)不会自动转换,需要改写成 TSL(Three Shading Language) 节点材质,这是迁移中最大的一块工作量;
  • 开发期可以用 forceWebGL: true 强制走回退路径,提前测试降级表现。

9.3 渐近增强的标准写法

js 复制代码
async function createRenderer(canvas) {
  if (navigator.gpu) {
    try {
      const adapter = await navigator.gpu.requestAdapter();
      if (adapter) {
        const r = new WebGPURenderer({ canvas, antialias: true });
        await r.init();
        return { renderer: r, backend: 'webgpu' };
      }
    } catch (e) {
      console.warn('WebGPU 初始化失败,降级 WebGL 2:', e.message);
    }
  }
  return { renderer: new THREE.WebGLRenderer({ canvas, antialias: true }), backend: 'webgl2' };
}

9.4 手写 WebGPU 的架构建议

如果不用引擎、直接写原生 WebGPU,建议按这个顺序搭骨架:

  1. 初始化层:adapter → device → 画布 configure,统一处理 device lost;
  2. 资源层:所有 buffer / texture 集中创建,usage 一次声明完整;
  3. 管线层 :渲染管线与计算管线在初始化时一次性创建(不要在渲染循环里创建管线,这是最大的性能陷阱);
  4. 绑定层:每帧变化的 uniform 用一个或多个绑定组管理;
  5. 帧循环层 :每帧 createCommandEncoder → 录制 → submit。

十、调试与性能工具

工具 用途
chrome://gpu 查看 WebGPU 是否被硬件加速、GPU 是否被黑名单
Chrome DevTools 控制台会输出带行号的 WGSL 编译错误
WebGPU Inspector(Chrome 扩展) 抓取 GPU 调用、查看资源
RenderDoc 支持 WebGPU 帧捕获,做深度性能分析
Spector.js WebGL 抓帧神器,看状态机变化
device.pushErrorScope() 精确定位是哪一次 GPU 调用出错
stats-gl / stats.js 帧率与内存监控

性能第一条 :WebGPU 的收益来自"减少 CPU 端开销",所以真正该优化的是管线创建次数、绑定组切换次数、命令缓冲录制方式,而不是着色器里的某行数学运算。


十一、常见报错速查

报错 / 现象 原因 解决办法
WebGL 黑屏,控制台无报错 着色器编译失败但未检查 加 COMPILE_STATUS 检查
navigator.gpu 存在但 requestAdapter() 返回 null 驱动被列入黑名单 / 无可用 GPU 走 WebGL 回退,不要反复重试
WebGPU 报 "canvas not configured" 忘了 context.configure() 在绘制前配置画布上下文
画面不更新 缓存了 getCurrentTexture() 的结果 每帧重新调用
创建 buffer 报 usage 错误 usage 未包含所需用途 创建时一次声明全(COPY_DST 等)
uniform 数据错乱 uniform buffer 未按 16 字节对齐 显式补 padding 或按 vec4 打包
device lost 频繁出现 显存耗尽 / 驱动 TDR 超时(约 2 秒) 关闭高显存占用的后台标签页、更新驱动、拆分计算任务
file:// 打开无法初始化 非安全上下文 用 localhost 或 https 起服务

十二、学习资源


十三、小结

  1. WebGL 是 OpenGL ES 的 Web 绑定,WebGPU 是现代原生 GPU API 的 Web 封装,两者没有继承关系,会长期共存。
  2. 心智模型是通用的:数据进显存 → 着色器描述计算 → 光栅化 → 写画布。变的只是"怎么把这些告诉 GPU"。
  3. WebGL 的核心动作是改状态,WebGPU 的核心动作是建对象。理解这一点,API 的记忆量减半。
  4. 计算着色器是 WebGPU 的护城河,也是浏览器端 AI 推理得以落地的基础。
  5. 2026 年的正确姿势:WebGPU 为主路径 + WebGL 2 自动回退,用 Three.js 等引擎可以几乎零成本获得这套能力。
  6. 别忘了检测分两步 :navigator.gpu 存在不等于能用,必须 requestAdapter() 成功才算数。

如果这篇文章对你有帮助,欢迎点赞收藏 🌹

标签:WebGL · WebGPU · WGSL · 图形学 · 前端 · 计算着色器

本文浏览器支持数据截至 2026 年 10 月,请以 caniuse.com/webgpu 的最新数据为准。

相关推荐
꧁细听勿语情꧂25 分钟前
直接选择排序,堆排序,冒泡排序,递归快速排序、hoare版本、挖坑法、lomuto前后指针,非递归快速排序
c语言·开发语言·数据结构·算法
SomeOtherTime28 分钟前
Rust集合(AI回答)
开发语言·后端·rust
曦樂~1 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
郝学胜-神的一滴1 小时前
LangChain Skill 实现(渐进披露 On-Demand Skill,LangChain 官方 Deep Agent Skill 模式)
开发语言·python·langchain·numpy
程序员Query1 小时前
day01-认识Go语言 + golang环境的安装
开发语言·后端·golang
§ꦿCFོ༉11 小时前
嵌入式八股文—C语言入门学习(四)
c语言·开发语言·学习
投图匠11 小时前
十种不同风格的 CTP接口 框架
开发语言·python
爱笑的k1115 小时前
nano-vllm 源码剖析:千行 Python 手写离线推理引擎的调度、显存与并行时序
开发语言·python·vllm
X1A0RAN16 小时前
PPT 图片批量处理教程:导出全部幻灯片 · 提取全部素材
开发语言·javascript·powerpoint