如何在 visionOS 上使用 MPS 和 CIFilter 实现特殊视觉效果

说明

在 visionOS 开发中,视觉效果一直都是开发的一个难点。尽管苹果推出了 ShaderGraph 来简化 Shader 的开发,在此基础上我开源了 RealityShaderExtension 框架来帮助降低 Shader 开发的门槛,但在实际开发中,我们仍然面临两个问题:

  • 数学与几何知识要求太高,难以开发出满意的效果
  • 某些效果如 高斯模糊GaussianBlur直方图Histogram 单纯依靠 ShaderGraph 难以编写的,且运行效率不佳

苹果针对 ShaderGraph 功能不够强大的弱点,给出的解决方案是:使用 LowLevelTexture + Compute Shader 更加灵活的实现各种算法功能,然而手写 Metal Compute Shader 代码依然是非常困难的。

不过,苹果有一个已经高度优化的 Compute Shader 框架:Metal Performance Shaders ,我们可以直接与 LowLevelTexture 一起使用。

同时,经过研究,在 UIKit 中常用的 CIFilter 图片处理框架,也是可以与 LowLevelTexture 一起使用的,这样就无需再手动编写各种算法代码了。

同时,不仅是图片可以处理,视频也可以继续使用 AVPlayer 播放的同时,添加 MPS/CIFilter 进行处理。

图片处理

对图片处理时,MPS 和 CIFilter 的基本步骤是一样的:

  • 处理流程: MPS/CIFilter -> LowLevelTexture -> TextureResource -> UnlitMaterial

Image(MPS)

使用 MPS 进行处理时:

  • 只需要通过 commandBufferLowLevelTesxture 中获取目标纹理 outTexture
  • 将源纹理和目标纹理传递给 MPS filter 即可。

关键代码如下:

swift 复制代码
func populateMPS(inTexture: MTLTexture, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

    // Set up the Metal command queue and compute command encoder,
    .....

    // Create a MPS filter.
    let blur = MPSImageGaussianBlur(device: device, sigma: model.blurRadius)

    // set input output
    let outTexture = lowLevelTexture.replace(using: commandBuffer)
    blur.encode(commandBuffer: commandBuffer, sourceTexture: inTexture, destinationTexture: outTexture)

    
    // The usual Metal enqueue process.
    .....
}

Image(CIFilter)

使用 CIFilter 进行处理时:

  • 需要根据 outTexturecommandBuffer 创建一个 CIRenderDestination
  • [可选] 为了更好与 Metal 协作,最好创建一个 GPU-Based CIContext
  • [可选] 如果遇到颜色空间显示不正确,可以设置 options 中 .workingColorSpace 为 sRGB 等。
  • 最后调用 ciContext.startTask 将处理后的图片写入 CIRenderDestination 中。

关键代码如下:

swift 复制代码
let blur = CIFilter(name: "CIGaussianBlur")

func populateCIFilter(inTexture: MTLTexture, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

    // Set up the Metal command queue and compute command encoder,
    .......
    
    // Set the CIFilter inputs
    blur?.setValue(CIImage(mtlTexture: inTexture), forKey: kCIInputImageKey)
    blur?.setValue(model.blurRadius, forKey: kCIInputRadiusKey)

    // set input output
    let outTexture = lowLevelTexture.replace(using: commandBuffer)
    let render = CIRenderDestination(mtlTexture: outTexture, commandBuffer: commandBuffer)

    // Create a Context for GPU-Based Rendering
    let ciContext = CIContext(mtlCommandQueue: commandQueue,options: [.cacheIntermediates: false, .workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!])

    if let outImage = blur?.outputImage {
        do {
            try ciContext.startTask(toRender: outImage, to: render)
        } catch  {
            print(error)
        }
    }

    // The usual Metal enqueue process.
    ......
}

视频处理

视频处理要稍微复杂一些,需要创建 AVMutableVideoComposition 来从 AVPlayer 中获取视频帧信息再进行处理,处理后的视频继续在 AVPlayer 中直接播放,也可以另外导出到 LowLevelTexture 中进行显示。

注意:视频处理在老版本的(即 Xcode 16.4 中原始的) Vision Pro 模拟器中不能正常工作,在新的模拟器"Apple Vision Pro 4K" 中 使用 CIFilter 处理后的颜色显示不正确。不过在真机测试中,都是正常的。

Video(CIFilter)

  • 处理流程:[ CIFilter + AVMutableVideoComposition + AVPlayerItem ] -> VideoMaterial

好消息是,苹果针对 CIFilter 有一个简单方案:

  • 在创建 AVMutableVideoComposition 时创建一个闭包
  • 在闭包中通过 AVAsynchronousCIImageFilteringRequest 获取适合 CIFilter 处理的视频帧数据
  • 源视频数据直接传给 CIFilter 处理后,重新写入 AVAsynchronousCIImageFilteringRequest 即可播放出模糊后的视频。
swift 复制代码
let asset: AVURLAsset....


let playerItem = AVPlayerItem(asset: asset)

let composition = try await AVMutableVideoComposition.videoComposition(with: asset) { request in
    populateCIFilter(request: request)
}
playerItem.videoComposition = composition


// Create a material that uses the VideoMaterial
let player = AVPlayer(playerItem: playerItem)
let videoMaterial = VideoMaterial(avPlayer: player)

真正的处理代码也非常简单,将 CIFilter 的输出重新写入到 request 中即可:

swift 复制代码
let ciFilter = CIFilter(name: "CIGaussianBlur")

func populateCIFilter(request: AVAsynchronousCIImageFilteringRequest) {
    let source = request.sourceImage
    ciFilter?.setValue(source, forKey: kCIInputImageKey)
    ciFilter?.setValue(model.blurRadius, forKey: kCIInputRadiusKey)

    if let output = ciFilter?.outputImage {
        request.finish(with: output, context: ciContext)
    } else {
        request.finish(with: FilterError.failedToProduceOutputImage)
    }
}

Video(MPS)

  • 处理流程: [ MPS + AVMutableVideoComposition + AVPlayerItem ] -> LowLevelTexture -> TextureResource -> UnlitMaterial

通过 MPS 来处理视频要更加复杂一些:

  • 我们需要自定义一个 customVideoCompositorClass ,赋值给 AVMutableVideoComposition
  • 实现它的协议方法,指定输入和输出的像素格式
  • startRequest() 中获取视频帧并转换为 MTLTexture ,由 MPS 进行处理
  • [可选] 将源视频写入回去,这样就能在 AVPlayer 中继续播放源视频

自定义一个 SampleCustomCompositor,并赋值给 composition.customVideoCompositorClass

swift 复制代码
let composition = try await AVMutableVideoComposition.videoComposition(withPropertiesOf: asset)
composition.customVideoCompositorClass = SampleCustomCompositor.self

let playerItem = AVPlayerItem(asset: asset)
playerItem.videoComposition = composition

SampleCustomCompositor 需要指定我们需要的视频帧像素格式,然后就可以在 startRequest() 中获取到对应格式的视频帧,进行模糊处理。

swift 复制代码
class SampleCustomCompositor: NSObject, AVVideoCompositing {
    .....
    // 指定我们需要的视频帧格式。一定要设置 kCVPixelBufferMetalCompatibilityKey,否则与 Metal 会出现兼容性问题,导致黑屏等
    var sourcePixelBufferAttributes: [String: any Sendable]? = [
        String(kCVPixelBufferPixelFormatTypeKey): [kCVPixelFormatType_32BGRA],
        String(kCVPixelBufferMetalCompatibilityKey): true // Critical! 非常重要
    ]
    // 我们处理后返回的视频帧格式
    var requiredPixelBufferAttributesForRenderContext: [String: any Sendable] = [
        String(kCVPixelBufferPixelFormatTypeKey):[kCVPixelFormatType_32BGRA],
        String(kCVPixelBufferMetalCompatibilityKey): true
    ]

    ....


    func startRequest(_ request: AVAsynchronousVideoCompositionRequest) {

        .....

        let requiredTrackIDs = request.videoCompositionInstruction.requiredSourceTrackIDs
        let sourceID = requiredTrackIDs[0]
        let sourceBuffer = request.sourceFrame(byTrackID: sourceID.value(of: Int32.self)!)!

       
        Task {@MainActor in
            // 将模糊后的视频输出到 LowLevelTexture 中
            populateMPS(sourceBuffer: sourceBuffer, lowLevelTexture: SampleCustomCompositor.llt!, device: SampleCustomCompositor.mtlDevice!)
        }
        // 保持原视频继续输出
        request.finish(withComposedVideoFrame: sourceBuffer)
    }


    @MainActor func populateMPS(sourceBuffer: CVPixelBuffer, lowLevelTexture: LowLevelTexture, device: MTLDevice) {

        .....

        // Now sourceBuffer should already be in BGRA format, create Metal texture directly
        var mtlTextureCache: CVMetalTextureCache? = nil
        CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &mtlTextureCache)

        let width = CVPixelBufferGetWidth(sourceBuffer)
        let height = CVPixelBufferGetHeight(sourceBuffer)
        var cvTexture: CVMetalTexture?
        let result = CVMetalTextureCacheCreateTextureFromImage(
            kCFAllocatorDefault,
            mtlTextureCache!,
            sourceBuffer,
            nil,
            .bgra8Unorm,
            width,
            height,
            0,
            &cvTexture
        )
        let bgraTexture = CVMetalTextureGetTexture(cvTexture)
  
        // Create a MPS filter with dynamic blur radius
        let blur = MPSImageGaussianBlur(device: device, sigma: Self.blurRadius)
 
        // set input output
        let outTexture = lowLevelTexture.replace(using: commandBuffer)
        blur.encode(commandBuffer: commandBuffer, sourceTexture: bgraTexture, destinationTexture: outTexture)

        // The usual Metal enqueue process.
        ....
    }
}

使用 customVideoCompositorClass + MPS ,可以在 AVPlayer 输出源视频(下图左)的同时,在 LowLevelTexture 中输出模糊后的视频(下图右):

参考

项目完整示例:github.com/XanderXu/MP...

参考资料:

相关推荐
斯裕科技2 个月前
从高端制造到民生场景:天元轻量化软件的“破局”之路
ar·xr·制造·vr·虚拟现实·增强现实
huoyingcg5 个月前
武汉火影数字|VR沉浸式空间制作 VR大空间打造
人工智能·科技·vr·虚拟现实·增强现实
苹果API搬运工5 个月前
开源框架 RealityShaderExtension,帮你将 Unity 和 Unreal 的 Shader 转到 visionOS
增强现实
jimumeta7 个月前
虚拟现实(VR)与增强现实(AR)有什么区别?
ar·vr·虚拟现实·增强现实
JovaZou9 个月前
Meta 发布 Quest 3S 头显及 AR 眼镜原型:开启未来交互新视界
ai·ar·交互·虚拟现实·增强现实
虹科数字化与AR9 个月前
安宝特分享 | AR技术重塑工业:数字孪生与沉浸式培训的创新应用
ar·数字孪生·ar眼镜·增强现实·工业ar
JovaZou9 个月前
Snap 发布新一代 AR 眼镜,有什么特别之处?
ai·ar·虚拟现实·华为snap·增强现实
学步_技术10 个月前
利用AI增强现实开发:基于CoreML的深度学习图像场景识别实战教程
人工智能·深度学习·ar·增强现实·coreml
斯裕科技10 个月前
新升级|优化航拍/倾斜模型好消息,支持处理多套贴图模型!
unity·ue5·3dsmax·虚拟现实·maya·增强现实