Skip to content

Conversation

@Zeophlite
Copy link
Owner

@Zeophlite Zeophlite commented Dec 6, 2025

Objective

Solution

  • Added GltfMaterial to bevy_gltf , this is a subset of fields in bevy_pbr::StandardMaterial , and is what bevy_gltf currently uses
  • Added MarkerMeshMaterial3d to bevy_material , this has a Handle to the GltfMaterial
  • Create bevy_gltf_render crate, that depends on bevy_gltf and bevy_pbr
  • Added a system swap_marker_mesh_material_3d to bevy_gltf_render that:
    • queries for MarkerMeshMaterial3d
    • creates a StandardMaterial from the GltfMaterial
    • removes the MarkerMeshMaterial3d
    • adds a MeshMaterial3d with the new GltfMaterial

Only downside is that if you forget to add GltfRenderPlugin then it silently doesn't render.

Please bikeshed MarkerMeshMaterial3d

Alternative

Define GltfMaterial in bevy_material
System swap_marker_mesh_material_3d in bevy_pbr
This is indirect, but also works.

See #5

Testing

  • cargo run --example load_gltf

@github-actions
Copy link

github-actions bot commented Dec 6, 2025

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

}
}

pub(crate) fn swap_marker_mesh_material_3d(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will require an entry in the migration guides, etc for people who are spawning gltf and managing materials, since it will introduce a delay in the appearance of StandardMaterial for SceneInstanceReady users (they will lose access to the StandardMaterial and instead have to use GltfMaterial) and lifecycle observer users (who will not see an On<Add, StandardMaterial> until after the scene has spawned and this system has run).

Also that migration guide will have to clearly note that removing the MarkerMeshMaterial3d is critical, or bevy will overwrite their materials (or add additional second materials if a user applied a custom material) on the next frame.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's another issue - a new StandardMaterial is created for each instance of a mesh with that material. So a glTF with a hundred mesh instances all using same material will end up creating a hundred duplicate StandardMaterial assets.

I've sketched out a potential alternative below - this gives the renderer a way to translate materials during load_gltf so there's no need for post-spawn fixup. It also means the glTF loader can print an error if the renderer hasn't provided a translator or fails during translation.

Unfortunately it's kinda ugly. But maybe as the asset pipeline and scene systems mature it can become more generic - some kind of BSN patch or a BSN -> BSN transform. Also might become simpler if MeshMaterial3d gets changed to a type erased handle, so insert_material is no longer required.

In bevy_gltf:

// Utility for letting renderers translate `GltfMaterial` into their own material type. The renderer
// should add this as a resource during `Plugin::build`.
#[derive(Resource)]
struct GltfMaterialTranslator {
    // Create a material asset from a `GltfMaterial` and a label.
    load_material: Box<dyn Fn(&GltfMaterial, &GltfAssetLabel, &mut LoadContext) -> Result<UntypedHandle, BevyError>>,
    // Insert a material component using a label that was previously passed to `load_material`.
    insert_material: Box<dyn Fn(&GltfAssetLabel, &mut LoadContext, &EntityWorldMut)> -> Result<(), BevyError>>,
}

struct GltfLoader {
    ...
    material_translator: Option<GltfMaterialTranslator>,
}

fn load_material(..., material_translator: &Option<GltfMaterialTranslator>) {
    let gltf_material: GltfMaterial = ...;
    
    if let Some(material_translator) = material_translator {
        match material_translator.load_material(&gltf_material, &material_label, load_context) {
            Ok(handle) return handle;
            Err(err) { warn!("{err}"); return /* TODO? Does `load_material` return a Result?*/; }
        }
    }
}

fn load_node(..., material_translator: &Option<GltfMaterialTranslator>) {
    ...
    
    // Replaces where `MeshMaterial3d` is inserted into `mesh_entity`.
    if let Some(material_translator) = material_translator {
        if let Err(err) = material_translator.insert_material(&material_label, load_context, &mut mesh_entity) {
            warn!("{err}");
        }
    }
    
    ...
}

impl Plugin for GltfPlugin {
    fn finish(&self, app: &mut App) {
        ...
        
        // Copy the material translator resource into the loader. This means we can access it in `GltfLoader::load`,
        // which doesn't have access to world resources.
        let material_translator = app.world().get_resource<GltfMaterialTranslator>().cloned();
        
        if material_translator.is_none() {
            warn!("Missing material translator");
        }
        
        ...
        
        GltfLoader { ..., material_translator }
    }
}

In bevy_pbr:

impl Plugin for PbrPlugin {
    fn build(...) {
        ...
        
        let gltf_material_translator = GltfMaterialTranslator {
            load_material: |gltf_material, label, load_context| {
                Ok(load_context
                    .labeled_asset_scope::<_, ()>(label.to_string(), |load_context| {
                        ... create a StandardMaterial from the GltfMaterial ...
                    }).untyped())
            },
            insert_material: |label, load_context, entity| {
                let handle = load_context.get_label_handle::<StandardMaterial>()
                    .ok_or_else(|| "TODO: error".into())?;
                    
                entity.insert(MeshMaterial3d(handle))
                Ok(())
            },
        };
        
        app.insert_resource(gltf_material_translator);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants