Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

ios duo — “侧翻 → 局部磨砂 → 内容逐渐隐身” Metal 实现包

按方案落地: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) 三种模式)

快速集成

  1. FoldEffect/ 整个文件夹拖进 Xcode 工程,勾选 App target。
  2. 两个 .metalFoldEffect.metal / FoldPage.metal)会自动进入 target 的 Compile Sources 并编译进 default Metal library(无需任何设置)。
  3. 预览效果:ContentView 里直接写 FoldDemoView()
  4. 接入自己的界面:按下方 API 选用。

⚠️ Swift Package / SwiftPM 不支持编译 .metal 文件。若工程是 SPM,请把两个 .metal 直接加入 App target(其余 Swift 文件可以留在包里)。

测试指南(怎么验证)

A. 有 Mac(推荐,唯一能真正编译的路径)

  1. Xcode → New → Project → iOS App(SwiftUI、Swift),Deployment Target 设 15.0(核心组件 14+,演示页用了少量 15+ API)。
  2. 把整个 FoldEffect/ 文件夹拖进工程,勾选 App target(2 个 .metal + 8 个 .swift 一起进入)。
  3. ContentViewFoldDemoView(),选真机 Run。
测试项 模拟器 真机 怎么看
磨砂 / 隐身动画 拖卡片 / 点「播放侧翻」/ 拉滑杆
Metal 3D 折页(模式「侧翻(Metal 3D)」) 0°→90°:自由边厚度、翻过 90° 的背面、铰链软阴影
陀螺仪驱动 ❌ 无传感器 打开「陀螺仪驱动」开关,倾斜设备(±35° 满量程)
帧率 / 发热 不代表真机 Xcode → Debug navigator → GPU / Energy
磨砂带贴边界的边缘表现 把 hingeX 拉到 0.05 / 0.95 看带边缘

B. 没有 Mac

  • 用云 Mac 构建(你现有 safevault_ios 的 Codemagic 流程可复用):新建最小 App target,把 FoldEffect/ 拖进去,workflow 出 TestFlight 包,用真机安装测试。
  • "Windows 本地直接编译 iOS" 不存在(Metal 编译器只在 Xcode 里),不要在这条路上花时间。

C. 验收清单(对着实机看)

  1. 0°→30°→60°→90°:磨砂带应是"吞掉内容",而不是一块贴上来的模糊玻璃。
  2. 铰链处:允许极细高光,出现明显白线/黑线说明 highlightStrength 或折痕参数需要调。
  3. 90° 附近:折叠半背面(压暗 + 偏冷)自然,与固定半的压盖关系正确、无穿插闪烁。
  4. 静止在 progress=0.6:不持续刷新(省电契约,可看 Xcode Energy 报告)。
  5. 2x / 3x 设备各看一遍:颗粒密度与模糊观感基本一致。

三套 API

A. iOS 17+:FoldContainer(SwiftUI 原生 shader,支持实时内容)

@State private var params = FoldParameters.standard

FoldContainer(params: params) {
    MyCardView()          // 任何 SwiftUI 内容,实时渲染
}

// 驱动动画
withAnimation(.easeInOut(duration: 0.45)) {
    params.progress = 1
}
  • 优点:无快照、内容实时、代码最少。
  • 内部:progress == 0 时零开销旁路;动画期间用 30fps TimelineView 驱动颗粒流动。
  • 也可用底层 API:MyCardView().foldEffect(params, time: t, viewSize: geo.size)

B. iOS 14+:FoldMetalView(MTKView 自建管线,推荐给“核心视觉”场景)

FoldMetalView(params: params, isAnimating: isAnimating, contentVersion: contentVersion) {
    MyCardView()
}
  • 数据流:内容只快照一次MTLTexture → 之后每帧纯 GPU 跑 foldFragment(折叠动画期间零截图、零 CPU)。
  • contentVersion 契约:内容发生真实变化(数据/状态更新)时才 += 1,触发一次重新快照;不变就一直复用旧纹理。绝不要每帧快照
  • isAnimating 契约:手势/动画期间传 true(60fps 渲染循环),结束后传 false(渲染循环暂停,仅补画静态帧)。
  • 适合“内容不变、只有效果在动”的折叠卡片场景;内容是实时动画(如 FoldGeometryView)时不要用这条路径

