Advanced Sprite Blitting
While the previous tutorial might have shown how to draw a basic sprite to the frame buffer, Libdragon’s RDPQ does allow for some more settings for advanced blit techniques that let you customise the way that sprites are rendered.
Blit settings
The main way to display sprites is by blitting them to the frame buffer. Blitting (Block Image Transfer) is basically how a block of pixels is transferred from one place in memory to another.
Without any settings, the sprite will be copied over to the frame buffer as-is. The only way to change anything (besides making a new sprite for every permutation) is to use the settings parameter of rdpq_sprite_blit()
. Here is the struct definition:
typedef struct rdpq_blitparms_s {
rdpq_tile_t tile;
int s0;
int t0;
int width;
int height;
bool flip_x;
bool flip_y;
int cx;
int cy;
float scale_x;
float scale_y;
float theta;
bool filtering;
int nx;
int ny;
} rdpq_blitparms_t;
rdpq_tile_t tile
is a tile number from TILE0
to TILE7
. The way the RDP handles textures in TMEM is that each texture has a tile number associated to it, kind of like a pointer to the start of where the texture is located. This setting is mostly useful when you have some manually-assigned textures that you want to preserve while calling this function.
If you only want to display a sub-area of a sprite (eg as part of a sprite sheet), you can use int width
and int height
to determine the size of the sub-area to be blitted, and then int s0
and int t0
to determine the offset. Values must be logical – you can’t select a subsection that overflows the sprite area.
Here is an example of using a sprite sheet and switching between a bunch of 16×16 sub-sprites:
// Change sprite every few seconds
if (t > 5) {
params.s0 = (params.s0 + 16) % brick_sprite->width;
params.t0 = (params.t0 + 16) % brick_sprite->height;
}
t = t > 5 ? 0 : t + 0.05;
bool flip_x
and bool flip_y
are boolean values that determine if the sprite should be flipped horizontally or vertically. Useful if you are using sprites that have symmetry or if you want to make a character ‘look’ in another direction.
Here is an example of flipping the sprite horizontally and vertically.
// Flip the sprite every few seconds
params.flip_x = t>5;
params.flip_y = t>2.5 && t<7.5;
t = t > 10 ? 0 : t + 0.05;
int cx
and int cy
are the centre of transformation for all the transformation options (scale_x
, scale_y
and theta
) in the rdpq_blitparms_t
. It starts at 0,0, which is the top-left corner of the sprite.
float scale_x
and float scale_y
stretch and shrink the sprite about the transformation centre. A value of 0 does nothing and a negative value will flip it.
float theta
will rotate the sprite clockwise by its value in radians about the transformation centre.
// Rotate the sprite by 2*pi radians about its centre
params.cx = params.cy = 8;
params.theta = t;
t = t > 6.3 ? 0 : t + 0.05;
// Grow the sprite every frame about its centre
params.cx = params.cy = 8;
params.scale_x = params.scale_y = t;
t = t > 10 ? 1 : t + 0.05;
bool filtering
enables texture filtering.
int nx
and int ny
set the max amount of repetitions of a repeating sprite. Values of 0 and 1 will show the sprite only once. I don’t think this one works properly,
Basic rendering modes
You can change the way that sprites are rendered by changing the mode used in the RDP.
General modes
Here is what happens when you try to draw a sprite while in standard
, copy
and fill
mode respectively:
rdpq_set_mode_standard();
rdpq_sprite_blit(brick_sprite, display_get_width()/4-8, display_get_height()/2-8, ¶ms);
rdpq_set_mode_copy(true);
rdpq_sprite_blit(brick_sprite, display_get_width()/2-8, display_get_height()/2-8, ¶ms);
rdpq_set_mode_fill(RGBA32(0x00, 0x00, 0x99, 0));
rdpq_sprite_blit(brick_sprite, display_get_width()*3/4-8, display_get_height()/2-8, ¶ms);
As you can see, there is no difference with standard
and copy
mode, but using fill
mode will just fill the sprite area with a solid colour.
Now let’s try doing the same, but applying some scaling and rotation.
As you can see, the standard
and fill
modes work as expected, but there is some severe distortion when using the copy
mode to render the sprite.
Summary:
- Use
standard
mode when you want to have the most flexibility.- Has advanced modes (filtering, dithering, mipmapping etc)
- Allows scaling and rotation
- Use
copy
mode when you want to display sprites simply- Is 4x as fast as standard mode
- Only works in 16 bits-per-pixel display mode
- Use
fill
mode when you just want to draw a solid colour.
Advanced rendering modes
Now that we’ve identified the use cases for copy
mode and fill
mode, let’s focus on how we can enhance the standard
rendering mode.
Filtering
For starters, we can add a filter on top of the sprite to change the way it is rendered by using the rdpq_mode_filter()
function. This function takes in one parameter which can be one of three different modes: FILTER_POINT
(default), FILTER_BILINEAR
or FILTER_MEDIAN
.
Let’s have a look at each one of them in action:
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_POINT);
rdpq_sprite_blit(brick_sprite, display_get_width()/4-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_sprite_blit(brick_sprite, display_get_width()/2-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_MEDIAN);
rdpq_sprite_blit(brick_sprite, display_get_width()*3/4-8, display_get_height()/2-8, &parms);
As you can see, FILTER_POINT
provides a nearest-neighbour style of enlarging while both FILTER_BILINEAR
and FILTER_MEDIAN
provide the exact same filter, which I believe to be just bilinear.
Basically, use FILTER_POINT
when you want something to have that blocky NES-style of graphics, and use FILTER_BILINEAR
for all others.
Blending
Blending is the process of combining the color of a source pixel with the color of a destination pixel to produce a new color, typically based on their respective transparency.In Libdragon, there are two blend modes, RDPQ_BLENDER_MULTIPLY
, RDPQ_BLENDER_MULTIPLY_CONST
and RDPQ_BLENDER_ADDITIVE
.
RDPQ_BLENDER_MULTIPLY
multiplies the values of each colour value of each pixel to get the target colour. This is the standard method for blending, and is usually what you’d expect when working with transparencies.
RDPQ_BLENDER_MULTIPLY_CONST
works in the same way, but instead blends using the alpha value in the fog colour. It’s useful for adding transparency to sprites that do not have an alpha channel.
RDPQ_BLENDER_ADDITIVE
adds the colour values of the source sprite and destination frame buffer. It is probably faster than using multiplication, but the effect isn’t as good and if the sum overflows it can cause some weird effects including negative colours.
Note that in the following example, the same sprite is being used for each blending mode which is just the same brick sprite but at 50% opacity, all being displayed on a black background that turns white. The 2nd sprite appears more opaque because the fog alpha is set to 75%. Also note that the additive formula works well with lower (darker) colours but starts exhibiting strange behaviour as the background turns white.
// Make the background change colour
rdpq_set_mode_fill(RGBA32(0xFF*t, 0xFF*t, 0xFF*t, 0));
rdpq_fill_rectangle(0, 0, display_get_width(), display_get_height());
t = t >= 1 ? 0 : t + 0.01;
rdpq_set_mode_standard();
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
rdpq_sprite_blit(brick_sprite, display_get_width()/4-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_set_fog_color(RGBA32(255, 255, 255, 191));
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY_CONST);
rdpq_sprite_blit(brick_sprite, display_get_width()/2-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_blender(RDPQ_BLENDER_ADDITIVE);
rdpq_sprite_blit(brick_sprite, display_get_width()*3/4-8, display_get_height()/2-8, &parms);
Here’s a couple of examples of the two blending methods when different objects overlap:
Basically, it’s best to leave it at the default setting of RDPQ_BLENDER_MULTIPLY
unless you want to programmatically set the sprite’s opacity. In that case, use RDPQ_BLENDER_MULTIPLY_CONST
.
Anti-aliasing
Anti-aliasing is a technique used to reduce the jagged edges (aliasing) in images by smoothing or blending the colors at the boundaries of objects. You can change the anti-alias mode in Libdragon by using the rdpq_mode_antialias()
function.
In the case of drawing sprites to the screen, there are three options for rendering them with this function. AA_NONE
, AA_STANDARD
and AA_REDUCED
.
rdpq_set_mode_standard();
rdpq_mode_antialias(AA_NONE);
rdpq_sprite_blit(brick_sprite, display_get_width()/4-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_antialias(AA_STANDARD);
rdpq_sprite_blit(brick_sprite, display_get_width()/2-8, display_get_height()/2-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_antialias(AA_REDUCED);
rdpq_sprite_blit(brick_sprite, display_get_width()*3/4-8, display_get_height()/2-8, &parms);
You don’t really need anti-aliasing if you’re not working with rotated sprites. Even then, the effect is pretty minimal and the anti-aliasing is only active on the outer edges of the sprite and not the interior.
Dithering
Dithering is used to simulate different shades of color by placing pixels of different colors close together, creating the illusion of a smoother gradient. The RDPQ library has four dithering modes which can be set for both the RGB and alpha channels. The first term in each mode is for RGB and the second is for alpha, eg DITHER_SQUARE_NOISE
will dither RGB with a custom ‘square’ algorithm, and then dither the alpha channel with the ‘random’ algorithm.
Here are all the possible settings that you can set by using the rdpq_mode_dithering()
function:
- DITHER_SQUARE_SQUARE
- DITHER_SQUARE_INVSQUARE
- DITHER_SQUARE_NOISE
- DITHER_SQUARE_NONE
- DITHER_BAYER_BAYER
- DITHER_BAYER_INVBAYER
- DITHER_BAYER_NOISE
- DITHER_BAYER_NONE
- DITHER_NOISE_SQUARE
- DITHER_NOISE_INVSQUARE
- DITHER_NOISE_NOISE
- DITHER_NOISE_NONE
- DITHER_NONE_BAYER
- DITHER_NONE_INVBAYER
- DITHER_NONE_NOISE
- DITHER_NONE_NONE
Here are some examples of using the different RGB dithering modes. Left is how they appear normally with bilinear filtering, and the right one is enhanced to exaggerate the dithering pattern.
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_NONE_NONE);
rdpq_sprite_blit(brick_sprite, display_get_width()*1/5, display_get_height()*1/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_SQUARE_NONE);
rdpq_sprite_blit(brick_sprite, display_get_width()*2/5, display_get_height()*1/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_BAYER_NONE);
rdpq_sprite_blit(brick_sprite, display_get_width()*3/5, display_get_height()*1/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_NOISE_NONE);
rdpq_sprite_blit(brick_sprite, display_get_width()*4/5, display_get_height()*1/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_NONE_NONE);
rdpq_sprite_blit(gradient_sprite, display_get_width()*1/5, display_get_height()*2/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_SQUARE_NONE);
rdpq_sprite_blit(gradient_sprite, display_get_width()*2/5, display_get_height()*2/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_BAYER_NONE);
rdpq_sprite_blit(gradient_sprite, display_get_width()*3/5, display_get_height()*2/3-8, &parms);
rdpq_set_mode_standard();
rdpq_mode_filter(FILTER_BILINEAR);
rdpq_mode_dithering(DITHER_NOISE_NONE);
rdpq_sprite_blit(gradient_sprite, display_get_width()*4/5, display_get_height()*2/3-8, &parms);
Alpha compare
Alphacompare is a mode that filters out the parts of a sprite that have an alpha value lower than a certain threshold. It’s useful if you want to create ‘disappearing’ effects to simulate fog, to create stencils or to improve performance by rendering fewer pixels.
Libdragon uses the rdpq_mode_alphacompare()
function, which has one int parameter. This number is the threshold of how much to filter, on a range of 0-255. Negative numbers instead render the sprite with an alpha dithering effect, which can reduce the number of pixels that are blended, at the cost of looking a bit wonky.
rdpq_set_mode_standard();
rdpq_mode_blender(RDPQ_BLENDER_MULTIPLY);
rdpq_mode_alphacompare(0);
rdpq_sprite_blit(gradient_sprite, display_get_width()*1/5, display_get_height()/2-8, &parms);
rdpq_mode_alphacompare(50);
rdpq_sprite_blit(gradient_sprite, display_get_width()*2/5, display_get_height()/2-8, &parms);
rdpq_mode_alphacompare(200);
rdpq_sprite_blit(gradient_sprite, display_get_width()*3/5, display_get_height()/2-8, &parms);
rdpq_mode_alphacompare(-75);
rdpq_sprite_blit(gradient_sprite, display_get_width()*4/5, display_get_height()/2-8, &parms);
Unused modes
There are some RDP modes that aren’t used in 2D sprite graphics:
rdpq_mode_combiner()
is used to combine textures, shades and lighting. Useful for 3D graphics, but not for this.rdpq_mode_fog()
activates and deactivates fog. Again, this is useful for 3D rendering, but not for 2D.rdpq_mode_zbuf()
activates the z-buffer, which isn’t needed for 2D sprites.rdpq_mode_zoverride()
writes a specific value to the z-buffer instead of the predetermined one.
And here are some that are used , but mostly just for handling textures in non-blit sprite drawing:
rdpq_mode_tlut()
is used to set the texture look up table moderdpq_mode_mipmap()
determines the mipmap mode and the number of levels used