Qt/C++ による GUI ソフトウェア作成の練習 5:Qt3D による描画

Debian の今メジャーバージョンでは,Qt5 から Qt6 へ移行に伴い,OpenGL による描画で私には解決できない問題が発生しました.
そこで今バージョンでは,公開している GUI ソフトウェアは Qt5 で開発することにしておいて,
次期バージョンのための描画方法として,Qt3D の練習をすることにしました.
このページは,Qt のサイトで公開されているサンプルコードを参考にして,Qt3D ではじめて描画の練習をした際の記録をまとめたものです.
そのため,記述内容があまり整理されていません.もう少し練習しておいて,Debian の次期バージョンへの移行期に書き直す予定です.

Qt3DWindow という描画用のウィンドウ内の QEntity というオブジェクトを操作することで描画がおこなえます.
例えば,描画される物体を増やしたい場合は,QEntity を追加します,
QEntity は,形状や位置などを設定するための QComponent クラスのインスタンスを複数持つことができます.

Before : 4:イベントの取得 Next : 6:ピッキング

インフォメーション

インストールした Debian パッケージ

パッケージ名(バージョン)
g++(12.2.0)GNU C++ コンパイラ
make (4.3)コンパイルを制御するユーティリティ
qt6-base-dev(6.4.2)Qt6 アプリケーションのビルドに使用されるヘッダ開発ファイル
qt6-3d-dev(6.4.2)Qt3D ライブラリを使用した Qt6 アプリケーションのビルドに使用されるヘッダ開発ファイル

ドキュメント

Qt3D に関しては,リファレンスマニュアルとサンプルコードがあります.


Qt3D のまとめノート

このセクションは,サンプルコードを書いて描画できた段階で,その先に進むために整理してみたものです.
リファレンスドキュメントの抜粋と,サンプルコードをいくつか書いてみた段階での理解が混在しています.

class Qt3DCore::QEntity および class Qt3DCore::QComponent

QEntity は,描画されるべき実体を記述するためのクラスです.
継承関係は,
Qt3DCore::QEntity → Qt3DCore::QNode → QObject
となっています.

QComponent は,QEntity の属性(形状,色,位置など)を設定するためのクラスです.
継承関係は,
Qt3DCore::QComponent → Qt3DCore::QNode → QObject
となっています.

このページで作成したコードでは,
Sphere = new Qt3DCore::QEntity(RootEntity);
と,親ノードをコンストラクタ引数として新規 QEntity を追加しています.

このページで作成したコードでは,
void addComponent(Qt3DCore::QComponent *comp)
という関数使って QEntity の属性を変更しています.

Qt3DExtras::Qt3DWindow

利用しているバージョンのリファレンスマニュアルには,Qt3DWindow のページが見当たりません.

QEntity や QComponent を描画するためのウィンドウです.
Qt3DWindow 内の QEntity や QComponent を操作することで描画シーンを作成します.

Qt3DWindow は内部に Qt3DRender::QCamera というカメラを持っいます.このページで作成したコードでは,
Qt3DRender::QCamera *camera() const
という関数でカメラへのポインタを取得しています.

Qt3DWindow は内部に QEntity の根(ルートエンティティ)を持っています.
下で作成したコードでは,
void setRootEntity(Qt3DCore::QEntity *root)
という関数を使い,新規に作成した QEntity を Qt3DWindow 内の QEntity の根としています.


ソースコード

レイアウトで作成したコードを変更して雛形としました.
このページで追加した箇所は赤色で示しています.

>

draw_obj.h


#include <Qt3DCore>
#include <Qt3DExtras>
#include <Qt3DRender>

class Draw_Obj : public QObject {
 private:
  Qt3DExtras::Qt3DWindow *DrawWindow;
  Qt3DCore::QEntity *RootEntity;  //これを Qr3DWindow のルートエンティティとする
  Qt3DCore::QEntity *Sphere;  //これから作成したインスタンスを RootEntity に追加する

 public:
  Draw_Obj();
  ~Draw_Obj() {}

  Qt3DExtras::Qt3DWindow *get_draw_window()
        { return DrawWindow; }

  void init();
  void add_obj();  //この関数内で *Sphere から球を作成し,RootEntity に追加する
};

draw_obj.cc

ルートエンティティに追加したエンティティーは,物体と光源です.カメラは,Qt3DExtras::Qt3DWindow() から取得しています.
これらのそれぞれについて,メンバ関数で属性を設定しています.


#include "draw_obj.h"

Draw_Obj::Draw_Obj()
 : DrawWindow(nullptr),
   RootEntity(nullptr),
   Sphere(nullptr)
{
 //*RootEntity をインスタンス化し,Qt3DWindow のルートエンティティとする
 RootEntity = new Qt3DCore::QEntity();
 DrawWindow = new Qt3DExtras::Qt3DWindow();
 DrawWindow->setRootEntity(RootEntity);
}

//この関数では,すでに描画されていれば,すべて解放し再構築している.何かうまい方法があるのかもしれない
void Draw_Obj::init() {
 if(Sphere) {
  delete Sphere;
  Sphere = nullptr;
 }

 if(RootEntity) {
  delete RootEntity;
  RootEntity = new Qt3DCore::QEntity();
 }

 if(DrawWindow) {
  delete DrawWindow;
  DrawWindow = new Qt3DExtras::Qt3DWindow();
 }

 DrawWindow->setRootEntity(RootEntity);

 //カメラの取得と設定
 Qt3DRender::QCamera *CameraEntity
        = DrawWindow->camera();
 CameraEntity->lens()->setPerspectiveProjection(
        45.0f, 1.0f, 10.f, 30.0f);  //引数は,float fieldOfView, float aspectRatio, float nearPlane, float farPlane
 CameraEntity->setPosition(QVector3D(0, 0, 20.0f));
 CameraEntity->setUpVector(QVector3D(0, 1, 0));
 CameraEntity->setViewCenter(QVector3D(0, 0, 0));

 //光源を作成
 Qt3DCore::QEntity *light_entity
        = new Qt3DCore::QEntity(RootEntity);
 //光源の作成と色,強度
 Qt3DRender::QPointLight *light
        = new Qt3DRender::QPointLight(light_entity);
 light->setColor("white");
 light->setIntensity(1);
 light_entity->addComponent(light);
 //光源の位置.デフォルトは原点
 Qt3DCore::QTransform *light_pos
        = new Qt3DCore::QTransform(light_entity);
 light_pos->setTranslation(
        CameraEntity->position());
 light_entity->addComponent(light_pos);
}

