Simplestar Game

How do I create a 3D game ?
http://simplestar.syuriken.jp/
since 17/01/2009
  • BACK
  • HOME
  • NEXT
  • |

:::::::::::034話 シェーダマテリアル@:::::::::::

どうも、星姫です。
モデルデータのフォーマットも策定していないのに、エフェクトファイル(.fx)を勉強するなんて
フライングもいいところでした。
何よりもまず、エフェクトファイルの活用を見越したモデルデータフォーマットを策定しないといけません!
ということで、ここに一例を示します。良かったら参考にしてね。

髪の毛と肌のマテリアルは別々…、スキンでそれぞれの頂点に影響を与えるボーンは
共通だったり、そうじゃなかったりします。
モデルデータフォーマットには悩むところ…そこで、これから示すフォーマットはどうでしょう? というのが今回のレッスンです。

また、実際にゲーム中でどのように描画されるのかを確認しながらモデルデータを編集できるようなツールも同時に考えていきましょう。

ではさっそく本題に入ります。

Blender から中間モデルデータを出力する

コチラから、エクスポーターを入手します。
同梱されているReadMe.txt に書かれているインストール方法でインストールしてください。
使い方は、Blenderに最初から付いているDirectX のエクスポーターとまったく同じです。

Blenderの使い方については、Blender2.5入門セット翻訳3点の内容を押さえていれば問題はありません。

さて、お次は出力したXMLデータから実際にゲームで使用するモデルデータに変換するツールを考えましょう。

中間モデルデータからゲーム用データへ変換し、編集できてプレビューも見られるエディタ&コンバータを作成する

なんという項目名でしょうか…欲張りですね。でも必要な機能かなぁと思うんですよ。
なんてったって、エフェクトファイルには頂点レイアウトを記述しなければなりませんからね。
頂点レイアウトって、データ作成時に決まるものですけど、数字から判断するのは正直厳しいです。
決定してしまったら、しまったで、これを編集することができないのもおかしいと思います。

よし!エディター兼コンバーターを作ろう!…という訳です。

GUI作成になりますが、今度はC++言語でGUIを作成してみましょう!(理由:前回のライブラリを使いたいので)
キーワードは[Visual C++]です。
作成方法はMFCを使うか、CLRを使うかの2通りです。ここはCLRでやってみましょうか。
理由は、シンプルスター先生が仕事でMFCを使っているからです。(やったことないのに挑戦してみたくて…)

まずは、VisualStudioを起動してください。
新規プロジェクトで下図のように、CLRでフォームアプリを選択します。

最初からフォームが作られていて、これをどんどこ自分の好きなように編集していきます。(下図が初期状態)
GUIの実装はだいたい直感操作でいけるはずです。すばらしい開発環境ですよね。

さて、ここで大きな間違いを犯していることに気づきました。

