前提
『マルチプラットフォームのためのOpenGL ES入門 基礎編―Android/iOS対応グラフィックスプログラミング』
の抜粋メモです。12章の内容。
コードについては断片のみなので、本書を読んでください。
スプライト
もともとは2Dゲーム機で画像を描画する仕組みや画像そのもののこと。
文脈によって変わるが、ここでの Sprite は2次元グラフィックス全般のこと。
スプライトの座標系
glViewport で使用するウィンドウ座標系は左下原点。
本書では便宜上、「左上を原点として、整数のピクセル単位で示される座標系」を スプライト座標系 と呼ぶ。
スプライト描画の前提条件
- スプライト座標系の任意の位置に、任意の大きさで画像描画できること
- 画像内の任意の位置を切り出して描画できること
任意の座標に四角形を描画する
ピクセル単位の任意位置へ描画できれば、ディスプレイサイズ(Android の dpi や解像度、iOS の contentScaleFactor)に合わせてスプライトのサイズを変更することは容易に、直感的になる。
ピクセル数が同一に、すなわち、解像度の低いディスプレイでは大きく、解像度の高いディスプレイでは小さく見えるようになる。
サンプル(sample_rendering_pixel_quad.c):
/**
* アプリのレンダリングを行う
* 毎秒60回前後呼び出される。
*/
void sample_RenderingPixelQuad_rendering( GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_RenderingPixelQuad *extension = (Extension_RenderingPixelQuad*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attr_posを有効にする
glEnableVertexAttribArray(extension->attr_pos);
// このブロックはカリングを含めて正しい順番で頂点が定義されていることをチェックします
#if 0 /* カリングチェック */
{
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
#endif /* カリングチェック */
{
// TODO 解説
// 四角形のXYWHを指定する
const GLint pixel_x = 50;
const GLint pixel_y = 100;
const GLint pixel_width = 200;
const GLint pixel_height = 300;
GLfloat VERTEX_LEFT = ((GLfloat) pixel_x / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_RIGHT = ((GLfloat) (pixel_x + pixel_width) / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_TOP = (((GLfloat) pixel_y / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
GLfloat VERTEX_BOTTOM = (((GLfloat) (pixel_y + pixel_height) / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
// 左上へ四角形描画
const GLfloat position[] = {
//
// v1(left top)
VERTEX_LEFT, VERTEX_TOP,
// v2(left bottom)
VERTEX_LEFT, VERTEX_BOTTOM,
// v3(right top)
VERTEX_RIGHT, VERTEX_TOP,
// v4(right bottom)
VERTEX_RIGHT, VERTEX_BOTTOM, };
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
{
// TODO 解説
// 四角形を右下に表示する
const GLint pixel_width = 150;
const GLint pixel_height = 100;
const GLint pixel_x = app->surface_width - pixel_width - 1; // はみ出していないことを確認するため、1ピクセル横にずらす
const GLint pixel_y = app->surface_height - pixel_height - 1; // はみ出していないことを確認するため、1ピクセル横にずらす
GLfloat VERTEX_LEFT = ((GLfloat) pixel_x / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_RIGHT = ((GLfloat) (pixel_x + pixel_width) / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_TOP = (((GLfloat) pixel_y / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
GLfloat VERTEX_BOTTOM = (((GLfloat) (pixel_y + pixel_height) / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
// 右下へ四角形描画
const GLfloat position[] = {
//
// v0(left top)
VERTEX_LEFT, VERTEX_TOP,
// v1(left bottom)
VERTEX_LEFT, VERTEX_BOTTOM,
// v2(right top)
VERTEX_RIGHT, VERTEX_TOP,
// v3(right bottom)
VERTEX_RIGHT, VERTEX_BOTTOM, };
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
以下で四角形の描画先を示し(ピクセル単位なので GLint)
const GLint pixel_x = 50;
const GLint pixel_y = 100;
const GLint pixel_width = 200;
const GLint pixel_height = 300;
それを OpenGL ES で描画を行うために正規化デバイス座標系に変換する
(LEFT, RIGHT は x 座標、TOP, BOTTOM は y 座標)
GLfloat VERTEX_LEFT = ((GLfloat) pixel_x / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_RIGHT = ((GLfloat) (pixel_x + pixel_width) / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_TOP = (((GLfloat) pixel_y / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
GLfloat VERTEX_BOTTOM = (((GLfloat) (pixel_y + pixel_height) / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
テクスチャアトラスを描画
任意の座標へスプライト描画ができれば、あとはテクスチャ付きで描画するだけで良いが、「テクスチャをそのまま描画する」という OpenGL の利用法は少ない、あるいは推奨されない。
OpenGL ES が推奨するのは、テクスチャ解像度は 2^n ピクセルの正方形だが、殆どの場合は任意のピクセル数の長方形or正方形を描画する必要がある。
そこで、複数の画像を1枚の大きな POT テクスチャに収めることにより、GPU上の制限もなく、テクスチャユニット切り替えやバインドの切り替えを行う必要もなくなる(特にテクスチャのバインドは GPU 負荷が大きい)。この テクスチャアトラス(Texture Atlas) 化と呼ぶ。
こんなかんじで
void sample_RenderingPixelQuadPixelUv_rendering(GLApplication *app) {
// サンプルアプリ用データを取り出す
Extension_RenderingPixelQuadPixelUv *extension = (Extension_RenderingPixelQuadPixelUv*) app->extension;
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// attr_posを有効にする
glEnableVertexAttribArray(extension->attr_pos);
glEnableVertexAttribArray(extension->attr_uv);
{
// データテーブルを用意する
struct AtlasTable {
/**
* 画像内のX座標
*/
int x;
/**
* 画像内のY座標
*/
int y;
/**
* 元画像の幅
*/
int width;
/**
* 元画像の高さ
*/
int height;
} imagetable[] = {
// アトラス化された画像のデータテーブル
// x, y, width, height
{ 513, 1, 458, 402 }, // 鹿の画像
{ 301, 513, 128, 128 },//
{ 1003, 61, 16, 16 }, //
{ 513, 403, 256, 256 }, //
{ 1, 513, 300, 400 }, //
{ 971, 61, 32, 32 }, //
{ 1, 913, 64, 64 }, //
{ 1003, 77, 8, 8 }, //
{ 1, 1, 512, 512 }, //
{ 971, 1, 50, 60 }, //
};
struct AtlasTable image = imagetable[3]; // [0]〜[9]で変更可能
// 四角形のXYWHを指定する
const GLint pixel_x = 0;
const GLint pixel_y = 0;
const GLint pixel_width = image.width;
const GLint pixel_height = image.height;
// 頂点位置を計算する
GLfloat VERTEX_LEFT = ((GLfloat) pixel_x / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_RIGHT = ((GLfloat) (pixel_x + pixel_width) / (GLfloat) app->surface_width) * 2.0f - 1.0f;
GLfloat VERTEX_TOP = (((GLfloat) pixel_y / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
GLfloat VERTEX_BOTTOM = (((GLfloat) (pixel_y + pixel_height) / (GLfloat) app->surface_height) * 2.0f - 1.0f) * -1.0f;
// UV位置を計算する
GLfloat UV_LEFT = (GLfloat) image.x / (GLfloat) extension->texture->width;
GLfloat UV_TOP = (GLfloat) image.y / (GLfloat) extension->texture->height;
GLfloat UV_RIGHT = (GLfloat) (image.x + image.width) / (GLfloat) extension->texture->width;
GLfloat UV_BOTTOM = (GLfloat) (image.y + image.height) / (GLfloat) extension->texture->height;
// 頂点位置
const GLfloat position[] = {
//
// v0(left top)
VERTEX_LEFT, VERTEX_TOP,
// v1(left bottom)
VERTEX_LEFT, VERTEX_BOTTOM,
// v2(right top)
VERTEX_RIGHT, VERTEX_TOP,
// v3(right bottom)
VERTEX_RIGHT, VERTEX_BOTTOM, };
// 指定したUV座標に変換する
const GLfloat uv[] = {
// triangle 0
// v0(left top)
UV_LEFT, UV_TOP,
// v1(left bottom)
UV_LEFT, UV_BOTTOM,
// v2(right top)
UV_RIGHT, UV_TOP,
// v3(right bottom)
UV_RIGHT, UV_BOTTOM, };
glVertexAttribPointer(extension->attr_pos, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) position);
glVertexAttribPointer(extension->attr_uv, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid*) uv);
glBindTexture(GL_TEXTURE_2D, extension->texture->id);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// バックバッファをフロントバッファへ転送する。プラットフォームごとに内部の実装が異なる。
ES20_postFrontBuffer(app);
}
UV座標の対応付けは先のサンプルと同じく、任意のピクセル座標を幅あるいは高さで割ってやればよい。
- X座標
テクスチャU座標 = X座標(ピクセル)/画像の幅(ピクセル) - Y座標
テクスチャV座標 = Y座標(ピクセル)/画像の高さ(ピクセル)
テクスチャアトラスの作成
これが便利らしい。テクスチャアトラス作成を支援するツールは他にもたくさんある。