QtでOpenGL(GLSL)を扱う


1.目的

3DCGを直接扱う方法の一つにはOpenGLの手段があります。(もう一つはDirectX)
それが映し出されるウィンドウが必要になるわけですが、OpenGLで有名なライブラリはGLFWであり、これについては多くの情報が転がっています。他にもQtを使う選択肢があり、Qtを使う場合についてメモを残します。

Qtは超有名なGUIライブラリでネット上に多くの情報が転がっていますが、OpenGLをQtであつかう記事は固定機能シェーダ(昔のOpenGLみたいな感じ)が多く、プログラマブルなGLSL(自分で記述する)を解説しているものが少なかったのでまとめ的な感じでのせます。

※QtやOpenGL自体の記事ではなく、QtでOpenGLを使うにはどうするかしか扱わないのであしからず。

とりあえず三角形まで描写したいと思います。

2. Qtをあつかうメリットとデメリット

メリット
- Qt自体が優秀なGUIツール
- Qtがプラットフォーム間の差異を吸収し、コンパイル時のフラグなどの煩わしさを減らしてくれる
- Qtをインストールするだけで準備が整う
- Qt自体に行列を扱うクラスがあるので、glm等を使う必要がない

特に2,3番目はありがたい!もしGLFWを使う場合、インストールにcmakeなどが必要になるし、パスも通さないといけないので結構面倒

デメリット
- Qtが容量を食う(25Gbとか)のでOpenGLだけのためだと割りに合わない
- GUIのウィジェット(コンテナ)等の概念が必要

3.概要

先に大まかな注意点と、見通しを書きます。

注意点

Qt自体、OpenGLに関してのクラスが多くあります。扱うのはQOpenGLWidgetQOpenGLFunctionsクラスを主に使います。(Ver 5.10) 他にもQOpenGLShaderProgramクラスなどいくつかクラスがありますが、OpenGLのgl*から始まる関数群を使うには先に挙げた二つさえあれば十分です。注意点としては、QGL*から始まるクラスは古いので非推奨です。
例えば、QGLWidgetの項目には

This class is obsolete. It is provided to keep old source code working. We strongly advise against using it in new code

とあります。要はQOpenGL*から始まるクラスを使いましょうってことです。

また、QtではOpenGLを使うのには二つの方法があります。一つが本来のOpenGLのAPIを使う方法であり、もう一つはQtが用意してくれているクラスを利用する方法です。後者は例えばQOpenGLShaderProgramクラスでプログラムを割り当てたりします。今回は前者の方を使います。

最後にGLSLのバージョンを指定するために、あらかじめQtに バージョンを伝える必要があるようです。これをやらないと、シェーダプログラムがコンパイルエラーを吐きました。

概要

QOpenGLWidgetとQOpenGLFunctionsを継承したクラスを作ります。QOpenGLWidgetにはinitializeGL(),resizeGL(int width, int height),paintGL()関数があるので基本的にそれらをオーバーライドします。それぞれ、初期化時、ウィンドウサイズのリサイズ時、描画時に呼ばれ、特にpaintGL()に関してはQWidget::update()を行うことでも実行されます。QOpenGLWidgetが通常のQWidgetの代わりで、QOpenGLFunctionsはGLEWのようなOpenGLの拡張的なイメージだと思います。

4.本題

環境:バージョンは Qt5.10 でコンパイラがclangです。

.proファイルの設定

まず、Qtのスタート画面から New Project を選択し、Qt Widgets Application を選び、適当な名前をつけてプロジェクトを作成します。この時、必要がないのでuiファイルのチェックボックスは外しておきましょう。その他諸々は適当で大丈夫です。

そのあと、Qtが自動で各ファイルを作成してくれます。しかし、main.cpp以外のソースファイルとヘッダファイルは今回使わないので削除しておっけいです。デフォルトだとmainWindowだと思います。
.proファイルは削除してはダメです.

次に.proファイルを編集します。OpenGLのモジュールを扱うには次のように追記する必要があります。(2018/3/29日訂正 Qt5では追記の必要なし argama147さんありがとうございます。)

qiita00.pro
//省略...
QT += OpenGL

このファイル名はプロジェクト名と同じになります。今回はqiita00としました。
これ以降.proファイルをいじる必要はありません。ファイルを追加しても、QtCreatorが勝手に編集してくれます。

main関数

上に書いた通り、OpenGLのバージョンを指定する必要があります。

