Skip to content

Commit

Permalink
ModifyVoxelModel as a Command
Browse files Browse the repository at this point in the history
  • Loading branch information
Utsira committed Jan 18, 2024
1 parent 1c2a89c commit 5127968
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 108 deletions.
Binary file modified assets/study.vox
Binary file not shown.
64 changes: 14 additions & 50 deletions examples/modify-voxels.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
use std::time::Duration;

use bevy::{
core_pipeline::{
bloom::BloomSettings,
core_3d::ScreenSpaceTransmissionQuality,
experimental::taa::{TemporalAntiAliasBundle, TemporalAntiAliasPlugin},
tonemapping::Tonemapping,
},
core_pipeline::{bloom::BloomSettings, tonemapping::Tonemapping},
prelude::*,
time::common_conditions::on_timer,
};
Expand All @@ -20,28 +15,17 @@ use rand::Rng;

// When a snowflake lands on the scenery, it is added to scenery's voxel data, so that snow gradually builds up
fn main() {
let mut app = App::new();
// Making this frequency not cleanly divisible by the snowflake speed ensures that expensive collisions
// don't all happen on the same frame
let snow_spawn_freq = Duration::from_secs_f32(0.213);
app.add_plugins((DefaultPlugins, PanOrbitCameraPlugin, VoxScenePlugin))
App::new()
.add_plugins((DefaultPlugins, PanOrbitCameraPlugin, VoxScenePlugin))
.add_systems(Startup, setup)
.add_systems(
Update,
(
spawn_snow.run_if(on_timer(snow_spawn_freq)),
update_snow,
remove_snow,
),
);
// *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);

app.run();
(spawn_snow.run_if(on_timer(snow_spawn_freq)), update_snow),
)
.run();
}

