【3Dゲーム開発#09】3Dモデルの表示

3Dゲーム開発

今回はいよいよ3Dモデルデータを使った3Dモデルの表示、をしていきましょう。

3Dゲーム感が一気に増してくる頃合いです、がんばっていきましょう。

前回の記事:

【3Dゲーム開発#08】3Dから2Dに落とす「ビュー変換・透視投影変換」
いよいよ(やっと)今回から3D描画のほうにはいっていきます。3D描画では奥行き(z)要素が加わりますが、人間の視覚と同様の描画をするには、単純に奥行きをいれればいいというだけのことではありません。また、3Dゲームではカメラ操作ができるのが一...

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

モデルデータの用意

まずは、モデルデータがないと話になりませんね。モデルデータを用意しましょう。

今回はシンプルなボックスを表示しましょう。

以下の画像のようにBlenderで初期表示されるボックスを、FBX形式で出力したデータを以下のリンクに配置してあります。こちらをダウンロードして利用しましょう。

box.fbxのDownload

blender_box

もちろん自分でBlenderで出力して用意してもかまいません。

Blenderを少しでも使えると、こういった簡単なテストモデルデータを用意したりできるので、使ってみるのも良いと思います。

参考:なぜFBX形式を使うか

Blenderのエクスポートメニューには様々な形式のファイルがありますが、今回はこの中から.fbxを使うとしました。

FBX形式を使うのは以下のような特徴があるためです。

  • メッシュ以外にも骨・アニメーション・マテリアルなど、多くのデータを格納可能
  • ゲーム制作の中間ファイルフォーマットとして最も一般的な形式の1つ
  • ゲームエンジン(Unity、Unreal Engine)での推奨もFBX
  • AutoDeskが管理しているフォーマットなので安心

ざっというと、3Dゲーム向けのデータとしてなんでも含めることができて汎用性が高い、その上業界的にも標準的に利用されているから、というところです。

特に、3Dゲームを作るなら、すぐにアニメーションデータも必要となりますので、FBXを使っておくのが今後のためにも無難です。

これだけ汎用的な分、ファイルサイズとしては大きくなりがちですが、中間ファイル(様々なソフトで扱える形式)として必要なのは汎用性です、こだわりがなければFBXで間違いないかと思います。

モデルデータのインポート:assimpの導入

モデルデータをプログラム上で扱うには、

  • データをロード
  • データをフォーマットに沿って解釈(パース)
  • 頂点データをメモリ上に作成

といったことが必要になります。

特に「データをフォーマットに沿って解釈(パース)」は、自前でやるのは大変ですので、こういったデータを利用するためにはライブラリを使うことが多いです。

今回は、「assimp」という様々な形式のデータのインポートが可能なライブラリを利用させてもらいましょう。

ちなみにfbx形式のデータなら、AutoDesk公式のFBXSDKを使うことでもfbxデータのロード、解釈が可能です。

ただ今後、FBX形式以外のデータも利用できたほうがなにかと便利ですので(フリーサイトでFBX以外のデータを拾うなど)、多くの形式に対応したassimpを使っていきます。

NuGetでAssimpを使えるように

Assimpは、Visual Studioのパッケージ管理機能「NuGet」で公開されているので、NuGetを使うことで簡単に導入できます。

Visual Studioのメニューから

[ツール] → [NuGet パッケージマネージャー] → [ソリューションのNuGetパッケージの管理]

を選択することでNuGetのパッケージ管理画面が開きます。

ここで右上のパッケージソース(↓画像の赤枠)から nuget.org を選びましょう。(nuget.orgがない場合は後述参照)

nuget_0

検索フィルタで「assimp」といれて、今回は最初にでてくる「Assimp」ではなく「AssimpCpp」↓をインストールしましょう。

「Assimp」の方はバージョンが低いためか、こちらで試す限りfbxのロードはうまくいかないため、バージョンも新しくfbxにも問題なく使えるAssimpCppを使っていきます。

