Explore HDR rendering with EDR
Graphics & Games 高级 26m

探索基于 EDR 的高动态范围渲染

Explore HDR rendering with EDR

2021年6月9日

在 Apple 官方观看视频

一句话判断

EDR(Extended Dynamic Range)是 Apple 在标准显示器上模拟 HDR 效果的方案——如果你的 App 处理 HDR 内容但只在 SDR 屏幕上显示,你一定要理解 EDR,否则你的高光细节全部被浪费了。

这场 Session 讲了什么

Session 介绍了 Extended Dynamic Range(EDR)渲染的概念和 API。EDR 允许 App 在支持 HDR 的显示器上输出超过 SDR 范围的亮度值,在不支持 HDR 的显示器上自动将高动态范围内容映射回 SDR 范围。

在 Apple 的显示系统中,SDR 的亮度定义为 1.0,EDR 允许亮度值超过 1.0——在 Pro Display XDR 上最高可达约 6.0(即 SDR 亮度的 6 倍),在 MacBook Pro 上可达约 2.0-3.0。这意味着你的渲染管线可以输出 float3(2.0, 1.5, 3.0) 这样的颜色值,系统会自动决定如何呈现:HDR 屏幕上直接显示更亮的像素,SDR 屏幕上做 tone mapping 压缩到 1.0 范围。Session 详细讲解了 CAEDRMetadataNSScreen.maximumExtendedDynamicRangeColorComponentValue 等 API 的使用方式。

值得深挖的点

EDR 的亮度头room(Headroom)概念

maximumExtendedDynamicRangeColorComponentValue 属性返回当前显示设备的最大 EDR 亮度倍数。在 Pro Display XDR 上大约是 6.0,普通 MacBook Pro 约 2.0-3.0,不支持 HDR 的显示器是 1.0。你的渲染管线应该根据这个值动态调整:headroom 大时保留更多高光细节,headroom 为 1.0 时做更激进的 tone mapping。这个值在运行时可能变化(比如用户从 HDR 外接显示器切换到内置 SDR 屏幕),需要通过 KVO 监听。

CAMetalLayer 的 EDR 配置

要在 Metal 渲染中使用 EDR,需要做三件事:设置 CAMetalLayer.wantsExtendedDynamicRangeContent = true、使用 RGBA16Float 像素格式、确保渲染输出的颜色值超过 1.0。系统自动处理后续步骤——不需要你指定输出是 HDR 还是 SDR。这种”write once, display everywhere”的设计很优雅,但也意味着你需要在渲染时注意颜色值的范围——不小心输出 10.0 的亮度在 SDR 屏幕上会被压缩成一团白色。

代码片段

// 配置 Metal layer 以支持 EDR 渲染
func setupEDRLayer(metalLayer: CAMetalLayer, screen: NSScreen?) {
    // 启用 EDR 内容输出
    metalLayer.wantsExtendedDynamicRangeContent = true
    
    // 必须使用 16 位浮点格式才能输出超过 1.0 的亮度值
    metalLayer.pixelFormat = .rgba16Float
    
    // 使用 Display P3 色彩空间
    metalLayer.colorspace = CGColorSpace(name: CGColorSpace.displayP3)
    
    // 查询当前设备的 EDR headroom
    if let screen = screen {
        let maxEDR = screen.maximumExtendedDynamicRangeColorComponentValue
        print("当前设备 EDR headroom: \(maxEDR)x")
        // Pro Display XDR: ~6.0
        // MacBook Pro (14"/16"): ~2.0-3.0  
        // 普通 SDR 显示器: 1.0
    }
}
// 监听 EDR headroom 变化(用户切换显示器时)
func monitorEDRChanges(screen: NSScreen) {
    screen.addObserver(self, 
                       forKeyPath: "maximumExtendedDynamicRangeColorComponentValue",
                       options: .new, context: nil)
}

override func observeValue(forKeyPath keyPath: String?, 
                           of object: Any?,
                           change: [NSKeyValueChangeKey: Any]?,
                           context: UnsafeMutableRawPointer?) {
    if keyPath == "maximumExtendedDynamicRangeColorComponentValue" {
        let newHeadroom = (object as? NSScreen)?
            .maximumExtendedDynamicRangeColorComponentValue ?? 1.0
        // 动态调整 tone mapping 参数
        updateToneMapping(headroom: newHeadroom)
    }
}
// Metal fragment shader 中输出 EDR 颜色
fragment float4 hdrFragmentShader(
    VertexOutput in [[stage_in]],
    texture2d<float, access::sample> hdrTexture [[texture(0)]],
    constant float& edrHeadroom [[buffer(0)]]
) {
    float4 color = hdrTexture.sample(sampler(mag_filter::linear, 
                                              min_filter::linear), 
                                      in.texCoord);
    
    // 应用色调映射,保留 EDR headroom 范围内的细节
    // Reinhard tone mapping 的 EDR 版本
    float luminance = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
    
    if (edrHeadroom > 1.0) {
        // HDR 显示器:保留高光细节直到 headroom 上限
        color.rgb = color.rgb * edrHeadroom;
    } else {
        // SDR 显示器:压缩到 [0, 1] 范围
        color.rgb = color.rgb / (1.0 + luminance);
    }
    
    return color;
}

最佳实践

  • 在渲染管线中使用 color.w 通道存储 EDR 亮度增益(brightness scale),而不是直接把 RGB 值乘到很大。这样在 tone mapping 时更容易控制。
  • 提供用户可调节的 EDR 强度滑块——不是所有用户都喜欢超亮的高光,尤其是夜间使用场景。
  • 测试 HDR 渲染时务必在 SDR 和 HDR 两种屏幕上验证效果。不要只在 HDR 屏幕上开发——你 SDR 用户看到的效果可能完全不同。

还有什么值得关注

  • iOS 设备的 EDR headroom 目前固定为 1.0(iPhone 和 iPad 不支持超过 SDR 的亮度输出),EDR 主要用于 macOS。
  • CAEDRMetadata 可以附加到 CALayer 上,告诉系统这段内容的 HDR 元数据(比如 MaxCLL、MaxFALL),用于更精确的显示。
  • AVPlayer 在播放 HDR 视频时自动使用 EDR,不需要开发者配置——这个 Session 主要针对自定义渲染场景。
WWDC 2021