Skip to content

场景就绪后改材质

glTF 加载的材质是自动映射的 StandardMaterial。有时候你想在加载后修改某个材质——比如给某个部件染色、替换贴图、调整金属度。问题在于:WorldAssetRoot 展开实体是异步的,spawn 之后立刻去改子实体,还没长出来。

WorldInstanceReady:等场景长完

WorldInstanceReadybevy_scene 提供的一个实体事件,在 WorldAssetRoot 的所有子实体就绪后触发。配合 Observer(第 8 章),可以精确地在场景展开完成后执行修改:

rust
use bevy::{gltf::GltfMaterialName, prelude::*, world_serialization::WorldInstanceReady};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_observer(override_material)
        .run();
}

#[derive(Component)]
struct RedTint;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
    ));

    commands.spawn((
        DirectionalLight {
            contact_shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 25.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    let helmet =
        asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"));

    commands.spawn(WorldAssetRoot(helmet.clone()));

    commands.spawn((
        WorldAssetRoot(helmet),
        Transform::from_xyz(-1.5, 0.0, 0.0),
        RedTint,
    ));
}

fn override_material(
    trigger: On<WorldInstanceReady>,
    mut commands: Commands,
    children: Query<&Children>,
    has_tint: Query<&RedTint>,
    mesh_mats: Query<(&MeshMaterial3d<StandardMaterial>, &GltfMaterialName)>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let Ok(_) = has_tint.get(trigger.entity) else {
        return;
    };
    for desc in children.iter_descendants(trigger.entity) {
        let Ok((mat_id, name)) = mesh_mats.get(desc) else {
            continue;
        };
        let Some(mat) = materials.get(mat_id.id()) else {
            continue;
        };
        if name.0 == "LeatherPartsMat" {
            let mut new_mat = mat.clone();
            new_mat.base_color = Color::srgb(0.9, 0.2, 0.2);
            commands
                .entity(desc)
                .insert(MeshMaterial3d(materials.add(new_mat)));
        }
    }
}

Listing 23-6:用 Observer + WorldInstanceReady 给特定材质染色(examples/listing-23-06.rs)

console
cargo run -p ch23-gltf --example listing-23-06

第二只头盔的皮革部件被染成红色。

工作原理

  1. setup 里 spawn 两个 WorldAssetRoot,第二个带 RedTint 标记组件
  2. add_observer(override_material) 注册一个全局 Observer,监听 WorldInstanceReady 事件
  3. 场景就绪后,Observer 拿到触发事件的实体,检查有没有 RedTint
  4. 遍历该实体的所有后代(children.iter_descendants),找到带 GltfMaterialName 的网格
  5. 匹配材质名 "LeatherPartsMat",克隆材质、改颜色、换回去

这个模式的核心是 iter_descendants——glTF 场景展开后是一个实体树,每个网格实体都带着 MeshMaterial3d<StandardMaterial>GltfMaterialName 组件。遍历这棵树就能找到任何你想修改的部件。

直接改 vs 换提货单

改材质有两种策略:

  • 改资产本体materials.get_mut(handle) 直接修改 Assets<StandardMaterial> 里的数据。所有引用这个 Handle 的实例都会受影响——第 14 章"改资产,所有提货单跟着变"的规矩。
  • 换提货单:克隆一份新材质、改好、materials.add 入库拿到新 Handle,然后 commands.entity(desc).insert(MeshMaterial3d(new_handle)) 替换组件。只有这一个实例受影响。

Listing 23-6 用的是后者——只染第二只头盔,第一只不受影响。