Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple worlds #17

Merged
merged 17 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,72 @@
# Changelog

## 0.5.0

Add support for multiple parallell world instances

**Breaking changes:**

Configuration is now supplied when adding the plugin

```rust
// First declare a config struct. It needs to derive `Resource`, `Clone` and `Default`
#[derive(Resource, Clone, Default)]
struct MyWorld;

// Then implement the `VoxelWorldConfig` trait for it:
impl VoxelWorldConfig for MyWorld {
// All the trait methods have defaults, so you only need to add the ones you want to alter
fn spawning_distance(&self) -> u32 {
15
}
}
```

Then when adding the plugin:

```rust
.add_plugins(VoxelWorldPlugin::with_config(MyWorld))
```

If you don't want to change any default config, you can simply do this:

```rust
.add_plugins(VoxelWorldPlugin::default())
```

Adding multiple worlds follows the same pattern. Just create different configuration structs and add a `VoxelWorldPlugin` for each.

```rust
.add_plugins(VoxelWorldPlugin::with_config(MyOtherWorld))
```

Each world instance can have it's own configuration and will keep track of it's own set of voxel data and chunks.

### The `VoxelWorld` system param now needs a type parameter to specify which world instance you want to select

The configuration struct adds the config values and its type also acts as a marker for the world instance.

```rust
fn my_system(
my_voxel_world: VoxelWorld<MyWorld>,
my_other_voxel_world: VoxelWorld<MyOtherWorld>
) {
// Set a voxel in `my_voxel_world`
my_voxel_world.set_voxel(pos, WorldVoxel::Solid(voxel_type))

// Set a voxel in `my_other_voxel_world`
my_other_voxel_world.set_voxel(pos, WorldVoxel::Solid(voxel_type))
}
```

If you initialized the plugin with `::default()`, you still need to explicitly specify the instance as `DefaultWorld`:

```rust
fn my_system(voxel_world: VoxelWorld<DefaultWorld>) { ... }
```

The `VoxelWorldRaycast` system param now also requires the same config type paramter as described above.

## 0.4.0

- Update to Bevy 0.13
Expand Down
103 changes: 76 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,42 @@ The world can be controlled in two main ways: through a terrain lookup function,