実は、 C++/CLI またはその他の .NET 言語 (C#、Visual Basic など) で使用できる
ライブラリを作成する場合、ライブラリはマネージド アッセンブリでなければならないのです。
つまり、前々回に作ったSimpleDX10ライブラリは、アンマネージド アッセンブリのため CLR のアプリケーションに組み込めません。

情報元: 再利用可能なコードの作成 (C++)

どうしても使いたい場合、方法が無いわけではありませんが、それなりに作業をしなくてはなりません。
詳細↓

.NETの中でこれまでのC/C++資産を生かすには

MFC入門

今回はSimpleDX10ライブラリをそのまま使いたいと思うので、MFCを選択しました。(CLRは今度勉強しようね)
作業の前に、VisualStudio2008の表記がまだService Pack 1でない方は
次のリンク先からインストーラをゲットして、更新しておいてください。

Microsoft Visual Studio 2008 Service Pack 1 (インストーラ)

まず、次のページを参考にアプリケーションのスケルトンプログラムを作成します。
(コーディングは一切なし、だれでも簡単に作れます。)

MFCアプリケーションを作成する方法

はい、ということで、らしく動くけど実際は仕事をしないアプリケーションができました。
初期デザインはOffice2007の黒です。
そのほか、SDI、MFC標準、プロパティ、ファイルビュー、クラスビュー、出力ウィンドウを用意しました。

さぁ、難しくなるのはここからです。とりあえず、当初の目的であるSimpleDX10をメインのビューに組み込みます。
具体的には、プリコンパイル済みヘッダーファイルでSimpleDX10.hをインクルードするだけです。
詳細は前回のレッスンの冒頭に書いたので、わからない方はそちらを参照してください。

組み込んだ結果がこちら↓

中央のメインビューがクリアされたのが確認できます。
この画面にモデルのプレビューを表示する予定。

続いて、モデルの読み込み部分を作成していきましょう。

XMLの読み込み(MFC)

いつかC#を使ってXMLファイルを読み込む方法を書きました。(詳細は 第30話 を参照)
基本的にルートのドキュメントから、タグ名や属性名を頼りに値を取ってくる操作ができれば
問題なかった気がします。
さて、MFCでは標準でXMLのパーサーは用意されていないので(…よね?)、使用するXMLパーサーを
選ぶところから始めましょう。

まぁ、「MFCでXML読み込み」なんてキーワードで調べれば、「MSXML」がまず目につくと思います。
サンプルとかすぐに実行してテストできるモノがあるかなぁと調べると、次のページが見つかりました。

MSXML を利用して XMLファイル を解析するには

hiramine.com様 いつもお世話になっております。
さて、ここのサンプルを参考にXMLファイルを読み込むことにしました。(覚えるべきは次の関数くらい?)

・XMLファイルからドキュメントを作成する
IXMLDOMDocumentPtr pXMLDOMDocument;
pXMLDOMDocument.CreateInstance(CLSID_DOMDocument);
VARIANT_BOOL varbResult;
pXMLDOMDocument->load( CComVariant(_T("Simplestar.xml")), &varbResult );

・タグ名からテキストを取得する
IXMLDOMNodeListPtr pXMLDOMNodeList = NULL;
pXMLDOMDocument->getElementsByTagName( _T("タグ名"), &pXMLDOMNodeList );
// ノードリストのうちの一つのノードの取得
IXMLDOMNodePtr pXMLDOMNode = NULL;
pXMLDOMNodeList->get_item( index, &pXMLDOMNode );
// エレメント型への変換
IXMLDOMElementPtr pXMLDOMElement = NULL;
pXMLDOMNode->QueryInterface( IID_IXMLDOMElement, (void**)&pXMLDOMElement );
// データ値の取得
CComBSTR bstrText;
pXMLDOMElement->get_text( &bstrText );

・タグ名と属性名から属性値を取得する
IXMLDOMNodeListPtr pXMLDOMNodeList = NULL;
pXMLDOMDocument->getElementsByTagName( _T("タグ名"), &pXMLDOMNodeList );
// ノードリストのうちの一つのノードの取得
IXMLDOMNodePtr pXMLDOMNode = NULL;
pXMLDOMNodeList->get_item( index, &pXMLDOMNode );
// エレメント型への変換
IXMLDOMElementPtr pXMLDOMElement = NULL;
pXMLDOMNode->QueryInterface( IID_IXMLDOMElement, (void**)&pXMLDOMElement );
// 属性値の取得
IXMLDOMAttribute* pAttributeNode = NULL;
CComVariant varValue;
pXMLDOMElement->getAttribute( _T("属性名"), &varValue );
varValue.bstrVal ←値の文字列

まずはどのタイミングでXMLファイルを読むべきなのか決めましょう。
まぁ、やっぱり「ファイルを開く」かな。

データ変換と頂点レイアウトの編集

XMLファイルを読み込み、モデルデータを解析したところで、実際にゲームで使うデータに変換するには
頂点レイアウトを決定しなければなりません。頂点レイアウトを決定すると、頂点バッファのストライドや
エフェクトファイルに書かれる頂点レイアウトの記述が変わります。

大前提として、Blenderで作成したモデルデータにおいて、全ての三角形はマテリアルを持ちます。
これは、私が決めたことなのでみなさんは知らないことです。そのマテリアルに1対1で対応するように
エフェクトファイルを用意しようと思います。これが前提です。絶対的なルールね。

Blenderから出力したファイルには複数のオブジェクトのデータが含まれており、なおかつ
オブジェクトには複数のマテリアルが設定されることになります。つまり、エフェクトファイルがそれだけ作られます。
このエフェクトファイルごとに、頂点レイアウトが設定されるわけですが、毎度ファイルを解析・変換するごとに
エフェクトファイルの数だけ頂点レイアウトを指定させるのは、めんどうです。(想像しただけでもやりたくない。)
そこで、最初は自動で頂点レイアウトを決定し、何の設定もせずにプレビューを見られるようにします。
また、任意のタイミングで頂点レイアウトを編集して、その都度エフェクトファイルとモデルデータが自動で書き換わり
ほぼノータイムでプレビューを見られるようにします。

とりあえず上記の仕様を満たすツールを作ってみましょう。(まだまだ追加したい要望はたくさんありますが…)
動作は「ファイルを開く」でxml形式ファイルを読み込み、ツールがあるフォルダに一時的に変換後のファイルを作り
プレビューにオブジェクトを表示します。フォルダ階層とかは私がすべて決定します。(やっぱりこういうの決めていくのは楽しいね!)
決定して作られたファイル・フォルダ構成をツールのファイルビューに表示させるようにしましょう。
そのファイルビューで選択した項目の詳細・データクラス階層などはクラスビューに表示させます。
さらに、そのクラスビューで選択した項目に関連する面や点がプレビューで点滅するようにします。
コレで行きましょう! ということで、作業開始♪

「ファイルを開く」のイベントハンドラの編集

まず、MFC初心者のみなさんは、ファイルを開く動作のコードを編集するとっかかりがつかめていないと
思います。ディフォルトでたくさんソースコードが作られましたが、注目すべきコードは次の箇所です。

ドキュメントの Serialize() 関数
MFCは最初、ドキュメント、ビュー、アプリ、メインフレームのクラスを作ります。
ドキュメントとは、このドキュメントのことです。

Serialize() 関数に書かれているコメントに従って編集します。
まぁ、まずはファイル選択ダイアログの拡張子フィルターを編集しますか。
編集場所は「リソースビュー」→「String Table」→「IDR_MAINFRAME」の4番目と5番目の文字列です。
そこを次のように修正します。
「SimpleConverter2\n\nSimpleData\n変換プロジェクト\n.conv\nSimpleConverter2.Document\nSimpleConverter2.Document」

結局、拡張子は.xmlにしませんでした。理由はゲームで使用するモデルは複数あるためです。
xmlを読み込むタイミングは、プロジェクトにモデルデータを追加するときにします。詳細は後述します。
フィルタ文字列の編集が済んだら次は、選択したファイルパスの取得です。

それはこんな感じ↓ まだ、保存と読み込みの関数を作っていないのでコメントアウトしておきます。

// SimpleDoc シリアル化

void SimpleDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // TODO: 格納するコードをここに追加してください。
        // SaveProject(ar.m_strFileName);
    }
    else
    {
        // TODO: 読み込むコードをここに追加してください。
        // LoadProject(ar.m_strFileName);
    }
}

