Hanaのプログラミング日記

プログラミング関連で勉強したことの記録です。

QtとOpenGLで三角形を描画する

概要

QtとOpenGLを使って三角形を描画します。
QtにはOpenGLの関数を使うための便利クラスが用意されているのでそれらを使います。

f:id:hana1187:20210321170850p:plain
三角形の描画

実行環境

準備

QOpenGLWidgetとQOpenGLFunctionsを継承したGLWidgetクラスを作成します。
この詳細は前回の記事をご覧ください。

手順

  1. シェーダーのセットアップ
  2. Vertex Buffer Objectのセットアップ
  3. Vertex Array Objectのセットアップ
  4. 頂点情報の書式を設定
  5. 描画

ソースコード

<glwidget.h>

#ifndef GLWIDGET_H
#define GLWIDGET_H

#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLShaderProgram>

class QOpenGLShaderProgram;
class GLWidget : public QOpenGLWidget, public QOpenGLFunctions
{
    Q_OBJECT
public:
    GLWidget(QWidget *parent = nullptr);
    virtual ~GLWidget();

    void initializeGL();
    void paintGL();
    void resizeGL(int w, int h);

private:
    QOpenGLShaderProgram program_;
    QOpenGLBuffer vbo_;
    QOpenGLVertexArrayObject vao_;
};

#endif // GLWIDGET_H

<glwidget.cpp>

#include "glwidget.h"
#include <QOpenGLShaderProgram>

GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent)
{

}

GLWidget::~GLWidget()
{
    if(vbo_.isCreated())
    {
        vbo_.destroy();
    }

    if(vao_.isCreated())
    {
        vao_.destroy();
    }
}

void GLWidget::initializeGL()
{
    initializeOpenGLFunctions();

    // シェーダー生成
    program_.addShaderFromSourceFile(QOpenGLShader::Vertex, "../GLWidget/shader/vshader.glsl");
    program_.addShaderFromSourceFile(QOpenGLShader::Fragment, "../GLWidget/shader/fshader.glsl");
    program_.link();
    program_.bind();

    // Vertex Buffer Object(VBO)生成
    QVector<GLfloat> vertices;
    vertices.append(-0.5f); vertices.append(-0.5f); vertices.append(0.0f);
    vertices.append(0.0f); vertices.append(0.5f); vertices.append(0.0f);
    vertices.append(0.5f); vertices.append(-0.5f); vertices.append(0.0f);
    vbo_.create();
    vbo_.bind();
    vbo_.setUsagePattern(QOpenGLBuffer::StaticDraw);
    vbo_.allocate(vertices.constData(), vertices.count() * sizeof(GLfloat));

    // Vertex Array Object(VAO)生成
    vao_.create();
    vao_.bind();

    // 頂点情報の書式を設定
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);

    vao_.release();
    vbo_.release();
}

void GLWidget::paintGL()
{
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    program_.bind();
    vao_.bind();
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

void GLWidget::resizeGL(int w, int h)
{
    Q_UNUSED(w);
    Q_UNUSED(h);
}

<頂点シェーダー: vshader.glsl>

#version 400 core
layout(location = 0) in vec3 position;

void main()
{
    gl_Position = vec4(position, 1.0);
}

<フラグメントシェーダー: fshader.glsl>

#version 400 core
out vec4 color;

void main()
{
    color = vec4(0.0, 0.0, 1.0, 1.0);
}

解説

1. シェーダーのセットアップ

QtでOpenGLを使うにはGLSL (OpenGL Shading Language)を使ってGPUに対する 描画命令を記述します。今回は頂点シェーダーとフラグメントシェーダーを使って 三角形を描画します。
はじめにシェーダーファイルを作成します。頂点シェーダーでは座標を決めるだけで、フラグメントシェーダーでは青色で塗りつぶしているだけのシンプルなシェーダーです。
ソースコードを見ていきます。QtにはOpenGLでシェーダーを使うためのクラスとしてQOpenGLShaderProgramクラスが用意されていて、シェーダーファイルの読み込み、コンパイル、リンクをQOpenGLShaderProgramクラスが行ってくれます。
addShaderFromSourceFile関数ではシェーダーの種類とファイルパスを指定すると、シェーダーファイルを読み込んでコンパイルを行います。addShaderFromSourceFile関数はbool型の関数で、コンパイルに失敗するとfalseを返します。このエラーチェックもとても便利です。
シェーダーをコンパイルした後はこれらをリンクし、バインドして準備完了です。

2. Vertex Buffer Object(VBO)のセットアップ

VBOとは頂点座標や色などの頂点の情報を格納するバッファのことです。詳しくはこちら
QtにはVBOを扱うためのクラスとしてQOpenGLBufferクラスが用意されています。 create関数で生成・バインドをした後に、usagePatternの設定と頂点データのセットを行います。
今回はvshader.glslで頂点座標しか設定していないので頂点座標用のVBOだけ生成します。 頂点カラーや法線なども設定する場合はそれぞれにVBOを作成します(これらをすべて一つのVBOにまとめることもできます)。
また、今回は頂点座標は動的に変わらないので、usagePatternはStaticDrawを指定しています。allocate関数でデータそのものと、データのバイト数を指定します。

3. Vertex Array Object(VAO)のセットアップ

VAOはVBOをまとめるためのものです。たくさんのVBOを使う場合に便利です。
QtでVAOを扱うクラスとしてQOpenGLVertexArrayObjectクラスが用意されていますので生成・バインドをします。

4. 頂点情報の書式を設定

VBOを生成し、VAOに関連付けた後に、VBOのデータの書式を設定します。vshader.glslではlocation=0として3次元ベクトルデータを定義していますのでその紐づけを行います。
glEnableVertexAttribArrayでロケーションIDを指定して有効にします。その後、glVertexAttribPointerで書式を設定します。

void QOpenGLFunctions::glVertexAttribPointer(GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *ptr)

  • index: ロケーションID。vshader.glslで頂点座標のロケーションIDは0を指定しているので、ここでは0を指定。
  • size: 1頂点あたりのデータ数。vshader.glslにvec3と定義しているのでデータ数は3つ。
  • type: データの型。今回はGLfloat型なのでGL_FLOATを指定。
  • normalized: データを正規化するか。今回はそのままの値を使うのでGL_FALSE。
  • stride: データ間のバイトオフセットを指定。0を指定するとデータが隙間なく入っていることを意味するので今回は0。
  • ptr: バインドされているデータの初期位置。

書式の設定が終わったらVAOとVBOをリリースします。
複数のVAOやVBOを使う場合、適切にリリースしないと意図しないところにデータが関連付けられるので不要になったらリリースします。

5. 描画

GLWidgetクラスのpaintGL関数で描画を行います。
描画時はシェーダープログラムとVAOをバインドして描画コール関数のglDrawArrays関数を呼ぶだけです。

以上です。

今回はQtに用意されているOpenGL用のクラスを使用しましたが、これらを使わずに通常のOpenGLの関数だけでも描画可能です。
Qtのクラスは便利なのですが、情報が少なすぎるのでGLSLに慣れるまでは大変苦労しました。ある程度GLSLの使い方に慣れるまでは、Qtの方のクラスは使わない方がいいのかなと思います。
GLSLに関してはここのサイトがとても詳しく解説されていたので参考にしました。
WebGLの解説サイトなので細かい部分は違いますが、OpenGLの概念を理解するのにはとても参考になりました。