💠

【URP】テッセレーションについて勉強してみる

2023/08/08に公開

はじめに

テッセレーションへの理解を深めるため、テッセレーションについてまとめてみようと思います。

この記事では、難しい理論は抜きにして、
テッセレーションをUnityのシェーダーから使う方法について理解することをゴールとします。

環境

  • Unity 2021.3.16f1
  • Universal RP 12.1.8

Chapter0. テッセレーションの概要

テッセレーションを利用することで、3DモデルのメッシュをGPU側で細かく割って表示することができます。

参考: https://learn.microsoft.com/ja-jp/windows/uwp/graphics-concepts/tessellator-stage--ts-

Chapter1. テッセレーションの中身

テッセレーションは、複数のステージにまたがって処理されます。

主に以下の5つのステージで処理されます。 (IA Stage、Geometry Stage、Rasterizer Stage、OM Stage などについては省略しています)

項目 概要
1. VS (頂点シェーダー ステージ) 頂点ごとの処理を実装
2. HS (ハルシェーダー ステージ) テッセレータ用のパラメータの設定を行う
3. TS (テッセレータ ステージ) メッシュを分割する (カスタマイズ不可)
4. DS (ドメインシェーダー ステージ) 分割後のメッシュの頂点計算を行う (MVP座標変換など)
5. PS (ピクセルシェーダー ステージ) 色の計算を行う (テクスチャサンプリングなど)

参考 : https://learn.microsoft.com/ja-jp/windows/uwp/graphics-concepts/graphics-pipeline

Chapter2. 早速シェーダーを書いてみる

早速シェーダーを書いてみましょう。

今回は、ポリゴンにテクスチャを張り付けるだけのシンプルなシェーダーを組んでみます。

サンプル : https://gist.github.com/rngtm/bfaea8b80aa0a6cb3a1ef2b2b43380e6

テッセレーションを行わない場合は 2つの関数(vert, frag)を書くことになりますが、
テッセレーションを使用する場合は、5つのシェーダー関数を書くことになります。

テッセレーションを行わない場合 (vert, frag)
テッセレーションを行わない場合
// 頂点シェーダー
v2f vert (appdata input)
{
    HsInput o;
    o.positionOS = TransformObjectToHClip(input.vertex, 1); // 座標変換
    o.uv = input.uv;
    return o;
}

// フラグメントシェーダー
half4 vert (v2f i) : SV_Target
{
    half4 col = tex2D(_MainTex, i.uv);
    return col;
}
テッセレーションを行う場合 (vert, hull, domain, frag)
//////////////////////////////////////
// 頂点シェーダー
//////////////////////////////////////
HsInput vert (VsInput input)
{ ... }

//////////////////////////////////////
// ハルシェーダー (制御点を出力)
//   https://learn.microsoft.com/ja-jp/windows/uwp/graphics-concepts/hull-shader-stage--hs-
//////////////////////////////////////
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("hullConst")]
[outputcontrolpoints(3)]
HsControlPointOutput hull(InputPatch<HsInput, 3> input, uint id : SV_OutputControlPointID)
{ ... }

//////////////////////////////////////
// ハルシェーダー (パッチ定数の出力)
//   https://learn.microsoft.com/ja-jp/windows/uwp/graphics-concepts/hull-shader-stage--hs-
//////////////////////////////////////
HsConstantOutput hullConst(InputPatch<HsInput, 3> i)
{ ... }

//////////////////////////////////////
// ドメインシェーダー
//   https://learn.microsoft.com/ja-jp/windows/uwp/graphics-concepts/domain-shader-stage--ds-
//////////////////////////////////////
[domain("tri")]
DsOutput domain(
    HsConstantOutput hsConst, 
    const OutputPatch<HsControlPointOutput, 3> input, 
    float3 bary : SV_DomainLocation)
{ ... }

//////////////////////////////////////
// フラグメントシェーダー
//////////////////////////////////////
half4 frag (DsOutput i) : SV_Target
{ ... }

1. VS (頂点シェーダー ステージ)

VSでは、ハルシェーダーの入力となる構造体 HsInput の作成を行います。
vert内では頂点座標変換を行わないのがポイントです。
(frag + vertなシェーダーを書く時は、vert内に頂点座標変換を実装していました)

// 頂点シェーダー 入力
struct VsInput
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

// ハルシェーダーの入力
struct HsInput
{
    float3 positionOS  : POS;
    float2 uv          : TEXCOORD0;
};

// 頂点シェーダー
HsInput vert (VsInput input)
{
    HsInput o;
    o.positionOS = input.vertex.xyz;
    o.uv = input.uv;
    return o;
}

2. HS (ハルシェーダー ステージ)

ハルシェーダーでは、以下の2つの関数を実装することになります。

  1. 制御点の出力
  2. パッチ定数の出力
    参考 : https://learn.microsoft.com/ja-jp/windows/win32/direct3d11/direct3d-11-advanced-stages-hull-shader-design

これらの出力データは、後続のテッセレーターStageにて処理されます。

【1】制御点の出力

vertシェーダーでは、HsInput という頂点データを後続のハルシェーダーへ流していました。

頂点シェーダー
HsInput vert (VsInput input)

ハルシェーダーには、パッチ (InputPatch) という単位で三角形の情報が入力されます。

ハルシェーダー (制御点を出力)
HsControlPointOutput hull(
  InputPatch<HsInput, 3> input, 
  uint id : SV_OutputControlPointID)

ハルシェーダーの実装は以下になります。 (制御点の出力)

// ハルシェーダーの出力制御点
struct HsControlPointOutput
{
    float3 positionOS : POS;
    float2 uv         : TEXCOORD0;
};

// ハルシェーダー (制御点を出力)
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("hullConst")]
[outputcontrolpoints(3)]
HsControlPointOutput hull(InputPatch<HsInput, 3> input, uint id : SV_OutputControlPointID)
{
    HsControlPointOutput output;
    output.positionOS = input[id].positionOS.xyz;
    output.uv         = input[id].uv;
    return output;
}

【2】パッチ定数の出力

ハルシェーダーでは、
パッチ (三角形) をどれくらい細かく分割するかというパッチ定数を出力する関数も実装する必要があります。
ハルシェーダーの実装は以下になります。 (パッチ定数の出力)

// 出力パッチデータ (テッセレータの入力)
struct HsConstantOutput
{
    float tessFactor[3]    : SV_TessFactor;
    float insideTessFactor : SV_InsideTessFactor;
};

// 出力パッチ定数 (テッセレータの入力)
HsConstantOutput hullConst(InputPatch<HsInput, 3> i)
{
    HsConstantOutput output;
    
    output.tessFactor[0] = _TessFactor.x;
    output.tessFactor[1] = _TessFactor.y;
    output.tessFactor[2] = _TessFactor.z;
    output.insideTessFactor = _TessFactor.w;

    return output;
}

3. TS (テッセレータ ステージ)

ハルシェーダーで作成した制御点とパッチ定数を元に、メッシュが分割されます。
テッセレータの処理を自分で書くことはできません。

4. DS (ドメインシェーダー ステージ)

テッセレーターによって分割された頂点の座標をドメインシェーダー内で計算します。

ドメインシェーダーの入力

ドメインシェーダーには、パッチ定数 HsConstantOutput と、三角形を構成する制御点 HsControlPointOutput の配列(サイズ=3)、
そしてドメインポイントの位置 baryが入力されます。
bary は重心座標になっています。

[domain("tri")]
DsOutput domain(
    HsConstantOutput hsConst, 
    const OutputPatch<HsControlPointOutput, 3> input, 
    float3 bary : SV_DomainLocation // ドメインポイントがハル上で占める位置 (https://learn.microsoft.com/ja-jp/windows/win32/direct3dhlsl/sv-domainlocation)
    )

ドメインポイント bary を利用して、三角形の各頂点 input の座標の加重平均を計算すると、
ドメインポイントの座標が求まります。

// ドメインポイントの座標を計算 (三角形の頂点の座標Positionの加重平均)
float3 positionOS =
    + bary.x * input[0].positionOS
    + bary.y * input[1].positionOS
    + bary.z * input[2].positionOS;

ドメインシェーダーの実装

// ドメインシェーダー出力 (フラグメントシェーダーの入力)
struct DsOutput
{
    float4 positionCS : SV_Position;
    float2 uv         : TEXCOORD0;
};

// ドメインシェーダー
[domain("tri")]
DsOutput domain(
    HsConstantOutput hsConst, 
    const OutputPatch<HsControlPointOutput, 3> input, 
    float3 bary : SV_DomainLocation // ドメインポイントがハル上で占める位置
    )
{
    // ドメインポイントの座標を計算 (三角形の頂点の座標Positionの加重平均)
    float3 positionOS =
        + bary.x * input[0].positionOS
        + bary.y * input[1].positionOS
        + bary.z * input[2].positionOS;

    // ドメインポイントの座標を計算 (三角形の頂点のUVの加重平均)
    float2 uv =
        + bary.x * input[0].uv
        + bary.y * input[1].uv
        + bary.z * input[2].uv;

    DsOutput output = (DsOutput)0;    
    output.positionCS = TransformObjectToHClip(positionOS); // 頂点座標のMVP座標変換
    output.uv = uv;
    return output;
}

5. PS (ピクセルシェーダー ステージ)

ドメインシェーダーの出力データを利用して、色の計算処理を実装します。
今回は、テクスチャをUVでサンプリングして出力するだけのシンプルなものです。

// フラグメントシェーダー
half4 frag (DsOutput i) : SV_Target
{
    half4 col = tex2D(_MainTex, i.uv);
    return col;
}

結果

マテリアルの _TessFactor を増やすと、メッシュが細かく分割されます。


_TessFactor = (1, 1, 1, 1)


_TessFactor = (2, 1, 1, 1)


_TessFactor = (1, 2, 1, 1)


_TessFactor = (1, 1, 2, 1)


_TessFactor = (1, 1, 1, 2)


_TessFactor = (5, 5, 5, 5)

Discussion