main.cpp
#include <QApplication>
#include <QSurfaceFormat> //追加

int main(int argc, char *argv[])
{
    QSurfaceFormat fmt;
    fmt.setVersion(4,0);//versionを指定
    fmt.setProfile(QSurfaceFormat::CoreProfile);//coreProfileを使う
    QSurfaceFormat::setDefaultFormat(fmt);//以上の設定を適応

    QApplication a(argc, argv);

    return a.exec();
}

QSurfaceFormatクラスで指定します。次に自作クラスとしてGLWidgetクラスを用意します。これにQOpenGLWidgetとQOpenGLFunctionsを継承させます。今回はスペースの関係上ヘッダファイルに全て定義するとします。

GLWidget.h
#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QVector>

class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions{
private:
    GLuint m_program;

    GLuint m_vao;
    GLuint m_vbo;

    QVector<GLfloat> m_vertices;

    const char *vshader_src =
            "#version 400 core\n"
            "layout(location = 0) in vec4 position;\n"
            "void main(){\n"
            "gl_Position = position;\n"
            "}\n";
    const char *fshader_src =
            "#version 400 core\n"
            "out vec4 color;\n"
            "void main(){\n"
            "color = vec4(1.0,1.0,1.0,1.0);\n"
            "}\n";

};
#endif // GLWIDGET_H

本来ならば、GLSLを他のファイルに書き込んだり、別に頂点座標などを持つ基底クラスを作るべきですが、三角形だけを描画するにはこれで良いでしょう。

次にコンストラクタとinitializeGL()の実装します。

GLWidget.h
//省略
public:
    GLWidget(QWidget *parent = nullptr):QOpenGLWidget(parent){
        m_vertices << -0.5f << -0.5f << 0.0f //三角形
                   <<  0.0f <<  0.5f << 0.0f
                   <<  0.5f << -0.5f << 0.0f;
    }
    //コンストラクタの次に呼ばれる
    void initializeGL(){
        initializeOpenGLFunctions();//初期化
        glClearColor(0.0f,0.0f,0.0f,1.0f);

        m_program = glCreateProgram();
        //vertex shaderの作成
        GLuint vshader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vshader,1,&vshader_src,0);
        glCompileShader(vshader);
        //fragment shaderの作成
        GLuint fshader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fshader,1,&fshader_src,0);
        glCompileShader(fshader);
        //プログラムにアタッチ
        glAttachShader(m_program,vshader);
        glAttachShader(m_program,fshader);
        //リンク
        glLinkProgram(m_program);
        //削除
        glDeleteShader(vshader);
        glDeleteShader(fshader);
    }

initializeOpenGLFunctions()関数は初期化のために必要なのでinitializeGL()関数で呼び出します。今回はQOpenGLFunctions()をprotected継承しましたが、継承せずに変数を作るやり方でも可能っぽいです。公式サイトの例ではそうなっていますが、継承したほうが見やすそうな気がしました。エラーのログとかは省略です。

次にvaoなどの設定とpaintGL()の実装をすればおっけいです。resizeGL()に関しては今回、行列等の変換はないので定義する必要はないです。

GLWidget.h
    void initializeGL(){

        //省略

        //vaoの作成
        glGenVertexArrays(1,&m_vao);
        glBindVertexArray(m_vao);
        //vboの作成
        glGenBuffers(1,&m_vbo);
        glBindBuffer(GL_ARRAY_BUFFER,m_vbo);
        glBufferData(GL_ARRAY_BUFFER,m_vertices.size()*sizeof(GLfloat),
                     m_vertices.constData(),GL_STATIC_DRAW);
        //vertex shaderのコードで頂点座標のロケーションは0に指定済
        glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,0,0);
        glEnableVertexAttribArray(0);
    }


    void paintGL(){
        glClear(GL_COLOR_BUFFER_BIT);
        glUseProgram(m_program);
        glBindVertexArray(m_vao);
        glDrawArrays(GL_TRIANGLES,0,m_vertices.size()/3); //ドローコール
    }

忘れずにメイン関数からこのクラスの変数を作って呼び出せば

main.cpp
#include "glwidget.h" //追加

int main(int argc, char *argv[]){
    //省略
    GLWidget w;
    w.show(); //表示
    return a.exec();
}

以上です。
初心者かつ初投稿なのでコメントしてくれるととても喜びます!!

参考ページ doc.qt.io/qt-5/qopenglwidget.html