16.頂点定義と頂点レイアウト

今回から、いよいよゲームのライブラリらしくなっていきます。まずは、「頂点定義と頂点レイアウト」から実装を始めていきたいと思います。最初に登場するのは「頂点」です。

Dx11とDx12共通でいえること
Dx11とDx12は「頂点定義」にも違いがあり、両環境別に頂点フォーマットを考える必要があります。
しかしDirectXは頂点の表現に「フレキシブルバーテックスフォーマット(通称FVF)」という考え方を持っていますが、この考え方は共通です。まずは「FVF」について説明します。
単純に「頂点」と言った場合、一番基本的な部分では、「XYZの値を持つデータ」と考えられます。この頂点が1つで「点」、2つあれば「線分」を表現できます。3つあれば「三角形」を表現できます。ここでは三角形について考えます。
三角形は3つの点を決定すればその間を直線で結ぶことで完全に形(面)が決定します。
ゲーム画面に3Dの三角形を表示する、ということは、3つの点データで構成された三角形に「座標変換」を施して、あたかも3次元に見えるように2次元のキャンバス上に表示させたものです。
今は「座標変換」のことは考えないとして、頂点そのものを考えます。各頂点がXYZの座標だけでなく「色」を持っていたとすれば描画する三角形に色を付けることができます。例えばすべての頂点が「黄色」の色情報を持っていれば黄色い面を持った三角形が描けるでしょう。もしそれぞれの点の色が違えば、頂点の色同士のグラデーションを表現できます。
その面の向き(面に対して直角な、正規化されたベクトル)を持てば「ライティング」を表現できます。
ある4角形の「模様」があって、その模様上のX方向Y方向の位置を各頂点に設定できれば「模様付きの面」を表現できます。

このように、各頂点に、位置情報以外のいろんな情報を入れることによって、さまざまな三角形の表現をすることができます。
しかしながら、それらの情報をすべての頂点につけることを想像してみてください。
ただ色だけか表現できればいいのに、法線や模様の位置情報など入れても、それらのデータは使用しません。つまり死んだ情報になります。それらの情報が小さいデータならいいのですが、色を表現するのには、float型で考えて、128ビット(32ビット×4)、法線も128ビット(ベクトルだからそうですね)、模様(これをテクスチャといいます)のXY方向の情報も64ビット、これに頂点そのものの情報も必ず必要ですから(最低96ビット、アラインメントを施す場合でで128ビット)の情報が必要になります。
そうすると上記のデータを全部入れた頂点は、1つの点なのに448ビット(56バイト)必要になってしまいます。通常、一つのオブジェクトを表すのに1000個とか2000個とかの頂点が必要です。それを「必要もないのに」一つ一つ上記のバイト数を要していたのではあきらかに無駄です。
そのために、DirectXでは「FVF」という仕組みを取り入れ、必要な情報だけ頂点に入れて表現できるようにしています。

「FVF」を実装するためには、DirectXに「このデータはどういうデータが入っている情報か」を伝える必要があります。それが「頂点定義」です。また、頂点内のデータの並びかたを「頂点レイアウト」といいますが、それもゲームプログラマ側で設定してあげなければなりません。

「頂点定義」「頂点レイアウト」を定義し、DirectXに伝える方法は「Dx12」と「Dx11」では違いがあります。おおむね手順は一緒なのですが、「頂点レイアウトのデータ型名」が違うのです。
ですので、「頂点を操作する」プログラムは「Dx12」向け、「Dx11」向けと別々に記述する必要があります。

この記事は、
コミット「Dx11版ウインドウ作成と共有ファイルの追加」
から、
コミット「Dx11、Dx12両頂点定義とプリミティブメッシュ作成の追加」
の間の作業です。
GitHubサイト
https://github.com/WiZFramework/BaseCross
を参照して下さい。

上記コミット間に環境を整備するのが大変な人は、最新のコミットでも今回の記事はほとんど違いはありませんので、最新のzipファイルなどをダウンロードした方は、Dx12、Dx11いずれかのソリューションを開き「Dx12Lib」もしくは「Dx11Lib」プロジェクトの「VertexHelper.h」を参照してください。
Dx12のソリューションはWindows10プラスWindowsSDKの環境でなければソリューションを開けません(もし開けてもビルドできません)このあたり、ブログでその違いをどのように記述していったらいいか、迷う部分ではありますが、「Dx12」に関する記事は、基本的に「Dx12」でのライブラリ作成方法を知りたいと思いますので、Dx12ビルドできない環境の人は(つまりWin8.1でDx11向けにこのブログを読んでる人は)、申し訳ありませんがあまり役に立たないと思います。できるだけDx11版の記述もしていきたいと思いますので、そのあたりご勘弁を。

