四、WebGPU 基础入门——Uniform 缓冲区与内存对齐

四、WebGPU 基础入门------Uniform 缓冲区与内存对齐

上一节,我们通过 WebGPU 创建了一个简单的三角形,并渲染到屏幕上。这节我们将学习如何使用 Uniform 缓冲区传递数据到着色器中。

1. Uniform 缓冲区

Uniform缓冲区是一种用于存储着色器中共享的全局统一数据的机制,支持在单次绘制调用中被所有着色器阶段读取使用,但不允许写入。

常见用法是传递一些不需要频繁更新的数据,比如:

  • 物体的变换矩阵(模型矩阵、视图矩阵、投影矩阵)。
  • 光照参数(光源位置、颜色、强度)。
  • 材质参数(漫反射系数、镜面反射系数)。
  • 纹理采样器(纹理单元、过滤模式、包裹模式)。
  • 动画参数(动画时间、帧数)。
  • 其他全局状态(如时间、帧数、视口大小等)。

2. 实战

首先,在uniform.wgsl中,我们先声明一个结构体。

wgsl 复制代码
struct Params {
    color: vec4f,
    offset: vec2f,
    scale: f32,
}

然后,我们声明了一个类型为该结构体的uniform变量。变量名为params,类型为Params

wgsl 复制代码
@group(0) @binding(0) var<uniform> params: Params;

然后我们更改顶点着色器返回的内容,以使用 uniforms。

wgsl 复制代码
struct VertexOutput {
    @builtin(position) position: vec4f,
}

@vertex
fn vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
    var pos = array(
        vec2f(0.0, 0.5),
        vec2f(-0.5, -0.5),
        vec2f(0.5, -0.5),
    );

    var output = VertexOutput(
        vec4f(pos[vertex_index] * params.scale + params.offset, 0.0, 1.0),
    );

    return output;
}

可以看到,我们将顶点位置乘以 scale,然后加上 offset。这样我们就可以设置三角形的大小并对其进行定位。

我们还修改了片段着色器,以返回 uniforms 的颜色

wgsl 复制代码
@fragment
fn fs() -> @location(0) vec4f {
    return params.color;
}

然后在rs-wgpu-learn/src/lib.rs中新建结构体。

rust 复制代码
#[repr(C)]
#[derive(Debug, PartialEq, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Params {
    pub color: [f32; 4],
    pub offset: [f32; 2],
    pub scale: f32,
}

修改WgpuApp添加bind_group字段。

rust 复制代码
pub struct WgpuApp {
    pub window: Arc<Window>,
    pub surface: wgpu::Surface<'static>,
    pub device: wgpu::Device,
    pub queue: wgpu::Queue,
    pub config: wgpu::SurfaceConfiguration,
    pub pipeline: wgpu::RenderPipeline,
    pub bind_group: wgpu::BindGroup, // 添加bind_group字段
}

接着创建Uniform缓冲区

rust 复制代码
impl WgpuApp {
    pub async fn new(window: Arc<Window>) -> Result<Self> {
        // ...
        // 修改导入wgsl文件的路径
        let shader = device.create_shader_module(include_wgsl!("../../source/uniform.wgsl"));

        // ...
        // 新增
        let params = Params {
            color: [1.0, 1.0, 0.0, 1.0],
            offset: [0.5, 0.5],
            scale: 0.5,
        };

        let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Uniform Buffer"),
            contents: bytemuck::bytes_of(&params),
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: None,
            layout: &pipeline.get_bind_group_layout(0),
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: buffer.as_entire_binding(),
            }],
        });

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

然后在渲染流程中使用bind_group

rust 复制代码
pub fn render(&mut self) -> Result<()> {
    // ...
    {
        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,
        });

        pass.set_pipeline(&self.pipeline);

        // 设置绑定组
        pass.set_bind_group(0, &self.bind_group, &[]);

        pass.draw(0..3, 0..1);
    }


    //...

    Ok(())
}

然后cargo run运行程序,不出意外,可以看到下面报错。

