アフィン変換

アフィン変換とはBGやスプライトに使われている機能です。画像全体を回転させたり、引き延ばしたような表示をしたり複雑な動きを行います。この解説はToncのThe Affine Transformation Matrixをベースに薄くした内容をお送りします(汗。数学の素養を身に着けたい場合はGoogleで検索するとウェブサイトがでてきますのでそちらをご覧ください。

アフィン変換とは

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

2.png

いきなり数式が出てきましたね。私は数学アレルギーなので気持ちはよくわかります。まあ難しい話を避けて通るので概要だけ掴んでいって頂ければ幸いです。最悪、ライブラリ化して引数に投げる数字だけを着目するのも全然ありだと思います。中身を知らないことをライブラリ化する、ブラックボックス化するってわけですし。とりあえずスプライトの機能を使って6つのパラメータのレジスタがどのような役割を持っているか見ていきましょう。

平行移動(Identity)

平行移動の話です。画像64×64を始点0,0から120,80に移動する、この実現には2つのパラメータを使います。OBJ Attribute 0と1です。

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

回転(Rotation)

上記図のように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);

Scaling(拡大縮小)

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);

Shear(せん断)

画像を平行四辺形に変化させることを言います。上記図のように擦りつぶれ気味な表示になります。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;
}

スプライト表示

1.png

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

START+SELECTReset
STARTDouble-size TRUE or FALSE
UP+SELECTY-Coordinate - 1
DOWN+SELECTY-Coordinate + 1
LEFT+SELECTX-Coordinate - 1
RIGHT+SELECTX-Coordinate + 1
A+SELECTScales x down
AScales x up
B+SELECTScales y down
BScales y up
LEFTShear x down
RIGHTShear x up
DOWNShear y down
UPShear y up
LRotates left
RR Rotates right

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);
3.png

行列を計算すると以下になります。回転、拡大縮小、せん断のチャンポンの完成です。

パラメータ行列コード
pasx*cos + hx*sin(ta*sa + tb*sc) >> 8
pbsx*-sin + hx*cos(ta*sb + tb*sd) >> 8
pchy*cos + sy*sin(tc*sa + td*sc) >> 8
pdhy*-sin * sy*cos(tc*sb + td*sd) >> 8
 
2.png

出典元

履歴


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