C++とDirectX 12で始めるゲームプログラミング

はじめに – DirectX 12とは

DirectX 12は、マイクロソフトが提供するWindowsプラットフォーム向けのグラフィックスAPI(Application Programming Interface)です。DirectXの最新バージョンであり、DirectX 11に比べてよりハードウェアに近いレベルで制御が可能になり、パフォーマンスの向上が期待できます。

DirectXとは

DirectXは、ゲームやマルチメディアアプリケーション開発に必要な機能を提供するAPIの集合体です。グラフィックス描画、サウンド、入力処理など、さまざまな機能が含まれており、DirectX 12は主にグラフィックス描画を担当します。

DirectX 12の特徴

DirectX 12の主な特徴は以下の通りです。

  • ローレベルAPI: ハードウェアへの直接的なアクセスを可能にし、CPUオーバーヘッドを削減します。開発者はより細かい制御が可能になる一方で、より複雑な管理が必要となります。
  • マルチスレッド対応: コマンドの並列処理を効率的に行えるように設計されており、マルチコアCPUの性能を最大限に引き出します。
  • Explicit Multi-GPU: 複数のGPUを効果的に利用するための機能を提供し、パフォーマンスを向上させます。
  • Resource Binding Model: リソース管理の柔軟性を高め、より効率的な描画を実現します。

なぜDirectX 12を学ぶのか

DirectX 12を学ぶことで、以下のメリットが得られます。

  • パフォーマンスの向上: ローレベルAPIにより、より効率的なグラフィックス処理を実現できます。
  • 最新技術への対応: 最新のグラフィックスハードウェアの機能を最大限に活用できます。
  • 高度なゲーム開発: 高度なグラフィックス表現や複雑なゲームロジックの実装が可能になります。
  • キャリアアップ: グラフィックスプログラミングのスキルは、ゲーム業界だけでなく、VR/AR、シミュレーションなど、幅広い分野で求められています。

このチュートリアルでは、C++を使いDirectX 12の基本的な概念から始め、実際に簡単な描画処理を実装しながら、DirectX 12プログラミングの基礎を習得することを目指します。

開発環境の構築

DirectX 12を用いた開発を行うには、いくつかのソフトウェアと設定が必要です。このセクションでは、必要なソフトウェアのインストールと設定について説明します。

必要なソフトウェア

  1. Visual Studio: Microsoft Visual Studioは、C++開発のための統合開発環境(IDE)です。Community版は無料で利用できます。

  2. Windows 10 SDK (またはそれ以降): DirectX 12 APIを含む、Windowsアプリケーション開発に必要なヘッダーファイルとライブラリが含まれています。

    • Visual Studioのインストーラーで、オプションのコンポーネントとしてWindows 10 SDK(またはそれ以降)を選択してインストールできます。通常、Visual StudioのC++ワークロードと一緒にインストールされます。
  3. グラフィックスドライバ: DirectX 12に対応したグラフィックスドライバが必要です。お使いのGPUメーカー(NVIDIA, AMD, Intelなど)の公式サイトから最新のドライバをダウンロードし、インストールしてください。

Visual Studioプロジェクトの作成

  1. Visual Studioを起動し、新しいプロジェクトを作成します。
  2. 「空のプロジェクト」または「Windows デスクトップ アプリケーション」を選択し、プロジェクト名と保存場所を指定します。
  3. プロジェクトが作成されたら、ソースファイル(.cpp)を追加します。(例:main.cpp

プロジェクトの設定

  1. Visual Studioのメニューから、「プロジェクト」→「プロパティ」を選択します。
  2. 「構成プロパティ」→「C/C++」→「全般」で、「追加のインクルードディレクトリ」にWindows SDKのインクルードディレクトリを追加します。通常、以下のようになります。
    • $(WindowsSDKDir)Include\um
    • $(WindowsSDKDir)Include\shared
    • $(WindowsSDKDir)Include\ucrt
  3. 「構成プロパティ」→「リンカー」→「全般」で、「追加のライブラリディレクトリ」にWindows SDKのライブラリディレクトリを追加します。アーキテクチャ(x64またはx86)に合わせて適切なディレクトリを指定してください。通常、以下のようになります。
    • $(WindowsSDKDir)Lib\10.0.xxxxx.0\um\x64 (x64の場合)
    • $(WindowsSDKDir)Lib\10.0.xxxxx.0\um\x86 (x86の場合)
  4. 「構成プロパティ」→「リンカー」→「入力」で、「追加の依存ファイル」にDirectX 12ライブラリを追加します。
    • d3d12.lib
    • dxgi.lib
    • d3dcompiler.lib (シェーダーのコンパイルに必要)

サンプルコードの準備

DirectX 12の基本的なコードを記述する前に、必要なヘッダーファイルをインクルードし、DirectX 12 APIを使用するための準備を行います。以下は、main.cppに記述する基本的なコードの例です。

#include <iostream>
#include <d3d12.h>
#include <dxgi1_4.h>
#include <d3dcompiler.h>

#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")

int main() {
    std::cout << "DirectX 12 is ready!" << std::endl;
    return 0;
}

このコードをコンパイルして実行し、「DirectX 12 is ready!」と表示されれば、開発環境の構築は完了です。

デバッグレイヤーの有効化 (オプション)

DirectX 12デバッグレイヤーは、開発中にAPIの使用に関するエラーや警告を表示し、デバッグを支援します。デバッグレイヤーを有効にするには、main.cppの先頭に以下のコードを追加します。ただし、出荷ビルドでは無効にする必要があります。

#ifdef _DEBUG
#include <dxgidebug.h>
#endif

そして、プログラム終了時にデバッグレイヤーを破棄する処理を追加します。

#ifdef _DEBUG
    IDXGIDebug1* debugInterface = nullptr;
    if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&debugInterface))))
    {
        debugInterface->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_ALL);
        debugInterface->Release();
    }
#endif

これで、DirectX 12の開発環境の構築は完了です。次のステップでは、DirectX 12の基本概念について学びます。

DirectX 12の基本概念

DirectX 12はローレベルAPIであり、DirectX 11と比較してより多くの制御を開発者に提供します。そのため、いくつかの重要な概念を理解することが、DirectX 12プログラミングを始める上で不可欠です。

1. デバイス (Device)

DirectX 12デバイスは、グラフィックスアダプタ(GPU)を表すオブジェクトです。これは、DirectX 11のID3D11Deviceに相当します。デバイスは、リソースの作成、コマンドキューの生成、パイプラインステートオブジェクトの作成など、DirectX 12における多くの操作の起点となります。

  • ID3D12Device: DirectX 12デバイスインターフェース。

2. コマンドキュー (Command Queue)

コマンドキューは、GPUに実行させるコマンドリストを格納するキューです。DirectX 12では、コマンドはコマンドリストに記録され、コマンドキューにサブミットされることでGPUに実行されます。コマンドキューは、コマンドリストの実行順序を制御するために使用されます。

  • ID3D12CommandQueue: コマンドキューインターフェース。

3. コマンドリスト (Command List)

