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 侧组装指令的极轻量操作,每帧重建完全不会影响性能。
相关推荐
张龙6872 分钟前
RAG 知识库问答系统实战:分块、混合检索、RRF 融合与重排全链路拆解
前端
用户1306095607232 分钟前
第二章学完后,我对 webpack5 前端工程化的一点理解
前端
a1117763 分钟前
小鸡下蛋 小游戏 html
前端·开源·html
码哥DFS11 分钟前
算法练习day1-备战2027届秋招
前端·javascript·数据结构·算法
Bigger14 分钟前
因为一句「华流才是最屌的」,我做了一个中国风设计的 Skill
前端·人工智能·设计
别怪我很水22 分钟前
Vue element admin 浏览器本地存储 localStorage、useStorage
前端·javascript·vue.js
手揽回忆怎么睡23 分钟前
Ubuntu 22.04 64位安装MinIO
linux·前端·ubuntu
铁皮饭盒26 分钟前
网页端OCR, 加载6mb大小模型, 又快又准, 百度这次真香
前端·后端
Web极客码42 分钟前
如何在WordPress中添加动态内容
服务器·前端·wordpress
云浪1 小时前
给 AI Agent 加上语音交互:ASR + 流式 TTS 实战
前端·人工智能·后端