Skip to content
This repository has been archived by the owner on Nov 29, 2022. It is now read-only.

feat: generate collision shapes from mesh via the PendingConvexCollision component (behind the collision-from-mesh feature flag) #190

Merged
merged 7 commits into from
Feb 19, 2022
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ categories = ["game-development", "simulation"]
all-features = true

[features]
default = []
default = ["collision-from-mesh"]
collision-from-mesh = ["heron_core/collision-from-mesh"]
2d = ["heron_rapier/2d"]
3d = ["heron_rapier/3d", "heron_core/3d"]
debug-2d = ["2d", "heron_debug/2d"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ heron = { version = "1.1.0", features = ["3d"] }
One must choose to use either `2d` or `3d`. If neither of the two features is enabled, the `PhysicsPlugin` won't be available.


* `collision-from-mesh` Add a component to generate convex hull collision for a mesh.
* `3d` Enable simulation on the 3 axes `x`, `y`, and `z`.
* `2d` Enable simulation only on the first 2 axes `x` and `y`.
* `debug-2d` Render 2d collision shapes.
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ all-features = true
[features]
default = []
3d = []
collision-from-mesh = ["bevy/render"]

[dependencies]
bevy = { version = "0.6.0", default-features = false }
Expand Down
229 changes: 229 additions & 0 deletions core/src/collision_from_mesh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use bevy::{prelude::*, render::mesh::VertexAttributeValues};

use crate::{CollisionShape, RigidBody};

/// Component which indicates that this entity or its children contains meshes which waiting for collision generation.
///
/// Once a mesh is added (for example, Bevy loads the GTLF scenes asynchronously), then the entity or its children will be added [`RigidBody`] and [`CollisionShape::ConvexHull`] (based on the geometry) components.
///
/// # Example
///
/// ```
/// # use bevy::prelude::*;
/// # use heron_core::*;
/// fn spawn(mut commands: Commands, asset_server: ResMut<AssetServer>) {
/// commands
/// .spawn()
/// .insert(Transform::default()) // Required to apply GLTF transforms in Bevy
/// .insert(GlobalTransform::default())
/// .insert(PendingConvexCollision {
/// body_type: RigidBody::Static,
/// border_radius: None,
/// })
/// .with_children(|parent| {
/// parent.spawn_scene(asset_server.load("cubes.glb#Scene0"));
/// });
/// }
/// ```
#[derive(Component)]
pub struct PendingConvexCollision {
/// Rigid body type which will be assigned to every scene entity.
pub body_type: RigidBody,
/// Border radius that will be used for [`CollisionShape::ConvexHull`].
pub border_radius: Option<f32>,
}

/// Generates collision and attaches physics body for all entities with [`PendingConvexCollision`].
pub(super) fn pending_collision_system(
mut commands: Commands<'_, '_>,
added_scenes: Query<'_, '_, (Entity, &Children, &PendingConvexCollision)>,
scene_elements: Query<'_, '_, &Children, Without<PendingConvexCollision>>,
mesh_handles: Query<'_, '_, &Handle<Mesh>>,
meshes: Res<'_, Assets<Mesh>>,
) {
for (entity, children, pending_collision) in added_scenes.iter() {
if generate_collision(
&mut commands,
pending_collision,
children,
&scene_elements,
&mesh_handles,
&meshes,
) {
// Only delete the component when the meshes are loaded and their is generated
commands.entity(entity).remove::<PendingConvexCollision>();
}
}
}

/// Recursively generate collision and attach physics body for the specified children.
/// Returns `true` if a mesh was found.
fn generate_collision(
commands: &mut Commands<'_, '_>,
pending_collision: &PendingConvexCollision,
children: &Children,
scene_elements: &Query<'_, '_, &Children, Without<PendingConvexCollision>>,
mesh_handles: &Query<'_, '_, &Handle<Mesh>>,
meshes: &Assets<Mesh>,
) -> bool {
let mut generated = false;
for child in children.iter() {
if let Ok(children) = scene_elements.get(*child) {
if generate_collision(
commands,
pending_collision,
children,
scene_elements,
mesh_handles,
meshes,
) {
generated = true;
}
}
if let Ok(handle) = mesh_handles.get(*child) {
generated = true;
let mesh = meshes.get(handle).unwrap(); // SAFETY: Mesh already loaded
let vertices = match mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap() {
VertexAttributeValues::Float32x3(vertices) => vertices,
_ => unreachable!(
"Mesh should have encoded vertices as VertexAttributeValues::Float32x3"
),
};
let mut points = Vec::with_capacity(vertices.len());
for vertex in vertices {
points.push(Vec3::new(vertex[0], vertex[1], vertex[2]));
}
commands
.entity(*child)
.insert(pending_collision.body_type)
.insert(CollisionShape::ConvexHull {
points,
border_radius: pending_collision.border_radius,
});
}
}

generated
}

#[cfg(test)]
mod tests {
use bevy::{
asset::AssetPlugin,
core::CorePlugin,
prelude::shape::{Capsule, Cube},
render::{options::WgpuOptions, RenderPlugin},
window::WindowPlugin,
};

use super::*;

// Allows run tests for systems containing rendering related things without GPU
pub(super) struct HeadlessRenderPlugin;

impl Plugin for HeadlessRenderPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(WgpuOptions {
backends: None,
..Default::default()
})
.add_plugin(CorePlugin::default())
.add_plugin(WindowPlugin::default())
.add_plugin(AssetPlugin::default())
.add_plugin(RenderPlugin::default());
}
}

