技術文章Qt

Qt + OpenGL 入門
Nobuhide Tsuda

概要

Qt で OpenGL を使用する方法について述べる

※ 筆者は OpenGL を使い始めたばかりであり、本稿はなんらかの間違いを含んでいるかもしれない。
間違いを見つけた場合は、優しく指摘してくださるとありがたい。

準備

OpenGL は OpenGL モジュールに含まれているので、OpenGL モジュールを有効にしなくてはいけない。
プロジェクト作成時に「OpenGL Library」にチェックを入れる。


既に作成しているプロジェクトに OpenGL を組み込む方法:
 VisualStudio Qt-Addin:Qt>Qt Project Settings を実行し、Qt Module タブで「OpenGL Library」にチェックを入れる。

QGLWidget

OpenGL を使用するには QGLWidget の派生クラスを定義する。

上記ではクラス名を GLWidget にし、親クラスを QGLWidit に、コンストラクタ引数を QWidget * に設定している。
このクラスは OpenGL の機能を持つ QWidget 派生クラスなので、中に3D画像を表示することが簡単に出来る。

宣言ファイルを開き、下記のように、initializeGL(), resizeGL(int, int), paintGL() の3つのメソッドを宣言します。
これらは QGLWidit の protected な仮想関数です。

class GLWidget : public QGLWidget
{
    Q_OBJECT
public:
    GLWidget(QWidget *parent = 0);
    ~GLWidget();
protected:
    void initializeGL();        //  OpenGL 初期化
    void resizeGL(int, int);    //  ウィジットリサイズ時のハンドラ
    void paintGL();             //  描画処理
};

それぞれの仮想関数の実装は以下のようにします。

void GLWidget::initializeGL()
{
    qglClearColor(Qt::lightGray);  // 背景色指定
}

initializeGL() では OpenGL の初期化を行います。
この例では qglClearColor(const QColor &) を用いて、ビューの背景色(クリアカラー)を指定しています。
OpenGL には glClearColor3b(uchar R, uchar G, uchar B) 等がある(3b の部分は引数の個数・タイプを表す)ので、 それらで色指定も可能ですが、qglClearColor() の方が、Qt の定義済みカラーなども使用できて便利だと思います。

void GLWidget::resizeGL(int width, int height)
{
    //  ビューポートの指定
    glViewport(0, 0, width, height);
}

resizeGL(int width, int height) は、ビューがリサイズされた時にコールされるハンドラです。
glViewport(x, y, width, height) により、OpenGL にビューサイズを指定します。

#include <GL/glu.h>
.....
void GLWidget::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT);   //  カラーバッファをクリア
    glMatrixMode( GL_PROJECTION );  //  射影行列
    glLoadIdentity();               //  変換行列を初期化
    gluPerspective(30.0,            //  視野角
                    (double)width() / (double)height(),     //  アスペクト比
                    1.0, 100.0);    //  視野範囲(近距離・遠距離)
    gluLookAt(  3.0, 4.0, 5.0,      //  視点位置
                0.0, 0.0, 0.0,      //  目標位置
                0.0, 1.0, 0.0);     //  上方向

    glMatrixMode( GL_MODELVIEW );   //  モデルビュー行列
    glLoadIdentity();               //  変換行列を初期化
    qglColor(Qt::black);            //  描画色指定
    glBegin(GL_LINES);              //  線分(GL_LINES)描画開始
    glVertex3d(0.0, 0.0, 0.0);      //  線分座標指定
    glVertex3d(0.0, 1.0, 0.0);
    .....
    glEnd();                        //  線分(GL_LINES)描画終了
    glFlush();                      //  OpenGL の命令をフラッシュ
}

paintGL() は OpenGL の描画を行うメソッドです。

最初に glClear(GL_COLOR_BUFFER_BIT) で、ビューをクリアします。 クリア色は initializeGL() で qglClearColor() または glClearColorXX() で指定した色が使用されます。

