mob_rapier

3D physics for Mob apps: Rapier 3D wrapped as a Rustler NIF, plus a named-world registry and the face-up decode rules the rapier_lab spike proved out.

Rapier is a Rust rigid-body physics engine by dimforge — production-quality, actively maintained, and fast enough to run a tabletop scene at 30 fps on a mid-range Android device. If you have not used it before, the Rapier home page has demos and the user guide explains the concepts one at a time. mob_rapier follows Rapier's own naming; if a knob surprises you, the matching page in Rapier's docs is the ground truth.

Extracted from rapier_lab after the API stabilised across the dice (d6, d10, d12, d20) and shells demos — bead rapier_lab-d00. rapier_lab now depends on this package and keeps only the demo screens; the physics primitives live here.

What ships

See guides/dice.md for the face-up rule per shape, and guides/physics_tuning.md for the density / damping / friction / restitution defaults each dynamic body inherits.

Usage

alias MobRapier.{Dice, Physics}
# One world, driven by name so agents / IEx can talk to it over dist.
:ok = Physics.new_world("dice_tray")
# A ground plane and four walls (per-wall extents; a wall is a cuboid).
_ground_id = Physics.add_static_cuboid_in("dice_tray", 1.0, 0.05, 1.0, 0.0, -0.05, 0.0)
# A d6, dropped from 20 cm above the origin.
die_id = Physics.add_cuboid_in("dice_tray", 0.015, 0.015, 0.015, 0.0, 0.20, 0.0)
# Kick it — the impulses' scale assumes the physics_tuning defaults
# (density 1000 kg/m³ → ~30 g die).
:ok = Physics.apply_impulse( "dice_tray", die_id, 0.010, 0.000, 0.008)
:ok = Physics.apply_torque_impulse("dice_tray", die_id, 5.0e-4, 0.0, 3.0e-4)
# Tick and read back until it stops moving; then decode which face is up.
:ok = Physics.step_in("dice_tray", 1.0 / 30.0)
case Physics.transforms_in("dice_tray") do
[{^die_id, _pos, {qx, qy, qz, qw}}] -> Dice.face_up_d6({qx, qy, qz, qw})
_ -> nil
end

The rapier_lab demo screens (dice, shells) show the same shape end to end — impulse, tick, read transforms, decode face-up when settled, report it on screen.

Host integration

mix mob.deploy cross-compiles the Rust crate for iOS + Android when the host's mob.exs registers it as a static NIF:

# mob.exs
config :mob_dev,
static_nifs: [
%{module: :lab_physics, archs: [:all]}
]

The crate name is lab_physics for historical reasons — renaming it churns more than it clarifies.

On device (mob_dev's static-NIF pipeline) Application.app_dir(:mob_rapier) raises because the flat OTP bundle doesn't register per-dep lib_dirs (see MOB-254). Consumers work around it by routing the Rustler on_load lookup through the host's app_dir:

# config/config.exs
config :mob_rapier, :otp_app, :my_app

Defaults to :mob_rapier, which is right for host dev + tests.

Native crate

native/lab_physics/ wraps rapier3d 0.22 behind a small Rustler surface. Contact events flow through a ChannelEventCollector per step; the buffer caps at 1024 events per channel with a contacts_dropped counter for saturation detection (bead rapier_lab-bvr).

The tuning surface — density, damping, friction, restitution — is a handful of constants at the top of src/lib.rs. See guides/physics_tuning.md for what each knob does, why the shipped values are what they are, and where to look in Rapier's docs when you want to override them.

License

MIT.