2012/08/18

SDLの使い方(画像の読込と画像データへの参照)

SDL単体ではBMP形式のみ対応しており、png,jpgやtiff等の別形式の画像の読み込みを行う場合SDL_imageを導入します。

SDL_imageで画像を読み込む場合は以下のようにIMG_Load関数を呼び出します。

[cpp]
if( GetFileAttributes( "test.png" ) != -1 )
{
SDL_Surface* image;
image = IMG_Load( "test.png" );

if( image == NULL )
{
sprintf( aszMessage, TEXT("画像の読み込みに失敗しました:%s"), SDL_GetError() );
OutputDebugString( aszMessage );
exit(1);
}
}
[/cpp]

IMG_Load関数が成功するとSDL_Surfaceオブジェクトへのポインタが返ってきます。このオブジェクト内に読み込んだファイルの画像データが保存されています。
このSDL_SurfaceオブジェクトをSDLのBlitSurface等に指定することで、画面に描画することができます。

少し話が変わりますが、各画像形式の読み込み処理を全て実装するのは手間なので、ライブラリ側に任せたいと考えてSDLに手を出した方もいらっしゃるかもしれません。

これは私がそうなのですが。GDI+を使用するという選択しもあったんですが、音関連の処理もライブラリに任せて本体のロジックのみ集中したい、そしていくつもライブラリをかき集めて管理するのが面倒ということでSDLを選択したわけなんですが、SDLには画像の読み込みだけしてもらってデータだけ欲しいと期待していました。

しかし、IMG_Loadを実行して帰ってくるのは隠蔽されたオブジェクトであるSDL_Surface。まぁ当然ですよね。 これを何とかしてデータだけ抽出したいと思い、SDL_Surfaceの構造を調査したところ割と簡単に見つけました。

結論からいうとSDL_Surface:Pixelsに画像データの先頭アドレスが格納されています。
これにwidth,heightの画像情報を合わせてビットマップを作成してやれば、異なる画像形式のファイルでも必要な画像データと画像情報が手に入ります。

そんなわけで、SDL_Surface::Pixelsの内容をSetPixel()関数を使用して画像に直接描画した画像が下図になります。



[cpp]
if( m_image != NULL )
{
int r, g, b;
for ( int i = 0; i < m_image->h; i++ ) {
for ( int j = 0; j < m_image->w; j++ ) {
// 1画素の R, G, B 成分は1バイトであること.画像データは隙間無くならんでいること.R, G, B の順に並んでいることを仮定している.
unsigned char* p = (unsigned char*)m_image->pixels;
int pixel_at = (i * (m_image->w) + j ) * m_image->format->BytesPerPixel;
r = p[pixel_at];
g = p[pixel_at + 1];
b = p[pixel_at + 2];

SetPixel( hdc, j, i, RGB( r, g, b ) );
}
}
}
[/cpp]

どこかでみた構図と画像ですが、これはpng画像を読み込んで画像データに直接操作できたことを証明できたことになるため、検証としては大きな成果です。

ただ、pngでは画像データの並びが"RGB,RGB,RGB,RGB..."と続くので何も問題はないのですが、bmpを読み込むと画像データの並びが"BGR,BGR,BGR,BGR..."と逆転しているので、読み込み後は拡張子に基づいて自前で調整処理を実装しないといけなさそうです。

描画処理についてはこちらのサイトのソースコードを参考に改変いたしました。
Linux で動く,SDL_image を用いて画像ファイルを読み込むプログラム例



12/09/13追記

SDLからRawデータを取得して画像処理を行い、再びSDL_Surfaceに押し込むことで画像処理のみ記述するという使い方も用途としてはお手軽にできるかもしれない。

SDL_imageの導入方法(libpng/zlibの静的リンクビルド)

SDL_imageを導入する時、気を付けなければいけないことについて備忘録として残す。

導入方法と言いつつ、ダウンロードしてSDLと関連付けてビルドするところまで端折る。なぜなら、他のサイトで画像つきで解説しているので冗長だから。

当記事では以下の環境を使用しています。
SDL_image1.2.12
SDLのバージョン1.2