まずは「Dx12Lib」の話です。Dx12のソリューションを開いているものとします。
上記コミットの後ろのほうコミット内の「Dx12Lib」の中を見ると「VertexHelper.h」のほかに「d3dx12.h」というのがあります。このファイルは、Dx12用のサンプルにあったマイクロソフト社が作成した、Dx12向けのユーティリティが入ってるヘッダです。

「Dx12Lib」内のソースはこの中に記述されているユーティリティを使用することもありますので、もし、コミット「Dx11版ウインドウ作成と共有ファイルの追加」から、順を追って実装していく形で勉強している人は、まず、このファイルを「Dx12Lib」内に入れてください。
そうしたうえで、「VertexHelper.h」も、以下のような初期状態でいいから「DxLib内」に作成し、以下のようにひな形部分だけ記述してください。

#pragma once

#include "stdafx.h"

namespace basecross{

}

そうして今度は「Common.h」を以下のように書き換えます

#pragma once

//ユーティリティ基本クラス(削除テンプレート、例外処理など)
#include "../DxLib/BaseHelper.h"
//XML読み込み
#include "../DxLib/XmlDoc.h"
//ベクトル計算の計算クラス
#include "../DxLib/MathVector.h"
//行列、クオータニオン、カラーなどの計算クラス
#include "../DxLib/MathExt.h"
//ベクトルのスタティック計算
#include "../DxLib/MathVectorEX.h"
//行列、クオータニオン、カラーなどのスタティック計算
#include "../DxLib/MathExtEX.h"
//衝突判定、補間処理用ユーティリティ
#include "../DxLib/TransHelper.h"
//Dx12ツール
#include "d3dx12.h"
//頂点定義、
#include "VertexHelper.h"

つまり、下部にある、

//Dx12ツール
#include "d3dx12.h"
//頂点定義、
#include "VertexHelper.h"

を追加します。コミット上ではこのほかに

//プリミティブ作成関数等
#include "../DxLib/MeshHelper.h"

も入ってますが、これは共有領域への追加なので、後ほど説明します。(今は追加しないでください。ファイルがまだありませんので)

