2011/06/25

いまさら DirectX10(はじめの一歩篇)

※ このブログはただのお勉強日記だよ! 参考になるような解説記事は一切ないよ!

今は昔。

ぼくもネイティブな 3D プログラミングなるものに憧れ、当時主流だった DirectX9 をつまみ喰いした事がありました。
当時、DirectX9 をあれこれ試したときに作ったプログラミングノート
あれから時代は移ろい、いつの間にか最新のトレンドも DirectX11 に代わってしまったようです。

そんなわけで、そろそろ最新の API を学び直す時期かなぁと思い立ったわけですが、残念ながらぼくのパソコン(のグラボ)は DirectX11 には対応していないので、仕方なく DirectX10 に手をつけてみる事にしました。

さてさて。

DirectX10 からは固定機能パイプラインが無くなり、プログラマブルシェーダの理解が不可欠となってしまいました。 Vista 時代を迎えてかなり思い切った仕様変更だと思います。

何を隠そう、ぼくはこのシェーダに手を付ける事が怖くて、DirectX9 の頃まではほぼ固定機能パイプラインに頼りっきりだったため、DirectX10 になって開発の敷居ががくんと上がってしまいました。

こうやってどんどん梯子を外されていくうちにゲイツ様への忠誠心が確実に薄らいでいますが、それはさておき。

今回は、おっかなびっくり DirectX10 なるものに触れてみようかと思います。



【Win32 API 直叩きでウィンドウをつくる】

まずは Windows 使いにとってすっかりおなじみとなった、「ただウィンドウを表示する」という初歩的なプログラムからスタート。

ただし、ミューテックス(セマフォ)によって多重起動を禁止していたり、画面のサイズを固定したりと、ちょこちょこ小賢しいギミックを混ぜてあります。

※ 多重起動を許可したアプリケーションで静的変数やグローバル変数を使うと、すべてのウィンドウでメモリ空間を共有する事になるため、場合によっては致命的なバグになります。今回は手抜きのために DirectX 関連でグローバル変数(しかもポインタ)を使うつもりでいるため、多重起動を禁止にしました。

// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//
// ウィンドウを表示するだけのプログラム
//
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
#include <windows.h>


// アプリケーションとウィンドウの初期化に必要な定数
// ----------------------------------------
const LPCTSTR APP_NAME   = TEXT("APP_TERCEL_TECH");
const LPCTSTR MUTEX_NAME = TEXT("MUTEX_TERCEL_TECH");

// クライアント領域の幅と高さ
// ----------------------------------------
const UINT WIDTH  = 400;
const UINT HEIGHT = 300;

// タイマー関連
// ----------------------------------------
const UINT TIMER_ID     = 100;  // 作成するタイマーの識別ID
const UINT TIMER_ELAPSE = 16;   // WM_TIMERの発生間隔。60fps≒16ms



// コールバック関数(メッセージ処理)
// ========================================
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
                            WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_CREATE:
        return 0;

    case WM_TIMER:
        if(wParam != TIMER_ID)  // 関係ないタイマーは無視
            return DefWindowProc(hWnd, uMsg, wParam, lParam);

        // ----------------------------------------
        // 
        // ここに定期更新処理を追加してください
        //       ※ 1秒間に約60回実行される(はず)
        //
        // ----------------------------------------

        return 0;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}


// エントリポイント
// ========================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{

    // 多重起動防止
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME);
    if(GetLastError() == ERROR_ALREADY_EXISTS)
    {
        // 多重起動を検知した際には、
        // 既存のウィンドウを最前面に表示してプログラムを終了する
        HWND existingWnd = FindWindow(APP_NAME, NULL);
        if(existingWnd != NULL)
        {
            if(IsIconic(existingWnd))
                ShowWindowAsync(existingWnd, SW_RESTORE);

            SetForegroundWindow(existingWnd);
        }
        return -1;
    }

    // ウィンドウクラス登録
    WNDCLASSEX wc;
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WindowProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = sizeof(LONG_PTR);
    wc.hInstance     = hInstance;
    wc.hIcon         = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hCursor       = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDC_ARROW),
                           IMAGE_CURSOR,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND + 1;
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = APP_NAME;
    wc.hIconSm = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    if(!RegisterClassEx(&wc)) return -1;

    // クライアント領域のサイズから、ウィンドウそのものの幅と高さを計算
    UINT windowWidth  = WIDTH  + GetSystemMetrics(SM_CXFIXEDFRAME) * 2;
    UINT windowHeight = HEIGHT + GetSystemMetrics(SM_CYFIXEDFRAME) * 2 
                               + GetSystemMetrics(SM_CYCAPTION);
    // ウィンドウ生成
    HWND hWnd = CreateWindow(APP_NAME,
                    TEXT("はじめてのウィンドウ"),
                    WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    windowWidth,
                    windowHeight,
                    NULL,
                    NULL,
                    hInstance,
                    NULL);
    if(!hWnd) return -1;

    // ウィンドウ表示
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // タイマー生成
    SetTimer(hWnd, TIMER_ID, TIMER_ELAPSE, NULL);
    
    // メッセージループ
    MSG msg;
    while(TRUE)
    {
        if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    KillTimer(hWnd, TIMER_ID);
    CloseHandle(hMutex);

    return (int)msg.wParam;
}

このコード自体は DirectX とは一切関係ないですが、誰が作ってもだいたい一緒になる割に、書くのがそれなりに厄介な代物なので、一応テンプレートにして置いておくことにしました。



【はじめての DirectX10】

上記のコードに、DirectX10 (Direct3D) を初期化するコードを追加します。

どうも DirectX9 時代とレンダリングパイプラインが変わったせいか、初期化のコードだけでも以前とはだいぶ違う印象です。

一言で言うと、『DirectX9 と同じ事を再現するだけでも、手続きがより繁雑になっている』といったところでしょうか。

DirectX10 からは、様々なリソースがかなり抽象的に扱われるようになったらしく、それらをパイプラインにバインドするだけでもワンクッション処理が必要だったりして、正直面倒なのです。


// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//
// はじめてのDirectX10プログラム
//
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
#include <windows.h>

#include <d3d10.h>
#include <d3dx10.h>

#pragma comment(lib, "d3d10.lib")
#pragma comment(lib, "d3dx10.lib")


// アプリケーションとウィンドウの初期化に必要な定数
// ----------------------------------------
const LPCTSTR APP_NAME   = TEXT("APP_TERCEL_TECH");
const LPCTSTR MUTEX_NAME = TEXT("MUTEX_TERCEL_TECH");

// クライアント領域の幅と高さ
// ----------------------------------------
const UINT WIDTH  = 400;
const UINT HEIGHT = 300;

// タイマー関連
// ----------------------------------------
const UINT TIMER_ID     = 100;  // 作成するタイマーの識別ID
const UINT TIMER_ELAPSE = 16;   // WM_TIMERの発生間隔。60fps≒16ms

// DirectX 関連(グローバル変数)
// ----------------------------------------
ID3D10Device*           g_pDevice = NULL;
IDXGISwapChain*         g_pSwapChain = NULL;
ID3D10RenderTargetView* g_pRenderTargetView = NULL;
ID3D10DepthStencilView* g_pDepthStencilView = NULL;
ID3D10Texture2D*        g_pDepthStencil = NULL;


// ポインタの二重解放を防ぐ SAFE_RELEASE マクロ
// ========================================
#define SAFE_RELEASE(x) if(x){x->Release(); x = 0;}


HRESULT InitD3D(HWND);
void DestroyD3D();
void Background(float r, float g, float b);


// コールバック関数(メッセージ処理)
// ========================================
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
                            WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_CREATE:
        return 0;

    case WM_TIMER:
        if(wParam != TIMER_ID)  // 関係ないタイマーは無視
            return DefWindowProc(hWnd, uMsg, wParam, lParam);

        // ----------------------------------------
        //
        // ここに定期更新処理を追加してください
        //
        // ----------------------------------------

        return 0;

    case WM_PAINT:
        // ----------------------------------------
        // 背景を水色で塗りつぶし
        // ----------------------------------------
        Background(0.0f, 0.5f, 1.0f);
        //          R     G     B
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}


// エントリポイント
// ========================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME);
    if(GetLastError() == ERROR_ALREADY_EXISTS)
    {
        HWND existingWnd = FindWindow(APP_NAME, NULL);
        if(existingWnd != NULL)
        {
            if(IsIconic(existingWnd))
                ShowWindowAsync(existingWnd, SW_RESTORE);

            SetForegroundWindow(existingWnd);
        }
        return -1;
    }

    WNDCLASSEX wc;
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WindowProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = sizeof(LONG_PTR);
    wc.hInstance     = hInstance;
    wc.hIcon         = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hCursor       = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDC_ARROW),
                           IMAGE_CURSOR,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND + 1;
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = APP_NAME;
    wc.hIconSm = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    if(!RegisterClassEx(&wc)) return -1;

    UINT windowWidth  = WIDTH  + GetSystemMetrics(SM_CXFIXEDFRAME) * 2;
    UINT windowHeight = HEIGHT + GetSystemMetrics(SM_CYFIXEDFRAME) * 2 
                               + GetSystemMetrics(SM_CYCAPTION);
    HWND hWnd = CreateWindow(APP_NAME,
                    TEXT("はじめてのDirectX10"),
                    WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    windowWidth,
                    windowHeight,
                    NULL,
                    NULL,
                    hInstance,
                    NULL);
    if(!hWnd) return -1;

    // ----------------------------------------
    // Direct3D の初期化
    // ----------------------------------------
    if(FAILED(InitD3D(hWnd))) return -1;

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    SetTimer(hWnd, TIMER_ID, TIMER_ELAPSE, NULL);
    MSG msg;
    while(TRUE)
    {
        if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    // ----------------------------------------
    // Direct3D の後始末
    // ----------------------------------------
    DestroyD3D();


    KillTimer(hWnd, TIMER_ID);
    CloseHandle(hMutex);

    return (int)msg.wParam;
}

// Direct3D の初期化
// ========================================
HRESULT InitD3D(HWND hWnd)
{
    // デバイスとスワップチェーン作成
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferCount = 1;
    sd.BufferDesc.Width = WIDTH;
    sd.BufferDesc.Height = HEIGHT;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality  =0;
    sd.Windowed = TRUE;

    if(FAILED(D3D10CreateDeviceAndSwapChain(NULL, 
        D3D10_DRIVER_TYPE_HARDWARE,
        NULL,
        0,
        D3D10_SDK_VERSION,
        &sd,
        &g_pSwapChain,
        &g_pDevice))) return FALSE;

    // レンダーターゲットビューの作成
    ID3D10Texture2D* pBackBuffer;
    g_pSwapChain->GetBuffer(0,
        __uuidof(ID3D10Texture2D),
        (LPVOID*)&pBackBuffer);
    g_pDevice->CreateRenderTargetView(pBackBuffer,
        NULL,
        &g_pRenderTargetView);
    SAFE_RELEASE(pBackBuffer);

    // 深度ステンシルビューの作成
    D3D10_TEXTURE2D_DESC descDepth;
    descDepth.Width = WIDTH;
    descDepth.Height = HEIGHT;
    descDepth.MipLevels = 1;
    descDepth.ArraySize = 1;
    descDepth.Format = DXGI_FORMAT_D32_FLOAT;
    descDepth.SampleDesc.Count = 1;
    descDepth.SampleDesc.Quality = 0;
    descDepth.Usage = D3D10_USAGE_DEFAULT;
    descDepth.BindFlags = D3D10_BIND_DEPTH_STENCIL;
    descDepth.CPUAccessFlags = 0;
    descDepth.MiscFlags = 0;
    g_pDevice->CreateTexture2D(&descDepth, 
        NULL, 
        &g_pDepthStencil);
    g_pDevice->CreateDepthStencilView(g_pDepthStencil, 
        NULL, 
        &g_pDepthStencilView);

    // レンダーターゲットビューと深度ステンシルビューを
    // パイプラインにバインド
    g_pDevice->OMSetRenderTargets(1, 
        &g_pRenderTargetView, 
        g_pDepthStencilView);

    // ビューポートの設定
    D3D10_VIEWPORT vp;
    vp.Width = WIDTH;
    vp.Height = HEIGHT;
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;
    vp.TopLeftX = 0;
    vp.TopLeftY = 0;
    g_pDevice->RSSetViewports(1, &vp);
    
    // ラスタライズ設定
    D3D10_RASTERIZER_DESC rdc;
    ZeroMemory(&rdc, sizeof(rdc));
    rdc.CullMode = D3D10_CULL_NONE;
    rdc.FillMode = D3D10_FILL_SOLID;
    ID3D10RasterizerState* pIr = NULL;
    g_pDevice->CreateRasterizerState(&rdc, &pIr);
    g_pDevice->RSSetState(pIr);
    SAFE_RELEASE(pIr);

    return S_OK;
}

// Direct3D の後始末
// ========================================
void DestroyD3D()
{
    SAFE_RELEASE(g_pSwapChain);
    SAFE_RELEASE(g_pRenderTargetView);
    SAFE_RELEASE(g_pDepthStencilView);
    SAFE_RELEASE(g_pDepthStencil);
    SAFE_RELEASE(g_pDevice);
}

// 指定したRGB値で画面を塗りつぶす
// ========================================
void Background(float r, float g, float b)
{
    float bgColor[4] = {r, g, b, 1};

    g_pDevice->ClearRenderTargetView(g_pRenderTargetView, bgColor);
    g_pDevice->ClearDepthStencilView(g_pDepthStencilView, 
        D3D10_CLEAR_DEPTH, 
        1.0f, 
        0);

    // 画面の更新
    g_pSwapChain->Present(0, 0);
}

最初はヘタにプログラムの構造を複雑化したりはせず、もとのソースに愚直に書き足していった方が理解が早まるので(ぼくの場合は)、ひたすらべた書きしました。

初期化全体の流れは、説明されれば『あー、なるほどね』と思えるものの、個々の処理に関しては今ひとつ必然性が見えないもの(なんでこれがここで必要になるの?という関数とか)もいくつかあるので、上記のコードを『ゼロから書け』と言われたら心が折れそうな勢いです。

そして、地味に嫌らしいのが、参照カウンタの挙動が旧バージョンから密かに変更されているという現象。これ、無意識のうちに凶悪なバグを生むので、留意しないとたいへんな事になります。



【HLSL ではじめてのシェーダ】

DirectX9 までは、ポリゴン1枚を表示する程度ならば、固定機能パイプラインを使ってさっくり作れたものでした(いやそれでも充分に面倒だったのだけど)。

ですが、これからはどんなに単純なレンダリングであってもプログラマブルパイプラインを通してレンダリングする必要があるため、HLSL なるシェーダ言語を使わなければならないそうです(DirectX8 時代はシェーダ言語ではなくアセンブラでした)。

まずは簡単なところから、実際に手を動かして、マウスで三角形ポリゴンをぐりぐり動かすサンプルを作ってみる事に。

【Simple.fx】

cbuffer global
{
    matrix g_mWVP;
    float4 g_PolyColor;
};

// 頂点シェーダ
float4 VS(float4 Pos:POSITION):SV_POSITION
{
    Pos = mul(Pos, g_mWVP);
    return Pos;
}

// ピクセルシェーダ
float4 PS(float4 Pos:SV_POSITION):SV_Target
{
    return g_PolyColor;
}


【Main.cpp】

// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//
// はじめてのシェーダプログラム
//
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
#include <windows.h>

#include <d3d10.h>
#include <d3dx10.h>

#pragma comment(lib, "d3d10.lib")
#pragma comment(lib, "d3dx10.lib")
#pragma comment(lib, "d3dCompiler.lib")


// アプリケーションとウィンドウの初期化に必要な定数
// ----------------------------------------
const LPCTSTR APP_NAME   = TEXT("APP_TERCEL_TECH");
const LPCTSTR MUTEX_NAME = TEXT("MUTEX_TERCEL_TECH");

// クライアント領域の幅と高さ
// ----------------------------------------
const UINT WIDTH  = 400;
const UINT HEIGHT = 300;

// タイマー関連
// ----------------------------------------
const UINT TIMER_ID     = 100;  // 作成するタイマーの識別ID
const UINT TIMER_ELAPSE = 16;   // WM_TIMERの発生間隔。60fps≒16ms



LONG mouseX, mouseY;           // マウスのXY座標


// DirectX 関連(グローバル変数)
// ----------------------------------------

// ウィンドウごとに必要
ID3D10Device*           g_pDevice = NULL;
IDXGISwapChain*         g_pSwapChain = NULL;
ID3D10RenderTargetView* g_pRenderTargetView = NULL;
ID3D10DepthStencilView* g_pDepthStencilView = NULL;
ID3D10Texture2D*        g_pDepthStencil = NULL;
ID3D10RasterizerState*  g_pRasterizerState;

// シェーダ関連(グローバル変数)
// ----------------------------------------

// モデルの種類ごとに必要
ID3D10InputLayout*     g_pVertexLayout;
ID3D10VertexShader*    g_pVertexShader;
ID3D10PixelShader*     g_pPixelShader;
ID3D10Buffer*          g_pConstantBuffer;

// モデルごとに必要
ID3D10Buffer*          g_pVertexBuffer;



// ポインタの二重解放を防ぐ SAFE_RELEASE マクロ
// ========================================
#define SAFE_RELEASE(x) if(x){x->Release(); x = 0;}



// DirectX用データ構造体
// ========================================

// 頂点
struct Vertex {
    D3DXVECTOR3 v;
};

// コンスタントバッファ
struct ConstantBuffer {
    D3DXMATRIX  mWVP;
    D3DXVECTOR4 vColor;
};



// 関数プロトタイプ宣言
// ========================================
HRESULT InitD3D(HWND);
HRESULT InitShader();
HRESULT InitPolygon();
void    RenderPolygon();
void    DestroyD3D();



// コールバック関数(メッセージ処理)
// ========================================
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
                            WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_CREATE:
        return 0;

    case WM_TIMER:
        if(wParam != TIMER_ID)  // 関係ないタイマーは無視
            return DefWindowProc(hWnd, uMsg, wParam, lParam);

        // ----------------------------------------
        //
        // ここに定期更新処理を追加してください
        //
        // ----------------------------------------

        // マウス座標の取得とか
        POINT mousePt;
        GetCursorPos(&mousePt);
        ScreenToClient(hWnd, &mousePt);
        mouseX = mousePt.x - WIDTH  / 2;
        mouseY = mousePt.y - HEIGHT / 2;

        InvalidateRect(hWnd, NULL, TRUE);
        return 0;

    case WM_PAINT:
        // ----------------------------------------
        // ポリゴン描画
        // ----------------------------------------
        RenderPolygon();
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}


// エントリポイント
// ========================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME);
    if(GetLastError() == ERROR_ALREADY_EXISTS)
    {
        HWND existingWnd = FindWindow(APP_NAME, NULL);
        if(existingWnd != NULL)
        {
            if(IsIconic(existingWnd))
                ShowWindowAsync(existingWnd, SW_RESTORE);

            SetForegroundWindow(existingWnd);
        }
        return -1;
    }

    WNDCLASSEX wc;
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WindowProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = sizeof(LONG_PTR);
    wc.hInstance     = hInstance;
    wc.hIcon         = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hCursor       = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDC_ARROW),
                           IMAGE_CURSOR,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    wc.hbrBackground = NULL;
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = APP_NAME;
    wc.hIconSm = (HICON)LoadImage(NULL,
                           MAKEINTRESOURCE(IDI_APPLICATION),
                           IMAGE_ICON,
                           0,
                           0,
                           LR_DEFAULTSIZE | LR_SHARED);
    if(!RegisterClassEx(&wc)) return -1;

    UINT windowWidth  = WIDTH  + GetSystemMetrics(SM_CXFIXEDFRAME) * 2;
    UINT windowHeight = HEIGHT + GetSystemMetrics(SM_CYFIXEDFRAME) * 2 
                               + GetSystemMetrics(SM_CYCAPTION);
    HWND hWnd = CreateWindow(APP_NAME,
                    TEXT("はじめてのシェーダ"),
                    WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX,
                    CW_USEDEFAULT,
                    CW_USEDEFAULT,
                    windowWidth,
                    windowHeight,
                    NULL,
                    NULL,
                    hInstance,
                    NULL);
    if(!hWnd) return -1;

    // ----------------------------------------
    // Direct3D の初期化
    // ----------------------------------------
    if(FAILED(InitD3D(hWnd))) return -1;


    // ----------------------------------------
    // シェーダの初期化
    // ----------------------------------------
    if(FAILED(InitShader())) return -1;


    // ----------------------------------------
    // ポリゴンの初期化
    // ----------------------------------------
    if(FAILED(InitPolygon())) return -1;



    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    SetTimer(hWnd, TIMER_ID, TIMER_ELAPSE, NULL);
    MSG msg;
    while(TRUE)
    {
        if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    // ----------------------------------------
    // Direct3D の後始末
    // ----------------------------------------
    DestroyD3D();


    KillTimer(hWnd, TIMER_ID);
    CloseHandle(hMutex);

    return (int)msg.wParam;
}




// Direct3D の初期化
// ========================================
HRESULT InitD3D(HWND hWnd)
{
    // デバイスとスワップチェーン作成
    DXGI_SWAP_CHAIN_DESC sd;
    ZeroMemory(&sd, sizeof(sd));
    sd.BufferCount = 1;
    sd.BufferDesc.Width = WIDTH;
    sd.BufferDesc.Height = HEIGHT;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferDesc.RefreshRate.Numerator = 60;
    sd.BufferDesc.RefreshRate.Denominator = 1;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality  =0;
    sd.Windowed = TRUE;

    if(FAILED(D3D10CreateDeviceAndSwapChain(NULL, 
        D3D10_DRIVER_TYPE_HARDWARE,
        NULL,
        0,
        D3D10_SDK_VERSION,
        &sd,
        &g_pSwapChain,
        &g_pDevice))) return FALSE;

    // レンダーターゲットビューの作成
    ID3D10Texture2D* pBackBuffer;
    g_pSwapChain->GetBuffer(0,
        __uuidof(ID3D10Texture2D),
        (LPVOID*)&pBackBuffer);
    g_pDevice->CreateRenderTargetView(pBackBuffer,
        NULL,
        &g_pRenderTargetView);
    SAFE_RELEASE(pBackBuffer);

    // 深度ステンシルビューの作成
    D3D10_TEXTURE2D_DESC descDepth;
    descDepth.Width = WIDTH;
    descDepth.Height = HEIGHT;
    descDepth.MipLevels = 1;
    descDepth.ArraySize = 1;
    descDepth.Format = DXGI_FORMAT_D32_FLOAT;
    descDepth.SampleDesc.Count = 1;
    descDepth.SampleDesc.Quality = 0;
    descDepth.Usage = D3D10_USAGE_DEFAULT;
    descDepth.BindFlags = D3D10_BIND_DEPTH_STENCIL;
    descDepth.CPUAccessFlags = 0;
    descDepth.MiscFlags = 0;
    g_pDevice->CreateTexture2D(&descDepth, 
        NULL, 
        &g_pDepthStencil);
    g_pDevice->CreateDepthStencilView(g_pDepthStencil, 
        NULL, 
        &g_pDepthStencilView);

    // レンダーターゲットビューと深度ステンシルビューを
    // パイプラインにバインド
    g_pDevice->OMSetRenderTargets(1, 
        &g_pRenderTargetView, 
        g_pDepthStencilView);

    // ビューポートの設定
    D3D10_VIEWPORT vp;
    vp.Width = WIDTH;
    vp.Height = HEIGHT;
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;
    vp.TopLeftX = 0;
    vp.TopLeftY = 0;
    g_pDevice->RSSetViewports(1, &vp);
    
    // ラスタライズ設定
    D3D10_RASTERIZER_DESC rdc;
    ZeroMemory(&rdc, sizeof(rdc));
    rdc.CullMode = D3D10_CULL_NONE;
    rdc.FillMode = D3D10_FILL_SOLID;
    g_pDevice->CreateRasterizerState(&rdc, &g_pRasterizerState);
    g_pDevice->RSSetState(g_pRasterizerState);

    return S_OK;
}


// シェーダの初期化
// ========================================
HRESULT InitShader() {
    // fxファイル読み込み
    ID3D10Blob* pCompiledShader = NULL;
    ID3D10Blob* pErrors         = NULL;

    // 頂点シェーダの作成
    if(FAILED(D3DX10CompileFromFile(TEXT("Simple.fx"),
        NULL,
        NULL,
        "VS",
        "vs_4_0",
        0,
        0,
        NULL,
        &pCompiledShader,
        &pErrors,
        NULL)))
    {
        // 読み込み失敗
        return E_FAIL;
    }
    SAFE_RELEASE(pErrors);

    if(FAILED(g_pDevice->CreateVertexShader(
        pCompiledShader->GetBufferPointer(),
        pCompiledShader->GetBufferSize(),
        &g_pVertexShader)))
    {
        // 頂点シェーダ作成失敗
        SAFE_RELEASE(pCompiledShader);
        return E_FAIL;
    }

    // 頂点インプットレイアウトを定義
    D3D10_INPUT_ELEMENT_DESC layout[] =
    {
        {
            "POSITION", 
            0, 
            DXGI_FORMAT_R32G32B32_FLOAT, 
            0, 
            0, 
            D3D10_INPUT_PER_VERTEX_DATA, 
            0
        },
    };
    UINT numElements = sizeof(layout)/sizeof(layout[0]);

    // 頂点インプットレイアウトを作成
    if(FAILED(g_pDevice->CreateInputLayout(layout,
        numElements, 
        pCompiledShader->GetBufferPointer(),
        pCompiledShader->GetBufferSize(),
        &g_pVertexLayout)))
    {
        return E_FAIL;
    }

    // Blobからピクセルシェーダを作成
    if(FAILED(D3DX10CompileFromFile(TEXT("Simple.fx"),
        NULL,
        NULL,
        "PS",
        "ps_4_0",
        0,
        0,
        NULL,
        &pCompiledShader,
        &pErrors,
        NULL)))
    {
        // 読み込み失敗
        return E_FAIL;
    }
    SAFE_RELEASE(pErrors);

    if(FAILED(g_pDevice->CreatePixelShader(
        pCompiledShader->GetBufferPointer(),
        pCompiledShader->GetBufferSize(),
        &g_pPixelShader)))
    {
        // ピクセルシェーダ作成失敗
        SAFE_RELEASE(pCompiledShader);
        return E_FAIL;
    }
    SAFE_RELEASE(pCompiledShader);

    // コンスタントバッファ作成
    D3D10_BUFFER_DESC cb;
    cb.BindFlags = D3D10_BIND_CONSTANT_BUFFER;
    cb.ByteWidth = sizeof(ConstantBuffer);
    cb.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
    cb.MiscFlags = 0;
    cb.Usage = D3D10_USAGE_DYNAMIC;

    if(FAILED(g_pDevice->CreateBuffer(&cb, 
        NULL,
        &g_pConstantBuffer)))
    {
        return E_FAIL;
    }
    return S_OK;
}

// ポリゴンの初期化
// ========================================
HRESULT InitPolygon() {
    // 頂点バッファ作成
    Vertex vertices[] =
    {
        D3DXVECTOR3( 0.0f,  0.5f, 0.0f),
        D3DXVECTOR3( 0.5f, -0.5f, 0.0f),
        D3DXVECTOR3(-0.5f, -0.5f, 0.0f),
    };

    D3D10_BUFFER_DESC bd;
    bd.Usage = D3D10_USAGE_DEFAULT;
    bd.ByteWidth = sizeof(Vertex) * 3;
    bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
    bd.CPUAccessFlags = 0;
    bd.MiscFlags = 0;

    D3D10_SUBRESOURCE_DATA initData;
    initData.pSysMem = vertices;
    if(FAILED(g_pDevice->CreateBuffer(&bd, 
        &initData, 
        &g_pVertexBuffer)))
    {
        return E_FAIL;
    }

    // 頂点バッファをセット
    UINT stride = sizeof(Vertex);
    UINT offset = 0;
    g_pDevice->IASetVertexBuffers(0, 1, &g_pVertexBuffer, &stride, &offset);

    return S_OK;
}


// ポリゴンの描画
// ========================================
void RenderPolygon()
{
    float bgColor[4] = {0, 0.5f, 1, 1};

    g_pDevice->ClearRenderTargetView(g_pRenderTargetView, bgColor);
    g_pDevice->ClearDepthStencilView(g_pDepthStencilView, 
        D3D10_CLEAR_DEPTH, 
        1.0f, 
        0);

    
    D3DXMATRIX mRotY, mRotX, mWorld;
    D3DXMATRIX mView;
    D3DXMATRIX mProj;

    // ワールド変換
    D3DXMatrixRotationY(&mRotY, mouseX / 100.0f);
    D3DXMatrixRotationX(&mRotX, mouseY / 100.0f);

    mWorld = mRotY * mRotX;

    // ビュー変換
    D3DXVECTOR3 vEyePt(0.0f, 1.0f, -2.0f);      // 視点
    D3DXVECTOR3 vLookAtPt(0.0f, 0.0f, 0.0f);    // 注視点
    D3DXVECTOR3 vUp(0.0f, 1.0f, 0.0f);          // 上方向
    D3DXMatrixLookAtLH(&mView, &vEyePt, &vLookAtPt, &vUp);

    // 射影変換
    D3DXMatrixPerspectiveFovLH(&mProj, 
        (FLOAT)D3DX_PI/4, 
        (FLOAT)WIDTH/(FLOAT)HEIGHT, 
        0.1f, 
        100.0f);

    // シェーダの登録
    g_pDevice->VSSetShader(g_pVertexShader);
    g_pDevice->PSSetShader(g_pPixelShader);

    // シェーダのコンスタントバッファにデータを渡す
    ConstantBuffer* pcb;
    if(SUCCEEDED(g_pConstantBuffer->Map(
        D3D10_MAP_WRITE_DISCARD,
        NULL,
        (void**)&pcb)))
    {
        // 変換行列
        D3DXMATRIX mWVP = mWorld * mView * mProj;
        D3DXMatrixTranspose(&mWVP, &mWVP);
        pcb->mWVP = mWVP;

        // カラー
        D3DXVECTOR4 vColor(0.5, 1, 0, 1);
        pcb->vColor = vColor;

        g_pConstantBuffer->Unmap();
    }

    // コンスタントバッファを使うシェーダの登録
    g_pDevice->VSSetConstantBuffers(0, 1, &g_pConstantBuffer);
    g_pDevice->PSSetConstantBuffers(0, 1, &g_pConstantBuffer);

    // 頂点インプットレイアウトをセット
    g_pDevice->IASetInputLayout(g_pVertexLayout);

    // プリミティブトポロジをセット
    g_pDevice->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // プリミティブをレンダリング
    g_pDevice->Draw(3, 0);

    // 画面の更新
    g_pSwapChain->Present(0, 0);
}



// Direct3D の後始末
// ========================================
void DestroyD3D()
{
    SAFE_RELEASE(g_pRasterizerState);
    SAFE_RELEASE(g_pSwapChain);
    SAFE_RELEASE(g_pRenderTargetView);
    SAFE_RELEASE(g_pDepthStencilView);
    SAFE_RELEASE(g_pDepthStencil);
    SAFE_RELEASE(g_pDevice);
}

Simple.fx が、くだんのシェーダファイルです。

シェーダは C 言語っぽい書き方が可能ですが、微妙に異なる構文がかえっていやらしい。

さらに、シェーダとアプリケーション側(C++)で整合性をとるため、慣れないうちは多少の煩わしさに耐えねばなりません。

たとえば、アプリケーション側のデータをシェーダに流し込むために必要なコンスタントバッファの構造体も、仕様を統一しなければならないのです(これが仕方のない事であるというのは重々解っているつもりではありますが……)。

【Simple.fx】
cbuffer global
{
    matrix g_mWVP;
    float4 g_PolyColor;
};

【Main.cpp】
struct ConstantBuffer {
    D3DXMATRIX  mWVP;
    D3DXVECTOR4 vColor;
};

ほかにも、頂点シェーダやピクセルシェーダをコンパイルする際、各々の関数名を明示的に指定する必要があったりとか色々アレですが、ざっと触ってみた印象としては『こんなもんかぁ』という感じです。

ちなみに、InitPolygon から RenderPolygon までの一連の処理のわかりやすい解説が、MSDN にあがっていました。



で。

さっそく気になる点がひとつ。

ポリゴンの頂点座標は、以下のように InitPolygon 関数で決め打ちしてしまっています。
// ポリゴンの初期化
// ========================================
HRESULT InitPolygon() {
    // 頂点バッファ作成
    Vertex vertices[] =
    {
        D3DXVECTOR3( 0.0f,  0.5f, 0.0f),
        D3DXVECTOR3( 0.5f, -0.5f, 0.0f),
        D3DXVECTOR3(-0.5f, -0.5f, 0.0f),
    };

    D3D10_BUFFER_DESC bd;
    bd.Usage = D3D10_USAGE_DEFAULT;
    bd.ByteWidth = sizeof(Vertex) * 3;
    bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
    bd.CPUAccessFlags = 0;
    bd.MiscFlags = 0;

    D3D10_SUBRESOURCE_DATA initData;
    initData.pSysMem = vertices;
    if(FAILED(g_pDevice->CreateBuffer(&bd, 
        &initData, 
        &g_pVertexBuffer)))
    {
        return E_FAIL;
    }

    // 頂点バッファをセット
    UINT stride = sizeof(Vertex);
    UINT offset = 0;
    g_pDevice->IASetVertexBuffers(0, 1, &g_pVertexBuffer, &stride, &offset);

    return S_OK;
}

並進や回転などの個別の運動はその都度コンスタントバッファに流し込めるので、形状が固定的なメッシュのレンダリング等はこれで充分でしょうが、たとえばデバッグ目的等で任意の長さの線分を空間中に惹きたい場合はどうするのかなぁ……(※ DirectX9 の頃までは、IDirect3DDevice9::DrawPrimitiveUP 関数で動的に線を引けたので、いちいち頂点バッファを使うまわりくどい真似はしませんでした)。

……と思ったけれど、よくよく考えたらコンスタントバッファにアクセスできるのだから、同様に頂点バッファに格納されている中身だって読み書きできて当たり前でした(てへぺろ

ただし、一度確保した頂点バッファへのポインタはしっかり保持し続けねばなりません(これも当然ですね)。

うーん。

なんというか、描画のパフォーマンスとグラフィックス表現の柔軟性を優先させた結果、データ結合度がものすごく高くなってしまっている気がして、個人的に少し気持ちが悪いです。
 慣れていくしかないのかなぁ……。



逆に、改良されたなと思ったのが、基本的にデバイスロストの回避処理をコーディングしなくてよくなった事。

従来ならば、(対策の甘い)DirectX プログラムを実行中に、別のフルスクリーンアプリケーション(スクリーンセーバー等)に切り替えたりすると、速攻でデバイスロストが発生してプログラムの再起動を余儀なくされたものです。

さらに、デバイスロストからの復活処理自体も、ヘタに書けばメモリリークをガンガン引き起こして本当に危なっかしかったのですが、そういった危険な橋を渡らずに済むのはよい事です。

試しに、上記のプログラムを実行中に『Ctrl + Alt + Delete』を押して、フルスクリーンのログオンフレームに強制的に切り替えてみました。

DirectX9 までは、たったこれだけの操作で簡単にデバイスロストが発生し、再起動を余儀なくされていましたが、今回は見事にびくともしませんでした。デバイスロスト回避コードを一切書いていないにも拘わらず、です。

このように、旧バージョンと比較して一長一短ありますが、せっかくだからもうちょっと使えるようになりたいなぁと思いました。まる。

1 件のコメント:

ひとことどうぞφ(・ω・,,)