Material trait:自定义材质
这一节你将实现第一个自定义材质:一个用纯色着色的方块。整条链路只有三个零件——一个 Rust 结构体、一段 WGSL 着色器、一行插件注册。
最简材质
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::AsBindGroup;
use bevy::shader::ShaderRef;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<UniformColorMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<UniformColorMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(UniformColorMaterial {
color: LinearRgba::new(0.2, 0.7, 0.3, 1.0),
})),
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(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct UniformColorMaterial {
#[uniform(0)]
color: LinearRgba,
}
impl Material for UniformColorMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/uniform_color.wgsl".into()
}
}Listing 36-1:最简自定义材质——uniform 纯色
运行:
cargo run -p ch36-shaders --example listing-36-01你会看到一个绿色方块。代码不多,但每一行都有讲究。
#[derive(AsBindGroup)]
UniformColorMaterial 派生了四个 trait:
Asset——让它能放进Assets<T>资产容器TypePath——反射系统的路径标识AsBindGroup——核心:告诉 GPU 如何把 Rust 结构体映射为着色器绑定组Debug、Clone——日常需要
AsBindGroup derive 宏会扫描字段上的属性标注,自动生成绑定组布局和绑定代码。#[uniform(0)] 表示"把 color 字段放到绑定组的第 0 号槽位,作为 uniform buffer 传给着色器"。
impl Material
Material trait 有十几个方法,但全部有默认实现。你只需要覆盖想自定义的部分。这里只覆盖了 fragment_shader(),返回一个 ShaderRef:
fn fragment_shader() -> ShaderRef {
"shaders/uniform_color.wgsl".into()
}"shaders/uniform_color.wgsl".into() 之所以能编译,是因为 ShaderRef 实现了 From<&'static str>,会自动包装为 ShaderRef::Path。ShaderRef 枚举有三个变体:
ShaderRef::Default——使用 Bevy 的默认着色器(PBR)ShaderRef::Handle(Handle<Shader>)——用Assets<Shader>中的句柄ShaderRef::Path(AssetPath)——用资产路径,运行时从磁盘或嵌入资产加载
MaterialPlugin
自定义材质必须注册对应的 MaterialPlugin:
.add_plugins(MaterialPlugin::<UniformColorMaterial>::default())MaterialPlugin<M> 在 Plugin::build 里做了几件事:初始化材质资产类型、注册绑定组布局、添加材质变更检测系统。每个自定义材质类型都需要一个独立的 MaterialPlugin 实例。
使用材质
在 setup 中,材质的使用方式和 StandardMaterial 一样:
MeshMaterial3d(materials.add(UniformColorMaterial { color: ... }))MeshMaterial3d 是一个组件,持有材质的句柄。materials.add() 把材质数据存入 Assets<UniformColorMaterial> 并返回句柄。
对应的 WGSL
#import bevy_pbr::forward_io::VertexOutput
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material_color: vec4<f32>;
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
return material_color;
}@group(#{MATERIAL_BIND_GROUP}) @binding(0) 和 Rust 侧的 #[uniform(0)] 对应。var<uniform> 声明这是一个 uniform buffer 变量。片段函数直接返回这个颜色——没有光照、没有纹理,纯粹演示数据如何从 Rust 流向 GPU。
自定义顶点着色器
默认的顶点着色器对大多数材质够用了。但如果你想在顶点阶段做点事情——比如顶点位移、自定义法线计算——就需要覆盖 vertex_shader()。
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::AsBindGroup;
use bevy::shader::ShaderRef;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<VertexFragmentMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<VertexFragmentMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Sphere::new(0.8).mesh().uv(32, 16))),
MeshMaterial3d(materials.add(VertexFragmentMaterial {})),
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(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct VertexFragmentMaterial {}
impl Material for VertexFragmentMaterial {
fn vertex_shader() -> ShaderRef {
"shaders/vertex_fragment.wgsl".into()
}
fn fragment_shader() -> ShaderRef {
"shaders/vertex_fragment.wgsl".into()
}
}Listing 36-2:自定义顶点着色器与片段着色器
运行:
cargo run -p ch36-shaders --example listing-36-02这个材质同时指定了 vertex_shader() 和 fragment_shader(),指向同一个 .wgsl 文件——WGSL 允许在一个文件里用 @vertex 和 @fragment 标注两个入口函数。
对应的着色器:
#import bevy_pbr::{
mesh_functions::get_world_from_local,
mesh_functions::mesh_position_local_to_clip,
}
struct Vertex {
@builtin(instance_index) instance_index: u32,
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_normal: vec3<f32>,
@location(1) uv: vec2<f32>,
};
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = mesh_position_local_to_clip(
get_world_from_local(vertex.instance_index),
vec4<f32>(vertex.position, 1.0),
);
out.world_normal = vertex.normal;
out.uv = vertex.uv;
return out;
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let light_dir = normalize(vec3<f32>(0.5, 1.0, 0.3));
let ndotl = max(dot(normalize(in.world_normal), light_dir), 0.0);
let base = vec3<f32>(0.2, 0.6, 1.0);
return vec4<f32>(base * (0.3 + 0.7 * ndotl), 1.0);
}Listing 36-2 对应的 WGSL:自定义顶点变换 + 简单漫反射光照
顶点着色器手动完成了坐标变换:用 get_world_from_local 拿模型矩阵,用 mesh_position_local_to_clip 变换到裁剪空间。片段着色器用法线和光照方向做了一个最朴素的 Lambert 漫反射——max(dot(normal, light_dir), 0.0) 控制明暗,基色乘以 (0.3 + 0.7 * ndotl) 保证暗面不是纯黑。
注意 Vertex 结构体的字段必须和 Mesh 的顶点属性布局匹配:@location(0) 是 position,@location(1) 是 normal,@location(2) 是 uv。这是 Bevy 的 Mesh 约定,不能随意调换。
Material trait 的其他方法
除了着色器,Material trait 还有几个常用方法:
| 方法 | 默认值 | 作用 |
|---|---|---|
alpha_mode() | AlphaMode::Opaque | 透明模式:Opaque、Blend、AlphaToCoverage 等 |
depth_bias() | 0.0 | 深度偏移,防止两个面重叠时的 z-fighting |
enable_prepass() | true | 是否启用 prepass(法线/运动向量预渲染) |
enable_shadows() | true | 该材质是否投射和接收阴影 |
specialize() | Ok(()) | 管线特化钩子,下一节会用到 |
这些方法都有合理默认值,只在需要时覆盖。
0.19 的 crate 拆分:bevy_shader 与 bevy_material
Bevy 0.19 对着色器和材质的代码组织做了一次重要的模块化拆分。在此之前,Shader 资产、ShaderRef、load_shader_library! 宏等散布在 bevy_render 中;AlphaMode、绑定组布局描述、管线特化函数等则深埋在 bevy_pbr 里。现在它们各自独立成 crate。
bevy_shader 成为独立 crate,集中管理 Shader 资产类型、ShaderRef 枚举和 load_shader_library! 宏。如果你需要在自定义材质之外的地方加载和引用着色器(比如 UI 材质或 compute shader),直接依赖 bevy_shader 即可,不必拉入整个渲染管线。
bevy_material 同样独立出来,承载材质抽象层的核心类型:AlphaMode、BindGroupLayoutDescriptor、MaterialProperties、以及各种特化(specialize)函数。这次拆分让材质系统的底层基础设施不再和具体的 PBR 实现耦合,为未来支持更多材质类型(如 2D 材质、自定义渲染管线中的材质)打下了基础。对日常使用 Material trait 的开发者来说,这些变化是透明的——你的代码不需要改动,但理解这个拆分有助于在需要深入渲染管线时知道去哪里找对应类型。