OpenGL ES2.0でポイントスプライト(2)
※OpenGLでポイントスプライト(1)の続きとなります。
サンプルプログラムの要点
ここでは前回の記事に掲載した、OpenGL ES2.0でポイントスプライトを利用するサンプルプログラムの要点についてメモしていきます。
GLKitを用いたテクスチャの読み込み
ポイントスプライトを利用するためには、当然ポイントに貼付けるためのテクスチャを用意する必要があります。しかし素のOpenGL ES2.0には、汎用的な画像ファイル(JPEGとかPNGとか)をテクスチャとして読み込む仕組みは用意されていません。そのため、本来なら自前でメソッドを実装したりしないといけないのですが、幸いなことにGLKitには画像ファイルのパスを指定するだけで、自動的にテクスチャを生成してくれる GLKTextureLoader が用意されています。以下に使い方のサンプルコードを示します(前回のサンプルプログラムより抜粋)。
//GLKBaseEffectを使用する準備。初期化のとき(setupGL メソッドの冒頭など)で行っておけば良い。 self.effect = [[GLKBaseEffect alloc] init]; //テクスチャのロード。以下の手続きで画像をテクスチャとして読み込み、テクスチャユニット0にバインドすることが出来る。 //「particle.png」というPNG画像を読み込む例 NSString* filePath = [[NSBundle mainBundle] pathForResource:@"particle" ofType:@"png"]; GLKTextureInfo *texInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil]; if (texInfo) { //確認用。このログ表示は削除しても構わない。 NSLog(@"Texture loaded successfully. name = %d size = (%d x %d)", texInfo.name, texInfo.width, texInfo.height); } self.effect.texture2d0.name = texInfo.name;
頂点シェーダにおけるポイントサイズの設定
サンプルプログラムにおける頂点シェーダの全コードを以下に再掲します。
//Shader.vsh attribute vec4 position; attribute vec4 color; varying vec4 vcolor; uniform mat4 modelViewProjectionMatrix; void main() { vcolor = color; gl_Position = modelViewProjectionMatrix * position; //ポイントのサイズをここで設定する gl_PointSize = 20.0; }
頂点シェーダのコードは非常にシンプルです。attribute変数 color に入力された各頂点の色は、そのまま(varying変数を介して)フラグメントシェーダに渡しています。attribute変数 position に入力された各頂点の位置は、渡された変換行列(モデルビュー行列と投影行列を乗算したもの)によって座標変換されます。
そして、組み込みの特殊変数である gl_PointSize に値を設定することにより、ポイントサイズをピクセル単位で設定することが出来ます。昔のOpenGLではシェーダを用いずとも "glPointSize(20.0f);" といった関数によりポイントサイズを変更出来たようですが、OpenGL ES2.0ではこのような関数は(私が試した限りでは)使えないようです。
またgl_PointSize に定数の値を設定すると、当然全てのポイントは同じサイズで表示されますが、場合によってはポイントに遠近感を付けて表示したい場合もあります。すなわち、視点に近いポイントほど大きく、遠いポイントほど小さく描画されるようにしたいわけです。これは以下のように、ある定数を gl_Position の w 要素で割った値を gl_PointSize に設定することにより容易に実現出来ます。
//gl_Positionには、既に(座標変換された)ポイントの位置が格納されているとする gl_PointSize = 200.0 / gl_Position.w;
詳しい原理については(私自身が理解しきれていないので)省きますが、視点の位置に対して、各ポイントの位置が離れていればいるほど w 要素の値は大きくなります。すなわち上記の方法によって、視点からの距離に反比例してポイントサイズの大きさが変わるようになり、以下の画像のように遠近感を付けることが出来ます。
フラグメントシェーダによるアルファテスト
サンプルプログラムにおけるフラグメントシェーダの全コードを以下に再掲します。
//Shader.fsh precision mediump float; uniform sampler2D s_texture; varying vec4 vcolor; void main() { //サンプラで取り込んだテクスチャを、変数"baseColor"に格納 vec4 baseColor = texture2D(s_texture, gl_PointCoord); //アルファ値が0.5未満である場合はフラグメントを破棄(アルファテスト) if(baseColor.a < 0.5){ discard; } else{ //元々のテクスチャの色に、各ポイントに設定された色を付けて出力する gl_FragColor = baseColor * vec4(vcolor); } }
フラグメントシェーダのコードも非常にシンプルであり、基本的にはサンプラによって取り込んだテクスチャの色に、頂点シェーダから(varying変数を介して)受け取った各頂点の色を付けて出力しているだけです。
そして、テクスチャを格納する変数 baseColor の a 要素には、テクスチャ画像のアルファ値が入っています。この値が0.5未満である場合には discard キーワードを呼び出しています。discard を呼び出すと、そのフラグメントは破棄されるため、アルファ値が0.5未満の領域を切り抜く(あるいは透明にする)ことが出来ます。
昔のOpenGLではシェーダを用いずとも、glAlphaFunc(GL_GREATER, 0.5); や glEnable(GL_ALPHA_TEST); といった関数によってアルファテストの機能を利用することが出来たのですが、例によってOpenGL ES2.0ではこれらはサポートされなくなったようです。
OpenGL ES2.0でポイントスプライト(1)
OpenGLでは、GL_POINTS で描かれる点(ポイント)の1つ1つにテクスチャを貼付ける「ポイントスプライト」という機能をサポートしています。これらのポイントは、そのサイズを拡大すると実は正方形として描かれており、その全面にテクスチャを貼付けることが出来ます。この正方形は常に視点に対して垂直な方向に表示される(あくまで点である以上、視点の方向によって見え方が変わらない)ため、ビルボードの代わりとして用いることが出来ます。
またアルファブレンドを有効にした状態で、様々な色を付けた多数の粒子(パーティクル)をポイントスプライトにより描画すると、非常に奇麗で幻想的なグラフィックを作ることが出来ます。更にパーティクルシステム(参考:Wikipedia)をポイントスプライトを用いて実装すると、炎、爆発、流水、霧といった事象を効率的に、かつ奇麗に表現することが出来るようです。
そこで、OpenGL ES2.0で実際にポイントスプライトを用いてグラフィックを描画する方法を模索しました。参考資料が少なく、かなりの試行錯誤と挫折を重ねましたが、最近になってようやくその方法が分かってきました。それについてメモしていきます。
シェーダ利用の必要性について
私は最初、シェーダなどを使わずにGLKitの範疇だけでポイントスプライトを使用することが出来ないか、色々調べて試行錯誤してみました。しかし私が調べた限りでは、GLKitの範疇だけでポイントサイズを変更したり、ポイントへテクスチャを貼付けたりする方法を見つけることが出来ませんでした。OpenGL ES2.0でポイントスプライトを実現するためには、(部分的にでも)シェーダを利用することが避けられないようです。そこで本記事では、GLKitとシェーダを併用する形での方法についてメモします。
簡単なサンプル
ひとまず、ポイントスプライトを利用した簡単なサンプルプログラムを作成しましたので、ソースとその実行手順を示します。なお、こちらのサンプルを作成するにあたっては、「OpenGL ES2.0 プログラミングガイド」という書籍をかなり参考にしています。
- 次に、こちらのリンク先にある「particle.png」という画像(アルファチャンネル付きのPNG画像として作成した、ポイント用のテクスチャ)を適当な場所に保存します。ファイル名は「particle.png」という名前のままにしておいてください。
- そして、保存した「particle.png」をプロジェクトに追加します。Xcodeのメニューから「File」→「Add Files to "(プロジェクト名)"」と選択するとファイルを読み込むためのダイアログが開きますので、先程保存した「particle.png」を探して選択し、読み込んでください。
- 続いて、ViewController.m の中身を、以下のコードに書き換えます(ViewController.h の書き換えは不要)。このコードは「OpenGL Game」テンプレートで自動生成されるサンプルを土台としており、前述のようにGLKitとシェーダを併用しています(シェーダのコンパイルやロード関連の方法については、土台となったサンプルの方法をほぼそのまま流用しています)。
//ViewController.m #import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) #define VERTEX_NUM 100 #define RADIUS 3.0f //各シェーダのuniform変数の位置を格納する配列。 enum { UNIFORM_MODELVIEWPROJECTION_MATRIX, UNIFORM_S_TEXTURE, NUM_UNIFORMS }; GLint uniforms[NUM_UNIFORMS]; //ポイントの座標および色の格納用配列 GLfloat vertex[VERTEX_NUM * 6]; @interface ViewController () { GLuint _program; GLuint _vertexArray; GLuint _vertexBuffer; GLKMatrix4 _modelViewProjectionMatrix; GLKMatrix4 modelViewMatrix; GLfloat _rotation; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; - (BOOL)loadShaders; - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file; - (BOOL)linkProgram:(GLuint)prog; - (BOOL)validateProgram:(GLuint)prog; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } // Dispose of any resources that can be recreated. } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; [self loadShaders]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); //glEnable(GL_DEPTH_TEST); デプステストはオフにしておく。 //テクスチャのロード。これだけで画像をテクスチャとして読み込み、テクスチャユニット0にバインドすることが出来る。便利! NSString* filePath = [[NSBundle mainBundle] pathForResource:@"particle" ofType:@"png"]; GLKTextureInfo *texInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:nil error:nil]; if (texInfo) { NSLog(@"Texture loaded successfully. name = %d size = (%d x %d)", texInfo.name, texInfo.width, texInfo.height); } self.effect.texture2d0.name = texInfo.name; //vertex[i]〜vertex[i+3]に、各ポイントの座標(半径"RADIUS"の球上のランダムな位置)を設定 //vertex[i+4]〜vertex[i+6]に、各ポイントの色(RGB各成分に0.5f〜1.0fの間のランダムな値)を設定 for(int i = 0; i < VERTEX_NUM * 6; i+=6){ float theta = GLKMathDegreesToRadians(arc4random()%360); float phi = GLKMathDegreesToRadians(arc4random()%360); vertex[i] = RADIUS * sin(theta) * cos(phi); vertex[i + 1] = RADIUS * sin(theta) * sin(phi); vertex[i + 2] = RADIUS * cos(theta); vertex[i + 3] = ((arc4random() % 10000) / 20000.0f) + 0.5f; vertex[i + 4] = ((arc4random() % 10000) / 20000.0f) + 0.5f; vertex[i + 5] = ((arc4random() % 10000) / 20000.0f) + 0.5f; } glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW); //各ポイントの座標、色を頂点配列に格納 glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribColor); glVertexAttribPointer(GLKVertexAttribColor, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(sizeof(float)*3)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; if (_program) { glDeleteProgram(_program); _program = 0; } } #pragma mark - GLKView and GLKViewController delegate methods - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 300.0f); self.effect.transform.projectionMatrix = projectionMatrix; modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -10.0f); modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f); _modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, modelViewMatrix); _rotation += self.timeSinceLastUpdate * 0.2f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); //ブレンディングの設定(デプステストはオフにしておく) glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); //シェーダをロードして、利用可能にする [self loadShaders]; glUseProgram(_program); //変数"_modelViewProjectionMatrix"を、頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)に渡す //(loadShadersメソッドの中で、"uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX]"に、uniform変数の場所を格納) glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m); //フラグメントシェーダでテクスチャを取り込むためのサンプラ。テクスチャユニット0に設定 glUniform1i(uniforms[UNIFORM_S_TEXTURE], 0); //ポイントの描画 glBindVertexArrayOES(_vertexArray); glDrawArrays(GL_POINTS, 0, VERTEX_NUM); glBindVertexArrayOES(0); } //以下、シェーダのロード関連のメソッド群 #pragma mark - OpenGL ES 2 shader compilation - (BOOL)loadShaders { GLuint vertShader, fragShader; NSString *vertShaderPathname, *fragShaderPathname; // Create shader program. _program = glCreateProgram(); // Create and compile vertex shader. vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"]; if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) { NSLog(@"Failed to compile vertex shader"); return NO; } // Create and compile fragment shader. fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"]; if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) { NSLog(@"Failed to compile fragment shader"); return NO; } // Attach vertex shader to program. glAttachShader(_program, vertShader); // Attach fragment shader to program. glAttachShader(_program, fragShader); // Bind attribute locations. // This needs to be done prior to linking. glBindAttribLocation(_program, GLKVertexAttribPosition, "position"); //頂点配列に格納した各ポイントの色を、頂点シェーダのattribute変数"color"に入力 glBindAttribLocation(_program, GLKVertexAttribColor, "color"); // Link program. if (![self linkProgram:_program]) { NSLog(@"Failed to link program: %d", _program); if (vertShader) { glDeleteShader(vertShader); vertShader = 0; } if (fragShader) { glDeleteShader(fragShader); fragShader = 0; } if (_program) { glDeleteProgram(_program); _program = 0; } return NO; } //頂点シェーダのuniform変数"modelViewProjectionMatrix"(mat4型)の場所を取得する。 uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX] = glGetUniformLocation(_program, "modelViewProjectionMatrix"); //フラグメントシェーダのuniform変数"s_texture"(テクスチャのサンプラ)の場所を取得する。 uniforms[UNIFORM_S_TEXTURE] = glGetUniformLocation(_program, "s_texture"); // Release vertex and fragment shaders. if (vertShader) { glDetachShader(_program, vertShader); glDeleteShader(vertShader); } if (fragShader) { glDetachShader(_program, fragShader); glDeleteShader(fragShader); } return YES; } - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file { GLint status; const GLchar *source; source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String]; if (!source) { NSLog(@"Failed to load vertex shader"); return NO; } *shader = glCreateShader(type); glShaderSource(*shader, 1, &source, NULL); glCompileShader(*shader); #if defined(DEBUG) GLint logLength; glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetShaderInfoLog(*shader, logLength, &logLength, log); NSLog(@"Shader compile log:\n%s", log); free(log); } #endif glGetShaderiv(*shader, GL_COMPILE_STATUS, &status); if (status == 0) { glDeleteShader(*shader); return NO; } return YES; } - (BOOL)linkProgram:(GLuint)prog { GLint status; glLinkProgram(prog); #if defined(DEBUG) GLint logLength; glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(prog, logLength, &logLength, log); NSLog(@"Program link log:\n%s", log); free(log); } #endif glGetProgramiv(prog, GL_LINK_STATUS, &status); if (status == 0) { return NO; } return YES; } - (BOOL)validateProgram:(GLuint)prog { GLint logLength, status; glValidateProgram(prog); glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(prog, logLength, &logLength, log); NSLog(@"Program validate log:\n%s", log); free(log); } glGetProgramiv(prog, GL_VALIDATE_STATUS, &status); if (status == 0) { return NO; } return YES; } @end
- 更に今回はシェーダを利用するため、シェーダのコードについても書き換える必要があります。まず頂点シェーダ(Shader.vsh)の中身を以下のコードに書き換えます。
//Shader.vsh attribute vec4 position; attribute vec4 color; varying vec4 vcolor; uniform mat4 modelViewProjectionMatrix; void main() { vcolor = color; gl_Position = modelViewProjectionMatrix * position; //ポイントのサイズをここで設定する gl_PointSize = 20.0; }
- 続いて、フラグメントシェーダ(Shader.fsh)の中身を以下のコードに書き換えます。
//Shader.fsh precision mediump float; uniform sampler2D s_texture; varying vec4 vcolor; void main() { //サンプラで取り込んだテクスチャを、変数"baseColor"に格納 vec4 baseColor = texture2D(s_texture, gl_PointCoord); //アルファ値が0.5未満である場合はフラグメントを破棄(アルファテスト) if(baseColor.a < 0.5){ discard; } else{ //元々のテクスチャの色に、各ポイントに設定された色を付けて出力する gl_FragColor = baseColor * vec4(vcolor); } }
- これを実行すると、以下のように球状に配置されたいくつものカラフルな点が描画されます。これらの点の一つ一つが、テクスチャ(particle.png)を貼付けられたポイントであり、各点に(頂点属性として)設定された色に応じた塗り分けがされて表示されます。またアルファブレンドを有効にしてあるため、各ポイントの色はブレンディングされて表示されます。
このサンプルプログラムのソースの詳細については、次回以降の記事にメモしていく予定です。
iOS Developer Programの更新手順
およそ1年前に登録したiPhone Developer Programの有効期限が迫ろうとしています。来年また役立つかもしれないので、更新手続きの方法を自分なりにメモしておきます(2013年6月21日時点の情報)。
- まず、iOS Developer Centerにアクセスしてログイン。すると以下のような文章が画面上部に表示されている(おそらく、有効期限が迫っている対象者にのみ?)。"Review Agreement"の部分がクリック出来るのでクリック。
ANNOUNCEMENT: Updated Program License Agreement
The license agreement for the iOS Developer Program has been updated. You must agree to it by for continued access to your membership benefits. Review Agreement
- "iOS Developer Program License Agreement"というタイトルのページ(要するに、ライセンス契約に同意するかの確認)に飛ぶので、内容を確認した上でチェックボックスにチェックを入れて、"I Agree"ボタンをクリック。
- "Menber Center"というページに飛ぶが、画面上部に以下のような文章が表示されている。"renew your membership(s)."の部分がクリック出来るのでクリック。
Your iOS Developer Program is about to expire.
To maintain your access to certain technical resources, please visit the Programs & Add-Ons section and renew your membership(s).
- "Add Your Program"というページに飛ぶ。ここには登録可能なProgramの一覧および価格が表示されており、iPhone以外にもMacやSafariのDeveloper Programが置かれている。今回はもちろんiPhone Developer Programにのチェックボックスにチェックを入れて、"Continue"ボタンをクリック(¥8400/yearでした)。
- "Review your purchase details"という内容確認のページに飛ぶので、確認後に"Continue"ボタンをクリック。
- "Proceed to your country's Apple Online Store to purchase."というページに飛ぶ。その下にJapanという表示があるのを確認して、iOS Developer Programの"Add to cart"ボタンをクリック。
- "Apple Store for Business"というページ(要するにアップルストア。ここから先は日本語サイト)に飛ぶので、カート内の商品としてiOS Developer Programが1つ入っていることを確認した上で、「注文手続きへ」ボタンをクリック。あとは画面の指示に従って、通常の通販と同じように注文を確定させて行けばOK 。一応、配送先住所の入力が必要になりますが、実際に何か物理的なモノが配送されてくることはありません。あくまで購入したのは「契約」です。
ピンチ操作による拡大縮小
例えば写真や3Dモデルなどのビューワーアプリ、あるいは地図アプリなどでは、ピンチ操作(2本の指を画面に振れさせ、摘むように指同士を近づけたり、あるいは遠ざけたりする操作)によって対象を拡大縮小する機能がよく備えられています。そこで、前の記事で作成した(クォータニオンを用いて)ドラッグ操作でモデルを回転させることが出来るアプリに、更にピンチ操作によりモデルの拡大縮小を行う機能を追加してみます。
Pinch Gesture Recognizer の利用
ピンチ操作を検出するための方法はいくつかあるようですが、XCode(この記事執筆時点のバージョンは4.6)のStoryboardのObject Libraryには、「Pinch Gesture Recognizer」というそのものズバリなオブジェクトが用意されていますので、これを用いる方法についてメモします(以下のメモは、前の記事で作成したアプリに付け足していく形とします。また、この方法はあくまで自己流であり、他にも方法があるかもしれません)。
- 最初に ViewController.h に、以下のようにデリゲート(UIGestureRecognizerDelegate)を追加します。
//ViewController.h #import <UIKit/UIKit.h> #import <GLKit/GLKit.h> @interface ViewController : GLKViewController<UIGestureRecognizerDelegate> @end
- 続いて、Storyboard 上で Object Library から Pinch Gesture Recognizer を探し、View の上にドラッグ&ドロップします。
- Pinch Gesture Recognizer のアイコンが追加されますので、このアイコンから ViewController のアイコンまで Control キーを押しながら(あるいは、右クリックをしながら)ドラッグ&ドロップします。ドラッグ時に青線が表示されるはずです。ドロップすると下図のようなポップアップが表示されますので、「Outlets」の下にある「delegate」を左クリックします。これで ViewController がピンチ操作を検知するようになります。
- あとは、ピンチ操作検出時のアクションを設定します。まず下図のように ViewController.h を表示し、Pinch Gesture Recognizer のアイコンから ViewController.h の適当な行まで、Control キーを押しながら(あるいは右クリックをしながら)ドラッグ&ドロップします。
- すると下図のようなダイアログが表示されますので、「Connection」の種類を「Action」にし、適当な名前(ここでは pinchGestureRecognized としました)を付けて、「Connect」ボタンをクリックします。
- これで、ViewController.m の中に、以下のようにピンチ操作を検知するたびに呼び出されるアクションメソッドが自動的に追加されます。
//ViewController.m の末尾に自動的に追加 - (IBAction)pinchGestureRecongized:(id)sender { }
- あとはアクションメソッドの中に、以下のようにモデルの拡大縮小を行うための処理を記述します。
- (IBAction)pinchGestureRecongized:(id)sender { GLfloat factor = [(UIPinchGestureRecognizer *)sender scale]; eyeDist += eyeDist * (1.0f - factor) * 0.05f; if(eyeDist > -5.0f) eyeDist = -5.0f; if(eyeDist < -100.0f) eyeDist = -100.0f; }
ここでは単純に、視点からモデル中心までの距離を表す変数(eyeDist 変数)の値を操作する方法をとります。以下のように scale プロパティを介して、ピンチ操作の倍率(最初に指を置いた位置に対して、指同士がどれだけ近づいたか(または離れたか))を取得することが出来ます。
GLfloat factor = [(UIPinchGestureRecognizer *)sender scale];
そして倍率(eyeDist は負の値ですので、(1.0f - factor) という計算を噛ませてあります)、および現在の eyeDist の値に比例させた補正値をeyeDist に加算します。補正値を現在の eyeDist の値に比例させるのがポイントであり、これによって常に一定の速度で拡大縮小が行われるように見せることが出来ます(これについては、もっとスマートな方法があるかもしれませんが…)。
実行してピンチ操作をしてみてください(iPhone シミュレータ上であれば、option キーを押しながらシミュレータの画面上をドラッグするとピンチ操作が入力出来ます。2つの白い円が、2本の指先の位置を表します)。ピンチインによってモデルが縮小され、ピンチアウトによってモデルが拡大されます。
クォータニオンによる視点回転操作
iPhone上で3Dグラフィックスを扱うのであれば、やはり画面タッチ操作により3Dモデルを自由に回転させたりしてみたいもの。画面がタッチされたことや、ドラッグの移動距離の検出自体は touchBegin/touchMoved/touchEnded メソッドを用いて簡単に実現出来ますが、それをモデルの回転とどう結びつけたら良いでしょうか。
単純なモデル回転方法と問題点
単純な発想としては、例えば垂直方向、水平方向へのドラッグ距離に応じた角度で、それぞれx軸、y軸回りにモデルを回転させるような方法があります。GLKitを用いるのであれば、以下のように GLKMatrix4Rotate() 関数などを用いれば実装は容易でしょう。
//angleX、angleYは角度を表す変数であり、touchMoved メソッドなどの中で //タッチ移動距離に応じた角度を計算して代入してあるとする。 baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, angleX, 1.0f, 0.0f, 0.0f); baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, angleY, 0.0f, 1.0f, 0.0f);
こんな方法でも一応は目的を実現出来ます。しかし、この方法だとモデルの向き次第では、例えば画面水平方向にタッチ移動したのにモデルが上下に回転したり、ドリルのようにz軸中心に回転したり、といったことが起こってしまい、あまり直感的なモデル回転が出来ません。
クォータニオンを用いたモデル回転
そこで「クォータニオン」という数学的概念を用いると、もっと直感的なモデル回転を実現することが可能です。クォータニオンは高校数学で習う複素数をさらに拡張したような概念です。複素数はその乗算(掛け算)によって平面における回転操作を表現することが出来ますが、クォータニオンでは3次元空間における任意の軸回りの回転操作を表現することが出来ます。
…とは言いながら、どうしてクォータニオンの演算により回転操作が実現出来るのかについての数式的な理解などはまだ出来ていません(爆)。しかし、ひとまず道具としてクォータニオンを使ってみることは可能です。ここでは床井研究室様*1のサンプルソースを参考に、GLKitを用いてドラッグ操作により3Dモデルを回転する簡単なプログラムを作成してみました。
XCode(この記事執筆時点のバージョンは4.6)で「OpenGL Game」テンプレートを選択してプロジェクトを作成し、ViewController.m の中身を以下のソースコードに書き換えれば動作します(はずです)。
#import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) #define SCALE (2.0f * M_PI) GLfloat gCubeVertexData[216] = { 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f }; @interface ViewController () { GLfloat _rotation; GLuint _vertexArray; GLuint _vertexBuffer; GLKMatrix4 modelViewMatrix1; GLKMatrix4 modelViewMatrix2; CGPoint _touchBegan; CGPoint _touchFinished; GLfloat eyeDist; //視点操作用のクオータニオンおよび行列 GLKQuaternion cquat; GLKQuaternion tquat; GLKMatrix4 rotMat; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; //クオータニオンおよび行列の初期化 cquat = GLKQuaternionIdentity; tquat = GLKQuaternionIdentity; rotMat = GLKMatrix4MakeWithQuaternion(cquat); eyeDist = -10.0f; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ _touchBegan = [[touches anyObject] locationInView:self.view]; } -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ //ドラッグの移動距離から、回転のクォータニオンを求め、更に行列に変換する。 _touchFinished = [[touches anyObject] locationInView:self.view]; GLfloat dx = (_touchFinished.x - _touchBegan.x) / self.view.bounds.size.width; GLfloat dy = (_touchFinished.y - _touchBegan.y) / self.view.bounds.size.height; GLfloat a = sqrtf(dx * dx + dy * dy); if(a != 0.0){ GLfloat ar = a * SCALE * 0.5; GLfloat as = sinf(ar) / a; GLKQuaternion dquat = GLKQuaternionMake(dy * as, dx * as, 0.0f, cosf(ar)); tquat = GLKQuaternionMultiply(dquat, cquat); rotMat = GLKMatrix4MakeWithQuaternion(tquat); } } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ cquat = tquat; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); glEnable(GL_DEPTH_TEST); glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData), gCubeVertexData, GL_STATIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribNormal); glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, sizeof(float)*6, BUFFER_OFFSET(12)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; } - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 300.0f); self.effect.transform.projectionMatrix = projectionMatrix; GLKMatrix4 baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, eyeDist); //回転のクォータニオンから変換された行列を、モデルビュー行列に乗じる baseModelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, rotMat); GLKMatrix4 modelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix); modelViewMatrix1 = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix1 = GLKMatrix4Rotate(modelViewMatrix1, _rotation, -1.0f, -1.0f, 1.0f); modelViewMatrix1 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix1); modelViewMatrix2 = GLKMatrix4MakeTranslation(0.0f, 0.0f, 1.5f); modelViewMatrix2 = GLKMatrix4Rotate(modelViewMatrix2, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix2 = GLKMatrix4Scale(modelViewMatrix2, 2.0f, 2.0f, 2.0f); modelViewMatrix2 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix2); _rotation += self.timeSinceLastUpdate * 0.5f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.65f, 0.65f, 0.65f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArrayOES(_vertexArray); self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); self.effect.transform.modelviewMatrix = modelViewMatrix1; [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); self.effect.light0.diffuseColor = GLKVector4Make(0.4f, 0.4f, 1.0f, 1.0f); self.effect.transform.modelviewMatrix = modelViewMatrix2; [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); } @end
クォータニオン関連の操作については、ほとんどサンプルソースをGLKit向けに移植したようなモノとなっておりますが、GLKitにはあらかじめクォータニオンを扱うための構造体(GLKQuarternion)や関数群が用意されておりますので、それを活用しています。そしてtouchBegin/touchMoved/touchEnded メソッドを用い、この中でクォータニオン関連の操作を行っています。
実行すると例によって、下図のように2つの立方体が表示されますが、ドラッグ操作によってモデル全体(別の言い方をすれば視点)をグリグリ回転させることが可能です。常にドラッグした方向通りに、直感的に回転をしてくれます。
追記(2013/09/08)
ソースコードに一部誤りがあったので訂正しました。viewDidLoad の中のクオータニオンおよび行列を初期化する部分で、tquat を単位クオータニオンで初期化する1行が抜けていました。
//クオータニオンおよび行列の初期化 cquat = GLKQuaternionIdentity; tquat = GLKQuaternionIdentity; //この1行が抜けていた rotMat = GLKMatrix4MakeWithQuaternion(cquat);
この行が抜けていると、回転の位置によってはモデルが一切表示されなくなる不具合が生じてしまうようです。これまで本ソースを参考にされた方には失礼致しました。
「OpenGL Game」テンプレートを使わずGLKitを使う方法
XCode(この記事執筆時点のバージョンは4.6)で「OpenGL Game」テンプレートを使わずに、例えば「Tabbed Application」など通常のテンプレートを用いたアプリ開発でも、GLKitでOpenGL ES 2.0を使えるようにする手順についてメモします。ここでは例として「Tabbed Application」を用いていますが、それ以外のテンプレートでも同様の手順で行けるはずです。
- まずは普通に、「Tabbed Application」テンプレートを選択してプロジェクトを作成します。
- プロジェクトに「GLKit.framework」「OpenGLES.framework」という2つのフレームワークを追加します。
- Storyboardエディタを開いて、既存の ViewController に貼付けられた UIView を削除します(今回の場合はTabbed Applicationなので、どちらか片方のみ(ここではFirstViewControllerの側)を削除してみます)
- 右下のObject Libraryから「GLKit View」を選んで、先程削除した UIView の代わりに貼付けます。
- GLKit View を貼付けた ViewController のヘッダファイル(ここでは FirstViewController.h )を開いて、以下のような書き換えを行います。GLKit/GLkit.h ヘッダファイルのインポートを追加して、UIViewControllerクラスではなく、GLKViewControllerクラスを継承するように書き換えます。
//FirstViewController.h #import <UIKit/UIKit.h> #import <GLKit/GLKit.h> //追加 @interface FirstViewController : GLKViewController //GLKViewControllerクラスを継承するように書き換え @end
- 後は、GLKit View を貼付けた ViewController のソースファイル(ここでは FirstViewController.m)に、GLKitを用いたコードを記述していくだけです。ここでは前の記事で記述した、GLKitのみで2つの回転する立方体を描画するコードをそのまま(ただし、ViewController の名前は FirstViewController に修正して)用いて実行してみます。以下のように、片方のタブでは OpenGL ES 2.0 によるグラフィックを表示、もう片方のタブでは通常のUIViewによる表示、といったアプリが作成出来ます。
「OpenGL Game」テンプレートを簡略化
前の記事にも書いたように、XCode 4.6(この記事の執筆時点)に用意されている「OpenGL Game」テンプレートでは、シェーダプログラムを用いるパターンと、GLKitを用いるパターンの両方で、OpenGL ES 2.0により立方体を描画するアプリを自動生成してくれます。
しかしこのテンプレートを足掛かりに、シェーダを用いずにGLKitのみを用いて自作アプリを開発しようとするなら、シェーダに関するコードは不要となります。そこで、こちらのサイト(外部)を参考にして、テンプレートの ViewController.m からシェーダに関するコードを削除してみました。以下に削除後のコードを掲載しておきます。しかし、ただ削除するだけでは面白くないので、少しだけ手を加えてあります。
#import "ViewController.h" #define BUFFER_OFFSET(i) ((char *)NULL + (i)) GLfloat gCubeVertexData[216] = { // Data layout for each line below is: // positionX, positionY, positionZ, normalX, normalY, normalZ, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f }; @interface ViewController () { GLKMatrix4 modelViewMatrix1; GLKMatrix4 modelViewMatrix2; float _rotation; GLuint _vertexArray; GLuint _vertexBuffer; } @property (strong, nonatomic) EAGLContext *context; @property (strong, nonatomic) GLKBaseEffect *effect; - (void)setupGL; - (void)tearDownGL; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; if (!self.context) { NSLog(@"Failed to create ES context"); } GLKView *view = (GLKView *)self.view; view.context = self.context; view.drawableDepthFormat = GLKViewDrawableDepthFormat24; [self setupGL]; } - (void)dealloc { [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; if ([self isViewLoaded] && ([[self view] window] == nil)) { self.view = nil; [self tearDownGL]; if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } self.context = nil; } // Dispose of any resources that can be recreated. } - (void)setupGL { [EAGLContext setCurrentContext:self.context]; self.effect = [[GLKBaseEffect alloc] init]; self.effect.light0.enabled = GL_TRUE; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); glEnable(GL_DEPTH_TEST); glGenVertexArraysOES(1, &_vertexArray); glBindVertexArrayOES(_vertexArray); glGenBuffers(1, &_vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData), gCubeVertexData, GL_STATIC_DRAW); glEnableVertexAttribArray(GLKVertexAttribPosition); glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0)); glEnableVertexAttribArray(GLKVertexAttribNormal); glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12)); glBindVertexArrayOES(0); } - (void)tearDownGL { [EAGLContext setCurrentContext:self.context]; glDeleteBuffers(1, &_vertexBuffer); glDeleteVertexArraysOES(1, &_vertexArray); self.effect = nil; } #pragma mark - GLKView and GLKViewController delegate methods - (void)update { float aspect = fabsf(self.view.bounds.size.width / self.view.bounds.size.height); GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 100.0f); self.effect.transform.projectionMatrix = projectionMatrix; GLKMatrix4 baseModelViewMatrix = GLKMatrix4MakeTranslation(0.0f, 0.0f, -8.0f); baseModelViewMatrix = GLKMatrix4Rotate(baseModelViewMatrix, _rotation, 0.0f, 1.0f, 0.0f); modelViewMatrix1 = GLKMatrix4MakeTranslation(0.0f, 0.0f, -1.5f); modelViewMatrix1 = GLKMatrix4Rotate(modelViewMatrix1, _rotation, 1.0f, 1.0f, 1.0f); modelViewMatrix1 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix1); modelViewMatrix2 = GLKMatrix4MakeTranslation(0.0f, 0.0f, 1.5f); modelViewMatrix2 = GLKMatrix4Rotate(modelViewMatrix2, _rotation, -1.0f, 1.0f, 1.0f); modelViewMatrix2 = GLKMatrix4Scale(modelViewMatrix2, 1.5f, 1.5f, 1.5f); modelViewMatrix2 = GLKMatrix4Multiply(baseModelViewMatrix, modelViewMatrix2); _rotation += self.timeSinceLastUpdate * 0.5f; } - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.65f, 0.65f, 0.65f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArrayOES(_vertexArray); self.effect.transform.modelviewMatrix = modelViewMatrix1; self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 0.4f, 0.4f, 1.0f); [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); self.effect.transform.modelviewMatrix = modelViewMatrix2; self.effect.light0.diffuseColor = GLKVector4Make(0.4f, 0.4f, 1.0f, 1.0f); [self.effect prepareToDraw]; glDrawArrays(GL_TRIANGLES, 0, 36); } @end
シェーダを用いて立方体を描画するコードを削除する代わりに、GLKitを用いて2つの立方体を描画するように手を加えました。インスタンス変数として2つのモデルビュー行列(GLKMatrix4 型の modelViewMatrix1, 2)を用意しておき、それぞれに異なる操作(移動、回転、スケーリング)を施しておきます。そして glkView メソッドの中で、それぞれの立方体を描画する直前に、エフェクトに対してモデルビュー行列(ついでに diffuseColorも)を設定し直しています。
これを実行すると、下図のように色と大きさの異なる2つの立方体が、くるくる回転します。
OpenGL ES 2.0 と GLKit
OpenGL ES 2.0の高い壁
iPhoneでも、OpenGLを用いて3Dグラフィックス全開なアプリが作成出来ると聞いて、前々からのOpenGL使いである私は早速学習に乗り出しました。しかし、iPhoneのようなスマホ向けに用いられているのは「OpenGL ES」という、本家とは異なるサブセットであることを知ったのです。
最初は「サブセットとは言っても、基本的な文法はきっとそんなに変わらないよね」とタカを括りながら学習を始めました。しかし、特に現在メインとなっているOpenGL ES 2.0では固定機能シェーダが廃止されており、それに伴って glBegin/glEnd とか、glPushMatrix/glPopMatrix とか、glOrtho とか glLight とか glMaterial とか、とにかく本家で慣れ親しんだ関数たちがごっそり消滅していました。その代わりにOpenGL ES 2.0 ではプログラマブルシェーダ(頂点シェーダ、フラグメントシェーダ)のみが実装されており、自前でシェーダプログラムを書くことによって、従来の固定機能シェーダよりも遥かに自由度の高いグラフィックス処理が可能であるようです。
しかし自由度が高くなった分、習得するためのハードルも高くなってしまったようです。素のOpenGL ES 2.0では、ちょっとした図形を描くだけでも、シェーダで視点、照明、座標変換といった様々な行列計算などを自前で実装しないといけずに、シェーダの文法などに関する知識は当然として、幾何学とか行列計算についても相応に高度な知識が必要とされる雰囲気でした。あらかじめ用意された関数やライブラリに頼るプログラミングにドップリ浸っていた自分には、非常に敷居が高く感じてしまいました。
便利なGLKitフレームワーク
そこで、Apple社が私のような存在を見かねてか(笑)、iOS 5.0のリリースと同時に「GLKit」というフレームワークを提供し始めたそうです。これは一言で言うと、シェーダを書くこと無しに、割と従来のOpenGLと近い感覚で、OpenGL ES 2.0によるプログラミングを可能にしてくれるフレームワークです。GLKitには、主に以下のような機能が備わっているそうです。
- 行列やベクトルなどの数学的計算機能や、更には従来の glOrtho や glFrustrum や gluLookAt といった視点変換行列の生成関数と同等の機能を持つ関数を提供するGLKMath
- 自力で画像ファイルをピクセルデータの配列に変換しなくても、画像ファイルのパスを指定するだけで、自動的にテクスチャを生成してくれるGLKTextureLoader
- UIViewクラスから派生しており、各種バッファの処理や画面のスナップショット機能なども備えたGLKView、GLKViewのビューコントローラーであり、アニメーションのFPS設定や一時停止、再開などの機能も備えたGLKViewController
- 自前でシェーダを書くこと無しに、視点変換行列、照明、マテリアル、テクスチャなどの設定や利用を、従来のOpenGLと近い感覚で行うことが可能になるGLKBaseEffect
この記事を執筆時点で私が用いている XCode 4.6 では、「OpenGL Game」というiOS Application向けのテンプレートが用意されており、シェーダプログラムを用いるパターンと、GLKitを用いるパターンの両方で、OpenGL ES 2.0により立方体を描画するアプリを自動生成してくれます。これを足掛かりとして、実際にGLKitを使って色々プログラムを書いてみましたが、実際に非常に便利なフレームワークです。シェーダプログラムを一切書かなくても、結構な範囲のグラフィックス処理が実現出来そうです(現在公開している「iPendulumWaves」というアプリは、GLKitだけで作成しました)。
まだまだGLKitについての情報はネット上にも書籍などにも少ないようですので、自分なりにGLKitを学習して理解したことがあったら、このブログにメモとして公開して行きたいと思います。もちろんGLKitだけで何もかも出来るというわけではなさそうですので、シェーダについても平行して学習して行きたいと思います。
プロパティ宣言の様々な書き方について
Objective-Cにおけるプロパティ宣言についてですが、XCode のバージョンアップなどに伴って様々な書き方が出来るようになっているようですので、簡単にまとめてメモしてみます。
最も基本的(であると思われる)な方法
まずは、一番基本的であると思われる方法でプロパティを用いたプログラムを書いてみます。
#import <Foundation/Foundation.h> @interface myClass : NSObject{ int num; } @property int number; @end @implementation myClass @synthesize number = num; -(void)showNumber{ NSLog(@"num = %i\n", num); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
最初にクラスのインタフェース部で、インスタンス変数(ここでは num)と、それにアクセスするためのプロパティ(ここでは number)を宣言します。そして実装部で、@synthesize 指示子を用いて以下のように、インスタンス変数とプロパティを対応させます。
@synthesize number = num;
これで以下のようにドット演算子を用いて、number プロパティを介して変数 num に外からアクセスすることが可能となります(ただし、変数 mc を id 型にしてしまうとドット演算子が使えないことに注意。型としてクラスを明示的に指定する必要があります)。
mc.number = 1;
変数名とプロパティ名が同一の場合
変数名とプロパティ名を同一にすれば、@synthesize 指定子を用いるときに、イコール演算子(=)を用いる必要が無くなります。
#import <Foundation/Foundation.h> @interface myClass : NSObject{ int number; } @property int number; @end @implementation myClass //イコール演算子(=)を用いる必要はなし! @synthesize number; -(void)showNumber{ NSLog(@"num = %i\n", number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
インスタンス変数の定義の省略
実は、プロパティだけを宣言して @synthesize 指定子を用いると、宣言したプロパティと同名、同型のインスタンス変数が裏側で自動的に定義されるようです。すなわち、インスタンス変数の定義を省略することが出来ます。
このインスタンス変数には、もちろんプロパティを介してアクセスすることが可能です。
#import <Foundation/Foundation.h> @interface myClass : NSObject //インスタンス変数は定義しない。プロパティのみ宣言! @property int number; @end @implementation myClass @synthesize number; -(void)showNumber{ //int 型のインスタンス変数 number が、自動的に生成されている。 NSLog(@"num = %i\n", number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }
@synthesize 指定子の省略
更にXCode 4.4からは、インスタンス変数の定義に加えて、@synthezie 指定子すら省略出来るようです。このときは、宣言したプロパティと同型で、プロパティ名の頭にアンダーバー(_)が付けられた名前のインスタンス変数が自動的に定義されるようです。
もちろん、このアンダーバーが付いたインスタンス変数にも、プロパティを介してアクセスすることが可能です。@synthesize 指定子を省略した(かつ、@dynamic 指定子も用いていない)場合には、@synthesize 指定子が裏側で自動的に補完されるようです。
#import <Foundation/Foundation.h> @interface myClass : NSObject //インスタンス変数は定義しない。プロパティのみ宣言! @property int number; @end @implementation myClass //@synthesize 指定子も省略! -(void)showNumber{ //int 型のインスタンス変数 _number が、自動的に生成されている。 //アンダーバー(_)が名前の頭に付くことに注意! NSLog(@"num = %i\n", _number); } @end int main(int argc, const char * argv[]) { myClass *mc = [[myClass alloc] init]; mc.number = 1; [mc showNumber]; return 0; }