基本的なメッシュを作成して表示する

3-D ユニバーサル Windows プラットフォーム (UWP) ゲームでは、通常、多角形を使用してゲーム内のオブジェクトとサーフェスを表します。 これらの多角形オブジェクトとサーフェスの構造を構成する頂点のリストは、メッシュと呼ばれます。 ここでは、キューブ オブジェクトの基本的なメッシュを作成し、レンダリングと表示のためにシェーダー パイプラインに提供します。

重要な ここに含まれるコード例では、型 (DirectX::XMFLOAT3 や DirectX::XMFLOAT4X4 など) と DirectXMath.h で宣言されたインライン メソッドを使用します。 このコードを切り取って貼り付ける場合は、プロジェクトで <#include DirectXMath.h> を使用してください。

 

知っておくべきこと

技術

[前提条件]

  • 線形代数と 3-D 座標系に関する基本的な知識
  • Visual Studio 2015 以降の Direct3D テンプレート

インストラクション

次の手順では、基本的なメッシュ キューブを作成する方法を示します。

手順 1: モデルのメッシュを構築する

ほとんどのゲームでは、ゲーム オブジェクトのメッシュは、特定の頂点データを含むファイルから読み込まれます。 これらの頂点の順序はアプリに依存しますが、通常はストリップまたはファンとしてシリアル化されます。 頂点データは、任意のソフトウェア ソースから取得することも、手動で作成することもできます。 頂点シェーダーが効果的に処理できる方法でデータを解釈するのは、ゲーム次第です。

この例では、キューブに単純なメッシュを使用します。 キューブは、パイプラインのこの段階のオブジェクト メッシュと同様に、独自の座標系を使用して表されます。 頂点シェーダーはその座標を受け取り、指定した変換行列を適用することで、同種の座標系で最終的な 2-D ビュー プロジェクションを返します。

キューブのメッシュを定義します。 (または、ファイルから読み込みます。それはあなたの呼び出しです!)

SimpleCubeVertex cubeVertices[] =
{
    { DirectX::XMFLOAT3(-0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 0.0f) }, // +Y (top face)
    { DirectX::XMFLOAT3( 0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 0.0f) },
    { DirectX::XMFLOAT3( 0.5f, 0.5f,  0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 1.0f) },
    { DirectX::XMFLOAT3(-0.5f, 0.5f,  0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 1.0f) },

    { DirectX::XMFLOAT3(-0.5f, -0.5f,  0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 1.0f) }, // -Y (bottom face)
    { DirectX::XMFLOAT3( 0.5f, -0.5f,  0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 1.0f) },
    { DirectX::XMFLOAT3( 0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 0.0f) },
    { DirectX::XMFLOAT3(-0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f) },
};

キューブの座標系では、立方体の中心が原点に配置され、y軸は左手系で上から下に向かっています。 座標値は、-1 から 1 の間の 32 ビット浮動小数点値として表されます。

各角かっこで囲まれたペアリングでは、2 番目の DirectX::XMFLOAT3 値グループによって、頂点に関連付けられている色が RGB 値として指定されます。 たとえば、最初の頂点 (-0.5、0.5、-0.5) は完全な緑色になります (G 値は 1.0 に設定され、"R" と "B" の値は 0 に設定されます)。

したがって、それぞれ特定の色を持つ 8 つの頂点があります。 各頂点と色のペアリングは、この例の頂点の完全なデータです。 頂点バッファーを指定するときは、この特定のレイアウトに留意する必要があります。 頂点データを理解できるように、この入力レイアウトを頂点シェーダーに提供します。

手順 2: 入力レイアウトを設定する

これで、メモリ内に頂点が格納されています。 ただし、グラフィックス デバイスには独自のメモリがあり、Direct3D を使用してアクセスします。 処理のために頂点データをグラフィックス デバイスに取り込むには、その方法をクリアする必要があります。頂点データをゲームから取得したときにグラフィックス デバイスが解釈できるように、頂点データのレイアウト方法を宣言する必要があります。 そのためには、ID3D11InputLayoutを使用します。

頂点バッファーの入力レイアウトを宣言して設定します。

const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,  0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR",    0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

ComPtr<ID3D11InputLayout> inputLayout;
m_d3dDevice->CreateInputLayout(
                basicVertexLayoutDesc,
                ARRAYSIZE(basicVertexLayoutDesc),
                vertexShaderBytecode->Data,
                vertexShaderBytecode->Length,
                &inputLayout)
);

