いまさらDirect3D11入門

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

ID3D11Bufferのバリエーション

定数バッファを使う際にID3D11Bufferを使いましたが、このクラスが表すことが出来るものにはまだ他にいくつかのバリエーションがあります。 ここではそれについて見ていきます。

ちなみにID3D11Bufferのことをバッファと呼んだりします。 以後、バッファと呼んでいきますので、出てきたときはID3D11Bufferのことだと思ってください。

概要

今パートでは球体を表示するシェーダと単純なデータの受け渡しを行うシェーダの2種類を元に様々なバッファのバリエーションについて見ていきます。 対応するプロジェクトはPart04_TypeOfBufferになります。

1.球体を描画するシェーダ

まず、比較するために定数バッファを使った球体描画シェーダのソースを見てみます。

// RenderSphereByConstantBuffer.hlsl
#include "Common.hlsli"
cbuffer Camera : register(b0)
{
  float4x4 cbInvProjection;
}
cbuffer Param : register(b1)
{
  float3 cbSpherePos;
  float cbSphereRange;
  float3 cbSphereColor;
  float pad;
};
RWTexture2D<float4> screen : register(u0);
[numthreads(1, 1, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  float2 screenSize;
  screen.GetDimensions(screenSize.x, screenSize.y);
  float4 rayDir = calRayDir(DTid, screenSize, cbInvProjection);
  //レイの方向に球体があるならその色を塗る
  screen[DTid] = calColor(rayDir.xyz, cbSpherePos, cbSphereRange, cbSphereColor);
}

シェーダ自体は単純なもので、球体を描画をする関数に定数バッファとして宣言したパラメータを渡しているだけです。 球体を描画している部分はCommon.hlsliで定義していますが、本筋ではないので省略します。

このパートのStructuredBufferByteAddressBufferはこのコードをベースにしています。 注目して見てもらいたい部分は定数バッファのParamの部分がどのように変わり、どのようにデータにアクセスしているかです。

2.StructuredBuffer

それではStructuredBufferについて見ていきましょう。 StructuredBufferは名前の通りCPU側の構造体をシェーダ内で直接読み込むことができるものになります。

シェーダ側

// RenderSphereByStructuredBuffer.hlsl
#include "Common.hlsli"
cbuffer Camera : register(b0)
{
  float4x4 cbInvProjection;
}
//sphereInfoの要素となる構造体定義
struct Param
{
  float3 pos;
  float range;
  float3 color;
  float pad;
};
//StructuredBufferはシェーダリソースビューとして設定する
StructuredBuffer<Param> sphereInfo : register(t0);
RWTexture2D<float4> screen : register(u0);
[numthreads(1, 1, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  float2 screenSize;
  screen.GetDimensions(screenSize.x, screenSize.y);
  float4 rayDir = calRayDir(DTid, screenSize, cbInvProjection);
  //レイの方向に球体があるならその色を塗る
  Param sphere = sphereInfo[0];
  screen[DTid] = calColor(rayDir.xyz, sphere.pos, sphere.range, sphere.color);
}

シェーダ内でも構造体を定義することができ、StructuredBufferの“<…>”には型名を指定してください。 データにアクセスするときは配列のように添え字を使ってアクセスします。 あとは、C++の構造体と同じように使います。

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

StructuredBufferはテクスチャと同じようにシェーダリソースビューとして扱われます。 なので、スロットの指定はテクスチャと同じで”register(t0)“みたいに行います。

CPU側

D3D11_BUFFER_DESC desc = {};
//StructuredBufferとして扱うときはMiscFlagにD3D11_RESOURCE_MISC_BUFFER_STRUCTUREDを指定する必要がある
desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
//構造体のサイズも一緒に設定すること
desc.StructureByteStride = sizeof(SphereInfo);
//後は他のバッファと同じ
desc.ByteWidth = sizeof(SphereInfo);
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;

//初期データの設定
SphereInfo info;
info.pos = DirectX::SimpleMath::Vector3(10, 0, 30.f);
info.range = 5.f;
info.color = DirectX::SimpleMath::Vector3(0.4f, 1.f, 0.4f);
D3D11_SUBRESOURCE_DATA initData;
initData.pSysMem = &info;
initData.SysMemPitch = sizeof(info);
//バッファ作成
auto hr = this->mpDevice->CreateBuffer(&desc, &initData, this->mpStructuredBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("構造化バッファの作成に失敗");
}
//シェーダリソースビューも作る必要がある
//また、Formatは必ずDXGI_FORMAT_UNKNOWNにしないといけない
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_UNKNOWN;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;
srvDesc.Buffer.FirstElement = 0;
srvDesc.Buffer.NumElements = 1;
// 次のコードでも設定出来る
//srvDesc.Buffer.ElementOffset = 0;
//srvDesc.Buffer.ElementWidth = 1;
// ViewDimensionにD3D11_SRV_DIMENSION_BUFFEREXを指定してもOK
//srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
//srvDesc.BufferEx.FirstElement = 0;
//srvDesc.BufferEx.Flags = 0;
//srvDesc.BufferEx.NumElements = 1;
hr = this->mpDevice->CreateShaderResourceView(this->mpStructuredBuffer.Get(), &srvDesc, this->mpStructuredBufferSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("構造化バッファのShaderResourceViewの作成に失敗");
}

定数バッファとの作成の違いはいくつか設定する項目が増えただけです。
ドキュメント: D3D11_BUFFER_DESC(日本語) (英語)

D3D11_BUFFER_DESCのメンバ一部

  • StructureByteStride

    構造体1個当たりのサイズを設定します。

  • MiscFlags

    様々なフラグを設定するときに使用します。 StructuredBufferを使う際はD3D11_RESOURCE_MISC_BUFFER_STRUCTUREDを指定する必要があります。
    ドキュメント: D3D11_RESOURCE_MISC_FLAG(日本語) D3D11_RESOURCE_MISC_FLAG(英語)

また、GPUに設定するときはシェーダリソースビューとして扱うためBindFlagsD3D11_BIND_SHADER_RESOURCEを設定する必要があります。

次にシェーダリソースビューを生成する際は、ViewDimensionD3D11_SRV_DIMENSION_BUFFERD3D11_SRV_DIMENSION_BUFFEREXを設定する必要があります。 各々設定したときのパラメータについてはサンプルを参考にしてください。 データを読み取り始めるオフセットと個数を設定する必要があります。 また、Formatには必ず、DXGI_FORMAT_UNKNOWNを設定する必要があります。

効率的なStructuredBufferの使い方はnVidia GameWorksのブログにて解説されています。 日本語訳もあった気がするのですが、見つからなかったので元記事のリンクを張っておきます。 端的に言うと構造体のサイズを16byteの倍数にすると効率よくデータにアクセスできるようです。
Understanding Structured Buffer Performance
Redundancy and Latency in Structured Buffer Use
How About Constant Buffers?

3.ByteAddressBuffer

シェーダ側

次に、ByteAddressBufferについて見ていきます。ByteAddressBufferは4バイト単位でデータを読み込むことが出来るものになります。

//RenderSphereByByteAddressBuffer.hlsl
#include "Common.hlsli"
cbuffer Camera : register(b0)
{
  float4x4 cbInvProjection;
}
//float3 pos;
//float range;
//float3 color;
#define POS (0 * 4)
#define RANGE (3 * 4)
#define COLOR (4 * 4)
ByteAddressBuffer sphereInfo : register(t0);
RWTexture2D<float4> screen : register(u0);
[numthreads(1, 1, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  float2 screenSize;
  screen.GetDimensions(screenSize.x, screenSize.y);
  float4 rayDir = calRayDir(DTid, screenSize, cbInvProjection);

  //レイの方向に球体があるならその色を塗る
  float3 spherePos = asfloat(sphereInfo.Load3(POS));
  float sphereRange = asfloat(sphereInfo.Load(RANGE));
  float3 color = asfloat(sphereInfo.Load3(COLOR));
  screen[DTid] = calColor(rayDir.xyz, spherePos, sphereRange, color);
}

ByteAddressBufferuintしか読み込むことしかできません。 なので、floatなど別の型を読み込みたい場合はasfloat関数などを利用する必要があります。 また、複数のuintを一度に読み込むことが出来る関数も用意されています。

構造体のようにデータを読み込むにはこちらで並びを意識する必要があるので手間がかかるものになります。 基本的にはStructuredBufferを使った方が手軽なのですが、頂点バッファというグラフィックスパイプラインで使うバッファをシェーダリソースビューとして扱う際はこれを使う必要が出てきます。 こちらのサイトでもByteAddressBufferを扱っているので参考にしてください。

ドキュメント:
ByteAddressBuffer (日本語) (英語)
asfloat (日本語) (英語)

CPU側

D3D11_BUFFER_DESC desc = {};
//ByteAddressBufferとして扱うときはMiscFlagにD3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSを指定する必要がある
desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
//後は他のバッファと同じ
desc.ByteWidth = sizeof(SphereInfo);
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
//初期データの設定
SphereInfo info;
info.pos = DirectX::SimpleMath::Vector3(-15, 0, 45.f);
info.range = 15.f;
info.color = DirectX::SimpleMath::Vector3(0.4f, 0.4f, 1.f);
D3D11_SUBRESOURCE_DATA initData;
initData.pSysMem = &info;
initData.SysMemPitch = sizeof(info);
auto hr = this->mpDevice->CreateBuffer(&desc, &initData, this->mpByteAddressBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("バイトアドレスバッファの作成に失敗");
}
//シェーダリソースビューの作成
//Formatは必ず、DXGI_FORMAT_R32_TYPELESSにする必要がある
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = DXGI_FORMAT_R32_TYPELESS;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
srvDesc.BufferEx.FirstElement = 0;
srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;
srvDesc.BufferEx.NumElements = sizeof(SphereInfo) / 4;
hr = this->mpDevice->CreateShaderResourceView(this->mpByteAddressBuffer.Get(), &srvDesc, this->mpByteAddressBufferSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("バイトアドレスバッファのShaderResourceViewの作成に失敗");
}

定数バッファとの違いは、MiscFlagsにD3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSを設定する必要があるだけです。

また、シェーダリソースビューを生成する場合、FormatにはDXGI_FORMAT_R32_TYPELESSを設定する必要があります。 ViewDimensionにはD3D11_SRV_DIMENSION_BUFFEREXを指定し、BufferEx.FlagsD3D11_BUFFEREX_SRV_FLAG_RAWを設定してください。

4.スタック操作を行うバッファ

DX11からシェーダ内でスタック操作を行うためにAppendStructuredBufferConsumeStructuredBufferが追加されました。 ここではそれらについて見ていきます。 なお、スタックへのプッシュとポップは同時にはできませんので、別のシェーダをそれぞれ行ってください。

プッシュ操作

// PushStack.hlsl
AppendStructuredBuffer<float4> stack : register(u0);
#define COLOR_COUNT 10
static const float4 tblColor[COLOR_COUNT] = {
  float4(1, 0, 0, 1),
  float4(0, 1, 0, 1),
  float4(0, 0, 1, 1),
  float4(1, 1, 0, 1),
  float4(1, 0, 1, 1),
  float4(0, 1, 1, 1),
  float4(0, 0, 0, 1),
  float4(1, 1, 1, 1),
  float4(1, 0.5f, 0, 1),
  float4(1, 0, 0.5f, 1),
};
[numthreads(1, 1, 1)]
void main( uint3 DTid : SV_DispatchThreadID )
{
  uint index = DTid.x % COLOR_COUNT;
  stack.Append(tblColor[index]);
}

ポップ操作

// PopStack.hlsl
ConsumeStructuredBuffer<float4> stack : register(u0);
RWStructuredBuffer<float4> buffer : register(u1);
[numthreads(1, 1, 1)]
void main( uint3 DTid : SV_DispatchThreadID )
{
  [branch] if (DTid.x < 10) {
    buffer[DTid.x] = stack.Consume();
  }
}

上2つのシェーダがやっていることはとても単純です。 GPU上で用意したデータをスタックに積んで、別のバッファに格納しているだけです。 ただGPU上では処理が並列に実行されているため、データの積まれる順番がどうなるかまでは制御できません。 なので、正しい順序が必要となる場合での使用は避けた方がいいでしょう。

スタック操作とは関係ありませんが、ポップ操作で使用してるRWStructuredBufferは書き込みができるStructuredBufferになります。

ドキュメント:
AppendStructuredBuffer (日本語) (英語)
ConsumeStructuredBuffer (日本語) (英語)
RWStructuredBuffer (日本語) (英語)

CPU側

//Scene::runStackBuffer関数 一部
//AppendStructuredBufferとConsumeStructuredBufferはStructuredBufferと同じ設定でバッファを作成する
D3D11_BUFFER_DESC desc = {};
desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
desc.ByteWidth = sizeof(Data) * count;//スタックの上限を決めている
desc.StructureByteStride = sizeof(Data);
desc.Usage = D3D11_USAGE_DEFAULT;
auto hr = this->mpDevice->CreateBuffer(&desc, nullptr, pStackBuffer.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("スタック操作用のバッファ作成に失敗");
}
//ビューは
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Format = DXGI_FORMAT_UNKNOWN;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = count;
uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_APPEND;
hr = this->mpDevice->CreateUnorderedAccessView(pStackBuffer.Get(), &uavDesc, pStackBufferUAV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("スタック操作用のバッファのUAVの作成に失敗");
}

AppendStructuredBufferConsumeStructuredBufferStructuredBufferと同じ設定でバッファを作成します。 ビューはアンオーダードアクセスビューになり、FormatにはDXGI_FORMAT_UNKNOWNを指定し、 Buffer.FlagsD3D11_BUFFER_UAV_FLAG_APPENDを指定してください。 後はビューは異なりますがStructuredBufferと同じ要領で設定します。

注意点として、シェーダ内でのスタック操作は可能ですが動的にメモリを確保するわけではありません。 作成したときに確保したメモリ量が上限となり、それを超えてプッシュしても追加されませんので注意してください。

スタック操作については以上になります。 サンプルコードのScene::runStackBuffer関数ではGPUからCPUへのデータの転送を行うコードもあるので一度目を通してください。

まとめ

今回はバッファのバリエーションについて見てきました。 ここで上げたもの以外にもまだありますが、似たような内容になるので省略します。

次回からは本題ともいえるグラフィックスパイプラインについて見ていきます。 グラフィックスパイプラインを使うとモデルなど三角形で表現されたものを自由に画面に描画できるようになりますが、 いろいろな決まりごとや裏で行っていることがあるので少しずつ説明していきたいと思います。

補足

レイトレースとラスタライズ法

今回のサンプルでは球体を描画するのに各画面のピクセルからカメラの方向に合ったレイを飛ばして球体と当たっていたら球体を描画するといった手順を踏んでいます。 このレイを飛ばして画面を描画する手法はレイトレースと呼ばれています。 レイトレースは映画やCGの研究などで使わており非常にリアルな絵を描画することができる手法です。 ただ、それ相応に重たい処理でもあるので、ゲームなどリアルタイムに描画する必要がある場合はラスタライズ法と呼ばれる手法が使われています。 ラスタライズ法では三角形や線分などのプリミティブ(英訳:Primitive)を使って物体を表現しており、複雑な物体を描画しようとするとそれだけ大量の三角形を描画する必要が出てきます。 GPUはこのラスタライズ法を高速に処理するための、つまり大量のプリミティブを高速に処理するために作られたハードウェアといえます。 これまで言葉だけが何回か出てきたグラフィックスパイプラインはこのラスタライズ法を行うための工程みたいなもので、ここまでの内容を踏まえますと、プリミティブを画面に描画するための工程だと言い換えることが出来るでしょう。

あと、今回のものはかなり粗末なものですが一応レイトレースと呼んでも差し支えないものになっていると思います。 レイトレースについては詳しくは知らないのですが、レイマーチングと呼ばれる手法を使うと手軽にレイトレースできるそうなので興味がある人は調べてみて下さい。
これがGPUの力!Three.jsによる“リアルタイム”なレイトレーシング
また、レイマーチングを使ったデモシーンが投稿されているサイトもあるようです。
shadertoy
このサイトではGLSLと呼ばれるシェーダ言語を使ったデモシーンを投稿できるサイトになります。 GLSLは文法自体はHLSLと似ているのでそこまで問題にはならないでしょう。
またレイマーチングをする際よく参考にされているサイトもあるので一度目を通してみてください。

<前 トップ 次>