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...
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.
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;
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;
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.
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);
}
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:
- 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
Nice project!