レアカードのホログラムみたいなカスタムのCIFilterを作ってみた

# レアカードのホログラムみたいなカスタムのCIFilterを作ってみた
TCGのレアカードのホログラムみたいなエフェクトをかけるCore ImageのカスタムのFilterをCIColorKernelを使って作成してみました

![holo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/2fa17830-3feb-cdc3-b7b9-42ef2ea9b2fe.gif)

# 実装
次の3ステップで実装できます
Voronoi図？とかの話は、それぞれのセクションで取り扱います

1. GameplayKitを使ってVoronoi図を作成する
1. カラフルなVoronoi図に変換する
1. ベースの画像にカラフルなVoronoi図を重ねる
1. rotation3DEffectの回転角度に合わせて色を変える

## GameplayKitを使ってVoronoi図を作成する
### Voronoi図とは

> *Voronoi図とはある距離空間上の任意の位置に配置された複数個の母点（英: site、サイト）に対して、同一距離空間上の他の点がどの母点に近いかによって領域分けされた図のことである。*
(wikipediaより)

この図のように、ランダムな点をいくつか取り、ピクセルごとにどの点が最も近いかで、色を塗り分けたグラフです

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/78852dfe-8af4-9649-0d6a-01b4ad91e4f2.png" width="200">

([wikipediaより](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%AD%E3%83%8E%E3%82%A4%E5%9B%B3#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:Coloured_Voronoi_2D.svg))

ゲームのグラフィックでは、結晶構造や水面などの表現に活用されているイメージです
今回はこのVoronoi図を使って、ホログラムの模様を作成しました

### GameplayKitを使ってを作成する
[GKVoronoiNoiseSource](https://developer.apple.com/documentation/gameplaykit/gkvoronoinoisesource)を利用すると簡単にVoronoi図を作成できます

```swift
let voronoiNoiseSource = GKVoronoiNoiseSource(frequency: 20, displacement: 1, distanceEnabled: false, seed: 555)
let noise = GKNoise(voronoiNoiseSource)
let noiseMap = GKNoiseMap(noise, size: .init(x: 1, y: 1), origin: .zero, sampleCount: .init(x: 900, y: 900), seamless: true)
let texture = SKTexture(noiseMap: noiseMap)
let cgImage = texture.cgImage()
let voronoi = CIImage(cgImage: cgImage)
```

結果はこちらです
Cellごとにグラデーションが付いていることがわかります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/a468138d-1cce-d6b0-2a78-7987902afdf9.jpeg" width="300">

## カラフルなVoronoi図に変換する
GameplayKitで作成したVoronoi図はモノクロのグラデーションだったので、これをカラーに変換します
CIColorKernelを利用します

```swift
let kernel = try! CIColorKernel(functionName: "mixHueGradient", fromMetalLibraryData: data)
let outputImage = kernel.apply(extent: voronoi.extent, arguments: [voronoi])!
```

metalをファイルを作成し、カスタムのmixHueGradientシェーダを作成します
0~1のグラデーションを色相に変換し適当にrgbの値に変えています
これにより、Cellによってカラフルな色に変わります

```c
float4 hue2Rgba(float h) {
    float hueDeg = h * 360.0;
    float x = (1 - abs(fmod(hueDeg / 60.0, 2) - 1));

    float4 rgba;
    if (hueDeg < 60)
        rgba = float4(1, x, 0, 1);
    else if (hueDeg < 120)
        rgba = float4(x, 1, 0, 1);
    else if (hueDeg < 180)
        rgba = float4(0, 1, x, 1);
    else if (hueDeg < 240)
        rgba = float4(0, x, 1, 1);
    else if ( hueDeg < 300)
        rgba = float4(x, 0, 1, 1);
    else
        rgba = float4(1, 0, x, 1);

    return rgba;
}

extern "C"
{
    float4 mixHueGradient(coreimage::sample_t v, coreimage::destination dest) {
        float4 rgba = hue2Rgba(fract(v.r * 33));
        return rgba;
    }
}
```

結果がこちらです
セルごとに色が付いていることがわかります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/afa7577c-2361-928e-f5cc-e2ece2781e96.jpeg" width="300">

## ベースの画像にカラフルなVoronoi図を重ねる
カラフルなVoronoi図を取得できたので、ホログラム化したい画像に重ねます
引数にベースの画像を追加します

```diff_swift
+ let inputImage = CIImage(cgImage: UIImage(named: "icon4")!.cgImage!)
let kernel = try! CIColorKernel(functionName: "mixHueGradient", fromMetalLibraryData: data)
- let outputImage = kernel.apply(extent: voronoi.extent, arguments: [voronoi])!
+ let outputImage = kernel.apply(extent: voronoi.extent, arguments: [inputImage, voronoi])!
```

今までは、カラフルなVoronoi図をそのまま出力してましたが、1つ目の画像とmixして出力します

```diff_cpp
- float4 mixHueGradient(coreimage::sample_t v, coreimage::destination dest) {
+ float4 mixHueGradient(coreimage::sample_t i, coreimage::sample_t v, coreimage::destination dest) {
    float4 rgba = hue2Rgba(fract(v.r * 33));
-     return rgba;
+     float4 mixed = mix(i, rgba, 0.1);
+     mixed.a = i.a;
+     return mixed;
}
```

結果がこちらです
ベースの画像にカラフルなVoronoi図を使ってホログラム的な効果がかかっています

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/df04c863-57ee-84a4-77ce-c36ec6466913.jpeg" width="300">


## rotation3DEffectの回転角度に合わせて色を変える
レアカードのホログラムの醍醐味の1つに傾けるとキラキラすることがあるので、これを再現できるようにします
Sliderで傾きを決め、rotation3DEffectで実際にImageを3次元的に回転させます

```swift
struct ContentView: View {
    @ObservedObject var vm = ContentViewModel()
    var body: some View {
        VStack(spacing: 60) {
            if let image = vm.image {
                Image(uiImage: image)
                    .resizable()
                    .frame(width: 300, height: 300)
                    .rotation3DEffect(.degrees(Double(vm.offset) * 45), axis: (x: 0, y: 1, z: 0))
            }
            Slider(value: $vm.offset, in: (-1...1), step: 0.01)
        }
    }
}
```

スライダーで決定した-1~1のoffsetを引数に追加します

```diff_swift
let inputImage = CIImage(cgImage: UIImage(named: "icon4")!.cgImage!)
let kernel = try! CIColorKernel(functionName: "mixHueGradient", fromMetalLibraryData: data)
- let outputImage = kernel.apply(extent: voronoi.extent, arguments: [inputImage, voronoi])!
+ let outputImage = kernel.apply(extent: voronoi.extent, arguments: [inputImage, voronoi, offset])!
```

シェーダでは、offsetを引数としてもらい、その引数分だけ色相をずらします
これにより、offsetが変更されると、他の色にリニアに変わります

```diff_c
- float4 mixHueGradient(coreimage::sample_t i, coreimage::sample_t v, coreimage::destination dest) {
-     float4 rgba = hue2Rgba(fract(v.r * 33));
+ float4 mixHueGradient(coreimage::sample_t i, coreimage::sample_t v, float o, coreimage::destination dest) {
+     float4 rgba = hue2Rgba(fract(v.r * 33 + o));
    float4 mixed = mix(i, rgba, 0.1);
    mixed.a = i.a;
    return mixed;
}
```

これの実行結果が最初に貼ったGIFです

# まとめ
レアカードのホログラムみたいなカスタムのCIFilterを作ってみました
GameplayKitのおかげで、簡単にVoronoi図を作成できました
今回はCoreImageでエフェクトをかけたのですが、SwiftUIがMetalシェーダのエフェクトに対応するらしいので、これからMetalでシェーダを書くのが流行ると色々楽しそうです