For an example on how to use a terrain lookup function, see [this example](https://github.com/splashdust/bevy_voxel_world/blob/main/examples/noise_terrain.rs).

## Modifying the world
## Basic setup

The `set_voxel` and `get_voxel` access functions are easily reached from any normal Bevy system:
Create a configuration struct for your world:

```rust
fn my_system(mut voxel_world: VoxelWorld) {
#[derive(Resource, Clone, Default)]
struct MyWorld;

impl VoxelWorldConfig for MyWorld {
// All options have defaults, so you only need to add the ones you want to modify.
// For a full list, see src/configuration.rs
fn spawning_distance(&self) -> u32 {
25
}
}
```

Then add the plugin with your config:

```rust
.add_plugins(VoxelWorldPlugin::with_config(MyWorld))
```

The config struct does two things:

- It supplies the configuration values
- Its type also acts as a world instance identifier. This means that you can create multiple worlds by adding multiple instances of the plugin as long as each instance has a unique configuration struct. [Here's an example of two worlds using different materials](https://github.com/splashdust/bevy_voxel_world/blob/main/examples/multiple_worlds.rs)

## Accessing the world

To access a voxel world instance in a system, you can use the `VoxelWorld` system param. `VoxelWorld` take one type parameter, which is the configuration struct for the world you want to access.

The `set_voxel` and `get_voxel` access functions can be used to manipulate the voxel data in the world.

```rust
fn my_system(mut voxel_world: VoxelWorld<MyWorld>) {
voxel_world.set_voxel(IVec3 { ... }, WorldVoxel::Solid(0));
}
```
Expand All @@ -38,33 +68,24 @@ Voxels are keyed by their XYZ coordinate in the world, specified by an `IVec3`.

`Solid` voxels holds a `u8` material type value. Thus, a maximum of 256 material types are supported. Material types can easily be mapped to indexes in a 2d texture array though a mapping callback.

A custom array texture can be supplied when initializing the plugin:

```rust
VoxelWorldPlugin::default()
.with_voxel_texture("images/materials.png", 6)
```

This should be image with a size of `W x (W * n)`, where `n` is the number of indexes. So an array of 4 16x16 px textures would be 16x64 px in size. The number of indexes is specified in the second parameter (6 in the example above).
A custom array texture can be supplied in the config. It should be image with a size of `W x (W * n)`, where `n` is the number of indexes. So an array of 4 16x16 px textures would be 16x64 px in size. The number of indexes is specified in the second parameter.

Then, to map out which indexes belong to which material type, you can supply a `texture_index_mapper` callback:

```rust
commands.insert_resource(VoxelWorldConfiguration {
texture_index_mapper: Arc::new(|vox_mat: u8| {
match vox_mat {
// Top brick
0 => [0, 1, 2],

// Full brick
1 => [2, 2, 2],

// Grass
2 | _ => [3, 3, 3],
}
}),
..Default::default()
});
impl VoxelWorldConfig for MyWorld {
fn texture_index_mapper(&self) -> Arc<dyn Fn(u8) -> [u32; 3] + Send + Sync> {
Arc::new(|vox_mat: u8| match vox_mat {
SNOWY_BRICK => [0, 1, 2],
FULL_BRICK => [2, 2, 2],
GRASS | _ => [3, 3, 3],
})
}

fn voxel_texture(&self) -> Option<(String, u32)> {
Some(("example_voxel_texture.png".into(), 4)) // Array texture with 4 indexes
}
}
```

The `texture_index_mapper` callback is supplied with a material type and should return an array with three values. The values indicate which texture index maps to `[top, sides, bottom]` of a voxel.
Expand All @@ -75,7 +96,35 @@ See the [textures example](https://github.com/splashdust/bevy_voxel_world/blob/m

### Custom shader support

If you need to customize materials futher, you can use `VoxelWorldMaterialPlugin` to register your own Bevy material. This allows you to use your own custom shader with `bevy_voxel_world`. See [this example](https://github.com/splashdust/bevy_voxel_world/blob/main/examples/custom_material.rs) for more details.
If you need to customize materials futher, you can use `.with_material(MyCustomVoxelMaterial)`, when adding the plugin, to register your own Bevy material. This allows you to use your own custom shader with `bevy_voxel_world`. See [this example](https://github.com/splashdust/bevy_voxel_world/blob/main/examples/custom_material.rs) for more details.

## Ray casting

To find a voxel location in the world from a pixel location on the screen, for example the mouse location, you can ray cast into the voxel world.

```rust
fn do_something_with_mouse_voxel_pos(
voxel_world_raycast: VoxelWorldRaycast<MyWorld>,
camera_info: Query<(&Camera, &GlobalTransform), With<VoxelWorldCamera>>,
mut cursor_evr: EventReader<CursorMoved>,
) {
for ev in cursor_evr.read() {
// Get a ray from the cursor position into the world
let (camera, cam_gtf) = camera_info.single();
let Some(ray) = camera.viewport_to_world(cam_gtf, ev.position) else {
return;
};

if let Some(result) = voxel_world_raycast.raycast(ray, &|(_pos, _vox)| true) {
// result.position will be the world location of the voxel as a Vec3
// To get the empty location next to the voxel in the direction of the surface where the ray intersected you can use result.normal:
// let empty_pos = result.position + result.normal;
}
}
}
```

See this [full example of ray casting](https://github.com/splashdust/bevy_voxel_world/blob/main/examples/ray_cast.rs) for more details.

## Gotchas

Expand Down
31 changes: 16 additions & 15 deletions examples/bombs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@ use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*, utils::HashMap};
use bevy_voxel_world::prelude::*;
use noise::{HybridMulti, NoiseFn, Perlin};
use std::time::Duration;
#[derive(Resource, Clone, Default)]
struct MainWorld;

impl VoxelWorldConfig for MainWorld {
fn spawning_distance(&self) -> u32 {
15
}

fn voxel_lookup_delegate(&self) -> VoxelLookupDelegate {
Box::new(move |_chunk_pos| get_voxel_fn())
}
}

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(VoxelWorldPlugin::default())
.add_plugins(VoxelWorldPlugin::with_config(MainWorld))
.add_systems(Startup, setup)
.add_systems(Update, move_camera)
.add_systems(Update, explosion)
Expand All @@ -19,17 +31,6 @@ struct ExplosionTimeout {
}

fn setup(mut commands: Commands) {
commands.insert_resource(VoxelWorldConfiguration {
// This is the spawn distance (in 32 meter chunks), centered around the camera.
spawning_distance: 15,

// Here we supply a closure that returns another closure that returns a voxel value for a given position.
// This may seem a bit convoluted, but it allows us to capture data in a sendable closure to be sent off
// to a differrent thread for the meshing process. A new closure is fetched for each chunk.
voxel_lookup_delegate: Box::new(move |_chunk_pos| get_voxel_fn()), // `get_voxel_fn` is defined below
..Default::default()
});

commands.spawn(ExplosionTimeout {
timer: Timer::from_seconds(0.25, TimerMode::Repeating),
});
Expand Down Expand Up @@ -63,7 +64,7 @@ fn setup(mut commands: Commands) {
// Ambient light, same color as sun
commands.insert_resource(AmbientLight {
color: Color::rgb(0.98, 0.95, 0.82),
brightness: 0.3,
brightness: 100.0,
});
}

Expand Down Expand Up @@ -92,7 +93,7 @@ fn get_voxel_fn() -> Box<dyn FnMut(IVec3) -> WorldVoxel + Send + Sync> {
let sample = match cache.get(&(pos.x, pos.z)) {
Some(sample) => *sample,
None => {
let sample = noise.get([x / 800.0, z / 800.0]) * 50.0;
let sample = noise.get([x / 800.0, z / 800.0]) * 25.0;
cache.insert((pos.x, pos.z), sample);
sample
}
Expand Down Expand Up @@ -120,7 +121,7 @@ fn move_camera(time: Res<Time>, mut cam_transform: Query<&mut Transform, With<Vo
}

fn explosion(
mut voxel_world: VoxelWorld,
mut voxel_world: VoxelWorld<MainWorld>,
camera: Query<&Transform, With<VoxelWorldCamera>>,
mut timeout: Query<&mut ExplosionTimeout>,
time: Res<Time>,
Expand Down
56 changes: 26 additions & 30 deletions examples/custom_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ use bevy::{
};
use bevy_voxel_world::{
prelude::*,
rendering::{
vertex_layout, VoxelWorldMaterialHandle, VoxelWorldMaterialPlugin,
VOXEL_TEXTURE_SHADER_HANDLE,
},
rendering::{vertex_layout, VOXEL_TEXTURE_SHADER_HANDLE},
};
use std::sync::Arc;

Expand All @@ -22,41 +19,40 @@ const RED: u8 = 0;
const GREEN: u8 = 1;
const BLUE: u8 = 2;

#[derive(Resource, Clone, Default)]
struct MyMainWorld;

impl VoxelWorldConfig for MyMainWorld {
fn texture_index_mapper(&self) -> Arc<dyn Fn(u8) -> [u32; 3] + Send + Sync> {
Arc::new(|vox_mat: u8| match vox_mat {
RED => [1, 1, 1],
GREEN => [2, 2, 2],
BLUE | _ => [3, 3, 3],
})
}
}

fn main() {
App::new()
.add_plugins(DefaultPlugins)
//
// We can tell `bevy_voxel_world` to skip setting up the default material, so that we can use our own
.add_plugins(VoxelWorldPlugin::default().without_default_material())
//
// We also need to tell `bevy_voxel_world` which material to assign.
// This can be any Bevy material, including ExtendedMaterial.
.add_plugins(VoxelWorldMaterialPlugin::<CustomVoxelMaterial>::default())
//
// Don't forget to Register the material with Bevy too.
// First we need to register the material with Bevy. This needs to be done before we add the
// `VoxelWorldPlugin` so that the plugin can find the material.
.add_plugins(MaterialPlugin::<CustomVoxelMaterial>::default())
//
// Then we can tell `bevy_voxel_world` to use that material when adding the plugin.
// bevy_voxel_world will add the material as an asset, so you can query for it later using
// `Res<Assets<CustomVoxelMaterial>>`.
.add_plugins(
VoxelWorldPlugin::with_config(MyMainWorld)
.with_material(CustomVoxelMaterial { _unused: 0 }),
)
//
.add_systems(Startup, (setup, create_voxel_scene))
.run();
}

fn setup(mut commands: Commands, mut materials: ResMut<Assets<CustomVoxelMaterial>>) {
// Register our custom material
let handle = materials.add(CustomVoxelMaterial { _unused: 0 });

// This resource is used to find the correct material handle
commands.insert_resource(VoxelWorldMaterialHandle { handle });

commands.insert_resource(VoxelWorldConfiguration {
// The arrays produces here can be read in the shader
texture_index_mapper: Arc::new(|vox_mat: u8| match vox_mat {
RED => [1, 1, 1],
GREEN => [2, 2, 2],
BLUE | _ => [3, 3, 3],
}),
..Default::default()
});

fn setup(mut commands: Commands) {
// Camera
commands.spawn((
Camera3dBundle {
Expand All @@ -67,7 +63,7 @@ fn setup(mut commands: Commands, mut materials: ResMut<Assets<CustomVoxelMateria
));
}

fn create_voxel_scene(mut voxel_world: VoxelWorld) {
fn create_voxel_scene(mut voxel_world: VoxelWorld<MyMainWorld>) {
// 20 by 20 floor
for x in -10..10 {
for z in -10..10 {
Expand Down
Loading
Loading