WebGPU 深度解析:浏览器图形的第三次革命——从 WebGL 的 20fps 到 WebGPU 的 120fps
WebGPU 是 W3C 历时 7 年打造的浏览器图形 API 新标准,于 2026 年在 Chrome/Firefox/Safari 全面稳定支持。它不仅是 WebGL 的替代品,更是浏览器端 GPU 计算的基石——Compute Shader 让浏览器可以跑机器学习推理、物理模拟、信号处理;wgpu 让 Rust 开发者一套代码横跨 Web/桌面/移动。本文深度解析 WebGPU 的架构设计、与 WebGL 的本质区别、Compute Shader 实战、wgpu Rust 开发全链路,以及从 WebGL 迁移的完整指南。
一、WebGPU vs WebGL:不只是换了个名字
1.1 为什么 WebGL 不够用了?
// WebGL 2.0:旋转一个 3D 模型的典型代码(50+ 行设置)
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
// 编译着色器
const vsSource = `#version 300 es
in vec4 aPosition;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * aPosition;
}
`;
const fsSource = `#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 手动编译、链接、验证...
function compileShader(gl, source, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vs = compileShader(gl, vsSource, gl.VERTEX_SHADER);
const fs = compileShader(gl, fsSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// ... 还有 30 行缓冲区、属性、uniform 设置
// ❌ WebGL 的根本问题:
// 1. 状态机模型(全局状态,容易冲突)
// 2. 无 Compute Shader(GPU 计算必须 hack)
// 3. CPU-GPU 同步开销大
// 4. 不支持现代 GPU 特性(mesh shader、ray tracing)
1.2 WebGPU:现代化 GPU API
// WebGPU:等价的代码(更清晰、更安全)
const canvas = document.getElementById('canvas');
const gpu = navigator.gpu;
const adapter = await gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const format = gpu.getPreferredCanvasFormat();
context.configure({ device, format });
// 着色器(WGSL 语言,类型安全)
const shaderCode = `
struct Uniforms {
modelViewMatrix: mat4x4f,
projectionMatrix: mat4x4f,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@vertex
fn vs_main(@location(0) position: vec3f) -> @builtin(position) vec4f {
return uniforms.projectionMatrix * uniforms.modelViewMatrix * vec4f(position, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0);
}
`;
const shaderModule = device.createShaderModule({ code: shaderCode });
// 渲染管线(声明式,无全局状态)
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [{
arrayStride: 12, // 3 × float32
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }]
}]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format }]
},
primitive: { topology: 'triangle-list' },
});
// ✅ WebGPU 的核心优势:
// 1. 对象化 API(无全局状态,多管线共存)
// 2. 原生 Compute Shader
// 3. 显式同步(Command Buffer 批量提交)
// 4. 映射现代 GPU(Vulkan/Metal/D3D12)
1.3 性能对比
| 指标 | WebGL 2.0 | WebGPU | 提升 |
|---|---|---|---|
| Draw Call 开销 | ~5μs | ~0.5μs | 10x |
| 最大 Draw Call/帧 | ~2000 | ~50000 | 25x |
| 100 万粒子模拟 | ~15 fps | ~60 fps | 4x |
| GPU 计算吞吐 | ❌ 不支持 | 2 TFLOPS+ | ∞ |
| 着色器编译错误 | 运行时字符串 | 结构化错误 | 开发体验 |
| 多线程渲染 | ❌ | ✅ | - |
二、WGSL:WebGPU 的着色器语言
2.1 WGSL 基础语法
// WGSL (WebGPU Shading Language) —— 类型安全的着色器语言
// ====== 基本类型 ======
var f32_val: f32 = 3.14;
var i32_val: i32 = 42;
var u32_val: u32 = 100u;
var bool_val: bool = true;
// 向量
var v2: vec2f = vec2f(1.0, 2.0);
var v3: vec3f = vec3f(1.0, 0.0, 0.0);
var v4: vec4f = vec4f(1.0, 0.0, 0.0, 1.0);
// 矩阵
var m3: mat3x3f = mat3x3f(
vec3f(1, 0, 0),
vec3f(0, 1, 0),
vec3f(0, 0, 1)
);
// 数组
var arr: array<f32, 4> = array<f32, 4>(1.0, 2.0, 3.0, 4.0);
// 结构体
struct Vertex {
@location(0) position: vec3f,
@location(1) normal: vec3f,
@location(2) uv: vec2f,
}
struct Uniforms {
model: mat4x4f,
view: mat4x4f,
projection: mat4x4f,
cameraPos: vec3f,
_padding: f32, // 对齐填充
lightPos: vec3f,
_padding2: f32,
}
// ====== 函数 ======
fn dot3(a: vec3f, b: vec3f) -> f32 {
return dot(a, b);
}
fn normalize3(v: vec3f) -> vec3f {
return normalize(v);
}
// Blinn-Phong 光照
fn blinn_phong(
normal: vec3f,
light_dir: vec3f,
view_dir: vec3f,
diffuse_color: vec3f,
specular_color: vec3f,
shininess: f32,
) -> vec3f {
let ambient = diffuse_color * 0.1;
let diff = max(dot(normal, light_dir), 0.0) * diffuse_color;
let half_dir = normalize(light_dir + view_dir);
let spec = pow(max(dot(normal, half_dir), 0.0), shininess) * specular_color;
return ambient + diff + spec;
}
2.2 顶点和片段着色器
// PBR 着色器(基于物理的渲染)
struct VertexInput {
@location(0) position: vec3f,
@location(1) normal: vec3f,
@location(2) uv: vec2f,
}
struct VertexOutput {
@builtin(position) clip_position: vec4f,
@location(0) world_position: vec3f,
@location(1) world_normal: vec3f,
@location(2) uv: vec2f,
}
struct Uniforms {
mvp: mat4x4f,
model: mat4x4f,
camera_pos: vec3f,
_pad1: f32,
light_pos: vec3f,
_pad2: f32,
light_color: vec3f,
_pad3: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var diffuse_map: texture_2d<f32>;
@group(0) @binding(2) var normal_map: texture_2d<f32>;
@group(0) @binding(3) var sampler0: sampler;
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clip_position = u.mvp * vec4f(input.position, 1.0);
output.world_position = (u.model * vec4f(input.position, 1.0)).xyz;
output.world_normal = (u.model * vec4f(input.normal, 0.0)).xyz;
output.uv = input.uv;
return output;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4f {
let N = normalize(input.world_normal);
let L = normalize(u.light_pos - input.world_position);
let V = normalize(u.camera_pos - input.world_position);
let H = normalize(L + V);
// 采样纹理
let albedo = textureSample(diffuse_map, sampler0, input.uv).rgb;
// PBR 参数(简化版)
let metallic = 0.0;
let roughness = 0.5;
let ao = 1.0;
// 漫反射
let F0 = mix(vec3f(0.04), albedo, metallic);
let NdotL = max(dot(N, L), 0.0);
let NdotV = max(dot(N, V), 0.001);
let NdotH = max(dot(N, H), 0.0);
let VdotH = max(dot(V, H), 0.0);
// Cook-Torrance BRDF
let alpha = roughness * roughness;
let alpha2 = alpha * alpha;
// GGX 法线分布
let denom = NdotH * NdotH * (alpha2 - 1.0) + 1.0;
let D = alpha2 / (3.14159265 * denom * denom);
// Fresnel(Schlick 近似)
let F = F0 + (1.0 - F0) * pow(1.0 - VdotH, 5.0);
// Smith 几何遮蔽
let k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
let G1 = NdotV / (NdotV * (1.0 - k) + k);
let G2 = NdotL / (NdotL * (1.0 - k) + k);
let G = G1 * G2;
let specular = (D * F * G) / (4.0 * NdotV * NdotL + 0.001);
// 最终颜色
let kD = (vec3f(1.0) - F) * (1.0 - metallic);
let Lo = (kD * albedo / 3.14159265 + specular) * u.light_color * NdotL;
let ambient = vec3f(0.03) * albedo * ao;
let color = ambient + Lo;
// HDR → LDR(Reinhard 色调映射)
let mapped = color / (color + vec3f(1.0));
return vec4f(mapped, 1.0);
}
三、Compute Shader:浏览器端的 GPU 通用计算
3.1 为什么 Compute Shader 是游戏规则改变者?
WebGL 2.0 无法做 GPU 通用计算——你必须把数据编码成纹理,用片段着色器"假装"计算,再读回纹理。WebGPU 的 Compute Shader 让 GPU 计算成为浏览器的一等公民。
3.2 实战:100 万粒子模拟
// WebGPU Compute Shader:100 万粒子 N-body 模拟
const PARTICLE_COUNT = 1_000_000;
const computeShader = `
struct Particle {
pos: vec2f,
vel: vec2f,
}
struct SimParams {
delta_time: f32,
gravity: f32,
damping: f32,
particle_count: u32,
}
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(0) @binding(1) var<uniform> params: SimParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
if (i >= params.particle_count) {
return;
}
var p = particles[i];
// 计算引力(简化:向中心吸引 + 邻近粒子排斥)
let center = vec2f(0.0, 0.0);
let to_center = center - p.pos;
let dist = length(to_center);
// 引力
let gravity_force = normalize(to_center) * params.gravity / (dist + 0.1);
// 速度更新
p.vel += gravity_force * params.delta_time;
// 阻尼
p.vel *= params.damping;
// 位置更新
p.pos += p.vel * params.delta_time;
// 边界碰撞
if (abs(p.pos.x) > 1.0) {
p.vel.x *= -0.8;
p.pos.x = clamp(p.pos.x, -1.0, 1.0);
}
if (abs(p.pos.y) > 1.0) {
p.vel.y *= -0.8;
p.pos.y = clamp(p.pos.y, -1.0, 1.0);
}
particles[i] = p;
}
`;
// 创建 Compute Pipeline
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: device.createShaderModule({ code: computeShader }),
entryPoint: 'main',
}
});
// 创建粒子数据缓冲区
const particleData = new Float32Array(PARTICLE_COUNT * 4);
for (let i = 0; i < PARTICLE_COUNT; i++) {
particleData[i * 4 + 0] = (Math.random() - 0.5) * 2; // pos.x
particleData[i * 4 + 1] = (Math.random() - 0.5) * 2; // pos.y
particleData[i * 4 + 2] = (Math.random() - 0.5) * 0.1; // vel.x
particleData[i * 4 + 3] = (Math.random() - 0.5) * 0.1; // vel.y
}
const particleBuffer = device.createBuffer({
size: particleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(particleBuffer, 0, particleData);
// 执行 Compute
function simulate() {
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
// 工作组数量 = ceil(PARTICLE_COUNT / 256)
const workgroupCount = Math.ceil(PARTICLE_COUNT / 256);
passEncoder.dispatchWorkgroups(workgroupCount);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(simulate);
}
simulate();
// 性能对比(100 万粒子):
// JavaScript CPU: ~2 fps
// WebGL Transform Feedback: ~15 fps
// WebGPU Compute Shader: ~60 fps ← 30 倍提升!
3.3 实战:浏览器端矩阵乘法
// WebGPU Compute Shader:GPU 矩阵乘法
// 比 JavaScript 快 100 倍
const matrixShader = `
struct Matrix {
size: vec2u, // rows, cols
data: array<f32>,
}
@group(0) @binding(0) var<storage, read> mat_a: Matrix;
@group(0) @binding(1) var<storage, read> mat_b: Matrix;
@group(0) @binding(2) var<storage, read_write> mat_c: Matrix;
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3u) {
let row = id.x;
let col = id.y;
if (row >= mat_a.size.x || col >= mat_b.size.y) {
return;
}
var sum: f32 = 0.0;
for (var i: u32 = 0u; i < mat_a.size.y; i = i + 1u) {
sum += mat_a.data[row * mat_a.size.y + i]
* mat_b.data[i * mat_b.size.y + col];
}
mat_c.data[row * mat_b.size.y + col] = sum;
}
`;
// 性能对比(1024 × 1024 矩阵乘法):
// JavaScript (纯 CPU): ~2,500 ms
// WebGPU Compute: ~25 ms ← 100 倍加速!
四、wgpu:Rust 的跨平台 WebGPU 实现
4.1 为什么 wgpu 重要?
wgpu 的跨平台能力:
┌─────────────┐
│ Rust 代码 │
└──────┬──────┘
│
┌──────┴──────┐
│ wgpu API │
└──────┬──────┘
│
┌──────────┼──────────┐
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
│Vulkan │ │ Metal │ │D3D12 │
│Linux/ │ │macOS/ │ │Windows│
│Win │ │iOS │ │ │
└───────┘ └───────┘ └───────┘
│ │ │
└──────────┼──────────┘
│
┌──────┴──────┐
│ WebAssembly │
│ (浏览器) │
└─────────────┘
一套 Rust 代码 → 5 个平台
4.2 wgpu Rust 实战:三角形渲染
// wgpu Rust:跨平台三角形
use wgpu::{include_wgsl, Backends, DeviceDescriptor, PowerPreference};
use winit::{event::Event, event_loop::EventLoop, window::Window};
async fn run() {
// 初始化窗口和事件循环
let event_loop = EventLoop::new().unwrap();
let window = Window::new(&event_loop).unwrap();
// 创建 wgpu 实例
let instance = wgpu::Instance::new(Backends::all());
let surface = instance.create_surface(&window).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(&DeviceDescriptor::default(), None)
.await
.unwrap();
// 着色器(内嵌 WGSL)
let shader = device.create_shader_module(include_wgsl!("shader.wgsl"));
// 渲染管线
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Triangle Pipeline"),
layout: None,
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Bgra8UnormSrgb,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
});
// 渲染循环
event_loop.run(move |event, _| {
if let Event::AboutToWait = event {
let output = surface.get_current_texture().unwrap();
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&pipeline);
render_pass.draw(0..3, 0..1); // 3 个顶点
drop(render_pass);
queue.submit(std::iter::once(encoder.finish()));
output.present();
}
}).unwrap();
}
4.3 wgpu 编译到 WebAssembly
# 将 wgpu Rust 应用编译到 Web
# 1. 添加 wasm 依赖
cargo add web-sys wgpu@23.0
# 2. 编译为 wasm
wasm-pack build --target web --out-dir pkg
# 3. HTML 中加载
```html
<!DOCTYPE html>
<html>
<head><title>wgpu Web</title></head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script type="module">
import init from './pkg/my_wgpu_app.js';
init();
</script>
</body>
</html>
同一份代码,5 个平台:
cargo run --target x86_64-unknown-linux-gnu # Linux (Vulkan)
cargo run --target aarch64-apple-darwin # macOS (Metal)
cargo run --target x86_64-pc-windows-msvc # Windows (D3D12)
wasm-pack build --target web # 浏览器 (WebGPU)
cargo run --target aarch64-linux-android # Android (Vulkan)
## 五、从 WebGL 迁移到 WebGPU
### 5.1 概念映射
| WebGL 概念 | WebGPU 概念 |
|-----------|------------|
| `gl.createBuffer()` | `device.createBuffer()` |
| `gl.bindBuffer()` | Buffer 绑定在 BindGroup 中 |
| `gl.createProgram()` | `device.createRenderPipeline()` |
| `gl.uniform*()` | `device.createBindGroup()` + uniform buffer |
| `gl.drawArrays()` | `renderPass.draw()` |
| `gl.readPixels()` | Buffer mapping + `getMappedRange()` |
| GLSL ES 3.0 | WGSL |
| 全局状态机 | 显式对象模型 |
### 5.2 迁移示例:Phong 光照
```javascript
// WebGL → WebGPU 迁移核心模式
// ====== 1. 着色器迁移 ======
// WebGL GLSL:
// uniform mat4 uModelViewMatrix;
// void main() { gl_Position = uModelViewMatrix * vec4(position, 1.0); }
// WebGPU WGSL:
// struct Uniforms { model_view: mat4x4f }
// @group(0) @binding(0) var<uniform> u: Uniforms;
// @vertex fn vs_main(@location(0) pos: vec3f) -> @builtin(position) vec4f {
// return u.model_view * vec4f(pos, 1.0);
// }
// ====== 2. 缓冲区迁移 ======
// WebGL: 创建 + 绑定 + 上传(隐式绑定)
// const buffer = gl.createBuffer();
// gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// WebGPU: 创建 + 上传 + 显式绑定到 BindGroup
const buffer = device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(buffer, 0, data);
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer } }],
});
// ====== 3. 渲染迁移 ======
// WebGL: 全局状态 → 绘制
// gl.useProgram(program);
// gl.bindVertexArray(vao);
// gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
// WebGPU: 命令编码 → 批量提交
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ /* ... */ });
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertexCount);
pass.end();
device.queue.submit([encoder.finish()]);
六、WebGPU 浏览器支持现状
| 浏览器 | 状态 | 版本 |
|---|---|---|
| Chrome | ✅ 稳定 | 113+ |
| Edge | ✅ 稳定 | 113+ |
| Firefox | ✅ 稳定 | 130+ |
| Safari | ✅ 稳定 | 18+ (macOS/iOS) |
| Deno | ✅ 稳定 | 1.38+ |
| Node.js | ⚠️ 实验性 | 需 node-webgpu 包 |
// 特性检测
async function initWebGPU() {
if (!navigator.gpu) {
console.error('WebGPU not supported');
// 回退到 WebGL
return initWebGL();
}
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance',
});
if (!adapter) {
console.error('No GPU adapter found');
return initWebGL();
}
// 检查特性
const requiredFeatures = [];
if (adapter.features.has('float32-filterable')) {
requiredFeatures.push('float32-filterable');
}
if (adapter.features.has('texture-compression-bc')) {
requiredFeatures.push('texture-compression-bc');
}
const device = await adapter.requestDevice({
requiredFeatures,
requiredLimits: {
maxBufferSize: adapter.limits.maxBufferSize,
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
},
});
return { adapter, device };
}
七、总结
7.1 WebGPU 的核心价值
| 维度 | WebGL 2.0 | WebGPU | 影响 |
|---|---|---|---|
| API 模型 | 全局状态机 | 显式对象模型 | 多管线共存,无状态冲突 |
| GPU 计算 | ❌ | Compute Shader | 浏览器可做 ML/物理模拟 |
| Draw Call | ~2000/帧 | ~50000/帧 | 25 倍场景复杂度 |
| 着色器语言 | GLSL | WGSL | 类型安全,结构化错误 |
| 跨平台 Rust | ❌ | wgpu | 一套代码 5 平台 |
7.2 何时用 WebGPU?
✅ 必须用 WebGPU 的场景:
1. 大规模粒子系统(>10 万粒子)
2. GPU 通用计算(矩阵运算、ML 推理、信号处理)
3. 复杂 3D 场景(>2000 Draw Call)
4. 需要跨 Web/桌面/移动的图形应用(wgpu)
⚠️ WebGL 仍然够用的场景:
1. 简单 2D Canvas 效果
2. 低多边形 3D 场景
3. 需要兼容旧浏览器
🚀 迁移策略:
1. 新项目直接用 WebGPU(加 WebGL 回退)
2. 旧项目逐步迁移(着色器 → WGSL、缓冲区 → BindGroup)
3. Rust 图形项目直接用 wgpu(天生跨平台)
一句话总结:WebGPU 不只是 WebGL 的替代品,它是浏览器端 GPU 计算的元标准——Compute Shader 让浏览器从"渲染器"进化为"通用 GPU 计算平台",wgpu 让 Rust 开发者一套代码跑遍 Web/桌面/移动。2026 年,所有主流浏览器已全面支持 WebGPU,现在是入场的最佳时机。
参考资源:
- WebGPU 规范:https://www.w3.org/TR/webgpu/
- WGSL 规范:https://www.w3.org/TR/WGSL/
- wgpu Rust:https://wgpu.rs/
- WebGPU 示例:https://webgpu.github.io/samples/
- Chrome WebGPU 文档:https://developer.chrome.com/docs/web-platform/webgpu/