Metal

Metal はApple A7以降を搭載したiOS機器、およびOS X El Capitan以降が動作する一部のMacコンピュータで利用可能な、GPU を利用可能にする機能。

以下、wikiより。

2017年10月時点でのMetal及びMetal2対応Macコンピュータは以下の通り[2]

  • iMac(Late 2012 以降)
  • MacBook(Early 2015 以降)
  • MacBook Pro(Mid 2012 以降)
  • MacBook Air(Mid 2012 以降)
  • Mac mini(Late 2012 以降)
  • Mac Pro(Late 2013 以降)
  • Mac Pro(Mid 2010 以降)でMetalをサポートするGPU(Nvidia Kepler以降、ATI Graphics Core Next以降)を搭載したもの

MetalはWWDC 2014にて発表され、iOS 8で初めて導入された[3]

MetalはC++11をベースとした新しいシェーディング言語Metal Shading Language(MSL)を利用する。

Metal Shading Language での実装例

#include <CoreImage/CoreImage.h>
 
extern "C" {
    namespace coreimage {
        float4 do_nothing(sample_t s) {
            return s;
        }
    }
}

Core Image Kernel Language での実装例

kernel vec4 do_nothing(__sample s) {
    return s.rgba;
}

CIColorKernel 参照

Color Kernel の特性

  • 戻り値の型はvec4(Core Image Kernel Language)またはfloat4(Metal Shading Language)で、出力画像のピクセルカラーを返します。
  • 0個以上の入力画像を使用する場合があります。各入力画像は次のパラメータで表される。__sample(Core Image Kernel Language)または sample_t(Metal Shading Language)。これらは、vec4型の画素(Core Image Kernel Language)または float(Metal Shading Language)で扱うことができる。

Creating a General Kernel in Swift

guard
    let url = Bundle.main.url(forResource: "default", withExtension: "metallib"),
    let data = try? Data(contentsOf: url) else {
    fatalError("Unable to get metallib")
}
 
guard let generalKernel = try? CIKernel(functionName: "myKernel", fromMetalLibraryData: data) else {
    fatalError("Unable to create CIKernel from myKernel")
}

詳細は、init(functionName:fromMetalLibraryData:) を参照。

カーネルの作成

最初のステップは、デフォルトのMetalライブラリーを表す Data オブジェクトを作成する。これをXcodeでビルドし、default.metallib として、url.Bundle メソッドでロードする。

コンパイラとリンカーのオプションの指定

MSLをのシェーダー言語として使用するには、Xcodeでプロジェクトのターゲットの[Build Settings ]タブにあるいくつかのオプションを指定する必要があります。指定する必要がある最初のオプションは、Other Metal Compiler Flags オプションの ”-fcikernel” フラグです。2つ目は、user-defined に、MTLLINKER_FLAGS キーで “-cikernel” を追加する。

CIKernel の呼び出し方

import UIKit

class MyFilter: CIFilter {
    private let kernel: CIColorKernel

@objc dynamic var inputImage : CIImage?

    override init() {
        let url = Bundle.main.url(forResource: "default", withExtension: "metallib")!
        let data = try! Data(contentsOf: url)
        kernel = try! CIColorKernel(functionName: "myColor", fromMetalLibraryData: data)
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var outputImage: CIImage? {
        guard let inputImage = inputImage else {return nil}
        return kernel.apply(extent: inputImage.extent, arguments: [inputImage])
    }
}

Metal Shading Language Specification


Metal Shading Language Specification – Apple Developer

MSL Vector 型

// .xyzw または .rgba で各要素にアクセスできる。
int4 test = int4(0, 1, 2, 3);
int a = test.x; // a = 0
int b = test.y; // b = 1
int c = test.z; // c = 2
int d = test.w; // d = 3
int e = test.r; // e = 0
int f = test.g; // f = 1
int g = test.b; // g = 2
int h = test.a; // h = 3

//xyzw の並びで 複数の要素にアクセスできる
float4 c;
c.xyzw = float4(1.0f, 2.0f, 3.0f, 4.0f);
c.z = 1.0f;
c.xy = float2(3.0f, 4.0f);
c.xyz = float3(3.0f, 4.0f, 5.0f);

// 順列
float4 pos = float4(1.0f, 2.0f, 3.0f, 4.0f);
float4 swiz = pos.wzyx; // swiz = (4.0f, 3.0f, 2.0f, 1.0f)
float4 dup = pos.xxyy; // dup = (1.0f, 1.0f, 2.0f, 2.0f)

// 代入
float4 pos = float4(1.0f, 2.0f, 3.0f, 4.0f);
// pos = (5.0, 2.0, 3.0, 6.0)
pos.xw = float2(5.0f, 6.0f);
// pos = (8.0, 2.0, 3.0, 7.0)
pos.wx = float2(7.0f, 8.0f);
// pos = (3.0, 5.0, 9.0, 7.0)
pos.xyz = float3(3.0f, 5.0f, 9.0f);

MSL Matrix 型

halfnxm // n x m 16bit floating point
floatnxm // n x m 32bit floating point

float4x4 m;
// This sets the 2nd column to all 2.0.
m[1] = float4(2.0f);
// This sets the 1st element of the 1st column to 1.0.
m[0][0] = 1.0f;
// This sets the 4th element of the 3rd column to 3.0.
m[2][3] = 3.0f;

カスタムフィルターの作成

参考: Writing Custom Kernels

カスタムフィルター

class CIKernel
カスタムCore Imageフィルターの作成に使用されるGPUベースの画像処理ルーチン。

class CIColorKernel
カスタムCore Imageフィルターの作成に使用される、画像の色情報のみを処理するGPUベースの画像処理ルーチン。

class CIWarpKernel
カスタムCore Imageフィルターの作成に使用される、画像内のジオメトリ情報のみを処理するGPUベースの画像処理ルーチン。

class CIBlendKernel
2つの画像をブレンドするために最適化されたGPUベースの画像処理ルーチン。

class CISampler
フィルターカーネルによる処理のためにピクセルサンプルを取得するオブジェクト。

参考

Core Image

What You Need to Know Before Writing a Custom Filter

Core Image Kernel Language

Container View とMainView間でのデータ交換

Container View とMainView間でのデータ交換をNotificationCenterで実施する。

メインの ViewController

import UIKit

class OrigViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: .notifyName, object: nil)
    }
    
    @objc func onNotification(notification:Notification)
    {
        print("OrigViewController: onNotification called")
        
        label1.text = notification.userInfo?["data"] as? String
    }
    
    
    @IBOutlet weak var label1: UILabel!
    @IBAction func mainButtonPressed(_ sender: Any) {
        
        NotificationCenter.default.post(name: .notifyName,
                                        object: nil,
                                        userInfo: ["data": "data from OrigViewController", "isImportant": true])
    }
}

extension Notification.Name {
    static let notifyName = Notification.Name("notifyName")
}

Containerの ViewController

import UIKit

class ContainerViewController1: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: .notifyName, object: nil)
    }
    
    @objc func onNotification(notification:Notification)
    {
        print("ContainerViewController1: onNotification called")
        
        label1.text = notification.userInfo?["data"] as? String
        
    }
    
    @IBOutlet weak var label1: UILabel!
    @IBAction func button1Pressed(_ sender: Any) {
        NotificationCenter.default.post(name: .notifyName,
                                        object: nil,
                                        userInfo: ["data": "data from Container", "isImportant": true])
    }
}
画面の関係
Container 側のボタンを押した場合。メインにも通知される。

Container View Error

Container View の追加

Container View を追加する。

部品を配置する。

Container View 用の ViewController クラスを実装する。

ここでは、ControllerViewController1.swift ファイルを追加し、次のコードを実装する。

import UIKit

class ContainerViewController1: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    @IBOutlet weak var label1: UILabel!
    
    @IBAction func button1Pressed(_ sender: Any) {
        label1.text = "Pressed"
    }
}

実行すると、次のエラーが発生する。

Thread 1: Exception: “-[UIViewController button1Pressed:]: unrecognized selector sent to instance 0x7f9e9b307890”

状況

スタックの詳細は次のとおり。

2020-07-17 10:05:13.714066+0900 TestScrollView1[10517:781374] [Storyboard] Unknown class ContainerViewController1 in Interface Builder file.

2020-07-17 10:05:18.843040+0900 TestScrollView1[10517:781374] -[UIViewController button1Pressed:]: unrecognized selector sent to instance 0x7f9e9b307890

2020-07-17 10:05:18.906229+0900 TestScrollView1[10517:781374] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[UIViewController button1Pressed:]: unrecognized selector sent to instance 0x7f9e9b307890’

*** First throw call stack:

Custom Class の Module が None のままになっている。

原因

 [Storyboard] Unknown class ContainerViewController1 in Interface Builder file. のメッセージにあるように、追加したファイルが認識されていない。

対応

プロジェクトを一旦クローズし、再度プロジェクトを開く。

Custom Class の Module に値が入っている。

UserDefaults

    func init()
        if UserDefaults.standard.bool(forKey: "V1.0") == false
        {
            let defaults = [
                "Initialized" : true,
                "R" : 1.0,
                "G" : 1.0,
                "B" : 1.0
                ] as [String : Any]
            UserDefaults.standard.register(defaults: defaults)
        }
    }

    func loadUserDefaults()
    {
        if UserDefaults.standard.bool(forKey: "V1.0") == true
        {
            r = UserDefaults.standard.double(forKey: "R")
            g = UserDefaults.standard.double(forKey: "G")
            b = UserDefaults.standard.double(forKey: "B")
        }
    }

   func saveSettings() {
        UserDefaults.standard.set(true, forKey: "V1.0")
        UserDefaults.standard.set(r, forKey: "R")
        UserDefaults.standard.set(g, forKey: "G")
        UserDefaults.standard.set(b, forKey: "B")
    }

CGImage から画素データにアクセス

        let cgimage:CGImage
        let data = cgimage.dataProvider?.data
        let bytes = CFDataGetBytePtr(data)!
        
        let bytesPerPixel = cgimage.bitsPerPixel / cgimage.bitsPerComponent

        for y in 0 ..< cgimage.height {
            for x in 0 ..< cgimage.width {
                    let offset = (y * cgimage.bytesPerRow) + (x * bytesPerPixel)
                    let b = bytes[offset]
                    let g = bytes[offset + 1]
                    let r = bytes[offset + 2]
                }
            }

配列、多次元配列、セット

        let nums1D = [1, 2, 3, 4, 5, 6, 7, 8]
        var array1D1: Array<Int>           // 宣言のみ
        var array1D2 = Array<Int>()        // 空の配列 その1
        var array1D3 = [Int]()             // 空の配列 省略型 その2
        var array1D4: [Int] = []           // 空の配列 省略型 その3

        // 二次元配列
        let nums2D = [[1, 2, 3, 4], [5, 6, 7, 8]]
        var array2D1: Array<Array<Int>>    // 宣言のみ
        var array2D2: Array<[Int]>         // 宣言のみ 省略型 その1
        var array2D3: [[Int]]              // 宣言のみ 省略型 その2
        var array2D4 = Array<Array<Int>>() // 空の配列 その1
        var array2D5 = [[Int]]()           // 空の配列 省略型 その2
        var array2D6: [[Int]] = [[]]       // 空の配列 省略型 その3

map

        let array: [Int] = [1, 2, 3, 4, 5]

        let array1 = array.map{i in  i * 2 }
        print(array1)

        let array2 = array.map{$0 * 2}           // クロージャーによる表記方
        print(array2)

セット

        let set1: Set = [1, 2, 3]
        print(set1)   // 結果: [2, 3, 1] 順番は保証されない
        

        let set2: Set = [1, 2, 3, 2, 3]
        print(set2)  // 結果:  [1, 3, 2] 順番は保証されない。同じ値を持てない(ユニーク)

UISlider が操作できない

Vertical Stack に UISlider を2つ追加してみたところ、操作ができない。

UIText を追加すると、操作ができるようになる。

原因がわからなかったが、レイアウト情報を与えていなかったのが原因のようだ。

Add Missing Constraints で不足しているレイアウト位置情報を与えてあげると、操作ができるようになった。

Swift ViewController で Fatal error

Swift のViewControllerでデザインしていて、コントロールを追加したり、削除したりしていると、時々次のようなエラーが発生する。

Thread : Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

これは、コントロールと、outputやaction の接続がおかしくなった場合に発生することがある。

対応としては、 outputやaction を削除して、もう一度作り直す。

CIImage でフィルタリング

extension CIImage {
    func invert() -> CIImage? {
        guard let invertFilter = CIFilter(name: "CIColorInvert") else {return nil}

        invertFilter.setValue(self, forKey: kCIInputImageKey)

        return  invertFilter.outputImage
    }

    func convert(saturation: Double, bright: Double, contrast: Double) -> CIImage? {
        guard let filter1 = CIFilter(name: "CIColorControls") else {return nil}

        filter1.setValue(self, forKey: kCIInputImageKey)
        filter1.setValue(saturation, forKey: "inputSaturation")
        filter1.setValue(bright, forKey: "inputBrightness")
        filter1.setValue(contrast, forKey: "inputContrast")

        return filter1.outputImage
        }
    
    func temperatureAndTint(inTemp: CGFloat, outTemp: CGFloat) -> CIImage? {
        guard let filter = CIFilter(name: "CITemperatureAndTint") else {return nil}
        filter.setValue(self, forKey: kCIInputImageKey)
        filter.setValue(CIVector(x: inTemp, y: 0), forKey: "inputNeutral")
        filter.setValue(CIVector(x: outTemp, y: 0), forKey: "inputTargetNeutral")
        return filter.outputImage
    }
    
    func rgbaMatrix() -> CIImage?
    {
        guard let filter = CIFilter(name: "CIColorMatrix") else {return nil}
        let ciImage = self
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        filter.setValue(CIVector(x: 1.0, y: 0, z: 0, w: 0.0), forKey: "inputRVector")
        filter.setValue(CIVector(x: 0, y: 1.0, z: 0, w: 0.0), forKey: "inputGVector")
        filter.setValue(CIVector(x: 0, y: 0, z: 1.0, w: 0.0), forKey: "inputBVector")
        filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
        filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBiasVector")
        return filter.outputImage
    }
}