纹理与采样器
纯色方块看腻了?这一节给材质加上纹理。AsBindGroup derive 宏用 #[texture] 和 #[sampler] 属性把纹理和采样器映射到着色器的绑定点——和 #[uniform] 一样声明式,但底层是完全不同的 GPU 资源类型。
带纹理的材质
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::<TexturedMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<TexturedMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(TexturedMaterial {
color: LinearRgba::WHITE,
base_texture: Some(asset_server.load("branding/icon.png")),
})),
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 TexturedMaterial {
#[uniform(0)]
color: LinearRgba,
#[texture(1)]
#[sampler(2)]
base_texture: Option<Handle<Image>>,
}
impl Material for TexturedMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/texture_sampler.wgsl".into()
}
}Listing 36-4:纹理 + 采样器 + uniform 颜色
运行:
cargo run -p ch36-shaders --example listing-36-04方块的每一面都贴上了 branding/icon.png 图标,同时被 color(这里设为白色,不影响原色)调制。
#[texture(N)] 与 #[sampler(N)]
#[texture(1)]
#[sampler(2)]
base_texture: Option<Handle<Image>>,两个属性标注在同一个字段上:
#[texture(1)]——把纹理的texture_2d<f32>绑定到 binding 1#[sampler(2)]——把纹理配套的采样器绑定到 binding 2
类型是 Option<Handle<Image>>。None 时 Bevy 会提供一个默认的 1x1 白色纹理——着色器代码不需要处理"没有纹理"的分支。
对应的 WGSL
#import bevy_pbr::forward_io::VertexOutput
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material_color: vec4<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_texture: texture_2d<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var base_sampler: sampler;
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let tex = textureSample(base_texture, base_sampler, mesh.uv);
return material_color * tex;
}三个绑定——uniform 颜色、纹理、采样器——分别对应 Rust 侧的 #[uniform(0)]、#[texture(1)]、#[sampler(2)]。textureSample 是 WGSL 内置函数,接收纹理、采样器和 UV 坐标,返回采样颜色。mesh.uv 来自 VertexOutput,是顶点着色器插值出来的纹理坐标。
材质颜色和纹理颜色逐分量相乘(material_color * tex)——白色(1,1,1,1)不影响纹理原色,其他颜色会"染"上去。这是材质调制纹理的标准手法。
Shader Defs:条件编译
有时你需要同一个着色器在不同实体上表现不同——有的实体要反色、有的不需要。Shader defs(着色器定义)就是 WGSL 的条件编译机制。
use bevy::mesh::MeshVertexBufferLayoutRef;
use bevy::pbr::{MaterialPipeline, MaterialPipelineKey};
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::render_resource::{
AsBindGroup, RenderPipelineDescriptor, SpecializedMeshPipelineError,
};
use bevy::shader::ShaderRef;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
MaterialPlugin::<ConditionalMaterial>::default(),
))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ConditionalMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(ConditionalMaterial {
color: LinearRgba::new(0.2, 0.6, 1.0, 1.0),
invert: false,
})),
Transform::from_xyz(-1.0, 0.5, 0.0),
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(ConditionalMaterial {
color: LinearRgba::new(0.2, 0.6, 1.0, 1.0),
invert: true,
})),
Transform::from_xyz(1.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)]
#[bind_group_data(ConditionalMaterialKey)]
struct ConditionalMaterial {
#[uniform(0)]
color: LinearRgba,
invert: bool,
}
impl Material for ConditionalMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/shader_defs.wgsl".into()
}
fn specialize(
_pipeline: &MaterialPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if key.bind_group_data.invert {
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader_defs.push("INVERT_COLOR".into());
}
Ok(())
}
}
#[repr(C)]
#[derive(Eq, PartialEq, Hash, Copy, Clone)]
struct ConditionalMaterialKey {
invert: bool,
}
impl From<&ConditionalMaterial> for ConditionalMaterialKey {
fn from(material: &ConditionalMaterial) -> Self {
Self {
invert: material.invert,
}
}
}Listing 36-5:用 shader defs 实现条件编译
运行:
cargo run -p ch36-shaders --example listing-36-05左边方块显示原始蓝色,右边方块显示反色(黄色)。两个实体用的是同一个材质类型、同一段着色器代码——区别仅在于 invert 字段为 true 还是 false。
#[bind_group_data] 与管线特化
Shader defs 不是通过 uniform 传递的——它们在管线创建时就确定了,影响的是着色器的编译产物。Bevy 用管线特化(pipeline specialization)机制实现这一点:
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
#[bind_group_data(ConditionalMaterialKey)]
struct ConditionalMaterial {
#[uniform(0)]
color: LinearRgba,
invert: bool,
}#[bind_group_data(ConditionalMaterialKey)] 告诉 Bevy:"这个材质的管线变体由 ConditionalMaterialKey 决定"。ConditionalMaterialKey 必须实现 Eq + PartialEq + Hash + Clone——Bevy 用它做管线缓存的哈希键。
invert 字段没有 #[uniform] 标注——它不会传给 GPU,只在 CPU 侧用来决定管线变体。
fn specialize(
_pipeline: &MaterialPipeline,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayoutRef,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
if key.bind_group_data.invert {
let fragment = descriptor.fragment.as_mut().unwrap();
fragment.shader_defs.push("INVERT_COLOR".into());
}
Ok(())
}specialize 方法在管线创建时被调用。key.bind_group_data 就是从 ConditionalMaterial 转换来的 ConditionalMaterialKey。当 invert 为 true 时,向管线的片段着色器阶段注入一个 "INVERT_COLOR" shader def。
From<&ConditionalMaterial> for ConditionalMaterialKey 实现负责这个转换:
impl From<&ConditionalMaterial> for ConditionalMaterialKey {
fn from(material: &ConditionalMaterial) -> Self {
Self { invert: material.invert }
}
}WGSL 侧的 #ifdef
#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> {
#ifdef INVERT_COLOR
return vec4<f32>(1.0 - material_color.rgb, material_color.a);
#else
return material_color;
#endif
}#ifdef INVERT_COLOR / #else / #endif 是 Bevy 的 shader 预处理指令。当 "INVERT_COLOR" shader def 存在时,编译走第一个分支(反色);否则走第二个分支(原色)。这是编译期行为——两种情况生成不同的管线,运行时没有分支开销。
ShaderDefVal 的类型
Shader def 不只是布尔开关。ShaderDefVal 枚举有三种形式:
Bool(String, bool)——"INVERT_COLOR".into()产生Bool("INVERT_COLOR", true)Int(String, i32)——ShaderDefVal::Int("LEVEL".into(), 3)UInt(String, u32)——ShaderDefVal::UInt("COUNT".into(), 16)
在 WGSL 侧,#ifdef 只判断"这个 def 是否存在";#if 可以做值比较(Bevy 的扩展语法)。
多个 uniform 的对齐
如果你在一个材质里放多个 #[uniform],每个 uniform 都是独立的绑定:
#[uniform(0)]
color: LinearRgba,
#[uniform(1)]
params: MaterialParams,对应的 WGSL:
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> material_color: vec4<f32>;
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var<uniform> params: MaterialParams;每个 uniform 内部仍然需要满足 GPU 对齐规则,但不同 uniform 之间的对齐是独立的——不用操心跨 uniform 的 padding 问题。