#[test]
fn pending_collision_assignes() {
let mut app = App::new();
app.add_plugin(HeadlessRenderPlugin)
.add_system(pending_collision_system);

let mut meshes = app.world.get_resource_mut::<Assets<Mesh>>().unwrap();
let cube = meshes.add(Cube::default().into());
let capsule = meshes.add(Capsule::default().into());

const REQUESTED_COLLISION: PendingConvexCollision = PendingConvexCollision {
body_type: RigidBody::Static,
border_radius: None,
};

let parent = app
.world
.spawn()
.insert(REQUESTED_COLLISION)
.with_children(|parent| {
parent.spawn().insert(cube);
parent.spawn().insert(capsule);
})
.id();

let mut query = app
.world
.query::<(&Handle<Mesh>, &RigidBody, &CollisionShape)>();
assert_eq!(
query.iter(&app.world).count(),
0,
"Mesh handles, rigid bodies and collision shapes shouldn't exist before update"
);

app.update();

assert_eq!(
query.iter(&app.world).count(),
2,
"Entities with mesh handles should have rigid bodies and collision shapes after update"
);

let meshes = app.world.get_resource::<Assets<Mesh>>().unwrap();
for (mesh_handle, body_type, collision_shape) in query.iter(&app.world) {
assert_eq!(
*body_type, REQUESTED_COLLISION.body_type,
"Assigned body type should be equal to specified"
);

let (points, border_radius) = match collision_shape {
CollisionShape::ConvexHull {
points,
border_radius,
} => (points, border_radius),
_ => panic!("Assigned collision shape must be a convex hull"),
};

let mesh = meshes.get(mesh_handle).unwrap();
let vertices = match mesh.attribute(Mesh::ATTRIBUTE_POSITION).unwrap() {
VertexAttributeValues::Float32x3(vertices) => vertices,
_ => unreachable!(
"Mesh should have encoded vertices as VertexAttributeValues::Float32x3"
),
};
for (point, vertex) in points.iter().zip(vertices) {
assert_eq!(
point.x, vertex[0],
"x collision value should be equal to mesh vertex value"
);
assert_eq!(
point.y, vertex[1],
"y collision value should be equal to mesh vertex value"
);
assert_eq!(
point.z, vertex[2],
"z collision value should be equal to mesh vertex value"
);
}

assert_eq!(
*border_radius, REQUESTED_COLLISION.border_radius,
"Assigned border radius should be equal to specified"
);
}

assert!(
!app.world
.entity(parent)
.contains::<PendingConvexCollision>(),
"Parent entity should have PendingConvexCollision removed"
);
}
}
7 changes: 7 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use std::sync::Arc;
use bevy::ecs::schedule::ShouldRun;
use bevy::prelude::*;

#[cfg(feature = "collision-from-mesh")]
pub use collision_from_mesh::PendingConvexCollision;
pub use constraints::RotationConstraints;
pub use events::{CollisionData, CollisionEvent};
pub use gravity::Gravity;
Expand All @@ -18,6 +20,8 @@ pub use physics_time::PhysicsTime;
pub use step::{PhysicsStepDuration, PhysicsSteps};
pub use velocity::{Acceleration, AxisAngle, Damping, Velocity};

#[cfg(feature = "collision-from-mesh")]
mod collision_from_mesh;
mod constraints;
mod events;
mod gravity;
Expand Down Expand Up @@ -76,6 +80,9 @@ impl Plugin for CorePlugin {
.add_stage_before(CoreStage::PostUpdate, crate::stage::ROOT, {
Schedule::default().with_stage(crate::stage::UPDATE, SystemStage::parallel())
});

#[cfg(feature = "collision-from-mesh")]
app.add_system(collision_from_mesh::pending_collision_system);
}
}

Expand Down