Win10 OBJ Model

Windows10(Store) で OBJ Model を Text Data で定義して描画します。
次に OBJ Model ファイルを読み込んで描画します。

前田稔(Maeda Minoru)の超初心者のプログラム入門

プログラムの説明

  1. Windows10(Store) で Wavefront のモデルファイル(*.obj)を描画します。
    DirectX9 では X-FILE が、DirectX10 では sdkmesh が使われていたのですが、WindowsStore には専用のモデルファイルは存在しないようです。
    そこで私の WindowsStore DirectX のページでは X FILE を主として扱うことにしました。
    所が X FILE の解析は結構難しく、その前段階としてプログラムが簡単な OBJ Model を取り上げることにします。
  2. 簡単な Cube の OBJ Model です。
    # TestModel
    g cube
    v -1 -1 -1
    v 1 -1 -1
    v -1 1 -1
    v 1 1 -1
    v -1 -1 1
    v 1 -1 1
    v -1 1 1
    v 1 1 1
    f 1 3 4 2
    f 1 5 7 3
    f 2 4 8 6
    f 1 2 6 5
    f 3 7 8 4
    f 5 6 8 7
    
  3. OBJ ファイルは TEXT 形式で、全ての行がキーワードで始まります。
  4. Cube の OBJ Model を文字列で定義して描画します。
    OBJ Model の文字コードは Shift-JIS が使われているので、STL の string で定義します。
    \n は改行コードです。前述の「簡単な Cube の OBJ Model」と比べてみて下さい。
    さすがに1行で書くには長すぎるので str1, str2, str3 に分けています。
        //☆ OBJ モデルの定義
        std::string str1 = "#TestModel\ng cube\n";
        std::string str2 =
        "v -1 -1 -1\nv 1 -1 -1\nv -1 1 -1\nv 1 1 -1\nv -1 -1 1\nv 1 -1 1\nv -1 1 1\nv 1 1 1\n";
        std::string str3 =
        "f 1 3 4 2\nf 1 5 7 3\nf 2 4 8 6\nf 1 2 6 5\nf 3 7 8 4\nf 5 6 8 7\n";
    
  5. Win10 Base を参照して「Cube を外したプロジェクト」を作成して下さい。
    コンパイル&実行すると、水色の画面に FPS だけが描画されます。
  6. OBJ Model を描画する Load_OBJ Class を組み込みます。
    Load_OBJ.h と Load_OBJ.cpp を Content のフォルダーに格納してプロジェクトに加えて下さい。
  7. Load_OBJ.h のソースコードです。
    str は OBJ Model のソースコードで、str1, str2, str3 を連結して格納します。
    VT は str を行(\n)で切り分けた vector 配列で VT_size がその大きさです。
    TK は VT の1行を解析するためにトークンで切り分けた vector 配列です。
    m_pos が頂点座標(三次元)の領域で、float3 の vector 配列で定義しています。
    m_idxP が頂点 Index の並びで、今回は4角形ポリゴンなので例えば[4, 1, 3, 4, 2, 4, 1, 5, 7, 3, ...]のように格納されます。
    m_idxP3 は m_idxP を3角形ポリゴンに変換した vector 配列で、例えば[1, 3, 4, 1, 4, 2, 1, 5, 7, 1, 7, 3, ...]のように格納されます。
    (4角形ポリゴン[4, 1, 3, 4, 2 ]が[1, 3, 4][1, 4, 2]に分割される)
    Debug() 関数はプログラムが完成すれば削除して下さい。
    #pragma once
    
    #include <vector>
    #include "..\Common\DeviceResources.h"
    #include "..\Common\StepTimer.h"
    #include "ShaderStructures.h"
    
    using namespace std;
    using namespace Platform;
    struct float3
    {
        float   x;
        float   y;
        float   z;
    };
    
    namespace App1
    {
        class Load_OBJ
        {
        public:
            Load_OBJ(const std::shared_ptr<DX::DeviceResources>& deviceResources);
            void Load(
                ID3D11Buffer **vertexBuffer,
                ID3D11Buffer **indexBuffer,
                uint32 *indexCount);
            void SetCullMode();     // カリングの設定
    
        private:
            std::shared_ptr<DX::DeviceResources> m_deviceResources;
            ID3D11RasterizerState  *m_RasterizerState;
    
            //☆ OBJ モデルの定義
            std::string str1 = "#TestModel g cube\n";
            std::string str2 =
            "v -1 -1 -1\nv 1 -1 -1\nv -1 1 -1\nv 1 1 -1\nv -1 -1 1\nv 1 -1 1\nv -1 1 1\nv 1 1 1\n";
            std::string str3 =
            "f 1 3 4 2\nf 1 5 7 3\nf 2 4 8 6\nf 1 2 6 5\nf 3 7 8 4\nf 5 6 8 7\n";
    
            std::string str;
            vector<string>  VT;             // 行(\n)で切り分け
            int             VT_size;        // VT の大きさ
            int             m_pt;           // VT の行 Index
            vector<string>  TK;             // 1行をトクンで切り分け
    
            vector<float3>  m_pos;              // 頂点座標
            vector<unsigned short> m_idxP;      // 頂点 Index の並び(角付)
            vector<unsigned short> m_idxP3;     // 頂点 Index(3P) の並び
            string  m_group;                    // Model Group
            string  Word;
    
            void PX_P3(vector<unsigned short> PX, vector<unsigned short> *P3);
            void ComputeNorm(VertexPosition *Vertices, int siz);
            void Token(string st);
    
            void Debug(char *str);
            void Debug(float3 f3);
            void Debug(vector<unsigned short> val);
        };
    }
    
  8. Load_OBJ.cpp のソースコードです。
    Debug() 関数はプログラムが完成すれば削除して下さい。
    // OBJ モデルをロード
    #include "pch.h"
    #include "..\Common\DirectXHelper.h"
    #include "Load_OBJ.h"
    
    using namespace App1;
    using namespace DirectX;
    using namespace Windows::Foundation;
    
    void Load_OBJ::Debug(char *str)
    {   WCHAR   wk[1000];
        MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,str,-1,wk,1000);
        OutputDebugString(wk);
        OutputDebugString(L"\r\n");
    }
    void Load_OBJ::Debug(float3 f3)
    {
        String^ str = ref new Platform::String(L"float3(");
        str += f3.x.ToString();
        str += "f , ";
        str += f3.y.ToString();
        str += "f , ";
        str += f3.z.ToString();
        str += "f), \r\n";
        OutputDebugString(str->Data());
    }
    void Load_OBJ::Debug(vector<unsigned short> val)
    {
        String^ str = ref new String(L"short[");
        str += val.size().ToString();
        str += L"]:";
        for (unsigned i = 0; i < val.size(); i++)
        {
            str += val[i].ToString();
            str += " ";
        }
        str += "\r\n";
        OutputDebugString(str->Data());
    }
    
    Load_OBJ::Load_OBJ(const std::shared_ptr<DX::DeviceResources>& deviceResources) :
        m_deviceResources(deviceResources)
    {
    }
    
    // Load OBJ Model
    void Load_OBJ::Load(
        ID3D11Buffer **vertexBuffer,
        ID3D11Buffer **indexBuffer,
        uint32 *indexCount)
    {
        float3  wf3;
        int     num, pt, pw;
    
        m_pos.clear();
        m_idxP.clear();
        m_idxP3.clear();
    
        str = str1 + str2 + str3;
        // str を行で切り分ける(str ⇒ VT)
        VT.clear();
        num = str.size();
        for (pt = 0; pt < num;)
        {
            pw = pt;
            for (pt++; pt < num && str[pt] != '\n'; pt++);
            if (pt >= num)  break;
            pt++;
            Word = Word.assign(str, pw, pt - pw);
            VT.push_back(Word);
            for(; str[pt] == '\r' || str[pt] == '\n' || str[pt] == ' '; pt++);
        }
        VT_size = VT.size();
    
    for(m_pt=0; m_pt<VT_size; m_pt++)   Debug((char *)VT[m_pt].data());
    
        for(m_pt=0; m_pt<VT_size; m_pt++)
        {   Token(VT[m_pt]);
            if (TK.size() < 1 || TK[0][0]=='#') continue;
            switch(TK[0][0])
            {   case  'g':
                    if (TK.size()>1)    m_group = TK[1];
                    break;
                case  'v':
                    wf3.x = (float)atof((char *)TK[1].data());
                    wf3.y = (float)atof((char *)TK[2].data());
                    wf3.z = (float)atof((char *)TK[3].data());
                    m_pos.push_back(wf3);
                    break;
                case  'f':
                    unsigned nn;
                    nn = TK.size() - 1;
                    m_idxP.push_back(nn);
                    for(unsigned j=1; j<=nn; j++)
                    {
                        num = (unsigned short)atoi((char *)TK[j].data());
                        m_idxP.push_back(num);
                    }
                    break;
                default:
                    Debug((char *)VT[m_pt].data());
            }
        }
    /*
    float3 f3;
    for (unsigned i = 0; i < m_pos.size(); i++)
    {
        f3 = m_pos[i];
        Debug(f3);
    }
    Debug(m_idxP);
    */
        // 4角ポリゴン ⇒ 3角ポリゴン
        PX_P3(m_idxP, &m_idxP3);
    Debug(m_idxP3);
    
        // 頂点データとインデックスデータを作成
        unsigned vtx_size = m_idxP3.size();
        VertexPosition *Vertices = new VertexPosition[vtx_size];
        unsigned short *Indices = new unsigned short[vtx_size];
    
        for(unsigned i=0; i<vtx_size; i++)
        {
            Vertices[i].pos.x = m_pos[m_idxP3[i]-1].x;
            Vertices[i].pos.y = m_pos[m_idxP3[i]-1].y;
            Vertices[i].pos.z = m_pos[m_idxP3[i]-1].z;
            Indices[i] = i;
        }
    
        // 法線ベクトルの計算
        ComputeNorm(Vertices, vtx_size);
    
        *indexCount = m_idxP3.size();
        // メッシュを生成
        D3D11_SUBRESOURCE_DATA vertexBufferData = {0};
        vertexBufferData.pSysMem = Vertices;
        vertexBufferData.SysMemPitch = 0;
        vertexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(VertexPosition)*(*indexCount), D3D11_BIND_VERTEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &vertexBufferDesc, &vertexBufferData, vertexBuffer));
    
        // インデックスを生成
        D3D11_SUBRESOURCE_DATA indexBufferData = {0};
        indexBufferData.pSysMem = Indices;
        indexBufferData.SysMemPitch = 0;
        indexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC indexBufferDesc(sizeof(unsigned short)*(*indexCount), D3D11_BIND_INDEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &indexBufferDesc, &indexBufferData, indexBuffer));
    
        // 領域の解放
        if (Vertices)   delete Vertices;
        if (Indices)    delete Indices;
    
        // カリング設定
        D3D11_RASTERIZER_DESC rasterizerDesc;
        ZeroMemory( &rasterizerDesc, sizeof( D3D11_RASTERIZER_DESC ) );
        //rasterizerDesc.CullMode = D3D11_CULL_BACK;
        rasterizerDesc.CullMode = D3D11_CULL_FRONT;
        rasterizerDesc.FillMode = D3D11_FILL_SOLID;
        rasterizerDesc.DepthClipEnable = FALSE;
        rasterizerDesc.MultisampleEnable = TRUE;
        rasterizerDesc.DepthBiasClamp = 0;
        rasterizerDesc.SlopeScaledDepthBias = 0;
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateRasterizerState(
                &rasterizerDesc, &m_RasterizerState));
    }
    
    // VT[m_pt]⇒st をトークンで切り分けて TK に格納
    void Load_OBJ::Token(string st)
    {
        unsigned pt, wk;
    
        TK.clear();
        for (pt = 0; pt<st.length(); )
        {
            wk = st.find_first_not_of(" ,;", pt);
            if (st[wk] == '\r' || st[wk] == '\n')   return;
            pt = wk;
            wk = st.find_first_of(" ,;\r\n", pt);
            if (wk > pt)
            {
                Word = Word.assign(st, pt, wk-pt);
                TK.push_back(Word);
                pt = wk;
            }
            else    pt++;
        }
    }
    
    // 4角ポリゴン ⇒ 3角ポリゴン
    void Load_OBJ::PX_P3(vector<unsigned short> PX, vector<unsigned short> *P3)
    {
        int    i, j, num, pn;
        num = PX.size();
        for (i = 0; i<num; )
        {
            pn = (int)PX[i];
            i++;
            switch (pn)
            {
            case 3:     //TRIANGLESTRIP 3
                P3->push_back(PX[i]);
                P3->push_back(PX[i + 1]);
                P3->push_back(PX[i + 2]);
                break;
            case 4:     //TRIANGLESTRIP 4
                P3->push_back(PX[i]);
                P3->push_back(PX[i + 1]);
                P3->push_back(PX[i + 2]);
                P3->push_back(PX[i]);
                P3->push_back(PX[i + 2]);
                P3->push_back(PX[i + 3]);
                break;
            default:
                for (j = 0; j <= pn - 3; j++)
                {
                    P3->push_back(PX[i]);
                    P3->push_back(PX[i + 1]);
                    P3->push_back(PX[i + 2]);
                }
                break;
            }
            i += pn;
        }
    }
    
    // 法線ベクトルの計算
    void Load_OBJ::ComputeNorm(VertexPosition *Vertices, int siz)
    {
        for (int i = 0; i<siz; i += 3)
        {
            float ax = Vertices[i + 2].pos.x - Vertices[i + 1].pos.x;
            float ay = Vertices[i + 2].pos.y - Vertices[i + 1].pos.y;
            float az = Vertices[i + 2].pos.z - Vertices[i + 1].pos.z;
            float bx = Vertices[i + 0].pos.x - Vertices[i + 1].pos.x;
            float by = Vertices[i + 0].pos.y - Vertices[i + 1].pos.y;
            float bz = Vertices[i + 0].pos.z - Vertices[i + 1].pos.z;
    
            float nx = ay * bz - az * by;
            float ny = az * bx - ax * bz;
            float nz = ax * by - ay * bx;
    
            Vertices[i + 0].norm.x = nx;
            Vertices[i + 0].norm.y = ny;
            Vertices[i + 0].norm.z = nz;
            Vertices[i + 1].norm = Vertices[i + 0].norm;
            Vertices[i + 2].norm = Vertices[i + 0].norm;
        }
    }
    
    // カリングの設定
    void Load_OBJ:: SetCullMode()
    {
        auto context = m_deviceResources->GetD3DDeviceContext();
        context->RSSetState(m_RasterizerState);
    }
    
  9. Sample3DSceneRenderer.h のソースコードです。
    #pragma once
    
    #include <string>
    #include "..\Common\DeviceResources.h"
    #include "ShaderStructures.h"
    #include "..\Common\StepTimer.h"
    #include "Load_OBJ.h"
    
    using namespace std;
    
    namespace App1
    {
        class Sample3DSceneRenderer
        {
        public:
            Sample3DSceneRenderer(const std::shared_ptr<DX::DeviceResources>& deviceResources);
            void CreateDeviceDependentResources();
            void CreateWindowSizeDependentResources();
            void ReleaseDeviceDependentResources();
            void Update(DX::StepTimer const& timer);
            void Render();
            void StartTracking();
            void TrackingUpdate(float positionX);
            void StopTracking();
            bool IsTracking() { return m_tracking; }
    
        private:
            void Rotate(float radians);
    
        private:
            std::shared_ptr<DX::DeviceResources> m_deviceResources;
    
            // キューブ ジオメトリの Direct3D リソース。
            Microsoft::WRL::ComPtr<ID3D11InputLayout>   m_inputLayout;
            Microsoft::WRL::ComPtr<ID3D11Buffer>        m_vertexBuffer;
            Microsoft::WRL::ComPtr<ID3D11Buffer>        m_indexBuffer;
            Microsoft::WRL::ComPtr<ID3D11VertexShader>  m_vertexShader;
            Microsoft::WRL::ComPtr<ID3D11PixelShader>   m_pixelShader;
            Microsoft::WRL::ComPtr<ID3D11Buffer>        m_constantBuffer;
            Load_OBJ    *model;
    
            // キューブ ジオメトリのシステム リソース。
            ModelViewProjectionConstantBuffer   m_constantBufferData;
            uint32  m_indexCount;
    
            // レンダリング ループで使用する変数。
            bool    m_loadingComplete;
            float   m_degreesPerSecond;
            bool    m_tracking;
        };
    }
    
  10. Sample3DSceneRenderer .cpp を修正します。
    Win10 Base では VertexPositionColor になっていますが、ShaderStructures.h に合わせて VertexPosition に書き換えて下さい。
    モデルが大きいので少しカメラを引きます。
        static const XMVECTORF32 eye = { 0.0f, 1.5f, 4.0f, 0.0f };
        static const XMVECTORF32 at = { 0.0f, -0.1f, 0.0f, 0.0f };
        static const XMVECTORF32 up = { 0.0f, 1.0f, 0.0f, 0.0f };
    
  11. Render() 関数ではカリングを設定して描画して下さい。
        // カリングを設定
        model->SetCullMode();
    
        // オブジェクトを描画します。
        context->DrawIndexed(m_indexCount, 0, 0);
    }
    
  12. モデルに合わせてシェーダは「頂点+法線」の物に入れ替えて下さい。
    シェーダは Windows10 Shader に掲載しています。
    vertexDesc [] の "COLOR" を "NORMAL" に修正します。
            static const D3D11_INPUT_ELEMENT_DESC vertexDesc [] =
            {
                { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
                { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            };
    
  13. Load_OBJ Class を生成して OBJ モデルをロードします。
        // 両方のシェーダーの読み込みが完了したら、メッシュを作成します。
        auto createCubeTask = (createPSTask && createVSTask).then([this] () {
            model = new Load_OBJ(m_deviceResources);
            model->Load(&m_vertexBuffer, &m_indexBuffer, &m_indexCount);
        });
    
        // メッシュが読み込まれたら、オブジェクトを描画する準備が完了します。
        createCubeTask.then([this] () {
            m_loadingComplete = true;
        });
    
  14. プログラムをかいつまんで説明します。
    Win10 Base を参照して「Cube を外したプロジェクト」で説明したように、 Sample3DSceneRenderer.cpp から Color Cubu 関係のソースを削除しています。
    代わりに OBJ Model を作成して描画します。
    Load_OBJ Class を生成して Load() でモデルを取得します。
        auto createCubeTask = (createPSTask && createVSTask).then([this] () {
            model = new Load_OBJ(m_deviceResources);
            model->Load(&m_vertexBuffer, &m_indexBuffer, &m_indexCount);
        });
    
  15. Load_OBJ の Constructor では str1 + str2 + str3 を連結して str に格納します。
    str を改行コード(\n)で切り分けて VT に格納します。
  16. Load() 関数でモデルを作成して vertexBuffer, indexBuffer, indexCount に設定します。
  17. m_pt が VT の行 Index で、VT[m_pt] の行をトークンで切り分けて TK に格納します。
    TK[0] の先頭文字に従って処理を分けます。
  18. 今回のモデルは4角形ポリゴンなので3角形ポリゴンに変換します。
  19. 頂点データ(Vertices)とインデックス(Indices)の領域を定義してモデルを組み立てます。
  20. 今回は法線が定義されていないのでプログラムで計算します。
  21. メッシュを生成して vertexBuffer に設定します。
  22. インデックスを生成して indexBuffer に設定します。
  23. DirectX Store ではポリゴンが裏向きなのでカリングモードを FRONT に設定します。

OBJ ファイルを読み込む

  1. Shift-JIS でタイプされた OBJ Model(cube.obj) を入力して描画します。
    .NET Framework ではHD上のファイルを自由に入力できたのですが、ストアアプリでは制限されています。
    ファイルピッカーを使えば入力出来るのですが、それはそれで面倒です。
    そこで cube.obj を直接プロジェクトに組み込むことにします。
    そのとき問題になるのが拡張子(.obj)で VC ではシステムファイル名として利用されています。
    そこで拡張子を .txt に代えてプロジェクトに組み込みます。
    ページ先頭の Cube OBJ Model を cube.txt の名前で Shift-JIS でタイプしてプロジェクトのフォルダーに格納して下さい。
  2. "cube.txt" を入力するために BasicReaderWriter.h と BasicReaderWriter.cpp を Content\ に格納して組み込みます。
    BasicReaderWriter.h や BasicReaderWriter.cpp は Windows10 DirecX Library に掲載しています。
  3. Load_OBJ.h から OBJ Model のソースコード(str1, str2, str3) を削除します。
  4. Load_OBJ.cpp の Load() 関数では "cube.txt" を入力して、vector 配列 VT; に行で切り分けて格納します。
    void Load_OBJ::Load(
        ID3D11Buffer **vertexBuffer,
        ID3D11Buffer **indexBuffer,
        uint32 *indexCount)
    {
        float3  wf3;
        int     num, pt, pw;
    
        m_pos.clear();
        m_idxP.clear();
        m_idxP3.clear();
    
        BasicReaderWriter^ reader = ref new BasicReaderWriter();
        Array<byte>^ buf = reader->ReadData("cube.txt");
        // ストリングを行で切り分ける(buf⇒VT)
        str = (char *)&(buf[0]);
        VT.clear();
        num = str.size();
        for (pt = 0; pt < num;)
        {   pw = pt;
            for (pt++; pt < num && str[pt] != '\n'; pt++);
            if (pt >= num)  break;
            pt++;
            Word = Word.assign(str, pw, pt - pw);
            VT.push_back(Word);
            for(; str[pt] == '\r' || str[pt] == '\n' || str[pt] == ' '; pt++);
        }
        VT_size = VT.size();
    
        ・・・
        以降は変わりません
    

帆船モデル


  1. 帆船モデル(galleon.obj) も描画することが出来るので試して下さい。
    galleon.obj を cube.txt の名前で格納して下さい。
    名前を変えても良いのですが、この方が簡単です。
  2. サイズが大きいので Sample3DSceneRenderer.cpp の eye と at を次のように修正します。
    	//static const XMVECTORF32 eye = { 0.0f, 1.5f, 4.0f, 0.0f };
    	//static const XMVECTORF32 at = { 0.0f, -0.1f, 0.0f, 0.0f };
    	static const XMVECTORF32 eye = { 0.0f, 1.5f, 70.0f, 0.0f };
    	static const XMVECTORF32 at = { 0.0f, 10.0f, 0.0f, 0.0f };
    	static const XMVECTORF32 up = { 0.0f, 1.0f, 0.0f, 0.0f };
    
  3. 帆船を白色で描画するために SamplePixelShader.hlsl を修正します。
        return float4(1.0, 1.0, 1.0, 1.0) * lightMagnitude;
    
  4. これで帆船モデルが上の画像のように白色で描画されます。
    帆船モデルの簡単な描画プロジェクトは X2017 OBJ Model を参照して下さい。
    ここからはプロジェクトの開発に必要なファイル一式を圧縮形式で提供しています。

[Next Chapter ↓] OBJ Normal Model

超初心者のプログラム入門(DirectX Store)