メインコンテンツまでスキップ

チュートリアル:OpenGL アプリケーションの構築

📚 Source Page

JUCE アプリケーション内で高性能レンダリングライブラリとして OpenGL を使い始める方法を学びます。オーディオアプリやプラグインで美しい 2D および 3D グラフィックスをレンダリングします。

レベル: 上級
プラットフォーム: Windows, macOS, Linux, iOS, Android
クラス: OpenGLAppComponent, OpenGLContext, OpenGLShaderProgram, OpenGLHelpers, Matrix3D, Vector3D

はじめに

このチュートリアルは OpenGL グラフィックスライブラリの基本的な理解を前提としています。OpenGLに慣れていない場合は、まずこちらで読んでください。

このチュートリアルのデモプロジェクトをこちらからダウンロードしてください:PIP | ZIP。プロジェクトを解凍し、最初のヘッダファイルを Projucer で開いてください。

警告

このプロジェクトの PIP バージョンを使用する場合は、Resourcesフォルダを生成された Projucer プロジェクトにコピーしてください。

この手順でサポートが必要な場合は、チュートリアル:Projucer Part 1: Projucer を始めようを参照してください。

デモプロジェクト

デモプロジェクトは、以下のスクリーンショットに示すように、Wavefront「.obj」ファイルを解析して標準的な OpenGL ティーポットオブジェクトを 3D グラフィックスで表示します:

デモプロジェクトアプリウィンドウ
デモプロジェクトアプリウィンドウ
ヒント

ここで紹介するコードは、JUCE サンプルのOpenGLAppExampleとほぼ同様です。

OpenGL アプリの構造

OpenGL API は多くの異なるプラットフォームとビルド環境で動作する強力で多用途なライブラリですが、3D レンダリングの原則はすべてのアプリケーションで類似しています。ここで探るいくつかの用語は、OpenGL がレンダリングルーチンをどのように実行するかを理解するための基本です:

  • GL コンテキスト:コンテキストは初期化段階で一度設定され、プラットフォーム固有の方法でグラフィックスレンダラーの GL 設定を記述し、必要な OpenGL 関数がアプリケーション内で使用するためにロードされます。
  • 投影行列:投影行列により、3D オブジェクトを 2D 平面に変換してシーンを画面にレンダリングできます。
  • ビュー行列:ビュー行列により、3D 環境で幾何学的変換を実行してシーン内にオブジェクトを配置できます。
  • シェーダー:オブジェクトの外観をカスタマイズするために、シェーダーは表面がどれだけ光沢があるか反射するか、3D オブジェクト上にライトとシャドウがどのように表示されるかなど、マテリアルのプロパティを記述するために使用されます。
  • 頂点:シーン内でレンダリングしようとしている 3D オブジェクトを定義する 3D ポイントを表します。これらは頂点シェーダー内で使用されます。
  • フラグメント:補間によって頂点間に存在するピクセルを表します。これらはフラグメントまたはピクセルシェーダー内で使用されます。
  • アトリビュート:シェーダー言語で使用される色やテクスチャ座標などの頂点パラメータを記述します。
  • ユニフォーム:シェーダー言語で使用されるが、シェーダープログラム間で一定のままのグローバルパラメータを記述します。
  • バリイング:頂点シェーダーとフラグメントシェーダー間で共有されるパラメータを記述します。
  • シェイプ:最終的にアプリケーションでレンダリングしたいポリゴンをカプセル化します。この場合、ティーポットです。

OpenGL シェーディング言語

OpenGL シェーディング言語または GLSL は、複数のオペレーティングシステムとハードウェアグラフィックスカードでグラフィックスレンダリングパイプラインを直接制御できる C 型言語です。GLSL を使用すると、オブジェクトの外観を記述するシェーダーと呼ばれる小さなプログラムを書くことができます。OpenGL を使用するか、スマートフォンやタブレットなどの組み込みシステム用に特別に設計されたサブセットライブラリ OpenGL ES を使用するかによって、言語構文は同じですが、パフォーマンスの考慮事項を考慮する必要があります。

例として、このチュートリアルで使用される頂点シェーダーは次のようになります:

// OpenGL and OpenGL ES
attribute vec4 position;
attribute vec4 sourceColour;
attribute vec2 textureCoordIn;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;

varying vec4 destinationColour;
varying vec2 textureCoordOut;

void main()
{
destinationColour = sourceColour;
textureCoordOut = textureCoordIn;
gl_Position = projectionMatrix * viewMatrix * position;
}

そしてこのチュートリアルで使用されるフラグメントシェーダーは次のようになります:

// OpenGL
varying vec4 destinationColour;
varying vec2 textureCoordOut;

void main()
{
vec4 colour = vec4 (0.95, 0.57, 0.03, 0.7);
gl_FragColor = colour;
}

// OpenGL ES
varying lowp vec4 destinationColour;
varying lowp vec2 textureCoordOut;

void main()
{
lowp vec4 colour = vec4 (0.95, 0.57, 0.03, 0.7);
gl_FragColor = colour;
}

ご覧のように、シェーダーは非常にシンプルで、OpenGL と OpenGL ES シェーダーの違いは最小限です。ここで使用されている GLSL の型、変数、関数は以下の通りです:

  • vec2/vec4:2 または 4 コンポーネントの浮動小数点ベクトルを表します。
  • mat4:4×4 の浮動小数点行列を表します。
  • lowp:OpenGL ES 用のより低い精度のデータ型を指定します。
  • attribute:頂点固有のパラメータを表します。
  • uniform:GL 環境を記述するグローバルパラメータを表します。
  • varying:頂点シェーダーとフラグメントシェーダー間で共有されるパラメータを表します。
  • gl_Position:頂点シェーダーが頂点操作を実行するための変換された頂点位置。
  • gl_FragColor:フラグメントシェーダーがフラグメント操作を実行するための色。
  • main():main 関数は頂点またはフラグメントシェーダーの計算が実行される場所です。

OpenGLAppComponent クラス

JUCE では、OpenGLAppComponentクラスはAudioAppComponentクラスと非常に似ていますが、グラフィカルアプリ用に使用されます。OpenGLAppComponentクラスから継承する場合、オーバーライドする必要があるいくつかの関数があります:

  • initialise():この関数はシェーダーなど、レンダリングに必要な GL オブジェクトを準備します。
  • render():render 関数は OpenGL レンダラーによって呼び出され、ここで投影行列とビュー行列が計算されて OpenGL コンテキストを描画します。
  • shutdown():この関数はシェーダーなど、レンダリングに使用された GL オブジェクトをクリアします。
  • shutdownOpenGL():サブクラスのデストラクタで、クラスが破棄される前に GL システムをシャットダウンするためにこの関数を呼び出す必要があります。

OpenGL の基本を探ったので、ティーポットのレンダリングの実装を始めましょう!

投影行列とビュー行列の計算

投影行列とビュー行列の計算を分離するために、後で使用するためにこれらの行列を返す 2 つのヘルパー関数を作成します。

まず、以下に示すように錐台と画面境界を使用して投影行列を計算します:

juce::Matrix3D<float> getProjectionMatrix() const
{
auto w = 1.0f / (0.5f + 0.1f); // [1]
auto h = w * getLocalBounds().toFloat().getAspectRatio (false); // [2]

return juce::Matrix3D<float>::fromFrustum (-w, w, -h, h, 4.0f, 30.0f); // [3]
}

錐台は、2 つの平行な平面でスライスすることによってポリゴンから切り出された形状で、Matrix3Dクラスは錐台から行列を返すfromFrustum()という便利な関数を提供します。上記の関数では:

  • [1]:まず、シナリオに適した任意の数で近平面上の錐台の半分の幅を定義する幅変数を宣言します。
  • [2]:次に、画面比率と幅変数に基づいて近平面上の錐台の半分の高さを定義する高さ変数を宣言します。
  • [3]:最後に、幅、高さ、近平面と遠平面の距離を引数としてfromFrustum()関数を使用して投影行列を取得します。これにより、正投影とは対照的に透視投影が得られます。

次に、ティーポットをアニメーションさせるための回転行列を使用してビュー行列を計算します。以下に示されます:

