Building a 3D Graphics Pipeline on an Arduino, Part 5

One of the advantages of our design is that we can rapidly port our 3D drawing code to another platform. In fact, we could port our code off the Arduino entirely and move it to a different microprocessor or even to your desktop computer.

All we have to do is to alter how we initialize our 3D library, change our begin/end calls, and change our low-level drawing routines. While we’re at it, we can also refine our 3D library to only draw in a subset of our screen. We may wish to do this, for example, if we’re creating a game and we want a separate status area.

So let’s begin.

For this exercise we’ll be porting the code to an Arduboy, a small hand held Arduino game unit with built-in game control buttons and a small black and white screen.


Altering the header.

Our current constructor for the G3D class contains a reference to our library, and we assume we use the entire screen. Let’s change the code two ways. First, we want to change the drawing library we pass in. Second, we want to pass in the area of the screen we’re drawing in.

So let’s update our class. I’m going to define a preprocessor token:

#define USELIBRARY	        2   // 1 = Adafruit, 2 = Arduboy

Now let’s update our G3D class. In our header file we’ll alter our constructor and create the appropriate definitions:

class G3D
{
    public:
#if USELIBRARY == 1
                G3D(Adafruit_GFX &lib, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
#elif USELIBRARY == 2
                G3D(Arduboy &lib, uint16_t x, uint16_t y, uint16_t w, uint16_t h);
#endif
                ~G3D();

        void    setColor(uint16_t c)
                    {
                        color = c;
                    }

        void    begin();
        void    end();

We also want to update the color declaration, since the Arduboy uses an 8-bit color value (which is set to either BLACK or WHITE), while the Adafruit library uses a 16-bit color value.

class G3D
{
    public:
#if USELIBRARY == 1
                G3D(Adafruit_GFX &lib, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
#elif USELIBRARY == 2
                G3D(Arduboy &lib, uint16_t x, uint16_t y, uint16_t width, uint16_t height);
#endif
                ~G3D();

#if USELIBRARY == 1
        void    setColor(uint16_t c)
                    {
                        color = c;
                    }
#elif USELIBRARY == 2
        void    setColor(uint8_t c)
                    {
                        color = c;
                    }
#endif

        void    begin();
        void    end();

These two changes require corresponding changes to the private values in our class. We need to update the library we reference, update the screen size parameters and the color:

    private:
        /*
         *  Internal state
         */
#if USELIBRARY == 1
        Adafruit_GFX &lib;
#elif USELIBRARY == 2
        Arduboy &lib;
#endif

        uint16_t xoffset;
        uint16_t yoffset;
        uint16_t width;
        uint16_t height;

        /*
         *  Current drawing color
         */

#if USELIBRARY == 1
        uint16_t color;
#elif USELIBRARY == 2
        uint8_t color;
#endif

        /*
         *  Stage 4 pipeline; 3D transformation
         */

These are all the changes we need to make to our header. From a public interface perspective, we simply update the value of USELIBRARY, and create our class linked to the correct library–and that’s it. There is no step 3.


G3D code changes.

For the body of our code, the changes are also similarly easy.

First, of course, we need to update our constructor. The body of the constructor is the same; we simply need to change the library name of the library passed in. We also stash the screen size parameters that we’ve changed.

/*  G3D::G3D
 *
 *      Construct our pipeline
 */

#if USELIBRARY == 1
G3D::G3D(Adafruit_GFX &l, uint16_t x, uint16_t y, uint16_t w, uint16_t h) : lib(l)
#elif USELIBRARY == 2
G3D::G3D(Arduboy &l, uint16_t x, uint16_t y, uint16_t width, uint16_t height) : lib(l)
#endif
{
    xoffset = x;
    yoffset = y;
    width = w;
    height = h;

    /* Initialize components of pipeline */
    p1init();
    p2init();
    p3init();
}

Now our implementation on the Adafruit GFX library called the internal startWrite and endWrite calls in order to reduce the number of times we had to turn on and off the Adafrut drawing engine. But the Arduboy library doesn’t require the same thing–so these calls effectively become ‘no-ops’:

void G3D::begin()
{
#if USELIBRARY == 1
    lib.startWrite();
#endif
}

void G3D::end()
{
#if USELIBRARY == 1
    lib.endWrite();
#endif
}

Finally we need to update our p1 move/draw and point routines. We need to do two things here: first, we need to offset our drawing to the pixel coordinates we supplied in our initializer. Second, we need to actually call the correct line drawing routines.

void G3D::p1movedraw(bool drawFlag, uint16_t x, uint16_t y)
{
    /*
     *  We use the p1drawflag and the drawFlag objects to determine the
     *  way to draw our line. For us, we're always drawing single
     *  segments, but we theoretically could roll up our lines into a
     *  collection of line segments and send them on close. (This
     *  requires hooking G3D::end().)
     */
    
    if (drawFlag) {
#if USELIBRARY == 1
        lib.writeLine(xoffset + p1x,xoffset + p1y,x,y,color);
#elif USELIBRARY == 2
        lib.drawLine(xoffset + p1x,xoffset + p1y,x,y,color);
#endif
    }
    
    p1draw = drawFlag;
    p1x = x;
    p1y = y;
}

void G3D::p1point(uint16_t x, uint16_t y)
{
#if USELIBRARY == 1
    lib.writePixel(xoffset + x,xoffset + y,color);
#elif USELIBRARY == 2
    lib.drawPixel(xoffset + x,xoffset + y,color);
#endif
}

And that’s it. Now our library can be used to generate 3D drawings on either an Arduboy or using the Adafruit GFX library.


Testing it out.

We can redo our cube test from the last time but on the Arduboy. Because the Arduboy has double-buffering the animation is smooth and quick–though it lacks the color of the Adafruit color display.

Our example needs to be rewritten to use the double-buffer drawing support of the Arduboy. We can also make use of the viewport feature to only draw on part of the screen; we’d do this if we wanted to make use of only part of the screen.

Most of the changes are relatively simple: we switch libraries and change how we start up our drawing system. The loop has the most changes, since we need to handle the double-buffering of the Arduboy API.

For the record, the complete source kit (just showing the Arduboy elements and their changes) is:

#include "G3D.h"

/*
 *  Drawing globals
 */

Arduboy arduboy;

// Graphics setup
G3D draw(arduboy,0,0,100,64);

// Rotation
static float GXAngle;
static float GYAngle;

void setup() 
{
    arduboy.beginNoLogo();
    arduboy.setFrameRate(50);
    arduboy.clear();
    arduboy.display();
    
    GXAngle = 0;
    GYAngle = 0;
}

void transform()
{
    draw.transformation.setIdentity();
    draw.perspective(1.0f,0.5f);
    draw.translate(0,0,-3.5);
    draw.rotate(AXIS_X,GXAngle);
    draw.rotate(AXIS_Y,GYAngle);
}

void drawBox(int x, int y, int z)
{
    draw.move(x-1,y-1,z-1);
    draw.draw(x+1,y-1,z-1);
    draw.draw(x+1,y+1,z-1);
    draw.draw(x-1,y+1,z-1);
    draw.draw(x-1,y-1,z-1);
    draw.draw(x-1,y-1,z+1);
    draw.draw(x+1,y-1,z+1);
    draw.draw(x+1,y+1,z+1);
    draw.draw(x-1,y+1,z+1);
    draw.draw(x-1,y-1,z+1);
    draw.move(x+1,y-1,z-1);
    draw.draw(x+1,y-1,z+1);
    draw.move(x+1,y+1,z-1);
    draw.draw(x+1,y+1,z+1);
    draw.move(x-1,y+1,z-1);
    draw.draw(x-1,y+1,z+1);
}

void loop()
{
    if (!arduboy.nextFrame()) return;
    
    arduboy.clear();
    
    draw.begin();
    draw.setColor(WHITE);
    transform();
    drawBox(0,0,0);
    draw.end();
    
    arduboy.display();

    GXAngle += 0.01;
    GYAngle += 0.02;
}

When we run this we get the following:

All the code here is at GitHub, so feel free to check it out. The main branch is the latest and greatest, and can be compiled (by changing the flag in G3D.h) on either the Arduboy or on any of Adafruit’s GFX compatible displays.

A quick note about the cube demo. If you watch it spin, sometimes the point of the cube nearest to you “disappears.” This is expected, and demonstrates clipping not just to the four boundaries of our screen, but to the near clipping plane. Think of it as the cube being really small and “clipping” against the camera lens of our virtual camera.


This has been a somewhat hastily written adventure into 3D, but I hope it has been an instructive one. If you found this series useful, please let me know. And if you find any problems with any of the code, or if you want to share any games you put together with this code, please let me know!

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: