いまさらDirect3D11入門

初めてグラフィックスAPIを触る人に向けて

テクスチャ

前2つのパートで何度か名前だけ出てきたテクスチャですが、このパートで簡単に見ていきます。 “簡単に”というのはテクスチャはID3D11Bufferとは違い特殊な機能があるのですが、すべて見ていくと長くなるので後で別パートと分けて見ていきたいと思います。

ここでは2次元配列のようなものである2次元テクスチャ(英訳:Texture2D)を使って、 ファイルからのデータの読み込みや、シェーダ内でのアクセス方法について説明していきます。

概要

このパートではテクスチャ(英訳:Texture)を使って画面をクリアする方法について見ていきます。 対応するプロジェクトはPart03_ClearScreenWithTextureになります。

  1. シェーダ内での使い方
    • Texture2Dキーワード
    • SamperStateキーワード
  2. GPUへの設定
  3. ID3D11Texture2D
    ID3D11Device::CreateTexture2D関数
  4. ID3D11SamplerState
    ID3D11Device::CreateDevice関数
  5. まとめ
  6. 補足
    • DirectXTKを使ったテクスチャの読み込み
    • テクスチャの種類
    • DXGI_FORMAT
    • シェーダ内での分岐命令

シェーダ内での使い方

Texture2Dキーワード

Texture2Dを使ったコードは下のようなものになります。

//ClearScreenWithTexture.hlsl
Texture2D<float4> image : register(t0);
RWTexture2D<float4> screen : register(u0);
[numthreads(1, 1, 1)]
void main(uint2 DTid : SV_DispatchThreadID) {
  uint2 image_size;
  image.GetDimensions(image_size.x, image_size.y);
  [branch]
  if (DTid.x < image_size.x && DTid.y < image_size.y) {
    screen[DTid] = image[DTid];
  } else {
    //imageの範囲外をクリアするときは黒で埋める
    screen[DTid] = float4(0, 0, 0, 1);
  }
}

Texture2Dは今まで出てきたRWTexture2Dとかなり似ています。 違いはデータの読み込みだけが可能なことと、読み込みのためにいくつかの関数が用意されていることです。 もちろん、上のコードのように添え字アクセス可能です。

ちなみにテクスチャの要素のことをテクセル(英訳:texel)と呼ばれたりしています。

読み込み関数にはサンプラステート(英訳:SamplerState)と呼ばれるものを使うものがあります。 サンプラステートについては次の項目で見ていきます。

ドキュメント:
テクスチャー オブジェクト(日本語)
Texture2D(日本語) (英語)

SamperStateキーワード

//ClearScreenWithSampler.hlsl
Texture2D<float4> image : register(t0);
SamplerState point_sampler : register(s0);
RWTexture2D<float4> screen : register(u0);
[numthreads(1, 1, 1)]
void main(uint2 DTid : SV_DispatchThreadID) {
  uint2 screen_size;
  screen.GetDimensions(screen_size.x, screen_size.y);
  float2 uv = (float2)DTid / (float2)screen_size;
  screen[DTid] = image.SampleLevel(point_sampler, uv, 0);
}

サンプラステートはテクスチャ読み込みの時に使うものになります。 上のサンプルの次の部分で実際に読み込んでいます。

//ClearScreenWithSampler.hlslの一部
float2 uv = (float2)DTid / (float2)screen_size;
screen[DTid] = image.SampleLevel(point_sampler, uv, 0);

Texture2D::SampleLevel関数でテクセルを読み込んでいます。
ドキュメント:SampleLevel (日本語) (英語)

SampleLevelの引数

  • 第1引数:使用するサンプラステート
  • 第2引数:サンプリングする場所を表すUV値
  • 第3引数:サンプリングするミップマップのレベル

    ミップマップについてはあとのパートで説明します。

サンプラステートを使ってテクスチャの要素を読み込むときはUV座標と呼ばれる座標系を使って読み込みます。 UV座標系はテクスチャのサイズを0~1の範囲で表したものになります。

例えば、横幅が200で縦幅が100のテクスチャの中央にあるテクセル(100,50)にアクセスしたい場合、 UV座標系では(0.5, 0.5)と表します。

