Simplestar Game

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

:::::::::::031話 3Dオブジェクトの表示A:::::::::::

どうも、星姫です。
今回はDirectX10を使って、3Dオブジェクトをモニタに表示します。

今回は3D描画ライブラリのDirectX 10に、どんな風に描画してもらうか情報を渡すだけ…なんですが
ちょっと設定項目が多すぎやしまんせんか?
そう思っているのは私だけ?

思えば3D数学を講座で扱おうと023話で3Dオブジェクトを表示しようとしたのが始まりでした。

これまで、3Dオブジェクトのデータを用意するためにBlenderから中間ファイルを出力し
その中間ファイルをバイナリデータに変換するコンバーターをC#で作成してきました。

え?Blenderのエクスポーターと、中間ファイルのコンバーターが欲しい?
それだけください!ってな方のためにDownloadページを設けました。
バグだらけですが(おいおい!)、使えそうと思うものはどんどん持っていってください。

さて本題に入ります。
まずは、変換したモデルデータ( .smplデータ)の読み込み方です。

入出力ストリームというのを使います。

こんな感じ↓
023話で示した

    // Create vertex buffer
    // Set vertex buffer
    // Create index buffer
    // Set index buffer

この部分を書き換えてみました。

    // Create Vertices Buffer and Indices Buffer
    {
        // ファイル読み込み
        ifstream ifs( "testData.smpl", ios_base::in | ios_base::binary );
        if (ifs.bad())
        {
            DEBUG_PRINT("ファイルの読み込みに失敗");
            return S_FALSE;
        }
        else if (ifs.good())
        {
            // ファイルサイズを取得
            const size_t fileSize = ifs.seekg(0, ios::end).tellg();
            ifs.seekg(0, ios::beg);
            // ファイルサイズ分のメモリ確保
            char *pTmpData = new char[fileSize];
            if (NULL == pTmpData)
            {
                DEBUG_PRINT("ファイルサイズ分のメモリ確保に失敗");
                return S_FALSE;
            }
            // ブロックリード
            memset(pTmpData, 0x0, fileSize);
            ifs.read(pTmpData, fileSize);
            // ファイルクローズ
            ifs.close();

            // ファイルヘッダへキャスト
            SMPLHeader *pStHeader = reinterpret_cast<SMPLHeader*>(pTmpData);

            // マジックコードの確認
            if (0 != memcmp( pStHeader->magicCode, SMPL_MAGIC_CODE, SMPL_CODE_SIZE))
            {
                DEBUG_PRINT("マジックコードが[%s]ではありません", SMPL_MAGIC_CODE);
                delete[]    pTmpData;
                return S_FALSE;
            }
            if (fileSize != pStHeader->intDataSize)
            {
                DEBUG_PRINT("実際のファイルサイズとヘッダ情報のファイルサイズが違います : load size = %d != header's size %d",
                    fileSize, pStHeader->intDataSize);
                return S_FALSE;
            }

            // Create Vertices Buffer
            D3D10_BUFFER_DESC bd;
            bd.Usage = D3D10_USAGE_DEFAULT;
            bd.ByteWidth = sizeof( SimpleVertex ) * pStHeader->numVertices;
            bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
            bd.CPUAccessFlags = 0;
            bd.MiscFlags = 0;
            D3D10_SUBRESOURCE_DATA InitData;
            InitData.pSysMem = reinterpret_cast<SimpleVertex*>(pTmpData + pStHeader->offsetVertices);
            hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );
            if( FAILED( hr ) )
            {
                DEBUG_PRINT("頂点バッファの作成に失敗");
                delete[] pTmpData;
                return hr;
            }

            // Set vertex buffer
            UINT stride = sizeof( SimpleVertex );
            UINT offset = 0;
            g_pd3dDevice->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );

            // Create Indices Buffer
            bd.Usage = D3D10_USAGE_DEFAULT;
            bd.ByteWidth = sizeof( DWORD ) * pStHeader->numFaces * 3;
            bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
            bd.CPUAccessFlags = 0;
            bd.MiscFlags = 0;
            InitData.pSysMem = reinterpret_cast<DWORD*>(pTmpData + pStHeader->offsetIndices);
            hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer );
            if( FAILED( hr ) )
            {
                DEBUG_PRINT("インデックスバッファの作成に失敗");
                delete[] pTmpData;
                return hr;
            }

            // Set index buffer
            g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );

            delete[] pTmpData;
        }
    }

いくつかこちらで定義した型を使っています。
それらを以下に示しますね。
また、SimpleConverterに頂点カラー情報を出力できる機能を追加しました。

こちらは、ファイルヘッダ情報を引き出すための構造体です。
ファイルデータのポインタをこれにキャストするだけで、簡単にファイル情報を聞き出せるようになります。

/**********************************************************************//**
  @struct            SMPLHeader
  @brief            .smplファイルヘッダ構造体
  @author            Simplestar
  @par    [説明]     
            version 1.0
*/
/***********************************************************************/
struct SMPLHeader
{
    bool    bLittleEndian;            //!< エンディアンフラグ
    char    reserved[3];            //!< 予約(常に0)
    char    magicCode[4];            //!< マジックコード{'s', 'm', 'p', 'l'}
    unsigned int format;            //!< 頂点フォーマット
    unsigned int offsetVertices;    //!< ファイル先頭から頂点リストまでのオフセット
    unsigned int numVertices;        //!< 頂点数
    unsigned int offsetIndices;        //!< ファイル先頭からインデックスリストまでのオフセット
    unsigned int numFaces;            //!< 面数
    unsigned int fileSize;            //!< ファイルサイズ
};

こちらは定数定義ですね。
無名の名前空間で囲うと、そのファイルの中だけで参照可能な定数を定義できます。
ファイル内で共有する定数は、こうした形で定義しましょう。

基本的に、グローバル定数は禁止です。
そうなると、チュートリアルのコードも修正しなければいけませんね。

namespace
{
    static const int    SMPL_CODE_SIZE        = 4;        //.smplファイルのマジックコードのサイズ
    static const char    SMPL_MAGIC_CODE[]    = "smpl";    //.smplファイルのコード文字列
}

こちらはデバッグ出力用の関数の定義です。
デバッグモード時に出力ウィンドウに設定した文字列と、ファイル名、行番号、関数名を出力します。

可変長引数とか、初心者には見慣れないものがあるけど、これprintf関数と同じ形式で使えるから便利ですよ。
一度書いたら、次回以降使いまわすものなので、めったに書くものではありません。
書き方を忘れてしまったら、ここに戻ってくるようにしましょう。

#define    __file__    (strrchr(__FILE__,'\\') + 1 )
#define    __line__    __LINE__
#define __func__    __FUNCSIG__

#ifdef DEBUG
    #define DEBUG_PRINT(...)  DebugPrint( __file__, __line__, __func__, __VA_ARGS__ )
#else
    #define DEBUG_PRINT(...) /* */
#endif

static inline void DebugPrint(const char *file, int line, const char *func, const char *fmt, ... )
{       
    va_list args;
    int len;
    char buffer[MAX_PATH];
    char str[MAX_PATH];
    wchar_t wStr[MAX_PATH];

    va_start( args, fmt );
    len = _vscprintf( fmt, args ) + 1; // for '\0'
    vsprintf_s( buffer, MAX_PATH, fmt, args );
    va_end( args );

    memset(str, 0x0, MAX_PATH);
    sprintf_s(str, MAX_PATH, "%s, %s: %d: %s\n", buffer, file, line, func);
   
    MultiByteToWideChar(CP_OEMCP, 0, str, MAX_PATH, wStr, MAX_PATH);
   
    OutputDebugString(static_cast<LPCTSTR>(wStr));   
}

さて、ファイルからモデルデータの読み込みと、良く使うデバッグ出力関数の確認ができたところで
これまでコピペしてきた DirectX 10 の設定部分の詳細を見ていきましょう。

え?、一番新しい DirectX は 11 なのに、なぜ 10 を扱うのかって?
それは、私のマシンが Windows Vista だからです。
11 を使うには、Windows 7 以上かつ、それを扱えるグラフィックボードが必要になります。
いつの日か…私のマシンが DirectX 11 を使えるようになったら、扱ってみようと思います。(まだ、そんなお金ないよ!)

まず、はじめにDirectX デベロッパー センターから開発者用のドキュメントを手に入れましょう。

DirectX デベロッパー センター

DirectX ドキュメント 日本語版
Windows DirectX グラフィック ドキュメント 日本語版

この2つくらいは、手に入るんじゃないでしょうか?
とりあえず、DirectX 10 とは? みたいな情報を一通り読んでみます。

…で、情報入力から画面出力までの流れについて、以下は最初に書かれている部分のほぼコピー&ペーストなのですが…

1.入力アセンブラー ステージ - パイプラインにデータ (三角形、線、およびポイント) を供給する
2.頂点シェーダー ステージ - 常に、単一の入力頂点を取り、単一の出力頂点を生成する
3.ジオメトリ シェーダー ステージ - 入力はプリミティブ全体である、入力プリミティブの破棄、または複数の新規プリミティブが出力される
4.ストリーム出力ステージ - メモリーにプリミティブ データを出力するように設計されている。メモリーに出力されたデータは、パイプラインに入力データとして読み込んで循環させるか、CPU から読める
5.ラスタライザー ステージ - ピクセル シェーダーの呼び出し方法の決定を実行する
6.ピクセル シェーダー ピクセル単位のデータ (カラーなど) を生成する
7.出力結合ステージ - 最終的なパイプラインの結果を生成する

…だそうです。

7つもステージがありますね、これ基本なので名前と順番は覚えないといけないみたいです。
DirectX 10 のチュートリアル05は、この7つのステージを書いているみたいなので、一つずつステージを追ってみましょう。

…と、その前にまだデバイス初期化の部分を明らかにしていませんでしたね。
そこから始めますか…

DirextXでウィンドウに描画するまでに必要なモノとして、まずグラフィックカードがあげられます。
PCで3Dゲームを楽しみたいという方は、一度はグラフィックカードのことを気にかけたことがあると思います。
DirectXはこのデバイス(装置)に描画の計算をさせるライブラリなので
まず、マシンが持つグラフィックカードへのアクセス子を作成するんです。
その時に使うのが次の関数です。

D3D10CreateDeviceAndSwapChain

なんか「スワップチェインも」とか書かれているけど、同時に作るっぽいね。
DXGI_SWAP_CHAIN_DESC というディスクリプタ(設定)にバックバッファというモニタの裏側の情報を設定して
D3D10CreateDeviceAndSwapChain関数にディスクリプタを渡して使うみたい。

とりあえず、ここまでで作られるアクセス子は次の二つ

・ID3D10Device* pd3dDevice
・IDXGISwapChain* pSwapChain

次に作るのはこちら、レンダリングターゲット

・ID3D10RenderTargetView* pRenderTargetView

ややこしいかもしれないけど、スワップチェインが持つバッファをリソースとして取り出し
次のデバイスの関数を元に作成するんだって…ドキュメントにも書いてあったよ。

pd3dDevice->CreateRenderTargetView

pd3dDeviceはこんな感じで、あちこちで使われるけど pSwapChain は、これ以降レンダリング結果をモニタへ表示する
Present関数を呼ぶまで出番は無いようですね。

で、次に作るのが深度ステンシル

・ID3D10Texture2D* pDepthStencil