C. iOS 14+:FoldFallbackView(无 Metal 近似回退)

FoldFallbackView(params: params) {
    MyCardView()
}
  • 用“blur 副本 + 带状 mask + opacity”近似;无散射/颗粒/高光,双层渲染,仅适合低要求场景。

Phase 2:SwiftUI 3D 视觉近似侧翻(FoldGeometryView

命名澄清: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 快照路径 —— 那会要求每帧 bump contentVersion,彻底击穿“动画期间零截图”优化。请用 FoldContainer(iOS 17+) 或 FoldFallbackView(iOS 14+)叠加几何层。

说明:Phase 2 的 foldAngle 是几何翻转的屏幕空间近似(rotation3DEffect + 透视)。 真·几何折页已完成 —— 见下方 Phase 3(FoldPageView)。

Phase 3:Metal 真·3D 折页(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 偏移高光方向即可。

参数表(FoldParameters

参数 默认值 含义
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 自由边挤出)

Shader 分层公式(FoldEffect.metal

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,带宽压力小。
  • 无纹理噪声:哈希噪声替代噪声纹理,省一次采样。
  • 30fpsFoldContainerTimelineView(.animation(minimumInterval: 1/30));MTKView 60fps 可按需下调 preferredFramesPerSecond

关键坐标系事实(已核实 Apple 官方文档)

  • layerEffect shader 的 positionuser-space coordinates(pt)maxSampleOffsetposition 同一坐标系 —— 因此 FoldShaderModifier 直接用 pt 传参,不乘 displayScale
  • maxSampleOffset 必须与 shader 实际最远采样距离严格一致:模糊采样 ± maxBlur、散射采样 ± diffusion/2,代码里取 max(maxBlur, diffusion/2) —— 不要手写其它值、也不要随手放大(浪费填充率),不一致会导致磨砂带边缘采样越界或被裁切。
  • MTKView 管线内部统一用像素drawableSize),内容纹理按 displayScale 生成保证清晰度,模糊半径由 Swift 侧换算为像素后传入。
  • Swift 侧 FoldUniforms 与 Metal FoldParams 字段顺序严格一致(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 混合。
  • 不宣称两路“完全视觉等价”:alpha 合成数学已对齐,但仍存在颜色空间(MTK 为 sRGB 纹理、gamma 空间混合)、边缘采样(layerEffect 透明 / MTK clampToEdge)、纹理格式(8-bit 纹理 vs 层光栅化)、颗粒密度(pt vs px)等差异。

下一步:Phase 3 之后的增强方向

Phase 3 已把 Geometry Pass 拿进 Metal(双半页 + 透视 + 深度遮挡 + 背面 + 厚度 + 软阴影)。继续逼近宣传片:

  1. 磨砂与折角联动effectWidthfoldAngle 收窄、hideAmount 随翻角增大(对应 0.90–1.00 收束段)。
  2. 镜面高光与环境光:给折叠半加与磨砂带高光同一光源方向的 specular;背面加轻微环境光渐变。
  3. 背景合成:当前“隐身”露出的是视图后方背景;若要“磨砂玻璃透出背景”的 Apple 级质感,需对背景做快照 + 玻璃合成。
  4. 手感曲线:翻角与 progress 用同一根 0.45s easeInOut;手势阶段直接映射 progress,不做弹簧,松手交给动画曲线。
  5. 重新评估 SwiftUI 原生 3D API:Apple 新文档已包含 visualEffect3D 等能力 —— 做“更 3D”的版本前先评估原生 API 是否已满足需求,而不是默认只能走 “rotation3DEffect → Metal” 路线。

拿到实机录屏后可精确拆参数(轴位置、翻角曲线、磨砂带宽与翻角的联动关系)。

About

iOS上实现DUO效果

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages