Skip to content

Commit

Permalink
Merge pull request #9 from Utsira/feature/component-hooks-observers
Browse files Browse the repository at this point in the history
Feature/component hooks observers
  • Loading branch information
Utsira authored Jul 27, 2024
2 parents 92a307b + 9932b54 commit 6285421
Show file tree
Hide file tree
Showing 18 changed files with 409 additions and 459 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Take a look in the `examples/` directory for complete working examples. To run a
cargo run --example <example name>
```

- To modify entities within a scene hierarchy using scene hooks, see the [`modify-scene` example](/examples/modify-scene.rs).
- To modify entities within a scene hierarchy using bevy observers, see the [`modify-scene` example](/examples/modify-scene.rs).
- If you want glowing emissive voxels, add an HDR and bloom-enabled camera. See the [`emissive-model` example](/examples/emissive-model.rs).
- Enabling Screen-Space Ambient Occlusion can give your voxel scenes more pop. See the [`ssao-model` example](/examples/ssao-model.rs).
- If you want glass voxels to refract other objects in the scene, enable specular transmission on your camera3d. See the [`transmission-scene` example](/examples/transmission-scene.rs).
Expand Down Expand Up @@ -102,5 +102,3 @@ TLDR: split up models containing glass voxels into convex chunks using Magica Vo
Forked from the excellent [`bevy_vox_mesh` crate](https://crates.io/crates/bevy_vox_mesh) by Lucas A.

Like `bevy-vox-mesh`, `bevy-vox-scene` uses [`dot-vox`](https://github.com/dust-engine/dot_vox) to parse the vox files and the greedy mesher from [`block-mesh-rs`] (https://github.com/bonsairobo/block-mesh-rs) to create efficient meshes.

`VoxelSceneHook` is adapted from [bevy-scene-hook](https://github.com/nicopap/bevy-scene-hook) by Nicola Papale.
2 changes: 1 addition & 1 deletion examples/emissive-model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::{core_pipeline::bloom::BloomSettings, prelude::*};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

fn main() {
App::new()
Expand Down
63 changes: 35 additions & 28 deletions examples/modify-scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use bevy::{
input::keyboard::KeyboardInput,
prelude::*,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneHook, VoxelSceneHookBundle};
use bevy_vox_scene::{VoxScenePlugin, VoxelModelInstance, VoxelSceneBundle};
use rand::Rng;
use std::f32::consts::PI;
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

/// 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 Expand Up @@ -69,36 +69,43 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
intensity: 500.0,
},
));
let asset_server = assets.clone();
commands.spawn((VoxelSceneHookBundle {
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"),

// 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()
},));
});
commands.observe(on_spawn_voxel_instance);
}

// Will run against every child entity that gets spawned in the scene
fn on_spawn_voxel_instance(
trigger: Trigger<OnAdd, VoxelModelInstance>,
model_query: Query<&VoxelModelInstance>,
mut commands: Commands,
assets: Res<AssetServer>,
) {
let mut entity_commands = commands.entity(trigger.entity());
let name = model_query
.get(trigger.entity())
.unwrap()
.model_name
.as_str();
match name {
"tank/black-light" => {
entity_commands.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
let mut rng = rand::thread_rng(); // random speed
entity_commands.insert(Fish(rng.gen_range(5.0..10.0)));
}
_ => {}
}
}

fn toggle_black_light(
Expand Down
30 changes: 19 additions & 11 deletions examples/modify-voxels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ use bevy::{
prelude::*,
time::common_conditions::on_timer,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{
ModifyVoxelCommandsExt, VoxScenePlugin, Voxel, VoxelModelInstance, VoxelRegion,
VoxelRegionMode, VoxelSceneHook, VoxelSceneHookBundle,
VoxelRegionMode, VoxelSceneBundle,
};
use rand::Rng;
use std::{ops::RangeInclusive, time::Duration};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

fn main() {
App::new()
Expand Down Expand Up @@ -58,19 +58,27 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
..default()
});

commands.spawn(VoxelSceneHookBundle {
commands.spawn(VoxelSceneBundle {
scene: assets.load("study.vox"),
hook: VoxelSceneHook::new(move |entity, commands| {
let Some(name) = entity.get::<Name>() else {
return;
};
if name.as_str() == "floor" {
commands.insert(Floor);
}
}),
transform: Transform::from_scale(Vec3::splat(0.05)),
..default()
});
commands.observe(on_spawn_voxel_instance);
}

fn on_spawn_voxel_instance(
trigger: Trigger<OnAdd, VoxelModelInstance>,
model_query: Query<&VoxelModelInstance>,
mut commands: Commands,
) {
let name = model_query
.get(trigger.entity())
.unwrap()
.model_name
.as_str();
if name == "floor" {
commands.entity(trigger.entity()).insert(Floor);
}
}

fn grow_grass(mut commands: Commands, query: Query<&VoxelModelInstance, With<Floor>>) {
Expand Down
2 changes: 1 addition & 1 deletion examples/scene-slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bevy::{
},
prelude::*,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

/// Asset labels aren't just for loading individual models within a scene, they can load any named group within a scene, a "slice" of the scene
/// Here, just the workstation is loaded from the example scene
Expand Down
2 changes: 1 addition & 1 deletion examples/ssao-model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bevy::{
pbr::ScreenSpaceAmbientOcclusionBundle,
prelude::*,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

/// Press any key to toggle Screen Space Ambient Occlusion
fn main() {
Expand Down
25 changes: 15 additions & 10 deletions examples/transmission-scene.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
use bevy::{
core_pipeline::{
bloom::BloomSettings, core_3d::ScreenSpaceTransmissionQuality, experimental::taa::{TemporalAntiAliasBundle, TemporalAntiAliasPlugin}, tonemapping::Tonemapping
}, pbr::{VolumetricFogSettings, VolumetricLight}, prelude::*
bloom::BloomSettings,
core_3d::ScreenSpaceTransmissionQuality,
experimental::taa::{TemporalAntiAliasBundle, TemporalAntiAliasPlugin},
tonemapping::Tonemapping,
},
pbr::{VolumetricFogSettings, VolumetricLight},
prelude::*,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{VoxScenePlugin, VoxelSceneBundle};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

fn main() {
let mut app = App::new();

app.add_plugins((DefaultPlugins, PanOrbitCameraPlugin, VoxScenePlugin))
.add_systems(Startup, setup);
.add_systems(Startup, setup);

// *Note:* TAA is not _required_ for specular transmission, but
// it _greatly enhances_ the look of the resulting blur effects.
// Sadly, it's not available under WebGL.
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
app.insert_resource(Msaa::Off)
.add_plugins(TemporalAntiAliasPlugin);
.add_plugins(TemporalAntiAliasPlugin);

app.run();
}

Expand Down Expand Up @@ -52,7 +57,7 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
},
VolumetricFogSettings::default(),
));

commands.spawn((
DirectionalLightBundle {
directional_light: DirectionalLight {
Expand All @@ -65,7 +70,7 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
},
VolumetricLight,
));

commands.spawn(VoxelSceneBundle {
scene: assets.load("study.vox"),
transform: Transform::from_scale(Vec3::splat(0.05)),
Expand Down
95 changes: 68 additions & 27 deletions examples/voxel-collisions.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
use std::time::Duration;

use bevy::{
core_pipeline::{bloom::BloomSettings, dof::{DepthOfFieldMode, DepthOfFieldSettings}, tonemapping::Tonemapping}, gizmos::gizmos, prelude::*, time::common_conditions::on_timer
core_pipeline::{
bloom::BloomSettings,
dof::{DepthOfFieldMode, DepthOfFieldSettings},
tonemapping::Tonemapping,
},
prelude::*,
time::common_conditions::on_timer,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{
ModifyVoxelCommandsExt, VoxScenePlugin, Voxel, VoxelModelCollection, VoxelModelInstance,
VoxelQueryable, VoxelRegion, VoxelRegionMode, VoxelScene, VoxelSceneBundle, VoxelSceneHook,
VoxelSceneHookBundle,
DidSpawnVoxelChild, ModifyVoxelCommandsExt, VoxScenePlugin, Voxel, VoxelModelCollection,
VoxelModelInstance, VoxelQueryable, VoxelRegion, VoxelRegionMode, VoxelScene, VoxelSceneBundle,
};
use rand::Rng;
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

#[derive(States, Debug, Clone, Default, Hash, Eq, PartialEq)]
enum AppState {
#[default]
Loading,
Ready,
}

// When a snowflake lands on the scenery, it is added to scenery's voxel data, so that snow gradually builds up
fn main() {
Expand All @@ -19,13 +31,17 @@ fn main() {
App::new()
.add_plugins((DefaultPlugins, PanOrbitCameraPlugin, VoxScenePlugin))
.add_systems(Startup, setup)
.add_systems(Update,
.add_systems(
Update,
(
spawn_snow.run_if(on_timer(snow_spawn_freq)),
spawn_snow.run_if(on_timer(snow_spawn_freq)),
update_snow,
focus_camera,
),
)
.run_if(in_state(AppState::Ready)),
)
.init_state::<AppState>()
.observe(on_assets_spawned)
.run();
}

Expand All @@ -34,6 +50,35 @@ struct Scenes {
snowflake: Handle<VoxelScene>,
}

/// The [`DidSpawnVoxelChild`] event is targeted at the root of a specific [`VoxelScene`], and triggers
/// once for each child [`VoxelModelInstance`] that gets spawned in that node graph.
/// This is useful when we will be spawning other voxel scenes, so that we can scope the observer
/// to one scene and not worry about adding in defensive code.
/// The event also includes the [`model_name`] and [`layer_name`] so you need fewer queries in your observer.
/// Compare with the observer triggered with [`Trigger<OnAdd, VoxelModelInstance>`] in [modify scene](./modify-scene.rs).
fn on_spawn_voxel_instance(trigger: Trigger<DidSpawnVoxelChild>, mut commands: Commands) {
// Note that we're interested in the child entity, which is `trigger.event().child`
// Not the root entity, which is `trigger.entity()`
let mut entity_commands = commands.entity(trigger.event().child);
let name = trigger.event().model_name.as_str();
match name {
"snowflake" => panic!("This should never be executed, because this observer is scoped to the 'workstation' scene graph"),
"workstation/computer" => {
// Focus on the computer screen by suppling the local voxel coordinates of the center of the screen
entity_commands.insert(FocalPoint(Vec3::new(0., 0., 9.)));
}
_ => {}
}
entity_commands.insert(Scenery);
}

fn on_assets_spawned(
_trigger: Trigger<OnAdd, FocalPoint>,
mut app_state: ResMut<NextState<AppState>>,
) {
app_state.set(AppState::Ready);
}

fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn((
Camera3dBundle {
Expand Down Expand Up @@ -66,22 +111,14 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
snowflake: assets.load("study.vox#snowflake"),
});

commands.spawn(VoxelSceneHookBundle {
// Load a slice of the scene
scene: assets.load("study.vox#workstation"),
hook: VoxelSceneHook::new(|entity, commands| {
if entity.get::<VoxelModelInstance>().is_some() {
commands.insert(Scenery);
}
if let Some(name) = entity.get::<Name>() {
if name.as_str() == "workstation/computer" {
// Focus on the computer screen
commands.insert(FocalPoint(Vec3::new(0., 0., 9.)));
}
}
}),
..default()
});
// NB the `on_spawn_voxel_instance` observer is scoped to just this scene graph: we don't want it firing when snowflakes are spawned later on.
commands
.spawn(VoxelSceneBundle {
// Load a slice of the scene
scene: assets.load("study.vox#workstation"),
..default()
})
.observe(on_spawn_voxel_instance);
}

#[derive(Component)]
Expand Down Expand Up @@ -169,13 +206,17 @@ fn update_snow(
}
}

// Focus the camera on the focal point when the camera moves
// Focus the camera on the focal point when the camera is first added and when it moves
fn focus_camera(
mut camera: Query<(&mut DepthOfFieldSettings, &GlobalTransform), Changed<Transform>>,
target: Query<(&GlobalTransform, &FocalPoint)>,
) {
let Some((target_xform, focal_point)) = target.iter().next() else { return };
let Ok((mut dof, camera_xform)) = camera.get_single_mut() else { return };
let Some((target_xform, focal_point)) = target.iter().next() else {
return;
};
let Ok((mut dof, camera_xform)) = camera.get_single_mut() else {
return;
};
let target_point = target_xform.transform_point(focal_point.0);
dof.focal_distance = camera_xform.translation().distance(target_point);
}
8 changes: 6 additions & 2 deletions examples/voxel-generation.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use bevy::{core_pipeline::bloom::BloomSettings, prelude::*};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{
VoxScenePlugin, Voxel, VoxelModelCollection, VoxelModelInstance, VoxelPalette, SDF,
};
use utilities::{PanOrbitCamera, PanOrbitCameraPlugin};

fn main() {
App::new()
Expand Down Expand Up @@ -35,7 +35,11 @@ fn setup_camera(mut commands: Commands, assets: Res<AssetServer>) {
}

fn setup(world: &mut World) {
let palette = VoxelPalette::from_colors(vec![bevy::color::palettes::css::BLUE.into(), bevy::color::palettes::css::ALICE_BLUE.into(), bevy::color::palettes::css::BISQUE.into()]);
let palette = VoxelPalette::from_colors(vec![
bevy::color::palettes::css::BLUE.into(),
bevy::color::palettes::css::ALICE_BLUE.into(),
bevy::color::palettes::css::BISQUE.into(),
]);
let data = SDF::cuboid(Vec3::splat(13.0))
.subtract(SDF::sphere(16.0))
.map_to_voxels(UVec3::splat(32), |d, _| match d {
Expand Down
Loading

0 comments on commit 6285421

Please sign in to comment.