スプライトで描画2013/11/08

テクスチャで画像を描画できるようになりましたが、毎度OpenGLを直接叩くのでは手順が面倒です。
描画を管理するスプライトクラスを作って、手軽に描画できるようにしましょう。

・スプライトクラス(sys::Sprite)
スプライトはテクスチャと、描画する範囲(切り抜き)から成っています。つまり1枚のテクスチャには複数のスプライトの画像を収めることができます(描画時、テクスチャの切り替えにはコストがかかるので、なるべく少ないテクスチャにまとめておいた方が効率が良いです)。

Sprite.h
#ifndef	___SPRITE_H___
#define	___SPRITE_H___

#include	"common.h"
#include	"Texture.h"


namespace sys
{

/****************
    スプライト
 ****************/
class Sprite
{
protected :
	static u32 const*	spr_color;		// カラー

public :

// 原点位置
enum
{
	X_CENTER	= 0x00,			// センタリング
	X_LEFT		= 0x01,			// 左揃え
	X_RIGHT		= 0x02,			// 右揃え
	Y_CENTER	= 0x00,			// センタリング
	Y_TOP		= 0x10,			// 上揃え
	Y_BOTTOM	= 0x20,			// 下揃え
};

	static void		set_color(u32 _color = 0xffffffff);		// カラー設定
	static void		set_color(u8 _r, u8 _g, u8 _b, u8 _a = 0xff)
					{
						set_color(RGBA(_r, _g, _b, _a));
					}
	static void		set_color(u32 const*);


	Texture*	texture;			// テクスチャ
	GLfloat		texcoord[4][XY];	// UV座標
	short		width, height;		// 大きさ
	short		ox, oy;				// 原点

	void	set(Texture*, SRect const*, int _origin = X_CENTER | Y_CENTER);				// 設定
	void	set(Texture*, int _origin = X_CENTER | Y_CENTER);							// 設定(テクスチャに合わせた大きさ)
	void	set(short, short, int _origin = X_CENTER | Y_CENTER);						// 設定(テクスチャ無し)
	void	set(short, const void*, int _origin = X_CENTER | Y_CENTER);
	void	set_origin(int _origin = X_CENTER | Y_CENTER);								// 原点位置設定

	void	draw(GLfloat*);			// 描画
	void	draw(float, float);
	void	draw(float, float, float, float);
	void	draw(float _x, float _y, float _scl)
			{
				draw(_x, _y, _scl, _scl);
			}
	void	draw(float, float, float, float, float);
	void	draw(float const*);
};

}

#endif
/****************** End of File ***************************************/

Sprite.cpp
/*******************************

		スプライト

 *******************************/

#include "Sprite.h"
#include "Renderer.h"


namespace sys
{

u32 const*	Sprite::spr_color;			// カラー


/**************************************
    設定
		引数	_tex    = テクスチャ
				_coord  = UV座標
				_origin = 原点位置
 **************************************/
void	Sprite::set(Texture* _tex, SRect const* _coord, int _origin)
{
	texture	= _tex;						// テクスチャ
	texcoord[0][X] = texcoord[2][X] = (GLfloat)_coord->x/_tex->width;		// UV座標
	texcoord[0][Y] = texcoord[1][Y] = (GLfloat)_coord->y/_tex->height;
	texcoord[1][X] = texcoord[3][X] = (GLfloat)(_coord->x + _coord->w)/_tex->width;
	texcoord[2][Y] = texcoord[3][Y] = (GLfloat)(_coord->y + _coord->h)/_tex->height;
	width	= _coord->w;				// 幅
	height	= _coord->h;				// 高さ
	set_origin(_origin);				// 原点位置設定
}

void	Sprite::set(Texture* _tex, int _origin)
{
	texture	= _tex;						// テクスチャ
	texcoord[0][X] = texcoord[2][X] = 0.0f;			// UV座標
	texcoord[0][Y] = texcoord[1][Y] = 0.0f;
	texcoord[1][X] = texcoord[3][X] = 1.0f;
	texcoord[2][Y] = texcoord[3][Y] = 1.0f;
	width	= _tex->width;				// 幅
	height	= _tex->height;				// 高さ
	set_origin(_origin);				// 原点位置設定
}

void	Sprite::set(short _width, short _height, int _origin)
{
	texture	= NULL;						// テクスチャ無し
	width	= _width;					// 幅
	height	= _height;					// 高さ
	set_origin(_origin);				// 原点位置設定
}

/************************************
    原点位置設定
		引数	_origin = 原点位置
 ************************************/
void	Sprite::set_origin(int _origin)
{
	switch ( _origin & 0x0f ) {
	  case X_CENTER :				// センタリング
		ox = width/2;
		break;

	  case X_LEFT :					// 左揃え
		ox = 0;
		break;

	  case X_RIGHT :				// 右揃え
		ox = width;
		break;
	}

	switch ( _origin & 0xf0 ) {
	  case Y_CENTER :				// センタリング
		oy = height/2;
		break;

	  case Y_TOP :					// 上揃え
		oy = 0;
		break;

	  case Y_BOTTOM :				// 下揃え
		oy = height;
		break;
	}
}


/***********************************
    カラー設定
		引数	_color = カラー値
 ***********************************/
void	Sprite::set_color(u32 _color)
{
	if ( _color == 0xffffffff ) {			// デフォルト
		static const
		u32		color_white[] =
				{
					0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
				};

		spr_color = color_white;
	}
	else if ( (_color != spr_color[0]) || (_color != spr_color[1]) || (_color != spr_color[2]) || (_color != spr_color[3]) ) {
		u32*	_p = (u32*)Renderer::get_prim_buffer(sizeof(u32)*4);		// カラーバッファ

		_p[0] = _color;
		_p[1] = _color;
		_p[2] = _color;
		_p[3] = _color;
		spr_color = _p;
	}
}

void	Sprite::set_color(u32 const* _color)
{
	u32*	_p = (u32*)Renderer::get_prim_buffer(sizeof(u32)*4);		// カラーバッファ

	_p[0] = _color[0];
	_p[1] = _color[1];
	_p[2] = _color[2];
	_p[3] = _color[3];
	spr_color = _p;
}


/***********************************
    描画
		引数	_x, _y   = 位置
				_sx, _sy = 拡大率
				_rot     = 回転角
 ***********************************/
void	Sprite::draw(GLfloat* _vertex)
{
	if ( texture ) {				// テクスチャ有り
		Renderer::use_shader(Renderer::SHADER_TEXTURE);		// シェーダ
		texture->bind();									// テクスチャ
		Renderer::set_texcoord(&texcoord[0][0]);			// UV座標
	}
	else {							// テクスチャ無し
		Renderer::use_shader(Renderer::SHADER_PLAIN);		// シェーダ
	}
	sys::Renderer::set_vertex(_vertex);						// 頂点
	Renderer::set_color((GLubyte*)spr_color);				// カラー

	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);					// 描画
}

void	Sprite::draw(float _x, float _y)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	_vertex[0*XY + X] = _vertex[2*XY + X] = _x - ox;
	_vertex[0*XY + Y] = _vertex[1*XY + Y] = _y - oy;
	_vertex[1*XY + X] = _vertex[3*XY + X] = _x + (width - ox);
	_vertex[2*XY + Y] = _vertex[3*XY + Y] = _y + (height - oy);
	draw(_vertex);
}

void	Sprite::draw(float _x, float _y, float _sx, float _sy)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	_vertex[0*XY + X] = _vertex[2*XY + X] = _x - _sx*ox;
	_vertex[0*XY + Y] = _vertex[1*XY + Y] = _y - _sy*oy;
	_vertex[1*XY + X] = _vertex[3*XY + X] = _x + _sx*(width - ox);
	_vertex[2*XY + Y] = _vertex[3*XY + Y] = _y + _sy*(height - oy);
	draw(_vertex);
}

void	Sprite::draw(float _x, float _y, float _sx, float _sy, float _rot)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	float	_cos = cosf(_rot), _sin = sinf(_rot),
			_cx0, _sx0, _cy0, _sy0, _cx1, _sx1, _cy1, _sy1;

	_cx0 = -_sx*ox; _sx0 = _sin*_cx0; _cx0 *= _cos;
	_cy0 = -_sy*oy; _sy0 = _sin*_cy0; _cy0 *= _cos;
	_cx1 = _sx*(width - ox); _sx1 = _sin*_cx1; _cx1 *= _cos;
	_cy1 = _sy*(height - oy); _sy1 = _sin*_cy1; _cy1 *= _cos;

	_vertex[0*XY + X] = _x + _cx0 - _sy0;
	_vertex[0*XY + Y] = _y + _cy0 + _sx0;
	_vertex[1*XY + X] = _x + _cx1 - _sy0;
	_vertex[1*XY + Y] = _y + _cy0 + _sx1;
	_vertex[2*XY + X] = _x + _cx0 - _sy1;
	_vertex[2*XY + Y] = _y + _cy1 + _sx0;
	_vertex[3*XY + X] = _x + _cx1 - _sy1;
	_vertex[3*XY + Y] = _y + _cy1 + _sx1;
	draw(_vertex);
}

void	Sprite::draw(float const* vertex)
{
	GLfloat*	_vertex = (GLfloat*)Renderer::get_prim_buffer(sizeof(GLfloat)*4*XY);		// 頂点バッファ

	for (int i = 0; i < 4*XY; i++) {
		_vertex[i] = vertex[i];
	}
	draw(_vertex);
}

}

/******************* End of File *******************************************************/

・初期化
void set(Texture* _tex, SRect const* _coord, int _origin)
void set(Texture* _tex, int _origin)
テクスチャ _tex、描画範囲座標 _coordでスプライトを初期化します。描画範囲を省略すると、テクスチャ自体と同じ大きさ(1枚のテクスチャ = 1枚のスプライト)になります。
_originはスプライトの原点を指定します。省略すると画像の中心が原点になります(sys::Sprite::X_CENTER | sys::Sprite::Y_CENTER)。

void set(short _width, short _height, int _origin)
テクスチャを指定しないと、幅 _width、高さ _heightの単色ポリゴンになります。

・描画
void draw(float _x, float _y)
座標(_x, _y)に描画します。

void draw(float _x, float _y, float _sx, float _sy)
縦と横の拡大率を指定して描画します。

void Sprite::draw(float _x, float _y, float _sx, float _sy, float _rot)
角度 _rot(単位:ラジアン)で回転して描画します。

void draw(float const* vertex)
直接4頂点(vertex[4][XY])を指定して描画します。


さて描画時に頂点座標をOpenGLに設定しますが、このデータをスプライト自体に持っておくと1度に1枚のスプライトしか描画できなくなります。よってRendererの方で汎用的なバッファを持っておいて、描画の度にそこから使用するようにします。
なお、UV座標はスプライトごとに固定なのでスプライト側で持っておきます。

Rendererの汎用的なプリミティブバッファの管理部分です。
def.hにバッファサイズ PRIM_BUF_SIZEを定義しておきます。

Renderer.cpp
	prim_buffer = (u8*)memalign(4, PRIM_BUF_SIZE);			// プリミティブ用汎用バッファ
	prim_p = 0;

/****************************************
    プリミティブバッファ取得
			引数	_size = 使用サイズ
			戻り値	バッファ
 ****************************************/
void*	Renderer::get_prim_buffer(u32 _size)
{
	if ( prim_p + _size > PRIM_BUF_SIZE ) {
		prim_p = 0;
	}

	void*	_ret = (void*)(prim_buffer + prim_p);

	prim_p += _size;
	return	_ret;
}


また、テクスチャのバインドなど変化していないのに設定し直すのは効率が悪いので、現在の値を覚えておくようにします。
ただし、頂点は無条件で設定しています。全く同じ座標に何枚もプリミティブを描画することは少ないと思われますので。
/**********************************************
    テクスチャ使用
		引数	tex = テクスチャオブジェクト
 **********************************************/
