Skip to content

Commit

Permalink
Add modify voxel model command
Browse files Browse the repository at this point in the history
  • Loading branch information
Utsira committed Jan 19, 2024
1 parent c80e3ee commit 9f90358
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 106 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.11.2"
version = "0.11.3"
repository = "https://github.com/Utsira/bevy_vox_scene"
authors = ["Oliver Dew <olidew@gmail.com>"]
edition = "2021"
Expand Down
74 changes: 35 additions & 39 deletions examples/modify-voxels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use bevy::{
};
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{
BoxRegion, ModifyVoxelModel, VoxScenePlugin, Voxel, VoxelModelInstance, VoxelQueryable,
VoxelRegion, VoxelSceneHook, VoxelSceneHookBundle,
ModifyVoxelCommandsExt, VoxScenePlugin, Voxel, VoxelModelInstance, VoxelQueryable, VoxelRegion,
VoxelSceneHook, VoxelSceneHookBundle,
};
use rand::Rng;
use std::{ops::RangeInclusive, time::Duration};
Expand Down Expand Up @@ -73,46 +73,42 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
fn grow_grass(mut commands: Commands, query: Query<&VoxelModelInstance, With<Floor>>) {
// All the floor tiles are instances of the same model, so we only need one instance
let Some(instance) = query.iter().next() else { return };
let region = BoxRegion {
let region = VoxelRegion::Box {
origin: IVec3::new(0, 4, 0),
size: IVec3::new(64, 8, 64),
};
commands.add(ModifyVoxelModel::new(
instance.0.id(),
VoxelRegion::Box(region),
|pos, voxel, model| {
if *voxel != Voxel::EMPTY {
return voxel.clone();
};
let mut rng = rand::thread_rng();
let value: u16 = rng.gen_range(0..5000);
if value > 20 {
return Voxel::EMPTY;
};
let vox_below = model
.get_voxel_at_point(pos - IVec3::Y)
.unwrap_or(Voxel::EMPTY);
let grass_voxels: RangeInclusive<u8> = 161..=165;
let grow_grass = grass_voxels.contains(&vox_below.0);
let mut plant_grass = !grow_grass && value < 5 && vox_below != Voxel::EMPTY;
if plant_grass {
// poisson disk effect: don't plant grass if too near other blades
'check_neighbors: for direction in [IVec3::NEG_X, IVec3::X, IVec3::NEG_Z, IVec3::Z]
{
let neighbor = model
.get_voxel_at_point(pos + direction)
.unwrap_or(Voxel::EMPTY);
if grass_voxels.contains(&neighbor.0) {
plant_grass = false;
break 'check_neighbors;
}
commands.modify_voxel_model(instance.0.id(), region, |pos, voxel, model| {
if *voxel != Voxel::EMPTY {
// don't overwrite any voxels
return voxel.clone();
};
let mut rng = rand::thread_rng();
let value: u16 = rng.gen_range(0..5000);
if value > 20 {
return Voxel::EMPTY;
};
let vox_below = model
.get_voxel_at_point(pos - IVec3::Y)
.unwrap_or(Voxel::EMPTY);
let grass_voxels: RangeInclusive<u8> = 161..=165;
let grow_grass = grass_voxels.contains(&vox_below.0);
let mut plant_grass = !grow_grass && value < 5 && vox_below != Voxel::EMPTY;
if plant_grass {
// poisson disk effect: don't plant grass if too near other blades
'check_neighbors: for direction in [IVec3::NEG_X, IVec3::X, IVec3::NEG_Z, IVec3::Z] {
let neighbor = model
.get_voxel_at_point(pos + direction)
.unwrap_or(Voxel::EMPTY);
if grass_voxels.contains(&neighbor.0) {
plant_grass = false;
break 'check_neighbors;
}
}
if plant_grass || grow_grass {
Voxel((161 + value % 5) as u8)
} else {
Voxel::EMPTY
}
},
));
}
if plant_grass || grow_grass {
Voxel((161 + value % 5) as u8)
} else {
Voxel::EMPTY
}
});
}
13 changes: 6 additions & 7 deletions examples/voxel-collisions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ use bevy::{
};
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
use bevy_vox_scene::{
BoxRegion, ModifyVoxelModel, VoxScenePlugin, Voxel, VoxelModel, VoxelModelInstance,
VoxelQueryable, VoxelRegion, VoxelScene, VoxelSceneBundle, VoxelSceneHook,
VoxelSceneHookBundle,
ModifyVoxelCommandsExt, VoxScenePlugin, Voxel, VoxelModel, VoxelModelInstance, VoxelQueryable,
VoxelRegion, VoxelScene, VoxelSceneBundle, VoxelSceneHook, VoxelSceneHookBundle,
};
use rand::Rng;

Expand Down Expand Up @@ -119,13 +118,13 @@ fn update_snow(
};
let flake_radius = 2;
let radius_squared = flake_radius * flake_radius;
let flake_region = BoxRegion {
let flake_region = VoxelRegion::Box {
origin: vox_pos - IVec3::splat(flake_radius),
size: IVec3::splat(1 + (flake_radius * 2)),
};
commands.add(ModifyVoxelModel::new(
commands.modify_voxel_model(
item_instance.0.id(),
VoxelRegion::Box(flake_region),
flake_region,
move |pos, voxel, model| {
// a signed distance field for a sphere, but _only_ drawing it on empty cells directly above solid voxels
if *voxel == Voxel::EMPTY && pos.distance_squared(vox_pos) <= radius_squared {
Expand All @@ -139,7 +138,7 @@ fn update_snow(
// else we return the underlying voxel, unmodified
voxel.clone()
},
));
);
commands.entity(snowflake).despawn();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
#![forbid(missing_docs, unsafe_code)]

use bevy::{
app::{App, Plugin, SpawnScene, Update},
app::{App, Plugin, SpawnScene},
asset::AssetApp,
ecs::schedule::IntoSystemConfigs,
};
Expand All @@ -63,7 +63,7 @@ use load::VoxSceneLoader;
pub use load::{VoxLoaderSettings, Voxel};

pub use scene::{
modify::{BoxRegion, ModifyVoxelModel, VoxelRegion},
modify::{ModifyVoxelCommandsExt, VoxelRegion},
queryable::VoxelQueryable,
VoxelLayer, VoxelModel, VoxelModelInstance, VoxelScene, VoxelSceneBundle, VoxelSceneHook,
VoxelSceneHookBundle,
Expand Down
118 changes: 61 additions & 57 deletions src/scene/modify.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::{
asset::{AssetId, Assets},
ecs::system::Command,
ecs::system::{Command, Commands},
math::IVec3,
render::mesh::Mesh,
};
Expand All @@ -15,37 +15,34 @@ use crate::{RawVoxel, Voxel, VoxelModel, VoxelQueryable};
/// ### Example
/// ```no_run
/// # use bevy::prelude::*;
/// # use bevy_vox_scene::{VoxelModel, ModifyVoxelModel, VoxelRegion, Voxel};
/// # fn setup(mut commands: Commands,
/// # handle: Handle<VoxelModel>)
/// # {
/// // overlay a voxel sphere over the loaded model
/// # use bevy_vox_scene::{VoxelModel, ModifyVoxelCommandsExt, VoxelRegion, Voxel};
/// # let mut commands: Commands = panic!();
/// # let model_handle: Handle<VoxelModel> = panic!();
/// // cut a sphere-shaped hole out of the loaded model
/// let sphere_center = IVec3::new(10, 10, 10);
/// let radius_squared = 10 * 10;
/// commands.add(
/// ModifyVoxelModel::new(handle.id(), VoxelRegion::All, move | position, voxel, model | {
/// let radius = 10;
/// let radius_squared = radius * radius;
/// let region = VoxelRegion::Box {
/// origin: sphere_center - IVec3::splat(radius),
/// size: IVec3::splat(1 + (radius * 2)),
/// };
/// commands.modify_voxel_model(
/// model_handle.id(),
/// region,
/// move | position, voxel, model | {
/// // a signed-distance function for a sphere:
/// if position.distance_squared(sphere_center) < radius_squared {
/// // inside of the sphere, coloured with voxels of index 7 in the palette
/// Voxel(7)
/// if position.distance_squared(sphere_center) <= radius_squared {
/// // inside of the sphere, return an empty cell
/// Voxel::EMPTY
/// } else {
/// // outside the sphere, return the underlying voxel value from the model
/// voxel.clone()
/// }
/// }),
/// },
/// );
/// # }
/// ```
pub struct ModifyVoxelModel {
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: Box<dyn Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>,
}

impl ModifyVoxelModel {
/// Returns a new [`ModifyVoxelModel`] command
///
/// Running this command will run the `modify` closure against every voxel within the `region` of the `model`.
pub trait ModifyVoxelCommandsExt {
/// Run the `modify` closure against every voxel within the `region` of the `model`.
///
/// ### Arguments
/// * `model` - the id of the [`VoxelModel`] to be modified (you can obtain this by from the [`bevy::asset::Handle::id()`] method).
Expand All @@ -59,19 +56,36 @@ impl ModifyVoxelModel {
///
/// ### Notes
/// The smaller the `region` is, the more performant the operation will be.
pub fn new<F: Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>(
fn modify_voxel_model<F: Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>(
&mut self,
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: F,
) -> Self {
Self {
) -> &mut Self;
}

impl ModifyVoxelCommandsExt for Commands<'_, '_> {
fn modify_voxel_model<F: Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>(
&mut self,
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: F,
) -> &mut Self {
self.add(ModifyVoxelModel {
model,
region,
modify: Box::new(modify),
}
});
self
}
}

struct ModifyVoxelModel {
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: Box<dyn Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>,
}

impl Command for ModifyVoxelModel {
fn apply(self, world: &mut bevy::prelude::World) {
let cell = world.cell();
Expand All @@ -90,45 +104,35 @@ impl Command for ModifyVoxelModel {
pub enum VoxelRegion {
/// The entire area of the model
All,
/// A [`BoxRegion`] within the model, expressed in voxel space
Box(BoxRegion),
/// A box region within the model, expressed in voxel space
Box {
/// The lower-back-left corner of the region
origin: IVec3,
/// The size of the region
size: IVec3,
},
}

impl VoxelRegion {
fn clamped(&self, size: IVec3) -> BoxRegion {
fn clamped(&self, model_size: IVec3) -> (IVec3, IVec3) {
match self {
VoxelRegion::All => BoxRegion {
origin: IVec3::ZERO,
size,
},
VoxelRegion::Box(box_area) => box_area.clamped(size),
VoxelRegion::All => (IVec3::ZERO, model_size),
VoxelRegion::Box { origin, size } => {
let origin = origin.clamp(IVec3::ZERO, model_size - IVec3::ONE);
let max_size = model_size - origin;
let size = size.clamp(IVec3::ONE, max_size);
(origin, size)
}
}
}
}

/// A box area of a voxel model expressed in voxel coordinates
pub struct BoxRegion {
/// The lower-back-left corner of the region
pub origin: IVec3,
/// The size of the region
pub size: IVec3,
}

impl BoxRegion {
fn clamped(&self, model_size: IVec3) -> BoxRegion {
let origin = self.origin.clamp(IVec3::ZERO, model_size - IVec3::ONE);
let max_size = model_size - origin;
let size = self.size.clamp(IVec3::ONE, max_size);
BoxRegion { origin, size }
}
}

fn modify_model(model: &mut VoxelModel, modifier: &ModifyVoxelModel, meshes: &mut Assets<Mesh>) {
let leading_padding = IVec3::splat(model.data.padding() as i32 / 2);
let size = model.size();
let region = modifier.region.clamped(size);
let start = leading_padding + region.origin;
let end = start + region.size;
let model_size = model.size();
let (origin, size) = modifier.region.clamped(model_size);
let start = leading_padding + origin;
let end = start + size;
let mut updated: Vec<RawVoxel> = model.data.voxels.clone();
for x in start.x..end.x {
for y in start.y..end.y {
Expand Down
56 changes: 56 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use bevy::{
app::App,
asset::{AssetApp, AssetPlugin, AssetServer, Assets, Handle, LoadState},
core::Name,
ecs::system::{Commands, Res, RunSystemOnce},
hierarchy::Children,
math::IVec3,
pbr::StandardMaterial,
render::{mesh::Mesh, texture::ImagePlugin},
utils::hashbrown::HashSet,
Expand Down Expand Up @@ -244,6 +246,60 @@ async fn test_spawn_system() {
app.update(); // fire the hooks
}

#[async_std::test]
async fn test_modify_voxels() {
let mut app = App::new();
let handle =
setup_and_load_voxel_scene(&mut app, "test.vox#outer-group/inner-group/dice").await;
app.update();
app.world.run_system_once(modify_voxels);
app.update();
let scene = app
.world
.resource::<Assets<VoxelScene>>()
.get(handle)
.expect("retrieve scene from Res<Assets>");
let model_handle = scene.root.model.as_ref().expect("Root should have a model");
let model = app
.world
.resource::<Assets<VoxelModel>>()
.get(model_handle)
.expect("retrieve model from Res<Assets>");
assert_eq!(
model.get_voxel_at_point(IVec3::splat(4)),
None,
"Max coordinate should be 3,3,3"
);
assert_eq!(
model.get_voxel_at_point(IVec3::splat(-1)),
None,
"Min coordinate should be 0,0,0"
);
let voxel = model
.get_voxel_at_point(IVec3::splat(2))
.expect("Retrieve voxel");
assert_eq!(voxel.0, 7, "Voxel material should've been changed to 7");
}

fn modify_voxels(mut commands: Commands, models: Res<Assets<VoxelModel>>) {
let id = models
.iter()
.filter_map(|(id, model)| {
if model.size() == IVec3::splat(4) {
Some(id)
} else {
None
}
})
.next()
.expect("There should be a dice model the size of which is 4 x 4 x 4");
let region = VoxelRegion::Box {
origin: IVec3::splat(2),
size: IVec3::ONE,
};
commands.modify_voxel_model(id, region, |_pos, _voxel, _model| Voxel(7));
}

/// `await` the response from this and then call `app.update()`
async fn setup_and_load_voxel_scene(app: &mut App, filename: &'static str) -> Handle<VoxelScene> {
app.add_plugins((
Expand Down

0 comments on commit 9f90358

Please sign in to comment.