aether-physics
Rapier3D physics integration
The aether-physics crate integrates the Rapier3D physics engine with Aether's ECS. It provides rigid body dynamics, collision detection, joints, and VR-specific interaction physics including grab, throw, hand collision, and haptic feedback.
Overview
Physics simulation runs as an ECS system, stepping the Rapier world each tick and synchronizing results back to entity components. The crate handles:
- Rigid body dynamics with configurable mass, damping, and gravity.
- Colliders in standard shapes (box, sphere, capsule, convex hull, trimesh) with collision layers and filters.
- Joints for constraining bodies (fixed, revolute, prismatic, spherical).
- Triggers for detecting overlap without generating contact forces.
- VR interaction for physically plausible hand interactions in virtual reality.
Key Types
PhysicsWorld
Wraps the Rapier physics pipeline and manages the simulation state. Typically accessed through the ECS as a resource.
use aether_physics::PhysicsWorld;
let mut physics = PhysicsWorld::new();
physics.set_gravity([0.0, -9.81, 0.0]);
RigidBody
Defines how an entity participates in physics simulation. Variants include dynamic, kinematic, and static.
use aether_physics::{RigidBody, RigidBodyType};
let body = RigidBody::new(RigidBodyType::Dynamic)
.with_mass(2.0)
.with_linear_damping(0.1)
.with_angular_damping(0.05);
world.insert(entity, body);
Collider
Defines the shape used for collision detection. Attach one or more colliders to an entity with a rigid body.
use aether_physics::{Collider, ColliderShape};
// Box collider with half-extents
let collider = Collider::new(ColliderShape::Box {
half_extents: [0.5, 0.5, 0.5],
})
.with_friction(0.6)
.with_restitution(0.3)
.with_collision_layer(CollisionLayer::DEFAULT);
world.insert(entity, collider);
Joint
Constrains two rigid bodies together. Useful for doors, chains, ragdolls, and mechanical assemblies.
use aether_physics::{Joint, JointType};
let hinge = Joint::new(JointType::Revolute {
axis: [0.0, 1.0, 0.0],
limits: Some((-1.57, 1.57)),
})
.with_body_a(door_frame)
.with_body_b(door_panel);
physics.create_joint(hinge);
VR Interaction Features
The crate includes a dedicated VR interaction layer built on top of the core physics simulation.
Grab
Entities tagged with Grabbable can be picked up by VR hand controllers. When a grab is initiated, a temporary joint is created between the hand and the object, allowing natural physical manipulation.
use aether_physics::{Grabbable, GrabSettings};
world.insert(entity, Grabbable {
settings: GrabSettings {
snap_to_hand: true,
two_hand_mode: TwoHandMode::ScaleAndRotate,
..Default::default()
},
});
Throw
When a grabbed object is released, its velocity is computed from the hand's recent motion history. This produces natural-feeling throws with accurate direction and force.
use aether_physics::ThrowSettings;
world.insert(entity, Grabbable {
settings: GrabSettings {
throw: ThrowSettings {
velocity_samples: 5,
max_speed: 20.0,
..Default::default()
},
..Default::default()
},
});
Hand Collision
Hand colliders track the VR controller positions and generate contact events when the player's hands interact with the environment. This enables pushing buttons, flipping switches, and touching surfaces.
use aether_physics::HandCollider;
// Hand colliders are typically set up by the input system automatically.
// Custom collision responses can be registered through the event bus:
for event in world.events().read::<HandContactEvent>() {
if event.other_entity == button_entity {
activate_button(event.other_entity);
}
}
Haptics
Contact events can trigger haptic feedback on the VR controller, providing tactile response when interacting with objects.
use aether_physics::HapticFeedback;
world.insert(entity, HapticFeedback {
on_contact: HapticPulse {
intensity: 0.5,
duration_ms: 50,
},
on_grab: HapticPulse {
intensity: 0.8,
duration_ms: 100,
},
});
Basic Usage
A minimal physics setup that creates a ground plane and a falling box:
use aether_ecs::World;
use aether_physics::{
PhysicsPlugin, RigidBody, RigidBodyType, Collider, ColliderShape,
};
let mut world = World::new();
world.add_plugin(PhysicsPlugin::default());
// Static ground plane
let ground = world.spawn();
world.insert(ground, RigidBody::new(RigidBodyType::Static));
world.insert(ground, Collider::new(ColliderShape::Box {
half_extents: [50.0, 0.1, 50.0],
}));
// Dynamic box that falls under gravity
let cube = world.spawn();
world.insert(cube, RigidBody::new(RigidBodyType::Dynamic).with_mass(1.0));
world.insert(cube, Collider::new(ColliderShape::Box {
half_extents: [0.5, 0.5, 0.5],
}));
world.insert(cube, Position { x: 0.0, y: 10.0, z: 0.0 });
// Step the simulation
world.tick();