Kjetil's Information Center: A Blog About My Projects

Space Ship Simulator in OpenGL

I finally finished a project I had suspended many years ago when I was looking into OpenGL. At that time my solution had problems that I now later have discovered was related to the dreaded Gimbal lock caused by the use of Euler angles. The new solution uses rotation matrices directly. It uses the GLUT library which is kind of deprecated, but still very widely available and convenient for simpler OpenGL programs and demos like this one.

This space ship simulator is quite simple, but allows movement in all directions. When the program is started, the "universe" is populated with some randomly generated (really small) planets which can be navigated around. Use W/S to pitch, A/S to yaw, Z/C to roll and R/F to increase/decrease thrusters.

Here is the code, compile it like so: gcc -o space space.c -lGL -lGLU -lglut -lm -Wall

#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define WORLD_SIZE 25 /* Distance to fake walls from origin. */

#define GRID_SPACING 5
#define GRID_START ((0 - WORLD_SIZE) + GRID_SPACING)
#define GRID_END (WORLD_SIZE - GRID_SPACING)
#define GRID_SIZE ((GRID_END - GRID_START) / GRID_SPACING)

#define PLANETS_MAX 10
#define PLANETS_CHANCE (GRID_SIZE * GRID_SIZE * GRID_SIZE) / PLANETS_MAX



typedef struct vector_s {
  float x;
  float y;
  float z;
} vector_t;

typedef struct planet_s {
  vector_t pos;
  float size;
  float color_r;
  float color_g;
  float color_b;
} planet_t;

typedef struct planet_distance_s {
  int planet_index;
  float distance;
} planet_distance_t;



static int camera_mode = 0;
static float ship_speed = 0.0;
static int old_mouse_x;
static int old_mouse_y;

static planet_t planets[PLANETS_MAX];
static int planets_count = 0;

static vector_t ship_forward_vector = {1,0,0};
static vector_t ship_up_vector      = {0,1,0};
static vector_t ship_right_vector   = {0,0,1};
static vector_t ship_position       = {0,0,0};



static vector_t vector_normalize(vector_t v)
{
  float magnitude;
  vector_t normalized;

  magnitude = sqrt((v.x * v.x) + (v.y * v.y) + (v.z * v.z));
  normalized.x = v.x / magnitude;
  normalized.y = v.y / magnitude;
  normalized.z = v.z / magnitude;

  return normalized;
}

static vector_t vector_cross_product(vector_t a, vector_t b)
{
  vector_t cross;

  cross.x = (a.y * b.z) - (a.z * b.y);
  cross.y = (a.z * b.x) - (a.x * b.z);
  cross.z = (a.x * b.y) - (a.y * b.x);

  return cross;
}

static vector_t vector_multiply(vector_t v, float f)
{
  vector_t product;

  product.x = v.x * f;
  product.y = v.y * f;
  product.z = v.z * f;

  return product;
}

static vector_t vector_sum(vector_t a, vector_t b)
{
  vector_t sum;

  sum.x = a.x + b.x;
  sum.y = a.y + b.y;
  sum.z = a.z + b.z;

  return sum;
}

static vector_t vector_difference(vector_t a, vector_t b)
{
  vector_t difference;

  difference.x = a.x - b.x;
  difference.y = a.y - b.y;
  difference.z = a.z - b.z;

  return difference;
}

static float vector_distance(vector_t a, vector_t b)
{
  return sqrt(((a.x - b.x) * (a.x - b.x)) +
              ((a.y - b.y) * (a.y - b.y)) +
              ((a.z - b.z) * (a.z - b.z)));
}



static void ship_pitch(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_yaw(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_difference(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_right_vector, sin(angle * (M_PI / 180)))));

  ship_right_vector = vector_cross_product(
    ship_forward_vector, ship_up_vector);
}

static void ship_roll(float angle)
{
  ship_right_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_right_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_advance(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_forward_vector, distance));
}

#ifdef DEBUG_MODE
static void ship_ascend(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_up_vector, distance));
}

static void ship_strafe(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_right_vector, distance));
}
#endif /* DEBUG_MODE */



