翻譯22 Unity中的曲面

hull and domain渲染階段
三角細分
控制細化
數組

Unity2017.1.0dom

image最終效果.ide

1 Hulls and Domains渲染階段

曲面細分是將表面切成更小更精細的技術。在咱們的例子中,咱們將三角形細分,最後獲得覆蓋相同空間的更多更小的三角形。
GPU可以拆分渲染提供給它的三角形。 這樣作有多種緣由,當三角形的一部分最終被剪切時。 咱們沒法控制,可是還有一個細分階段能夠配置。 此階段位於頂點vertex和片斷fragment着色器階段之間, 添加一個Hulls and Domains程序。函數

image流水程序佈局

1.1 建立Tessellation Shader

建立Tessellation.shader MyTessellation.cginc性能

Shader "Custom/Tessellation" { … }
#if !defined(TESSELLATION_INCLUDED)
#define TESSELLATION_INCLUDED

#endif

當使用Tessellation,最小的着色器目標等級是4.6。若是咱們不手動設置,Unity將會發出警告並自動使用該級別。咱們要把鑲嵌階段添加到base_pass和additive_pass,deferred_pass,陰影通道不必。優化

#pragma target 4.6
…
#include "MyFlatWireframe.cginc"
#include "MyTessellation.cginc"

建立一個quad、材質.ui

image

它由兩個等腰直角三角形組成。短邊長度爲1,長對角線長度爲√2.編碼

1.2 Hull Shaders程序-整數模式

相似geometry着色階段,Hull程序能夠處理三角形,四邊形,或等值線,必須告訴它應該在什麼表面上工做,並提供必要的數據。這是赫爾項目的工做。void函數開始spa

void MyHullProgram () {}

hull program在一個面片上運行,它做爲參數傳遞給hull,添加一個InputPatch參數

void MyHullProgram (InputPatch patch) {}

面片上可能有許多網格頂點,必須指定頂點的數據格式。如今咱們將使用VertexData結構體

void MyHullProgram (InputPatch<VertexData> patch) {}

當咱們處理三角形時,每一個面片將包含三個頂點。這個也必須指定爲InputPatch的第二個模板參數

void MyHullProgram (InputPatch<VertexData, 3> patch) {}

hull程序的工做是傳遞所需的頂點數據到Tessellation階段。雖然給它提供了一整個面片,但該函數每次只能處理面片中一個頂點,附加一個參數指定hull應該與哪一個(頂點)一塊兒工做。該參數是一個無符號整數,具備SV_OutputControlPointID語義

void MyHullProgram (
    InputPatch<VertexData, 3> patch,
    uint id : SV_OutputControlPointID
) {}
 

先只需將面片做爲數組,根據索引id而後返回所需的頂點數據

VertexData MyHullProgram (
    InputPatch<VertexData, 3> patch,
    uint id : SV_OutputControlPointID
) {
    return patch[id];
}

這看起來像一個函數程序

#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
#pragma hull MyHullProgram
#pragma geometry MyGeometryProgram

這將產生一些編譯錯誤,提示沒有正確配置hull着色器。與幾何函數同樣,它須要屬性來配置。首先,咱們必須明確地告訴它它適用於三角形。這是經過使用tri做爲參數的UNITY_domain屬性完成的

[UNITY_domain("tri")]
VertexData MyHullProgram …

還必須明確地指定每一個三角形一個patch輸出三個控制點,

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
VertexData MyHullProgram …

當GPU建立新的三角形時,它須要知道咱們想要將它們定義爲順時針仍是逆時針。像全部其餘三角形同樣,它們應該是順時針的。這是經過UNITY_outputtopology屬性來控制的。它的參數應該是triangle_cw

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
VertexData MyHullProgram …

GPU還須要經過UNITY_partitioning屬性被告知如何分割-整數模式

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
VertexData MyHullProgram …

除了劃分方法,GPU還須要知道patch應該被分割成多少部分。這不是一個常數值,它能夠在每一個patch中變化。咱們必須提供一個函數來計算它,叫作patch常數函數。

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("MyPatchConstantFunction")]
VertexData MyHullProgram …

1.3 Patch常數函數

每一個patch僅被調用一次patch常數函數,而不是每一個頂點都調用一次。實際上,此功能是與MyHullProgram並行運行。

image

爲了肯定如何細分三角形,GPU使用了四個細分因子。三角形patch的三個邊都有一個因子,必須做爲具備SV_TessFactor語義的float數組傳遞。 三角形的內部也有一個因子,使用SV_InsideTessFactor語義。

struct TessellationFactors 
{
    float edge[3] : SV_TessFactor;
    float inside : SV_InsideTessFactor;
};

patch常量函數以patch做爲輸入參數並輸出因子。全部因子設置爲1,這將致使tessellation階段不細分patch。

TessellationFactors MyPatchConstantFunction (InputPatch<VertexData, 3> patch) {
    TessellationFactors f;
    f.edge[0] = 1;
    f.edge[1] = 1;
    f.edge[2] = 1;
    f.inside = 1;
    return f;
}

1.4 Domain Shaders程序

一個完整的曲面細分階段須要Hull肯定面片,也要Domain生成頂點。

void MyDomainProgram () {}

兩者都做用在同一個階段,再次使用UNITY_domain屬性

[UNITY_domain("tri")]
void MyDomainProgram () {}

Domain參數須要細分因子以及patch面片.

[UNITY_domain("tri")]
void MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<VertexData, 3> patch
) {}

曲面細分階段肯定細分時,它不會產生任何新的頂點。 相反,它會爲這些頂點提供重心座標。 使用這些座標來導出最終頂點取決於Domain着色器。 爲其提供重心座標它們具備SV_DomainLocation語義。

[UNITY_domain("tri")]
void MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<VertexData, 3> patch,
    float3 barycentricCoordinates : SV_DomainLocation
) {}

在函數內部,咱們必須生成最終的頂點數據。

[UNITY_domain("tri")]
void MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<VertexData, 3> patch,
    float3 barycentricCoordinates : SV_DomainLocation
) {
    VertexData data;
}

爲了找到這個頂點的位置,使用重心座標。X、Y和Z座標決定了第一個、第二個和第三個控制點的權重。

VertexData data;
data.vertex =
    patch[0].vertex * barycentricCoordinates.x +
    patch[1].vertex * barycentricCoordinates.y +
    patch[2].vertex * barycentricCoordinates.z;

可是這就必須用一樣的方法插值全部數據。讓咱們爲它定義一個方便的宏,它能夠用於全部的向量大小

//    data.vertex =
//        patch[0].vertex * barycentricCoordinates.x +
//        patch[1].vertex * barycentricCoordinates.y +
//        patch[2].vertex * barycentricCoordinates.z;    
#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
    patch[0].fieldName * barycentricCoordinates.x + \
    patch[1].fieldName * barycentricCoordinates.y + \
    patch[2].fieldName * barycentricCoordinates.z;

MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)

除了頂點位置,還有法線,切線,和全部UV座標.

    MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
    MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
    MY_DOMAIN_PROGRAM_INTERPOLATE(tangent)
    MY_DOMAIN_PROGRAM_INTERPOLATE(uv)
    MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
    MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)

咱們惟一沒有插入的是實例id。由於Unity不一樣時支持GPU實例化和鑲嵌,因此複製這個ID是沒有意義的。爲了防止編譯錯誤,從三次着色器傳遞中刪除多編譯指令。這也將從着色器的GUI中移除實例化選項。

//#pragma multi_compile_instancing
//#pragma instancing_options lodfade force_same_maxcount_for_gl

如今咱們有了一個新的頂點,它將在這一階段以後被髮送到幾何程序或插值器。可是這些程序指望插值頂點數據,而不是頂點數據。爲了解決這個問題,讓Domain着色器接管了原始頂點程序的職責。這是經過在其中調用MyVertexProgram來完成的——就像其餘任何函數同樣——並返回結果。

[UNITY_domain("tri")]
InterpolatorsVertex MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<VertexData, 3> patch,
    float3 barycentricCoordinates : SV_DomainLocation
) {
    …
    return MyVertexProgram(data);
}

Now we can add the domain shader to our three shader passes, but we'll still get errors.

#pragma hull MyHullProgram
#pragma domain MyDomainProgram

1.5 控制頂點

MyVertexProgram只須要調用一次,只是咱們改變了它調用的位置。可是咱們仍然須要指定一個頂點程序在頂點着色器階段被調用,它位於Hull着色器以前。在這一點上,咱們不須要作任何事情,因此咱們可使用一個函數,它能夠直接經過頂點數據,而不須要修改。

VertexData MyTessellationVertexProgram (VertexData v) {
    return v;
}

使用這個函數給三個着色器的頂點程序傳遞數據。

#pragma vertex MyTessellationVertexProgram

這將產生另外一個編譯器錯誤,提示重用了位置語義。咱們必須爲咱們的頂點程序使用另外一個輸出結構,使用INTERNALTESSPOS語義來表示頂點位置。結構體的其他部分與VertexData相同,只是它歷來沒有實例ID。將其命名爲TessellationControlPoint。

struct TessellationControlPoint {
    float4 vertex : INTERNALTESSPOS;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv : TEXCOORD0;
    float2 uv1 : TEXCOORD1;
    float2 uv2 : TEXCOORD2;
};

改變MyTessellationVertexProgram它會把頂點數據放入一個控制點結構中並返回那個值

TessellationControlPoint MyTessellationVertexProgram (VertexData v) {
    TessellationControlPoint p;
    p.vertex = v.vertex;
    p.normal = v.normal;
    p.tangent = v.tangent;
    p.uv = v.uv;
    p.uv1 = v.uv1;
    p.uv2 = v.uv2;
    return p;
}

接下來,MyHullProgram也必須進行更改,以便使用TessellationControlPoint而不是VertexData。只有它的參數類型須要更改.

TessellationControlPoint MyHullProgram (
    InputPatch<TessellationControlPoint, 3> patch,
    uint id : SV_OutputControlPointID
) {
    return patch[id];
}

patch常數函數也是如此。

TessellationFactors MyPatchConstantFunction (
    InputPatch<TessellationControlPoint, 3> patch
) {
    …
}

Domain程序的參數類型也必須改變。

InterpolatorsVertex MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<TessellationControlPoint, 3> patch,
    float3 barycentricCoordinates : SV_DomainLocation
) {
    …
}

在這一點上,咱們終於有了一個正確的Tessellation着色器。

2 細分三角形

整個Tessellation設置的要點是,咱們能夠細分patch。這容許咱們用一組較小的三角形替換單個三角形。咱們如今就來作。

2.1 Tessellation Factors

三角形的細分是由它的Tessellation因子控制的。咱們在mypatchconstant函數中肯定這些因素。目前,咱們將它們都設置爲1,這不會產生視覺變化。Hull、Domain經過原始的頂點數據並無產生任何新的東西,將全部因子設置爲2,效果立顯。

TessellationFactors MyPatchConstantFunction (
    InputPatch<TessellationControlPoint, 3> patch
) {
    TessellationFactors f;
    f.edge[0] = 2;
    f.edge[1] = 2;
    f.edge[2] = 2;
    f.inside = 2;
    return f;
}

image image

Tessellation因子爲2 vs 3.

鑲嵌因子是偶數時,中心有一個頂點。當它們是奇數時,中心三角形會有代替。若是咱們使用較大的Tessellation因子,咱們會獲得多個三角形。每向中心移動一步,三角形被細分的數量就會減小2,直到最後獲得1或0個子邊。
image

2.1 不一樣的邊和因子組合

三角形如何細分是由因子控制。邊緣因子可用於覆蓋它們各自的邊緣被細分的數量。這隻會影響到原始的patch邊緣,而不會影響到生成的內部三角形。爲了清楚地看到這一點,將內因子設置爲7,同時保持邊因子爲1。

f.edge[0] = 1;
f.edge[1] = 1;
f.edge[2] = 1;
f.inside = 7;
image

內7,邊緣1.

邊緣因子也有可能大於內部因子。例如,將邊緣因子設置爲7,而將內部因子設置爲1。

f.edge[0] = 7;
f.edge[1] = 7;
f.edge[2] = 7;
f.inside = 1;
image

內1 邊緣7.

在本例中,內部因子被強制執行爲2,由於不然不會生成新的三角形。

2.3 配置因子屬性

硬編碼很差,增長可配置屬性.

float _TessellationUniform;
…
TessellationFactors MyPatchConstantFunction (
    InputPatch<TessellationControlPoint, 3> patch
) {
    TessellationFactors f;
    f.edge[0] = _TessellationUniform;
    f.edge[1] = _TessellationUniform;
    f.edge[2] = _TessellationUniform;
    f.inside = _TessellationUniform;
    return f;
}

增長1–64範圍限制.

_TessellationUniform ("Tessellation Uniform", Range(1, 64)) = 1

GUI擴展.

void DoTessellation () {
    GUILayout.Label("Tessellation", EditorStyles.boldLabel);
    EditorGUI.indentLevel += 2;
    editor.ShaderProperty
    (
        FindProperty("_TessellationUniform"),
        MakeLabel("Uniform")
    );
    EditorGUI.indentLevel -= 2;
}

在渲染模式和線框部分之間調用OnGUI中的這個方法。僅當所需屬性存在時才執行此操做.

public override void OnGUI (
    MaterialEditor editor, MaterialProperty[] properties
) {
    …
    DoRenderingMode();
    if (target.HasProperty("_TessellationUniform")) {
        DoTessellation();
    }
    if (target.HasProperty("_WireframeColor")) {
        DoWireframe();
    }
    …
}
image

可配置.

tesselation_integer

2.4 增長分數模式

儘管咱們使用了一個浮點數來設置因子,但咱們老是會以每條邊的等效細分結束。這是由於咱們使用的是整數模式。雖然這是一個很好的模式,看看鑲嵌如何工做,它阻止咱們平滑過渡之間的細分級別。幸運的是,還有分式劃分模式。咱們把模式改成fractional_odd。

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("fractional_odd")]
[UNITY_patchconstantfunc("MyPatchConstantFunction")]
TessellationControlPoint MyHullProgram …

tesselation_odd1

分數奇數分配

當使用整個奇數因子時,fractional_odd分區模式產生的結果與整數模式相同。但在奇因子之間轉換時,額外的邊細分會被分離和增加,或者收縮和合並。這意味着邊再也不老是被分割成等長的段。這種方法的優勢是細分層之間的過渡如今是平滑的。

tesselation_odd2

分數偶數分配

3 Tessellation Heuristics

什麼因子是最完美的?這是個問題,但這個問題沒有一個單一的客觀答案。

3.1 邊緣優先

細分因子必須提供,能夠肯定每一個頂點的因子,而後對每條邊進行平均。這些因子被儲存在一個紋理中。在任何狀況下,給定一條邊的兩個頂點,用一個單獨的函數來肯定係數是很方便的。

float TessellationEdgeFactor (
    TessellationControlPoint cp0, TessellationControlPoint cp1
) {
    return _TessellationUniform;
}

使用這個函數來處理mypatchconstant函數內的邊緣因子。

TessellationFactors MyPatchConstantFunction (
    InputPatch<TessellationControlPoint, 3> patch
) {
    TessellationFactors f;
    f.edge[0] = TessellationEdgeFactor(patch[1], patch[2]);
    f.edge[1] = TessellationEdgeFactor(patch[2], patch[0]);
    f.edge[2] = TessellationEdgeFactor(patch[0], patch[1]);
    f.inside = _TessellationUniform;
    return f;
}

對於內因子,咱們只須要用邊因子的平均值.

f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) * (1 / 3.0);

3.2 基於邊緣長度

因爲邊因子控制細分多少原始三角形的邊,以這些邊的長度爲基礎的因子,這是有意義的。例如,咱們能夠指定所需的三角形邊長。若是咱們獲得的三角形邊比這個長,咱們應該把它們再除以所需的長度。爲此添加一個變量。

float _TessellationUniform;
float _TessellationEdgeLength;

還能夠添加屬性。咱們使用範圍從0.1到1,默認值爲0.5。這是世界空間單位。

_TessellationUniform ("Tessellation Uniform", Range(1, 64)) = 1
_TessellationEdgeLength ("Tessellation Edge Length", Range(0.1, 1)) = 0.5

咱們須要一個着色器功能,使它有可能在基於所有或基於邊緣之間切換。使用_TESSELLATION_EDGE關鍵字,在咱們的三次傳遞中添加必需的指令.

#pragma shader_feature _TESSELLATION_EDGE

接下來,在MyLightingShaderGUI中添加一個enum類型來表示鑲嵌模式。

    enum TessellationMode {
        Uniform, Edge
    }

GUI擴展

void DoTessellation () {
        GUILayout.Label("Tessellation", EditorStyles.boldLabel);
        EditorGUI.indentLevel += 2;

        TessellationMode mode = TessellationMode.Uniform;
        if (IsKeywordEnabled("_TESSELLATION_EDGE")) {
            mode = TessellationMode.Edge;
        }
        EditorGUI.BeginChangeCheck();
        mode = (TessellationMode)EditorGUILayout.EnumPopup(
            MakeLabel("Mode"), mode
        );
        if (EditorGUI.EndChangeCheck()) {
            RecordAction("Tessellation Mode");
            SetKeyword("_TESSELLATION_EDGE", mode == TessellationMode.Edge);
        }

        if (mode == TessellationMode.Uniform) {
            editor.ShaderProperty(
                FindProperty("_TessellationUniform"),
                MakeLabel("Uniform")
            );
        }
        else {
            editor.ShaderProperty(
                FindProperty("_TessellationEdgeLength"),
                MakeLabel("Edge Length")
            );
        }
        EditorGUI.indentLevel -= 2;
    }
image

邊緣模式

如今咱們必須調整TessellationEdgeFactor。當定義_TESSELLATION_UNIFORM時,肯定兩個點的世界位置,而後計算它們之間的距離。這是世界空間中的邊長。邊因子等於這個長度除以指望的長度.

float TessellationEdgeFactor (
    TessellationControlPoint cp0, TessellationControlPoint cp1
) {
    #if defined(_TESSELLATION_EDGE)
        float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
        float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
        float edgeLength = distance(p0, p1);
        return edgeLength / _TessellationEdgeLength;
    #else
        return _TessellationUniform;
    #endif
}
image

不一樣的四邊形尺度,相同的指望邊長.

由於咱們如今用邊的長度來肯定邊的鑲嵌因子,咱們能夠獲得每條邊的不一樣因子。你能夠看到這種狀況發生在四邊形上,由於對角線比其餘邊長。當對四邊形使用非均勻尺度,在一維中拉伸它時,這一點也變得很明顯。

 image拉伸quad.

要作到這一點,很重要的一點是,共享一條邊的patch最終都要爲那條邊使用相同的鑲嵌因子。不然,生成的頂點將不能沿着那條邊匹配,這會在網格中產生可見的缺口。在咱們的例子中,咱們對全部的邊使用相同的邏輯。惟一的區別是控制點參數的順序。因爲浮點數的限制,這在技術上可能會產生不一樣的因素,但差別將是很是小的,不會被注意到.