コマンドリストは、GPUに実行させる一連のコマンドを記録したものです。描画コマンド、リソースのコピー、ステートの変更など、さまざまなコマンドを記録できます。コマンドリストは、コマンドアロケータと関連付けられます。

  • ID3D12GraphicsCommandList: グラフィックスコマンドリストインターフェース。

4. コマンドアロケータ (Command Allocator)

コマンドアロケータは、コマンドリストがコマンドを記録するために使用するメモリを管理します。コマンドリストは、特定のコマンドアロケータからメモリを割り当ててコマンドを記録し、記録が終わると、コマンドリストはコマンドキューにサブミットされます。

  • ID3D12CommandAllocator: コマンドアロケータインターフェース。

5. リソース (Resource)

リソースは、テクスチャ、バッファ、レンダーターゲットなど、GPUがアクセスできるメモリ領域を表します。DirectX 12では、リソースは明示的に作成および管理する必要があります。

  • ID3D12Resource: リソースインターフェース。

6. ディスクリプタ (Descriptor)

ディスクリプタは、シェーダーからリソースへのアクセス方法を記述する小さなオブジェクトです。テクスチャビュー、レンダーターゲットビュー、深度ステンシルビューなど、さまざまな種類のディスクリプタがあります。ディスクリプタは、ディスクリプタヒープに割り当てられます。

  • D3D12_CPU_DESCRIPTOR_HANDLE: CPU側のディスクリプタハンドル。
  • D3D12_GPU_DESCRIPTOR_HANDLE: GPU側のディスクリプタハンドル。

7. ディスクリプタヒープ (Descriptor Heap)

ディスクリプタヒープは、ディスクリプタを格納するためのメモリプールです。ディスクリプタヒープは、CPUアクセス可能なヒープとGPUアクセス可能なヒープの2種類があります。

  • ID3D12DescriptorHeap: ディスクリプタヒープインターフェース。

8. パイプラインステートオブジェクト (Pipeline State Object, PSO)

パイプラインステートオブジェクトは、グラフィックスパイプラインのすべてのステート(シェーダー、レンダーターゲット形式、深度ステンシル形式など)を定義するオブジェクトです。PSOを使用することで、ステートの変更によるオーバーヘッドを削減できます。

  • ID3D12PipelineState: パイプラインステートオブジェクトインターフェース。

9. フェンス (Fence)

フェンスは、GPUとCPU間の同期を行うためのメカニズムです。CPUはフェンスにシグナルを送り、GPUはフェンスにシグナルを送り返すことで、GPUの処理が完了したことをCPUが認識できます。

  • ID3D12Fence: フェンスインターフェース。

これらの概念を理解することで、DirectX 12の基本的なアーキテクチャと動作を把握することができます。次のセクションでは、これらの概念を実際にコードに落とし込み、具体的な実装方法を学びます。

コマンドキューとコマンドリスト

DirectX 12における描画処理は、コマンドキューとコマンドリストを介してGPUに指示を送ることで行われます。このセクションでは、コマンドキューとコマンドリストの役割、作成方法、使い方について詳しく解説します。

コマンドキュー (Command Queue)

コマンドキューは、GPUに実行させるコマンドリストを順序付けられた形で管理するオブジェクトです。コマンドキューは、コマンドリストがGPUに実行される順序を保証し、複数のコマンドリストをまとめて実行するために使用されます。

役割:

  • コマンドリストの実行順序の管理
  • 複数のコマンドリストのサブミット
  • GPUとCPU間の同期

作成方法:

コマンドキューは、デバイス(ID3D12Device)のCreateCommandQueueメソッドを使用して作成します。コマンドキューの作成時には、キューの種類 (D3D12_COMMAND_LIST_TYPE) を指定する必要があります。

ComPtr<ID3D12CommandQueue> commandQueue;

D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; // 通常の描画コマンド用

HRESULT hr = device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue));
if (FAILED(hr))
{
    // エラー処理
}
  • D3D12_COMMAND_LIST_TYPE_DIRECT: 通常の描画、コンピュート、コピー操作に使用されます。
  • D3D12_COMMAND_LIST_TYPE_COMPUTE: コンピュートシェーダーの実行に使用されます。
  • D3D12_COMMAND_LIST_TYPE_COPY: リソースのコピー操作に使用されます。

コマンドリスト (Command List)

コマンドリストは、GPUに実行させる一連のコマンドを記録したものです。描画コマンド、リソースの変更、ステートの設定など、さまざまな種類のコマンドを記録できます。コマンドリストは、コマンドアロケータと関連付けられます。

役割:

  • GPUに実行させるコマンドの記録
  • 描画、コンピュート、コピー操作の指示

作成方法:

コマンドリストは、デバイス(ID3D12Device)のCreateCommandListメソッドを使用して作成します。コマンドリストの作成時には、コマンドアロケータとパイプラインステートオブジェクト (PSO) を指定する必要があります。

ComPtr<ID3D12GraphicsCommandList> commandList;
ComPtr<ID3D12CommandAllocator> commandAllocator;

// コマンドアロケータの作成
HRESULT hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator));
if (FAILED(hr))
{
    // エラー処理
}

// コマンドリストの作成
hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), nullptr, IID_PPV_ARGS(&commandList));
if (FAILED(hr))
{
    // エラー処理
}

// コマンドリストの初期状態はクローズされている必要がある
commandList->Close();

コマンドリストの使用手順:

  1. コマンドアロケータのリセット: コマンドリストを再利用する場合、関連付けられたコマンドアロケータをリセットする必要があります。

    commandAllocator->Reset();
  2. コマンドリストのリセット: コマンドリストに新しいコマンドを記録する前に、リセットする必要があります。リセット時には、PSOを指定することができます(省略可能)。

    commandList->Reset(commandAllocator.Get(), nullptr); // PSOは省略可能
  3. コマンドの記録: コマンドリストに描画コマンド、リソースの変更、ステートの設定などのコマンドを記録します。

    // 例:レンダーターゲットの設定
    D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = ...; // レンダーターゲットビューのハンドル
    commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
    
    // 例:ビューポートとシザー矩形の設定
    D3D12_VIEWPORT viewport = ...;
    commandList->RSSetViewports(1, &viewport);
    D3D12_RECT scissorRect = ...;
    commandList->RSSetScissorRects(1, &scissorRect);
    
    // 例:描画コマンド
    commandList->DrawInstanced(3, 1, 0, 0);
  4. コマンドリストのクローズ: コマンドの記録が完了したら、コマンドリストをクローズします。クローズされたコマンドリストは、コマンドキューにサブミットすることができます。

    commandList->Close();
  5. コマンドキューへのサブミット: コマンドリストをコマンドキューにサブミットします。サブミットされたコマンドリストは、GPUによって実行されます。

    ID3D12CommandList* ppCommandLists[] = { commandList.Get() };
    commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
  6. GPUとCPUの同期: コマンドキューにサブミットされたコマンドリストの実行が完了するまでCPUが待機する必要がある場合、フェンスを使用して同期を取ります。

    // フェンスオブジェクトの作成
    ComPtr<ID3D12Fence> fence;
    UINT64 fenceValue = 1;
    device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
    
    // シグナルを送信
    commandQueue->Signal(fence.Get(), fenceValue);
    
    // CPUが待機
    if (fence->GetCompletedValue() < fenceValue)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, nullptr, false, EVENT_ALL_ACCESS);
        fence->SetEventOnCompletion(fenceValue, eventHandle);
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
    
    fenceValue++;

