数字のテクスチャを操る

先日の書籍OpenGL学習中です。

まだ最初の方しか読んでいませんが、

「1-7 数字の描画関数の作成」が個人的に興味深かったです。

要は、こんな感じのテクスチャ画像を用意しておいて、

こんな感じに画面に表示するつうことですな。


プログラムのイメージとしては、

int number = 123456789; /* 出力する数字の整数値 */
float x = 0.0f, y = 0.0f; /* 出力先の2次元座標 */
DrawNumber(number, x, y); /* こんな命令で上のように表示できたらいいね */

という感じです。
うまく応用すれば、数字とアルファベットのカスタムフォントを自在にレンダリングできそうです。
(元々カスタムフォントを導入する別の方法があるので、その必要性があるかどうかは置いといて・・・)



さて、参考書で上記の数字レンダリングを実現するサンプルが紹介されていたのですが、

void drawNumber(float x, float y, float width, float height, GLuint texture, int number, int figures, int red, int green, int blue, int alpha);

という、まぁ・・・なんともチャーミングな関数になっております。

サンプルとしては、これはこれで分かりやすくて良いのですが、

私は自分で使うときにもう少し楽に扱いたいので、以下のようにカスタマイズしてみました。

・テクスチャを引数で受け取るのではなく、カプセル化する
・描画メソッドの引数を減らす
・指定桁数未満の数字を描画する際に0埋めするかどうかをオプションで指定できるようにする
・中央寄せ以外に、右寄せ、左寄せオプションを指定できるようにする


以下、実装コードです。
【クラス定義】

//
// 数字テクスチャクラス
// テクスチャ要件
// ・4×4セル内に左上から順に数字が格納されていること
//
//		0 1 2 3
//		4 5 6 7
//		8 9 _ _
//		_ _ _ _
//
// ・テクスチャ画像全体の幅、高さが等しいこと
// ・それぞれの数字の幅、高さがすべて等しいこと
// ・テクスチャ画像全体の幅、高さのサイズが4の倍数であること
//
// 制約事項
// ・現在の実装では、マイナスの値を表示できない
// ・現在の実装では、整数のみ対応しており、小数点は表示できない
//
@interface NumberTexture2D : NSObject
{
    GLuint _texture;    // テクスチャID
    int _figures;       // 最大表示桁数
    BOOL _fillZero;     // ゼロ埋めフラグ(表示する数字の桁数が_figuresに満たない場合にゼロ埋め)
    float _dispSize;	// 1文字の表示サイズ
    int _align;         // テキストアラインメント
    int _r;             // 赤成分
    int _g;             // 緑成分
    int _b;             // 青成分
    int _a;             // アルファ成分
}

// 最大表示桁数
@property(assign, nonatomic, readwrite) int figures;

// ゼロ埋めフラグ
@property(assign, nonatomic, readwrite) BOOL fillZero;

// 1文字の表示サイズ
@property(assign, nonatomic, readwrite) float size;

// テキストアラインメント
@property(assign, nonatomic, readwrite) int align;

// 初期化メソッド
-(id)initWithImageName:(NSString*)textureName;

// 描画メソッド
-(void)draw:(int)number:(float)x:(float)y;

// 色指定メソッド(デフォルトは255, 255, 255, 255)
-(void)setColor:(int)r:(int)g:(int)b:(int)a;
@end

// テキストアラインメント定数列挙
enum {
    NUMBER_TEXTURE2D_ALIGN_CENTER = 1,    // 中央揃え(デフォルト)
    NUMBER_TEXTURE2D_ALIGN_RIGHT,         // 右揃え
    NUMBER_TEXTURE2D_ALIGN_LEFT,          // 左揃え
};


【クラス実装】

// 数字テクスチャクラス
@implementation NumberTexture2D

@synthesize figures = _figures;
@synthesize fillZero = _fillZero;
@synthesize size = _dispSize;
@synthesize align = _align;

// 初期化メソッド
// 戻り値		生成したオブジェクトへの参照値。生成に失敗した場合はnil
// 引数	textureName	テクスチャファイル名
-(id)initWithImageName:(NSString*)textureName {
    self = [super init];
	
    if (self) {
        // 画像ファイルを読み込む
        CGImageRef image = [UIImage imageNamed:textureName].CGImage;

        if (!image) {
            NSLog(@"Error:%@ not found.", textureName);
            return nil;
        }

        // 画像の大きさを取得する
        size_t width = CGImageGetWidth(image);
        size_t height = CGImageGetHeight(image);

        // サイズチェック
        if (width != height) {
            NSLog(@"width and height of the texture is not equal. width:%zu, height:%zu", width, height);
            return nil;
        }

        if (0 != width % 4) {
            NSLog(@"width and height of the texture is not a multiple of 4. width:%zu, height:%zu", width, height);
            return nil;
        }

        // テクスチャをロードする
        _texture = LoadTexture(image);

        if (!_texture) {
            return nil;
        }

        // 可変属性にデフォルト値を適用
        _figures = 4;
        _dispSize = 0.25f;
        _fillZero = NO;
        _align = NUMBER_TEXTURE2D_ALIGN_CENTER;
        _r = _g = _b = _a = 255;
    }
	
    return self;
}

// 破棄メソッド
-(void)dealloc {
    if (_texture) {
        glDeleteTextures(1, &_texture);
        _texture = 0;
    }

    [super dealloc];
}

// 描画メソッド
// 引数	number	描画する数値(0以上であること)
//	x	描画先X座標
//	y	描画先Y座標
//
// alignプロパティの値によって、x, yの指定位置が異なる。
//	・NUMBER_TEXTURE2D_ALIGN_CENTER:	(x, y)を中心として数字を描画する
//	・NUMBER_TEXTURE2D_ALIGN_RIGHT:		(x, y)を右端として、左側に数字を描画する
//	・NUMBER_TEXTURE2D_ALIGN_LEFT:		(x, y)を左端として、右側に数字を描画する
//
-(void)draw:(int)number:(float)x:(float)y {
    // 桁数が0以下の指定の場合は何も描画しない
    if (0 >= _figures) {
        return;
    }
	
    // 表示サイズが負の場合は何も描画しない
    if (0.0f > _dispSize) {
        return;
    }
	
    // 負の値は描画できないので、0とする
    number = MAX(0, number);
	
    // 実際に描画する数字の桁数を求める
    int rf;
	
    if (_fillZero) {
        rf = _figures;
    }
    else {
        int f = 1;

        for (int i = 10; 0 != number / i; i *= 10) {
            ++ f;
        }
		
        rf = MIN(_figures, f);
    }

    // 描画幅を求める
    float width = _dispSize * (float)rf;
	
    // 描画領域の右端の座標を求める
    float rightX;
	
    if (NUMBER_TEXTURE2D_ALIGN_CENTER == _align) {
        rightX = x + width / 2;
    }
    else if (NUMBER_TEXTURE2D_ALIGN_RIGHT == _align) {
        rightX = x;
    }
    else if (NUMBER_TEXTURE2D_ALIGN_LEFT == _align) {
        rightX = x + width;
    }
    else {
        return;
    }
	
    // 1桁目から数字を1文字ずつ描画する
    for (int i = 0; rf > i; ++ i) {
        // 描画する数字の中心座標を求める
        float figNX = rightX - _dispSize / 2 - (float)i * _dispSize;

        // 描画する数字を求める
        int numberToDraw = number / (int)pow(10.0, (double)i) % 10;
		
        // 描画する数字のUV座標を求める
        float u = numberToDraw % 4 * 0.25f;
        float v = numberToDraw / 4 * 0.25f;

        // 1文字描画する
        DrawTexture(_texture, figNX, y, _dispSize, _dispSize, u, v, 0.25f, 0.25f, _r, _g, _b, _a);
    }
}

// 色指定メソッド
// 引数	r	赤成分
//	g	緑成分
//	b	青成分
//	a	アルファ成分
-(void)setColor:(int)r:(int)g:(int)b:(int)a {
    _r = MIN(255, r);
    _r = MAX(0, r);
    _g = MIN(255, g);
    _g = MAX(0, g);
    _b = MIN(255, b);
    _b = MAX(0, b);
    _a = MIN(255, a);
    _a = MAX(0, a);
}

@end


途中出てくるLoadTexture関数と、DrawTexture関数は、ほぼ参考書の内容のままですが、多少カスタマイズしてありますので、コードを掲載しておきます。

// テクスチャ読み込み関数
// 戻り値	テクスチャID
// 引数	image	テクスチャ画像
GLuint LoadTexture(CGImageRef image) {
	GLuint texture;
	
	// 画像の大きさを取得する
	size_t width = CGImageGetWidth(image);
	size_t height = CGImageGetHeight(image);
	
	// ビットマップデータを用意する
	GLubyte* imageData = (GLubyte*)malloc(width * height * 4/* rgba */);
	
	if (!imageData) {
		NSLog(@"Error:failed to memory allocation.");
		return 0;
	}
	
	CGContextRef imageContext = CGBitmapContextCreate(imageData, width, height, 8, width * 4,
									  CGImageGetColorSpace(image),
									  kCGImageAlphaPremultipliedLast);
	
	CGContextDrawImage(imageContext, CGRectMake(0, 0, (CGFloat)width, (CGFloat)height), image);
	CGContextRelease(imageContext);
	
	// OpenGL用のテクスチャを生成
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);
	glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
	
	free(imageData);
	return texture;
}

// テクスチャ部分指定描画関数
// 引数	texture	テクスチャID
//	x	長方形の中心X座標
//	y	長方形の中心Y座標
//	width	幅
//	height	高さ
//	u	テクスチャ部分開始点U(横座標)
//	v	テクスチャ部分開始点V(縦座標)
//	tw	テクスチャ幅
//	th	テクスチャ高さ
//	r	赤成分(0〜255)
//	g	緑成分(0〜255)
//	b	青成分(0〜255)
//	a	透明度(0〜255)
void DrawTexture(GLuint texture, float x, float y, float width, float height,
				 float u, float v, float tw, float th, int r, int g, int b, int a) {
	GLfloat vertices[] = {
		x - width / 2, y - height / 2,
		x + width / 2, y - height / 2,
		x - width / 2, y + height / 2,
		x + width / 2, y + height / 2,
	};
	
	// マッピング座標の格納順は、左下、右下、左上、右上の順
	GLfloat texCoords[] = {
		u,      v + th,
		u + tw, v + th,
		u,      v,
		u + tw, v,
	};
	
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texture);
	glVertexPointer(2, GL_FLOAT, 0, vertices);
	glEnableClientState(GL_VERTEX_ARRAY);
	glColor4ub(r, g, b, a);
	glDisableClientState(GL_COLOR_ARRAY);
	glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);
	
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
	
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glDisable(GL_TEXTURE_2D);
}

【使い方】

// rendererクラスのinitでテクスチャオブジェクトを初期化しておく
numberTexture = [[NumberTexture2D alloc] initWithImageName:@"number_texture.png"];

// 表示形式はお好みで設定
numberTexture.figures = 10;
numberTexture.size = 0.2f;
numberTexture.fillZero = YES;
numberTexture.align = NUMBER_TEXTURE2D_ALIGN_CENTER;

// rendererクラスのrender内で数字を描画する
[numberTexture draw:123456789:0.0f:0.0f];

// rendererクラスのdeallocで破棄する
[numberTexture release];