■ 新・ゲーム開発講座




■ へっぽこプログラミング入門♪


■第57夜:JPEGを読む

今回は、ちょっと公開すべきか迷ったのですがJPEGを読む機能を追加します。
一応 OleLoadPicture() をいろいろいじってなんとか読めるようにはなったのですが、くににん的にはCOMインターフェイスなんてほとんど使ったこともなければ、IStream、IPcture も「なにそれ?」という知識量なので(大汗 ^^;)、おそらく解説しようにも正確さに欠けるところ多々だと思っています。そう、まさにオーパーツを拾った原始人の心持ちです(爆)。そのような次第で、「間違いがあってもごめんなさい」の前提で参ります。
ソースはこちらです。

■OleLoadPicture()とは?

Windowsの開発初期の頃、複数のアプリケーション間でデータを共有しようという目的でOLE (Object Linking and Embedding)という仕組みが考案されました。今では発展?してActive-Xにつながっているようですが、くににんには解説するだけの知識はありません(をい ^^;)。ふーん、コンポーネントがいっぱいあるのか〜COMっていうのか〜ふにゃふにゃ…ぐう(まて)

OleLoadPicture() はその中でお手軽に画像を読み込む関数です。どのへんがお手軽なのかといいますと、この関数ひとつで BMP、JPEG、GIF が読めてしまうのです。 OLEAUT32.DLL に依存しており、このDLLは Windowsをインストールすれば標準でシステムフォルダに入っているのですが、IEEXCELをインストール/アップデートするとそのたびに書き換えられて妖しげな機能が付加されているのだとか。Win98以降はそれなりに安定しているそうですが、システム上の自分以外のプログラムもこのDLLを参照して動作している可能性がある、ということは頭のどこかに置いておいたほうが良いかもしれません。

ところで OleLoadPicture() を利用するには、IStremIPictureインターフェースとやらを作らねばなりません。が、これもくににんにはオーパーツなので使い方がわかればそれでいいや…と割り切ります。

■仕様

BMPを読むコマンドとしては既に第21夜で #g_load コマンドを作りました。ここではそれを改造してJPEGも読めるようにしてしまいましょう。

書式:#g_load vram,file_name

指定サーフェイスに指定ファイル名のBMPを読み込む
vram :バックサーフェイス=BS 背景画面=BG パーツ画面=PT
File_name:拡張子まで含めたファイル名


■コマンド実行部

Text_Com_01.cppCom_g_load() に手を加えます。従来 Load_Bmp() で画像を読んでいたところを、これから作成するJPEGロード関数で置き換えます。
関数名は HLS_LoadJpeg() とでもしておきましょう。コマンドは、これだけです。



int Com_g_load()
{

char vram[32];
char f_name[256];
int x,y;

//画面指定の解釈
TEXT++;
strcpy( vram, Kaiseki_TextStr() ); //画面指定切り出し
while( *TEXT==',' || *TEXT==' ' || *TEXT==0x0a )TEXT++;

//ファイル名の解釈
strcpy( f_name, G_PATH ); //Path
strcat( f_name, Kaiseki_TextStr() );

HLS_LoadJpeg( Get_game_DC(vram),0,0,f_name ); //BMP,JPEG対応

//背景画面情報のキャッシュ(SAVE対策)
if( strcmp( "BS" , vram )==0 ) strcpy(BK_bmp_fname,f_name) ; //バックサーフェイス
if( strcmp( "BG" , vram )==0 ) strcpy(BG_bmp_fname,f_name) ; //背景画面
if( strcmp( "PT" , vram )==0 ) strcpy(PT_bmp_fname,f_name) ; //パーツ画面

return 0;

}


■Jpeg Loader

さてそれでは肝心のJPEGローダの部分です。まずはリスト(g_tool.cpp)から。
ファイル名チェックのところで故意に GIF を外しているのはユニシス対策です。今回読みたいのはJPEGであってGIFではありません。本稿でGIFの表示を扱うことはありませんのでご理解のほどよろしくお願いいたします(・ω・)ノ


//=========================================================
// JPEG/BMP/GIF 対応画像ローダー
//=========================================================

#include <ocidl.h> //←IPictureの定義
#include <olectl.h> //←OleLoadPicture()の定義

