diff --git a/.gitignore b/.gitignore index 80bd236..384e350 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target Cargo.lock .DS_Store -lcov.info \ No newline at end of file +lcov.info +.idea diff --git a/examples/modify-scene.rs b/examples/modify-scene.rs index 2ecc044..2c6ab64 100644 --- a/examples/modify-scene.rs +++ b/examples/modify-scene.rs @@ -1,5 +1,3 @@ -use rand::Rng; -use std::f32::consts::PI; use bevy::{ core_pipeline::{ bloom::BloomSettings, @@ -12,6 +10,8 @@ use bevy::{ }; use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle}; +use rand::Rng; +use std::f32::consts::PI; /// Uses the [`bevy_vox_scene::VoxelSceneHook`] component to add extra components into the scene graph. /// Press any key to toggle the fish tank black-light on and off diff --git a/src/lib.rs b/src/lib.rs index 1ed6b6b..13e97dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ //! # } //!``` #![forbid(missing_docs, unsafe_code)] + use bevy::{ app::{App, Plugin, SpawnScene}, asset::AssetApp, @@ -51,14 +52,12 @@ use bevy::{ }; mod loader; -mod voxel_scene; +mod scene; pub use loader::VoxLoaderSettings; #[doc(inline)] use loader::VoxSceneLoader; -pub use voxel_scene::{ - VoxelLayer, VoxelScene, VoxelSceneBundle, VoxelSceneHook, VoxelSceneHookBundle, -}; -use voxel_scene::VoxelModel; +use scene::VoxelModel; +pub use scene::{VoxelLayer, VoxelScene, VoxelSceneBundle, VoxelSceneHook, VoxelSceneHookBundle}; mod mesh; mod voxel; @@ -69,13 +68,12 @@ pub struct VoxScenePlugin; impl Plugin for VoxScenePlugin { fn build(&self, app: &mut App) { - app - .init_asset::() + app.init_asset::() .init_asset::() .register_asset_loader(VoxSceneLoader) .add_systems( SpawnScene, - (voxel_scene::spawn_vox_scenes, voxel_scene::run_hooks).chain(), + (scene::systems::spawn_vox_scenes, scene::systems::run_hooks).chain(), ); } } diff --git a/src/loader.rs b/src/loader.rs index e0887f9..1039bd4 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,3 +1,4 @@ +use crate::voxel::VoxelData; use anyhow::anyhow; use bevy::{ asset::{io::Reader, AssetLoader, AsyncReadExt, Handle, LoadContext}, @@ -11,9 +12,8 @@ use bevy::{ }; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::voxel::VoxelData; -use crate::voxel_scene::{self, LayerInfo, VoxelModel, VoxelNode, VoxelScene}; +use crate::scene::{self, LayerInfo, VoxelModel, VoxelNode, VoxelScene}; /// An asset loader capable of loading models in `.vox` files as usable [`bevy::render::mesh::Mesh`]es. /// @@ -194,10 +194,10 @@ impl VoxSceneLoader { .unwrap(); let has_varying_roughness = max_roughness - roughness - .iter() - .cloned() - .min_by(|a, b| a.partial_cmp(b).expect("tried to compare NaN")) - .unwrap() + .iter() + .cloned() + .min_by(|a, b| a.partial_cmp(b).expect("tried to compare NaN")) + .unwrap() > 0.001; let metalness: Vec = file @@ -212,10 +212,10 @@ impl VoxSceneLoader { .unwrap(); let has_varying_metalness = max_metalness - metalness - .iter() - .cloned() - .min_by(|a, b| a.partial_cmp(b).expect("tried to compare NaN")) - .unwrap() + .iter() + .cloned() + .min_by(|a, b| a.partial_cmp(b).expect("tried to compare NaN")) + .unwrap() > 0.001; let has_metallic_roughness = has_varying_roughness || has_varying_metalness; let metallic_roughness_texture: Option> = if has_metallic_roughness { @@ -347,11 +347,11 @@ impl VoxSceneLoader { load_context.labeled_asset_scope(format!("model/material/{}", index), |_| { let ior = 1.0 + (refraction_indices - .iter() - .cloned() - .reduce(|acc, e| acc + e) - .unwrap_or(0.0) - / refraction_indices.len() as f32); + .iter() + .cloned() + .reduce(|acc, e| acc + e) + .unwrap_or(0.0) + / refraction_indices.len() as f32); StandardMaterial { base_color_texture: Some(color_handle.clone()), emissive: if has_emissive { @@ -379,21 +379,20 @@ impl VoxSceneLoader { } }) }; - load_context.labeled_asset_scope(format!("model/{}", index), |_| { - VoxelModel { - data: VoxelData { - shape, - voxels: buffer - }, - mesh: mesh_handle, - material, - } + load_context.labeled_asset_scope(format!("model/{}", index), |_| VoxelModel { + data: VoxelData { + shape, + voxels: buffer, + }, + mesh: mesh_handle, + material, }); - }; + } // Scene graph - let root = voxel_scene::parse_xform_node(&file.scenes, &file.scenes[0], None, load_context); + let root = + scene::parse::parse_xform_node(&file.scenes, &file.scenes[0], None, load_context); let layers: Vec = file .layers .iter() @@ -403,7 +402,7 @@ impl VoxSceneLoader { }) .collect(); let mut subasset_by_name: HashMap = HashMap::new(); - voxel_scene::find_subasset_names(&mut subasset_by_name, &root); + scene::parse::find_subasset_names(&mut subasset_by_name, &root); for (subscene_name, node) in subasset_by_name { load_context.labeled_asset_scope(subscene_name.clone(), |_| VoxelScene { @@ -411,9 +410,6 @@ impl VoxSceneLoader { layers: layers.clone(), }); } - Ok(VoxelScene { - root, - layers, - }) + Ok(VoxelScene { root, layers }) } } diff --git a/src/scene/hook.rs b/src/scene/hook.rs new file mode 100644 index 0000000..86a1ec3 --- /dev/null +++ b/src/scene/hook.rs @@ -0,0 +1,87 @@ +use bevy::{ + ecs::{component::Component, system::EntityCommands, world::EntityRef}, + log::warn, +}; + +/// A component containing a closure that will be run against every entity spawned from a Voxel Scene +/// +/// Assign this component to an entity that also has a [Handle](VoxelScene) to execute a closure +/// against every entity that gets spawned in the graph of the Voxel Scene. +/// This allows you to specify, before the scene graph has been spawned, how entities at a deeper level +/// than the root should be modified. A common use-case would adding custom components to entities +/// depending on their name or [`VoxelLayer`]. +/// ```rust +/// # use bevy::{prelude::*, app::AppExit, utils::HashSet}; +/// # use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle}; +/// # +/// # fn main() { +/// # App::new() +/// # .add_plugins(( +/// # DefaultPlugins, +/// # VoxScenePlugin, +/// # )) +/// # .add_systems(Startup, setup) +/// # .add_systems(Update, assert_scene_loaded) +/// # .run(); +/// # } +/// # +/// #[derive(Component)] +/// struct Fish; +/// +/// fn setup( +/// mut commands: Commands, +/// assets: Res, +/// ) { +/// commands.spawn(( +/// VoxelSceneHookBundle { +/// scene: assets.load("study.vox#tank"), +/// +/// // This closure will be run against every child Entity that gets spawned in the scene +/// hook: VoxelSceneHook::new(move |entity, commands| { +/// let Some(name) = entity.get::() else { return }; +/// match name.as_str() { +/// // Node names give the path to the asset, with components separated by /. Here, "goldfish" and "tetra" are two fish types in the "tank" +/// "tank/goldfish" | "tank/tetra" => { +/// // add a marker Component. +/// commands.insert(Fish); +/// } +/// _ => {}, +/// } +/// }), +/// ..default() +/// }, +/// )); +/// } +/// # +/// # fn assert_scene_loaded( +/// # query: Query<&Name, With>, +/// # mut exit: EventWriter, +/// # ) { +/// # let all_fish: Vec<&str> = query.iter().map(|n| { n.as_str() }).collect(); +/// # if all_fish.is_empty() { return }; +/// # assert_eq!(all_fish.len(), 5); +/// # let expected_names: HashSet<&str> = ["tank/tetra", "tank/goldfish"].into(); +/// # let all_names: HashSet<&str> = HashSet::from_iter(all_fish); +/// # assert_eq!(expected_names, all_names); +/// # exit.send(AppExit); +/// # } +/// ``` +#[derive(Component)] +pub struct VoxelSceneHook { + pub(crate) hook: Box, +} + +impl VoxelSceneHook { + /// Create a new hook with the closure `hook`. This will be run against every entity that gets spawned in the scene graph. + pub fn new(hook: F) -> Self { + Self { + hook: Box::new(hook), + } + } +} + +impl Default for VoxelSceneHook { + fn default() -> Self { + Self::new(|_, _| warn!("Default VoxelSceneHook does nothing")) + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs new file mode 100644 index 0000000..a006df1 --- /dev/null +++ b/src/scene/mod.rs @@ -0,0 +1,118 @@ +mod hook; +pub(crate) mod parse; +pub(super) mod systems; +#[cfg(test)] +mod tests; +use bevy::{ + asset::{Asset, Handle}, + ecs::{bundle::Bundle, component::Component}, + math::Mat4, + pbr::StandardMaterial, + reflect::TypePath, + render::{mesh::Mesh, view::Visibility}, + transform::components::Transform, +}; +pub use hook::VoxelSceneHook; + +use crate::voxel::VoxelData; + +/// A component bundle for spawning Voxel Scenes. +/// +/// The root of the spawned scene will be the entity that has this bundle. +/// ```no_run +/// # use bevy::prelude::*; +/// # use bevy_vox_scene::VoxelSceneBundle; +/// +/// fn setup( +/// mut commands: Commands, +/// assets: Res, +/// ) { +/// commands.spawn(VoxelSceneBundle { +/// scene: assets.load("study.vox"), +/// ..default() +/// }); +/// +/// commands.spawn(VoxelSceneBundle { +/// // Load a single model using the name assigned to it in MagicaVoxel. +/// // If a model is nested in a named group, than the group will form part of the path +/// // Path components are separated with a slash +/// scene: assets.load("study.vox#workstation/desk"), +/// ..default() +/// }); +/// } +/// ``` +#[derive(Bundle, Default)] +pub struct VoxelSceneBundle { + /// A handle to a [`VoxelScene`], typically loaded from a ".vox" file via the [`bevy::asset::AssetServer`]. + /// This Entity will become the root of the spawned Voxel Scene. + pub scene: Handle, + /// The transform of the scene root. This will override whatever the root transform is in the Magica Voxel scene. + pub transform: Transform, + /// The visibility of the scene root. This will override whatever the root visibility is in the Magical Voxel scene. + pub visibility: Visibility, +} + +/// A component bundle for spawning Voxel Scenes, with a [`VoxelSceneHook`]. +/// +/// The root of the spawned scene will be the entity that has this bundle. +/// The [`VoxelSceneHook`] allows you to easily modify Entities deep within the scene hierarchy. +#[derive(Bundle, Default)] +pub struct VoxelSceneHookBundle { + /// A handle to a [`VoxelScene`], typically loaded from a ".vox" file via the [`bevy::asset::AssetServer`]. + /// This Entity will become the root of the spawned Voxel Scene. + pub scene: Handle, + /// A [`VoxelSceneHook`] allows you to specify a closure that will be run for each Entity spawned in the scene graph. + pub hook: VoxelSceneHook, + /// The transform of the scene root. This will override whatever the root transform is in the Magica Voxel scene. + pub transform: Transform, + /// The visibility of the scene root. This will override whatever the root visibility is in the Magical Voxel scene. + pub visibility: Visibility, +} + +/// A representation of the Voxel Scene Graph. +/// +/// To spawn a voxel scene, add a [Handle](VoxelScene), [`VoxelSceneBundle`], or [`VoxelSceneHookBundle`] to an Entity. +/// Voxel Scenes can be loaded from Magica Voxel .vox files. +#[derive(Asset, TypePath, Debug)] +pub struct VoxelScene { + pub(crate) root: VoxelNode, + pub(crate) layers: Vec, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct VoxelNode { + name: Option, + transform: Mat4, + children: Vec, + model: Option>, + is_hidden: bool, + layer_id: u32, +} + +#[derive(Asset, TypePath)] +pub(crate) struct VoxelModel { + pub data: VoxelData, + pub mesh: Handle, + pub material: Handle, +} + +#[derive(Debug, Clone)] +pub(crate) struct LayerInfo { + pub name: Option, + pub is_hidden: bool, +} + +#[derive(Component)] +pub struct VoxelModelInstance(Handle); + +/// A component specifying which layer the Entity belongs to, with an optional name. +/// +/// This can be configured in the Magica Voxel world editor. +#[derive(Component, Clone)] +pub struct VoxelLayer { + /// The identifier for the layer. Magic Voxel 0.99.6 allows you to assign nodes to one of 8 layers, + /// so this value will be an index in the range 0 through 7. + pub id: u32, + /// An optional name for the Layer, assignable in Magica Voxel layer editor. + pub name: Option, +} diff --git a/src/scene/parse.rs b/src/scene/parse.rs new file mode 100644 index 0000000..2209628 --- /dev/null +++ b/src/scene/parse.rs @@ -0,0 +1,146 @@ +use bevy::{ + asset::LoadContext, + log::warn, + math::{Mat3, Mat4, Quat, Vec3}, + utils::HashMap, +}; +use dot_vox::{Frame, SceneNode}; + +use super::VoxelNode; + +pub(crate) fn parse_xform_node( + graph: &Vec, + scene_node: &SceneNode, + parent_name: Option<&String>, + load_context: &mut LoadContext, +) -> VoxelNode { + match scene_node { + SceneNode::Transform { + attributes, + frames, + child, + layer_id, + } => { + let (accumulated, node_name) = + get_accumulated_and_node_name(parent_name, attributes.get("_name")); + let mut vox_node = VoxelNode { + name: node_name, + transform: transform_from_frame(&frames[0]), + is_hidden: parse_bool(attributes.get("_hidden").cloned()), + layer_id: *layer_id, + ..Default::default() + }; + parse_xform_child( + graph, + &graph[*child as usize], + &mut vox_node, + accumulated.as_ref(), + load_context, + ); + vox_node + } + SceneNode::Group { .. } | SceneNode::Shape { .. } => { + warn!("Found Group or Shape Node without a parent Transform"); + let mut vox_node = VoxelNode::default(); + parse_xform_child(graph, scene_node, &mut vox_node, parent_name, load_context); + vox_node + } + } +} + +pub(crate) fn find_subasset_names( + subassets_by_name: &mut HashMap, + node: &VoxelNode, +) { + if let Some(name) = &node.name { + if !subassets_by_name.contains_key(name) { + subassets_by_name.insert(name.to_string(), node.clone()); + } + } + for child in &node.children { + find_subasset_names(subassets_by_name, child); + } +} + +fn parse_xform_child( + graph: &Vec, + scene_node: &SceneNode, + partial_node: &mut VoxelNode, + parent_name: Option<&String>, + load_context: &mut LoadContext, +) { + match scene_node { + SceneNode::Transform { .. } => { + warn!("Found nested Transform nodes"); + partial_node.children = vec![parse_xform_node( + graph, + scene_node, + parent_name, + load_context, + )]; + } + SceneNode::Group { + attributes: _, + children, + } => { + partial_node.children = children + .iter() + .map(|child| { + parse_xform_node(graph, &graph[*child as usize], parent_name, load_context) + }) + .collect(); + } + SceneNode::Shape { + attributes: _, + models, + } => { + let handle = load_context.get_label_handle(format!("model/{}", models[0].model_id)); + partial_node.model = Some(handle); + } + } +} + +fn get_accumulated_and_node_name( + parent_name: Option<&String>, + node_name: Option<&String>, +) -> (Option, Option) { + match (parent_name, node_name) { + (None, None) => (None, None), + (None, Some(node_name)) => (Some(node_name.to_string()), Some(node_name.to_string())), + (Some(parent_name), None) => (Some(parent_name.to_string()), None), // allow group name to pass down through unnamed child + (Some(parent_name), Some(node_name)) => { + let accumulated = format!("{}/{}", parent_name, node_name); + (Some(accumulated.clone()), Some(accumulated)) + } + } +} + +fn parse_bool(value: Option) -> bool { + match value.as_deref() { + Some("1") => true, + Some("0") => false, + Some(_) => { + warn!("Invalid boolean string"); + false + } + None => false, + } +} + +fn transform_from_frame(frame: &Frame) -> Mat4 { + let Some(position) = frame.position() else { return Mat4::IDENTITY }; + let position = [-position.x as f32, position.z as f32, position.y as f32]; + let translation = Mat4::from_translation(Vec3::from_array(position)); + let rotation = if let Some(orientation) = frame.orientation() { + let (rotation, scale) = &orientation.to_quat_scale(); + let scale: Vec3 = (*scale).into(); + let quat = Quat::from_array(*rotation); + let (axis, angle) = quat.to_axis_angle(); + let mat3 = Mat3::from_axis_angle(Vec3::new(-axis.x, axis.z, axis.y), angle) + * Mat3::from_diagonal(scale); + Mat4::from_mat3(mat3) + } else { + Mat4::IDENTITY + }; + translation * rotation +} diff --git a/src/scene/systems.rs b/src/scene/systems.rs new file mode 100644 index 0000000..70a3307 --- /dev/null +++ b/src/scene/systems.rs @@ -0,0 +1,122 @@ +use crate::VoxelSceneHook; +use bevy::{ + asset::{Assets, Handle}, + core::Name, + ecs::{ + entity::Entity, + query::Without, + system::{Commands, Query, Res}, + world::World, + }, + hierarchy::{BuildChildren, Children}, + log::warn, + pbr::PbrBundle, + render::{prelude::SpatialBundle, view::Visibility}, + transform::components::Transform, +}; + +use super::{VoxelLayer, VoxelModel, VoxelModelInstance, VoxelNode, VoxelScene}; + +pub(crate) fn spawn_vox_scenes( + mut commands: Commands, + query: Query<( + Entity, + &Handle, + Option<&Transform>, + Option<&Visibility>, + )>, + vox_scenes: Res>, + vox_models: Res>, +) { + for (root, scene_handle, transform, visibility) in query.iter() { + if let Some(scene) = vox_scenes.get(scene_handle) { + spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene, &vox_models); + let mut entity = commands.entity(root); + entity.remove::>(); + if let Some(transform) = transform { + entity.insert(*transform); + } + if let Some(visibility) = visibility { + entity.insert(*visibility); + } + } + } +} + +fn spawn_voxel_node_recursive( + commands: &mut Commands, + voxel_node: &VoxelNode, + entity: Entity, + scene: &VoxelScene, + vox_models: &Res>, +) { + let mut entity_commands = commands.entity(entity); + if let Some(name) = &voxel_node.name { + entity_commands.insert(Name::new(name.clone())); + } + if let Some(model_handle) = &voxel_node.model { + if let Some(model) = vox_models.get(model_handle) { + entity_commands.insert(VoxelModelInstance(model_handle.clone())); + #[cfg(not(test))] + entity_commands.insert(PbrBundle { + mesh: model.mesh.clone(), + material: model.material.clone(), + ..Default::default() + }); + } else { + warn!("Model not found, omitting: {:?}", model_handle); + entity_commands.insert(SpatialBundle::default()); + } + } else { + entity_commands.insert(SpatialBundle::default()); + } + + if let Some(layer_info) = scene.layers.get(voxel_node.layer_id as usize) { + entity_commands.insert(( + VoxelLayer { + id: voxel_node.layer_id, + name: layer_info.name.clone(), + }, + if voxel_node.is_hidden || layer_info.is_hidden { + Visibility::Hidden + } else { + Visibility::Inherited + }, + )); + } + entity_commands + .insert(Transform::from_matrix(voxel_node.transform)) + .with_children(|builder| { + for child in &voxel_node.children { + let mut child_entity = builder.spawn_empty(); + let id = child_entity.id(); + spawn_voxel_node_recursive(child_entity.commands(), child, id, scene, vox_models); + } + }); +} + +pub(crate) fn run_hooks( + mut commands: Commands, + world: &World, + query: Query<(Entity, &VoxelSceneHook), Without>>, +) { + for (entity, scene_hook) in query.iter() { + run_hook_recursive(&mut commands, world, entity, scene_hook); + commands.entity(entity).remove::(); + } +} + +fn run_hook_recursive( + commands: &mut Commands, + world: &World, + entity: Entity, + scene_hook: &VoxelSceneHook, +) { + let entity_ref = world.entity(entity); + let mut entity_commands = commands.entity(entity); + (scene_hook.hook)(&entity_ref, &mut entity_commands); + let Some(children) = entity_ref.get::() else { return }; + for child in children.as_ref() { + run_hook_recursive(commands, world, *child, scene_hook); + } +} diff --git a/src/scene/tests.rs b/src/scene/tests.rs new file mode 100644 index 0000000..2065ad2 --- /dev/null +++ b/src/scene/tests.rs @@ -0,0 +1,233 @@ +use super::*; +use crate::VoxScenePlugin; +use bevy::{ + app::App, + asset::{AssetApp, AssetPlugin, AssetServer, Assets, LoadState}, + core::Name, + hierarchy::Children, + render::texture::ImagePlugin, + MinimalPlugins, +}; + +#[async_std::test] +async fn test_load_scene() { + let mut app = App::new(); + let handle = setup_and_load_voxel_scene(&mut app, "test.vox").await; + app.update(); + let scene = app + .world + .resource::>() + .get(handle) + .expect("retrieve test.vox from Res"); + let all_models: Vec<&VoxelModel> = app + .world + .resource::>() + .iter() + .map(|(_, asset)| asset) + .collect(); + assert_eq!( + all_models.len(), + 3, + "Same 3 models are instanced through the scene" + ); + assert_eq!(scene.layers.len(), 8); + assert_eq!( + scene + .layers + .first() + .unwrap() + .name + .as_ref() + .expect("Layer 0 name"), + "scenery" + ); + let outer_group = scene.root.children.first().expect("First object in scene"); + assert_eq!( + outer_group.name.as_ref().expect("Name of first obj"), + "outer-group" + ); + assert_eq!(outer_group.children.len(), 3); + let inner_group = outer_group + .children + .first() + .expect("First child of outer-group"); + assert_eq!( + inner_group.name.as_ref().expect("name of inner group"), + "outer-group/inner-group" + ); +} + +#[async_std::test] +async fn test_load_scene_slice() { + let mut app = App::new(); + let handle = setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group").await; + app.update(); + let scene = app + .world + .resource::>() + .get(handle) + .expect("retrieve test.vox from Res"); + assert_eq!(scene.layers.len(), 8); + assert_eq!( + scene + .layers + .first() + .unwrap() + .name + .as_ref() + .expect("Layer 0 name"), + "scenery" + ); + let inner_group = &scene.root; + assert_eq!( + inner_group.name.as_ref().expect("Name of first obj"), + "outer-group/inner-group" + ); + assert_eq!(inner_group.children.len(), 4); + let dice = inner_group + .children + .last() + .expect("Last child of inner-group"); + assert_eq!( + dice.name.as_ref().expect("name of dice"), + "outer-group/inner-group/dice" + ); +} + +#[async_std::test] +async fn test_transmissive_mat() { + let mut app = App::new(); + let handle = + setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group/walls").await; + app.update(); + let scene = app + .world + .resource::>() + .get(handle) + .expect("retrieve scene from Res"); + let walls = &scene.root; + let model = app + .world + .resource::>() + .get(walls.model.as_ref().expect("Walls has a model handle")) + .expect("retrieve model from Res"); + let mat_handle = &model.material; + let material = app + .world + .resource::>() + .get(mat_handle) + .expect("material"); + assert!(material.specular_transmission_texture.is_some()); + assert_eq!(material.specular_transmission, 1.0); + assert!((material.ior - 1.3).abs() / 1.3 <= 0.00001); + assert!(material.metallic_roughness_texture.is_some()); +} + +#[async_std::test] +async fn test_opaque_mat() { + let mut app = App::new(); + let handle = + setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group/dice").await; + app.update(); + let scene = app + .world + .resource::>() + .get(handle) + .expect("retrieve scene from Res"); + let dice = &scene.root; + let model = app + .world + .resource::>() + .get(dice.model.as_ref().expect("Walls has a model handle")) + .expect("retrieve model from Res"); + let mat_handle = &model.material; + let material = app + .world + .resource::>() + .get(mat_handle) + .expect("material"); + assert!(material.specular_transmission_texture.is_none()); + assert_eq!(material.specular_transmission, 0.0); + assert!(material.metallic_roughness_texture.is_some()); +} + +#[async_std::test] +async fn test_spawn_system() { + let mut app = App::new(); + let handle = setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group").await; + app.update(); + + assert_eq!( + app.world + .resource::() + .load_state(handle.clone()), + LoadState::Loaded + ); + let entity = app + .world + .spawn(VoxelSceneHookBundle { + scene: handle, + hook: VoxelSceneHook::new(move |entity, _| { + let Some(name) = entity.get::() else { return }; + let expected_names: [&'static str; 3] = [ + "outer-group/inner-group", + "outer-group/inner-group/dice", + "outer-group/inner-group/walls", + ]; + assert!(expected_names.contains(&name.as_str())); + }), + ..Default::default() + }) + .id(); + app.update(); + assert!(app.world.get::>(entity).is_none()); + assert_eq!( + app.world.query::<&VoxelLayer>().iter(&app.world).len(), + 5, + "5 voxel nodes spawned in this scene slice" + ); + assert_eq!( + app.world.query::<&Name>().iter(&app.world).len(), + 3, + "But only 3 of the voxel nodes are named" + ); + assert_eq!( + app.world + .get::(entity) + .expect("Name component") + .as_str(), + "outer-group/inner-group" + ); + let children = app + .world + .get::(entity) + .expect("children of inner-group") + .as_ref(); + assert_eq!(children.len(), 4, "inner-group has 4 children"); + assert_eq!( + app.world + .get::(*children.last().expect("last child")) + .expect("Name component") + .as_str(), + "outer-group/inner-group/dice" + ); + app.update(); // fire the hooks +} + +/// `await` the response from this and then call `app.update()` +async fn setup_and_load_voxel_scene(app: &mut App, filename: &'static str) -> Handle { + app.add_plugins(( + MinimalPlugins, + AssetPlugin::default(), + ImagePlugin::default(), + VoxScenePlugin, + )) + .init_asset::() + .init_asset::(); + let assets = app.world.resource::(); + assets + .load_untyped_async(filename) + .await + .expect(format!("Loaded {filename}").as_str()) + .typed::() +} diff --git a/src/voxel_scene.rs b/src/voxel_scene.rs deleted file mode 100644 index b499bc4..0000000 --- a/src/voxel_scene.rs +++ /dev/null @@ -1,686 +0,0 @@ -use crate::voxel::VoxelData; -use bevy::{ - asset::{Asset, Assets, Handle, LoadContext}, - core::Name, - ecs::{ - bundle::Bundle, - component::Component, - entity::Entity, - query::Without, - system::{Commands, EntityCommands, Query, Res}, - world::{EntityRef, World}, - }, - hierarchy::{BuildChildren, Children}, - log::warn, - math::{Mat3, Mat4, Quat, Vec3}, - pbr::{PbrBundle, StandardMaterial}, - reflect::TypePath, - render::{mesh::Mesh, prelude::SpatialBundle, view::Visibility}, - transform::components::Transform, - utils::HashMap, -}; -use dot_vox::{Frame, SceneNode}; - -/// A component bundle for spawning Voxel Scenes. -/// -/// The root of the spawned scene will be the entity that has this bundle. -/// ```no_run -/// # use bevy::prelude::*; -/// # use bevy_vox_scene::VoxelSceneBundle; -/// -/// fn setup( -/// mut commands: Commands, -/// assets: Res, -/// ) { -/// commands.spawn(VoxelSceneBundle { -/// scene: assets.load("study.vox"), -/// ..default() -/// }); -/// -/// commands.spawn(VoxelSceneBundle { -/// // Load a single model using the name assigned to it in MagicaVoxel. -/// // If a model is nested in a named group, than the group will form part of the path -/// // Path components are separated with a slash -/// scene: assets.load("study.vox#workstation/desk"), -/// ..default() -/// }); -/// } -/// ``` -#[derive(Bundle, Default)] -pub struct VoxelSceneBundle { - /// A handle to a [`VoxelScene`], typically loaded from a ".vox" file via the [`bevy::asset::AssetServer`]. - /// This Entity will become the root of the spawned Voxel Scene. - pub scene: Handle, - /// The transform of the scene root. This will override whatever the root transform is in the Magica Voxel scene. - pub transform: Transform, - /// The visibility of the scene root. This will override whatever the root visibility is in the Magical Voxel scene. - pub visibility: Visibility, -} - -/// A component bundle for spawning Voxel Scenes, with a [`VoxelSceneHook`]. -/// -/// The root of the spawned scene will be the entity that has this bundle. -/// The [`VoxelSceneHook`] allows you to easily modify Entities deep within the scene hierarchy. -#[derive(Bundle, Default)] -pub struct VoxelSceneHookBundle { - /// A handle to a [`VoxelScene`], typically loaded from a ".vox" file via the [`bevy::asset::AssetServer`]. - /// This Entity will become the root of the spawned Voxel Scene. - pub scene: Handle, - /// A [`VoxelSceneHook`] allows you to specify a closure that will be run for each Entity spawned in the scene graph. - pub hook: VoxelSceneHook, - /// The transform of the scene root. This will override whatever the root transform is in the Magica Voxel scene. - pub transform: Transform, - /// The visibility of the scene root. This will override whatever the root visibility is in the Magical Voxel scene. - pub visibility: Visibility, -} - -/// A representation of the Voxel Scene Graph. -/// -/// To spawn a voxel scene, add a [Handle](VoxelScene), [`VoxelSceneBundle`], or [`VoxelSceneHookBundle`] to an Entity. -/// Voxel Scenes can be loaded from Magica Voxel .vox files. -#[derive(Asset, TypePath, Debug)] -pub struct VoxelScene { - pub(crate) root: VoxelNode, - pub(crate) layers: Vec, -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct VoxelNode { - name: Option, - transform: Mat4, - children: Vec, - model: Option>, - is_hidden: bool, - layer_id: u32, -} - -#[derive(Asset, TypePath)] -pub(crate) struct VoxelModel { - pub data: VoxelData, - pub mesh: Handle, - pub material: Handle, -} - -#[derive(Debug, Clone)] -pub(crate) struct LayerInfo { - pub name: Option, - pub is_hidden: bool, -} - -#[derive(Component)] -pub struct VoxelModelInstance(Handle); - -/// A component specifying which layer the Entity belongs to, with an optional name. -/// -/// This can be configured in the Magica Voxel world editor. -#[derive(Component, Clone)] -pub struct VoxelLayer { - /// The identifier for the layer. Magic Voxel 0.99.6 allows you to assign nodes to one of 8 layers, - /// so this value will be an index in the range 0 through 7. - pub id: u32, - /// An optional name for the Layer, assignable in Magica Voxel layer editor. - pub name: Option, -} - -/// A component containing a closure that will be run against every entity spawned from a Voxel Scene -/// -/// Assign this component to an entity that also has a [Handle](VoxelScene) to execute a closure -/// against every entity that gets spawned in the graph of the Voxel Scene. -/// This allows you to specify, before the scene graph has been spawned, how entities at a deeper level -/// than the root should be modified. A common use-case would adding custom components to entities -/// depending on their name or [`VoxelLayer`]. -/// ```rust -/// # use bevy::{prelude::*, app::AppExit, utils::HashSet}; -/// # use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle}; -/// # -/// # fn main() { -/// # App::new() -/// # .add_plugins(( -/// # DefaultPlugins, -/// # VoxScenePlugin, -/// # )) -/// # .add_systems(Startup, setup) -/// # .add_systems(Update, assert_scene_loaded) -/// # .run(); -/// # } -/// # -/// #[derive(Component)] -/// struct Fish; -/// -/// fn setup( -/// mut commands: Commands, -/// assets: Res, -/// ) { -/// commands.spawn(( -/// VoxelSceneHookBundle { -/// scene: assets.load("study.vox#tank"), -/// -/// // This closure will be run against every child Entity that gets spawned in the scene -/// hook: VoxelSceneHook::new(move |entity, commands| { -/// let Some(name) = entity.get::() else { return }; -/// match name.as_str() { -/// // Node names give the path to the asset, with components separated by /. Here, "goldfish" and "tetra" are two fish types in the "tank" -/// "tank/goldfish" | "tank/tetra" => { -/// // add a marker Component. -/// commands.insert(Fish); -/// } -/// _ => {}, -/// } -/// }), -/// ..default() -/// }, -/// )); -/// } -/// # -/// # fn assert_scene_loaded( -/// # query: Query<&Name, With>, -/// # mut exit: EventWriter, -/// # ) { -/// # let all_fish: Vec<&str> = query.iter().map(|n| { n.as_str() }).collect(); -/// # if all_fish.is_empty() { return }; -/// # assert_eq!(all_fish.len(), 5); -/// # let expected_names: HashSet<&str> = ["tank/tetra", "tank/goldfish"].into(); -/// # let all_names: HashSet<&str> = HashSet::from_iter(all_fish); -/// # assert_eq!(expected_names, all_names); -/// # exit.send(AppExit); -/// # } -/// ``` -#[derive(Component)] -pub struct VoxelSceneHook { - hook: Box, -} - -impl VoxelSceneHook { - /// Create a new hook with the closure `hook`. This will be run against every entity that gets spawned in the scene graph. - pub fn new(hook: F) -> Self { - Self { - hook: Box::new(hook), - } - } -} - -impl Default for VoxelSceneHook { - fn default() -> Self { - Self::new(|_, _| warn!("Default VoxelSceneHook does nothing")) - } -} - -pub(super) fn spawn_vox_scenes( - mut commands: Commands, - query: Query<( - Entity, - &Handle, - Option<&Transform>, - Option<&Visibility>, - )>, - vox_scenes: Res>, - vox_models: Res>, -) { - for (root, scene_handle, transform, visibility) in query.iter() { - if let Some(scene) = vox_scenes.get(scene_handle) { - spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene, &vox_models); - let mut entity = commands.entity(root); - entity.remove::>(); - if let Some(transform) = transform { - entity.insert(*transform); - } - if let Some(visibility) = visibility { - entity.insert(*visibility); - } - } - } -} - -fn spawn_voxel_node_recursive( - commands: &mut Commands, - voxel_node: &VoxelNode, - entity: Entity, - scene: &VoxelScene, - vox_models: &Res>, -) { - let mut entity_commands = commands.entity(entity); - if let Some(name) = &voxel_node.name { - entity_commands.insert(Name::new(name.clone())); - } - if let Some(model_handle) = &voxel_node.model { - if let Some(model) = vox_models.get(model_handle) { - entity_commands.insert(VoxelModelInstance(model_handle.clone())); - #[cfg(not(test))] - entity_commands.insert(PbrBundle { - mesh: model.mesh.clone(), - material: model.material.clone(), - ..Default::default() - }); - } else { - warn!("Model not found, omitting: {:?}", model_handle); - entity_commands.insert(SpatialBundle::default()); - } - } else { - entity_commands.insert(SpatialBundle::default()); - } - - if let Some(layer_info) = scene.layers.get(voxel_node.layer_id as usize) { - entity_commands.insert(( - VoxelLayer { - id: voxel_node.layer_id, - name: layer_info.name.clone(), - }, - if voxel_node.is_hidden || layer_info.is_hidden { - Visibility::Hidden - } else { - Visibility::Inherited - }, - )); - } - entity_commands - .insert(Transform::from_matrix(voxel_node.transform)) - .with_children(|builder| { - for child in &voxel_node.children { - let mut child_entity = builder.spawn_empty(); - let id = child_entity.id(); - spawn_voxel_node_recursive(child_entity.commands(), child, id, scene, vox_models); - } - }); -} - -pub(super) fn run_hooks( - mut commands: Commands, - world: &World, - query: Query<(Entity, &VoxelSceneHook), Without>>, -) { - for (entity, scene_hook) in query.iter() { - run_hook_recursive(&mut commands, world, entity, scene_hook); - commands.entity(entity).remove::(); - } -} - -fn run_hook_recursive( - commands: &mut Commands, - world: &World, - entity: Entity, - scene_hook: &VoxelSceneHook, -) { - let entity_ref = world.entity(entity); - let mut entity_commands = commands.entity(entity); - (scene_hook.hook)(&entity_ref, &mut entity_commands); - let Some(children) = entity_ref.get::() else { return }; - for child in children.as_ref() { - run_hook_recursive(commands, world, *child, scene_hook); - } -} - -pub(crate) fn parse_xform_node( - graph: &Vec, - scene_node: &SceneNode, - parent_name: Option<&String>, - load_context: &mut LoadContext, -) -> VoxelNode { - match scene_node { - SceneNode::Transform { - attributes, - frames, - child, - layer_id, - } => { - let (accumulated, node_name) = - get_accumulated_and_node_name(parent_name, attributes.get("_name")); - let mut vox_node = VoxelNode { - name: node_name, - transform: transform_from_frame(&frames[0]), - is_hidden: parse_bool(attributes.get("_hidden").cloned()), - layer_id: *layer_id, - ..Default::default() - }; - parse_xform_child( - graph, - &graph[*child as usize], - &mut vox_node, - accumulated.as_ref(), - load_context, - ); - vox_node - } - SceneNode::Group { .. } | SceneNode::Shape { .. } => { - warn!("Found Group or Shape Node without a parent Transform"); - let mut vox_node = VoxelNode::default(); - parse_xform_child(graph, scene_node, &mut vox_node, parent_name, load_context); - vox_node - } - } -} - -pub(crate) fn find_subasset_names( - subassets_by_name: &mut HashMap, - node: &VoxelNode, -) { - if let Some(name) = &node.name { - if !subassets_by_name.contains_key(name) { - subassets_by_name.insert(name.to_string(), node.clone()); - } - } - for child in &node.children { - find_subasset_names(subassets_by_name, child); - } -} - -fn parse_xform_child( - graph: &Vec, - scene_node: &SceneNode, - partial_node: &mut VoxelNode, - parent_name: Option<&String>, - load_context: &mut LoadContext, -) { - match scene_node { - SceneNode::Transform { .. } => { - warn!("Found nested Transform nodes"); - partial_node.children = vec![parse_xform_node( - graph, - scene_node, - parent_name, - load_context, - )]; - } - SceneNode::Group { - attributes: _, - children, - } => { - partial_node.children = children - .iter() - .map(|child| { - parse_xform_node(graph, &graph[*child as usize], parent_name, load_context) - }) - .collect(); - } - SceneNode::Shape { - attributes: _, - models, - } => { - let handle = load_context.get_label_handle(format!("model/{}", models[0].model_id)); - partial_node.model = Some(handle); - } - } -} - -fn get_accumulated_and_node_name( - parent_name: Option<&String>, - node_name: Option<&String>, -) -> (Option, Option) { - match (parent_name, node_name) { - (None, None) => (None, None), - (None, Some(node_name)) => (Some(node_name.to_string()), Some(node_name.to_string())), - (Some(parent_name), None) => (Some(parent_name.to_string()), None), // allow group name to pass down through unnamed child - (Some(parent_name), Some(node_name)) => { - let accumulated = format!("{}/{}", parent_name, node_name); - (Some(accumulated.clone()), Some(accumulated)) - } - } -} - -fn parse_bool(value: Option) -> bool { - match value.as_deref() { - Some("1") => true, - Some("0") => false, - Some(_) => { - warn!("Invalid boolean string"); - false - } - None => false, - } -} - -fn transform_from_frame(frame: &Frame) -> Mat4 { - let Some(position) = frame.position() else { return Mat4::IDENTITY }; - let position = [-position.x as f32, position.z as f32, position.y as f32]; - let translation = Mat4::from_translation(Vec3::from_array(position)); - let rotation = if let Some(orientation) = frame.orientation() { - let (rotation, scale) = &orientation.to_quat_scale(); - let scale: Vec3 = (*scale).into(); - let quat = Quat::from_array(*rotation); - let (axis, angle) = quat.to_axis_angle(); - let mat3 = Mat3::from_axis_angle(Vec3::new(-axis.x, axis.z, axis.y), angle) - * Mat3::from_diagonal(scale); - Mat4::from_mat3(mat3) - } else { - Mat4::IDENTITY - }; - translation * rotation -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::VoxScenePlugin; - use bevy::{ - app::App, - asset::{AssetApp, AssetPlugin, AssetServer, LoadState}, - hierarchy::Children, - render::texture::ImagePlugin, - MinimalPlugins, - }; - - #[async_std::test] - async fn test_load_scene() { - let mut app = App::new(); - let handle = setup_and_load_voxel_scene(&mut app, "test.vox").await; - app.update(); - let scene = app - .world - .resource::>() - .get(handle) - .expect("retrieve test.vox from Res"); - let all_models: Vec<&VoxelModel> = app - .world - .resource::>() - .iter() - .map(|(_, asset)| asset) - .collect(); - assert_eq!( - all_models.len(), - 3, - "Same 3 models are instanced through the scene" - ); - assert_eq!(scene.layers.len(), 8); - assert_eq!( - scene - .layers - .first() - .unwrap() - .name - .as_ref() - .expect("Layer 0 name"), - "scenery" - ); - let outer_group = scene.root.children.first().expect("First object in scene"); - assert_eq!( - outer_group.name.as_ref().expect("Name of first obj"), - "outer-group" - ); - assert_eq!(outer_group.children.len(), 3); - let inner_group = outer_group - .children - .first() - .expect("First child of outer-group"); - assert_eq!( - inner_group.name.as_ref().expect("name of inner group"), - "outer-group/inner-group" - ); - } - - #[async_std::test] - async fn test_load_scene_slice() { - let mut app = App::new(); - let handle = setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group").await; - app.update(); - let scene = app - .world - .resource::>() - .get(handle) - .expect("retrieve test.vox from Res"); - assert_eq!(scene.layers.len(), 8); - assert_eq!( - scene - .layers - .first() - .unwrap() - .name - .as_ref() - .expect("Layer 0 name"), - "scenery" - ); - let inner_group = &scene.root; - assert_eq!( - inner_group.name.as_ref().expect("Name of first obj"), - "outer-group/inner-group" - ); - assert_eq!(inner_group.children.len(), 4); - let dice = inner_group - .children - .last() - .expect("Last child of inner-group"); - assert_eq!( - dice.name.as_ref().expect("name of dice"), - "outer-group/inner-group/dice" - ); - } - - #[async_std::test] - async fn test_transmissive_mat() { - let mut app = App::new(); - let handle = - setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group/walls").await; - app.update(); - let scene = app - .world - .resource::>() - .get(handle) - .expect("retrieve scene from Res"); - let walls = &scene.root; - let model = app - .world - .resource::>() - .get(walls.model.as_ref().expect("Walls has a model handle")) - .expect("retrieve model from Res"); - let mat_handle = &model.material; - let material = app - .world - .resource::>() - .get(mat_handle) - .expect("material"); - assert!(material.specular_transmission_texture.is_some()); - assert_eq!(material.specular_transmission, 1.0); - assert!((material.ior - 1.3).abs() / 1.3 <= 0.00001); - assert!(material.metallic_roughness_texture.is_some()); - } - - #[async_std::test] - async fn test_opaque_mat() { - let mut app = App::new(); - let handle = - setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group/dice").await; - app.update(); - let scene = app - .world - .resource::>() - .get(handle) - .expect("retrieve scene from Res"); - let dice = &scene.root; - let model = app - .world - .resource::>() - .get(dice.model.as_ref().expect("Walls has a model handle")) - .expect("retrieve model from Res"); - let mat_handle = &model.material; - let material = app - .world - .resource::>() - .get(mat_handle) - .expect("material"); - assert!(material.specular_transmission_texture.is_none()); - assert_eq!(material.specular_transmission, 0.0); - assert!(material.metallic_roughness_texture.is_some()); - } - - #[async_std::test] - async fn test_spawn_system() { - let mut app = App::new(); - let handle = setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group").await; - app.update(); - - assert_eq!( - app.world - .resource::() - .load_state(handle.clone()), - LoadState::Loaded - ); - let entity = app - .world - .spawn(VoxelSceneHookBundle { - scene: handle, - hook: VoxelSceneHook::new(move |entity, _| { - let Some(name) = entity.get::() else { return }; - let expected_names: [&'static str; 3] = [ - "outer-group/inner-group", - "outer-group/inner-group/dice", - "outer-group/inner-group/walls", - ]; - assert!(expected_names.contains(&name.as_str())); - }), - ..Default::default() - }) - .id(); - app.update(); - assert!(app.world.get::>(entity).is_none()); - assert_eq!( - app.world.query::<&VoxelLayer>().iter(&app.world).len(), - 5, - "5 voxel nodes spawned in this scene slice" - ); - assert_eq!( - app.world.query::<&Name>().iter(&app.world).len(), - 3, - "But only 3 of the voxel nodes are named" - ); - assert_eq!( - app.world - .get::(entity) - .expect("Name component") - .as_str(), - "outer-group/inner-group" - ); - let children = app - .world - .get::(entity) - .expect("children of inner-group") - .as_ref(); - assert_eq!(children.len(), 4, "inner-group has 4 children"); - assert_eq!( - app.world - .get::(*children.last().expect("last child")) - .expect("Name component") - .as_str(), - "outer-group/inner-group/dice" - ); - app.update(); // fire the hooks - } - - /// `await` the response from this and then call `app.update()` - async fn setup_and_load_voxel_scene( - app: &mut App, - filename: &'static str, - ) -> Handle { - app.add_plugins(( - MinimalPlugins, - AssetPlugin::default(), - ImagePlugin::default(), - VoxScenePlugin, - )) - .init_asset::() - .init_asset::(); - let assets = app.world.resource::(); - assets - .load_untyped_async(filename) - .await - .expect(format!("Loaded {filename}").as_str()) - .typed::() - } -}