[Qt][VTK]簡易な3Dモデルビューアーを作ってみる

# 0．はじめに
今回はQt(`Qt Widgets`)とVTKを使い、VTKファイル読み込みボタンとアプリ終了ボタンを伴った簡単な3Dモデルビューアーを作ってみます。
なお、単純に3Dモデルを表示するだけならVTKのみでもできますが、GUIとしての拡張を考えるとQtのWidgetクラスでビューアーを管理できた方がよいので、あえてQtを利用しています。
![usagi.png](https://qiita-image-store.s3.amazonaws.com/0/304390/6fc38f38-01de-45a5-363d-68c27c953aad.png)

# 1．VTKの可視化パイプライン
VTK形式の([vtkDataSet](https://vtk.org/doc/nightly/html/classvtkDataSet.html)の派生クラスを利用して表現される)3Dデータは、VTKが提供する可視化パイプラインに入力すると、単純に三次元形状を画面に描画するだけなら特に何も考えずに可視化することができます(たまに謎のエラーが生じたりしますが)。
![pipeline.png](https://qiita-image-store.s3.amazonaws.com/0/304390/e2cf178a-ca93-f98a-6ca5-8a0f791aa9e8.png)
※上図は[こちら](http://www.comp.tmu.ac.jp/shintani/japanese/openUniversity/node32.html)から拝借しました

また、さらに描画した3Dモデルに対してマウス操作等で手を加えたい場合は、[vtkRenderWindowInteractor](https://vtk.org/doc/nightly/html/classvtkRenderWindowInteractor.html)以下のクラスを利用します。

## 可視化パイプライン実装例(Qt未使用)
以下を実行すると、[vtkDataSetReader](https://vtk.org/doc/nightly/html/classvtkDataSetReader.html)で指定したvtkファイル([vtk legacy format](https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf))を前述のパイプラインにおけるソースとして取得し、window上に描画してくれます。

```sample_visualization_pipeline.cpp
#include <vtkSmartPointer.h>
#include <vtkDataSetReader.h>
#include <vtkUnstructuredGrid.h>
#include <vtkDataSetMapper.h>
#include <vtkActor.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkCamera.h>

// ↓がないとMapperのあたりでぶっこわれる。古いバージョンのVTKだといらない。
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);

int main(int, char *[])
{
  std::string fileName = "C:\\test\\test.vtk";

  auto reader = vtkSmartPointer<vtkDataSetReader>::New();
  reader->SetFileName(fileName.c_str());
  reader->Update();

  if (reader->GetOutput() == nullptr)
    return 0;

  auto mapper = vtkSmartPointer<vtkDataSetMapper>::New();
  mapper->SetInputData(reader->GetOutput());

  auto actor = vtkSmartPointer<vtkActor>::New();
  actor->SetMapper(mapper);

  auto renderer = vtkSmartPointer<vtkRenderer>::New();
  renderer->AddActor(actor);
  renderer->ResetCamera();
  renderer->GetActiveCamera()->Zoom(1.1);

  auto renderWin = vtkSmartPointer<vtkRenderWindow>::New();
  renderer->SetBackground(0.3, 0.6, 0.3);
  renderWin->SetSize(500, 500);
  renderWin->AddRenderer(renderer);

  auto interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
  interactor->SetRenderWindow(renderWin);

  interactor->Initialize();
  renderWin->Render();
  renderWin->SetWindowName(fileName.c_str());
  interactor->Start();

  return 0;
}
```

## 実行
![donut.png](https://qiita-image-store.s3.amazonaws.com/0/304390/4b8918be-638e-bc7a-df4e-c9337f8aa8f9.png)

# 2．QtでVTKビューアーを作る
Qtは基本的にWidgetというオブジェクトでGUI上のボタンやメニューなどのコンテンツを管理しているため、WidgetとしてVTKのビューアーを作れると他のコンテンツと連携がしやすくなります。

大変ありがたいことに、VTKライブラリにはQtでVTKを利用するためのWidgetクラスが提供されており、これを使うと簡単にQtで3Dモデルビューアーを作ることができます。
## Widgetsの親子関係
今回は一個のWindow内に3Dモデル用ビューアーとファイル入力ボタン、アプリ終了ボタンを備えたGUIを作るので、Widgetsの親子関係は以下に示す通りとなります。
![Widgetsの関係.png](https://qiita-image-store.s3.amazonaws.com/0/304390/45325103-793d-f5a1-a691-575a1dd02446.png)
ここで、図中のトップレベルウィジェットというものは親子関係の中で頂点となるWidgetのことを指します。
またQVTKViewerクラスは、[QVTKOpenGLWidget](https://vtk.org/doc/nightly/html/classQVTKOpenGLWidget.html)クラスの派生クラスとして新しく定義した自作クラスです。

## SignalとSlot
Widgets間の連携や、マウスクリックなどのイベント駆動で何かさせるといった事を簡単に実現できるQt独自の機能です。

今回は、以下の機能を実現してみます。

|Signal       |Slot                         |
|----------------|-------------------------------|
|ファイル入力ボタンを押下 |VTK Legacy formatのファイルを読み込むのためのダイアログを表示する |
|アプリ終了ボタンを押下 |アプリケーションを終了させる |

## 3Dモデルビューアー実装例
```sample_3dmodel_viewer.cpp
#include "QVTKViewer.h" 

// C++ std library
#include <iostream>

// Qt library
#include <QApplication>
#include <QtWidgets>

using namespace std;

int main(int argv, char **args)
{
  QApplication app(argv, args);

  // widgetを作成
  QVTKViewer *qVTKViewer = new QVTKViewer();
  QPushButton *readButton = new QPushButton("Read VTK Legacy");
  QPushButton *quitButton = new QPushButton("Quit");

  // signal & slot
  QObject::connect(readButton, SIGNAL(clicked()), qVTKViewer, SLOT(readVTKFile()));
  QObject::connect(quitButton, SIGNAL(clicked()), qApp, SLOT(quit())); 

  QVBoxLayout *layout = new QVBoxLayout();
  // addWidgetメソッドを使った順に上から配置される。
  layout->addWidget(qVTKViewer);
  layout->addWidget(readButton);
  layout->addWidget(quitButton);

  QWidget widget; // トップレベルウィジェット(一番てっぺんのやつ)
  widget.setLayout(layout); // layoutで追加したwidgetsを子としてセットする
  widget.setWindowTitle("Simple VTK Viewer");
  widget.show();

  return app.exec();  // イベント待ち無限ループ
}
```

``` QVTKViewer.h
#include <QtWidgets>
#include <QVTKOpenGLWidget.h>

class vtkRenderer;
class vtkGenericOpenGLRenderWindow; 
class vtkDataSetMapper;
class vtkActor;
class vtkUnstructuredGrid;

class QVTKViewer : public QVTKOpenGLWidget
{
  Q_OBJECT

public:
  QVTKViewer(QVTKOpenGLWidget *parent = nullptr);
  ~QVTKViewer();

  // override
  virtual QSize minimumSizeHint() const override; // ビューアーの最小サイズを決めとく

public slots:
  const bool readVTKFile();

private:
  // original
  void updateViewer();

  vtkUnstructuredGrid *unsGrid;
  std::string fileName;
};
```

``` QVTKViewer.cpp
#include "QVTKViewer.h"

// C++ std library
#include <string>

// qt library
#include <QString>
#include <QDebug>

// vtk library
#include <vtkRenderer.h>
#include <vtkGenericOpenGLRenderWindow.h> // vtkRenderWindowはQVTKOpenGLWidgetでサポートしていない。
//#include <QVTKInteractor.h> // interactorの処理はQVTKOpenGLWidget内部で行っている。
#include <vtkDataSetMapper.h>
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkUnstructuredGrid.h>
#include <vtkAppendFilter.h>  // 読み取った3DモデルをvtkUnstructuredGridに変換するために利用する。
#include <vtkDataSetReader.h>
#include <vtkSmartPointer.h>

// ↓がないとMapperのあたりでぶっこわれる。古いバージョンのVTKだといらない。
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);

#include <vtkDataSetWriter.h>
#include <vtkRendererCollection.h>

QVTKViewer::QVTKViewer(QVTKOpenGLWidget *parent) : QVTKOpenGLWidget(parent)
{
  this->unsGrid = vtkUnstructuredGrid::New();
  this->fileName = "";
  this->updateViewer();
}

QVTKViewer::~QVTKViewer()
{
  if (this->unsGrid)
    this->unsGrid->Delete();
}

QSize QVTKViewer::minimumSizeHint() const
{
  return QSize(500, 500);
}

void QVTKViewer::updateViewer()
{
  auto mapper = vtkSmartPointer<vtkDataSetMapper>::New();
  mapper->SetInputData(this->unsGrid);

  auto actor = vtkSmartPointer<vtkActor>::New();
  actor->SetMapper(mapper);

  auto renderer = vtkSmartPointer<vtkRenderer>::New();
  renderer->AddActor(actor);
  renderer->ResetCamera();
  renderer->GetActiveCamera()->Zoom(1.1);
  renderer->SetBackground(0.3, 0.6, 0.3);

  auto renderWin = vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New();
  renderWin->AddRenderer(renderer);
  this->SetRenderWindow(renderWin);
}

// 参考：https://qt.misfrog.com/posts/%E3%83%A1%E3%83%A2%E3%81%AE%E4%BF%9D%E5%AD%98%E3%81%A8%E3%83%AD%E3%83%BC%E3%83%89
const bool QVTKViewer::readVTKFile()
{
  QString tempFileName = QFileDialog::getOpenFileName(this, tr("Open File"), "",
    tr("VTK Legacy Files (*.vtk)"));
  this->fileName = tempFileName.toStdString();

  if (this->fileName == "")
    return false;

  auto reader = vtkSmartPointer<vtkDataSetReader>::New();
  reader->SetFileName(this->fileName.c_str());
  reader->Update();

  if (reader->GetOutput() == nullptr)
    return false;

  // メンバunsGridに渡しやすくするために、とりあえずvtkUnstructuredGridに変える。
  // 読み取り後、vtkPolyDataままだったりすると、
  // プログラムが壊れたり3Dモデルが正しく描画されないことがある。
  auto append = vtkSmartPointer<vtkAppendFilter>::New();
  append->SetInputConnection(reader->GetOutputPort());
  append->Update();

  // メモリを一度解放する。
  // これをやらないと新しく読み込んだ時にゴミが浮いたりする。
  if (this->unsGrid)
  {
    this->unsGrid->Delete();
    this->unsGrid = vtkUnstructuredGrid::New();
  }

  this->unsGrid->DeepCopy(reader->GetOutput());
  this->updateViewer();

  return true;
}
```

※コード中にて[vtkRenderWindow](https://vtk.org/doc/nightly/html/classvtkRenderWindow.html)ではなく[vtkGenericOpenGLRenderWindow](https://vtk.org/doc/nightly/html/classvtkGenericOpenGLRenderWindow.html)を使っているのは、[QVTKOpenGLWidget](https://vtk.org/doc/nightly/html/classQVTKOpenGLWidget.html)が[vtkGenericOpenGLRenderWindow](https://vtk.org/doc/nightly/html/classvtkGenericOpenGLRenderWindow.html)を利用することを前提に設計されているためです。

## 実行
実行後、`[Read VTK Legacy]`から拡張子(*.vtk)のVTKレガシー形式ファイル(ASCII/Binary)を読み込むと、ビューアーに3Dモデルが表示されます。
![demo2.gif](https://qiita-image-store.s3.amazonaws.com/0/304390/8b753be1-6e2d-08c3-aa6f-347d38809fe8.gif)

# 3．おわりに
Qt＋VTKで3Dモデルビューアーを作ってみました。
今回作ったものを拡張して、例えばビューアー上のモデルにおける特定の要素のみを抽出するなどの操作ができたり、何らかの加工した3Dモデルを出力するなどができれば尚面白そうですね。