0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【DirectX11】三角形を描いてみよう!

Posted at

前回はDirextXの初期化と画面のクリア方法を紹介しました。

今回はようやく、HLSLというシェーダー言語を使用して、三角形を描画してみたいと思います。

前提知識

  • C++の基礎構文を理解している
  • DirectXの初期化を行える

コード例

三角形を描画する最も簡単なコード

main.cpp
//-----------------------------------------------
//! @file   main.cpp
//! @brief  D3D11で三角形を描画する最小構成サンプル
//-----------------------------------------------
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include <d3dcompiler.h>
#include <cassert>

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

// グローバル変数(今回は最小構成なのでここに宣言)
HWND                     g_hWnd = nullptr; // ウィンドウハンドル
ID3D11Device* g_device = nullptr;          // D3D11デバイス
ID3D11DeviceContext* g_context = nullptr;  // D3D11デバイスコンテキスト
IDXGISwapChain* g_swapChain = nullptr;     // スワップチェイン
ID3D11RenderTargetView* g_rtv = nullptr;   // レンダーターゲットビュー
ID3D11VertexShader* g_vs = nullptr;        // 頂点シェーダー
ID3D11PixelShader* g_ps = nullptr;         // ピクセルシェーダー 
ID3D11InputLayout* g_inputLayout = nullptr;// 入力レイアウト
ID3D11Buffer* g_vertexBuffer = nullptr;    // 頂点バッファ

// 頂点型
struct Vertex {
	float x, y, z;    // 位置
	float r, g, b, a; // 色
};

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }
}

// ウィンドウ作成
HWND CreateSimpleWindow(HINSTANCE hInstance, int width, int height) {
	const wchar_t* kClass = L"D3D11TriangleClass";// ウィンドウクラス名

	WNDCLASS wc = {};                                   // ウィンドウクラス構造体
	wc.lpfnWndProc = WndProc;                           // ウィンドウプロシージャ
	wc.hInstance = hInstance;                           // インスタンスハンドル
	wc.lpszClassName = kClass;                          // ウィンドウクラス名
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW);        // カーソル

	RegisterClass(&wc);                                 // ウィンドウクラス登録

	RECT rc = { 0, 0, width, height };                 // ウィンドウサイズ設定
	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE); // ウィンドウサイズ調整
	// ウィンドウ作成
    HWND hWnd = CreateWindowEx(
        0, kClass, L"D3D11 Triangle",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        rc.right - rc.left, rc.bottom - rc.top,
        nullptr, nullptr, hInstance, nullptr
    );
	// ウィンドウ表示
    ShowWindow(hWnd, SW_SHOW);
	
	return hWnd; // ウィンドウハンドルを返す
}

// D3D11初期化(デバイス、スワップチェイン、RTV)
void InitD3D11(HWND hWnd, int width, int height) {
    // スワップチェイン設定(基本形)
	DXGI_SWAP_CHAIN_DESC scd = {};                      // スワップチェイン記述子
	scd.BufferCount = 1;    				            // バッファ数                
	scd.BufferDesc.Width = width;                       // バッファ幅
	scd.BufferDesc.Height = height;                     // バッファ高
    scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 一般的なカラー形式
    scd.BufferDesc.RefreshRate.Numerator = 60;          // 最小構成(厳密でなくてOK)
	scd.BufferDesc.RefreshRate.Denominator = 1;         // 最小構成(厳密でなくてOK)
	scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;  // レンダーターゲットとして使用
	scd.OutputWindow = hWnd;                            // 出力ウィンドウ
    scd.SampleDesc.Count = 1;                           // マルチサンプルなし
	scd.SampleDesc.Quality = 0;                         // 標準品質レベル
	scd.Windowed = TRUE;                                // ウィンドウモード
    scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;          // 旧来の最小形

	UINT createFlags = 0;                               // デバイス作成フラグ
#if defined(_DEBUG)
    createFlags |= D3D11_CREATE_DEVICE_DEBUG;           // デバッグレイヤー(インストール済みなら有効)
#endif

	D3D_FEATURE_LEVEL featureLevel;                    // フィーチャーレベル格納用
    HRESULT hr = D3D11CreateDeviceAndSwapChain(
        nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
        createFlags,
        nullptr, 0, // 既定のフィーチャーレベル一覧
        D3D11_SDK_VERSION,
        &scd, &g_swapChain,
        &g_device, &featureLevel,
        &g_context
    );
    assert(SUCCEEDED(hr) && "D3D11CreateDeviceAndSwapChain failed");

    // バックバッファからレンダーターゲットビュー作成
    ID3D11Texture2D* backBuffer = nullptr;
    hr = g_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
    assert(SUCCEEDED(hr));
    hr = g_device->CreateRenderTargetView(backBuffer, nullptr, &g_rtv);
    backBuffer->Release();
    assert(SUCCEEDED(hr));

    // ビューポート設定(画面サイズに合わせる)
	D3D11_VIEWPORT vp = {};                     // ビューポート構造体
	vp.TopLeftX = 0;                            // 左上X座標
	vp.TopLeftY = 0;                            // 左上Y座標
	vp.Width = static_cast<float>(width);       // 幅
	vp.Height = static_cast<float>(height);     // 高さ
	vp.MinDepth = 0.0f;                         // 最小深度
	vp.MaxDepth = 1.0f;                         // 最大深度
	g_context->RSSetViewports(1, &vp);          // ビューポート設定
}