まとめ

コマンドキューとコマンドリストは、DirectX 12における描画処理の中心となる概念です。コマンドリストに描画コマンドを記録し、コマンドキューにサブミットすることで、GPUに描画処理を実行させることができます。効率的な描画処理を行うためには、コマンドキューとコマンドリストの適切な管理と使用が重要となります。

リソースの作成と管理

DirectX 12では、テクスチャ、バッファ、レンダーターゲットなど、GPUがアクセスできるメモリ領域を「リソース」と呼びます。DirectX 12では、リソースの作成と管理を開発者が明示的に行う必要があります。このセクションでは、リソースの作成、メモリ管理、利用方法について詳しく解説します。

リソースの種類

DirectX 12には、様々な種類のリソースがあります。主なリソースの種類は以下の通りです。

  • バッファ (Buffer): 頂点データ、インデックスデータ、定数データなど、汎用的なデータを格納するために使用されます。
  • テクスチャ (Texture): 画像データ、レンダーターゲット、深度ステンシルバッファなどを格納するために使用されます。1D、2D、3Dテクスチャ、キューブマップなど、様々な種類があります。
  • レンダーターゲット (Render Target): 描画結果を格納するテクスチャです。
  • 深度ステンシルバッファ (Depth-Stencil Buffer): ピクセルの深度情報とステンシル情報を格納するために使用されます。

リソースの作成

リソースの作成には、ID3D12Device::CreateCommittedResourceメソッドを使用します。リソースを作成する際には、以下の情報を指定する必要があります。

  • ヒーププロパティ (Heap Properties): リソースを配置するヒープの種類とプロパティを指定します。
  • ヒープフラグ (Heap Flags): ヒープの作成オプションを指定します。
  • リソースの説明 (Resource Description): リソースの種類、形式、サイズなどの情報を指定します。
  • 初期リソース状態 (Initial Resource State): リソースの初期状態を指定します。
  • クリア値 (Optimized Clear Value): レンダーターゲットや深度ステンシルバッファの場合、初期値を指定します。
ComPtr<ID3D12Resource> vertexBuffer;

// ヒーププロパティの設定
D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_DEFAULT; // GPUのみがアクセス
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.CreationNodeMask = 0;
heapProperties.VisibleNodeMask = 0;

// リソースの説明の設定
D3D12_RESOURCE_DESC bufferDesc = {};
bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
bufferDesc.Alignment = 0;
bufferDesc.Width = vertexBufferSize; // バッファサイズ
bufferDesc.Height = 1;
bufferDesc.DepthOrArraySize = 1;
bufferDesc.MipLevels = 1;
bufferDesc.Format = DXGI_FORMAT_UNKNOWN;
bufferDesc.SampleDesc.Count = 1;
bufferDesc.SampleDesc.Quality = 0;
bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;

// リソースの作成
HRESULT hr = device->CreateCommittedResource(
    &heapProperties,
    D3D12_HEAP_FLAG_NONE,
    &bufferDesc,
    D3D12_RESOURCE_STATE_COPY_DEST, // 初期状態はコピー先
    nullptr,
    IID_PPV_ARGS(&vertexBuffer));

if (FAILED(hr))
{
    // エラー処理
}

ヒープの種類 (Heap Type)

ヒープの種類は、リソースの配置場所とアクセス方法を決定します。主なヒープの種類は以下の通りです。

  • D3D12_HEAP_TYPE_DEFAULT: GPUのみがアクセスできるメモリにリソースを配置します。レンダーターゲット、深度ステンシルバッファ、静的な頂点バッファやインデックスバッファなどに適しています。
  • D3D12_HEAP_TYPE_UPLOAD: CPUからGPUにデータを転送するために使用されるメモリにリソースを配置します。動的な頂点バッファやインデックスバッファ、定数バッファなどに適しています。
  • D3D12_HEAP_TYPE_READBACK: GPUからCPUにデータを転送するために使用されるメモリにリソースを配置します。計算結果の読み出しやデバッグなどに使用されます。

リソースの状態 (Resource State)

リソースの状態は、GPUがリソースにどのようにアクセスできるかを定義します。リソースの状態は、コマンドリストを使用して変更する必要があります。主なリソースの状態は以下の通りです。

  • D3D12_RESOURCE_STATE_COMMON: リソースが複数の用途で使用できる状態です。
  • D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER: 頂点バッファまたは定数バッファとして使用される状態です。
  • D3D12_RESOURCE_STATE_INDEX_BUFFER: インデックスバッファとして使用される状態です。
  • D3D12_RESOURCE_STATE_RENDER_TARGET: レンダーターゲットとして使用される状態です。
  • D3D12_RESOURCE_STATE_DEPTH_WRITE: 深度バッファとして書き込み可能状態です。
  • D3D12_RESOURCE_STATE_DEPTH_READ: 深度バッファとして読み込み可能状態です。
  • D3D12_RESOURCE_STATE_COPY_DEST: コピー先として使用される状態です。
  • D3D12_RESOURCE_STATE_COPY_SOURCE: コピー元として使用される状態です。
  • D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE: ピクセルシェーダーのリソースとして使用される状態です。
  • D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE: ピクセルシェーダー以外のシェーダーのリソースとして使用される状態です。

リソースの状態を変更するには、ID3D12GraphicsCommandList::ResourceBarrierメソッドを使用します。

D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = texture.Get(); // 状態を変更するリソース
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; // 変更前の状態
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; // 変更後の状態

commandList->ResourceBarrier(1, &barrier);

リソースへのデータ書き込み

CPUからリソースにデータを書き込むには、D3D12_HEAP_TYPE_UPLOADヒープに配置されたバッファを使用するか、UpdateSubresourceヘルパー関数を使用します。

D3D12_HEAP_TYPE_UPLOADヒープに配置されたバッファを使用する場合、Mapメソッドを使用してメモリをCPUからアクセス可能な状態にし、データを書き込んだ後、Unmapメソッドでアクセスを解除します。

ComPtr<ID3D12Resource> uploadBuffer;
// (Upload Bufferの作成は省略)

void* mappedData;
uploadBuffer->Map(0, nullptr, &mappedData);
memcpy(mappedData, vertexData, vertexBufferSize); // データのコピー
uploadBuffer->Unmap(0, nullptr);

リソースの解放

DirectX 12では、リソースは明示的に解放する必要があります。リソースを解放するには、Releaseメソッドを呼び出します。ただし、リソースがGPUで使用中の場合、すぐに解放することはできません。フェンスを使用してGPUの処理が完了するまで待機してから解放する必要があります。ComPtr を使用している場合は、自動的に参照カウントが管理されるため、明示的な Release は不要です。 ComPtr がスコープから外れる際に、参照カウントが 0 になれば自動的にリソースが解放されます。

まとめ

