OpenGLの概要:3Dテキストレンダリングチュートリアル

公開: 2022-03-11

DirectXやOpenGLなどのツールが利用できるようになった現在、3D要素をレンダリングするデスクトップアプリケーションを作成することはそれほど難しくありません。 ただし、多くのテクノロジと同様に、開発者がこのニッチに参入することを困難にする障害が存在する場合があります。 時間の経過とともに、DirectXとOpenGLの間の競争により、これらのテクノロジは開発者にとってよりアクセスしやすくなり、ドキュメントが改善され、熟練したDirectXまたはOpenGL開発者になるためのプロセスが容易になりました。

Microsoftによって導入および保守されているDirectXは、Windowsプラットフォームに固有のテクノロジです。 一方、OpenGLは、クロノスグループによって仕様が維持されている3Dグラフィックス分野向けのクロスプラットフォームAPIです。

openglの紹介

このOpenGLの紹介では、3Dテキストモデルをレンダリングするための非常に単純なアプリケーションを作成する方法を説明します。 Qt / Qt Creatorを使用してUIを実装し、このアプリケーションを複数のプラットフォームで簡単にコンパイルして実行できるようにします。 この記事用に作成されたプロトタイプのソースコードは、GitHubで入手できます。

このシンプルなアプリケーションの目的は、3Dモデルを生成し、それらをシンプルな形式のファイルに保存し、画面上で開いてレンダリングすることです。 レンダリングされたシーンの3Dモデルは回転可能でズーム可能であり、奥行きと寸法をより正確に把握できます。

前提条件

始める前に、このプロジェクトに役立ついくつかのツールを使用して開発環境を準備する必要があります。 最初に必要なのは、Qtフレームワークと関連するユーティリティです。これらはwww.qt.ioからダウンロードできます。 また、オペレーティングシステムの標準パッケージマネージャーからも利用できる場合があります。 その場合は、最初に試してみることをお勧めします。 この記事では、Qtフレームワークにある程度精通している必要があります。 ただし、フレームワークに精通していない場合でも、プロトタイプはフレームワークのかなり些細な機能に依存しているため、従うことに落胆しないでください。

WindowsでMicrosoftVisualStudio2013を使用することもできます。 その場合は、VisualStudio用の適切なQtアドインを使用していることを確認してください。

この時点で、GitHubからリポジトリのクローンを作成し、この記事を読みながらそれに従うことをお勧めします。

OpenGLの概要

まず、単一のドキュメントウィジェットを使用して単純なQtアプリケーションプロジェクトを作成します。 これは必要最低限​​のウィジェットであるため、コンパイルして実行しても、有用なものは何も生成されません。 Qt Designerを使用して、「新規…」、「開く…」、「閉じる」、「終了」の4つの項目を含む「ファイル」メニューを追加します。 これらのメニュー項目を対応するアクションにバインドするコードは、リポジトリーにあります。

「新規…」をクリックすると、次のようなダイアログがポップアップ表示されます。

openglポップアップ

ここで、ユーザーはテキストを入力し、フォントを選択し、結果のモデルの高さを微調整して、3Dモデルを生成することができます。 「作成」をクリックするとモデルが保存され、ユーザーが左下隅から適切なオプションを選択した場合はモデルも開く必要があります。 お分かりのように、ここでの目標は、ユーザーが入力したテキストを3Dモデルに変換し、それをディスプレイにレンダリングすることです。

プロジェクトは単純な構造になり、コンポーネントは少数のC++ファイルとヘッダーファイルに分割されます。

C++およびヘッダーファイル

createcharmodeldlg.h / cpp

ファイルにはQDialog派生オブジェクトが含まれています。 これは、ユーザーがテキストを入力し、フォントを選択し、結果をファイルに保存するか、3Dで表示するかを選択できるダイアログウィジェットを実装します。

gl_widget.h/cpp

