Add dedicated skinned rendering, pose restoration, shared Material and Material Instance slots, registry-driven components, Surface/Solari integration, transactional schema upgrades, navigation authoring, documentation, and evaluation evidence.
185 lines
6.8 KiB
Rust
185 lines
6.8 KiB
Rust
//! Material asset loading and inspector/browser integration.
|
|
|
|
use bevy::prelude::*;
|
|
use bevy_egui::egui;
|
|
use shared::{
|
|
ComponentInstanceId, LevelObject, MaterialAsset, MaterialDesc, MaterialRef,
|
|
RendererMaterialSlot,
|
|
};
|
|
|
|
use crate::assets::{EditorAsset, EditorAssetKind, EditorAssets};
|
|
use crate::history::{reflected_component_transaction, set_material_group_with_history};
|
|
use crate::ui::inspector::property_row;
|
|
|
|
pub fn load_material_asset(catalog_path: &str) -> Result<MaterialAsset, String> {
|
|
MaterialAsset::load_from_path(catalog_path)
|
|
}
|
|
|
|
pub fn material_desc_from_asset(catalog_path: &str) -> Result<MaterialDesc, String> {
|
|
let asset = load_material_asset(catalog_path)?;
|
|
let mut desc = asset.material;
|
|
desc.material_asset_path = Some(catalog_path.to_string());
|
|
Ok(desc)
|
|
}
|
|
|
|
pub fn apply_material_asset_to_selection(
|
|
world: &mut World,
|
|
asset: &EditorAsset,
|
|
selected: &bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities,
|
|
) -> Result<(), String> {
|
|
let path = asset
|
|
.path
|
|
.clone()
|
|
.ok_or_else(|| format!("Material has no source path: {}", asset.label))?;
|
|
let record = world
|
|
.get_resource::<crate::asset_db::AssetRegistry>()
|
|
.and_then(|registry| crate::asset_db::find_asset_by_path(registry, &path))
|
|
.ok_or_else(|| format!("Material is missing from the asset registry: {path}"))?;
|
|
let sub_asset_id = if shared::MaterialInstanceAsset::load_from_path(&path).is_ok() {
|
|
"material:instance"
|
|
} else {
|
|
"material:source"
|
|
};
|
|
let material_ref = MaterialRef::new(
|
|
shared::EditorAssetRef::new(record.id.as_string(), sub_asset_id, asset.label.clone())
|
|
.with_source_path(path.clone()),
|
|
);
|
|
let desc = material_desc_from_asset(&path).ok();
|
|
let entities = crate::ui::helpers::selected_level_entities(world, selected);
|
|
let mut renderer_changes = 0usize;
|
|
let mut legacy_changes = Vec::new();
|
|
for entity in entities {
|
|
if world.get::<LevelObject>(entity).is_none() {
|
|
continue;
|
|
}
|
|
if let Some(mut renderer) = world.get::<shared::StaticMeshRenderer>(entity).cloned() {
|
|
ensure_static_material_slots(&mut renderer);
|
|
for slot in &mut renderer.materials.slots {
|
|
slot.material = Some(material_ref.clone());
|
|
}
|
|
reflected_component_transaction(
|
|
world,
|
|
entity,
|
|
"Assign Material Slots",
|
|
shared::AUTHORING_COMPONENT_STATIC_MESH_RENDERER,
|
|
shared::COMPONENT_STATIC_MESH_RENDERER,
|
|
move |world, entity| {
|
|
world.entity_mut(entity).insert(renderer);
|
|
Ok(())
|
|
},
|
|
)?;
|
|
renderer_changes += 1;
|
|
continue;
|
|
}
|
|
if let Some(mut renderer) = world.get::<shared::SkinnedMeshRenderer>(entity).cloned() {
|
|
if renderer.materials.slots.is_empty() {
|
|
return Err(format!(
|
|
"Skinned renderer `{}` has no imported material slots; reimport its model",
|
|
renderer.path
|
|
));
|
|
}
|
|
for slot in &mut renderer.materials.slots {
|
|
slot.material = Some(material_ref.clone());
|
|
}
|
|
reflected_component_transaction(
|
|
world,
|
|
entity,
|
|
"Assign Material Slots",
|
|
shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
|
|
shared::COMPONENT_SKINNED_MESH_RENDERER,
|
|
move |world, entity| {
|
|
world.entity_mut(entity).insert(renderer);
|
|
Ok(())
|
|
},
|
|
)?;
|
|
renderer_changes += 1;
|
|
continue;
|
|
}
|
|
if let Some(desc) = desc.as_ref() {
|
|
legacy_changes.push((entity, desc.clone()));
|
|
}
|
|
}
|
|
if !legacy_changes.is_empty() {
|
|
set_material_group_with_history(world, legacy_changes);
|
|
}
|
|
if renderer_changes == 0 && desc.is_none() {
|
|
return Err("No selected renderable actor accepts this material".into());
|
|
}
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Applied shared material {}", asset.label);
|
|
Ok(())
|
|
}
|
|
|
|
fn ensure_static_material_slots(renderer: &mut shared::StaticMeshRenderer) {
|
|
for part in &mut renderer.slots {
|
|
if part.material_slot_id.0.trim().is_empty() {
|
|
part.material_slot_id = if part.id.0.trim().is_empty() {
|
|
ComponentInstanceId::new("slot:legacy")
|
|
} else {
|
|
ComponentInstanceId::new(format!("slot:{}", part.id.0))
|
|
};
|
|
}
|
|
if renderer.materials.slot(&part.material_slot_id).is_none() {
|
|
renderer.materials.slots.push(RendererMaterialSlot {
|
|
id: part.material_slot_id.clone(),
|
|
name: part.name.clone(),
|
|
source_material: part.material.clone().map(MaterialRef::new),
|
|
material: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn material_asset_picker_ui(
|
|
world: &mut World,
|
|
ui: &mut egui::Ui,
|
|
_entity: Entity,
|
|
material: &mut MaterialDesc,
|
|
_original: &MaterialDesc,
|
|
) -> bool {
|
|
let assets: Vec<(String, String)> = world
|
|
.resource::<EditorAssets>()
|
|
.assets
|
|
.iter()
|
|
.filter(|a| matches!(a.kind, EditorAssetKind::Material))
|
|
.filter_map(|a| Some((a.label.clone(), a.path.clone()?)))
|
|
.collect();
|
|
|
|
let current = material.material_asset_path.clone().unwrap_or_default();
|
|
let mut selected_label = assets
|
|
.iter()
|
|
.find(|(_, path)| path == ¤t)
|
|
.map(|(label, _)| label.clone())
|
|
.unwrap_or_else(|| "(none)".to_string());
|
|
|
|
let mut changed = false;
|
|
property_row(ui, "Material asset", |ui| {
|
|
egui::ComboBox::from_id_salt("material_asset_picker")
|
|
.selected_text(&selected_label)
|
|
.show_ui(ui, |ui| {
|
|
if ui.selectable_label(current.is_empty(), "(none)").clicked() {
|
|
material.material_asset_path = None;
|
|
selected_label = "(none)".into();
|
|
changed = true;
|
|
}
|
|
for (label, path) in &assets {
|
|
if ui
|
|
.selectable_label(&selected_label == label, label)
|
|
.clicked()
|
|
{
|
|
if let Ok(desc) = material_desc_from_asset(path) {
|
|
*material = desc;
|
|
selected_label = label.clone();
|
|
changed = true;
|
|
} else {
|
|
world.resource_mut::<crate::scene_io::SceneIo>().status =
|
|
format!("Could not load material {path}");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
changed
|
|
}
|