天天看點

使用Shader進行UGUI的優化

1. 前言 近期斷斷續續地做了一些優化的工作,包括資源加載、ui優化、效果分級各個方面。優化本身是一件瑣碎且耗神的事情,需要經曆問題定位、原因探查、優化方案設計和實作、效果驗證、資源修改多個步驟,也會涉及到各個職位之間的配合和協調。在這其中,可能帶來較大工作量的是對于之前普遍使用的一些方法/控件的優化,如果無法相容之前的使用接口,可能會給美術和程式帶來較大的疊代工作量。

UI是這其中可能越早發現問題收益越高的一塊内容,是以整理一下這段時間做了一些基于Shader來進行優化的思路和方法,以及分享一下自己建構的代替ugui提供的通用控件的那些Component,希望在項目中前期的同學可以提前發現類似的問題進行盡早的改進。

2. 優化目标

ugui已經提供了非常豐富的控件來給我們使用,但是出于通用性的考慮,其中很多控件的性能和效果都可能存在一些問題,又或者在頻繁更改ui數值的需求下會引發持續的Mesh重建導緻CPU的消耗。我們期望通過一些簡單的Shader邏輯來提升效果或者提高效率,主要會集中在如下幾個方面:

  • 降低Draw Call;
  • 減少Overdraw;
  • 減少UI Mesh重建次數和範圍。

接下來的内容,我們就從具體的優化内容上來分享下使用簡單的Shader進行UGUI優化的過程。

3. 小地圖

在我們遊戲中,玩家移動的時候右上角會一直有小地圖的顯示,這個功能在最初的實作方案中是使用ugui的mask元件來做的,給了一個方形的mask元件,然後根據玩家位置計算出地圖左下角的位置進行移動。這種實作方式雖然簡單,但是會有兩個問題:

  • Overdraw特别大,幾乎很多時候會有整個螢幕的overdraw;
  • 玩家在移動過程中,因為一直在持續移動圖檔的位置(做了适當的降頻處理),是以會一直有UI的Mesh重建過程。

當時的prefab已經被修改了,我簡單模拟一下使用Mask的方法帶來的Overdraw的效果如下圖所示:

使用Shader進行UGUI的優化

使用Mask元件帶來的Overdraw的問題

在上圖中可以看到,左側是小地圖在螢幕中的效果,右側是選擇Overdraw視圖之後的效果,整張圖檔都會有一個繪制的過程,占據幾乎整個螢幕(白框),而且Mask也是需要一次繪制過程,這樣就是兩個Drawcall。其實這裡ui同學為了表現品質感,在小地圖上又蒙了一層半透的外框效果,消耗更大一些。

針對這一問題,首先對于矩形的地圖,可以使用運作效率更高一些的RectMask2D元件,但這并不能有本質的提升,解決Overdraw最根本的方法還是不要繪制那麼大的貼圖然後通過蒙版或者clip的方式去掉,這是很浪費的方法。有過基本Shader概念的朋友應該可以想到修改uv的方法,這也是我們采用的方法——思路很簡單,就做一個和要顯示的大小一樣的RawImage控件,然後賦給它一個特殊的材質,在vs裡面修改要顯示的區域的uv就可以做到想要的效果。

直接貼出來Shader代碼如下:

[AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23

sampler

2

D _MainTex;

fixed

4

_UVScaleOffset;

sampler

2

D _BlendTexture;

v

2

f vert

(

appdata_t IN

)

{

v

2

f OUT;    

OUT.vertex

=

mul

(

UNITY_MATRIX_MVP

,

IN.vertex

)

;    

OUT.texcoord

=

IN.texcoord;

/

/

計算uv偏移

OUT.offsetcoord.xy

=

OUT.texcoord.xy

*

_UVScaleOffset.zw

+

_UVScaleOffset.xy;

#ifdef UNITY_HALF_TEXEL_OFFSET    

OUT.vertex.xy

-

=

(

_ScreenParams.zw

-1.0

)

;    

#endif    

return

OUT; 

}

fixed

4

frag

(

v

2

f IN

)

:

SV_Target    

{

half

4

color

=

tex

2

D

(

_MainTex

,

IN.offsetcoord

)

;

half

4

blendColor

=

tex

2

D

(

_BlendTexture

,

IN.texcoord

)

;

color.rgb

=

blendColor.rgb

+

color.rgb

*

(

1

-

blendColor.a

)

;

return

color;

}

核心的代碼就隻有加粗的那一句,給uv一個整體的縮放之後再加上左下角的偏移。之後C#邏輯就隻需要根據地圖的大小和玩家所在的位置計算出想要顯示的uv縮放和偏移值就可以了。玩家移動的時候隻需要修改材質的參數,這也不會導緻UI的mesh重建,一箭雙雕,解決兩個問題。

小地圖的外框也在材質中一并做了,減少一個draw call。最終的效果如下圖所示:

使用Shader進行UGUI的優化

優化後的Overdraw對比圖

這裡需要注意的是,對于image控件的material進行指派時,如果它在一個Mask控件之下,可能會遇到指派失效的問題,采用materialForRendering或者強制更新材質的方式可能會有新的Material的建立過程導緻記憶體配置設定,這些在優化之後可能帶來問題的點也是需要優化後進行驗證的。

4. Mask的使用 除了小地圖部分,遊戲中比如頭像、技能界面等處都大量地使用了Mask。當然通常情況下Mask不會帶來像小地圖那麼高Overdraw,但是因為ugui中的Mask需要一遍繪制過程,是以對于Drawcall的增加還是會有不少。而且Mask也存在邊緣鋸齒的問題,效果上UI同學也不夠滿意,是以我們針對像頭像這樣單張的Mask也進行了一下優化。

這裡補充兩點:

  • 在那篇文章的最後提到,我們自己拷貝了一個ThorImage類,開放部分接口然後繼承。我們後來改成了從Image直接繼承的方式,否則之前編寫的遊戲邏輯要在代碼上相容兩種Image,會比較煩,這些向前相容的需求也是在優化過程中需要額外考慮和處理的點。
  • 針對滾動清單這樣需要Mask的地方,一方面建議UI同學使用Rect Mask 2D元件,另外一方面為了邊緣的漸變效果為UI引入了Soft Mask 插件來提供邊緣的漸變處理。放一張Soft Mask自己的效果對比截圖,需要類似效果的朋友可以自己購買。
使用Shader進行UGUI的優化

Soft Mask 插件效果對比

UWA針對我們項目的深度優化報告裡提到:Soft Mask插件的Component在Disable邏輯裡有明顯的性能消耗,目前我們未開始針對這塊進行優化,不過隻在ui關閉的時候才有,是以優先級也比較低,想嘗試的朋友可以提前評估下性能。

5. 基于DoTween的動畫效果優化

在遊戲中,UI比較大量地使用了DoTween插件制作動畫效果來強調一些需要醒目提醒玩家的資訊。DoTween是一個非常好用的插件,無論是對于程式還是對于UI來說,都可以經過簡單的操作來實作較為好的動畫效果。

然而,對于UGUI來說,DoTween往往意味着持續的Canvas的重建,因為動畫通常是位置、旋轉和縮放的變化,這些都會導緻其所在的Canvas在動畫過程中一直有重建操作,比如我們遊戲中會有的如下圖所示的旋轉提醒的效果:

使用Shader進行UGUI的優化

這一效果在DoTween中通過不斷改變圖檔的旋轉來實作的,在我們profile過程中發現了可疑的持續canvas重建,最後通常會定位到類似這樣界面動畫的地方。使用Shader進行優化,隻需要把旋轉的過程拿到Shader的vs階段來做,同樣是修改uv資訊,材質代碼的vert函數如下:

[AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21

v

2

f vert

(

appdata_t IN

)

{

v

2

f OUT;    

OUT.vertex

=

mul

(

UNITY_MATRIX_MVP

,

IN.vertex

)

;

OUT.texcoord

=

IN.texcoord;

OUT.texcoord.xy

-

=

0.5

;

/

/

避免時間過長的時候影響精度

half t

=

fmod

(

_Time.y

,

2

*

UNITY_PI

)

;

t

*

=

_RotationSpeed;

half s

,

c;

sincos

(

t

,

s

,

c

)

;

half

2

x

2

rotationMatrix

=

half

2

x

2

(

c

,

-

s

,

s

,

c

)

;

OUT.texcoord.xy

=

mul

(

OUT.texcoord.xy

,

rotationMatrix

)

;

OUT.texcoord.xy

+

=

0.5

;

#ifdef UNITY_HALF_TEXEL_OFFSET    

OUT.vertex.xy

-

=

(

_ScreenParams.zw

-1.0

)

;    

#endif

OUT.color

=

IN.color;    

return

OUT; 

}

注意,這裡因為要求UI控件使用的是一張RawImage,是以uv的中心點就認為了是(0.5, 0.5)位置。通過參數可以做到從C#中傳遞uv的偏移和縮放資訊進而相容Image,但是因為材質不同導緻本來就無法合批,是以使用Image和Atlas帶來的優勢就沒有了,是以這裡隻簡單地支援RawImage。

Tips:注意所使用貼圖的邊緣處理,因為uv的旋轉可能會導緻超過之前0和1的範圍。首先貼圖的采樣方式要使用Clamp的方式,其次貼圖的邊緣要留出幾個像素的透明區域,保證即使在裝置上貼圖壓縮之後依然可以讓邊緣的效果正确。

另外使用材質進行優化的地方是自動尋路的提示效果:

使用Shader進行UGUI的優化

最初UI想使用DoTween來制作,但是覺得工作量有點大是以想找程式寫DoTween的代碼進行開發,為了減少ui的Canvas重建,使用材質來控制每一個字的縮放過程。同樣是在vert函數中針對uv進行修改即可:

[AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19

v

2

f vert

(

appdata_t IN

)

{

v

2

f OUT;

UNITY_SETUP_INSTANCE_ID

(

IN

)

;

UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO

(

OUT

)

;

/

/

根據時間和配置參數對頂點進行縮放操作

OUT.worldPosition

=

IN.vertex;

half t

=

fmod

(

(

_Time.y

-

_TimeOffset

)

*

_TimeScale

,

1

)

;

t

=

4

*

(

t

-

t

*

t

)

;

OUT.worldPosition.xy

-

=

_VertexParmas.xy;

OUT.worldPosition.xy

*

=

(

1

+

t

*

_ScaleRatio

)

;

OUT.worldPosition.xy

+

=

_VertexParmas.xy;

OUT.vertex

=

UnityObjectToClipPos

(

OUT.worldPosition

)

;

OUT.texcoord

=

IN.texcoord;

OUT.color

=

IN.color

*

_Color;

return

OUT;

}

Tips:這裡使用UWA群裡一位朋友之前提供的三角函數近似的方法來略微減少一下指令消耗:
使用Shader進行UGUI的優化

sin函數的近似模拟

這一效果的實作不像之前的旋轉效果那麼簡單,隻有Shader的修改就可以了。這裡需要額外處理的部分有如下幾點:

1. 逐個縮放的效果需要控制“自動尋路中...”這句話中的每一個字,是以在這個效果中每一個字都是一個Text控件。正在縮放的字使用特殊的縮放材質來繪制,其他的字依然使用預設的UI材質繪制,這意味着需要2個DrawCall來實作整體效果。

2. vert函數中需要擷取字型的中心點和長寬大小,然後進行縮放計算,也就是參數_VertexParmas的内容。經過測試,在ugui中,頂點的位置資訊worldPosition是其相對于Canvas的位置資訊,是以這裡需要在C#中進行計算,計算過程借助RectTransformUtility的ScreenPointToLocalPointInRectangle函數:

[AppleScript]  純文字檢視  複制代碼 ?

  1 2

Vector

2

tempPos;[

/

color][

/

size][

/

font

][

/

align][

font

=

微軟雅黑][size

=

3

][color

=

#2f4f4f]RectTransformUtility.ScreenPointToLocalPointInRectangle(ParentCanvas.transform as RectTransform,

ParentCanvas.worldCamera.WorldToScreenPoint

(

img.transform.

position

)

,

ParentCanvas.worldCamera

,

out tempPos

)

;

這裡的ParentCanvas是目前控件向上周遊找到的第一個Canvas對象。

Tips:這裡,搞清楚了vs中頂點的worldPosition對應的屬性之後,可以做很多有趣的事情,包括之前的旋轉效果也可以不再旋轉uv而是對頂點位置進行旋轉。

3. 關于_Time,它的y值與C#中對應的是Time.timeSinceLevelLoad。這裡為了實作界面一開始的時候第一個字是從0開始放大的,需要從C#傳遞給Shader一個起始時間,通過_Time.y - _TimeOffset 來確定起始效果。

最後直接貼一下C#部分的代碼好了,也很簡單,提供給UI配置縮放尺寸、縮放持續時間和間隔等參數,然後通過協程來控制字型的材質參數:

[AppleScript]  純文字檢視  複制代碼 ?

  001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

using

System;

using

System.Collections.Generic;

using

UnityEngine;

using

UnityEngine.EventSystems;

using

UnityEngine.UI;

using

DG.Tweening;

using

ThorUtils;

using

System.Collections;

namespace KEngine.UI

{

public

class

UIAutoExpand

:

MonoBehaviour

{

public float ScaleDuration

=

1

;

public float CycleInterval

=

5

;

public float ScaleRatio

=

0.5

f;

private Canvas ParentCanvas;

private Image[] images;

private Vector

4

[] meshSizes;

private int currentScaleIdx

=

-1

;

private Coroutine playingCoroutine;

private static Material UIExpandMat

=

null;

private static int VertexParamsID

=

-1

;

private static int TimeOffsetID

=

-1

;

private static int TimeScaleID

=

-1

;

private static int ScaleRatioID

=

-1

;

void Awake

(

)

{

/

/

初始化靜态變量

if

(

UIExpandMat

=

=

null

)

{

UIExpandMat

=

new

Material

(

Shader.Find

(

"ThorShader/UI/UIExpand"

)

)

;

VertexParamsID

=

Shader.PropertyToID

(

"_VertexParmas"

)

;

TimeOffsetID

=

Shader.PropertyToID

(

"_TimeOffset"

)

;

TimeScaleID

=

Shader.PropertyToID

(

"_TimeScale"

)

;

ScaleRatioID

=

Shader.PropertyToID

(

"_ScaleRatio"

)

;

}

UIExpandMat.SetFloat

(

TimeScaleID

,

1

/

ScaleDuration

)

;

UIExpandMat.SetFloat

(

ScaleRatioID

,

ScaleRatio

)

;

if

(

ParentCanvas

=

=

null

)

{

Transform trans

=

transform;

while

(

trans !

=

null

)

{

ParentCanvas

=

trans.GetComponent

<

Canvas

>

(

)

;

if

(

ParentCanvas !

=

null

)

{

break;

}

trans

=

trans.parent;

}

}

if

(

ParentCanvas

=

=

null || ParentCanvas.worldCamera

=

=

null

)

{

Debug.LogError

(

"The parent canvas of UIAutoExpand could not be empty!"

)

;

return

;

}

images

=

GetComponentsInChildren

<

Image

>

(

)

;

if

(

images.Length

>

)

{

meshSizes

=

new

Vector

4

[images.Length];

}

Vector

2

tempPos;

for

(

int i

=

; i

<

images.Length;

+

+

i

)

{

Image img

=

images;

tempPos.x

=

;

tempPos.y

=

;

RectTransformUtility.ScreenPointToLocalPointInRectangle

(

ParentCanvas.transform

as

RectTransform

,

ParentCanvas.worldCamera.WorldToScreenPoint

(

img.transform.

position

)

,

ParentCanvas.worldCamera

,

out tempPos

)

;

meshSizes.x

=

tempPos.x;

meshSizes.y

=

tempPos.y;

meshSizes.z

=

;

meshSizes.w

=

;

}

}

private void OnEnable

(

)

{

playingCoroutine

=

StartCoroutine

(

PlayExpandAni

(

)

)

;

}

private void OnDisable

(

)

{

if

(

playingCoroutine !

=

null

)

{

StopCoroutine

(

playingCoroutine

)

;

}

}

private void OnDestroy

(

)

{

StartScaleImage

(

-1

)

;

if

(

playingCoroutine !

=

null

)

{

StopCoroutine

(

playingCoroutine

)

;

}

}

private void StartScaleImage

(

int idx

)

{

if

(

images

=

=

null

)

{

return

;

}

if

(

currentScaleIdx

>

-1

&

&

currentScaleIdx

<

images.Length

)

{

Image preImg

=

images[currentScaleIdx];

preImg.material

=

null;

}

if

(

idx

<

|| idx

>

=

images.Length

)

{

return

;

}

currentScaleIdx

=

idx;

Image curImg

=

images[idx];

UIExpandMat.SetVector

(

VertexParamsID

,

meshSizes[idx]

)

;

curImg.material

=

UIExpandMat;

}

private IEnumerator PlayExpandAni

(

)

{

while

(

true

)

{

for

(

int i

=

; i

<

images.Length;

+

+

i

)

{

UIExpandMat.SetFloat

(

TimeOffsetID

,

Time.timeSinceLevelLoad

)

;

StartScaleImage

(

i

)

;

yield

return

Yielders.GetWaitForSeconds

(

ScaleDuration

)

;

}

StartScaleImage

(

-1

)

;

yield

return

Yielders.GetWaitForSeconds

(

CycleInterval

)

;

}

}

}

}

Tips:這裡使用了 Shader.PropertyToID 方法來減少給material指派過程中的消耗,對于攜程使用了一個Yielders類減少頻繁的記憶體配置設定。

總之,基于Shader來對持續的DoTween動畫進行優化,可以大大減少Canvas重建的幾率。而Shader中基于頂點和_Time屬性進行動畫計算的消耗非常少,比如通常的Image隻有四個頂點而已,再配合部分C#代碼提供給材質必須的參數,就可以實作更加複雜的ui動畫。尤其對于會長時間存在的動畫效果,如果可以善用Shader可以做到兼顧效果和效率。 6. 進度條 在進行戰鬥中的Profile的時候也是發現了每幀都有一個Canvas重建的過程,排查後發現是用于顯示倒計時效果的進度條在持續地被更新導緻的。 UGUI的進度條控件功能非常通用,但是層次很複雜,包括Background、Fill Area和Handle Slide Area三個部分。它是實作原理是基于Mesh的修改:

使用Shader進行UGUI的優化

從上面的gif可以看出,當Slider的value更改的時候,mesh會跟着調整。這可以做到一些UI想要的效果,比如讓Fill中的圖是一張九宮格的形式,就可以做出比較好看的進度條效果,保證拉伸之後的效果是正确的。 當你需要一條可能持續變化的進度條一直在顯示的時候,比如倒計時進度,持續的Canvas重建就不可避免。 針對具體的需求,通過Shader來進行一個簡單的ProgressBar也非常容易,通過對于alpha的控制就可以做到截取的效果: [AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

fixed

4

frag

(

v

2

f IN

)

:

SV_Target

{

half

4

color

=

(

tex

2

D

(

_MainTex

,

IN.texcoord

)

+

_TextureSampleAdd

)

*

IN.color;

#if _ISVERTICAL_ON

float uvValue

=

IN.texcoord.y

-

_UVRect.y;

float totalValue

=

_UVRect.w;

#else

float uvValue

=

IN.texcoord.x

-

_UVRect.x;

float totalValue

=

_UVRect.z;

#endif

#if _ISREVERSE_ON

uvValue

=

totalValue

-

uvValue;

#endif

color.a

*

=

uvValue

/

totalValue

<

_Progress;

color.a

*

=

UnityGet

2

DClipping

(

IN.worldPosition.xy

,

_ClipRect

)

;

#ifdef UNITY_UI_ALPHACLIP

clip

(

color.a

-

0.0

01

)

;

#endif

return

color;

}

這次是在ps階段進行處理,當然也可以在vs中模拟頂點的縮放效果或者處理uv的偏移。為了支援垂直和反向,這裡通過兩個宏來進行控制。在實作了條狀的進度條,然後準備根據UI的具體需求進行效果上優化的時候,UI同學表示設計方案修改了變成了圓形的進度條(=_=),而且是兩個方向同時展示進度,最終效果如下圖所示的效果:

使用Shader進行UGUI的優化

[AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19

fixed

4

frag

(

v

2

f IN

)

:

SV_Target

{

half

4

color

=

(

tex

2

D

(

_MainTex

,

IN.texcoord

)

+

_TextureSampleAdd

)

*

IN.color;

float theta

=

atan

2

(

(

IN.texcoord.y

-

_UVRect.y

)

/

_UVRect.w

-

0.5

,

(

IN.texcoord.x

+

1

E

-18

-

_UVRect.x

)

/

_UVRect.z

-0.5

)

;

[

/

color][

/

size][

/

font

][align

=

left][

font

=

微軟雅黑][size

=

3

][color

=

#2f4f4f]

[

/

color][

/

size][

/

font

][

/

align][

font

=

微軟雅黑][size

=

3

][color

=

#2f4f4f]

[align

=

left]

#ifdef IS_SYMMETRY[/align]

[align

=

left]        color.a

*

=

(

(

1

-

_Progress

)

*

UNITY_PI

<

abs

(

theta

)

)

;[

/

align]

[align

=

left]

#else[/align]

[align

=

left]        color.a

*

=

(

(

1

-

_Progress

)

*

UNITY_PI

*

2

<

theta

+

UNITY_PI

)

;[

/

align]

[align

=

left]

#endif[/align]

[align

=

left]        color.a

*

=

UnityGet

2

DClipping

(

IN.worldPosition.xy

,

_ClipRect

)

;[

/

align]

[align

=

left]

#ifdef UNITY_UI_ALPHACLIP[/align]

[align

=

left]        clip

(

color.a

-

0.0

01

)

;[

/

align]

[align

=

left]

#endif[/align]

[align

=

left]       

return

color;[

/

align]

[align

=

left]

}

這裡用了一個消耗比較大的atan2指令來進行弧度值的計算,支援對稱和非對稱的兩種方式,對稱的方式用于上面的特殊進度條,非對稱的方式用于下面這種環形的進度條。

使用Shader進行UGUI的優化

UGUI針對Image提供了Filled的Image Type來做環形進度條的效果,其原理是根據角度來更改Mesh實作的。 為了相容Image中使用的Atlas,這裡需要将uv資訊設定給材質: [AppleScript]  純文字檢視  複制代碼 ?

  01 02 03 04 05 06 07 08 09 10 11 12 13 14

/

/

/

<

summary

>

/

/

/

更新貼圖的uv值到材質中

/

/

/

注意:需要在Image更新的時候調用本邏輯

/

/

/

<

/

summary

>

public void UpdateImageUV

(

)

{

if

(

relativeImage !

=

null

)

{

Vector

4

uv

=

(

relativeImage.overrideSprite !

=

null

)

?

DataUtility.GetOuterUV

(

relativeImage.overrideSprite

)

:

defaultUV;

uv.z

=

uv.z

-

uv.x;

uv.w

=

uv.w

-

uv.y;

relativeImage.material.SetVector

(

UVRectId

,

uv

)

;

}

}

對于進度條的修改,尤其是環形進度條,是在Shader的ps階段做的,是以消耗可能還比較大,和Mesh重建的過程的消耗我沒有做具體的對比,相當于拿GPU換取CPU的消耗,有可能在某些裝置上還不夠劃算。這個具體使用哪種方法更好,或者是否需要繼續優化就看讀者自己具體的項目需求了。

7. 總結

針對UGUI的優化零零散散也做了不少,上面讨論到的是其中影響相對大的部分,另外一大塊内容是在UGUI中使用特效,這塊和本次部落格的主題關系不大就不放一起聊了。

可以看到,雖然從結果上看,這些優化後使用的Shader技術都非常非常簡單,大都是一些uv計算或者頂點位置的計算,相對于需要進行光照陰影等計算的3D Shader,UI中使用的Shader簡直連入門都算不上,但是通過合理地使用它,配合部分C#代碼邏輯,可以實作兼顧效果和效率的UI控件功能。 另外,雖然從結果看很簡單,仿佛每一個Shader都隻需要1-2個小時就可以完成,但是在排查問題和思考優化方法的過程中其實也花費了很多精力,有很多的糾結和思考。這些方案的對比和思考的過程由于時間關系沒有全部反映在這篇部落格裡,但你從Tips和一些隻言片語中也可以窺見到一些當時的心路曆程……除此之外,由于項目已經到了中後期,還有不少時間花費在新控件的易用性和向前相容的方面,以讓UI和程式同學可以用盡量少的時間來完成對于之前資源的優化工作。

最後,我想坦誠地說,對于UGUI和基于Shader的方案,我沒有進行定量的性能對比測試,是以也不能保證基于Shader的方法都一定效率更高,比如最後圓形的Mask就可能會是一個反例。我能做到的是盡量公正地從原理角度分析兩者之間在Overdraw、Drawcall和Canvas重建方面的性能差異,也可能有考慮不全的地方,歡迎大家一起讨論~

最後的Tips:除了一些“傻X”bug引發的“神級”優化之外,大部分的優化都是瑣碎而且成效不會那麼直接、顯著的工作。比如上面這些内容可能花費了我大約2個周左右的時間,還需要推進UI和程式對于已有的資源進行修改,而且面臨着需求變更的問題……然而,面對優化工作還是那句話——“勿以惡小而為之,勿以善小而不為。”

另外,讓團隊中的每一個人都了解更多更深入的技術原理,擁有對于性能消耗的警覺,才不至于讓問題在最後Profile的時刻集中爆發出來,而是被消化在日常開發的點點滴滴之中,這也是我對于理想團隊的期(huan)待(xiang)。

知乎@Funny David