ガンズターン 公式サイト

楽しいことに、まじめです。 ——ガンズターンアプリ研究所公式サイト

OpenGLES1.1で行列を使わずテクスチャ回転させる方法のメモ(2D限定)

Pocket

前置き長いので先に目次です。

「チャッピー」見てきましたよ

こんにちわ。
ガンズターンのRyosukeです。
先日の記事で、最後にちょっぴりだけ言及した映画「チャッピー」を、実際に見てきました。

いやー、個人的にはめちゃくちゃおもしろかったっす。

あんまり書くとネタバレになる&自分の頭の悪さが露呈することになりかねないので詳しい感想は省きますが、初期の「チャッピー」が見せてくれる「新しいAI像」には心を打たれました。

AIって「人工知能」であり「ソフトウェア」なわけですが、見方を変えると「生命そのもの」を作るのとほぼ同義ですよね。
いやまあ、物理的な身体を持っていない時点で「生命」と言い切ってしまうのは言い過ぎかもしれませんが、少なくとも「精神活動をする新たな存在」を作り出すことと同義ではあるわけです。

これまでのSF映画では、AIというとしゃちほこばった機械的な喋り方が主流でした。
ドラマ「ナイトライダー」に出てくる「ナイト2000」みたいに、ウィットに富んだ語り口のAIもたまに登場しますが、そうはいっても「既存のロボットの延長線」上に存在する「知能を持った便利な道具」ぐらいの扱いが多かったわけです。(少なくとも、これまで自分の見てきた映画の大部分では)

それが、この「チャッピー」では、生まれたばかりのAIをまるっきり「赤ん坊」として扱っていて、それがとても新鮮でした。

実際にAIが生まれたとして、やっぱり最初はなんにも知らないだろうから、この映画の「チャッピー」みたいに「まっさらな存在」として誕生するんだと思います。

そうして「まっさらな存在」として生まれ落ちた「新たな生命」が、どのように育っていくのか……。
それはやはり、周囲の人間がどのように接していくか、という部分に大きく影響を受けるわけで……。

性善説、性悪説などの、哲学的な話題と絡めて観賞してみても、とてもおもしろいテーマの映画なのではないでしょうか。

わたしはもともと「人工知能」に強く興味があったのでこの映画を見ようと思いましたが、これまであまり「人工知能」に興味を持たずにきた方がご覧になっても十分に楽しめる内容になっていると思います。(アクションシーンも多いですし)

「チャッピー」、オススメです。

……おっと、ほんの2、3行感想を書くつもりだったのが、気がつくとこんな長文になってしまいました。(汗)

今日は、題名にもある通り、OpenGLES1.1を使った2D表示の回転について、自分なりのメモを残しておきたいと思います。
いつも通りターゲットを絞りに絞った記事になりますが、どなたかのご参考になれば幸いです。

ちなみに、OpenGLESをまったく知らない人にとってはまるっきり意味不明な記事であることを最初に断っておきます。
いずれ、まとまった時間を使って、OpenGLESそのものの初心者向けの記事なんかも書きたいなあ、なんて夢想してるわけですが、それはまた別のお話。

1. OpenGLES2.0じゃないの?

はい。すみません。
わたし、基本的には2Dでゲームを作ることしかしないもので、今のところOpenGLES2.0を実開発に導入する予定はありません。
OpenGLES1.1でも、2D表示だけをするのであれば困った点はありませんし、これまで培ってきた自作クラスのほとんどがOpenGLES1.1用に最適化されていて、単純に2.0を導入すると途端に動かなくなってしまうものばかりでして……。

そんなわけで、「自作ゲームに3Dを導入する」OR「iOS端末がOpenGLES1.1に完全非対応になる」のいずれかの状況にならない限りは、今後もOpenGLES1.1を主力ライブラリとして開発を続けていくことになりそうです。

2. とりあえず「Sprite」クラスを実装します

今回は話を単純化するために以下の前提条件に絞って話を続けます。

  1. 使用する言語はObjective-C
  2. iOS端末上でOpenGLES1.1を用いた2D表示を行うものとする
  3. OpenGLES1.1を使うための事前準備は完了している
  4. プロジェクト内の任意の画像ファイルを読み込み、textureIdを割り当てることのできるloadTexture関数が実装されている。
    (GLuint loadTexture(NSString* fileName) という形式)
  5. とりあえず1アトラス画像について1画像のみ存在する時だけ考える。(バッチ処理的なことはしない)

