C++
Xcode
openFrameworks
of

openFrameworksでの3D描画について

使用環境

-macOS High Sierra
-プロセッサ intel Core i5 (2GHz)
-メモリ 8GB
-Xcode version9.2
-openFrameworks

下記の表に示したクラスが、今回使用したものです。

クラス名 説明
ofBox ofSphere 3Dプリミティブオブジェクトの描画
ofEasycam 簡単に利用可能なカメラ(視点)機能
ofLight ライティング効果
ofMesh 3D物体の全ての頂点情報を持ったデータ
ofVBO 3Dシーンの物体の情報(位置・色)の保存
ofShader プログラマブルシェーダ

openFrameworksでMeshを使い、大量の頂点を高速に描画する-not good but great
http://naoyashiga.hatenablog.com/entry/2014/04/06/204022

まず、3Dオブジェクトの描画

ofApp.hの部分で、必要なクラスのインスタンス生成をおこないます

ofApp.h
class ofApp : public ofBaseApp{
   /*
   中略
   */
   // インスタンス生成
   ofBoxPrimitive box;
   ofSpherePrimitive sphere;
   ofEasyCam cam;
   // Y軸の回転
   int Yaxis;
};

次に、インスタンス生成したものをofApp.cppの方で用います

ofApp.cpp
void ofApp::draw() {
   // 座標系を移動
   ofTranslate(ofGetWidth()/2, ofGetHeight()/2);
   ofSetColor(255);

   // カメラ開始
   cam.begin();
   ofRotateY(Yaxis++);
   // X軸, Y軸を描画
   ofDrawLine(-ofGetWidth()/2, 0, 0, ofGetWidth()/2, 0, 0);
   ofDrawLine(0, -ofGetHeight()/2, 0, 0, ofGetHeight()/2, 0);
   // 立方体
   box.set(200);
   box.setPosition(0, 130, 0);
   box.draw();
   // 球
   sphere.set(100, 16);
   sphere.setPosition(0, -130, 0);
   sphere.drawWireframe();
   // カメラ終了
   cam.end();
}

スクリーンショット 2017-12-27 3.03.39.png

実行結果はこのようになります。
draw()で塗りつぶし指定された立方体は塗りつぶされています
一方、drawWireframe()の球の方は、構成される線のみ描画されています

立体を、より立体に

次は、ofLightを用いて、ライト効果を与えより立体ぽく見えるように手を加えます
上記のコードに、便宜上

ofSetColor(0, 0, 255);で、描画色を青色に指定します
まず、ofLightクラスのインスタンス生成をofApp.h側でしておきます

ofApp.h
class ofApp : public ofBaseApp{
   /*
   中略
   */
   ofLight light;
};

openFrameworksでofLightを用いてライト効果を与える場合、光の反射具合を3つの要素の値を変更することで調整します

-鏡面反射(鏡と同じ、完全な光の反射。反射の法則に従って、入射角と反射角は反射面に対して同じ角度となる)
-拡散反射(光が物体表面の凹凸により、ランダムな反射を繰り返すもの。物体表面のなめらかさを決定する要素)
-環境反射(物体に映り込むであろう周囲環境を、反射しているように見せるもの)

言葉の説明だけではイメージしずらいので、参考画像を載せておきます 

鏡面反射

鏡面反射-通信用語の基礎知識
http://www.wdic.org/w/SCI/鏡面反射

拡散反射

Wikimedia Commons
https://commons.wikimedia.org/wiki/File:Difracao.png

環境反射

環境マッピング-Wikipedia
https://ja.wikipedia.org/wiki/環境マッピング

では、実際にライトを適用していこうと思います

先ほどインスタンス生成した、lightをofApp.cppの方で用いていきます

ofApp.cpp
void ofApp::setup() {
    ofBackground(0);
    // ライティングを有効に
    light.enable();
    // スポットライトを配置
    light.setSpotlight();
    // 照明の位置
    light.setPosition(-100, 100, 100);
    // 鏡面反射光の色
    light.setSpecularColor(ofFloatColor(1.0, 1.0, 1.0));
    // 拡散反射光の色
    light.setDiffuseColor(ofFloatColor(0.5, 0.5, 1.0));
    // 環境反射光の色
    light.setAmbientColor(ofFloatColor(0.5, 0.2, 0.2, 1.0));
}

このように、setup()の中で、lightに関する情報を初期化してしまう
light.enable();
でライティングを有効にする
light.setSpotlight();
で、lightをSpot Lightの設定にする

lightの種類には以下のようなものがある

スクリーンショット 2017-12-27 22.26.08.png

