Graphical solar system simulator using Java-based Processing

We’ll be writing our code using Processing. If you don’t know Processing, do check it out. It’s a super-simple C-like language, especially handy if you want to create quick-and-easy graphical apps for desktop, web or Android. If you already know C, you might understand the basics of the language just by reading this tutorial.

I should put a disclaimer here, though. The program we’ll be developing would not be scientifically accurate. You won’t be using it to launch any rockets.

As a matter of fact, there are only two things about physics that you need to know – the planets move in elliptical orbit around the sun and the orbits of the planets are at an inclination with respect to each other.

Before we start, we need to import the date module to Processing. This is necessary to obtain the current date and time from the system and perform calculations related to time.

import java.util.Date;

We need three values to draw the orbits, the eccentricity of the ellipse of the orbit, the semi-major axis (aphelion, in case of the planets in our solar system) of the ellipse and the inclination of the orbits. I got the values here. In Processing, we declare three arrays of floating point numbers to hold the values. I started every array with a zero to keep the index number of the array consistent with the numbers we have for the planets. And I included Pluto, although technically it’s no longer a planet, because of the very amazing orbit it has. It has the highest eccentricity and inclination in the list.

float eccentricity[] = {0,0.205,0.007,0.017,0.094,0.049,0.057,0.046,0.011,0.244};
float aphelion[] = {0.0,69.8,108.9,152.1,249.2,816.6,1514.5,3003.6,4545.7,7304.3};
float inclination[] = {0.0,7.0,3.4,0.0,1.9,1.3,2.5,0.8,1.8,17.2};

For obvious reasons, we won’t be drawing our planets to scale. Frankly, a realistically scaled model would be highly impractical. The diameter of the sun is 103 times that of Earth, for instance. We make up body size for our planets. We’ll be needing the orbital period and epoch of the planets to plot their position. The orbital period is the time needed for the planet to complete one orbit around the sun. Epoch is the time-stamp of the time (duh!) when the planets were in their last aphelion. We declare these values in arrays, as well.

float bodyRadii[] = {0.0,0.5,0.7,0.7,0.6,1.7,1.5,3,3,2};
float orbitPeriod[] = {0.0,126720.0,323568.0,525888.0,989280.0,6380640.0,15475680.0,44048160.0,86112000.0,130446720.0};
float epoch[]={0.0,13627665.75,13630579.0,13922899.58,14241140.2,18535221.46,25301781.93,20140822.57,74731252.68,75518933.97};

We then declare three more arrays. They contain the red, green and blue values for the color of the planets. We’ll use these randomly. In a more sophisticated version, we might use textures instead of solid colors.

int bodyColorR[] = {255,198,206,192,255,255,255,41,41,163};
int bodyColorG[] = {255,198,21,98,163,234,134,100,100,164};
int bodyColorB[] = {255,198,21,255,41,41,41,255,255,165};

In order to view the scene, we’ll be creating a pseudo-camera. We’ll do this using a combination of scaling and rotating. We’ll need several variables to hold the angle of rotations and the amount of scaling. We also have a variable to indicate the planet on which we are centered.

float camx = 0, camy = 0, camz = 0, scale = 1.0;
int center = 0;

Then we’ll declare the three values that we’ll be using to track the time of the application. The application will start with the system time as the start time, but we would be able to change the rate and direction of time flow, i.e. we would be able to view the planets and position of the stars at a future or past time. The currentTime is the time of which the positions are shown. The rate of flow of time is also required and is stored in timeRate. We also track if the app is running in the current system time or at different time in the past or future using fastForward.

long currentTime;
int timeRate = 0;
int fastForward = 0;

Finally, we’ll put some variables for some extra features. We will write the string stored in alert using the font f for five seconds since the time pointed to by startTime. We also have a variable orbit that we’ll use to toggle the plotting of the orbits between on and off.

PFont f;
int orbit = 1;
String alert = "Solar System Simulator";
long startTime;
int projection;

We have finished declaring our global variables. I told you Processing is like C. If you were programming using C, everything until now would have been exactly the same. But here is a minor difference between Processing and C. You all know how C program starts executing from the main() function, right? In Processing, there is no main function. What we have instead is two functions – setup() and draw(). The setup() function runs once when the program is starting up and the draw() function forever runs in a loop. If you’ve ever programmed an Arduino, you may have noticed the similarities. This is not a coincidence – Arduino is based off Processing.

First, we setup the application by specifying the size of the window we want and the type of renderer we need (P3D means that we want to use a 3D renderer). Then we specify the color of the stroke. 255 means white, in grayscale. After that, we initialize the font f to be of 14 size of Arial. Finally, we set the startTime to the current time of the application.

void setup() {
  size(832,624,P3D);
  stroke(255);
  f = createFont("Arial",14,true);
  textFont(f,14);
  Date d = new Date();
  startTime = d.getTime()/1000;
}

We then declare the continuously looping draw function. First, we call the incrTime function to change the currentTime value. We’ll get to that later. We draw the black background next. This is because space is dark and lonely. And then we write the string given by alert, if required, using the drawString() function call. Next we shift (translate) the origin from the top-left of the screen to the center of the screen. After that, we set the pseudo-camera by rotating in x, y and z-axis according to values of camx, camy and camz, respectively and then zooming according to our scale. Once that’s done, we call the focus() function to center on the body pointed to by the center variable. Then we draw a sphere of radius 10 of color white. This would be our sun. Finally, we use a for loop, looping once for each planet, to rotate the orbit by the amount of it’s inclination, plot the orbit around the sun and plot the body in it’s orbit at a position according to current time. We declare these inside a pushmatrix-popmatrix to push all our transformations of the coordinate systems into a matrix stack at the beginning of the transformation and restore the previous coordinate system at the end.

void draw() {
  incrTime();
  background(0, 0, 0); // We clear the background to black before drawing anything
  drawString();
  if (projection==0) perspective(PI/3.0, float(width)/float(height), 5, 50000); 
  else ortho(0, width, 0, height);

  // Set the pseudo-camera
  scale(scale);
  rotateX(camx);
  rotateY(camy);
  rotateZ(camz);
  focus(center);

  // We draw the sun at the center in white
  fill(255,255,255); 
  sphere(15);

  // This loop runs once for each planet in the array.
  for(int planet=1;planet<10;planet++) {
    pushMatrix();
    rotateY(radians(inclination[planet])); // This rotates the orbits by the required inclination
    if(orbit==1) plotOrbit(planet); // This function plots the orbit of each planet, if permitted by 'orbit'
    plotBody(planet); // This function plots the body around the body
    popMatrix();
  }
}

The drawString function is very simple. It is called once each time the program runs through the loop draw(). It checks if the time difference between variable startTime and current time is less than five seconds. If so, then it plots the string stored in alert into the left bottom corner of the screen.

void drawString() {
  Date d = new Date();
  long x = d.getTime()/1000;
  if((x-startTime)<5) {
    fill(bodyColorR[center],bodyColorG[center],bodyColorB[center]);
    text(alert,10,610);
  }
}

If we want to display a string, we call the setString function with the string to be displayed as a parameter. The passed string is stored in alert and the startTime is updated to current system time. So, when the program will loop in the draw() function, the drawString() function will plot the string for five seconds.

void setString(String str) {
  alert = str;
  Date d = new Date();
  startTime = d.getTime()/1000;
}

The incrTime function is used to set the currentTime of the application. If the rate of time flow is 0 (zero) and the time is not changed (fastForward is set to 0), we simply set the current time of the computer as the current time of the application. If the rate of time flow is positive or negative, we add or subtract, respectively, the value given by 10 to the power timeRate. Implementing the function is pretty straightforward.

void incrTime() {
  if(timeRate==0) {
    if(fastForward==0) {
      Date d = new Date();
      long x = d.getTime()/1000;
      currentTime = x/60;
    }
  } else if(timeRate>0) {
      currentTime+=pow(2,timeRate);
  } else if(timeRate<0) {
      currentTime-=pow(2,timeRate*(-1));
  }
}

Now, we need to draw the orbits of the planets. An orbit of a planet is essentially an ellipse with the sun at one of the focii. This may sound complex, but that’s simply because it’s actually complex. Thankfully, we don’t need any derivation or proofs here. The equation to draw an ellipse (in polar coordinate) with the focii at the origin is given by r(θ) = (a × (1-e²))/(1±(e × cosθ)), where a is the semi-major axis and e is the eccentricity. We find the value of r at close discrete value of θ between 0 and 2 × π. We can easily get the x and y-axis values in our good ol’ cartesian coordinates using x = r × cos(θ) and y = r × sin(θ). We use these formulas to draw our orbits using the plotOrbit function. It takes an integer as a parameter, which corresponds to the planet for which it’s drawing the orbit.

