SDL Source Tour Extra Vol.1

前書き

この番外編は、testgl.c や testsprite.cのコードを読み、 SDLのOpenGL対応への理解を深めることを目的としています。

本ソースツアーでは1.2.0を対象にすることになっていますが、 事情により番外編では執筆時点の最新版である1.2.7を対象にいたします。

testgl.c

グラデーションつきのキューブがくるくるとまわるというデモです。

ここでは、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() までがそれにあたります。

icon.bmpの描画

		/* 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に対して

といったような処理をすると、100x100のSDL_Surfaceに入っていた画像が 「原寸大」で表示されることになり、2Dのスプライトのように使うことが できるようになります。3番目の 「それを100x100ピクセルとして表示されるように配置」については、 後程出てくるSDL_GL_Enter2DMode()といったあたりが味噌になります。

サーフェスの変換

順序が逆になりますが、実際に、テクスチャを作成する処理は以下のようになります。

	/* 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のGL化を試みる

ここからはソースツアーとは名ばかりの文章が続きます。表題の通り、 前節までに理解したことを応用して、testspriteのスプライト表示部分を OpenGL化してみることにします。

まず、SDL_GL_LoadTexture、SDL_GL_Enable2DMode()/SDL_GL_Leave2DMode()、 それから、SDL_GL_LoadTexture内部で呼ばれるpower_of_two()などは そのまま使えるのでもってゆきましょう。あとは、スプライト表示部分を テクスチャつきポリゴンの配置に置き換え、1フレーム描画の最後に SDL_GL_SwapBuffers()することで目的は果たせそうです。

つづいて、testsprite.cのおおまかな流れを眺めます。

というわけで、GL対応をオプションの一つとして 実現できるような試みをしてみましょう。

まず、コマンドラインオプションの処理を追加します。

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と指定されていますので、テクスチャが貼り付けられた ポリゴンの最終的な様子は、

となります。一方、GL_MODULATEだと、RGBAともに テクスチャとポリゴンの乗算となっていますので、ポリゴンの 色を(1.0, 1.0, 1.0, 1.0)にしてやることで テクスチャの色とアルファを使用することができるようになります。 乗算ですから、たとえばポリゴンのアルファを減らせば全体的に 半透明になりますし、RGBをいじってやると色味を変えることもできるわけです。

したがって、カラーキーに対応させるためには、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にしている理由がよくわかりません。 どなたか教えてください...)

SDLとOpenGL

SDLとともに使うOpenGL

OpenGLをSDLとともに使うと、SDLのビデオ以外の部分、つまり イベントやオーディオやジョイスティック、CD-ROMなどの部分は そのまま利用することができます。SDLに慣れた人はもちろん、GLUTの 代替としても使いやすいのではないでしょうか。 個人的にはコールバックモデルよりもSDL風のループ記述の方が好きなので、 その点も気に入っています。

機能・性能から見るOpenGL

さて、testspriteをGL化したことで、

それぞれの性能(fps)を調べることができるようになりました。 testsprite程度の処理の場合は、 といった結果になることが大半ではないかと思います。 特に、ハードウエアサーフェスの性能が極立っているのではないかと思いますが、 ハードウエアサーフェスは、使えるプラットホームがあまり多くなく、 また、アルファブレンディングつきのblitがとても遅くなるという欠点があります。

ソフトウエアサーフェスは扱いが簡単で、性能もそこそこな上に、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つは、近いものは大きく見え、遠いものは小さく見えるという 投影変換(Perspective)を使用しているときに有効です。 我々が現在使っている正射影では見える大きさは変わりません。

というわけで、残りの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つの視点を混同すると、 望む変換をどのような順番で記述すればいいのかが分からなくなってしまいますので 注意しましょう。

pixelいじり

glTexSubImage2D()を使うと、すでにあるテクスチャのデータを 置き換えることたできます。我々が現在扱っている例では、SDL_Surfaceを 元にテクスチャを作成していますので、元のSDL_Surfaceをいじって、 再度テクスチャを生成しなおせばよいのですが、glTexSubImage2D()の方が 高速に置き換えることができます。

ここらへんの実装は次の節で...

スプライトまわりをパッケージ化

SDL_GL_LoadTextureによって、SDL_Surfaceとテクスチャを容易に結びつけることが できるようになりました。これをもう少し押しすすめて、よりそれっぽい パッケージを作ってみましょう。サポートしたいのは以下のようなものです。

GL_Sprite構造体

まあ名前はなんだっていいのですが、情報保持のための構造体を作ります。 こんな感じでしょうか。

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だけでやるとちょっと大変そうですね。


戻る

Zinnia (zinnia@risky-safety.org)
このWebコンテンツ(ここから辿れるもの)に対する コメントのメールは許可なく公開することがあります。