小嶋秀樹 | 研究室
日本語 | English
iPhone アプリ開発道場(実践編)

ここでは openFrameworks による2次元グラフィクスについて解説します.グラフィクスの初歩は「入門編」で扱いましたので,より高度なグラフィクス操作について説明していきます.

【準備1:座標系について】

デフォルトの座標系は,下図(左)に示すように,iPhone を縦に置いた状態で,画面に左上を原点 (0, 0) とし,右下を (319, 479) とする直交座標系です.横(X 軸)方向の幅が 320,縦(Y 軸)方向の高さが 480 となります.なお,これらは論理的な(プログラミング上の)な大きさで,実際のピクセル数は(iPhone の種類に応じて)320×480 ピクセルあるいは 640×960 ピクセルとなります.

iPhone の座標系 iPhone の座標系 iPhone の座標系

このデフォルト座標系が使いづらい場合は,たとえば OpenGL や数学で使う標準的な座標系に変更することができます.たとえば下にあげたプログラムのように,testApp::draw() の中で,まず ofTranslate(160, 239, 0) によって,元の座標系を X 軸方向に 160,Y 軸方向に 239 だけ平行移動させると,上図(中)の座標系が得られます.つぎに ofScale(1, −1, 1) によって,Y 軸の向きを反転させることで,上図(右)のような標準的な座標系(いわゆる右手系)が得られます.これ以降描画される図形は,この新しい座標系の中に配置されるようになります.

testApp.mm
#include "testApp.h"

void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
}
 
void testApp::update() {
}

void testApp::draw() {
    //  setup standard coordinate system
    ofTranslate(160, 239, 0);
    ofScale(1, -1, 1);
    //  snowman hip, body, head
    ofSetColor(255,   0,   0);  ofCircle(0, -48, 48);
    ofSetColor(255,  80,  80);  ofCircle(0,   0, 48);
    ofSetColor(255, 120, 120);  ofCircle(0,  48, 48);
}

(... 以下変更なし ...)
ofScale の例

発展課題: 上の例では,ofScale(1, −1, 1) とすることで,Y 軸の方向を反転させていましたが,本来 ofScale(sx, sy, sz) は,X 軸を sx 倍に,Y 軸を sy 倍に,Z 軸を sz 倍にすることで,座標系の拡大縮小(いわゆるスケール変換)を行なうものです.ofScale(2, 1, 1) とすると,X 軸方向(横方向)に2倍に引き伸ばされ,真円を描いても画面上には横長の楕円として表示されます.ofScale(1, 1, 1) は元の座標系に何の変化も与えません.いろいろな倍率を与えて,どのように描画されるかを確かめてください.(右図は ofScale(2, −1, 1) を適用した場合です.)

【準備2:座標系の Z 軸について】
iPhone の座標系
iPhone の座標系

いままで2次元のグラフィクスを扱っていましたが,ofTranslate() や ofScale() には座標系の Z 軸への操作が入っていました.この Z 軸とは,座標系を画面中央を原点とする右手系とした場合,左図のように,画面に垂直な軸のことです.手前側が正,奥側が負となります.

2次元の画面上での座標軸を平行移動やスケール変換するのであれば,ofTranslate(dx, dy, 0) や ofScale(sx, sy, 1) のように,XY 成分のみを操作すればよいでしょう.一方,画面上で座標軸を回転させる場合は,その回転軸は画面に垂直な Z 軸となります.現在の座標系を原点まわりに 60deg 回転させるには,下にあげた部分プログラムのように,ofRotateZ(60) を実行すればよいでしょう.

なお回転の方向は,軸の方向に「右ネジ」を進めたときの回転方向が正となります.Z 軸を回転させれば,XY 平面は原点を中心に反時計回りに回転します.