DirectX 12におけるリソースの作成と管理は、パフォーマンスに大きく影響します。適切なヒープの種類、リソースの状態、データ転送方法を選択することで、効率的なグラフィックス処理を実現できます。

シェーダープログラミング入門

DirectX 12におけるシェーダープログラミングは、GPU上で実行されるプログラムを作成し、頂点の変換やピクセルの色計算など、グラフィックスパイプラインの各段階をカスタマイズするために不可欠です。ここでは、シェーダーの基本、HLSLの記述、コンパイル、そしてDirectX 12でのシェーダーの使用方法について解説します。

シェーダーとは

シェーダーは、GPU上で実行される小さなプログラムです。DirectX 12では、主にHLSL (High-Level Shading Language) と呼ばれる言語を使って記述します。シェーダーは、グラフィックスパイプラインの異なる段階で実行され、頂点の位置、テクスチャ座標、色などのデータを処理します。

主なシェーダーの種類:

  • 頂点シェーダー (Vertex Shader): 頂点データを処理し、頂点の位置、法線、テクスチャ座標などを変換します。
  • ピクセルシェーダー (Pixel Shader): ラスタライズされたピクセル(フラグメント)ごとに色を計算します。
  • ジオメトリシェーダー (Geometry Shader): 頂点シェーダーの出力に基づいて新しいジオメトリを生成します。
  • コンピュートシェーダー (Compute Shader): グラフィックスパイプラインとは独立して、汎用的な計算処理を行います。

HLSL (High-Level Shading Language)

HLSLは、DirectXで使用されるシェーダー言語です。C++に似た構文を持ち、ベクトル、行列、テクスチャなどのデータ型をサポートしています。

HLSLの基本構造:

// 変数の宣言
float4 Position : SV_POSITION; // 頂点の位置 (システムセマンティクス)
float2 TexCoord : TEXCOORD0; // テクスチャ座標

// 定数の宣言
cbuffer ConstantBuffer : register(b0)
{
    float4x4 WorldViewProjection; // ワールド・ビュー・プロジェクション行列
};

// テクスチャの宣言
Texture2D Texture : register(t0); // テクスチャオブジェクト
SamplerState Sampler : register(s0); // サンプラーステート

// 頂点シェーダーのエントリーポイント
float4 VSMain(float3 position : POSITION, float2 texCoord : TEXCOORD) : SV_POSITION
{
    TexCoord = texCoord;
    return mul(WorldViewProjection, float4(position, 1.0f));
}

// ピクセルシェーダーのエントリーポイント
float4 PSMain(float4 position : SV_POSITION, float2 texCoord : TEXCOORD) : SV_TARGET
{
    return Texture.Sample(Sampler, texCoord); // テクスチャの色をサンプリング
}
  • 変数: 入力変数と出力変数は、セマンティクス (例: SV_POSITION, TEXCOORD0) を用いて定義されます。
  • 定数バッファ (Constant Buffer): シェーダーに定数データを渡すために使用されます。
  • テクスチャとサンプラー: テクスチャオブジェクトとサンプラーステートを組み合わせて、テクスチャの色をサンプリングします。
  • エントリーポイント: VSMain は頂点シェーダーのエントリーポイント、PSMain はピクセルシェーダーのエントリーポイントです。SV_POSITION は、クリップ空間における頂点の位置を出力することを意味し、SV_TARGET は、ピクセルシェーダーが出力する色を表します。

重要な HLSL データ型:

  • float: 32ビット浮動小数点数
  • float2, float3, float4: 2, 3, 4要素の浮動小数点ベクトル
  • float4x4: 4×4浮動小数点行列
  • texture2D: 2Dテクスチャ
  • sampler: サンプラーステート

シェーダーのコンパイル

HLSLで記述されたシェーダーは、コンパイラによってGPUが実行可能な形式に変換する必要があります。DirectX 12では、D3DCompile 関数や D3DCompileFromFile 関数を使用してシェーダーをコンパイルします。コンパイルされたシェーダーは、バイトコードとして格納されます。

#include <d3dcompiler.h>

// シェーダーをコンパイルする関数
HRESULT CompileShader(
    const std::wstring& filename,  // シェーダーファイルのパス
    const std::string& entryPoint, // エントリーポイント名
    const std::string& target,     // シェーダーターゲット (例: "vs_5_0", "ps_5_0")
    ComPtr<ID3DBlob>& shaderBlob) // コンパイルされたシェーダーのバイトコード
{
    UINT compileFlags = 0;
#ifdef _DEBUG
    compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

    ComPtr<ID3DBlob> errorBlob;
    HRESULT hr = D3DCompileFromFile(
        filename.c_str(),
        nullptr,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        entryPoint.c_str(),
        target.c_str(),
        compileFlags,
        0,
        &shaderBlob,
        &errorBlob);

    if (FAILED(hr))
    {
        if (errorBlob)
        {
            OutputDebugStringA(reinterpret_cast<char*>(errorBlob->GetBufferPointer()));
        }
        return hr;
    }

    return S_OK;
}

// シェーダーのコンパイル例
ComPtr<ID3DBlob> vertexShaderBlob;
HRESULT hr = CompileShader(L"VertexShader.hlsl", "VSMain", "vs_5_0", vertexShaderBlob);
if (FAILED(hr))
{
    // エラー処理
}

ComPtr<ID3DBlob> pixelShaderBlob;
hr = CompileShader(L"PixelShader.hlsl", "PSMain", "ps_5_0", pixelShaderBlob);
if (FAILED(hr))
{
    // エラー処理
}

DirectX 12 でのシェーダーの使用

コンパイルされたシェーダーのバイトコードは、パイプラインステートオブジェクト (PSO) を作成するために使用されます。PSOは、グラフィックスパイプラインのすべてのステート(シェーダー、レンダーターゲット形式、深度ステンシル形式など)を定義するオブジェクトです。

// パイプラインステートオブジェクト (PSO) の作成
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = inputLayoutDesc; // 入力レイアウト
psoDesc.pRootSignature = rootSignature.Get(); // ルートシグネチャ
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); // 頂点シェーダー
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); // ピクセルシェーダー
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // ラスタライザーステート
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // ブレンドステート
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); // 深度ステンシルステート
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; // レンダーターゲット形式
psoDesc.SampleDesc.Count = 1;

ComPtr<ID3D12PipelineState> pipelineState;
HRESULT hr = device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
if (FAILED(hr))
{
    // エラー処理
}

ルートシグネチャ (Root Signature)

ルートシグネチャは、シェーダーがアクセスできるリソース (定数バッファ、テクスチャなど) を定義します。ルートシグネチャは、シェーダーとGPU間のインターフェースとして機能します。ルートシグネチャは、パイプラインステートオブジェクト (PSO) の作成時に指定する必要があります。

// ルートシグネチャの定義
CD3DX12_ROOT_PARAMETER rootParameters[1];
CD3DX12_ROOT_CONSTANTS rootConstants(sizeof(ConstantBuffer) / 4, 0); // 定数バッファのサイズを DWORD (4バイト) 単位で指定
rootParameters[0].InitAsConstants(rootConstants, D3D12_SHADER_VISIBILITY_VERTEX); // 頂点シェーダーからアクセス