デバイスの pd3dDevice->CreateTexture2D 関数で作成してます。
また、深度ステンシルビューっていう次の

・ID3D10DepthStencilView* pDepthStencilView

もデバイスの pd3dDevice->CreateDepthStencilView 関数で作成してますね。

続いて、ここまでに作成してきたビュー2つ(pRenderTargetView, pDepthStencilView)
を出力結合ステージ関係で渡してなんか設定している様子

pd3dDevice->OMSetRenderTargets

さらに、今回のDirectX 10 から、やらなければいけない作業の一つである、ビューポートの設定を行ってました

具体的にはデバイスの pd3dDevice->RSSetViewports 関数を呼ぶだけです

さて、次にエフェクトをファイルから作成していますね。
ファイルとは、プロジェクトにぶら下がっている.fxファイルのことなんだけど、ちょっと中身を確認してみましょうか。

//--------------------------------------------------------------------------------------
// File: Tutorial05.fx
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------

//--------------------------------------------------------------------------------------
// Constant Buffer Variables
//--------------------------------------------------------------------------------------
matrix World;
matrix View;
matrix Projection;

//--------------------------------------------------------------------------------------
struct VS_INPUT
{
    float4 Pos : POSITION;
    float4 Color : COLOR;
};

struct PS_INPUT
{
    float4 Pos : SV_POSITION;
    float4 Color : COLOR;
};


//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
PS_INPUT VS( VS_INPUT input )
{
    PS_INPUT output = (PS_INPUT)0;
    output.Pos = mul( input.Pos, World );
    output.Pos = mul( output.Pos, View );
    output.Pos = mul( output.Pos, Projection );
    output.Color = input.Color;
   
    return output;
}


//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PS( PS_INPUT input) : SV_Target
{
    return input.Color;
}


//--------------------------------------------------------------------------------------
technique10 Render
{
    pass P0
    {
        SetVertexShader( CompileShader( vs_4_0, VS() ) );
        SetGeometryShader( NULL );
        SetPixelShader( CompileShader( ps_4_0, PS() ) );
    }
}

定数として、ワールドとビュー、プロジェクションの3つの行列が宣言されていて
頂点シェーダー
ピクセルシェーダー
パスが一つだけのテクニック…が書かれているのが分かります。

ここで、エフェクト、テクニックってなんでしょうね?
ここでいう何なのかとは、DirectXにおいて何を示しているかですよ。
これらについて、ドキュメントにはこう書かれていました。(コピペ)

DirectX エフェクトは、HLSL で記述された表現とエフェクト フレームワーク固有の構文で設定されたパイプライン ステートの集合です。
テクニックとは、レンダリング パスの集合です (1 つ以上のパスが存在する必要があります)。
エフェクト内のパイプライン ステートの一つです。

初心者にはわかりずらいでしょ、この文章…
あの、HLSL ってのはシェーダーを扱う時に使う言語を指してます。
つまり…そのシェーダー言語で書かれた一連の処理を「エフェクト」と呼び
「エフェクト」内のパイプラインの一つを「テクニック」と呼ぶみたいですね。

そんなわけで、.fx ファイルから名前を指定してテクニックへのアクセス子を取得しています。
次の2つが.fxファイルから作られます。

・ID3D10Effect* pEffect
・ID3D10EffectTechnique* pTechnique

また、.fxファイル内の変数(今回は行列)のアクセス子を取ってきてます。
名前やインデックスから取得なんて、ID3D10Effectがファイルを解析してデータで持っているイメージがつかめます。

    // Obtain the variables
    g_pWorldVariable = g_pEffect->GetVariableByName( "World" )->AsMatrix();
    g_pViewVariable = g_pEffect->GetVariableByName( "View" )->AsMatrix();
    g_pProjectionVariable = g_pEffect->GetVariableByName( "Projection" )->AsMatrix();

次に記述されているのが頂点レイアウトの作成です。