仕様の詳細を決めます。
最初に起動した状態では、ファイルビューが表示され、一時的なルートフォルダが一つ、その下に
モデルデータごとのフォルダ、その下にエフェクトファイルのフォルダとアニメーションのフォルダがあり
それぞれ対応するフォルダの中に、変換したモデルデータファイル、エフェクトファイル、アニメーションファイルが
あり、一番上のモデルをプレビューに表示します。

ルートのフォルダを右クリックすると、ポップアップメニューが現れ「追加(A)」が有効になっていて
そこをクリックすると、xmlファイルの読み込みダイアログが開き、読み込みに成功後、自動でプレビューを表示する。
とまぁ、まずはそこまで作ってみましょう。

最初、クラスビューが前に出てますよね、これをファイルビューに直すには…

    DockPane(&m_wndClassView);
    CDockablePane* pTabbedBar = NULL;
    m_wndFileView.AttachToTabWnd(&m_wndClassView, DM_SHOW, TRUE, &pTabbedBar);

↑こうしました。結果、最初ファイルビューが表に出るようになります。

ああ、前回の構成が保存されちゃうようなら、そうならないようにしておいてください。
こう↓です。

// MainFrame コンストラクション/デストラクション

MainFrame::MainFrame()
{
    // TODO: メンバ初期化コードをここに追加してください。

    // @debug 前回のウィンドウドッキング状態を復元しない
    EnableLoadDockState(FALSE);
    theApp.m_nAppLook = theApp.GetInt(_T("ApplicationLook"), ID_VIEW_APPLOOK_OFF_2007_BLACK);
}

