Help us understand the problem. What is going on with this article?

QtでOpenGL入門 - まずは歴史から -

はじめに

@RIO18020 さんの「Qt World Summit 2019」以降、2日、3日とも空白となってしまいました。穴埋め記事も考えたのですが、今回はちょっと手が回らなくて空きっぱなしです。埋めて下さる方がいるとよいのですが・・・まぁ、時間が取れれば何か入れます。

もう少し布教活動頑張らないとダメですねぇ・・・orz

背景

最近、ちょっとやりたいことがあって、OpenGLの勉強を再開しました。まぁ、つまり過去にも勉強しようとしたのですが挫折しています。とはいえ、昔挫折の原因となった行列周りも、衛星軌道の計算とか、機械学習とか、どうも避けて通るのが難しい時もあり「数学ガールの秘密ノート/行列が描くもの」を読んでみたり、いろいろな方のアドバイスを受けながら、少しずつ勉強しているような状況です。

まぁ、そのようなわけで、OpenGLに再度手を出したので、残念な記事しか書ける気がしませんが、同じ道を歩こうとする方のわずかでも道標になればということでOpenGL関連の記事を書いておきます。

Qtの3DCG状況

Qtでは3DCGを行う方法は複数用意されています。

  • C++でQOpenGL*クラスを使いOpenGL APIを使う
  • - QMLでQt Canvas 3D を使う (Qt 5.13で削除)
  • C++でQt3D*クラスを使う
  • QMLでQt3D.* を使う
  • C++でQVulkan*クラスを使いVulkan APIを使う
  • QMLでQt Quick 3Dを使う (Qt 5.14でテクノロジープレビューとして導入予定)
  • QtWebEngine + WebGL

C++でQOpenGL*クラスを使いOpenGL APIを使う

もっとも古くからあり、多くのドキュメントがあるのはOpenGLです。OpenGLでは、固定パイプラインの頃から現在のシェーダー言語による方式までサポートしてきましたが、長年に渡る拡張により、古くからのしがらみ、古い設計思想、汎用性を重視してきたため今時のHWの性能を十分に引き出せないなどの課題が山積し、陳腐化してきました。

そのためApple社はより低レベルなAPIであるMetalを発表し、macOSがOpenGLのサポートを取りやめる方向にあり、OpenGLを策定しているクロノス・グループも低レベルなAPIであるVulkanを策定するなど、現在は過渡期にあります。

QMLでQt Canvas 3D を使う

Qt Canvas 3Dモジュールは、Qt5.4からテクノロジープレビューとしてQt Quickに導入されたモジュールで、Qt Quick JavaScriptからWebGLライクに3D描画を呼び出せる機能として提供されていました。three.jsのような便利なライブラリも利用することができてWebGLのサンプルなどを組み込んで動かしてみるには便利そうだったのですが、5.13で削除となりました。
まぁ、WebGLでガリガリ学習するのであれば、別にQtは必要なくブラウザで遊べば良いじゃんと言われるとその通りです。

削除の方向になった理由は把握していませんが、おそらくQt6に向けて、Qt Quickの描画周りをOpenGL/OpenGL ES便りではなく、MetalやDirect3D等で動作できるよう抽象レイヤを作る方針になっているため、OpenGLにべったりな機能を削除する方向性なのかと思います。

C++/QMLでQt3D*クラスを使う

Qt 3Dは、Qt5.5あたりでテクノロジープレビューとして導入されました。Qt 3Dは主にKDAB社がコントリビュートした機能で、3Dの構築、レンダリング、操作のための高レベルなAPIを提供しています。
メリットとしては、OpenGLを意識せず、より高レベルなAPIで簡単に触って行ける点、デメリットとしてはOpenGLのような一般的な知識ではないため、Qt 3D専用の知識になってしまう辺りでしょうか。

KDAB社主導のため従来のQtのAPIとは命名規則や設計思想が微妙に異なるので違和感を感じるとの声も聞きますが、Boothなどで @shin1_okada さんの技術同人誌も出ていますし、KDABさんのサンプルなどありますので、ぜひ一度触ってみて下さい。

C++でQVulkan*クラスを使いVulkan APIを使う

いつから導入されていたのかは確認していませんが、ドキュメントをちらほら眺めてみたら、Vulkan対応もしれっと入っていました。
VulkanはOpenGLの反省からHWよりの低レイヤなAPI群となっており、学習するには3DCGの知識だけではなくGPUの知識もかなり必要となっているようで、非常に難しそうです。

学習し始めのhermit4には敷居が高いので、誰かカレンダーに記事書いてくれる人がいるとうれしいです。

QMLでQt Quick 3D

Qt 3DはKDAB社が開発し提供した機能でしたが、Qt Quick 3DはThe Qt Companyが主導で開発を進めているモジュールで、Qt 3Dより簡単に3Dを扱えるようにすることを目的としているらしいです。
Qt Quick 3Dは、とにかく簡単に3Dを実現することを目的としており、Qt 3Dは3DCGに理解のある人が細かなレベルでの制御をしたり、複雑なユースケースに対応できるよう考慮しているという流れのようです。
なお、最近のThe Qt CompanyらしくGPLと商用ライセンスでの提供のようですので、ライセンス周りにはお気をつけ下さい。

Qt WebEngine + WebGL

Qt WebEngineは、Chromiumベースであり、WebGLも利用可能です。WebGLで作ったものをアプリケーションにしたくなった時の選択肢としてはありなのかもしれません。

なぜ今時 OpenGL なのか

ということで、Qtで3Dを扱かう方法はいくつかあるのですが、

  • どうせ勉強するなら仕事で活かすことも考慮したい
  • hermit4は仕事上非常に古い環境で生息している
  • Qtが採用できるとは限らない
  • 時間が惜しいので日本語の書籍などの多い環境で学習したい

ということもあってOpenGLからスタートしようと思います。

復讐編

過去に挫折を味わったとはいえ、OpenGLに触ったのは初めてではないので、まずは過去の知識のお試しから。

  1. プロジェクトをQt Widgets Applicationとして作成します

    openglproj.png

  2. 名前は適当に決めてください

    projname.png

  3. ビルドシステムはCMakeにしました

    buildsystem.png

  4. ウィンドウはひとまずQMainWindowベースで作成

    baseclass.png

  5. 新しいクラスを追加します

    • プロジェクトツリーのプロジェクト名を右クリック
    • コンテキストメニューからAdd Newを選択します
      スクリーンショット 2019-12-04 12.39.45.png
    • C++クラスを選択
      スクリーンショット 2019-12-04 12.40.07.png
    • OpenGLRendererクラスをQObjectを継承させて作成します
      スクリーンショット 2019-12-04 13.03.14.png
  6. CMakeLists.txtにファイルを足す
    Qt Creator 4.10の環境では、今の所新しいファイルは自動追加されませんでした。手作業で編集する必要がありました。

CMakeLists.txt
cmake_minimum_required(VERSION 3.5)

project(3dcg1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt5 COMPONENTS Widgets REQUIRED)

add_executable(3dcg1
  main.cpp
  mainwindow.cpp
  mainwindow.h
  openglrenderer.cpp
  openglrenderer.h
)

target_link_libraries(3dcg1 PRIVATE Qt5::Widgets)

ここからは、コードの実装です。

openglrenderer.h
#ifndef OPENGLRENDERER_H
#define OPENGLRENDERER_H

#include <QObject>
#include <QOpenGLFunctions_1_0> // ...[1]

class OpenGLRenderer : public QObject, protected QOpenGLFunctions_1_0
{
    Q_OBJECT
public:
    explicit OpenGLRenderer(QObject *parent = nullptr);
    void initializeGL();
    void paintGL();
signals:

public slots:
};

#endif // OPENGLRENDERER_H

[1] 今回は、復習なのでOpenGL 1.0にしました。QOpenGLFunctions*は、OpenGLのバージョン毎に異るAPIセットです。どのAPIを利用するかはユーザーが選択する事になります。

openglrenderer.cpp
#include "openglrenderer.h"

OpenGLRenderer::OpenGLRenderer(QObject *parent) : QObject(parent)
{
}

void OpenGLRenderer::initializeGL()
{
    initializeOpenGLFunctions();       // .. [2]
    glClearColor(0.3f,0.3f,0.3f,1.0f); // .. [3]
}

void OpenGLRenderer::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT); // ..[4]
}

[2] OpenGLの初期化処理をまとめた関数で、最初に呼び出す必要があります。
[3] Color Bufferに色を設定しています。
[4] Color Bufferの内容で画面を塗りつぶします

続いて、この描画を実際にさせるウィンドウのため、MainWindowをOpenGL向けに変更します。

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QOpenGLWindow>

class OpenGLRenderer;

class MainWindow : public QOpenGLWindow
{
    Q_OBJECT

public:
    MainWindow(QWindow *parent = nullptr);
    ~MainWindow() Q_DECL_OVERRIDE;

protected:
    void initializeGL() Q_DECL_OVERRIDE;
    void paintGL() Q_DECL_OVERRIDE;

private:
    OpenGLRenderer *renderer_;
};
#endif // MAINWINDOW_H
mainwinodw.cpp
#include "mainwindow.h"
#include "openglrenderer.h"

