2D/3D Graphics Library (TGX)

Hi all,

I’ve been playing around with the SmartMatrix for a while now, and it’s been fun! However, I wondered if there was a good Teensy-based graphics library that would allow 2D/3D graphics to be rendered on the SmartMatrix. And I found that there was! I haven’t seen it mentioned in the forums here, but I thought I’d share it in case someone else found it useful (apologies if you have already covered this, or if it’s common knowledge).

The library is available here: https://github.com/vindar/tgx

I didn’t write the library, nor do I have any relation to the author(s), and all credit must go to them.

I put together the code below for you to try it out, and it’s heavily based on the Teensy 4 3D borg_cube example from the library. It renders a rotating cube with a constantly changing texture created in real-time (see image below). The changes required to make it work with SmartMatrix were pretty minimal:

  • Remove all the references to the TFT display that the example uses
  • Resize the frame buffers for the display size (128 x 128 in my case)
  • Change the colour depth from RGB565 to RGB24
  • Use memcpy to copy the rendering frame buffer to the SmartMatrix frame buffer (I could write directly to the SM frame buffer, but I wanted to keep it as close to the example as possible).

On a Teensy 4.1, I’m getting consistent 159 FPS, which is very impressive I think! I believe it should work on other set-ups just by changing the SM parameters at the top of the file. There are other examples that come with the library if anyone wants to experiment with it.

I hope that’s of interest to anyone.
welshcoder

The code:

#include <Arduino.h>
#include <tgx.h>
#include <font_tgx_OpenSans_Bold.h>
#include <MatrixHardware_Teensy4_ShieldV5.h>        // SmartLED Shield for Teensy 4 (V5)
#include <SmartMatrix.h>

#define COLOR_DEPTH 24                  // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)

const uint16_t kMatrixWidth = 128;       // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 128;      // Set to the height of your display
const uint8_t kRefreshDepth = 36;       // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage.  36 is typically good, drop down to 24 if you need to.  On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48.  On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4;       // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate.  (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN;   // Choose the configuration that matches your panels.  See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = ( SM_HUB75_OPTIONS_BOTTOM_TO_TOP_STACKING); //(SM_HUB75_OPTIONS_NONE);        // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kIndexedLayerOptions = (SM_INDEXED_OPTIONS_NONE);
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
const uint8_t kScrollingLayerOptions = (SM_SCROLLING_OPTIONS_NONE);

SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);

SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);

const int defaultBrightness = 254;        // full (100%) brightness

//const int defaultBrightness = (15*255)/100;       // dim: 15% brightness

const int defaultScrollOffset = 6;

const rgb24 defaultBackgroundColor = {0x00, 0, 0};

// Teensy 4.0 has the LED on pin 13

const int ledPin = 13;

using namespace std;

// let's not burden ourselves with the tgx:: prefix

using namespace tgx;

// screen dimension (landscape mode)

static const int SLX = kMatrixWidth;

static const int SLY = kMatrixHeight;

// main screen framebuffer (48K in DTCM for fastest access)

RGB24 fb[SLX * SLY];                

// zbuffer (64K in DMAMEM)

DMAMEM float zbuf[SLX * SLY];          

// image that encapsulates the framebuffer fb.

Image<RGB24> im(fb, SLX, SLY);

// the texture image

const int tex_size = 128;

RGB24 texture_data[tex_size*tex_size];

Image<RGB24> texture(texture_data, tex_size, tex_size);

// 3D mesh drawer : using perspective projection.

Renderer3D<RGB24, SLX, SLY, true, false> rendererP;

// 3D mesh drawer : using orthoscopic projection.

Renderer3D<RGB24, SLX, SLY, true, true> rendererO;

// the cube 8 vertices

fVec3 tab_vertices[8] =

    {

    {-1,-1,1},

    {1,-1,1},

    {1,1,1},

    {-1,1,1},

    {-1,-1,-1},

    {1,-1,-1},

    {1,1,-1},

    {-1,1,-1}

    };

// texture indices

fVec2 tab_tex[4] =

    {

    {0.0f, 0.0f},

    {1.0f, 0.0f},

    {1.0f, 1.0f},

    {0.0f, 1.0f},

    };

// list of quads.

uint16_t vert_ind[6 * 4] =

    {

    0, 1, 2, 3, // front

    1, 5, 6, 2, // right

    5, 4, 7, 6, // back

    4, 0, 3, 7, // left

    0, 4, 5, 1, // bottom

    3, 2, 6, 7  // top

    };

// use the same texture on each quad

uint16_t tex_ind[6 * 4] =

    {

    0, 1, 2, 3,

    0, 1, 2, 3,

    0, 1, 2, 3,

    0, 1, 2, 3,

    0, 1, 2, 3,

    0, 1, 2, 3

    };

// the setup() method runs once, when the sketch starts

void setup() {

    // initialize the digital pin as an output.

    pinMode(ledPin, OUTPUT);

    Serial.begin(115200);

    matrix.addLayer(&backgroundLayer);

    matrix.begin();

    matrix.setBrightness(defaultBrightness);

    backgroundLayer.enableColorCorrection(true);

    // clear screen

    backgroundLayer.fillScreen(defaultBackgroundColor);

    backgroundLayer.swapBuffers();

   

    const float ratio = ((float)SLX) / SLY;

    // setup the 3D renderer with perspective projection

    rendererP.setOffset(0, 0);

    rendererP.setImage(&im);

    rendererP.setZbuffer(zbuf, SLX * SLY);

    rendererP.setPerspective(45, ratio, 0.1f, 1000.0f);

    rendererP.setCulling(1);

    rendererP.useBilinearTexturing(true);

    // setup the 3D renderer with orthoscopic projection

    rendererO.setOffset(0, 0);

    rendererO.setImage(&im);

    rendererO.setZbuffer(zbuf, SLX * SLY);

    rendererO.setOrtho(-1.8*ratio, 1.8 *ratio, -1.8, 1.8, 0.1f, 1000.0f);

    rendererO.setCulling(1);

    rendererO.useBilinearTexturing(true);

    // initial textrure color

    texture.fillScreen(RGB565_Blue);

}

             

/** draw a random rectangle on the texture */

void splash()

    {

    static int count = 0;    

    static RGB565 color;

    if (count == 0)

        color = RGB565((int)random(32), (int)random(64), (int)random(32));

    count = (count + 1) % 400;

    iVec2 pos(random(tex_size), random(tex_size));

    int r = random(10);

    texture.drawRect(iBox2( pos.x - r, pos.x + r, pos.y - r, pos.y + r ), color);

    }

/** draw the current fps on the image */

void fps(const char* str)

    {

    static elapsedMillis em = 0; // number of milli elapsed since last fps update

    static int fps = 0;         // last fps

    static int count = 0;       // number of frames since the last update

    // recompute fps every second.

    count++;

    if ((int)em > 1000)

        {

        em = 0;

        fps = count;

        count = 0;

        }

    // display

    //im.drawText(str, {3,12 }, RGB565_Red, font_tgx_OpenSans_Bold_10, false);

    char buf[10];

    sprintf(buf, "%d FPS", fps);

    auto B = im.measureText(buf, { 0,0 }, font_tgx_OpenSans_Bold_10, false);

    im.drawText(buf, { SLX - B.lx() - 3,12 }, RGB565_Red, font_tgx_OpenSans_Bold_10, false);

    }

elapsedMillis em = 0; // time

int nbf = 0; ; // number frames drawn

int projtype = 0; // current projection used.

void loop()

    {

   

    // model matrix

    fMat4 M;

    M.setRotate(em / 11.0f, { 0,1,0 });

    M.multRotate(em / 23.0f, { 1,0,0 });

    M.multRotate(em / 41.0f, { 0,0,1 });

    M.multTranslate({ 0, 0, -5 });

    if (projtype)

        {        

        im.fillScreen(RGB565_Black); // erase the screen

        rendererP.clearZbuffer(); // clear the z buffer        

        rendererP.setModelMatrix(M);// position the model        

        rendererP.drawQuads(TGX_SHADER_TEXTURE, 6, vert_ind, tab_vertices, nullptr, nullptr, tex_ind, tab_tex, &texture); // draw !        

        fps("Perspective projection"); // overlay some infos

        }

    else

        {

        im.fillScreen(RGB565_Gray); // erase the screen

        rendererO.clearZbuffer();

        rendererO.setModelMatrix(M);

        rendererO.drawQuads(TGX_SHADER_TEXTURE, 6, vert_ind, tab_vertices, nullptr, nullptr, tex_ind, tab_tex, &texture); // draw !

        fps("Orthoscopic projection"); // overlay some infos

        }

    // update the screen (async).

    memcpy(backgroundLayer.getRealBackBuffer(),fb,sizeof(fb));

    backgroundLayer.swapBuffers();

    // add a random rect on the texture.

    splash();

    // swith between perspective and orthogonal projection every 1000 frames.

        if (nbf++ % 1000 == 0)

            {

            projtype = 1 - projtype;

            }

    }

       

/** end of file */
1 Like

Looks great, thanks for sharing!

No worries!

In the meantime, here’s the “scream” example, complete with a RGB24 version of the image used: https://gist.github.com/welshcoder/97905928c586bfad62068bc90f083ec8

This one runs at about 16 to 20 FPS, but I’m still very impressed with what that little chip is doing in real-time!

Looks cool. I’ve been using matrices for pictures and gif videos. I must try this. Thanks for sharing!