资讯动态

一个着色器做出整个效果:Mac Duo 的 Metal 透视投影、高斯金字塔模糊与 Gamma 暗化全解

发布时间:2026/10/14 17:16:51 来源:尧图企业网站定制
【免费下载链接】Mac-DuoWish you could bring the iPhone Duo effect to your MacBook?项目地址https://gitcode.com/gh_mirrors/mac/Mac-Duo点击查看免费下载Mac Duo是一个开源的 macOS 菜单栏应用它把 iPhone「合盖双卡」Duo效果搬到了 MacBook 上当盖子越合越小Mac Duo 的 Metal 着色器就会实时让屏幕画面倾斜、模糊、变暗就像屏幕正朝你身后沉下去一样。整个视觉效果的灵魂是一段不到百行的 Metal 片段着色器——透视投影、高斯金字塔模糊、Gamma 暗化全部在同一个单 Pass 全屏渲染中完成。本文带你完整拆解这套 Metal 渲染管线的实现思路即使你是 Metal 新手也能看懂每一步。先看全景一个像素是怎么被画出来的Mac Duo 在内置屏幕上方覆盖了一个无边框、不拦截点击的透明窗口DepthOverlay.swift。每一帧的画面生产流程如下读盖子角度—— LidController.swift 轮询盖子角度传感器并用一个临界阻尼弹簧CriticallyDampedSpring.swift把读数平滑成视觉角度避免传感器跳变。算透视四角—— DepthOverlay.swift 里的DepthGeometry用纯三角学算出画面四个角此刻应落在屏幕玻璃上的哪个位置。解投影矩阵—— Homography.swift 求出 3×3 齐次变换矩阵着色器使用它的逆矩阵。一次全屏着色—— DepthShaders.swift 中的片段着色器对每个屏幕像素做三件事反投影回原画面 → 从高斯金字塔取一级模糊样本 → 按 Gamma 规则压暗。源码地图速查文件角色DepthShaders.swiftMetal 着色器源码整个效果的核心DepthRenderer.swift渲染管线建纹理、建高斯金字塔、编码每一帧Homography.swift矩形到任意四边形的投影矩阵DepthOverlay.swift覆盖窗口 透视几何计算BlurGradient.swift模糊 / 变暗随合盖进度的曲线Preferences.swift面板参数与默认值顶点着色器一个三角形扫满全屏顶点部分只干一件事——把三个顶点输出成一个比屏幕还大的三角形vertex float4 depthVertex(uint vertexID [[vertex_id]]) { const float2 corners[3] { float2(-1.0, -3.0), float2(-1.0, 1.0), float2(3.0, 1.0) }; return float4(corners[vertexID], 0.0, 1.0); }三个点在裁剪空间覆盖了整个视口左下 (-1,-3)、左上 (-1,1)、右侧 (3,1)因此一片三角形就画满了整屏不需要拼网格。这也是为什么DepthRenderer里每次只调用一次drawPrimitives(type: .triangle, vertexCount: 3)——整帧只有一次绘制。片段着色器第一步把每个屏幕像素反投影回原画面这是透视投影的精髓不是把画面拉变形而是每个屏幕像素独立去问我对应原画面上的哪个点。Swift 端屏幕坐标与画面坐标屏幕玻璃在物理世界里是固定的而画面被想象成一张以屏幕底边为铰链、在世界空间里往后转的纸。DepthOverlay.swift 中的DepthGeometry.corners做的事眼睛位置由viewingDistance观看距离单位是屏幕高度的倍数确定画面后转角度separation recession × 合盖转角上限 88°超过 90° 画面就背对屏幕了每个画面坐标(x, y)按scale depth / (depth y * sin(separation))收缩投影——离铰链越远的点缩得越多画面于是变成一个上窄下宽的四边形。四个角按左下、右下、右上、左上的顺序交给 Homography.swiftstatic func matrix(width: Double, height: Double, to corners: [SIMD2Double]) - simd_double3x3它用经典的 Heckbert 方到四边形解法直接解出屏幕 → 画面的 3×3 齐次矩阵。DepthRenderer.render里对它求逆let forward Homography.matrix(width: ..., to: corners) let inverse forward.inverse正矩阵在 Swift 里算逆矩阵送进 GPU——Swift 端负责重数学着色器只做轻采样。着色器端逆映射 高度坐标float3 mapped screenToPicture * float3(screenPoint, 1.0); float2 picturePoint mapped.xy / mapped.z; // 齐次除法每个屏幕像素经逆矩阵映回原画面坐标映出画面范围的像素直接输出黑色。随后着色器算出一个关键量heightfloat height clamp(picturePoint.y / screenSize.y, 0.0, 1.0);height在铰链边画面底边为 0在远离铰链的顶边为 1。后面模糊和变暗都只靠这一个高度值驱动——这正是近处清晰、远处模糊变暗的透视感来源。片段着色器第二步高斯金字塔模糊一像素一次采样想要远处模糊最容易的做法是整张图做一次大半径模糊——但那样整张图都会糊。正确做法是逐像素模糊半径不同直接做的话每像素要卷积成百上千次采样GPU 扛不住。Mac Duo 的答案是高斯金字塔mipmap 金字塔。纹理怎么建黑边 金字塔DepthRenderer.swift 的makePicture先把画面贴到一张比它四周各宽120pt 黑边的纹理上然后调用MPSImageGaussianPyramidcenterWeight: 0.375即标准 3 抽头高斯核在 GPU 上逐级降采样一次建好所有 mip 级别。这 120pt 黑边是着墨不多的巧思着色器注释里写道——纹理本来就带着黑底所以画面边缘不需要特殊处理。模糊采样到画面外时采到的是纯黑模糊与黑色自然融合屏幕边缘看起来就像照片贴在黑卡纸上往你身后推。模糊半径如何对应到金字塔级别float blur strength * (blurFloor (1.0 - blurFloor) * height); float mipLevel clamp(log2(max(blur * maxRadius, 1.0)), 0.0, maxLevel); float4 colour picture.sample(linearSampler, texCoord, level(mipLevel));三个要点逐像素模糊blur随height从铰链到顶部线性增大blurFloor即设置里的 Blur spread控制铰链处保留多少模糊——0 表示只有远端糊1 表示整张图均匀糊一次采样替代卷积高斯金字塔的第 n 级降采样其模糊程度近似 σ 2ⁿ 像素所以log2(想要的模糊半径)就是该取哪一级——mipLevel由此直接算出钳制在[0, maxLevel]半径单位换算maxBlurRadius以 pt 为单位默认 135pt着色器里乘上pixelScale换成物理像素Retina 屏 ×2。对比传统后处理整帧一次模糊无法表达近清晰远模糊的景深变化而这里每个像素独立取金字塔级别每像素只付一次采样的代价这就是一个着色器做出整个效果的关键。片段着色器第三步Gamma 暗化让变暗符合亮度设定画面远端还要随合盖进度变暗盖子完全合上时屏幕本就该黑。着色器末尾float spread smoothstep(0.0, max(dimReach, 0.02), height); float fade dimStrength * (dimFloor (1.0 - dimFloor) * spread); colour.rgb * pow(1.0 - maxDim * fade, 2.2);变暗曲线smoothstep让变暗强度在 0 到dimReach默认 0.5即画面下半部之间平滑爬升到满格。用 smoothstep 而不是硬截断是为了在变暗达满的高度处不留下可见的硬边界铰链留底dimFloor固定 0.2保证铰链侧至少有远端 20% 的变暗画面从亮到暗是连续渐变maxDimDimming默认 100%是远端最暗能到多黑1.0 即接近全黑。为什么最后要pow(..., 2.2)纹理用的是bgra8Unorm_srgb格式着色器采样得到的是线性光。如果在编码值上直接乘 0.5感知亮度会远不止减半。把变暗因子折算 2.2 次幂后再乘到线性光上变暗 50%在视觉上才真的等于亮度降一半——这就是标题里Gamma 暗化的含义。参数如何流入着色器每帧一次 setFragmentBytesSwift 侧把两个端点的数据打包进一个Uniforms结构体7 个 float4布局与着色器端严格一致每帧经encoder.setFragmentBytes一次写入。关键参数速查面板参数默认值到达着色器后控制什么Start angle开始角度90°触发效果的盖子角度Full effect after完全生效角度60°模糊爬满所需的合盖跨度Blur模糊半径135 ptmaxRadius远端最大模糊半径Blur spread模糊扩散0%blurFloor铰链处保留的模糊比例Dimming变暗100%maxDim远端最大变暗观看距离 / 退避系数6 倍屏高 / 1.0透视四角几何模糊和变暗的力度曲线由 BlurGradient.swift 负责合盖进度经过pow(progress, 1.6)模糊起步慢与pow(progress, 0.7)变暗起步快两条幂曲线让暗先于糊到达更接近真实光学感受。写在最后回头看这个不到百行的片段着色器它其实就干了三件事反投影——用逆齐次矩阵把屏幕变回一张离你更远的照片金字塔采样——每像素从log2算出的 mip 级取一次样本零卷积完成景深模糊Gamma 折算——pow(·, 2.2)让变暗强度诚实地等于你的设定值。再加上 Swift 端 120pt 黑边、临界阻尼弹簧和 Heckbert 投影矩阵的铺垫iPhone Duo 的味道就在 MacBook 上落地了。想动手验证克隆仓库后用 Xcode 打开执行./build.sh --run在支持盖子角度传感器的 MacBookmacOS 14上合盖就能看到这套 Metal 管线的实时效果。赞分享【免费下载链接】Mac-DuoWish you could bring the iPhone Duo effect to your MacBook?项目地址https://gitcode.com/gh_mirrors/mac/Mac-Duo点击查看免费下载相关推荐three.js TriangleBlurShader 详解可分离金字塔滤波的三角形模糊着色器three.js TriangleBlurShader 详解可分离金字塔滤波的三角形模糊着色器 TriangleBlurShader 是 three.js 官前端3D渲染图形学探索GaussianBlur一个高效、灵活的高斯模糊库探索GaussianBlur一个高效、灵活的高斯模糊库 在图像处理的世界里高斯模糊是一个常用的技术用于减少噪声、平滑图像或创造特殊视觉效果。今天我们想要Coil模糊圖像加載從高斯模糊到半透明效果Coil模糊圖像加載從高斯模糊到半透明效果 你是否還在為Android應用中的圖像加載體驗不佳而煩惱閃爍的白屏、生硬的切換動畫、過度佔用內存的大圖片——這些移动开发图像处理缓存抽象创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