Skip to content

Commit

Permalink
Add scene hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Utsira committed Jan 13, 2024
1 parent 412afd9 commit 822f5a5
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 111 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = "A Bevy engine plugin for loading Magica Voxel world files and ren
keywords = ["Bevy", "voxel", "Magica-Voxel"]
categories = ["game-development", "graphics", "rendering", "rendering::data-formats"]
license = "MIT"
version = "0.10.4"
version = "0.11.0"
repository = "https://github.com/Utsira/bevy_vox_scene"
authors = ["Oliver Dew <[email protected]>"]
edition = "2021"
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
<code>bevy_vox_scene</code>
</h1>

<a href="https://crates.io/crates/bevy_vox_scene">
<img height="24" src="https://img.shields.io/crates/v/bevy_vox_scene?style=for-the-badge"/>
</a>

[![Latest version](https://img.shields.io/crates/v/bevy_vox_scene.svg)](https://crates.io/crates/bevy_vox_scene)
[![CI](https://github.com/Utsira/bevy_vox_scene/actions/workflows/ci.yml/badge.svg)](https://github.com/Utsira/bevy_vox_scene/actions/workflows/ci.yml)

A plugin for [the Bevy Engine](https://bevyengine.org) which allows loading [Magica Voxel](https://ephtracy.github.io) `.vox` files directly into a Bevy scene graph.
Expand Down
Binary file modified assets/study.vox
Binary file not shown.
140 changes: 62 additions & 78 deletions examples/modify-scene.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::f32::consts::PI;
use rand::Rng;

use bevy::{prelude::*, core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping, core_3d::ScreenSpaceTransmissionQuality, experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasBundle}}, input::keyboard::KeyboardInput};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle, VoxelEntityReady};
use bevy::{prelude::*, core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping, core_3d::ScreenSpaceTransmissionQuality, experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasBundle}}, input::keyboard::KeyboardInput, ecs::system::EntityCommands};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle};
use bevy_panorbit_camera::{PanOrbitCameraPlugin, PanOrbitCamera};

/// Uses the [`bevy_vox_scene::VoxelEntityReady`] event to add extra components into the scene graph.
/// 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
fn main() {
let mut app = App::new();
Expand All @@ -15,10 +15,8 @@ fn main() {
PanOrbitCameraPlugin,
VoxScenePlugin,
))
.init_resource::<TankSize>()
.add_systems(Startup, setup)
.add_systems(Update, (
add_components_to_voxel_entities.run_if(on_event::<VoxelEntityReady>()),
toggle_black_light.run_if(on_event::<KeyboardInput>()),
swim_fish,
));
Expand All @@ -33,6 +31,8 @@ fn main() {
app.run();
}

// Systems

fn setup(
mut commands: Commands,
assets: Res<AssetServer>,
Expand Down Expand Up @@ -64,77 +64,36 @@ fn setup(
specular_map: assets.load("pisa_specular.ktx2"),
},
));

commands.spawn(VoxelSceneBundle {
// "tank" is the name of the group containing the glass walls, the body of water, the scenery in the tank and the fish
scene: assets.load("study.vox#tank"),
transform: Transform::from_scale(Vec3::splat(0.05)),
..default()
});
}

#[derive(Component)]
struct EmissiveToggle {
is_on: bool,
on_material: Handle<StandardMaterial>,
off_material: Handle<StandardMaterial>,
}

impl EmissiveToggle {
fn toggle(&mut self) {
self.is_on = !self.is_on;
}

fn material(&self) -> &Handle<StandardMaterial> {
match self.is_on {
true => &self.on_material,
false => &self.off_material,
}
}
}

#[derive(Component)]
struct Fish(f32);

#[derive(Resource, Default)]
struct TankSize(Vec3);
let asset_server = assets.clone();
commands.spawn((
VoxelSceneHookBundle {
// "tank" is the name of the group containing the glass walls, the body of water, the scenery in the tank and the fish
scene: assets.load("study.vox#tank"),

fn add_components_to_voxel_entities(
mut commands: Commands,
mut event_reader: EventReader<VoxelEntityReady>,
mesh_query: Query<&Handle<Mesh>>,
meshes: Res<Assets<Mesh>>,
mut tank_size: ResMut<TankSize>,
assets: Res<AssetServer>,
) {
let mut rng = rand::thread_rng();
for event in event_reader.read() {
// If we are spawning multiple scenes we could match on the scene that the entity was spawned from. Here we just check it is the scene we're expecting.
if event.scene_name != "study.vox#tank" { return };
match event.name.as_str() {
// Node names give the path to the asset, with components separated by /. Here, "black-light" belongs to the "tank" group
"tank/black-light" => {
commands.entity(event.entity).insert(EmissiveToggle {
is_on: true,
on_material: assets.load("study.vox#material"), // emissive texture
off_material: assets.load("study.vox#material-no-emission"), // non-emissive texture
});
},
"tank/goldfish" | "tank/tetra" => {
// Make fish go brrrrr
commands.entity(event.entity).insert(Fish(rng.gen_range(5.0..10.0)));
}
"tank/water" => {
// measure size of tank
let Ok(mesh_handle) = mesh_query.get_component::<Handle<Mesh>>(event.entity) else { return };
let Some(mesh) = meshes.get(mesh_handle) else { return };
let Some(aabb) = mesh.compute_aabb() else { return };
let half_extent: Vec3 = aabb.half_extents.into();
tank_size.0 = half_extent - Vec3::splat(6.0); // add a margin
}
_ => {},
}
}
// 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, "black-light" belongs to the "tank" group
"tank/black-light" => {
commands.insert(EmissiveToggle {
is_on: true,
on_material: asset_server.load("study.vox#material"), // emissive texture
off_material: asset_server.load("study.vox#material-no-emission"), // non-emissive texture
});
},
"tank/goldfish" | "tank/tetra" => {
// Make fish go brrrrr
let mut rng = rand::thread_rng(); // random speed
commands.insert(Fish(rng.gen_range(5.0..10.0)));
}
_ => {},
}
}),
transform: Transform::from_scale(Vec3::splat(0.05)),
..default()
},
));
}

