レアカードのホログラムみたいなカスタムのCIFilterを作ってみた
TCGのレアカードのホログラムみたいなエフェクトをかけるCore ImageのカスタムのFilterをCIColorKernelを使って作成してみました
実装
次の3ステップで実装できます
Voronoi図?とかの話は、それぞれのセクションで取り扱います
- GameplayKitを使ってVoronoi図を作成する
- カラフルなVoronoi図に変換する
- ベースの画像にカラフルなVoronoi図を重ねる
- rotation3DEffectの回転角度に合わせて色を変える
GameplayKitを使ってVoronoi図を作成する
Voronoi図とは
Voronoi図とはある距離空間上の任意の位置に配置された複数個の母点(英: site、サイト)に対して、同一距離空間上の他の点がどの母点に近いかによって領域分けされた図のことである。
(wikipediaより)
この図のように、ランダムな点をいくつか取り、ピクセルごとにどの点が最も近いかで、色を塗り分けたグラフです

ゲームのグラフィックでは、結晶構造や水面などの表現に活用されているイメージです
今回はこのVoronoi図を使って、ホログラムの模様を作成しました
GameplayKitを使ってを作成する
GKVoronoiNoiseSourceを利用すると簡単にVoronoi図を作成できます
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ごとにグラデーションが付いていることがわかります

カラフルなVoronoi図に変換する
GameplayKitで作成したVoronoi図はモノクロのグラデーションだったので、これをカラーに変換します
CIColorKernelを利用します
let kernel = try! CIColorKernel(functionName: "mixHueGradient", fromMetalLibraryData: data)
let outputImage = kernel.apply(extent: voronoi.extent, arguments: [voronoi])!
metalをファイルを作成し、カスタムのmixHueGradientシェーダを作成します
0~1のグラデーションを色相に変換し適当にrgbの値に変えています
これにより、Cellによってカラフルな色に変わります
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;
}
}
結果がこちらです
セルごとに色が付いていることがわかります

ベースの画像にカラフルなVoronoi図を重ねる
カラフルなVoronoi図を取得できたので、ホログラム化したい画像に重ねます
引数にベースの画像を追加します
+ 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して出力します
- 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図を使ってホログラム的な効果がかかっています

rotation3DEffectの回転角度に合わせて色を変える
レアカードのホログラムの醍醐味の1つに傾けるとキラキラすることがあるので、これを再現できるようにします
Sliderで傾きを決め、rotation3DEffectで実際にImageを3次元的に回転させます
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を引数に追加します
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が変更されると、他の色にリニアに変わります
- 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でシェーダを書くのが流行ると色々楽しそうです