Qt3D を使ってみよう

# はじめに

昨年の5月に [The Qt Company](https://www.qt.io) に入社した@shin1_okadaです。

[Qt Advent Calendar 2018]() の 11日目は、[Qt3D](http://doc.qt.io/qt-5/qt3d-index.html) を使い簡単なアプリケーションを作って解説してみたいと思います。 3D プログラミングというと、とっつきにくい印象ですが、この記事を見た後で「結構簡単？」と思って頂けたらとうれしいです。

[Qt3D](http://doc.qt.io/qt-5/qt3d-index.html) は、[C++](http://doc.qt.io/qt-5/qt3d-cpp.html) と [QML](http://doc.qt.io/qt-5/qt3d-qml.html) どっちの環境でも使うことができますが、本記事は [QML](http://doc.qt.io/qt-5/qt3d-qml.html) を使います。

# とりあえず 3D モデルを描画してみよう

突然ですが 3D のサンプルといえば、[ティーポット(参考：Wikipedia)](https://ja.wikipedia.org/wiki/Utah_teapot)ですよね。ということで、ティーポットを描画してみましょう。サンプルのデータは [ここから](http://inst.eecs.berkeley.edu/~cs184/sp09/assignments/teapot.obj) ダウンロードして、任意のフォルダーに保存してください。

それでは、サンプルを見ていきましょう。

最初に QtCreator で 新規にプロジェクト(Qt Quick Application - Empty)を作成してください。そして、テンプレートで用意された **main.qml** に、[QtQuick](http://doc.qt.io/qt-5/qtquick-index.html) と [Qt3D](http://doc.qt.io/qt-5/qt3d-index.html) を混在して使えるようにする [Scene3D](http://doc.qt.io/qt-5/qml-qtquick-scene3d-scene3d.html) を追加します。

``` main.qml

import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Scene3D 2.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Utah Teapot")

    //[0] QtQuick の環境で Qt3D を使えるようにします
    Scene3D {
        anchors.fill: parent
        focus: true
        aspects: ["input", "logic"]
        Stage {}
    }
}
```

次に、 [Qt3D](http://doc.qt.io/qt-5/qt3d-index.html) で描画する部分ですが、 最初にカメラの設定を行います。3次元空間にデータを描画しますので、どの位置から観測するのかを決める必要があります。

そして、3D モデルデータを描画するためには [Mesh](http://doc.qt.io/qt-5/qml-qt3d-render-mesh.html) を利用します。今回は、Wavefront OBJ 形式 (.obj) のフォーマットのデータを使いましたが、それ以外にも以下のフォーマットを使うことができます。

+ Stanford Triangle Format PLY
+ STL (STereoLithography)

また、 3D のシーンを扱うために Autodesk 社の FBX 形式のフォーマットも利用する事ができます。 

``` Stage.qml
import Qt3D.Core 2.0
import Qt3D.Input 2.0
import Qt3D.Render 2.0
import Qt3D.Extras 2.0

Entity {
    id: root

    //カメラの設定
    Camera {
        id: camera
        projectionType: CameraLens.PerspectiveProjection
        fieldOfView: 45
        aspectRatio: 16 / 9
        nearPlane: 0.1
        farPlane : 1000.0
        // 上をx, y, zのどれにするかを指定します
        upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
        // カメラの位置を指定します
        position: Qt.vector3d( 0.0, 5.0, 8.0 )
        //　3D データの中心点が底面であり、ちょっとだけ画面上側に描画されるので位置調整
        viewCenter: Qt.vector3d( 0.0, 1.0, 0.0 )
    }

    //カメラの移動方法を指定
    OrbitCameraController {
        camera: camera
    }

    components: [
        //描画のための設定
        RenderSettings {
            activeFrameGraph: ForwardRenderer {
                // 背景は白
                clearColor: Qt.rgba(1.0, 1.0, 1.0, 1.0)
                camera: camera
            }
        },
        //[4]　マウス等の入力のための設定
        InputSettings {}
    ]

    //3D モデルを読み込む
    Mesh {
        id: object
        source: "file:///<<格納したフォルダー>>/teapot.obj"
    }

    //3D モデルの表面の質感を指定（適当にグリーンにしてみました）
    PhongMaterial {
        id: material
        diffuse: "lightgreen"
        ambient: "green"
    }

    Entity {
        components: [ object, material ]
    }
}
```

これだけです。

そして動作結果です。グリーンのティーポットが描画されましたね。当然マウス操作もできますよ。

![sample1.png](https://qiita-image-store.s3.amazonaws.com/0/251604/eca12d9a-9ae4-e6ad-93a5-ab50a7cf0482.png)

質感は [Material](http://doc.qt.io/qt-5/qt3dextras-module.html#materials) を使って指定するのですが、Qt には、複数の [Material](http://doc.qt.io/qt-5/qt3dextras-module.html#materials) や多くのパラメータが用意されているので、お好みの色や質感に変更してみてください。より細かく好みの質感を目指そうとすると OpenGL のシェーダーランゲージ (GLSL) との戦いとなります。が、そこは別の機会にしたいと思います。

# 図形を描いてみよう

前のサンプルでは、別に用意した 3D モデルを描画しましたが、プログラムで生成した結果（座標）を可視化したいという事もありますよね。そのような時には、[GeometryRenderer](http://doc.qt.io/qt-5/qml-qt3d-render-geometryrenderer.html) と [Geometry](http://doc.qt.io/qt-5/qml-qt3d-render-geometry.html) を使います。（ [Mesh](http://doc.qt.io/qt-5/qml-qt3d-render-mesh.html) も実装の親クラスも [GeometryRenderer](http://doc.qt.io/qt-5/qml-qt3d-render-geometryrenderer.html) ですね。）

さて、次は、頂点座標を用意して正立方体を描いてみることにします。その正立方体の頂点座標を管理するために **Cube.qml** を追加します。

```Cube.qml
import Qt3D.Render 2.0

// カスタムの図形を描画する
GeometryRenderer {
    id: root
    // 描画の方法を選ぶ（今回は線で描画）
    primitiveType: GeometryRenderer.Lines

    // 頂点データのバッファ。今回は頂点データ(x, y, z)のみ。色は法線、インデックスなど混在させることもできます
    Buffer {
        id: vertexBuffer
        type: Buffer.VertexBuffer
        data: createGeometry()
    }

    geometry: Geometry{
        Attribute{
            name: defaultPositionAttributeName
            attributeType: Attribute.VertexAttribute
            vertexBaseType: Attribute.Float
            vertexSize: 3     // x, y, z
            byteOffset: 0
            byteStride: 3 * 4 // (x, y, z) * sizeof(float)
            count: 24         // 頂点座標の数
            buffer: vertexBuffer
        }
    }

    // 頂点座標のデータを用意します
    function createGeometry() {
        var array = [
                 // x,  y,  z
                    1,  1,  1,
                    1,  1, -1,
                   -1,  1,  1,
                   -1,  1, -1,

                    1, -1,  1,
                    1, -1, -1,
                   -1, -1,  1,
                   -1, -1, -1,

                    1,  1,  1,
                   -1,  1,  1,
                    1,  1, -1,
                   -1,  1, -1,

                    1, -1,  1,
                   -1, -1,  1,
                    1, -1, -1,
                   -1, -1, -1,

                    1,  1,  1,
                    1, -1,  1,
                    1,  1, -1,
                    1, -1, -1,

                   -1,  1,  1,
                   -1, -1,  1,
                   -1,  1, -1,
                   -1, -1, -1,
                ];

        var position = new Float32Array(array.length);
        for (var i=0; i<array.length; i++) {
            position[i] = array[i];
        }
        return position;
    }

}
```

描画するモデルの大きさが変わるので、カメラの位置を若干変更しています。そして、 [Mesh](http://doc.qt.io/qt-5/qml-qt3d-render-mesh.html) の箇所を先ほど作成した **Cube** に置き換えてください。

```Stage.qml
    Camera {
        ...
        position: Qt.vector3d( 0.0, 2.0, 5.0 )
        viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
    ...

    // Mesh から変更
    Cube {
        id: object
    }
```

そして実行結果です。正立方体が描画されていますね。当然、先ほどと同じようにマウス操作もできますよ。

![sample2.png](https://qiita-image-store.s3.amazonaws.com/0/251604/2e584950-e7ad-e5b1-c425-58719791dcca.png)


# 正立方体の辺を一本ずつ描画してみよう

2つめのサンプルでは、正立方体を一度に描画していましたが、辺を一本ずつ描画するように改修してみましょう。描画の範囲の指定には [スライダー](http://doc.qt.io/qt-5/qml-qtquick-controls2-slider.html) を使ってみましょう。　

```main.qml

// スライダーを使えるように QtQuick Controls 2 をインポートします
import QtQuick.Controls 2.0

    ...
    Scene3D {
        Stage {
            ...
            // スライダーの値を渡します。
            idx: slider.value
        }
    }

    // スライダーをウィンドウの下に配置します。
    Slider {
        id: slider
        width: 640
        from: 0
        to: 24
        stepSize: 1
        anchors.bottom: parent.bottom
    }
}
```

**Stage.qml** には、スライダーの値を **Cube.qml** に渡すための処理を追加します。

```Stage.qml

Entity {
    id: root

    // スライダーの値を受け取ります。
    property int idx:0

    ...
    Cube {
        id: object
        // スライダーの値を渡します。
        idx: root.idx
    }
    ...
```

ついでに、線の色も変えてみようと思います。頂点座標 (x, y, z) に色情報を載せて 頂点データ(x, y, z, R, G, B) とします。そして、拡張した頂点データを [Geometry](http://doc.qt.io/qt-5/qml-qt3d-render-geometry.html) が扱えるように、 色情報を扱う [Attribute](http://doc.qt.io/qt-5/qml-qt3d-render-attribute.html) を追加します。そして、各 [Attribute](http://doc.qt.io/qt-5/qml-qt3d-render-attribute.html) が、今どの範囲の頂点座標を指し示しているのかを指定するために、属性 [count](http://doc.qt.io/qt-5/qt3drender-qattribute.html#count-prop) にスライダーの値を設定します。（今回のサンプルでは、頂点座標、色情報を同じバッファで扱うようにしましたが、バッファを分けることもできますが、ここでは割愛します）

```Cube.qml

GeometryRenderer{
    id: root
    primitiveType: GeometryRenderer.Lines

    // スライダーの値を受け取ります。
    property int idx

    ...
    geometry: Geometry{
        // 頂点座標
        Attribute{
            ...
            byteStride: 6 * 4   // (x, y, z, R, G, B) * sizeof(float) 
            // スライダーの値で描画する位置を指定します
            count: root.idx
            ...
        }
        // 色
        Attribute{
            name: defaultColorAttributeName
            attributeType: Attribute.VertexAttribute
            vertexBaseType: Attribute.Float
            vertexSize: 3       // R, G, B
            byteOffset: 3 * 4   // 次のRGBデータまでのオフセット(x, y, zをスキップ)
            byteStride: 6 * 4   // (x, y, z, R, G, B) * sizeof(float)
            // スライダーの値で描画する位置を指定します
            count: root.idx
            buffer: vertexBuffer
        }
    }

     // [12] 頂点座標のデータを用意します。今回は色のデータも同時に指定
    function createGeometry() {
        var array = [
                 // x,  y,  z,   R,  G,  B
                    1,  1,  1,   1,  0,  0,
                    1,  1, -1,   1,  0,  0,
                    1,  1,  1,   1,  0,  0,
                   -1,  1,  1,   1,  0,  0,

                   -1,  1,  1,   1,  0,  0,
                   -1,  1, -1,   1,  0,  0,
                    1,  1, -1,   1,  0,  0,
                   -1,  1, -1,   1,  0,  0,

                    1, -1,  1,   0,  1,  0,
                    1, -1, -1,   0,  1,  0,
                    1, -1,  1,   0,  1,  0,
                   -1, -1,  1,   0,  1,  0,

                   -1, -1,  1,   0,  1,  0,
                   -1, -1, -1,   0,  1,  0,
                    1, -1, -1,   0,  1,  0,
                   -1, -1, -1,   0,  1,  0,

                    1,  1,  1,   0,  0,  1,
                    1, -1,  1,   0,  0,  1,
                   -1,  1,  1,   0,  0,  1,
                   -1, -1,  1,   0,  0,  1,

                   -1,  1, -1,   0,  0,  1,
                   -1, -1, -1,   0,  0,  1,
                    1,  1, -1,   0,  0,  1,
                    1, -1, -1,   0,  0,  1,
                ];

        var position = new Float32Array(array.length);
        for (var i=0; i<array.length; i++) {
            position[i] = array[i];
        }
        return position;
    }
}
```

実行結果です。スライダーを動かすと、正立方体の一辺づつ描画します。線の色も赤、青、緑となっていますね。

![sample3.png](https://qiita-image-store.s3.amazonaws.com/0/251604/3b18196b-7d2c-c423-31ab-809d3e59eed7.png)

# 終わりに

駆け足で3本のサンプルを解説しましたが、いかがだったでしょうか。（覚悟を決めて...）続きは薄い本でw

明日は @helicalgear の番ですね。お楽しみに！