juce::Matrix3D<float> getViewMatrix() const
{
auto viewMatrix = juce::Matrix3D<float>::fromTranslation ({ 0.0f, 0.0f, -10.0f }); // [4]
auto rotationMatrix = viewMatrix.rotation ({ -0.3f,
5.0f * std::sin ((float) getFrameCounter() * 0.01f),
0.0f }); // [5]

return viewMatrix * rotationMatrix; // [6]
}
  • [4]:まず、行列を 10 単位後方にシーンに押し込むベクトルで平行移動した単位行列を作成します。これにより、ティーポットが画面の中央に配置されますが、少し離れた位置になります。
  • [5]:次に、レンダリングフレームカウンターに応じて y 軸周りにティーポットを回転させる、前に定義した行列からの回転行列を作成します。これにより、sin 関数のレートで回転方向が前後に変わります。
  • [6]:最後に、行列を乗算して回転を適用し、ビュー行列を返します。

数学的計算部分が完了したので、次にシェーダープログラムを書き始めることができます。

OpenGL シェーダーの作成

チュートリアルのコードベース全体で使用するいくつかの便利なメンバー変数を定義することから始めましょう:

juce::String vertexShader;
juce::String fragmentShader;

std::unique_ptr<juce::OpenGLShaderProgram> shader;
std::unique_ptr<Shape> shape;
std::unique_ptr<Attributes> attributes;
std::unique_ptr<Uniforms> uniforms;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

ここでは、この GL コンテキストで使用するシェイプ、アトリビュート、ユニフォームへのいくつかのポインタと、シェーダープログラムを管理するOpenGLShaderProgramオブジェクトを定義しました。また、次のステップで示すように頂点シェーダーとフラグメントシェーダーを定義する 2 つの文字ポインタもあります:

void createShaders()
{
vertexShader = R"(
attribute vec4 position;
attribute vec4 sourceColour;
attribute vec2 textureCoordIn;

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;

varying vec4 destinationColour;
varying vec2 textureCoordOut;

void main()
{
destinationColour = sourceColour;
textureCoordOut = textureCoordIn;
gl_Position = projectionMatrix * viewMatrix * position;
})";

fragmentShader =
#if JUCE_OPENGL_ES
R"(varying lowp vec4 destinationColour;
varying lowp vec2 textureCoordOut;)"
#else
R"(varying vec4 destinationColour;
varying vec2 textureCoordOut;)"
#endif
R"(
void main()
{)"
#if JUCE_OPENGL_ES
R"( lowp vec4 colour = vec4(0.95, 0.57, 0.03, 0.7);)"
#else
R"( vec4 colour = vec4(0.95, 0.57, 0.03, 0.7);)"
#endif
R"( gl_FragColor = colour;
})";

createShaders()関数では、まず改行を挿入して前に示したシェーダーを文字ポインタにコピーします。この関数は後でOpenGLAppComponentinitialise()関数で呼び出されます。頂点シェーダーは基本的に、変換行列つまり投影行列に続いてビュー行列の積に「gl_Position」変数を設定することで、シェイプ内のすべての頂点の位置を設定します。フラグメントシェーダーでは、「gl_FragColor」変数を指定された色に設定することでピクセルの色が指定されます。

createShaders()関数の後半では、現在の GL コンテキスト内で新しいシェーダープログラムを作成し[1]、以下のようにいくつかの初期化を行います:

std::unique_ptr<juce::OpenGLShaderProgram> newShader (new juce::OpenGLShaderProgram (openGLContext)); // [1]
juce::String statusText;

if (newShader->addVertexShader (juce::OpenGLHelpers::translateVertexShaderToV3 (vertexShader)) // [2]
&& newShader->addFragmentShader (juce::OpenGLHelpers::translateFragmentShaderToV3 (fragmentShader))
&& newShader->link())
{
shape.reset();
attributes.reset();
uniforms.reset();

shader.reset (newShader.release()); // [3]
shader->use();

shape.reset (new Shape());
attributes.reset (new Attributes (*shader));
uniforms.reset (new Uniforms (*shader));

statusText = "GLSL: v" + juce::String (juce::OpenGLShaderProgram::getLanguageVersion(), 2);
}
else
{
statusText = newShader->getLastError(); // [4]
}
}
  • [2]:まず頂点シェーダーを追加し、次にフラグメントシェーダーを追加し、コンパイルされたシェーダーを単一のプログラムにリンクしようとします。
  • [3]:シェーダーのコンパイルとリンクが成功した場合、シェイプ、アトリビュート、ユニフォームのポインタをクリアし、新しく作成したシェーダーをシェーダープログラムポインタに割り当て、シェイプ、アトリビュート、ユニフォームのポインタ用に新しいオブジェクトをインスタンス化できます。
  • [4]:シェーダーのコンパイルが失敗した場合に備えて、初期化ステータスを追跡できます。

では、頂点、アトリビュート、ユニフォーム、シェイプを表す便利な構造体を定義しましょう。

Vertex 構造体

頂点を表すために、以下に示すように 4 つの重要な変数が必要です:

struct Vertex
{
float position[3];
float normal[3];
float colour[4];
float texCoord[2];
};
  • ポジション:position 配列は 3D ローカル空間での頂点位置を表します。
  • ノーマル:normal 配列は、隣接する面の法線から計算された問題の頂点の法線ベクトルの方向を表します。
  • カラー:colour 配列は RGBA 形式での頂点の色を表します。
  • テクスチャ座標:テクスチャが使用される場合、これは問題の頂点で使用するテクスチャの 2D 座標を表します。

Attributes 構造体

attributes 構造体は基本的に複数のOpenGLShaderProgram::Attributeオブジェクトを一緒に保持するコンテナクラスで、保存するアトリビュートはここで定義されています:

std::unique_ptr<juce::OpenGLShaderProgram::Attribute> position, normal, sourceColour, textureCoordIn;

これらは、アトリビュートが頂点シェーダープログラムで頂点パラメータを記述することを意図しているため、前に定義した Vertex 構造体の変数に正確に対応しています。

期待通り、コンストラクタで次のステップで定義されるプライベートヘルパー関数を呼び出してシェーダープログラムを引数として渡すことで、これらのアトリビュートを作成します:

explicit Attributes (juce::OpenGLShaderProgram& shaderProgram)
{
position.reset (createAttribute (shaderProgram, "position"));
normal.reset (createAttribute (shaderProgram, "normal"));
sourceColour.reset (createAttribute (shaderProgram, "sourceColour"));
textureCoordIn.reset (createAttribute (shaderProgram, "textureCoordIn"));
}

ヘルパー関数は順番にOpenGLShaderProgram::Attributeコンストラクタを呼び出して新しいオブジェクトをインスタンス化します:

private:
static juce::OpenGLShaderProgram::Attribute* createAttribute (juce::OpenGLShaderProgram& shader,
const juce::String& attributeName)
{
using namespace ::juce::gl;

if (glGetAttribLocation (shader.getProgramID(), attributeName.toRawUTF8()) < 0)
return nullptr;

return new juce::OpenGLShaderProgram::Attribute (shader, attributeName.toRawUTF8());
}

ただし、上記ではglGetAttribLocation()関数を使用してアトリビュートがシェーダープログラムに存在するかどうかを最初にチェックします。返される数値が-1 の場合、アトリビュートのインスタンス化を中止します。

enable()関数では、以下に示すようにglVertexAttribPointer()glEnableVertexAttribArray()関数を呼び出すことで、すべてのアトリビュートが(存在するかチェックした後)有効化されます:

void enable()
{
using namespace ::juce::gl;

if (position.get() != nullptr)
{
glVertexAttribPointer (position->attributeID, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex), nullptr);
glEnableVertexAttribArray (position->attributeID);
}

if (normal.get() != nullptr)
{
glVertexAttribPointer (normal->attributeID, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 3));
glEnableVertexAttribArray (normal->attributeID);
}

if (sourceColour.get() != nullptr)
{
glVertexAttribPointer (sourceColour->attributeID, 4, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 6));
glEnableVertexAttribArray (sourceColour->attributeID);
}

if (textureCoordIn.get() != nullptr)
{
glVertexAttribPointer (textureCoordIn->attributeID, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 10));
glEnableVertexAttribArray (textureCoordIn->attributeID);
}
}

glVertexAttribPointer()関数は、インデックス、サイズ、保持するデータの型などの情報で頂点アトリビュートデータの配列を定義します。最後の引数が、構造体で前に定義された他のアトリビュートに関してデータのオフセットを累積的に指定していることに注意してください。次にglEnableVertexAttribArray()関数は、コンテキスト内で使用する実際の配列を有効にします。

disable()関数では、すべてのアトリビュートに対してglDisableVertexAttribArray()関数を呼び出すことで正反対のことを行います:

void disable()
{
using namespace ::juce::gl;

if (position.get() != nullptr)
glDisableVertexAttribArray (position->attributeID);
if (normal.get() != nullptr)
glDisableVertexAttribArray (normal->attributeID);
if (sourceColour.get() != nullptr)
glDisableVertexAttribArray (sourceColour->attributeID);
if (textureCoordIn.get() != nullptr)
glDisableVertexAttribArray (textureCoordIn->attributeID);
}

Uniforms 構造体

uniforms 構造体は同様に、ここで定義されているように同じ方法で複数のOpenGLShaderProgram::Uniformオブジェクトを含みます:

std::unique_ptr<juce::OpenGLShaderProgram::Uniform> projectionMatrix, viewMatrix;

これらは頂点シェーダープログラムで前に定義した行列変数に正確に対応しています。

期待通り、コンストラクタで次のステップで定義されるプライベートヘルパー関数を呼び出してシェーダープログラムを引数として渡すことで、これらのアトリビュートを作成します:

explicit Uniforms (juce::OpenGLShaderProgram& shaderProgram)
{
projectionMatrix.reset (createUniform (shaderProgram, "projectionMatrix"));
viewMatrix.reset (createUniform (shaderProgram, "viewMatrix"));
}

ヘルパー関数は順番にOpenGLShaderProgram::Uniformコンストラクタを呼び出して新しいオブジェクトをインスタンス化します:

private:
static juce::OpenGLShaderProgram::Uniform* createUniform (juce::OpenGLShaderProgram& shaderProgram,
const juce::String& uniformName)
{
using namespace ::juce::gl;

if (glGetUniformLocation (shaderProgram.getProgramID(), uniformName.toRawUTF8()) < 0)
return nullptr;

return new juce::OpenGLShaderProgram::Uniform (shaderProgram, uniformName.toRawUTF8());
}
};

ただし、上記ではglGetUniformLocation()関数を使用してユニフォームがシェーダープログラムに存在するかどうかを最初にチェックします。返される数値が-1 の場合、ユニフォームのインスタンス化を中止します。

Shape 構造体

shape 構造体は、OpenGL 用語でティーポットオブジェクトを定義する場所です。メンバー変数は、ティーポットモデル用の Wavefront Obj ファイルと、すぐ下でサブ構造体として定義される頂点バッファの配列を格納するために使用されます:

WavefrontObjFile shapeFile;
juce::OwnedArray<VertexBuffer> vertexBuffers;

まず頂点バッファがどのように定義されているか見てみましょう。これは基本的に、後でレンダリングするために準備する頂点バッファとインデックスバッファだけでなく、メッシュ内のインデックスの総数を含みます:

GLuint vertexBuffer, indexBuffer;
int numIndices;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VertexBuffer)
};

クラスコンストラクタは以下の方法で頂点バッファを初期化します:

explicit VertexBuffer (WavefrontObjFile::Shape& aShape)
{
using namespace ::juce::gl;

numIndices = aShape.mesh.indices.size(); // [1]

glGenBuffers (1, &vertexBuffer); // [2]
glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);

juce::Array<Vertex> vertices;
createVertexListFromMesh (aShape.mesh, vertices, juce::Colours::green); // [3]

glBufferData (GL_ARRAY_BUFFER, // [4]
static_cast<GLsizeiptr> (static_cast<size_t> (vertices.size()) * sizeof (Vertex)),
vertices.getRawDataPointer(),
GL_STATIC_DRAW);

glGenBuffers (1, &indexBuffer); // [5]
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData (GL_ELEMENT_ARRAY_BUFFER,
static_cast<GLsizeiptr> (static_cast<size_t> (numIndices) * sizeof (juce::uint32)),
aShape.mesh.indices.getRawDataPointer(),
GL_STATIC_DRAW);
}
  • [1]:まず描画したいメッシュのインデックス数を取得します。
  • [2]:次にglGenBuffers()関数で単一の頂点バッファ用のバッファオブジェクト名を生成し、glBindBuffer()関数で頂点アトリビュートをそれにバインドします。
  • [3]:以下で定義するヘルパー関数を使用して、ティーポットメッシュから頂点リストを作成します。
  • [4]:次にglBufferData()関数を呼び出して頂点リストを頂点バッファにコピーできます。
  • [5]:最後に、バッファオブジェクト名を生成し、頂点配列インデックスをそれにバインドし、インデックスをインデックスバッファにコピーすることで、インデックスバッファに対しても同じことを行います。

