#bevy #materialize #material #properties #processor #inheritance

bevy_materialize

Load, store, and apply type-erased materials in Bevy

11 releases (4 breaking)

new 0.5.0-rc.5 Apr 17, 2025
0.5.0-rc.3 Apr 5, 2025
0.5.0-rc.2 Mar 27, 2025
0.4.2 Mar 18, 2025
0.1.0 Feb 3, 2025

#96 in Game dev

Download history 115/week @ 2025-01-31 27/week @ 2025-02-07 394/week @ 2025-02-14 159/week @ 2025-02-21 39/week @ 2025-02-28 30/week @ 2025-03-07 290/week @ 2025-03-14 214/week @ 2025-03-21 128/week @ 2025-03-28 199/week @ 2025-04-04 246/week @ 2025-04-11

806 downloads per month
Used in 4 crates (3 directly)

MIT/Apache

1.5MB
1K SLoC

bevy_materialize

Crate for loading and applying type-erased materials in Bevy.

Built-in supported formats are json, and toml, but you can easily add more.

Usage Example (TOML)

First, add the MaterializePlugin to your App.

use bevy::prelude::*;
use bevy_materialize::prelude::*;

fn example_main() {
    App::new()
        // ...
        .add_plugins(MaterializePlugin::new(TomlMaterialDeserializer))
        // ...
        .run();
}

Loading

The API for adding to an entity is quite similar to MeshMaterial3d<...>, just with GenericMaterial3d storing a Handle<GenericMaterial> instead, which you can load from a file.

use bevy::prelude::*;
use bevy_materialize::prelude::*;

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    commands.spawn((
        Mesh3d(asset_server.add(Cuboid::from_length(1.).into())),
        GenericMaterial3d(asset_server.load("materials/example.toml")),
    ));
}

assets/materials/example.toml

# The type name of the material. Can either be the full path (e.g. bevy_pbr::pbr_material::StandardMaterial),
# or, if only one registered material has the name, just the name itself.
# If this field is not specified, defaults to StandardMaterial
type = "StandardMaterial"

[material]
# Asset paths are relative to the material's path,
# unless they start with a '/', then they will be relative to the assets folder.
base_color_texture = "example.png"
emissive = [0.1, 0.2, 0.5, 1.0]
alpha_mode = { Mask = 0.5 }

# Optional custom properties, these can be whatever you want.
[properties]
# This one is built-in, and sets the entity's Visibility when the material is applied.
visibility = "Hidden"
collision = true
sounds = "wood"

For simplicity, you can also load a GenericMaterial directly from an image file, which by default puts a StandardMaterial internally. You can change the material that it uses via

use bevy::prelude::*;
use bevy_materialize::{prelude::*, load::simple::SimpleGenericMaterialLoader};

MaterializePlugin::new(TomlMaterialDeserializer).with_simple_loader(Some(SimpleGenericMaterialLoader {
    material: |image| StandardMaterial {
        base_color_texture: Some(image),
        // Now it's super shiny!
        perceptual_roughness: 0.1,
        ..default()
    }.into(),
    ..default()
}));

// This would disable the image loading functionality entirely.
MaterializePlugin::new(TomlMaterialDeserializer).with_simple_loader(None);

NOTE: This loader seems to take priority over Bevy's image loader when it doesn't know which asset you want, so if you're loading images as untyped assets you'll have to turn this off.

File Extensions

Currently, the supported file extensions are: (Replace toml with the file format you're using)

  • toml
  • mat
  • mat.toml
  • material
  • material.toml

Feel free to just use the one you like the most.

Properties

For retrieving custom properties from a material, the API is pretty simple.

use bevy::prelude::*;
use bevy_materialize::prelude::*;
use bevy_materialize::generic_material::GetPropertyError;

fn retrieve_properties_example(material: &GenericMaterial) {
    // The type returned is based on the generic of the property. For example, VISIBILITY is a MaterialProperty<Visibility>.
    let _: Result<&Visibility, GetPropertyError> = material.get_property(GenericMaterial::VISIBILITY);
}

For creating your own properties, you should make an extension trait for GenericMaterial, then register it with your app.

use bevy::prelude::*;
use bevy_materialize::prelude::*;

pub trait MyMaterialProperties {
    const MY_PROPERTY: MaterialProperty<f32> = MaterialProperty::new("my_property");
}
impl MyMaterialProperties for GenericMaterial {}

fn example_main() {
    App::new()
        .register_material_property(GenericMaterial::MY_PROPERTY)
        // ...
    ;
}

MaterialProperty is just a helper struct that bundles the type and key together, and technically isn't necessary for any of this.

Registering

When creating your own custom materials, all you have to do is register them in your app like so.

App::new()
    // ...
    .register_generic_material::<YourMaterial>()

This will also register the type if it hasn't been registered already.

You can also register a shorthand if your material's name is very long (like if it's an ExtendedMaterial<...>).

App::new()
    // ...
    .register_generic_material_shorthand::<YourMaterialWithALongName>("YourMaterial")

This will allow you to put the shorthand in your file's type field instead of the type name.

Headless

For headless contexts like dedicated servers where you only want properties, but no materials, you can turn off the bevy_pbr feature on this crate by disabling default features, and manually adding the loaders you want.

bevy_materialize = { version = "...", default-features = false, features = ["toml"] }

Inheritance

When creating a bunch of PBR materials, your files might look something like this

# example.toml
[material]
base_color_texture = "example.png"
occlusion_texture = "example_ao.png"
metallic_roughness_texture = "example_mr.png"
normal_map_texture = "example_normal.png"
depth_map = "example_depth.png"

This is a lot of boilerplate, especially considering you have to manually rename 5 instances of your material name for every material.

This is where inheritance comes in, you can make a file like

# pbr.toml
[material]
base_color_texture = "${name}.png"
occlusion_texture = "${name}_ao.png"
metallic_roughness_texture = "${name}_mr.png"
normal_map_texture = "${name}_normal.png"
depth_map = "${name}_depth.png"

#{name} is a special pattern that gets replaced to the name of the material loaded. (This functionality can be turned off from the plugin)

Now you can rewrite your example.toml into

inherits = "pbr.toml"

This is much less boilerplate, and you can just copy and paste it without needing to manually rename everything. You can still override and add more fields to the sub-material, this just gives you a handy baseline.

TIP: Like other assets, if you start the path with a '/', it is relative to the assets folder rather than the material's. This is useful for setups with a bunch of subfolders.

Processors

bevy_materialize has a processor API wrapping Bevy's ReflectDeserializerProcessor. This allows you to modify data as it's being deserialized. For example, this system is used for loading assets, treating strings as paths.

It's used much like Rust's iterator API, each processor having a child processor that is stored via generic. If you want to make your own, check out AssetLoadingProcessor for a simple example of an implementation, then use it with your MaterializePlugin.

pub struct MyProcessor<P: MaterialSubProcessor>(pub P);
impl<P: MaterialSubProcessor> MaterialSubProcessor for MyProcessor<P> {
	// ...
}

MaterializePlugin::new(TomlMaterialDeserializer) // type: MaterializePlugin<..., AssetLoadingProcessor<()>>
    .with_processor(MyProcessor) // type: MaterializePlugin<..., MyProcessor<AssetLoadingProcessor<()>>>

Supported Bevy Versions

Bevy bevy_materialize
0.16 0.5
0.15 0.1-0.4

Dependencies

~27–61MB
~1M SLoC