サンプルコードのダウンロード
では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「effec_04.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「effec_04.exe」をダブルクリックし、実行してみてください(何かキーを押すと終了します)。
どうでしょう?画面が切り替わり、写真が波紋のような歪み方をすると思います(謎。
というわけで、今回もサインカーブを使用して画像を歪ませたいと思います。
今回は解りづらいかも
さて、今まで演出講座2・3回にてサインカーブを使用して地球を揺らしてきましたが、今回は右の写真のように、写真を波紋のように歪めてみます。
今まで真面目に演出講座2・3回を勉強された方なら、おそらく写真を見ただけで「だいたいこうやっているのだろう」という見当は付いていると思います。
んー、ちょっと初心者にはきつい内容かもしれないので、こんなものもあるんだぁーぐらいに流してください。
というかゲーム関連で、波紋使うかどうかは謎です。
まずは数学のお勉強
さて今回はプログラム中に「直線距離の算出」をするので、まずここでざっと説明しておきます。
左の図のように直角三角形がある場合、
という公式があります(ピタゴラスの定理だったか?)。
高校1年くらいには習うと思いますが(詳しくは数学の先生に聞いてください)。
実際は、x座標,y座標があるので、点A(ax,ay)から点B(bx,by)の直線距離は
以下の図のようになります。
パソコン上の座標はY軸の方向が逆になるのですが、今回は2乗しているため符号が必ずプラスになるので
その辺を考える必要はありません。
これを実際にC言語で記述すると、
length = sqrt((ax-bx)*(ax-bx) + (ay-by)*(ay-by));
みたいな感じになりますね。
DirectDraw初期化
さて、サンプルコードを見ながら順番に説明していきます。
今回は基本的な部分はWinMain()内で全てやっていますが、DirectDrawの初期化の部分も少し変更していますので注意してください。
ちなみにDirectDraw関連の基本的なコードは「DirectDraw基礎 第13回」のサンプルコードを
使用しています(MODE Xを使用します)。
ではまず、グローバル変数としてlpGraphを宣言しておきます。
これは写真を読み込んでおく場所として使用します。
LPDIRECTDRAWSURFACE lpBack=NULL; //オフスクリーンサーフェイス(バックバッファ) LPDIRECTDRAWSURFACE lpWork=NULL; //オフスクリーンサーフェイス(作業用バッファ) LPDIRECTDRAWSURFACE lpGraph=NULL; //オフスクリーンサーフェイス(作業用バッファ その2)
宣言しただけでは意味が無いので、StartDirectDraw自作関数内でオフスクリーンサーフェイスを作成します。
BOOL StartDirectDraw(HWND hw){ 略 //作業用オフスクリーンサーフェイス作成 ZeroMemory(&ddsd,sizeof(ddsd)); ddsd.dwSize=sizeof(ddsd); ddsd.dwFlags=DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; //強制的にシステムメモリ内に作成する ddsd.ddsCaps.dwCaps=DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY; ddsd.dwWidth=320; ddsd.dwHeight=240; if((lpDD->CreateSurface(&ddsd,&lpWork,NULL))!=DD_OK) return FALSE; if((lpDD->CreateSurface(&ddsd,&lpGraph,NULL))!=DD_OK) return FALSE;
今回の場合、オフスクリーンサーフェイスへは、頻繁に直接アクセスするのでDDSCAPS_SYSTEMMEMORYを指定して強制的にシステムメモリ内へ作成します(ビデオメモリ内サーフェイスへの直接アクセスは速度が遅い)。
さて、DirectDrawの設定はこれぐらいです。
あ、ちなみに最後にlpGraphを解放するのを忘れないようにしてくださいね。
よく解らない解説
まず写真(bridge.bmp)をあらかじめ作成しておいたオフスクリーンサーフェイス(lpGraph)へ読み込んでおきます。
//----------[ メイン関数 ]---------------------------------------------------------------------- int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){ 略 //DirectDraw開始 StartDirectDraw(hw); //パレット設定 LoadPalette("bridge.bmp"); //ビットマップを作業用サーフェイスへ読み込む LoadBitmap(lpGraph,"bridge.bmp",0,0,320,240);
次に、高速化のためにサイン・コサインテーブルを作成します。
//サイン、コサインテーブル作成(256で一周とする) float fsin[256],fcos[256]; for(int i=0;i<256;i++){ fsin[i]=(float)sin(i*3.1415926535/128); fcos[i]=(float)cos(i*3.1415926535/128); }
普通は360度で一周ですが、今回は256度で一周とします。
こうすることで fsin[BYTE(…)] と、BYTE型(unsigned char型)にキャストするだけで不正なアクセスを防ぐことが出来ます。
次に、あらかじめ各座標毎に、中心からの距離・中心への角度(0-255度)を算出しておきます。
これも高速化のためです。
float length[240][320]; BYTE angle[240][320]; int x,y; for(y=0;y<240;y++) for(x=0;x<320;x++){ length[y][x]=(float)sqrt((160-x)*(160-x)+(120-y)*(120-y)); angle[y][x]=BYTE(atan2(-double(120-y),double(160-x))/3.141592*128+256); }
距離の算出については、上の方でやりましたね?
また、角度の算出については「ゲーム制作基礎 第11回」でやりました。
//サーフェイスのロックで使用 DDSURFACEDESC desc_src,desc_dest; ZeroMemory(&desc_src,sizeof(DDSURFACEDESC)); ZeroMemory(&desc_dest,sizeof(DDSURFACEDESC)); desc_src.dwSize=desc_dest.dwSize=sizeof(DDSURFACEDESC); BYTE *p_src,*p_dest; //ちなみに src=参照元 dest=転送先 int sx,sy; int pitch_src,pitch_dest; int count=0; float width=0.f,width_max=20,width_add=0.15f; BYTE color; while(1){ //疑似タイマー処理 tim=timeGetTime(); count++; //カウンタ //ゆれの大きさを時間によって変化させる if(width_add>0){ if((width+=width_add)>width_max) width_add=-width_add; } else { //0まで戻してしまうとちょっと歪むのでやめる if((width+=width_add)<=0.05f){ width=0.05f; width_add=-width_add; } }
↑のは大体解ると思います。
ゆれの大きさの部分に関しては前回・前々回と大体同じです。
次に、あらかじめ画像を読み込んでおいたサーフェイス(lpGraph)と、作業用サーフェイス(lpWork)をロックし、そのサーフェイスへの先頭アドレスを取得します。
//ロック lpGraph->Lock(NULL,&desc_src,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL); p_src=(BYTE*)desc_src.lpSurface; pitch_src=desc_src.lPitch; lpWork->Lock(NULL,&desc_dest,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL); p_dest=(BYTE*)desc_dest.lpSurface; pitch_dest=desc_dest.lPitch;
サーフェイスのロックについては「DirectDraw基礎 第12回」でやりました。
そんでもって次のが心臓部。
BYTE *p_angle=&angle[0][0]; float *p_length=&length[0][0]; for(y=0;y<240;y++){ for(x=0;x<320;x++){ //参照元をずらす float len=fsin[BYTE((-count+*p_length)*15)]*width; sx=int(x+fcos[*p_angle]*len); sy=int(y-fsin[*p_angle]*len); color=0; if(sx>=0 && sx<320 && sy>=0 && sy<240) color=*(p_src+pitch_src*sy+sx); *(p_dest++)=color; p_angle++; p_length++; } p_dest+=pitch_dest-320; }
ポインタいじりまくりですね。
ポインタの概念がしっかり解っていないと理解出来ないと思います。
まず、今までの演出は「転送先の座標を計算する」というパターンですが今回はこれでは穴が空くので逆に「転送先が転送元の座標を計算する」方法でやっています。
ですから画面全体(320×240)について全て計算を行います。
おおまかに説明すると、「画面中心からの距離(p_length)+時間(count)」を使ってサインカーブを参照し、ゆれの幅を計算します。
その揺れの幅分だけ転送元の座標を中央に向かってずらします。あとは転送元の色を転送先へ転送します。
一通り終わったらロックを解除し、一度バックバッファへ転送し、フリップして画面に表示させます。
//アンロック lpWork->Unlock(desc_dest.lpSurface); lpGraph->Unlock(desc_src.lpSurface); Blt(lpBack,0,0,320,240,lpWork,0,0,FALSE); Flip(); //フリッピング あとは省略(爆
解ってしまえば簡単ですね(^^;
細かい部分で何か解らない事があれば掲示板へどうぞ。