さて。
いささか卑怯な前提条件(とくに4.のあたりは凶悪)ですが、気にせず話を進めます。
上記前提条件が整っている状態で、まずは「Sprite」クラスを実装しましょう。
といっても、ここではめちゃくちゃ単純化したものを作ります。
ざっとコードを書くとこんな感じ。

//Sprite.h
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>

@interface Sprite : NSObject {
    GLuint textureId;       // スプライトのテクスチャーId
    float x, y;             // スプライトの画面上の表示座標
    float width, height;    // スプライトの画面上の横幅、縦幅
    float u, v;             // スプライトの、アトラス画像上の左上座標
    float pw, ph;           // スプライトの、アトラス画像上の横幅、縦幅
    float angle;            // スプライトの回転角度
}

@property(nonatomic, assign) GLuint textureId;
@property(nonatomic, assign) float x, y;
@property(nonatomic, assign) float width, height;
@property(nonatomic, assign) float u, v;
@property(nonatomic, assign) float pw, ph;
@property(nonatomic, assign) float angle;

- (void) draw;

@end

とりあえず、メソッドは init と draw だけあれば十分でしょう。
ここで、いきなり回転を考慮したdrawを書いてもよいのですが、それではブログの記事が終了しちゃうので、もったいぶってまずは回転を考慮しないバージョンのdrawを書きます。

//Sprite.m

#import "Sprite.h"

@implementation Sprite

@synthesize textureId;    
@synthesize x, y;
@synthesize width, height;
@synthesize u, v;
@synthesize pw, ph;
@synthesize angle;

- (id) init {
    if (self = [super init]) {
        self.x = 0.0f;
        self.y = 0.0f;
        self.width = 100.0f;
        self.height = 100.0f;
        self.u = 0.0f;
        self.v = 0.0f;
        self.pw = 1.0f;
        self.ph = 1.0f;
        self.angle = 0.0f;
    }
    return self;
}

//まずは回転(angle)を考慮しないdraw
- (void) draw {
    //頂点の配列
    //一つのスプライトあたり6頂点 x 2要素(x,y)
    GLfloat vertices[6 * 2];

    //色の配列
    //一つのスプライトあたり6頂点 x 4要素(r,g,b,a)
    GLubyte colors[6 * 4];

    //テクスチャマッピングの配列
    //一つのスプライトあたり6頂点 x 2要素(x,y)
    GLfloat texCoords[6 * 2];

    //各配列のカレントインデックス
    int vertexIndex = 0;
    int colorIndex = 0;
    int texCoordIndex = 0;

    float vLeft1, vLeft2, vRight1, vRight2, 
          vTop1, vTop2, vBottom1, vBottom2;
    float halfW = 0.5f*self.width;
    float halfH = 0.5f*self.height;

    /* ここからあとで手を加える部分です。 */

    //各頂点の計算を簡単にするための値
    vLeft1 = -halfW + self.x;
    vLeft2 = vLeft1;
    vRight1 = halfW + self.x;
    vRight2 = vRight1;
    vTop1 = halfH + self.y;
    vTop2 = vTop1;
    vBottom1 = -halfH + self.y;
    vBottom2 = vBottom1;

    /* ここまでがあとで手を加える部分です。 */

    //ポリゴン1の各頂点
    vertices[vertexIndex++] = vLeft2;
    vertices[vertexIndex++] = vBottom1; //左下

    vertices[vertexIndex++] = vRight2;
    vertices[vertexIndex++] = vBottom2; //右下

    vertices[vertexIndex++] = vLeft1;
    vertices[vertexIndex++] = vTop1; //左上

    //ポリゴン2の各頂点
    vertices[vertexIndex++] = vRight2;
    vertices[vertexIndex++] = vBottom2; //右下

    vertices[vertexIndex++] = vLeft1;
    vertices[vertexIndex++] = vTop1; //左上

    vertices[vertexIndex++] = vRight1;
    vertices[vertexIndex++] = vTop2; //右上

    //色
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //左下

    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //右下

    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //左上

    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //右下

    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //左上

    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = 255;
    colors[colorIndex++] = alpha; //右上

    //マッピング座標の計算を簡単にするための値
    float leftU = self.u;
    float rightU = self.u + self.pw;
    float topV = self.v;
    float bottomV = self.v + self.ph;

    //ポリゴン1のマッピング座標
    texCoords[texCoordIndex++] = leftU ;
    texCoords[texCoordIndex++] = topV; //左上

    texCoords[texCoordIndex++] = rightU;
    texCoords[texCoordIndex++] = topV; //右上

    texCoords[texCoordIndex++] = leftU;
    texCoords[texCoordIndex++] = bottomV; //左下

    //ポリゴン2のマッピング座標
    texCoords[texCoordIndex++] = rightU;
    texCoords[texCoordIndex++] = topV; //右上

    texCoords[texCoordIndex++] = leftU;
    texCoords[texCoordIndex++] = bottomV; //左下

    texCoords[texCoordIndex++] = rightU;
    texCoords[texCoordIndex++] = bottomV; //右下        

    //描画処理
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, self.textureId);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glEnableClientState(GL_VERTEX_ARRAY);
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
    glEnableClientState(GL_COLOR_ARRAY);

    glTexCoordPointer(2, GL_FLOAT, 0, texCoords);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glDrawArrays(GL_TRIANGLES, 0, activeSpriteCount * 6);

    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisable(GL_TEXTURE_2D);
}

