アフィン変換とは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にcos(θ)、−sin(θ)、sin(θ)、cos(θ)をえいや!っと投げ入れます。パラメータは4つ。PA〜PDはチュートリアルで説明した部分です。
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, pb, pc, 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関数などの標準関数が使えません。つまりBIOSを使うか自作しないといけません。以下に、512個の16bit sinテーブルの生成コードを表します。整数部4bits, 分数部分12bitsとなります。私は数学者ではありませんのでこのテーブルの妥当性がわかりませんけれど、いい感じに計算してくれてると信じて使わせていただいています(^^;。
// 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; } if(i == 384) { hw = 0xF000; } 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) { if(isDouble == FALSE) { x -= 64/2; y -= 64/2; isDouble = TRUE; } else { x += 64/2; y += 64/2; isDouble = FALSE; } 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; } } }
x,yについてはSprSetXy関数を使います。これはわかりやすいですね。一方のpa,pb,pc,pdについては複雑です。
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);
行列を計算すると以下になります。回転、拡大縮小、せん断のチャンポンの完成です。
パラメータ | 行列 | コード |
pa | sx*cos + hx*sin | (ta*sa + tb*sc) >> 8 |
pb | sx*-sin + hx*cos | (ta*sb + tb*sd) >> 8 |
pc | hy*cos + sy*sin | (tc*sa + td*sc) >> 8 |
pd | hy*-sin * sy*cos | (tc*sb + td*sd) >> 8 |