3.3 基於屏幕空間邊緣長度

雖然咱們如今能夠控制世界空間中的三角形邊長,但這與它們在屏幕空間中的顯示方式並不一致。tessellation的要點是增長更多的三角形時。因此咱們不想再細分那些看起來很小的三角形。因此讓咱們用屏幕空間的邊長來代替。

首先,改變邊長屬性的範圍。咱們將使用像素代替世界單位,因此5-100這樣的範圍更有意義。

_TessellationEdgeLength ("Tessellation Edge Length", Range(5, 100)) = 50

將世界空間計算替換爲屏幕空間計算。要作到這一點,這些點必須轉換到剪輯空間而不是世界空間。而後它們的距離在2D中肯定,使用它們的X和Y座標,除以它們的W座標,將它們投影到屏幕上。

//float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
//float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
//float edgeLength = distance(p0, p1);

float4 p0 = UnityObjectToClipPos(cp0.vertex);
float4 p1 = UnityObjectToClipPos(cp1.vertex);
float edgeLength = distance(p0.xy / p0.w, p1.xy / p1.w);
return edgeLength / _TessellationEdgeLength;

如今咱們在裁剪空間中獲得告終果,它是一個大小爲2的統一立方體,適合顯示。爲了轉換爲像素,咱們必須按像素的顯示大小進行縮放。實際上,因爲顯示器不多是方形的,爲了獲得最精確的結果,在肯定距離以前,咱們應該分別縮放X和Y座標。可是讓咱們經過簡單的縮放屏幕高度來看看它是什麼樣子.

return edgeLength * _ScreenParams.y / _TessellationEdgeLength;
image

相同的世界尺寸,不一樣的屏幕顯示尺寸.

們的三角形邊緣如今被細分根據他們是多大渲染。位置、旋轉和縮放都會影響到這一點。結果,Tessellation的數量改變時,事情在運動。.

3.4 基於視野距離

純粹依賴於邊的視覺長度的一個缺點是,在世界空間中很長的邊在屏幕空間中可能會很是小。這可能致使這些邊根本沒有被細分,而其餘邊被細分不少,當曲面細分被用來增長近距離的細節或生成複雜的輪廓。

另外一種方法是使用世界空間的邊緣長度,可是根據視圖距離調整因素。越遠的東西,它應該在視覺上顯得越小,所以它須要的鑲嵌就越少。用邊長除以邊長到攝像機的距離。咱們能夠用邊的中點來肯定這個距離。

//float4 p0 = UnityObjectToClipPos(cp0.vertex);
//float4 p1 = UnityObjectToClipPos(cp1.vertex);
//float edgeLength = distance(p0.xy / p0.w, p1.xy / p1.w);
//return edgeLength * _ScreenParams.y / _TessellationEdgeLength;

float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
float edgeLength = distance(p0, p1);

float3 edgeCenter = (p0 + p1) * 0.5;
float viewDistance = distance(edgeCenter, _WorldSpaceCameraPos);
return edgeLength / (_TessellationEdgeLength * viewDistance);

咱們仍然能夠保持Tessellation依賴於顯示大小,經過簡單地分解屏幕高度,並保持咱們的5-100滑塊範圍。注意,這些值再也不直接對應於顯示像素。當你改變相機的視場時,這一點是很是明顯的,這根本不會影響Tessellation。因此這種簡單的方法並不適用於使用可變視場的遊戲,例如放大和縮小。

return edgeLength * _ScreenParams.y / (_TessellationEdgeLength * viewDistance);
image

基於邊緣長度和視圖距離.

3.5 基於內部因子

雖然Tessellation可能看起來在這一點上工做得很好。當使用一個統一的四邊形時,它不是很明顯,可是當使用一個變形的立方體時,它就變得很明顯。 image內部因素不正確的立方體.

