基於UE4/Unity繪製地圖基礎元素-線(下篇)

基於UE4/Unity繪製地圖基礎元素-線(上篇)程序員

前言

上篇中記錄了繪製線的基本流程,而下篇主要是對繪製線中遇到的性能和效果問題進行闡述。在繪製完一條線而且但願給其加上描邊樣式時,會遇到不可避免的閃爍問題。而在繪製大量的交錯道路時,須要同時考慮繪製性能和閃爍問題如何解決。本文總結了高效繪製描邊線的方法,並對調研過的解決Z-Fighting閃爍的方案進行闡述。編程

像素圓角渲染的性能優化

在上篇中介紹了逐像素剔除產生圓角的方法,歸納的來講,爲了達到動態圓滑的目的,將原來CPU中的數學計算移入了片元着色器中進行。這樣作雖然能獲得最圓滑的效果,卻也給GPU帶來了壓力。以圓角線帽代碼爲例,受GPU處理方式影響,動態分支的if/else指令須要被所有執行,同時discard指令也會影響GPU的Early Z優化,兩者都會對性能產生影響。segmentfault

fixed4 frag (v2f i) : SV_Target
{
    if(i.geometryInfo.x < 0)  // 起點側線帽
    {    
        if(dot(float2(i.geometryInfo.x, i.geometryInfo.y), float2(i.geometryInfo.x, i.geometryInfo.y)) > 1)
        {   
            discard; // 距離圓心距離大於1則剔除
        }
    } 
    else if(i.geometryInfo.x > 1) // 終點側線帽
    {
        if(dot(float2(i.geometryInfo.x - 1, i.geometryInfo.y), float2(i.geometryInfo.x - 1, i.geometryInfo.y)) > 1)
        {   
            discard; 
        }
    }

    return i.color;
 }

所以在片元着色器中指令的性能優化上,主要是將其邏輯改成線性,移除動態分支,並以Alpha Blending代替discard。簡化流程的主要工具是CG標準函數step/clamp/lerp,其定義以下,靈活運用這些函數就能夠規避動態分支。性能優化

簡化流程後的片元着色器代碼以下,經過消除動態分支語句和discard指令減小性能開銷,犧牲部分代碼的可讀性,但提高了並行效率。其中爲了肯定像素是否屬於線帽構造了二次函數,實際上也能夠構造其餘類型的函數達到目的。app

fixed4 frag (v2f i) : SV_Target
{
    fixed4 clearColor = 0;
    fixed  isClear = 0;
    
    fixed origin = clamp(i.geometryInfo.z, 0 ,1);  // 兩側線帽x值收縮到0和1
    fixed4 isCap = step(0, origin * (origin - 1)); // 構建二值函數,線帽爲1,線段爲0
    fixed2 dist = fixed2(i.geometryInfo.z - origin, i.geometryInfo.w); // 構建距離向量
    isClear = step(1, dot(dist, dist)) * isCap; // 距離小於1(不須要剔除)爲0,距離大於等於1(須要剔除)且是線帽像素,則爲1
    
    return lerp(i.color, clearColor, isClear);
 }

繪製線的描邊

根據上篇完成一條線的繪製後,爲了使線易於觀察,一般須要使得線具備描邊樣式。實際上,上篇中展現的線已經爲了美觀都帶上了描邊,但要讓線有描邊部分還須要進行額外的繪製。ide

爲了減小頂點數增長並簡化三角剖分的計算,一般是在繪製的填充線之下使用描邊線寬進行一次一樣的擴展繪製,描邊線寬構造產生的面更大,使得兩個線構成的面疊加展現就能夠達到線描邊的效果。這種方案的描邊寬度爲(sideLineWidth - lineWidth) / 2 。函數

描邊線的基本原理如上所述,而在實際的繪製中能夠針對填充線和描邊線的特性,對渲染邏輯進行優化。在實踐中主要進行了如下探索:工具

一、提取變化點

能夠看到描邊線和填充線在繪製時的擴展方向是同樣的,差異在於根據擴展向量擴展的線寬不一樣。所以能夠將擴充頂點的計算抽離到頂點着色器中並行進行,數據處理時只計算擴充的基準向量,將其和線寬信息藉助uv結構一同傳入shader中,這樣兩部分的線就能夠複用同一個Shader進行渲染。但兩部分的線仍須要分兩次進行繪製,消耗兩個Draw Call。性能

