Post-Touchdown Physics

Overview

Extends landing mechanics (#745) with realistic rigid-body dynamics after surface contact. Instead of snapping to upright on touchdown, the ship enters a contact state where bounce, slide, and topple are simulated over multiple ticks. The player retains thrust control throughout.

States

The ship’s surface interaction now uses three states:

State landed_body_name surface_contact_state Description
Flying "" "" Normal orbital/atmospheric flight
Contact body name "contact" Ship touching surface, dynamics active
Landed body name "landed" Ship stable on surface, attached
Tipped body name "tipped" Ship fallen over, needs RCS to right

State Transitions

Flying → Contact    Surface contact detected (any speed < crash threshold)
                    Angular velocity zeroed, attitude aligned to surface normal
Contact → Landed   Ship stable: tilt < 5°, angular velocity < 0.01 rad/s,
                    vertical speed < 0.5 m/s, horizontal speed < 0.5 m/s
Contact → Tipped   Ship tilt exceeds 80° (past center of mass over footprint)
Contact → Flying    Ship bounces above surface or player thrusts off
Contact → Crash     Vertical speed exceeds crash threshold (50 m/s)
Tipped → Contact   Player fires RCS torque, ship lifts off tipped state
Landed → Flying    Main engine + RCS upward thrust > weight

Crash Thresholds

Condition Threshold Result
Vertical speed at initial contact > 50 m/s Crash (too fast)
Re-contact after bounce > 50 m/s Crash
Tipped ship contacts ground at high speed > 50 m/s Crash

The original 10 m/s landing threshold becomes the bounce threshold: impacts below 10 m/s settle without bouncing, 10–50 m/s bounce.

Contact Physics Model

Coordinate Frame

All contact physics operates in the body’s surface-local frame:

  • Radial axis (r_hat): unit vector from body center through contact point (local “up”)
  • Tangent plane: perpendicular to radial axis (local “ground”)
  • Velocities decomposed into radial (vertical) and tangential (horizontal) components

Ground Reaction Forces

When the ship is at or below the surface, a penalty-based contact force pushes it outward:

# Penetration depth (positive when below surface)
penetration = surface_radius - distance_to_body_center

if penetration > 0:
    # Spring-damper normal force (along r_hat)
    F_normal = k_spring * penetration - c_damper * v_radial

    # Clamp to non-negative (ground can push, not pull)
    F_normal = max(0, F_normal)

Spring-damper parameters (per ship class, derived from mass):

# Critical damping for settling without oscillation
k_spring = ship_mass * 100.0      # N/m — stiff enough for 1-tick settling at low speed
c_damper = 2 * sqrt(k_spring * ship_mass)  # Critical damping coefficient

Friction

Coulomb friction model with static/kinetic coefficients:

# Tangential velocity (horizontal motion on surface)
v_tangential = v_rel - dot(v_rel, r_hat) * r_hat
v_tan_mag = |v_tangential|

# Friction force opposes tangential motion
if v_tan_mag > 0.01:  # kinetic friction
    F_friction = -mu_kinetic * F_normal * normalize(v_tangential)
else:  # static friction (arrest residual motion)
    F_friction = -v_tangential * ship_mass / dt  # cancel tangential velocity
    # Clamp to static friction limit
    if |F_friction| > mu_static * F_normal:
        F_friction = -mu_kinetic * F_normal * normalize(v_tangential)

Friction coefficients (all ship classes):

Coefficient Value Notes
mu_static 0.8 Regolith-on-metal, prevents sliding on moderate slopes
mu_kinetic 0.5 Sliding friction

Slope Effects

On a sloped surface, gravity has a tangential component that causes sliding:

# Local gravity vector
g_vec = -G * body_mass / r^2 * r_hat  # points toward body center

# Decompose into normal and tangential
g_normal = dot(g_vec, surface_normal) * surface_normal
g_tangential = g_vec - g_normal

# Ship slides if tangential gravity exceeds static friction
# (handled automatically by the friction model above)

The surface normal at the contact point accounts for terrain slope (terrain normal, not just radial direction). For the initial implementation, surface normal equals r_hat (radial direction). Terrain-normal slope effects can be added later when per-vertex normals are available.

Bounce

When a ship contacts the surface with vertical speed between 10 and 50 m/s, the spring-damper produces a bounce. The coefficient of restitution is implicit in the damping ratio:

# With critical damping (zeta = 1.0), bounce is minimal
# For visible bounce, use underdamped system (zeta = 0.3):
c_damper = 2 * zeta * sqrt(k_spring * ship_mass)

Damping ratio: zeta = 0.3 (underdamped — produces 1–2 visible bounces before settling)

Topple Dynamics

Topple uses the ship’s full inertia tensor to compute angular acceleration from gravitational torque:

# Contact point: feet of the ship (center of mass is above by ground_contact_offset)
# When tilted, gravity through CoM creates a torque about the contact point

# CoM offset from contact point in ICRF
com_offset = ship_attitude.rotate(Vec3(0, 0, ground_contact_offset))

# Gravity force at CoM
F_gravity = -local_g * ship_mass * r_hat

# Torque about contact point
tau_gravity = cross(com_offset, F_gravity)

# Angular acceleration using inertia tensor (body frame)
tau_body = ship_attitude.conjugate().rotate(tau_gravity)
I = ship.get_inertia_tensor()
alpha_body = I_inverse @ tau_body

# Convert back to ICRF
alpha_icrf = ship_attitude.rotate(alpha_body)

The ship topples when the center of mass moves outside the support polygon (landing leg footprint). This happens when the tilt angle exceeds arctan(leg_radius / com_height) — approximately 37° for most ships.

Recovery torque: The player can fire RCS rotation to oppose the gravitational torque. The attitude controller operates normally during contact state, so existing RCS torque commands work.

Tipped State

A ship enters tipped state when tilt exceeds 80° during contact. In this state:

  • Ship is resting on its side, oriented by the gravitational torque settling
  • Position is pinned to the surface (same as landed)
  • RCS rotation is enabled — player can fire torque to right the ship
  • When RCS torque lifts the ship enough that tilt < 80° AND angular velocity has a righting component, transition back to contact state
  • Main engine thrust still checked for launch (if thrust axis happens to point away from surface)

Integration

Contact physics runs within the existing ship update loop (_update_ship). When surface_contact_state == "contact":

def _update_ship_contact(ship, body, dt):
    """Simulate contact dynamics for one tick."""
    # 1. Compute surface position and penetration
    r_vec = ship.position - body.position
    r = |r_vec|
    r_hat = r_vec / r
    surface_r = body.radius + terrain_height + ground_contact_offset
    penetration = surface_r - r

    # 2. Relative velocity (subtract body velocity + rotation)
    v_rel = ship.velocity - body.velocity - cross(body_omega, r_vec)
    v_radial = dot(v_rel, r_hat)
    v_tangential = v_rel - v_radial * r_hat

    # 3. Normal force (spring-damper)
    F_normal_mag = max(0, k_spring * penetration - c_damper * v_radial)
    F_normal = F_normal_mag * r_hat

    # 4. Friction force
    F_friction = compute_friction(v_tangential, F_normal_mag, ship.mass, dt)

    # 5. Gravity
    F_gravity = -local_g * ship.mass * r_hat

    # 6. Player thrust (if any)
    F_thrust = compute_thrust(ship)

    # 7. Total force → linear acceleration
    F_total = F_normal + F_friction + F_gravity + F_thrust
    accel = F_total / ship.mass
    ship.velocity += accel * dt
    ship.position += ship.velocity * dt

    # 8. Gravitational torque about contact point
    tau_gravity = compute_gravity_torque(ship, r_hat, local_g)
    tau_rcs = compute_rcs_torque(ship)  # player input
    tau_total = tau_gravity + tau_rcs

    # 9. Angular acceleration using inertia tensor
    I = ship.get_inertia_tensor()
    alpha = I_inverse @ attitude.conjugate().rotate(tau_total)
    ship.angular_velocity += attitude.rotate(alpha) * dt
    ship.attitude = integrate_quaternion(ship.attitude, ship.angular_velocity, dt)

    # 10. State transition checks
    check_contact_transitions(ship, penetration, v_radial, tilt_angle)

Fuel Consumption

During contact state:

  • Main engine and RCS consume fuel normally when active
  • Reaction wheels consume no fuel (same as always)
  • Attitude controller is active (responds to hold modes)

Ship State Fields

New field on Ship model:

Field Type Default Description
surface_contact_state string "" "" (flying), "contact", "landed", "tipped"

The existing landed_body_name field is reused — set to the body name for all surface states (contact, landed, tipped), empty when flying.

Events

ship.contact

Published to galaxy:ships Redis stream on initial surface contact.

event: ship.contact
ship_id: <uuid>
player_id: <uuid>
body: <string>

ship.landed

Existing event, now published when transitioning from contactlanded (not on initial touch).

ship.tipped

Published when ship topples past recovery angle.

event: ship.tipped
ship_id: <uuid>
player_id: <uuid>
body: <string>

ship.righted

Published when ship recovers from tipped state.

event: ship.righted
ship_id: <uuid>
player_id: <uuid>
body: <string>

Client Display

Contact State

  • Landing HUD shows active descent/velocity data
  • Ship mesh is positioned by server state (bouncing/sliding visible)
  • Status shows “CONTACT” in yellow

Tipped State

  • Status shows “TIPPED” in red
  • Landing HUD shows tilt angle prominently
  • Hint text: “Use RCS rotation to right the ship”

Landed State

  • Same as current (status “LANDED”, HUD shows “Landed”)

Proto Changes

Add surface_contact_state to ShipState protobuf message:

string surface_contact_state = 30;  // "", "contact", "landed", "tipped"

Configuration

New settings in services/physics/src/config.py:

Setting Default Description
contact_crash_speed 50.0 Max vertical speed before crash (m/s)
contact_bounce_speed 10.0 Vertical speed above which ship bounces (m/s)
contact_settle_tilt_deg 5.0 Max tilt for transition to landed (degrees)
contact_settle_angular_vel 0.01 Max angular velocity for settled (rad/s)
contact_settle_linear_vel 0.5 Max linear velocity for settled (m/s)
contact_tipped_angle_deg 80.0 Tilt angle for tipped state (degrees)
contact_damping_ratio 0.3 Spring-damper damping ratio
contact_friction_static 0.8 Static friction coefficient
contact_friction_kinetic 0.5 Kinetic friction coefficient

Implementation Files

Physics Service

  • services/physics/src/simulation.py — Contact physics in _update_ship()
  • services/physics/src/contact.py — New module: contact forces, friction, torque
  • services/physics/src/models.py — Add surface_contact_state field
  • services/physics/src/config.py — Contact parameters
  • services/physics/src/redis_state.py — Serialize contact state
  • services/physics/proto/physics.proto — Add contact state to ShipState

Web Client

  • services/web-client/src/cockpitLandingHud.js — Contact/tipped display
  • services/web-client/src/fleetHud.js — Status text for contact/tipped

Tick Engine

  • services/tick-engine/src/maneuver_landing.py — Detect contact state for autopilot completion

Dependencies

  • #745 Landing mechanics (done)
  • #744 Server-side terrain height (done)

Out of Scope

  • Leg compression animation: Visual-only effect, deferred
  • Terrain-normal slope: Uses radial direction for now; per-vertex terrain normals later
  • Multi-contact-point: Single contact point at ship center; multi-leg contact later
  • Damage model: Tipped ships survive intact; damage system is a separate feature

Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.