在立方體的狀況下,組成一個面的兩個三角形各有一個很是不一樣的內部因子。四邊形和立方體面之間的惟一區別是定義三角形頂點的順序。Unity的默認立方體不使用對稱的三角形佈局,而quad使用對稱的三角形佈局。這代表,邊緣的順序明顯影響內部因子。咱們只是取邊緣因子的平均值,因此它們的順序不重要。必定是別的什麼地方出了問題。

在計算內部因子時再次顯式調用TessellationEdgeFactors函數。從邏輯上講,這不該該有什麼區別,由於咱們只是執行了兩次徹底相同的計算。着色器編譯器確定會優化它.

//f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) * (1 / 3.0);
f.inside =
    (TessellationEdgeFactor(patch[1], patch[2]) +
    TessellationEdgeFactor(patch[2], patch[0]) +
    TessellationEdgeFactor(patch[0], patch[1])) * (1 / 3.0);
image

內部因子正確的立方體.

顯然,這確實有效果,由於兩面三角形如今最終都使用幾乎相同的內部因子。這裏發生了什麼?

patch常量函數與Hull着色器的其他部分並行調用。但實際上它能夠變得更復雜。着色器編譯器也可以並行化邊緣因子的計算。MyPatchConstantFunction內部的代碼被分解和部分複製,並被一個並行計算三個邊緣因子的分支進程替換。一旦這三個過程都完成了,他們的結果被結合起來並用來計算內部因素。

它不影響咱們的着色器的結果,只會影響它的性能。不幸的是,在OpenGL Core生成的代碼中有一個bug。在計算內部因子時,不使用三個邊因子,而只使用第三個邊因子。它只是訪問了索引2三次,而不是索引0,1和2。因此咱們老是獲得一個內因子等於第三條邊因子。

對於patch常數函數,着色器編譯器將並行化設置爲優先級。它會盡快拆分,而後沒法再優化TessellationEdgeFactor的重複調用。咱們以三個程序結束,每一個程序計算兩個點的世界位置、距離、最終因子。而後還有一個計算內部因子的程序,如今它還計算三個點的世界位置,以及所涉及的全部距離和因素。因爲咱們如今正在對內部因子進行全部工做,所以對邊緣因子也單獨完成部分工做是沒有意義的。

事實證實,若是咱們首先計算這些點的世界位置,而後分別對邊緣和內部因子計算TessellationEdgeFactor,則着色器編譯器將決定不爲每一個邊緣因子分開單獨的程序。咱們最終獲得了一個能夠所有計算的流程。在這種狀況下,着色器編譯器確實優化了TessellationEdgeFactor的重複調用。

float TessellationEdgeFactor (float3 p0, float3 p1) {
    #if defined(_TESSELLATION_EDGE)
//        float3 p0 = mul(unity_ObjectToWorld, cp0.vertex).xyz;
//        float3 p1 = mul(unity_ObjectToWorld, cp1.vertex).xyz;
        …
    #else
        return _TessellationUniform;
    #endif
}

TessellationFactors MyPatchConstantFunction (
    InputPatch<TessellationControlPoint, 3> patch
) {
    float3 p0 = mul(unity_ObjectToWorld, patch[0].vertex).xyz;
    float3 p1 = mul(unity_ObjectToWorld, patch[1].vertex).xyz;
    float3 p2 = mul(unity_ObjectToWorld, patch[2].vertex).xyz;
    TessellationFactors f;
    f.edge[0] = TessellationEdgeFactor(p1, p2);
    f.edge[1] = TessellationEdgeFactor(p2, p0);
    f.edge[2] = TessellationEdgeFactor(p0, p1);
    f.inside =
        (TessellationEdgeFactor(p1, p2) +
        TessellationEdgeFactor(p2, p0) +
        TessellationEdgeFactor(p0, p1)) * (1 / 3.0);
    return f;
}
相關文章
相關標籤/搜索