void testApp::draw() {
    //  setup standard coordinate system
    ofTranslate(160, 239, 0);
    ofScale(1, -1, 1);
    //  turn 60deg
    ofRotateZ(60);
    //  snowman hip, body, head
    ofSetColor(255,   0,   0);  ofCircle(0, -48, 48);
    ofSetColor(255,  80,  80);  ofCircle(0,   0, 48);
    ofSetColor(255, 120, 120);  ofCircle(0,  48, 48);
}

ちなみに X 軸や Y 軸回りの回転も可能です.後述するように,透視投影された3次元グラフィクスとして描画されます.たとえば,上の部分プログラムの ofRotateZ(60) を ofRotateX(60) あるいは ofRotateY(60) として実行してみてください.描画結果をよく見ると,透視投影されているのがわかるはずです.

iPhone の座標系 iPhone の座標系

画面の中心以外で回転させたい場合は,座標系を平行移動してから回転してください.たとえば,平行移動 ofTranslate(0, 80, 0) をしてから回転 ofRotateZ(60) をした場合,左図のような結果が得られます.

iPhone の座標系 iPhone の座標系

逆に,回転 ofRotateZ(60) をしてから平行移動 ofTranslate(0, 80, 0) をした場合,右図のような結果が得られます.この違いをしっかりと理解しておいてください.

座標系の操作については「音・映像メディア / Pure Data 画像生成」に詳しい解説があります.こちらも併せて参照してください.

実践:2次元グラフィクス

少しだけ複雑な2次元グラフィクスのアプリケーションをつくります.ofRect() や ofCircle() などで人形のカタチをつくり,それを動かします.

【第1段階:人形を表示する】

座標系を平行移動あるいは回転させ,その位置に ofRect() や ofCircle() を配置します.ここでは ofSetRectMode() によって,ofRect() の動作を,中心座標と幅・高さを指定して長方形を描画するモード (OF_RECTMODE_CENTER) にしています.デフォルトでは,対角をなす2つの頂点の座標を与えるモード (OF_RECTMODE_CORNER) となっています.

iPhone の座標系
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    ofSetCircleResolution(32);
    ofSetRectMode(OF_RECTMODE_CENTER);
}

void testApp::update() {}

void testApp::draw() {
    //  setup standard coordinate system
    ofTranslate(160, 239);
    ofScale(1, -1);
    //  color
    ofSetColor(200, 200, 200);
    //  body
    ofRect(0, 0, 50, 80);
    //  head
    ofPushMatrix();
      ofTranslate(0, 80);
      ofCircle(0, 0, 30);
    ofPopMatrix();
    //  left leg (upper/lower)
    ofPushMatrix();
      ofTranslate(-10, -45);
      ofRotateZ(-30);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
      ofTranslate(0, -35);
      ofRotateZ(60);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  right leg (upper/lower)
    ofPushMatrix();
      ofTranslate(10, -45);
      ofRotateZ(30);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
      ofTranslate(0, -35);
      ofRotateZ(-60);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  left arm (upper/lower)
    ofPushMatrix();
      ofTranslate(-35, 40);
      ofRotateZ(-30);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
      ofTranslate(0, -30);
      ofRotateZ(-60);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
    ofPopMatrix();
    //  right arm (upper/lower)
    ofPushMatrix();
      ofTranslate(35, 40);
      ofRotateZ(30);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
      ofTranslate(0, -30);
      ofRotateZ(60);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
    ofPopMatrix();
}

注意して見てもらいたいのが ofPushMatrix() と ofPopMatrix() です.これらに挟まれた部分(字下げされた部分)での座標系への操作(平行移動・回転・スケール変換)は,その外部に影響を与えません.(上の例では,外部では標準座標系のままとなっています.)

では,ofPushMatrix() と ofPopMatrix() で挟まれた内部では,どのような処理をしているのでしょうか.左足を描画する部分を例として詳しく見てみましょう.

    //  left leg (upper/lower)
    ofPushMatrix();
      ofTranslate(-10, -45);    //  (1)
      ofRotateZ(-30);           //  (2)
      ofTranslate(0, -35);      //  (3)
      ofRect(0, 0, 20, 50);     //  (4)
      ofTranslate(0, -35);      //  (5)
      ofRotateZ(60);            //  (6)
      ofTranslate(0, -35);      //  (7)
      ofRect(0, 0, 20, 50);     //  (8)
    ofPopMatrix();