このコードでは、頂点のレイアウト (具体的には、頂点リスト内の各要素に含まれるデータ) を指定します。 ここでは、basicVertexLayoutDescでは、次の 2 つのデータ コンポーネントを指定します。

  • POSITION: シェーダーに提供される位置データの HLSL セマンティックです。 このコードでは、DirectX::XMFLOAT3、より具体的には、3D 座標 (x、y、z) に対応する 3 つの 32 ビット浮動小数点値を持つ構造体です。 均一な "w" 座標を指定する場合は float4 を使用することもできます。その場合は、DXGI_FORMAT_R32G32B32A32_FLOATを指定します。 DirectX::XMFLOAT3 と float4 のどちらを使用するかは、ゲームの特定のニーズに応えます。 メッシュの頂点データが使用する形式に正しく対応していることを確認してください。

    各座標値は、オブジェクトの座標空間で、-1 から 1 までの浮動小数点値として表されます。 頂点シェーダーが完了すると、変換された頂点は同次空間(パースペクティブ補正された)ビュー投影空間内にあります。

    "ただし、列挙値は XYZ ではなく RGB を示します。 良い目! カラー データと座標データのどちらの場合も、通常は 3 つまたは 4 つのコンポーネント値を使用するので、両方に同じ形式を使用してみませんか? HLSL セマンティックは、フォーマット名ではなく、シェーダーがデータをどのように扱うかを示します。

  • COLOR: カラー データの HLSL セマンティックです。 POSITIONと同様に、3 つの 32 ビット浮動小数点値 (DirectX::XMFLOAT3) で構成されます。 各値には、赤 (r)、青 (b)、緑 (g) の色成分が含まれており、0 から 1 までの浮動小数点数で表されます。

    COLOR 値は、通常、シェーダー パイプラインの最後に 4 コンポーネントの RGBA 値として返されます。 この例では、すべてのピクセルについて、シェーダー パイプラインで "A" アルファ値を 1.0 (最大不透明度) に設定します。

形式の完全な一覧については、DXGI_FORMATを参照してください。 HLSL セマンティクスの完全な一覧については、「セマンティクスの」を参照してください。

ID3D11Device::CreateInputLayout を呼び出し、Direct3D デバイスに入力レイアウトを作成します。 次に、実際にデータを保持できるバッファーを作成する必要があります。

手順 3: 頂点バッファーを投入する

頂点バッファーには、メッシュ内の各三角形の頂点の一覧が含まれます。 この一覧では、すべての頂点が一意である必要があります。 この例では、キューブの頂点が 8 個あります。 頂点シェーダーはグラフィックス デバイス上で実行され、頂点バッファーから読み取られ、前の手順で指定した入力レイアウトに基づいてデータが解釈されます。

次の例では、バッファーの説明とサブリソースを指定します。これは、頂点データの物理マッピングと、グラフィックス デバイス上のメモリ内で処理する方法について Direct3D にさまざまなことを伝えます。 これは、何でも含む可能性がある汎用のID3D11Bufferを使用するために必要です。 バッファー内の各頂点要素のサイズや頂点リストの最大サイズなど、Direct3D がバッファーの物理メモリ レイアウトを確実に理解できるように、D3D11_BUFFER_DESC 構造体と D3D11_SUBRESOURCE_DATA 構造体が提供されます。 ここでバッファー メモリへのアクセスと、バッファー メモリの走査方法を制御することもできますが、このチュートリアルの範囲を少し超えています。

バッファーを構成したら、ID3D11Device::CreateBuffer を呼び出して実際に作成します。 明らかに、複数のオブジェクトがある場合は、一意のモデルごとにバッファーを作成します。

頂点バッファーを宣言して作成します。

D3D11_BUFFER_DESC vertexBufferDesc = {0};
vertexBufferDesc.ByteWidth = sizeof(SimpleCubeVertex) * ARRAYSIZE(cubeVertices);
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;

D3D11_SUBRESOURCE_DATA vertexBufferData;
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;

ComPtr<ID3D11Buffer> vertexBuffer;
m_d3dDevice->CreateBuffer(
                &vertexBufferDesc,
                &vertexBufferData,
                &vertexBuffer);

頂点の読み込みが完了しました。 しかし、これらの頂点を処理する順序は何ですか? これは、頂点にインデックスの一覧を指定するときに処理されます。これらのインデックスの順序は、頂点シェーダーによって処理される順序です。

手順 4: インデックス バッファーを埋める

次に、各頂点のインデックスの一覧を指定します。 これらのインデックスは、頂点バッファー内の頂点の位置 (0 から始まる) に対応します。 これを視覚化するには、メッシュ内の各一意の頂点に ID などの一意の番号が割り当てられていることを考慮してください。 この ID は、頂点バッファー内の頂点の整数位置です。

8 つの番号付き頂点を持つキューブを

このキューブの例では、8 つの頂点があり、辺に対して 6 つのクワッドが作成されます。 クアッドを三角形に分割し、8つの頂点を使用して合計12個の三角形を作ります。 三角形あたり 3 つの頂点には、インデックス バッファーに 36 個のエントリがあります。 この例では、このインデックス パターンは三角形リストと呼ばれ、プリミティブ トポロジを設定するときに D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST として Direct3D に指定します。

三角形がポイントと辺を共有する場合は冗長性が多く、インデックスを一覧表示する最も非効率的な方法である可能性があります。 たとえば、三角形がひし形の辺を共有する場合、次のように 4 つの頂点の 6 つのインデックスを一覧表示します。

インデックス の順序でひし形 を構築する順序

  • 三角形 1: [0, 1, 2]
  • 三角形 2: [0, 2, 3]

ストリップ トポロジまたはファン トポロジでは、トラバーサル中に多くの冗長な側面 (画像内のインデックス 0 からインデックス 2 への側など) を排除する方法で頂点を並べ替えてください。大規模なメッシュの場合、頂点シェーダーの実行回数が大幅に減り、パフォーマンスが大幅に向上します。 ただし、シンプルな状態に保ち、三角形リストを使用します。

頂点バッファーのインデックスを単純な三角形リスト トポロジとして宣言します。

unsigned short cubeIndices[] =
{   0, 1, 2,
    0, 2, 3,

    4, 5, 6,
    4, 6, 7,

    3, 2, 5,
    3, 5, 4,

    2, 1, 6,
    2, 6, 5,

    1, 7, 6,
    1, 0, 7,

    0, 3, 4,
    0, 4, 7 };

8 つの頂点しかない場合、バッファー内の 36 個のインデックス要素は非常に冗長です。 冗長性の一部を排除し、ストリップやファンなどの別の頂点リストタイプを使用する場合は、ID3D11DeviceContext::IASetPrimitiveTopology メソッドに特定の D3D11_PRIMITIVE_TOPOLOGY 値を指定するときに、その種類を指定する必要があります。

さまざまなインデックス リスト手法の詳細については、「プリミティブ トポロジの」を参照してください。

手順 5: 変換マトリックスの定数バッファーを作成する

頂点の処理を開始する前に、実行時に各頂点に適用 (乗算) される変換マトリックスを指定する必要があります。 ほとんどの 3-D ゲームには、次の 3 つがあります。

  • オブジェクト (モデル) 座標系からワールド座標系全体に変換する 4x4 行列。
  • ワールド座標系からカメラ (ビュー) 座標系に変換する 4x4 行列。
  • カメラ座標系から 2-D ビュー投影座標系に変換する 4x4 行列。

これらのマトリックスは、定数バッファーでシェーダーに渡されます。 定数バッファーは、シェーダー パイプラインの次のパスの実行全体を通じて一定のメモリ領域であり、HLSL コードからシェーダーから直接アクセスできます。 各定数バッファーは 2 回定義します。最初はゲームの C++ コードで、(少なくとも) シェーダー コードの C のような HLSL 構文では 1 回定義します。 2 つの宣言は、型とデータの配置の観点から直接対応する必要があります。 シェーダーが HLSL 宣言を使用して C++ で宣言されたデータを解釈し、型が一致しない場合や、データの配置がオフになっている場合は、エラーを見つけるのが簡単です。

定数バッファーは HLSL によって変更されません。 ゲームが特定のデータを更新するときに変更できます。 多くの場合、ゲーム開発者は 4 つのクラスの定数バッファーを作成します。フレームごとに更新用に 1 種類です。モデル/オブジェクトごとに 1 種類の更新。ゲーム状態の更新ごとに 1 種類の更新。と、ゲームの有効期間中に変化しないデータの 1 つの型。

この例では、3つのマトリックスに関連するデータのうち、変更されないのは DirectX::XMFLOAT4X4 データの1つだけです。

メモ ここに示すコード例では、列主行列を使用します。 代わりに、HLSL で row_major キーワードを使用し、ソース マトリックス データも行メジャーであることを確認することで、行メジャー マトリックスを使用できます。 DirectXMath は行メジャー マトリックスを使用し、row_major キーワードで定義された HLSL マトリックスと直接使用できます。

 

各頂点の変換に使用する 3 つのマトリックスの定数バッファーを宣言して作成します。

struct ConstantBuffer
{
    DirectX::XMFLOAT4X4 model;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
};
ComPtr<ID3D11Buffer> m_constantBuffer;
ConstantBuffer m_constantBufferData;

// ...

// Create a constant buffer for passing model, view, and projection matrices
// to the vertex shader.  This allows us to rotate the cube and apply
// a perspective projection to it.

D3D11_BUFFER_DESC constantBufferDesc = {0};
constantBufferDesc.ByteWidth = sizeof(m_constantBufferData);
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags = 0;
constantBufferDesc.MiscFlags = 0;
constantBufferDesc.StructureByteStride = 0;
m_d3dDevice->CreateBuffer(
                &constantBufferDesc,
                nullptr,
                &m_constantBuffer
             );

m_constantBufferData.model = DirectX::XMFLOAT4X4( // Identity matrix, since you are not animating the object
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f);

);
// Specify the view (camera) transform corresponding to a camera position of
// X = 0, Y = 1, Z = 2.  

m_constantBufferData.view = DirectX::XMFLOAT4X4(
            -1.00000000f, 0.00000000f,  0.00000000f,  0.00000000f,
             0.00000000f, 0.89442718f,  0.44721359f,  0.00000000f,
             0.00000000f, 0.44721359f, -0.89442718f, -2.23606800f,
             0.00000000f, 0.00000000f,  0.00000000f,  1.00000000f);

通常、デバイス固有のリソースを設定するときにプロジェクション マトリックスを宣言します。乗算の結果は、現在の 2-D ビューポート サイズ パラメーター (多くの場合、ディスプレイのピクセルの高さと幅に対応する) と一致する必要があるためです。 変更された場合は、それに応じて x 座標と y 座標の値をスケーリングする必要があります。

 

// Finally, update the constant buffer perspective projection parameters
// to account for the size of the application window.  In this sample,
// the parameters are fixed to a 70-degree field of view, with a depth
// range of 0.01 to 100.  

float xScale = 1.42814801f;
float yScale = 1.42814801f;
if (backBufferDesc.Width > backBufferDesc.Height)
{
    xScale = yScale *
                static_cast<float>(backBufferDesc.Height) /
                static_cast<float>(backBufferDesc.Width);
}
else
{
    yScale = xScale *
                static_cast<float>(backBufferDesc.Width) /
                static_cast<float>(backBufferDesc.Height);
}
m_constantBufferData.projection = DirectX::XMFLOAT4X4(
            xScale, 0.0f,    0.0f,  0.0f,
            0.0f,   yScale,  0.0f,  0.0f,
            0.0f,   0.0f,   -1.0f, -0.01f,
            0.0f,   0.0f,   -1.0f,  0.0f
            );

ここにいる間は、ID3D11DeviceContextに頂点バッファーとインデックス バッファーと、使用しているトポロジを設定します。

// Set the vertex and index buffers, and specify the way they define geometry.
UINT stride = sizeof(SimpleCubeVertex);
UINT offset = 0;
m_d3dDeviceContext->IASetVertexBuffers(
                0,
                1,
                vertexBuffer.GetAddressOf(),
                &stride,
                &offset);

m_d3dDeviceContext->IASetIndexBuffer(
                indexBuffer.Get(),
                DXGI_FORMAT_R16_UINT,
                0);

 m_d3dDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

大丈夫です! 入力アセンブリが完了しました。 すべてがレンダリング用に配置されています。 頂点シェーダーを動かしましょう。

手順 6: 頂点シェーダーを使用してメッシュを処理する

メッシュを定義する頂点を含む頂点バッファーと、頂点の処理順序を定義するインデックス バッファーが用意されたので、頂点シェーダーに送信します。 コンパイル済みの高度なシェーダー言語として表される頂点シェーダー コードは、頂点バッファー内の頂点ごとに 1 回実行されるため、頂点ごとの変換を実行できます。 最終的な結果は、通常、2-D プロジェクションです。

(頂点シェーダーを読み込んだか?そうでない場合は、「DirectX ゲームでリソースを読み込む方法」を確認してください。

ここでは、頂点シェーダーを作成します。...

// Set the vertex and pixel shader stage state.
m_d3dDeviceContext->VSSetShader(
                vertexShader.Get(),
                nullptr,
                0);

...定数バッファーを設定します。

m_d3dDeviceContext->VSSetConstantBuffers(
                0,
                1,
                m_constantBuffer.GetAddressOf());

オブジェクト座標からワールド座標への変換と、2-D ビュー投影座標系への変換を処理する頂点シェーダー コードを次に示します。 また、頂点ごとの簡単な照明を適用して、物事を美しくします。 これは頂点シェーダーの HLSL ファイル (この例では SimplerVertexShader.hlsl) にあります。

cbuffer simpleConstantBuffer : register( b0 )
{
    matrix model;
    matrix view;
    matrix projection;
};

struct VertexShaderInput
{
    DirectX::XMFLOAT3 pos : POSITION;
    DirectX::XMFLOAT3 color : COLOR;
};

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float4 color : COLOR;
};