// 外部HLSLをコンパイルし、シェーダーと入力レイアウトを作る
void CreateShadersAndInputLayout() {
	// シェーダーブロブとエラーブロブ
	ID3DBlob* vsBlob = nullptr;     // 頂点シェーダーブロブ
	ID3DBlob* psBlob = nullptr;     // ピクセルシェーダーブロブ
	ID3DBlob* errorBlob = nullptr;  // エラーブロブ

	// 頂点シェーダーをコンパイル
	HRESULT hr = D3DCompileFromFile(
		L"TriangleVS.hlsl", nullptr, nullptr,
		"main", "vs_5_0",
		0, 0, &vsBlob, &errorBlob
	);
	// コンパイル失敗時
	if (FAILED(hr)) {
		if (errorBlob) {
			OutputDebugStringA((char*)errorBlob->GetBufferPointer());
			errorBlob->Release();
		}
		assert(false && "VS compile failed: check file path or syntax");
	}
	// 頂点シェーダー作成
	hr = g_device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &g_vs);
	assert(SUCCEEDED(hr));

	// ピクセルシェーダー
	hr = D3DCompileFromFile(
		L"TrianglePS.hlsl", nullptr, nullptr,
		"main", "ps_5_0",
		0, 0, &psBlob, &errorBlob
	);
	// コンパイル失敗時
	if (FAILED(hr)) {
		if (errorBlob) {
			OutputDebugStringA((char*)errorBlob->GetBufferPointer());
			errorBlob->Release();
		}
		assert(false && "PS compile failed: check file path or syntax");
	}
	// ピクセルシェーダー作成
	hr = g_device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &g_ps);
	assert(SUCCEEDED(hr));

	// 入力レイアウト(POSITION: float3, COLOR: float4)
	D3D11_INPUT_ELEMENT_DESC layout[] = {
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, 0,                          D3D11_INPUT_PER_VERTEX_DATA, 0 },
		{ "COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, UINT(3 * sizeof(float)),    D3D11_INPUT_PER_VERTEX_DATA, 0 },
	};
	// 入力レイアウト作成
	hr = g_device->CreateInputLayout(
		layout, ARRAYSIZE(layout),
		vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(),
		&g_inputLayout
	);
	assert(SUCCEEDED(hr));

	// Blob解放
	if (vsBlob) vsBlob->Release();
	if (psBlob) psBlob->Release();
	if (errorBlob) errorBlob->Release();
}

// 三角形の頂点バッファを作成
void CreateTriangleVertexBuffer() {
	// NDC座標で画面中央の三角形
	Vertex vertices[3] = {
		{  0.0f,  0.5f, 0.0f, 1, 0, 0, 1 }, // 上:赤
		{  0.5f, -0.5f, 0.0f, 0, 1, 0, 1 }, // 右下:緑
		{ -0.5f, -0.5f, 0.0f, 0, 0, 1, 1 }, // 左下:青
	};
	// 頂点バッファ記述子
	D3D11_BUFFER_DESC bd = {};				// バッファ記述子
	bd.ByteWidth = UINT(sizeof(vertices));	// バッファサイズ
	bd.Usage = D3D11_USAGE_DEFAULT;			// デフォルト使用
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;// 頂点バッファとして使用
	// 初期データ設定
	D3D11_SUBRESOURCE_DATA initData = {};	// サブリソースデータ
	initData.pSysMem = vertices;			// 頂点データポインタ
	// 頂点バッファ作成
	HRESULT hr = g_device->CreateBuffer(&bd, &initData, &g_vertexBuffer);
	assert(SUCCEEDED(hr));
}

// フレーム描画:三角形を表示
void RenderFrame() {
    // クリアカラー(暗めの紺)
    float clearColor[4] = { 0.05f, 0.05f, 0.08f, 1.0f };
    g_context->OMSetRenderTargets(1, &g_rtv, nullptr);
    g_context->ClearRenderTargetView(g_rtv, clearColor);

    // IA(Input Assembler)ステージにバッファ・レイアウト・トポロジを設定
    g_context->IASetInputLayout(g_inputLayout);
    UINT stride = sizeof(Vertex);
    UINT offset = 0;
    g_context->IASetVertexBuffers(0, 1, &g_vertexBuffer, &stride, &offset);
    g_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // シェーダーをバインド
    g_context->VSSetShader(g_vs, nullptr, 0);
    g_context->PSSetShader(g_ps, nullptr, 0);

    // 3頂点を描画
    g_context->Draw(3, 0);

    // 画面に反映
    g_swapChain->Present(1, 0);
}

// 後片付け(SAFE RELEASE)
void Cleanup() {
    if (g_vertexBuffer) { g_vertexBuffer->Release();  g_vertexBuffer = nullptr; }
    if (g_inputLayout) { g_inputLayout->Release();   g_inputLayout = nullptr; }
    if (g_vs) { g_vs->Release();            g_vs = nullptr; }
    if (g_ps) { g_ps->Release();            g_ps = nullptr; }
    if (g_rtv) { g_rtv->Release();           g_rtv = nullptr; }
    if (g_swapChain) { g_swapChain->Release();     g_swapChain = nullptr; }
    if (g_context) { g_context->Release();       g_context = nullptr; }
    if (g_device) { g_device->Release();        g_device = nullptr; }
}

// エントリポイント
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
    // 1) ウィンドウ作成
    const int kWidth = 800;
    const int kHeight = 600;
    g_hWnd = CreateSimpleWindow(hInstance, kWidth, kHeight);

    // 2) D3D11初期化
    InitD3D11(g_hWnd, kWidth, kHeight);

    // 3) シェーダー&入力レイアウト
    CreateShadersAndInputLayout();

    // 4) 頂点バッファ
    CreateTriangleVertexBuffer();

    // メッセージループ&レンダリング
    MSG msg = {};
    while (msg.message != WM_QUIT) {
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else {
            RenderFrame();
        }
    }

    Cleanup();
    return 0;
}
TriangleVS.hlsl
//--------------------------------
//! @file TriangleVS.hlsl
//! @brief 三角形描画用頂点シェーダー
//--------------------------------