まず,この外側の座標系(標準座標系)を,左足の付け根(胴体の左下)に (1) 平行移動させます.さらに −30deg だけ座標系を (2) 回転させます.これは股関節の回転にあたります.得られた座標系の中で,下方(Y 軸の負方向)に (3) 平行移動し,この位置を中心としてフトモモとなる (4) 長方形を配置します.さらに下方に (5) 平行移動し,得られた座標系を 60deg だけ (6) 回転させます.これは膝関節の回転にあたります.こうして得られた座標系の中で,下方に (7) 平行移動し,この位置にスネとなる (8) 長方形を配置しています.

発展課題: このままでは人形が宙に浮いているようなので,人形が立つ舞台をつくります.奥行感のある台形の舞台です.残念ながら openFrameworks では台形を直接描画する関数が用意されていません.そこで,ofVertex() を並べて頂点列を与え,それを ofBeginShape() と ofEndShape() で囲むことで,閉じたポリゴン(多角形)を生成します.デフォルトでは,こうして生成されたポリゴンも前景色で塗りつぶされます.

iPhone の座標系
void testApp::draw() {
    //  setup standard coordinate system
    ofTranslate(160, 239);
    ofScale(1, -1);
    //  ground
    ofSetColor(60, 60, 60);
    ofBeginShape();
      ofVertex(-160, -220);
      ofVertex(-120, -120);
      ofVertex( 119, -120);
      ofVertex( 159, -220);
      ofVertex(-160, -220);
    ofEndShape();
    //  color
    ofSetColor(200, 200, 200);
    //  body

    (... 以下変更なし ...)
【第2段階:人形を踊らせる】

人形の手足を動かして踊らせてみます.駆動力となるのは正弦関数 sin() です.引数として与える rad を少しずつ増加させ,正弦波を作り出します.この正弦波を利用して,左足(股関節・膝関節)の角度 degLLeg,右足(股関節・膝関節)の角度 degRLeg,そして左腕(肩関節・肘関節)の角度 degLArm,右腕(肩関節・肘関節)の角度 degRArm をある範囲で振動させます.また,身体全体の垂直位置を表わす movBody も,この正弦波を利用して振動させます.

class testApp : public ofxiPhoneApp {
private:
    float rad;
    float degLLeg, degRLeg, degLArm, degRArm;
    float movBody;
public:
    (... 変更なし ...)
};
iPhone の座標系
iPhone の座標系
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    ofSetCircleResolution(32);
    ofSetRectMode(OF_RECTMODE_CENTER);
    //  parameters
    rad = 0;
    degLLeg = degRLeg = 0;
    degLArm = degRArm = 0;
    movBody = 0;
}

void testApp::update() {
    float s = sin(rad);
    degLLeg = 30 + 30 * s;
    degRLeg = 30 - 30 * s;
    degLArm = 30 - 30 * s;
    degRArm = 30 + 30 * s;
    movBody = 15 * sin(rad * 2 - HALF_PI);
    rad += 0.1;
}

