# Physics (`oxide-physics`) — Stage 9

Oxide's physics is a **feature-gated module** (`oxide-physics`) built on
[`rapier3d`](https://rapier.rs), added to an app through the Stage-5 module
system. It is a comprehensive rigid-body system — collision, scene queries,
joints, and a kinematic character controller — not a thin wrapper. This document
grows piece by piece as Stage 9 lands; it currently covers the **data model and
module wiring** (piece 1).

## Design: the ECS is the source of truth

A physics object is described by two plain, serializable, reflected components on
a scene entity:

- [`RigidBody`](#rigidbody) — *how* it moves (or that it doesn't).
- [`Collider`](#collider) — *what shape* it is, its material, and the
  [`LayerMask`](layers.md) filtering of what it collides with.

The rapier simulation world is a **transient resource rebuilt from these
components**, never the authoritative store. That has a deliberate payoff: the
Stage-8.7 [play-mode snapshot](play-mode.md) captures these components like any
other, so **Play** runs the simulation, **Stop** reverts the authored components,
and the next **Play** rebuilds the rapier world fresh — with no special-casing.
The [`Transform`](scene.md) is the authoritative pose; the simulation writes it
back each fixed step (a later piece).

## Adding physics to an app

```rust
use oxide_engine::app::{App, DefaultModules};
use oxide_physics::PhysicsModule;

let mut app = App::new();
app.add_modules(DefaultModules);
app.add_module(PhysicsModule); // registers RigidBody/Collider + PhysicsSettings
```

`PhysicsModule` registers the component types for reflection (so they are
dual-editable from the inspector and from scripts/RON, and captured by the play
snapshot) and inserts a [`PhysicsSettings`] resource holding the global gravity
vector. Like every module it can be enabled, disabled, or removed as a unit, so a
game that never uses physics never compiles it in.

## RigidBody

The dynamics half of a physics object — attach alongside a `Collider`.

| Field | Meaning |
|-------|---------|
| `kind` | `Dynamic` (simulated), `Kinematic` (game-moved, unaffected by forces), or `Static` (immovable world geometry) |
| `mass` | Mass in kg; `0` derives it from the collider's `density` |
| `linear_damping` / `angular_damping` | Velocity drag (`0` = none) |
| `gravity_scale` | Per-body gravity multiplier (`1` normal, `0` floats) |
| `ccd` | Continuous collision detection for fast bodies (off by default) |

```rust
use oxide_physics::{RigidBody, RigidBodyKind};

let dynamic = RigidBody::default();          // a fully-simulated body
let floor   = RigidBody::static_body();      // immovable
let platform = RigidBody::kinematic();       // moved by the game
```

## Collider

The shape + material half — attach on its own for static geometry, or with a
`RigidBody` for a moving body. The shape is a flat selector plus dimension fields
(mirroring `MeshRenderer`'s `PrimitiveShape`), so the inspector renders a clean
combo + drag-values:

| `shape` | Dimensions used |
|---------|-----------------|
| `Box` | `half_extents` (per-axis half sizes) |
| `Sphere` | `radius` |
| `Capsule` | `radius` + `half_height` (axis = local `+Y`) |
| `Cylinder` | `radius` + `half_height` (axis = local `+Y`) |

Material/filter fields: `friction`, `restitution`, `density`, `sensor` (a trigger
that reports overlap without resolving contact), and the `membership` / `filter`
[`LayerMask`](layers.md)s. Two colliders interact only when each one's
`membership` intersects the other's `filter`, so collision groups, triggers, and
(later) scene queries all use the engine's one shared filtering primitive.

```rust
use oxide_engine::math::Vec3;
use oxide_engine::layer::LayerMask;
use oxide_physics::Collider;

let ground  = Collider::cuboid(Vec3::new(10.0, 0.5, 10.0));
let ball    = Collider::ball(0.5);
let trigger = Collider::cuboid(Vec3::splat(1.0)).as_sensor()
    .with_layers(LayerMask::layer(0), LayerMask::layer(1)); // only fires for layer 1
```

Convex-hull and triangle-mesh colliders (which need mesh data) are a later
Stage-9 piece.

## Simulation

`PhysicsModule` installs a [`PhysicsWorld`] resource — the rapier simulation plus
an entity ↔ body map — and a `FixedUpdate` system that, each fixed step:

1. **syncs** the rapier world to the scene (inserts a body+collider for each new
   physics entity, removes bodies for despawned ones, pushes kinematic targets),
2. **steps** rapier by one [`fixed_delta`](modules.md), then
3. **writes back** each moved body's pose onto its entity's `Transform`.

So physics runs whenever the app advances its fixed timestep — including the
editor's **Play** mode, which is the first real consumer (Play drops a body,
Stop reverts it). Nothing extra is wired: the play snapshot already captures the
components.

```rust
# use oxide_engine::app::{App, DefaultModules};
# use oxide_engine::math::{Transform, Vec3};
# use oxide_physics::{PhysicsModule, RigidBody, Collider};
let mut app = App::new();
app.add_modules(DefaultModules);
app.add_module(PhysicsModule);

let ball = app.scene.spawn("ball", Transform::from_translation(Vec3::new(0.0, 5.0, 0.0)));
app.scene.world_mut().insert_one(ball, RigidBody::default()).unwrap();
app.scene.world_mut().insert_one(ball, Collider::ball(0.5)).unwrap();

for _ in 0..120 { app.step(); }      // one fixed tick each
// the ball has fallen; its Transform.translation.y is now lower
```

### Forces & control

[`PhysicsWorld`] exposes by-entity control so game code never touches rapier
handles: `set_linear_velocity` / `linear_velocity`, `apply_impulse`,
`apply_force`, `apply_torque_impulse`, and `wake`. Reach the resource with
`app.get_resource_mut::<PhysicsWorld>()`.

### Collision & trigger events

When two colliders start or stop touching, the step records a `CollisionEvent`
`{ a, b, started, sensor }`. `started` distinguishes **enter** (`true`) from
**exit** (`false`); `sensor` distinguishes a **trigger** overlap (one collider is
a sensor — nothing was resolved, things pass through) from a **solid contact**.
Events accumulate across every fixed sub-step and are cleared at the first step
of each frame, so a system reading after `Update` sees them all:

```rust
# use oxide_physics::PhysicsWorld;
# fn read(app: &oxide_engine::app::App, player: oxide_engine::scene::Entity) {
let physics = app.get_resource::<PhysicsWorld>().unwrap();
for ev in physics.trigger_events() {
    if let Some(other) = ev.other(player) {
        if ev.started { /* player entered a trigger zone */ }
    }
}
# }
```

"Stay" (ongoing overlap) is not an event — query the current state with
`is_intersecting(a, b)` or `intersecting_pairs()`. Collision filtering uses the
collider `membership`/`filter` `LayerMask`s: two colliders interact only when
each one's membership intersects the other's filter, so a trigger can be made to
fire only for, say, the Player layer.

### Scene queries

[`PhysicsWorld`] answers spatial questions against the simulated colliders, all
filtered by a `LayerMask` (pass `LayerMask::ALL` to hit anything):

| Query | Returns |
|-------|---------|
| `raycast(origin, dir, max_distance, mask)` | first `RayHit { entity, toi, point, normal }` |
| `sphere_cast(origin, radius, dir, max_distance, mask)` | first hit of a swept sphere (a "thick raycast") |
| `overlap_sphere(center, radius, mask)` | every entity whose collider overlaps the sphere |
| `point_overlap(point, mask)` | every entity whose collider contains the point |

```rust
# use oxide_engine::math::Vec3;
# use oxide_engine::layer::LayerMask;
# use oxide_physics::PhysicsWorld;
# fn pick(physics: &PhysicsWorld) {
if let Some(hit) = physics.raycast(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y, 100.0, LayerMask::ALL) {
    // hit.entity / hit.point / hit.normal
}
# }
```

During simulation the step keeps the query world in sync. To query the **edited**
scene outside Play (e.g. the editor's raycast probe), build a transient world and
sync it once: `sync_to_scene(scene)` rebuilds the rapier world from the scene's
`Collider`/`RigidBody` components and refreshes the query pipeline **without
stepping**, so a following `raycast`/`overlap_*` reflects the current colliders.

```rust
# use oxide_engine::math::Vec3;
# use oxide_engine::layer::LayerMask;
# use oxide_engine::scene::Scene;
# use oxide_physics::PhysicsWorld;
# fn probe(scene: &Scene) {
let mut world = PhysicsWorld::new();
world.sync_to_scene(scene); // query-able, no Play / no step
let _ = world.raycast(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y, 100.0, LayerMask::ALL);
# }
```

### Joints / constraints

`add_joint(a, b, kind, anchor_a, anchor_b)` connects two entities' bodies and
returns a `JointId` (`remove_joint` undoes it). Both bodies must already be in
the simulation (true after the first step in which their components exist). The
`JointKind`s and the DOF they leave:

| Kind | Constraint |
|------|-----------|
| `Fixed` | a rigid weld — zero relative DOF |
| `Spherical` | ball-and-socket — anchors stay coincident, free rotation (3 rot. DOF) |
| `Revolute { axis }` | hinge — 1 rotational DOF about `axis` |
| `Prismatic { axis }` | slider — 1 translational DOF along `axis` |

Joints live in the `PhysicsWorld` (not the ECS), so — unlike components — they
are not captured by the play snapshot; game/setup code recreates them on each
Play. Editor-authored joint *components* await a serializable entity-reference
type (backlog).

### Character controller

A `CharacterController` component is a **kinematic capsule** — it never reacts to
forces. The game asks it to move and the controller resolves that against the
world (move-and-slide along walls, auto-step small ledges, snap to ground on
slopes, refuse slopes steeper than `max_slope_degrees`), reporting whether the
character ended up grounded. The entity carries only a `CharacterController` (no
`RigidBody`/`Collider`), so it is never simulated and never self-collides.

```rust
# use oxide_engine::math::Vec3;
# use oxide_physics::PhysicsWorld;
# fn tick(app: &mut oxide_engine::app::App, player: oxide_engine::scene::Entity, dt: f32) {
let desired = Vec3::new(input_x, -9.81 * dt, input_z); // move + gravity
let movement = {
    let physics = app.get_resource::<PhysicsWorld>().unwrap();
    physics.move_character(&app.scene, player, desired, dt).unwrap()
};
let mut t = app.scene.local_transform(player).unwrap();
t.translation += movement.translation;          // apply the resolved move
app.scene.set_local_transform(player, t);
// movement.grounded → e.g. allow jump
# let (input_x, input_z) = (0.0, 0.0);
# }
```

Tune the capsule (`radius`, `half_height`) and the rules (`max_slope_degrees`,
`step_offset`, `snap_to_ground`, `skin_width`) on the component. This is the
basis reused by the Stage-15 prototyping kit's character.

### Limitations (current)

`Transform::scale` is not yet applied to collider dimensions (author the shape at
its true size), and bodies are simulated in world space — keep physics bodies at
the scene root (or under unscaled parents) for now. Both are lifted in later
pieces.

## Roadmap (Stage 9 pieces)

1. **Component data model + module wiring** — `RigidBody`, `Collider`,
   `PhysicsModule`, `PhysicsSettings`. ✅
2. **Rapier-backed simulation** — build the world from components, step on
   `FixedUpdate`, write transforms back, by-entity forces/velocities. ✅
3. **Collision groups/masks via `LayerMask`, sensors, collision/trigger
   events** (enter/exit + stay queries). ✅
4. **Scene queries** — raycast, sphere-cast, sphere/point overlap, all
   `LayerMask`-filtered. ✅
5. **Joints/constraints** — fixed, spherical, revolute, prismatic (programmatic
   API). ✅
6. **Kinematic character controller** — capsule move-and-slide, step offset,
   slope limit, grounded. ✅
7. **Examples** — `physics_stack` (boxes settle + ball lands) and
   `character_capsule` (walk/climb/jump/wall), headless console demos. ✅ (this
   piece)
8. Editor: **(8a)** RigidBody/Collider/CharacterController are addable and
   editable in the inspector (no per-type code), and `PhysicsModule` runs in
   **Play** — drop a body, watch it fall, Stop reverts it (on `dev`, awaiting
   eye-check). **(8b)** collider shape wireframe gizmos in the viewport — box,
   sphere, capsule, and cylinder outlines, **green** for solid colliders and
   **amber** for sensors (triggers), with the selected entity drawn thicker.
   The outline mirrors the simulation, which ignores `Transform::scale`, so it
   shows the exact shape rapier builds (translation + rotation only). Toggle it
   from **View ▸ Show Colliders** (on by default). **(8c)** raycast debug viz —
   a **View ▸ Raycast Probe** toggle (off by default, a debug aid to verify the
   raycast API and inspect colliders): with it on, a viewport **click** casts the
   editor camera→cursor ray against the edited scene's colliders (via
   [`PhysicsWorld::sync_to_scene`](#scene-queries), so it needs no Play) and
   **freezes** the ray into the world. The viewport then redraws it every frame —
   the ray in **cyan**, plus on a hit a **magenta** dot at the surface point and
   a short whisker along the surface normal — so **orbiting the camera reveals it
   as a real 3D line** (a ray cast from the live camera is otherwise just a point
   in that same camera's view). ✅

**Stage 9 is complete** — all pieces are on `main`.

[`PhysicsSettings`]: #adding-physics-to-an-app
[`PhysicsWorld`]: #simulation