void	Renderer::bind_texture(GLuint tex)
{
	if ( tex != current_texture ) {
		glBindTexture(GL_TEXTURE_2D, tex);
		current_texture = tex;
	}
}

/********************************
    カラー設定
		引数	color = カラー
 ********************************/
void	Renderer::set_color(GLubyte const* color)
{
	if ( color != current_color ) {
		glVertexAttribPointer(current_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, color);
		current_color = color;
	}
}

/********************************
    テクスチャUV座標設定
		引数	coord = UV座標
 ********************************/
void	Renderer::set_texcoord(GLfloat const* coord)
{
	if ( coord != current_texcoord ) {
		glVertexAttribPointer(current_shader->texcoord, 2, GL_FLOAT, GL_FALSE, 0, coord);
		current_texcoord = coord;
	}
}

/***********************************
    頂点座標設定
		引数	vertex = 頂点座標
 ***********************************/
void	Renderer::set_vertex(GLfloat const* vertex)
{
	glVertexAttribPointer(current_shader->position, 2, GL_FLOAT, GL_FALSE, 0, vertex);
}


それでは、実際の使用例です。
AppMain.cpp

#include "common.h"
#include "Sprite.h"


// テクスチャ
enum
{
	TEX_PHOTO = 0,			// 背景
	TEX_BALL,				// ビー玉
	TEX_MAX,
};

// スプライト
enum
{
	SPR_PHOTO = 0,			// 背景
	SPR_BALL_BLUE,			// 青ビー玉
	SPR_BALL_GREEN,			// 緑ビー玉
	SPR_BALL_RED,			// 赤ビー玉
	SPR_BALL_YELLOW,		// 黄ビー玉
	SPR_MAX,
};

static sys::Texture		texture[TEX_MAX];			// テクスチャ
static sys::Sprite		sprite[SPR_MAX];			// スプライト

static int	cnt;			// カウンタ


/************
    初期化
 ************/
void	init_app(void)
{
	static const
	char*	tex_name[TEX_MAX] =
			{
				"photo.png",
				"biidama.png",
			};
	u8*		_buf;

	for (int i = 0; i < TEX_MAX; i++) {				// テクスチャ読み込み
		_buf = (u8*)sys::load_asset(tex_name[i]);
		texture[i].load_png(_buf);
		free(_buf);
	}


	struct SprDef
	{
		short	tex_num;			// テクスチャ番号
		SRect	coord;				// UV座標
	};

	static const
	SprDef	spr_def[SPR_MAX] =
			{
				{TEX_PHOTO,		{  0,   0, 640, 960}},			// 背景
				{TEX_BALL,		{  0,   0,  60,  60}},			// 青ビー玉
				{TEX_BALL,		{ 60,   0,  60,  60}},			// 緑ビー玉
				{TEX_BALL,		{  0,  60,  60,  60}},			// 赤ビー玉
				{TEX_BALL,		{ 60,  60,  60,  60}},			// 黄ビー玉
			};

	for (int i = 0; i < SPR_MAX; i++) {				// スプライト設定
		sprite[i].set(&texture[spr_def[i].tex_num], &spr_def[i].coord);
	}

	cnt = 0;			// カウンタ
}

/******************************
    稼働
		戻り値	アプリ続行か
 ******************************/
Bool	update_app(void)
{
	sprite[SPR_PHOTO].draw(0.0f, 0.0f);			// 背景
	for (int i = 0; i < 4; i++) {				// ビー玉
		int		t = ((cnt + i*15) % 60) - 30;

		sprite[SPR_BALL_BLUE + i].draw(i*128 - 192, 400 - (30*30 - t*t)/2);
	}
	cnt++;

	return	TRUE;
}

/**************** End of File *************************************************/

跳ねるビー玉

※ビー玉の画像は「かわいいフリー素材集 いらすとや」様から使わせていただきました。どうもありがとうございます。


プロジェクト一式は、こちらです。