読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

テッセレーション基礎

Shader Unity Graphics

はじめに

UnityでTessellationについて勉強したので基本的なところを纏めておきます。

Tessellationとは

f:id:es_program:20161017162039g:plain:w400

GPUを利用して頂点の分割を行う技術です。

D3D10でのレンダリングパイプラインは

f:id:es_program:20161017163403g:plain:w400

MSDNより引用

となっていました。 大抵の場合はVertex-ShaderとPixel-Shaderを書いて...という感じでした。

D3D11からはD3D10までの各パイプラインをサポートする他、追加のステージでTessellationを行うことが出来るようになっています。

f:id:es_program:20161017163745j:plain:w400

MSDNより引用

上記のパイプラインステージを簡略化すると

f:id:es_program:20161017162622g:plain:w400

MSDNより引用

のようになっていて、Tessellationを利用する場合

  • Hull-Shader Stage

    • Main-Hull-Shader (メインハルシェーダー)
    • Patch-Constant-Function (パッチ定数関数)

    上記の2つの関数を用意する

  • Tessellation Stage

    固定機能なので何もしなくていい

  • Domain-Shader Stage

    上記の関数を用意する

が必要になります。

実際にTessellationを行うシンプルなコード

実際にTessellationを行うコードです。

Shader "Unlit/SimpleTessellation"
{
    Properties{
        _TessFactor("Tess Factor",Vector) = (2,2,2,2)
    }

        SubShader{
            Pass{
                CGPROGRAM

   #pragma vertex VS
   #pragma fragment FS
   #pragma hull HS
   #pragma domain DS
   #define INPUT_PATCH_SIZE 3
   #define OUTPUT_PATCH_SIZE 3

                uniform vector _TessFactor;

                struct appdata {
                    float4 w_vert:POSITION;
                };
                struct v2h {
                    float4 pos:POS;
                };
                struct h2d_main {
                    float3 pos:POS;
                };
                struct h2d_const {
                    float tess_factor[3] : SV_TessFactor;
                    float InsideTessFactor : SV_InsideTessFactor;
                };
                struct d2f {
                    float4 pos:SV_Position;
                };

                struct f_input {
                    float4 vertex:SV_Position;
                    float4 color:COLOR0;
                };

                v2h VS(appdata i) {
                    v2h o = (v2h)0;
                    o.pos = i.w_vert;
                    return o;
                }

                h2d_const HSConst(InputPatch<v2h, INPUT_PATCH_SIZE> i) {
                    h2d_const o = (h2d_const)0;
                    o.tess_factor[0] = _TessFactor.x;
                    o.tess_factor[1] = _TessFactor.y;
                    o.tess_factor[2] = _TessFactor.z;
                    o.InsideTessFactor = _TessFactor.w;
                    return o;
                }

                [domain("tri")]
                [partitioning("integer")]
                [outputtopology("triangle_cw")]
                [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
                [patchconstantfunc("HSConst")]
                h2d_main HS(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id:SV_OutputControlPointID) {
                    h2d_main o = (h2d_main)0;
                    o.pos = i[id].pos;
                    return o;
                }

                [domain("tri")]
                d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) {
                    d2f o = (d2f)0;
                    float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
                    o.pos = mul(UNITY_MATRIX_MVP, float4(pos, 1));
                    return o;
                }

                float4 FS(f_input i) : SV_Target {
                    return float4(1, 0, 0, 1);
                }

                ENDCG
            }
    }
}

はじめて見るとちょっと慣れない表記があったりと困惑しますが、順番に解説していきます。

用語について

コード解説の前に、Tessellationで使われる用語についてです。詳しい解説は載ってないので誤って解釈している部分があるかもしれません。というかあると思います。

  • ドメイン
    • 分割に利用するプリミティブ形状
  • コントロールポイント
    • 頂点分割で使う制御点
  • パッチ
    • ポリゴン分割処理を行う際に使用するコントロールポイントの集合(1つのパッチで1~32までのコントロールポイントが割当可)
  • テッセレーション係数
    • パッチごとに指定する頂点の分割係数

Hull-Shader Stage

Hull-Shader Stageでは、頂点シェーダーから与えられた頂点データ(Object-Space)に対して

  • コントロールポイントの割当
  • パッチごとにテッセレーション係数の割当
    • 現在のパッチでどの程度頂点分割をかけるかという量を割り当てる

という2つのことが必要になります。 これらそれぞれを行う関数のことを

  • Main-Hull-Shader
    • メインハルシェーダー
  • Patch-Constant-Function
    • パッチ定数関数

と呼びます(この2つの処理はハードウェアで並行して実行されます)。

Main-Hull-Shaderはコントロールポイントごとに1回、Patch-Constant-Functionはパッチごとに1回実行されます。

頂点データは、Tessellationを行う場合はObject-Spaceで与えるようにします。

これらの出力が次のTessellation Stageに渡されます。 Tessellation Stageでは、これらの値を利用して実際に頂点が分割され、その出力をDomain-Shaderに渡します。 Tessellation Stageは固定機能なので、プログラマーが手を出す部分はありません。

Patch-Constant-Function

比較的理解が簡単なこちらから。

Patch-Constant-Functionはパッチごとに、どの程度頂点を分割するかを決める係数を詰め込みます。 基本的には係数を構造体に詰め込んで、次のステージであるTessellatorに渡すたけで、あとはTessellatorがその係数を見ながら頂点を分割してくれます。

先程載せた実際にTessellationを行うシンプルなコードの中では、以下の抜粋した部分がPatch-Constant-Functionに該当します。

                struct h2d_const {
                    float tess_factor[3] : SV_TessFactor;
                    float InsideTessFactor : SV_InsideTessFactor;
                };

                h2d_const HSConst(InputPatch<v2h, INPUT_PATCH_SIZE> i) {
                    h2d_const o = (h2d_const)0;
                    o.tess_factor[0] = _TessFactor.x;
                    o.tess_factor[1] = _TessFactor.y;
                    o.tess_factor[2] = _TessFactor.z;
                    o.InsideTessFactor = _TessFactor.w;
                    return o;
                }

SV_TessFactor、SV_InsideTessFactorセマンティクスを持つ構造体h2d_constを定義して、そこに適当な値を詰め込む関数HSConstを定義しています。 このセマンティクスを持つ値をTessellatorが解釈して分割を実行します。

InputPatch<v2h, INPUT_PATCH_SIZE>は頂点シェーダーから出力された値がパッチ単位で入ったものです。 INPUT_PATCH_SIZEはCGPROGRAMの先頭あたりで3という値にしています。これは1つのパッチが何個のコントロールポイントを持つかという値です。

HSConstの出力h2d_constはTessellatorで分割に使われた後、Domain-Shaderに渡されるため、ここで渡された値はDomain-Shaderで参照できます。

h2d_constでテッセレーション係数以外の任意の値を保持させて、頂点シェーダーから入力されたパッチデータを使って何か値を計算してDomain-Shaderで使うといったことも出来ます。

注意点としては、分割に指定するドメインによって必要になる出力が変わってきます。 今回のサンプルではドメインに三角形を指定していますが、それ以外の場合ではSV_InsideTessFactorを指定できないケースもあります。 この場合エラーになるのですぐにわかると思います。

Main-Hull-Shader

Main-Hull-Shaderはパッチのに対してコントロールポイントを割り当てて出力する関数です。

Tessellationを行うシンプルなコードの中では、以下の抜粋した部分がMain-Hull-Shaderに該当します。

                struct h2d_main {
                    float3 pos:POS;
                };

                [domain("tri")]
                [partitioning("integer")]
                [outputtopology("triangle_cw")]
                [outputcontrolpoints(OUTPUT_PATCH_SIZE)]
                [patchconstantfunc("HSConst")]
                h2d_main HS(InputPatch<v2h, INPUT_PATCH_SIZE> i, uint id:SV_OutputControlPointID) {
                    h2d_main o = (h2d_main)0;
                    o.pos = i[id].pos;
                    return o;
                }

1つのコントロールポイントを出力するだけなので、出力用構造体はシンプルです。 Patch-Constant-Functionの出力同様、Main-Hull-Shaderの出力もDomain-Shaderで参照できます。

この例ではMain-Hull-Shader自体もかなりシンプルで、Vertex-Shaderから出力された位置情報をそのまま出力用の構造体に詰め込んで出力しているだけです。

Main-Hull-Shaderについている属性はそれぞれ

  • domain
  • partitioning
    • 分割方法。(integer/fractional_eve,fractional_odd/pow2)から選択。
  • outputtopology
    • 出力された頂点が形成するトポロジー。(point/line/triangle_cw/triangle_ccw)から選択。
  • outputcontrolpoints
    • 出力されるコントロールポイント数(入力と違っても良い)。
  • patchconstantfunc
    • Patch-Constant-Functionを指定する。

となっています。詳しくはこちら.aspx)のページを参照して下さい。

Domain-Shader Stage

Tessellation Stageの出力をもとに、分割後の頂点を処理します。 Geometry-Shaderを呼び出さない場合はここでWVP変換をする必要があります。 DisplacementMapを利用して変位を与える処理などもここで記述できます。

Tessellationを行うシンプルなコードの中では、以下の抜粋した部分がDomain-Shaderに該当します。

                struct d2f {
                    float4 pos:SV_Position;
                };

                [domain("tri")]
                d2f DS(h2d_const hs_const_data, const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i, float3 bary:SV_DomainLocation) {
                    d2f o = (d2f)0;
                    float3 pos = i[0].pos * bary.x + i[1].pos * bary.y + i[2].pos * bary.z;
                    o.pos = mul(UNITY_MATRIX_MVP, float4(pos, 1));
                    return o;
                }

Domain-Shaderの出力用構造体は頂点位置のみ出力するようにしているので、とてもシンプルです。

Doman-Shader関数の引数はそれぞれ

  • h2d_const hs_const_data
    • Patch-Constant-Functionの出力値を参照できる
  • const OutputPatch<h2d_main, OUTPUT_PATCH_SIZE> i
    • 出力パッチデータ(Main-Hull-Shaderの出力値を参照できる)
  • float3 bary:SV_DomainLocation
    • Tessellatorによる分割後の頂点の位置を求めるためのパラメーター

となっています。 SV_DomainLocationセマンティクスを持つパラメータの意味ですが、Tessellation Stageの出力は、分割後の頂点位置がそのまま出力されるわけではなく、このパラメーターを用いてDomain-Shader内で計算する必要があるため存在します。

ドメインにtri(三角形)を指定しているため、SV_DomainLocationは三角形の重心座標を意味するパラメータになり、その型はfloat3になります。 ドメインが異なると、SV_DomainLocationが意味する値と型が変わります。 詳しくはMSDN.aspx)で。

例では、パッチの各コントロールポイントと重心座標の値を計算して、WVP変換してその位置を出力しています。