WebGPU:下一代 Web 图形与计算 API
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 根据着色器中的资源声明自动创建绑定组布局,适合简单场景。
录制渲染指令
现在我们可以编写渲染循环,每一帧完成以下步骤:
- 从上下文获取当前可用的纹理视图(作为颜色附着)。
- 创建命令编码器,开始渲染通道。
- 设置管线,执行绘制调用。
- 结束通道,提交命令缓冲区到设备队列。
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);
顶点缓冲区需要使用 VERTEX 与 COPY_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);
}