ここでは openFrameworks による2次元グラフィクスについて解説します.グラフィクスの初歩は「入門編」で扱いましたので,より高度なグラフィクス操作について説明していきます.
デフォルトの座標系は,下図(左)に示すように,iPhone を縦に置いた状態で,画面に左上を原点 (0, 0) とし,右下を (319, 479) とする直交座標系です.横(X 軸)方向の幅が 320,縦(Y 軸)方向の高さが 480 となります.なお,これらは論理的な(プログラミング上の)な大きさで,実際のピクセル数は(iPhone の種類に応じて)320×480 ピクセルあるいは 640×960 ピクセルとなります.
このデフォルト座標系が使いづらい場合は,たとえば OpenGL や数学で使う標準的な座標系に変更することができます.たとえば下にあげたプログラムのように,testApp::draw() の中で,まず ofTranslate(160, 239, 0) によって,元の座標系を X 軸方向に 160,Y 軸方向に 239 だけ平行移動させると,上図(中)の座標系が得られます.つぎに ofScale(1, −1, 1) によって,Y 軸の向きを反転させることで,上図(右)のような標準的な座標系(いわゆる右手系)が得られます.これ以降描画される図形は,この新しい座標系の中に配置されるようになります.
#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(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次元のグラフィクスを扱っていましたが,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) として実行してみてください.描画結果をよく見ると,透視投影されているのがわかるはずです.
画面の中心以外で回転させたい場合は,座標系を平行移動してから回転してください.たとえば,平行移動 ofTranslate(0, 80, 0) をしてから回転 ofRotateZ(60) をした場合,左図のような結果が得られます.
逆に,回転 ofRotateZ(60) をしてから平行移動 ofTranslate(0, 80, 0) をした場合,右図のような結果が得られます.この違いをしっかりと理解しておいてください.
座標系の操作については「音・映像メディア / Pure Data 画像生成」に詳しい解説があります.こちらも併せて参照してください.
少しだけ複雑な2次元グラフィクスのアプリケーションをつくります.ofRect() や ofCircle() などで人形のカタチをつくり,それを動かします.
座標系を平行移動あるいは回転させ,その位置に ofRect() や ofCircle() を配置します.ここでは ofSetRectMode() によって,ofRect() の動作を,中心座標と幅・高さを指定して長方形を描画するモード (OF_RECTMODE_CENTER) にしています.デフォルトでは,対角をなす2つの頂点の座標を与えるモード (OF_RECTMODE_CORNER) となっています.
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() で囲むことで,閉じたポリゴン(多角形)を生成します.デフォルトでは,こうして生成されたポリゴンも前景色で塗りつぶされます.
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
(... 以下変更なし ...)
人形の手足を動かして踊らせてみます.駆動力となるのは正弦関数 sin() です.引数として与える rad を少しずつ増加させ,正弦波を作り出します.この正弦波を利用して,左足(股関節・膝関節)の角度 degLLeg,右足(股関節・膝関節)の角度 degRLeg,そして左腕(肩関節・肘関節)の角度 degLArm,右腕(肩関節・肘関節)の角度 degRArm をある範囲で振動させます.また,身体全体の垂直位置を表わす movBody も,この正弦波を利用して振動させます.
class testApp : public ofxiPhoneApp {
private:
float rad;
float degLLeg, degRLeg, degLArm, degRArm;
float movBody;
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;
}
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回分のタップ時刻 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 を更新しています.