fn toggle_black_light(
Expand All @@ -151,19 +110,44 @@ fn toggle_black_light(
fn swim_fish(
mut query: Query<(&mut Transform, &Fish)>,
time: Res<Time>,
tank_size: Res<TankSize>,
) {
let tank_half_extents = Vec3::new(29.0, 20.0, 25.0);
for (mut transform, fish) in query.iter_mut() {
let x_direction = transform.forward().dot(Vec3::X);
if (x_direction < -0.5 && transform.translation.x < -tank_size.0.x) || (x_direction > 0.5 && transform.translation.x > tank_size.0.x) {
if (x_direction < -0.5 && transform.translation.x < -tank_half_extents.x) || (x_direction > 0.5 && transform.translation.x > tank_half_extents.x) {
// change direction at tank edges
transform.rotate_axis(Vec3::Y, PI);
}
// slow down when near the edge
let slow_down = 1.0 - ((transform.translation.x.abs() - (tank_size.0.x - 4.2)) / 5.0).clamp(0.0, 1.0);
let slow_down = 1.0 - ((transform.translation.x.abs() - (tank_half_extents.x - 4.2)) / 5.0).clamp(0.0, 1.0);
let forward = transform.forward();
transform.translation += forward * (time.delta_seconds() * fish.0 * slow_down);
// make them weave up and down
transform.translation.y = (transform.translation.x * 0.1).sin() * 6.0;
}
}

// Components

#[derive(Component)]
struct EmissiveToggle {
is_on: bool,
on_material: Handle<StandardMaterial>,
off_material: Handle<StandardMaterial>,
}

impl EmissiveToggle {
fn toggle(&mut self) {
self.is_on = !self.is_on;
}

fn material(&self) -> &Handle<StandardMaterial> {
match self.is_on {
true => &self.on_material,
false => &self.off_material,
}
}
}

#[derive(Component)]
struct Fish(f32);
11 changes: 6 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@
//!```
use bevy::{
app::{App, Plugin, SpawnScene},
asset::AssetApp,
asset::AssetApp, ecs::schedule::IntoSystemConfigs,
};

mod loader;
mod voxel_scene;
pub use voxel_scene::VoxelEntityReady;
pub use voxel_scene::{VoxelSceneBundle, VoxelScene, VoxelLayer};
pub use voxel_scene::{VoxelSceneBundle, VoxelSceneHookBundle, VoxelScene, VoxelLayer, VoxelSceneHook};
pub use loader::VoxLoaderSettings;
#[doc(inline)]
use loader::VoxSceneLoader;
Expand All @@ -57,7 +56,9 @@ impl Plugin for VoxScenePlugin {
app
.init_asset::<voxel_scene::VoxelScene>()
.register_asset_loader(VoxSceneLoader)
.add_event::<VoxelEntityReady>()
.add_systems(SpawnScene, voxel_scene::spawn_vox_scenes);
.add_systems(SpawnScene, (
voxel_scene::spawn_vox_scenes,
voxel_scene::run_hooks
).chain());
}
}
90 changes: 67 additions & 23 deletions src/voxel_scene.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bevy::{ecs::{bundle::Bundle, component::Component, system::{Commands, Query, Res}, entity::Entity, event::{Event, EventWriter}}, asset::{Handle, Asset, Assets}, transform::components::Transform, reflect::TypePath, math::{Mat4, Vec3, Mat3, Quat}, render::{mesh::Mesh, view::Visibility, prelude::SpatialBundle}, pbr::{StandardMaterial, PbrBundle}, core::Name, hierarchy::BuildChildren, log::warn};
use bevy::{ecs::{bundle::Bundle, component::Component, system::{Commands, EntityCommands, Query, Res}, entity::Entity, world::{EntityRef, World}, query::Without}, asset::{Handle, Asset, Assets}, transform::components::Transform, reflect::TypePath, math::{Mat4, Vec3, Mat3, Quat}, render::{mesh::Mesh, view::Visibility, prelude::SpatialBundle}, pbr::{StandardMaterial, PbrBundle}, core::Name, hierarchy::{BuildChildren, Children}, log::warn};
use dot_vox::{SceneNode, Frame};

#[derive(Bundle, Default)]
Expand All @@ -8,6 +8,14 @@ pub struct VoxelSceneBundle {
pub visibility: Visibility,
}

#[derive(Bundle, Default)]
pub struct VoxelSceneHookBundle {
pub scene: Handle<VoxelScene>,
pub hook: VoxelSceneHook,
pub transform: Transform,
pub visibility: Visibility,
}

#[derive(Asset, TypePath, Debug)]
pub struct VoxelScene {
pub name: String,
Expand Down Expand Up @@ -44,23 +52,35 @@ pub struct VoxelLayer {
pub name: Option<String>,
}

#[derive(Event)]
pub struct VoxelEntityReady {
pub scene_name: String,
pub entity: Entity,
pub name: String,
pub layer_id: u32,
#[derive(Component)]
pub struct VoxelSceneHook {
hook: Box<dyn Fn(&EntityRef, &mut EntityCommands) + Send + Sync + 'static>,
}

impl VoxelSceneHook {
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")
})
}
}

pub(crate) fn spawn_vox_scenes(
mut commands: Commands,
query: Query<(Entity, &Transform, &Visibility, &Handle<VoxelScene>)>,
vox_scenes: Res<Assets<VoxelScene>>,
mut event_writer: EventWriter<VoxelEntityReady>,
) {
for (root, transform, visibility, scene_handle) in query.iter() {
if let Some(scene) = vox_scenes.get(scene_handle) {
spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene, &mut event_writer);
spawn_voxel_node_recursive(&mut commands, &scene.root, root, scene);
commands.entity(root)
.remove::<Handle<VoxelScene>>()
.insert((*transform, *visibility));
Expand All @@ -73,9 +93,11 @@ fn spawn_voxel_node_recursive(
voxel_node: &VoxelNode,
entity: Entity,
scene: &VoxelScene,
event_writer: &mut EventWriter<VoxelEntityReady>,
) {
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) = voxel_node.model_id.and_then(|id| {
if let Some(model) = scene.models.get(id) {
Some(model)
Expand Down Expand Up @@ -109,17 +131,34 @@ fn spawn_voxel_node_recursive(
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, event_writer);
spawn_voxel_node_recursive(child_entity.commands(), child, id, scene);
}
});
if let Some(name) = &voxel_node.name {
entity_commands.insert(Name::new(name.clone()));
event_writer.send(VoxelEntityReady {
scene_name: scene.name.clone(),
entity,
name: name.to_string(),
layer_id: voxel_node.layer_id
});
}

pub(super) fn run_hooks(
mut commands: Commands,
world: &World,
query: Query<(Entity, &VoxelSceneHook), Without<Handle<VoxelScene>>>,
) {
for (entity, scene_hook) in query.iter() {
run_hook_recursive(&mut commands, world, entity, scene_hook);
commands.entity(entity).remove::<VoxelSceneHook>();
}
}

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::<Children>() else { return };
for child in children.as_ref() {
run_hook_recursive(commands, world, *child, scene_hook);
}
}

Expand Down Expand Up @@ -221,7 +260,7 @@ fn transform_from_frame(frame: &Frame) -> Mat4 {

#[cfg(test)]
mod tests {
use bevy::{app::App, asset::{AssetPlugin, AssetServer, LoadState, AssetApp}, MinimalPlugins, render::texture::ImagePlugin, hierarchy::Children, reflect::Enum};
use bevy::{app::App, asset::{AssetPlugin, AssetServer, LoadState, AssetApp}, MinimalPlugins, render::texture::ImagePlugin, hierarchy::Children};
use crate::VoxScenePlugin;
use super::*;

Expand Down Expand Up @@ -293,21 +332,26 @@ mod tests {
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::<AssetServer>().load_state(handle.clone()), LoadState::Loaded);
let entity = app.world.spawn(VoxelSceneBundle {
let entity = app.world.spawn(VoxelSceneHookBundle {
scene: handle,
hook: VoxelSceneHook::new(move |entity, _| {
let Some(name) = entity.get::<Name>() 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::<Handle<VoxelScene>>(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::<Name>(entity).expect("Name component").as_str(), "outer-group/inner-group");
let children = app.world.get::<Children>(entity).expect("children of inner-group").as_ref();
assert_eq!(children.len(), 4, "inner-group has 4 children");
assert_eq!(app.world.get::<Name>(*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()`
Expand Down

0 comments on commit 822f5a5

Please sign in to comment.