QOpenGLWidget派生オブジェクトの実装が含まれています。 このウィジェットは、3Dシーンをレンダリングするために使用されます。

mainwindow.h / cpp

メインアプリケーションウィジェットの実装が含まれています。 これらのファイルは、Qt Creatorウィザードによって作成されたため、変更されていません。

main.cpp

main(…)関数が含まれています。この関数は、メインアプリケーションウィジェットを作成して画面に表示します。

model2d_processing.h / cpp

2Dシーンの作成機能が含まれています。

model3d.h/cpp

3Dモデルオブジェクトを格納し、操作(保存、ロードなど)を可能にする構造が含まれています。

model_creator.h / cpp

3Dシーンモデルオブジェクトの作成を可能にするクラスの実装が含まれています。

OpenGLの実装

簡潔にするために、Qt Designerを使用したユーザーインターフェイスの実装の明らかな詳細と、インタラクティブ要素の動作を定義するコードはスキップします。 このプロトタイプアプリケーションには確かにいくつかの興味深い側面があります。これらは重要であるだけでなく、カバーしたい3Dモデルのエンコーディングとレンダリングにも関連しています。 たとえば、このプロトタイプでテキストを3Dモデルに変換する最初のステップでは、テキストを2Dモノクロ画像に変換します。 この画像が生成されると、画像のどのピクセルがテキストを形成し、どのピクセルが単なる「空の」スペースであるかを知ることができます。 OpenGLを使用して基本的なテキストをレンダリングする簡単な方法がいくつかありますが、OpenGLを使用した3Dレンダリングの詳細をカバーするために、このアプローチを採用しています。

この画像を生成するために、QImage::Format_Monoフラグを使用してQImageオブジェクトをインスタンス化します。 知っておく必要があるのは、テキストの一部であるピクセルとそうでないピクセルだけなので、モノクロ画像は問題なく機能するはずです。 ユーザーがテキストを入力すると、このQImageオブジェクトが同期的に更新されます。 フォントサイズと画像の幅に基づいて、ユーザー定義の高さ内にテキストが収まるように最善を尽くします。

次に、テキストの一部であるすべてのピクセル(この場合は黒のピクセル)を列挙します。 ここでの各ピクセルは、個別の正方形のような単位として扱われます。 これに基づいて、三角形のリストを生成し、それらの頂点の座標を計算して、それらを3Dモデルファイルに保存できます。

独自の単純な3Dモデルファイル形式ができたので、レンダリングに集中できます。 OpenGLベースの3Dレンダリングの場合、QtはQOpenGLWidgetと呼ばれるウィジェットを提供します。 このウィジェットを使用するには、次の3つの関数をオーバーライドできます。

  • initializeGl()-これは初期化コードが行くところです
  • paintGl()-このメソッドは、ウィジェットが再描画されるたびに呼び出されます
  • resizeGl(int w、int h)-このメソッドは、サイズ変更されるたびにウィジェットの幅と高さで呼び出されます

3dmodelファイル形式

initializeGlメソッドで適切なシェーダー構成を設定することにより、ウィジェットを初期化します。

 glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);

最初の行は、他のピクセルの後ろにあり、見えないピクセルではなく、私たちに近いレンダリングされたピクセルのみをプログラムに表示させます。 2行目は、フラットシェーディング手法を指定しています。 3行目は、法線が指す方向に関係なく、プログラムに三角形をレンダリングさせます。

初期化されると、paintGlが呼び出されるたびにモデルをディスプレイにレンダリングします。 paintGlメソッドをオーバーライドする前に、バッファーを準備する必要があります。 そのためには、最初にバッファハンドルを作成します。 次に、ハンドルをバインドポイントの1つにバインドし、ソースデータをバッファーにコピーし、最後にプログラムにバッファーのバインドを解除するように指示します。

 // Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);

オーバーライドするpaintGlメソッド内で、頂点の配列と通常のデータの配列を使用して、各フレームの三角形を描画します。

 QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);

