cobra

This is an update of the original Gameduino wireframe demo.

The background star layer is a seamless 256x256 texture by Patrick Hoesly. It is drawn as a single 480x272 sprite.

Patrick also made the 'sun' image. The code draws it twice, rotating in opposite directions, to give it a little shimmer.

The Cobra spacecraft model is transformed and lit by the Arduino, which then draws flat polygons for each visible face (draw_faces()). It then makes a second pass to render the edges using smooth lines (draw_edges()).

The tiny red/green navigation lights are a single sprite, colored appropriately (draw_navlight()).

The spinning of the ship is controlled via the touch-screen using trackball code based on original code in projtex.c by David Yu and David Blythe of SGI.

#include <EEPROM.h>
#include <SPI.h>
#include <GD2.h>

#include "cobra_assets.h"

////////////////////////////////////////////////////////////////////////////////
//                                  3D Projection
////////////////////////////////////////////////////////////////////////////////

static float model_mat[9] = { 1.0, 0.0, 0.0,
                        0.0, 1.0, 0.0,
                        0.0, 0.0, 1.0 };
static float normal_mat[9] = { 1.0, 0.0, 0.0,
                        0.0, 1.0, 0.0,
                        0.0, 0.0, 1.0 };

#define M(nm,i,j)       ((nm)[3 * (i) + (j)])

void mult_matrices(float *a, float *b, float *c)
{
  int i, j, k;
  float result[9];
  for(i = 0; i < 3; i++) {
    for(j = 0; j < 3; j++) {
      M(result,i,j) = 0.0f;
      for(k = 0; k < 3; k++) {
        M(result,i,j) +=  M(a,i,k) *  M(b,k,j);
      }
    }
  }
  memcpy(c, result, sizeof(result));
}

// Based on glRotate()
// Returns 3x3 rotation matrix in 'm'
// and its invese in 'mi'

static void rotate(float *m, float *mi, float angle, float *axis)
{
  float x = axis[0];
  float y = axis[1];
  float z = axis[2];

  float s = sin(angle);
  float c = cos(angle);

  float xx = x*x*(1-c);
  float xy = x*y*(1-c);
  float xz = x*z*(1-c);
  float yy = y*y*(1-c);
  float yz = y*z*(1-c);
  float zz = z*z*(1-c);

  float xs = x * s;
  float ys = y * s;
  float zs = z * s;

  m[0] = xx + c;
  m[1] = xy - zs;
  m[2] = xz + ys;

  m[3] = xy + zs;
  m[4] = yy + c;
  m[5] = yz - xs;

  m[6] = xz - ys;
  m[7] = yz + xs;
  m[8] = zz + c;

  mi[0] = m[0];
  mi[1] = xy + zs;
  mi[2] = xz - ys;

  mi[3] = xy - zs;
  mi[4] = m[4];
  mi[5] = yz + xs;

  mi[6] = xz + ys;
  mi[7] = yz - xs;
  mi[8] = m[8];
}

static void rotation(float angle, float *axis)
{
  float mat[9];
  float mati[9];

  rotate(mat, mati, angle, axis);
  mult_matrices(model_mat, mat, model_mat);
  mult_matrices(mati, normal_mat, normal_mat);
}


#define N_VERTICES  (sizeof(COBRA_vertices) / 3)

typedef struct {
  int x, y;
  float z;
} xyz;

static xyz projected[N_VERTICES];

void project(float distance)
{
  byte vx;
  prog_char *pm = COBRA_vertices; 
  prog_char *pm_e = pm + sizeof(COBRA_vertices);
  xyz *dst = projected;
  int8_t x, y, z;

  while (pm < pm_e) {
    x = pgm_read_byte_near(pm++);
    y = pgm_read_byte_near(pm++);
    z = pgm_read_byte_near(pm++);
    float xx = x * model_mat[0] + y * model_mat[3] + z * model_mat[6];
    float yy = x * model_mat[1] + y * model_mat[4] + z * model_mat[7];
    float zz = x * model_mat[2] + y * model_mat[5] + z * model_mat[8] + distance;
    float q = 240 / (100 + zz);
    dst->x = 16 * (240 + xx * q);
    dst->y = 16 * (136 + yy * q);
    dst->z = zz;
    dst++;
  }
}

static void transform_normal(int8_t &nx, int8_t &ny, int8_t &nz)
{
  int8_t xx = nx * normal_mat[0] + ny * normal_mat[1] + nz * normal_mat[2];
  int8_t yy = nx * normal_mat[3] + ny * normal_mat[4] + nz * normal_mat[5];
  int8_t zz = nx * normal_mat[6] + ny * normal_mat[7] + nz * normal_mat[8];
  nx = xx;
  ny = yy;
  nz = zz;
}

#define EDGE_BYTES  5
static byte visible_edges[EDGE_BYTES];

void draw_faces()
{
  memset(visible_edges, 0, sizeof(visible_edges));

  prog_uchar *p = COBRA_faces; 
  byte n;
  int c = 1;
  Poly po;
  while ((n = pgm_read_byte_near(p++)) != 0xff) {
    int8_t nx = pgm_read_byte_near(p++);
    int8_t ny = pgm_read_byte_near(p++);
    int8_t nz = pgm_read_byte_near(p++);
    byte face_edges[EDGE_BYTES];
    for (byte i = 0; i < EDGE_BYTES; i++)
      face_edges[i] = pgm_read_byte_near(p++);
    byte v1 = pgm_read_byte_near(p);
    byte v2 = pgm_read_byte_near(p + 1);
    byte v3 = pgm_read_byte_near(p + 2);
    long x1 = projected[v1].x;
    long y1 = projected[v1].y;
    long x2 = projected[v2].x;
    long y2 = projected[v2].y;
    long x3 = projected[v3].x;
    long y3 = projected[v3].y;
    long area = (x1 - x3) * (y2 - y1) - (x1 - x2) * (y3 - y1);

    if (area > 0) {
      for (byte i = 0; i < EDGE_BYTES; i++)
        visible_edges[i] |= face_edges[i];
      po.begin();
      for (int i = 0; i < n; i++) {
        byte vi = pgm_read_byte_near(p++);
        xyz *v = &projected[vi];
        po.v(v->x, v->y);
      }
      {
        transform_normal(nx, ny, nz);

        uint16_t r = 10, g = 10, b = 20;  // Ambient

        int d = -ny;                      // diffuse light from +ve Y
        if (d > 0) {
          r += d >> 2;
          g += d >> 1;
          b += d;
        }
                                          // use specular half angle
        d = ny * -90 + nz * -90;          // Range -16384 to +16384
        if (d > 8192) {
          byte l = pgm_read_byte_near(shiny + ((d - 8192) >> 4));
          r += l;
          g += l;
          b += l;
        }

        GD.ColorRGB(min(255, r), min(255, g), min(255, b));
      }
      po.draw();
    } else {
      p += n;
    }
    c += 1;
  }
}

void draw_edges()
{
  GD.ColorRGB(0x2e666e);
  GD.Begin(LINES);
  GD.LineWidth(20);

  prog_uchar *p = COBRA_edges; 
  byte *pvis = visible_edges;
  byte vis;

  for (byte i = 0; i < sizeof(COBRA_edges) / 2; i++) {
    if ((i & 7) == 0)
      vis = *pvis++;
    byte v0 = pgm_read_byte_near(p++);
    byte v1 = pgm_read_byte_near(p++);

    if (vis & 1) {
      int x0 = projected[v0].x;
      int y0 = projected[v0].y;
      int x1 = projected[v1].x;
      int y1 = projected[v1].y;

      GD.Vertex2f(x0,y0);
      GD.Vertex2f(x1,y1);
    }
    vis >>= 1;
  }
}

static void draw_navlight(byte nf)
{
  float l0z = projected[N_VERTICES - 2].z;
  float l1z = projected[N_VERTICES - 1].z;
  byte i;
  if (nf == 0)  // draw the one with smallest z
    i = (l0z < l1z) ? (N_VERTICES - 2) : (N_VERTICES - 1);
  else
    i = (l0z < l1z) ? (N_VERTICES - 1) : (N_VERTICES - 2);

  GD.SaveContext();
  GD.BlendFunc(SRC_ALPHA, ONE);
  GD.Begin(BITMAPS);
  GD.BitmapHandle(LIGHT_HANDLE);
 
  GD.ColorRGB((i == N_VERTICES - 2) ? 0xfe2b18 : 0x4fff82);
  GD.Vertex2f(projected[i].x - (16 * LIGHT_WIDTH / 2),
              projected[i].y - (16 * LIGHT_WIDTH / 2));
  GD.RestoreContext();
}

/*****************************************************************/