// ルートシグネチャの説明
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

// ルートシグネチャのシリアライズ
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
if (FAILED(hr))
{
    // エラー処理
}

// ルートシグネチャの作成
ComPtr<ID3D12RootSignature> rootSignature;
hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
if (FAILED(hr))
{
    // エラー処理
}

まとめ

シェーダープログラミングは、DirectX 12で高度なグラフィックス効果を実現するために不可欠です。HLSLでシェーダーを記述し、コンパイルし、パイプラインステートオブジェクト (PSO) を作成することで、GPU上で実行されるグラフィックス処理をカスタマイズできます。ルートシグネチャは、シェーダーと GPU 間のインターフェースとして機能し、シェーダーがアクセスできるリソースを定義します。

描画パイプラインの構築

DirectX 12における描画パイプラインは、GPUが頂点データを処理し、最終的にピクセルを画面に描画するまでの一連の処理段階を指します。このセクションでは、描画パイプラインの各段階、設定方法、およびDirectX 12での実装について解説します。

描画パイプラインの各段階

描画パイプラインは、主に以下の段階で構成されます。

  1. 入力アセンブラ (Input Assembler, IA): 頂点バッファから頂点データを読み込み、プリミティブ(三角形、線など)を組み立てます。
  2. 頂点シェーダー (Vertex Shader, VS): 頂点データを変換し、頂点の位置、法線、テクスチャ座標などを計算します。
  3. ハルシェーダー (Hull Shader, HS): テッセレーションを行う際に、パッチ単位の制御を行います。(オプション)
  4. テッセレーター (Tessellator, TS): ハルシェーダーの出力に基づいて、より詳細なジオメトリを生成します。(オプション)
  5. ドメインシェーダー (Domain Shader, DS): テッセレーションされた頂点の位置を計算します。(オプション)
  6. ジオメトリシェーダー (Geometry Shader, GS): プリミティブを生成、破棄、または変更します。(オプション)
  7. ラスタライザー (Rasterizer, RS): プリミティブをピクセルに変換し、ピクセルシェーダーに渡す準備をします。
  8. ピクセルシェーダー (Pixel Shader, PS): ピクセルごとに色を計算し、テクスチャのサンプリングやライティング処理を行います。
  9. 出力マージャ (Output Merger, OM): ピクセルシェーダーの出力をフレームバッファに書き込みます。深度テスト、ステンシルテスト、ブレンド処理などを行います。

DirectX 12 での描画パイプライン構築

DirectX 12で描画パイプラインを構築するには、以下の手順が必要です。

  1. 入力レイアウト (Input Layout) の定義: 頂点データの形式(位置、法線、テクスチャ座標など)を定義します。
  2. ルートシグネチャ (Root Signature) の作成: シェーダーがアクセスできるリソース(定数バッファ、テクスチャなど)を定義します。
  3. シェーダーのコンパイル: HLSLで記述されたシェーダーをコンパイルします。
  4. パイプラインステートオブジェクト (Pipeline State Object, PSO) の作成: 描画パイプラインのすべてのステート(シェーダー、レンダーターゲット形式、深度ステンシル形式など)を定義します。
  5. コマンドリストへの描画コマンドの記録: コマンドリストに描画コマンドを記録します。

各段階の設定

1. 入力レイアウト (Input Layout) の定義:

入力レイアウトは、頂点バッファから頂点データをどのように読み込むかを定義します。各頂点要素の形式、オフセット、スロットなどを指定します。

// 入力レイアウトの定義
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

// 入力レイアウトの説明
D3D12_INPUT_LAYOUT_DESC inputLayoutDesc = {};
inputLayoutDesc.pInputElementDescs = inputElementDescs;
inputLayoutDesc.NumElements = _countof(inputElementDescs);

2. ルートシグネチャ (Root Signature) の作成:

ルートシグネチャは、シェーダーがアクセスできるリソースを定義します。定数バッファ、テクスチャ、サンプラーなどをルートパラメータとして指定します。

// ルートシグネチャの定義
CD3DX12_ROOT_PARAMETER rootParameters[2];
CD3DX12_DESCRIPTOR_RANGE descriptorRange;

// テクスチャのディスクリプタレンジ
descriptorRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); // テクスチャを1つ

// ルートパラメータの初期化
rootParameters[0].InitAsConstantBufferView(0); // 定数バッファ
rootParameters[1].InitAsDescriptorTable(1, &descriptorRange); // テクスチャ

// ルートシグネチャの説明
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, rootParameters, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

// ルートシグネチャのシリアライズ
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);
if (FAILED(hr))
{
    // エラー処理
}

// ルートシグネチャの作成
ComPtr<ID3D12RootSignature> rootSignature;
hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
if (FAILED(hr))
{
    // エラー処理
}

3. シェーダーのコンパイル:

HLSLで記述されたシェーダーをコンパイルします。コンパイルされたシェーダーは、バイトコードとして格納されます。 (前のセクション参照)

4. パイプラインステートオブジェクト (PSO) の作成:

PSOは、描画パイプラインのすべてのステートを定義します。シェーダー、入力レイアウト、レンダーターゲット形式、深度ステンシル形式などを指定します。

// パイプラインステートオブジェクト (PSO) の作成
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = inputLayoutDesc; // 入力レイアウト
psoDesc.pRootSignature = rootSignature.Get(); // ルートシグネチャ
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); // 頂点シェーダー
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); // ピクセルシェーダー
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // ラスタライザーステート
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // ブレンドステート
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); // 深度ステンシルステート
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; // レンダーターゲット形式
psoDesc.SampleDesc.Count = 1;

ComPtr<ID3D12PipelineState> pipelineState;
HRESULT hr = device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
if (FAILED(hr))
{
    // エラー処理
}

5. コマンドリストへの描画コマンドの記録:

コマンドリストに描画コマンドを記録します。頂点バッファの設定、ルートシグネチャの設定、PSOの設定、描画命令などを記録します。

// コマンドリストへの描画コマンドの記録
commandList->SetGraphicsRootSignature(rootSignature.Get()); // ルートシグネチャの設定
commandList->SetPipelineState(pipelineState.Get()); // PSOの設定
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // プリミティブトポロジーの設定
commandList->IASetVertexBuffers(0, 1, &vertexBufferView); // 頂点バッファの設定
commandList->DrawInstanced(3, 1, 0, 0); // 描画命令

まとめ

描画パイプラインの構築は、DirectX 12アプリケーションの中核となる部分です。各段階の設定を正しく行うことで、GPUが効率的に頂点データを処理し、最終的なイメージを画面に描画することができます。入力レイアウト、ルートシグネチャ、シェーダー、PSOなどの設定を理解し、適切に組み合わせることで、様々なグラフィックス効果を実現できます。

三角形の描画

このセクションでは、DirectX 12を使用して最も基本的な図形である三角形を描画する方法をステップバイステップで解説します。これまでのセクションで説明した内容を基に、実際にコードを記述して三角形を表示させるまでの手順を説明します。

1. 頂点データの準備

まず、三角形の頂点データを定義します。各頂点は、位置情報を持つ必要があります。オプションとして、色情報やテクスチャ座標などを含めることも可能です。ここでは、シンプルな例として、位置情報のみを持つ頂点データを定義します。

