【3Dゲーム開発#03】ポリゴンの表示

前回までで、DirectX(Direct3D11)の基本的な設定をして、描画APIを使う準備を整えました。

今回は、DirectXを使って3D描画の最小要素となるポリゴン(三角形1つ)を描画していきます。

前回の記事:【3Dゲーム開発#02】Direct3D11のセットアップ

3Dゲーム開発記事のまとめ:「3Dゲーム開発」

描画の前に:数学ライブラリ・プリコンパイル済みヘッダーを使う用意

ポリゴン等3D描画に入っていくと、Math(数学系)のライブラリが必要になります。

ベクトルや行列の演算がでてきますし、座標系としてもfloatを3つセットや、16個セット(4×4)で扱いたいことも増えます。

ということで数学ライブラリを用意するのですが、DirectXでは基本的な数学ライブラリも提供しています。

#include <DirectXMath.h>

で利用が可能です。

ただ、これを今後数学を使いたい場所が出るたびに、すべてのファイルで #include としていくのは得策ではありません。

そこでプリコンパイル済みヘッダーというものを使います。

今回は、pch.hというプリコンパイル済みヘッダーファイルを追加して、#include <DirectXMath.h>や前回も使ったDX_SAFE_RELEASEといったマクロを記述して、これらをどこでも使える状態とします。

pch.hの内容は以下の通りです。

#pragma once

#include <d3d11.h>
#pragma comment(lib, "d3d11.lib")
#include <DirectXMath.h>
#define DX_SAFE_RELEASE(x)    { if(x) { (x)->Release(); (x)=nullptr; } }

class Renderer;

これをプリコンパイル済みヘッダーとして利用するには、ちょっとした設定が必要です。プリコンパイル済みヘッダーの設定方法は以下の記事で紹介していますので、詳しくはそちらを参照ください。

Visual StudioでC++プリコンパイル済みヘッダーの設定方法

ポリゴン描画

頂点構造体の作成

まずは1頂点の情報をもたせる構造体(struct)を作成しましょう。

いまはひとまずPositionさえ持てれば十分です。

上述のプリコンパイル済みヘッダーでDirectXMath.hが使えるようになっているので、以下のように3D座標をもつstruct定義をします。

#pragma once

struct Vertex
{
    DirectX::XMFLOAT3    Position = {};

    Vertex() {}
    Vertex(float x, float y, float z) {
        Position.x = x;
        Position.y = y;
        Position.z = z;
    }
};

また、Vertex.hはよく使う基礎的な構造体なので、pch.hにも追記しておきましょう。

Triangle構造体の作成

続いてポリゴンを表す、Vertexを複数(3つ)もったTriangleクラスを作りましょう。

こちらには、後述する頂点バッファーの作成用の関数CreateVertexBufferと、このTriangleを描画するメソッドDrawも実装準備しておきます。

今回作る三角形は3次元座標ですが、まずは2Dで見たときにもわかりやすいよう、XY座標で設定、Zは0、という構成で初期化します。

適当に原点から0.5ずつずらした正三角形で定義しています。

#pragma once

struct Triangle
{
    static constexpr size_t VERTEX_NUM = 3;
    Vertex   vertices[VERTEX_NUM];
    ID3D11Buffer* vertexBuffer = nullptr;

    Triangle();
    ~Triangle();
    bool CreateVertexBuffer(Renderer& renderer);
    void Draw(Renderer& renderer);
};
#include "Triangle.h"
#include "Renderer.h"

Triangle::Triangle()
{
    Vertices[0] = { 0.f, 0.5f, 0.f };
    Vertices[1] = { 0.5f, -0.5f, 0.f };
    Vertices[2] = { -0.5f, -0.5f, 0.f };
}

Triangle::~Triangle()
{
    DX_SAFE_RELEASE(VertexBuffer);
}

bool Triangle::CreateVertexBuffer(Renderer& renderer)
{
    // 後述
}

void Triangle::Draw(Renderer& renderer)
{
    // 後述
}

頂点バッファーの作成

ここからいよいよ本格的に描画の準備(描画パイプラインの組み立て)となります。

まずは、描画処理に流し込む頂点データを設定する必要があります。

この流し込むデータ定義を「頂点バッファ」といいます。

まずは、頂点バッファ作成のコードを見てみましょう。以下のとおりです。

bool Triangle::CreateVertexBuffer(Renderer& renderer)
{
    D3D11_BUFFER_DESC vertexBufferDesc = {};
    vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    vertexBufferDesc.ByteWidth = sizeof(Vertex) * 3;
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

    D3D11_SUBRESOURCE_DATA vertexSubData;
    vertexSubData.pSysMem = Vertices;

    auto hr = renderer.GetDevice()->CreateBuffer(
        &vertexBufferDesc,
        &vertexSubData,
        &VertexBuffer
    );

    if (FAILED(hr)) return false;

    return true;
}