//--------------------------------
// 入力構造体
//--------------------------------
struct VSInput
{
	float3 position : POSITION; // 3D座標(今回はZ=0)
	float4 color : COLOR;		// 頂点カラー
};

//--------------------------------
// 出力構造体
//--------------------------------
struct VSOutput
{
	float4 position : SV_Position; // 画面に向けての最終位置
	float4 color : COLOR;		   // ピクセルへ渡すカラー
};

//--------------------------------
//! @brief 三角形描画用頂点シェーダー
//! @param IN [in] 頂点シェーダー入力
//! @return 出力構造体
//--------------------------------
VSOutput main(VSInput IN)
{
    VSOutput OUT;								// 出力構造体を宣言
    OUT.position = float4(IN.position, 1.0);	// 位置情報を設定
    OUT.color = IN.color;						// 頂点カラーを設定
    return OUT;                                 // 出力構造体を返す
}
TrianglePS.hlsl
//------------------------------
//! @file TrianglePS.hlsl
//! @brief 三角形描画用ピクセルシェーダー
//------------------------------

//--------------------------------
// 頂点シェーダーから受け取った情報の構造体
//--------------------------------
struct VSOutput
{
    float4 position : SV_Position; // 画面に向けての最終位置 
    float4 color : COLOR;          // ピクセルへ渡すカラー
};

//--------------------------------
//! @brief 三角形描画用ピクセルシェーダー
//! @param IN [in] 頂点シェーダーからの出力
//! @return ピクセルカラー
//--------------------------------
float4 main(VSOutput IN) : SV_Target 
{
    return IN.color; // 補間された色が入ってくる
}

ソリューションエクスプローラーhlslファイルを右クリックして、プロパティからプロパティウィンドウを開いてください。
スクリーンショット (77).png
ウィンドウ左から構成プロパティ->全般を選択し、ウィンドウ右の項目の種類HLSLコンパイラへ変更してください。
スクリーンショット (78).png
TriangleVS.hlslTrianglePS.hlslに変更を施したら、コンパイルを行います。
スクリーンショット (76).png
三色の三角形が描画されたら成功です。

グローバル変数

初めに、グローバル空間に必要な変数を宣言しておきます。
大規模になるとクラスにまとめますが、本記事では小規模のためグローバルに宣言を行います。

main.cpp
// グローバル変数(今回は最小構成なのでここに宣言)
HWND                     g_hWnd = nullptr; // ウィンドウハンドル
ID3D11Device* g_device = nullptr;          // D3D11デバイス
ID3D11DeviceContext* g_context = nullptr;  // D3D11デバイスコンテキスト
IDXGISwapChain* g_swapChain = nullptr;     // スワップチェイン
ID3D11RenderTargetView* g_rtv = nullptr;   // レンダーターゲットビュー
ID3D11VertexShader* g_vs = nullptr;        // 頂点シェーダー
ID3D11PixelShader* g_ps = nullptr;         // ピクセルシェーダー 
ID3D11InputLayout* g_inputLayout = nullptr;// 入力レイアウト
ID3D11Buffer* g_vertexBuffer = nullptr;    // 頂点バッファ

本記事で初めて出てきた型は以下の通りです。

型名 役割
ID3D11VertexShader 頂点シェーダーを表すインターフェース。頂点データを受け取り、座標変換や頂点属性処理を行う。
ID3D11PixelShader ピクセルシェーダーを表すインターフェース。ラスタライズ後のピクセルごとに色を計算・出力する。
ID3D11InputLayout 頂点バッファのデータ構造をGPUに伝えるためのインターフェース。POSITIONCOLORなどのセマンティクスを対応付ける。
ID3D11Buffer 頂点バッファやインデックスバッファなど、GPUに渡すデータを保持するためのインターフェース。今回は三角形の頂点座標と色を格納。

頂点情報の構造体定義

次に、シェーダーで使用する頂点情報をcpp側で構造体で定義します。
構造体名は、頂点を意味するVertexとしておきます。

main.cpp
// 頂点型
struct Vertex {
	float x, y, z;    // 位置
	float r, g, b, a; // 色
};

後にシェーダーに送りたい頂点情報を定義しておきます。
本記事では座標と色以外は使用しないため、このような定義となっております。

イベント処理

次にイベント処理を書きます。
特に新しく紹介することはないです。

main.cpp
// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }
}

ウィンドウの作成

次に、ウィンドウ作成処理を関数にまとめておきます。

main.cpp
// ウィンドウ作成
HWND CreateSimpleWindow(HINSTANCE hInstance, int width, int height) {
	const wchar_t* kClass = L"D3D11TriangleClass";// ウィンドウクラス名

	WNDCLASS wc = {};                                   // ウィンドウクラス構造体
	wc.lpfnWndProc = WndProc;                           // ウィンドウプロシージャ
	wc.hInstance = hInstance;                           // インスタンスハンドル
	wc.lpszClassName = kClass;                          // ウィンドウクラス名
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW);        // カーソル

	RegisterClass(&wc);                                 // ウィンドウクラス登録

	RECT rc = { 0, 0, width, height };                 // ウィンドウサイズ設定
	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE); // ウィンドウサイズ調整
	// ウィンドウ作成
    HWND hWnd = CreateWindowEx(
        0, kClass, L"D3D11 Triangle",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        rc.right - rc.left, rc.bottom - rc.top,
        nullptr, nullptr, hInstance, nullptr
    );
	// ウィンドウ表示
    ShowWindow(hWnd, SW_SHOW);
	
	return hWnd; // ウィンドウハンドルを返す
}

