Physics Museum  ·  After Hours
Gallery 06 / Mech. · Hours: 24 / 7 · ↗ Repo
Now showing — six exhibits

Physics
Museum.

Six small Unity scenes that each isolate one principle of classical mechanics — the kind of room where you'd press a brass button and watch a single equation play itself out in the spotlight.

Engine
Unity 6 PhysX 5.1 · URP
Language
C# 9 MonoBehaviour · Rigidbody API
Exhibits
06 Five demos · One reset plinth
Repository
/physics-museumgithub.com/nathanaelhub

A room for
every equation.

Most physics tutorials demo the engine. This catalogue does the opposite — each scene is tuned away from engine defaults until only the equation remains.

Where the equation is a closed-form trajectory, the Rigidbody is removed and transform.position is written directly. Where the equation is F = m·a, the force mode is intentionally mass-dependent so heavy bodies feel heavy. Where damping matters, Unity 6's renamed linearDamping is named exactly because aerodynamic drag and the solver's velocity damping are not the same thing — and the museum should not pretend otherwise.

Each exhibit ships with its formula, its working code, and the one design decision that keeps the lesson legible. Press the buttons.

01— of six —
Hall A · Kinematics3 m × 4 m vitrine

The Kinematic
Catapult.

A projectile under gravity, written as a closed-form trajectory and read out frame by frame.

Formula — projectile position at time t
\[ x_f \;=\; \tfrac{1}{2}\,a\,t^{2} \;+\; v_i\,t \;+\; x_i \]
vitrine 01 / live
vᵢ range— m t0.00 s
Design
note
Intentionally not a Rigidbody. Drag, substep integration, and contact resolution would each stamp their fingerprints on the trajectory — and the trajectory is the exhibit. Writing transform.position directly keeps the formula pure.
KinematicCatapult.csC# · MonoBehaviour
using UnityEngine;

public class KinematicCatapult : MonoBehaviour
{
    public Vector2 vxRange = new(3.5f, 6.8f);
    public Vector2 vyRange = new(6.0f, 9.6f);
    public float   g       = 9.81f;

    Vector3 x0;
    Vector3 v;
    float   t;
    bool    flying;

    void Awake() => x0 = transform.position;

    public void Launch()
    {
        v = new Vector3(
            Random.Range(vxRange.x, vxRange.y),
            Random.Range(vyRange.x, vyRange.y),
            0f);
        transform.position = x0;
        t = 0f;
        flying = true;
    }

    void Update()
    {
        if (!flying) return;
        t += Time.deltaTime;

        // x_f = ½ a t² + vᵢ t + xᵢ  — written directly.
        // No Rigidbody: drag and contact resolution stay out of the lesson.
        Vector3 a = Vector3.down * g;
        transform.position = 0.5f * a * t * t + v * t + x0;

        if (transform.position.y <= x0.y) flying = false;
    }
}
02— of six —
Hall A · Newton II8 m straight track

The Drag
Race.

Two cubes, identical thrust, one is ten times heavier. The √10 lives in the timing strip.

Formula — Newton's Second Law, solved for acceleration
\[ F \;=\; m\,a \qquad \Longrightarrow \qquad a \;=\; \dfrac{F}{m} \]
vitrine 02 / live
light · 10 kg— s heavy · 100 kg— s ratio— ×
Design
note
ForceMode.Force is the mass-dependent choice — and the choice. Using ForceMode.Acceleration would have hidden the lesson by giving both cubes the same a, and the heavy cube would finish in a dead heat with the light one. The √10 is the exhibit.
DragRace.csC# · Rigidbody
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class DragRace : MonoBehaviour
{
    [Tooltip("Newtons — identical for the 10 kg and 100 kg cube.")]
    public float thrust = 80f;

    Rigidbody rb;
    void Awake() => rb = GetComponent<Rigidbody>();

    // ForceMode.Force is mass-dependent.
    // a = F / m  →  the 100 kg cube takes √10 ≈ 3.16× longer to cover the track.
    public void Throttle() =>
        rb.AddForce(Vector3.forward * thrust, ForceMode.Force);
}
03— of six —
Hall B · Dampingtall vitrine · vertical

The Skydiver
& Parachute.

Gravity pulls; damping pushes back. At equilibrium, velocity stops climbing.

Formula — terminal velocity from balance of forces
\[ m\,g \;=\; b\,v \qquad \Longrightarrow \qquad v_{\text{term}} \;=\; \dfrac{m\,g}{b} \]
vitrine 03 / live
v0.0 m/s b0.20 v_term49.0 m/s
Design
note
Unity 6 renamed Rigidbody.drag to linearDamping — a long-overdue clarification, because aerodynamic drag (a function of , area, and air density) and the engine's linear-velocity damping are not the same thing. Naming them apart lets the engine say so out loud.
SkydiverChute.csC# · Unity 6
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class SkydiverChute : MonoBehaviour
{
    public float bFreefall = 0.2f;   // open-air damping
    public float bChute    = 1.5f;   // parachute-deployed damping

    Rigidbody rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        // Unity 6: Rigidbody.drag → Rigidbody.linearDamping.
        // It is *not* aerodynamic drag; it is the solver's
        // linear-velocity damping coefficient, and the rename
        // says so explicitly.
        rb.linearDamping = bFreefall;
    }

    public void DeployParachute() => rb.linearDamping = bChute;
}
04— of six —
Hall B · Impulsethree rigid blocks · interactive

