UGUI中的Canvas、Camera和Canvas Scaler

投影空間和屏幕空間

先不說UI,如果是一個普通渲染流程,一個Cube普通地被渲染。

800X600的屏幕中,右邊的Cube和中間的Image大小相仿。(可能由於網頁顯示問題導致本圖顯示尺寸變小。請自行腦補成其真實800x600大小,起碼比下圖248X186大好多)

800X600的屏幕中,右邊的Cube和中間的Image大小相仿。(可能由於網頁顯示問題導致本圖顯示尺寸變小。請自行腦補成其真實800x600大小,起碼比下圖248X186大好多)

248X186的屏幕中,發現正方體已變小,但Image卻沒變

248X186的屏幕中,發現正方體已變小,但Image卻沒變

如果屏幕在現實中變小了,這個Cube在現實中也會變小。
具體地說,屏幕變小時,Cube在投影空間中是恆定大小的。但透視空間轉換到屏幕空間時,一般都是使用默認的GPU流程:直接把投影空間投影面的四個角直接映射到屏幕空間的四個角。因爲屏幕變小了,屏幕空間就變小了,所以這個Cube也就變小了。

總之,屏幕變小不影響投影空間,但會讓屏幕空間變小。

Canvas

注,文章以下部分如不特殊註明,都認爲Canvas.RenderMode是ScreenSpace,而並非WorldSpace。

Canvas Scaler

Canvas Scaler的各種模式最終都是設置了Canvas.scaleFactor(還有Canvas.referencePixelsPerUnit,但並非本文重點)。
Canvas.scaleFactor會影響Canvas的RectTransform的ScaleXYZ。

Canvas.RenderMode:ScreenSpace - Overlay

當是ScreenOverlay的情況,因爲根本沒有經過投影空間,而是直接在屏幕空間繪製,所以可以更加簡單地討論問題。

因爲在屏幕空間的座標直接繪製像素,所以Canvas的RectTransform的width*scaleX必須等於屏幕的寬、RectTransform的height*scaleY必須等於屏幕的高。
其中,scaleXY事實上是可以通過Canvas.scaleFactor給腳本設置的,也就是說:

Canvas的核心功能是,自動根據屏幕設備的分辨率、當前的scaleXY,算出正確的width和height,確保和屏幕匹配。

Canvas的核心功能是自動根據屏幕設備的分辨率、當前的scaleXY,算出正確的width和height,確保和屏幕匹配。

Canvas的核心功能是自動根據屏幕設備的分辨率、當前的scaleXY,算出正確的width和height,確保和屏幕匹配。

注,Overlay情況下,Scene View是世界空間的、Canvas是屏幕空間的,兩者處於不同空間,它們的大小比較也就沒有可比性了。但也總得把Canvas在SceneView顯示出來對吧,所以Unity這裏的Editor實現是,屏幕空間1像素等於世界空間1米地把Canvas在SceneView顯示出來。

CanvasScaler.ScaleMode: ConstantPixelSize


以圖中Scale Factor=1爲例,ConstantPixelSize的作用是直接設置scaleXYZ爲1。所以,Canvas的width和height總是等於屏幕大小(244x183)。
所以當設備分辨率變小時,scaleXYZ總是1,UI元素的大小並不改變,但Canvas的width和height變小了,所以UI元素佔比Canvas的比例也隨之變大。結果:屏幕分辨率變小,UI元素在設備的實際像素數不變,也就大小不變。

CanvasScaler.ScaleMode: ScaleWithScreenSize

ScreenMatchMode.MatchWidthOrHeight


以圖中Canvas的Reference Resolution等於800X600比例爲4:3、屏幕恰好是同比例4:3的244X183爲例,
scaleXYZ總是等於屏幕分辨率/參考分辨率=0.305,所以Canvas的width和height總是800X600。
所以當屏幕分辨率變小於800X600時,Canvas的width/height不變。Canvas的scaleXYZ變小,Canvas本身和UI元素的全局scaleXYZ都變小,但UI元素佔比Canvas的比例不變。結果:屏幕分辨率變小,UI元素在設備的比例不變,所以UI的實際像素數變小。
事實上,CanvasScaler是ScaleWithScreenSize的作用就是:

根據參考分辨率和當前設備分辨率,得出一個“合適”的scaleFactor。至於設備分辨率和參考分辨率比例不同,這個問題交由Canvas自動設置width/height來解決。

當設備分辨率比參考分辨率大時,scaleFactor需要“合適”的大,當設備分辨率比參考分辨率小時,scaleFactor需要“合適”地小。
這個“合適”,在內部實現的代碼是:

float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);

ScreenMatchMode.Expand

scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);

scaleFactor儘可能小,可知,結果是儘量把UI在設備屏幕給放大

ScreenMatchMode.Shrink

scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);

scaleFactor儘可能大,可知,結果是儘量把UI在設備屏幕給縮小

CanvasScaler.ScaleMode: ConstantPhysicalSize

根據屏幕的PPI信息和ConstantPhysicalSize本身的配置信息,得出一個“合適”的scaleFactor,以達到UI在不同PPI設備上的最終大小都是一致的。

Canvas.RenderMode:ScreenSpace - Camera

由於使用了Camera,所以此時的Canvas和Scene View裏的任意GameObject一樣,都是處於世界空間了。
所以對於ScreenSpace - Camera的Canvas來說,關鍵是如何把處於世界空間中的Canvas能夠佔滿投影空間的投影平面。

然而,由於在世界座標系中任意GameObject的默認scaleXYZ都是1,Canvas也不例外。所以,需要根據Camera的設置、Canvas的plane distance設置,算出scaleFactorCamera出來。

正交(Orthographic)Camera

正交Camera的可調參數是Size。Size的定義爲視體的投影平面高的1/2。 比如Camera的Size是300,即投影平面高爲600。投影平面的寬根據平面的高和屏幕比例得出。
所以Canvas必須和這個投影平面一樣大小(且位置重合)。此時,有兩種方法和投影平面大小一樣:

  1. 調整Canvas的width/height

  2. 調整Canvas的ScaleXY

由於第1種方法會導致切換Overlay和Camera的方式時,UI的表現不一致,所以Unity的實現是採取了第2種方法。即:

當Canvas是Screen Space - Overlay時,
Canvas.RectTransform.scaleXYZ = Canvas.scaleFactor
當Canvas是Screen Space - Camera時,
Canvas.RectTransform.scaleXYZ = Canvas.scaleFactor * scaleFactorCamera

所以,在這種情況下,

  • Canvas.height==Screen.height

  • Canvas.height * scaleFactor * scaleFactorCamera = Camera.size * 2,

  • 所以scaleFactorCamera = Camera.size * 2 / Canvas.height / scaleFactor = Camera.size * 2 / Screen.height / scaleFactor

透視(Perspective)Camera

透視Camera的可調參數爲fov。同時Canvas的plane distance也認爲是參數之一。

  • Canvas.height == Screen.height

  • Canvas.height * scaleFactor * scaleFactorCamera = 2 * Canvas.planeDistance * tan(Camera.fov/2)

  • 所以scaleFactorCamera = 2 * Canvas.planeDistance * tan(Camera.fov/2) / Screen.height / scaleFactor

CanvasScaler

至此,已求出不同Camera模式的scaleFactorCamera。

在ScreenSpace - Camera的情況下,不管CanvasScaler的模式再怎麼變,都是在上面Overlay的情況下的CanvasScaler的討論基礎上,再乘多一個scaleFactorCamera即可。

WorldSpace

Canvas的width和height、scaleXYZ都可以任意設置。Canvas不再自動根據屏幕設備的分辨率、當前的scaleXY,算出正確的width和height。
此時的CanvasScaler在此模式下幾乎形同虛設。只是拿來設置Canvas.referencePixelsPerUnit



文/DonaldW(簡書作者)
原文鏈接:http://www.jianshu.com/p/96fd1fbe8409
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章