//UV座標の計算の一例
//この例ではuvの値は{0.5f, 0.5f}になる
float2 textureSize = {200.f, 100.f};
float2 texel = {100.f, 50.f};
float2 uv = texel / textureSize;

UV座標系を使う際の利点はテクスチャのサイズが変わっても同じUVで同じテクセルの場所を指定できる点でしょうか。 これはTexture2Dキーワードのコードと上のコードを比較してもらえばお分かりいただけるかと思います。

始めに登場した添字を使ったコードの場合はテクスチャの範囲外にアクセスしないように条件分岐しています。 シェーダでは条件分岐はコストがCPUと比べてコストが高い処理になります。 現在ではそこまで問題にはなりませんが、昔だとシェーダ内では条件分岐自体が出来ないことが当たり前でした。 また、特に工夫をしない限り画面に合うようにテクスチャでクリアすることはできません。 工夫をしたとしてもUV座標の計算と同じ処理を書くことになり、テクスチャのサイズが小さければドットが目立ってしまいます。 この問題も隣接するテクセルを補間することで対応できますが、サンプラを使えばGPUのハードウェア機能を利用して隣接するテクセルを補間することが出来ます。 ただ、サンプラを使った場合はコストが添え字アクセスよりもかかりますので、添え字アクセスで十分な場合は添え字を使うなど場合によって使い分けてください。

また、UVの値が0~1の範囲を超えてしまった場合の動作はサンプラの作成時に決めることが可能です。 サンプルのデフォルトでは0~1範囲を超えてしまったらまた0~1の範囲で繰り返すようにサンプリングします。 これは実際に見てもらった方がわかりやすいと思いますので、サンプルコードを書き換えてみてください。

後、Texture2DSamplerStateのスロットの設定は定数バッファ、アンオーダードアクセスビューと同じです。 Texture2Dでは”register(t0)”と”t”を使い、 SamplerStateでは”register(s0)”と”s”を使ってください。

シェーダ側でのテクスチャの使い方は以上になります。 テクスチャを使うときはサンプラも一緒に使うことが多いで、セットで覚えていきましょう。

次はCPU側でのテクスチャとサンプラの設定の仕方を見ていきます。

GPUへの設定

//Scene::onRender関数の一部
//テクスチャの設定
std::array<ID3D11ShaderResourceView*, 1> ppSRVs = { { this->mpImageSRV.Get(), } };
this->mpImmediateContext->CSSetShaderResources(0, static_cast<UINT>(ppSRVs.size()), ppSRVs.data());
//サンプラの設定
std::array<ID3D11SamplerState*, 1> ppSamplers = { { this->mpSampler.Get(), } };
this->mpImmediateContext->CSSetSamplers(0, static_cast<UINT>(ppSamplers.size()), ppSamplers.data());

シェーダ内でTexture2Dを使うときはID3D11ShaderResourceViewとして扱い、 SamplerStateの場合はID3D11SamplerStateとして扱います。

GPUへの設定の仕方も今まで出てきた定数バッファやアンオーダードアクセスビューと同じです。 設定するものがID3D11ShaderResourceViewID3D11SamplerStateになっただけです。

ドキュメント:
ID3D11DeviceContext::CSSetShaderResources(日本語) ID3D11DeviceContext::CSSetShaderResources(英語)
ID3D11DeviceContext::CSSetSamplers(日本語) ID3D11DeviceContext::CSSetSamplers(英語)

シェーダへ設定できるものまとめ

シェーダへ設定できるものは今回出たもので一通り出てきました。

  • 定数バッファ (ID3D11Buffer)
  • シェーダリソースビュー (ID3D11ShaderResourceView)
  • サンプラステート (ID3D11SamplerState)
  • アンオーダードアクセスビュー (ID3D11UnorderedAccessView)
この内、シェーダリソースビューとアンオーダードアクセスビューを表すID3D11ShaderResourceViewID3D11UnorderedAccessViewID3D11Viewというクラスから派生したものになります。 DX11ではリソースをGPUで使う場合、ビューと呼ばれるGPUがリソースにどのようにアクセスするかを表しているものを介して設定します。 1つのリソースに複数のビューを作ることが可能で、例えばあるシェーダでデータを書き込んで別のシェーダでそのデータを使いたいといった場合は ID3D11ShaderResourceViewとID3D11UnorderedAccessViewを1個ずつ作る必要があります。 あるリソースがどのビューを作ることが出来るかは作成時にD3D11_BIND_FLAGを使って指定します。 リソースの作り方によっては同時に作ることが出来ないビューも出てきます。 その時は作成に失敗しますのでエラーメッセージを読んで対応してください。

定数バッファはビューを使わずに設定できますが、Direct3D12ではビューを持つように変わっています。 サンプラステートもリソースではないのでビューは必要ないです。

GPUに設定できるもので残っているものはグラフィックスパイプライン関連の設定やクエリ関連のものになりますので随時説明していきます。

ID3D11Texture2D

ここまでの内容でシェーダ内での使い方と設定の仕方がわかりました。 最後にTexture2Dを表すID3D11Texture2DID3D11ShaderResourceViewの作成は以下のコードになります。

//生成するID3D11Texture2Dの情報
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = 256;
desc.Height = 128;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.SampleDesc.Count = 1;
//画像データを適当に生成
//設定する値はdesc.Formatに合ったものにすること
std::vector<uint32_t> rawData;
rawData.resize(desc.Width * desc.Height);
for (auto y = 0; y < desc.Height; ++y) {
  for (auto x = 0; x < desc.Width; ++x) {
    auto index = y * desc.Width + x;
    if (index % 52 < 10) {
      rawData[index] = 0xff55ffff;
    } else {
      rawData[index] = 0xffff5555;
    }
  }
}
D3D11_SUBRESOURCE_DATA initData;
initData.pSysMem = rawData.data();
initData.SysMemPitch = sizeof(rawData[0]) * desc.Width;//1行当たりのデータ長
initData.SysMemSlicePitch = sizeof(rawData[0]) * desc.Width * desc.Height;//ここでは全体のサイズ
//テクスチャを表すID3D11Texture2Dの作成
auto hr = this->mpDevice->CreateTexture2D(&desc, &initData, this->mpTex2D.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11Textureの作成に失敗");
}
//シェーダで使うためのビュー作成
D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc = {};
viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
viewDesc.Format = desc.Format;
viewDesc.Texture2D.MipLevels = desc.MipLevels;
viewDesc.Texture2D.MostDetailedMip = 0;
hr = this->mpDevice->CreateShaderResourceView(this->mpTex2D.Get(), &viewDesc, this->mpTex2DSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11ShaderResourceViewの作成に失敗");
}

コードは長くなっていますが、ID3D11Bufferの作成と似ていますので理解はしやすいと思います。

まず、ID3D11Texture2DID3D11Device::CreateTexture2D関数で作成しています。

auto hr = this->mpDevice->CreateTexture2D(&desc, &initData, this->mpTex2D.GetAddressOf());

ドキュメント:ID3D11Device::CreateTexture2D (日本語) (英語)

ID3D11Device::CreateTexture2D

  • 第1引数:D3D11_TEXTURE2D_DESC

    作成するID3D11Texture2Dの情報を表すD3D11_TEXTURE2D_DESCを渡します。 上のコードで設定しているメンバが最低限ひつようとなるものです。
    ドキュメント: D3D11_TEXTURE2D_DESC(日本語) D3D11_TEXTURE2D_DESC(英語)

    D3D11_TEXTURE2D_DESCのメンバ一部

    • Width

      作成するテクスチャの横幅

    • Height

      作成するテクスチャの縦幅

    • BindFlags

      作成したいビューの指定。ここでは読み取りだけ行いたいのでD3D11_BIND_SHADER_RESOURCEを指定しています。
      ドキュメント: D3D11_BIND_FLAG(日本語) D3D11_BIND_FLAG(英語)

    • Format

      テクセルあたりのデータ形式。ここでは赤、緑、青、透明度を8bitで表すDXGI_FORMAT_R8G8B8A8_UNORMを指定しています。 末尾のUNORMについては補足を参照してください。
      ドキュメント: DXGI_FORMAT(日本語) DXGI_FORMAT(日本語)

    • MipLevels

      テクスチャの専用機能のうちの一つであるミップマップの数を指定します。 ミップマップが必要なくとも必ず、1以上を指定してください。 ミップマップについては別パートで説明します。

    • ArraySize

      テクスチャの専用機能のうちの一つである配列の数を指定します。 配列にしない場合は1を指定してください

    • SampleDesc.Count

      テクスチャの専用機能のうちの一つであるマルチサンプリングを使う際の1テクセル当たりのサブピクセル数を指定します。 マルチサンプリングを使わない場合は1を指定してください マルチサンプリングについては別パートで説明します。

  • 第2引数:D3D11_SUBRESOURCE_DATA

    初期データを表すものです。 定数バッファとの違いは1行当たりのデータ長をSysMemPitchに設定しているところと、 テクスチャ全体のデータ長をSysMemSlicePitchに設定している所です。

  • 第3引数:ID3D11Texture2D

    作成したID3D11Texture2Dを受け取る変数