#[derive(Resource)]
Expand All @@ -56,11 +40,6 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
hdr: true,
..Default::default()
},
camera_3d: Camera3d {
screen_space_specular_transmission_quality: ScreenSpaceTransmissionQuality::High,
screen_space_specular_transmission_steps: 1,
..default()
},
transform: Transform::from_xyz(15.0, 40.0, 90.0).looking_at(Vec3::ZERO, Vec3::Y),
tonemapping: Tonemapping::SomewhatBoringDisplayTransform,
..Default::default()
Expand All @@ -70,8 +49,6 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
intensity: 0.3,
..default()
},
#[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))]
TemporalAntiAliasBundle::default(),
EnvironmentMapLight {
diffuse_map: assets.load("pisa_diffuse.ktx2"),
specular_map: assets.load("pisa_specular.ktx2"),
Expand Down Expand Up @@ -99,14 +76,12 @@ struct Snowflake(Quat);
#[derive(Component)]
struct Scenery;

#[derive(Component)]
struct ToBeDespawned;

fn spawn_snow(mut commands: Commands, scenes: Res<Scenes>) {
let mut rng = rand::thread_rng();
let position = Vec3::new(rng.gen_range(-30.0..30.0), 80.0, rng.gen_range(-20.0..20.0)).round()
+ Vec3::splat(0.5);
let rotation_axis = Vec3::new(rng.gen_range(-0.5..0.5), 1.0, rng.gen_range(-0.5..0.5)).normalize();
let rotation_axis =
Vec3::new(rng.gen_range(-0.5..0.5), 1.0, rng.gen_range(-0.5..0.5)).normalize();
let angular_velocity = Quat::from_axis_angle(rotation_axis, 0.01);
commands.spawn((
Snowflake(angular_velocity),
Expand All @@ -121,10 +96,7 @@ fn spawn_snow(mut commands: Commands, scenes: Res<Scenes>) {
fn update_snow(
mut commands: Commands,
mut snowflakes: Query<(Entity, &Snowflake, &mut Transform), Without<Scenery>>,
scenery: Query<
(Entity, &GlobalTransform, &VoxelModelInstance),
(With<Scenery>, Without<Snowflake>),
>,
scenery: Query<(&GlobalTransform, &VoxelModelInstance), (With<Scenery>, Without<Snowflake>)>,
models: Res<Assets<VoxelModel>>,
) {
for (snowflake, snowflake_angular_vel, mut snowflake_xform) in snowflakes.iter_mut() {
Expand All @@ -135,7 +107,7 @@ fn update_snow(
if old_ypos.trunc() == snowflake_xform.translation.y.trunc() {
continue;
}
for (item, item_xform, item_instance) in scenery.iter() {
for (item_xform, item_instance) in scenery.iter() {
let Some(model) = models.get(item_instance.0.id()) else { continue };
let vox_pos =
model.global_point_to_voxel_space(snowflake_xform.translation, item_xform);
Expand All @@ -151,15 +123,14 @@ fn update_snow(
origin: vox_pos - IVec3::splat(flake_radius),
size: IVec3::splat(1 + (flake_radius * 2)),
};
commands.entity(item).insert(ModifyVoxelModel::new(
commands.add(ModifyVoxelModel::new(
item_instance.0.id(),
VoxelRegion::Box(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 {
if let Some(voxel_below) = model.get_voxel_at_point(pos - IVec3::Y) {
if voxel_below == Voxel::EMPTY {
return Voxel::EMPTY;
} else {
if voxel_below != Voxel::EMPTY {
// draw our snow material
return Voxel(234);
}
Expand All @@ -169,14 +140,7 @@ fn update_snow(
voxel.clone()
},
));
commands.entity(snowflake).insert(ToBeDespawned);
commands.entity(snowflake).despawn();
}
}
}

/// Defer despawning by a frame, so that the flake doesn't flicker when it lands
fn remove_snow(mut commands: Commands, query: Query<Entity, With<ToBeDespawned>>) {
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ impl Plugin for VoxScenePlugin {
.add_systems(
SpawnScene,
(scene::systems::spawn_vox_scenes, scene::systems::run_hooks).chain(),
)
.add_systems(Update, scene::modify::modify_voxels);
);
}
}
109 changes: 53 additions & 56 deletions src/scene/modify.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
use bevy::{
asset::Assets,
ecs::{
component::Component,
entity::Entity,
system::{Commands, Query, ResMut},
},
asset::{AssetId, Assets},
ecs::system::Command,
math::IVec3,
render::mesh::Mesh,
};
use ndshape::Shape;

use crate::{RawVoxel, Voxel, VoxelModel, VoxelModelInstance, VoxelQueryable};
use crate::{RawVoxel, Voxel, VoxelModel, VoxelQueryable};

/// Programatically modify the voxels in the instanced voxel model
/// Command that programatically modifies the voxels in a model.
///
/// Attaching this component to an entity that also has a [`VoxelModelInstance`] will
/// run the closure against every voxel in the region.
/// This command will run the closure against every voxel within the region of the model.
///
/// ### Example
/// ```no_run
/// # use bevy::prelude::*;
/// # use bevy_vox_scene::{VoxelSceneBundle, ModifyVoxelModel, VoxelRegion, Voxel};
/// # use bevy_vox_scene::{VoxelModel, ModifyVoxelModel, VoxelRegion, Voxel};
/// # fn setup(mut commands: Commands,
/// # assets: Res<AssetServer>)
/// # handle: Handle<VoxelModel>)
/// # {
/// // overlay a voxel sphere over the loaded model
/// let sphere_center = IVec3::new(10, 10, 10);
/// let radius_squared = 10 * 10;
/// commands.spawn((
/// VoxelSceneBundle {
/// scene: assets.load("study.vox#workstation/desk"),
/// ..default ()
/// },
/// ModifyVoxelModel::new(VoxelRegion::All, move | position, voxel, model | {
/// commands.add(
/// ModifyVoxelModel::new(handle.id(), VoxelRegion::All, 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
Expand All @@ -42,22 +33,22 @@ use crate::{RawVoxel, Voxel, VoxelModel, VoxelModelInstance, VoxelQueryable};
/// voxel.clone()
/// }
/// }),
/// ));
/// );
/// # }
/// ```
#[derive(Component)]
pub struct ModifyVoxelModel {
pub(crate) region: VoxelRegion,
pub(crate) modify: Box<dyn Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>,
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: Box<dyn Fn(IVec3, &Voxel, &VoxelModel) -> Voxel + Send + Sync + 'static>,
}

impl ModifyVoxelModel {
/// Returns a new [`ModifyVoxelModel`] component
/// Returns a new [`ModifyVoxelModel`] command
///
/// Attaching this component to an entity that also has a [`VoxelModelInstance`] will
/// run the `modify` closure against every voxel within the `region`.
/// Running this command will 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).
/// * `region` - a [`VoxelRegion`] defining the area of the voxel model that the modifier will operate on.
/// * `modify` - a closure that will run against every voxel within the `region`.
///
Expand All @@ -69,16 +60,32 @@ 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>(
model: AssetId<VoxelModel>,
region: VoxelRegion,
modify: F,
) -> Self {
Self {
region: region,
model,
region,
modify: Box::new(modify),
}
}
}

impl Command for ModifyVoxelModel {
fn apply(self, world: &mut bevy::prelude::World) {
let cell = world.cell();
let perform = || -> Option<()> {
let mut meshes = cell.get_resource_mut::<Assets<Mesh>>()?;
let mut models = cell.get_resource_mut::<Assets<VoxelModel>>()?;
let mut model = models.get_mut(self.model)?;
modify_model(&mut model, &self, &mut meshes);
Some(())
};
perform();
}
}

/// The region of the model to modify
pub enum VoxelRegion {
/// The entire area of the model
Expand Down Expand Up @@ -116,37 +123,27 @@ impl BoxRegion {
}
}

pub(crate) fn modify_voxels(
mut commands: Commands,
query: Query<(Entity, &ModifyVoxelModel, &VoxelModelInstance)>,
mut models: ResMut<Assets<VoxelModel>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
for (entity, modifier, instance) in query.iter() {
let Some(model) = models.get_mut(instance.0.id()) else { continue; };

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 mut updated: Vec<RawVoxel> = model.data.voxels.clone();
for x in start.x..end.x {
for y in start.y..end.y {
for z in start.z..end.z {
let index = model.data.shape.linearize([x as u32, y as u32, z as u32]) as usize;
let source: Voxel = model.data.voxels[index].clone().into();
updated[index] = RawVoxel::from((modifier.modify)(
IVec3::new(x, y, z) - leading_padding,
&source,
model,
));
}
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 mut updated: Vec<RawVoxel> = model.data.voxels.clone();
for x in start.x..end.x {
for y in start.y..end.y {
for z in start.z..end.z {
let index = model.data.shape.linearize([x as u32, y as u32, z as u32]) as usize;
let source: Voxel = model.data.voxels[index].clone().into();
updated[index] = RawVoxel::from((modifier.modify)(
IVec3::new(x, y, z) - leading_padding,
&source,
model,
));
}
}
model.data.voxels = updated;
meshes.insert(&model.mesh, model.data.remesh());
// TODO: also update material if transparency has changed
commands.entity(entity).remove::<ModifyVoxelModel>();
}
model.data.voxels = updated;
meshes.insert(&model.mesh, model.data.remesh());
// TODO: also update material if transparency has changed
}

0 comments on commit 5127968

Please sign in to comment.