大きな手順としては

  • 頂点バッファ定義の構造体(D3D11_BUFFER_DESC)を設定
  • データ本体であるサブリソース定義(D3D11_SUBRESOURCE_DATA)を頂点データで初期化
  • これらの定義からCreateBufferメソッドで頂点バッファ(ID3D11Buffer)を取得

となります。

D3D11_BUFFER_DESCD3D11_SUBRESOURCE_DATAには様々なOptionがありますが、ひとまず描画するだけなら上記設定でOKです。

オプションが気になる場合公式ドキュメント等を参照しましょう。(D3D11_BUFFER_DESCD3D11_SUBRESOURCE_DATA)。

1点だけ補足すると、サブリソースというのが少しわかりにくいですが、DirectXでは頂点バッファなどのリソース(メモリ領域)はサブリソースの集合で定義する、という形で構成されており、バッファリソースを作る際には必ずサブリソース定義を介して作成する決まりとなっているためです。

頂点バッファでは、使えるサブリソース定義は1つなので冗長に感じますが、テクスチャの複数サイズ定義などの場合には、サブリソースが複数あることで柔軟なリソースが作れるといった、汎用的な拡張性の意味があります。

シェーダーの作成

描画データの用意ができたので、それを描画するために必須となるシェーダー「頂点シェーダー」と「ピクセルシェーダー」を用意しましょう。

頂点シェーダーの作成

シェーダーは、Visual Studioのプロジェクトへのファイル追加手順で、通常ファイルと同様にウィザードから作成できます。

新しい項目の追加ウィザードで、[HLSL] → [頂点シェーダーファイル] を選択して、適当な名前を決めて(ここではデフォルトのVertexShader.hlslのまま)追加します。(下画像参照)

vertex_shader_add

追加した頂点シェーダーの内容はあらかじめ以下のようになっていると思います。

float4 main( float4 pos : POSITION ) : SV_POSITION
{
    return pos;
}

頂点シェーダーは、各頂点ごとに呼ばれます。

このコードは、描画データとして渡された頂点ごとのデータとしてPositionを受け取って、とくに変更しないまま利用する(返す)、というコードになっています。

POSITIONやSV_POSITIONはhlslのセマンティクス(意味づけ)といい、描画データとシェーダーへの変数との紐付け行うものです。

後述するシェーダーのコンパイル時に、セマンティクスを指定して、入力データをどのように解釈するかがセマンティクスをベースに決定されます。

公式:セマンティクス

ピクセルシェーダーの作成

同様にしてピクセルシェーダーも追加します。

新しい項目の追加ウィザードで、[HLSL] → [ピクセルシェーダーファイル] を選択して、適当な名前を決めて(ここではデフォルトのPixelShader.hlslのまま)追加します。(下画像参照)

pixel_shader_add

追加したピクセルシェーダーの内容はあらかじめ以下のようになっていると思います。

float4 main() : SV_TARGET
{
    return float4(1.0f, 1.0f, 1.0f, 1.0f);
}

ピクセルシェーダーは、入力データが描画される位置の画面ピクセルごとに呼ばれます。

このコードでは、そのピクセルの色を白で描画する、という意味になります。

参考:シェーダーのプロパティの確認

上記のようにシェーダーをウィザードから作成しましたが、場合によっては既存のファイルを追加することもありますね。

そこで、シェーダーのプロパティ設定がどうなっているか確認して、既存ファイルのプロパティも正しい設定ができるようになりましょう。

例えば、頂点シェーダーは以下のようになっています。

vertex_shader_props

重要な項目としてはひとまずは

  • エントリポイント : シェーダーがどの関数名で実行されるか
  • シェーダーの種類 : 頂点シェーダー、ピクセルシェーダーなど
  • シェーダーモデル : シェーダーの機能レベル

といったところです。

既存のファイルを追加した際もこのあたりのプロパティが正しいか確認・設定するようにしましょう。

シェーダーを使えるようにする(コンパイル・入力レイアウト作成)

必要最低限のシェーダーの準備ができたので、これらをDirectXで使えるようにしましょう。

そのためには、シェーダーをコンパイルしてシェーダーオブジェクトを作成する必要があります。

シェーダーをコンパイルする方法は複数ありますが、ここではD3DCompileFromFile()APIを使ってコンパイルしましょう。

この方法なら、実行後にコンパイルするのでVisual Studioプロジェクト内でビルドするだけで完結できます。

今回はShaderを管理するShaderクラスを作り、RendererにCompileShaderメソッドを実装してコンパイルします。

また、頂点シェーダーにはどういったデータ構造の頂点データが渡ってくるかを定義した「入力レイアウト(InputLayout)」も必要です。

頂点シェーダーとセットで必要なので、コンパイル処理に合わせて組み込みましょう。

以下が、シェーダーコンパイル、頂点入力レイアウトの作成の一通りの処理です。

bool Renderer::CompileShader(const WCHAR* vsPath, const WCHAR* psPath, Shader& outShader)
{
    ID3DBlob* vsBlob = nullptr;
    ID3DBlob* errBlob = nullptr;
    auto pDevice = GetDevice();

    // シェーダーコンパイル
    auto hr = D3DCompileFromFile(
        vsPath,
        nullptr,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        "main",
        "vs_4_0",
        D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
        0, 
        &vsBlob, 
        &errBlob
    );
    if (FAILED(hr)) return false;

    // 頂点シェーダ作成(シェーダオブジェクト作成)
    ID3D11VertexShader* pVertexShader = nullptr;
    hr = pDevice->CreateVertexShader(
        vsBlob->GetBufferPointer(),
        vsBlob->GetBufferSize(),
        nullptr,
        &pVertexShader
    );
    if (FAILED(hr)) return false;

    // 入力レイアウトオブジェクト作成
    ID3D11InputLayout* pInputLayout = nullptr;
    D3D11_INPUT_ELEMENT_DESC layout[] = {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    };
    hr = pDevice->CreateInputLayout(
        layout,
        _countof(layout),
        vsBlob->GetBufferPointer(),
        vsBlob->GetBufferSize(),
        &pInputLayout
    );
    if (FAILED(hr)) return false;

    // ピクセルシェーダー作成
    ID3DBlob* psBlob = nullptr;
    hr = D3DCompileFromFile(
        psPath,
        nullptr,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        "main",
        "ps_4_0",
        D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
        0, 
        &psBlob, 
        &errBlob
    );

    ID3D11PixelShader* pPixelShader = nullptr;
    hr = pDevice->CreatePixelShader(
        psBlob->GetBufferPointer(),
        psBlob->GetBufferSize(),
        nullptr,
        &pPixelShader
    );
    if (FAILED(hr)) return false;

    outShader.pVertexShader = pVertexShader;
    outShader.pPixelShader = pPixelShader;
    outShader.pInputLayout = pInputLayout;

    return true;
}
struct Shader
{
    Shader() {}
    ~Shader() {
        DX_SAFE_RELEASE(pVertexShader);
        DX_SAFE_RELEASE(pPixelShader);
        DX_SAFE_RELEASE(pInputLayout);
    }

    ID3D11VertexShader* pVertexShader = nullptr;
    ID3D11PixelShader* pPixelShader = nullptr;
    ID3D11InputLayout* pInputLayout = nullptr;
};

あとはShaderのインスタンスをRendererに持たせて、初期化時にコンパイルされるように呼び出しましょう。

class Renderer
{
    ...
    Shader  DefaultShader;
    ...
}

bool Renderer::Initialize(HWND hWindow)
{
    ...
    CompileShader(L"VertexShader.hlsl", L"PixelShader.hlsl", DefaultShader);
    ...
}

これでシェーダーを使う準備ができました。

シェーダーを使って描画

ようやくポリゴン描画準備が整いました!

これまで用意した、描画データ(Triangle)、シェーダー(Shader)を使って描画しましょう。

準備した描画データ・シェーダーを使う設定をしてあげたうえでDrawを呼べば描画されます。

void Renderer::Draw()
{
    if (!pImmediateContext_ || !pRenderTargetView_) return;

    pImmediateContext_->OMSetRenderTargets(1, &pRenderTargetView_, nullptr);

    float color[] = { 0.f, 0.f, 0.f, 0.f };
    pImmediateContext_->ClearRenderTargetView(pRenderTargetView_, color);

    pImmediateContext_->IASetInputLayout(defaultShader_.pInputLayout);
    pImmediateContext_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    pImmediateContext_->VSSetShader(defaultShader_.pVertexShader, nullptr, 0);
    pImmediateContext_->PSSetShader(defaultShader_.pPixelShader, nullptr, 0);

    sampleTriangle_.Draw(*this);
}
void Triangle::Draw(Renderer& renderer)
{
    auto pDeviceContext = renderer.GetDeviceContext();
    uint32_t strides[1] = { sizeof(Vertex) };
    uint32_t offsets[1] = { 0 };
    pDeviceContext->IASetVertexBuffers(0, 1, &VertexBuffer, strides, offsets);
    pDeviceContext->Draw(VERTEX_NUM, 0);
}

結果表示

結果として以下のように真っ白な三角形が描画されます。

draw_triangle

次はこの三角形を移動・回転・スケール変換してみます。

次回の記事:【3Dゲーム開発 – 13】2次元変換

3Dゲーム開発記事のまとめ:「3Dゲーム開発」

コメント

  1. […] シェーダーに渡す頂点データは、入力レイアウト(=どういった頂点データか)を教えて上げる必要がありました(参考:入力レイアウト作成)。 […]

タイトルとURLをコピーしました