まず、SDL_imageをHGからCloneしてSDLとリンクさせてビルドさせる場合、pngやjpgを読み込むときにexeと同じ位置に、SDL_image\VisualC\external\lib\x86にあるlibpng15-15.dllやlinjpeg-8.dllを配置しないといけない。

基本的にdllを配置して実行環境を構築したくないので、staticlibでビルドする方法を考える。

まず、どこでDLLの読み込み指定を行っているのか?
これはSDL_imageのプロジェクトプロパティのプリプロセッサ項目に以下のような項目がある。

(一部抜粋)

[html]
LOAD_JPG_DYNAMIC=\"libjpeg-8.dll\"
LOAD_PNG_DYNAMIC=\"libpng15-15.dll\"
LOAD_TIF_DYNAMIC=\"libtiff-5.dll\"
LOAD_WEBP_DYNAMIC=\"libwebp-2.dll\"
PNG_USE_DLL
ZLIB_DLL
[/html]

これらの項目が定義されているとSDL_imageは実行時にDLLを読み込もうとするので、これらの定義を削除してやれば外部参照が未解決状態になり、各ライブラリへのリンクを求められる状態になります。

ここでは、ライセンス的に安全とわかっているlibpngのみ削除してstaticlibでリンクします。
削除する項目は「LOAD_PNG_DYNAMIC~」と「PNG_USE_DLL」「ZLIB_DLL」です。後者二つのプリプロセッサはプロジェクトで使用していないようですが、 DLLの使用を示唆する記述は削除しておきました。

この状態までくれば後は必要なライブラリを関連付けしてビルドしてやれば静的リンクを行ったバージョンのSDL_image.libが出来上がります。SDL_image.libをリンクしたプロジェクト内でlibpng.libとzlib.libをリンクすることでも同様に実現可能でしょう。この辺りはケースバイケースなので、都合がいい方を選択するといいと思います。

最後になりますが、静的リンクを行う際にはライセンスを必ず確認してください。

今回はzlib/libpngライセンスを使用しているlipngとzlibのみ使用したので問題になりませんが、もしGPL/LGPLだった場合、ソースを静的にリンクするとライセンスに感染してしまうのでフリーウェアとして開発していない場合は注意してください。

2012/08/17

SDLイベントキューの実装を解析

SDLではOSが送ってくるウィンドウメッセージをいったんSDL側で全て吸収してSDL独自形式に変換した上で、SDL_PollEventを使用してユーザー側に渡して処理をさせます。
ここではそのウィンドウメッセージをどう処理しているのかコードを辿っていきたいと思う。

まず、SDLにおけるウィンドウプロシージャの実装はSDL_sysevents.cに定義されているWinMessage関数内で行われている。
ここから様々なサブシステムにメッセージを投げて処理をしていると思われるがウィンドウプロシージャ側から辿るのが難しそうなので逆方向からアプローチを行う。

ユーザーがイベントを取得するSDL_PollEvent側から辿ってみる。
SDL_PollEvent(正確にはSDL_PeepEvent)で使用するSDL_EventQ(イベントキュー)はグローバルメンバ変数で、SDL_events.cに次のように定義されている

[cpp]
/* SDL_events.c */
/* Private data -- event queue */
#define MAXEVENTS 128
static struct {
SDL_mutex *lock;
int active;
int head;
int tail;
SDL_Event event[MAXEVENTS];
int wmmsg_next;
struct SDL_SysWMmsg wmmsg[MAXEVENTS];
} SDL_EventQ;
[/cpp]

構造体の仕様から、SDLの内部処理でeventメンバに対して随時ウィンドウメッセージを処理したイベントを追加されることが予想される。では、実際にどこでイベントが追加されるかというとSDL_AddEvent内で処理される。

簡単に流れを整理するとWinMessage>(SDL入力処理?)>SDL_AddEvent>SDL_PollEventの順に処理されることでメッセージをやり取りするのではないかと推測できる。