DirectXの初期化

初期化関数を作成します。

main.cpp
// D3D11初期化(デバイス、スワップチェイン、RTV)
void InitD3D11(HWND hWnd, int width, int height) {
    // スワップチェイン設定(基本形)
	DXGI_SWAP_CHAIN_DESC scd = {};                      // スワップチェイン記述子
	scd.BufferCount = 1;    				            // バッファ数                
	scd.BufferDesc.Width = width;                       // バッファ幅
	scd.BufferDesc.Height = height;                     // バッファ高
    scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 一般的なカラー形式
    scd.BufferDesc.RefreshRate.Numerator = 60;          // 最小構成(厳密でなくてOK)
	scd.BufferDesc.RefreshRate.Denominator = 1;         // 最小構成(厳密でなくてOK)
	scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;  // レンダーターゲットとして使用
	scd.OutputWindow = hWnd;                            // 出力ウィンドウ
    scd.SampleDesc.Count = 1;                           // マルチサンプルなし
	scd.SampleDesc.Quality = 0;                         // 標準品質レベル
	scd.Windowed = TRUE;                                // ウィンドウモード
    scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;          // 旧来の最小形

	UINT createFlags = 0;                               // デバイス作成フラグ
#if defined(_DEBUG)
    createFlags |= D3D11_CREATE_DEVICE_DEBUG;           // デバッグレイヤー(インストール済みなら有効)
#endif

	D3D_FEATURE_LEVEL featureLevel;                    // フィーチャーレベル格納用
    HRESULT hr = D3D11CreateDeviceAndSwapChain(
        nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
        createFlags,
        nullptr, 0, // 既定のフィーチャーレベル一覧
        D3D11_SDK_VERSION,
        &scd, &g_swapChain,
        &g_device, &featureLevel,
        &g_context
    );
    assert(SUCCEEDED(hr) && "D3D11CreateDeviceAndSwapChain failed");

    // バックバッファからレンダーターゲットビュー作成
    ID3D11Texture2D* backBuffer = nullptr;
    hr = g_swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer);
    assert(SUCCEEDED(hr));
    hr = g_device->CreateRenderTargetView(backBuffer, nullptr, &g_rtv);
    backBuffer->Release();
    assert(SUCCEEDED(hr));

    // ビューポート設定(画面サイズに合わせる)
	D3D11_VIEWPORT vp = {};                     // ビューポート構造体
	vp.TopLeftX = 0;                            // 左上X座標
	vp.TopLeftY = 0;                            // 左上Y座標
	vp.Width = static_cast<float>(width);       // 幅
	vp.Height = static_cast<float>(height);     // 高さ
	vp.MinDepth = 0.0f;                         // 最小深度
	vp.MaxDepth = 1.0f;                         // 最大深度
	g_context->RSSetViewports(1, &vp);          // ビューポート設定
}

D3D11_VIEWPORTは、ウィンドウのどこに描画を行うかの値を指定するための構造体です。
メンバは以下の通りです。

メンバ 説明
TopLeftX FLOAT ビューポート左上の X 座標(ピクセル単位)
TopLeftY FLOAT ビューポート左上の Y 座標(ピクセル単位)
Width FLOAT ビューポートの幅(ピクセル単位)
Height FLOAT ビューポートの高さ(ピクセル単位)
MinDepth FLOAT 深度の最小値(通常 0.0f)
MaxDepth FLOAT 深度の最大値(通常 1.0f)
main.cpp
 // ビューポート設定(画面サイズに合わせる)
	D3D11_VIEWPORT vp = {};                     // ビューポート構造体
	vp.TopLeftX = 0;                            // 左上X座標
	vp.TopLeftY = 0;                            // 左上Y座標
	vp.Width = static_cast<float>(width);       // 幅
	vp.Height = static_cast<float>(height);     // 高さ
	vp.MinDepth = 0.0f;                         // 最小深度
	vp.MaxDepth = 1.0f;                         // 最大深度

g_contextのメンバRSSetViewports()でビューポート(画面に描画される領域)を作成してください。
引数は以下の通りです。

引数名 説明
NumViewports UINT 設定するビューポートの数。通常は1
pViewports const D3D11_VIEWPORT* ビューポート構造体へのポインタ。配列で複数指定可能
main.cpp
	g_context->RSSetViewports(1, &vp);          // ビューポート設定

シェーダーのコンパイル

外部ファイルに記述したシェーダーは関数を通じてコンパイルする必要があるため、その処理を記述します。
シェーダーファイルの解説は後に行います。