今回はsetSpotlight()を用いる
次に、スポットライトの位置を設定する
ここではofTranslate()前の場所での実行のため、座標軸は標準である
light.setPosition(-100, 100, 100);
そして、鏡面反射、拡散反射、環境反射の色を設定していく

ofApp.setup()
    // 鏡面反射光の色
    light.setSpecularColor(ofFloatColor(1.0, 1.0, 1.0));
    // 拡散反射光の色
    light.setDiffuseColor(ofFloatColor(0.5, 0.5, 1.0));
    // 環境反射光の色
    light.setAmbientColor(ofFloatColor(0.5, 0.2, 0.2, 1.0));

ofFloatColor(r, g, b, a)
colorをsetする際に用いられているofFloatColor()
イメージでわかっているとは思うのですが、新たな発見があるかもなので、公式サイトで調べてみました

Typedef PixelType Bit Depth Min.value Max.value
ofColor unsigned char 8 0 255
ofShortColor unsigned short 16 0 65535
ofFloatColor float vares 0.0 1.0

ofFloatColorに関していえば、r, g, b, (a)の値をそれぞれ0 ~ 1の値で示すことができるというような感じでしょうか?
このofFloatColorを用いて、3種類のライト効果に色を与えました
これでいざ実行してみます!
スクリーンショット 2017-12-27 22.44.59.png

立方体を見てみるとわかるように、奥の面に光が当たっているように見えます
処理の順序的に、深度を無視して、処理した画像の上から光をさらに描画してしまうため、奥の面に光が当たっているように見えてしまいます。
深度テストを有効にし、さらに、描画をスムーズにすることでこの問題を解決することができます

ofApp.cpp
void ofApp::setup() {
   // 深度テストを有効に
   ofSetDepthTest(true);
   // 頂点の色情報を格納する
   ofEnableSmoothing(); 
}

こちらの二つを実行しておくことで、深度テストを行い、どちらの方が奥にあるのかの判断をした上で、スムーズな描画を行ってくれます。
スクリーンショット 2017-12-28 14.29.53.png
この通り、先ほどのような、奥の面に対して光が当たるようなことは起きていません。

ofMeshと、ofVboの使い方

-ofMesh(3D空間の頂点と、各頂点の法線・頂点色・テクスチャ座標の集まりを表す)
-ofVbo(a Vertex Buffer Object(VBO)は、グラフィックスカード上で一度に全ての頂点情報を格納して、描画する)

まず、ofMeshの使い方から

ofApp.h
class ofApp : public ofBaseApp{
   /*
   中略
   */
   // インスタンス生成
   ofMesh mesh;
   // メッシュの幅と高さ
   int w, h;
}

いつも通り、ofApp.cppの方で使うものを、ofApp.h側であらかじめ宣言しておきます

ofApp.cpp
void ofApp::setup() {
    // 画面の設定
    ofBackground(0);
    ofEnableDepthTest();
    cam.setDistance(100);
    // メッシュの幅と高さの初期化
    w = 100;
    h = 100;
    // メッシュの各頂点の色を初期化
    for (int i = 0; i < w; i++) {
        for (int j = 0; j < h; j++) {
            mesh.addColor(ofFloatColor(0.5, 0.8, 1.0));
        }
    }
}

次に、ofApp.cppのsetup内での処理を見ていきます
// メッシュの各頂点の色を初期化
の部分で、for文の入れ子にして、縦横全ての頂点にofFloatColor()で設定した色をセットしています。
これで、全ての頂点が同じofFloatColor(0.5, 0.8, 1.0)の色を保持したことになります

次に、update()内での処理を書いていきます

ofApp.cpp
void ofApp::update() {
    // まず、現在の全ての頂点情報を消去(vertices->頂点)
    mesh.clearVertices();
    // メッシュの全ての頂点位置を更新、それを頂点情報として追加
    for (int i = 0; i < w; i++) {
        for (int j = 0; j < h; j++) {
            float x = sin(i * 0.1 + ofGetElapsedTimef()) * 10.0;
            float y = sin(j * 0.15 + ofGetElapsedTimef()) * 10.0;
            float z = x + y;
            mesh.addVertex(ofVec3f(i - w/2, j - h/2, z));

        }
    }
}

// まず、現在の全ての頂点情報を消去の部分で、
meshに対して、clearVertices()を実行しています。この、clearVertices()関数は、メッシュ全ての頂点を削除する機能を持ちます
他にも、meshに対して扱えるclear関連の機能をまとめました。

関数名 機能
clear() メッシュの全ての頂点・頂点色・頂点インデックス・テクスチャ座標を削除
clearColors() メッシュの全ての頂点職を削除      
clearIndices() メッシュの全ての頂点インデックスを削除。よって、メッシュは頂点のみの状態になる
clearNormals() メッシュの全ての法線を削除
clearTexCoords() メッシュの全てのテクスチャ座標を削除
clearVertices() メッシュの全ての頂点を削除