次はSDL_AddEventをだれが呼び出しているかについて調べていきたい。
以降から多少複雑になるが、SDL_AddEventを呼び出している処理はSDL_PollEvent内でも呼び出しているSDL_PeepEventsによって追加処理も行われる。
これはSDL_PeepEventsと引数であるSDL_eventactionが次のような定義になっているためである。
定義を見るとSDL_ADDEVNETを渡せばイベントを登録するし、それ以外であればイベントを取得する処理になるのがわかる。

[cpp]
int SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action,
Uint32 mask)

/* SDL_eventactionの定義 * /
typedef enum {
SDL_ADDEVENT,
SDL_PEEKEVENT,
SDL_GETEVENT
} SDL_eventaction;
[/cpp]

ではさらに辿ってSDL_PeepEventに対してSDL_ADDEVENTを渡している処理を追ってみる。
SDL_PeepEventをさらにラップしているSDL_PushEventを経由して多岐にわたるソースコードからイベントが登録されている。
SDL_PushEventを使用してイベントを登録しているサブシステムは以下の通り。

  • sdl_mouse.c

  • sdl_keyboard.c

  • sdl_joystick.c

  • sdl_events.c

  • sdl_resize.c

  • sdl_quit.c

  • sdl_expose.c

  • sdl_active.c

説明は省くがsdl_mouse、sdl_keyboardやsdl_joystick等入力処理からの登録がほとんどである。
これを集約してさらに元をたどるとWinMessageに行き当たる。

以上でSDLがウィンドウプロシージャからのメッセージをどの様にユーザーに提供しているのか大まかに把握できた。

SBL_PollEventの実装を解析

結論から言ってしまうとSBL_PollEventの中身は通常のWin32アプリケーションで実装されるメッセージポンプと同様の処理を行うSDL_PumpEventsとメッセージキューに貯まっているウィンドウズメッセージを横取りするSBL_PeepEventsによって実装されています。

SDL_PumpEventsの実装を辿っていくと最終的に次のコードが見つかります。
これは典型的なメッセージポンプ処理の実装です。

[cpp]
// SDL_dibevents.c
void DIB_PumpEvents(_THIS)
{
MSG msg;

while ( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE) ) {
if ( GetMessage(&msg, NULL, 0, 0) > 0 ) {
DispatchMessage(&msg);
}
}

if ( SDL_GetAppState() & SDL_APPMOUSEFOCUS ) {
DIB_GenerateMouseMotionEvent( this );
}
}
[/cpp]

SBL_PeepEventの実装を辿っていくと「SBLが管理しているイベントキューの中にある全てのイベント種類の中から最初の1つを取得する」という動作を行います。取得したイベントはSDL_CutEventが呼び出され、メッセージキューから消えます。



余談ですが、spot=(spot+1)%MAXEVENTの式があり、MAXEVENTS=128の指定のため、おそらくSDLのイベントキューのサイズは128だろうと推測する。

SDLの使い方

SuzudoraExの開発に当たり、SDLを導入してIO周りの処理にかかるコストを削減することにしたわけだが、SDLの実装方法について毎回調べるのは不毛だと気付いたので実装メモを残していこうと思う。

まず、WindowsだろうがLinuxだろうがインストール方法は別サイトにいくらでもあるので除外。
ここではライブラリが開発環境に組み込まれたことを前提とする。
また、SDLmain.libライブラリはOSによるスタートアップ(エントリポイント)の依存コードを減らすために組み込むライブラリなので、当サイトでは扱わないものとする。

メモにあるソースコードを見るに当たり、SDLの関数リファレンスを参照してほしい。

SDL最少コード


以下のコードは全く意味のないコードだがSDLを使用するための必要最低限のコードである。

[cpp]
if( SDL_Init( SDL_INIT_VIDEO ) != 0){
exit(1);
}

SDL_Quit();
[/cpp]

解説としてはSDL_InitでSDLサブシステムとして登録されているシンボルを指定して初期化する。
これを行わないと各サブシステムの関数呼び出し時にエラーが発生するらしい。
ここで出てくるSDL_INIT_VIDEO等複数初期化したい場合はSDL.hに下記のように定義してあるので、論理和で結合してSDL_Initに渡してやればいい。もしくはSDL_INIT_EVERYTHINGで初期化する。

SDL_Quit()は後始末処理を行っているのでアプリケーション終了時に必ず呼び出すこと。

[cpp]
// SDL.h
#define SDL_INIT_TIMER 0x00000001
#define SDL_INIT_AUDIO 0x00000010
#define SDL_INIT_VIDEO 0x00000020
#define SDL_INIT_CDROM 0x00000100
#define SDL_INIT_JOYSTICK 0x00000200
#define SDL_INIT_NOPARACHUTE 0x00100000 /**< Don't catch fatal signals */
#define SDL_INIT_EVENTTHREAD 0x01000000 /**< Not supported on all OS's */
#define SDL_INIT_EVERYTHING 0x0000FFFF
[/cpp]