OpenGL の描画は図形の3次元頂点座標を、モデルビューと射影行列を使って座標変換することで、ビューの2次元座標に変換します。
変換行列を操作する関数は、どの変換行列でも共通なので、glMatrixMode() を使ってどの変換行列かを指定します。
glLoadIdentity() は、変換行列を単位行列に初期化するものです。
gluPerspective() は、視野角度、画面のアスペクト比、視野範囲(近距離・遠距離)を指定するものです。
gluLookAt() は図形を見る視点、目標位置、視線の上方向を指定します。
以上で射影行列の設定は終わりです。

ついで、glMatrixMode(GL_MODELVIEW) によりモデルビュー行列を選択し、glLoadIdentity() で初期化します。
以上で、このあと指定する3次元座標値をビューの2次元座標に正しく変換することができるようになります。

qglColor(const QColor &) は描画する図形の色を指定します。

glBegin(...) はそれ以降に指定する頂点座標の図形種別を指定するものです。

GL_POINTS
GL_LINES直線
GL_LINE_STRIP折れ線
GL_LINE_LOOP閉じた折れ線
GL_TRIANGLES三角形
GL_QUADS四角形
GL_TRIANGLE_STRIP1辺を共有した三角形群(帯状)
GL_TRIANGLE_FAN1辺を共有した三角形群(扇状)
GL_QUAD_STRIP1辺を共有した四角形群
GL_POLYGON凸多角形

glVertexXX で頂点座標を指定します。

glEnd() で描画を終了し、glFlush() で OpenGL の命令をフラッシュします。

立方体をワイヤーフレーム表示

enum {
    A = 0, B, C, D, E, F, G, H,
};
GLdouble vertexCube[][3] = {
    {-1.0,-1.0,-1.0 }, // A
    { 1.0,-1.0,-1.0 }, // B
    { 1.0, 1.0,-1.0 }, // C
    {-1.0, 1.0,-1.0 }, // D
    {-1.0,-1.0, 1.0 }, // E
    { 1.0,-1.0, 1.0 }, // F
    { 1.0, 1.0, 1.0 }, // G
    {-1.0, 1.0, 1.0 }  // H
};

int edge[][2] = {
    { A, B },
    { B, C },
    { C, D },
    { D, A },
    { E, F },
    { F, G },
    { G, H },
    { H, E },
    { A, E },
    { B, F },
    { C, G },
    { D, H },
};
void GLWidget::paintGL()
{
    .....
    glBegin(GL_LINES);              //  線分(GL_LINES)描画開始
    for (int i = 0; i < 12; ++i) {
        glVertex3dv(vertexCube[edge[i][0]]);
        glVertex3dv(vertexCube[edge[i][1]]);
    }
    glEnd();                        //  線分(GL_LINES)描画終了
    glFlush();                      //  OpenGL の命令をフラッシュ
}

立方体を面表示

void GLWidget::initializeGL()
{
    qglClearColor(Qt::lightGray);  // 背景色指定
    glEnable(GL_DEPTH_TEST);       //  for 印面消去
}
int face[][4] = {
    {A, B, C, D},
    {A, E, F, B},
    {B, F, G, C},
    {C, G, H, D},
    {D, H, E, A},
    {E, F, G, H},
};
void GLWidget::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   //  バッファをクリア
    ....
    glBegin(GL_QUADS);              //  四角形(GL_QUADS)描画開始
    double col[][3] = {
        {1.0, 0.0, 0.0},
        {0.0, 1.0, 0.0},
        {1.0, 1.0, 0.0},
        {0.0, 0.0, 1.0},
        {1.0, 0.0, 1.0},
        {0.0, 1.0, 1.0},
    };
    for (int i = 0; i < 6; ++i) {
        glColor3f(col[i][0], col[i][1], col[i][2]);
        glVertex3dv(vertexCube[face[i][0]]);
        glVertex3dv(vertexCube[face[i][1]]);
        glVertex3dv(vertexCube[face[i][2]]);
        glVertex3dv(vertexCube[face[i][3]]);
    }
    glEnd();                        //  四角形(GL_QUADS)描画終了
    glFlush();                      //  OpenGL の命令をフラッシュ
}

