504.【.bmfファイル】のデータ構造

 この項ではFbx2BinVS2015が作成するデータ構造についての説明です。
 まず初めにお断りしたいのは、このデータ構造が確定的なものではないということです。
 .bmfという拡張子はバイナリモデルファイルを略した(英語化)したものですが、全然確定したものではありません。
 これは責任逃れのために述べているのではなく、ファイルフォーマットというのは各自あるいは各チームが考えるものだということです。
 ですから.bmfは、1つの可能性(方法)を提案しているもので、これがベストであるわけでもなく、むしろ、FBXからの独自フォーマットへの変換及び実装は、どんどん新しく定義してほしいということです。
 そのことを前提にこの項の説明を行いたいと思います。

バイナリファイルとは

 ここでバイナリファイルについて少し説明します。
 コンピュータのデータ(プログラムもそうですが)は、基本帝にバイト単位で考えられます。
 1バイトは8ビットであり、ここには0x00から0xFFまでの256個のデータを収めることができます。
 この中の番号に、人間に直接伝えられる文字を割り当てたのがアスキーコードをはじめとする文字コードです。
 アスキーコード1バイト文字で。記号を含めても0x00から0xFFまでの番号をすべて使用しているわけではありません。日本語などのマルチバイト文字を含めてもすべてのコードを使用しません。
 バイナリファイル文字コード以外のデータも扱える(いうなら0x00から0xFFのどのコードも使用する可能性がある)データ形式です。ですから、文字コードのみ使用されるデータをテキストデータと称しますが、バイナリデータテキストデータを含むすべてのデータを指します。
 さてそのことを前提に3.141592のいう数値をデータ化する場合を考えてみましょう。テキスト形式で考えれば、この中には記号を含め8文字あります。すべてアスキーコードないですので8バイトです。しかし、バイナリで案がえればfloat型であれば32ビット(つまり4バイト)ですから、半分の大きさで済みます(浮動小数点なので誤差もありますが)。つまりテキスト形式であればいったん文字に変換した文字データで考えますが、バイナリでは直接データにするので、その分効率が良くなります。
 ただ、64ビット環境であれば3.141592はテキストと同じ8バイト使用しますので大きさは一緒ですが、その分精度も15桁くらいになります(floatの精度は7桁くらいです)。ここではややこしいのでfloat型で考えます。

ブロックヘッダ

 3Dモデルのデータは、いろんな内容を含める必要があります。頂点データ、インデックスデータ、マテリアルなどです。アニメーションを形成するボーン行列の配列も必要です。そういう違ったパターンのデータを一つのバイナリファイルに収めるにはどうしたらいいでしょうか?
 すめてのモデルファイルのサイズが同じであれば、先頭から何バイトは頂点、次はインデックス・・・のようにできますが、そんなはずはありません。頂点データが1メガ続く場合もあれば、50キロくらいの場合もあります。そのようなサイズの違った複数のブロックを管理する手法としてブロックヘッダという考え方があります。
 ブロックヘッダはデータ形式とバイト数を管理する小さな構造体です。それは、そのヘッダの直後から、ヘッダで指定したデータが、ヘッダに記載されているバイト数続くという意味になります。
 図解すると以下のようになります。

 

図0504a

 

 ブロックデータをコード化したものは以下のようになります。
//--------------------------------------------------------------------------------------
/// ブロックタイプ定義(モデルメッシュ読み込み用)
//--------------------------------------------------------------------------------------
enum class BlockType {
    Vertex, ///< 頂点
    Index,  ///< インデックス
    Material,   ///< マテリアル
    MaterialCount,  ///< マテリアル数
    SkinedVertex,   ///< スキン頂点
    BoneCount,  ///< ボーン数
    AnimeMatrix,    ///< アニメーション行列
    VertexWithTangent,  ///< タンジェント付き頂点
    SkinedVertexWithTangent,    ///< スキンタンジェント付き頂点
    End = 100   ///< 終了
};

//--------------------------------------------------------------------------------------
/// ブロックヘッダ構造体
//--------------------------------------------------------------------------------------
struct BlockHeader {
    BlockType m_Type;   ///< ブロックタイプ
    UINT m_Size;    ///< バイト数
};
 BlockHeader構造体enum class BlockTypeのメンバとバイト数で形成されています。enum classは数値ですから、数値とサイズ8バイトのデータです。このあと、ヘッダでしたいされたタイプのデータが指定したサイズ続きます。

データの保存

 以上を踏まえて、データの保存を考えてみます。
 以下はスタティックメッシュの保存部分ですが、
void FbxMeshObject::SaveStaticBinFile(const wstring& Dir, const wstring& FileName, 
    size_t MeshIndex, float Scale) {
    try {
        string header("BDV1.0");
        if (header.size() < 16) {
            header.resize(16, '\0');
        }
        vector<VertexPositionNormalTexture> vertices;
        vector<VertexPositionNormalTangentTexture> vertices_withtan;

        vector<uint16_t> indices;
        vector<MaterialEx> materials;
        vector< shared_ptr<TextureResource> > textures;
        auto PtrDraw = GetComponent<BcPNTStaticModelDraw>(false);
        auto PtrTanDraw = GetComponent<BcPNTnTStaticModelDraw>(false);
        shared_ptr<FbxMeshResource2> Mesh;
        if (m_WithTangent) {
            Mesh = dynamic_pointer_cast<FbxMeshResource2>(PtrTanDraw->GetMeshResource());
            Mesh->GetStaticVerticesIndicesMaterialsWithTangent(vertices_withtan,
                 indices, materials);
            for (auto& v : vertices_withtan) {
                v.position *= Scale;
            }
        }
        else {
            Mesh = dynamic_pointer_cast<FbxMeshResource2>(PtrDraw->GetMeshResource());
            Mesh->GetStaticVerticesIndicesMaterials(vertices, indices, materials);
            for (auto& v : vertices) {
                v.position *= Scale;
            }
        }

        wstring filename = Dir + FileName;

        ofstream ofs(filename, ios::out | ios::binary);
        ofs.write(header.c_str(), 16);
        //頂点の保存
        BlockHeader VerTexHeader;
        if (m_WithTangent) {
            VerTexHeader.m_Type = BlockType::VertexWithTangent;
            VerTexHeader.m_Size = 
            (UINT)vertices_withtan.size() * sizeof(VertexPositionNormalTangentTexture);
            ofs.write((const char*)&VerTexHeader, sizeof(BlockHeader));
            ofs.write((const char*)&vertices_withtan.front(), VerTexHeader.m_Size);
        }
        else {
            VerTexHeader.m_Type = BlockType::Vertex;
            VerTexHeader.m_Size = (UINT)vertices.size() * sizeof(VertexPositionNormalTexture);
            ofs.write((const char*)&VerTexHeader, sizeof(BlockHeader));
            ofs.write((const char*)&vertices.front(), VerTexHeader.m_Size);
        }
        //インデックスの保存
        BlockHeader IndexHeader;
        IndexHeader.m_Type = BlockType::Index;
        IndexHeader.m_Size = (UINT)indices.size() * sizeof(uint16_t);
        ofs.write((const char*)&IndexHeader, sizeof(BlockHeader));
        ofs.write((const char*)&indices.front(), IndexHeader.m_Size);
        //マテリアルの保存
        //マテリアル数のヘッダの保存
        BlockHeader MaterialCountHeader;
        MaterialCountHeader.m_Type = BlockType::MaterialCount;
        MaterialCountHeader.m_Size = (UINT)materials.size();
        ofs.write((const char*)&MaterialCountHeader, sizeof(BlockHeader));
        //マテリアル本体の保存
        wchar_t Drivebuff[_MAX_DRIVE];
        wchar_t Dirbuff[_MAX_DIR];
        wchar_t FileNamebuff[_MAX_FNAME];
        wchar_t Extbuff[_MAX_EXT];
        BlockHeader MaterialHeader;
        MaterialHeader.m_Type = BlockType::Material;
        for (auto mat : materials) {
            wstring TextureFileName = mat.m_TextureResource->GetTextureFileName();
            ::ZeroMemory(Drivebuff, sizeof(Drivebuff));
            ::ZeroMemory(Dirbuff, sizeof(Dirbuff));
            ::ZeroMemory(FileNamebuff, sizeof(FileNamebuff));
            ::ZeroMemory(Extbuff, sizeof(Extbuff));
            //モジュール名から、各ブロックに分ける
            _wsplitpath_s(TextureFileName.c_str(),
                Drivebuff, _MAX_DRIVE,
                Dirbuff, _MAX_DIR,
                FileNamebuff, _MAX_FNAME,
                Extbuff, _MAX_EXT);
            TextureFileName = FileNamebuff;
            TextureFileName += Extbuff;

            SaveMaterialEx SaveMat;
            SaveMat.m_StartIndex = mat.m_StartIndex;
            SaveMat.m_IndexCount = mat.m_IndexCount;
            SaveMat.m_Diffuse = mat.m_Diffuse;
            SaveMat.m_Specular = mat.m_Specular;
            SaveMat.m_Ambient = mat.m_Ambient;
            SaveMat.m_Emissive = mat.m_Emissive;
            UINT TextureStrSize = (TextureFileName.size() + 1) * sizeof(wchar_t);
            MaterialHeader.m_Size = sizeof(SaveMaterialEx) + TextureStrSize;
            ofs.write((const char*)&MaterialHeader, sizeof(BlockHeader));
            ofs.write((const char*)&SaveMat, sizeof(SaveMaterialEx));
            ofs.write((const char*)TextureFileName.c_str(), TextureStrSize);

        }
        //End(ヘッダのみ)
        BlockHeader EndHeader;
        EndHeader.m_Type = BlockType::End;
        EndHeader.m_Size = 0;
        ofs.write((const char*)&EndHeader, sizeof(BlockHeader));
        ofs.close();

    }
    catch (...) {
        throw;
    }

}
 赤くなっているところがファイルの保存を行っているところです。
 最初の
    ofs.write(header.c_str(), 16);
 はファイルヘッダです。関数の先頭で
    string header("BDV1.0");
    if (header.size() < 16) {
        header.resize(16, '\0');
    }
 のように設定されています。ファイルヘッダはこのファイルが間違いなくBaseCrossで使用するデータかを表すものです。拡張子だけでファイルを特定するのは危険です。世の中にはいろんなデータがありますから。
 ここでは"BDV1.0"という文字列が入り、その後に0が合わせて16バイト分あります。
 続くデータはブロックヘッダです。
    //頂点の保存
    BlockHeader VerTexHeader;
    if (m_WithTangent) {
        VerTexHeader.m_Type = BlockType::VertexWithTangent;
        VerTexHeader.m_Size = 
        (UINT)vertices_withtan.size() * sizeof(VertexPositionNormalTangentTexture);
        ofs.write((const char*)&VerTexHeader, sizeof(BlockHeader));
        ofs.write((const char*)&vertices_withtan.front(), VerTexHeader.m_Size);
    }
    else {
        VerTexHeader.m_Type = BlockType::Vertex;
        VerTexHeader.m_Size = (UINT)vertices.size() * sizeof(VertexPositionNormalTexture);
        ofs.write((const char*)&VerTexHeader, sizeof(BlockHeader));
        ofs.write((const char*)&vertices.front(), VerTexHeader.m_Size);
    }
 頂点用のブロックヘッダを定義し、タンジェントが含まれるかどうかBlockType::VertexWithTangentもしくはBlockType::Vertexを保存しています。
 続いてはインデックスです。こちらはタンジェントが含まれるかどうかで違いはありませんので
    //インデックスの保存
    BlockHeader IndexHeader;
    IndexHeader.m_Type = BlockType::Index;
    IndexHeader.m_Size = (UINT)indices.size() * sizeof(uint16_t);
    ofs.write((const char*)&IndexHeader, sizeof(BlockHeader));
    ofs.write((const char*)&indices.front(), IndexHeader.m_Size);
 のように書けます。このように、マテリアルも保存し、最後に
    //End(ヘッダのみ)
    BlockHeader EndHeader;
    EndHeader.m_Type = BlockType::End;
    EndHeader.m_Size = 0;
    ofs.write((const char*)&EndHeader, sizeof(BlockHeader));
    ofs.close();
 でデータの終了を表します。

データの読み込み

 Fbx2BinVS2015データを書きだすだけです。読み込みはゲーム側になります。
 以下はスタティックモデルメッシュリソースに読み込むコードです。
void MeshResource::ReadBaseData(const wstring& BinDataDir, const wstring& BinDataFile,
    vector<VertexPositionNormalTexture>& vertices,
     vector<VertexPositionNormalTangentTexture>& vertices_withtan,
    vector<uint16_t>& indices, vector<MaterialEx>& materials) {
    vertices.clear();
    vertices_withtan.clear();
    indices.clear();
    materials.clear();
    wstring DataFile = BinDataDir + BinDataFile;
    BinaryReader Reader(DataFile);
    //ヘッダの読み込み
    auto pHeader = Reader.ReadArray<char>(16);
    string str = pHeader;
    if (str != "BDV1.0") {
        throw BaseException(
            L"データ形式が違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }
    //頂点の読み込み
    auto blockHeader = Reader.Read<BlockHeader>();
    if (!(blockHeader.m_Type == BlockType::Vertex 
        || blockHeader.m_Type == BlockType::VertexWithTangent)) {
        throw BaseException(
            L"頂点のヘッダが違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }
    if (blockHeader.m_Type == BlockType::Vertex) {
        auto VerTexSize 
        = blockHeader.m_Size / sizeof(VertexPositionNormalTexturePOD);
        auto pVertex 
        = Reader.ReadArray<VertexPositionNormalTexturePOD>((size_t)VerTexSize);
        for (UINT i = 0; i < VerTexSize; i++) {
            VertexPositionNormalTexture v;
            v.position.x = pVertex[i].position[0];
            v.position.y = pVertex[i].position[1];
            v.position.z = pVertex[i].position[2];
            v.normal.x = pVertex[i].normal[0];
            v.normal.y = pVertex[i].normal[1];
            v.normal.z = pVertex[i].normal[2];
            v.textureCoordinate.x = pVertex[i].textureCoordinate[0];
            v.textureCoordinate.y = pVertex[i].textureCoordinate[1];
            vertices.push_back(v);
        }
    }
    else if (blockHeader.m_Type == BlockType::VertexWithTangent) {
        auto VerTexSize = blockHeader.m_Size / sizeof(VertexPositionNormalTangentTexturePOD);
        auto pVertex 
        = Reader.ReadArray<VertexPositionNormalTangentTexturePOD>((size_t)VerTexSize);
        for (UINT i = 0; i < VerTexSize; i++) {
            VertexPositionNormalTangentTexture v;
            v.position.x = pVertex[i].position[0];
            v.position.y = pVertex[i].position[1];
            v.position.z = pVertex[i].position[2];
            v.normal.x = pVertex[i].normal[0];
            v.normal.y = pVertex[i].normal[1];
            v.normal.z = pVertex[i].normal[2];
            v.tangent.x = pVertex[i].tangent[0];
            v.tangent.y = pVertex[i].tangent[1];
            v.tangent.z = pVertex[i].tangent[2];
            v.tangent.w = pVertex[i].tangent[3];
            v.textureCoordinate.x = pVertex[i].textureCoordinate[0];
            v.textureCoordinate.y = pVertex[i].textureCoordinate[1];
            vertices_withtan.push_back(v);
        }
    }
    else {
        throw BaseException(
            L"頂点の型が違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }

    //インデックスの読み込み
    blockHeader = Reader.Read<BlockHeader>();
    if (blockHeader.m_Type != BlockType::Index) {
        throw BaseException(
            L"インデックスのヘッダが違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }

    auto IndexSize = blockHeader.m_Size / sizeof(uint16_t);
    auto pIndex = Reader.ReadArray<uint16_t>((size_t)IndexSize);
    for (UINT i = 0; i < IndexSize; i++) {
        indices.push_back(pIndex[i]);
    }

    //マテリアルの読み込み
    //マテリアル数の読み込み
    blockHeader = Reader.Read<BlockHeader>();
    if (blockHeader.m_Type != BlockType::MaterialCount) {
        throw BaseException(
            L"マテリアル数のヘッダが違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }
    UINT MaterialCount = blockHeader.m_Size;
    for (UINT i = 0; i < MaterialCount; i++) {
        //テクスチャファイル名が可変長なので注意。
        blockHeader = Reader.Read<BlockHeader>();
        if (blockHeader.m_Type != BlockType::Material) {
            throw BaseException(
                L"マテリアルのヘッダが違います",
                DataFile,
                L"MeshResource::ReadBaseData()"
            );
        }
        UINT TextureFileNameSize = blockHeader.m_Size - sizeof(MaterialExPOD);
        auto rMaterial = Reader.Read<MaterialExPOD>();
        MaterialEx ToM;
        //!開始インデックス
        ToM.m_StartIndex = rMaterial.m_StartIndex;
        //!描画インデックスカウント
        ToM.m_IndexCount = rMaterial.m_IndexCount;
        //! デフィーズ(物体の色)
        ToM.m_Diffuse.x = rMaterial.m_Diffuse[0];
        ToM.m_Diffuse.y = rMaterial.m_Diffuse[1];
        ToM.m_Diffuse.z = rMaterial.m_Diffuse[2];
        ToM.m_Diffuse.w = rMaterial.m_Diffuse[3];
        //! スペキュラー(反射光)
        ToM.m_Specular.x = rMaterial.m_Specular[0];
        ToM.m_Specular.y = rMaterial.m_Specular[1];
        ToM.m_Specular.z = rMaterial.m_Specular[2];
        ToM.m_Specular.w = rMaterial.m_Specular[3];
        //! アンビエント(環境色)
        ToM.m_Ambient.x = rMaterial.m_Ambient[0];
        ToM.m_Ambient.y = rMaterial.m_Ambient[1];
        ToM.m_Ambient.z = rMaterial.m_Ambient[2];
        ToM.m_Ambient.w = rMaterial.m_Ambient[3];
        //! エミッシブ(放射光)
        ToM.m_Emissive.x = rMaterial.m_Emissive[0];
        ToM.m_Emissive.y = rMaterial.m_Emissive[1];
        ToM.m_Emissive.z = rMaterial.m_Emissive[2];
        ToM.m_Emissive.w = rMaterial.m_Emissive[3];
        auto pTexture = Reader.ReadArray<wchar_t>(TextureFileNameSize / sizeof(wchar_t));
        wstring TextureFileStr = pTexture;
        TextureFileStr = BinDataDir + TextureFileStr;
        ToM.m_TextureResource = ObjectFactory::Create<TextureResource>(TextureFileStr);
        materials.push_back(ToM);
    }

    //Endの読み込み
    blockHeader = Reader.Read<BlockHeader>();
    if (blockHeader.m_Type != BlockType::End) {
        throw BaseException(
            L"Endヘッダが違います",
            DataFile,
            L"MeshResource::ReadBaseData()"
        );
    }
}
 順を追っていけば理解できると思いますが、1点VertexPositionNormalTexturePODのようになんとかPODという型が出てきます。
 これはC++基本型と互換の取れた構造体という意味で、ファイル内はfloatやintなどC++で定義されている型で読み込んだほうが間違いがないために、いったんなんとかPOD読み込んでからVertexPositionNormalTexture型などに変換します。
 以下はVertexPositionNormalTexturePODですが
//--------------------------------------------------------------------------------------
/// VertexPositionNormalTexture読み込み用構造体
//--------------------------------------------------------------------------------------
struct VertexPositionNormalTexturePOD {
    float position[3];  ///< 位置情報
    float normal[3];    ///< 法線
    float textureCoordinate[2]; ///< テクスチャUV
};
 のようにfloat型のみで構成される構造体となっています。この構造体にいったん読んだ後VertexPositionNormalTexture型に変換しているのがわかると思います。

 以上、いくつかの項目に分けてFbx2BinVS2015を説明してきました。Fbx2BinVS2015のようなデータ作成ツールゲーム本体は密接にかかわりあってるのがわかると思います。
 と同時に、この関係を維持すればオリジナルなデータ形式をどんどん作成できるのがわかったと思います。
 ぜひ、Fbx2BinVS2015をカスタマイズ、あるいは0からデータ変換ツールを作成し、ゲームに配置してみましょう。きっとかなり勉強になると思います。