struct Vertex
{
    float Position[3]; // 頂点の位置
};

// 三角形の頂点データ
Vertex vertices[] =
{
    { { 0.0f, 0.5f, 0.0f } },    //
    { { 0.5f, -0.5f, 0.0f } },   // 右下
    { { -0.5f, -0.5f, 0.0f } }  // 左下
};

const UINT vertexBufferSize = sizeof(vertices); // 頂点バッファのサイズ

2. 頂点バッファの作成

頂点データをGPUに転送するために、頂点バッファを作成します。頂点バッファは、D3D12_HEAP_TYPE_UPLOADヒープに配置し、CPUから書き込み可能な状態にします。

ComPtr<ID3D12Resource> vertexBuffer;
ComPtr<ID3D12Resource> vertexBufferUploadHeap;

// ヒーププロパティの設定 (Upload Heap)
D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.CreationNodeMask = 0;
heapProperties.VisibleNodeMask = 0;

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

// リソースの作成 (Upload Heap)
HRESULT hr = device->CreateCommittedResource(
    &heapProperties,
    D3D12_HEAP_FLAG_NONE,
    &bufferDesc,
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&vertexBufferUploadHeap));

if (FAILED(hr))
{
    // エラー処理
}

// 頂点バッファへのデータコピー
void* dataBegin;
CD3DX12_RANGE readRange(0, 0);  // CPUが読み込まないことを示す
hr = vertexBufferUploadHeap->Map(0, &readRange, &dataBegin);
if (FAILED(hr))
{
   // エラー処理
}
memcpy(dataBegin, vertices, vertexBufferSize);
vertexBufferUploadHeap->Unmap(0, nullptr);

3. 頂点バッファビューの作成

頂点バッファビューは、頂点バッファのメモリ位置、サイズ、および頂点データの形式を定義します。

// 頂点バッファビューの作成
D3D12_VERTEX_BUFFER_VIEW vertexBufferView = {};
vertexBufferView.BufferLocation = vertexBufferUploadHeap->GetGPUVirtualAddress(); // GPUアドレス
vertexBufferView.StrideInBytes = sizeof(Vertex); // 頂点サイズ
vertexBufferView.SizeInBytes = vertexBufferSize; // バッファサイズ

4. 入力レイアウトの定義

頂点シェーダーに入力される頂点データの形式を定義します。ここでは、位置情報のみを持つ頂点データに対応する入力レイアウトを定義します。

// 入力レイアウトの定義
D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

// 入力レイアウトの説明
D3D12_INPUT_LAYOUT_DESC inputLayoutDesc = {};
inputLayoutDesc.pInputElementDescs = inputElementDescs;
inputLayoutDesc.NumElements = _countof(inputElementDescs);

5. ルートシグネチャの作成

このシンプルな例では、定数バッファやテクスチャを使用しないため、空のルートシグネチャを作成します。

ComPtr<ID3D12RootSignature> rootSignature;

// ルートシグネチャの説明
D3D12_ROOT_SIGNATURE_DESC rootSigDesc;
rootSigDesc.NumParameters = 0;
rootSigDesc.pParameters = nullptr;
rootSigDesc.NumStaticSamplers = 0;
rootSigDesc.pStaticSamplers = nullptr;
rootSigDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

// シリアライズしてルートシグネチャを作成
ComPtr<ID3DBlob> signature;
ComPtr<ID3DBlob> error;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);

if(error)
{
    OutputDebugStringA((char*)error->GetBufferPointer());
}

if (FAILED(hr))
{
    // エラー処理
}

hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
if (FAILED(hr))
{
    // エラー処理
}

6. シェーダーのコンパイル

頂点シェーダーとピクセルシェーダーをコンパイルします。

VertexShader.hlsl:

struct VSInput
{
    float3 Position : POSITION;
};

struct PSInput
{
    float4 Position : SV_POSITION;
};

PSInput VSMain(VSInput input)
{
    PSInput output;
    output.Position = float4(input.Position, 1.0f);
    return output;
}

PixelShader.hlsl:

float4 PSMain(float4 position : SV_POSITION) : SV_TARGET
{
    return float4(1.0f, 0.0f, 0.0f, 1.0f); // 赤色
}
#include <d3dcompiler.h>

// シェーダーのコンパイル(前のセクションで定義)
ComPtr<ID3DBlob> vertexShaderBlob;
HRESULT hr = CompileShader(L"VertexShader.hlsl", "VSMain", "vs_5_0", vertexShaderBlob);
if (FAILED(hr))
{
    // エラー処理
}

ComPtr<ID3DBlob> pixelShaderBlob;
hr = CompileShader(L"PixelShader.hlsl", "PSMain", "ps_5_0", pixelShaderBlob);
if (FAILED(hr))
{
    // エラー処理
}

7. パイプラインステートオブジェクト (PSO) の作成

描画パイプラインの状態を定義するPSOを作成します。

// パイプラインステートオブジェクト (PSO) の作成
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = inputLayoutDesc; // 入力レイアウト
psoDesc.pRootSignature = rootSignature.Get(); // ルートシグネチャ
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get()); // 頂点シェーダー
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get()); // ピクセルシェーダー
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // ラスタライザーステート
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // ブレンドステート
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); // 深度ステンシルステート
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLELIST;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM; // レンダーターゲット形式
psoDesc.SampleDesc.Count = 1;

ComPtr<ID3D12PipelineState> pipelineState;
hr = device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
if (FAILED(hr))
{
    // エラー処理
}

8. コマンドリストへの描画コマンドの記録

コマンドリストに、描画に必要なコマンドを記録します。ルートシグネチャの設定、PSOの設定、頂点バッファの設定、描画命令などを記録します。

// コマンドリストへの描画コマンドの記録
commandList->SetGraphicsRootSignature(rootSignature.Get()); // ルートシグネチャの設定
commandList->SetPipelineState(pipelineState.Get()); // PSOの設定
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // プリミティブトポロジーの設定
commandList->IASetVertexBuffers(0, 1, &vertexBufferView); // 頂点バッファの設定
commandList->DrawInstanced(3, 1, 0, 0); // 描画命令 (3頂点、1インスタンス)

9. 描画の実行

コマンドリストをクローズし、コマンドキューにサブミットして、描画を実行します。

commandList->Close();
ID3D12CommandList* ppCommandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

これらのステップを完了すると、画面に赤い三角形が表示されるはずです。この例は非常にシンプルですが、DirectX 12で描画を行うための基本的な流れを理解するのに役立ちます。

重要なポイント:

  • 頂点バッファは、GPUからアクセス可能なメモリに配置する必要があります。
  • 入力レイアウトは、頂点バッファの形式と一致している必要があります。
  • ルートシグネチャは、シェーダーがアクセスできるリソースを定義します。
  • PSOは、描画パイプラインの状態を定義します。
  • コマンドリストには、描画に必要なすべてのコマンドを記録する必要があります。

この基本的な三角形の描画を基に、テクスチャマッピング、ライティング、シェーダーエフェクトなどを追加していくことで、より複雑なグラフィックス表現を実現することができます。

