WebGPU 浏览器图形 API 完整教程:从第一个三角形到 Compute Shader

WebGPU 是 W3C 推出的下一代浏览器图形与计算 API,取代了已有十年历史的 WebGL。本教程从浏览器环境配置开始,完整讲解 WebGPU 的适配器与设备枚举、命令编码器、渲染管线、顶点缓冲、 uniform 绑定、纹理加载与采样,以及 Compute Shader 通用计算编程,帮助前端开发者迈入现代 GPU 编程时代。

为什么需要 WebGPU

WebGL 自 2011 年发布以来,为浏览器带来了近十年的 3D 图形能力。然而,WebGL 的设计深受 OpenGL ES 2.0/3.0 的束缚——全局状态机、隐式驱动管理、单线程命令提交,这些特性在移动设备上尚可接受,但在追求极致性能的桌面端和现代 GPU 上已显陈旧。WebGPU 的诞生正是为了解决这些根本性问题。

WebGPU 并非 WebGL 的简单升级,而是一次彻底的架构革新。它吸取了 Vulkan、Metal 和 DirectX 12 三大现代图形 API 的设计理念,将显式控制、低开销驱动和多线程友好带入浏览器环境。与 WebGL 的隐式状态机不同,WebGPU 采用基于对象的显式管线:渲染管线、绑定组、缓冲区和纹理都是独立的对象,状态变化通过对象替换而非全局状态翻转来实现。

在性能层面,WebGPU 的命令缓冲(Command Buffer)机制允许开发者在 JavaScript 主线程上批量录制 GPU 命令,然后一次性提交执行。这种方式不仅减少了 JavaScript 与 GPU 之间的往返开销,还为未来的多线程命令录制(Worker 线程生成命令缓冲)预留了架构空间。实测显示,在相同场景下,WebGPU 的 CPU 开销可比 WebGL 降低 30%~70%,特别是在大量绘制调用(Draw Call)的场景中优势尤为明显。

此外,WebGPU 首创性地将通用 GPU 计算(GPGPU)作为一等公民引入浏览器。Compute Shader 的完整支持使得浏览器端可以进行矩阵运算、图像处理、物理模拟、机器学习推理等计算密集型任务,而无需借助 WebGL 的片段着色器 hack。这为浏览器端 AI 推理、视频编解码加速和科学可视化开辟了新的可能性。

浏览器支持与环境检测

截至 2026 年,WebGPU 已在所有主流浏览器中落地:Chrome 113+、Firefox 118+、Safari 17+ 均提供完整的 WebGPU 支持。Edge 浏览器随 Chromium 内核同步支持。检测浏览器是否支持 WebGPU 非常简单:

if (!navigator.gpu) {
  console.error('当前浏览器不支持 WebGPU,请使用最新版 Chrome/Firefox/Safari');
  throw new Error('WebGPU not supported');
}

WebGPU API 完全通过 JavaScript 暴露,无需额外插件或扩展。navigator.gpu 是全局入口对象,提供 requestAdapter() 方法用于枚举可用的 GPU 适配器。适配器(GPUAdapter)代表一个物理 GPU 设备,现代笔记本电脑通常有两个适配器:集成核显(power-saving)和独立显卡(high-performance)。

const adapter = await navigator.gpu.requestAdapter({
  powerPreference: 'high-performance'
});
if (!adapter) {
  throw new Error('无法获取 GPU 适配器');
}

获取适配器后,通过 adapter.requestDevice() 创建一个逻辑设备(GPUDevice)。设备是 WebGPU 编程的核心句柄,所有的缓冲区、纹理、管线和命令都通过设备对象创建:

const device = await adapter.requestDevice({
  requiredFeatures: [],
  requiredLimits: {}
});

requiredFeatures 可选请求特定硬件特性(如着色器原子操作、纹理压缩格式),requiredLimits 可请求超出默认限制的资源配额(如最大纹理尺寸、绑定组数量)。对于大多数入门场景,使用默认配置即可。

第一个 WebGPU 三角形

每个图形 API 的学习都从绘制一个彩色三角形开始。以下代码完整展示了 WebGPU 从零到三角形的全部步骤,包括着色器代码、渲染管线配置和命令提交。

首先定义 WGSL(WebGPU Shading Language)着色器。WGSL 是 WebGPU 的原生着色器语言,语法类似 Rust 与 GLSL 的混合体:

// 顶点着色器
@vertex
fn vs_main(@location(0) position: vec2<f32>,
           @location(1) color: vec3<f32>) -> VertexOutput {
  var output: VertexOutput;
  output.position = vec4<f32>(position, 0.0, 1.0);
  output.color = color;
  return output;
}

struct VertexOutput {
  @builtin(position) position: vec4<f32>,
  @location(0) color: vec3<f32>,
}

// 片段着色器
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
  return vec4<f32>(input.color, 1.0);
}

WGSL 使用显式的阶段标记 @vertex 和 @fragment 区分着色器类型。@location(N) 用于指定顶点属性的插槽索引,@builtin(position) 是裁剪空间位置的专用语义。这种显式标记避免了 GLSL 中 varying 变量命名一致的隐式约定。

接下来创建渲染管线:

const shaderModule = device.createShaderModule({
  code: wgslCode
});

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'vs_main',
    buffers: [{
      arrayStride: 5 * 4, // 5 floats * 4 bytes
      attributes: [
        { shaderLocation: 0, offset: 0, format: 'float32x2' },
        { shaderLocation: 1, offset: 2 * 4, format: 'float32x3' }
      ]
    }]
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'fs_main',
    targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }]
  },
  primitive: { topology: 'triangle-list' }
});

WebGPU 的渲染管线是高度结构化的对象。顶点阶段定义了顶点着色器入口、顶点缓冲区布局和属性格式;片段阶段定义了片段着色器入口和输出目标格式;图元阶段指定了拓扑类型(三角形列表、带状、线框等)。值得注意的是,WebGPU 采用 “管线即状态” 的设计:一旦管线创建完成,其配置就是不可变的,运行时切换效果需要创建多个管线对象。

顶点数据通过 GPUBuffer 上传:

const vertices = new Float32Array([
  // x, y, r, g, b
  0.0,  0.5,  1.0, 0.0, 0.0,  // 顶点 1:顶部,红色
  -0.5, -0.5,  0.0, 1.0, 0.0,  // 顶点 2:左下,绿色
   0.5, -0.5,  0.0, 0.0, 1.0   // 顶点 3:右下,蓝色
]);

const vertexBuffer = device.createBuffer({
  size: vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  mappedAtCreation: true
});
new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
vertexBuffer.unmap();

mappedAtCreation: true 优化了初始数据上传:缓冲区在创建时即映射到 CPU 可写地址空间,数据写入后通过 unmap() 解除映射,GPU 即可访问。日常使用 device.queue.writeBuffer() 进行动态更新。

最后是渲染循环的核心——命令编码与提交:

function frame() {
  const commandEncoder = device.createCommandEncoder();
  const renderPass = commandEncoder.beginRenderPass({
    colorAttachments: [{
      view: context.getCurrentTexture().createView(),
      loadOp: 'clear',
      clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
      storeOp: 'store'
    }]
  });

  renderPass.setPipeline(pipeline);
  renderPass.setVertexBuffer(0, vertexBuffer);
  renderPass.draw(3);
  renderPass.end();

  device.queue.submit([commandEncoder.finish()]);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

WebGPU 的命令模型是显式的:先创建命令编码器,然后在其上录制渲染通道,设置管线和顶点缓冲,最后调用 draw() 发起绘制。loadOp: 'clear' 表示每帧开始前清空颜色附件,storeOp: 'store' 表示渲染结果写回纹理。命令录制完成后,通过 finish() 编译为可提交的命令缓冲,最终通过设备的队列(Queue)异步提交给 GPU 执行。

Uniform 缓冲与变换矩阵

静态三角形虽能验证管线配置,但真正的 3D 图形需要动态变换。WebGPU 通过 Uniform 绑定组(Bind Group)向着色器传递变换矩阵、光照参数等每帧变化的数据。

WGSL 中声明 uniform 块的方法:

struct Uniforms {
  mvp: mat4x4<f32>,
  time: f32,
};

@group(0) @binding(0)
var<uniform> u: Uniforms;

@group(M) 和 @binding(N) 组成二维绑定坐标,与 JavaScript 侧的绑定组布局严格对应。在 JavaScript 中创建 uniform 缓冲区:

const uniformBuffer = device.createBuffer({
  size: 64 + 4, // mat4 (64 bytes) + float (4 bytes),需对齐到 16 字节
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [{
    binding: 0,
    resource: { buffer: uniformBuffer }
  }]
});

每帧更新 uniform 数据时,先计算 MVP 矩阵:

function updateUniforms(time) {
  const model = mat4.rotationY(time);
  const view = mat4.lookAt(eye, target, up);
  const proj = mat4.perspective(fov, aspect, near, far);
  const mvp = mat4.multiply(proj, mat4.multiply(view, model));

  device.queue.writeBuffer(uniformBuffer, 0, mvp);
  device.queue.writeBuffer(uniformBuffer, 64, new Float32Array([time]));

  renderPass.setBindGroup(0, bindGroup);
}

WebGPU 的 uniform 缓冲布局遵循 WGSL 的内存布局规则:标量和 vecN 对齐到 4N 字节,mat4x4 对齐到 16 字节,结构体整体对齐到最大成员对齐值。这个布局与 WGSL 的 @align() 和 @size() 属性一致,避免了 GLSL 中 layout (std140) 和 (std430) 的混淆。

纹理加载与采样

2D 贴图是 3D 渲染的核心要素。WebGPU 的纹理 API 比 WebGL 更灵活,支持多层级 mipmap、多种采样模式和丰富的纹理格式。

加载图片并创建纹理:

const img = new Image();
img.src = 'texture.png';
await img.decode();

const texture = device.createTexture({
  size: [img.width, img.height],
  format: 'rgba8unorm',
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
  mipLevelCount: 1
});

device.queue.copyExternalImageToTexture(
  { source: img },
  { texture: texture },
  [img.width, img.height]
);

创建采样器以控制纹理过滤和寻址模式:

const sampler = device.createSampler({
  magFilter: 'linear',
  minFilter: 'linear',
  mipmapFilter: 'linear',
  addressModeU: 'repeat',
  addressModeV: 'repeat'
});

在绑定组中绑定纹理和采样器:

const bindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [
    { binding: 0, resource: sampler },
    { binding: 1, resource: texture.createView() },
    { binding: 2, resource: { buffer: uniformBuffer } }
  ]
});

WGSL 着色器中通过 texture_2d 和 sampler 类型采样:

@group(0) @binding(0) var mySampler: sampler;
@group(0) @binding(1) var myTexture: texture_2d<f32>;

@fragment
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
  return textureSample(myTexture, myTexture, uv);
}

textureSample() 函数在 WGSL 中执行硬件双线性(或三线性 mipmap)纹理采样,支持各向异性过滤。WebGPU 的纹理子资源视图系统(Texture View)允许将同一纹理的不同层级、切片或维度格式作为独立绑定资源,这比 WebGL 的单一纹理对象更具表达力。

Compute Shader 与 GPU 通用计算

WebGPU 最具革命性的特性是将 Compute Shader 作为原生功能引入浏览器。Compute Shader 不经过渲染管线,直接在 GPU 的通用计算单元上执行任意并行计算。

以下是一个并行向量加法的 Compute Shader:

@group(0) @binding(0) var<storage, read> a: array<f32>;
@group(0) @binding(1) var<storage, read> b: array<f32>;
@group(0) @binding(2) var<storage, read_write> c: array<f32>;

@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
  let idx = id.x;
  if (idx < arrayLength(&a)) {
    c[idx] = a[idx] + b[idx];
  }
}

@workgroup_size(256) 定义了每个工作组的线程数。NVIDIA GPU 的 Warp 大小为 32,AMD 的 Wavefront 为 64,选择 256(32 和 64 的公倍数)确保线程充分利用。var<storage, read_write> 声明了可读写的存储缓冲区,这是 Compute Shader 传递大规模数据的主要方式。

JavaScript 侧的执行流程:

const computePipeline = device.createComputePipeline({
  layout: 'auto',
  compute: {
    module: shaderModule,
    entryPoint: 'main'
  }
});

const commandEncoder = device.createCommandEncoder();
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
computePass.dispatchWorkgroups(Math.ceil(dataLength / 256));
computePass.end();
device.queue.submit([commandEncoder.finish()]);

Compute Shader 的应用场景远超向量加法:图像卷积(高斯模糊、边缘检测)、矩阵乘法(深度学习推理)、物理模拟(粒子系统、流体动力学)、音频处理(FFT 频谱分析)都可以高效实现。以矩阵乘法为例,利用共享内存(var<workgroup>)和分块技术,WebGPU 的 Compute Shader 可以达到理论峰值性能的 80% 以上。

