【免费下载链接】Mac-Duo
Wish you could bring the iPhone Duo effect to your MacBook?
Mac Duo 把 iPhone Duo 风格的合盖景深效果搬到了 MacBook 上:合上盖子时,屏幕画面会像一张钉在转轴上的纸一样向后倾斜、离焦模糊并逐渐变暗。本文拆解这个 Mac Duo 景深效果背后的两套核心数学——把合盖角度换算成透视四边形的铰链透视模型,以及把画面精确铺到任意四边形的Heckbert 单应矩阵推导,不假设你熟悉计算机图形学。
效果总览:合盖时屏幕发生了什么 📺
整个效果可以拆成四步流水线:
- 读角度—— MacBook 内置的铰链角度传感器以约 10 Hz 报出盖子开合角度(0° 为合拢,约 130° 为完全打开),见 Sources/LidAngleKit/LidAngleSensor.swift
- 算形状—— 铰链透视模型根据角度算出画面四个角在屏幕上的新位置,得到一个"上窄下宽"的四边形
- 铺画面—— Heckbert 单应矩阵把整个画面一次性"贴"到这个四边形上
- 加氛围—— GPU 逐像素做可变焦模糊与渐暗:越远离转轴越虚、越暗
全部渲染由 GPU 以"每帧一个全屏 pass"完成,覆盖层不拦截点击,点击直接穿透到下层的真实应用。
铰链透视模型:一张钉在屏幕底边的纸 📐
Mac Duo 的画面不是随便"缩小上移",而是模拟一个真实的物理场景:
画面是一张钉在屏幕**底边(转轴处)**的纸;盖子每合一点,纸就在三维空间里向后转一点;而你的眼睛纹丝不动——是玻璃屏在你眼前旋转。
这个模型全部实现于 Sources/MacDuo/DepthOverlay.swift 的DepthGeometry结构体中。
第一步:纸要转多少度。盖子从起始角start合到当前角current,走过的角度叫travel。纸的旋转角separation是travel乘以一个"后退倍率"recession(默认 2,纸走得比盖子快,透视感更明显),并封顶在 88°,防止纸转到与屏幕平行、彻底背对观众:
travel = max(start − current, 0) separation = min(recession × travel, 88°)第二步:眼睛在哪里。用屏幕高度 H 和观看距离比v(默认 2.7,即眼睛离屏幕约 2.7 个屏幕高度),以转轴为原点、桌面为水平轴,眼睛的世界坐标只需在效果开始时算一次:
reach = H·v + (H/2)·cos α # 眼睛到转轴的水平和距离 rise = (H/2)·sin α # 眼睛高于转轴的高度,α 为起始角第三步:玻璃在眼睛底下转。当盖子走到角度 θ 时,把"固定的眼睛"换算到玻璃屏自己的坐标系里(沿玻璃方向along、垂直离玻璃的距离depth):
along = reach·cos θ + rise·sin θ depth = max(reach·sin θ − rise·cos θ, H/10)关键就在depth:盖子越合拢,眼睛离玻璃平面越近,透视畸变就越夸张。底部的H/10是数值保护,避免距离趋近于零时除以无穷大。
第四步:投影每个点。纸上距转轴高度为 y 的点,因纸向后转了separation,比玻璃平面"多退后"了y·sin(separation),它投回玻璃平面的位置被压缩为y·cos(separation)。针孔透视给出统一的比例因子:
scale(y) = depth / (depth + y·sin separation) x' = W/2 + (x − W/2) · scale(y) y' = along + (y·cos separation − along) · scale(y)注意两个坐标都是围绕中心点 (W/2, along) 缩放的——这正是人眼看到的透视收敛:画面上边缘(离转轴最远)变窄、被纵向压缩,底边纹丝不动。把四个角分别代进去,就得到了渲染所需的任意四边形。
Heckbert 单应矩阵:把矩形精确投到四边形 🔍
四个角算出来后,剩下的问题变成一个经典命题:找一个 3×3 的单应矩阵H,把屏幕矩形整体映射到这个四边形,且四条边都要严格落在直线上。
为什么恰好能解?单应矩阵有 9 个元素但只差一个整体缩放,即8 个自由度;而 4 个角点提供 4×2 =8 个方程。方程数正好等于未知数,存在唯一解——不需要任何迭代拟合,直接代数求解。这正是平面透视投影的美妙之处。
1979 年 Heckbert 给出的闭式解非常精巧。先在单位正方形上推导(四角按左下、右下、右上、左上记为 p0~p3),把 p0 钉在原点、矩阵第三行写成 (g, h, 1),先解出两个"扭转量" g 和 h:
dx1 = x1 − x2 dx2 = x3 − x2 dx3 = x0 − x1 + x2 − x3 dy1 = y1 − y2 dy2 = y3 − y2 dy3 = y0 − y1 + y2 − y3 D = dx1·dy2 − dx2·dy1 g = (dx3·dy2 − dx2·dy3) / D h = (dx1·dy3 − dx3·dy1) / D然后其余 6 个元素只是四角坐标的简单线性组合:
a = x1 − x0 + g·x1 b = x3 − x0 + h·x3 c = x0 d = y1 − y0 + g·y1 e = y3 − y0 + h·y3 f = y0 H = | a b c | | d e f | | g h 1 |几个值得玩味的性质:
- g、h 度量"扭曲"。dx3、dy3 本质是四边形偏离平行四边形的量;当目标恰好是平行四边形时 dx3 = dy3 = 0,于是 g = h = 0,矩阵退化为纯仿射变换(平移、缩放、剪切),没有透视成分。Homography.swift 里那两个
1e-9/1e-12的数值保护,正是为了覆盖这个退化情形。 - 四个角点都能验证。例如 H·(0,0,1) = (c, f, 1) 直接落回 p0;H·(1,0,1) = (x1(1+g), y1(1+g), 1+g),做齐次除法后正好是 p1。
- 坐标单位折叠。推导用的是 0~1 的单位正方形,而画面坐标是 0~W、0~H 的点坐标,只需把前两根列向量分别除以 W 和 H(见 Homography.swift 的"Fold in"部分),一行代码完成量纲换算。
GPU 上的逆向映射:每帧一次全屏 pass ✨
拿到正方向矩阵后,最直觉的做法是"正着画"——把画面上的每个像素算出屏幕新位置再写下去。但这会留下窟窿:多个源像素可能挤到同一个目标像素,而有些目标像素谁也没写。
Mac Duo 选择了逆向映射(Sources/MacDuo/DepthRenderer.swift):每帧先算出正矩阵 F 再求逆,得到 F⁻¹,然后在片元着色器里对每个输出像素反查它来自画面上的哪个位置(Sources/MacDuo/DepthShaders.swift):
p = F⁻¹ · (px, py, 1) picturePoint = p.xy / p.z # 齐次除法,|p.z| < 1e-6 的像素直接判为画面外可变焦模糊是景深感的灵魂。画面上传纹理时先由MPSImageGaussianPyramid预建一整条高斯模糊金字塔(每高一级,模糊半径约翻倍),片元着色器只需一次采样就取出任意强度的模糊——模糊半径 σ 对应哪一级,取个对数即可:
height = picturePoint.y / H # 0 在转轴,1 在顶边 blur = strength · (floor + (1 − floor) · height) # 越远离转轴越糊 mipLevel = log2(max(blur · maxRadius, 1)) colour = picture.sample(texCoord, level(mipLevel))于是画面底部(离眼睛最近的纸边)保持锐利,顶部平滑地融进模糊。纹理四周预留了 120pt 的纯黑边,保证大半径模糊采样到的是真正的黑色而不是图片边缘。
变暗同样按高度渐变:用smoothstep从转轴向顶边铺开(保证没有可见的过渡边缘),亮度因子再按 2.2 次幂做伽马补偿,让"变暗 40%"在视觉上就是真实的暗了四成,而不是先暗后跳:
spread = smoothstep(0, dimReach, height) rgb *= (1 − maxDim · fade)^2.2数学之外的巧思:10 Hz 传感器与 120 Hz 屏幕 🌊
传感器每秒只报 10 次角度,屏幕却以 60~120 Hz 刷新。如果直接吃原始读数,画面会一卡一卡地"跳格"。Mac Duo 在 Sources/MacDuo/CriticallyDampedSpring.swift 里用一个临界阻尼弹簧在每一帧刷新时把角度"追赶"到最新读数:
a = ω²(target − x) − 2ω·ẋ # 临界阻尼:刚好不振荡 ẋ += a·dt # 半隐式欧拉,ω·dt < 2 时稳定 x += ẋ·dt频率 ω 取 16 rad/s。"临界阻尼"意味着角速度以不振荡、不滞后的最快速度收敛到目标——传感器每 100 ms 推一次,弹簧在两次读数之间自己把补间动画跑完,几何形状(对角度单调)因此天然获得丝滑感。
小结:三件数学工具,一条渲染流水线 🧮
| 环节 | 核心数学 | 源码位置 |
|---|---|---|
| 角度输入 | HID 报告,0.01° 或 1° 两档精度,10 Hz | LidAngleSensor.swift |
| 铰链透视 | 视线固定、玻璃旋转:scale = depth / (depth + y·sin φ) | DepthOverlay.swift#L24-L55 |
| 单应矩阵 | Heckbert 8 自由度闭式解,2×2 方程解出 g、h | Homography.swift#L12-L44 |
| 逆向映射 + 可变焦 | F⁻¹ 逐像素反查 + 高斯金字塔log2选级 | DepthShaders.swift#L31-L81 |
| 帧间平滑 | 临界阻尼弹簧,半隐式欧拉积分 | CriticallyDampedSpring.swift#L19-L23 |
回顾全文:铰链透视模型负责"该长什么样"(物理直觉 → 四边形),Heckbert 单应矩阵负责"怎么一次性贴上去"(8 个自由度、闭式解、无迭代),逆向映射 + 高斯金字塔负责"看起来像真的"(逐像素可变焦)。三者叠在一个全屏 fragment pass 里,就合成了你合上 MacBook 盖子时看到的那一瞬景深。想不动盖子就看效果,可以在设置面板里触发预览——它用一段脚本化的角度扫掠走完全相同的管线。
【免费下载链接】Mac-Duo
Wish you could bring the iPhone Duo effect to your MacBook?
相关推荐
超简单!用 Motion Canvas 矩阵变换实现拟 3D 透视效果:从入门到精通
超简单!用 Motion Canvas 矩阵变换实现拟 3D 透视效果:从入门到精通 Motion Canvas 是一款强大的代码驱动动画工具,它让开发者能够通
图形学音视频前端Medical-Transformer揭秘:MICCAI 2021突破性医学影像分割技术全解析
Medical Transformer揭秘:MICCAI 2021突破性医学影像分割技术全解析 Medical Transformer是MICCAI 2021会
Subgen Webhook集成完全指南:Plex、Jellyfin、Emby、Tautulli配置详解
Subgen Webhook集成完全指南:Plex、Jellyfin、Emby、Tautulli配置详解 Subgen是一款基于OpenAI Whisper模型
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考