テクスチャマッピング

テクスチャマッピングは、3Dオブジェクトの表面に画像を貼り付ける技術であり、リアリティを高めるために不可欠です。このセクションでは、DirectX 12でテクスチャマッピングを実装する方法を解説します。テクスチャのロード、リソースの作成、サンプラーの設定、シェーダーの変更など、具体的な手順を説明します。

1. テクスチャのロードとリソースの作成

まず、テクスチャ画像をロードし、GPUで使用できる形式に変換します。DirectX 12では、DirectXTexライブラリなどを使用して、画像ファイルを読み込み、DXGIフォーマットに変換することができます。ここでは、DirectXTexの使用を前提として、テクスチャリソースを作成する手順を説明します。

1.1 DirectXTexライブラリの準備:

DirectXTexライブラリは、GitHubからダウンロードできます。https://github.com/microsoft/DirectXTex

ライブラリをビルドし、プロジェクトにインクルードディレクトリとライブラリディレクトリを追加します。

1.2 テクスチャのロード:

#include <DirectXTex.h>

ComPtr<ID3D12Resource> texture; // テクスチャリソース
ComPtr<ID3D12Resource> textureUploadHeap; // アップロードヒープ

// テクスチャ画像のファイルパス
std::wstring texturePath = L"texture.png"; // 例:texture.png

// DirectXTexを使用して画像をロード
ScratchImage image;
HRESULT hr = LoadFromWICFile(texturePath.c_str(), WIC_FLAGS_NONE, nullptr, image);
if (FAILED(hr))
{
    // エラー処理
}

// テクスチャリソースの説明を作成
D3D12_RESOURCE_DESC textureDesc = {};
textureDesc.MipLevels = image.GetMetadata().mipLevels;
textureDesc.Format = image.GetMetadata().format;
textureDesc.Width = image.GetMetadata().width;
textureDesc.Height = (UINT)image.GetMetadata().height;
textureDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
textureDesc.DepthOrArraySize = 1;
textureDesc.SampleDesc.Count = 1;
textureDesc.SampleDesc.Quality = 0;
textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;

// テクスチャリソースの作成 (Default Heap)
D3D12_HEAP_PROPERTIES heapProperties = {};
heapProperties.Type = D3D12_HEAP_TYPE_DEFAULT;
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.CreationNodeMask = 0;
heapProperties.VisibleNodeMask = 0;

hr = device->CreateCommittedResource(
    &heapProperties,
    D3D12_HEAP_FLAG_NONE,
    &textureDesc,
    D3D12_RESOURCE_STATE_COPY_DEST,
    nullptr,
    IID_PPV_ARGS(&texture));
if (FAILED(hr))
{
    // エラー処理
}

// アップロードヒープの作成
UINT64 uploadBufferSize;
device->GetCopyableFootprints(&textureDesc, 0, 1, 0, nullptr, nullptr, nullptr, &uploadBufferSize);

D3D12_RESOURCE_DESC uploadBufferDesc = {};
uploadBufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
uploadBufferDesc.Alignment = 0;
uploadBufferDesc.Width = uploadBufferSize;
uploadBufferDesc.Height = 1;
uploadBufferDesc.DepthOrArraySize = 1;
uploadBufferDesc.MipLevels = 1;
uploadBufferDesc.Format = DXGI_FORMAT_UNKNOWN;
uploadBufferDesc.SampleDesc.Count = 1;
uploadBufferDesc.SampleDesc.Quality = 0;
uploadBufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
uploadBufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;

heapProperties.Type = D3D12_HEAP_TYPE_UPLOAD;
hr = device->CreateCommittedResource(
    &heapProperties,
    D3D12_HEAP_FLAG_NONE,
    &uploadBufferDesc,
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&textureUploadHeap));
if (FAILED(hr))
{
    // エラー処理
}

// テクスチャデータをアップロードヒープにコピー
D3D12_SUBRESOURCE_DATA textureData = {};
textureData.pData = image.GetPixels();
textureData.RowPitch = image.GetImages()->rowPitch;
textureData.SlicePitch = image.GetImages()->slicePitch;

UpdateSubresource(commandList.Get(), texture.Get(), textureUploadHeap.Get(), 0, 0, 1, &textureData);

// リソースバリアを設定して、テクスチャをシェーダーリソースとして使用できるようにする
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = texture.Get();
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;

commandList->ResourceBarrier(1, &barrier);

2. サンプラーの作成

サンプラーは、テクスチャの色をどのようにサンプリングするかを定義します。フィルタリングモード、アドレスモード、ミップマップ設定などを指定します。サンプラーは、ディスクリプタヒープに作成されます。

ComPtr<ID3D12DescriptorHeap> samplerHeap;

// サンプラーヒープの説明
D3D12_DESCRIPTOR_HEAP_DESC samplerHeapDesc = {};
samplerHeapDesc.NumDescriptors = 1;
samplerHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER;
samplerHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; // シェーダーから参照可能
samplerHeapDesc.NodeMask = 0;

HRESULT hr = device->CreateDescriptorHeap(&samplerHeapDesc, IID_PPV_ARGS(&samplerHeap));
if (FAILED(hr))
{
    // エラー処理
}

// サンプラーの作成
D3D12_SAMPLER_DESC samplerDesc = {};
samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR; // 線形フィルタリング
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP; // U方向のアドレスモード
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP; // V方向のアドレスモード
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP; // W方向のアドレスモード
samplerDesc.MipLODBias = 0;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
samplerDesc.BorderColor[0] = 0;
samplerDesc.BorderColor[1] = 0;
samplerDesc.BorderColor[2] = 0;
samplerDesc.BorderColor[3] = 0;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D12_FLOAT32_MAX;

// サンプラーのディスクリプタを作成
device->CreateSampler(&samplerDesc, samplerHeap->GetCPUDescriptorHandleForHeapStart());

3. シェーダーの変更

頂点シェーダーとピクセルシェーダーを変更して、テクスチャ座標を受け取り、テクスチャの色をサンプリングするようにします。

VertexShader.hlsl:

struct VSInput
{
    float3 Position : POSITION;
    float2 TexCoord : TEXCOORD; // テクスチャ座標
};

struct PSInput
{
    float4 Position : SV_POSITION;
    float2 TexCoord : TEXCOORD; // テクスチャ座標
};

PSInput VSMain(VSInput input)
{
    PSInput output;
    output.Position = float4(input.Position, 1.0f);
    output.TexCoord = input.TexCoord;
    return output;
}

PixelShader.hlsl:

Texture2D Texture : register(t0); // テクスチャ
SamplerState Sampler : register(s0); // サンプラー

float4 PSMain(float4 position : SV_POSITION, float2 texCoord : TEXCOORD) : SV_TARGET
{
    return Texture.Sample(Sampler, texCoord); // テクスチャの色をサンプリング
}

4. ルートシグネチャの変更

ルートシグネチャを変更して、テクスチャとサンプラーをシェーダーに渡せるようにします。ディスクリプタテーブルを使用します。

