WebGPU 是新兴的 Web 图形和计算 API,旨在取代 WebGL 并提供更现代的 GPU 功能访问方式。它提供了更低的开销、更好的性能以及更直观的 API 设计。本文将介绍 WebGPU 的核心概念,并通过几个实用示例展示其强大功能。
WebGPU 简介
WebGPU 的设计灵感来自 Vulkan、Metal 和 Direct3D 12 等现代图形 API,具有以下优势:
- 更高效的 CPU 使用率
- 更好的多线程支持
- 更直观的 API 设计
- 支持计算着色器
- 更精细的资源控制
基础设置
首先,我们需要检查浏览器是否支持 WebGPU 并初始化适配器和设备:
async function initWebGPU() {
if (!navigator.gpu) {
throw new Error('WebGPU not supported on this browser.');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('No appropriate GPUAdapter found.');
}
const device = await adapter.requestDevice();
return device;
}
initWebGPU().then(device => {
console.log('WebGPU device initialized:', device);
// 可以开始使用 WebGPU
});
示例 1:绘制三角形
让我们从经典的 "Hello World" 图形程序开始 - 绘制一个彩色三角形。
HTML 结构
<!DOCTYPE html>
<html>
<head>
<title>WebGPU Triangle</title>
<style>
canvas { width: 640px; height: 480px; display: block; }
</style>
</head>
<body>
<canvas id="webgpu-canvas" width="640" height="480"></canvas>
<script src="triangle.js"></script>
</body>
</html>
JavaScript 代码 (triangle.js)
async function init() {
// 初始化 WebGPU
if (!navigator.gpu) {
alert('WebGPU not supported on this browser.');
return;
}
const canvas = document.getElementById('webgpu-canvas');
const context = canvas.getContext('webgpu');
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
// 配置画布
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: format,
alphaMode: 'opaque'
});
// 创建渲染管线
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: `
@vertex
fn vertexMain(@location(0) position: vec2f,
@location(1) color: vec3f) -> @builtin(position) vec4f {
return vec4f(position, 0.0, 1.0);
}
`
}),
entryPoint: 'vertexMain',
buffers: [{
arrayStride: 5 * 4, // 2个位置 + 3个颜色 = 5个float32 (每个4字节)
attributes: [
{
// 位置
shaderLocation: 0,
offset: 0,
format: 'float32x2'
},
{
// 颜色
shaderLocation: 1,
offset: 2 * 4,
format: 'float32x3'
}
]
}]
},
fragment: {
module: device.createShaderModule({
code: `
@fragment
fn fragmentMain(@location(0) color: vec3f) -> @location(0) vec4f {
return vec4f(color, 1.0);
}
`
}),
entryPoint: 'fragmentMain',
targets: [{ format: format }]
},
primitive: {
topology: 'triangle-list'
}
});
// 顶点数据 (位置 + 颜色)
const vertices = new Float32Array([
// 位置 颜色
0.0, 0.5, 1.0, 0.0, 0.0, // 顶部顶点,红色
-0.5, -0.5, 0.0, 1.0, 0.0, // 左下顶点,绿色
0.5, -0.5, 0.0, 0.0, 1.0 // 右下顶点,蓝色
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
vertexBuffer.unmap();
// 渲染
function render() {
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: textureView,
clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
loadOp: 'clear',
storeOp: 'store'
}]
});
renderPass.setPipeline(pipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(3); // 绘制3个顶点
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
init();
示例 2:计算着色器 - 矩阵乘法
WebGPU 的强大之处不仅在于图形渲染,还在于通用计算能力。下面是一个矩阵乘法的计算着色器示例。
async function matrixMultiplication() {
const device = await initWebGPU();
// 矩阵尺寸
const M = 16;
const N = 16;
const K = 16;
// 创建输入矩阵
const matrixA = new Float32Array(M * K);
const matrixB = new Float32Array(K * N);
// 填充随机值
for (let i = 0; i < M * K; i++) matrixA[i] = Math.random();
for (let i = 0; i < K * N; i++) matrixB[i] = Math.random();
// 创建 GPU 缓冲区
const bufferA = device.createBuffer({
size: matrixA.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(bufferA, 0, matrixA);
const bufferB = device.createBuffer({
size: matrixB.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(bufferB, 0, matrixB);
const bufferResult = device.createBuffer({
size: M * N * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});
const readBuffer = device.createBuffer({
size: M * N * 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
// 创建计算管线
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: device.createShaderModule({
code: `
@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(8, 8)
fn main(@builtin(global_invocation_id) global_id: vec3u) {
let m = global_id.x;
let n = global_id.y;
if (m >= ${M}u || n >= ${N}u) {
return;
}
var sum = 0.0;
for (var k = 0u; k < ${K}u; k = k + 1u) {
sum = sum + a[m * ${K}u + k] * b[k * ${N}u + n];
}
result[m * ${N}u + n] = sum;
}
`
}),
entryPoint: 'main'
}
});
// 创建绑定组
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: bufferA } },
{ binding: 1, resource: { buffer: bufferB } },
{ binding: 2, resource: { buffer: bufferResult } }
]
});
// 执行计算
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(M / 8), Math.ceil(N / 8));
passEncoder.end();
// 复制结果到可读缓冲区
commandEncoder.copyBufferToBuffer(
bufferResult, 0,
readBuffer, 0,
M * N * 4
);
device.queue.submit([commandEncoder.finish()]);
// 读取结果
await readBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readBuffer.getMappedRange());
console.log('Matrix A:', matrixA);
console.log('Matrix B:', matrixB);
console.log('Result:', result);
readBuffer.unmap();
}
matrixMultiplication();
示例 3:纹理处理
WebGPU 可以高效处理纹理数据。下面是一个简单的图像处理示例,将图像转换为灰度。
async function textureProcessing() {
const device = await initWebGPU();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 加载测试图像
const img = new Image();
img.src = 'https://via.placeholder.com/256';
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
// 获取图像数据
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
// 创建纹理
const texture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT
});
// 上传纹理数据
device.queue.writeTexture(
{ texture: texture },
pixels,
{ bytesPerRow: canvas.width * 4 },
[canvas.width, canvas.height]
);
// 创建输出纹理
const outputTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'rgba8unorm',
usage: GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.COPY_SRC
});
// 创建计算管线
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: device.createShaderModule({
code: `
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
@group(0) @binding(1) var outputTexture: texture_storage_2d<rgba8unorm, write>;
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) global_id: vec3u) {
let texCoord = vec2u(global_id.xy);
let size = textureDimensions(inputTexture);
if (texCoord.x >= size.x || texCoord.y >= size.y) {
return;
}
let color = textureLoad(inputTexture, texCoord, 0);
let gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
textureStore(outputTexture, texCoord, vec4f(gray, gray, gray, 1.0));
}
`
}),
entryPoint: 'main'
}
});
// 创建绑定组
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: texture.createView()
},
{
binding: 1,
resource: outputTexture.createView()
}
]
});
// 创建输出缓冲区
const outputBuffer = device.createBuffer({
size: canvas.width * canvas.height * 4,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});
// 执行计算
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(
Math.ceil(canvas.width / 8),
Math.ceil(canvas.height / 8)
);
passEncoder.end();
// 复制结果到缓冲区
commandEncoder.copyTextureToBuffer(
{ texture: outputTexture },
{ buffer: outputBuffer, bytesPerRow: canvas.width * 4 },
[canvas.width, canvas.height]
);
device.queue.submit([commandEncoder.finish()]);
// 读取结果
await outputBuffer.mapAsync(GPUMapMode.READ);
const result = new Uint8Array(outputBuffer.getMappedRange());
// 在画布上显示结果
const resultImageData = new ImageData(
new Uint8ClampedArray(result),
canvas.width,
canvas.height
);
context.putImageData(resultImageData, 0, 0);
document.body.appendChild(canvas);
outputBuffer.unmap();
}
textureProcessing();
性能优化技巧
-
资源重用:尽可能重用缓冲区、纹理和管线等资源,避免频繁创建和销毁。
-
批量提交:将多个命令编码器合并为一个提交,减少 CPU-GPU 通信开销。
-
合理使用绑定组:将频繁一起使用的资源放在同一个绑定组中。
-
异步操作:利用 WebGPU 的异步特性,合理安排资源上传和命令提交。
-
管线缓存:对于复杂的渲染管线,考虑缓存管线对象。
浏览器支持与未来展望
目前 WebGPU 已在 Chrome 113+、Firefox Nightly 和 Safari Technology Preview 中得到支持。随着规范的稳定和浏览器的实现完善,WebGPU 有望成为 Web 图形和计算的新标准。
WebGPU 为 Web 带来了前所未有的图形和计算能力,使得在浏览器中运行高性能 3D 应用、科学计算和机器学习推理成为可能。它的出现将极大扩展 Web 应用的可能性边界。
总结
本文介绍了 WebGPU 的基础知识,并通过三个实用示例展示了其能力:
- 图形渲染 - 绘制彩色三角形
- 通用计算 - 矩阵乘法
- 图像处理 - 灰度转换
WebGPU 的学习曲线相对陡峭,但其强大的功能和性能提升使得这一投入非常值得。随着生态系统的成熟,WebGPU 将成为 Web 高性能图形和计算的基石。