アフィン変換とはBGやスプライトに使われている機能です。全体を回転させたり、引き延ばしたような表示をしたり複雑な動きを行います。この解説はToncのThe Affine Transformation Matrixをベースに薄くした内容をお送りします。数学の素養を身に着けたい場合はGoogleで検索するとウェブサイトがでてきますのでそちらをご覧ください。
一言でいうと移動、回転、拡大縮小、せん断を同時に行える機能のことです。この変換がすごいのはたった6つのパラメータを決定するだけで実現できるということです。もちろん回転だけしたい、移動だけしたい場合も可能です。もう少し具体的にいうと画面x=120, y=80に移動して、回転を30度にして、キャラを2倍に拡大して、せん断(詳細は後述)する、というチャンポンができます。
いきなり数式が出てきましたねー。私は数学アレルギーなので気持ちはよくわかります。高校数学のツライ記憶・・・しくしく。まあ難しい話を避けて通るので概要だけ掴んでいって頂ければ幸いです。最悪、ライブラリ化して引数に投げる数字だけを着目するのも全然ありだと思います。中身を知らないことをライブラリ化する、ブラックボックス化するってわけですし。とりあえずスプライトの機能を使って6つのパラメータのレジスタがどのような役割を持っているか見ていきましょう。
平行移動の話です。画像64×64を始点0,0から120,80に移動する、この実現には2つのパラメータを使います。OBJ Attribute 0と1です。
typedef struct { u16 attr0; u16 attr1; u16 attr2; u16 dummy; } OBJATTR;
Bit Expl. 0-7 Y-Coordinate (0-255)
Bit Expl. 0-8 X-Coordinate (0-511)
OAMのPA, PB, PC, PDに対して上記図のように角度θ(angle)をcos(θ)、−sin(θ)、sin(θ)、cos(θ)にえいや!っと投げ入れます。この実現には4つのパラメータを使います。チュートリアルで説明した部分です。
typedef struct { u16 dummy0[3]; s16 pa; u16 dummy1[3]; s16 pb; u16 dummy2[3]; s16 pc; u16 dummy3[3]; s16 pd; } OBJAFFINE;
OBJAFFINE* rot = (OBJAFFINE*)OAM + num; rot->pa = GetCos(angle); rot->pb = GetSin(angle); rot->pc = -GetSin(angle); rot->pd = GetCos(angle);
OAMの縦の変化率がPAとPB、横の変化率がPCとPDです。かける値は倍率の逆数です。こちらもチュートリアルで説明した部分です。
倍率 | PA~PD |
200% | 128, 0, 0, 128 |
100% | 256, 0, 0, 256 |
50% | 512, 0, 0, 512 |
OBJAFFINE* rot = (OBJAFFINE*)OAM + num; rot->pa = Div(256 * 100, xsc); rot->pb = 0; rot->pc = 0; rot->pd = Div(256 * 100, ysc);
画像を平行四辺形に変化させることを言います。上記図のように擦りつぶれ気味な表示になります。pb, pcに対して値を入れると変化します。
OBJAFFINE* rot = (OBJAFFINE*)OAM + num; rot->pa = 256; rot->pb = x方向の増減; rot->pc = y方向の増減; rot->pd = 256;
最初に説明した6つのパラメータとはOBJ Attribute0,1のx,y、あとはpa~pdです。数学でいう行列の掛け算を行うように各パラメータの変化値をチャンポンします。
OBJ0x:そのまま使う OBJ1y:そのまま使う pa = pa回転 * pa拡大縮小 * paせん断 pb = pb回転 * pb拡大縮小 * pbせん断 pc = pc回転 * pc拡大縮小 * pcせん断 pd = pd回転 * pd拡大縮小 * pdせん断
Bit Expl. 0-7 Fractional portion (8 bits) 8-14 Integer portion (7 bits) 15 Sign (1 bit)
pa~pdは16bit単位です。整数部分と分数部分に着目してください。今までpa = 256などの訳のわからない数値は、16進数でいう0x0100であり整数部分の1を表していました。分数部分が8bitsであることはsin, cos関数などのdouble型が使えません。つまり自作しないといけません。以下に512個の16bit sinテーブルの生成コードを表します。整数部 4bits, 分数部分12 bitsとなります。私は数学者ではありませんのでこのテーブルの妥当性がわかりませんけれど、正しいと信じて使わせていただいています(^^;。
// Example sine lut generator #include <stdio.h> #include <math.h> #define M_PI_ 3.1415926535f #define SIN_SIZE 512 #define SIN_FP 12 int main() { FILE *fp= fopen("sinlut.c", "w"); fprintf(fp, "//\n// Sine lut; %d entries, LUT of 16bit values in 4.%d format.\n//\n\n", SIN_SIZE, SIN_FP); fprintf(fp, "const short sin_lut[%d] = \n{", SIN_SIZE); unsigned short hw; int i; for(i=0; i<SIN_SIZE; i++) { hw = (unsigned short)(sin(i * 2 * M_PI_ / SIN_SIZE) * (1 << SIN_FP)); if(i % 8 == 0) { fputs("\n\t", fp); } // 2023/05/09 fixed. i==128 is cos(0). hw:0xffff -> 0x1000. if(i == 128) { hw = 0x1000; } fprintf(fp, "0x%04X, ", hw); } fputs("\n};\n", fp); fclose(fp); return 0; }
操作は説明書を書かなければいけないほど膨大です。各ボタンを一通りお試しください。
START+SELECT | Reset |
START | Double-size TRUE or FALSE |
UP+SELECT | Y-Coordinate - 1 |
DOWN+SELECT | Y-Coordinate + 1 |
LEFT+SELECT | X-Coordinate - 1 |
RIGHT+SELECT | X-Coordinate + 1 |
A+SELECT | Scales x down |
A | Scales x up |
B+SELECT | Scales y down |
B | Scales y up |
LEFT | Shear x down |
RIGHT | Shear x up |
DOWN | Shear y down |
UP | Shear y up |
L | Rotates left |
R | R Rotates right |
#include "lib/gba.h" #include "irq.arm.h" #include "bg.h" #include "key.h" #include "spr.h" #include "math.h" //--------------------------------------------------------------------------- int main(void) { REG_WSCNT = 0x4317; IrqInit(); KeyInit(); BgInit(); SprInit(); s32 x = 240/2 - 64/2; s32 y = 160/2 - 64/2; s32 pa = 0x0100; s32 pb = 0x0000; s32 pc = 0x0000; s32 pd = 0x0100; s32 ta = pa; s32 tb = pb; s32 tc = pc; s32 td = pd; s32 angle = 0; bool isDouble = FALSE; // metro SprSetSize(0, OBJ_SIZE(3), OBJ_SQUARE, OBJ_16_COLOR); SprSetChr (0, 0); SprSetPal (0, 0); SprSetRotScale(0, 0, TRUE); // Shadow SprSetSize(1, OBJ_SIZE(3), OBJ_SQUARE, OBJ_16_COLOR); SprSetChr (1, 0); SprSetPal (1, 1); SprSetScaleRot(1, 0x0100, 0, 0, 0x0100); SprSetRotScale(1, 1, TRUE); for(;;) { VBlankIntrWait(); s32 ss = MathSin(angle); s32 cc = MathCos(angle); s32 sa = cc; s32 sb = -ss; s32 sc = ss; s32 sd = cc; pa = (ta*sa + tb*sc) >> 8; pb = (ta*sb + tb*sd) >> 8; pc = (tc*sa + td*sc) >> 8; pd = (tc*sb + td*sd) >> 8; SprSetScaleRot(0, pa, pb, pc, pd); SprSetXy(0, x, y); SprSetXy(1, x, y); SprSetDubleFlag(0, 0, isDouble); SprSetDubleFlag(1, 1, isDouble); BgAsciiDrawPrintf(1, 17, "P=|%4X %4X|", pa, pb); BgAsciiDrawPrintf(1, 18, " |%4X %4X|", pc, pd); KeyExec(); u16 cnt = KeyGetCnt(); u16 trg = KeyGetTrg(); // START+SELECT: Reset if((trg & KEY_START) && (trg & KEY_SELECT)) { x = 240/2 - 64/2; y = 160/2 - 64/2; pa = 0x0100; pb = 0x0000; pc = 0x0000; pd = 0x0100; ta = pa; tb = pb; tc = pc; td = pd; angle = 0; isDouble = FALSE; continue; } // START: Double-size flag if(trg & KEY_START) { isDouble = (isDouble == TRUE) ? FALSE : TRUE; continue; } // UP, DOWN, LEFT, RIGHT+SELECT: Move if((cnt & KEY_UP) && (cnt & KEY_SELECT)) { y--; continue; } if((cnt & KEY_DOWN) && (cnt & KEY_SELECT)) { y++; continue; } if((cnt & KEY_LEFT) && (cnt & KEY_SELECT)) { x--; continue; } if((cnt & KEY_RIGHT) && (cnt & KEY_SELECT)) { x++; continue; } // A+SELECT: Scales x down if((cnt & KEY_A) && (cnt & KEY_SELECT)) { ta += 4; continue; } // A: Scales x up if(cnt & KEY_A) { ta -= 4; continue; } // B+SELECT: Scales y down if((cnt & KEY_B) && (cnt & KEY_SELECT)) { td += 4; continue; } // A: Scales x up if(cnt & KEY_B) { td -= 4; continue; } // LEFT: Shear x down if(cnt & KEY_LEFT) { tb -= 4; continue; } // RIGHT: Shear x up if(cnt & KEY_RIGHT) { tb += 4; continue; } // DOWN: Shear y down if(cnt & KEY_DOWN) { tc -= 4; continue; } // UP: Shear y up if(cnt & KEY_UP) { tc += 4; continue; } // L: Rotates left if(cnt & KEY_L) { angle += 128; continue; } // R Rotates right if(cnt & KEY_R) { angle -= 128; continue; } } }