static void idle()
{
  if (ship_speed != 0) {
    ship_advance(ship_speed);
  }

  glutPostRedisplay();

#ifdef DEBUG_MODE
  fprintf(stderr, "Speed: %.2f, X: %.2f, Y: %.2f, Z: %.2f\n",
    ship_speed, ship_position.x, ship_position.y, ship_position.z);
#endif /* DEBUG_MODE */
}

static void draw_ship(void)
{
  float rotation[16];

  glTranslatef(ship_position.x, ship_position.y, ship_position.z);

  rotation[0]  = ship_right_vector.x;
  rotation[1]  = ship_right_vector.y;
  rotation[2]  = ship_right_vector.z;
  rotation[3]  = 0;
  rotation[4]  = ship_up_vector.x;
  rotation[5]  = ship_up_vector.y;
  rotation[6]  = ship_up_vector.z;
  rotation[7]  = 0;
  rotation[8]  = ship_forward_vector.x;
  rotation[9]  = ship_forward_vector.y;
  rotation[10] = ship_forward_vector.z;
  rotation[11] = 0;
  rotation[12] = 0;
  rotation[13] = 0;
  rotation[14] = 0;
  rotation[15] = 1;
  glMultMatrixf(rotation);

  glScalef(0.5, 0.5, 0.5);
  glColor3f(0.9, 0.9, 0.9);

  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
}

#ifdef DEBUG_MODE
static void draw_axis_indicator(void)
{
  /* Draw it at origin. */
  GLfloat old_line_width;
  glGetFloatv(GL_LINE_WIDTH, &old_line_width);
  glLineWidth(3.0);

  glBegin(GL_LINES);
    /* Red X-Axis */
    glColor3f(1, 0, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(1, 0, 0); 
    /* Green Y-Axis */
    glColor3f(0, 1, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 1, 0);
    /* Blue Z-Axis */
    glColor3f(0, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, 1); 
  glEnd();

  glLineWidth(old_line_width);
}
#endif /* DEBUG_MODE */

static int planet_distance_compare(const void *p1, const void *p2)
{
  return ((planet_distance_t *)p1)->distance <
         ((planet_distance_t *)p2)->distance;
}

static void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();

  if (camera_mode == 0) {
    /* Inside ship. */
    vector_t view_point;
    view_point = vector_sum(ship_position, ship_forward_vector);

    gluLookAt(ship_position.x, ship_position.y, ship_position.z,
              view_point.x, view_point.y, view_point.z,
              ship_up_vector.x, ship_up_vector.y, ship_up_vector.z);
  } else {
    /* Outside ship, a static look towards origin. */
    glRotatef(45.0, 1.0, 0.0, 0.0);
    glRotatef(120.0, 0.0, 1.0, 0.0);
    glTranslatef(WORLD_SIZE / 2, 0 - (WORLD_SIZE / 2), WORLD_SIZE / 2);
  }

  /* Draw floor/roof/walls, which are actually big cubes! */
  glColor3f(0.0, 0.0, 0.0);

  glPushMatrix();
    glTranslatef(0.0, -WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(-WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, -WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  /* Calculate distance from ship to planets, because they need to be
     drawn/rendered from the furthest away first. */
  planet_distance_t distance[PLANETS_MAX];
  for (int i = 0; i < planets_count; i++) {
    distance[i].planet_index = i;
    distance[i].distance = vector_distance(ship_position, planets[i].pos);
  }
  qsort(distance, planets_count, sizeof(planet_distance_t),
    planet_distance_compare);

  /* Draw planets. */
  for (int i = 0; i < planets_count; i++) {
    int n = distance[i].planet_index;
    glPushMatrix();
      GLUquadric *quadric = gluNewQuadric();
      if (quadric != 0) {
        glTranslatef(planets[n].pos.x, planets[n].pos.y, planets[n].pos.z);
        glColor3f(planets[n].color_r, planets[n].color_g, planets[n].color_b);
        gluSphere(quadric, planets[n].size, 20, 20);
        gluDeleteQuadric(quadric);
      }
    glPopMatrix();
  }
    
  if (camera_mode != 0) {
    glPushMatrix();
      draw_ship();
    glPopMatrix();
  }

#ifdef DEBUG_MODE
  glPushMatrix();
    draw_axis_indicator();
  glPopMatrix();
#endif /* DEBUG_MODE */

  glFlush();
  glutSwapBuffers();
}

static void reshape(int w, int h)
{
  glViewport(0, 0, (GLsizei)w, (GLsizei)h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 150.0);
  glMatrixMode(GL_MODELVIEW);
}

static void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 's':
    ship_pitch(-1.0);
    break;
  case 'w':
    ship_pitch(1.0);
    break;
  case 'd':
    ship_yaw(-1.0);
    break;
  case 'a':
    ship_yaw(1.0);
    break;
  case 'z':
    ship_roll(-1.0);
    break;
  case 'c':
    ship_roll(1.0);
    break;
  case 'r':
    ship_speed += 0.01;
    break;
  case 'f':
    ship_speed -= 0.01;
    break;
#ifdef DEBUG_MODE
  case 't':
    ship_advance(0.1);
    break;
  case 'g':
    ship_advance(-0.1);
    break;
  case 'y':
    ship_ascend(0.1);
    break;
  case 'h':
    ship_ascend(-0.1);
    break;
  case 'u':
    ship_strafe(0.1);
    break;
  case 'j':
    ship_strafe(-0.1);
    break;
  case 'x':
    camera_mode = (camera_mode) ? 0 : 1;
    break;
#endif /* DEBUG_MODE */
  case 'q':
    exit(0);
    break;
  }
}

