01:按步解析 —— 绘制固定三角形

目标:用最原始的 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:即使三角形不动,也必须每帧重新录制 passcommandBuffer 吗?不能复用吗?

  • 绝对不能复用
  • 原因 :看代码的 A 步骤 。Canvas 系统为了防止画面撕裂,使用的是双缓冲机制,每一帧给你的 currentView 都是一张物理地址完全不同的新底片。
  • 复用的后果 :如果把 commandBuffer 存成全局变量复用,它内部硬编码的永远是第一帧的底片地址。到了第二帧,它尝试往一张已经过期的显存底片上画,浏览器会立刻抛出 Invalid State 错误。
  • 结论:只要渲染目标是动态 Canvas,必须每帧重新录制 Encoder 和 Pass。由于这只是 CPU 侧组装指令的极轻量操作,每帧重建完全不会影响性能。
相关推荐
漂流瓶jz1 小时前
从TailwindCSS到UnoCSS:原子化CSS框架接入、特性与配置
前端·css·react.js
原鸣清1 小时前
Swift 面试高频五连问:Optional、Task、Actor、Concurrency 和 OC 差异
前端
前端Hardy1 小时前
谁还没⽤过shadcn/ui?114k+星标,不装NPM包,前端组件自由终于实现了
前端·javascript·vue.js
morestrive1 小时前
基于 fabric.js 实现浏览器端矢量 PDF 导出
前端·github
Bolt2 小时前
用 pnpm 11 省掉项目里的 .nvmrc 与 .npmrc
前端·npm·node.js
猪猪聪明_V2 小时前
前端码农的本地项目启动器
前端·javascript
时光不负努力2 小时前
每天一个高级前端知识 - Day 21
前端
暗不需求2 小时前
前端性能优化 防抖与节流完全指南:从原理到最佳实践
前端·javascript·面试
@大迁世界2 小时前
45.什么是内联条件表达式(inline conditional expressions)?在事件处理里怎么用?
开发语言·前端·javascript·react.js·ecmascript