ビットマップを回転させる方法にはアフィン変換
x = cos*x-sin*y
y = sin*x+cos*y (…だっけ?)
を使う方法と、2D ポリゴンにテクスチャを貼ってポリゴンを回転させる 方法があります。 アフィン変換の方は参考文献も多いのでここではテクスチャマッピングを使う方法を 説明します。
テクスチャマッピングといってもそんなに難しい計算はなくて 実は簡単な四則演算のみで実現する事が出来ます。基本は
「テクスチャ画像(回転させたい元のビットマップ)を斜め読みして (描画画面の)ポリゴンに上のラインから(ライン毎に)順に描画していく」
ことです(そういえばベーマガの 99/7 月号であの有名な呉ソフトの呉英二氏が これと同じ事をコラムで書いていました)。
具体的には、まずポリゴンの座標データ(下のソースの POLYDATA 構造体)から ポリゴンの各辺(エッジ)の(描画画面における) x 座標を上のラインから順に スキャンして求めます。同時にその求めた(描画画面における)座標に対応 するテクスチャ画像上での (x,y) 座標もスキャンしていきます。 これを行うのが下の VOID ScanLine() 関数で、 スキャンした結果は SCANDATA 構造体の配列(サイズは描画画面の縦ピクセル数) に入れていきます。 あとはそのスキャンした結果を使ってテクスチャデータを「斜め読み」 して描画画面に上からライン毎に描画して行くだけです(これを行うのが VOID DrawPolygon() です)。
まあ詳しくはソースを参考にして下さい(手抜き(^^;)。 また、C マガジンの 99年 5 月号(か 6 月号のどっちか)にも 詳しい説明が載っています。
なお 380x160 の test.bmp ビットマップは自前で用意して下さい。 z,x で回転、カーソルキーで移動します。また右クリックすると計 算回数のテストを行います。それとリンクに winmm.lib を追加しておいて下さい。
ところで今回は VOID DrawPolygon() のテクスチャ画像の「斜め読み」 の部分で double 型を使って計算してました。具体的には
double tx,ty,tdx,tdy; // テクスチャ座標計算用
の部分です。double 型を使うと当然遅くなり、MMX150の マシンでは 22 FPS しか計算できません。そこで次回は 「斜め読み」の計算部分を固定小数点化して高速化を図ろうと思います。
// 2D ポリゴンテクスチャサンプル 99/6/17
// (C) tokai@fides.dti.ne.jp
#include < windows.h>
#include < math.h>
#define BITWIDTH 388 // ビットマップ幅
#define BITHEIGHT 160 // ビットマップ高さ
#define WINWIDTH 640 // クライアント領域幅
#define WINHEIGHT 320 // 高さ
#define TESTTIME 10000 // テストする時間
#define POLYGONPOINT 4 // ポリゴンの頂点数
#define PI 3.14159265358979324
// ポリゴンデータ
typedef struct
{
LONG x[POLYGONPOINT]; // 各頂点の座標
LONG y[POLYGONPOINT];
LONG tx[POLYGONPOINT]; // 各頂点に対応するテクスチャ画像上の座標
LONG ty[POLYGONPOINT];
}POLYDATA,*LPPOLYDATA;
// ポリゴンのエッジ座標
typedef struct
{
LONG minX; // 左側
LONG maxX; // 右側
LONG minTx; // 点(minX,y) に対応するテクスチャ座標での x 座標
LONG minTy; // 点(minX,y) に対応するテクスチャ座標での y 座標
LONG maxTx; // 点(maxX,y) に対応するテクスチャ座標での x 座標
LONG maxTy; // 点(maxX,y) に対応するテクスチャ座標での y 座標
}SCANDATA,*LPSCANDATA;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL Create256DIB(HWND,HBITMAP*,LPBITMAPINFO*,LPBYTE*,LONG,LONG);
VOID PasteBitmapToDIB(HDC,HINSTANCE,HBITMAP,LPSTR);
char szClassName[] = "POLY2D";
VOID ScanLine(LPPOLYDATA,LPSCANDATA,LONG,LONG,LPLONG,LPLONG);
VOID DrawPolygon(LPPOLYDATA,LPSCANDATA,LPBYTE,LONG,LONG,LPBYTE,LONG,LONG);
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPreInst,
LPSTR lpszCmdLine,int nCmdShow)
{
HWND hWnd;
MSG lpMsg;
WNDCLASS wndClass;
// ウィンドウ登録
if (!hPreInst) {
wndClass.style=CS_HREDRAW|CS_VREDRAW;
wndClass.lpfnWndProc=WndProc;
wndClass.cbClsExtra=0;
wndClass.cbWndExtra=0;
wndClass.hInstance=hInst;
wndClass.hIcon= NULL;
wndClass.hCursor=LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
wndClass.lpszMenuName=NULL;
wndClass.lpszClassName=szClassName;
if (!RegisterClass(&wndClass)) return FALSE;
}
// ウィンドウ作成
RECT rt;
rt.left = 0;rt.right = WINWIDTH;
rt.top = 0;rt.bottom = WINHEIGHT;
AdjustWindowRect(&rt,WS_OVERLAPPEDWINDOW,FALSE);
hWnd = CreateWindow(szClassName,
"2D ポリゴン",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
rt.right-rt.left,rt.bottom-rt.top,NULL,NULL,hInst,NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
// メッセージループ
while (GetMessage(&lpMsg, NULL, 0, 0)) {
TranslateMessage(&lpMsg);
DispatchMessage(&lpMsg);
}
return (lpMsg.wParam);
}
// プロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
static HINSTANCE hInst;
HDC hDC;
DWORD i;
static POLYDATA polyData={
0,BITWIDTH,BITWIDTH,0,
0,0,BITHEIGHT,BITHEIGHT,
0,BITWIDTH,BITWIDTH,0,
0,0,BITHEIGHT,BITHEIGHT
};
static SCANDATA scanData[WINHEIGHT];
static LONG x[POLYGONPOINT]={0,BITWIDTH,BITWIDTH,0};
static LONG y[POLYGONPOINT]={0,0,BITHEIGHT,BITHEIGHT};
static LONG nTheta = 30;
static LONG xx = 300 ,yy = 150;
double prex,prey,theta;
// 描画画面
static HDC hBufDC;
static HBITMAP hBufDib;
static LPBYTE lpBufDibByte;
static LPBITMAPINFO lpBufDibBitInfo;
// DIB
static HBITMAP hBitmapDib;
static LPBYTE lpDibByte;
static LPBITMAPINFO lpDibBitInfo;
switch (msg) {
case WM_CREATE: // 初期化
// インスタンス取得
hInst = (HINSTANCE)GetWindowLong(hWnd,GWL_HINSTANCE);
// DIB 作成
Create256DIB(hWnd,&hBufDib,&lpBufDibBitInfo,&lpBufDibByte,WINWIDTH,WINHEIGHT);
Create256DIB(hWnd,&hBitmapDib,&lpDibBitInfo,&lpDibByte,BITWIDTH,BITHEIGHT);
// 描画画面作成
hDC = GetDC(hWnd);
hBufDC = CreateCompatibleDC(hDC);
SelectObject(hBufDC,hBufDib);
ReleaseDC(hWnd,hDC);
// DIB にビットマップコピー(test.bmp)
PasteBitmapToDIB(hBufDC,hInst,hBitmapDib,"test.bmp");
// 画面クリア
FillMemory(lpBufDibByte,WINWIDTH*WINHEIGHT,0);
// 座標セット
for(i = 0; i< POLYGONPOINT ; i++){
theta = nTheta*PI/180;
prex = (double)x[i]-BITWIDTH/2;
prey = (double)y[i]-BITHEIGHT/2;
polyData.x[i] = (LONG)(prex*cos(theta)-prey*sin(theta)) + xx;
polyData.y[i] = (LONG)(prex*sin(theta)+prey*cos(theta)) + yy;
}
// ポリゴン描画
DrawPolygon(&polyData,scanData,lpBufDibByte,WINWIDTH,WINHEIGHT,lpDibByte,BITWIDTH,BITHEIGHT);
break;
case WM_PAINT: // 描画(再描画のみ)
PAINTSTRUCT ps;
hDC = BeginPaint(hWnd,&ps);
BitBlt(hDC,ps.rcPaint.left,ps.rcPaint.top,
ps.rcPaint.right-ps.rcPaint.left,
ps.rcPaint.bottom-ps.rcPaint.top,
hBufDC,ps.rcPaint.left,ps.rcPaint.top,SRCCOPY);
ReleaseDC(hWnd,hDC);
break;
// キー入力
case WM_KEYDOWN:
switch((int)wp){
case 'Z':
nTheta -= 5;
break;
case 'X':
nTheta += 5;
break;
case VK_UP:
yy-=5;
break;
case VK_DOWN:
yy+=5;
break;
case VK_LEFT:
xx-=5;
break;
case VK_RIGHT:
xx+=5;
break;
}
if(nTheta < 0) nTheta = 359;
else if(nTheta > 360) nTheta -= 360;
// 座標セット
for(i = 0; i< POLYGONPOINT ; i++){
theta = nTheta*PI/180.;
prex = (double)x[i]-BITWIDTH/2;
prey = (double)y[i]-BITHEIGHT/2;
polyData.x[i] = (LONG)(prex*cos(theta)-prey*sin(theta)) + xx;
polyData.y[i] = (LONG)(prex*sin(theta)+prey*cos(theta)) + yy;
}
// 画面消去
FillMemory(lpBufDibByte,WINWIDTH*WINHEIGHT,0);
// ポリゴン描画
DrawPolygon(&polyData,scanData,lpBufDibByte,WINWIDTH,WINHEIGHT,lpDibByte,BITWIDTH,BITHEIGHT);
InvalidateRect(hWnd,NULL,FALSE);
break;
case WM_RBUTTONDOWN: // 右ボタン押した
DWORD dwStartTime,dwTime;
CHAR szStr[256];
int i;
if(MessageBox(hWnd,"テストを開始しますか?","確認",MB_YESNO)==IDYES){
i = 0;
dwTime = TESTTIME;
dwStartTime = timeGetTime();
while(timeGetTime()-dwStartTime < dwTime){
DrawPolygon(&polyData,scanData,lpBufDibByte,WINWIDTH,WINHEIGHT,lpDibByte,BITWIDTH,BITHEIGHT);
i++;
}
wsprintf(szStr,"%d 回/%d 秒\n\n平均 %d 回",i,dwTime/1000,(int)((double)i/(double)(dwTime/1000)));
MessageBox(hWnd,szStr,"結果",MB_OK);
}
break;
case WM_CLOSE: // クローズ
// 描画画面開放
DeleteDC(hBufDC);
DeleteObject(hBufDib);
GlobalFree(lpBufDibBitInfo);
// DIB 開放
DeleteObject(hBitmapDib);
GlobalFree(lpDibBitInfo);
DestroyWindow(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return(DefWindowProc(hWnd, msg, wp, lp));
}
return (0L);
}
//------------------------------------------------
// 256 色 DIB 作成
BOOL Create256DIB(HWND hWnd,HBITMAP* lphDib,LPBITMAPINFO* lppBitInfo,LPBYTE* lppDibByte,
LONG dwWidth,LONG dwHeight){
HDC hDC;
// 4 の倍数かチェック
if(dwWidth % 4 != 0) return FALSE;
// カラーパレット分のメモリ確保
*lppBitInfo=(LPBITMAPINFO)GlobalAlloc(GPTR,sizeof(BITMAPINFO)+sizeof(RGBQUAD)*(256-1));
if(*lppBitInfo==NULL){
MessageBox(hWnd, "メモリの確保に失敗しました。",
"エラー", MB_OK|MB_SETFOREGROUND|MB_ICONERROR);
return FALSE;
}
// インフォ構造体設定
(*lppBitInfo)->bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
(*lppBitInfo)->bmiHeader.biWidth=dwWidth;
(*lppBitInfo)->bmiHeader.biHeight=dwHeight;
(*lppBitInfo)->bmiHeader.biPlanes=1;
(*lppBitInfo)->bmiHeader.biBitCount=8; // 256 色
(*lppBitInfo)->bmiHeader.biCompression=BI_RGB; // 非圧縮
(*lppBitInfo)->bmiHeader.biSizeImage=dwWidth*dwHeight;
(*lppBitInfo)->bmiHeader.biXPelsPerMeter=0;
(*lppBitInfo)->bmiHeader.biYPelsPerMeter=0;
(*lppBitInfo)->bmiHeader.biClrUsed=0;
(*lppBitInfo)->bmiHeader.biClrImportant=0;
// パレット設定
DWORD i;
for(i=0;i< 256;i++){
(*lppBitInfo)->bmiColors[i].rgbBlue = 0;
(*lppBitInfo)->bmiColors[i].rgbGreen = 0;
(*lppBitInfo)->bmiColors[i].rgbRed = 0;
(*lppBitInfo)->bmiColors[i].rgbReserved = 0;
}
for(i=0;i< 38;i++){
(*lppBitInfo)->bmiColors[1+i].rgbBlue = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+114+i].rgbBlue = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+152+i].rgbBlue = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+38+i].rgbGreen = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+114+i].rgbGreen = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+190+i].rgbGreen = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+76+i].rgbRed = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+152+i].rgbRed = (BYTE)((i+1)*6);
(*lppBitInfo)->bmiColors[1+190+i].rgbRed = (BYTE)((i+1)*6);
}
for(i=0;i< 28;i++){
(*lppBitInfo)->bmiColors[228+i].rgbBlue = (BYTE)((i+1)*9);
(*lppBitInfo)->bmiColors[228+i].rgbGreen = (BYTE)((i+1)*9);
(*lppBitInfo)->bmiColors[228+i].rgbRed = (BYTE)((i+1)*9);
}
// DIB 作成
hDC = GetDC(hWnd);
*lphDib = CreateDIBSection(hDC,*lppBitInfo,0,(VOID**)lppDibByte,NULL, 0);
ReleaseDC(hWnd,hDC);
return TRUE;
}
//-------------------------------------------------------------------
// DIB にビットマップをコピー
VOID PasteBitmapToDIB(HDC hDC,HINSTANCE hInst,HBITMAP hDib,LPSTR szBitmapFile)
{
HBITMAP hBitmap;
HDC hMemDC,hMemDC2;
hBitmap = (HBITMAP)LoadImage(hInst,szBitmapFile,IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
hMemDC = CreateCompatibleDC(hDC);
hMemDC2 = CreateCompatibleDC(hDC);
SelectObject(hMemDC,hDib);
SelectObject(hMemDC2,hBitmap);
BitBlt(hMemDC,0,0,BITWIDTH,BITHEIGHT,hMemDC2,0,0,SRCCOPY);
DeleteDC(hMemDC);
DeleteDC(hMemDC2);
DeleteObject(hBitmap);
}
//--------------------------------------------
// ポリゴン描画関数 double 使用版
// Y 軸スキャン も double 使用で平均 22 FPS
//
VOID DrawPolygon(LPPOLYDATA lpPolyData, // ポリゴンデータ
LPSCANDATA lpScanData, // Y 軸スキャン用データのバッファ
LPBYTE lpDest, // 描画画面のバッファ
LONG nDestWidth, // 描画座標幅
LONG nDestHeight, // 描画座標高さ
LPBYTE lpTex, // テクスチャのバッファ
LONG nTexWidth, // テクスチャ幅
LONG nTexHeight // テクスチャ高さ
){
LONG x,y; // ループ用
LONG nStartY,nEndY; // 描画開始を開始する Y 座標、終了座標
LONG maxX,minX;
DWORD dwTexSize = (DWORD)(nTexHeight*nTexWidth); // テクスチャのサイズ
DWORD dwReadPoint; // テクスチャデータを読み込むアドレス
// Y 軸のスキャン
ScanLine(lpPolyData,lpScanData,nDestWidth,nDestHeight,&nStartY,&nEndY);
// 範囲外なら描画しない
if(nStartY >= nDestHeight || nEndY < 0) return;
// バッファのベースアドレスセット
lpDest += ((nDestHeight-1-nStartY)*nDestWidth);
// 上から描画していく
for(y = nStartY; y < = nEndY; y++){
maxX = lpScanData[y].maxX;
minX = lpScanData[y].minX;
if(maxX >= 0){
double tx,ty,tdx,tdy; // テクスチャ座標計算用
// 初期テクスチャ座標セット
tx = (double)lpScanData[y].minTx;
ty = (double)lpScanData[y].minTy;
// X が 1 増加した時のテクスチャ座標での
// x と y の(斜め読みの)変位を計算
if(maxX != minX){
tdx = (double)(lpScanData[y].maxTx-lpScanData[y].minTx)/(maxX-minX);
tdy = (double)(lpScanData[y].maxTy-lpScanData[y].minTy)/(maxX-minX);
}
else { // 一点の時
tdx = 0;
tdy = 0;
}
// 左側エッジ(minX)が 0 より小さいなら 0 になるまで回す
while(minX < 0){
minX++;
tx += tdx;
ty += tdy;
}
// 左から右に横に描画していく
maxX = min(nDestWidth-1,maxX);
for(x = minX; x < = maxX; x++){
// テクスチャデータを読み込む点を計算
dwReadPoint = (DWORD)(nTexHeight-1-(LONG)ty)*nTexWidth+(LONG)tx;
if(dwReadPoint < dwTexSize) *(lpDest+x) = lpTex[dwReadPoint];
// テクスチャ座標移動(斜め読み)
tx += tdx;
ty += tdy;
}
}
// バッファのベースアドレス更新
lpDest -= nDestWidth;
}
}
//-----------------------------------------------------
// ポリゴンのエッジの描画座標を計算する関数
//
// Y 軸方向に上から下へ順にスキャンして lpScanData にポリゴンのエッジの
// 描画座標での X 軸の Max 値と Min 値を入れていく。
// ついでにそのエッジの座標に対応するテクスチャ座標の (x,y) も計算して入れていく。
VOID ScanLine(LPPOLYDATA lpPolyData, // ポリゴンデータ
LPSCANDATA lpScanData, // エッジ座標のスキャンデータ
LONG nScrWidth, // 描画画面幅
LONG nScrHeight, // 描画画面高さ
LPLONG lpnStartY, // 描画を開始する Y 座標
LPLONG lpnEndY // 描画を終了する Y 座標
){
LONG i,v1,v2,y,v1y,v2y,endY;
// 描画開始位置、終了位置初期化
*lpnStartY = nScrHeight;
*lpnEndY = -1;
// スキャンデータ初期化
for(i=0;i< nScrHeight;i++){
lpScanData[i].minX = nScrWidth;
lpScanData[i].maxX = -1;
}
// ポリゴンの各辺毎にスキャンする
for(i=0;i< POLYGONPOINT;i++){
// 頂点番号セット
v1 = i;
v2 = (v1+1)%POLYGONPOINT;
// 頂点 V1 と V2 の y 軸が同じ時(水平線の時)
if(lpPolyData->y[v1] == lpPolyData->y[v2]){
y = lpPolyData->y[v1];
// 範囲外の時はチェックしない
if(y < nScrHeight && y >= 0){
// スタート位置計算
*lpnStartY = min(y,*lpnStartY);
*lpnEndY = max(y,*lpnEndY);
// 頂点 V1 の方が左になるようにする
if(lpPolyData->x[v1] > lpPolyData->x[v2]){
v1 = v2;
v2 = i;
}
// Max と Min の計算
if(y >=0 && y < nScrHeight){
if(lpPolyData->x[v1] < lpScanData[y].minX){
lpScanData[y].minX = lpPolyData->x[v1];
lpScanData[y].minTx = lpPolyData->tx[v1];
lpScanData[y].minTy = lpPolyData->tx[v1];
}
if(lpPolyData->x[v2] > lpScanData[y].maxX){
lpScanData[y].maxX = lpPolyData->x[v2];
lpScanData[y].maxTx = lpPolyData->tx[v2];
lpScanData[y].maxTy = lpPolyData->ty[v2];
}
}
}
}
else{ // 頂点 v1 と v2 の y 座標が異なる場合
// 頂点 V1 の方が上になるようにする
if(lpPolyData->y[v1] > lpPolyData->y[v2]){
v1 = v2;
v2 = i;
}
v1y = lpPolyData->y[v1];
v2y = lpPolyData->y[v2];
// 範囲外の時はチェックしない
if(v1y < nScrHeight || v2y >= 0){
// スタート位置計算
*lpnStartY = min(v1y,*lpnStartY);
*lpnEndY = max(v2y,*lpnEndY);
// y が 1 増加した時の描画座標での x の変位と
// テクスチャ座標での x と y の変位を計算
double x,dx; // 描画座標計算用
double tx,ty,tdx,tdy; // テクスチャ座標計算用
//x 方向の変位計算
dx = (double)(lpPolyData->x[v2]-lpPolyData->x[v1])/(v2y-v1y);
// テクスチャ座標の変位計算
tdx = (double)(lpPolyData->tx[v2]-lpPolyData->tx[v1])/(v2y-v1y);
tdy = (double)(lpPolyData->ty[v2]-lpPolyData->ty[v1])/(v2y-v1y);
// 初期描画座標セット
x = (double)lpPolyData->x[v1];
// 初期テクスチャ座標セット
tx = (double)lpPolyData->tx[v1];
ty = (double)lpPolyData->ty[v1];
// サーチ範囲計算
endY = min(nScrHeight-1,v2y);
// 上から順にサーチ開始
for(y = v1y; y < = endY; y++){
// Max と Min の計算
if(y>=0){
if((LONG)x < lpScanData[y].minX){
lpScanData[y].minX = (LONG)x;
lpScanData[y].minTx = (LONG)tx;
lpScanData[y].minTy = (LONG)ty;
}
if((LONG)x > lpScanData[y].maxX){
lpScanData[y].maxX = (LONG)x;
lpScanData[y].maxTx = (LONG)tx;
lpScanData[y].maxTy = (LONG)ty;
}
}
// 座標移動
x += dx;
tx += tdx;
ty += tdy;
}
}
}
}
// スタート位置計算
*lpnStartY = max(0,*lpnStartY);
*lpnEndY = min(nScrHeight-1,*lpnEndY);
}