Software graphics
Libdragon has a library of software graphics procedures that use the CPU to render sprites onto the frame buffer. These are generally slower than using the RDP, but allow for greater flexibility.
Data structures
There aren’t many data structures involved except for sprite_t
and color_t
. They contain information about a sprite image and colour data respectively. Generally you don’t need to interact with it too much since they’re mostly dealt with through functions.
typedef struct {
/** @brief Width in pixels */
uint16_t width;
/** @brief Height in pixels */
uint16_t height;
/** @brief Bit depth expressed in bytes. A 32 bits would be '4' here */
uint8_t bitdepth;
/** @brief Sprite format. Currently unused */
uint8_t format;
/** @brief Number of horizontal slices for spritemaps */
uint8_t hslices;
/** @brief Number of vertical slices for spritemaps */
uint8_t vslices;
/** @brief Start of graphics data */
uint32_t data[0];
} sprite_t;
typedef struct __attribute__((packed)) {
/** @brief Red component */
uint8_t r;
/** @brief Green component */
uint8_t g;
/** @brief Blue component */
uint8_t b;
/** @brief Alpha component */
uint8_t a;
} color_t;
Working with colours
There are three ways to represent colours in Libdragon:
color_t
– the above data structure which is 32-bitsuint16_t
– a 16-bit integeruint32_t
– a 32-bit integer
It’s important to know how to switch between these formats, so Libdragon provides some function for this.
// Macros
#define RGBA16(rx,gx,bx,ax)
#define RGBA32(rx,gx,bx,ax)
// Functions
inline uint16_t color_to_packed16(color_t c);
inline uint32_t color_to_packed32(color_t c);
inline color_t color_from_packed16(uint16_t c);
inline color_t color_from_packed32(uint32_t c);
uint32_t graphics_make_color(int r, int g, int b, int a);
uint32_t graphics_convert_color(color_t color);
That’s a bit much to take in, so here’s a handy table to show what each function changes which input into what output.
From RGBA | From color_t | From 16-bit | From 32-bit | |
To color_t | – | color_from_packed16() | color_from_packed32() | |
To 16-bit | RGBA16() | color_to_packed16() | ||
To 32-bit | RGBA32() | color_to_packed32() | ||
To (display bit depth) | graphics_make_color() | graphics_convert_color() |
The last row with graphics_make_color()
and graphics_convert_color()
will automatically output 16 or 32-bit colours depending on the bit depth of the current display as defined in display_init()
.
Drawing with Libdragon
Before we get into drawing with sprites, let’s look at some of the basic drawing functionality that is included in Libdragon. It’s pretty straightforward:
// Draw individual pixels to the frame buffer
void graphics_draw_pixel(surface_t* surf, int x, int y, uint32_t c);
void graphics_draw_pixel_trans(surface_t* surf, int x, int y, uint32_t c);
// Draw straight lines to the frame buffer
void graphics_draw_line(surface_t* surf, int x0, int y0, int x1, int y1, uint32_t c);
void graphics_draw_line_trans(surface_t* surf, int x0, int y0, int x1, int y1, uint32_t c);
// Draw a rectangle to the frame buffer
void graphics_draw_box(surface_t* surf, int x, int y, int width, int height, uint32_t color);
void graphics_draw_box_trans(surface_t* surf, int x, int y, int width, int height, uint32_t color);
The functions are very straight forward. Each one needs a surface_t
frame buffer as input (returned from display_lock()
), which is where the shape will be drawn to. It also needs x/y coordinates to determine where to place the shape and its size. Finally, the functions need a colour (16 or 32-bit int).
There is also a transparent version of each function which is useful only in 32-bit colour since it has to calculate the alpha-blending. The transparent version is generally to be avoided unless needed since it is slower than the standard one.
Drawing example: Random shapes
This is an example of these functions in action. The demo draws pixels, lines and rectangles to the frame buffer every frame. You can switch from Pixel mode to Line mode to Box mode and back with the A button. You can download the ROM here using the password graphicallysoftware
. Have a look at the source code to see how it works.
#include <libdragon.h>
#include <stdlib.h>
#include <string.h>
#define FRAME_BUFFER_SIZE 320*240*2
#define BOX_MIN_SIZE 5
#define BOX_MAX_SIZE 30
enum {
MODE_DOT,
MODE_LINE,
MODE_RECTANGLE
};
int main(void) {
display_context_t disp;
char buffer[FRAME_BUFFER_SIZE] = {0};
unsigned char i;
struct controller_data controllers;
char mode = 0;
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 32, GAMMA_NONE, FILTERS_RESAMPLE);
controller_init();
// Main loop
while (1) {
// Find a free display buffer
while(!(disp = display_lock()));
// Bring back the frame buffer from the backup
memcpy(disp->buffer, buffer, FRAME_BUFFER_SIZE);
// Mode switcher
if (mode == MODE_DOT) {
// Draw 100 dots
for (i=0; i<100; i++) {
// Draw a randomly coloured pixel in a random location
graphics_draw_pixel(
disp,
rand()%display_get_width(),
rand()%display_get_height(),
graphics_make_color (
rand()%255,
rand()%255,
rand()%255,
255
)
);
}
} else if (mode == MODE_LINE) {
// Draw a line
graphics_draw_line(
disp,
rand()%display_get_width(),
rand()%display_get_height(),
rand()%display_get_width(),
rand()%display_get_height(),
graphics_make_color (
rand()%255,
rand()%255,
rand()%255,
255
)
);
} else if (mode == MODE_RECTANGLE) {
// Draw a rectangle
graphics_draw_box(disp,
rand()%(display_get_width()-BOX_MAX_SIZE),
rand()%(display_get_height()-BOX_MAX_SIZE),
BOX_MIN_SIZE + (rand() % BOX_MAX_SIZE),
BOX_MIN_SIZE + (rand() % BOX_MAX_SIZE),
graphics_make_color (
rand()%255,
rand()%255,
rand()%255,
255
)
);
}
// Show the display
display_show(disp);
// Save the frame buffer to a backup
memcpy(buffer, disp->buffer, FRAME_BUFFER_SIZE);
// Handle controller input
controller_scan();
controllers = get_keys_down();
if (controllers.c[0].A) {
mode = (mode+1) % 3;
}
}
}
Software sprites
The software graphics renderer can also be used to place sprites on the frame buffer. Once you’ve drawn your PNG, converted it to a .sprite file and loaded it onto the DFS, it is time to display it on the screen. These are the functions that are used to render sprites:
void graphics_draw_sprite(surface_t* surf, int x, int y, sprite_t *sprite);
void graphics_draw_sprite_stride(surface_t* surf, int x, int y, sprite_t *sprite, int offset);
void graphics_draw_sprite_trans(surface_t* surf, int x, int y, sprite_t *sprite);
void graphics_draw_sprite_trans_stride(surface_t* surf, int x, int y, sprite_t *sprite, int offset);
All of these functions do pretty much the same thing – display a sprite – with a few variations.
graphics_draw_sprite()
– Draw a single opaque sprite.graphics_draw_sprite_stride()
– Draw an opaque sprite from a spritesheet.graphics_draw_sprite_trans()
– Draw a single sprite with transparency.graphics_draw_sprite_trans_stride()
– Draw a sprite with transparency from a spritesheet.
Note that the x/y coordinates of each sprite refer to the position of their top-left corner relative to the origin (top-left corner of the display).
Transparency
If your sprite has transparency but you don’t use a _trans function, it will still load but all transparent pixels will be opaque. Here is an example of the same sprite loaded with both functions:
Spritesheets
The _stride
functions have an additional parameter, which is called the offset. This is used to determine which sprite in a spritesheet to use. It works from left to right, then top to bottom. Remember that you need to set the rows/columns in mksprite in order for this to work correctly. This Super Mario Bros spritesheet shows the order in which a 2×2 spritesheet’s indices would be accessed:
Spritesheet example: Moving satellite
Here is an example of a spritesheet in action.
#include <libdragon.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
display_context_t disp;
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);
dfs_init(DFS_DEFAULT_LOCATION);
// Load the spritesheet into memory
int f = dfs_open("/satellite.sprite");
sprite_t *satellite = malloc(dfs_size(f));
dfs_read(satellite, 1, dfs_size(f), f);
dfs_close(f);
// Frame counter
int i = 0;
// Main loop
while (1) {
// Load and clear the frame buffer
while(!(disp = display_lock()));
// Clear the screen
graphics_fill_screen(disp, graphics_make_color (0, 0, 0, 255));
// Draw the sprite from the spritesheet
graphics_draw_sprite_trans_stride(
disp, // Load into frame buffer
i%display_get_width(), // Move it towards the right
100, // Don't move up or down
satellite, // Load this spritesheet
(i>>2)%36 // Select the next sprite in the spritesheet every 4 frames
);
i++;
// Show the display
display_show(disp);
}
}
Text and fonts
It’s important to be able to easily display test on screen since this is the primary method by which you will show information to the player, be it character dialogue or status information. While it is possible to do it via the sprite drawing functions, Libdragon provides a simple library that allows you to make your own fonts and display text to the screen.
void graphics_set_default_font(void);
void graphics_set_font_sprite(sprite_t *font);
void graphics_draw_character(surface_t* surf, int x, int y, char c);
void graphics_draw_text(surface_t* surf, int x, int y, const char * const msg);
void graphics_set_color(uint32_t forecolor, uint32_t backcolor);
Setting fonts
The default font is the one that you’d find in the N64 console, which is a plain black and white terminal-esque font.
You can change the font by using these two functions:
// Reset the font back to the default one
void graphics_set_default_font(void);
// Set a new font
void graphics_set_font_sprite(sprite_t *font);
Setting a new font requires a spritesheet similar to those in the previous section. Just that this time, you’ll need one that has at least 128 slices (indices/offset 0-127) to correspond with each ASCII character. You can add a spritesheet with additional characters if you want, but they will appear as blanks, squares or gibberish.
Here is an example of a sprite sheet. If you want to make your own, it’s best to use a grid to help align your characters and then remove the grid for the final output.
Note: that sprite sheets have only 1-bit of pixel depth – opaque or transparent. The above spritesheet is coloured purple, but the text renderer will ignore that and make it all white or whatever colour has been defined by graphics_set_color()
. If you want to have fonts that are more complex than this, you’ll have to do something else. Also all fonts must be monospace by nature.
Drawing text
The other two functions serve to draw text to the screen. graphics_draw_character()
draws one character at a time, and graphics_draw_text()
draws a null-terminated string.
Note that if you want to format it similar to a printf() function, you should instead use sprintf() to save the string and then summon it. Here is an example:
#include <libdragon.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
display_context_t disp;
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 2, GAMMA_NONE, FILTERS_RESAMPLE);
dfs_init(DFS_DEFAULT_LOCATION);
// Load the font into memory
int f = dfs_open("/font-sheet.sprite");
sprite_t *fontSheet = malloc(dfs_size(f));
dfs_read(fontSheet, 1, dfs_size(f), f);
dfs_close(f);
// Set the font spritesheet
graphics_set_font_sprite(fontSheet);
// Frame counter
int i = 0;
char frameText[1024] = {0};
// Main loop
while (1) {
// Load and clear the frame buffer
while(!(disp = display_lock()));
graphics_fill_screen(disp, graphics_make_color (0, 0, 0, 255));
// Draw the letter 'A'
graphics_draw_character(disp, 5, 10, 'A');
// Draw some static text
graphics_draw_text(disp, 5, 30, "Static text");
// Save formatted text to string buffer
sprintf(frameText, "Frame :%i", i++);
// Draw that string to screen
graphics_draw_text(disp, 5, 50, frameText);
// Show the display
display_show(disp);
// Increase the counter
}
}
Colouring text
Text has only one bit of colour depth, so there is only a foreground colour (the text itself) and a background colour. By default, these are white and transparent. It doesn’t matter what colours you use in your sprite sheet, these are the ones you’re going to get.
In order to change the text colour, you need to use this function:
void graphics_set_color(uint32_t forecolor, uint32_t backcolor);
And here it is modifying the previous source code to give us some colour in between lines:
// Draw the letter 'A'
graphics_draw_character(disp, 5, 10, 'A');
// Change colour to purple
graphics_set_color(
graphics_make_color(0x3D, 0x17, 0x37, 255),
graphics_make_color(0, 0, 0, 255)
);
// Draw some static text
graphics_draw_text(disp, 5, 30, "Static text");
// Save formatted text to string buffer
sprintf(frameText, "Frame :%i", i++);
// Draw that string to screen
graphics_draw_text(disp, 5, 50, frameText);
// Change back to white
graphics_set_color(
0xFFFFFFFF,
graphics_make_color(0, 0, 0, 255)
);
Advanced text
Of course, the built-in methods for writing text to the N64 frame buffer work well, but you’re not always going to want plain monochromatic monospaced characters.
This section is TBD.
The last graphics function
There is just one other function that doesn’t really fit anywhere else.
void graphics_fill_screen (surface_t *disp, uint32_t c);
This one is really simple. It converts every pixel in our frame buffer to be the same colour. It’s useful for clearing the screen before drawing a frame so that the previous frame doesn’t persist. Otherwise, the above satellite image will appear like this, with the previous renderings still in place: