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: GitHub - vindar/tgx: tgx - a tiny/teensy 3D graphics library
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 */