WebGPU:下一代 Web 图形与计算 API 实践指南

75 阅读5分钟

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();

性能优化技巧

  1. 资源重用:尽可能重用缓冲区、纹理和管线等资源,避免频繁创建和销毁。

  2. 批量提交:将多个命令编码器合并为一个提交,减少 CPU-GPU 通信开销。

  3. 合理使用绑定组:将频繁一起使用的资源放在同一个绑定组中。

  4. 异步操作:利用 WebGPU 的异步特性,合理安排资源上传和命令提交。

  5. 管线缓存:对于复杂的渲染管线,考虑缓存管线对象。

浏览器支持与未来展望

目前 WebGPU 已在 Chrome 113+、Firefox Nightly 和 Safari Technology Preview 中得到支持。随着规范的稳定和浏览器的实现完善,WebGPU 有望成为 Web 图形和计算的新标准。

WebGPU 为 Web 带来了前所未有的图形和计算能力,使得在浏览器中运行高性能 3D 应用、科学计算和机器学习推理成为可能。它的出现将极大扩展 Web 应用的可能性边界。

总结

本文介绍了 WebGPU 的基础知识,并通过三个实用示例展示了其能力:

  1. 图形渲染 - 绘制彩色三角形
  2. 通用计算 - 矩阵乘法
  3. 图像处理 - 灰度转换

WebGPU 的学习曲线相对陡峭,但其强大的功能和性能提升使得这一投入非常值得。随着生态系统的成熟,WebGPU 将成为 Web 高性能图形和计算的基石。