版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1446298
本文简述了 Unity 中 CanvasScaler 的一点知识
制作 UI 时,一般都需要进行多分辨率适配,基本的方法大概有以下几种:
- UI 参照单一的分辨率(参考分辨率)进行制作,实际显示时按照某种方式调整到实际的设备分辨率
- UI 按照所有可能的分辨率分别进行制作,实际显示时选择对应的设备分辨率显示
- 上述两种方法(间)的某种平衡方式(譬如根据占比较高的几种分辨率来制作UI)
UGUI 中的多分辨率适配支持第一种方法,类型 CanvasScaler 包含了相关的分辨率调整逻辑.
CanvasScaler 在 Scale With Screen Size 的 UI 缩放模式下支持 3 种屏幕适配模式:
- Match Width Or Height
- Expand
- Shrink
后两种模式比较容易理解(不了解的朋友可以直接参看文档),只是第一种适配模式(Match Width Or Height)让人觉得有些生疏,相关文档是这么说的:
Scale the canvas area with the width as reference, the height as reference, or something in between
解释的有些含糊,我们直接看下代码:
代码语言:javascript复制// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 2) / 2 = 1.25
// In logarithmic space the average is (-1 1) / 2 = 0
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);
可以看到代码中首先将宽高的缩放比例都进行了取对数的操作(转换到了对数空间),然后在对数空间进行线性插值,接着再进行了指数操作(转换回了原始空间),注释里举了一个例子:
假设参考分辨率的宽是实际分辨率的宽的2倍(此时 screenSize.x / m_ReferenceResolution.x 等于 0.5, 我们将其记为 a),参考分辨率的高则是实际分辨率的高的0.5倍(此时 screenSize.y / m_ReferenceResolution.y 等于 2, 我们将其记为 b),并且设插值比例(m_MatchWidthOrHeight, 我们将其记为 t)为 0.5,那么如果直接进行线性插值(设要求解的缩放值为 s),则有:
s=(1−t)∗a t∗b ⟹ s=(1−0.5)∗0.5 0.5∗2=1.25 begin{aligned} & s = (1 - t) * a t * b implies & s = (1 - 0.5) * 0.5 0.5 * 2 = 1.25 end{aligned} s=(1−t)∗a t∗b⟹s=(1−0.5)∗0.5 0.5∗2=1.25
如果进行对数空间插值的话(对数基底设为 2),则有:
log2a=log20.5=−1log2b=log22=1log2s=(1−t)∗log2a t∗log2b ⟹ log2s=(1−0.5)∗(−1) 0.5∗1=0 ⟹ s=2log2s=20=1 begin{aligned} & log_2{a} = log_2{0.5} = -1 & log_2{b} = log_2{2} = 1 & log_2{s} = (1 - t) * log_2{a} t * log_2{b} implies & log_2{s} = (1 - 0.5) * (-1) 0.5 * 1 = 0 implies & s = 2 ^ {log_2{s}} = 2 ^ 0 = 1 end{aligned} log2a=log20.5=−1log2b=log22=1log2s=(1−t)∗log2a t∗log2b⟹log2s=(1−0.5)∗(−1) 0.5∗1=0⟹s=2log2s=20=1
关于对数空间插值的原理,我是这么理解的:
实际上而言,对于具体给定的 a 和 b, 我们要插值的并不是 a, b 本身,而是他们所代表的"缩放程度",当 a = 0.5 时,其代表的是缩小一倍,即 a=2−1a = 2 ^ {-1}a=2−1,而 b = 2 时,其代表的是放大一倍,即 b=21b = 2 ^ {1}b=21,一般的有:
a=2a′b=2b′s=2(1−t)∗a′ t∗b′ begin{aligned} & a = 2 ^ {a'} & b = 2 ^ {b'} & s = 2 ^ {(1 - t) * a' t * b'} end{aligned} a=2a′b=2b′s=2(1−t)∗a′ t∗b′
将上式翻译一下便是之前的示例代码了.
实际上,上述的计算过程是可以简化的,延续上面的等式,我们有:
a=2a′b=2b′s=2(1−t)∗a′ t∗b′ ⟹ a′=log2ab′=log2bs=2(1−t)∗log2a t∗log2b=2(1−t)∗log2a∗2t∗log2b=(2log2a)1−t∗(2log2b)t=a1−t∗bt begin{aligned} & a = 2 ^ {a'} & b = 2 ^ {b'} & s = 2 ^ {(1 - t) * a' t * b'} & implies & a' = log_2{a} & b' = log_2{b} s & = 2 ^ {(1 - t) * log_2{a} t * log_2{b}} & = 2 ^ {(1 - t) * log_2{a} } * 2 ^ {t * log_2{b}} & = (2 ^ {log_2{a}})^{1 - t} * (2 ^ {log_2{b}}) ^ {t} & = a ^ {1 - t} * b ^ {t} end{aligned} sa=2a′b=2b′s=2(1−t)∗a′ t∗b′⟹a′=log2ab′=log2b=2(1−t)∗log2a t∗log2b=2(1−t)∗log2a∗2t∗log2b=(2log2a)1−t∗(2log2b)t=a1−t∗bt
相关代码大概是这个样子:
代码语言:javascript复制scaleFactor = Mathf.Pow(screenSize.x / m_ReferenceResolution.x, 1 - m_MatchWidthOrHeight) * Mathf.Pow(screenSize.y / m_ReferenceResolution.y, m_MatchWidthOrHeight);
简单的 profile 显示,简化过的代码比原始代码快 35% 左右,当然,可读性上也更差了一些~
番外
如果需要在 Lua 脚本中进行 profile,很多朋友可能就直接选择就地编码了,但实际上,我们可以进一步封装相关操作,下面是一个简单的实现:
代码语言:javascript复制-- simple profile implementation
local profile_infos = {}
local function on_profile_end_default(profile_info)
print("[Profile]Profile elapsed time : " .. profile_info.elapsed .. "s(" .. profile_info.elapsed * 1000 .. "ms)")
end
function _G.ProfileStart(start_callback)
local profile_info = { start = os.clock() }
table.insert(profile_infos, profile_info)
if start_callback then
start_callback(profile_info)
end
end
function _G.ProfileEnd(end_callback)
end_callback = end_callback or on_profile_end_default
local profile_info = profile_infos[#profile_infos]
if profile_info then
profile_info.elapsed = os.clock() - profile_info.start
if end_callback then
end_callback(profile_info)
end
table.remove(profile_infos)
else
print("[Profile]Incorrect profile info, seems profile start and profile end do not match ...")
end
end
使用时直接在相关代码块中添加 ProfileStart 和 ProfileEnd 即可(假设代码可以访问到 _G):
代码语言:javascript复制ProfileStart()
// logic to profile here ...
ProfileEnd()