深度测试与多重采样

完整的 3D 渲染需要深度缓冲(Depth Buffer)正确处理遮挡关系。WebGPU 创建深度纹理并在渲染通道中配置:

const depthTexture = device.createTexture({
  size: [canvas.width, canvas.height],
  format: 'depth24plus',
  usage: GPUTextureUsage.RENDER_ATTACHMENT
});

const renderPass = commandEncoder.beginRenderPass({
  colorAttachments: [...],
  depthStencilAttachment: {
    view: depthTexture.createView(),
    depthClearValue: 1.0,
    depthLoadOp: 'clear',
    depthStoreOp: 'store'
  }
});

渲染管线的深度状态配置为:

const pipeline = device.createRenderPipeline({
  // ...
  depthStencil: {
    depthWriteEnabled: true,
    depthCompare: 'less',
    format: 'depth24plus'
  }
});

多重采样抗锯齿(MSAA)在 WebGPU 中通过设置 sampleCount: 4 并在渲染管线与纹理中启用多重采样来实现。相比后处理抗锯齿(FXAA、TAA),MSAA 提供更清晰的边缘质量且计算开销可控。

性能优化与最佳实践

WebGPU 的显式控制模型赋予了开发者充分的优化空间,但也要求遵循最佳实践:

最小化命令缓冲提交次数。每次 device.queue.submit() 都会触发 GPU 同步和命令解析开销。理想情况下,一帧内的所有渲染和计算命令应录制到单个命令缓冲中一次性提交。

复用渲染管线对象。管线创建是昂贵的 CPU 操作,涉及驱动层级的着色器编译和状态验证。应将管线对象缓存并在多帧间复用,避免运行时创建。

批量绑定更新。setBindGroup() 的开销虽低于 WebGL 的 uniform 逐个设置,但频繁切换绑定组仍会降低性能。将使用相同材质和纹理的网格排序后批量绘制。

合理使用缓冲区映射。mapAsync() 是 CPU 与 GPU 之间的异步同步点,过度使用会造成管线停顿。对于每帧更新的 uniform 数据,使用 writeBuffer() 而非映射写入。

Compute Shader 的屏障同步。当 Compute Shader 的后续读取依赖前一次写入时(如 ping-pong 缓冲),使用 commandEncoder.pipelineBarrier() 确保内存可见性。WebGPU 的显式屏障模型避免了隐式同步的猜测开销。

调试与工具生态

现代浏览器的开发者工具已深度集成 WebGPU 支持。Chrome DevTools 的 “Performance” 面板可捕获 GPU 时间线,分析每帧的 CPU 录制时间和 GPU 执行时间。“Memory” 面板可追踪 GPU 内存占用,识别纹理和缓冲区的泄漏。

Chrome 的 chrome://gpu 页面详细列出了当前 GPU 的设备信息、驱动版本和 WebGPU 特性支持列表。若遇到渲染异常,首先检查 adapter.info 返回的供应商和架构信息是否匹配预期硬件。

着色器调试方面,WebGPU 的 createShaderModule() 支持 compilationInfo 查询,可获取精确的编译警告和错误位置(行列号)以及错误片段。这比 WebGL 的 getShaderInfoLog() 提供的纯文本日志要精确得多。

WebGPU vs WebGL 选型指南

WebGPU 并非在所有场景下都优于 WebGL。对于简单的 2D 图形、数据可视化图表(D3.js 风格的 SVG/Canvas 替代)或现有大量 WebGL 代码的项目,迁移成本可能超过收益。WebGL 的成熟度、工具生态和学习资源仍然具有优势。

WebGPU 最适合以下场景:高性能 3D 渲染(游戏、CAD、数字孪生)、大规模数据可视化(百万级点渲染)、GPU 通用计算(浏览器端 AI 推理、图像处理)、以及需要现代图形特性的新项目的开发。随着 Safari 和 Firefox 的完整支持,WebGPU 已具备跨生产环境部署的条件。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「game」更多文章

  1. 《游戏服务端编程实践》附录 D:参考资料与社区索引
  2. 《游戏服务端编程实践》附录 C:示例项目目录结构
  3. 《游戏服务端编程实践》附录 B:性能调优 Checklist