编程小知识之 CanvasScaler 的一点知识

2019-06-14 20:45:21 浏览数 (1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 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} ​log2​a=log2​0.5=−1log2​b=log2​2=1log2​s=(1−t)∗log2​a t∗log2​b⟹log2​s=(1−0.5)∗(−1) 0.5∗1=0⟹s=2log2​s=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} s​a=2a′b=2b′s=2(1−t)∗a′ t∗b′⟹a′=log2​ab′=log2​b=2(1−t)∗log2​a t∗log2​b=2(1−t)∗log2​a∗2t∗log2​b=(2log2​a)1−t∗(2log2​b)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()

0 人点赞