Flight Simulator GUI: Physics-Based Aircraft Simulation with Real-Time Rendering
A 60Hz physics engine with 6-DOF dynamics and ImGui instrumentation

I've always wanted to build a flight sim from the ground up instead of just messing around in X-Plane, so over a few weeks I put one together in C++ with OpenGL and ImGui. The goal wasn't a pretty demo — I wanted the plane to actually fly like a plane, so I went with real 6-degree-of-freedom rigid body dynamics instead of faking the motion. Physics updates at a fixed 60 Hz using RK4 integration, which took me embarrassingly long to get right (more on that below).

For the cockpit I built out the standard "six-pack" of instruments — airspeed, altimeter, attitude indicator, heading, VSI, and turn coordinator — all drawn by hand with ImGui's DrawList API instead of using pre-made widgets, since I wanted them to actually look like gauges. There's also a first-person 3D view with an artificial horizon and a compass tape scrolling across the top.

Flight Simulator in Action

Screen capture of the sim running — physics, gauges, and 3D view all live


How it's put together

I split things up so the physics doesn't know or care about how it's being drawn, which made debugging way easier — I could print raw state to the console without dealing with the render loop at all.

┌─────────────────────────────────────────────────────────┐
│                     Main Loop (60 Hz)                    │
│                      (main.cpp)                          │
└────────────┬────────────────────────────┬────────────────┘
             │                            │
             ▼                            ▼
    ┌────────────────┐          ┌─────────────────┐
    │ Input Handler  │          │   Renderer      │
    │                │          │  (OpenGL +      │
    │ - Keyboard     │          │   ImGui)        │
    │ - Controls     │          │                 │
    └────────┬───────┘          └────────┬────────┘
             │                           │
             ▼                           │
    ┌────────────────┐                  │
    │    Aircraft    │◄─────────────────┘
    │                │         (renders)
    │ - State        │
    │ - Properties   │
    │ - Aero Coeff   │
    └────────┬───────┘
             │
             ▼
    ┌──────────────────┐
    │ Flight Dynamics  │
    │                  │
    │ - Forces         │
    │ - Moments        │
    │ - RK4 Integration│
    └────────┬─────────┘
             │
             ▼
    ┌──────────────────┐
    │   Atmosphere     │
    │                  │
    │ - ISA Model      │
    │ - ρ, P, T, a     │
    └──────────────────┘
                

Data flow, one tick at a time

User Input ──► InputHandler ──► Aircraft State
                                      │
                                      ▼
                            FlightDynamics.update()
                                      │
                    ┌─────────────────┴─────────────────┐
                    │                                   │
                    ▼                                   ▼
            calculateForces()                  calculateMoments()
                    │                                   │
                    └─────────────────┬─────────────────┘
                                      │
                                      ▼
                            RK4 Integration (4 steps)
                                      │
                                      ▼
                          Update Aircraft State
                                      │
                    ┌─────────────────┴─────────────────┐
                    │                                   │
                    ▼                                   ▼
            Instruments.render()              Renderer.render3D()
                

What's actually in it

Aircraft Module

Holds everything about the plane's current state and aero properties.

  • Position in NED frame (x, y, z)
  • Velocity in body frame (u, v, w)
  • Angular rates (p, q, r)
  • Euler angles (roll, pitch, yaw)
  • Control surfaces (elevator, aileron, rudder)

Flight Dynamics

Where the actual physics happens — this is the part I rewrote the most.

  • Force calculations (lift, drag, thrust)
  • Moment calculations (roll, pitch, yaw)
  • 4th-order Runge-Kutta integration
  • Ground collision detection

Atmosphere Model

A simplified ISA model so air density actually changes with altitude.

  • Temperature, pressure, density
  • Speed of sound calculation
  • Troposphere & stratosphere modeling
  • Altitude-dependent properties

Instruments

The cockpit gauges, drawn frame by frame.

  • Airspeed indicator (0-200 knots)
  • Altimeter (0-10,000 feet)
  • Attitude indicator (artificial horizon)
  • Heading indicator (compass)
  • Vertical speed indicator (±2000 fpm)

Stack


The aero model

