Skip to content

纹理与采样器

纯色方块看腻了?这一节给材质加上纹理。AsBindGroup derive 宏用 #[texture]#[sampler] 属性把纹理和采样器映射到着色器的绑定点——和 #[uniform] 一样声明式,但底层是完全不同的 GPU 资源类型。

带纹理的材质

rust
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 颜色

运行:

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

方块的每一面都贴上了 branding/icon.png 图标,同时被 color(这里设为白色,不影响原色)调制。

#[texture(N)]#[sampler(N)]

rust
#[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

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 的条件编译机制。

rust
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 实现条件编译

运行:

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

左边方块显示原始蓝色,右边方块显示反色(黄色)。两个实体用的是同一个材质类型、同一段着色器代码——区别仅在于 invert 字段为 true 还是 false

#[bind_group_data] 与管线特化

Shader defs 不是通过 uniform 传递的——它们在管线创建时就确定了,影响的是着色器的编译产物。Bevy 用管线特化(pipeline specialization)机制实现这一点:

rust
#[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 侧用来决定管线变体。

rust
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。当 inverttrue 时,向管线的片段着色器阶段注入一个 "INVERT_COLOR" shader def。

From<&ConditionalMaterial> for ConditionalMaterialKey 实现负责这个转换:

rust
impl From<&ConditionalMaterial> for ConditionalMaterialKey {
    fn from(material: &ConditionalMaterial) -> Self {
        Self { invert: material.invert }
    }
}

WGSL 侧的 #ifdef

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> {
#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 都是独立的绑定:

rust
#[uniform(0)]
color: LinearRgba,
#[uniform(1)]
params: MaterialParams,

对应的 WGSL:

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 问题。