21 releases (9 breaking)

new 0.10.1 Dec 8, 2024
0.9.0 Nov 17, 2024
0.8.0 Jul 20, 2024
0.5.1 Mar 16, 2024
0.3.3 Nov 18, 2023

#48 in Game dev

Download history 35/week @ 2024-09-19 45/week @ 2024-09-26 3/week @ 2024-10-03 2/week @ 2024-10-10 1/week @ 2024-10-17 8/week @ 2024-10-31 7/week @ 2024-11-07 215/week @ 2024-11-14 30/week @ 2024-11-21 110/week @ 2024-11-28 127/week @ 2024-12-05

482 downloads per month

MIT/Apache

115KB
2.5K SLoC

bevy_voxel_world

Crates.io License Bevy tracking


What is bevy_voxel_world

This plugin makes it easy to generate and modify voxel terrains in Bevy. bevy_voxel_world handles multithreaded meshing, chunk spawning/despawning, texture mapping and provides an easy to use API that can be accessed from any system.

bvw_480

$ cargo run -r --example noise_terrain

The world can be controlled in two main ways: through a terrain lookup function, and directly by set_voxel and get_voxel functions. The world has two "layers" of voxel information, one that is procedural and determined by the terrain lookup function, and one that is controlled by set_voxel and persisted in a HashMap. The persistent layer always overrides the procedural layer. This way, the world can be infinitely large, but we only need to store information about voxels that are deliberately changed. In the current implementation, the proceduaral layer is cached for spawned chunks, so it may still use a lot of memory if the spawning distance is large.

For an example on how to use a terrain lookup function, see this example.

Basic setup

Create a configuration struct for your world:

#[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:

.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

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.

fn my_system(mut voxel_world: VoxelWorld<MyWorld>) {
    voxel_world.set_voxel(IVec3 { ... }, WorldVoxel::Solid(0));
}

This will update the voxel value at the given location in the persisting HashMap, and cause bevy_voxel_world to queue the affected chunk for re-meshing.

Voxels are keyed by their XYZ coordinate in the world, specified by an IVec3. The type of voxel is specified by the WorldVoxel type. A voxel can be Unset, Air or Solid.

Voxel materials

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 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:

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.

See the textures example for a runnable example of this.

Screenshot 2023-11-06 at 21 50 05

Custom shader support

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 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.

fn do_something_with_mouse_voxel_pos(
    voxel_world: VoxelWorld<MyWorld>,
    camera_info: Query<(&Camera, &GlobalTransform), With<VoxelWorldCamera<MyWorld>>>,
    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(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 for more details.

Gotchas

bevy_voxel_world began as an internal part of a game that I'm working on, but I figured that it could be useful as a standalone plugin, for myself and perhaps for others, so I decided to break it out and make it public as a crate.

In its current state, there are still various hard-coded assumptions that works well enough for my usecase, but may not suit everyone. Over time, the aim is to generalize and make bevy_voxel_world more configurable. There are also many potential performance optimizations that I have not prioritized yet at this point.

Currently only "blocky", Minecraft-like, voxels are supported, and there is no support for "half-slabs". Meshing is handled by block-mesh-rs, and only the "simple" algorithm is used (i.e, no greedy meshing.)

Feedback, issues and pull requests are welcomed!


Bevy compatibility

bevy bevy_voxel_world
0.15 ^0.10.0
0.14 0.8.0
0.13 0.4.0
0.12 0.3.6
0.11 0.2.2

Dependencies

~22–33MB
~535K SLoC