main.cpp
// 外部HLSLをコンパイルし、シェーダーと入力レイアウトを作る
void CreateShadersAndInputLayout() {
	// シェーダーブロブとエラーブロブ
	ID3DBlob* vsBlob = nullptr;     // 頂点シェーダーブロブ
	ID3DBlob* psBlob = nullptr;     // ピクセルシェーダーブロブ
	ID3DBlob* errorBlob = nullptr;  // エラーブロブ

	// 頂点シェーダーをコンパイル
	HRESULT hr = D3DCompileFromFile(
		L"TriangleVS.hlsl", nullptr, nullptr,
		"main", "vs_5_0",
		0, 0, &vsBlob, &errorBlob
	);
	// コンパイル失敗時
	if (FAILED(hr)) {
		if (errorBlob) {
			OutputDebugStringA((char*)errorBlob->GetBufferPointer());
			errorBlob->Release();
		}
		assert(false && "VS compile failed: check file path or syntax");
	}
	// 頂点シェーダー作成
	hr = g_device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &g_vs);
	assert(SUCCEEDED(hr));

	// ピクセルシェーダー
	hr = D3DCompileFromFile(
		L"TrianglePS.hlsl", nullptr, nullptr,
		"main", "ps_5_0",
		0, 0, &psBlob, &errorBlob
	);
	// コンパイル失敗時
	if (FAILED(hr)) {
		if (errorBlob) {
			OutputDebugStringA((char*)errorBlob->GetBufferPointer());
			errorBlob->Release();
		}
		assert(false && "PS compile failed: check file path or syntax");
	}
	// ピクセルシェーダー作成
	hr = g_device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &g_ps);
	assert(SUCCEEDED(hr));

	// 入力レイアウト(POSITION: float3, COLOR: float4)
	D3D11_INPUT_ELEMENT_DESC layout[] = {
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, 0,                          D3D11_INPUT_PER_VERTEX_DATA, 0 },
		{ "COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, UINT(3 * sizeof(float)),    D3D11_INPUT_PER_VERTEX_DATA, 0 },
	};
	// 入力レイアウト作成
	hr = g_device->CreateInputLayout(
		layout, ARRAYSIZE(layout),
		vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(),
		&g_inputLayout
	);
	assert(SUCCEEDED(hr));

	// Blob解放
	if (vsBlob) vsBlob->Release();
	if (psBlob) psBlob->Release();
	if (errorBlob) errorBlob->Release();
}

シェーダーブロブとは、データの塊のことです。
コンパイルしたシェーダーの内容を一時的に保持するために宣言します。

main.cpp
    // シェーダーブロブとエラーブロブ
	ID3DBlob* vsBlob = nullptr;     // 頂点シェーダーブロブ
	ID3DBlob* psBlob = nullptr;     // ピクセルシェーダーブロブ
	ID3DBlob* errorBlob = nullptr;  // エラーブロブ

D3DCompileFromFile()を使用して頂点シェーダーファイルをコンパイルします。
引数は以下の通りです。

引数名 本記事の値 説明
pFileName LPCWSTR L"TriangleVS.hlsl" コンパイル対象のHLSLファイル名(Unicode文字列)。ここでは頂点シェーダーファイル
pDefines const D3D_SHADER_MACRO* nullptr マクロ定義。未使用なので nullptr
pInclude ID3DInclude* nullptr #include の管理。未使用なので nullptr
pEntrypoint LPCSTR "main" シェーダーのエントリーポイント関数名。ここでは main
pTarget LPCSTR "vs_5_0" コンパイルターゲット(シェーダーモデル)。ここでは Vertex Shader 5.0
Flags1 UINT 0 コンパイルオプション。0 はデフォルト設定
Flags2 UINT 0 エフェクト用フラグ。通常は 0
ppCode ID3DBlob** &vsBlob コンパイル成功時にバイナリコードが格納される Blob
ppErrorMsgs ID3DBlob** &errorBlob コンパイル失敗時にエラーメッセージが格納される Blob
main.cpp
// 頂点シェーダーをコンパイル
	HRESULT hr = D3DCompileFromFile(
		L"TriangleVS.hlsl", nullptr, nullptr,
		"main", "vs_5_0",
		0, 0, &vsBlob, &errorBlob
	);

コンパイルしたら、g_deviceのメンバ、CreateVertexShader()を使用してGPUに処理を登録します。
引数は以下の通りです。

引数名 本記事の値 説明
pShaderBytecode const void* vsBlob->GetBufferPointer() コンパイル済みシェーダーのバイナリコードへのポインタ
BytecodeLength SIZE_T vsBlob->GetBufferSize() バイナリコードのサイズ(バイト数)
pClassLinkage ID3D11ClassLinkage* nullptr 動的リンク用。未使用なので nullptr
ppVertexShader ID3D11VertexShader** &g_vs 作成された頂点シェーダーを受け取るポインタ
main.cpp
	// 頂点シェーダー作成
	hr = g_device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &g_vs);

ピクセルシェーダーにも同じことを行います。
関数名が異なるだけですので、説明は省略します。

main.cpp
	// ピクセルシェーダー
	hr = D3DCompileFromFile(
		L"TrianglePS.hlsl", nullptr, nullptr,
		"main", "ps_5_0",
		0, 0, &psBlob, &errorBlob
	);
	// コンパイル失敗時
	if (FAILED(hr)) {
		if (errorBlob) {
			OutputDebugStringA((char*)errorBlob->GetBufferPointer());
			errorBlob->Release();
		}
		assert(false && "PS compile failed: check file path or syntax");
	}
	// ピクセルシェーダー作成
	hr = g_device->CreatePixelShader(psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &g_ps);
	assert(SUCCEEDED(hr));

D3D11_INPUT_ELEMENT_DESCでGPU に伝える頂点データを宣言します。
メンバは以下の通りです。

