いまさらDirect3D11入門

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

テッセレーション

今回はテッセレーションについて見ていきます。 テッセレーションはDX11から導入されたシェーダでシェーダモデル5.0以上に対応しているGPUで動作します。 これを利用することで1つの三角形や線分、四角形をより細かく分割することが出来ます。 この機能はディスプレイスメントマッピング(英訳:Displacement Mapping)やカメラからの距離に応じてポリゴンの数を調節するLevel of Detail(略称:LOD)などに利用されているようです。

テッセレーションは頂点シェーダの後に実行される3つのステージから構成されており、ハルシェーダとドメインシェーダの2つのシェーダとテッセレータステージのGPUの固定機能に分けることが出来ます。 ハルシェーダでテッセレータステージがどのようにプリミティブを分割するかを決め、ドメインシェーダで分割した頂点の移動などの処理を行います。 GPUの固定機能を使用しているためシステムセマンティクスの意味を理解することがテッセレーションの理解につながります。

参考サイト:
ドキュメント テッセレーションの概要 (日本語)(英語)
「DirectX 11」のテッセレーション-テッセレーションの意味と重要性
西川善司の3DゲームファンのためのDirectX 11テッセレーション活用講座

概要

今パートではテッセレーションの使い方について見ていきます。 対応するプロジェクトはPart12_Tessellationになります。

三角形の分割

それでははじめに三角形の分割を仕方を見ながらハルシェーダとドメインシェーダについて見ていきましょう。

ハルシェーダ

ハルシェーダは与えられたプリミティブをどのように分割するかを決めるシェーダになります。 ちなみに分割を行うプリミティブのことをパッチ(英訳:Patch)と呼ばれているようです。

ドキュメント:ハル シェーダーの設計 (日本語) (英語)

// HSTriangle.hlsl

cbuffer Param : register(b0)
{
  float cbEdgeFactor;//三角形の辺の分割量の指定
  float cbInsideFactor;//三角形の内部の分割量の指定
};

//頂点シェーダからの入力
struct VS_CONTROL_POINT_OUTPUT
{
  float3 pos : POSITION;
};

//パッチ定数関数の入力値
struct HS_CONTROL_POINT_OUTPUT
{
  float3 pos : POSITION;
};
//パッチ定数関数の出力値
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[3] : SV_TessFactor;
  float InsideTessFactor  : SV_InsideTessFactor;
};

// パッチ定数関数の定義
#define NUM_CONTROL_POINTS 3
HS_CONSTANT_DATA_OUTPUT CalcHSPatchConstants(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> ip,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONSTANT_DATA_OUTPUT Output;

  Output.EdgeTessFactor[0] = Output.EdgeTessFactor[1] = Output.EdgeTessFactor[2] = cbEdgeFactor;
  Output.InsideTessFactor = cbInsideFactor;

  return Output;
}

//ハルシェーダのエントリポイント定義
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("CalcHSPatchConstants")]
HS_CONTROL_POINT_OUTPUT main(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> inputPatch,
  uint i : SV_OutputControlPointID,
  uint PatchID : SV_PrimitiveID )
{
  HS_CONTROL_POINT_OUTPUT Output;
  Output.pos = inputPatch[i].pos;
  return Output;
}

今まで見てきたシェーダと比べますとコード量や設定するものが多いですが順に見ていきます。

エントリポイント

まず、エントリポイントとなるmain関数についてみていきます。

//ハルシェーダのエントリポイント定義
[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("CalcHSPatchConstants")]
HS_CONTROL_POINT_OUTPUT main(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> inputPatch,
  uint i : SV_OutputControlPointID,
  uint PatchID : SV_PrimitiveID )
{
  HS_CONTROL_POINT_OUTPUT Output;
  Output.pos = inputPatch[i].pos;
  return Output;
}

ハルシェーダのエントリポイントには属性が多数用意されています。