このようにして「Dx12Lib」内「VertexHelper.h」の準備を整えたら、まず、以下のように記述してみましょう。

    //--------------------------------------------------------------------------------------
    /// 位置と法線とテクスチャを持つ入力レイアウトの定義
    //--------------------------------------------------------------------------------------
    const D3D12_INPUT_ELEMENT_DESC VertexPositionNormalTextureLayout[] =
    {
        { "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 
                 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
                 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,
                 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    };

    //--------------------------------------------------------------------------------------
    /// 位置と法線とテクスチャを持つ頂点の定義
    //--------------------------------------------------------------------------------------
    struct VertexPositionNormalTexture
    {
        VertexPositionNormalTexture()
        { }

        VertexPositionNormalTexture(XMFLOAT3 const& pos, XMFLOAT3 const& norm, XMFLOAT2 const& tex)
            : position(pos),
            normal(norm),
            textureCoordinate(tex)
        { }

        VertexPositionNormalTexture(FXMVECTOR pos, FXMVECTOR norm, FXMVECTOR tex)
            : position(pos),
            normal(norm),
            textureCoordinate(tex)
        { }

        Vector3 position;
        Vector3 normal;
        Vector2 textureCoordinate;

        static const D3D12_INPUT_ELEMENT_DESC* GetVertexElement(){
            return VertexPositionNormalTextureLayout;
        }
        static UINT GetNumElements(){
            return  ARRAYSIZE( VertexPositionNormalTextureLayout );
        }
    };

これは、「位置情報」「法線」「テクスチャ位置」を持つFVFの頂点定義と頂点レイアウトです。

BasaeCrossでは、頂点定義(頂点データの構造体)の名前に意味を持たせてます。
たとえばこの例ですと「VertexPositionNormalTexture」という構造体名ですが、「Position」は「位置情報」、「Normal」は「法線」、「Texture」は「テクスチャ位置」を表します。このようにしておくと、様々な頂点定義型があるなかで、混乱を最小限に抑えることができます。
ここにはもう一つ、「頂点レイアウト」という「データの並びを定義する」データが必要です。これは「頂点レイアウト定義」といったもので

    const D3D12_INPUT_ELEMENT_DESC VertexPositionNormalTextureLayout[] =
    {
        { "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT,
               0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "NORMAL",0, DXGI_FORMAT_R32G32B32_FLOAT,
               0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT,
               0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    };

という記述で表されていますが、「D3D12_INPUT_ELEMENT_DESC」というのは構造体で、「VertexPositionNormalTextureLayout」というのは構造体の配列です。C言語風に初期化処理しています。このレイアウト定義は、実際に描画するときにアクセラレータに設定しなければなりませんので非常に重要です。
しかし、頂点定義と頂点レイアウトは、関連あるにもかかわらず、別に考えなければならないので混乱のもとになります。そのため、頂点定義のほう(VertexPositionNormalTexture側)に「GetVertexElement()」と「GetNumElements()」というstatic関数を設け、それを介して、その頂点レイアウト定義の情報を得られるようになっています。
使い方としては

    //レイアウト定義のポインタの取得
    VertexPositionNormalTexture::GetVertexElement();
    //レイアウト定義のサイズ
    VertexPositionNormalTexture::GetNumElements();

のようになります。これをDx12の場合「パイプラインステート」というオブジェクトの構築時にシェーダとともに設定します。その方法は実際にDx12描画の説明で行います。

このように、「VertexHelper.h」には、ほかにもいろいろなタイプの頂点定義、頂点レイアウトの定義が含まれていますので、0から作り始めた人は必要と思われるものを実装するとよいでしょう。

これは「Dx12」版のみならず、「Dx11」版でも同様です。
続いて「Dx11Lib」の話です。Dx11のソリューションを開いてください。新規に作る場合は「Dx11Lib」にDx12版同様、「VertexHelper.h」を作ってください。
最新のコミットなどを見ている人は「VertexHelper.h」を表示してください。Dx12版に合わせ、「VertexPositionNormalTexture頂点定義」のブロックを以下に示します。

    //--------------------------------------------------------------------------------------
    /// 位置と法線とテクスチャを持つ入力レイアウトの定義
    //--------------------------------------------------------------------------------------
    const D3D11_INPUT_ELEMENT_DESC VertexPositionNormalTextureLayout[] =
    {
        {"SV_Position",0, DXGI_FORMAT_R32G32B32_FLOAT,
             0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"NORMAL",0, DXGI_FORMAT_R32G32B32_FLOAT,
             0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"TEXCOORD",0, DXGI_FORMAT_R32G32_FLOAT,
             0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    };

    //--------------------------------------------------------------------------------------
    /// 位置と法線とテクスチャを持つ頂点の定義
    //--------------------------------------------------------------------------------------
    struct VertexPositionNormalTexture
    {
        VertexPositionNormalTexture()
        { }

        VertexPositionNormalTexture(XMFLOAT3 const& pos, XMFLOAT3 const& norm, XMFLOAT2 const& tex)
            : position(pos),
            normal(norm),
            textureCoordinate(tex)
        { }

        VertexPositionNormalTexture(FXMVECTOR pos, FXMVECTOR norm, FXMVECTOR tex)
            : position(pos),
            normal(norm),
            textureCoordinate(tex)
        { }

        Vector3 position;
        Vector3 normal;
        Vector2 textureCoordinate;

        static const D3D11_INPUT_ELEMENT_DESC* GetVertexElement(){
            return VertexPositionNormalTextureLayout;
        }
        static UINT GetNumElements(){
            return  ARRAYSIZE( VertexPositionNormalTextureLayout );
        }
    };

このように、ほとんどDx12と同じですが、頂点レイアウト定義の型が「D3D11_INPUT_ELEMENT_DESC」になっています。内容も少しですが違っています。
しかし

    //レイアウト定義のポインタの取得
    VertexPositionNormalTexture::GetVertexElement();
    //レイアウト定義のサイズ
    VertexPositionNormalTexture::GetNumElements();

のように同じようにレイアウト定義の情報を取得できる形になっています。
「Common.h」も修正しますのでこちらも参照してください。「Dx11Lib」には「Dx12Lib」にあったような「d3dx12.h」のようなファイルはありませんので注意してください。

このように「Dx12Lib」と「Dx11Lib」は、内容が違うクラスや構造体でも、「同じように扱える」「同じ名前の関数を持つ」ことで、2つのプラットフォームの違いを吸収しようとしています。
この考え方は、今後個別のクラスや関数を記述するときも、ある程度守られます。
BasCrossは「共有領域」をできるだけ増やし、「個別領域」であってもできるだけ同じように扱えることを目標にしています。

それでは次回は、「プリミティブ頂点を作成する」関数などを紹介します。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP