GB101:Affine Sprites
GB101 Class Notes | |
---|---|
|
Affine Sprites[edit]
Remember that extra 16-bit short within our sprite definition? These control sprite affine transformation, or basically rotation and scaling. It's not really simply a rot/scale matrix, and don't let the TONC guy ever hear you say it is, but for our purposes we're going to treat it as such. First let's review the data structures..
typedef struct OBJ_ATTR
{
u16 attr0;
u16 attr1;
u16 attr2;
s16 fill;
} ALIGN4 OBJ_ATTR;
typedef struct OBJ_AFFINE
{
u16 fill0[3];
s16 pa;
u16 fill1[3];
s16 pb;
u16 fill2[3];
s16 pc;
u16 fill3[3];
s16 pd;
} ALIGN4 OBJ_AFFINE;
BJ_ATTR obj_buffer[128];
OBJ_AFFINE *const obj_aff_buffer= (OBJ_AFFINE*)obj_buffer;
So you're pointing these data structures at the same block of memory. It's really confusing to look at at first, but it goes like this:
Nintendo weaved this data together like this for mysterious reasons that nobody can seem to agree on. The TONC guy thinks it's for word alignment, I think it has to do with the occult. The end result is that we have 128 sprites and 32 affines.
Matrix Math[edit]
So this pa, pb, pc, and pd are parts of a matrix. This matrix makes up the affine transformation and includes rotation, scaling and shearing all at the same time. It looks like this:
<math> \begin{bmatrix} pa & pb \\ pc & pd \end{bmatrix} = \begin{bmatrix} \frac{\cos (\propto)}{s_x} & \frac{-\sin (\propto)}{s_x} \\ \frac{\sin (\propto)}{s_y} & \frac{\cos (\propto)}{s_y} \end{bmatrix} </math>
Don't get all bleary eyed, we're just going to use the libtonc functions to handle this for us..
// Set the OBJ_AFFINE to its identity (think of 0 for addition/subtraction and 1 for multiplication/division)
void obj_aff_identity(OBJ_AFFINE *oaff);
// Set scaling
void obj_aff_scale(OBJ_AFFINE *oaff, FIXED sx, FIXED sy);
// Set x shearing
void obj_aff_shearx(OBJ_AFFINE *oaff, FIXED hx);
// Set y shearing
void obj_aff_sheary(OBJ_AFFINE *oaff, FIXED hy);
// Set rotation (alpha is in sort of FIXED see below)
void obj_aff_rotate(OBJ_AFFINE *oaff, u16 alpha);
// Set rotation and scaling together
void obj_aff_rotscale(OBJ_AFFINE *oaff, FIXED sx, FIXED sy, u16 alpha);
Implementation[edit]
#include <tonc.h>
#include <string.h>
#include "link.h"
// The buffer to store the OAM entries between vblanks
OBJ_ATTR obj_buffer[128];
OBJ_AFFINE *obj_aff_buffer= (OBJ_AFFINE*)obj_buffer;
// This is our main loop
void obj_test() {
// our position (center screen)
int x = VID_WIDTH/2-16, y = VID_HEIGHT/2-16;
// tile id, palette bank
u32 tid = 128, pb = 0;
// Angle
int alpha = 0;
// Offset for angle, 1 binary radian..
int alpha_ofs = 1<<8;
// Scale, init to 1x.
FIXED scale = 1<<8;
// Ofset for scale
int scale_ofs = 4;
// A small string for debugging
char str[32];
// Get a pointer to the first OAM entry
OBJ_ATTR *link= &obj_buffer[0];
obj_set_attr(link,
ATTR0_SQUARE | ATTR0_AFF | ATTR0_AFF_DBL, // Affine ON, double-size mode
ATTR1_SIZE_16 | ATTR1_AFF_ID(0), // Use Affine 0
ATTR2_PALBANK(pb) | tid
);
obj_set_pos(link, x, y);
// Get our AFFINE for this sprite
OBJ_AFFINE *linkAff = &obj_aff_buffer[0];
// Set the identity of the sprite so we don't scale it out
// into infinity..
obj_aff_identity(linkAff);
// Initialize the OAM by copying everything
oam_copy(oam_mem, obj_buffer, 128);
while(1) {
// Wait for VBlank
vid_vsync();
// Rotate by 1 binary radian
alpha += 1<<8;
// Reset to zero brads if we go over 256
if (alpha > 256<<8)
alpha = 0;
// Scale by +/- 4/256ths
scale += scale_ofs;
// Bounce scale if it reaches 3x (64) or 0.25x (512)
if (scale > 2<<9 || scale < 1<<6) {
scale_ofs = -scale_ofs;
}
// Rotate and scale sprite.
obj_aff_rotscale(linkAff,scale,scale,alpha);
// Copy the bufferred OAM entries to the actual OAM memory
oam_copy(oam_mem, obj_buffer, 1);
// Update OBJ_AFFINEs separately
obj_aff_copy(obj_aff_mem, obj_aff_buffer, 1);
// Clear all the screenblock entries on page 31, essentially clearscreen for the text engine..
SBB_CLEAR(31);
// Format the text into a string
siprintf(str,"rot=%5d (%3d) scl=%5d (%1d)",alpha,alpha>>8,scale,scale>>8);
// Write out our debugging information
se_puts(0,0,str,0);
}
}
int main() {
// Load the link sprite into tile memory vram page 4
memcpy(&tile_mem[4][0], linkTiles, linkTilesLen);
// Load the palette memory into palette memory
memcpy(pal_obj_mem, linkPal, linkPalLen);
// Set up our OAM entry buffer
oam_init(obj_buffer, 128);
// Set mode 0, bg 0, sprites enabled, 1d sprites
REG_DISPCNT = DCNT_OBJ | DCNT_OBJ_1D | DCNT_MODE(0) | DCNT_BG0;
// Initialize the text engine
txt_init_std();
// Set up the text tiles on BG0
txt_init_se(0, BG_CBB(0)|BG_SBB(31), 0, CLR_RED, 1);
// Start the demo
obj_test();
return 0;
}
Some things to note[edit]
Angles in TONC[edit]
TONC represents angles as fixed point, but not as standard radians. Instead a full 360-degrees is 256.256, or 65535. It's more convenient for the computer to think of an entire circle as 216 instead of 2π. To convert to radians use α*π/32768. I don't know what the term for this unit is, but I'm calling it a binary radian.
Scaling is not what you expect[edit]
The GBA generally does things from the opposite view from you. It's not scaling the pixels into the video it's halving the pixels from the tile memory. As a result the scaling (and lots of other things) work backwards. Values above 1 (256 in fixed point) will shrink the sprite and values below 256 will grow the sprite.
Also notice in this demo that we're using the double size sprite flag. This makes the clipping area for the sprite twice as large (32x32 in this case) but if you scale too large you'll get clipping anyway.
Debugging and writing text[edit]
Eventually you'll need to get some more information about what's going on, or wrong. There's no native way to print anything to the screen, nor any sort of open-source debuggger (there are some good memory inspection tools in some emulators). The easiest way to debug is to use a text writing library, like the one included with tonclib. Just enable an extra background (BG0 is the highest level) and use the txt_init_* methods to initialize everything. TONC is set up for backgrounds that use 16 x 16-color palettes instead of the single 256-color palette we've used in the past, so you may have to play with the last value of tonc_init_se to keep it from trashing your palette.