ID3D11Texture2Dを作成した後はシェーダから読み込むためにID3D11ShaderResourceViewを作成します。

hr = this->mpDevice->CreateShaderResourceView(this->mpTex2D.Get(), &viewDesc, this->mpTex2DSRV.GetAddressOf());

ドキュメント:ID3D11Device::CreateShaderResourceView (日本語) (英語)

ID3D11Device::CreateShaderResourceView

  • 第1引数:ID3D11Resource

    ビューを作成したいID3D11Resourceを渡します。 ID3D11ResourceはID3D11Texture2DやID3D11Bufferの親クラスになりますので、直接ID3D11Texture2Dを渡すことが出来ます。

  • 第2引数:D3D11_SHADER_RESOURCE_VIEW_DESC

    作成するビューの情報を設定したD3D11_SHADER_RESOURCE_VIEW_DESCを渡します。 設定できる項目はリソースによって変わりますので、ここでは共通部分だけ説明します。
    ドキュメント: D3D11_SHADER_RESOURCE_VIEW_DESC(日本語) D3D11_SHADER_RESOURCE_VIEW_DESC(英語)

    D3D11_SHADER_RESOURCE_VIEW_DESCのメンバ一部

    1. Format: GPU上のデータのフォーマット

      基本的には作成したリソースと同じDXGI_FORMATを設定します。 いくつかのDXGI_FORMATは互換性を持っているのでその時は使いたいフォーマットをここで設定します。

    2. ViewDimension: リソースタイプの指定

      ここで設定したタイプによって設定する項目が変わります。 サンプルではD3D11_SRV_DIMENSION_TEXTURE2Dを指定しているのでTexture2Dメンバを設定しています。

  • 第3引数:ID3D11ShaderResourceView

    作成したID3D11ShaderResourceViewを受け取る変数

以上、長くなりましたがID3D11Texture2DID3D11ShaderResourceViewの作成は以上になります。 設定する項目が多いだけで行っていることは簡単ですので、すぐ慣れることでしょう。

ID3D11SamplerState

SamplerStateを表すID3D11SamplerStateの作成は以下のコードになります。

//サンプラステートの作成
D3D11_SAMPLER_DESC desc = {};
desc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
auto hr = this->mpDevice->CreateSamplerState(&desc, this->mpSampler.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ポイントサンプラーの作成に失敗");
}

上のコードのID3D11Device::CreateSamplerState関数でID3D11SamplerStateを作成しています。

ドキュメント:ID3D11Device::CreateSamplerState (日本語) (英語)

ID3D11Device::CreateSamplerState

  • 第1引数:D3D11_SAMPLER_DESC

    作成するID3D11SamplerStateの情報を表すD3D11_SAMPLER_DESCを渡します。 重要となるのは上のコードで使っているメンバになります。
    ドキュメント: D3D11_SAMPLER_DESC(日本語) D3D11_SAMPLER_DESC(英語)

    D3D11_SAMPLER_DESCのメンバ一部

    • Filter: D3D11_FILTER

      サンプリングするときの補間の仕方を決めるためのものです。 テクスチャを拡大か縮小するときとミップマップをサンプリングするときの3パターンの補間のやり方を指定します。

    • AddressU, AddressV, AddressW: D3D11_TEXTURE_ADDRESS_MODE

      サンプリングの時UVの値が0~1の範囲外だった時どうするかを指定するためのものです。 Uは横、Vは縦、Wは奥行きに対応しています。 画像の横方向は繰り返すけど、縦方向は範囲外になったら指定した色で塗りつぶすといった設定が可能です。

  • 第2引数:ID3D11SamplerState

    作成したID3D11SamplerStateを受け取る変数