頂点リストからメッシュを作成するヘルパー関数は以下のように定義されています:

static void createVertexListFromMesh (const WavefrontObjFile::Mesh& mesh, juce::Array<Vertex>& list, juce::Colour colour)
{
auto scale = 0.2f; // [6]
WavefrontObjFile::TextureCoord defaultTexCoord { 0.5f, 0.5f };
WavefrontObjFile::Vertex defaultNormal { 0.5f, 0.5f, 0.5f };

for (auto i = 0; i < mesh.vertices.size(); ++i) // [7]
{
const auto& v = mesh.vertices.getReference (i);
const auto& n = i < mesh.normals.size() ? mesh.normals.getReference (i) : defaultNormal;
const auto& tc = i < mesh.textureCoords.size() ? mesh.textureCoords.getReference (i) : defaultTexCoord;

list.add ({ {
scale * v.x,
scale * v.y,
scale * v.z,
},
{
scale * n.x,
scale * n.y,
scale * n.z,
},
{ colour.getFloatRed(), colour.getFloatGreen(), colour.getFloatBlue(), colour.getFloatAlpha() },
{ tc.x, tc.y } }); // [8]
}
}
  • [6]:まずメッシュスケール、デフォルトのテクスチャ座標、デフォルトの法線ベクトル用のいくつかのローカル変数を定義します。
  • [7]:次にメッシュ内のすべての頂点に対して、頂点位置、法線ベクトル、テクスチャ座標への参照を取得し、前に定義した新しい Vertex オブジェクトを作成します。
  • [8]:作成された Vertex オブジェクトでは、位置と法線ベクトルをスケールし、ダミーの緑色を割り当て、最後に頂点リストに追加します。

デストラクタでは、各変数に対してglDeleteBuffers()関数を呼び出すことで頂点バッファとインデックスバッファを削除します:

~VertexBuffer()
{
using namespace ::juce::gl;

glDeleteBuffers (1, &vertexBuffer);
glDeleteBuffers (1, &indexBuffer);
}

以下で定義されるbind()関数は、シェイプが描画されるときに呼び出され、glBindBuffer()関数を使用して頂点バッファとインデックスバッファをバインドします:

void bind()
{
using namespace ::juce::gl;

glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
}

では、ティーポットのバイナリデータが WavefrontObjFile 変数にロードされる shape コンストラクタに戻りましょう:

Shape()
{
auto dir = juce::File::getCurrentWorkingDirectory();

int numTries = 0;

while (!dir.getChildFile ("Resources").exists() && numTries++ < 15)
dir = dir.getParentDirectory();

if (shapeFile.load (dir.getChildFile ("Resources").getChildFile ("teapot.obj")).wasOk())
for (auto* s : shapeFile.shapes)
vertexBuffers.add (new VertexBuffer (*s));
}
警告

「teapot.obj」ファイルがプロジェクトのResourcesフォルダに存在することを確認してください。

ロードが成功した場合、WavefrontObjFile オブジェクトに含まれるすべてのシェイプを反復し、新しい VertexBuffer オブジェクトを作成して頂点バッファ配列に追加できます。

最後に、後でOpenGLAppComponentrender()関数で呼び出されるdraw()関数を実装します。以下のように定義されています:

void draw (Attributes& glAttributes)
{
using namespace ::juce::gl;

for (auto* vertexBuffer : vertexBuffers)
{
vertexBuffer->bind();

glAttributes.enable();
glDrawElements (GL_TRIANGLES, vertexBuffer->numIndices, GL_UNSIGNED_INT, nullptr);
glAttributes.disable();
}
}

メンバー変数配列内のすべての頂点バッファに対して、まずbind()関数を呼び出して頂点バッファとインデックスバッファを GL コンテキストにバインドします。次に、前に定義したenable()関数をすべてのアトリビュートに対して呼び出してデータで配列を埋めます。最後に、glDrawElements関数がアトリビュートが無効化されて空になる前に、頂点バッファに含まれる 3 つの頂点のセットをすべて三角形として描画します。

すべてを組み合わせる

これでティーポットをレンダリングするためのすべてのコンポーネントが揃ったので、すべてを組み合わせましょう。

