Unity URP テッセレーション
ついに私らしくなくなってしまった
記事ディレクトリ
注を読んでください:
- この記事の URP バージョンは 10.8.1 です。
- この記事では、Tessellation.hlslのコンテンツを組み合わせて説明します。
1 サーフェスの細分化とテッセレーション
サーフェス サブディビジョンまたはサブディビジョン サーフェス (サブディビジョン サーフェス) は、再帰的アルゴリズムを通じて粗い幾何学的メッシュを洗練するテクノロジーを指します。テッセレーションは、シーン内の幾何学的オブジェクトの頂点セットを三角形などの適切なレンダリング構造に分割できる、サーフェス細分化の特定の手段です。「テッセレーション」を「テッセレーション」と呼ぶ場合もあります。
サーフェスのサブディビジョンを通じて、メモリ内に低位のモデルを維持し、リソースを節約するために、需要に応じて三角形のメッシュを動的に増やすことができます。また、GPU レベルで LOD を実装し、いくつかの要因 (カメラ距離など) を通じてサーフェスの細分化の度合いを調整して、近くの領域にハイモードが存在し、遠くにローモードが存在するようにすることもできます。
Direct3D 11 より前では、この種の操作を実現するには、グリッドを CPU ステージでリファインしてから GPU に渡すだけでしたが、これは比較的非効率でした。したがって、テッセレーション ステージ (Tessllation Stage) が Direct3D 11 に導入され、そのタスクが GPU に引き渡されました。テッセレーション ステージは、ハル シェーダー
、テッセレーター、ドメイン カラーリングの 3 つのステージに分かれています。ドメイン シェーダー ステージ (ドメイン シェーダー)。
1.1 ハルシェーダ
ハル シェーダは実際には、コンスタント ハル シェーダとコントロール ポイント ハル シェーダの 2 つのフェーズ (フェーズ) で構成されます。
コンスタント ハル シェーダは各パッチを処理し、その主なタスクはメッシュの分割数をガイドするために使用されるメッシュのテッセレーション係数を出力することです。ここでは例として三角パッチを取り上げます。
// 三角面片
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = 4;
o.edgeFactor[1] = 4;
o.edgeFactor[2] = 4;
o.insideFactor = 4;
return o;
}
コンスタント ハル シェーダーは、パッチのすべての頂点 (または制御点) を入力として受け取ります。InputPatch<VertexOut,3>
これはVertexOut
、頂点シェーダーによって出力される構造であり、その後ろの数字 3 は、三角形パッチに渡される 3 つの頂点データを表します (およびなど、四角形のパッチを渡す場合、これはInputPatch<VertexOut,4>
) である必要があります。
このコードには、次の 3 つのセマンティクスも含まれています。
SV_PrimitiveID
: 受信パッチの ID 値を指定します。ここで渡されるパラメータは、patchID
特定のニーズに応じて操作できます。SV_TESSFACTOR
: 対応するエッジを識別するために使用される細分化係数SV_INSIDETESSFACTOR
: 内部を識別するために使用される細分化係数
渡したものは三角形パッチであるため、当然のことながら 3 つのエッジ細分割係数と 1 つの内部細分割係数が出力されます。入力が 4 隅パッチの場合、PatchTess の構造は次のようになります。
// 四角面片
struct PatchTess {
float edgeFactor[4] : SV_TESSFACTOR; // 分别对应四角面片的四个边
float insideFactor[2] : SV_INSIDETESSFACTOR; // 分别对应内部细分的列数与行数
};
ここでは分割係数をすべて 4 に固定していますが、実際の運用では、カメラ位置に基づいて係数サイズを調整するなど、LOD の効果を達成するためのさまざまな戦略に応じて係数のサイズを柔軟に調整できます。については後述します。
コントロール ポイント ハル シェーダーを使用すると、三角形を 3 回ベジェ三角形パッチに変更するなど、各出力頂点の位置やその他の情報を変更できます。
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut ControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionOS = patch[id].positionOS;
o.texcoord = patch[id].texcoord;
return o;
}
ただし、説明の便宜上、この関数はここではあまり多くのことを行わず、値を渡すだけです。この複雑な書き方を段階的に見てみましょう。
-
domain
:パッチタイプ。パラメータには、三角形サーフェスtri、四角形サーフェスquad、等値線 isolineが含まれます。 -
partitioning
: テッセレーションモード。パラメータはinteger、fractional_even、fractional_oddです。整数
これは、新しい頂点の追加はサブディビジョンの整数部分のみに依存し、Opengl の対応するモードが であることを意味します
equal_spacing
。
このモードは整数のみを受け取るため、サブディビジョン レベルが変更されると、グラフィックスに明らかな突然変異(ポップ) が生じます。たとえば、子午線に依存すると、立方体から球体に突然変化するなどです。
小数偶数
最も近い偶数 n を取り上げ、セグメント全体を同じ長さの n-2 個の部分に切り、両端が短くなります。
分数_奇数
最も近い奇数 n を取り出し、セグメント全体を同じ長さの n-2 個の部分に切り、両端が短い部分にします。
-
outputtopology
: サブディビジョンによって作成される三角形パッチの順序。パラメータは時計回りに三角形_cw、反時計回りに三角形_ccwです。 -
patchconstantfunc
: コンスタント ハル シェーダの関数名を指定します。 -
outputcontrolpoints
: シェル シェーダの実行数。各実行でコントロール ポイントが生成されます。この数は入力制御点の数と一致している必要はありません。たとえば、4 つの制御点を入力すると 16 個の制御点を出力できます。 -
maxtessfactor
: プログラムが使用する最大サブディビジョン係数。Direct3D 11 は、最大テッセレーション係数 64 をサポートします。 -
SV_OutputControlPointID
: このセマンティクスは、現在操作されているコントロール ポイントのインデックス ID を識別します。
詳細については、「テッセレーション ステージ」を参照してください。
コンスタント ハル シェーダ ステージとコントロール ポイント ハル シェーダ ステージは、ハードウェアによって並行して実行されます。コンスタント ハル シェーダーはパッチごとに 1 回実行され、エッジ サブディビジョン係数などの情報を出力します。コントロール ポイント ハル シェーダは、コントロール ポイントごとに 1 回実行され、対応するコントロール ポイントまたは派生コントロール ポイントを出力します。
1.2 モザイクステージのテッセレーター
この段階では制御できず、プロセス全体がハードウェアによって制御されます。この段階で、ハードウェアは前のテッセレーション係数に従ってパッチを再分割します。
三角形パッチを例に挙げ、サブディビジョン モードを選択してinteger
エッジのサブディビジョン係数を大きくすると、三角形の各辺が対応する数値に分割されることがわかります。
内部細分割係数を増やすと、内部テッセレーションのルールが少し直感的ではないことがわかります。内部因子の数は内部三角形の数に直接対応しないためです。
内側の三角形リングの数は細分割数のほぼ半分であり、細分割数が偶数の場合、最内層が頂点になります。
細分割に関する具体的なルールについては、 Opengl 公式 Web サイトのテッセレーションの説明を参照してください。
1.3 ドメインシェーダーステージ ドメインシェーダー
ドメイン シェーダは、各コントロールの「頂点シェーダ」に相当します。
通常の頂点シェーダーと同様に、各コントロール ポイントの頂点位置やその他の情報を計算する必要があります。
struct DomainOut
{
float4 positionCS : SV_POSITION;
float3 color : TEXCOORD0;
};
[domain("tri")]
DomainOut DomainShader (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.texcoord = texcoord;
return output;
}
ここで注目に値するのは、細分化された頂点位置を取得する方法です。三角形のパッチを例に挙げると、基本的な 3 つの制御点情報を渡しpatch
、SV_DOMAINLOCATION
意味論的な細分化後の頂点パラメータ座標を使用します。ここでは重心座標 (u、v、w) を示します。(四隅パッチの場合、この値は2次元座標(u,v)となります)
頂点の重心座標を使用して補間して、対応する頂点情報を取得できます。
ドメイン シェーダで処理されたデータは、パイプラインに従ってジオメトリ シェーダとフラグメント シェーダに渡されます。
2 具体的な実装
上記では、テッセレーション ステージの各部分の担当内容を簡単に紹介します。次は実践してみます!
2.2 さまざまなセグメンテーション戦略
2.2.1 フラット テッセレーション
上記のコードをアセンブルするだけで、最も単純な形式の平面テッセレーションを取得できます。
Shader "Tessellation/Flat Tessellation"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 0
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
_InsideFactor ("InsideFactor", Range(1,8)) = 4
}
SubShader
{
Tags {
"RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex FlatTessVert
#pragma fragment FlatTessFrag
#pragma hull FlatTessControlPoint
#pragma domain FlatTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionOS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut FlatTessVert(Attributes input){
VertexOut o;
o.positionOS = input.positionOS;
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut FlatTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionOS = patch[id].positionOS;
o.texcoord = patch[id].texcoord;
return o;
}
[domain("tri")]
DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.texcoord = texcoord;
return output;
}
half4 FlatTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
#pragma hull
このコードは、と を介して#pragma domain
ハル シェーダとドメイン シェーダを指定します。また、これらのモードを切り替えるのに便利な列挙型のマクロ定義を定義します。
平面テッセレーションは位置情報の単なる線形補間であり、細分化されたパターンは以前よりも三角形の数が若干増えただけであり、単独で使用するとモデルを滑らかにすることはできません。通常、でこぼこした平面を作成するためにディスプレイスメント マップ (ディスプレイスメント マップ) と組み合わせて使用されます。
2.2.2 PN テッセレーション
以前の試みでは、ハル シェーダのコントロール ポイント ステージをあまり設計しませんでした。ここで、別のコントロール ポイント戦略を試すことができます。
シェル シェーダー ステージでは、三角形パッチ (3 つの制御点) を 3 回ベジェ三角形パッチ (3 次ベジェ三角形パッチ、10 個の制御点を持つパッチ) に変換します。この戦略は、曲線点法線三角形 (PN 三角形) と呼ばれます。 )。フラット テッセレーションとは異なり、ディスプレイスメント マップがなくても、モデルの形状を変更し、輪郭を滑らかにすることができます。
コントロール ポイントの増加により、ハル シェーダーの出力時に各頂点はさらに 2 つの頂点情報を保持する必要があります (中央のコントロール ポイント b111 は直接計算できます)。たとえば、b030 は b021 と b012 の頂点情報を保持する必要がある場合があります。
この戦略に従って、コードを再設計してみましょう。
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
float3 positionOS1 : TEXCOORD1; // 三角片元每个顶点多携带两个顶点信息
float3 positionOS2 : TEXCOORD2;
};
float3 ComputeCP(float3 pA, float3 pB, float3 nA){
return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
}
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PNTessControlPoint(InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut output;
const uint nextCPID = id < 2 ? id + 1 : 0;
output.positionOS = patch[id].positionOS;
output.normalOS = patch[id].normalOS;
output.texcoord = patch[id].texcoord;
output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);
return output;
}
出力構造体ではHullOut
、positionOS1 とpositionOS2 を使用して追加のコントロール ポイント情報を保存します。簡単な計算で隣接する頂点の ID を取得しますnextCPID
。現在の最上行と隣接する頂点を使用して、2 点間の追加の制御点を計算できます。
ComputeCP
関数の原理は実際には単純な幾何学的関係です。上の図を例として取り上げます (画像: CurvedPNTriangles )。各点の前に次の関係が存在します。
ドメイン シェーダー ステージをもう一度見てみましょう。
[domain("tri")]
DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float u = bary.x;
float v = bary.y;
float w = bary.z;
float uu = u * u;
float vv = v * v;
float ww = w * w;
float uu3 = 3 * uu;
float vv3 = 3 * vv;
float ww3 = 3 * ww;
float3 b300 = patch[0].positionOS;
float3 b210 = patch[0].positionOS1;
float3 b120 = patch[0].positionOS2;
float3 b030 = patch[1].positionOS;
float3 b021 = patch[1].positionOS1;
float3 b012 = patch[1].positionOS2;
float3 b003 = patch[2].positionOS;
float3 b102 = patch[2].positionOS1;
float3 b201 = patch[2].positionOS2;
float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
float3 V = (b003 + b030 + b300) / 3.0;
float3 b111 = E + (E - V) / 2.0f;
// 插值获得细分后的顶点位置
float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w
+ b210 * uu3 * v
+ b120 * vv3 * u
+ b021 * vv3 * w
+ b012 * ww3 * v
+ b102 * ww3 * u
+ b201 * uu3 * w
+ b111 * 6.0 * w * u * v;
// 此处简化了法线的计算
float3 normalOS = patch[0].normalOS * u
+ patch[1].normalOS * v
+ patch[2].normalOS * w;
normalOS = normalize(normalOS);
float2 texcoord = patch[0].texcoord * u
+ patch[1].texcoord * v
+ patch[2].texcoord * w;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.normalWS = TransformObjectToWorldNormal(normalOS);
output.uv = texcoord;
return output;
}
制御点の数が 10 に増加すると、重心座標を使用した補間の計算の複雑さは当然増加します。したがって、法線を扱う場合、ここでは 3 つの制御点を補間する最も単純な方法が使用されます。
コード全体をここに貼り付けます。
Shader "Tessellation/PN Tri Tessellation + Cubic Bezier Triangle Patch"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd )]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1, 8)) = 4
_InsideFactor ("InsideFactor", Range(1, 8)) = 4
}
SubShader
{
Tags {
"RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex PNTessVert
#pragma fragment PNTessFrag
#pragma hull PNTessControlPoint
#pragma domain PNTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
float3 positionOS1 : TEXCOORD1;
float3 positionOS2 : TEXCOORD2;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float2 uv : TEXCOORD1;
};
VertexOut PNTessVert(Attributes input){
VertexOut o = (VertexOut)0;
o.positionOS = input.positionOS;
o.normalOS = input.normalOS;
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
float3 ComputeCP(float3 pA, float3 pB, float3 nA){
return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PNTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut output;
const uint nextCPID = id < 2 ? id + 1 : 0;
output.positionOS = patch[id].positionOS;
output.normalOS = patch[id].normalOS;
output.texcoord = patch[id].texcoord;
output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);
return output;
}
[domain("tri")]
DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float u = bary.x;
float v = bary.y;
float w = bary.z;
float uu = u * u;
float vv = v * v;
float ww = w * w;
float uu3 = 3 * uu;
float vv3 = 3 * vv;
float ww3 = 3 * ww;
float3 b300 = patch[0].positionOS;
float3 b210 = patch[0].positionOS1;
float3 b120 = patch[0].positionOS2;
float3 b030 = patch[1].positionOS;
float3 b021 = patch[1].positionOS1;
float3 b012 = patch[1].positionOS2;
float3 b003 = patch[2].positionOS;
float3 b102 = patch[2].positionOS1;
float3 b201 = patch[2].positionOS2;
float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
float3 V = (b003 + b030 + b300) / 3.0;
float3 b111 = E + (E - V) / 2.0f;
float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w
+ b210 * uu3 * v
+ b120 * vv3 * u
+ b021 * vv3 * w
+ b012 * ww3 * v
+ b102 * ww3 * u
+ b201 * uu3 * w
+ b111 * 6.0 * w * u * v;
float3 normalOS = patch[0].normalOS * u
+ patch[1].normalOS * v
+ patch[2].normalOS * w;
normalOS = normalize(normalOS);
float2 texcoord = patch[0].texcoord * u
+ patch[1].texcoord * v
+ patch[2].texcoord * w;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.normalWS = TransformObjectToWorldNormal(normalOS);
output.uv = texcoord;
return output;
}
half4 PNTessFrag(DomainOut input) : SV_Target{
Light mainLight = GetMainLight();
half3 baseColor = SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,input.uv).xyz;
half NdotL = saturate(dot(input.normalWS, mainLight.direction) * 0.5 + 0.5);
half3 diffuseColor = mainLight.color * NdotL;
return half4(diffuseColor * baseColor ,1.0);
}
ENDHLSL
}
}
}
ここでは何気なく三角形面の法線を崩しており、Shader操作の効果は以下の通りです。
しかし、現在のアプローチには欠陥があり、法線が異なるモデルを同じ位置に配置すると、分割後にモデルのエッジが不連続になり、亀裂(Crack)が発生します。
この問題を解決するために、NVIDIA は改良された戦略PN-AEN (隣接エッジ法線を使用した点法線三角形) を採用しました。関連リンクは次のとおりです。
この戦略では、データの前処理中に隣接情報を含むインデックス バッファーが生成されます。細分割する際に、隣接する頂点情報により亀裂を除去できます。
ただし、Unity で隣接情報のインデックスバッファを生成するのは不便なようで、このデータを生成するには手動で操作する必要があるかもしれません。Asset Store の誰かがPN-AEN Crack-Free Tessellation Displacement を実装しているのを見かけましたので、興味のある方はぜひご覧ください。(または直接アートをハード凹面にします!)
2.2.3 電話のテッセレーション
Phone Tessellation は PN Tessellation と似ており、これもモデルの輪郭を滑らかにする目的があります。Phone Tessellation では、より少ない計算量で同様の効果を実現できるというだけです。さらに詳しい内容については、 「 電話のテッセレーション 」を参照することをお勧めします。
中心となるアイデアは、生成された頂点PPPを 3 つの頂点の接平面に投影し、これらの投影点を重心座標で補間して、最終的に頂点P ∗ P^*P∗。
コードでは、ドメイン シェーダーを変更する必要があります。コード全体は以下に掲載されています。
Shader "Tessellation/Flat Tri Tessellation + Phone Tess"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1,16)) = 4
_InsideFactor ("InsideFactor", Range(1,16)) = 4
_PhoneShape ("PhoneShape", Range(0, 1)) = 0.5
}
SubShader
{
Tags {
"RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex PhoneTriTessVert
#pragma fragment PhoneTriTessFrag
#pragma hull PhoneTriTessControlPoint
#pragma domain PhoneTriTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
float _PhoneShape;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut PhoneTriTessVert(Attributes input){
VertexOut o;
o.positionWS = TransformObjectToWorld(input.positionOS);
o.normalWS = TransformObjectToWorldNormal(input.normalOS);
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PhoneTriTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionWS = patch[id].positionWS;
o.texcoord = patch[id].texcoord;
o.normalWS = patch[id].normalWS;
return o;
}
[domain("tri")]
DomainOut PhoneTriTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z;
positionWS = PhongTessellation(positionWS, patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, patch[0].normalWS, patch[1].normalWS, patch[2].normalWS, bary, _PhoneShape);
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformWorldToHClip(positionWS);
output.texcoord = texcoord;
return output;
}
half4 PhoneTriTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
PhongTessellation
関数の内容は以下の通りです。
// ===================== GeometricTools.hlsl ======================
float3 ProjectPointOnPlane(float3 position, float3 planePosition, float3 planeNormal)
{
return position - (dot(position - planePosition, planeNormal) * planeNormal);
}
// ===================== Tessellation.hlsl =========================
// p0, p1, p2 triangle world position
// p0, p1, p2 triangle world vertex normal
real3 PhongTessellation(real3 positionWS, real3 p0, real3 p1, real3 p2, real3 n0, real3 n1, real3 n2, real3 baryCoords, real shape)
{
// 分别计算三个切平面的投影点
real3 c0 = ProjectPointOnPlane(positionWS, p0, n0);
real3 c1 = ProjectPointOnPlane(positionWS, p1, n1);
real3 c2 = ProjectPointOnPlane(positionWS, p2, n2);
// 利用质心坐标插值得到最终顶点位置
real3 phongPositionWS = baryCoords.x * c0 + baryCoords.y * c1 + baryCoords.z * c2;
// 通过shape 控制平滑程度
return lerp(positionWS, phongPositionWS, shape);
}
Shaderの動作効果は以下の通りです。
同様に、Phone Tessellation も PN Tessellation と同様に、モデルの法線を使用して表面を滑らかにしますが、頂点位置に複数の法線方向がある場合にも亀裂が発生します。
2.3 さまざまな細分化係数
これまでのところ、サブディビジョン係数はすべて Shader パネルによって制御されています。次に、アルゴリズムを使用して細分化係数を柔軟に調整します。
2.3.1 カメラ距離に基づく
カメラに近い位置をより細分化するために、コンスタント シェル シェーダーのコードを次のように調整しました。
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
float3 cameraPosWS = GetCameraPositionWS();
real3 triVectexFactors = GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = max(1.0, tessFactors.x);
o.edgeFactor[1] = max(1.0, tessFactors.y);
o.edgeFactor[2] = max(1.0, tessFactors.z);
o.insideFactor = max(1.0, tessFactors.w);
return o;
}
ここでカメラの位置が取得され、3 つの頂点のワールド空間座標がGetDistanceBasedTessFactor
関数に渡されます。
// Tessellation.hlsl
real3 GetDistanceBasedTessFactor(real3 p0, real3 p1, real3 p2, real3 cameraPosWS, real tessMinDist, real tessMaxDist)
{
real3 edgePosition0 = 0.5 * (p1 + p2);
real3 edgePosition1 = 0.5 * (p0 + p2);
real3 edgePosition2 = 0.5 * (p0 + p1);
// In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,
// so the compiler will be able to optimize distance() to length().
real dist0 = distance(edgePosition0, cameraPosWS);
real dist1 = distance(edgePosition1, cameraPosWS);
real dist2 = distance(edgePosition2, cameraPosWS);
// The saturate will handle the produced NaN in case min == max
real fadeDist = tessMaxDist - tessMinDist;
real3 tessFactor;
tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);
tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);
tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);
return tessFactor;
}
tessMinDist
この関数は、最小サブディビジョン距離から最も遠いサブディビジョン距離まで、各辺の中点とカメラの間の距離を取得しtessMaxDist
、サブディビジョン係数は徐々に 0 まで減衰します。
エッジ細分割係数を取得した後、内部細分割係数は 3 つの辺の平均として単純に処理されます。
// Tessellation.hlsl
real4 CalcTriTessFactorsFromEdgeTessFactors(real3 triVertexFactors)
{
real4 tess;
tess.x = triVertexFactors.x;
tess.y = triVertexFactors.y;
tess.z = triVertexFactors.z;
tess.w = (triVertexFactors.x + triVertexFactors.y + triVertexFactors.z) / 3.0;
return tess;
}
完全なコードは以下に掲載されています。
Shader "Tessellation/Flat Tri Tessellation + Distance-Based"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {
}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
[IntRange]_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
_TessMinDist ("TessMinDist", Range(0,10)) = 10.0
_FadeDist ("FadeDist", Range(1,20)) = 15.0
}
SubShader
{
Tags {
"RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex DistanceBasedTessVert
#pragma fragment DistanceBasedTessFrag
#pragma hull DistanceBasedTessControlPoint
#pragma domain DistanceBasedTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _TessMinDist;
float _FadeDist;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut DistanceBasedTessVert(Attributes input){
VertexOut o;
o.positionWS = TransformObjectToWorld(input.positionOS);
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
float3 cameraPosWS = GetCameraPositionWS();
real3 triVectexFactors = GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = max(1.0, tessFactors.x);
o.edgeFactor[1] = max(1.0, tessFactors.y);
o.edgeFactor[2] = max(1.0, tessFactors.z);
o.insideFactor = max(1.0, tessFactors.w);
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut DistanceBasedTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionWS = patch[id].positionWS;
o.texcoord = patch[id].texcoord;
return o;
}
[domain("tri")]
DomainOut DistanceBasedTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformWorldToHClip(positionWS);
output.texcoord = texcoord;
return output;
}
half4 DistanceBasedTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
シェーダー適用効果は以下の通りです。
2.3.2 画面のフットプリントに基づく
コンスタント ハル シェーダを次のように変更します。
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
real3 triVectexFactors = GetScreenSpaceTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, GetWorldToHClipMatrix() , _ScreenParams, _TriangleSize);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = tessFactors.x;
o.edgeFactor[1] = tessFactors.y;
o.edgeFactor[2] = tessFactors.z;
o.insideFactor = tessFactors.w;
return o;
}
GetScreenSpaceTessFactor
機能は次のとおりです。
// Tessellation.hlsl
// Reference: http://twvideo01.ubm-us.net/o1/vault/gdc10/slides/Bilodeau_Bill_Direct3D11TutorialTessellation.pdf
// Compute both screen and distance based adaptation - return factor between 0 and 1
real3 GetScreenSpaceTessFactor(real3 p0, real3 p1, real3 p2, real4x4 viewProjectionMatrix, real4 screenSize, real triangleSize)
{
// Get screen space adaptive scale factor
real2 edgeScreenPosition0 = ComputeNormalizedDeviceCoordinates(p0, viewProjectionMatrix) * screenSize.xy;
real2 edgeScreenPosition1 = ComputeNormalizedDeviceCoordinates(p1, viewProjectionMatrix) * screenSize.xy;
real2 edgeScreenPosition2 = ComputeNormalizedDeviceCoordinates(p2, viewProjectionMatrix) * screenSize.xy;
real EdgeScale = 1.0 / triangleSize; // Edge size in reality, but name is simpler
real3 tessFactor;
tessFactor.x = saturate(distance(edgeScreenPosition1, edgeScreenPosition2) * EdgeScale);
tessFactor.y = saturate(distance(edgeScreenPosition0, edgeScreenPosition2) * EdgeScale);
tessFactor.z = saturate(distance(edgeScreenPosition0, edgeScreenPosition1) * EdgeScale);
return tessFactor;
}
一般的な考え方は、ComputeNormalizedDeviceCoordinates
スクリーン座標が関数計算によって取得され、次にスクリーン サイズを乗算してscreenSize
頂点のスクリーン位置を取得するというものです。画面の上端の長さが より短い場合triangleSize
、その細分化係数は減衰されます。
2.3.3 その他
『DirectX12 3D ゲーム開発実践戦闘』では、サブディビジョン係数を計算するための 2 つの測定基準もあります。
-
三角形の向きに応じて、たとえば、アウトライン (シルエットのエッジ) の周囲にある三角形には、他の位置よりも詳細な情報が含まれている必要があります。パッチ法線と視線方向の内積により等高線付近かどうかを判断します。
リアルタイムの線形シルエットの強化について詳しくは、こちらをご覧ください。
-
粗さに応じて: 粗くて凹凸のある表面には、滑らかな表面よりも詳細なテッセレーションが必要です。粗さデータはテクスチャを通じて取得して、テッセレーションの数を決定できます。
3 参考文献
- テッセレーション
- Direct3d-11-高度なステージ-テッセレーション
- 曲線PN三角形
- テッセレーションに亀裂が入っています
- PN-AEN-Triangles-ホワイトペーパー
- 電話のテッセレーション
- リアルタイムの線形シルエット強化
- Direct3D 11 チュートリアル: テッセレーション
レベルが限られているので、間違いがあってもご容赦ください(〃'▽'〃)