/* simple trackball-like motion control */
/* Based on projtex.c - by David Yu and David Blythe, SGI */

float angle, axis[3] = {0,1,0};
float lastPos[3];

void
ptov(int x, int y, int width, int height, float v[3])
{
  float d, a;

  /* project x,y onto a hemi-sphere centered within width, height */
  v[0] = (2.0 * x - width) / width;
  v[1] = (2.0 * y - height) / height;
  d = sqrt(v[0] * v[0] + v[1] * v[1]);
  v[2] = cos((M_PI / 2.0) * ((d < 1.0) ? d : 1.0));
  a = 1.0 / sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
  v[0] *= a;
  v[1] *= a;
  v[2] *= a;
}

void
startMotion(int x, int y)
{
  angle = 0.0;
  ptov(x, y, 480, 272, lastPos);
}

void
trackMotion(int x, int y)
{
  float curPos[3], dx, dy, dz;

  ptov(x, y, 480, 272, curPos);

  dx = curPos[0] - lastPos[0];
  dy = curPos[1] - lastPos[1];
  dz = curPos[2] - lastPos[2];
  angle = (M_PI / 2) * sqrt(dx * dx + dy * dy + dz * dz);

  axis[0] = lastPos[1] * curPos[2] - lastPos[2] * curPos[1];
  axis[1] = lastPos[2] * curPos[0] - lastPos[0] * curPos[2];
  axis[2] = lastPos[0] * curPos[1] - lastPos[1] * curPos[0];

  float mag = 1 / sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
  axis[0] *= mag;
  axis[1] *= mag;
  axis[2] *= mag;

  lastPos[0] = curPos[0];
  lastPos[1] = curPos[1];
  lastPos[2] = curPos[2];
}

/*****************************************************************/


void setup()
{
  Serial.begin(57600);
  GD.begin();
  LOAD_ASSETS();
  GD.BitmapHandle(BACKGROUND_HANDLE);
  GD.BitmapSize(BILINEAR, REPEAT, REPEAT, 480, 272);
}

static byte prev_touching;
static uint16_t t;

static void draw_sun(int x, int y, int rot)
{
  GD.cmd_loadidentity();
  GD.cmd_translate(F16(SUN_WIDTH / 2), F16(SUN_WIDTH / 2));
  GD.cmd_rotate(rot);
  GD.cmd_translate(-F16(SUN_WIDTH / 2), -F16(SUN_WIDTH / 2));
  GD.cmd_setmatrix();
  GD.Vertex2f(x - (16 * SUN_WIDTH / 2), y - (16 * SUN_WIDTH / 2));
}

void loop()
{
  GD.Begin(BITMAPS);
  GD.SaveContext();
  GD.BitmapHandle(BACKGROUND_HANDLE);
  GD.cmd_translate(-(long)t << 14, (long)t << 13);
  GD.cmd_rotate(3312);
  GD.cmd_setmatrix();
  GD.Vertex2ii(0, 0, 0, 0);
  GD.RestoreContext();

  int et = t - 720;
  int sun_x = (480 * 16) - (et << 2),
      sun_y = (100 * 16) + (et << 1);
  GD.SaveContext();
  GD.PointSize(52 * 16);
  GD.ColorRGB(0x000000);
  GD.Begin(POINTS);
  GD.Vertex2f(sun_x, sun_y);
  GD.RestoreContext();

  GD.SaveContext();
  GD.Begin(BITMAPS);
  GD.BlendFunc(ONE, ONE);
  GD.BitmapHandle(SUN_HANDLE);
  GD.ColorRGB(0xb0a090);
  draw_sun(sun_x, sun_y, t << 6);
  draw_sun(sun_x, sun_y, -t << 6);
  GD.RestoreContext();

  GD.get_inputs();
  byte touching = (GD.inputs.x != -32768);
  if (!prev_touching && touching)
    startMotion(GD.inputs.x, GD.inputs.y);
  else if (touching)
    trackMotion(GD.inputs.x, GD.inputs.y);
  prev_touching = touching;

  unsigned long t0 = micros();

  if (angle != 0.0f)
    rotation(angle, axis);

  project(0);
  draw_navlight(1);
  draw_faces();
  GD.RestoreContext();
  draw_edges();
  draw_navlight(0);
  GD.RestoreContext();

  caption(et, "Textures by Patrick Hoesly");
  GD.swap();

  t++;
}