メンバ名 説明
SemanticName LPCSTR シェーダー側で使うセマンティクス名(例: "POSITION", "COLOR"
SemanticIndex UINT 同じセマンティクスを複数使う場合の番号(例: 行列の各列に 0,1,2,3 を付ける)
Format DXGI_FORMAT 頂点要素のデータ型(例: DXGI_FORMAT_R32G32B32_FLOAT は float3)
InputSlot UINT 頂点バッファのスロット番号。複数バッファを使う場合に指定
AlignedByteOffset UINT 頂点構造体内でのバイトオフセット。0D3D11_APPEND_ALIGNED_ELEMENT を指定
InputSlotClass D3D11_INPUT_CLASSIFICATION 頂点ごと (D3D11_INPUT_PER_VERTEX_DATA) かインスタンスごと (D3D11_INPUT_PER_INSTANCE_DATA) か
InstanceDataStepRate UINT インスタンシング時に何インスタンスごとに進めるか。通常は 0
main.cpp
	// 入力レイアウト(POSITION: float3, COLOR: float4)
	D3D11_INPUT_ELEMENT_DESC layout[] = {
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, 0,                          D3D11_INPUT_PER_VERTEX_DATA, 0 },
		{ "COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, UINT(3 * sizeof(float)),    D3D11_INPUT_PER_VERTEX_DATA, 0 },
	};

g_deviceCreateInputLayout()で、GPU側に頂点データを実際に作成します。
引数は以下の通りです。

引数名 本記事の値 意味
pInputElementDescs const D3D11_INPUT_ELEMENT_DESC* layout 頂点要素の配列。POSITION(float3) と COLOR(float4) を定義
NumElements UINT ARRAYSIZE(layout) = 2 頂点要素の数。今回は2つ
pShaderBytecode const void* vsBlob->GetBufferPointer() コンパイル済み頂点シェーダーのバイナリコードへのポインタ
BytecodeLength SIZE_T vsBlob->GetBufferSize() バイナリコードのサイズ(バイト数)
ppInputLayout ID3D11InputLayout** &g_inputLayout 作成された入力レイアウトを受け取るポインタ
main.cpp
	// 入力レイアウト作成
	hr = g_device->CreateInputLayout(
		layout, ARRAYSIZE(layout),
		vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(),
		&g_inputLayout
	);
	assert(SUCCEEDED(hr));

使い終わったシェーダーブロブを解放しておきます

main.cpp
	// Blob解放
	if (vsBlob) vsBlob->Release();
	if (psBlob) psBlob->Release();
	if (errorBlob) errorBlob->Release();

頂点の作成

次に、三角形の頂点を作成していきます。

main.cpp
// 三角形の頂点バッファを作成
void CreateTriangleVertexBuffer() {
	// NDC座標で画面中央の三角形
	Vertex vertices[3] = {
		{  0.0f,  0.5f, 0.0f, 1, 0, 0, 1 }, // 上:赤
		{  0.5f, -0.5f, 0.0f, 0, 1, 0, 1 }, // 右下:緑
		{ -0.5f, -0.5f, 0.0f, 0, 0, 1, 1 }, // 左下:青
	};
	// 頂点バッファ記述子
	D3D11_BUFFER_DESC bd = {};				// バッファ記述子
	bd.ByteWidth = UINT(sizeof(vertices));	// バッファサイズ
	bd.Usage = D3D11_USAGE_DEFAULT;			// デフォルト使用
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;// 頂点バッファとして使用
	// 初期データ設定
	D3D11_SUBRESOURCE_DATA initData = {};	// サブリソースデータ
	initData.pSysMem = vertices;			// 頂点データポインタ
	// 頂点バッファ作成
	HRESULT hr = g_device->CreateBuffer(&bd, &initData, &g_vertexBuffer);
	assert(SUCCEEDED(hr));
}

まずは頂点データを作成します。
型は先ほど宣言したVertexです。

main.cpp
// NDC座標で画面中央の三角形
	Vertex vertices[3] = {
		{  0.0f,  0.5f, 0.0f, 1, 0, 0, 1 }, // 上:赤
		{  0.5f, -0.5f, 0.0f, 0, 1, 0, 1 }, // 右下:緑
		{ -0.5f, -0.5f, 0.0f, 0, 0, 1, 1 }, // 左下:青
	};

次に、D3D11_BUFFER_DESC を作成して、頂点バッファの設定準備を行います。
メンバは以下の通りです。

メンバ 本記事の値 意味
ByteWidth UINT(sizeof(vertices)) バッファ全体のサイズ(頂点配列のサイズ分)。ここでは 3頂点分の構造体サイズ
Usage D3D11_USAGE_DEFAULT GPU が通常使用するバッファ。CPU から直接書き込まない前提で最も一般的
BindFlags D3D11_BIND_VERTEX_BUFFER このバッファを「頂点バッファ」として使うことを指定
CPUAccessFlags 0 CPU からアクセスしない(Usage が DEFAULT の場合は通常 0)
MiscFlags 0 特殊用途なし
StructureByteStride 0 構造化バッファで使う場合の要素サイズ。頂点バッファでは不要なので 0
main.cpp
// 頂点バッファ記述子
D3D11_BUFFER_DESC bd = {};				// バッファ記述子
bd.ByteWidth = UINT(sizeof(vertices));	// バッファサイズ
bd.Usage = D3D11_USAGE_DEFAULT;			// デフォルト使用
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;// 頂点バッファとして使用

D3D11_SUBRESOURCE_DATAは、頂点の初期データをGPUに渡すための設定です。
メンバは以下の通りです。

メンバ名 説明
pSysMem const void* 初期データのポインタ。バッファやテクスチャを作成するときに CPU 側のメモリを指定する
SysMemPitch UINT テクスチャ用。1行ごとのバイト数(頂点バッファでは不要なので 0)
SysMemSlicePitch UINT テクスチャ用。1スライスごとのバイト数(頂点バッファでは不要なので 0)
main.cpp
	// 初期データ設定
	D3D11_SUBRESOURCE_DATA initData = {};	// サブリソースデータ
	initData.pSysMem = vertices;			// 頂点データポインタ

準備ができたので、g_vertexBufferCreateBuffer()で頂点バッファを作成します。
引数は以下の通りです。

引数名 本記事の値 意味
pDesc &bd バッファの性質を定義する構造体。サイズや用途(頂点バッファ)を指定
pInitialData &initData 初期データを指定する構造体。ここでは vertices 配列を GPU にコピー
ppBuffer &g_vertexBuffer 作成された頂点バッファを受け取るポインタ
main.cpp
	// 頂点バッファ作成
	HRESULT hr = g_device->CreateBuffer(&bd, &initData, &g_vertexBuffer);

描画処理

次に、1フレーム分の描画処理を記述します。

main.cpp
// フレーム描画:三角形を表示
void RenderFrame() {
	// クリアカラー(暗めの紺)
	float clearColor[4] = { 0.05f, 0.05f, 0.08f, 1.0f };	// RGBA
	g_context->OMSetRenderTargets(1, &g_rtv, nullptr);		// レンダーターゲット設定
	g_context->ClearRenderTargetView(g_rtv, clearColor);	// 画面クリア

	// IA(Input Assembler)ステージにバッファ・レイアウト・トポロジを設定
	g_context->IASetInputLayout(g_inputLayout);		// 入力レイアウト設定
	UINT stride = sizeof(Vertex);					// 頂点サイズ
	UINT offset = 0;								// オフセット
	g_context->IASetVertexBuffers(0, 1, &g_vertexBuffer, &stride, &offset);		// 頂点バッファ設定
	g_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);	// トポロジ設定

	// シェーダーをバインド
	g_context->VSSetShader(g_vs, nullptr, 0);	// 頂点シェーダー設定
	g_context->PSSetShader(g_ps, nullptr, 0);	// ピクセルシェーダー設定

	// 3頂点を描画
	g_context->Draw(3, 0);		// 頂点数、開始頂点位置

	// 画面に反映
	g_swapChain->Present(1, 0); // 垂直同期ありで表示
}

