Skip to content

ShaderType 与 Uniform

Listing 36-1 只传了一个 vec4<f32> 给着色器——用 LinearRgba 就行。但真实场景往往需要传递更复杂的数据:一组参数、一个动画时间戳、多个颜色。这时候你需要自定义 uniform 结构体。

ShaderType derive

ShaderTypebevy::render::render_resource 中的一个 trait,描述一个类型如何映射到 GPU 的 uniform buffer。LinearRgbaVec3Mat4 等 Bevy 内置类型都已经实现了它。你自己的结构体需要手动派生:

rust
#[derive(ShaderType, Debug, Clone)]
struct MaterialParams {
    base_color: LinearRgba,
    pulse: f32,
    _padding: Vec3,
}

ShaderType derive 会自动计算字段的 GPU 内存布局(对齐、大小),并生成序列化代码。注意ShaderType 不自动派生 DebugClone,你需要显式加上——Material trait 要求它的绑定组数据同时满足这两个约束。

_padding 字段不是可选的。GPU 的 uniform buffer 对齐规则比 Rust 严格——vec4<f32> 要求 16 字节对齐。如果你的结构体末尾不足 16 字节的倍数,需要手动补齐。Vec3(三个 f32,12 字节)正好把 f32 后面的空隙填满。

完整示例:脉冲发光

rust
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::AsBindGroup;
use bevy::render::render_resource::ShaderType;
use bevy::shader::ShaderRef;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            MaterialPlugin::<PulseMaterial>::default(),
        ))
        .add_systems(Startup, setup)
        .add_systems(Update, animate_pulse)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<PulseMaterial>>,
) {
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(PulseMaterial {
            params: MaterialParams {
                base_color: LinearRgba::new(0.8, 0.2, 0.4, 1.0),
                pulse: 0.0,
                _padding: Vec3::ZERO,
            },
        })),
        Transform::from_xyz(0.0, 0.5, 0.0),
    ));

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

#[derive(ShaderType, Debug, Clone)]
struct MaterialParams {
    base_color: LinearRgba,
    pulse: f32,
    _padding: Vec3,
}

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct PulseMaterial {
    #[uniform(0)]
    params: MaterialParams,
}

impl Material for PulseMaterial {
    fn fragment_shader() -> ShaderRef {
        "shaders/shader_type_uniform.wgsl".into()
    }
}

fn animate_pulse(time: Res<Time>, mut materials: ResMut<Assets<PulseMaterial>>) {
    for (_, material) in materials.iter_mut() {
        material.params.pulse = time.elapsed_secs() * 3.0;
    }
}

Listing 36-3:ShaderType uniform 结构体——脉冲发光

运行:

console
cargo run -p ch36-shaders --example listing-36-03

方块的颜色会随时间明暗脉冲。秘密在 animate_pulse 系统里:每帧从 Assets<PulseMaterial> 中取出材质,修改 params.pulse 字段。因为 PulseMaterial 持有 #[uniform(0)] 标注的 MaterialParams,Bevy 会在下一帧自动把更新后的数据上传到 GPU。

对应的着色器:

wgsl
#import bevy_pbr::forward_io::VertexOutput

struct MaterialParams {
    base_color: vec4<f32>,
    pulse: f32,
    _padding: vec3<f32>,
};

@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> params: MaterialParams;

@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
    let intensity = 0.5 + 0.5 * sin(params.pulse);
    return vec4<f32>(params.base_color.rgb * intensity, params.base_color.a);
}

WGSL 侧的 MaterialParams 结构体必须和 Rust 侧的字段顺序、类型一一对应:

RustWGSL说明
LinearRgbavec4<f32>4 个 f32
f32f32标量
Vec3vec3<f32>3 个 f32(用于 padding)

着色器里用 sin(params.pulse) 计算一个 0 到 1 的强度值,乘以基色的 RGB 通道——颜色的亮度随时间振荡。

#[uniform(N)] 的工作原理

当你在结构体字段上标注 #[uniform(0)] 时,AsBindGroup derive 宏会:

  1. 为这个字段创建一个绑定点(binding 0),类型为 BindingType::Bufferbuffer_type: UniformBuffer
  2. as_bind_group 实现中,把字段序列化为字节,写入 GPU uniform buffer
  3. bind_group_layout 实现中,声明绑定点的类型和可见阶段

一个绑定组里可以有多个 uniform。只要绑定点编号不冲突(#[uniform(0)]#[uniform(1)]……),你可以把不同参数放在不同槽位。但实践中更常见的做法是把所有参数打包进一个 ShaderType 结构体,用一个 uniform 传过去——减少绑定组切换次数,GPU 更高兴。

何时需要 padding

GPU 的对齐规则来自 WebGPU / WGSL 规范:

  • 标量(f32u32)对齐到自身大小
  • vec2<T> 对齐到 8 字节
  • vec3<T>vec4<T> 对齐到 16 字节
  • 结构体对齐到其最大成员的对齐值

如果你的 Rust 结构体末尾是 f32(4 字节),但后面跟的是结构体结束——如果结构体总大小不是 16 的倍数,WGSL 侧的 struct 会自动补齐,但 ShaderType derive 会报错。解决方案:在末尾加一个 _padding 字段补到 16 字节的倍数。Vec3(12 字节)配 f32(4 字节)正好 16;Vec2(8 字节)配两个 f32 也行。