三、WebGPU 基础入门——绘制三角型

本节将通过一个简单的例子,带你一步步认识WebGPU的渲染管线,并学习如何编写顶点着色器和片元着色器。我们将使用TypeScript和Rust两种编程语言来实现这个例子。

由于两种语言渲染的环境不一样,这节就分开来讲解。

1. TypeScript 实现

在开始之前,我们需要先修改一下style.css文件,将画布的尺寸设置为100%。

css 复制代码
canvas {
  border: 1px solid black;
  width: 100vw;
  height: 100vh;
}

body,
html {
  margin: 0;
  padding: 0;
  overflow: hidden;
}

我们接着上一节的代码,将main函数清空,然后请求GPU适配器和设备。

ts 复制代码
async function main() {
  // 请求 GPU 适配器
  const adapter = await navigator?.gpu.requestAdapter({
    powerPreference: "low-power",
  });

  // 请求 GPU 设备
  const device = await adapter?.requestDevice();
  if (!device) {
    throw new Error("Failed to create device");
  }
}

接着我们创建一个Canvas元素,并获取WebGPU的上下文。

ts 复制代码
// 创建画布元素
const canvas = document.createElement("canvas");
document.querySelector("#app")?.appendChild(canvas);

// 获取WebGPU上下文
const ctx = canvas.getContext("webgpu");
if (!ctx) {
  throw new Error("Couldn't get WebGPU context");
}

然后我们配置画布的纹理格式。**TextureFormat(格式)**是WebGPU中定义颜色缓冲区像素存储方式的核心参数,决定了每个像素的位深、颜色空间和内存布局。通过调用getPreferredCanvasFormat(),浏览器会根据当前设备的硬件特性(如GPU架构、驱动支持)自动返回最优的纹理格式,从而确保渲染性能和兼容性。

ts 复制代码
// 获取浏览器推荐的最优画布格式(自动适配设备最佳渲染格式)
const preferredFormat = navigator.gpu.getPreferredCanvasFormat();

// 配置画布上下文,绑定设备并指定纹理格式
ctx.configure({
  device,
  format: preferredFormat, // 格式决定了颜色精度、内存占用和渲染管线兼容性
});

接着在source/triangle.wgsl文件中编写顶点着色器和片元着色器。

顶点着色器vs:根据 vertexIndex 从 pos 数组中选择对应的顶点坐标,形成三角形的三个顶点。