void Draw_Obj::add_obj() {
 if(Sphere) return;

 //球体となるべきエンティティの作成
 Sphere = new Qt3DCore::QEntity(RootEntity);

 //球体の形状.Rings と Slices は,描画時の分割の設定.Slices が経度 Rings が緯度,かな?
 Qt3DExtras::QSphereMesh *mesh
        = new Qt3DExtras::QSphereMesh();
 mesh->setRings(20);
 mesh->setSlices(20);
 mesh->setRadius(2.5);
 Sphere->addComponent(mesh);

 //球体のマテリアル
 Qt3DExtras::QPhongMaterial *material
        = new Qt3DExtras::QPhongMaterial();
 material->setDiffuse(QColor(QRgb(0xFF0000)));
 Sphere->addComponent(material);

 //球体の位置も QComponent で設定
 Qt3DCore::QTransform *transform
        = new Qt3DCore::QTransform();
 transform->setTranslation(
        QVector3D(1.0f, 1.0f, 1.0f));
 Sphere->addComponent(transform);
}

main_window.h


#include <QMainWindow>
#include <QMenuBar>
#include <QHBoxLayout>
#include "draw_obj.h"

class Main_Window : public QMainWindow {
 private:
  Q_OBJECT

  QMenuBar *menu_bar;
  QMenu *file_menu;
  QAction *action_quit;

  QHBoxLayout *CentralLayout;
  QWidget *Container;
  Draw_Obj DrawObj;
  Qt3DExtras::Qt3DWindow *DrawWindow;

  //描画とクリアをメニューコマンドとするので,スロット関数とする
 private slots:
  void slot_draw();
  void slot_clear() { DrawObj.init(); }

 public:
  Main_Window(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
};

main_window.cc


#include "main_window.h"

Main_Window::Main_Window(QWidget *parent, Qt::WindowFlags f)
 : QMainWindow(parent, f),
   menu_bar(menuBar()),
   CentralLayout(nullptr),
   Container(nullptr),
   DrawWindow(nullptr)
{
 action_quit = new QAction("終了(&Q)", this);
 connect(action_quit, SIGNAL(triggered()), this, SLOT(close()));

 file_menu = new QMenu("ファイル(&F)", this);
 file_menu->addAction(action_quit);
 menu_bar->addMenu(file_menu);

 //メニューコマンドを実行したときに呼び出される関数を設定
 QAction *action_draw
        = new QAction(tr("描画(&D)"), this);
 connect(action_draw, SIGNAL(triggered()), this,
        SLOT(slot_draw()));

 QAction *action_clear
        = new QAction(tr("クリア(&C)"), this);
 connect(action_clear, SIGNAL(triggered()), this,
        SLOT(slot_clear()));

 //View メニューの追加
 QMenu *view_menu = new QMenu(tr("表示(&V)"), this);
 view_menu->addAction(action_draw);
 view_menu->addAction(action_clear);
 menu_bar->addMenu(view_menu);

 QWidget *central_widget = new QWidget(this);
 setCentralWidget(central_widget);
 CentralLayout = new QHBoxLayout(central_widget);

 DrawWindow = DrawObj.get_draw_window();
 Container = createWindowContainer(DrawWindow);
 CentralLayout->addWidget(Container);

 QSize screenSize = screen()->size();
 setMaximumSize(screenSize);

 setMinimumWidth(600);
 setMinimumHeight(600);
}

//この関数でも,すでに描画されていれば,すべて解放し再構築している.何かうまい方法があるのかもしれない
void Main_Window::slot_draw() {
 DrawObj.init();

 CentralLayout->removeWidget(Container);
 delete Container;
 DrawWindow = DrawObj.get_draw_window();
 Container = createWindowContainer(DrawWindow);
 CentralLayout->addWidget(Container);

 DrawObj.add_obj();
}

main.cc


#include <QMainWindow>
#include <QApplication>
#include "main_window.h"

int main(int argc, char *argv[]) {
 QApplication app(argc, argv);

 Main_Window window(nullptr, Qt::Window);

 window.show();

 return app.exec();
}

tmp.pro

qmake のプロジェクトファイルです.名前を変更しても問題ありません.


TEMPLATE = app
TARGET = window
QT += widgets
INCLUDEPATH += .
HEADERS += main_window.h draw_obj.h
SOURCES += main.cc main_window.cc draw_obj.cc

QT += 3dcore
QT += 3dextras
#QT += 3drender
#QT += 3dinput

ビルドと実行

Qt3D で球を描画

まず Makefile を作成.
~/tmp$ qmake6

次いでビルド.実行ファイル window が生成します.
~/tmp$ make

ビルドに成功したら実行.
~/tmp$ ./window &
とするとウィンドウが表示されます.
メニューで [表示(V)]-[描画(D)] とすると球が描画されました.
球が描画された状態で [表示(V)]-[クリア(C)] とすると球が消えました.
どうやら成功したようです.


参考書の検索