Real-time OpenGL 4.3 simulation visualizing N-body gravitational dynamics with custom renderer and physics engine. Features Newtonian gravity, elastic collisions, surface interactions, and procedural geometry.
This is learning project meant to help me understand the basics of simulation rendering and physics. Feel free to fork this project and customize it to your liking.
Star the repo if you think this is cool!
A comprehensive implementation demonstrating:
- Modern OpenGL rendering (core profile 4.3+)
- Newtonian gravity simulation with pairwise force calculation
- Elastic collision response with position correction
- Surface collisions with bounce physics and resting states
- Procedural sphere generation via subdivided cube geometry
- Wireframe grid surfaces with configurable density
- Fixed timestep accumulator for deterministic physics
- FPS camera with mouse look and 6DOF movement
- Blinn-Phong shading with emissive light sources
- Gravitational attraction: Pairwise force calculation between all bodies using scaled gravitational constant
- Collision detection: Sphere-sphere and sphere-surface overlap testing
- Collision response:
- Elastic ball-to-ball collisions with momentum conservation
- Position-based penetration resolution to prevent jittering
- Surface bouncing with coefficient of restitution (energy loss)
- Resting state detection to stop micro-bounces
- Force accumulation: Support for gravitational forces, impulses, and external forces
- Euler integration: Position and velocity updates with configurable timestep (dt = 1/60s)
- Distance softening: Prevents singularities when bodies get too close
- Boundary detection: Simulation termination when bodies cross thresholds
- Procedural geometry:
- Spheres with configurable radius and subdivision levels
- Planar surfaces (filled or wireframe grid)
- Dynamic lighting: Single emissive sphere as point light source
- FPS camera:
- Full 6DOF movement (WASD + Space/Ctrl for vertical)
- Mouse look with pitch/yaw control
- Smooth movement with deltaTime scaling
- Real-time metrics: FPS display updated in window title
- Lazy vertex generation: Deferred mesh creation to avoid redundant calculations
- Modular subsystems: Clean separation between Renderer and Physics engines
- Body-Sphere synchronization: Automatic position sync between physics and rendering
- Fixed timestep physics: Deterministic simulation decoupled from variable frame rates
- Vector-based body management: Dynamic body storage with support for mass, forces, and velocities
Three-body system in equilateral triangle:
- Red ball (top): Position (0, 15, -2), Mass 30e11 kg
- Green ball (bottom-right): Position (8.66, 10, -2), Mass 30e11 kg
- Blue ball (bottom-left): Position (-8.66, 10, -2), Mass 30e11 kg
- Light source: Static emissive white sphere
- Ground surface: Wireframe grid at y = -2.0
All spheres have radius 0.5 units and start at rest. Gravitational constant scaled to 0.1 for visible interactions at human timescales.
include/
application.h # Main app loop, setup, coordination
body.h # Body struct + BodyConfig
settings.h # Global constants (screen size, FOV)
Renderer/
renderer.h # Render loop, sphere/surface drawing
camera.h # FPS camera with mouse input
shader.h # Shader compilation/uniform helpers
mesh.h # Sphere/Surface structs, Mesh container
Sphere3D.h # Procedural sphere generation
Surface3D.h # Planar surface/wireframe grid
Physics/
physics.h # Physics engine (integration, collision)
src/
main.cpp # Entry point
glad.c # OpenGL loader
Renderer/
renderer.cpp
camera.cpp
shader.cpp
Sphere3D.cpp
Surface3D.cpp
Physics/
physics.cpp
shaders/
vObj.glsl # Vertex shader (MVP transform)
fObj.glsl # Fragment shader (Blinn-Phong)
config.h.in → build/config.h # CMake-generated paths
CMakeLists.txt
LICENSE
sudo apt install build-essential cmake pkg-config libglfw3-dev libglm-devmkdir -p build
cmake -S . -B build
cmake --build build -j$(nproc)
./build/ThreeBodyProblem| Input | Action |
|---|---|
| Mouse | Camera look (pitch/yaw) |
| W / S | Move forward / backward |
| A / D | Strafe left / right |
| Space | Move up |
| Left Ctrl | Move down |
| ESC | Quit application |
Pairwise gravitational forces using Newton's law:
F = G * (m₁ * m₂) / r²- Scaled gravitational constant (G = 0.1) for visible effects at simulation scale
- Distance softening (minimum r² = 1.0) prevents infinite forces at close range
- Forces calculated once per pair using
j = i + 1loop structure - Force accumulators reset each frame, accumulated during pair iteration
Ball-to-ball collisions:
- Elastic collision using momentum/energy conservation formulas
- Position-based penetration correction pushes spheres apart
- Prevents jittering by separating overlapping bodies immediately
Ball-to-surface collisions:
- Velocity reflection in y-axis only (horizontal motion preserved)
- Coefficient of restitution = 0.8 (20% energy loss per bounce)
- Resting threshold: velocities < 0.1 m/s set to zero
- Position clamping prevents sinking through surface
Fixed timestep accumulator pattern:
accumulator += frameTime;
while (accumulator >= dt) {
// Reset forces
// Calculate pairwise gravity
// Update velocities and positions (Euler integration)
// Check surface/sphere collisions
// Apply collision responses
accumulator -= dt;
}
renderer.RenderFrame();Key parameters:
- dt: 1/60 seconds (60 Hz physics)
- Speed multiplier: 3.0x (affects position updates only)
- Velocity decay: Disabled (λ = 0.0)
Spheres use deferred generation:
- Default radius = -1.0 (sentinel value)
- Vertex generation skipped if radius < 0
setRadius()triggers one-time mesh generation- Result cached in VAO/VBO for subsequent frames
Surface3D dual-mode rendering:
- Filled mode: Single quad (2 triangles, 4 vertices)
- Wireframe mode: Grid mesh (configurable rows × cols, GL_LINES primitive)
- Grid density controlled via
setGridDensity(rows, cols) - Orientation support: XY, XZ, YZ planes
- Single light source (one emissive sphere)
- Euler integration (first-order accuracy, potential energy drift)
- O(n²) collision/gravity (all pairs checked every frame)
- Derived normals (spheres use normalized position; surfaces lack explicit normals)
- No spatial partitioning (broadphase optimization needed for 100+ bodies)
- No trajectory visualization (motion history not recorded)
Balls stopped moving unexpectedly:
- Fixed
isZero()modifying vectors → now read-only check - Moved boundary check outside inner loops
- Fixed
ivsjtypo in gravity loop
Balls sank through surface:
- Added position clamping:
body.Position.y = surfaceY + radius - Implemented resting threshold to stop micro-bounces
Runaway velocities/explosion:
- Added distance softening:
r² = max(r², 1.0)prevents division by near-zero - Reduced scaled G from 10.0 → 0.1 to compensate for speed multiplier
- Limited gravity calculation to non-light-source bodies
Balls merged and jittered:
- Position correction in collision response pushes spheres apart
- Prevents stuck-together oscillation
- ~2,000 lines of code (excluding third-party libraries)
- 60 FPS target sustained with 4 bodies (3 dynamic + 1 light)
- Physics: 60 Hz fixed timestep, <0.1ms per frame
- Rendering: ~15ms per frame (includes vsync wait)
- Tested configurations:
- 3 spheres (radius 0.5, 6 subdivision levels) + wireframe grid
- Grid density: 20×20 (400 vertices, 760 line segments)
- Gravitational force accumulation (Newton's law)
- Elastic collision response with momentum conservation
- Surface collision with bounce physics
- Position-based penetration correction
- Distance softening for numerical stability
- Resting state detection
Blank screen?
- Check OpenGL 4.3 support:
glxinfo | grep "OpenGL version" - Verify shader compilation (check terminal output)
Mouse snaps to different angle on startup?
- Fixed via firstMouse flag (ignores initial callback)
Bodies don't move?
- Check if
isZero()is modifying velocities (should be read-only) - Ensure gravity loop uses
j = i + 1(notj = 0)
Balls sink through surface?
- Position clamping should be:
body.Position.y = surfaceY + radius(not<comparison) - Surface collision must reverse y-velocity:
body.Velocity.y *= -0.8f
Balls explode/fly off screen?
- Check distance softening:
fDistanceSq = max(fDistanceSq, 1.0f) - Verify scaled G is reasonable (0.01 to 1.0 for typical masses)
- Ensure light source is skipped in gravity calculation
Wireframe not showing?
- Call
surface.setWireframe(true)beforedrawSurface() - Check
surface.mesh.isWireframeflag propagates to renderer
GNU General Public License v3.0 — see LICENSE
- GLFW — windowing and input handling
- GLAD — OpenGL function loader
- GLM — mathematics library
- Lines of Code: ~1,600 (excluding libraries)
- Files: 10+ source/header files
- Architecture: Modular (Renderer + Physics subsystems)
- Language: C++17
- Graphics API: OpenGL 4.3 Core Profile