bevy_shatter 0.1.0

A bevy plugin to allow simulating shattered glass
Documentation
//! # **bevy_shatter**
//!
//! Procedural glass shattering plugin for the [Bevy game engine](https://bevyengine.org/)
//!
//! **Note**: This plugin uses [avian3d](https://github.com/Jondolf/avian) for collider generation, but [rapier3d](https://rapier.rs/) integration should be trivial to add in the future
//!
//! # Usage
//!
//! **Plugin**
//!
//! Add the [`ShatterPlugin`] to register the needed hooks and resources.
//!
//! **Creating glass**
//!
//! Add the [`Glass`] component to an entity, using the [`Transform::scale`] as width, height and thickness. A helper is available in [`AutoGlass`] to add other needed components automatically, such as a mesh and a transform with the correct scale.
//!
//! **Shattering glass**
//!
//! Add the [`Shattered`] component to an entity that has [`Glass`], and glass shards will automatically be created.
//!
//! # Customizing behaviour
//!
//! This plugin prioritizes user control instead of guessing what the user wants to do, at a cost of convenience for the simpler use cases. You are responsible, for example, for adding [`RigidBody::Dynamic`] to each shard of glass (if that's what you need), and you can customize the entities using hooks.
//!
//! **Making the original glass entity hidden**
//!
//! This plugin does not assume what you want to do with the original [`Glass`] entity. If you want it to be hidden when the glass shatters, this will have to be done manually by inserting [`Visibility::Hidden`].
//!
//! **Shards**
//!
//! Are entities with the [`Shard`] component.
//! You can use this to, for example, make an OnAdd hook that automatically makes shards have a dynamic rigid body when added.
//!
//! **Shard relationship**
//!
//! Shards and their Glass are related using [`ShardOf`] and [`Shards`]. You can use this to delete all the shards belonging to a glass, make all the shards have the same material as their glass, etc.
//!
//!
//! # Examples
//!
//! See the [`examples/`](https://github.com/IVSOP/bevy_shatter/tree/main/examples) folder.
//!
//! # Compatibility
//!
//! | `bevy_shatter` | `bevy` |
//! | :--            | :--    |
//! | `0.1.0`        | `0.16` |
//!
//! # How it works
//!
//! Currently, the glass is broken into cells using a voronoi diagram. These cells are then extruded to 3D, creating a shard.
//!
//! Each cell's position is chosen by dividing the glass into a grid, to ensure they are all roughly the same size, and then picking random points within each cell of the grid.
//!
//! # Contributing
//!
//! This plugin is in very early development. PRs and forks are welcome. See TODO.md for a list of things that are missing

use avian3d::prelude::*;
use bevy::{
    asset::RenderAssetUsages,
    platform::collections::HashMap,
    prelude::*,
    render::mesh::{Indices, PrimitiveTopology},
};
use voronator::{delaunator::*, VoronoiDiagram};

mod autoglass;
pub use autoglass::*;

mod plugin;
pub use plugin::*;

// TODO: store num_cell_points as floats??
/// The component that marks an entity as glass that can be shattered. No other components are added to the entity, so you should add a material, mesh, etc. Feel free to take the mesh from [`GlassMesh`]. See [`AutoGlass`] for a quick way to spawn glass with some default components.
///
/// **Note:** the [`Transform::scale`] will be used to get the width (x), height (y) and thickness (z) of the glass.
#[derive(Component, Clone, Debug)]
pub struct Glass {
    /// The number of cell points to be used along the width and the height. Either passed in manually or through [`Glass::new_with_density`].
    /// Increasing this number means that more shattered glass pieces will be spawned, with smaller sizes,
    /// increasing computational cost
    pub num_cell_points: UVec2,
}

impl Glass {
    /// Generates glass using a density value (number of cells per unit of distance),
    /// automatically computing the number of cells
    pub fn new_with_density(width: f32, height: f32, cells_per_unit: f32) -> Self {
        let cells_x: u32 = (cells_per_unit * width).floor() as u32;
        let cells_y: u32 = (cells_per_unit * height).floor() as u32;

        Self {
            num_cell_points: UVec2::new(cells_x, cells_y),
        }
    }

    /// Generates glass using an XY grid for the number of cells
    pub fn new(num_cell_points: UVec2) -> Self {
        Self { num_cell_points }
    }