私はメインフレームのコンストラクタに追加しました。

一時的なルートフォルダは、アプリの実行ファイルがあるフォルダの中の、Temporary フォルダとします。
フォルダパスやアイテムの情報など、どうしましょう?

とりあえずEXEファイルのパスは…

    TCHAR exePath[_MAX_PATH];
    ::GetModuleFileName(NULL, exePath, sizeof(exePath));

これで取得できます。そこからフォルダパスを取得し、一時的なTemporaryフォルダを作成するには…

/**********************************************************************//**
    @brief        一時ファイル保存先フォルダパスの取得
    @param[out]    tmpDirPath    :    取得パス保存先
    @author        Simplestar
    @par    [説明]
            一時保存ファイルパスがある場合そのパスを返します。
            フォルダが無い場合はフォルダを作成します。
    @retval        true        :    成功
    @retval        false        :    失敗
*/
/***********************************************************************/
bool CFileView::GetTempDirPath( CString& tempDirPath ) const
{
    if (!m_tempDirPath.IsEmpty())
    {
        tempDirPath = m_tempDirPath;
    }
    else
    {
        // exeファイルのパスを取得
        TCHAR exePath[_MAX_PATH];
        ::GetModuleFileName(NULL, exePath, sizeof(exePath));

        // exeファイルのパスを分解
        TCHAR drive[_MAX_DRIVE];
        TCHAR dir[_MAX_DIR];
        TCHAR fname[_MAX_FNAME];
        TCHAR ext[_MAX_EXT];
        if( 0 != _tsplitpath_s(exePath, drive, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT))
        {
            ASSERT(false);
            return false;
        }
       
        // 一時保存先フォルダパスの作成
        tempDirPath = CString(drive) + CString(dir) + TEMPORARY_DIR_NAME;
    }

    // 一時保存先フォルダが無ければ作成
    if (!PathIsDirectory(tempDirPath))
    {
        if ( !CreateDirectory(tempDirPath, NULL) )
        {
            return false;
        }
    }

    return true;
}

こんな感じ↑、で実装しました。

次に、一時保存フォルダ以下のアイテム(ファイルやフォルダ)をファイルビューに表示させます。
指定したフォルダパス以下のアイテムを追加して、アイテムがフォルダだったら、そのフォルダパスで再帰処理
をするという流れで作ってみましょう。

で、作ったのがこんな感じ↓

/**********************************************************************//**
    @brief        親アイテムにディレクトリパス以下のアイテムを追加
    @param[in]    dirPath    :    ディレクトリパス
    @param[in]    hParent    :    親のツリーアイテムハンドル
    @author        Simplestar
    @par    [説明]
                hParent を省略するとルートアイテムになります。
                追加するアイテムがフォルダだったら再帰します。
                最下層まで読み込むので、ファイル数が多い場合
                処理が重くなります。
*/
/***********************************************************************/
void CFileView::AddItems(const CString& dirPath, HTREEITEM hParent /*= TVI_ROOT*/ )
{
    // 検索文字列の作成
    CString searchMaskStr;
    searchMaskStr.Format( _T("%s%s"), dirPath.GetString(), _T("\\*.*") );

    // ファイル・フォルダ情報格納先
    HANDLE hFind;
    WIN32_FIND_DATA findData;

    // 最初のファイルを取得
    hFind = FindFirstFile(searchMaskStr.GetString(), &findData);
    if(INVALID_HANDLE_VALUE == hFind)
    {
        return;
    }

    do
    {
        // フォルダ名 "." ".." はスキップ
        if(0 == wcscmp(findData.cFileName, _T("."))
            || 0 == wcscmp(findData.cFileName, _T("..")))
        {
            continue;
        }

        // フォルダ名 ".svn" もスキップ
        if(0 == wcscmp(findData.cFileName, _T(".svn")))
        {
            continue;
        }

        if(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            // ディレクトリであれば、ディレクトリ名アイテムを親アイテムに再帰処理
            HTREEITEM hChild = m_wndFileView.InsertItem(findData.cFileName, 0, 0, hParent);
            // ディレクトリパスの作成
            CString subDirPath = dirPath + CString("\\") + CString(findData.cFileName);
            // 再帰
            AddItems(subDirPath, hChild);
        }
        else
        {
            // ファイルアイテムを追加
            m_wndFileView.InsertItem(findData.cFileName, 2, 2, hParent);
        }
    }
    while(FindNextFile(hFind, &findData));
    FindClose(hFind);
}