以上でサンプラステートの作成を終わります。

まとめ

このパートではテクスチャとそれからデータを読み取るためのサンプラについて見てきました。

この3パートでDX11で使う主要なものは大体触ってきました。 C++でいう関数的な役割のシェーダと変数であるID3D11Bufferとテクスチャの使い方がわかればDX11を使う上でもう悩むこともないでしょう。

あとここまでのパートでいくつかのリソースなどを作成してきましたが、大まかにパターンが決まっているのに気づきましたか? DX11では何か作るときはD3D11_XXX_DESCみたいな構造体を使うことが多いです。 その構造体について調べれば何ができるかが大体わかりますので是非ドキュメントを読んでいってください。

補足

DirectXTKを使ったテクスチャの読み込み

上ではID3D11Texture2Dをプログラム上で直接生成していましたが、ペイントなど他のツール上で生成した画像ファイルから読み込むことが多いです。 DX11だけだとファイルから画像データを読み込むといった関数は用意されていませんが、Microsoftが提供しているDirectXTKを使用することで読み込むことが出来ます。

Microsoft::WRL::ComPtr<ID3D11Resource> pTex2D;
auto hr = DirectX::CreateWICTextureFromFile(this->mpDevice.Get(), L"image.png", &pTex2D, this->mpImageSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("画面クリア用の画像の読み込みに失敗");
}
hr = pTex2D.Get()->QueryInterface<ID3D11Texture2D>(this->mpImage.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ID3D11ResourceからID3D11Texture2Dへの変換に失敗");
}
    

DirectX::CreateWICTextureFromFile関数はpngなど様々な画像ファイルを読み込みID3D11ResourceID3D11ShaderResourceViewを生成してくれます。 ID3D11ResourceID3D11Texture2Dの親クラスになります。 ID3D11ResourceからID3D11Texture2Dに変換するには上のコードのようにQueryInterface関数を使用してください。

テクスチャの種類

今回はID3D11Texture2Dを使いましたが、 テクスチャには他にID3D11Texture1DID3D11Texture3Dがあります。 簡単に言うと1次元配列と3次元配列みたいなものです。 これらもテクスチャなのでID3D11Texture2Dと同じことができます。
ドキュメント:
ID3D11Texture1D (日本語) (英語)
ID3D11Texture3D (日本語) (英語)

DXGI_FORMAT

ID3D11Texture2Dを作成するときフォーマットを指定する必要がありました。 DX11ではDXGI_FORMATを使ってフォーマットを指定します。 種類がたくさんありますが、1要素の成分数とそのビット幅をR,G,B,Aとその隣の数値で表し、末尾に型を指定します。 末尾の型にはFLOAT,UINT,INTがありシェーダ内では文字通りfloat,uint,intを表します。 TYPELESSはビット幅だけを指定するものになり、その際ビューで改めて型を指定します。当然互換性のあるフォーマットしか指定できません。 SNORMUNORMはそれぞれ符号付と符号なしの整数を表し、シェーダ内で使うときは数値は-1~1と0~1に正規化されます。

//例)8ビットの成分を4つ持つ符号なし正規化整数
// データに0x80ff00ffを渡すとシェーダ内ではfloat4(1, 0, 1, 約0.5)になる
DXGI_FORMAT format = DXGI_FORMAT_R8G8B8A8_UNORM;
uint32_t data = 0xffffffff;
    

その他、圧縮形式を表すものがあります。 圧縮形式についてはこちらを参考にしてください。
ドキュメント DXGI_FORMAT(日本語) DXGI_FORMAT(英語)

シェーダ内での分岐命令

シェーダ内でif文を使う際、ifキーワードの前にいくつかの属性を設定できます。 今パートではbranch属性を使いました。この属性はif文に渡された条件に応じて片方のコードしか実行しないように指定するものです。 詳しくはドキュメントを読んでください。
ドキュメント: if ステートメント(日本語) if Statement(英語)

また同じようにforやwhileも属性を指定できます。 こちらも詳細はドキュメントをご覧ください
ドキュメント: フロー制御(日本語) Flow Control(英語)

<前 トップ 次>