    // TODO: how to generate a lot of random numbers as fast as possible?
    // TODO: break this into more than one method
    /// Spawns the entities that make up the shattered glass and makes the old glass invisible.
    fn shatter(
        &self,
        glass_entity: Entity,
        glass_transf: &Transform,
        glass_material: Handle<StandardMaterial>,
        mut commands: Commands,
        mut meshes: ResMut<Assets<Mesh>>,
    ) {
        // voronator crashes when the cells overlap or are too close
        // when using a lot of cells or a very small glass, this actually becomes a pain
        // you might also just get unlucky with the RNG gods and have the game crash for no apparent reason
        // to ensure this can never happen, I define a safety margin EPSILON, and cells must have at least that distance from each other
        // FIX: also consider the case where it is not possible to conserve this distance, but at that point it's mostly user error
        const EPSILON: f32 = 0.001;

        let width = glass_transf.scale.x;
        let height = glass_transf.scale.y;
        let thickness = glass_transf.scale.z;

        // the full cell width, used to determine the center of each cell
        let cell_width: f32 = width / self.num_cell_points.x as f32;
        let cell_height: f32 = height / self.num_cell_points.y as f32;

        // the max offset a point can be in from the center of the cell
        let cell_offset = Vec2::new((cell_width / 2.0) - EPSILON, (cell_height / 2.0) - EPSILON);

        // 2*cell_offset, representing the max offset from one edge of the cell to the other, instead of just to the center
        let full_cell_offset = cell_offset * 2.0;

        let mut cells: Vec<(f64, f64)> = Vec::new();

        // build the cells from bottom left to top right
        for y in 0..self.num_cell_points.y {
            for x in 0..self.num_cell_points.x {
                let cell_center = Vec2::new(x as f32 * cell_width, y as f32 * cell_height);

                let bottom_left = cell_center - cell_offset;

                // generate a random float inside the cell, using cell_offset_2
                // then offset it by the bottom left position
                let rand = Vec2::new(fastrand::f32(), fastrand::f32());

                let position = (rand * full_cell_offset) + bottom_left;

                cells.push((position.x as f64, position.y as f64));
            }
        }

        let voronoi_diagram =
            VoronoiDiagram::<Point>::from_tuple(&(0., 0.), &(width as f64, height as f64), &cells)
                .expect("Error generating Voronoi diagram");

        // to allow shard baking, this is now done manually by the user
        // // mark original entity as invisible
        // commands.entity(glass_entity).insert(Visibility::Hidden);

        // it is (much) easier to offset the vertices themselves than the transform,
        // so every shard uses this transform which corresponds to the bottom left of the glass
        let shard_transform = glass_transf.with_scale(Vec3::ONE)
            * Transform::from_translation(Vec3::new(-width, -height, thickness) / 2.0);

        // iterate voronoi cells and triangulate them
        // also need to extrude since we want 3D mesh
        // TODO: consider generating the normals myself
        // TODO: extruding vertices was way harder than I expected, I have no idea thy I use negative values like -width and -thickness,
        // if it works it works. try to replace this with some lib that can extrude meshes in the future, I couldn't find anything decent and lightweight
        for (cell_id, cell) in voronoi_diagram.cells().iter().enumerate() {
            let points = cell.points();
            let shard_center = cells[cell_id];

            // if the cell has less than 3 points it can't be triangulated, this is extremely rare, have to find out why it happens
            if let Some(delaunay) = triangulate::<Point>(points) {
                // Original vertices are used as the top (z = 0)
                let mut verts: Vec<Vec3> = points
                    .iter()
                    .map(|point| Vec3::new(point.x as f32, point.y as f32, 0.0))
                    .collect();
                let n = verts.len();

                // Extruded vertices as the bottom (z = -thickness)
                let mut top_verts: Vec<Vec3> = points
                    .iter()
                    .map(|point| Vec3::new(point.x as f32, point.y as f32, -thickness))
                    .collect();
                verts.append(&mut top_verts);

                // now we have to make edges to join the bottom and top vertices.
                // from here on this was mostly made by grok as I couldn't find any resources on this, and
                // making the triangles have the exact order you need them to have is hard
                let mut edge_count: HashMap<(usize, usize), i32> = HashMap::new();
                for triangle in delaunay.triangles.chunks(3) {
                    let edges = [
                        (triangle[0], triangle[1]),
                        (triangle[1], triangle[2]),
                        (triangle[2], triangle[0]),
                    ];
                    for &(a, b) in edges.iter() {
                        *edge_count.entry((a, b)).or_insert(0) += 1;
                        *edge_count.entry((b, a)).or_insert(0) -= 1;
                    }
                }

                // Only keep edges that appear once (boundary edges)
                let boundary_edges: Vec<(usize, usize)> = edge_count
                    .iter()
                    .filter(|&(&(_, _), &count)| count == 1)
                    .map(|(&(a, b), _)| (a, b))
                    .collect();

                let mut indices: Vec<u32> = Vec::new();

                // Bottom faces (reversed for outward facing)
                for triangle in delaunay.triangles.chunks(3) {
                    indices.extend_from_slice(&[
                        triangle[2] as u32,
                        triangle[1] as u32,
                        triangle[0] as u32,
                    ]);
                }

                // Top faces
                for triangle in delaunay.triangles.chunks(3) {
                    indices.extend_from_slice(&[
                        (triangle[0] + n) as u32,
                        (triangle[1] + n) as u32,
                        (triangle[2] + n) as u32,
                    ]);
                }

                // Side faces with proper winding
                // TODO: calculate normals here??
                for &(a, b) in boundary_edges.iter() {
                    indices.extend_from_slice(&[
                        a as u32,
                        b as u32,
                        (b + n) as u32,
                        (b + n) as u32,
                        (a + n) as u32,
                        a as u32,
                    ]);
                }

                // Create the mesh
                // I assume I will never need the mesh on the CPU again
                let mut mesh = Mesh::new(
                    PrimitiveTopology::TriangleList,
                    RenderAssetUsages::RENDER_WORLD,
                )
                .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, verts)
                // .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
                .with_inserted_indices(Indices::U32(indices));

                // collider
                let collider =
                // Collider::trimesh_from_mesh(&mesh) // this has abysmal performance for some reason, but works fine in rapier
                Collider::convex_hull_from_mesh(&mesh) // this is probably slow to create but is the only way I can get stable performance with avian
                .expect("Could not make trimesh out of the extrusion mesh for a cell");

                // add the normals. this is VERY inneficient but whatever, had many issues doing it manually
                // also, should this be done before collider??
                mesh = mesh.with_duplicated_vertices().with_computed_flat_normals();

                // spawn the glass shard
                commands.spawn((
                    shard_transform,
                    Mesh3d(meshes.add(mesh)),
                    MeshMaterial3d(glass_material.clone()),
                    collider,
                    ShardOf(glass_entity),
                    Shard {
                        pos: Vec2::new(shard_center.0 as f32, shard_center.1 as f32),
                    },
                ));
            } else {
                // delaunay failed
                warn!("Failed to triangulate a glass shard, skipping it");
            }
        }
    }

    /// Projects a point onto the glass, returning a position that is relative to the bottom left.
    /// This is useful since [`Shard`] also uses a relative Vec2 position
    pub fn project_to_glass(&self, glass_transf: &Transform, point: Vec3) -> Vec2 {
        let up = glass_transf.up().as_vec3();
        let right = glass_transf.right().as_vec3();
        let half_width = glass_transf.scale.x / 2.0;
        let half_height = glass_transf.scale.y / 2.0;

        let bottom_left = glass_transf.translation - (right * half_width) - (up * half_height);

        // I make two planes, with an origin at the bottom left of the glass
        // one of them is pointing to the right, the other is pointing up
        // this way, for example, by projecting the point to the bottom plane that is pointing up,
        // I can get how far the point has moved along the width of the glass
        let isometry = Isometry3d::from_translation(bottom_left);
        let left_plane = InfinitePlane3d::new(right);
        let bottom_plane = InfinitePlane3d::new(up);

        let left_proj = left_plane.project_point(isometry, point);
        let bottom_proj = bottom_plane.project_point(isometry, point);

        Vec2::new(
            // distance from left to point
            left_proj.distance(point),
            // distance from bottom to point
            bottom_proj.distance(point),
        )
    }
}

