ShaderType 与 Uniform
Listing 36-1 只传了一个 vec4<f32> 给着色器——用 LinearRgba 就行。但真实场景往往需要传递更复杂的数据:一组参数、一个动画时间戳、多个颜色。这时候你需要自定义 uniform 结构体。
ShaderType derive
ShaderType 是 bevy::render::render_resource 中的一个 trait,描述一个类型如何映射到 GPU 的 uniform buffer。LinearRgba、Vec3、Mat4 等 Bevy 内置类型都已经实现了它。你自己的结构体需要手动派生:
#[derive(ShaderType, Debug, Clone)]
struct MaterialParams {
base_color: LinearRgba,
pulse: f32,
_padding: Vec3,
}ShaderType derive 会自动计算字段的 GPU 内存布局(对齐、大小),并生成序列化代码。注意:ShaderType 不自动派生 Debug 和 Clone,你需要显式加上——Material trait 要求它的绑定组数据同时满足这两个约束。
_padding 字段不是可选的。GPU 的 uniform buffer 对齐规则比 Rust 严格——vec4<f32> 要求 16 字节对齐。如果你的结构体末尾不足 16 字节的倍数,需要手动补齐。Vec3(三个 f32,12 字节)正好把 f32 后面的空隙填满。
完整示例:脉冲发光
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 结构体——脉冲发光
运行:
cargo run -p ch36-shaders --example listing-36-03方块的颜色会随时间明暗脉冲。秘密在 animate_pulse 系统里:每帧从 Assets<PulseMaterial> 中取出材质,修改 params.pulse 字段。因为 PulseMaterial 持有 #[uniform(0)] 标注的 MaterialParams,Bevy 会在下一帧自动把更新后的数据上传到 GPU。
对应的着色器:
#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 侧的字段顺序、类型一一对应:
| Rust | WGSL | 说明 |
|---|---|---|
LinearRgba | vec4<f32> | 4 个 f32 |
f32 | f32 | 标量 |
Vec3 | vec3<f32> | 3 个 f32(用于 padding) |
着色器里用 sin(params.pulse) 计算一个 0 到 1 的强度值,乘以基色的 RGB 通道——颜色的亮度随时间振荡。
#[uniform(N)] 的工作原理
当你在结构体字段上标注 #[uniform(0)] 时,AsBindGroup derive 宏会:
- 为这个字段创建一个绑定点(binding 0),类型为
BindingType::Buffer,buffer_type: UniformBuffer - 在
as_bind_group实现中,把字段序列化为字节,写入 GPU uniform buffer - 在
bind_group_layout实现中,声明绑定点的类型和可见阶段
一个绑定组里可以有多个 uniform。只要绑定点编号不冲突(#[uniform(0)]、#[uniform(1)]……),你可以把不同参数放在不同槽位。但实践中更常见的做法是把所有参数打包进一个 ShaderType 结构体,用一个 uniform 传过去——减少绑定组切换次数,GPU 更高兴。
何时需要 padding
GPU 的对齐规则来自 WebGPU / WGSL 规范:
- 标量(
f32、u32)对齐到自身大小 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 也行。