大多数浏览器和
Developer App 均支持流媒体播放。
-
借助 Core Image、Metal 和 SwiftUI 显示 EDR 内容
探索如何借助基于 Core Image 的多平台 SwiftUI App 在扩展动态范围 (EDR) 中进行渲染,并为其提供支持。我们将简要介绍使用 ViewRepresentable 向 MTKView 显示 CIImages 的最佳实践。我们还将分享启用 EDR 渲染的简单步骤,并探索 150 多种支持 EDR 的内置 CIFilter 中的一部分。
资源
相关视频
WWDC22
Tech Talks
-
下载
David: 欢迎大家 我叫 David Hayward 是 Core Image 团队的 一名软件工程师 今天我将介绍如何在 Core Image App 中 显示扩展动态范围的内容 我的讨论将分为四个部分 首先 我将介绍我们平台上的 EDR 的一些重要术语 接着我将描述一个 新的 Core Image 示例项目 然后我将使用它来演示 如何添加对 EDR 的支持 最后 我将向您展示如何 使用 CIFilter 创建 生成 EDR 内容的图像
那么让我们从一些关键术语开始吧 SDR 或标准动态范围 是表示 RGB 颜色的传统方法 使用 0 表示黑色 1 表示白色 相比之下 建议使用 EDR 或扩展动态范围 来表现超出正常范围的 RGB 颜色
与 SDR 一样 0 代表黑色 1 代表与 SDR 白色 相同的亮度 但是对于 EDR 大于 1 的值 可用于表示 更亮的像素
但请记住 虽然允许使用大于 1 的值 但超出 Headroom 的值将被截除
Headroom 由显示器 当前最大 Nit 除以 SDR 白色的 Nit 得出
请注意 headroom 值 可能因显示器而异 也可能随着环境条件 或显示器亮度的变化而变化
我建议您观看演讲 “Explore EDR on iOS” 以便更深入地讨论这些概念 您可以在 App 中 显示各种 EDR 内容源 首先 一些文件格式 如 TIFF 和 OpenEXR 可以存储用于 EDR 的浮点值
此外还可以使用 AVFoundation 从 HDR 视频格式获取帧
Metal API 可用于 将 EDR 环境渲染为纹理 此外 ProRAW DNG 文件 可通过渲染展示 EDR 高光
2021 年的演讲 “Capture and process ProRAW images” 对此进行了详细描述
在下一部分的演示中 我将描述如何在 SwiftUI App中 将 Core Image 与 Metal 结合使用 稍后 我将概述如何将 EDR 支持 添加到这个 App
我们最近发布了 一个新的示例代码项目 该项目演示了 如何在 SwiftUI 多平台 App 中 结合 Core Image 和 Metal Kit View 的最佳实践 我建议您下载示例并查看代码 但是让我借此机会向您展示 它的外观和工作原理
该示例绘制了一个动画的 程序化的 CIImage 通过 Metal 视图展示 为了获得最佳性能 该示例使用了 MTKView 为了保持代码的简单性 App 通过显示一个动态棋盘 CIImage 代表您的 App 要展示的内容
此外 该 App 使用 SwiftUI 因此可以跨 macOS iOS 和 iPadOS 平台 使用通用代码库
该项目是从几个简短的源文件构建的 所以让我来描述一下 这些类是如何交互的
这个 App 有三个重要部分 第一个也是最重要的是 MetalView 它提供了 一个兼容 SwiftUI 的 View 实现 包装了 MTKView 类
由于 MTKView 类是以 macOS 上的 NSView 和其他平台上的 UIView 为基础的 所以 MetalView 实现 使用 ViewRepresentable 来桥接 SwiftUI 和特定于平台的 MTKView 类
但是 MTKView 并不直接负责渲染 相反 它使用其委托来完成这项工作
在这个 App 中 Renderer 类 是 MTKView 的委托 它负责初始化图形状态对象 如 Metal 命令队列 和 Core Image 上下文
它还实现了 MetalView 委托 所需的 draw() 方法
但是 Renderer 并不直接负责 确定要绘制的图像 相反 它使用其 imageProvider 块 来获取要绘制的 CIImage
在这个 App 中 ContentView 类实现了 提供要渲染的 CIImage 的代码块
简而言之 MetalView 调用 其委托进行绘制 Renderer draw() 方法 调用 ContentView 来提供要绘制的图像
让我从 MetalView 类中的 makeView() 代码开始 更详细地讨论这三个类中的代码 调用 makeView() 创建 MTKView 时 它会将视图的委托 设置为 Renderer 的 StateObject 这是实现包装 NSView 或 UIView 的 SwiftUI 视图的规范方法
接下来 它将设置 preferredFramesPerSecond 以指定渲染视图的频率 此属性很重要 因为它决定了 视图绘制如何驱动 让我来描述一下它是如何工作的
此示例是一个动画 App 因此代码将 view.preferredFramesPerSecond 设置为所需的帧速率
通过设置此选项 可以配置 MTKView 驱动本身视图绘制事件的计时
这会导致视图的渲染委托 定期调用 draw() 继而会请求内容提供者 为当前时间创建一个 CIImage
并且这个过程会不断重复 直到动画暂停
在其他情况下 例如对于图像编辑 App 最好让用户可以通过控件交互 来驱动何时绘制视图
通过将 enableSetNeedsDisplay 设置为 true 就可以将 MTKView 配置为以控件 来驱动绘制事件的计时 移动控件时 应调用 updateView() 方法
然后视图的委托 将调用一次 draw()
并且每次绘制都会要求内容提供者 为当前控件状态创建一个 CIImage
这种方法也很适合 由视频帧的到达时间 来驱动的绘制事件
我对 MetalView 类的讨论到此结束 接下来 Renderer 委托中 最重要的代码是 draw() 方法
渲染器的 draw() 方法 按周期性帧率调用 当 draw() 方法被调用时 需要确定反映视图 所在显示器分辨率的内容比例因子 这是必需的 因为 CIImage 的计量单位是像素而不是点 每次调用 draw() 方法时 都执行此操作很重要 因为如果视图移动到不同的显示器 此属性可能会更改
接下来 它使用 mtlTextureProvider 创建一个 CIRenderDestination
然后它调用内容提供者 产生一个 CIImage 用于当前时间和比例因子 然后让这个返回的图像 位于视图可见区域的中心 并覆盖在不透明的背景上 然后我们开始将 CIImage 渲染到 视图目标
ContentView 类中 最重要的代码是 init() 方法
init() 方法负责创建 Content 视图的主体 这样做将建立到 Renderer 和 MetalView 类的连接
首先 它用图像提供程序块 创建一个 Renderer 对象
该块负责按照请求的时间和比例 返回一个 CIImage
最后 它将 ContentView 的 body 设置为使用 该 Renderer 的 MetalView
好了 现在完成了 我们有一个简单的 可以使用 Core Image 进行渲染的 SwiftUI App 接下来让我们看看如何修改此 App 以支持使用 EDR headroom 进行渲染
向该App 添加 EDR 支持非常简单 步骤 1 是初始化 EDR 的视图 步骤 2 是在每次渲染之前计算 headroom 步骤 3 是使用可用的 headroom 创建 CIImage 让我来向您展示 这些附加功能的实际代码 首先 MetalView 类中 还需要添加一个小环节 当您创建视图时 您需要设置 layer 的 wantsExtendedDynamicRangeContent 属性 并告诉视图它的 pixelFormat 应该是 .rgba16Float 它的色彩空间应该是 扩展的和线性的
其次 需要对 Renderer 类的 draw() 方法 进行一些更改
在 draw() 方法中 我们需要添加代码 来获取视图的当前屏幕 然后通过屏幕询问 当前的 EDR headroom
然后将 headroom 作为参数 传递给图像提供程序块 请注意 每次调用 draw() 方法时 都必须这样做 headroom 是一个动态属性 会根据环境条件 或显示亮度的变化而变化
第三个变化是 ContentView 类中的 提供程序块
在这里 我们需要将 headroom 参数 添加到图像提供程序块声明中 我们可以通过 CIFilter 使用 headroom 返回一个 CIImage 让它在用户的 EDR 显示器上 看起来非常漂亮 总而言之 这是向该 App 添加 EDR 支持的三个简单步骤 为 EDR 初始化视图 在每次渲染之前确定 headroom 并通过给定的 headroom 构建一个 CIImage 来显示 这将是接下来演讲的主题 既然 App 支持 EDR 那么让我们 使用 CIFilter 来生成 CIImage 让它显示一些 EDR 内容
Core Image 内置的 150 多个 过滤器支持 EDR 这意味着所有这些过滤器 既可以生成含有 EDR 内容的图像 也可以处理含有 EDR 内容的图像 例如 CIColorControls 和 CIExposureAdjust 过滤器 可以让您的 App 改变 改变带 EDR 色彩的图像的 亮度 色调 饱和度和对比度 在给定 EDR 颜色参数的情况下 一些过滤器 如梯度过滤器 可以生成图像
我们今年添加的三个新过滤器 也支持 EDR 图像 最值得注意的是 CIAreaLogarithmicHistogram 可以生成 任意亮度值范围的直方图
CIColorCube 过滤器是 我们的过滤器示例 我们今年做了更新 可以更好地处理 EDR 输入图像
所有这些内置的过滤器都可以正常工作 因为 Core Image 工作色彩空间 是无限制的和线性的 允许 RGB 值超出 0 到 1 的范围 在开发 App 时 您可以检查 给定的过滤器是否支持 EDR
为此 您需要创建一个过滤器实例 然后向过滤器的属性询问它的 categories 然后检查该数组是否包含 kCICategoryHighDynamicRange 此外 我们添加的一个新功能是 Xcode QuickLook 对 CIFilter 变量的调试支持 这将显示每个过滤器类的文档 包括每个输入参数的类别和要求
通过所有这些 EDR 过滤器 您的 App 可以 为其其内容提供无限可能种效果 在我今天要描述的示例中 我将在示例 App 的棋盘格图案中 添加一个具有明亮镜面反射的 波纹效果
要创建这种效果 我们需要一个 rippleTransition 过滤器的实例
接下来 我们将输入图像和目标图像 都设置为棋盘格图像
然后我们设置过滤器输入 控制波纹效果的中心点 和动画时间
然后通过 shadingImage 设置渐变 以便在波纹上产生 镜面高光反射
最后 我们从过滤器 获取 outputImage 套用所有我们设置的过滤器输入
我来描述一下如何创建将用于 创建波纹镜面高光效果的 shadingImage 我们可以从位图数据创建这个图像 但是为了获得更好的性能 我们可以用程序生成这个 CIImage
为此 我们创建了 一个 linearGradient 过滤器的实例 这个过滤器在给定两个点和两个 CIColor 的情况下创建一个渐变
我们希望镜面反射是白色的 其亮度基于当前的 headroom 但限制在一个合理的最大值内
您使用的限制将取决于 您希望应用的效果的外观
在无限制线性色彩空间中 应使用这个白色级别设置 color0
color1 设置为清除色
Point0 和 Point1 按这些坐标设置 以便镜面反射从左上角方向显示
然后将过滤器的输出图像裁剪为 波纹过滤器所需的大小
由此产生的具有镜面反射效果的波纹 只是您可以在 App 中 执行操作的简单示例 然而它确实说明了一个重要的原则 最好适度使用明亮的像素 少即是多 这样明亮的像素会更耀眼
我们现在有了一个可运行的 App 它使用 两个内置的 CIFilter 来实现 EDR 效果 请随意尝试其他内置的 EDR 过滤器 接下来我想花几分钟时间讨论 如何更好地使用 CIColorCube 过滤器 以及在编写自己的 自定义过滤器时的一些注意事项
一个非常受欢迎的过滤器是 CIColorCubeWithColorSpace 通常,此过滤器用于为 SDR 图像 套用外观 这个过滤器甚至用于实现 一些照片 App 中的效果 如 Process Instant 和 Tonal
通常这样使用的立方体数据 会有一个严格的限制 数据只能输入和输出 0 到 1 范围内的 RGB 颜色
避免此限制的一种方法是告诉 CIColorCubeWithColorSpace 过滤器 使用 EDR 色彩空间 例如 HLG 或 PQ
这可以为 EDR 内容生成最佳结果 但这需要在色彩空间范围内 创建有效的新立方体数据 此外 您可能需要增加立方体的维度 相反 您可能希望继续在 EDR 图像上 使用 SDR 立方体数据 今年的新功能是您可以要求过滤器 推演 SDR 立方体数据 要启用此功能 请按正常方式设置 SDR 立方体数据 然后设置过滤器的新属性 extrapolate
将此设置为 “true” 后 就可以通过过滤器 从一个 EDR 输入图像 获得一个 EDR 输出图像
我今天要介绍的最后一个主题 是创建自定义 CIKernel 的一些最佳实践
首先 检查内核代码中的数学运算 那些将 RGB 值限制在 0 到 1 范围内的 那些诸如 clamp min max 等等的函数
在许多情况下 这些限制可以安全地消除 内核将正常工作
其次 即使 RGB 值 可以超出 0 到 1 的范围 alpha 值仍必须介于 0 和 1 之间 否则在混合或显示图像时 会出现未定义的行为
在此示例中 内核无意中 将 alpha 通道乘以 5 而正确的行为是 仅将 RGB 值乘以 5
我的演讲到此结束 总结一下 今天我们学习了如何向 Core Image SwiftUI App 添加对 EDR headroom 的支持 以及如何使用各种内置的 CIFilter 来创建和修改 EDR 内容 感谢收看
-
-
5:17 - Metal View
// Metal View struct MetalView: ViewRepresentable { @StateObject var renderer: Renderer func makeView(context: Context) -> MTKView { let view = MTKView(frame: .zero, device: renderer.device) view.delegate = renderer // Suggest to Core Animation, through MetalKit, how often to redraw the view. view.preferredFramesPerSecond = 30 // Allow Core Image to render to the view using Metal's compute pipeline. view.framebufferOnly = false return view }
-
7:12 - Renderer
// Renderer func draw(in view: MTKView) { if let commandBuffer = commandQueue.makeCommandBuffer(), let drawable = view.currentDrawable { // Calculate content scale factor so CI can render at Retina resolution. #if os(macOS) var contentScale = view.convertToBacking(CGSize(width: 1.0, height: 1.0)).width #else var contentScale = view.contentScaleFactor #endif let destination = CIRenderDestination(width: Int(view.drawableSize.width), height: Int(view.drawableSize.height), pixelFormat: view.colorPixelFormat, commandBuffer: commandBuffer, mtlTextureProvider: { () -> MTLTexture in return drawable.texture }) let time = CFTimeInterval(CFAbsoluteTimeGetCurrent() - self.startTime) // Create a displayable image for the current time. var image = self.imageProvider(time, contentScaleFactor) image = image.transformed(by: CGAffineTransform(translationX: shiftX, y: shiftY)) image = image.composited(over: self.opaqueBackground) _ = try? self.cicontext.startTask(toRender: image, from: backBounds, to: destination, at: CGPoint.zero)
-
8:09 - ContentView
// ContentView import CoreImage.CIFilterBuiltins init(struct ContentView: View { var body: some View { // Create a Metal view with its own renderer. let renderer = Renderer( imageProvider: { (time: CFTimeInterval, scaleFactor: CGFloat) -> CIImage in var image: CIImage // create image using CIFilter.checkerboardGenerator... return image }) MetalView(renderer: renderer) } }
-
9:17 - MetalView changes
if let caMtlLayer = view.layer as? CAMetalLayer { caMtlLayer.wantsExtendedDynamicRangeContent = true view.colorPixelFormat = MTLPixelFormat.rgba16Float view.colorspace = CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3) }
-
9:35 - Get headroom
let screen = view.window?.screen; #if os(macOS) let headroom = screen?.maximumExtendedDynamicRangeColorComponentValue ?? 1.0 #else let headroom = screen?.currentEDRHeadroom ?? 1.0 #endif var image = self.imageProvider(time, contentScaleFactor, headroom)
-
10:05 - Use headroom
imageProvider: { (time: CFTimeInterval, scaleFactor: CGFloat, headroom: CGFloat) -> CIImage in var image: CIImage // Use CIFilters to create image for time / scale / headroom / ... return image })
-
12:42 - Ripple effect
let ripple = CIFilter.rippleTransition() ripple.inputImage = image ripple.targetImage = image ripple.center = CGPoint(x: 512.0, y: 384.0) ripple.time = Float(fmod(time*0.25, 1.0)) ripple.shadingImage = shading image = ripple.outputImage
-
13:34 - Generating the shading image
let gradient = CIFilter.linearGradient() let w = min( headroom, 8.0 ) gradient.color0 = CIColor(red: w, green: w, blue: w, colorSpace: CGColorSpace(name: CGColorSpace.extendedLinearSRGB)!)! gradient.color1 = CIColor.clear gradient.point0 = CGPoint(x: sin(angle)*90.0 + 100.0, y: cos(angle)*90.0 + 100.0) gradient.point1 = CGPoint(x: sin(angle)*85.0 + 100.0, y: cos(angle)*85.0 + 100.0) let shading = gradient.outputImage?.cropped(to: CGRect(x: 0, y: 0, width: 200, height: 200))
-
16:13 - CIColorCube and EDR
let f = CIFilter.colorCubeWithColorSpace() f.cubeDimension = 32 f.cubeData = sdrData f.extrapolate = true f.inputImage = edrImage let edrResult = f.outputImage
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。