使用 Core Image、Metal 和 SwiftUI 展示 EDR 内容
Display EDR content with Core Image, Metal, and SwiftUI
2022年6月6日
一句话判断
如果你需要在 App 中展示 HDR/EDR 内容(如 HDR 视频帧、ProRAW 高光、OpenEXR 图像),这场 Session 提供了从概念到代码的完整实现路径。
这场 Session 讲了什么
Core Image 团队的 David Hayward 系统讲解了 EDR(Extended Dynamic Range)在 Core Image 中的使用。EDR 允许像素值超过标准的 0-1 范围,用大于 1 的值表示比 SDR 白更亮的像素,前提是不超过 display headroom。
Session 围绕一个新发布的示例项目展开:如何在 SwiftUI + Metal Kit View + Core Image 的组合架构中渲染动态图像,然后如何在这套架构上添加 EDR 支持。三个关键步骤:初始化 View 支持 EDR、每次渲染前获取当前 headroom、构建利用 headroom 的 CIImage。
值得深挖的点
Headroom 是动态变化的。 它由显示器的最大尼特值除以 SDR 白的尼特值得出,不同显示器、不同环境亮度、不同显示亮度设置下 headroom 都不同。因此每次 draw() 都需要重新获取,不能缓存。
Core Image + Metal + SwiftUI 的三层架构。 MetalView(SwiftUI 包装 MTKView)调用 Renderer(MTKView delegate)执行 draw(),Renderer 通过 imageProvider 回调 ContentView 获取 CIImage。这种分层让渲染管线和内容生成分离,逻辑清晰。
CIRenderDestination 的使用。 通过 Metal texture 创建 CIRenderDestination,将 CIImage 渲染到 Metal view。Content scale factor 需要在每次 draw() 时获取,因为 View 可能被移到不同的显示器上。
代码片段
// 步骤 1:初始化 MetalView 支持 EDR
func makeView() -> MTKView {
let view = MTKView()
view.delegate = renderer
// 启用 EDR 支持
view.layer?.wantsExtendedDynamicRangeContent = true
view.pixelFormat = .rgba16Float
view.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearSRGB)!
view.preferredFramesPerSecond = 60
return view
}
// 步骤 2:在 draw() 中获取 headroom
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable else { return }
// 每次渲染都获取当前 headroom
let screen = view.window?.screen
let headroom = screen?.maximumPotentialExtendedDynamicRangeColorComponentValue ?? 1.0
let scale = view.traitCollection.displayScale
let destination = CIRenderDestination(mtlTextureProvider: { drawable.texture })
// 将 headroom 传给内容生成器
if let image = imageProvider(CACurrentMediaTime(), scale, headroom) {
let centered = image.transformed(by: .init(translationX: offsetX, y: offsetY))
try? context.startTask(toRender: centered, to: destination)
}
}
// 步骤 3:构建利用 headroom 的 CIImage
func createEDRImage(time: TimeInterval, scale: CGFloat, headroom: Float) -> CIImage {
// SDR 背景区域:值在 0-1 范围
let baseImage = CIImage(color: CIColor(red: 0.1, green: 0.1, blue: 0.1))
// EDR 高亮区域:值可以超过 1.0
let edrColor = CIColor(red: 0.0,
green: CGFloat(headroom) * 0.8,
blue: CGFloat(headroom) * 0.5)
let highlight = CIImage(color: edrColor).cropped(to: highlightRect)
return baseImage.composited(over: highlight)
}
最佳实践
- EDR 内容需要 16-bit 浮点像素格式。 使用
.rgba16Float和 extended linear sRGB 色彩空间,标准 8-bit 格式无法表达超过 1.0 的值。 - 动画场景用
preferredFramesPerSecond驱动。 让 MTKView 自己驱动 draw 时机。编辑类 App 则用enableSetNeedsDisplay,让用户交互触发重绘。 - Headroom 随时可能变化,不要缓存。 每次进入 draw() 都重新获取,确保内容亮度与当前显示环境匹配。
- CIImage 用像素而非点来度量。 Content scale factor 是两者之间的桥梁,渲染前必须正确设置。
还有什么值得关注
- EDR 内容来源包括:TIFF/OpenEXR 文件、AVFoundation 的 HDR 视频帧、Metal 渲染的 EDR 纹理、ProRAW DNG 文件
- 推荐观看 “Explore EDR on iOS” 了解 EDR 的基础概念
- 2021 年的 “Capture and process ProRAW images” 详细讲解了 ProRAW 的 EDR 渲染
- 新示例项目 “Using Core Image with Metal in SwiftUI” 可从 Apple Developer 网站下载