立方体をくるくる回す(アニメーション)

OpenGL でアニメーションを行うには、実際のアニメーションと同じように、 少しずつ変化した画像を短い時間間隔で順に表示します。
そのためには、タイマーオブジェクトを使って、1秒間に数10フレーム表示するようにします。

class MainWindow : public QMainWindow
{
protected slots:
    void    onTimer();
private:
    QTimer  *m_timer;
};
MainWindow::MainWindow(QWidget *parent, Qt::WFlags flags)
    : QMainWindow(parent, flags)
{
    m_glWidget = new GLWidget;
    setCentralWidget(m_glWidget);
    m_timer = new QTimer();
    connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer()));
    m_timer->start(20);     //  50fps
}
void MainWindow::onTimer()
{
    m_glWidget->rotate(5);      //  5度回転
}
class GLWidget : public QGLWidget
{
    .....
public:
    void    rotate(int);        //  立方体を回転
private:
    int     m_r;                //  回転角度
};
GLWidget::GLWidget(QWidget *parent)
    : QGLWidget(parent)
{
    m_r = 0;
}
void GLWidget::rotate(int d)
{
    m_r = (m_r + d) % 360;
    update();       //  ウィジットを無効化して再描画
}
void GLWidget::paintGL()
{
    .....
    glRotated((double)m_r, 0.0, 1.0, 0.0);
    glBegin(GL_LINES);              //  線分(GL_LINES)描画開始
    .....
}

視点位置をスライダーで変更可能に

画面の適当なところにスライダーを配置。
とりあえず、ツールバーに配置してみた。

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);
    ui.mainToolBar->addWidget(m_sliderEyeY = new QSlider);
    m_sliderEyeY->setOrientation(Qt::Horizontal);
    m_sliderEyeY->setMinimum(0);
    m_sliderEyeY->setMaximum(10*10);
    setCentralWidget(m_glWidget = new GLWidget());
    connect(m_sliderEyeY, SIGNAL(valueChanged(int)), m_glWidget, SLOT(eyeYChanged(int)));
    .....
}

GLWidget に視点Y座標を保持するメンバ変数、視点Y座標指定スロット追加

class GLWidget : public QGLWidget
{
    .....
public slots:
    void    eyeYChanged(int);

private:
    int     m_r;
    double  m_eyeY;
};
void GLWidget::eyeYChanged(int y)   //  y = [0, 100]
{
    m_eyeY = (double)y / 5 - 10.0;
    update();
}

描画メソッドで、視点Y座標を参照

//  描画処理
void GLWidget::paintGL()
{
    .....
    gluLookAt(  6.0, m_eyeY, 10.0,      //  視点位置
                0.0, 0.0, 0.0,      //  目標位置
                0.0, 1.0, 0.0);     //  上方向
    .....
}

複数の同形オブジェクトを配置

複数の同形オブジェクトを配置する時、glBegin() 〜 glEnd() 間で、座標値をずらしながら glVertex3d() を記述しても良いが、 座標指定は同一とし、座標変換で同形オブジェクトを配置する方法もあるぞ。

void GLWidget::paintGL()
{
    .....
    for(int z = -4; z < 5; ++z) {
        for(int x = -4; x < 5; ++x) {
            glLoadIdentity();               //  変換行列を初期化
            glTranslated((double)x*3, 0.0, -(double)z*3);
            glRotated((double)m_r, 0.0, 1.0, 0.0);      //  Y軸方向について回転
            glBegin(GL_QUADS);              //  四角形(GL_QUADS)描画開始
            for (int i = 0; i < 6; ++i) {
                glColor3f(col[i][0], col[i][1], col[i][2]);
                glVertex3dv(vertexCube[face[i][0]]);
                glVertex3dv(vertexCube[face[i][1]]);
                glVertex3dv(vertexCube[face[i][2]]);
                glVertex3dv(vertexCube[face[i][3]]);
            }
            glEnd();                        //  四角形(GL_QUADS)描画終了
        }
    }
    .....
}

※ 回転と平行移動演算は可換ではないので順番に注意。