今回は、clearVertices()で、全ての頂点を削除しています。
次に、全ての頂点を削除したので、新しい頂点を生成しなければなりません。そこで次のfor文の中でx, y, z座標を計算して、新たな頂点座標を代入しています。
x, y座標にはsin関数を用いています。θの部分の値を*0.1 と *0.15と微妙にずらすことで、x, y座標それぞれ違った動きを実現しています。

ofGetElapsedTimef()は初めて出てきた関数なので調べて見ました。

関数 機能
ofGetElapsedTimef() ofResetElapsedTimeCounter()が呼ばれてからの経過時間をfloatで返します。実行時に自動的に、ofResetElapsedTimeCounter()は一度実行されるので、経過時間を返すことができます

つまり、ofResetElapsedTimeCounter()からの経過時間ですね、

次に、ofVec3fについてです

ofVec3fは三次元ベクトルのデータを保持するクラスです。
サンプルコードを見てみると、
ocVec3f(i - w/2, j - h/2, z)
(i -w/2, j - h/2, z)を要素にもつ三次元ベクトルを示しています。
そして、このベクトルの示す場所を頂点とするために、
mesh.addVertex(ofVec3f(~略~))で、頂点情報を追加しています。
**これが、ofApp::updateの概要です

次に、draw()を実装していきます

cpp.ofApp
ofApp::draw() {
    // メッシュの描画
    ofSetHexColor(0xffffff);
    // カメラの開始
    cam.begin();
    // 頂点の位置をドットで表示
    glPointSize(2.0);
    mesh.drawVertices();
   // カメラ停止
    cam.end();
    // ログの表示
    string info;
    info = "vertex num = " + ofToString(w * h, 0) + "\n";
    info += "FPS = " + ofToString(ofGetFrameRate(), 2) + "\n";
    ofDrawBitmapString(info, 30, 30);
}

まず、一行目のofSetHexColor(0xffffff)で、色を16進数で指定(今回は0xffffff:白色)
meshの描画色には影響しない!
why?なぜ?
meshは頂点情報を保持している。つまり、頂点の色の情報も保持している。

ofApp.cpp
void ofApp::setup() {
for (int i = 0; i < w; i++) {
   for (int j = 0; j < h; j++) {
      mesh.addColor(ofFloatColor(0.5, 0.8, 1.0));
   }
}
}

の部分で、メッシュに対して、頂点の色情報を加えている。よって、このdraw内で描画色を変更しても、メッシュに対しては影響しない
cam.begin()でカメラを開始して、
glPointSize(2.0)で、openGLでのてんの描画サイズを2.0に設定します
そして、mesh.drawVertices();で、メッシュの保持している全ての頂点を描画します。
最後に、cam.end()でカメラ終了を行えば、
スクリーンショット 2017-12-28 22.29.55.png

このような、sinカーブから形作られた形状が描画されます。
ちなみに、ログ表示で用いたofToString()の二つ目の引数は、変換の正確性を指定しています。
フレームレートを表示する箇所で、ofToString(ofGetFrameRate(), 2);というのは、%.2fのような意味合いで、
小数点第2位までの正確性で文字列変換します。

ofVbo(Vertices Buffer Object)の使い方

ofVboを使うことで、先ほど描画したような、大量の頂点情報を高速にCPU側で演算し、描画するようなものを、
あらかじめ頂点情報をひとまとめにして、openGLに渡し、そしてGPU側で一気に処理することでCPUの負荷を軽減し、高速な描画を可能とするものです。

例えば、上記のメッシュを用いて描画したものは,ofVboを用いると、

ofApp.h
class ofApp: public ofBaseApp {
    /*
    中略
    */
    // クラス定数
    static const int WIDTH = 100;
    static const int HEIGHT = 100;
    static const int NUM_PARTICLES = WIDTH * HEIGHT;
    // インスタンス生成
    ofEasyCam cam;
    ofVbo myVbo;
     // 3次元ベクトルで頂点情報を格納する
    ofVec3f myVerts[NUM_PARTICLES];
    // 頂点の色情報を格納する
    ofFloatColor myColor[NUM_PARTICLES];
};

いつも通り、用いるものをofApp.h側で準備し、
今回作成した、クラス定数は、ofApp.cpp側で用いるために、ofApp.cpp側で、宣言をしなければいけません。
よって、

ofApp.cpp
const int ofApp::WIDTH;
const int ofApp::HEIGHT;
const int ofApp::NUM_PARTICLES;

