はじめに
本記事は、私の所属するIAMASの体験拡張プロジェクト内で行う勉強会の発表資料になります。
ここでは、openFrameworksにおける私のオススメの描画方法を紹介します。oFの基本的な解説は yoppa.org の記事が十分すぎますが、本記事は3D図形をいかに効率よく書くかに焦点を絞っています。
まず初学者は、ofDrawBox(), ofDrawLine()などのProcessingライクな関数を使うのがわかりやすいですが、表現としての可能性が限定的かつパフォーマンスとしても改善の余地があります。ここでは一歩進んで、パフォーマンスを意識しながらも動的な3Dの図形を書くのに応用の効く方法を紹介します。
サンプルはどれも正八面体を描画するコードになります。回転、拡大などの変数を行列としてまとめて、その参照を引数として渡し、図形をつくります。プログラムを動かすためには、以下の ofApp.h と ofApp.cpp を作成し、以降で紹介するコードを ofApp.cpp に加筆修正してビルドします。
ofApp.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#pragma once #include "ofMain.h" class ofApp : public ofBaseApp{ public: void setup(); void update(); void draw(); void drawByGL(ofMatrix4x4 * mat); void createOctaByMesh(ofMatrix4x4 * mat); void createOctaByVbo(ofMatrix4x4 * mat); void createOctaByVboMesh(ofMatrix4x4 * mat); ofMatrix4x4 m; ofMesh mesh; ofVbo vbo; ofVboMesh vboMesh; vector<ofVec3f> ov = { ofVec3f(1,0,0), ofVec3f(-1,0,0), ofVec3f(0,1,0), ofVec3f(0,-1,0), ofVec3f(0,0,1), ofVec3f(0,0,-1) }; vector<unsigned> oi = { 0, 2, 4, 0, 4, 3, 0, 3, 5, 0, 5, 2, 1, 2, 5, 1, 5, 3, 1, 3, 4, 1, 4, 2 }; ofEasyCam cam; }; |
ofApp.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include "ofApp.h" void ofApp::setup(){ ofBackground(255); // matrix for affine transforming m.glScale(100, 100, 100); m.glRotate(20, 0, 0, 1); // create } void ofApp::update(){ } void ofApp::draw(){ ofSetColor(0); cam.begin(); // draw cam.end(); } |
4つの方法
1. glBegin – glEnd
OpenGLのAPIであるため、ofBeginShape(), ofEndShape()などoFの描画関数よりもローレベルで高いパフォーマンスが期待できます。そのためか多くの先人のoFコードがこの機能を使っているのを発見できます。Meshつくるほどでも無く、頂点座標から点や線を描画したいときなどに重宝します。glBegin()の引数には、GL_POINTS, GL_TRIANGLES_STRIP などといった頂点をどのように描画するかを取り決めたEnum値の指定が可能です。このあたりの引数の使い分けは、慣れると便利ですし、データのつくりやすさから描画の仕方を考えることできます。
glBegin(), glEnd()の間には、glColor–(), glNormal–(), glVertex–(), glIndex–()といった関数によって、描画に必要な情報を記述します。glVertex–()のみで描画は可能で、その他は任意です。–の部分は、ベクトルの次元をあらわす数字と成分の桁つまりデータ型を意味するアルファベットを表す文字が入ります。3次元のfloatであれば、glVertex3f(), 2次元のdoubleであればglVertex2d()となります。(この接尾句の表現は他でもよくみるかもしれません。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void ofApp::draw(){ ofSetColor(0); cam.begin(); drawByGL(&m); cam.end(); } void ofApp::drawByGL(ofMatrix4x4 * mat){ ofNoFill(); ofVec3f vec; glBegin(GL_TRIANGLES); for (int i = 0; i < oi.size(); i++) { vec = ov[ oi[i] ] * *mat; glVertex3f(vec.x, vec.y, vec.z); } glEnd(); } |
2. ofMesh
頂点の個数だけ、頂点情報、法線情報、頂点カラー情報を持ちます。また、それらをポリゴン(三角形)として解釈するためにインデックス情報を持ちます。このインスタンスは描画する際、頂点情報のみが必須で、あとは任意情報となりますが、例えばインデックス情報があれば頂点の個数を削減できますし、法線情報があればライティングによる陰影づけなどが可能となります。目的に応じて必要なデータを付与してあげます。
mesh.draw()が呼ばれるたびに、頂点配列をもとに、glDrawElements() が内部で呼ばれていますが、mesh.draw()でとれる引数は、OF_MESH_WIREFRAME, OF_MESH_FILL, OF_MESH_POINTSの3種で、先程のglBeginの引数よりも種類が少ないことが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
void ofApp::setup(){ ofBackground(255); m.glScale(100, 100, 100); m.glRotate(20, 0, 0, 1); createOctaByMesh(&m); // createOctaByMeshWithNormal(&m); } void ofApp::draw(){ ofSetColor(0); cam.begin(); mesh.draw(OF_MESH_WIREFRAME); cam.end(); } // 法線なしのMesh void ofApp::createOctaByMesh(ofMatrix4x4 * mat){ vector<ofVec3f> vec; for (int i = 0; i < ov.size(); i++) { vec.push_back(ov[i] * *mat); } mesh.addVertices(vec); mesh.addIndices(oi); } // 面法線があるMesh void ofApp::createOctaByMeshWithNormal(ofMatrix4x4 * mat){ for (int i = 0; i < 8; i++) { ofVec3f v0 = ov[ oi[i*3] ] * *mat; ofVec3f v1 = ov[ oi[i*3+1] ] * *mat; ofVec3f v2 = ov[ oi[i*3+2] ] * *mat; ofVec3f n = (v1 - v0).getCrossed(v2 - v0); mesh.addVertex(v0); mesh.addVertex(v1); mesh.addVertex(v2); mesh.addNormal(n); mesh.addNormal(n); mesh.addNormal(n); } } |
3. ofVbo
Vertex Buffer Object とは頂点情報をGPU側にバッファーとして確保し、毎フレームのバッファリングを省略できるためパフォーマンスの向上を企図できる機能です。openFrameworksではofVboとして利用します。ただし、ofMeshのような便利な関数群がありません。その変わり描画の際、GL_POINTS, GL_TRIANGLES_STRIP などglBegin()と同じEnum値の引数を使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void ofApp::setup(){ ofBackground(255); m.glScale(100, 100, 100); m.glRotate(20, 0, 0, 1); createOctaByVbo(&m); } void ofApp::draw(){ ofSetColor(0); cam.begin(); ofNoFill(); vbo.draw(GL_LINES, 0, oi.size()); cam.end(); } void ofApp::createOctaByVbo(ofMatrix4x4 * mat){ vector<ofVec3f> vecs; for (int i = 0; i < oi.size(); i++) { vecs.push_back(ov[ oi[i] ] * *mat); } vbo.setVertexData(vecs.data(), vecs.size(), GL_STATIC_DRAW); } |
4. ofVboMesh
ofVboMeshは頂点情報のバッファリングをしながらも、ofMeshを継承しており、同じ関数が呼び出せます。個人的には、ofVboMeshの利用をまず第一に考えるのをおすすめします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void ofApp::setup(){ ofBackground(255); m.glScale(100, 100, 100); m.glRotate(20, 0, 0, 1); createOctaByVboMesh(&m); } void ofApp::draw(){ ofSetColor(0); cam.begin(); vboMesh.draw(OF_MESH_WIREFRAME); cam.end(); } void ofApp::createOctaByVboMesh(ofMatrix4x4 * mat){ vector<ofVec3f> vecs; for (int i = 0; i < ov.size(); i++) { vecs.push_back(ov[i] * *mat); // mesh.addVertex(ov[i] * *mat); } vboMesh.addVertices(vecs); vboMesh.addIndices(oi); } |