TOP > PROGRAM

Direct3D 12 入門

1 はじめに…

いよいよDirect3D12の正式版が公開されました。まずは,いつものように三角形ポリゴンを表示することから始めていきます。
ポリゴンの描画

2 アプリの初期化の流れ

d3d11の時のように,ざっとアプリケーションの処理フローをまとめておきましょう。
先に断わっておきますが,今回はユニバーサルアプリではなくデスクトップアプリとして作っているので,ウィンドウの生成とかはレガシーなやり方でいきます。*.xaml使ったナウいやり方にはそのうち時間があれば紹介します。しばらくクラシックなままで行きます。あしからず。
基本的にはDirect3Dオブジェクト作って,描画コマンドを作って,実行して表示させる。作ったものは後始末するという流れです。
d3d11の時は,ID3D11DeviceContextとして色々と隠蔽されていたものが,表に出てくるのでその分手数が増えます。基本的にはコマンドリストに描画コマンドを積んで行って最後にコマンドキューにコミットして実行させるといった感じです。コンシューマーゲーム機で開発しているゲームプログラマの人であれば説明はいらないでしょう。この辺の説明が欲しい人はMantleAPIのドキュメントに説明があるので,一読しておくとよいでしょう。

アプリ初期化の流れ
図 1: アプリ初期化の流れ

3 ウィンドウの生成

D3D11の時と変わりません。普通にWindownAPI使って作ります。
以上。

4 デバイスの生成

デバイスの生成はD3D11と比べると大分楽になりました。下記のような感じです。

    // デバイスを生成.
    hr = D3D12CreateDevice( nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS( m_pDevice.GetAddress() ) );
    if ( FAILED( hr ))
    {
        ELOG( "Error : D3D12CreateDevice() Failed." );
        return false;
    }

D3D12CreateDeviceの第1引数はビデオアダプターへのポインタを設定します。nullptrを設定した場合はデフォルトアダプターが使用されます。 第2引数はデバイス生成に成功する最小の機能レベルを設定します。D3D12を使う場合は基本D3D11_FEATURE_LEVEL_11_0以上を設定しておけば問題ないでしょう。第3引数はインタフェースのGUID,第4引数はインタフェースへのポインタになりますが,IID_PPV_ARGSマクロを使っておくと簡単で済むので,IID_PPV_ARGSマクロを使っています。

5 コマンドキュー・コマンドアロケータ・コマンドリストの生成

コマンドキューは,d3d11のImmediateContextのように直積みしていくやり方で今回は行くので,Type に D3D12_COMMAND_LIST_DIRECT を設定しておきます。

    // コマンドキューを生成.
    {
        D3D12_COMMAND_QUEUE_DESC desc;
        ZeroMemory( &desc, sizeof(desc) );
        desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
        desc.Type  = D3D12_COMMAND_LIST_TYPE_DIRECT;

        hr = m_pDevice->CreateCommandQueue( &desc, IID_PPV_ARGS( m_pCmdQueue.GetAddress() ) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateCommandQueue() Failed." );
            return false;
        }
    }
    // コマンドアロケータを生成.
    hr = m_pDevice->CreateCommandAllocator( 
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        IID_PPV_ARGS(m_pCmdAllocator.GetAddress()));
    if ( FAILED( hr ) )
    {
        ELOG( "Error : ID3D12Device::CreateCommandAllocator() Failed." );
        return false;
    }

    // コマンドリストを生成.
    hr = m_pDevice->CreateCommandList(
        0, 
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        m_pCmdAllocator.GetPtr(),
        nullptr,
        IID_PPV_ARGS(m_pCmdList.GetAddress()) );
    if ( FAILED( hr ) )
    {
        ELOG( "Error : ID3D12Device::CreateCommandList() Failed." );
        return false;
    }
    m_pCmdList->Close();

適切な引数を設定すればいいだけなので,特に説明の必要はないですね。APIの説明自体はMSDNに載っているのでそちらを参照しましょう。

6 レンダーターゲットの生成

まずは,スワップチェインを生成しましょう。基本的にはd3d11のときと変わりません。d3d11の時はデバイス生成と同時に作れましたが,同様のメソッドがd3d12には無いので,IDXGIFactoryを作って,そこからIDXGISwapChainを生成します。d3d12ではレンダーターゲットを作る際には明示的にダブルバッファなどにする必要があり,フレームバッファ番号の管理をする必要があります。CirculatedBuffer的なクラスを作って管理するのもありですが,IDXGISwapChain3にGetCurrentBackBufferIndex()というメソッドがあるので,これを利用して管理することにします。そのため,IDXGISwapChainをIDXGISwapChain3にする必要があるので,QueryInterface()を使ってIDXGISwapChain3に変換します。

    asdx::RefPtr<IDXGIFactory4> pFactory;
    hr = CreateDXGIFactory1( IID_PPV_ARGS( pFactory.GetAddress() ) );
    if ( FAILED( hr ) )
    {
        ELOG( "Error : CreateDXGIFactory() Failed." );
        return false;
    }
    // スワップチェインを生成.
    {
        // スワップチェインの設定.
        DXGI_SWAP_CHAIN_DESC desc;
        ZeroMemory( &desc, sizeof(desc) );
        desc.BufferCount        = BufferCount;
        desc.BufferDesc.Width   = m_Width;
        desc.BufferDesc.Height  = m_Height;
        desc.BufferDesc.Format  = DXGI_FORMAT_R8G8B8A8_UNORM;
        desc.BufferUsage        = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        desc.SwapEffect         = DXGI_SWAP_EFFECT_FLIP_DISCARD;
        desc.OutputWindow       = m_hWnd;
        desc.SampleDesc.Count   = 1;
        desc.Windowed           = TRUE;

        // スワップチェインを生成.
        asdx::RefPtr<IDXGISwapChain> pSwapChain;
        hr = pFactory->CreateSwapChain( m_pCmdQueue.GetPtr(), &desc, pSwapChain.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : IDXGIFactory::CreateSwapChain() Failed." );
            return false;
        }

        // IDXGISwapChain3にする.
        hr = pSwapChain->QueryInterface( IID_PPV_ARGS( m_pSwapChain.GetAddress() ) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : IDXGISwapChain::QueryInterface() Failed." );
            return false;
        }

        // フレームバッファ番号を設定.
        m_FrameIndex = m_pSwapChain->GetCurrentBackBufferIndex();
    }

続いて,レンダーターゲットビューを生成するために,レンダーターゲット用のディスクリプタヒープを生成しておきます。d3d12でのDescriptorとは単一リソースのバインディングの基本単位であり,DescriptorHeapはDescriptorの連続するメモリ割り当てのコレクションです。間違っている可能性は大いにあると思うので注意して欲しいのですが,ディスクリプタヒープの自分なりの解釈ですと,ちょっとコンシューマーゲームのAPIを叩いたことがある人なら,グラフィックスカードによってメモリアライメントの制約がことなったりすることをご存じでしょう。例えば,どこぞのゲーム機だとテクスチャは ○○ byte アライメントじゃないといけないので,アプリケーション側で ○○ byte アライメントでメモリ確保しておいてくださいとか,そんな制約があったりします。コンシューマーゲーム機であれば,グラフィックスカードなどのハードウェアの仕様は基本的に固定されるので,仕様通りにしておけばよいです。ですが,PCの場合はグラフィックスカードが差し替え可能でベンダーごと,またグラフィックスカードのシリーズによってもその制約は変わる可能性が考えられます。そこでそれらの仕様の左右されないように内部で隠蔽しておけばよいと考えられて作られたのがDescriptorHeapなんじゃないかというのが個人的な解釈です。もちろん大いに間違っている可能性はあります。ですが,DescriptorHeapを設けることで,その辺のめんどっちい制約をユーザーに公開せず隠蔽することができるんじゃないかなぁと勝手に思っています。Direct3D12の設計者ではないし,設計者と話したことも無いので間違っている可能性はかなり高いです。まぁそんな話はとりあえず横に置いておいて,レンダーターゲットビューを生成するためにDescriptorHeapが必要なので,下記のような感じで生成します。

        // レンダーターゲットビュー用ディスクリプタヒープの設定.
        D3D12_DESCRIPTOR_HEAP_DESC desc;
        ZeroMemory( &desc, sizeof(desc) );
        desc.NumDescriptors = BufferCount;
        desc.Type           = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        desc.Flags          = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

        // レンダーターゲットビュー用ディスクリプタヒープを生成.
        hr = m_pDevice->CreateDescriptorHeap( &desc, IID_PPV_ARGS( m_pRtvHeap.GetAddress() ) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateDescriptorHeap() Failed." );
            return false;
        }

        // レンダーターゲットビューのディスクリプタサイズを取得.
        m_RtvDescriptorSize = m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

今回のサンプルではダブルバッファ化しているので,BufferCount=2をNumDescriptorHeapに設定しています。Typeはレンダーターゲットビュー用なので,D3D12_DESCRIPTOR_HEAP_TYPE_RTVを設定しています。 続いて,レンダーターゲットビューの生成ですが,下記のような感じで行います。

    // レンダーターゲットビューを生成.
    {
        // CPUハンドルの先頭を取得.
        auto handle = m_pRtvHeap->GetCPUDescriptorHandleForHeapStart();

        D3D12_RENDER_TARGET_VIEW_DESC desc;
        desc.Format               = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
        desc.ViewDimension        = D3D12_RTV_DIMENSION_TEXTURE2D;
        desc.Texture2D.MipSlice   = 0;
        desc.Texture2D.PlaneSlice = 0;

        // フレームバッファ数分ループさせる.
        for( UINT i=0; i<BufferCount; ++i )
        {
            // バックバッファ取得.
            hr = m_pSwapChain->GetBuffer( i, IID_PPV_ARGS( m_pRenderTarget[i].GetAddress() ) );
            if ( FAILED( hr ) )
            {
                ELOG( "Error : IDXGISwapChain3::GetBuffer() Failed." );
                return false;
            }

            // レンダーターゲットビューを生成.
            m_pDevice->CreateRenderTargetView( m_pRenderTarget[i].GetPtr(), &desc, handle );

            // ハンドルのポインタを進める.
            handle.ptr += m_RtvDescriptorSize;
        }
    }

