はじめに
Qtアドベントカレンダーも20日目にしてようやく1つめの記事を書いたhermit4です。みなさま、大変ご無沙汰しておりましたがお変わりないでしょうか。
昨日は、@tukimi002-madokaさんの「スライダーとスピンボックスを連動させる」でした。Qtの大きな特徴であるシグナルとスロットの基本となる例ですよね。
Qtに初めて触れた頃は、この方法でシグナルとスロットを学んですごく感動したものです。Qt初めて触るとか、あまり触れた事がない方は是非お試しください。
さて、私事ですが、実はリモートワークで激太り後、色々と病気を併発しまして、医者の指導のもと体調管理に時間を割いておりました。健康が微妙とはいえしがない下請け人生の底辺フリーランスですので、減るわけでもない仕事をこなしつつ、健康に気を使った食事療法と運動で日々終わるような感じでしたが、幸い体調面は成果も出始めていて、血液検査の結果も大幅に改善しつつあるような状況です。
まぁ、そんなこんなで、勉強もあまり捗っておらず、これを書きたいというものもなかったのですが、年に一度のアドベントカレンダーですから、一つは記事を書かねばということで参加しました。カレンダーに記事を1つしか登録してないのは、Qtアドベントカレンダーが始まって以来かもしれません・・・。まぁ、来年はがんばります。
というわけで、ネタもこれと言ったものがものが無いのですが、OpenGLを中途半ばなまま放置してたっけなとQtでOpenGL入門 - まずは歴史から -を読み返して、色々ダメだったことに気がついたので、その辺りの訂正記事と補足をかければ良いかなと思います。
前回記事の訂正
前回の記事ですが、入門なのだからOpenGL勉強を始める最初の一歩にすべきところ、その先のことを考えて色気を出して余計な作りにしていました。
確かまごろく先生あたりに言われたことに、「入門はエンターテイメントであり、必要なところに絞って単純でわかった気にさせることから始めなくてはならない」と教わったのに、すっかり忘れてました。
OpenGLをやりたいときに選択肢としてQtを考えた人の事を思うと、まずは触ってみるという段階で余計な作りになっていました。というわけでその辺りをバッサリ削除して最小限で動くコードに直しておきたいなと思います。
古典的なコード
[反省点] 将来のためという言い訳の必要な実装は、必要になるまで用意しない
cmake_minimum_required(VERSION 3.6)
project(example 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 Gui REQUIRED) # [1]
add_executable(example
main.cpp
mainwindow.cpp
mainwindow.h
) # [2]
target_link_libraries(example PRIVATE Qt5::Gui) # [1]
[1] 前回は、Qt5::Widgetsを使っていましたが、単純にOpenGLを始めるだけならGuiで十分でした。その先にQt Widgetsと組み合わせて色々となるとWidgetsの方が都合が良い場合もあるのですが、入門で例示した程度だとGuiで始めるべきでした。
[2] 前回の記事にはrenderer.cppとrenderer.hを用意して描画周りを別クラスに押し込めようとしていましたが、まず触ってみるだけの段階では不要です。イベント処理などを諸々追加していこうとか思うとQOpenGLWindowから切り離す方が良い場合もあるのですが、色気を出しすぎてました。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtGui/QOpenGLWindow>
#include <QtGui/QOpenGLFunctions_1_0>
class MainWindow : public QOpenGLWindow, protected QOpenGLFunctions_1_0
{
Q_OBJECT
public:
MainWindow(QWindow *parent = nullptr);
~MainWindow();
protected:
void initializeGL();
void paintGL();
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
MainWindow::MainWindow(QWindow *parent)
: QOpenGLWindow(QOpenGLWindow::NoPartialUpdate,parent)
{
}
MainWindow::~MainWindow()
{
}
void MainWindow::initializeGL() //[3]
{
initializeOpenGLFunctions();
glClearColor(0.3f,0.3f,0.3f,1.0f);
}
void MainWindow::paintGL() // [3]
{
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();
}
[3] 以前はRendererクラスとやらに分けてありましたが、MainWindowにコードを戻しました。
#include "mainwindow.h"
#include <QtGui/QGuiApplication> // [4]
#include <QtGui/QSurfaceFormat>
int main(int argc, char *argv[])
{
QSurfaceFormat fmt;
fmt.setRenderableType(QSurfaceFormat::OpenGL);
fmt.setVersion(1,0);
QSurfaceFormat::setDefaultFormat(fmt);
QGuiApplication a(argc, argv); // [4]
MainWindow w;
w.show();
return a.exec();
}
[4] QtのGUI本ではQApplicationが呼び出されることが多いのですが、あれはQt Widgetsに必要となるクラスで、QtGuiではQApplicationの親クラスに当たるQGuiApplicationを使うことになります。
シェーダーのコード
前回の記事は、かなりタチが悪いことに純粋なOpenGLではなくQt APIで記述するやり方になっていました。
OpenGLはクロノス・グループが定義しているAPIで、QtではそのAPIをQOpenGLFunctions系のクラスのメンバとして用意することでこのクラスを継承したクラスで利用できるようにしています。
しかし、OpenGL APIはC言語向けに設計されていて、Qtユーザーには馴染みにくく、色々不便なことから、より簡単にQtユーザーにわかり易く利用できるようQOpenGLFunctionsとは別のクラスとして幾つかの機能が用意されています。
前回はQOpenGLShaderProgramというシェーダー用のQt APIでコードを書き付けましたが、これだとOpenGLを知っているけどQtを知らない方は「???」となるわけで、素直にOpenGLのAPIを使うなら以下のようになるのかと思います。
修正なし
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtGui/QOpenGLWindow>
#include <QtGui/QOpenGLFunctions> // [5]
class MainWindow : public QOpenGLWindow, protected QOpenGLFunctions // [5]
{
Q_OBJECT
public:
MainWindow(QWindow *parent = nullptr);
~MainWindow();
protected:
void initializeGL();
void paintGL();
private:
GLuint programId_;
GLint vertexLocation_;
GLint colorLocation_;
};
#endif // MAINWINDOW_H
[5] シェーダーを利用するのでQOpenGLFunctionsに変更しています。QOpenGLFunctionsはQOpenGL ES 2.0環境とデスクトップで共通に利用できる機能を集めたクラスです。なお前回記述漏れしていましたがOpenGL ES 3.xとの共通APIを含むクラスとしてQOpenGLExtraFunctionsも用意されています。
#include "mainwindow.h"
// [6] GLSL programs
static const char *vertexShaderSource =
"#version 120\n"
"#ifndef GL_ES_VERSION_2_0\n"
"#define highp\n"
"#endif\n"
"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 =
"#version 120\n"
"#ifndef GL_ES_VERSION_2_0\n"
"#define highp\n"
"#endif\n"
"varying highp vec4 frag_color;\n"
"void main(void)\n"
"{\n"
" gl_FragColor = frag_color;\n"
"}";
MainWindow::MainWindow(QWindow *parent)
: QOpenGLWindow(QOpenGLWindow::NoPartialUpdate,parent)
{
}
MainWindow::~MainWindow()
{
glDeleteProgram(programId_);
}
void MainWindow::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.0f,0.0f,0.0f,1.0f); // [7]
GLuint vertexShaderId = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShaderId, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShaderId);
GLuint fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShaderId, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShaderId);
programId_ = glCreateProgram();
glAttachShader(programId_, vertexShaderId);
glAttachShader(programId_, fragmentShaderId);
glDeleteShader(vertexShaderId);
glDeleteShader(fragmentShaderId);
glLinkProgram(programId_);
vertexLocation_ = glGetAttribLocation(programId_, "vertex");
colorLocation_ = glGetAttribLocation(programId_, "color");
}
void MainWindow::paintGL()
{
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,
};
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(programId_);
glEnableVertexAttribArray(vertexLocation_);
glVertexAttribPointer(vertexLocation_, 2, GL_FLOAT, false, 0, triangleVertices);
glEnableVertexAttribArray(colorLocation_);
glVertexAttribPointer(colorLocation_, 4, GL_FLOAT, false, 0, traiangleColors);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(colorLocation_);
glDisableVertexAttribArray(vertexLocation_);
glUseProgram(0);
}
[6] GLSL(OpenGL Shader Language)は、version指定や互換性のためのdefineなども必要になります。これらは、Qt APIの時はQt側である程度よろしくしてくれていたので省略していましたが、OpenGL APIで利用する際には正しく記載する必要があります。
[7] 背景色は昔の書き方の結果と区別するため黒一色に変更しています。
#include "mainwindow.h"
#include <QtGui/QGuiApplication> // [4]
int main(int argc, char *argv[])
{
QGuiApplication a(argc, argv); // [4]
MainWindow w;
w.show();
return a.exec();
}
古いAPIを必要としないので、フォーマットの指定は削除しても動作します。
実行結果
こちらは以前と変わらず以下のようになります。
古代版 | シェーダー版 |
---|---|
補足説明
QOpenGLWindowは、初回のpaintGLもしくはresizeGLに先立ってinitializeGLを呼び出します。
描画のために必要な初期化処理はここで行うことになります。
例えば、QOpenGLFunctions系やQOpenGLExtraFunctionsは、QOpenGLContextとの関連づけを行うためinitializeOpenGLFunctions()を呼び出す必要があります。この呼び出し前にOpenGL functionsを呼び出すと対象のcontextが与えられていない状態でクラッシュしますが、paintGLやresizeGL前にinitializeGLが呼び出される事が保証されているため、initializeGLの最初で呼び出しておけば安全です。
また、glClearするための配色をglClearColor(R,G,B,A)で登録したり、描画処理前に事前にしておきたい初期化処理(シェーダーのソースコードのコンパイルやリンク)などはここで実施することになります。
この例は、(-0.5,0.5)(-0.5,-0.5)(0.5,-0.5)の3頂点で三角形を形成して描画するよう指定しています。
この座標はQtユーザーにはぱっと見何がどうなっているのか分からないかもしれません。答えは簡単でOpenGLとQtとで座標系が異なります。
width=100, height=100の正方形なウィンドウに等倍で描画すると、Qt座標(ウィンドウ座標)では、(25,25)(25,50),(75,50)となります。
昔の書き方としては、glBeginでどう描くかを指定してglBegin()からglEnd()間で頂点を指定していました。サンプルコードでは省いていましたが、OpenGLだとバッファリングされる場合があるため、最後にglFlush()するよう学んだ方もいるかと思います。
glBegin(GL_TRIANGLES);
glVertex2f(-0.5f, 0.5f);
glVertex2f(-0.5f,-0.5f);
glVertex2f(0.5f, -0.5f);
glEnd();
glFlush();
一方シェーダーの場合、Vertexシェーダーで、gl_Positionという組み込み変数に頂点情報を登録した上で、0番めから3点を三角形として描画するという指示を出しています。
attribute vec4 vertex;
void main(void)
{
gl_Position = vertex;
}
static GLfloat const triangleVertices[] = {
-0.5f, 0.5f,
-0.5f, -0.5f,
0.5f, -0.5f,
};
vertexLocation_ = glGetAttribLocation(programId_, "vertex");
:
glVertexAttribPointer(vertexLocation_, 2, GL_FLOAT, false, 0, triangleVertices);
:
glDrawArrays(GL_TRIANGLES, 0, 3);
配色については、旧来は各頂点の前にglColor3fでRGBを指定していました。
glColor3f(1.0f, 0.0f, 0.0f); // (-0.5, 0.5)のcolor
glColor3f(0.0f, 1.0f, 0.0f); // (-0.5, -0.5)のcolor
glColor3f(0.0f, 0.0f, 1.0f); // ( 0.5, -0.5)のcolor
シェーダーでは頂点属性として定義したcolorをfrag_color変数を経由してフラグメントシェーダーのgl_FragColorにRGBAの4値の頂点分の配列を引き渡しています。
attribute highp vec4 color;
varying highp vec4 frag_color;
void main(void)
{
:
frag_color = color;
}
varying highp vec4 frag_color;
void main(void)
{
gl_FragColor = frag_color;
}
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,
};
:
colorLocation_ = glGetAttribLocation(programId_, "color");
:
glVertexAttribPointer(colorLocation_, 4, GL_FLOAT, false, 0, traiangleColors);
この時のColor値ですが、QtのQColorでは、Red,Green,Blue,Alphaをそれぞれ0〜255のint値で表現しますが、OpenGLでは、0.0f〜1.0fで表現します。馴染みがない指定方法で困るという方は、QColor::redF(), QColor::greenF(), QColor::blueF(), QColor::alphaF()を利用して変換すると幸せになれるのかもしれません。
Qtクラスを使ったシェーダー対応コード
Qtを使うとどんな風に便利になるかも含めて見せるため、こちらも少し変えてみます。
シェーダープログラムは別ファイルにします。文字列で用意しているより圧倒的に読み易くなりますね。
attribute highp vec4 vertex;
attribute highp vec4 color;
varying highp vec4 frag_color;
void main(void)
{
gl_Position = vertex;
frag_color = color;
}
varying highp vec4 frag_color;
void main(void)
{
gl_FragColor = frag_color;
}
これらをQt Resource systemを使ってリソース化してしまいます。
<RCC>
<qresource prefix="/">
<file>vertex.glsl</file>
<file>fragment.glsl</file>
</qresource>
</RCC>
cmake_minimum_required(VERSION 3.6)
project(example 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 Gui REQUIRED)
add_executable(example
main.cpp
mainwindow.cpp
mainwindow.h
glsl.qrc
)
target_link_libraries(example PRIVATE Qt5::Gui)
qrc ファイルをソースコードに追加するだけです。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtGui/QOpenGLWindow>
#include <QtGui/QOpenGLFunctions>
class QOpenGLShaderProgram;
class MainWindow : public QOpenGLWindow, protected QOpenGLFunctions
{
Q_OBJECT
public:
MainWindow(QWindow *parent = nullptr);
~MainWindow();
protected:
void initializeGL();
void paintGL();
private:
QOpenGLShaderProgram *program_;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QtGui/QOpenGLShaderProgram>
MainWindow::MainWindow(QWindow *parent)
: QOpenGLWindow(QOpenGLWindow::NoPartialUpdate,parent), program_(nullptr)
{
}
MainWindow::~MainWindow()
{
}
void MainWindow::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.0f,0.0f,0.0f,1.0f);
program_ = new QOpenGLShaderProgram(this);
program_->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vertex.glsl");
program_->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fragment.glsl");
program_->link();
}
void MainWindow::paintGL()
{
static const QVector<QVector2D> triangleVertices = {
{-0.5, 0.5},
{-0.5, -0.5},
{ 0.5, -0.5},
};
static const QVector<QVector4D> traiangleColors = {
{1.0, 0.0, 0.0, 1.0},
{0.0, 1.0, 0.0, 1.0},
{0.0, 0.0, 1.0, 1.0},
};
glClear(GL_COLOR_BUFFER_BIT);
program_->bind();
program_->enableAttributeArray("vertex");
program_->enableAttributeArray("color");
program_->setAttributeArray("vertex", triangleVertices.constData());
program_->setAttributeArray("color", traiangleColors.constData());
glDrawArrays(GL_TRIANGLES, 0, 3);
program_->disableAttributeArray("vertex");
program_->disableAttributeArray("color");
program_->release();
}
シェーダープログラムはリソースファイルとしてプログラムに埋め込み、QOpenGLShaderProgramクラスのaddShaderFromSourceFileにファイル名を指定して直接読み込んでいます。あとは名前指定で属性を指定して処理しています。
OpenGLは、ファイルから読み取るといったAPIがなく、実際のデータを受け渡さなくてはなりませんが、Qtを使うとその辺りをサポートしてくれているため、別ファイルにした上でスッキリしたコードにすることができます。その代わりQtが利用できない環境への移植性はガッツリ落ちるわけですが・・・。
頂点なんかのデータも、C++の最近の初期化子とQVector,QVector2D,QVector4Dの組み合わせで非常にすっきりと書く事ができますね。ここまでシンプルなら書いてみようかって気になるに違いありません・・・え、ならないですか、そうですか。
OpenGLで主に利用するクラス群
次回記事を書く気力が出たらテクスチャーとか、、Surface周りとバージョン指定、さらに続くならコンテキストとスレッドなど、触れたい事は色々あるわけですが、ここでは軽くどんなクラス群が出てくるのかなという名前だけ紹介しておきます。
サーフェースとコンテキスト
- QOpenGLContext
- QOpenGLContextGroup
- QSurface
- QSurfaceFormat
- QOpenGLVersionProfile
- QWindow(QOpenGLWindowの親クラス)
- QOffscreenSurface(描画をしないサーフェース)
Function/ExtraFunction
- QAbstractOpenGLFunctions
- QOpenGLFunctions
- QOpenGLFunctions_[major]_[minor]
- QOpenGLExtraFunctions
シェーダー
- QOpenGLShader
- QOpenGLShaderProgram
テクスチャー
- QOpenGLTexture
- QOpenGLTextureBlitter
バッファとオブジェクト
- QOpenGLBuffer
- QOpenGLFramebufferObject
- QOpenGLFramebufferObjectFormat
- QOpenGLVertexArrayObject
- QOpenGLVertexArrayObject::Binder
デバッグ及びプロファイル
- QOpenGLDebugLogger
- QOpenGLDebugMessage
- QOpenGLTimeMonitor
- QOpenGLTimerQuery
CPUでの演算とか、データの受け渡しとか、テクスチャ用のイメージ読み込み等
- QColor
- QVector2D
- QVector3D
- QVector4D
- QMatrix
- QMatrix4x4
- QGenericMatrix
- QRect
- QRectF
- QSize
- QSizeF
- QImage
まとめ
というわけで、2年ほど前に適当な記事をざっくり書きすぎた馬鹿な私を助けてあげて・・・という心の声に従って2年前とあまり代わり映えのしない記事を書いてしまったわけで、怒られが発生しそうな気もしますが、2年後の自分とか、どこかの誰かの役に立つ時がくれば良いなぁとか思ってます。
やりたいことの方は多少形になっているのですが、もともと他所様のフレームワークに手を入れていてライセンス周りの許諾書とか確認しきれていないところがあるので面倒になって止まってます。