#author("2023-05-10T16:18:52+09:00","","")
#author("2023-05-10T19:18:09+09:00","","")
* アフィン変換 [#yf70394b]
アフィン変換とはBGやスプライトに使われている機能です。全体を回転させたり、引き延ばしたような表示をしたり複雑な動きを行います。この解説はToncの[[The Affine Transformation Matrix:https://www.coranac.com/tonc/text/affine.htm]]をベースに薄くした内容をお送りします。数学の素養を身に着けたい場合はGoogleで検索するとウェブサイトがでてきますのでそちらをご覧ください。

** アフィン変換とは [#w07774a2]
一言でいうと移動、回転、拡大縮小、せん断を同時に行います。この変換がすごいのはたった6つのパラメータ数値を決定するだけで各状態を実現できる、ということです。もちろん回転だけしたい、移動だけしたい一部の起動だけ使いたい場合も可能です。具体的には画面の120,80に移動して、回転を30度にして、キャラを2倍に拡大して、せん断(詳細は後述)する、ということです。
一言でいうと移動、回転、拡大縮小、せん断を同時に行える機能のことです。この変換がすごいのはたった6つのパラメータを決定するだけで実現できるということです。もちろん回転だけしたい、移動だけしたい場合も可能です。もう少し具体的にいうと画面x=120, y=80に移動して、回転を30度にして、キャラを2倍に拡大して、せん断(詳細は後述)する、というチャンポンができます。

#ref(1.png,nolink)
#ref(2.png,nolink)

構成は以下の通りです。mode0で全て16色。bg0:ASCII文字(256x256)、bg1:マスク(256x256)、bg2:ステージ(512x256)となっておりステージのみ512x256です。
いきなり数式が出てきましたねー。私は数学アレルギーなので気持ちはよくわかります。高校数学のツライ記憶・・・しくしく。まあ難しい話を避けて通るので概要だけ掴んでいって頂ければ幸いです。最悪、ライブラリ化して引数に投げる数字だけを着目するのも全然ありだと思います。中身を知らないことをライブラリ化する、ブラックボックス化するってわけですし。とりあえずスプライトの機能を使って6つのパラメータのレジスタがどのような役割を持っているか見ていきましょう。

 	REG_BG2HOFS = 120;
 	REG_BG2VOFS = 64;
** 平行移動(Identity) [#fa22c0ff]
平行移動の話です。画像64×64を始点0,0から120,80に移動する、この実現には2つのパラメータを使います。OBJ Attribute 0と1です。

 typedef struct {
 	u16 attr0;
 	u16 attr1;
 	u16 attr2;
 	u16 dummy;
 } OBJATTR;

- OBJ Attribute 0 (R/W)
   Bit   Expl.
   0-7   Y-Coordinate           (0-255)

- OBJ Attribute 1 (R/W)
   Bit   Expl.
   0-8   X-Coordinate           (0-511)

** 回転(Rotation) [#p894df2d]
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;
 
 	REG_WIN0H  = 0;
 	REG_WIN0V  = SCREEN_HEIGHT;
 	REG_WININ  = WIN_0_BG0 | WIN_0_BG2;
 	REG_WINOUT = WIN_0_BG0 | WIN_0_BG1;
 rot->pa =  GetCos(angle);
 rot->pb =  GetSin(angle);
 rot->pc = -GetSin(angle);
 rot->pd =  GetCos(angle);

ステージ表示には始点座標を変えています。さらにウィンドウの内部と外部にどれを表示させるかの設定を加えています。
** Scaling(拡大縮小) [#db364f35]
OAMの縦の変化率がPAとPB、横の変化率がPCとPDです。かける値は倍率の逆数です。こちらもチュートリアルで説明した部分です。

 /* Create an array of horizontal offsets for a circular window.
 *	The offsets are to be copied to REG_WINxH each HBlank, either 
 *	by HDMA or HBlank isr. Offsets provided by modified 
 *	Bresenham's circle routine (of course); the clipping code is not
 *	optional.
 *
 *	\param x0	X-coord of circle center.
 *	\param y0	Y-coord of circle center.
 *	\param rr	Circle radius.
 */
| 倍率 | PA~PD         |
| 200% | 128, 0, 0, 128 |
| 100% | 256, 0, 0, 256 |
|  50% | 512, 0, 0, 512 |

 OBJAFFINE* rot = (OBJAFFINE*)OAM + num;
 
 u16 BgWinh[SCREEN_HEIGHT+1] ALIGN(4);
 rot->pa = Div(256 * 100, xsc);
 rot->pb = 0;
 rot->pc = 0;
 rot->pd = Div(256 * 100, ysc);

** Shear(せん断) [#e736ac65]
画像を平行四辺形に変化させることを言います。上記図のように擦りつぶれ気味な表示になります。pb, pcに対して値を入れると変化します。

 OBJAFFINE* rot = (OBJAFFINE*)OAM + num;
 
 IWRAM_CODE void BgCreateWindowCircleDma(s32 x0, s32 y0, s32 rr)
 rot->pa = 256;
 rot->pb = x方向の増減;
 rot->pc = y方向の増減;
 rot->pd = 256;

**  パラメータの正体 [#a8e363d7]
最初に説明した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せん断

- pa, pb, pc, 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()
 {
 	// Zero clear
 	for(vs32 i=0; i<SCREEN_HEIGHT+1; i++)
 	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++)
 	{
 		BgWinh[i] = 0;
 		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);
 
 	s32 x=0, y=rr, d=1-rr;
 	u32 tmp;
 	return 0;
 }
 
 	while(y >= x)
** スプライト表示 [#d74b9ebd]

-[[github:https://github.com/akkera102/gbadev-ja/tree/main/doc22%20%E3%82%A2%E3%83%95%E3%82%A3%E3%83%B3%E5%A4%89%E6%8F%9B]]
#ref(1.png,nolink)

操作は説明書を書かなければいけないほど膨大です。各ボタンを一通りお試しください。

| 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(;;)
 	{
 		// Side octs
 		tmp  = BgClamp(x0 + y, 0, SCREEN_WIDTH+1);
 		tmp += BgClamp(x0 - y, 0, SCREEN_WIDTH+1) << 8;
 		VBlankIntrWait();
 
 		// o4, o7
 		if(BgInRange(y0-x, 0, SCREEN_HEIGHT))
 
 		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))
 		{
 			BgWinh[y0 - x]= tmp;
 			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;
 		}
 
 		// o0, o3
 		if(BgInRange(y0+x, 0, SCREEN_HEIGHT))
 		// START: Double-size flag
 		if(trg & KEY_START)
 		{
 			BgWinh[y0 + x]= tmp;
 			isDouble = (isDouble == TRUE) ? FALSE : TRUE;
 			continue;
 		}
 
 		// Change in y: top/bottom octs
 		if(d >= 0)
 		// UP, DOWN, LEFT, RIGHT+SELECT: Move
 		if((cnt & KEY_UP)    && (cnt & KEY_SELECT))
 		{
 			tmp  = BgClamp(x0 + x, 0, SCREEN_WIDTH+1);
 			tmp += BgClamp(x0 - x, 0, SCREEN_WIDTH+1) << 8;
 			y--;
 			continue;
 		}
 
 			// o5, o6
 			if(BgInRange(y0-y, 0, SCREEN_HEIGHT))
 			{
 				BgWinh[y0 - y]= tmp;
 			}
 		if((cnt & KEY_DOWN)  && (cnt & KEY_SELECT))
 		{
 			y++;
 			continue;
 		}
 
 			// o1, o2
 			if(BgInRange(y0+y, 0, SCREEN_HEIGHT))
 			{
 				BgWinh[y0 + y]= tmp;
 			}
 		if((cnt & KEY_LEFT)  && (cnt & KEY_SELECT))
 		{
 			x--;
 			continue;
 		}
 
 			d -= 2 * (--y);
 		if((cnt & KEY_RIGHT) && (cnt & KEY_SELECT))
 		{
 			x++;
 			continue;
 		}
 
 		d += 2 * (x++) + 3;
 	}
 		// 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;
 		}
 
 /*
 	for(vs32 i=0; i<SCREEN_HEIGHT+1; i++)
 	{
 		TRACE("%d: %4x\n", i, BgWinh[i]);
 	}
 	for(;;){}
 */
 		// 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;
 		}
 
 	REG_DMA3CNT = 0;
 	REG_DMA3SAD = (u32)&BgWinh[1];
 	REG_DMA3DAD = (u32)&REG_WIN0H;
 	REG_DMA3CNT = 1 | (DMA_DST_RELOAD | DMA_REPEAT | DMA_HBLANK | DMA_ENABLE);
 }
 //---------------------------------------------------------------------------
 IWRAM_CODE s32 BgClamp(s32 val, s32 min, s32 max)
 {
 	if(val < min)
 	{
 		return min;
     }
 
 	if(val > max)
 	{
 		return max;
 		// 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;
 		}
 	}
 
 	return val;
 }
 //---------------------------------------------------------------------------
 IWRAM_CODE bool BgInRange(s32 x, s32 min, s32 max)
 {
 	return ((x)>=(min)) && ((x)<(max)) ? TRUE : FALSE;
 }

注目してほしいのは、以下の部分でHBLANK毎にDMAを発生させていることです。GBAの画面の高さは160ですので160回HBLANKを発生させています。1ライン分のウィンドウサイズを2バイト、REG_WIN0Hに格納しています。チラツキもなくハードウェア機能で実現しており非常に芸術点が高いです。

 	REG_DMA3CNT = 0;
 	REG_DMA3SAD = (u32)&BgWinh[1];
 	REG_DMA3DAD = (u32)&REG_WIN0H;
 	REG_DMA3CNT = 1 | (DMA_DST_RELOAD | DMA_REPEAT | DMA_HBLANK | DMA_ENABLE);

** 出典元 [#pb4ac969]
- Tonc

** 履歴 [#j0abcb62]
- 2023/04/30
- 2023/05/10


トップ   一覧 検索 最終更新   ヘルプ   最終更新のRSS