Help us understand the problem. What is going on with this article?

Qt3D を使ってみよう

More than 1 year has passed since last update.

はじめに

昨年の5月に The Qt Company に入社した@shin1_okadaです。

Qt Advent Calendar 2018 の 11日目は、Qt3D を使い簡単なアプリケーションを作って解説してみたいと思います。 3D プログラミングというと、とっつきにくい印象ですが、この記事を見た後で「結構簡単?」と思って頂けたらとうれしいです。

Qt3D は、C++QML どっちの環境でも使うことができますが、本記事は QML を使います。

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

突然ですが 3D のサンプルといえば、ティーポット(参考:Wikipedia)ですよね。ということで、ティーポットを描画してみましょう。サンプルのデータは ここから ダウンロードして、任意のフォルダーに保存してください。

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

最初に QtCreator で 新規にプロジェクト(Qt Quick Application - Empty)を作成してください。そして、テンプレートで用意された main.qml に、QtQuickQt3D を混在して使えるようにする Scene3D を追加します。

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 で描画する部分ですが、 最初にカメラの設定を行います。3次元空間にデータを描画しますので、どの位置から観測するのかを決める必要があります。

そして、3D モデルデータを描画するためには Mesh を利用します。今回は、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

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

図形を描いてみよう

前のサンプルでは、別に用意した 3D モデルを描画しましたが、プログラムで生成した結果(座標)を可視化したいという事もありますよね。そのような時には、GeometryRendererGeometry を使います。( Mesh も実装の親クラスも GeometryRenderer ですね。)

さて、次は、頂点座標を用意して正立方体を描いてみることにします。その正立方体の頂点座標を管理するために 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 の箇所を先ほど作成した 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

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

2つめのサンプルでは、正立方体を一度に描画していましたが、辺を一本ずつ描画するように改修してみましょう。描画の範囲の指定には スライダー を使ってみましょう。 

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 が扱えるように、 色情報を扱う Attribute を追加します。そして、各 Attribute が、今どの範囲の頂点座標を指し示しているのかを指定するために、属性 count にスライダーの値を設定します。(今回のサンプルでは、頂点座標、色情報を同じバッファで扱うようにしましたが、バッファを分けることもできますが、ここでは割愛します)

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

終わりに

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away