PixelShaderInput SimpleVertexShader(VertexShaderInput input)
{
    PixelShaderInput vertexShaderOutput;
    float4 pos = float4(input.pos, 1.0f);

    // Transform the vertex position into projection space.
    pos = mul(pos, model);
    pos = mul(pos, view);
    pos = mul(pos, projection);
    vertexShaderOutput.pos = pos;

    // Pass the vertex color through to the pixel shader.
    vertexShaderOutput.color = float4(input.color, 1.0f);

    return vertexShaderOutput;
}

その cbufferの が一番上にあるのを見えますか? これは、前に C++ コードで宣言したのと同じ定数バッファーに対する HLSL アナログです。 そして、VertexShaderInputstructは? 入力レイアウトと頂点データ宣言と同じように見える理由です。 C++ コードの定数バッファーと頂点データ宣言は、HLSL コードの宣言と一致し、符号、型、データの配置が含まれていることが重要です。

PixelShaderInput は、頂点シェーダーのメイン関数によって返されるデータのレイアウトを指定します。 頂点の処理が完了すると、2-D 投影空間内の頂点位置と、頂点ごとの照明に使用される色が返されます。 グラフィックス カードは、シェーダーによるデータ出力を使用して、パイプラインの次のステージでピクセル シェーダーを実行するときに色付けする必要がある "フラグメント" (可能なピクセル) を計算します。

手順 7: ピクセル シェーダーを通してメッシュを渡す

通常、グラフィックス パイプラインのこの段階では、オブジェクトの可視投影サーフェスに対してピクセル単位の操作を実行します。 テクスチャが好きな人もいますが、サンプルの目的としては、このステージを通過するだけで構いません。

まず、ピクセル シェーダーのインスタンスを作成します。 ピクセル シェーダーは、シーンの 2-D プロジェクション内のすべてのピクセルに対して実行され、そのピクセルに色が割り当てられます。 この場合、頂点シェーダーによって返されるピクセルの色を直接渡します。

ピクセル シェーダーを設定します。

m_d3dDeviceContext->PSSetShader( pixelShader.Get(), nullptr, 0 );

HLSL でパススルー ピクセル シェーダーを定義します。

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    // Draw the entire triangle yellow.
    return float4(1.0f, 1.0f, 0.0f, 1.0f);
}

頂点シェーダー HLSL とは別の HLSL ファイルにこのコードを配置します (SimplePixelShader.hlsl など)。 このコードは、ビューポート内のすべての表示ピクセル (描画する画面の部分のメモリ内表現) に対して 1 回実行されます。この場合、画面全体にマップされます。 これで、グラフィックス パイプラインが完全に定義されました。

手順 8: メッシュのラスター化と表示

パイプラインを実行してみましょう。 これは簡単です: ID3D11DeviceContext::DrawIndexedを呼び出します。

そのキューブを描画します。

// Draw the cube.
m_d3dDeviceContext->DrawIndexed( ARRAYSIZE(cubeIndices), 0, 0 );
            

グラフィックス カード内では、各頂点はインデックス バッファーで指定された順序で処理されます。 コードが頂点シェーダーを実行し、2-D フラグメントが定義されると、ピクセル シェーダーが呼び出され、三角形が色付けされます。

次に、画面にキューブを配置します。

そのフレーム バッファーをディスプレイに表示します。

// Present the rendered image to the window.  Because the maximum frame latency is set to 1,
// the render loop is generally  throttled to the screen refresh rate, typically around
// 60 Hz, by sleeping the app on Present until the screen is refreshed.

m_swapChain->Present(1, 0);

これで完了です。 モデルがいっぱいのシーンの場合は、複数の頂点バッファーとインデックス バッファーを使用します。また、モデルの種類によってシェーダーが異なる場合もあります。 各モデルには独自の座標系があり、定数バッファーで定義したマトリックスを使用して共有ワールド座標系に変換する必要があります。

注釈

このトピックでは、自分で作成する単純なジオメトリの作成と表示について説明します。 ファイルからより複雑なジオメトリを読み込み、それをサンプル固有の頂点バッファー オブジェクト (.vbo) 形式に変換する方法の詳細については、「DirectX ゲームでリソースを読み込む方法」を参照してください。