按方案落地:SwiftUI 驱动动画(foldProgress 0→1),Metal Shader 按“离折叠轴的距离”决定效果强弱,包含渐进模糊、磨砂散射、颗粒噪声、平滑隐身与折叠轴高光。三级递进:Phase 1 = 磨砂(layerEffect / MTK),Phase 2 = SwiftUI 3D 视觉近似侧翻,Phase 3 = Metal 真·3D 折页(双半页几何 + 透视 + 深度遮挡 + 背面 + 页边厚度 + 软阴影)。可直接拖进 Xcode 工程使用。
ios duo/
├── README.md
└── FoldEffect/
├── FoldEffect.metal # 磨砂着色器(两套入口:layerEffect / MTKView 的 Pass B)
├── FoldParameters.swift # 统一参数(磨砂 + 几何 + 页边厚度)
├── FoldShaderModifier.swift # iOS 17+ SwiftUI 原生 shader 封装(FoldContainer)
├── FoldMetalView.swift # iOS 14+ MTKView 单 Pass 管线(纯磨砂基础版)
├── FoldPage.metal # Phase 3:真·3D 折页 Pass A 几何着色器
├── FoldPageView.swift # Phase 3:双 Pass 渲染管线 + SwiftUI 封装(FoldPageView)
├── FoldGeometry.swift # Phase 2:SwiftUI 3D 视觉近似侧翻层 + 分段曲线
├── FoldMotionDriver.swift # 可选输入源:CoreMotion 设备姿态(陀螺仪融合)
├── FoldFallbackView.swift # iOS 14+ 无 Metal 近似回退
└── FoldDemoView.swift # 演示页(磨砂 / 侧翻(SwiftUI) / 侧翻(Metal 3D) 三种模式)
- 把
FoldEffect/整个文件夹拖进 Xcode 工程,勾选 App target。 - 两个
.metal(FoldEffect.metal/FoldPage.metal)会自动进入 target 的 Compile Sources 并编译进 default Metal library(无需任何设置)。 - 预览效果:
ContentView里直接写FoldDemoView()。 - 接入自己的界面:按下方 API 选用。
⚠️ Swift Package / SwiftPM 不支持编译 .metal 文件。若工程是 SPM,请把两个.metal直接加入 App target(其余 Swift 文件可以留在包里)。
- Xcode → New → Project → iOS App(SwiftUI、Swift),Deployment Target 设 15.0(核心组件 14+,演示页用了少量 15+ API)。
- 把整个
FoldEffect/文件夹拖进工程,勾选 App target(2 个.metal+ 8 个.swift一起进入)。 ContentView写FoldDemoView(),选真机 Run。
| 测试项 | 模拟器 | 真机 | 怎么看 |
|---|---|---|---|
| 磨砂 / 隐身动画 | ✅ | ✅ | 拖卡片 / 点「播放侧翻」/ 拉滑杆 |
| Metal 3D 折页(模式「侧翻(Metal 3D)」) | ✅ | ✅ | 0°→90°:自由边厚度、翻过 90° 的背面、铰链软阴影 |
| 陀螺仪驱动 | ❌ 无传感器 | ✅ | 打开「陀螺仪驱动」开关,倾斜设备(±35° 满量程) |
| 帧率 / 发热 | 不代表真机 | ✅ | Xcode → Debug navigator → GPU / Energy |
| 磨砂带贴边界的边缘表现 | ✅ | ✅ | 把 hingeX 拉到 0.05 / 0.95 看带边缘 |
- 用云 Mac 构建(你现有 safevault_ios 的 Codemagic 流程可复用):新建最小 App target,把
FoldEffect/拖进去,workflow 出 TestFlight 包,用真机安装测试。 - "Windows 本地直接编译 iOS" 不存在(Metal 编译器只在 Xcode 里),不要在这条路上花时间。
- 0°→30°→60°→90°:磨砂带应是"吞掉内容",而不是一块贴上来的模糊玻璃。
- 铰链处:允许极细高光,出现明显白线/黑线说明
highlightStrength或折痕参数需要调。 - 90° 附近:折叠半背面(压暗 + 偏冷)自然,与固定半的压盖关系正确、无穿插闪烁。
- 静止在 progress=0.6:不持续刷新(省电契约,可看 Xcode Energy 报告)。
- 2x / 3x 设备各看一遍:颗粒密度与模糊观感基本一致。
@State private var params = FoldParameters.standard
FoldContainer(params: params) {
MyCardView() // 任何 SwiftUI 内容,实时渲染
}
// 驱动动画
withAnimation(.easeInOut(duration: 0.45)) {
params.progress = 1
}- 优点:无快照、内容实时、代码最少。
- 内部:
progress == 0时零开销旁路;动画期间用 30fpsTimelineView驱动颗粒流动。 - 也可用底层 API:
MyCardView().foldEffect(params, time: t, viewSize: geo.size)。
FoldMetalView(params: params, isAnimating: isAnimating, contentVersion: contentVersion) {
MyCardView()
}- 数据流:内容只快照一次进
MTLTexture→ 之后每帧纯 GPU 跑foldFragment(折叠动画期间零截图、零 CPU)。 contentVersion契约:内容发生真实变化(数据/状态更新)时才+= 1,触发一次重新快照;不变就一直复用旧纹理。绝不要每帧快照。isAnimating契约:手势/动画期间传true(60fps 渲染循环),结束后传false(渲染循环暂停,仅补画静态帧)。- 适合“内容不变、只有效果在动”的折叠卡片场景;内容是实时动画(如
FoldGeometryView)时不要用这条路径。
FoldFallbackView(params: params) {
MyCardView()
}- 用“blur 副本 + 带状 mask + opacity”近似;无散射/颗粒/高光,双层渲染,仅适合低要求场景。
命名澄清:Phase 2 = SwiftUI 3D 视觉近似版(屏幕空间 rotation3DEffect + 透视); v2 = Metal 双纹理真·3D 折页版(见文末)。
磨砂 shader 负责“内容进入折叠区域后逐渐消失”的观感;FoldGeometryView 负责把内容“翻起来”。二者是独立的两层,组合使用:
Content
├── Geometry Pass(FoldGeometry.swift):绕铰链 rotation3DEffect + 透视 + 压缩 + 折痕明暗 + 投影
└── Frost Pass(FoldEffect.metal):渐进模糊 / 散射 / 颗粒 / 隐身 / 高光
// iOS 17+
FoldContainer(params: params.resolvedGeometry(), isAnimating: isAnimating) {
FoldGeometryView(params: params.resolvedGeometry()) { MyCardView() }
}
// iOS 14+
FoldFallbackView(params: params.resolvedGeometry()) {
FoldGeometryView(params: params.resolvedGeometry()) { MyCardView() }
}resolvedGeometry() 按方案分段曲线从 progress 派生几何参数:
| progress | 阶段 | 派生结果 |
|---|---|---|
| 0.00–0.25 | 磨砂开始出现 | foldAngle ≈ 0,磨砂 mask 开始增强 |
| 0.25–0.65 | 磨砂带扩展 + 内容压缩 | hingeDepth / compression 上升 |
| 0.65–0.90 | 明显 3D 翻转 | foldAngle 0 → maxAngle(默认 96°) |
| 0.90–1.00 | Alpha 收束 | 由磨砂层 hideAmount 承担 |
新增几何参数(FoldParameters 末尾字段,shader 不读取,仅 FoldGeometryView 消费):
| 参数 | 默认 | 含义 |
|---|---|---|
foldAngle |
0 | 折叠侧绕铰链翻转角度(度,0→180) |
perspective |
0.5 | 透视强度(越小畸变越强,建议 0.2~1.0) |
hingeDepth |
0 | 铰链折痕明暗 / 投影强度 0..1 |
compression |
0 | 折叠侧额外收窄 0..1(透视之外的“挤压”) |
foldsLeft |
false | true = 左半向右翻折;默认右半向左翻折 |
⚠️ 几何层每帧都在变,不能放进FoldMetalView快照路径 —— 那会要求每帧 bumpcontentVersion,彻底击穿“动画期间零截图”优化。请用FoldContainer(iOS 17+) 或FoldFallbackView(iOS 14+)叠加几何层。说明:Phase 2 的
foldAngle是几何翻转的屏幕空间近似(rotation3DEffect + 透视)。 真·几何折页已完成 —— 见下方 Phase 3(FoldPageView)。
定位:把 Geometry Pass 拿进 Metal 的目标版本。Phase 2(SwiftUI 近似)保留作为 快速预览路径;Phase 3 是 Duo 视觉的主路径。
SwiftUI 内容 ──快照一次(仅 contentVersion / scale 变化)──▶ MTLTexture
│
├─ Pass A(FoldPage.metal):双半页真实 3D 几何
│ · 铰链旋转:x = hingeX + side·u·cosθ,z = u·sinθ
│ · 透视投影(z=0 平面不漂移,铰链位置稳定不抖)
│ · 深度遮挡(depth32Float + .lessEqual:折叠半翻过 90° 正确压盖)
│ · 背面处理(|θ|>90° 自动显示背面:压暗 + 偏冷)
│ · 页边厚度(自由边沿法线挤出窄条,随角度打光)
│ · 铰链软阴影(贴在固定半平面的渐变条,随 |sinθ| 增强)
▼
├─ Pass B(FoldEffect.metal 的 foldFragment,代码零改动复用)
│ 磨砂 / 散射 / 颗粒 / 隐身 / 铰链高光(屏幕空间)
▼
屏幕
// iOS 14+,不依赖 layerEffect
FoldPageView(params: params, isAnimating: isAnimating, contentVersion: contentVersion) {
MyCardView()
}关键特性:
- 几何在 GPU:折角变化不触发任何快照(对比 Phase 2 需要实时 SwiftUI 路径),动画期间依然零截图。
- 一次 draw call 24 顶点(4 quad × 6),无顶点缓冲,
vertex_id反解 (quad, corner)。 - 角度约定(v3 规范):
foldAngle > 0= 折叠半朝观察者翻起;反向用foldsLeft或负角。 - 初始 0° 时 Pass A 输出与原始内容逐像素一致(透视在 z=0 平面不产生位移)。
演示页模式「侧翻(Metal 3D)」即该路径。
定位澄清:折叠效果本体是
foldProgress驱动的动画,陀螺仪不是"实现方式",而是可选输入源。 真·折叠设备(折叠屏)有铰链角度传感器;iPhone 没有 —— "随铰链角度变化"只能在 App 层用交互/传感器模拟。
内置 FoldMotionDriver(CoreMotion 融合姿态 + 低通平滑 + 省电启停):
@StateObject private var motion = FoldMotionDriver()
.onAppear { motion.start() } // 进入页面才采集
.onDisappear { motion.stop() } // 离开立即停(省电)
.onReceive(motion.$roll) { _ in
guard gyroDriven else { return }
// 倾角 → 折叠进度(例:±35° 满量程,倾斜方向决定折叠侧)
let degrees = motion.rollDegrees
params.foldsLeft = degrees > 0
params.progress = min(abs(degrees) / 35, 1)
}- 采集源:
CMMotionManager.startDeviceMotionUpdates(using: .xArbitraryZVertical)—— 用融合姿态而非原始陀螺仪角速度(原始值需积分、会漂移)。 - 30Hz 采集 + 低通滤波(
smoothing可调);设备静止时传感器不再上报 → 自然停止刷新。 - 模拟器没有陀螺仪(
isDeviceMotionAvailable == false),演示页会给出提示;用滑杆 / 拖拽代替测试。 - 扩展点:想做"玻璃高光随设备倾斜移动"(Liquid Glass 风格),把
roll / pitch作为新 uniform 传给foldPageFragment/foldFragment偏移高光方向即可。
| 参数 | 默认值 | 含义 |
|---|---|---|
progress |
0 | 动画进度 0→1(0=正常,1=完全侧翻) |
hingeX |
0.5 | 折叠轴 x 位置(归一化 0..1) |
effectWidth |
0.18 | 磨砂带宽度(占视图宽度比例) |
maxBlur |
18 | 轴心最大模糊半径(pt) |
hideAmount |
0.85 | 隐身强度(1 = 轴心内容完全消失) |
noiseAmount |
0.035 | 磨砂颗粒强度(>0.05 会像电视雪花) |
diffusion |
6 | 磨砂散射偏移(pt) |
highlightStrength |
0.08 | 折叠轴高光强度(>0.15 会像白线) |
highlightFalloff |
80 | 高光衰减速度(80 = 极细玻璃反光) |
thickness |
2.5 | Phase 3 页边厚度(pt,foldPage 自由边挤出) |
mask = 1 - smoothstep(0, effectWidth, |uv.x - hingeX|) // 磨砂带 mask
effect = mask * smoothstep(0, 1, progress) // 再乘动画进度
blur = effect · maxBlur // 1) 沿 X 轴 9-tap 渐进模糊
scatter = (hashNoise - 0.5) · effect · diffusion // 2) 磨砂散射
grain = (hash - 0.5) · noiseAmount · effect // 3) 极细颗粒
alpha *= 1 - effect · hideAmount // 4) 隐身(平滑淡出,非瞬间消失)
rgb += exp(-dist·falloff) · progress · strength // 5) 折叠轴高光距离折叠轴越近:模糊 ↑、散射 ↑、颗粒 ↑、透明度 ↓、高光 ↑。
| 方式 | 最低 iOS | 实时内容 | 依赖 |
|---|---|---|---|
FoldPageView(Metal 真·3D 折页) |
14.0 | ✅ 几何在 GPU;仅内容变化需 bump contentVersion |
Metal |
FoldContainer(layerEffect) |
17.0 | ✅ | SwiftUI ShaderLibrary |
FoldMetalView(MTKView 单 Pass) |
14.0 | 需 bump contentVersion |
Metal(所有真机均支持) |
FoldGeometryView(Phase 2 近似几何) |
14.0 | ✅ | 无 |
FoldFallbackView |
14.0 | ✅ | 无 |
演示页 FoldDemoView 建议 deployment target ≥ iOS 15(用了少量 iOS 15 API)。
- 三段省电:
progress == 0完全旁路 →progress > 0且非动画时渲染静态效果(不刷新时间)→ 仅isAnimating期间启动时间驱动(30fps Timeline / 60fps MTKView)。手势停止、动画结束立即停刷新。 - 带状早退:shader 内
effect < 0.0005直接返回原色,磨砂计算只发生在折叠带内(等价于把模糊区域限制在矩形)。 - 一维 9-tap 模糊:非二维 kernel,带宽压力小。
- 无纹理噪声:哈希噪声替代噪声纹理,省一次采样。
- 30fps:
FoldContainer用TimelineView(.animation(minimumInterval: 1/30));MTKView 60fps 可按需下调preferredFramesPerSecond。
layerEffectshader 的position是 user-space coordinates(pt),maxSampleOffset与position同一坐标系 —— 因此FoldShaderModifier直接用 pt 传参,不乘displayScale。maxSampleOffset必须与 shader 实际最远采样距离严格一致:模糊采样± maxBlur、散射采样± diffusion/2,代码里取max(maxBlur, diffusion/2)—— 不要手写其它值、也不要随手放大(浪费填充率),不一致会导致磨砂带边缘采样越界或被裁切。- MTKView 管线内部统一用像素(
drawableSize),内容纹理按displayScale生成保证清晰度,模糊半径由 Swift 侧换算为像素后传入。 - Swift 侧
FoldUniforms与 MetalFoldParams字段顺序严格一致(10 个 float +float2,共 48 字节),改动任何一侧必须同步另一侧。
- UIKit/AppKit 原生视图不能进入 layerEffect 过滤层(会显示占位图),不要把
FoldContainer套在MTKView/UIViewRepresentable外面。 FoldMetalView快照的是静态内容:卡片内有动画/输入框/视频/网络图片时,内容变化需contentVersion += 1(代价=重新快照一次);高频实时内容(含FoldGeometryView几何动画)请用FoldContainer(iOS 17+)或FoldFallbackView(iOS 14+)。- 隐身是 alpha 淡出,露出的是视图后方的背景。若要“磨砂玻璃透出背景”的 Apple 级质感,需要额外对背景做快照 + 玻璃合成(见下)。
- 两条渲染路径的合成约定不同(各自已按正确约定实现;改 shader 时不要混用):
- stitchable 入口按 premultiplied 约定返回颜色。依据:①
Layer.sample()文档明确返回 premultiplied RGBA;②Shader总文档明确 Shader 输出为 premultiplied 颜色 —— 两者结合推断layerEffect返回值同为 premultiplied(并非该页面逐字写明);如文档或实测有出入,以真机实测为准。实现上:rgb随新 alpha 缩放,颗粒/高光乘新 alpha 后叠加。 - MTK 的两个入口(
foldFragment/foldPageFragment)返回直色 + alpha,依赖 pipeline 的sourceAlpha混合。
- stitchable 入口按 premultiplied 约定返回颜色。依据:①
- 不宣称两路“完全视觉等价”:alpha 合成数学已对齐,但仍存在颜色空间(MTK 为 sRGB 纹理、gamma 空间混合)、边缘采样(layerEffect 透明 / MTK clampToEdge)、纹理格式(8-bit 纹理 vs 层光栅化)、颗粒密度(pt vs px)等差异。
Phase 3 已把 Geometry Pass 拿进 Metal(双半页 + 透视 + 深度遮挡 + 背面 + 厚度 + 软阴影)。继续逼近宣传片:
- 磨砂与折角联动:
effectWidth随foldAngle收窄、hideAmount随翻角增大(对应 0.90–1.00 收束段)。 - 镜面高光与环境光:给折叠半加与磨砂带高光同一光源方向的 specular;背面加轻微环境光渐变。
- 背景合成:当前“隐身”露出的是视图后方背景;若要“磨砂玻璃透出背景”的 Apple 级质感,需对背景做快照 + 玻璃合成。
- 手感曲线:翻角与
progress用同一根 0.45s easeInOut;手势阶段直接映射progress,不做弹簧,松手交给动画曲线。 - 重新评估 SwiftUI 原生 3D API:Apple 新文档已包含
visualEffect3D等能力 —— 做“更 3D”的版本前先评估原生 API 是否已满足需求,而不是默认只能走 “rotation3DEffect → Metal” 路线。
拿到实机录屏后可精确拆参数(轴位置、翻角曲线、磨砂带宽与翻角的联动关系)。