目标:用最原始的 API 画一个绿色三角形。
前置准备 :新建 index.html,必须使用本地服务器(如 VSCode 的 Live Server)启动。
Step 1: 唤醒显卡与关联画布
这是所有逻辑的起点。先把显卡认出来,把画纸铺好。
HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGPU 调试实战 - 第一个三角形</title>
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #222; }
canvas { width: 800px; height: 600px; background-color: #000; }
</style>
</head>
<body>
<canvas id="gpuCanvas" width="800" height="600"></canvas>
<script>
async function main() {
// 1. 请求显卡与设备
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("无法获取 GPU Adapter");
const device = await adapter.requestDevice();
// 2. 配置画布上下文
const canvas = document.getElementById('gpuCanvas');
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: format,
alphaMode: 'premultiplied'
});
// 【调试节点 1】跑到这里,按 F12 看控制台。
// 如果报 `navigator.gpu is undefined` -> 说明你是双击打开的 HTML。必须用 Live Server。
// 如果无报错,说明显卡接管成功。
Step 2: 注入硬编码着色器 (WGSL)
既然是固定三角形,我们不传外部数据,直接在显存里"凭空捏造"三个点。
HTML
// 3. 编写并编译 WGSL 着色器
const wgslCode = `
@vertex
fn vs_main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>( 0.0, 0.5), // 正上方
vec2<f32>(-0.5, -0.5), // 左下角
vec2<f32>( 0.5, -0.5) // 右下角
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 1.0, 0.0, 1.0); // 纯绿色
}
`;
const shaderModule = device.createShaderModule({ code: wgslCode });
// 【调试节点 2】
// WebGPU 的坐标系 (0,0) 在屏幕中心,Y轴是向上的。
// 如果你把代码里的 `0.5` 改成 `-0.5`,三角形的尖端就会朝下。
Step 3: 组装渲染模具 (Pipeline)
把写好的着色器装入管线。管线一旦创建,不可修改。
js
// 4. 创建渲染管线
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs_main'
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format: format }]
},
primitive: { topology: 'triangle-list' }
});
// 【调试节点 3】
// 这里的 `targets: [{ format: format }]` 必须和 Step 1 中画纸的 format 一模一样。
// 如果手写成 'rgba8unorm' 等不匹配的格式,后续绘制时会报 Validation Error。
Step 4: 录制指令与呈现 (Render Loop & 深度解惑)
这里是图形学的核心:拿到新底片 -> 录制动作 -> 提交显卡。
js
// 5. 渲染循环
function render() {
// A. 拿到这一帧的全新底片
const currentView = context.getCurrentTexture().createView();
// B. 开启录制机
const encoder = device.createCommandEncoder();
// C. 设置画板通道
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: currentView,
clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }, // 深灰底色
loadOp: 'clear', // 画之前清空上一帧
storeOp: 'store' // 画完保留在显存上呈现
}]
});
// D. 挂载模具,下达绘制命令
pass.setPipeline(pipeline);
pass.draw(3);
// E. 闭环:必须结束录制
pass.end();
// F. 打包并提交
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
requestAnimationFrame(render);
}
render();
}
main();
</script>
</body>
</html>
💡 核心机制解惑 (针对刚才代码中的 Render Loop)
Q1:代码里的这个 pass 对象是怎么释放的?
- 动作闭环 :代码执行
pass.end()时,是在告诉 WebGPU 编码器:"这个通道录完了,解除锁定"。 - 内存释放 :
pass只是一个普通的 JavaScript 局部常量,当render()函数这一帧跑完后,它就失去了引用。V8 引擎的垃圾回收器(GC)会自动把它在内存中清理掉,不需要手动调用销毁方法。 - 【调试节点 4】 :如果你忘了写
pass.end()直接走到下一步执行encoder.finish(),控制台会立刻报错:Command encoder is locked。因为渲染通道未关闭,指令无法打包提交。
Q2:即使三角形不动,也必须每帧重新录制 pass 和 commandBuffer 吗?不能复用吗?
- 绝对不能复用。
- 原因 :看代码的 A 步骤 。Canvas 系统为了防止画面撕裂,使用的是双缓冲机制,每一帧给你的
currentView都是一张物理地址完全不同的新底片。 - 复用的后果 :如果把
commandBuffer存成全局变量复用,它内部硬编码的永远是第一帧的底片地址。到了第二帧,它尝试往一张已经过期的显存底片上画,浏览器会立刻抛出Invalid State错误。 - 结论:只要渲染目标是动态 Canvas,必须每帧重新录制 Encoder 和 Pass。由于这只是 CPU 侧组装指令的极轻量操作,每帧重建完全不会影响性能。