Skip to content

Instantly share code, notes, and snippets.

@dmh43
Last active September 28, 2022 05:38
Show Gist options
  • Save dmh43/eb251614c11aad1ba286 to your computer and use it in GitHub Desktop.
Save dmh43/eb251614c11aad1ba286 to your computer and use it in GitHub Desktop.
A simple physics simulator with OpenGL
#include <GL/freeglut.h>
#include <vector>
#include <cmath>
const double pi = 3.141593;
typedef struct PhysVector {
float x;
float y;
};
typedef PhysVector Coords;
typedef PhysVector Velocity;
class Planet {
public:
float mass;
Coords loc;
Velocity v;
};
const Coords origin = {0, 0};
const Coords stopped = {0, 0};
void physicsLoop(int);
void display(void);
void mouseHandler(int, int, int, int);
void mouseMotionHandler(int, int);
void newPlanet(float);
int mouse_x_pos, mouse_y_pos;
bool left_click = false;
std::vector<Planet> solarsystem;
void display(void) {
glClear(GL_COLOR_BUFFER_BIT); // blit blank display
for (int i = 0; i < solarsystem.size(); i++){ // draw a circle
Planet &planet = solarsystem[i];
glBegin(GL_POLYGON);
for(float arc = 0; arc < 2*pi; arc+=0.5){
glVertex2f(cos(arc) + planet.loc.x, sin(arc) + planet.loc.y);
}
glEnd();
}
glFlush();
glutSwapBuffers();
}
void mouseHandler(int button, int state, int x, int y) {
mouseMotionHandler(x, y);
if (state == GLUT_DOWN) {
if (button == GLUT_LEFT_BUTTON) {
left_click = true;
return;
}
}
}
void mouseMotionHandler(int x, int y) {
mouse_x_pos = x;
mouse_y_pos = y;
}
void newPlanet(float m) {
Planet planet;
planet.loc.x = mouse_x_pos;
planet.loc.y = mouse_y_pos;
planet.v = stopped;
planet.mass = m;
solarsystem.push_back(planet);
}
void physicsLoop(int val) {
display();
if(left_click) {
newPlanet(50.0f);
left_click = false;
}
for (int i = 0; i < solarsystem.size(); i++) {
Planet &planet = solarsystem[i];
for (int j = 0; j < solarsystem.size(); j++) {
if (i == j) {
continue;
}
const Planet &other_planet = solarsystem[j];
float dist = sqrt((other_planet.loc.x - planet.loc.x) * (other_planet.loc.x - planet.loc.x) +
(other_planet.loc.y - planet.loc.y) * (other_planet.loc.y - planet.loc.y));
if (dist < 3) {
planet.v.x = ((planet.v.x * planet.mass + other_planet.v.x * other_planet.mass)
/ (planet.mass + other_planet.mass)); //perfectly inelastic collision
planet.v.y = ((planet.v.y * planet.mass + other_planet.v.y * other_planet.mass)
/ (planet.mass + other_planet.mass));
planet.mass += other_planet.mass;
solarsystem.erase(solarsystem.begin()+j); // delete absorbred planet
}
else {
// add component of accel due to gravity in each coordinate
planet.v.x += 0.001*(other_planet.mass / dist * (other_planet.loc.x - planet.loc.x));
planet.v.y += 0.001*(other_planet.mass / dist * (other_planet.loc.y - planet.loc.y));
}
}
planet.loc.x += planet.v.x; //increment position
planet.loc.y += planet.v.y;
}
glutTimerFunc(1, physicsLoop, 0);
}
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE);
glutInitWindowSize(320, 240);
glutCreateWindow("C(++)olar System");
glOrtho(0, 320.0, 240.0, 0, 0, 1); // Orient and define grid
glutDisplayFunc(display);
glutMouseFunc(mouseHandler);
glutMotionFunc(mouseMotionHandler);
physicsLoop(0);
glutMainLoop();
return 0;
}

C(++)olar System

So, I decided to dabble a bit in interfacing OpenGL with C++. I've done a bit of work with Python and PyGame and figured that a lot of those concepts would apply here as well, but with less abstract functions available. Overall the code came out to be pretty simple to implement, and with Flycheck, gdb and the (really helpful in comparison to gcc with C) feedback from g++, it was actually somewhat of a breeze. Highly recommended exercise.

Oh, and since I have a terrible sense of humor, I've named this little project, C(++)olar System. Which arguably is better than Solar Cystem. I didn't realize, until it was too late, that this isn't a solar system since the objects you draw on the screen are meant to be planets and not stars...

Some Planning

All I wanted from this first version of C(++)olar System, was to be able to place dots, which represent planets, in a window, and have them attract each other in a way analogous to the way gravity does between objects.

I quickly realized that I'd also have to decide what to do when planets collide. I figured it would be simple, and still interesting, if all collisions between plannets were perfectly inelastic. So, in this way, planets would gain mass over time, and accordingly, increase their gravitational pull on other planets.

Working with vectors

In order to easily do vector algebra, I created a simple PhysVector struct containing an x value and a y value. The Coords and Velocity structs inherit from it to define the position and velocity of each planet. I also define origin ad stopped to represent the bottom left corner of the screen and the velocity of a planet which is not moving with respect to the OpenGL reference frame.

I could have simplified the code further by implementing PhysVector as a class which overload the arithmetic operations, but figured that was overkill for what I was doing. There's probably a library out there for this kind of thing, but Googling for anything containg the terms 'vector' and 'C++' is difficult because of the vector feature of C++.

typedef struct PhysVector {
  float x;
  float y;
};

typedef PhysVector Coords;

