Skip to content

Commit

Permalink
Refactor scene into sub-modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Utsira committed Jan 15, 2024
1 parent b6ffa4f commit fc69d07
Show file tree
Hide file tree
Showing 10 changed files with 743 additions and 728 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
Cargo.lock
.DS_Store
lcov.info
lcov.info
.idea
4 changes: 2 additions & 2 deletions examples/modify-scene.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use rand::Rng;
use std::f32::consts::PI;
use bevy::{
core_pipeline::{
bloom::BloomSettings,
Expand All @@ -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
Expand Down
14 changes: 6 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,20 @@
//! # }
//!```
#![forbid(missing_docs, unsafe_code)]

use bevy::{
app::{App, Plugin, SpawnScene},
asset::AssetApp,
ecs::schedule::IntoSystemConfigs,
};

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;

Expand All @@ -69,13 +68,12 @@ pub struct VoxScenePlugin;

impl Plugin for VoxScenePlugin {
fn build(&self, app: &mut App) {
app
.init_asset::<voxel_scene::VoxelScene>()
app.init_asset::<VoxelScene>()
.init_asset::<VoxelModel>()
.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(),
);
}
}
58 changes: 27 additions & 31 deletions src/loader.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::voxel::VoxelData;
use anyhow::anyhow;
use bevy::{
asset::{io::Reader, AssetLoader, AsyncReadExt, Handle, LoadContext},
Expand All @@ -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.
///
Expand Down Expand Up @@ -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<f32> = file
Expand All @@ -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<Handle<Image>> = if has_metallic_roughness {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<LayerInfo> = file
.layers
.iter()
Expand All @@ -403,17 +402,14 @@ impl VoxSceneLoader {
})
.collect();
let mut subasset_by_name: HashMap<String, VoxelNode> = 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 {
root: node,
layers: layers.clone(),
});
}
Ok(VoxelScene {
root,
layers,
})
Ok(VoxelScene { root, layers })
}
}
87 changes: 87 additions & 0 deletions src/scene/hook.rs
Original file line number Diff line number Diff line change
@@ -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>](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<AssetServer>,
/// ) {
/// 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::<Name>() 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<Fish>>,
/// # mut exit: EventWriter<AppExit>,
/// # ) {
/// # 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<dyn Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>,
}

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<F: Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>(hook: F) -> Self {
Self {
hook: Box::new(hook),
}
}
}

impl Default for VoxelSceneHook {
fn default() -> Self {
Self::new(|_, _| warn!("Default VoxelSceneHook does nothing"))
}
}
118 changes: 118 additions & 0 deletions src/scene/mod.rs
Original file line number Diff line number Diff line change
@@ -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<AssetServer>,
/// ) {
/// 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<VoxelScene>,
/// 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<VoxelScene>,
/// 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>](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<LayerInfo>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct VoxelNode {
name: Option<String>,
transform: Mat4,
children: Vec<VoxelNode>,
model: Option<Handle<VoxelModel>>,
is_hidden: bool,
layer_id: u32,
}

#[derive(Asset, TypePath)]
pub(crate) struct VoxelModel {
pub data: VoxelData,
pub mesh: Handle<Mesh>,
pub material: Handle<StandardMaterial>,
}

#[derive(Debug, Clone)]
pub(crate) struct LayerInfo {
pub name: Option<String>,
pub is_hidden: bool,
}

#[derive(Component)]
pub struct VoxelModelInstance(Handle<VoxelModel>);

/// 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<String>,
}
Loading

0 comments on commit fc69d07

Please sign in to comment.