assimp_cpp

これだけで、そのプロジェクトでAssimpを使用する準備が整いました。

NuGetでインストールしたパッケージはソリューションのあるディレクトリの「package」というディレクトリ以下で管理されています。

パッケージソースに nuget.org がない場合の追加方法

パッケージソースにnuget.orgがない場合もあるので、ない場合はその右にある歯車(設定)から、自分で追加しましょう。

nuget_org_setting

右上「+」ボタンから追加します。

名前は自由ですが「nuget.org」が無難です。

ソースは「https://api.nuget.org/v3/index.json」となります。(参考)

Assimpの使い方概要

Assimpの詳細な使い方は公式マニュアル「Assimp C++インターフェース」を参照しましょう。

基本的には、このマニュアルにあるとおり

  • Assimp::Importerを使って、モデルをロード・解釈
  • モデルデータは aiScene 構造体で取得
  • aiSceneに含まれる aiMesh でメッシュデータを取得
Assimp::Importer importer;
const aiScene* scene = importer.ReadFile(pFile, Flag);
const aiMesh* mesh = scene->mMeshes[0];
// meshから頂点データなどを取得

今回実際に使ったコードは以降で見ていきます。

モデルのロード・解釈

さっそく、実際に用意したデータをロードして表示まで組み込んでみましょう。

Modelクラスの追加

今まではTriangleクラスで表示してましたが、これからはModelを表示していくのでModelクラスを追加します。

Modelは「複数のメッシュで構成される1つの表示物」という定義とします。

ですので、Meshを複数持てるように構成します。

Meshはもちろん1つでも構わず、今回のBoxのようなシンプルなモデルはMesh1つとなります。

class Mesh;

class Model
{
public:
    Model();
    ~Model();

    bool Setup(Renderer& renderer, const char* filePath);
    void Terminate();

    void Draw(Renderer& renderer);

private:
    void setupTransform(Renderer& renderer);

private:
    Mesh*   meshes_ = nullptr;
    u_int   meshNum_ = 0;
};
#include "Model.h"
#include "Mesh.h"
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

Model::Model()
{
}

Model::~Model()
{
    SAFE_DELETE_ARRAY(meshes_);
}

bool Model::Setup(Renderer& renderer, const char* filePath)
{
    // load処理
    Assimp::Importer importer;
    u_int flag = aiProcess_Triangulate;
    auto pScene = importer.ReadFile(filePath, flag);

    if (pScene == nullptr) return false;

    meshNum_ = pScene->mNumMeshes;
    if (meshNum_ > 0) {
        meshes_ = new Mesh[meshNum_];
        for (u_int meshIdx = 0; meshIdx < pScene->mNumMeshes; ++meshIdx) {
            auto pMeshData = pScene->mMeshes[meshIdx];
            if (meshes_[meshIdx].Setup(renderer, pMeshData) == false) {
                return false;
            }
        }
    }

    return true;
}

void Model::Terminate()
{
    for (u_int meshIdx = 0; meshIdx < meshNum_; ++meshIdx) {
        meshes_[meshIdx].Terminate();
    }
    SAFE_DELETE_ARRAY(meshes_);
    meshNum_ = 0;
}

void Model::Draw(Renderer& renderer)
{
    for (u_int meshIdx = 0; meshIdx < meshNum_; ++meshIdx) {
        meshes_[meshIdx].Draw(renderer);
    }
}

void Model::setupTransform(Renderer& renderer)
{
    // Triangle同様の処理
    ...
}

Meshクラスの追加

そして、Modelがもつ各メッシュを管理・描画するMeshクラスを追加します。

このMeshがこれまで描画で使っていた、頂点データの構築や、描画APIを叩くといった、3Dオブジェクト描画のコアの部分となります。

また描画処理は、これまでにやっていた頂点バッファのみを指定する方法から、頂点インデックスバッファも指定する形に変わっています。

そういった点を踏まえながら、次項でMeshの処理の中身とともに見ていきます。

struct aiMesh;

class Mesh
{
public:
    Mesh();
    ~Mesh();

    bool Setup(Renderer& renderer, aiMesh* pMeshData);
    void Terminate();
    void Draw(Renderer& renderer);

private:
    bool createVertexBuffer(Renderer& renderer);
    bool createIndexBuffer(Renderer& renderer);
    void destroyVertexBuffer();
    void destroyIndexBuffer();

private:
    // データの解釈ワーク
    Vertex*     vertices_ = nullptr;
    u_int*      indices_ = nullptr;
    u_int       vertexNum_ = 0;
    u_int       indexNum_ = 0;

    // バッファリソース
    ID3D11Buffer* vertexBuffer_ = nullptr;
    ID3D11Buffer* indexBuffer_ = nullptr;
};

インデックスバッファを使った描画処理にする

頂点インデックスを使う意味

通常メッシュデータは、頂点データとは別に、頂点インデックスデータも持ちます。

頂点インデックスデータは、頂点を使う組み合わせを頂点データのインデックスで指定したデータ、です。

実際の例を見たほうがわかりやすいので、以下の図の例で見てみましょう。

why_index_buffer

この図では、①と②の三角形2つを描画しています。

このとき、①はA,B,Cの頂点データ、②はB,C,Dの頂点データを使っています。

このとき、頂点データリストのみで描画しようとすると、頂点データ配列としては

[A, B, C, B, C, D]

という配列が必要になりますね。

こうなると、全く同じB,Cの頂点データが、重複して利用されることになります。

重複すると、重複した分頂点データ用のメモリ(頂点バッファなど)が大きくなりますし、頂点シェーダの計算も同じ計算を複数行うことになります。

こういった2つの三角形のみなら、大した問題ではないですが、複雑なメッシュになるとこのコストは非常に無駄なコストとなります。

そこで、頂点データとは別に、頂点インデックスデータを持たせて解決します。

この例なら、

  • 頂点データは、[A, B, C, D]の4つのみ
  • インデックスは前から順に0, 1, 2, 3
  • 頂点インデックスデータは、[0, 1, 2, 1, 2, 3]

とすることで、頂点データは4つで済ませるということになります。

頂点データ・頂点インデックスデータの解釈

ということで、改めてMeshの処理の中の、頂点データ、頂点インデックスデータを取得しているところが以下となります。

aiMeshから頂点データ(mVertices)の取得、頂点インデックスデータ(mFaces の mIndices)の取得、をします。

bool Mesh::Setup(Renderer& renderer, aiMesh* pMeshData)
{
    // 頂点データ取得
    vertexNum_ = pMeshData->mNumVertices;
    vertices_ = new Vertex[vertexNum_];
    for (u_int vertexIdx = 0; vertexIdx < vertexNum_; ++vertexIdx) {
        auto& pos = pMeshData->mVertices[vertexIdx];
        vertices_[vertexIdx].Position = XMFLOAT3(pos.x, pos.y, pos.z);
        constexpr float COLOR = 0.5f;
        vertices_[vertexIdx].Color = XMFLOAT4(COLOR, COLOR, COLOR, 1.f);
    }
    if (createVertexBuffer(renderer) == false) {
        return false;
    }

    // 頂点インデックスデータ取得(TriangleList前提)
    indexNum_ = pMeshData->mNumFaces * 3;
    indices_ = new u_int[indexNum_];
    for (u_int faceIdx = 0; faceIdx < pMeshData->mNumFaces; ++faceIdx) {
        auto& face = pMeshData->mFaces[faceIdx];
        assert(face.mNumIndices == 3);
        for (u_int idx = 0; idx < 3; ++idx) {
            indices_[faceIdx * 3 + idx] = face.mIndices[idx];
        }
    }
    if (createIndexBuffer(renderer) == false) {
        return false;
    }

    return true;
}