g_contextIASetInputLayout()で、先ほど作成したg_inputLayoutをGPUに認識させます。

main.cpp
    // IA(Input Assembler)ステージにバッファ・レイアウト・トポロジを設定
	g_context->IASetInputLayout(g_inputLayout);		// 入力レイアウト設定

g_contextIASetVertexBuffers()で、頂点バッファをGPUに渡します。
引数は以下の通りです。

引数名 本記事の値 意味
StartSlot UINT 0 頂点バッファをバインドする開始スロット番号。通常は 0
NumBuffers UINT 1 バインドする頂点バッファの数。今回は 1 つ
ppVertexBuffers ID3D11Buffer* const* &g_vertexBuffer 頂点バッファのポインタ配列。ここでは 1 つだけ指定
pStrides const UINT* &stride (= sizeof(Vertex)) 1頂点あたりのサイズ(バイト数)。頂点構造体のサイズ
pOffsets const UINT* &offset (= 0) バッファ先頭からの読み取り開始位置。通常は 0

g_contextIASetPrimitiveTopology()トポロジを設定します。
トポロジとは、頂点をどうつなげて図形にするかのことです。
今回は、3頂点ごとに1枚の三角形として解釈を行う、D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELISTを引数にしています。

main.cpp
	g_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);	// トポロジ設定

次に、g_contextVSSetShaderPSSetShaderを使用して、作成したシェーダーパイプライン(頂点データから画像を作るまでの流れ)にセットします。

main.cpp
	// シェーダーをバインド
	g_context->VSSetShader(g_vs, nullptr, 0);	// 頂点シェーダー設定
	g_context->PSSetShader(g_ps, nullptr, 0);	// ピクセルシェーダー設定

g_contextDrawで、実際に描画を行います。
引数は以下の通りです。

引数名 本記事の値 意味
VertexCount UINT 3 描画する頂点数。今回は 3 頂点を使う
StartVertexLocation UINT 0 頂点バッファの先頭から読み始める位置
main.cpp
// 3頂点を描画
g_context->Draw(3, 0);

リソースを片付ける関数を作成しておきます。

main.cpp
// 後片付け(SAFE RELEASE)
void Cleanup() {
	if (g_vertexBuffer) { g_vertexBuffer->Release();  g_vertexBuffer = nullptr; }
	if (g_inputLayout) { g_inputLayout->Release();   g_inputLayout = nullptr; }
	if (g_vs) { g_vs->Release();            g_vs = nullptr; }
	if (g_ps) { g_ps->Release();            g_ps = nullptr; }
	if (g_rtv) { g_rtv->Release();           g_rtv = nullptr; }
	if (g_swapChain) { g_swapChain->Release();     g_swapChain = nullptr; }
	if (g_context) { g_context->Release();       g_context = nullptr; }
	if (g_device) { g_device->Release();        g_device = nullptr; }
}

エントリポイント

エントリポイントとなるwWinMain()を書きます。
特に目新しいこともないので、追加の解説は省略します。

main.cpp
// エントリポイント
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
	// 1) ウィンドウ作成
	const int kWidth = 800;
	const int kHeight = 600;
	g_hWnd = CreateSimpleWindow(hInstance, kWidth, kHeight);

	// 2) D3D11初期化
	InitD3D11(g_hWnd, kWidth, kHeight);

	// 3) シェーダー&入力レイアウト
	CreateShadersAndInputLayout();

	// 4) 頂点バッファ
	CreateTriangleVertexBuffer();

	// メッセージループ&レンダリング
	MSG msg = {};
	while (msg.message != WM_QUIT) {
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else {
			RenderFrame();
		}
	}

	Cleanup();
	return 0;
}

頂点シェーダー/Vertex Shader

頂点シェーダーとは、GPUの描画パイプラインにおいて頂点ごとの処理を行うプログラムです。
g_contextIASetVertexBuffers()から渡された頂点データをVSInputとして受け取り、座標変換などを行います。