wgsl 复制代码
@vertex
fn vs(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4f {
    var pos = array(vec2f(0.0, 0.5), // 顶点1(顶部中心)
    vec2f(-0.5, -0.5), // 顶点1(顶部中心)
    vec2f(0.5, -0.5) // 顶点1(顶部中心)
    );
    return vec4f(pos[vertex_index], 0.0, 1.0); // 扩展为4D向量
}
  • vertexIndex:通过 @builtin(vertex_index) 获取当前顶点索引(0、1、2),对应三角形的三个顶点。
  • 返回 vec4f 向量,前两个分量 (x, y) 是顶点的二维坐标,z 设为 0.0(假设在XY平面),w 设为 1.0(符合齐次坐标规范)。
  • 通过 @builtin(position) 标记,此向量表示顶点在裁剪空间中的位置: X 范围:-1.0(左)到 +1.0(右) Y 范围:-1.0(底)到 +1.0(顶)

片段着色器fs:计算每个像素(片元)的最终颜色,决定渲染效果。

wgsl 复制代码
@fragment
fn fs() -> @location(0) vec4f {
    return vec4f(1.0, 0.0, 0.0, 1.0); // 纯红色(不透明)
}
  • 返回 vec4f 表示颜色,格式为 RGBA:R=1.0(红色),G=0,B=0,A=1.0(不透明)。
  • 通过 @location(0) 标记,结果写入渲染管线的第一个颜色目标(如画布)。

接着创建着色器模块

ts 复制代码
// 在文件顶部导入着色器文件
import triangle from "../../source/triangle.wgsl?raw";

//...
const shader = device.createShaderModule({
  code: triangle,
});

然后创建渲染管线 在WebGPU中,渲染管线(Render Pipeline)是一组预定义的配置,用于控制图形渲染的整个流程,从顶点数据输入到最终像素输出。

ts 复制代码
// 创建渲染管线
const pipeline = device.createRenderPipeline({
  layout: "auto",
  vertex: {
    module: shader,
    entryPoint: "vs",
  },
  fragment: {
    module: shader,
    entryPoint: "fs",
    targets: [
      {
        format: preferredFormat,
      },
    ],
  },
});

接下来创建渲染命令

ts 复制代码
// main函数中...
function render(){
  // 创建命令编码器(用于记录一系列GPU执行命令)
  const encoder = device.createCommandEncoder();

  // 获取当前Canvas的输出纹理(WebGPU渲染目标)
  const output = ctx.getCurrentTexture();
  const view = output.createView(); // 创建纹理视图用于渲染目标绑定

  // 开始渲染通道配置
  const pass = encoder.beginRenderPass({
    colorAttachments: [ // 配置颜色附件数组(此处仅使用一个主颜色目标)
      {
        view, // 绑定之前创建的纹理视图作为渲染目标
        clearValue: { r: 0, g: 0, b: 0, a: 1 }, // 设置清除颜色为黑色(RGB 0,0,0)
        loadOp: "clear", // 渲染前清除颜色缓冲区
        storeOp: "store", // 渲染完成后将结果存储到颜色缓冲区
      },
    ],
  });

  // 绑定当前渲染管线配置(顶点/片元着色器等)
  pass.setPipeline(pipeline);

  // 执行绘制命令:绘制3个顶点构成的三角形
  // 参数3表示顶点数量(与顶点着色器中数组长度一致)
  pass.draw(3);

  // 结束当前渲染通道的配置
  pass.end();

  // 生成最终的命令缓冲区(包含所有已记录的渲染指令)
  const commandBuffer = encoder.finish(); 
  device.queue.submit([commandBuffer]); // 将命令提交到GPU队列执行
}

最后运行该代码,可以看到一个红色的三角形。

如果你放大浏览器窗口,可能会发现三角形的边缘是块状的。这是因为canvas标签的默认分辨率为 300x150 像素。我们希望调整画布的分辨率,使其与显示的尺寸相匹配。

使用ResizeObserver来监听画布尺寸的变化,并根据新的尺寸重新设置画布的分辨率。

ts 复制代码
const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    const canvas = entry.target;
    const width = entry.contentBoxSize[0].inlineSize;
    const height = entry.contentBoxSize[0].blockSize;
    canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
    canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
    // 重新绘制
    render();
  }
});
observer.observe(canvas);

最后整理一下代码,封装成一个类WebGPUApp。最终代码如下:

ts 复制代码
import "./style.css";
import triangle from "../../source/triangle.wgsl?raw";