インデックスバッファを使った描画処理で描画

DirectX描画処理をIndexBufferを使った形式で、リソースバッファ作成、描画していきます。

VertexBuffer、IndexBufferの作成

インデックスバッファを使って描画するので、これまではVertexBuffer作成のみでしたが、加えて同様にIndexBufferを作成します。

インデックスバッファは、ただのunsigned intの配列なので、4Byteのindex数分のサイズでリソース確保します。

BindFlagsだけ D3D11_BIND_INDEX_BUFFER にするのを忘れないようにしましょう。

頂点バッファ作成時よりも単純な処理ですね。

bool Mesh::createVertexBuffer(Renderer& renderer)
{
    D3D11_BUFFER_DESC vertexBufferDesc = {};
    vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    vertexBufferDesc.ByteWidth = sizeof(Vertex) * vertexNum_;
    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;
}

bool Mesh::createIndexBuffer(Renderer& renderer)
{
    D3D11_BUFFER_DESC indexBufferDesc = {};
    indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    indexBufferDesc.ByteWidth = indexNum_ * 4;
    indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;

    D3D11_SUBRESOURCE_DATA indexSubData;
    indexSubData.pSysMem = indices_;

    auto hr = renderer.GetDevice()->CreateBuffer(
        &indexBufferDesc,
        &indexSubData,
        &indexBuffer_
    );

    if (FAILED(hr)) return false;

    return true;
}

IndexBufferを設定した描画

あとは今までのVertexBufferのみでの描画処理に

  • IASetIndexBuffer() でインデックスバッファをセット
  • 描画は DrawIndexed() をコール

とするだけです。

void Mesh::Draw(Renderer& renderer)
{
    auto pDeviceContext = renderer.GetDeviceContext();
    size_t strides[1] = { sizeof(Vertex) };
    size_t offsets[1] = { 0 };
    pDeviceContext->IASetVertexBuffers(0, 1, &vertexBuffer_, strides, offsets);
    pDeviceContext->IASetIndexBuffer(indexBuffer_, DXGI_FORMAT_R32_UINT, 0);

    pDeviceContext->DrawIndexed(indexNum_, 0, 0);

}

その他の変更点

今回は、Modelクラスの作成がほぼすべてで、あとは使うだけです。以下のようにSceneManagerで使いましょう。

また、ソースコード全体の量も多くなってきたので、細かい点すべてを挙げることは難しいです。

記事末にはこのプロジェクトの全ソースコードがあるので、うまくいかない場合などはそちらと見比べる、Diffをとるなどしてみましょう。

SceneManagerで使うモデルをSetup

ModelクラスでModelデータを扱えるようになっているので、あとは実際に使うモデルデータのパスを指定して、ModelのSetup、描画を呼んであげればよいです。

SceneManagerに以下のようにmodelの処理を追加しましょう。また、前回までのsampleTriangleの処理はもう使わないので消してしまいます。

モデルのパスは、今回はわかりやすい&間違いないということで絶対パスで指定していますので、こちらは自分の環境に合わせて変えましょう。

void SceneManager::Initialize(Renderer& renderer)
{
    ...
    const char* MODEL_PATH = "C:/Users/Keita/projects/GameDev/Assets/box.fbx";
    sampleModel_.Setup(renderer, MODEL_PATH);
}

void SceneManager::Terminate()
{
    sampleModel_.Terminate();
}

void SceneManager::Draw()
{
    ...
    sampleModel_.Draw(*pRenderer_);
}

ボックスの描画結果

前回同様にCameraを上に移動だけさせて、Boxを描画した結果が以下となります。

draw_model

ソースコード

以下が今回の実装があるコードです。

https://github.com/prog-log/GameDev/blob/master/06-DrawModel

コメント

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