/**********************************************************************//**
    @brief        指定ディレクトリパス以下のアイテムツリーをビューに追加
    @param[in]    dirPath    :    ディレクトリパス
    @author        Simplestar
    @par    [説明]
                アイテム名はディレクトリパスです。
                表記はボールド、また、あらかじめ展開しています。   
*/
/***********************************************************************/
void CFileView::FillFileView(const CString& dirPath)
{
    // 指定フォルダパス以下のアイテムを全てファイルビューに追加
    {
        m_wndFileView.DeleteAllItems();
        HTREEITEM hRoot = m_wndFileView.InsertItem(dirPath, 0, 0);
        m_wndFileView.SetItemState(hRoot, TVIS_BOLD, TVIS_BOLD);
        AddItems(dirPath, hRoot);

        // ルートフォルダを展開
        m_wndFileView.Expand(hRoot, TVE_EXPAND);
    }
}

Temporary フォルダに適当にファイルやフォルダを置いてテストします。
実行結果がこちら↓

これらは空のファイルなので、アイテムを選択しても情報は得られません。

次は、ファイルビューにてポップアップメニュー(右クリックメニュー)を開いた時に
メニューに「モデルの追加(A)」の項目があり、これを選択するとxmlファイルの選択を促すダイアログが現れる
という動作を実装してみましょう。

ポップアップメニュー(右クリックメニュー)の編集

編集場所は、「リソースビュー」→「Menu」です。右クリックでメニューを挿入します。
プロパティにてIDを「IDR_POPUP_FILE_VIEW」に設定し、次のように項目を追加します。

ファイルビューで右クリックしたときに呼ばれるイベントハンドラはすでに実装されているので
それをマネして、次のように追加・修正します。

void SimpleConverter2App::PreLoadState()
{
    BOOL bNameValid;
    CString strName;
    bNameValid = strName.LoadString(IDS_EDIT_MENU);
    ASSERT(bNameValid);
    GetContextMenuManager()->AddMenu(strName, IDR_POPUP_EDIT);
    bNameValid = strName.LoadString(IDS_EXPLORER);
    ASSERT(bNameValid);
    GetContextMenuManager()->AddMenu(strName, IDR_POPUP_EXPLORER);
    // ファイルビューのポップアップメニューを追加
    bNameValid = strName.LoadString(IDS_FILE_VIEW);
    ASSERT(bNameValid);
    GetContextMenuManager()->AddMenu(strName, IDR_POPUP_FILE_VIEW);
}
void CFileView::OnContextMenu(CWnd* pWnd, CPoint point)
{
    CTreeCtrl* pWndTree = (CTreeCtrl*) &m_wndFileView;
    ASSERT_VALID(pWndTree);

    if (pWnd != pWndTree)
    {
        CDockablePane::OnContextMenu(pWnd, point);
        return;
    }

    if (point != CPoint(-1, -1))
    {
        // クリックされた項目の選択:
        CPoint ptTree = point;
        pWndTree->ScreenToClient(&ptTree);

        UINT flags = 0;
        HTREEITEM hTreeItem = pWndTree->HitTest(ptTree, &flags);
        if (hTreeItem != NULL)
        {
            pWndTree->SelectItem(hTreeItem);
        }
    }

    pWndTree->SetFocus();
    theApp.GetContextMenuManager()->ShowPopupMenu(IDR_POPUP_FILE_VIEW, point.x, point.y, this, TRUE);
}