TriangleVS.hlsl
//--------------------------------
//! @file TriangleVS.hlsl
//! @brief 三角形描画用頂点シェーダー
//--------------------------------

//--------------------------------
// 入力構造体
//--------------------------------
struct VSInput
{
	float3 position : POSITION; // 3D座標(今回はZ=0)
	float4 color : COLOR;		// 頂点カラー
};

//--------------------------------
// 出力構造体
//--------------------------------
struct VSOutput
{
	float4 position : SV_Position; // 画面に向けての最終位置
	float4 color : COLOR;		   // ピクセルへ渡すカラー
};

//--------------------------------
//! @brief 三角形描画用頂点シェーダー
//! @param IN [in] 頂点シェーダー入力
//! @return 出力構造体
//--------------------------------
VSOutput main(VSInput IN)
{
    VSOutput OUT;								// 出力構造体を宣言
    OUT.position = float4(IN.position, 1.0);	// 位置情報を設定
    OUT.color = IN.color;						// 頂点カラーを設定
    return OUT;                                 // 出力構造体を返す
}

まずは、VSInputとして、受け取る構造体をシェーダー側でも定義しておきます。
VSInputcpp側で設定したD3D11_INPUT_ELEMENT_DESC と順番が一致する必要があります。
また、:の後にあるものはセマンティクスといい、D3D11_INPUT_ELEMENT_DESCで設定した名前と一致する必要があります。

main.cpp
	// 入力レイアウト(POSITION: float3, COLOR: float4)
	D3D11_INPUT_ELEMENT_DESC layout[] = {
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,    0, 0,                          D3D11_INPUT_PER_VERTEX_DATA, 0 },
		{ "COLOR",    0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, UINT(3 * sizeof(float)),    D3D11_INPUT_PER_VERTEX_DATA, 0 },
	};
TriangleVS.hlsl
//--------------------------------
// 入力構造体
//--------------------------------
struct VSInput
{
	float3 position : POSITION; // 3D座標(今回はZ=0)
	float4 color : COLOR;		// 頂点カラー
};

VSOutputとして出力するデータの構造体を定義します。
この値はラスタライザーという、三角形をピクセルに分解する場所に渡され、ラスタライザーは後のピクセルシェーダーに値を渡します。
SV_Positionというセマンティクスは特殊で、これを定義しないと、コンパイルエラーになります。

TriangleVS.hlsl
//--------------------------------
// 出力構造体
//--------------------------------
struct VSOutput
{
	float4 position : SV_Position; // 画面に向けての最終位置
	float4 color : COLOR;		   // ピクセルへ渡すカラー
};

main()関数内では、受け取った値を加工します。
本記事では入力情報をそのまま出力しています。

TriangleVS.hlsl
//--------------------------------
//! @brief 三角形描画用頂点シェーダー
//! @param IN [in] 頂点シェーダー入力
//! @return 出力構造体
//--------------------------------
VSOutput main(VSInput IN)
{
    VSOutput OUT;								// 出力構造体を宣言
    OUT.position = float4(IN.position, 1.0);	// 位置情報を設定
    OUT.color = IN.color;						// 頂点カラーを設定
    return OUT;                                 // 出力構造体を返す
}

ピクセルシェーダー/Pixel Shader

ピクセルシェーダーとは、GPUの描画パイプラインにおいて各ピクセルの最終的な色や出力値を決定するプログラムです。

TrianglePS.hlsl
//------------------------------
//! @file TrianglePS.hlsl
//! @brief 三角形描画用ピクセルシェーダー
//------------------------------

//--------------------------------
// 頂点シェーダーから受け取った情報の構造体
//--------------------------------
struct VSOutput
{
    float4 position : SV_Position; // 画面に向けての最終位置 
    float4 color : COLOR;          // ピクセルへ渡すカラー
};

//--------------------------------
//! @brief 三角形描画用ピクセルシェーダー
//! @param IN [in] 頂点シェーダーからの出力
//! @return ピクセルカラー
//--------------------------------
float4 main(VSOutput IN) : SV_Target 
{
    return IN.color; // 補間された色が入ってくる
}

頂点シェーダーから受け取る情報を構造体として定義します。

TrianglePS.hlsl
//--------------------------------
// 頂点シェーダーから受け取った情報の構造体
//--------------------------------
struct VSOutput
{
    float4 position : SV_Position; // 画面に向けての最終位置 
    float4 color : COLOR;          // ピクセルへ渡すカラー
};

main()では頂点シェーダーからの出力を加工して、バックバッファに戻り値のピクセルを書き込みます。
引数リストの後に : セマンティクスとすることで、GPUパイプラインへどのように伝えるかを指定します。
SV_Targetは、戻り値がレンダーターゲットに書き込む色であることを意味します。

TrianglePS.hlsl
//--------------------------------
//! @brief 三角形描画用ピクセルシェーダー
//! @param IN [in] 頂点シェーダーからの出力
//! @return ピクセルカラー
//--------------------------------
float4 main(VSOutput IN) : SV_Target 
{
    return IN.color; // 補間された色が入ってくる
}

総括

  • 描画パイプラインとはGPUが頂点データから画面に表示されるピクセルへ変換する流れのこと。
  • パイプライン内の頂点シェーダーで、座標変換などを行う。
  • パイプライン内のピクセルシェーダーで、ピクセルごとの色を決める。
  • シェーダーコードhlslという言語で記述する。
  • hlslで書いたシェーダーc++側でコンパイルし、ID3D11DeviceContext*を通してパイプラインに登録を行う。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?