这是由于WebGPU数据内存布局导致的。建议先阅读WebGPU Memory Layout

WebGPU 内存对齐规则

以下是 WGSL 内存对齐的核心规则总结,按数据类型分类整理:

1. 基本类型对齐规则

标量类型 :对齐值等于自身字节大小(均为 2 的幂次)。 • i32/u32/f32 → 4 字节 • f16 → 2 字节 • 向量类型 :对齐值为元素类型大小的 2 的幂次向上取整。 • vec2<f32> → 8 字节(元素类型 4 字节 × 2) • vec3<f32> → 16 字节(因存储缓冲区特殊规则) • 矩阵类型 :每列对齐到对应向量类型的对齐值。 • mat3x3<f32> 每列 vec3<f32> → 对齐到 16 字节

2. 结构体对齐规则

成员对齐 : • 首个成员偏移量为 0,后续成员偏移量必须是其类型对齐值的整数倍。 • 示例:struct { f32 a; vec2<f32> b; } 中,a 占 4 字节后需填充 4 字节,使 b 偏移 8 字节。 • 整体对齐:结构体总大小必须是其成员最大对齐值的整数倍。 • 示例:若最大对齐值为 16,则结构体大小需向上填充至 16 的倍数(如 12→16)。

3. 数组对齐规则

元素间隔(Stride) :等于元素类型的对齐值。

array<vec3<f32>> 每个元素间隔为 16 字节(12 字节数据 + 4 字节填充)。


Params 结构体的字段及其类型如下:

  • color: [f32; 4]
    • 类型:vec4<f32>
    • 大小:16 字节(4 个 f32 每个占 4 字节)
    • 对齐:16 字节(vec4<f32> 的对齐要求)
    • 偏移量:0 字节(结构体的第一个字段,偏移量从 0 开始)
  • offset: [f32; 2]
    • 类型:vec2<f32>
    • 大小:8 字节(2 个 f32 每个占 4 字节)
    • 对齐:8 字节(vec2<f32> 的对齐要求)
    • 偏移量:16 字节(前一个字段 color 的大小为 16 字节,满足对齐要求)
  • scale: f32
    • 类型:f32
    • 大小:4 字节
    • 对齐:4 字节(f32 的对齐要求)
    • 偏移量:24 字节(前一个字段 offset 的大小为 8 字节,满足对齐要求)
    • 补全: scale 之后需要填充 4 字节,以满足结构体整体对齐要求(最大对齐值为 16 字节)。

参考下图

然后修改Params 结构体,添加补全字段 _pad_scale,并将其对齐到 16 字节。

rust 复制代码
#[repr(C, align(16))]
#[derive(Debug, PartialEq, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Params {
    /// size: 16, offset: 0, type: `vec4<f32>`
    pub color: [f32; 4],
    /// size: 8, offset: 16 (2*8), type: `vec2<f32>`
    pub offset: [f32; 2],
    /// size: 4, offset: 24 (4*6), type: `f32`
    pub scale: f32,
    pub _pad_scale: [u8; 0x8 - core::mem::size_of::<f32>()],
}

impl Params {
    pub const fn new(color: [f32; 4], offset: [f32; 2], scale: f32) -> Self {
        Self {
            color,
            offset,
            scale,
            _pad_scale: [0; 0x8 - core::mem::size_of::<f32>()],
        }
    }
}

impl WgpuApp {
    /// 异步构造函数:初始化WebGPU环境
    pub async fn new(window: Arc<Window>) -> Result<Self> {

        // ...
        // 使用new函数创建Params
        let params = Params::new([1.0, 1.0, 0.0, 1.0], [0.5, 0.5], 0.5);

        let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("Uniform Buffer"),
            contents: bytemuck::bytes_of(&params),
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
        });

        // ...
    }

}

然后cargo run运行,不出意外,可以得到一个黄色的三角形。