The Impulse
Shooter.

Hit a block dead-centre, it translates. Hit a corner, it tumbles. The hit-point is the design.

Formula — impulse → linear and angular velocity change
\[ \Delta v \;=\; \dfrac{J}{m} \qquad \Delta \omega \;=\; I^{-1}\,(\mathbf{r} \times \mathbf{J}) \]
vitrine 04 / live — click any block
impulseJ = 6 N·s hits0 last τ— N·m·s
Design
note
AddForceAtPosition — not AddForce — is the soul of the exhibit. Striking a Jenga block dead-centre and striking it on the corner has to feel different in the hand, or there is no exhibit. The cross product r × J is what supplies the difference.
ImpulseShooter.csC# · Rigidbody
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class ImpulseShooter : MonoBehaviour
{
    public float impulseMagnitude = 6f;   // N·s

    Rigidbody rb;
    void Awake() => rb = GetComponent<Rigidbody>();

    void OnMouseDown()
    {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (!Physics.Raycast(ray, out var hit) || hit.rigidbody != rb) return;

        Vector3 J = Vector3.up * impulseMagnitude;

        // AddForceAtPosition splits the impulse two ways:
        //   Δv  = J / m
        //   Δω  = I⁻¹ ( r × J )       with r = hit.point − centre
        // AddForce would discard r — and with it, the exhibit.
        rb.AddForceAtPosition(J, hit.point, ForceMode.Impulse);
    }
}
05— of six —
Hall C · Rotationunit cube · I = 1/6 kg·m²

The Spin
Cabinet.

A torque-impulse spins a unit cube. Damping pulls it back down. The constant is the homework.

Formula — angular impulse → angular velocity
\[ \Delta \omega \;=\; I^{-1}\,\boldsymbol{\tau}_{\text{impulse}} \]
vitrine 05 / live
ω0.0 rad/s revs0.00 τ3 N·m·s
Design
note
The default torqueVector = (0, 10, 0) in the C# gives Δω ≈ 60 rad/s on a 1 kg unit cube — close to 10 revolutions per second. The constant wants tuning; the cabinet is the lesson, the constant is homework left for the visitor.
SpinCabinet.csC# · AddTorque
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class SpinCabinet : MonoBehaviour
{
    [Tooltip("τ in N·m·s — homework left for the visitor.")]
    public Vector3 torqueVector = new(0f, 10f, 0f);

    Rigidbody rb;
    void Awake() => rb = GetComponent<Rigidbody>();

    // Δω = I⁻¹ · τ_impulse.
    // For a unit cube of mass 1 kg this is Δω ≈ 60 rad/s ≈ 10 rev/s.
    public void ApplySpin() =>
        rb.AddTorque(torqueVector, ForceMode.Impulse);
}
06— of six —
Atrium · Resetmaster switch · ceremonial

The Reset
Plinth.

A single brass button that sets every other exhibit back to opening hours.

Formula — state restoration
\[ \mathbf{x},\,\mathbf{R},\,\mathbf{v},\,\boldsymbol{\omega} \;\longleftarrow\; \mathbf{x}_0,\;\mathbf{R}_0,\;\mathbf{0},\;\mathbf{0} \]
Restore the
opening hours.

Press once. Positions, rotations, velocities, and angular velocities are returned to their starting frames. The solver is then told, gently, to forget the last hour.

Subjects under control
  • 01Catapultidle
  • 02Drag Raceidle
  • 03Skydiveridle
  • 04Impulseidle
  • 05Spin Cabinetidle
Design
note
The non-obvious line is rb.Sleep(). Zeroing a Rigidbody's velocity while it is still in contact with the floor lets the next FixedUpdate hand it a micro-velocity from contact resolution — and the exhibit looks subtly broken. Sleep tells the solver: don't touch this one.
ResetPlinth.csC# · master switch
using UnityEngine;

public class ResetPlinth : MonoBehaviour
{
    public Rigidbody[] subjects;
    Vector3[]    x0;
    Quaternion[] R0;

    void Awake()
    {
        x0 = new Vector3[subjects.Length];
        R0 = new Quaternion[subjects.Length];
        for (int i = 0; i < subjects.Length; i++)
        {
            x0[i] = subjects[i].position;
            R0[i] = subjects[i].rotation;
        }
    }

    public void ResetAll()
    {
        for (int i = 0; i < subjects.Length; i++)
        {
            var rb = subjects[i];
            rb.position        = x0[i];
            rb.rotation        = R0[i];
            rb.linearVelocity  = Vector3.zero;
            rb.angularVelocity = Vector3.zero;

            // Zeroing while in contact lets the next FixedUpdate hand the
            // body a micro-velocity from contact resolution, and the
            // exhibit looks subtly broken. Sleep tells the solver:
            // don't touch this one.
            rb.Sleep();
        }
    }
}
↳ Notes from the curator

The museum
is the argument.

Game-engine physics is a black box wrapped in friendly methods. The danger isn't that the box is wrong — it's that the box is right in ways you didn't intend.

Every exhibit here picks the least convenient API that still works, because it's the one that forces the equation to be visible. ForceMode.Force over .Acceleration. AddForceAtPosition over AddForce. transform.position over a Rigidbody, when the equation is closed-form.

Read the code, press the buttons, then go open the repository. The lights stay on.