実装後、ビルドしてテストを行った結果↓

「モデルの追加(A)」の項目が現れました。
これをクリックした時にXMLファイル選択ダイアログを表示するには
まず、リソースの項目を右クリックしてイベントハンドラの追加を行います。

コマンドかつ追加先クラスをファイルビューにして追加します。

すると、次のようなコードが勝手に追加されるので、この関数の中にファイル選択ダイアログを
表示させるコードを追加します。

void CFileView::OnXmlAdd()
{
    // TODO: ここにコマンド ハンドラ コードを追加します。
    // @debug ファイル選択ダイアログの表示
}

ファイル選択ダイアログは
こちら↓を参考に実装してみましょう。

ファイル選択ダイアログ

読めば誰でも実装できる説明ですね。
こちら↓私が実装したイベントハンドラです。

/**********************************************************************//**
    @brief        ポップアップメニューの「モデルの追加(A)」のイベントハンドラ
    @author        Simplestar
    @par    [説明]
            複数選択可能なXMLファイル選択ダイアログを表示
            複数のXMLファイルパスを構築し、これを解析にかけます。
            解析するのは別クラスになります。@debug クラス名は未定
            変換処理、変換後のファイルの出力後、ファイルビューの更新を行います。
*/
/***********************************************************************/
void CFileView::OnXmlAdd()
{
    CStringArray    xmlFileList;    // XMLファイルパスリスト
    bool            bError = false;    // エラーフラグ

    // ファイル選択ダイアログの初期化
    CString        filter("中間データ (*.xml)|*.xml||");
    CFileDialog    fileSelDlg(TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT, filter);

    // ファイル名リスト用のメモリ確保
    CString            strBuf;
    try
    {
        fileSelDlg.GetOFN().lpstrFile = strBuf.GetBuffer(MAX_PATH * 256);
        fileSelDlg.GetOFN().nMaxFile = MAX_PATH * 256;
    }
    catch (...)
    {
        // メモリ確保に失敗
        bError = 1;
    }

    // ファイル選択ダイアログの表示
    if (!bError)
    {
        if (IDOK != fileSelDlg.DoModal())
        {
            bError = true;
        }
    }

    // ID_OKが押されている場合、選択したファイルパスのリストの作成
    POSITION        startPosition = NULL;
    if (!bError)
    {
        UpdateData(TRUE);
        if (NULL == (startPosition = fileSelDlg.GetStartPosition()))
        {
            bError = true;
        }
        else
        {
            CString        filePath;
            while (startPosition)
            {
                filePath = fileSelDlg.GetNextPathName(startPosition);
                xmlFileList.Add(filePath);
            }
        }
        UpdateData(FALSE);
    }

    // 確保したメモリを解放する
    strBuf.ReleaseBuffer();

    // @debug xmlファイルの解析、変換、変換後のファイルを出力(別クラスの仕事)
    // 解析に失敗した場合、そのファイルの読み込みをスキップする
    // 正常に処理できるものは処理をして、駄目だったファイルを理由を付けて報告する
    // @debug 選択したものの先頭ファイルをプレビューに表示

    // @debug 選択パスの確認
    for (int i=0; i<xmlFileList.GetSize();++i)
    {
        CString filePath = xmlFileList.GetAt(i);
    }
}

ちょっと長くなったので
中間データのxmlファイルを解析してデータを保持するクラスと
データを変換出力するクラス
は次回に持ち越します。

2010/12/18 初記。
2011/01/09 追加。

あけましておめでとう!
年末は実家で家族麻雀をするのが、恒例行事なのです。
久しぶりに役満であがりました。「大三元」です。
しばらく先になるかと思いますが、物理エンジンの講座の時に麻雀パイを
使用してみようかなぁと構想してます。

:::::::::::034話 シェーダマテリアル@:::::::::::

  • BACK
  • HOME
  • NEXT
ホームページ制作 フリー素材 無料WEB素材
Copyright (C) Simplestar All Rights Reserved.