今週の進み具合 #3

更新が遅くなりました(汗)
今週実装したことを振り返っていきます。

進み具合 (2013年12月15日 - 12月27日)

GLSL シェーダのコンパイルとリンク、シェーダプログラムの適用、エフェクトパスの生成を実装しました。 また、エフェクトパスを使用するにあたって、入力レイアウトも作成しました。 実際に四角形を描画してみました。

Drawing rectangle

どことなく哀愁を感じます(汗)

入力レイアウト

今回、入力レイアウトを作成するにあたって、OpenGL を使った実装では Vertex Array Object (VAO) を使用しました。入力レイアウトの概念をクライアント(フレームワークを使用するユーザ)に API として公開するのは悩んだところです。入力レイアウトを導入した理由は、よりパフォーマンスが得られ、現世代のグラフィックス API である Direct3D 11 と OpenGL 4 を使った実装と非常に相性がいいことが挙げられます。

現世代のゲームにおけるレンダリングでは、ハードウェアインスタンシング(またはジオメトリインスタンシング)など、複数の頂点バッファを同時に扱うケースが考えられます。これに対応するため 2 つの方法を考えました:

  • (A) ドローコール毎に、現在バインドされている 1 から N 個の頂点バッファの頂点要素の情報を参照し、エンジン側で入力レイアウトを生成、バインドする。
  • (B) あらかじめ入力レイアウトを作成しておき、クライアント側でドローコール前に入力レイアウトをバインドする。

(A) の方法では、クライアントに入力レイアウトオブジェクトを公開する必要はありません。エンジンの内部では、クライアントによってグラフィックスコンテキストに設定された複数個の頂点バッファを参照して入力レイアウトを生成します。フレーム毎に、(ID3D11InputLayout や Vertex Buffer Array といった)入力レイアウトオブジェクトを生成すれば、すぐさまグラフィックスメモリが不足します。そのため、あらかじめ CRC32 などを使って頂点要素情報からハッシュ値を計算し、これを頂点要素情報の識別子として頂点バッファごとに持たせておきます。グラフィックスコンテキストの内部で、識別子から入力レイアウトを検索し、存在しなければ入力レイアウトを生成する、というような実装になります。以下に疑似コードを載せます。

struct VertexDeclaration
{
    std::vector<VertexElement> elements;
    std::size_t strideBytes;
    std::uint32_t hashKey;
};

struct VertexBuffer
{
    GLuint vertexBuffer; // or ID3D11Buffer*
    VertexDeclaration vertexDeclaration;
};
std::map<std::uint32_t, std::unique_ptr<InputLayout>> inputLayouts;
std::vector<std::shared_ptr<VertexBuffer>> vertexBuffers;

void GraphicsContext::Draw()
{
    std::uint32_t hashKey = 0;
    for (auto vertexBuffer: vertexBuffers) {
        hashKey += vertexBuffer->vertexDeclaration.hashKey;
    }

    auto iter = inputLayouts.find(hashKey);
    if (iter == std::end(inputLayouts)) {
        inputLayouts[hashKey] = std::make_unique<InputLayout>();
    }

    inputLayouts[hashKey]->Apply();

    // draw call
    ...
}

この方法では、あらかじめハッシュ値を生成し、識別子として頂点バッファごとに持つ必要があります。また、フレーム毎に入力レイアウトをマップから検索する必要もあります。以前はこの方法を採用していました。

今回は (B) を採用しました。Direct3D 11, OpenGL 4 以上をターゲットにしていることが大きな理由です。 対して (B) の方法では、クライアントに入力レイアウトを公開することになります。クライアント側で入力レイアウトオブジェクトを生成し、頂点バッファやシェーダプログラムと同じようにオブジェクトを描画毎に適用します。作成済みの入力レイアウトがグラフィックスコンテキストに設定されるため、内部で入力レイアウトのマップを持つ必要がなくなり、ハッシュ値を計算し保持しておく必要もありません。また入力レイアウトを検索する必要もありません。

struct VertexDeclaration
{
    std::vector<VertexElement> elements;
    std::size_t strideBytes;
};
std::shared_ptr<InputLayout> inputLayout;
std::vector<std::shared_ptr<VertexBuffer>> vertexBuffers;

void GraphicsContext::SetInputLayout(std::shared_ptr<InputLayout> const& inputLayoutIn)
{
    inputLayout = inputLayoutIn;
}

void GraphicsContext::Draw()
{
    inputLayout->Apply();

    // draw call
    ...
}

(B) で気をつけることは、ユーザが適切な入力レイアウトオブジェクトを作成できるようにすることです。今回の実装ではシェーダリフレクションを使って、既定値の入力レイアウトを自動生成できる API を用意しました。

void MyGame::Intialize()
{
    effectPass = std::make_shared<EffectPass>(graphicsDevice, ...);
    inputLayout = std::make_shared<InputLayout>(graphicsDevice, effectPass);
}

void MyGame::Draw()
{
    ...

    graphicsContext->SetInputLayout(inputLayout);
    graphicsContext->SetVertexBuffer(vertexBuffer);
    effectPass->Apply();
    ...
}

今年中の実装予定

今年中にエフェクトパラメータを実装します。Direct3D だと定数バッファ(Constant Buffer)、OpenGL だと Uniform Buffer と呼ばれている箇所です。エフェクトパラメータが片付いたら、レンダーターゲットとテクスチャを実装する予定です。またシェーダとテクスチャに関してはアセットが絡んでくるので、ランタイムエンジンと切り分けて、少しずつアセットパイプラインを考える必要がありそうです。

(次回 "今週の進み具合 #4" はあるのでしょうか…。)

感謝

エフェクトの実装を終えてすぐにテスト描画してみたところ、はじめ四角形も何も表示されていませんでした。途方に暮れていたところ、pull request をいただきました。見事解決し、無事に四角形を描画することができました。pull request を送っていただいた @bis83 さん、ありがとうございます!

Leave a Reply