WebGPU:下一代 Web 图形与计算 API

FreeGuideOnline 最新 2026-07-03

javascript if (!navigator.gpu) { alert('当前浏览器不支持 WebGPU,请升级或更换浏览器。'); }


另外,为了能在本地运行示例并加载纹理等资源,建议启用本地服务器。你可以使用 VS Code 的 Live Server 插件,或通过 Node.js 的 `http-server` 快速搭建。

## 获取 GPU 适配器与设备

WebGPU 的入口对象是 `navigator.gpu`。调用 `requestAdapter()` 可以获取一个适配器实例,它代表了物理 GPU 的抽象。随后通过适配器请求一个逻辑设备(`GPUDevice`),所有的资源创建与指令发出都将依赖该设备。

```javascript
async function initWebGPU() {
  // 请求适配器
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) {
    throw new Error('未能获取 GPU 适配器。');
  }

  // 请求设备,可在此处指定所需特性
  const device = await adapter.requestDevice();

  return { adapter, device };
}

适配器与设备的获取都是异步操作,因此我们使用 async/await。通常你只会为整个应用创建一个设备实例。

配置画布与上下文

WebGPU 可以通过 Canvas 元素将渲染结果直接呈现到页面上。我们需要从 <canvas> 中获取 GPU 上下文,并配置其格式与用途。

const canvas = document.getElementById('gpuCanvas');
const context = canvas.getContext('webgpu');

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

context.configure({
  device: device,
  format: presentationFormat,
  alphaMode: 'premultiplied', // 可选:控制画布透明混合方式
});

getPreferredCanvasFormat() 会根据系统显示器返回最优格式,通常是 'rgba8unorm''bgra8unorm'

编写第一个着色器:三角形的顶点与片元着色

WebGPU 使用 WGSL(WebGPU Shading Language)编写着色器。以下是最简单的单色三角形所需的两段着色器代码。

顶点着色器:处理每个顶点的位置数据。

@vertex
fn main(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4<f32> {
  var pos = array<vec2<f32>, 3>(
    vec2(0.0, 0.5),
    vec2(-0.5, -0.5),
    vec2(0.5, -0.5)
  );
  return vec4<f32>(pos[vertexIndex], 0.0, 1.0);
}

这里我们利用内置的 vertex_index 直接生成三个顶点,无需额外的顶点缓冲区。

片元着色器:为每个像素输出颜色。

@fragment
fn main() -> @location(0) vec4<f32> {
  return vec4<f32>(1.0, 0.0, 0.0, 1.0); // 红色
}

将上述 WGSL 代码以字符串形式保存,以便后续创建着色器模块。

创建渲染管线

渲染管线(GPURenderPipeline)是固定功能单元与可编程着色器的组合体。我们需要创建一个管道对象,描述顶点布局、着色器以及颜色输出的混合状态。

const shaderModule = device.createShaderModule({
  code: shaderCode, // 包含顶点和片元着色器的 WGSL 字符串
});

const pipeline = device.createRenderPipeline({
  layout: 'auto', // 自动从着色器推导管线布局
  vertex: {
    module: shaderModule,
    entryPoint: 'main',  // 顶点着色器入口
  },
  fragment: {
    module: shaderModule,
    entryPoint: 'main',  // 片元着色器入口
    targets: [
      {
        format: presentationFormat, // 输出格式与画布一致
      },
    ],
  },
  primitive: {
    topology: 'triangle-list', // 绘制三角形列表
  },
});

layout: 'auto' 让 WebGPU 根据着色器中的资源声明自动创建绑定组布局,适合简单场景。

录制渲染指令

现在我们可以编写渲染循环,每一帧完成以下步骤:

  1. 从上下文获取当前可用的纹理视图(作为颜色附着)。
  2. 创建命令编码器,开始渲染通道。
  3. 设置管线,执行绘制调用。
  4. 结束通道,提交命令缓冲区到设备队列。
function render() {
  // 获取当前帧的纹理视图
  const textureView = context.getCurrentTexture().createView();

  // 颜色附件描述
  const colorAttachment = {
    view: textureView,
    clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
    loadOp: 'clear',
    storeOp: 'store',
  };

  // 创建命令编码器
  const commandEncoder = device.createCommandEncoder();
  const renderPass = commandEncoder.beginRenderPass({
    colorAttachments: [colorAttachment],
  });

  // 设置管线并绘制
  renderPass.setPipeline(pipeline);
  renderPass.draw(3); // 绘制3个顶点

  // 结束渲染通道
  renderPass.end();

  // 提交命令
  device.queue.submit([commandEncoder.finish()]);

  // 请求下一帧
  requestAnimationFrame(render);
}

启动引擎:

initWebGPU().then(({ device }) => {
  // 配置上下文、创建管线等操作...
  requestAnimationFrame(render);
});

如果一切正常,你将看到一个充满活力的红色三角形显示在画布中央。

传递顶点数据:使用缓冲区

对于更复杂的几何体,你需要显式提供顶点数据。WebGPU 使用 GPUBuffer 存储顶点属性,并在管线中声明顶点缓冲区布局。

假设我们要绘制一个包含位置和颜色的正方形(由两个三角形组成):

// 顶点数据:每项包含 x, y, r, g, b, a
const vertices = new Float32Array([
  // 位置         // 颜色
  -0.5,  0.5,     1.0, 0.0, 0.0, 1.0,
   0.5,  0.5,     0.0, 1.0, 0.0, 1.0,
  -0.5, -0.5,     0.0, 0.0, 1.0, 1.0,
   0.5, -0.5,     1.0, 1.0, 0.0, 1.0,
]);

// 创建顶点缓冲区并上传数据
const vertexBuffer = device.createBuffer({
  size: vertices.byteLength,
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);

顶点缓冲区需要使用 VERTEXCOPY_DST 标记,以便我们能够向其写入数据。

修改管线,添加顶点缓冲区布局描述:

const pipeline = device.createRenderPipeline({
  layout: 'auto',
  vertex: {
    module: shaderModule,
    entryPoint: 'main',
    buffers: [
      {
        arrayStride: 6 * 4, // 每个顶点占6个float,共24字节
        attributes: [
          { shaderLocation: 0, offset: 0, format: 'float32x2' }, // 位置
          { shaderLocation: 1, offset: 2 * 4, format: 'float32x4' }, // 颜色
        ],
      },
    ],
  },
  // ...片元与其余配置不变
});

对应地,更新 WGSL 着色器以接收这些属性:

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

@vertex
fn main(@location(0) pos : vec2<f32>, @location(1) col : vec4<f32>) -> VertexOutput {
  var output : VertexOutput;
  output.position = vec4<f32>(pos, 0.0, 1.0);
  output.color = col;
  return output;
}

@fragment
fn main(@location(0) color : vec4<f32>) -> @location(0) vec4<f32> {
  return color;
}

绘制时改为:

renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(4);

至此,你已掌握了最基础的显式顶点数据传递方式。

资源绑定:Uniform 缓冲区

除了顶点属性,着色器通常还需要常量数据(如变换矩阵、时间等)。这些可以通过 Uniform 缓冲区提供。

创建一个 Uniform 缓冲区,包含一个简单的 4x4 模型矩阵:

const uniformBuffer = device.createBuffer({
  size: 64, // 4x4 float矩阵 = 64字节
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// 准备一个简单的缩放矩阵
const matrix = new Float32Array([
  0.5, 0,   0, 0,
  0,   0.5, 0, 0,
  0,   0,   1, 0,
  0,   0,   0, 1,
]);
device.queue.writeBuffer(uniformBuffer, 0, matrix);

在管线中我们需要手动处理绑定组布局。先创建绑定组布局,描述将有一个 Uniform 缓冲区绑定在 binding 0 处:

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.VERTEX,
      buffer: { type: 'uniform' },
    },
  ],
});

然后使用该布局创建管线(将之前的 layout: 'auto' 改为显式布局):

const pipelineLayout = device.createPipelineLayout({
  bindGroupLayouts: [bindGroupLayout],
});

const pipeline = device.createRenderPipeline({
  layout: pipelineLayout,
  // 其余配置不变...
});

最后,创建绑定组并关联实际的缓冲区:

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

在着色器中声明 Uniform:

@group(0) @binding(0) var<uniform> modelMatrix : mat4x4<f32>;

@vertex
fn main(@location(0) pos : vec2<f32>) -> @builtin(position) vec4<f32> {
  return modelMatrix * vec4<f32>(pos, 0.0, 1.0);
}

在渲染通道中设置绑定组:

renderPass.setBindGroup(0, bindGroup);

现在三角形的顶点坐标会根据矩阵被缩放,你可以在每一帧更新 Uniform 缓冲区来实现动画效果。

纹理采样入门

WebGPU 提供了丰富的纹理操作支持。下面展示如何创建 GPU 纹理并从图像加载数据。

async function createTextureFromImage(device, imageUrl) {
  const response = await fetch(imageUrl);
  const imageBitmap = await createImageBitmap(await response.blob());

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

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

  return texture;
}

创建采样器与绑定组,将纹理传入着色器:

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

textureBindGroup = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(1), // 假设纹理在 group 1
  entries: [
    { binding: 0, resource: texture.createView() },
    { binding: 1, resource: sampler },
  ],
});

在着色器中使用:

@group(1) @binding(0) var tex : texture_2d<f32>;
@group(1) @binding(1) var samp : sampler;

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

计算着色器基础

计算管线独立于渲染管线,可用于通用并行任务。以下是一个简单的计算着色器,将两个数组相加:

@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> result : array<f32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3<u32>) {
  let index = id.x;
  result[index] = a[index] + b[index];
}

我们需要创建存储缓冲区,并配置计算管线:

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

// 执行计算
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, computeBindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(dataLength / 64));
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);

// 读取结果
device.queue.readBuffer(resultBuffer, 0, resultArray);

计算着色器极大地扩展了 WebGPU 的应用场景,例如粒子系统、物理模拟、图像处理等。

错误处理与调试

在开发过程中,尽早启用错误回调有助于快速定位问题:

device.addEventListener('uncapturederror', (event) => {
  console.error('WebGPU 错误:', event.error);
});

同时,管线创建、着色器编译等操作的详细信息可以通过异步获取:

const info = await pipeline.getCompilationInfo();
if (info.messages.length > 0) {
  console.log('着色器消息:', info.messages);
}