typedef PhysVector Velocity;

Creating a "Solar System"

To keep track of planet data, we'll make a Planet class which gives us the mass, position and velocity of a planet.

class Planet {
public:
  float mass;
  Coords loc;
  Velocity v;
};

These planets will be stored in a global vector called solarsystem

std::vector<Planet> solarsystem;

Dive into OpenGL

At this point, I looked into what steps I had to take to work with OpenGL. There's a lot of information about OpenGL out there. After skimming through a few tutorials about how to draw shapes and respond to mouse movemets and clicks, I wrote a few of the helper functions.

void mouseHandler(int button, int state, int x, int y) {
  mouseMotionHandler(x, y);

  if (state == GLUT_DOWN) {
    if (button == GLUT_LEFT_BUTTON) {
    left_click = true;
    return;
    }
  }
}

void mouseMotionHandler(int x, int y) {
  mouse_x_pos = x;
  mouse_y_pos = y;
}

These are passed into their respective GLUT functions to register our code with the mouse interactions with the windowing system.

int main(int argc, char **argv) {
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE);
  glutInitWindowSize(320, 240);
  glutCreateWindow("C(++)olar System");
  glOrtho(0, 320.0, 240.0, 0, 0, 1); // Orient and define grid
  glutDisplayFunc(display);
  glutMouseFunc(mouseHandler);
  glutMotionFunc(mouseMotionHandler);

  physicsLoop(0);

  glutMainLoop();
  return 0;
}

Here we initialize our GLUT environment and call our physics loop and then the main GLUT event handling loop, glutMainLoop. We call glutInitDisplayMode with GLUT_DOUBLE to initialize a double buffered display. This allows us to draw all the objects onto a second, hidden buffer, and then latch them all at once to the currently displayed buffer.

glOrtho allows us to define our orthogonal plane coordinates with respect to the window coordinates. I like to think of the bottom left corner of the window as (0, 0), so I set the top and right edges to 240 and 320, respectively.

The display loop that we register with glutDisplayFunc simply iterates through the list of planets, and draws each one as a circle. This might be indistinguishable from a circle on your display, but my monitor has a pretty low resolution, so what looks good to me may not look good to you. Feel free to tweak any of these values for your own display.

void display(void) {
  glClear(GL_COLOR_BUFFER_BIT); // blit blank display

  for (int i = 0; i < solarsystem.size(); i++){ // draw a circle
    Planet &planet = solarsystem[i];
    glBegin(GL_POLYGON);
    for(float arc = 0; arc < 2*pi; arc+=0.5){
      glVertex2f(cos(arc) + planet.loc.x, sin(arc) + planet.loc.y);
    }
    glEnd();
  }

  glFlush();
  glutSwapBuffers();
}

Each planet is drawn inside a glBegin call which lets us draw OpenGL graphics. In this case, the graphic is a GL_POLYGON. Once all the planets are done, the graphics flushed to the buffer, ensuring that drawing is complete before swapping the buffers and displaying the next frame.

Enter: Newton

A simple way to calculate the gravity between planets is to look at every planet, calculate the distance between that planet and every other planet, if a collision has occured then merge the two planets (assuming a perfectly inelastic collision), calculate the acceleration of that planet and the change in velocity.

We want this calculation to occur regularly, so we register it as a OpenGL timer function and schedule the gravity calculation to be called within the next milisecond with glutTimerFunc(1, physicsLoop,0);.

physicsLoop takes a int argument because the glutTimerFunc expects the callback function to receive a single int argument which specifies whether the callback should be canceled; this is done by passing a non-zero value to the third argument of glutTimerFunc. Since we are not using this feature, I write a 0 each time.

void physicsLoop(int val) {
  display();

  if(left_click) {
      newPlanet(50.0f);
       left_click = false;
    }

  for (int i = 0; i < solarsystem.size(); i++) {
      Planet &planet = solarsystem[i];
      for (int j = 0; j < solarsystem.size(); j++) {
        if (i == j) {
          continue;
        }
          const Planet &other_planet = solarsystem[j];

          float dist = sqrt((other_planet.loc.x - planet.loc.x) * (other_planet.loc.x - planet.loc.x) +
                            (other_planet.loc.y - planet.loc.y) * (other_planet.loc.y - planet.loc.y));
          if (dist < 3) {
            planet.v.x = ((planet.v.x * planet.mass + other_planet.v.x * other_planet.mass)
                          / (planet.mass + other_planet.mass)); //perfectly inelastic collision
            planet.v.y = ((planet.v.y * planet.mass + other_planet.v.y * other_planet.mass)
                          / (planet.mass + other_planet.mass));
            planet.mass += other_planet.mass;
            solarsystem.erase(solarsystem.begin()+j); // delete absorbred planet
          }
          else {
            // add component of accel due to gravity in each coordinate
            planet.v.x += 0.001*(other_planet.mass / dist * (other_planet.loc.x - planet.loc.x));
            planet.v.y += 0.001*(other_planet.mass / dist * (other_planet.loc.y - planet.loc.y));
           }
      }
      planet.loc.x += planet.v.x; //increment position
      planet.loc.y += planet.v.y;
  }

  glutTimerFunc(1, physicsLoop, 0);
}

That's it!

The rest should be pretty self-explanitory. hopefully this helped you get started writing OpenGL apps of your own. I'll post back here with updates. For now, here is a list of ideas I'd like to implement in the future:

Ideas to Implement

  • Increase size of planet based on mass
  • Pause simulation to place planet
  • Allow planets to be placed with an initial velocity
  • Support for colored planets or animated sprites
  • Scroll bar to select mass of new planets
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment