UGUI中的Canvas、Camera和Canvas Scaler

f#投影空间和屏幕空间
先不说UI,如果是一个普通渲染流程,一个Cube普通地被渲染。

800X600的屏幕中,右边的Cube和中间的Image大小相仿。(可能由于网页显示问题导致本图显示尺寸变小。请自行脑补成其真实800x600大小,起码比下图248X186大好多)
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,确保和屏幕匹配。

注,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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容