D3D10_INPUT_ELEMENT_DESC layout[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D10_INPUT_PER_VERTEX_DATA, 0 },
            { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D10_INPUT_PER_VERTEX_DATA, 0 },
        };
        UINT numElements = sizeof( layout ) / sizeof( layout[0] );

レイアウトの要素をシェーダの入力形式と合うようにして、記述しているようですね。
ほか、次のようにテクニックの0番目のパスのディスクリプタを取得し、レイアウトを作成しています。

        D3D10_PASS_DESC PassDesc;
        g_pTechnique->GetPassByIndex( 0 )->GetDesc( &PassDesc );
        hr = g_pd3dDevice->CreateInputLayout( layout, numElements, PassDesc.pIAInputSignature,
            PassDesc.IAInputSignatureSize, &g_pVertexLayout );

ここで作られるのが、次の頂点レイアウトですね。

・ID3D10InputLayout* pVertexLayout

    // Set the input layout
    g_pd3dDevice->IASetInputLayout( g_pVertexLayout );

レイアウトを作ったらさっそく、デバイスに設定していますね。

            // Create Vertices Buffer
            D3D10_BUFFER_DESC bd;
            {
                bd.Usage = D3D10_USAGE_DEFAULT;
                bd.ByteWidth = sizeof( SimpleVertex ) * pStHeader->numVertices;
                bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
                bd.CPUAccessFlags = 0;
                bd.MiscFlags = 0;
            }
            D3D10_SUBRESOURCE_DATA InitData;
            InitData.pSysMem = reinterpret_cast<SimpleVertex*>(pTmpData + pStHeader->offsetVertices);
            hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer );
            if( FAILED( hr ) )
            {
                DEBUG_PRINT("頂点バッファの作成に失敗");
                delete[] pTmpData;
                return hr;
            }

            // Set vertex buffer
            UINT stride = sizeof( SimpleVertex );
            UINT offset = 0;
            g_pd3dDevice->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );

            // Create Indices Buffer
            {
                bd.Usage = D3D10_USAGE_DEFAULT;
                bd.ByteWidth = sizeof( DWORD ) * pStHeader->numFaces * 3;
                bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
                bd.CPUAccessFlags = 0;
                bd.MiscFlags = 0;
            }
            InitData.pSysMem = reinterpret_cast<DWORD*>(pTmpData + pStHeader->offsetIndices);
            hr = g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pIndexBuffer );
            if( FAILED( hr ) )
            {
                DEBUG_PRINT("インデックスバッファの作成に失敗");
                delete[] pTmpData;
                return hr;
            }

            // Set index buffer
            g_pd3dDevice->IASetIndexBuffer( g_pIndexBuffer, DXGI_FORMAT_R32_UINT, 0 );

続いてお決まりの、頂点バッファの作成とインデックスバッファの作成を行っています。
次の2つが作られます。

・ID3D10Buffer* pVertexBuffe
・ID3D10Buffer* pIndexBuffer

最後にトポロジの設定をしていますね。データのトポロジと合わせて三角形リストを指定します。

    // Set primitive topology
    g_pd3dDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

さて、ここまでデバイスの初期化と入力アセンブラステージ、各シェーダのステージ(.fxファイルを確認しただけですが)を見てきました。
これが全7ステージですが、いかがでしたか?(やっぱ煩雑だよね…覚えることが多いね。)

ちょっと、まとめてみましょう!
次に列挙するのが、最低限必要となる変数です。

D3D10_DRIVER_TYPE          driverType        = D3D10_DRIVER_TYPE_NULL;
ID3D10Device*              pd3dDevice        = NULL;
IDXGISwapChain*            pSwapChain        = NULL;
ID3D10RenderTargetView*    pRenderTargetView    = NULL;
ID3D10Texture2D*            pDepthStencil    = NULL;
ID3D10DepthStencilView*    pDepthStencilView    = NULL;
ID3D10Effect*              pEffect        = NULL;
ID3D10EffectTechnique*      pTechnique        = NULL;
ID3D10InputLayout*          pVertexLayout    = NULL;
ID3D10Buffer*              pVertexBuffer    = NULL;
ID3D10Buffer*              pIndexBuffer    = NULL;
ID3D10EffectMatrixVariable* pWorldVariable    = NULL;
ID3D10EffectMatrixVariable* pViewVariable    = NULL;
ID3D10EffectMatrixVariable* pProjectionVariable = NULL;

これらの作り方を、これまで示してきたんですね。

バックバッファであるスワップチェインを画面解像度から作成し
中にあるバッファをリソースとして扱いレンダリングターゲットとして登録する。
深度ステンシルも同サイズのリソースとして用意して、ステンシルビューとして出力結合ステージへ登録。
デバイスはハードウェアに依存して作られ、エフェクトは.fxファイルから作られ
テクニックとシェーダで使う変数はエフェクトから作られる。

どうです?ほんの少しスッキリしました?(いや、初見の方はわからんかもしれません)
では、描画関数を確認して本レッスンを締めくくりましょう!

ということで、次が描画関数です。

    //
    // Clear the back buffer
    //
    float ClearColor[4] = { 0.0f, 0.125f, 0.3f, 1.0f }; //red, green, blue, alpha
    g_pd3dDevice->ClearRenderTargetView( g_pRenderTargetView, ClearColor );

    //
    // Clear the depth buffer to 1.0 (max depth)
    //
    g_pd3dDevice->ClearDepthStencilView( g_pDepthStencilView, D3D10_CLEAR_DEPTH, 1.0f, 0 );

    //
    // Update variables for the first cube
    //
    g_pWorldVariable->SetMatrix( ( float* )&g_World1 );
    g_pViewVariable->SetMatrix( ( float* )&g_View );
    g_pProjectionVariable->SetMatrix( ( float* )&g_Projection );

    //
    // Render the first cube
    //
    D3D10_TECHNIQUE_DESC techDesc;
    g_pTechnique->GetDesc( &techDesc );
    for( UINT p = 0; p < techDesc.Passes; ++p )
    {
        g_pTechnique->GetPassByIndex( p )->Apply( 0 );
        g_pd3dDevice->DrawIndexed( 36, 0, 0 );
    }

    //
    // Present our back buffer to our front buffer
    //
    g_pSwapChain->Present( 0, 0 );

レンダリングターゲットと深度ステンシルビューをクリアして、シェーダで使う行列を設定してます。
テクニックを、テクニックのパスの数(今回は1個ですね)だけ繰り返し適用しながら
インデックスバッファの先頭から36個のインデックスを描画する命令を出しています。
最後にスワップチェインのバックバッファをフロントバッファへ切り替える関数を呼んでいます。

…と、まぁこんな感じでDirectX 10での3Dオブジェクトの描画の流れがつかめたでしょうか?
え?、行列って何ですかって?
そんな方は23話を読み返せば大丈夫です。

今回の DirectX 10 の入門講座をもって具体的にわかってきたことを挙げてみましょう。

ゲーム中に複数のオブジェクトを描画することが考えられますが、その場合
インデックスバッファに全てのオブジェクトの情報を納めておき(頂点バッファにもね)
描画時はオフセットとインデックス数を指定して描画するという手法が見えてきます。

決して、バッファを再構築しながら描画するなんて考えちゃだめだよ。
毎度ロードを挟むようなものなので、非常に低速なゲームになってしまうことが予想できます。

さて、DirectX 10 の描画における基礎を学んだところで、3D描画をもっと簡単にする独自の
ライブラリを作成してみましょう。
それでは次回を、お楽しみに!

2010/10/17 初記。
2010/10/31 追加。
2010/11/03 追加。

:::::::::::031話 3Dオブジェクトの表示A:::::::::::

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