static void motion(int x, int y)
{
  if (x > old_mouse_x) {
    ship_yaw(1.0);
  } else if (x < old_mouse_x) {
    ship_yaw(-1.0);
  }

  if (y > old_mouse_y) {
    ship_pitch(-1.0);
  } else if (y < old_mouse_y) {
    ship_pitch(1.0);
  }

  old_mouse_x = x;
  old_mouse_y = y;
}

void generate_planets(void)
{
  for (int x = GRID_START; x < GRID_END; x += GRID_SPACING) {
    for (int y = GRID_START; y < GRID_END; y += GRID_SPACING) {
      for (int z = GRID_START; z < GRID_END; z += GRID_SPACING) {
        if (x == 0 && y == 0 && z == 0) {
          /* Ignore origin. */
          continue;
        }

        if ((rand() % PLANETS_CHANCE) == 0) {
          planets[planets_count].pos.x = (x - 2) + (rand() % 4);
          planets[planets_count].pos.y = (y - 2) + (rand() % 4);
          planets[planets_count].pos.z = (z - 2) + (rand() % 4);
          planets[planets_count].size = 
            ((rand() % ((GRID_SPACING * 10) - 1)) / 20.0) + 0.1;
          planets[planets_count].color_r = (rand() % 11) / 10.0;
          planets[planets_count].color_g = (rand() % 11) / 10.0;
          planets[planets_count].color_b = (rand() % 11) / 10.0;

#ifdef DEBUG_MODE
          printf("X=%.1f, Y=%.1f, Z=%.1f, Size=%.1f, Col=%.1f;%.1f;%.1f\n", 
            planets[planets_count].pos.x,
            planets[planets_count].pos.y,
            planets[planets_count].pos.z,
            planets[planets_count].size,
            planets[planets_count].color_r,
            planets[planets_count].color_g,
            planets[planets_count].color_b);
#endif /* DEBUG_MODE */

          if ((planets_count + 1) >= PLANETS_MAX) {
            return;
          } else {
            planets_count++;
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  srand(time(NULL));
  generate_planets();
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
  glutInitWindowSize(800, 800);
  glutInitWindowPosition(100, 100);
  glutCreateWindow(argv[0]);
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glShadeModel(GL_FLAT);
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutIdleFunc(idle);
  glutKeyboardFunc(keyboard);
  glutMotionFunc(motion);
  glutMainLoop();
  return 0;
}
          


Here is a screenshot of what it looks like, just some "planets" that looks like spheres:

Space Ship Simulator Screenshot


Topic: Scripts and Code, by Kjetil @ 05/03-2021, Article Link