This part took the most reading. I pulled together a standard 6-DOF model with linearized stability derivatives — basically the same approach you'd find in an intro flight dynamics textbook (I leaned on Etkin's Dynamics of Flight for a lot of this), scaled down to something I could actually implement in a semester's worth of spare time.

Forces (Body Frame):
X = -D·cos(α) + L·sin(α) + T - mg·sin(θ)
Y = Y_side + mg·sin(φ)·cos(θ)
Z = -D·sin(α) - L·cos(α) + mg·cos(φ)·cos(θ)
Moments (Body Frame):
L_roll = ½ρV²S·b·Cl (Rolling moment)
M_pitch = ½ρV²S·c·Cm (Pitching moment)
N_yaw = ½ρV²S·b·Cn (Yawing moment)
Aerodynamic Coefficients:
CL = CL₀ + CLα·α + CLδe·δe
CD = CD₀ + K·CL²
Cm = Cm₀ + Cmα·α + Cmδe·δe + Cmq·q̂
Cl = Clβ·β + Clδa·δa + Clδr·δr + Clp·p̂
Cn = Cnβ·β + Cnδa·δa + Cnδr·δr + Cnr·r̂

Why RK4

My first pass used plain Euler integration and, predictably, the plane would slowly gain energy out of nowhere and spiral off into space if I left it running for a couple minutes. Switched to 4th-order Runge-Kutta and that drift basically disappeared. It's more math per timestep, but at 60 Hz it's not even close to a bottleneck, and it's what actual flight sim software uses for the same reason.

k₁ = f(t, y)
k₂ = f(t + dt/2, y + k₁·dt/2)
k₃ = f(t + dt/2, y + k₂·dt/2)
k₄ = f(t + dt, y + k₃·dt)

y_new = y + (k₁ + 2k₂ + 2k₃ + k₄)·dt/6

Why it's worth the extra code:


Controls

Keyboard        →    Aircraft Control
──────────────────────────────────────
W/S, ↑/↓        →    Elevator (pitch)
A/D, ←/→        →    Aileron (roll)
Q/E             →    Rudder (yaw)
Z/X, PgUp/PgDn  →    Throttle
Space           →    Center controls
P               →    Pause/Resume
R               →    Reset
ESC             →    Exit
                

Coordinate frames

Two frames of reference get used throughout, and honestly getting the sign conventions consistent between them was one of the more annoying bugs to track down.

NED (North-East-Down)

Inertial reference frame

     North (x)
       ↑
       │
       │
       └────► East (y)
      ╱
     ╱
    ↓ Down (z)
                        

Body Frame

Aircraft-fixed reference

     Forward (x)
       ↑
       │ (nose)
       │
       └────► Right (y)
      ╱
     ╱
    ↓ Down (z)
                        

Going between the two just means building the rotation matrix R(φ,θ,ψ) from the current Euler angles.


Some numbers

                            
#include "aircraft.hpp"
#include "flight_dynamics.hpp"
#include "renderer.hpp"
#include "input_handler.hpp"
#include <chrono>

int main() {
    // Initialize systems
    Renderer renderer;
    Aircraft aircraft;
    FlightDynamics dynamics(&aircraft);
    InputHandler input(&aircraft);
    
    // Main loop at 60 Hz
    const double dt = 1.0 / 60.0;
    double accumulator = 0.0;
    
    auto lastTime = std::chrono::high_resolution_clock::now();
    
    while (!renderer.shouldClose()) {
        auto currentTime = std::chrono::high_resolution_clock::now();
        double frameTime = std::chrono::duration<double>(
            currentTime - lastTime).count();
        lastTime = currentTime;
        
        accumulator += frameTime;
        
        // Physics update with fixed timestep
        while (accumulator >= dt) {
            input.processInput();
            dynamics.update(dt);
            accumulator -= dt;
        }
        
        // Render frame
        renderer.render(aircraft);
    }
    
    return 0;
}
                            
                        

What I'd add next

If I keep working on this, here's the list I keep coming back to:


What I got out of this

This was basically my crash course in:

Honestly the hardest part wasn't any single piece of math — it was getting all the coordinate frames, sign conventions, and unit systems to agree with each other. But watching the plane actually respond correctly to control inputs for the first time, after a lot of debugging sessions that ended with the aircraft spinning into the ground, was a pretty good payoff.