// ルートシグネチャの定義
CD3DX12_ROOT_PARAMETER rootParameters[2];
CD3DX12_DESCRIPTOR_RANGE textureDescriptorRange;
CD3DX12_DESCRIPTOR_RANGE samplerDescriptorRange;

// テクスチャのディスクリプタレンジ
textureDescriptorRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0); // テクスチャを1つ

// サンプラーのディスクリプタレンジ
samplerDescriptorRange.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0); // サンプラーを1つ

// ルートパラメータの初期化
rootParameters[0].InitAsConstantBufferView(0); // 定数バッファ (例:ワールド・ビュー・プロジェクション行列)
rootParameters[1].InitAsDescriptorTable(1, &textureDescriptorRange, D3D12_SHADER_VISIBILITY_PIXEL); // テクスチャ
// サンプラーはテクスチャと同じディスクリプタテーブルで渡すこともできます

// スタティックサンプラー (ルートシグネチャ内で定義)
D3D12_STATIC_SAMPLER_DESC staticSamplers[1];
staticSamplers[0].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
staticSamplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
staticSamplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
staticSamplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
staticSamplers[0].MipLODBias = 0;
staticSamplers[0].MaxAnisotropy = 1;
staticSamplers[0].ComparisonFunc = D3D12_COMPARISON_FUNC_ALWAYS;
staticSamplers[0].BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
staticSamplers[0].MinLOD = 0;
staticSamplers[0].MaxLOD = D3D12_FLOAT32_MAX;
staticSamplers[0].ShaderRegister = 0; // s0
staticSamplers[0].RegisterSpace = 0;
staticSamplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;

// ルートシグネチャの説明
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, rootParameters, 1, staticSamplers, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

// ルートシグネチャのシリアライズ、作成(省略)

5. PSO の変更

ルートシグネチャを使用するようにPSOを更新します。

6. 頂点データの変更

頂点データにテクスチャ座標を追加します。

struct Vertex
{
    float Position[3];
    float TexCoord[2]; // テクスチャ座標
};

// 頂点データ
Vertex vertices[] =
{
    { { 0.0f, 0.5f, 0.0f }, { 0.5f, 0.0f } },
    { { 0.5f, -0.5f, 0.0f }, { 1.0f, 1.0f } },
    { { -0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f } }
};

7. コマンドリストにリソースを設定

コマンドリストで、テクスチャとサンプラーのディスクリプタを設定します。

// テクスチャとサンプラーのディスクリプタヒープを設定
ID3D12DescriptorHeap* ppHeaps[] = { textureHeap.Get(), samplerHeap.Get() };
commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

// ルートシグネチャのディスクリプタテーブルを設定
commandList->SetGraphicsRootDescriptorTable(1, textureHeap->GetGPUDescriptorHandleForHeapStart());

まとめ

これらの手順に従うことで、DirectX 12アプリケーションにテクスチャマッピングを実装できます。テクスチャのロード、サンプラーの作成、シェーダーの変更、ルートシグネチャの更新など、多くの手順が必要ですが、それぞれの役割を理解することで、より高度なグラフィックス表現を実現できます。

まとめと今後の学習

このチュートリアルでは、C++とDirectX 12を使用してゲームプログラミングを始めるための基礎を学びました。DirectX 12の基本的な概念、開発環境の構築、リソースの作成と管理、シェーダープログラミング、描画パイプラインの構築、そして基本的な図形である三角形の描画、テクスチャマッピングについて解説しました。

まとめ

DirectX 12は、ローレベルAPIであり、DirectX 11と比較してより高度な制御が可能になります。その反面、DirectX 11よりも複雑な管理が必要となります。このチュートリアルで紹介した内容は、DirectX 12プログラミングのほんの入り口にすぎませんが、これらの基礎を理解することで、より高度なグラフィックス表現やゲーム開発に挑戦するための足がかりとなるでしょう。

  • DirectX 12の基本概念: デバイス、コマンドキュー、コマンドリスト、リソース、ディスクリプタなど、DirectX 12のアーキテクチャを理解しました。
  • 開発環境の構築: Visual Studio、Windows SDK、グラフィックスドライバなど、必要なソフトウェアのインストールと設定を行いました。
  • リソースの作成と管理: テクスチャ、バッファなど、GPUがアクセスできるメモリ領域を管理する方法を学びました。
  • シェーダープログラミング: HLSLでシェーダーを記述し、コンパイルし、DirectX 12で使用する方法を学びました。
  • 描画パイプラインの構築: 描画パイプラインの各段階を設定し、三角形を描画しました。
  • テクスチャマッピング: 3Dオブジェクトの表面に画像を貼り付ける方法を学びました。

今後の学習

このチュートリアルを終えたあなたは、DirectX 12プログラミングの基礎を身につけました。さらに学習を進めることで、より高度な技術を習得し、素晴らしいゲームやグラフィックスアプリケーションを開発できるようになるでしょう。

おすすめの学習トピック:

  • 高度なシェーダー技術: ライティング、シャドウイング、ポストエフェクトなど、高度なシェーダー技術を学ぶことで、よりリアルなグラフィックス表現を実現できます。
  • コンピュートシェーダー: グラフィックスパイプラインとは独立して、汎用的な計算処理を行うコンピュートシェーダーを学ぶことで、物理シミュレーションやAI処理など、様々な処理をGPUで高速化できます。
  • ジオメトリシェーダーとテッセレーション: ジオメトリシェーダーとテッセレーションを学ぶことで、ポリゴンの数を動的に調整し、ディテールの高い表現を実現できます。
  • 高度なメモリ管理: リソースのライフサイクル、アロケーション、ヒープ管理など、高度なメモリ管理技術を学ぶことで、パフォーマンスを最適化できます。
  • マルチスレッドレンダリング: 複数のスレッドを使用してレンダリング処理を並列化することで、CPUの負荷を分散し、パフォーマンスを向上させることができます。
  • DirectX 12 Ultimate: DirectX Raytracing (DXR) や Variable Rate Shading (VRS) など、DirectX 12 Ultimateの最新機能を学ぶことで、最先端のグラフィックス技術を体験できます。
  • ゲームエンジン: Unreal EngineやUnityなど、ゲームエンジンを利用することで、DirectX 12の低レベルな詳細を気にせずに、ゲーム開発に集中できます。

学習リソース:

  • Microsoft DirectX Graphics Documentation: https://learn.microsoft.com/en-us/windows/win32/direct3d12/directx-graphics-reference
  • 書籍: DirectX 12に関する書籍を読むことで、より体系的に知識を深めることができます。
  • オンラインチュートリアル: 様々なWebサイトやブログで、DirectX 12に関するチュートリアルが公開されています。
  • サンプルコード: Microsoftやその他の開発者が公開しているサンプルコードを参考にすることで、実践的な知識を習得できます。
  • コミュニティ: DirectX 12に関するコミュニティに参加することで、他の開発者と情報交換や質問を行うことができます。

最後に

DirectX 12プログラミングは、奥深く、やりがいのある分野です。根気強く学習を続けることで、必ず素晴らしい成果を上げることができるでしょう。このチュートリアルが、あなたのDirectX 12プログラミングの旅の始まりとなることを願っています。頑張ってください!

投稿者 dodo

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です