/// A glass shard is a [`ShardOf`] a certain glass.
/// This allows you to get information on what glass caused certain shards to spawn.
#[derive(Component)]
#[relationship(relationship_target = Shards)]
pub struct ShardOf(pub Entity);

/// A glass will have shards as its children. This allows you to get all the shards originating from a glass.
/// With this, you could, for example, despawn the glass entity along with all of its shards.
#[derive(Component, Deref)]
#[relationship_target(relationship = ShardOf)]
pub struct Shards(Vec<Entity>);

/// Every glass shard has this component, so you can use it with a hook to customize the shards.
#[derive(Component)]
pub struct Shard {
    /// Position in the glass, relative to the bottom left point.
    /// Values are negative sometimes, I have no idea how or why
    pub pos: Vec2,
}

/// Resource created by the plugin with a 1x1x1 cube collider.
/// Used by [`AutoGlass`], but you can use it too.
#[derive(Resource)]
struct GlassCollider(pub Collider);

/// Resource created by the plugin with a 1x1x1 cube mesh.
/// Used by [`AutoGlass`], but you can use it too.
#[derive(Resource)]
pub struct GlassMesh(pub Handle<Mesh>);

/// Add this component to an entity with the [`Glass`] component to shatter it,
/// which creates all the glass shards.
#[derive(Component)]
pub struct Shattered;

/// Hook to spawn glass shards when [`Shattered`] is added to a Glass entity
fn shatter_hook(
    trigger: Trigger<OnAdd, Shattered>,
    glasses: Populated<(&Glass, &Transform, &MeshMaterial3d<StandardMaterial>)>,
    mut commands: Commands,
    meshes: ResMut<Assets<Mesh>>,
) {
    let entity = trigger.target();

    let (glass, transform, material) = glasses
        .get(entity)
        .expect("Trying to shatter an entity without Glass");

    glass.shatter(
        entity,
        transform,
        material.0.clone(),
        commands.reborrow(),
        meshes,
    );
}