いまさらDirect3D11入門

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

テクスチャ専用機能

今パートではテクスチャの専用機能について見ていきます。 サンプラによるデータの読み込みもバッファとの違いになりますが、テクスチャの配列を作れたり、ミップマップと呼ばれる低解像度のデータを1つのテクスチャにまとめて使えたりとデータ構造もバッファとは違ってきます。 3DCGではこの機能を活かした技法もありますので、簡単な使い方を覚えておくと理解が早くなるのではないでしょうか。

ドキュメント:Direct3D 11 のテクスクチャーの概要 (日本語) (英語)

概要

それではテクスチャの専用機能について見ていきましょう。 対応するプロジェクトはPart16_Texture2になります。

ミップマップ

まず、ミップマップについて見ていきます。

ミップマップは元となるテクスチャのサイズを半分に縮小したものになります。 普通のテクスチャと比べますとメモリ量が増えますが、テクスチャを縮小してサンプリングするときにはより自然な見た目になったり、ミップマップの個々のテクスチャは解像度が小さくなるのでメモリアクセスが速度が速くなったりします。 詳しい説明はWikipedeiaこちらのドキュメントを参照してください。

ここではミップマップを持つテクスチャの作成と書き込み、読み込みについて説明していきます。

作成

ミップマップを持つテクスチャを作るときはD3D11_TEXTURE2D_DESCMipLevelsに必要となるミップマップの数を指定します。 その他は今までのテクスチャの作成と同じです。 ちなみにミップマップの各要素をミップレベルと呼びます。

GPUに設定するときもシェーダリソースビューやアンオーダードアクセスビューを使用します。 ただし、アンオーダードアクセスビューを使うときはミップレベルの数分用意する必要があるので忘れないで下さい。 シェーダリソースビューは1つのビューで全ミップレベルにアクセスできますが、作成時の設定で1つのミップマップにしかアクセス出来ないようにしたり、最大レベルを指定したり出来ます。

// Scene::initMipmap関数の一部
//テクスチャの作成
auto desc = makeTex2DDesc(this->width(), this->height(), DXGI_FORMAT_R8G8B8A8_UNORM);
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;
//ミップレベルを最大を指定している。ミップマップを使わない時は1を必ず指定すること
desc.MipLevels = calMaxMipLevel(desc.Width, desc.Height);
// 下のコードはID3D11DeviceContext::GenerateMipsを使ってミップマップを自動生成するときに使う
// desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;
auto hr = this->mpDevice->CreateTexture2D(&desc, nullptr, this->mMipmap.mpResource.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ミップマップ用のテクスチャ作成に失敗");
}
//シェーダリソースビューの場合は1つで全ミップレベルにアクセスできるが、設定で1つのミップレベルのみだけにアクセスできる
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Format = desc.Format;
srvDesc.Texture2D.MipLevels = desc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
hr = this->mpDevice->CreateShaderResourceView(this->mMipmap.mpResource.Get(), &srvDesc, this->mMipmap.mpSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("ミップマップ用のシェーダリソースビュー作成に失敗");
}
//各ミップレベルごとのアンオーダードアクセスビューを作成する必要がある
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
uavDesc.Format = desc.Format;
this->mMipmap.mpUAVs.reserve(desc.MipLevels);
for (UINT level = 0; level < desc.MipLevels; ++level) {
  uavDesc.Texture2D.MipSlice = level;
  ID3D11UnorderedAccessView* pUAV = nullptr;
  hr = this->mpDevice->CreateUnorderedAccessView(this->mMipmap.mpResource.Get(), &uavDesc, &pUAV);
  if (FAILED(hr)) {
    throw std::runtime_error("ミップマップ用のアンオーダードアクセスビュー作成に失敗");
  }
  this->mMipmap.mpUAVs.emplace_back(pUAV);
  pUAV->Release();
}

ID3D11DeviceContext::GenerateMips

サンプルではアンオーダードアクセスビューを使って各ミップレベルの内容を書き込んでいました。 ですが、ID3D11DeviceContext::GenerateMipsを使用すると、最上位のミップレベルの内容をもとに下層のミップレベルを自動生成することが出来ます。 機能レベルによっては対応していないフォーマットがあるので注意してください。 ID3D11DeviceContext::GenerateMipsを使用するときはBindFlagsD3D11_BIND_RENDER_TARGETD3D11_BIND_SHADER_RESOURCEを指定した上でMiscFlagsにD3D11_RESOURCE_MISC_GENERATE_MIPSを設定する必要があります。

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

書き込み

各ミップレベルへの書き込みを行うシェーダは以下のようになります。 と言いましても、今までのRWTexture2Dと変わりません。 GPUへの設定も変わりません。書き込みたいミップレベルのアンオーダードアクセスビューを設定するのを忘れないようにするだけです。

/// CSMipmapWrite.hlslの一部
cbuffer Param :register(b0)
{
  float4 cbBaseColor;
}
RWTexture2D<float4> output : register(u0);
float noise_(float x, float y);
[numthreads(8, 8, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  uint2 image_size;
  output.GetDimensions(image_size.x, image_size.y);
  [branch]
  if(DTid.x == 0) {
    output[DTid] = float4(1, 0, 0, 1);
  } else if(DTid.y == 0) {
    output[DTid] = float4(0, 1, 0, 1);
  } else if(DTid.x+1 == image_size.x) {
    output[DTid] = float4(0, 1, 1, 1);
  } else if(DTid.y+1 == image_size.y) {
    output[DTid] = float4(1, 0, 1, 1);
  } else if(DTid.x < image_size.x && DTid.y < image_size.y) {
    float2 pos = DTid / (float2)image_size;
    pos += dot(cbBaseColor, float4(1.2f, 4.5f, 10.9f, 3.01f));
    pos = pos * 25;
    float factor = (noise_(pos.x, pos.y) * 0.5f + 0.5f);
    output[DTid] = cbBaseColor * (1 + sin((factor / 2) * 50.f)) / 2.f;
  }
}

読み込み

ミップマップの読み込みは少し異なります。

サンプラを使う場合はSampleLevelの第3引数に読み込みたいミップレベルを指定します。 添字を使った場合はmips.Operator[][]を使用します。 使い方は下のコードを参考にしてください。

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

// CSMipmapRead.hlsl
cbuffer Param :register(b0)
{
  uint cbMipLevel;
  bool cbIsUseSampler;
  uint2 pad;
}
Texture2D<float4> image : register(t0);
SamplerState linear_sampler : register(s0);
RWTexture2D<float4> screen : register(u0);
[numthreads(8, 8, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  uint2 image_size;
  uint arrayCount;
  uint numMipLevels;
  image.GetDimensions(cbMipLevel, image_size.x, image_size.y, numMipLevels);

  [branch]
  if (cbIsUseSampler) {
    //サンプラを使ったアクセス
    uint2 screen_size;
    screen.GetDimensions(screen_size.x, screen_size.y);
    float2 uv = (float2)DTid / (float2)screen_size * 4.f;
    screen[DTid] = image.SampleLevel(linear_sampler, uv, cbMipLevel);
  } else {
    //添字を使ったアクセス
    [branch]
    if (DTid.x < image_size.x && DTid.y < image_size.y) {
      screen[DTid] = image.mips[min(cbMipLevel, numMipLevels-1)][DTid];
    } else {
      screen[DTid] = float4(0, 0, 0, 1);
    }
  }
}

配列

次は配列について見ていきます。 この配列は文字通りの配列で、テクスチャは配列を作ることが出来ます。

作成

配列を持つテクスチャを作るにはD3D11_TEXTURE2D_DESCArraySizeに配列の要素数を指定するだけできます。 配列でないテクスチャを作るときは必ず1を指定してください。

後、ミップマップと異なりアンオーダードアクセスビューは1つで全要素にアクセスできます。 シェーダリソースビューはミップマップと同じで1つで全要素にアクセス可能です。

補足ですが、配列とミップマップは同時作成することが可能ですので必要に応じて設定を行ってください。

// Scene::initArrayの一部
auto desc = makeTex2DDesc(this->width(), this->height(), DXGI_FORMAT_R8G8B8A8_UNORM);
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS;
desc.ArraySize = arrayLength;
auto hr = this->mpDevice->CreateTexture2D(&desc, nullptr, this->mArray.mpResource.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("テクスチャ配列用のテクスチャ作成に失敗");
}
//シェーダリソースビューは1つで全要素にアクセスできる
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
srvDesc.Format = desc.Format;
srvDesc.Texture2DArray.MipLevels = desc.MipLevels;
srvDesc.Texture2DArray.MostDetailedMip = 0;
srvDesc.Texture2DArray.FirstArraySlice = 0;
srvDesc.Texture2DArray.ArraySize = desc.ArraySize;
hr = this->mpDevice->CreateShaderResourceView(this->mArray.mpResource.Get(), &srvDesc, this->mArray.mpSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("テクスチャ配列用のシェーダリソースビュー作成に失敗");
}
//テクスチャ配列は1つのUnorderedAccessViewで全要素にアクセス可能(ミップマップは固定されるので注意)
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2DARRAY;
uavDesc.Format = desc.Format;
uavDesc.Texture2DArray.MipSlice = 0;
uavDesc.Texture2DArray.FirstArraySlice = 0;
uavDesc.Texture2DArray.ArraySize = desc.ArraySize;
ID3D11UnorderedAccessView* pUAV = nullptr;
hr = this->mpDevice->CreateUnorderedAccessView(this->mArray.mpResource.Get(), &uavDesc, &pUAV);
if (FAILED(hr)) {
  throw std::runtime_error("テクスチャ配列用のアンオーダードアクセスビュー作成に失敗");
}
this->mArray.mpUAVs.emplace_back(pUAV);
pUAV->Release();

書き込み

配列に書き込む際は座標に加えて書き込み先の要素の添字を指定します。

例えばRWTexture2Dの場合ならuint3を使ってアクセスします。 uint3.xyでテクスチャの座標を、uint3.zで配列の添字を指定します。

cbuffer Param :register(b0)
{
  float4 cbBaseColor;
  uint cbArrayIndex;
  float3 pad;
}
RWTexture2DArray<float4> output : register(u0);
float noise_(float x, float y);
[numthreads(8, 8, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  uint2 image_size;
  uint arrayLength;
  output.GetDimensions(image_size.x, image_size.y, arrayLength);
  uint3 outputIndex = uint3(DTid, cbArrayIndex);
  [branch]
  if (DTid.x < image_size.x && DTid.y < image_size.y) {
    float2 pos = DTid / (float2)image_size;
    pos += dot(cbBaseColor, float4(1.2f, 4.5f, 10.9f, 3.01f));
    pos = pos * 25;
    float factor = (noise_(pos.x, pos.y) * 0.5f + 0.5f);
    output[outputIndex] = cbBaseColor * (1 + sin(pos.x + factor / 0.25f)) / 2.f;
  }
}

読み込み

配列の読み込みも書き込みと同じく、座標と配列の添字を指定することでアクセスできます。

// CSArrayRead.hlsl
cbuffer Param :register(b0)
{
  uint cbArrayIndex;
  bool cbIsUseSampler;
  uint2 pad;
}
Texture2DArray<float4> image : register(t0);
SamplerState linear_sampler : register(s0);
RWTexture2D<float4> screen : register(u0);
[numthreads(8, 8, 1)]
void main(uint2 DTid : SV_DispatchThreadID)
{
  uint2 image_size;
  uint arrayCount;
  uint arrayLength;
  image.GetDimensions(image_size.x, image_size.y, arrayLength);
  [branch]
  if (cbIsUseSampler) {
    uint2 screen_size;
    screen.GetDimensions(screen_size.x, screen_size.y);
    float3 uv_and_index = float3((float2)DTid / (float2)screen_size, cbArrayIndex);
    screen[DTid] = image.SampleLevel(linear_sampler, uv_and_index, 0);
  } else {
    [branch]
    if (DTid.x < image_size.x && DTid.y < image_size.y) {
      screen[DTid] = image[uint3(DTid, min(cbArrayIndex, arrayLength - 1))];
      //ミップレベルを指定したアクセス
      //screen[DTid] = image.mip[mipLevel][uint3(DTid, min(cbArrayIndex, arrayLength - 1))];
    } else {
      screen[DTid] = float4(0, 0, 0, 1);
    }
  }
}

キューブマップ

次はキューブマップについて見ていきます。

キューブマップは環境マッピングという鏡などの映り込みや周囲の環境光を再現するときによく使われるものです。 キューブマップ自体のデータ構造はテクスチャ配列と変わりません。ですがデータの並びに意味があります。 0番目はX軸方向の環境を,1番目は-X軸方向の環境,2番目はy軸方向の環境を…といった感じです。

もちろんキューブマップも配列やミップマップを持つことができます。

作成

キューブマップを作成するときはArraySizeに6を指定します。 配列の場合は6の倍数になるようにします。

// Scene::initCubemap関数
auto desc = makeTex2DDesc(1024, 1024, DXGI_FORMAT_R8G8B8A8_UNORM);
desc.ArraySize = 6;//必ず、6の倍数にすること
// キューブマップとして扱うときはD3D11_RESOURCE_MISC_TEXTURECUBEを指定する必要がある
desc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
auto hr = this->mpDevice->CreateTexture2D(&desc, nullptr, this->mCubemap.mpResource.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("キューブマップの生成に失敗");
}
//シェーダリソースビューの作成
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
srvDesc.Format = desc.Format;
srvDesc.TextureCube.MipLevels = desc.MipLevels;
srvDesc.TextureCube.MostDetailedMip = 0;
hr = this->mpDevice->CreateShaderResourceView(this->mCubemap.mpResource.Get(), &srvDesc, this->mCubemap.mpSRV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("キューブマップ用のシェーダリソースビュー作成に失敗");
}
//レンダーターゲットビューの作成
D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
rtvDesc.Format = desc.Format;
rtvDesc.Texture2DArray.ArraySize = 6;
rtvDesc.Texture2DArray.FirstArraySlice = 0;
rtvDesc.Texture2DArray.MipSlice = 0;
hr = this->mpDevice->CreateRenderTargetView(this->mCubemap.mpResource.Get(), &rtvDesc, this->mCubemap.mpRTV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("キューブマップ用のレンダーターゲットビュー作成に失敗");
}

書き込み

書き込みにはアンオーダードアクセスビューを使うことも出来ますが、サンプルではグラフィックスパイプラインを利用して書き込んでいます。 グラフィックスパイプラインを利用することでリアルタイムに周囲の環境を保存することができます。 ただ、それだと処理負荷が高くなるので、事前に描画した結果をファイルに保存し、実行時にそれを読み込むといった使い方もあります。

どちらにせよ、グラフィックスパイプラインを利用すると楽に周囲の環境を書き込むことが出来ます。

// GSRenderCubemap.hlslの一部
struct GSOutput
{
  float4 pos : SV_POSITION;
  float4 color : TEXCOORD0;
  uint arrayIndex : SV_RenderTargetArrayIndex;
};
[maxvertexcount(3 * 6)]
void main(
  point Dummy input[1],
  inout TriangleStream< GSOutput > output
)
{
  //6面分の三角形を生成している
  [unroll] for (uint n = 0; n < 6; ++n) {
    [unroll] for (uint i = 0; i < 3; i++) {
      GSOutput element;
      element.pos = gPosTable[i];
      element.color = gColorTable[n];
      //どの要素に書き込むか指定している
      element.arrayIndex = n;
      output.Append(element);
    }
    output.RestartStrip();
  }
}

上のSV_RenderTargetArrayIndexがもっとも重要な部分になります。 レンダーターゲットにはテクスチャ配列を指定することが出来ますが、何もしなければ0番目の要素にしか書き込みができません。 それを防ぐためにSV_RenderTargetArrayIndexを指定した変数に書き込みたい要素の添字を指定します。

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

読み込み

キューブマップの読み込みは他のものとは大きく異なります。 読み込みはサンプラのみで、UV座標ではなく方向を表すfloat3でサンプリングする場所を指定します。 このことからキューブマップを環境マッピングを行うのに特化したものといえるでしょう。

// CSUSeCubemap.hlslの一部
TextureCube<float4> cubemap : register(t0);
SamplerState linear_sampler : register(s0);
RWTexture2D<float4> screen : register(u0);
[numthreads(8, 8, 1)]
void main( uint3 DTid : SV_DispatchThreadID )
{
  float2 screenSize;
  screen.GetDimensions(screenSize.x, screenSize.y);
  float4 rayDir = calRayDir(DTid, screenSize, cbInvProjection);
  //レイの方向に球体があるならその色を塗る
  const float3 spherePos = float3(0, 0, 30.f);
  const float sphereRange = 10.f;
  float3 p = rayDir.xyz * dot(rayDir.xyz, spherePos);
  float len = distance(p, spherePos);
  [branch] if (len < sphereRange) {
    //球体の法線を求める
    // ... 省略
    float3 reflectDir = reflect(N, rayDir.xyz);
    //ここでキューブマップからサンプリングしている
    screen[DTid.xy] = cubemap.SampleLevel(linear_sampler, reflectDir, 0);
  } else {
    screen[DTid.xy] = float4(0, 0.125f, 0.2f, 1);
  }
}

マルチサンプリング

次はマルチサンプリングテクスチャです。

マルチサンプリングはMSAAというアンチエイリアスの1手法で使われるもので、ポリゴンのギザギザを滑らかにするために使われます。

マルチサンプリングテクスチャはテクセル1つにつき複数のサンプリングポイントを内部に持つものになります。 サンプリングポイントはテクセル内全体にまんべんなく配置されていて、ラスタライズの際にサンプリングポイントの1つでもプリミティブの内部にあるなら描画されます。

作成

マルチサンプリングテクスチャの作るにはD3D11_TEXTURE2D_DESCSampleDescを使用します。 SampleDescはメンバにCount(サンプリング数)Quality(品質)の2つ持っています。 QualityはGPUによっては使用できる値が異なりますので、必ずID3D11Device::CheckMultisampleQualityLevelsで使えるか確認してください。

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

// Scene::initMultisamplingの一部
auto desc = makeTex2DDesc(this->width(), this->height(), DXGI_FORMAT_R8G8B8A8_UNORM);
desc.BindFlags = D3D11_BIND_RENDER_TARGET;
//GPUによっては使える設定が異なるので、希望するサンプリングポイント数を指定し確認している。
desc.SampleDesc.Count = samplingCount;
this->mpDevice->CheckMultisampleQualityLevels(desc.Format, desc.SampleDesc.Count, &desc.SampleDesc.Quality);
if (0 == desc.SampleDesc.Quality) {
  throw std::runtime_error("指定したフォーマットとサンプリング数はデバイスが対応していません");
}
desc.SampleDesc.Quality -= 1;//そのまま使うと上限を超えるので1引いている
auto hr = this->mpDevice->CreateTexture2D(&desc, nullptr, this->mMultiSampling.mpResource.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("マルチサンプリング生成に失敗");
}
//ビューの作成
D3D11_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DMS;
rtvDesc.Format = desc.Format;
hr = this->mpDevice->CreateRenderTargetView(this->mMultiSampling.mpResource.Get(), &rtvDesc, this->mMultiSampling.mpRTV.GetAddressOf());
if (FAILED(hr)) {
  throw std::runtime_error("マルチサンプリング用のレンダーターゲットビュー作成に失敗");
}

作成後、アンチエイリアスを適応するにはID3D11DeviceContext::ResolveSubresourceを使用し他のマルチサンプリングではないテクスチャへコピーを行います。

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

// Scene::runMultisamplingの一部
D3D11_TEXTURE2D_DESC desc;
this->mMultiSampling.mpResource->GetDesc(&desc);
this->mpImmediateContext->ResolveSubresource(this->mpBackBuffer.Get(), 0, this->mMultiSampling.mpResource.Get(), 0, desc.Format);

まとめ

テクスチャの専用機能については以上です。 バッファかテクスチャどちらを使うかはこれらの機能を見て決めるといいでしょう。

補足

サブリソースの添字について

ID3D11DeviceContextのコピー関数のいくつかやマップ関数にサブリソースを指定する引数がありました。 サブリソースはメインのリソースと今回見てきたミップマップや配列の各要素を指します。 サブリソースも添字でアクセスし、その並び順はミップマップを優先したものとなりますので使うときは注意してください。

//ミップレベル3,要素数が2の配列を持つテクスチャのサブリソースの添字一覧
int miplevel0_array0 = 0;
int miplevel1_array0 = 1;
int miplevel2_array0 = 2;
int miplevel0_array1 = 3;
int miplevel1_array1 = 4;
int miplevel2_array1 = 5;
    

<前 トップ 次>