3. 作成した「Sprite」クラスの使い方

これは簡単です。
例えば「 opengl2dtest.png」という名前の以下のような画像を表示させたいのであれば、実際に描画をしたい処理の中で、以下のように記述してあげます。

黄色いヘルメット

表示させたい画像

//描画したい処理の実装部分の冒頭
#import "Sprite.h"

/* 中略 */

// 実際に描画をしたい処理(例えばdrawingというメソッドがあったとして)    
- (void) drawing {
    Sprite *spr = [[Sprite alloc] init];
    spr.textureId = loadTexture(@"opengl2dtest.png");
    [spr draw];
}

こうしてやれば、画面のどこかに黄色いヘルメットの画像が表示される……はず(笑)。

実際には、OpenGLの座標系をどのように定義するかで表示のされ方が左右されるので、あなたの実装環境上でどのように表示されるかまではわかりませんが……。

まあここでは「どのようにテクスチャーを回転させるか」という説明に絞りたいので、詳細は割愛。

長くなってますが、ここまでが準備段階です。

4. 回転を考える

実は、回転をさせるにあたって、参考書通りに実装してもどうしてもうまくいかなかったので、この記事を書こうと思いました。

んで、参考書の式のどこかがまちがっているはずなのにどこが間違っているのかぜんぜんわからず、けっきょく手計算で一から考え直した結果、以下のような実装に落ち着きました。
(いきなり結論ですみません。 ><; )

手を加えるのは、もとの「draw」メソッドの中の「vLeft1」〜「vBottom2」の各値を計算する部分だけです。

具体的には以下のように変更します。

    /* ここからあとで手を加える部分です。 */

    //各頂点の計算を簡単にするための値
    float rad = self.angle*(M_PI/180.0f); //回転角のラジアン化
    float cos = cosf(rad);
    float sin = sinf(rad); 

    vLeft1 = self.x - halfW*cos - halfH*sin;
    vLeft2 = self.x - halfW*cos + halfH*sin;
    vRight1 = self.x + halfW*cos - halfH*sin;
    vRight2 = self.x + halfW*cos + halfH*sin;
    vTop1 = self.y - halfW*sin + halfH*cos;
    vTop2 = self.y + halfW*sin + halfH*cos;
    vBottom1 = self.y - halfW*sin - halfH*cos;
    vBottom2 = self.y + halfW*sin - halfH*cos;

    /* ここまでがあとで手を加える部分です。 */

結論だけ書いてみると、回転角をラジアン化して、それの正弦(sin)と余弦(cos)を計算して、ポリゴンの頂点位置計算に組み入れているだけですね。

こうしてやれば、Spriteのangleを適宜変更してやることで、その画像が回転します。(するはずです。笑)

それだけなのに、この式を自力で導出するのに、かつて丸一日費やしたおバカなわたしでした。

……というわけで、たったこれだけのことをメモとして残しておきたいがためだけに、大量の紙幅を費やしてしまいました。

たぶん、OpenGL知らない人にはなんのこっちゃわけわかめな記事である上に、OpenGL知ってる人にとってみたら「今更そんなこと書かれても」的な記事だったかもしれません。

……でも、自分にとってはそれなりの知恵熱をあげて生み出した式だったりするわけで……。
(そして、今となってはこの式の詳しい導出方法も忘れてしまっているという……汗)

ひょっとしたら行列使って回転を実装した方が100倍簡単だったかも……なんて今更ながらに思ったりもするわけですが。
ブログの恥は書き捨て、ともよく言われてますので、ここに晒そうと思います。

今日は一段と、独りよがりな記事になってしまったことを反省しつつ……。

それではまた。ガンズターンのRyosukeでした! m(_ _)m

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

トラックバックURL: http://www.gunsturn.com/2015/06/09/opengl_round_2d_image/trackback/