void testApp::draw() {
    //  setup standard coordinate system
    ofTranslate(160, 239);
    ofScale(1, -1);
    //  ground
    ofSetColor(60, 60, 60);
    ofBeginShape();
      ofVertex(-160, -220);
      ofVertex(-120, -120);
      ofVertex( 119, -120);
      ofVertex( 159, -220);
      ofVertex(-160, -220);
    ofEndShape();
    //  color
    ofSetColor(200, 200, 200);
    //  body
    ofTranslate(0, movBody);
    ofRect(0, 0, 50, 80);
    //  head
    ofPushMatrix();
      ofTranslate(0, 80);
      ofCircle(0, 0, 30);
    ofPopMatrix();
    //  left leg (upper/lower)
    ofPushMatrix();
      ofTranslate(-10, -45);
      ofRotateZ(-(30 + degLLeg));
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
      ofTranslate(0, -35);
      ofRotateZ(60 + degLLeg);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  right leg (upper/lower)
    ofPushMatrix();
      ofTranslate(10, -45);
      ofRotateZ(30 + degRLeg);
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
      ofTranslate(0, -35);
      ofRotateZ(-(60 + degRLeg));
      ofTranslate(0, -35);
      ofRect(0, 0, 20, 50);
    ofPopMatrix();
    //  left arm (upper/lower)
    ofPushMatrix();
      ofTranslate(-35, 40);
      ofRotateZ(-(30 + degLArm));
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
      ofTranslate(0, -30);
      ofRotateZ(-(60 + degLArm));
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
    ofPopMatrix();
    //  right arm (upper/lower)
    ofPushMatrix();
      ofTranslate(35, 40);
      ofRotateZ(30 + degRArm);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
      ofTranslate(0, -30);
      ofRotateZ(60 + degRArm);
      ofTranslate(0, -30);
      ofRect(0, 0, 20, 45);
    ofPopMatrix();
}

発展課題: 人形は踊りましたか.周期や位相を変えることで,複雑な踊りをつくることができます.また,ここでは股関節と膝関節には同じ動きを与えていますし,肩関節と肘関節にも同じ動きを与えていますが,これらを独立に制御することで,より複雑な踊りをつくりだすことができます.チャレンジしてみてください.

【第3段階:リズムを与えて踊らせる】

最後にオマケとして,タッチスクリーンをタップすることでリズムを与えて,人形を踊らせるように改造してみましょう.ここでは,最も新しい3回分のタップ時刻 msec1, msec2, msec3 を記録し,それらの間隔から,人形が踊る周期 interval を計算しています.interval の初期値は 1000msec です.

class testApp : public ofxiPhoneApp {
private:
    float rad;
    float degLLeg, degRLeg, degLArm, degRArm;
    float movBody;
    int msec1, msec2, msec3;
    float interval;
public:
    (... 変更なし ...)
};
void testApp::setup() {
    ofRegisterTouchEvents(this);
    ofxAccelerometer.setup();
    ofxiPhoneAlerts.addListener(this);
    ofBackground(0, 0, 0);
    ofSetFrameRate(30);
    ofSetCircleResolution(32);
    ofSetRectMode(OF_RECTMODE_CENTER);
    //  parameters
    rad = 0;
    degLLeg = degRLeg = 0;
    degLArm = degRArm = 0;
    movBody = 0;
    //  rhythm
    msec1 = msec2 = msec3 = 0;
    interval = 1000;
}

void testApp::update() {
    float s = sin(rad);
    degLLeg = 30 + 30 * s;
    degRLeg = 30 - 30 * s;
    degLArm = 30 - 30 * s;
    degRArm = 30 + 30 * s;
    movBody = 15 * sin(rad * 2 - HALF_PI);
    rad += 0.5 * 1000 / interval * TWO_PI / 30;
}

void testApp::touchDown(ofTouchEventArgs &touch){
    //  update time stamps
    msec1 = msec2;
    msec2 = msec3;
    msec3 = ofGetElapsedTimeMillis();
    //  if consistent, update interval
    float inter12 = msec2 - msec1;
    float inter23 = msec3 - msec2;
    float mean = (inter12 + inter23) / 2;
    if (abs(inter12 - inter23) / mean < 0.1) interval = mean;
}

タップされたときに呼び出される testApp::touchDown() は,まず古いタップ時刻の記録を押し出し,現在の時刻(起動時からの経過時間)を ofGetElapsedTimeMillis() によって読み出して msec3 に記録します.時間間隔 inter12, inter23 を計算し,それらの差を平均と比べて 10% 未満であれば,interval を更新しています.