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

69 篇文章 5 订阅
63 篇文章 1 订阅

本文简述了 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

解释的有些含糊,我们直接看下代码:

// 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=(1t)a+tbs=(10.5)0.5+0.52=1.25

如果进行对数空间插值的话(对数基底设为 2),则有:

l o g 2 a = l o g 2 0.5 = − 1 l o g 2 b = l o g 2 2 = 1 l o g 2 s = ( 1 − t ) ∗ l o g 2 a + t ∗ l o g 2 b    ⟹    l o g 2 s = ( 1 − 0.5 ) ∗ ( − 1 ) + 0.5 ∗ 1 = 0    ⟹    s = 2 l o g 2 s = 2 0 = 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=(1t)log2a+tlog2blog2s=(10.5)(1)+0.51=0s=2log2s=20=1

关于对数空间插值的原理,我是这么理解的:

实际上而言,对于具体给定的 a 和 b, 我们要插值的并不是 a, b 本身,而是他们所代表的"缩放程度",当 a = 0.5 时,其代表的是缩小一倍,即 a = 2 − 1 a = 2 ^ {-1} a=21,而 b = 2 时,其代表的是放大一倍,即 b = 2 1 b = 2 ^ {1} b=21,一般的有:

a = 2 a ′ b = 2 b ′ 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=2ab=2bs=2(1t)a+tb

将上式翻译一下便是之前的示例代码了.

实际上,上述的计算过程是可以简化的,延续上面的等式,我们有:

a = 2 a ′ b = 2 b ′ s = 2 ( 1 − t ) ∗ a ′ + t ∗ b ′    ⟹    a ′ = l o g 2 a b ′ = l o g 2 b s = 2 ( 1 − t ) ∗ l o g 2 a + t ∗ l o g 2 b = 2 ( 1 − t ) ∗ l o g 2 a ∗ 2 t ∗ l o g 2 b = ( 2 l o g 2 a ) 1 − t ∗ ( 2 l o g 2 b ) t = a 1 − t ∗ b t \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=2ab=2bs=2(1t)a+tba=log2ab=log2b=2(1t)log2a+tlog2b=2(1t)log2a2tlog2b=(2log2a)1t(2log2b)t=a1tbt

相关代码大概是这个样子:

scaleFactor = Mathf.Pow(screenSize.x / m_ReferenceResolution.x, 1 - m_MatchWidthOrHeight) * Mathf.Pow(screenSize.y / m_ReferenceResolution.y, m_MatchWidthOrHeight);

简单的 profile 显示,简化过的代码比原始代码快 35% 左右,当然,可读性上也更差了一些~

番外

如果需要在 Lua 脚本中进行 profile,很多朋友可能就直接选择就地编码了,但实际上,我们可以进一步封装相关操作,下面是一个简单的实现:

-- 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):

ProfileStart()

// logic to profile here ...

ProfileEnd()
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值