指定できる属性

  1. domain

    ハルシェーダで処理を行うパッチを指定します。三角形のtri,四角形のquad,線分のisolineの3つ指定出来ます。

  2. partitioning

    分割方法を指定します。指定できるのはinteger,fractional_even,fractional_odd,pow2になります。

  3. outputcontrolpoints

    ハルシェーダが作成する制御点の個数を指定します。

  4. outputtopology

    テッセレータの出力するプリミティブを指定します。line,triangle_cw,triangle_ccwが指定できます。

  5. patchconstantfunc

    パッチ定数データを計算する関数を指定します。上のコードだとCalcHSPatchConstants関数がパッチ定数データを計算する関数になります。

  6. maxtessfactor

    最大となる分割係数の値を指定します。

エントリポイントの引数にあるInputPatchはハルシェーダの入力値となる制御点の配列を表しています。 制御点の型と個数は<…>で指定します。

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

説明が後になってしまいましたが、エントリポイントは1つのパッチにつきその制御点の個数分呼びだされます。

パッチ定数関数

パッチ定数関数がパッチ1つごとに1回実行され、分割数の決定やこちらで用意したパラメータを計算します。 言い換えるとパッチを分割するためのデータを計算する関数となります。

// HSTriangle.hlsl
//パッチ定数関数の出力値
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[3] : SV_TessFactor;
  float InsideTessFactor  : SV_InsideTessFactor;
};
// パッチ定数関数の定義
HS_CONSTANT_DATA_OUTPUT CalcHSPatchConstants(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> ip,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONSTANT_DATA_OUTPUT Output;

  Output.EdgeTessFactor[0] = Output.EdgeTessFactor[1] = Output.EdgeTessFactor[2] = cbEdgeFactor;
  Output.InsideTessFactor = cbInsideFactor;

  return Output;
}

分割数を指定するにはシステムセマンティックのSV_TessFactorSV_InsideTessFactorを利用します。

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

この2つは分割するプリミティブによって意味合いが変わります。 三角形の場合は以下の意味になります。

三角形の場合の分割数の指定

  • SV_TessFactor

    三角形の辺の分割数を指定します。 三角形の場合、このシステムセマンティクスを指定する場合は必ず要素数が3つのfloat型の配列にする必要があります。 配列の0番目は三角形の0番目の頂点と向き合っている辺に対応しています。 配列の1番目は三角形の1番目の頂点と向き合っている辺と、配列の2番目は三角形の2番目の頂点と向き合っている辺にそれぞれ対応しています。

  • SV_InsideTessFactor

    三角形の内部の分割数を指定します。 三角形の場合、このシステムセマンティクスを指定する場合は必ずfloat型の変数にする必要があります。

手動で分割数を決める際は不均等な分割になってしまう可能性があります。 意図的にそうしている場合はいいですが、避けたい場合はHLSLの組み込み関数を利用するといいでしょう。 コード例はサンプルを御覧ください。

ドキュメント:三角形のテッセレーション係数の修正を行う組み込み関数
ProcessTriTessFactorsAvg (日本語) (英語)
ProcessTriTessFactorsMax (日本語) (英語)
ProcessTriTessFactorsMin (日本語) (英語)

ハルシェーダは以上のことに注意して実装する必要があります。

ドメインシェーダ

ドメインシェーダはテッセレータステージで分割、生成した頂点を加工するシェーダになります。

ドキュメント:ドメイン シェーダーの設計 (日本語) (英語)

// DSTriangle.hlsl
uint2 random(uint stream, uint sequence){
  //実装は省略
}
float4 calColor(float3 domain){
  //実装は省略
}

//出力データ
struct DS_OUTPUT
{
  float4 pos  : SV_POSITION;
  float4 color : TEXCOORD0;
};
// テッセレータが出力した頂点
struct HS_CONTROL_POINT_OUTPUT
{
  float3 pos : POSITION;
};
// 出力パッチ定数データ。
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[3] : SV_TessFactor;
  float InsideTessFactor : SV_InsideTessFactor;
};
#define NUM_CONTROL_POINTS 3
//エントリポイントの定義
[domain("tri")]
DS_OUTPUT main(
  HS_CONSTANT_DATA_OUTPUT input,
  float3 domain : SV_DomainLocation,
  const OutputPatch<HS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> patch)
{
  DS_OUTPUT output;
  output.pos = float4(patch[0].pos * domain.x + patch[1].pos * domain.y + patch[2].pos * domain.z, 1);
  output.color = calColor(domain);
  return output;
}

使用している構造体が多いですが、エントリポイント自体はシンプルな処理になります。

//エントリポイントの定義
[domain("tri")]
DS_OUTPUT main(
  //パッチ定数関数からの出力値
  HS_CONSTANT_DATA_OUTPUT input,
  //パッチ内での位置
  float3 domain : SV_DomainLocation,
  //ハルシェーダから出力された制御点
  const OutputPatch<HS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> patch)
{
  DS_OUTPUT output;
  output.pos = float4(patch[0].pos * domain.x + patch[1].pos * domain.y + patch[2].pos * domain.z, 1);
  output.color = calColor(domain);
  return output;
}

エントリポイントにつけるdomain属性はパッチのプリミティブを指定します。 ハルシェーダと同じくtri、quad、isolineが指定できます。

ドキュメント:domain属性 (日本語) (英語)

エントリポイントの引数にはハルシェーダからの出力値とパッチ内での位置(SV_DomainLocation)が渡されます。

SV_DomainLocation

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

生成された頂点のパッチ上の位置を求めるために使うものになります。 ドメインシェーダのエントリポイントには必ずこのセマンティクスを付けた引数を用意する必要があります。 このセマンティクスを付けた変数の型はdomain属性で指定したプリミティブによって異なります。 三角形の場合はfloat3にする必要があります。 float3の各要素はxが0番目の頂点に、yは1番目の頂点、zが2番目の頂点の係数になります。 位置を求めるには以下のように補間してください。

output.pos = float4(patch[0].pos * domain.x + patch[1].pos * domain.y + patch[2].pos * domain.z, 1);
    

三角形の分割は以上になります。 設定しないといけないものが多いですが使っていくうちに慣れるでしょう。 残りの四角形と線分は三角形と異なる部分について見てきます。

四角形の分割

四角形を分割する際は各domain属性quadを指定してください。

ハルシェーダ

処理自体は三角形の分割とそう変わりはありません。 分割数の指定の仕方が少し変わったぐらいでしょうか。

// HSQuad.hlsl一部
//パッチ定数関数の出力
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[4] : SV_TessFactor;
  float InsideTessFactor[2] : SV_InsideTessFactor;
};
#define NUM_CONTROL_POINTS 4
// パッチ定数関数
HS_CONSTANT_DATA_OUTPUT CalcHSPatchConstants(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> ip,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONSTANT_DATA_OUTPUT Output;
  [unroll] for (uint i = 0; i < NUM_CONTROL_POINTS; ++i) {
    Output.EdgeTessFactor[i] = cbEdgeFactor;
  }
  Output.InsideTessFactor[0] = Output.InsideTessFactor[1] = cbInsideFactor;
  return Output;
}
//エントリポイント
[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("CalcHSPatchConstants")]
[maxtessfactor(16.f)]
HS_CONTROL_POINT_OUTPUT main(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> inputPatch,
  uint i : SV_OutputControlPointID,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONTROL_POINT_OUTPUT Output;
  Output.pos = inputPatch[i].pos;
  return Output;
}

四角形の場合の分割数の指定

  • SV_TessFactor

    四角形の辺の分割数を指定します。 四角形の場合、このシステムセマンティクスを指定する場合は必ず要素数が4つのfloat型の配列にする必要があります。
    配列の0番目は四角形の0番目のと2番目をつなぐ辺に対応しています。
    配列の1番目は四角形の0番目のと1番目をつなぐ辺に対応しています。
    配列の2番目は四角形の1番目のと3番目をつなぐ辺に対応しています。
    配列の3番目は四角形の2番目のと3番目をつなぐ辺に対応しています。

  • SV_InsideTessFactor

    四角形の内部の分割数を指定します。 四角形の場合、このシステムセマンティクスを指定する場合は必ず要素数が2つのfloat型の配列にする必要があります。 内部の分割はいわゆる縦と横それぞれの分割になります。
    配列の0番目は四角形の0番目のと1番目をつなぐ辺の向きに分割する数になります。
    配列の1番目は四角形の0番目のと2番目をつなぐ辺の向きに分割する数になります。

三角形の時と同じように分割数の調節を行う組み込み関数が用意されています。

ドキュメント:四角形のテッセレーション係数の修正を行う組み込み関数
ProcessQuadTessFactorsAvg (日本語) (英語)
ProcessQuadTessFactorsMax (日本語) (英語)
ProcessQuadTessFactorsMin (日本語) (英語)
Process2DQuadTessFactorsAvg (日本語) (英語)
Process2DQuadTessFactorsMax (日本語) (英語)
Process2DQuadTessFactorsMin (日本語) (英語)

ドメインシェーダ

ドメインシェーダも行う処理自体に変わりはありません。 パッチ内の位置を計算する方法が変わるぐらいでしょう。

// DSQuad.hlsl
uint2 random(uint stream, uint sequence){
  //実装省略
}
float4 calColor(float2 domain){
  //実装省略
}
//ドメインシェーダの出力
struct DS_OUTPUT
{
  float4 pos  : SV_POSITION;
  float4 color : TEXCOORD0;
};
// ハルシェーダからの出力制御点
struct HS_CONTROL_POINT_OUTPUT
{
  float3 pos : POSITION;
};
// 出力パッチ定数データ。
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[4]: SV_TessFactor;
  float InsideTessFactor[2] : SV_InsideTessFactor;
};
#define NUM_CONTROL_POINTS 4
//エントリポイント
[domain("quad")]
DS_OUTPUT main(
  HS_CONSTANT_DATA_OUTPUT input,
  float2 domain : SV_DomainLocation,
  const OutputPatch<HS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> patch)
{
  DS_OUTPUT output;
  //4点間の補間 bilinear interpolation
  output.pos = float4(
    patch[0].pos * (1 - domain.x) * (1 - domain.y)+
    patch[1].pos * domain.x * (1 - domain.y) +
    patch[2].pos * (1 - domain.x) * domain.y +
    patch[3].pos * domain.x * domain.y, 1);
  output.color = calColor(domain);
  return output;
}

四角形の時のSV_DomainLocation

四角形の場合はfloat2にする必要があります。 意味は0~1の範囲のUV座標系とおなじになります。
(0,0)で0番目の頂点の位置に、
(1,0)で1番目の頂点の位置に、
(0,1)で2番目の頂点の位置に、
(1,1)で3番目の頂点の位置になります。 位置を求めるには以下のように補間してください。

//4点間の補間 bilinear interpolation
output.pos = float4(
  patch[0].pos * (1 - domain.x) * (1 - domain.y)+
  patch[1].pos * domain.x * (1 - domain.y) +
  patch[2].pos * (1 - domain.x) * domain.y +
  patch[3].pos * domain.x * domain.y, 1);
    

線分の分割

線分を分割する際は各domain属性isolineを指定してください。

ハルシェーダ

線分も前2つとほぼ同じです。 ただし、分割数を指定するためのSV_TessFactorの意味が少々異なり、SV_InsideTessFactorは必要ありません。

// HSIsoline.hlslの一部
//パッチ定数データ
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[2] : SV_TessFactor;
};
#define NUM_CONTROL_POINTS 2
// パッチ定数関数
HS_CONSTANT_DATA_OUTPUT CalcHSPatchConstants(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> ip,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONSTANT_DATA_OUTPUT Output;

  Output.EdgeTessFactor[0] = cbDetailFactor;
  Output.EdgeTessFactor[1] = cbDensityFactor;
  return Output;
}
//エントリポイント
[domain("isoline")]
[partitioning("pow2")]
[outputtopology("line")]
[outputcontrolpoints(2)]
[patchconstantfunc("CalcHSPatchConstants")]
[maxtessfactor(64.f)]
HS_CONTROL_POINT_OUTPUT main(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> inputPatch,
  uint i : SV_OutputControlPointID,
  uint PatchID : SV_PrimitiveID)
{
  HS_CONTROL_POINT_OUTPUT Output;
  Output.pos = inputPatch[i].pos;
  return Output;
}

線分の場合の分割数の指定

  • SV_TessFactor

    線分の辺の分割数を指定します。 線分の場合、このシステムセマンティクスを指定する場合は必ず要素数が2つのfloat型の配列にする必要があります。
    配列の0番目は線分の本数を調節するものになります。
    配列の1番目は1線分の分割数になります。

線分も同じように分割数の調節を行う組み込み関数が用意されています。

ドキュメント:線分のテッセレーション係数の修正を行う組み込み関数
ProcessIsolineTessFactors (日本語) (英語)

ドメインシェーダ

ドメインシェーダもあまり変わりはありません。 SV_DomainLocationの意味が変わったぐらいです。

// DSIsoline.hlsl
uint2 random(uint stream, uint sequence){
  //実装省略
}
float4 calColor(float2 domain){
  //実装省略
}
struct DS_OUTPUT
{
  float4 pos  : SV_POSITION;
  float4 color : TEXCOORD0;
};
// 出力制御点
struct HS_CONTROL_POINT_OUTPUT
{
  float3 pos : POSITION;
};
// 出力されたパッチ定数データ。
struct HS_CONSTANT_DATA_OUTPUT
{
  float EdgeTessFactor[2] : SV_TessFactor;
};
#define NUM_CONTROL_POINTS 2
[domain("isoline")]
DS_OUTPUT main(
  HS_CONSTANT_DATA_OUTPUT input,
  float2 domain : SV_DomainLocation,
  const OutputPatch<HS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> patch)
{
  DS_OUTPUT output;
  //適当に位置をずらしている
  //実際にはパッチ定数関数の出力値を元に処理を行う形になると思う。
  output.pos = float4(lerp(patch[0].pos, patch[1].pos, domain.x), 1);
  output.pos.x += 0.5f * domain.y;
  //上に凸な曲線を描くためにyの位置をずらしている。
  const float RATE = abs(domain.x - 0.5f) * 2.f;
  const float PI = 3.14159265f;
  output.pos.y = cos(RATE * 0.5f * PI);

  output.color = calColor(domain);
  output.color.b = 0;
  return output;
}

線分の時のSV_DomainLocation

線分の場合はfloat2にする必要があります。 x成分が生成した線分を表すものでy成分が分割数となります。 両方共0~1の範囲を取りますので注意してください。 線分の場合はパッチ定数関数の出力を使うのが必須となりそうです。

点から三角形を生成

テッセレーションステージもジオメトリシェーダと同じく点から三角形を生成することが出来ます。 その際はパッチの制御点に1を指定する必要があります。

// HSPointToTriangle.hlslの一部
//もとは点なので制御点は1つになる
//InputPatch<>にて使用している
#define NUM_CONTROL_POINTS 1
// パッチ定数関数
HS_CONSTANT_DATA_OUTPUT CalcHSPatchConstants(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> ip,
  uint PatchID : SV_PrimitiveID)
{
  //...省略
}
[domain("tri")]//生成するプリミティブを指定する
//..省略
HS_CONTROL_POINT_OUTPUT main(
  InputPatch<VS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> inputPatch,
  uint i : SV_OutputControlPointID,
  uint PatchID : SV_PrimitiveID)
{
  //..省略
}

// DSPointToTriangle.hlslの一部
// ドメインシェーダもハルシェーダと同じように設定する
// 制御点以外は三角形の分割と同じになる
//制御点は点なので当然1となる
//OutputPatch<>で使っている
#define NUM_CONTROL_POINTS 1
[domain("tri")]
DS_OUTPUT main(
  HS_CONSTANT_DATA_OUTPUT input,
  float3 domain : SV_DomainLocation,
  const OutputPatch<HS_CONTROL_POINT_OUTPUT, NUM_CONTROL_POINTS> patch)
{
  //...省略
}

InputPatchとOutputPatchの制御点数に1を設定している以外は三角形の分割と同じです。

まとめ

今回はテッセレーションについて見てきました。 GPUの固定機能を使用しているため設定する部分が多く、使いこなすには知識も必要となります。 ですが行っていることは複雑ではないのでハルシェーダ、バッチ定数関数、ドメインシェーダの役割を把握すればうまくあつかえるようになるのではないでしょうか?

後、本文では説明しませんでしたがテッセレーションを使用する際、入力アセンブラステージのプリミティブトポロジにはパッチの制御点の個数を表すものを設定する必要がありますので忘れずに設定してください。

このパートでグラフィックスパイプラインも一通り見終えました。 ここまでくればDX11の一山を登り終えたといえます。 シェーダについてはこれ以上新しい物はありません。 ここまでの内容があれば後は3DCG知識があれば実装に困ることはあまりないと思います。

<前 トップ 次>