C++ OpenGL/GLUT Solar System Simulation with Lighting and Camera Controls
C++ OpenGL/GLUT programs render to a window created and driven by GLUT’s event loop. OpenGL performs drawing; GLUT supplies cross‑platform windowing, input, and callbacks.
- Install GLUT (Ubuntu/Debian): sudo apt-get update && sudo apt-get install freeglut3 freeglut3-dev
A minimal GLUT program wires display, idle, and keyboard callbacks and starts the loop:
// main.cpp
#include <GL/glut.h>
#include "scene.hpp"
static const int kWinW = 700;
static const int kWinH = 700;
static const int kWinX = 60;
static const int kWinY = 40;
SolarScene gScene;
static void displayCb() { gScene.render(); }
static void idleCb() { gScene.tick(); }
static void keyCb(unsigned char key, int x, int y) { gScene.onKey(key, x, y); }
int main(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
glutInitWindowSize(kWinW, kWinH);
glutInitWindowPosition(kWinX, kWinY);
glutCreateWindow("Solar System (OpenGL/GLUT)");
glutDisplayFunc(displayCb);
glutIdleFunc(idleCb);
glutKeyboardFunc(keyCb);
glutMainLoop();
return 0;
}
Double buffering draws into a hidden back buffer and swaps to the screen in one operation, avoiding flicker. GLUT_DOUBLE enables this; glutSwapBuffers() performs the swap.
OpenGL core matrix usage
- glMatrixMode selects the target matrix stack: GL_PROJECTION (projection), GL_MODELVIEW (model and view), GL_TEXTURE (textures).
- Always glLoadIdentity() before setting up a new transform block.
- Use gluPerspective for perspective frustum and gluLookAt for camera transform.
Common immediate-mode calls
- glEnable(GLenum cap): toggle features such as GL_LIGHTING, GL_LIGHT0, GL_DEPTH_TEST.
- glPushMatrix / glPopMatrix: save/restore the current transform.
- glTranslatef, glRotatef: compose transforms.
- glBegin/glEnd with glVertex*: submit primitives. Example: circle via line loop:
// Draw a 2D circle of radius r
static void drawCircle(float r, int segments) {
glBegin(GL_LINE_LOOP);
for (int i = 0; i < segments; ++i) {
float t = 2.0f * 3.1415926535f * i / segments;
glVertex2f(r * cosf(t), r * sinf(t));
}
glEnd();
}
Object model
- Every body has: radius, orbit radius, orbital period, spin speed, RGBA color, an optional parent body, and two angles (orbit, spin).
- Assumptions: circular orbits, uniform angular velocities, one simulation tick equals one day.
// body.hpp
#ifndef BODY_HPP
#define BODY_HPP
#include <GL/glut.h>
class Body {
public:
Body(GLfloat radius, GLfloat orbitRadius, GLfloat orbitDays, GLfloat spinDegPerTick, Body* parent);
virtual ~Body() {}
virtual void step(GLfloat dt);
virtual void render();
GLfloat color[4];
protected:
GLfloat r_;
GLfloat orbitR_;
GLfloat orbitDegPerDay_; // degrees progressed per day for revolution
GLfloat spinDegPerTick_; // degrees per tick for self-rotation
GLfloat orbitAngle_;
GLfloat spinAngle_;
Body* parent_;
void applyParentTransform() const;
void drawOrbit() const;
void drawSphere() const;
};
class PlanetBody : public Body {
public:
PlanetBody(GLfloat radius, GLfloat orbitRadius, GLfloat orbitDays, GLfloat spinDegPerTick,
Body* parent, const GLfloat rgb[3]);
void render() override; // sets material, then draws
};
class EmissiveBody : public PlanetBody { // e.g., the Sun (light emitter)
public:
EmissiveBody(GLfloat radius, GLfloat spinDegPerTick, const GLfloat rgb[3]);
void render() override; // configures GL_LIGHT0 + emissive material
};
#endif
// body.cpp
#include "body.hpp"
#include <cmath>
static const float kPI = 3.14159265358979323846f;
Body::Body(GLfloat radius, GLfloat orbitRadius, GLfloat orbitDays, GLfloat spinDegPerTick, Body* parent)
: r_(radius), orbitR_(orbitRadius),
orbitDegPerDay_(orbitDays > 0 ? 360.0f / orbitDays : 0.0f),
spinDegPerTick_(spinDegPerTick),
orbitAngle_(0.0f), spinAngle_(0.0f), parent_(parent) {
color[0] = color[1] = color[2] = color[3] = 1.0f;
}
void Body::step(GLfloat dt) {
orbitAngle_ += orbitDegPerDay_ * dt;
spinAngle_ += spinDegPerTick_ * dt;
}
void Body::applyParentTransform() const {
if (parent_ && parent_->orbitR_ > 0.0f) {
glRotatef(parent_->orbitAngle_, 0.0f, 0.0f, 1.0f);
glTranslatef(parent_->orbitR_, 0.0f, 0.0f);
}
}
void Body::drawOrbit() const {
if (orbitR_ <= 0.0f) return;
glColor3f(0.6f, 0.6f, 0.6f);
glBegin(GL_LINE_LOOP);
const int segments = 256;
for (int i = 0; i < segments; ++i) {
float t = 2.0f * kPI * i / segments;
glVertex2f(orbitR_ * std::cos(t), orbitR_ * std::sin(t));
}
glEnd();
}
void Body::drawSphere() const {
glutSolidSphere(r_, 36, 28);
}
void Body::render() {
glPushMatrix();
{
applyParentTransform();
drawOrbit();
glRotatef(orbitAngle_, 0.0f, 0.0f, 1.0f);
glTranslatef(orbitR_, 0.0f, 0.0f);
glRotatef(spinAngle_, 0.0f, 0.0f, 1.0f);
glColor3f(color[0], color[1], color[2]);
drawSphere();
}
glPopMatrix();
}
PlanetBody::PlanetBody(GLfloat radius, GLfloat orbitRadius, GLfloat orbitDays, GLfloat spinDegPerTick,
Body* parent, const GLfloat rgb[3])
: Body(radius, orbitRadius, orbitDays, spinDegPerTick, parent) {
color[0] = rgb[0];
color[1] = rgb[1];
color[2] = rgb[2];
color[3] = 1.0f;
}
void PlanetBody::render() {
// Simple material: subtle ambient/diffuse, modest specular, faint emission
const GLfloat amb[] = {0.1f, 0.1f, 0.1f, 1.0f};
const GLfloat diff[] = {color[0], color[1], color[2], 1.0f};
const GLfloat spec[] = {0.3f, 0.3f, 0.3f, 1.0f};
const GLfloat emi[] = {color[0] * 0.05f, color[1] * 0.05f, color[2] * 0.05f, 1.0f};
const GLfloat shin = 60.0f;
glMaterialfv(GL_FRONT, GL_AMBIENT, amb);
glMaterialfv(GL_FRONT, GL_DIFFUSE, diff);
glMaterialfv(GL_FRONT, GL_SPECULAR, spec);
glMaterialfv(GL_FRONT, GL_EMISSION, emi);
glMaterialf (GL_FRONT, GL_SHININESS, shin);
Body::render();
}
EmissiveBody::EmissiveBody(GLfloat radius, GLfloat spinDegPerTick, const GLfloat rgb[3])
: PlanetBody(radius, 0.0f, 0.0f, spinDegPerTick, nullptr, rgb) {}
void EmissiveBody::render() {
// White point light at the origin (w=1 means positional light)
const GLfloat pos[] = {0.0f, 0.0f, 0.0f, 1.0f};
const GLfloat amb[] = {0.0f, 0.0f, 0.0f, 1.0f};
const GLfloat dif[] = {1.0f, 1.0f, 1.0f, 1.0f};
const GLfloat spc[] = {1.0f, 1.0f, 1.0f, 1.0f};
glLightfv(GL_LIGHT0, GL_POSITION, pos);
glLightfv(GL_LIGHT0, GL_AMBIENT, amb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, dif);
glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
// Strongly emissive material for the star
const GLfloat starEmi[] = {color[0], color[1], color[2], 1.0f};
glMaterialfv(GL_FRONT, GL_EMISSION, starEmi);
Body::render();
}
Scene management and camera
// scene.hpp
#ifndef SCENE_HPP
#define SCENE_HPP
#include <GL/glut.h>
#include "body.hpp"
class SolarScene {
public:
SolarScene();
~SolarScene();
void render();
void tick();
void onKey(unsigned char key, int x, int y);
private:
static const int kCount = 10;
Body* bodies_[kCount];
GLdouble eye_[3];
GLdouble target_[3];
GLdouble up_[3];
};
#endif
// scene.cpp
#include "scene.hpp"
// Camera defaults
static const GLdouble kCamR = 700.0;
static const GLdouble kEye[3] = {0.0, -kCamR, kCamR};
static const GLdouble kAt [3] = {0.0, 0.0, 0.0};
static const GLdouble kUp [3] = {0.0, 0.0, 1.0};
// One tick is one day; spins advance by this many degrees per tick
static const float kTickDays = 1.0f;
static const float kSpinStep = 3.0f;
// Body indices
enum Id { Sun, Mercury, Venus, Earth, Moon, Mars, Jupiter, Saturn, Uranus, Neptune };
// Radii (visual scale units)
#define SUN_R 48.74f
#define MER_R 7.32f
#define VEN_R 18.15f
#define EAR_R 19.13f
#define MOO_R 6.15f
#define MAR_R 10.19f
#define JUP_R 42.90f
#define SAT_R 36.16f
#define URA_R 25.56f
#define NEP_R 24.78f
// Orbit radii
#define MER_ORB 62.06f
#define VEN_ORB 115.56f
#define EAR_ORB 168.00f
#define MOO_ORB 26.01f
#define MAR_ORB 228.00f
#define JUP_ORB 333.40f
#define SAT_ORB 428.10f
#define URA_ORB 848.00f
#define NEP_ORB 949.10f
// Orbital periods (days)
#define MER_DAYS 87.0f
#define VEN_DAYS 225.0f
#define EAR_DAYS 365.0f
#define MOO_DAYS 30.0f
#define MAR_DAYS 687.0f
#define JUP_DAYS 1298.4f
#define SAT_DAYS 3225.6f
#define URA_DAYS 3066.4f
#define NEP_DAYS 6014.8f
#define SET3(a, x, y, z) do { (a)[0]=(x); (a)[1]=(y); (a)[2]=(z); } while(0)
SolarScene::SolarScene() {
eye_[0]=kEye[0]; eye_[1]=kEye[1]; eye_[2]=kEye[2];
target_[0]=kAt[0]; target_[1]=kAt[1]; target_[2]=kAt[2];
up_[0]=kUp[0]; up_[1]=kUp[1]; up_[2]=kUp[2];
GLfloat c[3];
// Sun
SET3(c, 1.0f, 0.9f, 0.3f);
bodies_[Sun] = new EmissiveBody(SUN_R, kSpinStep, c);
// Inner planets
SET3(c, 0.3f, 0.3f, 0.7f);
bodies_[Mercury] = new PlanetBody(MER_R, MER_ORB, MER_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 1.0f, 0.7f, 0.1f);
bodies_[Venus] = new PlanetBody(VEN_R, VEN_ORB, VEN_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 0.2f, 0.8f, 0.3f);
bodies_[Earth] = new PlanetBody(EAR_R, EAR_ORB, EAR_DAYS, kSpinStep, bodies_[Sun], c);
// Moon orbits Earth
SET3(c, 0.9f, 0.9f, 0.8f);
bodies_[Moon] = new PlanetBody(MOO_R, MOO_ORB, MOO_DAYS, kSpinStep, bodies_[Earth], c);
// Outer planets
SET3(c, 1.0f, 0.5f, 0.5f);
bodies_[Mars] = new PlanetBody(MAR_R, MAR_ORB, MAR_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 1.0f, 1.0f, 0.6f);
bodies_[Jupiter] = new PlanetBody(JUP_R, JUP_ORB, JUP_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 0.6f, 0.9f, 0.6f);
bodies_[Saturn] = new PlanetBody(SAT_R, SAT_ORB, SAT_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 0.7f, 0.7f, 0.8f);
bodies_[Uranus] = new PlanetBody(URA_R, URA_ORB, URA_DAYS, kSpinStep, bodies_[Sun], c);
SET3(c, 0.6f, 0.7f, 1.0f);
bodies_[Neptune] = new PlanetBody(NEP_R, NEP_ORB, NEP_DAYS, kSpinStep, bodies_[Sun], c);
}
SolarScene::~SolarScene() {
for (int i = 0; i < kCount; ++i) delete bodies_[i];
}
void SolarScene::render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(0.72f, 0.72f, 0.74f, 1.0f);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(70.0, 1.0, 1.0, 4.0e7);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(eye_[0], eye_[1], eye_[2], target_[0], target_[1], target_[2], up_[0], up_[1], up_[2]);
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
for (int i = 0; i < kCount; ++i) bodies_[i]->render();
glutSwapBuffers();
}
void SolarScene::tick() {
for (int i = 0; i < kCount; ++i) bodies_[i]->step(kTickDays);
glutPostRedisplay();
}
void SolarScene::onKey(unsigned char key, int, int) {
const GLdouble delta = 20.0;
switch (key) {
case 'w': eye_[1] += delta; break; // move camera +Y
case 'x': eye_[1] -= delta; break; // move camera -Y
case 'a': eye_[0] -= delta; break; // move camera -X
case 'd': eye_[0] += delta; break; // move camera +X
case 's': eye_[2] += delta; break; // move camera +Z
case 'S': eye_[2] -= delta; break; // move camera -Z
case 'r':
eye_[0]=kEye[0]; eye_[1]=kEye[1]; eye_[2]=kEye[2];
target_[0]=kAt[0]; target_[1]=kAt[1]; target_[2]=kAt[2];
up_[0]=kUp[0]; up_[1]=kUp[1]; up_[2]=kUp[2];
break;
case 27: // ESC
std::exit(0);
break;
default: break;
}
}
Build system
# Makefile
CXX := g++
CXXFLAGS:= -O2 -std=c++11
LDFLAGS := -lglut -lGLU -lGL
SRC := main.cpp body.cpp scene.cpp
BIN := solarsystem
all: $(BIN)
$(BIN): $(SRC)
$(CXX) $(CXXFLAGS) $(SRC) $(LDFLAGS) -o $@
clean:
rm -f $(BIN) *.o
Run:
make && ./solarsystem
Perspective and lighting recap
- Camera: gluLookAt(eye, target, up) uses nine parameters to position and orient the view.
- Projection: set GL_PROJECTION, identity, gluPerspective(fovY, aspect, zNear, zFar).
- Lighting: enable GL_LIGHTING and at least one light (GL_LIGHT0). Position/diffuse/specular are set with glLightfv; matreials with glMaterialf/glMaterialfv.
- Posiitonal vs directional light: when the w component of position is 1→positional (x,y,z), when 0→directional (infinite) with direction (x,y,z).
Floating-point types in OpenGL immediate mode are commonly expressed with GLfloat.