Unity中UI曲面化

姜進發表於2017-09-20

在VR下面,曲面UI可以提升使用者在場景中的沉浸感,獲得更好的視覺體驗

方案選擇

  1. 做一套基於曲面的UI
    我們專案中基本只用到Image和Text兩種,Image是比較好處理的,直接將Texture貼到一個曲面的mesh上就可以了,但是Text相對比較麻煩。我們無法簡單的取到某一段文字的Texture,必須自己從字型檔案裡面裁剪每個文字的Texture,然後拼接到一個Texture,然後再將最終的紋理貼到一個曲面mesh上。

  2. 單獨將平面UI渲染到一個紋理,然後貼到曲面mesh上
    這個方案在實現曲面化上是沒有問題的,但是為了滿足VR下面的立體效果,我們兩隻眼睛看到的東西是有一定差異的,通過這些差異才有立體感,如果是同一個曲面的UI Texture,UI上的一些立體效果會損失(比如UI向前浮動)。另外,如果使用這種方法,我們眼睛看到的UI和真實的UI有很大的差異,需要重新設計凝視輸入。

  3. 讓UI沿著曲面分佈,且每個UI元素有曲面效果
    讓UI沿著曲面分佈是比較好實現的,只需要計算合理的位置和角度,讓UI整體上呈現出曲面的效果。目前有很多VR的曲面效果都是採用這種簡單的方法實現的,但是這種方案實現的曲面效果並不是很好,是一種假曲面化的效果。如下圖所示,在上下邊緣可以看到很明顯的直線。

    UI元素沿著曲面分佈
    UI元素沿著曲面分佈

    所以在沿著曲面分佈的前提下,如果每個”UI元素都有曲面效果“,那麼整體上才會看出曲面效果。如下圖所示
    UI元素都有曲面效果
    UI元素都有曲面效果

結合實現難度和效果,我們選擇了方案 3

曲面化中的數學原理

無論是讓UI沿著曲面分佈,還是實現每個UI自身的曲面效果,實質上都是做同一種數學運算——計算平面上的點對映到曲面上的座標。
由於我們曲面化是一個圓柱面,圓柱軸心線與Y軸平行,變化前後Y軸座標是一樣的,下面是原理:

掘金不支援數學公式
掘金不支援數學公式

幾何示意圖
幾何示意圖

其中的關鍵點是變換前後的長度對應關係弧長與半徑的比是角度。上面給出的過程是一種特殊情況,實際過程中會有些變化,比如圓心不在原點,但是都是可以通過以上的方法推匯出來結果。

曲面化

讓UI沿著曲面分佈

對每個UI計算曲面化之後的座標,上面已經給出了計算方法,需要注意的是,計算玩座標之後還需要調整UI的角度,讓UI的前方是圓心到變換之後座標的方向。比如上面變換之後位於B點的UItransform.forward = transform.position.normalized
到了這一步,整體的UI就有了上面所說的假曲面化的效果。

每個UI元素的曲面效果

Unity提供了BaseMeshEffect對UI元素生成的mesh做一些修改來實現一些效果,不同Unity版本這個API有些差異,這裡用到的是Unity5.3.4,主要是重寫ModifyMesh(VertexHelper vh)方法。
ModifyMesh方法主要內容:

public override void ModifyMesh(VertexHelper vh)
{
        base.ModifyMesh(vh);
        if (!this.IsActive() || !bendEnable)
            return;

        /* 
        檢查是否需要重新生成或修改頂點座標,如果不需要,則使用已經快取的頂點座標 
        */

        if (cachedVertices == null || cachedTriangles == null || verticesDirty)
        {
            // 需要修改頂點,首先將Unity生成的頂點取出來
            List<UIVertex> originUIVertices = new List<UIVertex>();
            vh.GetUIVertexStream(originUIVertices);
            /*
            對頂點做一些變換,包括增加頂點以及重新計算頂點座標,對於Image和Text有不同的處理方式
            */
        }

        // 如果材質改變,重新給定點著色
        if (materialDirty)
        {
            UpdateVertiecsColor(cachedVertices);
            materialDirty = false;
        }
        // 清除Unity生成的頂點,將我們重新計算的頂點設定到mesh上
        vh.Clear();
        vh.AddUIVertexStream(cachedVertices, cachedTriangles);
        // 根據生成頂點的型別也可以使用vh.AddUIVertexTriangleStream(cachedVertices)設定頂點
}複製程式碼

1. 檢查頂點是否需要更新

當Unity發現UI需要更新的時候會呼叫ModifyMesh(VertexHelper vh),Unity自己觸發UI更新的條件有尺寸改變和材質改變,我們也可以使用Graphic.SetAllDirty() Graphic.SetVerticesDirty() 觸發。但是並非所有的情況下都需要重新計算頂點座標,當我們計算出一個UI的曲面狀態下的頂點之後,很少需要重新計算,我們只在UI尺寸改變的情況下才觸發重新計算頂點,當然可以根據實際使用情況調整策略。頂點計算比較耗時,建議先判斷在計算。

2. 取出頂點

取出來的頂點是一個UIVertex的列表,一般情況下,列表中每3個構成一個三角形,如果改變列表中元素的位置,會導致UI顯示異常,所以最後輸出給VertexHelper的頂點也是有順序的。

3. 頂點計算

這一步中對於Text和Image有較大差異,主要原因在於一般的Text自己都有足夠多且細分的三角形,只需要重新計算頂點的座標就可以有很好的曲面效果,但是一般情況下,Image只有兩個三角形(Sliced模式下Tiled模式會多一些,但是依然不夠細分),四個頂點,對四個頂點重新計算之後依然是一個平面的效果,所以需要考慮給Image的mesh新增一些頂點,讓Image上的三角形足夠細分。

對於Text的處理
cachedVertices = new List<UIVertex>();
vh.GetUIVertexStream(cachedVertices);
BendMeshCylinder(cachedVertices);複製程式碼

其中的BendMeshCylinder(cachedVertices)函式就是將傳入的頂點變換到圓柱曲面上,需要注意的是UIVertex裡面的座標是相對UI自身的區域性座標。處理過程不改變UIVertex列表的順序,處理完的依然保持之前的三角形順序,所以最後直接使用vh.AddUIVertexTriangleStream(cachedVertices)設定頂點。

對於Image的處理

一般情況下的Image只需要4個頂點就可以構成兩個三角形,但是從VertexHelper裡面取出來的頂點有6個,每三個構成一個三角形,重複使用了其中的兩個頂點,如下圖所示,第0,1,2和3,4,5分別構成一個三角形,0和5為同一個頂點,2和3為同一個頂點。


最初的想法是忽略Unity自己生成的頂點,直接在程式碼中根據原始頂點的規律生成一個足夠細分的mesh,然後對所有頂點採用和Text裡面相同的計算就可以有曲面效果,如下圖所示,因為我們只需要圓柱曲面效果,所以在Y軸方向不需要細分,這樣能大大減少三角形的數量。

這種方案一般情況下是夠用的,但是當遇到Sliced或者Tiled模式的Image的時候就會有問題:當我們計算新增頂點座標的時候,需要給頂點指定一個uv值,這個值將決定圖片渲染在這一點的uv,普通圖片的uv值是從0到1的均勻分佈,所以直接根據新計算的座標在整個Image上的位置就知道uv值,但是Sliced和Tiled的uv不是0到1的均勻分佈,根據座標是無法直接算出uv值的。比如下圖是一個Sliced模式的mesh,編號為1的頂點Y軸上是整個Image高度的0.1,但是uv中v(Y軸)的值使0.3,如果我們忽略這些值,直接生成均勻分佈的點,Sliced的特性就沒有了

所以需要在保持Unity計算出來的頂點,然後再在這些頂點的基礎上進行線性插值計算新增的頂點。程式碼如下:

List<UIVertex> originUIVertices = new List<UIVertex>();
vh.GetUIVertexStream(originUIVertices);
TrisToQuads(originUIVertices);
for (int i = 0; i < originUIVertices.Count; i += 4)
{
    CreateQuads(originUIVertices, i, cachedVertices, cachedTriangles);
}
BendMeshCylinder(cachedVertices);複製程式碼

首先在TrisToQuads(originUIVertices)裡面是將每六個頂點構成的兩個三角形合併為四個頂點構成的四邊形,然後在CreateQuads(originUIVertices, i, cachedVertices, cachedTriangles)裡面對每個四邊形內部進行線性插值計算新增頂點,每個四邊形內部點的uv都可以根據座標在四邊形內部的位置計算出來。如下圖所示,紅色點為新增頂點,當四邊形在X軸方向足夠細分,就不需要再新增頂點,通常情況下Sliced模式的圖片邊緣是足夠細分的。


我們發現,四個頂點就足以描述兩個三角形,但是需要知道兩個三角形與四個頂點的對應關係,三角形更多的情況下可以通過頂點的複用使得頂點數量更少,比如上圖中有36個三角形,但是隻有28個頂點,我們需要快取曲面變化之後的頂點,避免後面的重複計算,所以採用頂點複用可以讓我們快取的頂點數量大大的減少,但是我們需要快取一個頂點與構成的三角形的對應關係的int列表,這個列表中每三個數描述一個三角形,數值對應著快取的頂點列表的索引。頂點生成完成之後,和Text中一樣對頂點的座標進行曲面變換。最後我們設定頂點的時候需要告訴VertexHelper頂點與三角形的對應關係,vh.AddUIVertexStream(cachedVertices, cachedTriangles)

4. 改變頂點的顏色

如果改變UI的顏色屬性,會觸發MaterialDirty,我們可以通過Graphic.RegisterDirtyMaterialCallback監聽這個改變,然後在ModifyMesh()改變頂點的顏色。另外不建議監聽Graphic.RegisterDirtyVerticesCallback來確定是否需要重新計算頂點,因為改變頂點顏色,這個回撥也會呼叫。


曲面化的原理如上,如果要真正運用起來,還需要配合一個Editor。需要注意的是,最好不要在曲面化狀態調整UI的座標和角度,不僅很難調整到想要的位置,而且會影響整體曲面化的效果。
文章裡面主要聚焦圓柱面,如果是球面,主要要修改UI座標的計算方法,和Image三角形細化方法,如果是其他更復雜的曲面,不太建議用這種方式處理,因為涉及到曲面的資料計算,效果也很難保證。

相關文章