MainWindow::MainWindow(QWindow *parent)
    : QOpenGLWindow(QOpenGLWindow::NoPartialUpdate,parent),
      renderer_(new OpenGLRenderer(this))
{
}

MainWindow::~MainWindow()
{
}

void MainWindow::initializeGL()
{
    renderer_->initializeGL();
}

void MainWindow::paintGL()
{
    renderer_->paintGL();
}

Qtどドキュメントなどでは、QOpenGLWindowにQOpenGLFunctions*を多重継承させてWindowの中に直接初期化処理などを書いていますが、QOpenGLWindowではなくQOpenGLWidgetで切り出して利用したくなった場合などに分割してあると便利そうです。

このOpenGLRendererクラスにQObjectを継承させているのはオブジェクトの破棄を親にお任せするためです。このようにクラス分割をして実装する方法自体は、SRA社の朝木さんに教わりました。いろいろ勉強になることを教えていただいて感謝です。

ドキュメントには直接継承する方法を推奨と書かれていて、描画だからQOpenGLWindowsに多重継承すべきなのかと勘違いしていましたが、どうやらQOpenGLContextから関数ポインタを取り出して利用する方法もあるようで、その方法よりは、直接継承した方が良いよということのようですね。

最後に、main.cppです。

main.cpp
#include "mainwindow.h"

#include <QApplication>
#include <QSurfaceFormat>

int main(int argc, char *argv[])
{
#include "mainwindow.h"

#include <QApplication>
#include <QSurfaceFormat>

int main(int argc, char *argv[])
{
    QSurfaceFormat fmt;
    fmt.setRenderableType(QSurfaceFormat::OpenGL);
    fmt.setVersion(1,0);
    QSurfaceFormat::setDefaultFormat(fmt);
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

今回は、OpenGL 1.0関数を使うため、ドライバに対して1.0を使うよという宣言のため、QSurfaceFormatでデフォルトのフォーマットを登録しています。自分の使うAPIとドライバに設定するバージョンはそろえておかないとおかしな挙動を示したり不具合の要因になるそうです。

Qtのデフォルトは、今の所 2.0でQOpenGLFunctions (OpenGL ES 2.0)を使う限りは、この設定は不要のようです。

これでビルドを行い、実行すると灰色に塗りつぶされたウィンドウが生成されます。

スクリーンショット 2019-12-04 13.59.15.png

お約束ということで、三角形を作成して見ましょう。

openglrenderer.cpp
void OpenGLRenderer::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_TRIANGLES);     // .. [5]
    glVertex2f(-0.5f, 0.5f);
    glVertex2f(-0.5f,-0.5f);
    glVertex2f(0.5f, -0.5f);
    glEnd();                   // .. [5]
}

[5] glBegin()からglEnd()の間に頂点座標を登録します。

これをリビルドして実行すると白い三角形が描画されます。

スクリーンショット 2019-12-04 14.04.33.png

流石に白では寂しいので色を追加してみましょう。

openglrenderer.cpp
void OpenGLRenderer::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT);
    glBegin(GL_TRIANGLES);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex2f(-0.5f, 0.5f);
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex2f(-0.5f,-0.5f);
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex2f(0.5f, -0.5f);
    glEnd();
}

各頂点の前に色を指定して行きました。これを描画すると
スクリーンショット 2019-12-04 14.12.09.png

のようになります。

OpenGLの歴史とシェーダーベースのOpenGL

hermit4と同じく古いOpenGLを学習して止まっている人はこのコードを普通のものとして受け止めると思います。が、この書き方は太古の書き方扱いされ、今では参考にすべきでないとして、3DCGをやっている方々からの怒られが発生します。

この書き方ですと

・固定パイプラインに依存
・即時実行モードを使用
・ステート変数へのデフォルト値使用

といった問題をはらんでいます。上記のpaintGLのうち、シェーダーへ移行後のOpenGLではglClear()以外は非推奨です。固定パイプライン(固定機能パイプライン・固定機能シェーダー)は、描画のための一連の流れがあらかじめ決められていることを意味しています。
つまりglBegin()とglEnd()で与えられた情報をどのように設定してどのように描画するかが事前に定義されて、順に演算処理されて行きます。

3点のみ、2Dの平面程度ならどうということもありませんが、実際の3DCGは膨大な頂点をどの方向から見ているのか、どの方向から光源が当たり、周囲の環境に対してどのような描画となるのかの膨大な計算が行われます。

これらの計算はGPUが、頂点の計算、クリッピング、ラスタイズ、テクスチャ適応など、それぞれに応じた専用回路により処理されていくわけですが、即時描画するための処理をしていくと、負荷の高い回路と、すぐに終わって暇な回路とができて効率が悪くなります。

そこで、これらをよりプログラマブルにして有効活用しよう登場したのがプログラマブルシェーダです。glBegin()/glEnd()の時代は終わりを告げ、シェーダーベースの時代になっていました。つまり何が言いたいかというと過去に勉強した内容が・・・いえ、辞めましょう。前向きに新しいことをやりましょう。

Qtのサポートも含めてざっくりとOpenGLのバージョンを眺めると、Qt 5.13の時点では、4.5までのAPIを用意しています(使えるかどうかは環境依存ですが)。

Version Qt Functions 概要
OpenGL 1.0 QOpenGLFunctions_1_0
OpenGL 1.1 QOpenGLFunctions_1_1 テクスチャに対応
OpenGL 1.2 QOpenGLFunctions_1_2
OpenGL 1.3 QOpenGLFunctions_1_3
OpenGL 1.4 QOpenGLFunctions_1_4
OpenGL 1.5 QOpenGLFunctions_1_5 プログラマブルシェーダー拡張機能(GLSL 1.0)
OpenGL 2.0 QOpenGLFunctions_2_0 シェーディング言語標準仕様(GLSL 1.1)
OpenGL 2.1 QOpenGLFunctions_2_1
OpenGL 3.0 QOpenGLFunctions_3_0 大規模アップデート(古いAPIが非推奨に)
OpenGL 3.1 QOpenGLFunctions_3_1 固定機能シェーダー廃止
OpenGL 3.2 QOpenGLFunctions_3_2_Core
QOpenGLFunctions_3_2_Compatibility
ジオメトリシェーダー対応
OpenGL 3.3 QOpenGLFunctions_3_3_Core
QOpenGLFunctions_3_3_Compatibility
OpenGL 4.0 QOpenGLFunctions_4_0_Core
QOpenGLFunctions_4_0_Compatibility
テッセレーションシェーダー
OpenGL 4.1 QOpenGLFunctions_4_1_Core
QOpenGLFunctions_4_1_Compatibility
OpenGL 4.2 QOpenGLFunctions_4_2_Core
QOpenGLFunctions_4_2_Compatibility
アトミックカウンタ実装
OpenGL 4.3 QOpenGLFunctions_4_3_Core
QOpenGLFunctions_4_3_Compatibility
GPGPU用シェーダー追加
OpenGL 4.4 QOpenGLFunctions_4_4_Core
QOpenGLFunctions_4_4_Compatibility
バッファ制御、非同期クエリ
OpenGL 4.5 QOpenGLFunctions_4_5_Core
QOpenGLFunctions_4_5_Compatibility
並列コンパイル

この中で一番大きな変換点はOpenGL 3.1でしょうか。固定機能シェーダーが廃止となっています。

また、組み込み機器、特にスマートフォンの登場により注目されたのが、OpenGL ES(OpenGL for Embedded System)です。特に爆発的に売れた時代、iPhone 3GS以降, Android 2.2以降で使われたOpenGL ES 2.0は固定パイプラインのAPIを持たず、シェーダー言語による実装が必須となっていたため急速に以降が進んだのかと思われます。

Qtは、OpenGL ES 2.0を標準として

Version Qt Functions 概要
OpenGL ES 1.0 N/A OpenGL 1.3のサブセット
OpenGL ES 1.1 N/A OpenGL 1.5のサブセット
OpenGL ES 2.0 QOpenGLFunctions_ES2 OpenGL 2.0のサブセット
OpenGL ES 3.0 N/A OpenGL 3.3のサブセット
OpenGL ES 3.1 N/A OpenGL 4.3のサブセット

ちなみに、QOpenGLFunctionsは、OpenGL ES 2.0とOpenGL 1.xの共通サブセットを含む、全てのES 2.0関数のラッパーを提供するクラスです。ですので、Qtの記事で、バージョンの付与されていないQOpenGLFunctionsが利用されていたら対象はOpenGL ES 2.0です。

Qtは、Linux,macOS,Windows,iOS,Androidなど多様な環境をサポートしているので、標準はOpenGL ES 2.0ということでしょう。まぁ、macOSでのOpenGLサポートが無くなると考えると、これから先はOpenGLではなく抽象化した物を使ってね・・・ということになるのでしょうけど。

