キューブ OpenGL ES 2.0の例

テクスチャ付き3D立方体をユーザー入力で手動回転させる方法を示します。

Cube OpenGL ES 2.0のサンプルは、OpenGL ES 2.0とQtを使用して、テクスチャ付きの3D立方体をユーザー入力で手動回転させる方法を示しています。ポリゴン形状を効率的に扱う方法と、プログラマブル・グラフィックス・パイプライン用のシンプルな頂点シェーダとフラグメント・シェーダの書き方を示しています。さらに、3Dオブジェクトの向きを表現するためにクォータニオンを使用する方法を示しています。

このサンプルはOpenGL ES 2.0用に書かれていますが、デスクトップOpenGLでも動作します。なぜなら、このサンプルは十分にシンプルで、ほとんどの部分においてデスクトップOpenGL APIも同じだからです。OpenGLサポートなしでもコンパイルできますが、その場合はOpenGLサポートが必要であることを示すラベルが表示されるだけです。

Screenshot of the Cube example running on N900

この例は2つのクラスから構成されています:

  • MainWidget extends 、OpenGL ES 2.0の初期化と描画、マウスとタイマーのイベント処理を含んでいます。QOpenGLWidget
  • GeometryEngine はポリゴン・ジオメトリを扱います。ポリゴン・ジオメトリを頂点バッファ・オブジェクトに転送し、頂点バッファ・オブジェクトからジオメトリを描画します。

まず、MainWidget でOpenGL ES 2.0を初期化することから始めます。

OpenGL ES 2.0の初期化

OpenGL ES 2.0はもう固定的なグラフィックス・パイプラインをサポートしていないので、自分自身で実装する必要があります。そのため、グラフィックス・パイプラインは非常に柔軟になっていますが、同時に、最も単純な例でさえ実行するためにグラフィックス・パイプラインを実装しなければならないため、難しくなっています。また、どのようなパイプラインがアプリケーションに必要かをユーザーが決めることができるため、グラフィックスパイプラインをより効率的にすることもできます。

まず、頂点シェーダーを実装する必要があります。頂点データとモデル・ビュー・プロジェクション・マトリックス(MVP)をパラメータとして取得します。MVP行列を使って頂点位置をスクリーン空間に変換し、テクスチャ座標をフラグメントシェーダに渡します。テクスチャ座標はポリゴンの面で自動的に補間されます。

void main()
{
    // Calculate vertex position in screen space
    gl_Position = mvp_matrix * a_position;

    // Pass texture coordinate to fragment shader
    // Value will be automatically interpolated to fragments inside polygon faces
    v_texcoord = a_texcoord;
}

その後、グラフィックスパイプラインの 2 番目の部分であるフラグメントシェーダを実装する必要があります。このエクササイズでは、テクスチャリングを処理するフラグメントシェーダを実装する必要があります。補間されたテクスチャ座標をパラメータとして受け取り、与えられたテクスチャからフラグメントの色を調べます。

void main()
{
    // Set fragment color from texture
    gl_FragColor = texture2D(texture, v_texcoord);
}

QOpenGLShaderProgram を使って、シェーダコードをコンパイル、リンク、グラフィックパイプラインにバインドすることができます。このコードでは、Qt Resource ファイルを使用してシェーダーのソースコードにアクセスします。

void MainWidget::initShaders()
{
    // Compile vertex shader
    if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl"))
        close();

    // Compile fragment shader
    if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fshader.glsl"))
        close();

    // Link shader pipeline
    if (!program.link())
        close();

    // Bind shader pipeline for use
    if (!program.bind())
        close();
}

次のコードは、深度バッファリングとバックフェイスカリングを有効にします。

    // Enable depth buffer
    glEnable(GL_DEPTH_TEST);

    // Enable back face culling
    glEnable(GL_CULL_FACE);

Qt Resource ファイルからテクスチャをロードする

QOpenGLWidget インターフェースは、QImage から OpenGL テクスチャメモリにテクスチャをロードするメソッドを実装しています。OpenGLテクスチャ・ユニットの指定とテクスチャ・フィルタリング・オプションの設定には、OpenGLが提供する関数を使用する必要があります。

void MainWidget::initTextures()
{
    // Load cube.png image
    texture = new QOpenGLTexture(QImage(":/cube.png").mirrored());

    // Set nearest filtering mode for texture minification
    texture->setMinificationFilter(QOpenGLTexture::Nearest);

    // Set bilinear filtering mode for texture magnification
    texture->setMagnificationFilter(QOpenGLTexture::Linear);

    // Wrap texture coordinates by repeating
    // f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2)
    texture->setWrapMode(QOpenGLTexture::Repeat);
}

立方体のジオメトリ

OpenGLでポリゴンをレンダリングする方法はたくさんありますが、最も効率的な方法は、三角形のストリップ・プリミティブだけを使用し、グラフィックス・ハードウェア・メモリから頂点をレンダリングすることです。OpenGLには、このメモリ領域にバッファオブジェクトを作成し、頂点データをこれらのバッファに転送するメカニズムがあります。OpenGLの用語では、これらを頂点バッファ・オブジェクト(Vertex Buffer Objects:VBO)と呼びます。

Cube faces and vertices

このようにして、立方体の面は三角形に分解されます。頂点は、三角形ストリップを使用して頂点の順序を正しくするために、このように順序付けられます。OpenGLは、頂点の順序に基づいて三角形の前面と背面を決定します。デフォルトでは、OpenGLは前面に対して反時計回りの順序を使用します。この情報は、三角形の裏面をレンダリングしないことによってレンダリング性能を向上させる裏面カリングで使用されます。こうすることで、グラフィックス・パイプラインは、画面に向いていない三角形の側面のレンダリングを省略することができます。

頂点バッファオブジェクトの作成と、頂点バッファオブジェクトへのデータ転送は、QOpenGLBuffer を使ってとても簡単にできます。MainWidgetは、GeometryEngineインスタンスがOpenGLコンテキスト・カレントで生成・破棄されるようにします。こうすることで、コンストラクタでOpenGLリソースを使用し、デストラクタで適切なクリーンアップを行うことができます。

GeometryEngine::GeometryEngine()
    : indexBuf(QOpenGLBuffer::IndexBuffer)
{
    initializeOpenGLFunctions();

    // Generate 2 VBOs
    arrayBuf.create();
    indexBuf.create();

    // Initializes cube geometry and transfers it to VBOs
    initCubeGeometry();
}

GeometryEngine::~GeometryEngine()
{
    arrayBuf.destroy();
    indexBuf.destroy();
}
    // Transfer vertex data to VBO 0
    arrayBuf.bind();
    arrayBuf.allocate(vertices, 24 * sizeof(VertexData));

    // Transfer index data to VBO 1
    indexBuf.bind();
    indexBuf.allocate(indices, 34 * sizeof(GLushort));

VBOからプリミティブを描画し、プログラマブル・グラフィックス・パイプラインに頂点データの場所を指示するには、いくつかのステップが必要です。まず、使用するVBOをバインドする必要があります。その後、シェーダープログラムの属性名をバインドし、バインドされたVBOにどのようなデータがあるかを設定します。最後に、もう1つのVBOのインデックスを使って三角形のストリッププリミティブを描画します。

void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program)
{
    // Tell OpenGL which VBOs to use
    arrayBuf.bind();
    indexBuf.bind();

    // Offset for position
    quintptr offset = 0;

    // Tell OpenGL programmable pipeline how to locate vertex position data
    int vertexLocation = program->attributeLocation("a_position");
    program->enableAttributeArray(vertexLocation);
    program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));

    // Offset for texture coordinate
    offset += sizeof(QVector3D);

    // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data
    int texcoordLocation = program->attributeLocation("a_texcoord");
    program->enableAttributeArray(texcoordLocation);
    program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));

    // Draw cube geometry using indices from VBO 1
    glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr);
}

遠近投影

QMatrix4x4 ヘルパーメソッドを使うと、遠近投影行列を簡単に計算できます。この行列を使って頂点をスクリーン空間に投影します。

void MainWidget::resizeGL(int w, int h)
{
    // Calculate aspect ratio
    qreal aspect = qreal(w) / qreal(h ? h : 1);

    // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees
    const qreal zNear = 3.0, zFar = 7.0, fov = 45.0;

    // Reset projection
    projection.setToIdentity();

    // Set perspective projection
    projection.perspective(fov, aspect, zNear, zFar);
}

3Dオブジェクトの向き

クオータニオンは3Dオブジェクトの向きを表す便利な方法です。クオータニオンは非常に複雑な数学を伴いますが、幸いなことに、クオータニオンの背後にある必要な数学はすべてQQuaternion に実装されています。そのため、立方体の向きをクォータニオンで保存することができ、与えられた軸を中心に立方体を回転させるのは非常に簡単です。

次のコードは、マウスイベントに基づいて回転軸と角速度を計算します。

void MainWidget::mousePressEvent(QMouseEvent *e)
{
    // Save mouse press position
    mousePressPosition = QVector2D(e->position());
}

void MainWidget::mouseReleaseEvent(QMouseEvent *e)
{
    // Mouse release position - mouse press position
    QVector2D diff = QVector2D(e->position()) - mousePressPosition;

    // Rotation axis is perpendicular to the mouse position difference
    // vector
    QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();

    // Accelerate angular speed relative to the length of the mouse sweep
    qreal acc = diff.length() / 100.0;

    // Calculate new rotation axis as weighted sum
    rotationAxis = (rotationAxis * angularSpeed + n * acc).normalized();

    // Increase angular speed
    angularSpeed += acc;
}

QBasicTimer は、シーンのアニメートと立方体の向きの更新に使用されます。回転は、クォータニオンを乗算するだけで連結できます。

void MainWidget::timerEvent(QTimerEvent *)
{
    // Decrease angular speed (friction)
    angularSpeed *= 0.99;

    // Stop rotation when speed goes below threshold
    if (angularSpeed < 0.01) {
        angularSpeed = 0.0;
    } else {
        // Update rotation
        rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation;

        // Request an update
        update();
    }
}

モデルビュー行列は、クォータニオンを使用して、ワールドをZ軸で移動することによって計算されます。この行列と射影行列を掛け合わせることで、シェーダプログラム用のMVP行列が得られます。

    // Calculate model view transformation
    QMatrix4x4 matrix;
    matrix.translate(0.0, 0.0, -5.0);
    matrix.rotate(rotation);

    // Set modelview-projection matrix
    program.setUniformValue("mvp_matrix", projection * matrix);

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。