这时候,可能有同学会问了,博主博主,你的内存布局计算太难了,有没有更简单又好用的方法。 有的,兄弟有的,经过我的调研,发现一个非常好用的库叫wgsl-bindgen。有兴趣的可以查看文档,这里就不详细介绍了。

接下来介绍在typescript中使用Uniform缓冲区。 首先安装依赖:

bash 复制代码
pnpm add webgpu-utils

然后创建Uniform缓冲区以及绑定组,最后在渲染流程中使用绑定组。 逻辑与上面rust版本一样,这里就不再赘述了。 具体代码如下:

ts 复制代码
import "./style.css";
import uniformWgsl from "../../source/uniform.wgsl?raw";
import { makeShaderDataDefinitions, makeStructuredView } from "webgpu-utils";

class WebGPUApp {
  constructor(
    public device: GPUDevice,
    public queue: GPUQueue,
    public canvas: HTMLCanvasElement,
    public ctx: GPUCanvasContext,
    public pipeline: GPURenderPipeline,
    public bindGroup: GPUBindGroup
  ) {
    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: uniformWgsl, // 加载 WGSL 着色器代码
    });

    // 创建渲染管线
    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: shader,
        entryPoint: "vs", // 顶点着色器入口
      },
      fragment: {
        module: shader,
        entryPoint: "fs", // 片元着色器入口
        targets: [
          {
            format: preferredFormat, // 渲染目标格式
          },
        ],
      },
    });

    // 使用工具函数解析着色器中的 uniform 数据定义
    const defs = makeShaderDataDefinitions(uniformWgsl);
    const params = makeStructuredView(defs.uniforms.params);

    // 设置 uniform 数据的初始值
    params.set({
      color: [1.0, 1.0, 0, 1], // 设置颜色为黄色
      scale: 0.5, // 缩放因子
      offset: [0.5, 0.5], // 偏移量
    });

    // 创建 GPU 缓冲区以存储 uniform 数据
    const buffer = device.createBuffer({
      size: params.arrayBuffer.byteLength, // 缓冲区大小与 uniform 数据大小一致
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, // 用作 uniform 缓冲区并支持写入
    });

    // 将 uniform 数据写入缓冲区
    device.queue.writeBuffer(buffer, 0, params.arrayBuffer);

    // 创建绑定组,将 uniform 缓冲区绑定到着色器
    const bindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0), // 获取绑定组布局
      entries: [
        {
          binding: 0, // 对应着色器中的 binding 位置
          resource: {
            buffer, // 绑定 uniform 缓冲区
          },
        },
      ],
    });

    return new WebGPUApp(
      device,
      device.queue,
      canvas,
      ctx,
      pipeline,
      bindGroup
    );
  }

  public render() {
    const { device, ctx, pipeline, bindGroup } = 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);
    pass.setBindGroup(0, bindGroup); // 将 uniform 数据绑定到渲染管线
    // 执行绘制命令:绘制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();

最后

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

相关推荐
曹牧3 分钟前
HTML字符实体和转义字符串
前端·html
小希爸爸9 分钟前
2、中医基础入门和养生
前端·后端
局外人LZ13 分钟前
前端项目搭建集锦:vite、vue、react、antd、vant、ts、sass、eslint、prettier、浏览器扩展,开箱即用,附带项目搭建教程
前端·vue.js·react.js
G_GreenHand27 分钟前
Dhtmlx Gantt教程
前端
鹿九巫28 分钟前
【CSS】层叠,优先级与继承(四):层叠,优先级与继承的关系
前端·css
卓怡学长30 分钟前
w304基于HTML5的民谣网站的设计与实现
java·前端·数据库·spring boot·spring·html5
宝拉不想努力了33 分钟前
vue element使用el-table时,切换tab,table表格列项发生错位问题
前端·vue.js·elementui
YONG823_API37 分钟前
深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
java·前端·自动化
鱼樱前端38 分钟前
前端必知必会:JavaScript 对象与数组克隆的 7 种姿势,从浅入深一网打尽!
前端·javascript
小希爸爸1 小时前
1、中医基础入门和养生
前端·后端