QtによるOpenGL(GLSL)の記事は、すでにQiitaにあったのですが、比較のためにどんな感じになるかここでも載せておきます。利用するのはQOpenGLFunctions (OpenGL ES 2.0)です。ドライバのデフォルトは2.0で初期化されるはずですので、main.cppはシンプルにします。

main.cpp
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
openglrenderer.h
#ifndef OPENGLRENDERER_H
#define OPENGLRENDERER_H

#include <QObject>
#include <QOpenGLFunctions>

class QOpenGLShaderProgram;

class OpenGLRenderer : public QObject, QOpenGLFunctions
{
    Q_OBJECT
public:
    explicit OpenGLRenderer(QObject *parent = nullptr);

    void initilizeGL();
    void paintGL();

private:
    QOpenGLShaderProgram *program_;
    int vertexLocation_;
    int colorLocation_;
};

#endif // OPENGLRENDERER_H

新しくメンバ変数を増やしています。

openglrenderer.cpp
#include "openglrenderer.h"
#include <QOpenGLShaderProgram> // .. [6]

// [7] GLSL programs
static const char *vertexShaderSource =
        "attribute highp vec4 vertex;\n"
        "attribute highp vec4 color;\n"
        "varying highp vec4 frag_color;\n"
        "void main(void)\n"
        "{\n"
        "    gl_Position = vertex;\n"
        "    frag_color = color;\n"
        "}";
static const char *fragmentShaderSource =
        "varying highp vec4 frag_color;\n"
        "void main(void)\n"
        "{\n"
        "    gl_FragColor = frag_color;\n"
        "}";

OpenGLRenderer::OpenGLRenderer(QObject *parent) : QObject(parent), program_(nullptr)
{
}

void OpenGLRenderer::initilizeGL()
{
    initializeOpenGLFunctions();
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f); // .. [8]
    program_ = new QOpenGLShaderProgram(this);
    program_->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource); // ..[9]
    program_->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource); // ..[9]
    program_->link(); // ..[10]
    vertexLocation_ = program_->attributeLocation("vertex");
    colorLocation_ = program_->attributeLocation("color");
}


void OpenGLRenderer::paintGL()
{
    glClear(GL_COLOR_BUFFER_BIT);

    static GLfloat const triangleVertices[] = {
        -0.5f, 0.5f,
        -0.5f, -0.5f,
        0.5f, -0.5f,

    };
    static GLfloat const traiangleColors [] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
    };

    program_->bind(); // [11]
    program_->enableAttributeArray(vertexLocation_); // [12]
    program_->setAttributeArray(vertexLocation_, triangleVertices,2); // [13]
    program_->enableAttributeArray(colorLocation_); // [12]
    program_->setAttributeArray(colorLocation_, traiangleColors, 4); // [13]
    glDrawArrays(GL_TRIANGLES, 0, 3); // [14]

    program_->disableAttributeArray(vertexLocation_);
    program_->disableAttributeArray(colorLocation_);
    program_->release(); // [11]
}

[6] QOpenGLShaderProgramを利用します
[7] GLSLで記述するプログラムです。今回は別ファイルにはせずそのまま流し込みます
[8] 背景色が同じだとどちらの実行結果かわかりにくいので黒背景にしました
[9] 頂点シェーダーとフラグメントシェーダーのソースコードを登録,コンパイルします
[10] コンパイルしたシェーダーをリンクします
[11] QOpenGLShaderProgramに登録されたシェーダーをアクティブ化します。releaseで解放となるまで、このシェーダーが有効となります。
[12] Attribute変数の有効化
[13] Attribute変数にC++側で用意した配列の値を設定します
[14] 描画

このプログラムを実行すると以下のようになります。

スクリーンショット 2019-12-04 20.57.36.png

背景はあえて変更していますが、頂点座標と配色は同じですので、結果は背景色以外は同じになっています。

OpenGL 1.0 OpenGL ES 2.0
スクリーンショット 2019-12-04 14.12.09.png スクリーンショット 2019-12-04 20.57.36.png

まとめ

実は、最初はQtでOpenGLというタイトルで記事を書こうかと思ったら、既にそれらしい記事があったので、昔の記憶を掘り起こしつつ、古いコードもまだ動きますよと見せながら、今時のコードと並べて見た次第です。

ついでだから、Qt 3Dで書くには・・・と題して同じ三角形でも書いてみようかなぁ。
とりあえず、明日もカレンダーは空白ですので、埋める余裕のある方は、ぜひお願いします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away