voxy 0.1.0

Voxel engine for Bevy
Documentation
use crate::{Chunk, Emission, Palette, PaletteSample, VoxelMaterial};
use bevy::{
    asset::{io::Reader, AssetLoader, LoadContext, LoadState},
    ecs::system::EntityCommands,
    prelude::*,
    utils::ConditionalSendFuture,
};
use block_mesh::{MergeVoxel, Voxel, VoxelVisibility};
use dot_vox::DotVoxData;
use ndshape::{RuntimeShape, Shape};
use smol::io::AsyncReadExt;

pub struct VoxFileAssetPlugin;

impl Plugin for VoxFileAssetPlugin {
    fn build(&self, app: &mut App) {
        app.init_asset::<VoxFileAsset>()
            .init_asset_loader::<VoxAssetLoader>()
            .add_systems(Update, load_assets);
    }
}

#[derive(Clone, Copy, Default)]
pub struct AssetVoxel {
    pub idx: u8,
}

impl Voxel for AssetVoxel {
    fn get_visibility(&self) -> VoxelVisibility {
        if self.idx == 0 {
            VoxelVisibility::Empty
        } else {
            VoxelVisibility::Opaque
        }
    }
}

impl MergeVoxel for AssetVoxel {
    type MergeValue = u8;

    fn merge_value(&self) -> Self::MergeValue {
        self.idx
    }
}

pub struct VoxFilePalette {
    pub samples: Vec<PaletteSample>,
}

impl Palette for VoxFilePalette {
    type Voxel = AssetVoxel;

    fn sample(
        &self,
        voxel: &Self::Voxel,
        _indices: &[u32; 6],
        _positions: &[[f32; 3]; 4],
        _normals: &[[f32; 3]; 4],
    ) -> PaletteSample {
        self.samples[voxel.idx as usize]
    }
}

#[derive(Debug, Asset, TypePath)]
pub struct VoxFileAsset {
    pub file: DotVoxData,
}

impl VoxFileAsset {
    pub fn palette(&self) -> VoxFilePalette {
        VoxFilePalette {
            samples: self
                .file
                .palette
                .iter()
                .enumerate()
                .map(|(idx, color)| PaletteSample {
                    color: Color::srgb_u8(color.r, color.g, color.b),
                    emission: Emission {
                        alpha: self.file.materials[idx]
                            .properties
                            .get("_emit")
                            .and_then(|s| s.parse().ok())
                            .unwrap_or_default(),
                        intensity: 1.,
                    },
                })
                .collect::<Vec<_>>(),
        }
    }

    pub fn chunks<'a>(
        &'a self,
        palette: &'a VoxFilePalette,
    ) -> impl Iterator<Item = Chunk<&'a VoxFilePalette, Vec<AssetVoxel>, RuntimeShape<u32, 3>>> + 'a
    {
        self.file.models.iter().map(move |model| {
            let shape =
                RuntimeShape::<u32, 3>::new([model.size.x + 2, model.size.y + 2, model.size.z + 2]);

            let mut voxels = vec![AssetVoxel::default(); shape.size() as usize];
            for voxel in &model.voxels {
                voxels[shape.linearize([voxel.x as u32 + 1, voxel.z as u32 + 1, voxel.y as u32 + 1])
                    as usize] = AssetVoxel { idx: voxel.i };
            }

            Chunk {
                palette,
                voxels,
                shape,
                min: UVec3::ZERO,
                max: UVec3::new(model.size.x, model.size.y, model.size.z),
            }
        })
    }

    fn spawn(
        &self,
        mut entity_commands: EntityCommands,
        meshes: &mut Assets<Mesh>,
        materials: &mut Assets<VoxelMaterial>,
    ) {
        let palette = self.palette();

        for chunk in self.chunks(&palette) {
            entity_commands.with_children(|parent| {
                for (idx, voxel) in chunk.voxels.iter().enumerate() {
                    let sample = chunk.palette.samples[voxel.idx as usize];

                    let [x, y, z] = chunk.shape.delinearize(idx as _).map(|n| n as f32);

                    if sample.emission.alpha > 0. {
                        parent.spawn(PointLightBundle {
                            point_light: PointLight {
                                intensity: sample.emission.alpha * 100_000.,
                                range: 10.,
                                ..default()
                            },
                            transform: Transform::from_xyz(x, y, z),
                            ..default()
                        });
                    }
                }
            });

            entity_commands.insert(MaterialMeshBundle {
                material: materials.add(VoxelMaterial),
                mesh: meshes.add(chunk),
                ..default()
            });
        }
    }
}

#[derive(Default)]
pub struct VoxAssetLoader;

impl AssetLoader for VoxAssetLoader {
    type Asset = VoxFileAsset;

    type Settings = ();

    type Error = Box<dyn std::error::Error + Send + Sync>;

    fn load<'a>(
        &'a self,
        reader: &'a mut Reader,
        _settings: &'a Self::Settings,
        _load_context: &'a mut LoadContext,
    ) -> impl ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>> {
        async move {
            let mut buf = Vec::new();
            reader.read_to_end(&mut buf).await?;

            let file = dot_vox::load_bytes(&buf).unwrap();
            Ok(VoxFileAsset { file })
        }
    }
}

#[derive(Component)]
struct Loaded;

fn load_assets(
    mut commands: Commands,
    query: Query<(Entity, &Handle<VoxFileAsset>), Without<Loaded>>,
    asset_server: Res<AssetServer>,
    vox_assets: Res<Assets<VoxFileAsset>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<VoxelMaterial>>,
) {
    for (entity, handle) in &query {
        if asset_server.load_state(handle) == LoadState::Loaded {
            commands.entity(entity).insert(Loaded);

            let vox = vox_assets.get(handle).unwrap();
            vox.spawn(commands.entity(entity), &mut meshes, &mut materials);
        }
    }
}