void plotOrbit(int id) {
    float circumference = 2*PI;
    float a = aphelion[id];
    float e = eccentricity[id];
    float prevx = -1;
    float prevy = -1;
    for(float theta=0.0;theta<circumference;theta+=0.01) {
      float r = (a*(1-(e*e)))/(1-(e*cos(theta)));
      float x = r*cos(theta);
      float y = r*sin(theta);
      if(prevx == -1 && prevy == -1) {
        prevx = x;
        prevy = y;
      } else {
        line(prevx,prevy,x,y);
        prevx = x;
        prevy = y;
      }
    }
}

Now that we are done plotting the orbits of the planets, we should plot the planets in their correct position in their orbit using the current time to find the position of the planet. We use the formula, θ = ((2 × π)/ orbitPeriod) × (currentTime–epoch), to find the θ at the current time of the application. Then, by using the three formulas mentioned in the last paragraph, we find the r, and then subsequently, x and y values for the planets. After that, we draw a sphere at the point (x,y) of radius given by bodyRadii*4 (to scale it up to be visible) and colors given by the three color arrays. All these are done in the following plotBody function.

void plotBody(int id) {
        float timeDiff = currentTime - epoch[id];
        if(timeDiff<0) timeDiff-=orbitPeriod[id];        
        float i = ((2*PI)/orbitPeriod[id])*timeDiff;
        float circumference = 2*PI;
        float a = aphelion[id];
        float e = eccentricity[id];
        float r = (a*(1-(e*e)))/(1-(e*cos(i)));
        float x = r*cos(i);
        float y = r*sin(i);

        pushMatrix();
          translate(x, y, 0);
          noStroke();          
          fill(bodyColorR[id],bodyColorG[id],bodyColorB[id]);
          sphere(bodyRadii[id]*4);
          stroke(255);
        popMatrix();
}

The focus() function acts in a similar fashion. We translate the scene to bring the planet we want to the center of the screen and then rotate the scene along the z-axis by the angle given by the inclination of the planet’s orbit. Of course, if we want to center on the sun, then we don’t need to do anything. The function is pretty straightforward.

void focus(int id) {
  if(id==0) return;
  else {
        float timeDiff = currentTime - epoch[id];
        if(timeDiff<0) timeDiff-=orbitPeriod[id];        
        float i = ((2*PI)/orbitPeriod[id])*timeDiff;
        float circumference = 2*PI;
        float a = aphelion[id];
        float e = eccentricity[id];
        float r = (a*(1-(e*e)))/(1-(e*cos(i)));
        float x = r*cos(i);
        float y = r*sin(i);
        translate(x,y,0);
        rotateY(radians(inclination[id]));
        rotateZ(PI);
  }
}

By this time, you should be able to run the application and be presented with the application window. You should be able to see the Sun at the center and the four terrestrial planets around it. But we don’t want this, do we? We want to fly around in space and time. Don’t we all fantasize about controlling all of space and time?

The flying around in space is fairly simple. We just have to change the value of cam-x, cam-y, and scale. When we change these values, we rotate and change scale of the entire 3D scene. Manipulation of time is simple too. We already saw how the timeRate and fastForward allows us to modify currentTime, i.e. the time of which the positions are being shown. We need to allow ourselves to change these values during run-time using the keyboard.

We use the keyPressed() function of Processing to implement our keyboard event response code. This function is automatically called by Processing whenever a key is pressed on the keyboard. The key that is pressed is stored in the key variable, or, in case of non-ASCII keys, the keyCode variable.

void keyPressed() {
  if (key == 'C' || key == 'c') {
    center = (center+1)%10;
    key = (char)(center+'0');
  }

  if (key == 'A' || key == 'a') camz-=0.01;
  else if (key == 'D' || key == 'd') camz+=0.01;
  else if (key == 'W' || key == 'w') scale+=0.025;
  else if (key == 'S' || key == 's') {
    if(scale>0.05) scale-=0.025;
  }
  else if (key == 'O' || key == 'o') {
    if(orbit==1) orbit=0;
    else orbit=1;
  }
  else if (key == 'P' || key == 'p') {
    if (projection == 1) {
      projection=0;
      setString("Perspective projection");
    }
    else {
      projection=1;
      setString("Orthographic projection");
    }
  }
  else if (key == 'R' || key == 'r') {
    center = 0;
    timeRate = 0;
    fastForward = 0;
  }
  else if (key == 'M' || key == 'm') {
    if(timeRate<15) { timeRate++; fastForward = 1; } } else if (key == 'N' || key == 'n') { if(timeRate>-15) {
      timeRate--;
      fastForward = 1;
    }
  }
  else if (key == CODED) {
    if (keyCode == UP) camx+=0.01;
    else if (keyCode == DOWN) camx-=0.01;
    else if (keyCode == LEFT) camy-=0.01;
    else if (keyCode == RIGHT) camy+=0.01;
  }
  else if ((key-'0') < 10) { // Numerical
    int value = key - '0';
    center = value;
    if(value==0) setString("FOCUS: Sun");
    else if(value==1) setString("FOCUS: Mercury");
    else if(value==2) setString("FOCUS: Venus");
    else if(value==3) setString("FOCUS: Earth");
    else if(value==4) setString("FOCUS: Mars");
    else if(value==5) setString("FOCUS: Jupiter");
    else if(value==6) setString("FOCUS: Saturn");
    else if(value==7) setString("FOCUS: Uranus");
    else if(value==8) setString("FOCUS: Neptune");
    else if(value==9) setString("FOCUS: Pluto");
  }
}

And with that, our application is finally complete. We can now combine all the codes we have discussed till now and put them together into a single file. If we copy the following code into the Processing IDE and run the sketch, we would see an application window opening up which displays the app shown in the video.

import java.util.Date;
float eccentricity[] = {0,0.205,0.007,0.017,0.094,0.049,0.057,0.046,0.011,0.244};
float aphelion[] = {0.0,69.8,108.9,152.1,249.2,816.6,1514.5,3003.6,4545.7,7304.3};
float inclination[] = {0.0,7.0,3.4,0.0,1.9,1.3,2.5,0.8,1.8,17.2};
float bodyRadii[] = {0.0,0.5,0.7,0.7,0.6,1.7,1.5,3,3,2};
float orbitPeriod[] = {0.0,126720.0,323568.0,525888.0,989280.0,6380640.0,15475680.0,44048160.0,86112000.0,130446720.0};
float epoch[]={0.0,13627665.75,13630579.0,13922899.58,14241140.2,18535221.46,25301781.93,20140822.57,74731252.68,75518933.97};
int bodyColorR[] = {255,198,206,192,255,255,255,41,41,163};
int bodyColorG[] = {255,198,21,98,163,234,134,100,100,164};
int bodyColorB[] = {255,198,21,255,41,41,41,255,255,165};
float camx = 0, camy = 0, camz = 0, scale = 1.0;
int center = 0;
long currentTime;
int timeRate = 0;
int fastForward = 0;
PFont f;
int orbit = 1;
String alert = "Solar System Simulator";
long startTime;
int projection;
void setup() {
  size(832,624,P3D);
  stroke(255);
  f = createFont("Arial",14,true);
  textFont(f,14);
  Date d = new Date();
  startTime = d.getTime()/1000+5;
}
void draw() {
  incrTime();
  
  background(0, 0, 0); // We clear the background to black before drawing anything
  drawString();
  if (projection==0) perspective(PI/3.0, float(width)/float(height), 5, 50000); 
  else ortho(0, width, 0, height);
  translate(width/2, height/2, 0); // We bring the origin of drawing area to the center of screen  
 
  // Set the pseudo-camera
  scale(scale);
  rotateX(camx);
  rotateY(camy);
  rotateZ(camz);
  focus(center);
 
  // We draw the sun at the center in white
  fill(255,255,255); 
  sphere(15);
 
  // This loop runs once for each planet in the array.
  for(int planet=1;planet<10;planet++) {
    pushMatrix();
    rotateY(radians(inclination[planet])); // This rotates the orbits by the required inclination
    if(orbit==1) plotOrbit(planet); // This function plots the orbit of each planet, if permitted by 'orbit'
    plotBody(planet); // This function plots the body around the body
    popMatrix();
  }
}
void drawString() {
  Date d = new Date();
  long x = d.getTime()/1000;
  if((x-startTime)<5) { fill(bodyColorR[center],bodyColorG[center],bodyColorB[center]); text(alert,10,610); } } void setString(String str) { alert = str; Date d = new Date(); startTime = d.getTime()/1000; } void incrTime() { if(timeRate==0) { if(fastForward==0) { Date d = new Date(); long x = d.getTime()/1000; currentTime = x/60; } } else if(timeRate>0) {
      currentTime+=pow(2,timeRate);
  } else if(timeRate<0) {
      currentTime-=pow(2,timeRate*(-1));
  }
}
void plotOrbit(int id) {
    float circumference = 2*PI;
    float a = aphelion[id];
    float e = eccentricity[id];
    float prevx = -1;
    float prevy = -1;
    for(float theta=0.0;theta<circumference;theta+=0.01) {
      float r = (a*(1-(e*e)))/(1-(e*cos(theta)));
      float x = r*cos(theta);
      float y = r*sin(theta);
      if(prevx == -1 && prevy == -1) {
        prevx = x;
        prevy = y;
      } else {
        line(prevx,prevy,x,y);
        prevx = x;
        prevy = y;
      }
    }
}
void plotBody(int id) {
        float timeDiff = currentTime - epoch[id];
        if(timeDiff<0) timeDiff-=orbitPeriod[id];        
        float i = ((2*PI)/orbitPeriod[id])*timeDiff;
        float circumference = 2*PI;
        float a = aphelion[id];
        float e = eccentricity[id];
        float r = (a*(1-(e*e)))/(1-(e*cos(i)));
        float x = r*cos(i);
        float y = r*sin(i);
 
        pushMatrix();
          translate(x, y, 0);
          noStroke();          
          fill(bodyColorR[id],bodyColorG[id],bodyColorB[id]);
          sphere(bodyRadii[id]*4);
          stroke(255);
        popMatrix();
}
void focus(int id) {
  if(id==0) return;
  else {
        float timeDiff = currentTime - epoch[id];
        if(timeDiff<0) timeDiff-=orbitPeriod[id]; float i = ((2*PI)/orbitPeriod[id])*timeDiff; float circumference = 2*PI; float a = aphelion[id]; float e = eccentricity[id]; float r = (a*(1-(e*e)))/(1-(e*cos(i))); float x = r*cos(i); float y = r*sin(i); translate(x,y,0); rotateY(radians(inclination[id])); rotateZ(PI); } } void keyPressed() { if (key == 'C' || key == 'c') { center = (center+1)%10; key = (char)(center+'0'); } if (key == 'A' || key == 'a') camz-=0.01; else if (key == 'D' || key == 'd') camz+=0.01; else if (key == 'W' || key == 'w') scale+=0.025; else if (key == 'S' || key == 's') { if(scale>0.05) scale-=0.025;
  }
  else if (key == 'P' || key == 'p') {
    if (projection == 1) {
      projection=0;
      setString("Perspective projection");
    }
    else {
      projection=1;
      setString("Orthographic projection");
    }
  }
  else if (key == 'O' || key == 'o') {
    if(orbit==1) orbit=0;
    else orbit=1;
  }
  else if (key == 'R' || key == 'r') {
    center = 0;
    timeRate = 0;
    fastForward = 0;
  }
  else if (key == 'M' || key == 'm') {
    if(timeRate<15) { timeRate++; fastForward = 1; } } else if (key == 'N' || key == 'n') { if(timeRate>-15) {
      timeRate--;
      fastForward = 1;
    }
  }
  else if (key == CODED) {
    if (keyCode == UP) camx+=0.01;
    else if (keyCode == DOWN) camx-=0.01;
    else if (keyCode == LEFT) camy-=0.01;
    else if (keyCode == RIGHT) camy+=0.01;
  }
  else if ((key-'0') < 10) { // Numerical
    int value = key - '0';
    center = value;
    if(value==0) setString("FOCUS: Sun");
    else if(value==1) setString("FOCUS: Mercury");
    else if(value==2) setString("FOCUS: Venus");
    else if(value==3) setString("FOCUS: Earth");
    else if(value==4) setString("FOCUS: Mars");
    else if(value==5) setString("FOCUS: Jupiter");
    else if(value==6) setString("FOCUS: Saturn");
    else if(value==7) setString("FOCUS: Uranus");
    else if(value==8) setString("FOCUS: Neptune");
    else if(value==9) setString("FOCUS: Pluto");
  }
}