この番外編は、testgl.c や testsprite.cのコードを読み、 SDLのOpenGL対応への理解を深めることを目的としています。
本ソースツアーでは1.2.0を対象にすることになっていますが、 事情により番外編では執筆時点の最新版である1.2.7を対象にいたします。
グラデーションつきのキューブがくるくるとまわるというデモです。
ここでは、6面体をどうやって作っているのか、ということではなくて、 このデモにSDLがどうやって関わっているのかという点について検証してゆきます。
忘れられがちなのですが、testglでは-logoオプションをつけることで、 キューブの他にlogo.bmpが走りまわる動作をさせることができます。ちょうど testspriteのスプライト数が1つになったような感じです。
いきなり3Dの話を始める前に、まずはこの2Dっぽい表示について理解することに しましょう。
SDL_opengl.hというファイルをincludeします。このヘッダファイルは、 GL関係のヘッダが置かれている位置の違いなどを吸収した上で、GL、GLUを 使う準備をしてくれます。
mainを眺めてみると、コマンドライン引数の解釈をした後、 RunGLTestを呼び出していることがわかります。
通常と同じように、SDL_SetVideoModeをするのですが、その前に SDL_GL_SetAttributeにより、OpenGLの設定を行っています。 SDL_GL_DOUBLEBUFFERを指定することにより、flipを利用することができます (描画は裏プレーンに行い、コマンドによりプレーン切り替えをする)
SDL_SetVideoModeには、SDL_SWSURFACEやSDL_HWSURFACEではなく、 SDL_OPENGLまたはSDL_OPENGLBLITが渡されます。 SDL_OPENGLBLITは現在では使用を推奨されていません。以後はSDL_OPENGLの 場合のみを扱っています。
SDL_SetVideoModeが成功した後、情報を表示し、GLのコンテキストを整備してゆきます。 その後、whileループによって1フレーム描画のループに入ります。次の節から、 少しずつその内容を整理してゆきます。まず、描画ループの方を先にやっつけます。
glClearColor( 0.0, 0.0, 0.0, 1.0 ); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glClearColorは、glClear(GL_COLOR_BUFFER_BIT)で塗り潰す色です。 順番にR, G, B, Aとなっています。
は、ここでは略します。glBegin(GL_QUADS)〜glEnd() までがそれにあたります。
/* Draw 2D logo onto the 3D display */ if ( logo ) { if ( USE_DEPRECATED_OPENGLBLIT ) { DrawLogoBlit(); } else { DrawLogoTexture(); } }現在はOPENGLBLITではないほうを追っていますので、DrawLogoTexture()に 飛びます。
SDL_Surface *screen = SDL_GetVideoSurface();ディスプレイサーフェスを取得しています。取得したサーフェスは ウインドウのサイズなどを得るために使用しています。 SDL_OPENGLを有効にしたディスプレイサーフェスに対しての書き込み、つまり SDL_BlitSurfaceやpixelsへのアクセスは不正となりますのでご注意下さい。
logo.bmpは、テクスチャとして作成され、それをポリゴンに貼り付けて 表示しています。テクスチャはGLuintのハンドル(番号)により管理されます (global_texture)。
まず、logo.bmpをSDL_LoadBMPで読み込み、SDL_Surfaceを作成します。
/* Load the image (could use SDL_image library here) */ image = SDL_LoadBMP(LOGO_FILE); if ( image == NULL ) { return; } w = image->w; h = image->h;その後、SDL_GL_LoadTextureという関数を呼び出して テクスチャを生成します。
SDL_GL_LoadTextureは、SDL_Surfaceをテクスチャに変換するための 汎用的な関数として作成されています。
OpenGLではテクスチャの縦横サイズは2^nになっている必要があります。
GLuint SDL_GL_LoadTexture(SDL_Surface *surface, GLfloat *texcoord) { GLuint texture; int w, h; SDL_Surface *image; SDL_Rect area; Uint32 saved_flags; Uint8 saved_alpha; /* Use the surface width and height expanded to powers of 2 */ w = power_of_two(surface->w); h = power_of_two(surface->h);power_of_twoの実装は省略します。サーフェスの縦横サイズ以上で 最小の2^nを求める処理になっています。
たとえば、100x100のSDL_Surfaceをテクスチャにする場合、 テクスチャのサイズは128x128となります。テクスチャ側の (0,0)-(99,99)までは、元のSDL_Surfaceの画像をそのまま使うとして、 残りの部分はどうしましょうか。SDL_GL_LoadTextureでは、 残りの部分には何もしないようになっています。使用時に、 (0,0)-(99,99)より外の部分は使わないようにします。そのための 座標計算処理が以下の4行です。
texcoord[0] = 0.0f; /* Min X */ texcoord[1] = 0.0f; /* Min Y */ texcoord[2] = (GLfloat)surface->w / w; /* Max X */ texcoord[3] = (GLfloat)surface->h / h; /* Max Y */
SDL_Surfaceではピクセルで指定していた座標ですが、 テクスチャが作成されると、以後はテクスチャ座標系という 0.0〜1.0の座標を指定することになります。 先程の128x128のテクスチャでいうと、左上端が(0.0, 0.0)、 右下端が(1.0, 1.0)といった具合です。 上記4行の処理では、実際に有効な画像が入っている場所がテクスチャ座標で 言うといくつにあたるのかということを計算しています。 先程の例で言うと、(99,99)までが有効なピクセルですから、 テクスチャ座標になおすと(0.78125, 0.78125)となります。
テクスチャをポリゴンにはりつけるとき、各ポリゴンの頂点に対応する テクスチャの座標を指定することができます。ですから、 100x100のSDL_Surfaceに対して
順序が逆になりますが、実際に、テクスチャを作成する処理は以下のようになります。
/* Create an OpenGL texture for the image */ glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels); SDL_FreeSurface(image); /* No longer needed */ return texture;glGenTexturesによってテクスチャハンドルを取得し、 glBindTextureでバインドした後、 glTexImage2Dによってピクセルデータの読み込みを行います。
glTexImage2Dに読ませるピクセルデータの並びかたはGL_RGBAと指定しています。 SDL_GL_LoadTextureに与えられたsurfaceを、glTexImage2Dで読み込めるような 並びに変換したSDL_Surfaceを用意し(image)、そのピクセルデータ(image->pixels)を glTexImage2Dに渡してやります。
変換作業は以下のようになっています。
image = SDL_CreateRGBSurface( SDL_SWSURFACE, w, h, 32, #if SDL_BYTEORDER == SDL_LIL_ENDIAN /* OpenGL RGBA masks */ 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 #else 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF #endif ); if ( image == NULL ) { return 0; } /* Save the alpha blending attributes */ saved_flags = surface->flags&(SDL_SRCALPHA|SDL_RLEACCELOK); saved_alpha = surface->format->alpha; if ( (saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA ) { SDL_SetAlpha(surface, 0, 0); } /* Copy the surface into the GL texture image */ area.x = 0; area.y = 0; area.w = surface->w; area.h = surface->h; SDL_BlitSurface(surface, &area, image, &area); /* Restore the alpha blending attributes */ if ( (saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA ) { SDL_SetAlpha(surface, saved_flags, saved_alpha); }ピクセルの並びは、SDL_CreateRGBSurfaceのマスクとして指定しています。 そのようにして作成されたimageに対して、元のSDL_Surface(surface)から Blitを行っています。
以上の操作により、SDL_Surfaceのピクセルデータをテクスチャに変換することが できます。
DrawLogoTextureの処理に戻ります。
実際に表示をさせる部分は以下のようになっています。
/* Show the image on the screen */ SDL_GL_Enter2DMode(); glBindTexture(GL_TEXTURE_2D, global_texture); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texMinX, texMinY); glVertex2i(x, y ); glTexCoord2f(texMaxX, texMinY); glVertex2i(x+w, y ); glTexCoord2f(texMinX, texMaxY); glVertex2i(x, y+h); glTexCoord2f(texMaxX, texMaxY); glVertex2i(x+w, y+h); glEnd(); SDL_GL_Leave2DMode();global_textureをバインドし、GL_TRIANGLE_STRIPを描画します。 具体的には、三角形2つを組み合わせて長方形を作るような感じです。 長方形を作るためのGL_QUADというのもありますが、testglでは GL_TRIANGLE_STRIPを使用しているようです。 glTexCooord2fは、続くglVertex2iで指定された頂点に対応する テクスチャ座標となります。ここで与えられた座標は、 SDL_GL_LoadTextureで計算された4つの座標であることは言うまでもありません。
glVertex2iに与えている座標は、SDL_Surfaceでお馴染の、 左上が(0,0)で、右下が(w,h)という座標です。OpenGLのデフォルトは ワールド座標系と呼ばれる、画面(ビューポート)中心が(0,0)で、 右に進むと+、左に進むと-になるような座標系が使われますが、 SDL_GL_Enter2DMode()によって、画面上のピクセル単位で座標が指定できるような 座標系を構築しています。
glViewport(0, 0, screen->w, screen->h);ビューポートを設定します。ここでは、作成されたウインドウ全体を 対象とするという程度の理解で充分でしょう...
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0.0, (GLdouble)screen->w, (GLdouble)screen->h, 0.0, 0.0, 1.0);glOrthoは正射影の視体積を設定する関数です。 それぞれ、左、右、下、上、手前、奥の端の座標を指定しています。 この指定によって、SDL_Surfaceの世界と同じような座標系を設定することができます。
ここからはソースツアーとは名ばかりの文章が続きます。表題の通り、 前節までに理解したことを応用して、testspriteのスプライト表示部分を OpenGL化してみることにします。
まず、SDL_GL_LoadTexture、SDL_GL_Enable2DMode()/SDL_GL_Leave2DMode()、 それから、SDL_GL_LoadTexture内部で呼ばれるpower_of_two()などは そのまま使えるのでもってゆきましょう。あとは、スプライト表示部分を テクスチャつきポリゴンの配置に置き換え、1フレーム描画の最後に SDL_GL_SwapBuffers()することで目的は果たせそうです。
つづいて、testsprite.cのおおまかな流れを眺めます。
まず、コマンドラインオプションの処理を追加します。
if ( strcmp(argv[argc], "-flip") == 0 ) { videoflags ^= SDL_DOUBLEBUF; } else (追加始め) if( strcmp(argv[argc], "-gl") == 0 ){ videoflags &= ~(SDL_HWSURFACE | SDL_SWSURFACE | SDL_ANYFORMAT); videoflags ^= SDL_OPENGL; } else (追加終わり) if ( strcmp(argv[argc], "-fullscreen") == 0 ) { videoflags ^= SDL_FULLSCREEN; } elseサーフェス系のオプションは、いちおう全部外しておきましょう。 以後、 -glが指定されたかどうかの判別は、videoflags & SDL_OPENGLでとりあえず よいでしょう。
SDL_SetVideoModeに先立ってOpenGLの初期設定を行います。 testgl.cのものを一部修正してそのまま載せちゃいましょう。
if ( videoflags & SDL_OPENGL ){ int rgb_size[3]; /* Initialize the display */ switch (video_bpp) { case 8: rgb_size[0] = 3; rgb_size[1] = 3; rgb_size[2] = 2; break; case 15: case 16: rgb_size[0] = 5; rgb_size[1] = 5; rgb_size[2] = 5; break; default: rgb_size[0] = 8; rgb_size[1] = 8; rgb_size[2] = 8; break; } SDL_GL_SetAttribute( SDL_GL_RED_SIZE, rgb_size[0] ); SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, rgb_size[1] ); SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, rgb_size[2] ); SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 ); SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 ); }つづいて、LoadSpriteでicon.bmpを読ませた後に、それを テクスチャに変換する作業を挿入します。 まず、グローバル変数としてテクスチャのハンドルと、座標を保持する配列を 作ります。ついでにSDL_opengl.hもincludeしておきましょう...
#include "SDL_opengl.h" static GLuint texture; static GLfloat texcoord[4];本来であればHAVE_OPENGLで括るべきなんでしょうが、とりあえず無視します。 あとで上げるパッチではそこらへんも考慮したものを載せるつもりです。
LoadSpriteの直後に、以下のコードを挿入します。
if ( videoflags & SDL_OPENGL ){ /* Create texture */ texture = SDL_GL_LoadTexture(sprite, texcoord); if ( texture == 0 ){ exit(1); } glClearColor(0.0, 0.0, 0.0, 1.0); }glClearColorは、その後で出てくるbackgroundの処理を先取りして行っています。
ここまでできれば、あとはMoveSpriteをGL化するだけで完成です。
MoveSpritesの中身で分岐させることも考えられますが、ここは少々横着して MoveSpritesを元にしたGL専用の関数を作成しましょう。 メインループの処理を以下のように変更します。
(変更前) MoveSprites(screen, background); (変更後) if( videoflags & SDL_OPENGL) MoveSpritesGL(screen); else MoveSprites(screen, background);
MoveSpritesを元に、MoveSpriteGLを作成します。
void MoveSpritesGL(SDL_Surface *screen) { int i, nupdates; SDL_Rect area, *position, *velocity; nupdates = 0; /* Erase all the sprites if necessary */ if ( sprites_visible ) { /* SDL_FillRect を glClearに変更 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } SDL_GL_Enter2DMode();描画に先立って、SDL_GL_Enter2DMode()しておきます。 続いて、テクスチャをバインドします。今回は、すべてのポリゴンに同じ テクスチャを貼り付けますので、ループの外で1回だけbindすればOkです。 (さらに言うと、今回は3Dと2Dの混合表示ではないので、SDL_GL_Enter2DMode()/ SDL_GL_Leave2DMode()はメインループの外にあってもよいことになります)
glBindTexture(GL_TEXTURE_2D, texture);
描画本体です。といってもスプライトの移動ルーチンなどはまったく一緒で、 最後のSDL_BlitSurfaceのところだけが変わっています。
/* Move the sprite, bounce at the wall, and draw */ for ( i=0; i<numsprites; ++i ) { position = &positions[i]; velocity = &velocities[i]; position->x += velocity->x; if ( (position->x < 0) || (position->x >= (screen->w - sprite_w)) ) { velocity->x = -velocity->x; position->x += velocity->x; } position->y += velocity->y; if ( (position->y < 0) || (position->y >= (screen->h - sprite_w)) ) { velocity->y = -velocity->y; position->y += velocity->y; } /* Blit the sprite onto the screen */ area = *position; /* SDL_BlitSurface()のかわりに以下を実行させる */ glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(position->x, position->y); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(position->x + sprite_w, position->y); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(position->x, position->y + sprite_h); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(position->x + sprite_w, position->y + sprite_h); glEnd(); /* ここまで */ sprite_rects[nupdates++] = area; }
後始末をして終わりです。
SDL_GL_Leave2DMode(); SDL_GL_SwapBuffers(); sprites_visible = 1; }GL対応という点では無駄の多いコードですが、素直に置き換えができるのが お分かりかと思います。
以上のコードを実行してみると分かりますが、本物のtestspriteでは 実現できていたカラーキー(抜き色)の処理ができていません。
RGBAのテクスチャと、それを貼り付けるポリゴンは、SDL_Surfaceでいうと それぞれpixelのアルファ値、Surface全体のアルファ値として利用したいところです。 それができれば、カラーキーにあたる部分を透過にしておくことで抜き色の 処理ができます。
SDL_GL_LoadTexture内で、テンポラリサーフェスを作ってBlitなどを 行っていましたが、実はあの処理には、テクスチャのピクセルデータの並びに 変換する他に、カラーキーをアルファに変換する作用を持たせています。 ですから、本来であればカラーキーもきちんと実現できるはずなのですが、 SDL_GL_Enter2DMode()の設定の関係でその部分がうまく働いていません。
テクスチャとポリゴンの色とアルファの関係は、glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, ???)で決定することができます。 pyOpenGLのドキュメントに、その内容が 書かれています。現在、SDL_GL_Enter2DModeの中で GL_DECALと指定されていますので、テクスチャが貼り付けられた ポリゴンの最終的な様子は、
したがって、カラーキーに対応させるためには、SDL_GL_Enter2DMode()の直後に 以下の2行を追加すればよいことになります。
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glColor4d(1.0, 1.0, 1.0, 1.0);(glTexEnvfの方は、SDL_GL_Enter2DModeの方をいじるという手もあります。 正直、SDL_GL_Enter2DModeでGL_DECALにしている理由がよくわかりません。 どなたか教えてください...)
OpenGLをSDLとともに使うと、SDLのビデオ以外の部分、つまり イベントやオーディオやジョイスティック、CD-ROMなどの部分は そのまま利用することができます。SDLに慣れた人はもちろん、GLUTの 代替としても使いやすいのではないでしょうか。 個人的にはコールバックモデルよりもSDL風のループ記述の方が好きなので、 その点も気に入っています。
さて、testspriteをGL化したことで、
ソフトウエアサーフェスは扱いが簡単で、性能もそこそこな上に、SDLが動く ほとんど全てのプラットホームで使用可能です。OpenGLは、Windows、MacOSXなどでは 比較的容易にハードウエアサポートが使用できるでしょうが、 Linuxや*BSDなどでは現状なかなか大変な面もあります。
しかし、ハードウエアサポートが使えるときの性能の高さに加え、 アルファブレンディングや、拡大縮小/回転などの変形が容易でしかも高速という 利点は、ハードウエア/ソフトウエアサーフェスではなかなか得がたいものだと 思います。 次節から、OpenGLでそのような変形を行うにはどうすればよいのかという 点について考察します。
以下はすべて、MoveSpriteGL()内の下記の部分を対象としています。
/* SDL_BlitSurface()のかわりに以下を実行させる */ glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(position->x, position->y); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(position->x + sprite_w, position->y); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(position->x, position->y + sprite_h); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(position->x + sprite_w, position->y + sprite_h); glEnd(); /* ここまで */
すでに述べた通り、ポリゴンのアルファ値が スプライト全体のアルファ値のように扱うことができます。 他の色成分も、スプライトの色味として使用することができます。
拡大縮小にはいくつかの方法が考えられると思います。
というわけで、残りの2つのやりかたを考えてみます。
テクスチャ座標をそのままにした状態で、ポリゴンのサイズを変えてやれば よいことになります。
double ratio = 0.5; glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(position->x, position->y); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(position->x + sprite_w * ratio, position->y); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(position->x, position->y + sprite_h * ratio); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(position->x + sprite_w * ratio, position->y + sprite_h * ratio); glEnd();ratio = 0.5 なら半分、2.0なら2倍のサイズに見えるはずです。
double ratio = 0.5; glLoadIdentity(); glScalef(ratio, ratio, 1.0); glMatrixMode(GL_MODELVIEW); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(position->x / ratio, position->y / ratio); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(position->x / ratio + sprite_w, position->y / ratio); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(position->x / ratio, position->y / ratio + sprite_h); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(position->x / ratio + sprite_w, position->y / ratio + sprite_h); glEnd();
glScalef(x, y, z)というものを使うと、倍率を変えることができます。 x, y, z はそれぞれの倍率です。 左上が(0,0)、右下が(640,480)のときにglScalef(0.5, 0.5, 1.0)すると、 左上はそのままで、右下が(1280,960)になるというふうに、 座標そのものが間延びしたり縮んだりするイメージです。 (ここらへん、いい加減な説明なので誤解を招きそう)
glScale、glTranslate、glRotateなどの呼び出しは、 回転してから移動して拡大といったように、 積み重ねて呼び出すことができます。拡大縮小、回転、移動などは すべて行列の演算として実現できますが、それをどんどん追加してゆく イメージです。glLoadIdentity()は、すでに積み重なっている それらの行列を破棄して、単位行列(つまり何も変換しない)にします。 その上で、glScalefで拡大・縮小の処理を行います。
は、ポリゴンの座標を変えてあげればよいことは言うまでもないのですが...
glTranslatefで座標軸を平行移動させて、ポリゴンを新しい座標軸の原点の まわりに置くような処理が可能です。
glLoadIdentity(); glTranslatef(position->x + sprite_w / 2, position->y + sprite_h / 2, 0.0); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(-sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(-sprite_w / 2, sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(sprite_w / 2, sprite_h / 2); glEnd();座標軸を、表示位置 + スプライトのサイズの半分だけ移動し、ポリゴンを その原点のまわりに配置します。目に見える結果は同じです。
先程の「倍率を変える」も、以下のように書き直すことができます。
double ratio = 2.0; glLoadIdentity(); glTranslatef(position->x + sprite_w / 2, position->y + sprite_h / 2, 0.0); glScalef(ratio, ratio, 1.0); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(-sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(-sprite_w / 2, sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(sprite_w / 2, sprite_h / 2); glEnd();座標の再計算の処理がなくなり、とても分かりやすくなりました。
もちろん、回転したように見えるようにポリゴンの座標を計算してやることで 回転を実現することもできますが、今回はglRotateを使ってみます。 このとき指定するのは回転する角度と軸です。 たとえば、glRotatef(45.0, 0.0, 0.0, 1.0) とすると、Z軸を回転軸として 45度回転します。回転軸から遠い部分はそれだけ大きく移動することになりますが、 前節で挙げた座標軸の平行移動を使うことで、各々のスプライトを 中心に回転させることができるわけです。
double angle = 45.0; glLoadIdentity(); glTranslatef(position->x + sprite_w / 2, position->y + sprite_h / 2, 0.0); glRotatef(angle, 0.0, 0.0, 1.0); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(texcoord[0], texcoord[1]); glVertex2i(-sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[1]); glVertex2i(sprite_w / 2, -sprite_h / 2); glTexCoord2f(texcoord[0], texcoord[3]); glVertex2i(-sprite_w / 2, sprite_h / 2); glTexCoord2f(texcoord[2], texcoord[3]); glVertex2i(sprite_w / 2, sprite_h / 2); glEnd();GL_TRIANGLE_STRIP以下は、先程の「倍率を変える」とまったく同じになっています。 違うのは、その前段階で指定する変換の種類だけです。
最後に出てきた回転の例を再度見てみます。
glTranslatef(position->x + sprite_w / 2, position->y + sprite_h / 2, 0.0); glRotatef(angle, 0.0, 0.0, 1.0); // 以下、原点のまわりにスプライトを貼る処理先程から、座標軸を変えるという視点でこの変換を見てきました。 つまり、この例では
逆に、物体を変形するという視点からこの変換を見てみると以下のようになります。
上記の2つの視点を混同すると、 望む変換をどのような順番で記述すればいいのかが分からなくなってしまいますので 注意しましょう。
glTexSubImage2D()を使うと、すでにあるテクスチャのデータを 置き換えることたできます。我々が現在扱っている例では、SDL_Surfaceを 元にテクスチャを作成していますので、元のSDL_Surfaceをいじって、 再度テクスチャを生成しなおせばよいのですが、glTexSubImage2D()の方が 高速に置き換えることができます。
ここらへんの実装は次の節で...
SDL_GL_LoadTextureによって、SDL_Surfaceとテクスチャを容易に結びつけることが できるようになりました。これをもう少し押しすすめて、よりそれっぽい パッケージを作ってみましょう。サポートしたいのは以下のようなものです。
まあ名前はなんだっていいのですが、情報保持のための構造体を作ります。 こんな感じでしょうか。
typedef struct { GLuint texture; SDL_Surface* surface; int surface_dirty; GLfloat texcoord_maxx, texcoord_maxy; } GL_Sprite;minx/minyは0.0なので保持しなくてよいでしょう... SDL_Surfaceをいじったときはsurface_dirtyを1にします。
では確保の処理です。
GL_Sprite* GL_Sprite_new(SDL_Surface* surface) { GL_Sprite* result = malloc(sizeof(GL_Sprite)); GLfloat texcoord[4]; if(result == NULL) return result; result->surface = surface; result->texture = SDL_GL_LoadTexture(surface, texcoord); result->texcoord_maxx = texcoord[2]; result->texcoord_maxy = texcoord[3]; result->surface_dirty = 0; return result; }素直に実装しているだけですね。続いて描画
void GL_Sprite_bind(GL_Sprite* sprite) { if(sprite->surface_dirty) GL_Sprite_subimage(sprite); glBindTexture(GL_TEXTURE_2D, sprite->texture); } void GL_Sprite_draw(GL_Sprite* sprite) { int w, h; w = sprite->surface->w; h = sprite->surface->h; GL_Sprite_bind(sprite); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(0.0, 0.0); glVertex2i(-w / 2, -h / 2); glTexCoord2f(sprite->texcoord_maxx, 0.0); glVertex2i(w / 2, -h / 2); glTexCoord2f(0.0, sprite->texcoord_maxy); glVertex2i(-w / 2, h / 2); glTexCoord2f(sprite->texcoord_maxx, sprite->texcoord_maxy); glVertex2i(w / 2, h / 2); glEnd(); }GL_Sprite_subimage()は、SDL_GL_LoadTextureとそっくりな処理を して、glTexImage2Dの部分をglTexSubImage2Dにするだけなので省略です。
以上のコードを使うと、GL_Sprite* gl_spriteに対して適切な タイミングで初期化を行い、GL_Sprite_draw(gl_sprite) とするだけで 描画が完了することになります。 呼び出す前にいちいちglTranslatefで位置を決めなきゃいけないのは 面倒といえば面倒ですので、指定座標に描画するものも作っておきましょう。
void GL_Sprite_draw_xy(GL_Sprite* sprite, int x, int y) { int w, h; w = sprite->surface->w; h = sprite->surface->h; GL_Sprite_bind(sprite); glBegin(GL_TRIANGLE_STRIP); glTexCoord2f(0.0, 0.0); glVertex2i(x, y); glTexCoord2f(sprite->texcoord_maxx, 0.0); glVertex2i(x + w, y); glTexCoord2f(0.0, sprite->texcoord_maxy); glVertex2i(x, y + h); glTexCoord2f(sprite->texcoord_maxx, sprite->texcoord_maxy); glVertex2i(x + w, y + h); glEnd(); }変換処理を入れるなら原点まわりの方が扱いやすいでしょうが、 特定の場所に表示したいだけ、たとえば、
頂点ごとにテクスチャの座標を割当てられるのと同様に、 頂点ごとに色を指定することができます。頂点と頂点の間は 距離に応じた中間色で表示されますので、以下のようにすると ちょっと影の薄いsmileyが動くことになります。
int w, h; w = gl_sprite->surface->w; h = gl_sprite->surface->h; GL_Sprite_bind(gl_sprite); glBegin(GL_TRIANGLE_STRIP); glColor4f(1.0, 1.0, 1.0, 1.0); glTexCoord2f(0.0, 0.0); glVertex2i(-w / 2, -h / 2); glColor4f(1.0, 1.0, 1.0, 0.0); glTexCoord2f(gl_sprite->texcoord_maxx, 0.0); glVertex2i(w / 2, -h / 2); glColor4f(1.0, 1.0, 1.0, 1.0); glTexCoord2f(0.0, gl_sprite->texcoord_maxy); glVertex2i(-w / 2, h / 2); glColor4f(1.0, 1.0, 1.0, 0.0); glTexCoord2f(gl_sprite->texcoord_maxx, gl_sprite->texcoord_maxy); glVertex2i(w / 2, h / 2); glEnd();アルファではなくて、黒になるように変化させると (つまりglColor4f(1.0, 1.0, 1.0, 0.0) ではなくて glColor4f(0.0, 0.0, 0.0, 1.0)とすると) 影のあるSmileyになります。 さらに右下だけ赤っぽくすると、夕日の中のSmileyとか、 上半分を青っぽくすると朝礼中のSmiley...とか、 この手の処理は SDL_Surfaceだけでやるとちょっと大変そうですね。