はじめに
昨年の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 に、QtQuick と Qt3D を混在して使えるようにする Scene3D を追加します。
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 形式のフォーマットも利用する事ができます。
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 ]
}
}
これだけです。
そして動作結果です。グリーンのティーポットが描画されましたね。当然マウス操作もできますよ。
質感は Material を使って指定するのですが、Qt には、複数の Material や多くのパラメータが用意されているので、お好みの色や質感に変更してみてください。より細かく好みの質感を目指そうとすると OpenGL のシェーダーランゲージ (GLSL) との戦いとなります。が、そこは別の機会にしたいと思います。
図形を描いてみよう
前のサンプルでは、別に用意した 3D モデルを描画しましたが、プログラムで生成した結果(座標)を可視化したいという事もありますよね。そのような時には、GeometryRenderer と Geometry を使います。( Mesh も実装の親クラスも GeometryRenderer ですね。)
さて、次は、頂点座標を用意して正立方体を描いてみることにします。その正立方体の頂点座標を管理するために 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 に置き換えてください。
Camera {
...
position: Qt.vector3d( 0.0, 2.0, 5.0 )
viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
...
// Mesh から変更
Cube {
id: object
}
そして実行結果です。正立方体が描画されていますね。当然、先ほどと同じようにマウス操作もできますよ。
正立方体の辺を一本ずつ描画してみよう
2つめのサンプルでは、正立方体を一度に描画していましたが、辺を一本ずつ描画するように改修してみましょう。描画の範囲の指定には スライダー を使ってみましょう。
// スライダーを使えるように 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 に渡すための処理を追加します。
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 にスライダーの値を設定します。(今回のサンプルでは、頂点座標、色情報を同じバッファで扱うようにしましたが、バッファを分けることもできますが、ここでは割愛します)
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;
}
}
実行結果です。スライダーを動かすと、正立方体の一辺づつ描画します。線の色も赤、青、緑となっていますね。
終わりに
駆け足で3本のサンプルを解説しましたが、いかがだったでしょうか。(覚悟を決めて...)続きは薄い本でw
明日は @helicalgear の番ですね。お楽しみに!