前述のように、アプリはMainContentComponentクラスでここに示されているようにOpenGLAppComponentクラスから継承しています:

class MainContentComponent : public juce::OpenGLAppComponent
{
public:

クラスコンストラクタでは、通常通りsetSize()関数を使用してウィンドウのサイズを設定します:

MainContentComponent()
{
setSize (800, 600);
}

クラスデストラクタでは、shutdownOpenGL()関数を呼び出してクラスが破棄される前に OpenGL システムがシャットダウンされることを確認します:

~MainContentComponent() override
{
shutdownOpenGL();
}

前述のように、OpenGLAppComponentクラスはグラフィックスアプリケーションの実装を容易にするスタートアップとシャットダウン関数を提供します。initialise()関数では、前に定義したヘルパー関数createShaders()を呼び出して頂点シェーダーとフラグメントシェーダーを準備します。以下のように示されます:

void initialise() override
{
createShaders();
}

shutdown()関数では、すべてのメンバー変数ポインタを null に設定することでリークがないことを確認します。以下のようにします:

void shutdown() override
{
shader.reset();
shape.reset();
attributes.reset();
uniforms.reset();
}

次に、以下に説明するようにOpenGLAppComponent::render()関数をオーバーライドして実際のレンダリングを実行します:

void render() override
{
using namespace ::juce::gl;

jassert (juce::OpenGLHelpers::isContextActive());

auto desktopScale = (float) openGLContext.getRenderingScale(); // [1]
juce::OpenGLHelpers::clear (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); // [2]

glEnable (GL_BLEND); // [3]
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

glViewport (0,
0,
juce::roundToInt (desktopScale * (float) getWidth()),
juce::roundToInt (desktopScale * (float) getHeight())); // [4]

shader->use(); // [5]

if (uniforms->projectionMatrix.get() != nullptr) // [6]
uniforms->projectionMatrix->setMatrix4 (getProjectionMatrix().mat, 1, false);

if (uniforms->viewMatrix.get() != nullptr) // [7]
uniforms->viewMatrix->setMatrix4 (getViewMatrix().mat, 1, false);

shape->draw (*attributes); // [8]

// Reset the element buffers so child Components draw correctly
glBindBuffer (GL_ARRAY_BUFFER, 0); // [9]
glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, 0);
}
  • [1]:まずOpenGLHelpers::isContextActive()関数を使用して GL コンテキストがアクティブであることを確認し、レンダリングディスプレイのスケールファクターを取得できるようにします。
  • [2]:次に適切なルック&フィールの色で背景を塗ることでディスプレイをクリアできます。
  • [3]:これはチュートリアルの範囲を超えますが、glEnable()関数は計算されたフラグメントの色をカラーバッファの値とブレンドする「GL_BLEND」オプションを有効にします。ブレンド方法は、透明度の計算を指定することでglBlendFunc()関数で指定されます。
  • [4]:glViewport()関数は、レンダリングディスプレイのスケールファクターで幅と高さを乗算することで、デバイス画面に対して GL ウィンドウのビューポートを設定します。
  • [5]:シェーダーポインタでuse()関数を呼び出すことで、この GL コンテキストで使用したいシェーダーを指定します。
  • [6]:シェーダーを計算するためにヘルパー関数から投影行列をユニフォーム変数として設定します。
  • [7]:シェーダーを計算するためにヘルパー関数からビュー行列もユニフォーム変数として設定します。
  • [8]:最後に、シェイプポインタで前に定義したdraw()関数を呼び出し、引数として指定された GL コンテキストとアトリビュート内でティーポットをレンダリングします。
  • [9]:また、GL コンテキストでglBindBuffer()関数を使用して頂点アトリビュートと頂点配列インデックスを空にすることを確認します。
ヒント

「gl」プレフィックスを持つすべての関数は、JUCE ライブラリではなく、開発マシンの OpenGL ライブラリに含まれています。

まとめ

このチュートリアルでは、OpenGL JUCE アプリケーションのセットアップ方法を学びました。特に以下のことを行いました:

  • OpenGLAppComponentクラスの機能を学びました。
  • Wavefront Obj ファイルを OpenGL レンダラーにロードしました。
  • 投影行列とビュー行列を計算しました。
  • シェーダーを使用してOpenGLContextを設定しレンダリングの外観を構成しました。

関連項目