この三文が必要になります。これ、僕が最初にコードを書いたときに見つけるのに苦労したのですが、どうやらクラス定数は、他の変数と違って、ofApp.h側で宣言していても、直接そのまま用いることはできないようです。この三文書いてなかったがために、かなりの時間喰われました…

次に、中身の処理を書いていきます

ofApp.cpp
void ofApp::setup() {
    // 画面の設定
    ofBackground(0);
    ofEnableDepthTest();
    ofEnableBlendMode(OF_BLENDMODE_ADD);
    cam.setDistance(100);
    // 頂点情報を初期化
    for (int i = 0; i < WIDTH; i++) {
        for (int j = 0; j < HEIGHT; j++){
            myVerts[i * WIDTH + j].set(i - WIDTH/2, j - HEIGHT/2, 0);
            myColor[i * WIDTH + j].set(0.5, 0.8, 1.0, 1.0);
        }
    }
    // 頂点バッファに位置と色の情報を設定
    myVbo.setVertexData(myVerts, NUM_PARTICLES, GL_DYNAMIC_DRAW);
    myVbo.setColorData(myColor, NUM_PARTICLES, GL_DYNAMIC_DRAW);
}

画面の設定に関しては、今までの説明で十分です。
頂点情報の初期化も今までと同じことを配列に格納しているだけです。
個人的に、openGLでのモードの設定のところで、GL_DYNAMIC_DRAWというのがなぜ、これなのか気になったので調べて見ました。
結果としては、ここに入るかもしれない選択肢は主に3つあって、
-GL_STATIC_DRAW(内容に変化の少ない要点バッファ)
-GL_DYNAMIC_DRAW(毎回処理毎に内容を書き換えるバッファ)
-GL_STREAM_COPY(パーティクルなどを構成する、変換フィードバックバッファ)

ちなみに、バッファとは、コンピュータでデータを一時的に記憶する場所。

だそうです、

今回は、毎フレーム毎に、頂点の位置情報を変化させるので、GL_DYNAMIC_DRAWということでした、

ofApp.cpp
void ofApp::update() {<img width="1022" alt="スクリーンショット 2017-12-28 23.20.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/185248/57bef5fb-fa2a-8423-28a1-83dfea03487c.png">

    // メッシュの全ての頂点位置を更新、それを頂点情報として追加
    for (int i = 0; i < WIDTH; i++) {
        for (int j = 0; j < HEIGHT; j++) {
            float x = sin(i * 0.1 + ofGetElapsedTimef()) * 10.0;
            float y = sin(j * 0.15 + ofGetElapsedTimef()) * 10.0;
            float z = x + y;
            myVerts[i * WIDTH + j] = ofVec3f(i - WIDTH/2, j - HEIGHT/2, z);
        }
    }
    // 頂点バッファの頂点の座標情報を更新
    myVbo.updateVertexData(myVerts, NUM_PARTICLES);
}

myVertsの配列に、ofVec3fを用いて位置ベクトルで座標を計算し、その位置ベクトルの示す頂点情報をmyVboに更新します。
これで、myVboは新しい頂点情報を持ったVertices Buffer Objectとなり、それをopenGLに渡すことで、GPU側で計算することができます。

ofApp.cpp
void ofApp::draw() {
    // カメラ開始
    cam.begin();
    // 頂点の位置をドットで表示
    glPointSize(2.0);
    myVbo.draw(GL_POINTS, 0, NUM_PARTICLES);
    // カメラ停止
    cam.end();
    // ログの表示
    string info;
    info = "vertex num = " + ofToString(w * h, 0) + "\n";
    info += "FPS = " + ofToString(ofGetFrameRate(), 2) + "\n";
    ofDrawBitmapString(info, 30, 30);
}

実際のdrawの中身も、人間側として行うことは、myVbo(ofVboインスタンス)に対して、draw()関数を用いるのみです。
draw()関数の引数の意味を知るために、Definitionを見てみました。

Definition
void ofVbo::draw(int drawMode, int first, int total) const{
    ofGetGLRenderer()->draw(*this,drawMode,first,total);
}

てなってました.VBOのインスタンス生成されたものが持っている頂点情報のうち、どこからどこまでを描画するかの数を第二、第三引数で受けていたのですね

では、いざ、こちらを実行してみますと、

スクリーンショット 2017-12-28 23.20.49.png

できました!

こんな感じで、ofVboを用いてGPU側で処理を行って描画することも可能です。

まとめ

openFrameworksを触り始めてまだまだですが、今回こちらの記事を参考に、学習したことをまとめさせていただきました。拙いまとめですが、お役に立てれば幸いです。間違いなどご指摘ございましたら、お教えくださいますようお願いいたします。