class WebGPUApp {
  constructor(
    public device: GPUDevice,
    public queue: GPUQueue,
    public canvas: HTMLCanvasElement,
    public ctx: GPUCanvasContext,
    public pipeline: GPURenderPipeline
  ) {
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const canvas = entry.target as HTMLCanvasElement;
        const width = entry.contentBoxSize[0].inlineSize;
        const height = entry.contentBoxSize[0].blockSize;
        canvas.width = Math.min(width, device.limits.maxTextureDimension2D);
        canvas.height = Math.min(height, device.limits.maxTextureDimension2D);
      }
    });
    observer.observe(canvas);
  }

  public static async create() {
    const adapter = await navigator.gpu.requestAdapter();
    // 请求GPU设备
    const device = await adapter?.requestDevice();
    if (!device) {
      throw new Error("Couldn't request WebGPU device");
    }

    // 创建画布元素
    const canvas = document.createElement("canvas");
    document.querySelector("#app")?.appendChild(canvas);

    // 获取WebGPU上下文
    const ctx = canvas.getContext("webgpu");
    if (!ctx) {
      throw new Error("Couldn't get WebGPU context");
    }

    // 获取首选画布格式
    const preferredFormat = navigator.gpu.getPreferredCanvasFormat();

    // 配置画布上下文
    ctx.configure({
      device,
      format: preferredFormat,
    });

    // 创建着色器模块
    const shader = device.createShaderModule({
      code: triangle,
    });

    // 创建渲染管线
    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: shader,
        entryPoint: "vs",
      },
      fragment: {
        module: shader,
        entryPoint: "fs",
        targets: [
          {
            format: preferredFormat,
          },
        ],
      },
    });
    return new WebGPUApp(device, device.queue, canvas, ctx, pipeline);
  }

  public render() {
    const { device, ctx, pipeline } = this;
    // 创建命令编码器(用于记录一系列GPU执行命令)
    const encoder = device.createCommandEncoder();

    // 获取当前Canvas的输出纹理(WebGPU渲染目标)
    const output = ctx.getCurrentTexture();
    const view = output.createView(); // 创建纹理视图用于渲染目标绑定

    // 开始渲染通道配置
    const pass = encoder.beginRenderPass({
      colorAttachments: [
        // 配置颜色附件数组(此处仅使用一个主颜色目标)
        {
          view, // 绑定之前创建的纹理视图作为渲染目标
          clearValue: { r: 0, g: 0, b: 0, a: 1 }, // 设置清除颜色为黑色(RGB 0,0,0)
          loadOp: "clear", // 渲染前清除颜色缓冲区
          storeOp: "store", // 渲染完成后将结果存储到颜色缓冲区
        },
      ],
    });

    // 绑定当前渲染管线配置(顶点/片元着色器等)
    pass.setPipeline(pipeline);

    // 执行绘制命令:绘制3个顶点构成的三角形
    // 参数3表示顶点数量(与顶点着色器中数组长度一致)
    pass.draw(3);

    // 结束当前渲染通道的配置
    pass.end();

    // 生成最终的命令缓冲区(包含所有已记录的渲染指令)
    const commandBuffer = encoder.finish(); // 修正拼写错误:commanderBuffer → commandBuffer
    device.queue.submit([commandBuffer]); // 将命令提交到GPU队列执行
  }
}

async function main() {
  const app = await WebGPUApp.create();

  // 使用 requestAnimationFrame 实现持续渲染
  const renderLoop = () => {
    app.render();
    requestAnimationFrame(renderLoop);
  };

  requestAnimationFrame(renderLoop);
}

// 调用主函数
main();

由于获取adapterdevice都是异步操作,但是不能在构造函数中使用async关键字,所以我们使用了create静态方法来创建WebGPUApp实例。在构造函数中,使用ResizeObserver来监听画布尺寸的变化,并根据新的尺寸重新设置画布的分辨率。

2. Rust 实现

开始之前先确定依赖是否安装好,Cargo.toml文件如下:

toml 复制代码
[package]
name = "rs-wgpu-learn"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.97"
bytemuck = { version = "1.22.0", features = ["derive"] }
env_logger = "0.11.6"
log = "0.4.26"
parking_lot = "0.12.3"
pollster = "0.4.0"
wgpu = "24.0.1"
winit = "0.30.9"

然后在lib.rs中编写代码: 由于Rust中没有类的概念,所以我们使用结构体来封装WebGPU相关的数据。

rust 复制代码
use anyhow::Result;
use std::sync::Arc;
use wgpu::{Color, include_wgsl};
use winit::window::Window;

// Wgpu应用核心结构体
pub struct WgpuApp {
    pub window: Arc<Window>,                // 窗口对象
    pub surface: wgpu::Surface<'static>,    // GPU表面(用于绘制到窗口)
    pub device: wgpu::Device,               // GPU设备抽象
    pub queue: wgpu::Queue,                 // 命令队列(用于提交GPU命令)
    pub config: wgpu::SurfaceConfiguration, // 表面配置(格式、尺寸等)
    pub pipeline: wgpu::RenderPipeline,     // 渲染管线(包含着色器、状态配置等)
}

impl WgpuApp {
    /// 异步构造函数:初始化WebGPU环境
    pub async fn new(window: Arc<Window>) -> Result<Self> {
        // 1. 创建WebGPU实例
        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());

