bevy_materialize/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3pub mod animation;
4pub mod color_space_fix;
5#[cfg(feature = "bevy_pbr")]
6pub mod erased_material;
7pub mod generic_material;
8pub mod load;
9pub mod material_property;
10pub mod prelude;
11pub mod value;
12
13#[cfg(feature = "bevy_pbr")]
14use std::any::TypeId;
15use std::sync::Arc;
16
17#[cfg(feature = "bevy_pbr")]
18use bevy::{
19	pbr::{ExtendedMaterial, MaterialExtension},
20	reflect::{GetTypeRegistration, Typed},
21};
22use color_space_fix::ColorSpaceFixPlugin;
23use generic_material::GenericMaterialShorthands;
24use material_property::MaterialPropertyRegistry;
25
26use bevy::prelude::*;
27#[cfg(feature = "bevy_pbr")]
28use generic_material::GenericMaterialApplied;
29use load::{
30	GenericMaterialLoader, asset::AssetLoadingProcessor, deserializer::MaterialDeserializer, processor::MaterialProcessor,
31	simple::SimpleGenericMaterialLoader,
32};
33use prelude::*;
34
35pub struct MaterializePlugin<D: MaterialDeserializer, P: MaterialProcessor> {
36	pub deserializer: Arc<D>,
37	/// If [`None`], doesn't register [`SimpleGenericMaterialLoader`].
38	pub simple_loader: Option<SimpleGenericMaterialLoader>,
39	/// Whether to add [`AnimationPlugin`](animation::AnimationPlugin), animating materials with the [`ANIMATION`](GenericMaterial::ANIMATION) property. (Default: `true`)
40	pub animated_materials: bool,
41	// Whether to replace special patterns in text, such as replacing `${name}` with the name of the material loading. (Default: `true`)
42	pub do_text_replacements: bool,
43	/// Whether to automatically set maps in [`StandardMaterial`] that aren't supposed to be to sRGB to linear if necessary.
44	pub standard_material_color_space_fix: bool,
45	pub processor: P,
46}
47impl<D: MaterialDeserializer, P: MaterialProcessor + Clone> Plugin for MaterializePlugin<D, P> {
48	fn build(&self, app: &mut App) {
49		let type_registry = app.world().resource::<AppTypeRegistry>().clone();
50
51		if let Some(simple_loader) = self.simple_loader.clone() {
52			app.register_asset_loader(simple_loader);
53		}
54
55		let shorthands = GenericMaterialShorthands::default();
56		let property_registry = MaterialPropertyRegistry::default();
57
58		#[rustfmt::skip]
59		app
60			.add_plugins(MaterializeMarkerPlugin)
61			.insert_resource(shorthands.clone())
62			.insert_resource(property_registry.clone())
63			.register_type::<GenericMaterial3d>()
64			.init_asset::<GenericMaterial>()
65			.register_generic_material_sub_asset::<GenericMaterial>()
66			.register_asset_loader(GenericMaterialLoader {
67				type_registry,
68				shorthands,
69				property_registry,
70				deserializer: self.deserializer.clone(),
71				do_text_replacements: self.do_text_replacements,
72				processor: self.processor.clone(),
73			})
74		;
75
76		if self.animated_materials {
77			app.add_plugins(animation::AnimationPlugin);
78		}
79
80		if self.standard_material_color_space_fix {
81			app.add_plugins(ColorSpaceFixPlugin);
82		}
83
84		#[cfg(feature = "bevy_image")]
85		app.register_generic_material_sub_asset::<Image>();
86
87		#[cfg(feature = "bevy_pbr")]
88		#[rustfmt::skip]
89		app
90			.register_material_property(GenericMaterial::VISIBILITY)
91			.register_generic_material::<StandardMaterial>()
92			.add_systems(PreUpdate, (
93				reload_generic_materials,
94				visibility_material_property, // Must be before `insert_generic_materials`
95				insert_generic_materials,
96			).chain())
97		;
98	}
99}
100impl<D: MaterialDeserializer> MaterializePlugin<D, AssetLoadingProcessor<()>> {
101	/// Creates a new [`MaterializePlugin`] with an [`AssetLoadingProcessor`].
102	pub fn new(deserializer: D) -> Self {
103		Self::new_with_processor(deserializer, AssetLoadingProcessor(()))
104	}
105}
106
107impl<D: MaterialDeserializer, P: MaterialProcessor> MaterializePlugin<D, P> {
108	/// Use over [`MaterializePlugin::new`] if you don't want to use an [`AssetLoadingProcessor`].
109	pub fn new_with_processor(deserializer: D, processor: P) -> Self {
110		Self {
111			deserializer: Arc::new(deserializer),
112			simple_loader: Some(default()),
113			animated_materials: true,
114			do_text_replacements: true,
115			standard_material_color_space_fix: true,
116			processor,
117		}
118	}
119
120	/// If [`None`], doesn't register [`SimpleGenericMaterialLoader`].
121	pub fn with_simple_loader(self, loader: Option<SimpleGenericMaterialLoader>) -> Self {
122		Self {
123			simple_loader: loader,
124			..self
125		}
126	}
127
128	/// Whether to replace special patterns in text, such as replacing `${name}` with the name of the material loading. (Default: `true`)
129	pub fn with_text_replacements(self, value: bool) -> Self {
130		Self {
131			do_text_replacements: value,
132			..self
133		}
134	}
135
136	/// Whether to add [`AnimationPlugin`](animation::AnimationPlugin), animating materials with the [`ANIMATION`](GenericMaterial::ANIMATION) property.
137	pub fn with_animated_materials(self, value: bool) -> Self {
138		Self {
139			animated_materials: value,
140			..self
141		}
142	}
143
144	/// Whether to automatically set maps in [`StandardMaterial`] that aren't supposed to be to sRGB to linear if necessary.
145	pub fn with_standard_material_color_space_fix(self, value: bool) -> Self {
146		Self {
147			standard_material_color_space_fix: value,
148			..self
149		}
150	}
151
152	/// Adds a new processor to the processor stack. The function specified takes in the old processor and produces a new one.
153	///
154	/// Zero-sized processors are usually tuples, meaning you can just put their type name (e.g. `.with_processor(MyProcessor)`).
155	pub fn with_processor<NewP: MaterialProcessor>(self, f: impl FnOnce(P) -> NewP) -> MaterializePlugin<D, NewP> {
156		MaterializePlugin {
157			deserializer: self.deserializer,
158			simple_loader: self.simple_loader,
159			animated_materials: self.animated_materials,
160			do_text_replacements: self.do_text_replacements,
161			standard_material_color_space_fix: self.standard_material_color_space_fix,
162			processor: f(self.processor),
163		}
164	}
165}
166impl<D: MaterialDeserializer + Default, P: MaterialProcessor + Default> Default for MaterializePlugin<D, P> {
167	fn default() -> Self {
168		Self::new_with_processor(default(), default())
169	}
170}
171
172/// Added when a [`MaterializePlugin`] is added. Can be used to check if any [`MaterializePlugin`] has been added.
173pub struct MaterializeMarkerPlugin;
174impl Plugin for MaterializeMarkerPlugin {
175	fn build(&self, _app: &mut App) {}
176}
177
178// Can't have these in a MaterializePlugin impl because of the generics.
179// ////////////////////////////////////////////////////////////////////////////////
180// // SYSTEMS
181// ////////////////////////////////////////////////////////////////////////////////
182
183#[cfg(feature = "bevy_pbr")]
184pub fn insert_generic_materials(
185	mut commands: Commands,
186	query: Query<(Entity, &GenericMaterial3d), Without<GenericMaterialApplied>>,
187	generic_materials: Res<Assets<GenericMaterial>>,
188) {
189	for (entity, holder) in &query {
190		let Some(generic_material) = generic_materials.get(&holder.0) else { continue };
191
192		let material = generic_material.handle.clone();
193		commands
194			.entity(entity)
195			.queue(move |entity: EntityWorldMut<'_>| material.insert(entity))
196			.insert(GenericMaterialApplied);
197	}
198}
199
200#[cfg(feature = "bevy_pbr")]
201pub fn reload_generic_materials(
202	mut commands: Commands,
203	mut asset_events: EventReader<AssetEvent<GenericMaterial>>,
204	query: Query<(Entity, &GenericMaterial3d), With<GenericMaterialApplied>>,
205) {
206	for event in asset_events.read() {
207		let AssetEvent::Modified { id } = event else { continue };
208
209		for (entity, holder) in &query {
210			if *id == holder.0.id() {
211				commands.entity(entity).remove::<GenericMaterialApplied>();
212			}
213		}
214	}
215}
216
217impl GenericMaterial {
218	/// Material property that sets the visibility of the mesh it's applied to.
219	#[cfg(feature = "bevy_pbr")]
220	pub const VISIBILITY: MaterialProperty<Visibility> = MaterialProperty::new("visibility");
221}
222
223#[cfg(feature = "bevy_pbr")]
224pub fn visibility_material_property(
225	mut query: Query<(&GenericMaterial3d, &mut Visibility), Without<GenericMaterialApplied>>,
226	generic_materials: Res<Assets<GenericMaterial>>,
227) {
228	for (generic_material_holder, mut visibility) in &mut query {
229		let Some(generic_material) = generic_materials.get(&generic_material_holder.0) else { continue };
230		let Ok(new_visibility) = generic_material.get_property(GenericMaterial::VISIBILITY) else { continue };
231
232		*visibility = *new_visibility;
233	}
234}
235
236#[cfg(feature = "bevy_pbr")]
237pub trait MaterializeAppExt {
238	/// Register a material to be able to be created via [`GenericMaterial`].
239	///
240	/// This also registers the type if it isn't already registered.
241	///
242	/// NOTES:
243	/// - [`from_world`](FromWorld::from_world) is only called once when the material is registered, then that value is cloned each time a new instance is required.
244	/// - If you're registering an [`ExtendedMaterial`] that requires [`FromWorld`], you should use [`register_extended_generic_material(...)`](MaterializeAppExt::register_extended_generic_material).
245	fn register_generic_material<M: Material + Reflect + Struct + FromWorld + GetTypeRegistration>(&mut self) -> &mut Self;
246
247	/// Registers an [`ExtendedMaterial`] using [`FromWorld`], and sets a shorthand to it.
248	///
249	/// This function is necessary if either the base or extension requires [`FromWorld`], as [`ExtendedMaterial`] has a [`Default`] impl that would conflict with a [`FromWorld`] impl.
250	/// This is also why the base and extension are separate generic parameters.
251	fn register_extended_generic_material<
252		Base: Material + FromReflect + Typed + Struct + FromWorld + GetTypeRegistration,
253		Ext: MaterialExtension + FromReflect + Typed + Struct + FromWorld + GetTypeRegistration,
254	>(
255		&mut self,
256		shorthand: impl Into<String>,
257	) -> &mut Self;
258
259	/// Same as [`register_generic_material`](MaterializeAppExt::register_generic_material), but with a provided default value.
260	///
261	/// This main use of this is for extended materials, allowing you to specify defaults for the base material that you wouldn't be able to otherwise.
262	fn register_generic_material_with_default<M: Material + Reflect + Struct + GetTypeRegistration>(&mut self, default_value: M) -> &mut Self;
263
264	/// If your material name is really long, you can use this to register a shorthand that can be used in place of it.
265	///
266	/// This is namely useful for extended materials, as those type names tend to have a lot of boilerplate.
267	///
268	/// # Examples
269	/// ```ignore
270	/// # App::new()
271	/// .register_generic_material_shorthand::<YourOldReallyLongNameOhMyGoshItsSoLong>("ShortName")
272	/// ```
273	/// Now you can turn
274	/// ```toml
275	/// type = "YourOldReallyLongNameOhMyGoshItsSoLong"
276	/// ```
277	/// into
278	/// ```toml
279	/// type = "ShortName"
280	/// ```
281	fn register_generic_material_shorthand<M: GetTypeRegistration>(&mut self, shorthand: impl Into<String>) -> &mut Self;
282}
283#[cfg(feature = "bevy_pbr")]
284impl MaterializeAppExt for App {
285	fn register_generic_material<M: Material + Reflect + Struct + FromWorld + GetTypeRegistration>(&mut self) -> &mut Self {
286		let default_value = M::from_world(self.world_mut());
287		self.register_generic_material_with_default(default_value)
288	}
289
290	fn register_extended_generic_material<
291		Base: Material + FromReflect + Typed + Struct + FromWorld + GetTypeRegistration,
292		Ext: MaterialExtension + FromReflect + Typed + Struct + FromWorld + GetTypeRegistration,
293	>(
294		&mut self,
295		shorthand: impl Into<String>,
296	) -> &mut Self {
297		let base = Base::from_world(self.world_mut());
298		let extension = Ext::from_world(self.world_mut());
299		self.register_generic_material_with_default(ExtendedMaterial { base, extension })
300			.register_generic_material_shorthand::<ExtendedMaterial<Base, Ext>>(shorthand)
301	}
302
303	fn register_generic_material_with_default<M: Material + Reflect + Struct + GetTypeRegistration>(&mut self, default_value: M) -> &mut Self {
304		let mut type_registry = self.world().resource::<AppTypeRegistry>().write();
305		if type_registry.get(TypeId::of::<M>()).is_none() {
306			type_registry.register::<M>();
307		}
308
309		type_registry.get_mut(TypeId::of::<M>()).unwrap().insert(ReflectGenericMaterial {
310			default_value: Box::new(default_value),
311		});
312
313		drop(type_registry);
314
315		self
316	}
317
318	fn register_generic_material_shorthand<M: GetTypeRegistration>(&mut self, shorthand: impl Into<String>) -> &mut Self {
319		self.world()
320			.resource::<GenericMaterialShorthands>()
321			.values
322			.write()
323			.unwrap()
324			.insert(shorthand.into(), M::get_type_registration());
325		self
326	}
327}