0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MetalFXのMTLFXSpatialScalerでUpscalingしてみた

Posted at

MetalFXのMTLFXSpatialScalerでUpscalingしてみた

MetalFXはWWDC22で発表されたリアルタイムアップスケーリング技術です
低解像度で描画したフレームを高解像度に拡大して表示することで、GPU負荷を小さくし、フレームレートを向上させることができます

sequence.png

MetalFXでは各フレームを個別にUpscalingする「MTLFXSpatialScaler」と、前フレームの情報やモーションベクターを活用してUpscalingする「MTLFXTemporalScaler」の2つの方法が提供されています
この2つのうち、シンプルに導入できるMTLFXSpatialScalerを試してみました

入力画像 2倍Upscaling画像
original.png scaled.png

WWDC22での発表はこちらです

Appleのサンプルコードはこちらです

今回初めて知ったのですが、英語ではアプコン/アップコンバートではなくUpscalingということが多いようです

MTLFXSpatialScalerでUpscalingする

MTLTextureをUpscalingするサンプルコード

さっそく、MTLFXSpatialScalerを使って、inputTextureをoutputTextureにUpscalingするサンプルコードです

// 前準備
let inputTexture: any MTLTexture = //...
let outputTexture: any MTLTexture = //...

let device: any MTLDevice = MTLCreateSystemDefaultDevice()!
let commandQueue: any MTLCommandQueue = device.makeCommandQueue()!

let desc = MTLFXSpatialScalerDescriptor()
desc.inputWidth = inputTexture.width
desc.inputHeight = inputTexture.height
desc.outputWidth = outputTexture.width
desc.outputHeight = outputTexture.height
desc.colorTextureFormat = inputTexture.pixelFormat
desc.outputTextureFormat = outputTexture.pixelFormat
desc.colorProcessingMode = .perceptual
let mfxSpatialScaler: any MTLFXSpatialScaler = desc.makeSpatialScaler(device: device)!

// Upscalingの実行
let commandBuffer: any MTLCommandBuffer = commandQueue.makeCommandBuffer()!
mfxSpatialScaler.colorTexture = inputTexture
mfxSpatialScaler.outputTexture = outputTexture
mfxSpatialScaler.encode(commandBuffer: commandBuffer)
commandBuffer.commit()
commandBuffer.waitUntilCompleted()

実装のポイント

  • inputTexture
    • usageMTLTextureUsage.shaderRead に設定されている必要があります
  • outputTexture
    • usageMTLTextureUsage.renderTarget に、storageModeMTLStorageMode.privateに設定されている必要があります
    • また、inputTextureとpixelFormatを一致させる必要があるようです
  • colorProcessingMode
  • MTLFXSpatialScalerの使いまわし
    • 実際にゲームなどで毎フレーム実行する場合は、MTLFXSpatialScalerを使いまわすようにするのが良いそうです
    • MTLFXSpatialScalerのインスタンス作成には時間がかかるそうです
      参考) https://developer.apple.com/documentation/metalfx#overview

実行結果

こちらが実行結果です!

入力画像 2倍Upscaling画像
image4.png res_8.png

少し効果がわかりにくいですが、、、
以下の画像は末端の部分を拡大してみました
たしかに、滑らかにUpscalingしていることがわかります

入力画像 2倍Upscaling画像
original.png scaled.png

まとめ

MetalFXのMTLFXSpatialScalerでUpscalingするのを試してみました
簡単に、滑らかにUpscalingすることができました
シンプルに組み込むことができるので、簡単にMetalベースのパイプラインのゲームやARに導入できそうです
ただ、あまりにも荒い画像や、セルルックのアウトラインのような細い線は苦手のようなので、使い所を見極める必要がありそうな印象でした

今後の展望

MTLFXSpatialScalerは各フレームを独立してUpscalingするので、シンプルに実現できました
一方で、MTLFXTemporalScalerは各フレームに加えて、depth情報やモーションベクターなどを使ってUpscalingします
これにより、動きのあるシーンでも高精度なUpscalingを実現できるそうですが、RealityKit / SceneKitでサクッとこれらの対応を入れられなさそうなので、今回は断念しましたがいつかチャレンジしてみたいです

具体的には、MTLFXTemporalScalerでは各フレームのカラーバッファに加えて以下が必要です

  • depthTexture: MTLTexture
    • カラーバッファに対応する深度バッファ(Zバッファ)
  • motionTexture: MTLTexture
    • モーションベクタ(velocity buffer)、各ピクセルが前フレームからどこに移動したかを示す
    • RG16Float などで、X/Y のベクトル成分を格納
  • jitterOffset
    • サブピクセル単位のジッターオフセット
    • テンポラル・アンチエイリアシング (TAA) 用
    • カラーバッファの描画の時点で少しずつサンプリング位置をずらす

MTLFXSpatialScalerを使ってUIImageをUpscalingするアプリコード全文

最後に、動作するアプリのコード全文を掲載しておきます
UIImageからMTLTextureへ変換し、Upscalingして、再度MTLTextureからUIImageへ変換して画面に表示します

実際に動作するアプリのコード全文はこちら!
import SwiftUI
import Metal
import MetalKit
import MetalFX

struct ContentView: View {
    @State var vm = ContentViewModel()

    var body: some View {
        VStack {
            Image(uiImage: vm.original)
                .resizable()
                .scaledToFit()
            if let result = vm.result {
                Image(uiImage: result)
                    .resizable()
                    .scaledToFit()
            }
        }
        .onAppear {
            vm.upscale()
        }
    }
}

@Observable
class ContentViewModel {
    let original = UIImage(resource: .image)
    var result: UIImage?

    private let device: any MTLDevice
    private let commandQueue: any MTLCommandQueue
    private let upscaler: Upscaler

    init() {
        device = MTLCreateSystemDefaultDevice()!
        commandQueue = device.makeCommandQueue()!
        upscaler = Upscaler(device: device)
    }

    func upscale() {
        let inputTexture = original.toTexture(device: device)
        let commandBuffer: any MTLCommandBuffer = commandQueue.makeCommandBuffer()!
        let outputTexture = upscaler.upscale(commandBuffer: commandBuffer, inputTexture: inputTexture, scale: 2)
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()
        result = outputTexture.toUIImage()
    }
}

class Upscaler {
    private let device: any MTLDevice
    private var mfxSpatialScaler: (any MTLFXSpatialScaler)!
    private var outputTexture: (any MTLTexture)!

    init(device: any MTLDevice) {
        self.device = device
    }

    func upscale(commandBuffer: any MTLCommandBuffer, inputTexture: any MTLTexture, scale: Float) -> any MTLTexture {
        // TODO: サイズやpixelFormatが異なる場合も作り直す
        if mfxSpatialScaler == nil {
            outputTexture = createEmptyTexture(
                width: Int(Float(inputTexture.width) * scale),
                height: Int(Float(inputTexture.height) * scale),
                pixelFormat: inputTexture.pixelFormat
            )
            let desc = MTLFXSpatialScalerDescriptor()
            desc.inputWidth = inputTexture.width
            desc.inputHeight = inputTexture.height
            desc.outputWidth = outputTexture.width
            desc.outputHeight = outputTexture.height
            desc.colorTextureFormat = inputTexture.pixelFormat
            desc.outputTextureFormat = outputTexture.pixelFormat
            desc.colorProcessingMode = .perceptual
            mfxSpatialScaler = desc.makeSpatialScaler(device: device)!
        }

        mfxSpatialScaler.colorTexture = inputTexture
        mfxSpatialScaler.outputTexture = outputTexture
        mfxSpatialScaler.encode(commandBuffer: commandBuffer)
        return outputTexture
    }

    private func createEmptyTexture(width: Int, height: Int, pixelFormat: MTLPixelFormat) -> any MTLTexture {
        let descriptor = MTLTextureDescriptor()
        descriptor.pixelFormat = pixelFormat
        descriptor.width = width
        descriptor.height = height
        // .renderTargetは、Upscalingのために必要
        // .shaderReadは、UIImage変換のために必要
        descriptor.usage = [.renderTarget, .shaderRead]
        descriptor.storageMode = .private
        descriptor.textureType = .type2D
        descriptor.mipmapLevelCount = 1
        return device.makeTexture(descriptor: descriptor)!
    }
}

extension UIImage {
    func toTexture(device: any MTLDevice) -> any MTLTexture {
        let loader = MTKTextureLoader(device: device)
        return try! loader.newTexture(cgImage: cgImage!, options: [:])
    }
}

extension MTLTexture {
    func toUIImage() -> UIImage {
        let ci = CIImage(mtlTexture: self, options: [:])!
        let mat = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: ci.extent.height)
        let context = CIContext()
        let cg = context.createCGImage(ci.transformed(by: mat), from: ci.extent)!
        return UIImage(cgImage: cg)
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?