        // 2. 创建窗口表面
        let surface = instance.create_surface(window.clone())?;

        // 3. 请求图形适配器(选择GPU)
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(), // 默认选择高性能GPU
                compatible_surface: Some(&surface),                 // 需要与表面兼容
                force_fallback_adapter: false,
            })
            .await
            .ok_or_else(|| anyhow::anyhow!("No adapter found"))?;

        // 4. 创建设备和命令队列
        let (device, queue) = adapter
            .request_device(
                &wgpu::DeviceDescriptor {
                    label: Some("Device"),
                    required_features: wgpu::Features::empty(),
                    required_limits: wgpu::Limits::default(),
                    memory_hints: wgpu::MemoryHints::Performance,
                },
                None,
            )
            .await?;

        // 5. 配置表面(设置像素格式、尺寸等)
        let config = surface
            .get_default_config(
                &adapter,
                window.inner_size().width.max(1),  // 确保最小宽度为1
                window.inner_size().height.max(1), // 确保最小高度为1
            )
            .unwrap();
        surface.configure(&device, &config);

        // 6. 创建着色器模块(加载WGSL着色器)
        let shader = device.create_shader_module(include_wgsl!("../../source/triangle.wgsl"));

        // 7. 创建渲染管线
        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("Render Pipeline"),
            layout: None, // 使用默认管线布局
            vertex: wgpu::VertexState {
                module: &shader,         // 顶点着色器模块
                entry_point: Some("vs"), // 入口函数
                buffers: &[],            // 顶点缓冲区布局(本示例为空)
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,         // 片元着色器模块
                entry_point: Some("fs"), // 入口函数
                targets: &[Some(wgpu::ColorTargetState {
                    format: config.format,                  // 使用表面配置的格式
                    blend: Some(wgpu::BlendState::REPLACE), // 混合模式:直接替换
                    write_mask: wgpu::ColorWrites::ALL,     // 允许写入所有颜色通道
                })],
                compilation_options: Default::default(),
            }),
            primitive: Default::default(), // 使用默认图元配置(三角形列表)
            depth_stencil: None,           // 禁用深度/模板测试
            multisample: Default::default(), // 多重采样配置
            multiview: None,
            cache: None,
        });

        Ok(Self {
            window,
            surface,
            device,
            queue,
            config,
            pipeline,
        })
    }

    /// 执行渲染操作
    pub fn render(&mut self) -> Result<()> {
        // 1. 获取当前帧缓冲区
        let output = self.surface.get_current_texture()?;

        // 2. 创建纹理视图
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        // 3. 创建命令编码器
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());

        // 4. 开始渲染通道
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(Color::BLACK), // 用黑色清除背景
                        store: wgpu::StoreOp::Store,             // 存储渲染结果
                    },
                    resolve_target: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            // 5. 设置渲染管线
            pass.set_pipeline(&self.pipeline);

            // 6. 绘制调用(绘制3个顶点,组成一个三角形)
            pass.draw(0..3, 0..1);
        }

        // 7. 提交命令到队列
        let command_buffer = encoder.finish();
        self.queue.submit(std::iter::once(command_buffer));

        // 8. 呈现渲染结果
        output.present();

        Ok(())
    }

    /// 处理窗口大小变化
    pub fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
        self.config.width = size.width.max(1);
        self.config.height = size.height.max(1);
        // 重新配置表面(更新尺寸)
        self.surface.configure(&self.device, &self.config);
    }
}
  • 初始化阶段:创建WebGPU实例→适配器→设备/队列→配置表面→创建渲染管线
  • 渲染循环:获取帧缓冲区→创建命令→设置渲染通道→执行绘制→提交命令→呈现结果
  • 窗口调整:更新表面配置确保渲染尺寸与窗口匹配

然后在main.rs中编写下面代码:

rust 复制代码
use log::info;
use parking_lot::Mutex;
use rs_wgpu_learn::WgpuApp;
use std::{rc::Rc, sync::Arc};
use winit::{
    application::ApplicationHandler, event::WindowEvent, event_loop::EventLoop,
    window::WindowAttributes,
};