bool _check_g_filename( char *f_name ) //ファイル名チェック
{

char buf[10],str[256];
int len;

//ファイル名の最後の4文字を切り出す
len = strlen(f_name);
buf[0] = *(f_name + len -4);
buf[1] = *(f_name + len -3);
buf[2] = *(f_name + len -2);
buf[3] = *(f_name + len -1);
buf[4] = 0;

//原始的な比較(笑)
if( strcmp( buf, ".jpg")==0 ) return true;
if( strcmp( buf, ".JPG")==0 ) return true;
if( strcmp( buf, ".bmp")==0 ) return true;
if( strcmp( buf, ".BMP")==0 ) return true;

sprintf( str,"■サポート外のファイル形式 [ %s ] が指定されてるにょ♪\n\n"
" 以下の拡張子のファイル名を指定してほしいにょ(・ω・)ノシ\n\n"
" [ .bmp ] [ .BMP ] [ .jpg ] [ .JPG ]",buf);

msg( str,"_check_g_filename()");

return false;

}

void HLS_LoadJpeg( HDC dest_DC,int x,int y, char *f_name )
// 画面の特定座標に f_name で与えられるファイル名の画像データを読み込む。
//
// dest_DC : 転送先のDC
// x,y : 転送座標
// f_name : ファイル名(*.bmp と *.jpg に対応)
{

char str[256];
HGLOBAL h_buffer; //バッファ領域のハンドル
LPVOID ptr_buffer; //バッファ領域のポインタ
IStream *ptr_IStream; //IStreamインターフェイスへのポインタ
IPicture *ptr_IPicture=NULL; //IPictureインターフェイスへのポインタ
DWORD f_size; //ファイルサイズ
DWORD read_size; //読み込みサイズ受け取り用
HANDLE hFile; //ファイルハンドル

//ファイルの拡張子をチェック
if( _check_g_filename(f_name)==false )return;

//------------------------------------
// ファイルを読む
//------------------------------------
//ファイルをOPEN
hFile = CreateFile((LPCTSTR)f_name,GENERIC_READ,0,NULL,
OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL );
if(hFile == INVALID_HANDLE_VALUE){
sprintf( str,"ファイル【 %s 】が開けないにょ!(T▽T)",f_name);
msg( str,"HLS_LoadJpeg()");
return; //OPEN出来なかったら漢らしく去る
}

//バッファ用にヒープを確保
f_size = GetFileSize(hFile, NULL); //ファイルサイズを取得
h_buffer = GlobalAlloc(GMEM_MOVEABLE,f_size); //GMEM_MOVEABLE:再配置可能
if( h_buffer == (void*)0xFFFFFFFF ){
msg( "ヒープメモリが確保できないにょ!","HLS_LoadJpeg()");
CloseHandle(hFile); //ヒープが確保出来なかったら漢らしく去る
return;
}
//確保した領域をロック(排他アクセス領域にする)して先頭アドレスを取得
ptr_buffer = GlobalLock(h_buffer);

ReadFile( hFile, ptr_buffer, f_size, &read_size, NULL ); //ファイルを読み込む

//----------------------------------------------------
//確保したメモリ上にストリームオブジェクトを作る
//----------------------------------------------------
// → 読んだファイルにコントロールギアをぺたっと貼り付けるイメージかな?(・ω・)
CreateStreamOnHGlobal(
h_buffer, //メモリ領域のハンドル
TRUE, //ストリームオブジェクト開放時にメモリも開放するか?→YES
&ptr_IStream //インターフェースのアドレスを受けとる
);
//ここでJPEGが化けるらしい
DWORD obj_size;
obj_size = GlobalSize(h_buffer);
OleLoadPicture(ptr_IStream,obj_size,TRUE,IID_IPicture,(LPVOID*)&ptr_IPicture);

//----------------------------------------
// ここから転送ね
//----------------------------------------
short type;
HBITMAP hBitmap;
OLE_HANDLE hOle = NULL; //←NULLで初期化しないとハングするのよぉ〜(+▽+)
HDC work_DC; //作業用のDC
HDC ole_DC; //作業用のDC
OLE_YSIZE_HIMETRIC hight; //画像高さ
OLE_YSIZE_HIMETRIC width; //画像幅

ptr_IPicture->get_Type(&type);
if(type == PICTYPE_BITMAP){
ptr_IPicture->get_Handle(&hOle);
}else{
msg("絵じゃないみたい?","");
}

hBitmap = (HBITMAP)hOle; //無理矢理BITMAPのハンドルにする
work_DC=GetDC(hwnd); //主(表)画面のDCの内容を取得
ole_DC =CreateCompatibleDC(work_DC); //同じ設定でOLE_DC作成
SelectObject(ole_DC,hBitmap); //読んだ画像に結びつける
ptr_IPicture->get_Height( &hight ); //高さを取得
ptr_IPicture->get_Width( &width ); //幅を取得

BitBlt(dest_DC,x,y,(int)hight,(int)width,ole_DC,0,0,SRCCOPY); //転送!

//--------------------------------
// 後始末
//--------------------------------
DeleteDC(ole_DC); //DC廃棄 → CreateCompatibleDC()で作ったやつなので DeleteDC()で廃棄する
ReleaseDC(hwnd,work_DC); //作業用DCを開放

ptr_IStream->Release(); //開放 →この書式でないとダメみたい
ptr_IPicture->Release(); //開放 →メモリ領域も一緒に開放

GlobalUnlock(h_buffer); //ロック解除
CloseHandle(hFile); //ファイル閉じる

}


さてそれでは簡単にオーパーツの使い方を♪
ここでは、ヒープ領域からファイルサイズぶんのメモリを確保し、そこに画像データを読み込んでからストリームオブジェクトに化けさせています。重要なのは CreateStreamOnHGlobal() IStream オブジェクトを作っているところと、その直後の OleLoadPicture() です。OleLoadPicture()でいったい画像はどこに展開されるのだろう…と不思議なのですが、直後の ptr_IPicture->get_Type(&type)BITMAPであることの確認が出来て、get_Handle() でハンドルが取得できてしまうのですからプログラマの関与しないところで勝手にオブジェクトが作成されているんでしょうね…。

で、せっかく読めてもそれがメモリのどこかにひっそりと存在しているのでは意味がありません。get_Height()get_Width()で高さと幅を求め、get_Handle()で取得したハンドルでDCを作成し、外部の(ここでは関数の引数として渡される dest_DC の)画面にBitBlt()でコピーしてしまいます。その後 IStream、IPicture を破棄してファイルを閉じます。

さて、実行してみると…おお、たしかにJPEGが読めます(^^;) これで、なにか作品を作ってネットで配布しようという場合でもずいぶん助かりそうですね。




ところで、こういうワザができるとシステムメニューやデフォルトの背景画など、すべてをJPEG化したくなるのが人情というものですが…実はくににんは推奨しません。メニュー窓の表示などは、明らかにレスポンスが悪化します。また Load_Bmp() と 今回作成した HLS_LoadJpeg() で比較すると、内部で LoadImage() を使用している Load_Bmp() のほうが明らかに良いパフォーマンスを示します。OleLoadPicture()は便利ではありますが、速度の点では不利なようです。

そのような次第で、今回作成したJpegローダ HLS_LoadJpeg() で画像読み込み部を置き換えるのは、プログラム内では

1)#g_load コマンド

2)システムメニューのロードにおける、画面の再現部分
(画像キャッシュの内容によってはJPEGを読まねなら
ない場合があるため)


の2箇所に絞ります(具体的にどこを直したかはソースを見てください ^^)。
またこのお陰で、フォルダ GDAT (画像ファイルを入れておく場所) に格納する画像ファイルについて、BMPとJPGが入り乱れる可能性が出てきたので、簡単なルールを決めておきましょう。

・デフォルト背景 back00.bmp は必ず BMP であること
(絵柄は差し替えてもOK)

・システムBMP(窓枠とカーソル、black.bmp、white.bmp)
は必ず BMP であること

・上記以外のゲームで使用するCGは、BMPでもJPEGでもよい

このあたりの使い分けは、プログラマのセンスによってずいぶん違った価値観で判断される部分もあろうと思います。面倒だからどちらかに統一しろ、という主張もあるかもしれません。しかしくににん的には、@速度に優れるがBMPしか読めないAJPEGも読めるが速度は遅い という2つの手段が使えるのであれば、それぞれに適した場所で使い分ければよいだけだと思うわけです。まあ人好き好きというのもあるでしょうから、皆さんなりに考えてみてください。

…とゆーか、IStreamはもう少しちゃんと勉強しなきゃダメぴょんですね(汗汗汗 ^^;)