SDL_Initはまとめて論理和で結合して渡してもいいし、SDL_Initを複数回呼び出していい。
重複するシンボルがあったとしても既に登録してあれば無視されるので安全が保たれている。
しかし注意したいのは初期化後、何らかの処理を行った後にさらにSDL_Initを使用する場合だ。

SDL_Initは内部で次の3つの処理を行っている。そのため、エラーメッセージがクリアされてしまう場合があるため、(逆に言えば気にしないでいいならばSDL_Initを使用してもいい)素直にSDL_InitSubSystemを使用して初期化したほうがいいだろう。

  • SDL_Init処理

    • エラーメッセージのクリア

    • SDL_InitSubSystemの呼び出し

    • パラシュートコードの登録

SDL_InitSubSystem の使用方法としては「スタートアップではSDL_INIT_VIDEOだけ初期化したけど、あとでジョイスティックの実装をすることになった。でもSDL_Initに付け足すとコードファイルが別々になって後々修正が大変」というこ時にジョイスティックの実装開始地点もしくは初期化処理にSDL_InitSubSystem にSDL_INIT_JOYSTICKを指定して初期化するもののようだ。

SDLのウィンドウ使用しないでSDLオブジェクトを利用する方法


SDLではSDLが用意した初期化処理を行い、SDLが生成するウィンドウを使用して各メソッド・オブジェクトを弄ることになる。こうすると、ユーザー側ではWindowsやLinux等のOSレベルのネイティブな処理を考えなくてよくなるが、自前で画像を加工しようと思っている筆者からすると有難迷惑にもほどがある。何とかSDLが生成するウィンドウを使用せずに、画像の読み込みを行ったオブジェクトの生データを横取りできないか考える。

調査したところ、割と簡単そうだった。
SDL_imageライブラリを導入した上で、Load_IMG関数を使用することによってSDL_Surface*が返ってくる。このSDL_Surfaceの公開メンバにwidth,height,picth等画像情報が含まれている。

これが一番 重要だが、SDL_Surface::pixelsの中身は画像データ配列の先頭アドレスが入っている。
これらを利用することで、 各画像形式の読み込み処理を意識せずにデータのみ横取りすることができると予想される。

まだ未検証なので、次の更新までには確認してみたい。
[追記] 確認が取れました。こちらで実際に読み込んだデータを直接参照して描画できることを確認しています。

もし可能であるならば、SDLを意識せずにDIBを作成するラッパークラス等を作ってみたいと思う。

SDLウィンドウのウィンドウプロシージャを乗っ取る


SDLが生成するウィンドウからウィンドウプロシージャをフックするのは割と簡単。

SDL内部のグローバルな変数に"SDL_Window"があるので、これをGetWindowLong/SetWindowLongのターゲットに指定するとウィンドウプロシージャの乗っ取りができる。

しかし、乗っ取ったからと言って何か利便性があるのかと考える微妙である。
まぁ、SDLがサポートしていないウィンドウメッセージをどうしても使いたいという場合は乗っ取ったウィンドウプロシージャで処理してそれ以外はSDLのウィンドウプロシージャに丸投げする。という使い方ならできるかもしれない。

GWL_USERDATAは使用できると面白そうだけど、SDL内でオブジェクトのやり取りに使われていないことを確認しないと危なくて使えない。

何か有用性があればぜひ教えてほしい。