まずは,先ほど生成したDescriptorHeapからヒープの先頭ハンドルをGetCPUDescriptorHandleForHeapStart()を使ってとってきます。
スワップチェインを生成時にはSRGBを設定するとエラーになるので指定できません。今時であればリニアワークフローに対応しているのは当たり前の時代なので,SRGB化したいです。そのためには,レンダーターゲットビューのフォーマットをSRGB付きに設定する必要があるので,D3D12_RENDER_TARGET_VIEW_DESC で Format に DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, ViewDimension に D3D12_RTV_DIMENSION_TEXTURE_2D を指定します。MipSliceとPlanceSliceは各自の設定により異なりますので,適切に設定してください。今回のサンプルではMipSlice=0, PlaneSlice=0を設定しています。このD3D12_RENDER_TARGET_VIEW_DESCの設定で,ダブルバッファ化するためにループさせてレンダーターゲットビューを生成します。レンダーターゲットビューの生成はID3D12Device::CreateRenderTargetView()を用いて行います。この生成時にリソースが必要となるため,IDXGISwapChain3::GetBuffer() を用いてリソースを取得しています。先ほどDescriptorHeapは連続するメモリだと説明しました。なので,451行目のGetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)で取ってきたサイズ分加算して,ハンドル位置を適切に設定しています。
これでレンダーターゲットビューの準備ができました。

7 深度ステンシルバッファの生成

次は深度ステンシルバッファの生成を行います。基本的にはレンダーターゲットと同じフローですが,レンダーターゲットはバックバッファを元に生成できますが,深度ステンシルバッファはリソースがないので自分で生成を行ってから深度ステンシルビューを生成します。下記のような感じです。

        // 深度ステンシルビュー用ディスクリプタヒープの設定.
        desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;

        // 深度ステンシルビュー用ディスクリプタヒープを生成.
        hr = m_pDevice->CreateDescriptorHeap( &desc, IID_PPV_ARGS( m_pDsvHeap.GetAddress() ) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateDescriptorHeap() Failed." );
            return false;
        }

        // 深度ステンシルビューのディスクリプタサイズを取得.
        m_DsvDescriptorSize = m_pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    // 深度ステンシルビューを生成.
    {
        // ヒーププロパティの設定.
        D3D12_HEAP_PROPERTIES prop;
        prop.Type                   = D3D12_HEAP_TYPE_DEFAULT;
        prop.CPUPageProperty        = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
        prop.MemoryPoolPreference   = D3D12_MEMORY_POOL_UNKNOWN;
        prop.CreationNodeMask       = 1;
        prop.VisibleNodeMask        = 1;

        // リソースの設定.
        D3D12_RESOURCE_DESC desc;
        desc.Dimension          = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
        desc.Alignment          = 0;
        desc.Width              = m_Width;
        desc.Height             = m_Height;
        desc.DepthOrArraySize   = 1;
        desc.MipLevels          = 0;
        desc.Format             = DXGI_FORMAT_D32_FLOAT;
        desc.SampleDesc.Count   = 1;
        desc.SampleDesc.Quality = 0;
        desc.Flags              = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
        desc.Layout             = D3D12_TEXTURE_LAYOUT_UNKNOWN;

        // クリア値の設定.
        D3D12_CLEAR_VALUE clearValue;
        clearValue.Format               = DXGI_FORMAT_D32_FLOAT;
        clearValue.DepthStencil.Depth   = 1.0f;
        clearValue.DepthStencil.Stencil = 0;

        // リソースを生成.
        hr = m_pDevice->CreateCommittedResource( 
            &prop,
            D3D12_HEAP_FLAG_NONE,
            &desc,
            D3D12_RESOURCE_STATE_DEPTH_WRITE,
            &clearValue,
            IID_PPV_ARGS(m_pDepthStencil.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
            return false;
        }

        // 深度ステンシルビューの設定.
        D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
        dsvDesc.Format        = DXGI_FORMAT_D32_FLOAT;
        dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
        dsvDesc.Flags         = D3D12_DSV_FLAG_NONE;

        // 深度ステンシルビューを生成.
        m_pDevice->CreateDepthStencilView(
            m_pDepthStencil.GetPtr(),
            &dsvDesc,
            m_pDsvHeap->GetCPUDescriptorHandleForHeapStart() );
    }

8 頂点バッファの生成

続いては,頂点バッファの生成処理です。頂点バッファはリソースなので,ヒーププロパティの設定を行い,CommitedResource()で生成するといった感じになります。
頂点バッファへのデータの書き込みは,ID3D12Resource::Map()でポインタを取ってきて,memcpy()でデータを書き込むといった感じになります。書き込み終わったあとはID3D12Resource::Unmap()を呼んでおきます。
あとはレンダーターゲットビューや深度ステンシルビューと同じノリで,頂点バッファビューを設定しておきます。 ID3D12Resource::GetGPUVirtualAddress()でバッファロケーションとサイズなどを設定するだけなので特に問題はないでしょう。

///////////////////////////////////////////////////////////////////////////////////////////////////
// Vertex structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct Vertex
{
    asdx::Vector3 Position;     //!< 位置座標です.
    asdx::Vector3 Normal;       //!< 法線ベクトルです.
    asdx::Vector2 TexCoord;     //!< テクスチャ座標です.
    asdx::Vector4 Color;        //!< 頂点カラーです.
};
    // 頂点バッファの生成.
    {
        // 頂点データ.
        Vertex vertices[] = {
            { {  0.0f,  1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
            { {  1.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
            { { -1.0f, -1.0f, 0.0f }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
        };

        // ヒーププロパティの設定.
        D3D12_HEAP_PROPERTIES prop;
        prop.Type                 = D3D12_HEAP_TYPE_UPLOAD;
        prop.CPUPageProperty      = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
        prop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
        prop.CreationNodeMask     = 1;
        prop.VisibleNodeMask      = 1;

        // リソースの設定.
        D3D12_RESOURCE_DESC desc;
        desc.Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER;
        desc.Alignment          = 0;
        desc.Width              = sizeof(vertices);
        desc.Height             = 1;
        desc.DepthOrArraySize   = 1;
        desc.MipLevels          = 1;
        desc.Format             = DXGI_FORMAT_UNKNOWN;
        desc.SampleDesc.Count   = 1;
        desc.SampleDesc.Quality = 0;
        desc.Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
        desc.Flags              = D3D12_RESOURCE_FLAG_NONE;

        // リソースを生成.
        hr = m_pDevice->CreateCommittedResource(
            &prop,
            D3D12_HEAP_FLAG_NONE,
            &desc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(m_pVertexBuffer.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
            return false;
        }

        // マップする.
        UINT8* pData;
        hr = m_pVertexBuffer->Map( 0, nullptr, reinterpret_cast<void**>( &pData ) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Resource::Map() Failed." );
            return false;
        }

        // 頂点データをコピー.
        memcpy( pData, vertices, sizeof(vertices) );

        // アンマップする.
        m_pVertexBuffer->Unmap( 0, nullptr );

        // 頂点バッファビューの設定.
        m_VertexBufferView.BufferLocation = m_pVertexBuffer->GetGPUVirtualAddress();
        m_VertexBufferView.StrideInBytes  = sizeof(Vertex);
        m_VertexBufferView.SizeInBytes    = sizeof(vertices);
    }

9 定数バッファの生成

ただポリゴン描くだけだと面白くないので,ポリゴンを回転させるところまでやってみます。そのために,ワールド行列を設定できるようにします。ついでなので,ビューイング変換と射影変換も行えるようにするためビュー行列と射影行列もシェーダ側に渡せるようにしておきましょう。シェーダ側に渡すには,d3d11と同じように定数バッファとして渡してやるので,定数バッファを生成しておく必要があります。
まずは,定数バッファ生成のためにディスクリプタヒープを用意します。この時,Flags にはシェーダから参照可能にするために D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE を設定しておきます。Type には定数バッファなので,D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV を指定しておきます。

    // 定数バッファ用ディスクリプターヒープを生成.
    {
        D3D12_DESCRIPTOR_HEAP_DESC desc = {};
        desc.NumDescriptors = 1;
        desc.Flags          = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
        desc.Type           = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;

        hr = m_pDevice->CreateDescriptorHeap( &desc, IID_PPV_ARGS(m_pCbvHeap.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateDescriptorHeap() Failed." );
            return false;
        }
    }

続いて先ほどの頂点バッファのようにリソースを生成します。先ほどは頂点バッファビューを作りましたが,今度は定数バッファビューを作成します。今回定数バッファは毎フレーム更新を行うので,Map()しっぱなしにしておきます。データの更新は頂点バッファ同様にmemcpy()で行います。

    // 定数バッファを生成.
    {
        // ヒーププロパティの設定.
        D3D12_HEAP_PROPERTIES prop = {};
        prop.Type                 = D3D12_HEAP_TYPE_UPLOAD;
        prop.CPUPageProperty      = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
        prop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
        prop.CreationNodeMask     = 1;
        prop.VisibleNodeMask      = 1;

        // リソースの設定.
        D3D12_RESOURCE_DESC desc = {};
        desc.Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER;
        desc.Alignment          = 0;
        desc.Width              = sizeof(ResConstantBuffer);
        desc.Height             = 1;
        desc.DepthOrArraySize   = 1;
        desc.MipLevels          = 1;
        desc.Format             = DXGI_FORMAT_UNKNOWN;
        desc.SampleDesc.Count   = 1;
        desc.SampleDesc.Quality = 0;
        desc.Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
        desc.Flags              = D3D12_RESOURCE_FLAG_NONE;

        // リソースを生成.
        hr = m_pDevice->CreateCommittedResource(
            &prop,
            D3D12_HEAP_FLAG_NONE,
            &desc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(m_pConstantBuffer.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateCommittedResource() Failed." );
            return false;
        }

        // 定数バッファビューの設定.
        D3D12_CONSTANT_BUFFER_VIEW_DESC bufferDesc = {};
        bufferDesc.BufferLocation = m_pConstantBuffer->GetGPUVirtualAddress();
        bufferDesc.SizeInBytes    = sizeof(ResConstantBuffer);

        // 定数バッファビューを生成.
        m_pDevice->CreateConstantBufferView( &bufferDesc, m_pCbvHeap->GetCPUDescriptorHandleForHeapStart() );

        // マップする. アプリケーション終了まで Unmap しない.
        // "Keeping things mapped for the lifetime of the resource is okay." とのこと。
        hr = m_pConstantBuffer->Map( 0, nullptr, reinterpret_cast<void**>(&m_pCbvDataBegin) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Resource::Map() Failed." );
            return false;
        }

        // アスペクト比算出.
        auto aspectRatio = static_cast<FLOAT>( m_Width ) / static_cast<FLOAT>( m_Height );

        // 定数バッファデータの設定.
        m_ConstantBufferData.World = asdx::Matrix::Identity();
        m_ConstantBufferData.View  = asdx::Matrix::CreateLookAt( asdx::Vector3(0.0f, 0.0f, 5.0f), asdx::Vector3(0.0f, 0.0f, 0.0f), asdx::Vector3( 0.0f, 1.0f, 0.0f ) );
        m_ConstantBufferData.Proj  = asdx::Matrix::CreatePerspectiveFieldOfView( asdx::F_PIDIV4, aspectRatio, 1.0f, 1000.0f );

        // コピる.
        memcpy( m_pCbvDataBegin, &m_ConstantBufferData, sizeof(m_ConstantBufferData) );
    }

10 ルートシグニチャの生成

次は,ルートシグニチャの生成です。D3D12から追加された新しいオブジェクトです。ルートシグニチャはリソースとシェーダの対応付けを行うためのテーブルの定義で,レジタスとのバインド状態をしているもののようです。今回のサンプルでは下記のようにして生成を行っています。

    // ルートシグニチャを生成.
    {
        // ディスクリプタレンジの設定.
        D3D12_DESCRIPTOR_RANGE range;
        range.RangeType          = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
        range.NumDescriptors     = 1;
        range.BaseShaderRegister = 0;
        range.RegisterSpace      = 0;
        range.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;

        // ルートパラメータの設定.
        D3D12_ROOT_PARAMETER param;
        param.ParameterType                       = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
        param.ShaderVisibility                    = D3D12_SHADER_VISIBILITY_VERTEX;
        param.DescriptorTable.NumDescriptorRanges = 1;
        param.DescriptorTable.pDescriptorRanges   = &range;

        // ルートシグニチャの設定.
        D3D12_ROOT_SIGNATURE_DESC desc;
        desc.NumParameters     = 1;
        desc.pParameters       = &param;
        desc.NumStaticSamplers = 0;
        desc.pStaticSamplers   = nullptr;
        desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
                   | D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS
                   | D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS
                   | D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS
                   | D3D12_ROOT_SIGNATURE_FLAG_DENY_PIXEL_SHADER_ROOT_ACCESS;

        asdx::RefPtr<ID3DBlob> pSignature;
        asdx::RefPtr<ID3DBlob> pError;

        // シリアライズする.
        hr = D3D12SerializeRootSignature(
            &desc,
            D3D_ROOT_SIGNATURE_VERSION_1,
            pSignature.GetAddress(),
            pError.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : D3D12SerializeRootSignataure() Failed." );
            return false;
        }

        // ルートシグニチャを生成.
        hr = m_pDevice->CreateRootSignature(
            0,
            pSignature->GetBufferPointer(),
            pSignature->GetBufferSize(),
            IID_PPV_ARGS(m_pRootSignature.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateRootSignature() Failed." );
            return false;
        }
    }

定数バッファを1つ使うため,ディスクリプタレンジのタイプに D3D12_DESCRIPTOR_RANGE_TYPE_CBV を設定し,NumDescriptors は 1 を設定しています。ルートパラメータでは,一応テーブルを使うので,D3D12_ROOT_PARAMTER_TYPE_DESCRIPTOR_TABLE を指定,頂点シェーダから参照できる必要があるので,ShaderVisibilityには D3D12_SHADER_VISIBILITY_VERTEX を指定。NumDescriptorRanges はディスクリプタレンジが1つなので 1 を設定します。D3D12_ROOT_SIGNATURE_DESC では,ルートパラメータが今回は1つで済むので,NumParameters に 1 を設定,Flags には D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT と 今回設定する定数バッファが頂点シェーダ以外では必要ないので,頂点シェーダ以外からはアクセスできないようにフラグを立てています。あとは設定をD3D12SerializeRootSignature()でシリアライズして,ID3D12Device::CreateRootSignature() でルートシグニチャを生成します。

11 パイプラインステートの生成

いわゆるレンダーステートの設定というのはパイプラインステートで行います。d3d11ではブレンドステートやラスタライザーステートなど細かい単位で変更ができたのですが,やはりオーバーヘッドが高いのでしょうか?「あれを変えるとこれを変えなきゃいけない。面倒くさい全部設定してくれ!」と言わんばかりに,全部設定しなければいけません。かなりめんどっちぃです。設定項目についてはd3d11とあまり変わりないので,地味に設定していきましょう。

    // パイプラインステートの生成.
    {
        asdx::RefPtr<ID3DBlob> pVSBlob;
        asdx::RefPtr<ID3DBlob> pPSBlob;

        // 頂点シェーダのファイルパスを検索.
        std::wstring path;
        if ( !SearchFilePath( L"SimpleVS.cso", path ) )
        {
            ELOG( "Error : File Not Found." );
            return false;
        }

        // コンパイル済み頂点シェーダを読み込む.
        hr = D3DReadFileToBlob( path.c_str(), pVSBlob.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : D3DReadFileToBlob() Failed." );
            return false;
        }

        // ピクセルシェーダのファイルパスを検索.
        if ( !SearchFilePath( L"SimplePS.cso", path ) )
        {
            ELOG( "Error : File Not Found." );
            return false;
        }

        // コンパイル済みピクセルシェーダを読み込む.
        hr = D3DReadFileToBlob( path.c_str(), pPSBlob.GetAddress() );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : D3DReadFileToBlob() Failed." );
            return false;
        }

        // 入力レイアウトの設定.
        D3D12_INPUT_ELEMENT_DESC inputElements[] = {
            { "POSITION",  0, DXGI_FORMAT_R32G32B32_FLOAT,    0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
            { "NORMAL",    0, DXGI_FORMAT_R32G32B32_FLOAT,    0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
            { "TEXCOORD",  0, DXGI_FORMAT_R32G32_FLOAT,       0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
            { "VTX_COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        };

        // ラスタライザーステートの設定.
        D3D12_RASTERIZER_DESC descRS;
        descRS.FillMode                 = D3D12_FILL_MODE_SOLID;
        descRS.CullMode                 = D3D12_CULL_MODE_NONE;
        descRS.FrontCounterClockwise    = FALSE;
        descRS.DepthBias                = D3D12_DEFAULT_DEPTH_BIAS;
        descRS.DepthBiasClamp           = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
        descRS.SlopeScaledDepthBias     = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
        descRS.DepthClipEnable          = TRUE;
        descRS.MultisampleEnable        = FALSE;
        descRS.AntialiasedLineEnable    = FALSE;
        descRS.ForcedSampleCount        = 0;
        descRS.ConservativeRaster       = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

        // レンダーターゲットのブレンド設定.
        D3D12_RENDER_TARGET_BLEND_DESC descRTBS = {
            FALSE, FALSE,
            D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
            D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
            D3D12_LOGIC_OP_NOOP,
            D3D12_COLOR_WRITE_ENABLE_ALL
        };

        // ブレンドステートの設定.
        D3D12_BLEND_DESC descBS;
        descBS.AlphaToCoverageEnable  = FALSE;
        descBS.IndependentBlendEnable = FALSE;
        for( UINT i=0; i<D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i )
        { descBS.RenderTarget[i] = descRTBS; }

        // パイプラインステートの設定.
        D3D12_GRAPHICS_PIPELINE_STATE_DESC desc = {};
        desc.InputLayout                     = { inputElements, _countof(inputElements) };
        desc.pRootSignature                  = m_pRootSignature.GetPtr();
        desc.VS                              = { reinterpret_cast<UINT8*>( pVSBlob->GetBufferPointer() ), pVSBlob->GetBufferSize() };
        desc.PS                              = { reinterpret_cast<UINT8*>( pPSBlob->GetBufferPointer() ), pPSBlob->GetBufferSize() };
        desc.RasterizerState                 = descRS;
        desc.BlendState                      = descBS;
        desc.DepthStencilState.DepthEnable   = FALSE;
        desc.DepthStencilState.StencilEnable = FALSE;
        desc.SampleMask                      = UINT_MAX;
        desc.PrimitiveTopologyType           = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
        desc.NumRenderTargets                = 1;
        desc.RTVFormats[0]                   = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
        desc.DSVFormat                       = DXGI_FORMAT_D32_FLOAT;
        desc.SampleDesc.Count                = 1;

        // パイプラインステートを生成.
        hr = m_pDevice->CreateGraphicsPipelineState( &desc, IID_PPV_ARGS(m_pPipelineState.GetAddress()) );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Device::CreateGraphicsPipelineState() Failed." );
            return false;
        }
    }

12 描画コマンドの生成

さて、ようやく下準備が終わり描画コマンドの生成です。
まず,コマンドアロケータとコマンドリストをリセットして,コマンドリストに描画コマンドを積んでいきます。今回のサンプルでの描画処理は下記のようになります。

//-------------------------------------------------------------------------------------------------
//      描画時の処理です.
//-------------------------------------------------------------------------------------------------
void App::OnRender(FLOAT elapsedSec)
{
    // 回転角を増やす.
    m_RotateAngle += ( elapsedSec / 0.5f );

    // ワールド行列を更新.
    m_ConstantBufferData.World = asdx::Matrix::CreateRotationY( m_RotateAngle );

    // 定数バッファを更新.
    memcpy( m_pCbvDataBegin, &m_ConstantBufferData, sizeof(m_ConstantBufferData) );


    // コマンドアロケータとコマンドリストをリセット.
    m_pCmdAllocator->Reset();
    m_pCmdList->Reset( m_pCmdAllocator.GetPtr(), m_pPipelineState.GetPtr() );

    // ディスクリプタヒープを設定.
    m_pCmdList->SetDescriptorHeaps( 1, m_pCbvHeap.GetAddress() );

    // ルートシグニチャを設定.
    m_pCmdList->SetGraphicsRootSignature( m_pRootSignature.GetPtr() );

    // ディスクリプタヒープテーブルを設定.
    m_pCmdList->SetGraphicsRootDescriptorTable( 0, m_pCbvHeap->GetGPUDescriptorHandleForHeapStart() );

    // ビューポートの設定.
    m_pCmdList->RSSetViewports( 1, &m_Viewport );

    // シザー矩形の設定.
    m_pCmdList->RSSetScissorRects( 1, &m_ScissorRect );

    // リソースバリアの設定.
    // Present ---> RenderTarget
    D3D12_RESOURCE_BARRIER barrier;
    ZeroMemory( &barrier, sizeof(barrier) );
    barrier.Type                    = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
    barrier.Flags                   = D3D12_RESOURCE_BARRIER_FLAG_NONE;
    barrier.Transition.pResource    = m_pRenderTarget[m_FrameIndex].GetPtr();
    barrier.Transition.StateBefore  = D3D12_RESOURCE_STATE_PRESENT;
    barrier.Transition.StateAfter   = D3D12_RESOURCE_STATE_RENDER_TARGET;
    barrier.Transition.Subresource  = D3D12_RESOURCE_BARRIER_FLAG_NONE;
    m_pCmdList->ResourceBarrier( 1, &barrier );

    // レンダーターゲットのハンドルを取得.
    auto handleRTV = m_pRtvHeap->GetCPUDescriptorHandleForHeapStart();
    auto handleDSV = m_pDsvHeap->GetCPUDescriptorHandleForHeapStart();
    handleRTV.ptr += ( m_FrameIndex * m_RtvDescriptorSize );

    // レンダーターゲットの設定.
    m_pCmdList->OMSetRenderTargets( 1, &handleRTV, FALSE, &handleDSV );

    // レンダーターゲットビューをクリア.
    const FLOAT clearColor[] = { 0.39f, 0.58f, 0.92f, 1.0f };
    m_pCmdList->ClearRenderTargetView( handleRTV, clearColor, 0, nullptr );

    // 深度ステンシルビューをクリア.
    m_pCmdList->ClearDepthStencilView( handleDSV, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr );

    // プリミティブトポロジーの設定.
    m_pCmdList->IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST );

    // 頂点バッファビューを設定.
    m_pCmdList->IASetVertexBuffers( 0, 1, &m_VertexBufferView );

    // 描画コマンドを生成.
    m_pCmdList->DrawInstanced( 3, 1, 0, 0 );

    // リソースバリアの設定.
    // RenderTarget ---> Present
    barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
    barrier.Transition.StateAfter  = D3D12_RESOURCE_STATE_PRESENT;
    m_pCmdList->ResourceBarrier( 1, &barrier );

    // コマンドの記録を終了.
    m_pCmdList->Close();

    // コマンド実行.
    ID3D12CommandList* ppCmdLists[] = { m_pCmdList.GetPtr() };
    m_pCmdQueue->ExecuteCommandLists( _countof(ppCmdLists), ppCmdLists );

    // 表示する.
    m_pSwapChain->Present( 1, 0 );

    // コマンドの完了を待機.
    WaitForGpu();
}

基本的にはd3d11と変わらない感じですが,d3d12で新しく気を付けることはリソースハザードでしょうか。
例えばなんですが,あるリソースをレンダーターゲットとして使った後にテクスチャとして使いたいような場合があります。d3d11まではDirect3Dランタイムとドライバがレンダーターゲットからテクスチャへの移行を検知してリソースのバインド状態を変更したり,GPUパイプラインをフラッシュしてくれていましたが,オーバーヘッドを無くすためにそれらのケアをユーザー側で行わなければいけなくなりました。もしこれをしない場合には書き込み途中のレンダーターゲットをテクスチャとして使ってしまうような事態になります。そこで,今はレンダーターゲットに描いているからバリア張るね~とやっているのが,967行目付近のリソースバリアの設定です。レンダーターゲットに描き終わったので,今度は表示用にバリア張るね~とやっているのが,1003行目付近のリソースバリアの設定です。描画コマンドを積み終わったら,ID3D12GraphicsCommandList::Close()を呼び区切りコマンドを積み,ID3D12CommandQueue::ExecuteCommandLists()を呼び出してコマンドを実行します。

13 描画コマンドの完了待機

完了の待機は,Direct3D12の予習でやったのとほとんど同じです。

//-------------------------------------------------------------------------------------------------
//      コマンドの完了を待機します.
//-------------------------------------------------------------------------------------------------
void App::WaitForGpu()
{
    // シグナル状態にして,フェンス値を増加.
    const UINT64 fence = m_FenceValue;
    auto hr = m_pCmdQueue->Signal( m_pFence.GetPtr(), fence );
    if ( FAILED( hr ) )
    {
        ELOG( "Error : ID3D12CommandQueue::Signal() Failed." );
        return;
    }
    m_FenceValue++;

    // 完了を待機.
    if ( m_pFence->GetCompletedValue() < fence )
    {
        hr = m_pFence->SetEventOnCompletion( fence, m_FenceEvent );
        if ( FAILED( hr ) )
        {
            ELOG( "Error : ID3D12Fence::SetEventOnCompletaion() Failed." );
            return;
        }

        WaitForSingleObject( m_FenceEvent, INFINITE );
    }

    // フレームバッファ番号を更新.
    m_FrameIndex = m_pSwapChain->GetCurrentBackBufferIndex();
}

14 Direct3D オブジェクトの破棄処理

最後に破棄処理ですが,d3d11の場合はID3D11DeviceContext::ClearState()とID3D11DeviceContext::Flush()を行ってからオブジェクトを解放し,アプリケーションを終了させました。
D3D12でも同じように破棄処理を行ってあげましょう。ただし,対応するメソッドがないので上記App::WaitForGpu()を呼んでからオブジェクトを解放という感じになります。

//-------------------------------------------------------------------------------------------------
//      D3D12の終了処理です.
//-------------------------------------------------------------------------------------------------
void App::TermD3D()
{
    // コマンドの終了を待機.
    WaitForGpu();

    // イベントハンドルを閉じる.
    CloseHandle(m_FenceEvent);
    m_FenceEvent = nullptr;

    for( UINT i=0; i<BufferCount; ++i )
    { m_pRenderTarget[i].Reset(); }

    m_pDepthStencil.Reset();

    m_pSwapChain    .Reset();
    m_pFence        .Reset();
    m_pCmdList      .Reset();
    m_pCmdAllocator .Reset();
    m_pCmdQueue     .Reset();
    m_pDevice       .Reset();
}
//--------------------------------------------------------------------------------------------------
//      アプリケーション固有の終了処理です.
//--------------------------------------------------------------------------------------------------
void App::TermApp()
{
    m_pConstantBuffer->Unmap( 0, nullptr );

    m_pConstantBuffer.Reset();
    m_pCbvHeap.Reset();

    m_pVertexBuffer.Reset();
    m_VertexBufferView.BufferLocation = 0;
    m_VertexBufferView.SizeInBytes    = 0;
    m_VertexBufferView.StrideInBytes  = 0;

    m_pPipelineState.Reset();
    m_pRootSignature.Reset();
}

15 おわりに…

単なるポリゴン表示でも,大変ですね。
結構はしょった説明になってしまいましたが,一応ポリゴン表示までの流れを説明したということにしたいと思います。
次回はテクスチャマッピングを扱う予定です。

16 ソースコード

本ソースコードおよびプログラムはMIT Licenseに準じます。
プログラムの作成にはMicrosoft Visual Studio Community 2015, 及び Windows SDK 10.0.10240.0を用いています。
D3D12_Triangle