パフォーマンスを向上させるために、プロトタイプアプリケーションでVertex Buffer Object(VBO)を使用しました。 これにより、データをビデオメモリに保存し、レンダリングに直接使用できます。 これに代わる方法として、レンダリングコードからデータ(頂点座標、法線、色)を提供します。

 glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();

これはより簡単な解決策のように思えるかもしれません。 ただし、これにはデータがビデオメモリバスを通過する必要があるため、パフォーマンスに深刻な影響があります。これは比較的遅いプロセスです。 paintGlメソッドを実装した後、シェーダーに注意を払う必要があります。

 m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));

OpenGLでは、シェーダーはGLSLと呼ばれる言語を使用して実装されます。 この言語は、レンダリング前に3Dデータを簡単に操作できるように設計されています。 ここでは、頂点シェーダーとフラグメントシェーダーの2つのシェーダーが必要になります。 頂点シェーダーでは、変換行列を使用して座標を変換し、回転とズームを適用し、色を計算します。 フラグメントシェーダーでは、フラグメントに色を割り当てます。 次に、これらのシェーダープログラムをコンパイルし、コンテキストにリンクする必要があります。 OpenGLは、プログラム内のパラメーターに外部からアクセスまたは割り当てることができるように、2つの環境をブリッジする簡単な方法を提供します。

 // Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);

頂点シェーダーコードでは、元の頂点に変換行列を適用して、新しい頂点の位置を計算します。

 gl_Position = matrixVertex * vec4(coordVertexes, 1.0);

この変換行列を計算するために、スクリーンスケール、シーンの変換、スケール、回転、および中央揃えといういくつかの個別のマトリックスを計算します。 次に、最終的な変換行列を計算するために、これらの行列の積を求めます。 モデルの中心を原点(0、0、0)に変換することから始めます。これは、画面の中心でもあります。 回転は、ポインティングデバイスを使用したユーザーのシーンとの相互作用によって決定されます。 ユーザーはシーンをクリックし、ドラッグして回転させることができます。 ユーザーがクリックすると、カーソル位置が保存され、移動後、2番目のカーソル位置が表示されます。 これらの2つの座標とシーンの中心を使用して、三角形を形成します。 いくつかの簡単な計算に従って、回転角を決定し、この変更を反映するように回転行列を更新できます。 スケーリングについては、OpenGLウィジェットのX軸とY軸のスケーリング係数を変更するためにマウスホイールに依存するだけです。 モデルは、シーンがレンダリングされる平面の背後に保持するために0.5だけ逆変換されます。 最後に、自然なアスペクト比を維持するために、長辺に沿ったモデル拡張の減少を調整する必要があります(OpenGLシーンとは異なり、レンダリングされるウィジェットは、いずれかの軸に沿って異なる物理的寸法を持つ場合があります)。 これらすべてを組み合わせて、次のように最終的な変換行列を計算します。

 void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }

結論

このOpenGL3Dレンダリングの概要では、udがビデオカードを利用して3Dモデルをレンダリングできるようにするテクノロジーの1つを探りました。 これは、同じ目的でCPUサイクルを使用するよりもはるかに効率的です。 非常に単純なシェーディング手法を使用し、マウスからのユーザー入力を処理することでシーンをインタラクティブにしました。 ビデオメモリとプログラムの間でデータをやり取りするためにビデオメモリバスを使用することは避けました。 1行のテキストを3Dでレンダリングしただけでも、より複雑なシーンを非常によく似た方法でレンダリングできます。

公平を期すために、このチュートリアルでは、3Dモデリングとレンダリングの表面をほとんど傷つけていません。 これは広大なトピックであり、このOpenGLチュートリアルでは、3Dゲームやモデリングソフトウェアを構築するために知っておく必要があるのはこれだけだとは言えません。 ただし、この記事の目的は、この領域をのぞき見し、OpenGLを使用して3Dアプリケーションを簡単に構築できることを示すことです。