fn main() -> anyhow::Result<()> {
    // 初始化日志系统(配置为仅显示INFO及以上级别的日志)
    env_logger::builder()
        .filter_level(log::LevelFilter::Info)
        .init();

    // 创建事件循环(窗口系统的核心事件处理器)
    let event_loop = EventLoop::new()?;
    // 创建应用实例并运行事件循环
    let mut app = App::default();
    event_loop.run_app(&mut app)?;
    Ok(())
}

// 主应用结构体
#[derive(Default)]
struct App {
    /// WGPU应用实例的共享引用(使用 Rc + Mutex 实现跨线程安全访问)
    wgpu_app: Rc<Mutex<Option<WgpuApp>>>,
}

// ApplicationHandler trait 是 winit 窗口库的核心事件处理接口,主要用于管理应用程序生命周期和窗口事件。
impl ApplicationHandler for App {
    /// 当应用恢复/启动时触发(主要初始化入口)
    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
        info!("Resumed");
        // 防止重复初始化
        if self.wgpu_app.lock().is_some() {
            return;
        }

        // 1. 创建窗口
        let window = Arc::new(
            event_loop
                .create_window(
                    WindowAttributes::default().with_title("Wgpu Learn"), // 设置窗口标题
                )
                .unwrap(),
        );

        // 2. 同步初始化WGPU应用(使用pollster阻塞等待异步初始化)
        let wgpu_app = pollster::block_on(WgpuApp::new(window)).unwrap();

        // 3. 存储WGPU应用实例
        self.wgpu_app.lock().replace(wgpu_app);
    }

    /// 处理窗口事件(核心事件循环)
    fn window_event(
        &mut self,
        event_loop: &winit::event_loop::ActiveEventLoop,
        _window_id: winit::window::WindowId,
        event: winit::event::WindowEvent,
    ) {
        let mut app_guard = self.wgpu_app.lock();
        // 确保WGPU应用已初始化
        if app_guard.is_none() {
            return;
        }
        let app = app_guard.as_mut().unwrap();

        match event {
            // 关闭窗口请求
            WindowEvent::CloseRequested => {
                info!("Window close requested");
                event_loop.exit(); // 退出事件循环
            }

            // 重绘请求(驱动渲染循环)
            WindowEvent::RedrawRequested => {
                // 执行窗口预呈现通知
                app.window.pre_present_notify();

                // 执行实际渲染操作
                app.render().unwrap();

                // 请求下一帧重绘(维持持续渲染)
                app.window.request_redraw();
            }

            // 窗口大小变化事件
            WindowEvent::Resized(size) => {
                // 更新WGPU表面配置
                app.resize(size);
                info!("Window resized to {:?}", size);
            }

            // 其他未处理事件
            _ => {}
        }
    }
}

最后在rs-wgpu-learn文件夹下运行cargo run命令来启动应用程序。

最后

本节源码位于Github如果本文对你有启发,欢迎点赞⭐收藏📚关注👀,你的支持是我持续创作深度技术内容的最大动力。

相关推荐
Hello.Reader3 小时前
调试 Rust + WebAssembly 版康威生命游戏
游戏·rust·wasm
helloweilei6 小时前
Rust学习笔记 之 char类型
rust
三棵杨树7 小时前
TypeScript从零开始(五):条件类型与映射类型
typescript
随笔记7 小时前
如何用vite构建工具搭建react项目
react.js·typescript·vite
星光不问赶路人7 小时前
TypeScript类型unknown
前端·typescript
无名之逆18 小时前
Hyperlane:Rust 生态中的轻量级高性能 HTTP 服务器库,助力现代 Web 开发
服务器·开发语言·前端·后端·http·面试·rust
无名之逆19 小时前
轻量级、高性能的 Rust HTTP 服务器库 —— Hyperlane
服务器·开发语言·前端·后端·http·rust
无名之逆20 小时前
探索Hyperlane:用Rust打造轻量级、高性能的Web后端框架
服务器·开发语言·前端·后端·算法·rust
Source.Liu20 小时前
【CXX】6.9 CxxVector<T> — std::vector<T>
c++·rust·cxx