二、從數據上改進爲一個Draw Call調用

基於頂點着色器的思考,兩個線的繪製只有頂點位置和顏色的不一樣,所以能夠模擬Batching操做,將兩條線的mesh數據進行合併,就能夠在一個Draw Call調用進行繪製。能夠看到,在兩個mesh的合併過程當中只須要對三角形索引根據頂點數進行調整,其他的數據均可以直接合並。優化

public LineMesh CombineLineMesh(LineMesh appendMesh)
{
    int index = this.vertices.Count;
    for (int i = 0; i < appendMesh.triangles.Count; ++i)
    {
        appendMesh.triangles[i] += index;
    }

    this.triangles.AddRange(appendMesh.triangles);
    this.vertices.AddRange(appendMesh.vertices);
    this.color32s.AddRange(appendMesh.color32s);
    this.geometrys.AddRange(appendMesh.geometrys);
    this.parameters.AddRange(appendMesh.parameters);

    return this;
}

三、從繪製方式上改進爲一個Draw Call調用

雖然探索2中已經達到了一個Draw Call進行渲染,可是描邊線和填充線是使用兩組頂點進行的渲染,本着能省則省的精神,爲了減小頂點數,能夠考慮在一組頂點中,根據描邊線寬和填充線寬的比例信息,一次性繪製出整個線。這種作法須要利用上篇文章中爲了繪製圓角引入的geometry信息,x信息能夠標識長度,而y值就能夠做爲寬度方向上的標識。若定義ratio爲線寬的比值,則可根據片元着色器中y值的分佈肯定渲染顏色。

ratio = lineWidth / sideLineWidth
abs(y)∈[0,ratio] -> color
abs(y)∈(ratio,1] -> sideColor

這個方案能夠只使用一組頂點繪製完描邊線,但也存在一些問題:

一、在線帽和拐角的圓角支持上須要相似同心圓的繪製邏輯,須要再引入額外的條件判斷,對邏輯複雜度和性能都有影響。

二、在繪製大量相互交錯的線時,線的壓蓋順序須要動態的去調整,會遇到一部分交錯線的全部填充部分要壓蓋全部描邊部分,而一次性繪製的線是沒法支撐這一效果的。

綜上,從繪製方式上的改進有其侷限性,探索2的繪製方式更爲合適。

解決閃爍Z-fighting問題

繪製方案肯定之後,在繪製時遇到的下一個問題就是線的Z-fighting問題,即觀察時線一直在閃爍。其緣由是描邊線和填充線重疊部分所在的世界座標徹底一致,座標轉換後受深度緩衝精度影響致使片元在渲染時無序經過深度檢測,最終表現爲面的閃爍問題。

Z-fighting問題算是繪製線的最後一個障礙,其中涉及許多圖形學的基礎知識,在探索解決方案的過程當中也對渲染的全流程有了更多的認識,探索的方案總結以下:

一、調整頂點的世界座標

解決Z-fighting問題的第一步是定位出深度值衝突的對象。在繪製帶描邊的線這個場景中,致使閃爍的緣由是描邊線和填充線的重疊部分世界座標高度值一致,致使座標轉換後片元深度值一致。所以能夠在衝突的面的高度值上增長一點兒偏移,經過改變局部座標影響轉換後的深度值,最終能夠看到閃爍現象消失。

根據前面的討論,修改局部座標的操做能夠放在Shader中並行進行,以Unity爲例,經過設置一個priority變量用於微調頂點y方向的偏移,從而控制顯示的優先級。

fillLineMesh.priority = 1;

v2f vert (a2v v)
{
    v2f o;
    float4 pos = v.vertex + float4(v.parameter.x, 0, v.parameter.y, 0) * v.parameter.z; // 根據向量和線寬計算實際頂點位置
    pos += float4(0, priority / 100, 0, 0); // 頂點y方向進行微調,須要把握微調大小
    
    o.pos = UnityObjectToClipPos(pos); 
    o.color = v.color;
    o.geometry = v.geometry;

    return o;
}

這種方式能暫時解決閃爍問題,但在將攝像頭位置拉遠後仍會出現。其緣由是深度緩衝的精度有限,所以距離攝像頭越遠須要的偏移量越大,微調的偏移量須要根據頂點和攝像頭的距離動態調控。在實際操做中,視線方向與頂點微調方向多數狀況下並不相同,而在解決大量線重疊的Z-fighting時,大量偏移的累加可能會從視覺上觀察到線不共面,與全部線在同一平面的地圖展現方式不符,所以方案一一般僅做爲初步驗證Z-fighting緣由的工具。

二、使用Offset指令

Unity ShaderLab提供了微調偏移的Offset指令,指令定義和計算公式以下:

Offset Factor, Units
offset = m * factor + r * units

其中m是由系統計算出的多邊形深度斜率的最大值,多邊形越是與近裁剪面平行,m就越接近於0,r是深度值可分辨的最小單位,是由系統指定的常量。若多邊形與裁剪面平行,則可使用factor=0,units=1的組合控制偏移,而對於與裁剪面有夾角的多邊形,須要factor一同控制偏移量的大小,Offset結果大於0會使得多邊形遠離近裁剪面進行偏移,具體的參數值須要實踐過程當中進行摸索確認。

使用Offset指令做用於裁剪空間的深度值能夠解決多個Object之間的Z-fighting問題,但當爲了減小Draw Call將全部線合併爲一個mesh後就沒法使用了,所以須要藉助於其原理手動調控同一mesh中不一樣線的深度信息。

三、調整頂點的裁剪座標

深度信息是在片元着色器以後計算獲得的,所以沒法經過着色器的可編程部分直接更改。但深度信息是由裁剪空間的齊次座標計算而來,所以能夠經過操控裁剪空間座標達到調整深度的目的。

在光柵化以前,座標會進行模型-視圖-投影變換由局部座標轉換爲裁剪座標,其中由觀察空間經由投影矩陣變換獲得的就是裁剪空間齊次座標,其後轉換爲屏幕空間獲得的NDC座標z值由齊次座標的z/w得來,決定了深度值。由觀察空間座標轉換爲裁剪座標須要如下參數:

f:遠裁剪面

n:近裁剪面

fov:視角

aspect:攝像機橫縱比

設觀察空間座標爲

則轉換到裁剪空間座標爲:

根據深度值規則,在裁剪座標z值上添加-z*offset的偏移便可將深度向後微調offset大小。在UE4的material中,也能夠經過調整Pixel Depth Offset達到偏移的效果。

v2f vert (a2v v)
{
    v2f o;
    o.pos = float4(UnityObjectToViewPos(float3(v.vertex.xyz)), 1.0);
    float z = o.pos.z;
    o.pos = mul(UNITY_MATRIX_P,  o.pos);
    o.pos.z = o.pos.z - z * v.parameter.z/1E8;// 使用parameter.z存儲頂點偏移信息

    return o;
}

四、調整深度檢測

上述方案都是經過在不一樣的面之間構造微小偏移來解決Z-fighting問題,而另外一種思路是不增長偏移,經過指定渲染時的壓蓋規則,先繪製的面被後繪製的面壓蓋,最終顯示出正確的圖像。這種方案須要首先理解深度檢測的概念。

深度檢測在片元着色器以後進行,每一個片元攜帶自身的深度值與深度緩衝內的深度值進行比較檢測,若檢測經過,深度緩衝內的值將被設爲該深度值。若檢測失敗,則丟棄該片元。Unity ShaderLab使用ZWrite和ZTest兩個指令控制這一過程:

  • ZWrite控制檢測經過後,是否將片元深度寫入深度緩衝,默認開啓(ZWrite On)
  • ZTest定義深度值經過深度檢測的規則,默認是當片元深度值小於等於深度緩衝內的深度值時經過深度檢測(ZTest LEqual)

在繪製二維地圖這一case中,不須要更改深度緩衝的寫入策略,只須要將深度檢測的策略改成所有經過便可:

ZWrite On
ZTest Always

小結

對於閃爍問題,前三個探索方案核心都是構造微小偏移,若fighting的面數過多,形成微小偏移大量疊加產生量變,可能會對圖形的透視顯示大小產生影響,這時推薦使用方案四。而對於多Object的狀況,能夠搭配方案二與方案四共同使用,效果更佳。

至此,已經解決了繪製線的全部問題,下圖使用各類純色進行了道路線繪製,若是效果不滿意,還能夠嘗試進行紋理貼圖,使得道路線更加酷炫。

做者:程序員阿Tu

連接:https://zhuanlan.zhihu.com/p/...

來源:知乎

著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索