SwiftUIのViewにMetal Shaderを使ってレアカードのホログラムみたいなEffectをかけてみた
iOS17からSwiftUIにMetalのShaderを使ってEffectをかける機能が追加されました
colorEffect、distortionEffect、layerEffectの3つのmodifierです
今回は、colorEffectを使って、TCGのレアカードのホログラムみたいなEffectをSwiftUIのViewにかけてみました

View自体にEffectをかけるため、Imageだけではなく、TextやoverlayのborderにもEffectがかかっています
CIFilterを利用する場合は一度画像化する必要がありますが、colorEffectはTextなどViewに直接Effectをかけるため動的なコンテンツで特に便利そうです(今回の場合、"Rotate: X°"の部分)
実装方法
ホログラムなどによく用いられるVoronoi図を用意し、0~1のグラデーションを色相に変換し、カラフルにします
この時、Voronoiの値に0~1に変換した傾きの値をOffsetとして足して小数点部分を取ることで、傾きに応じてリニアに色が変わるようします
カラフルにしたVoronoi図を元のViewに加算合成します
これらの処理のうち、色相変換と加算合成をMetal Shaderで実行しています
色相変換部分の補足
Voronoiの値に0~1に変換した傾きの値をOffsetとして足して小数点部分を取ることで、傾きに応じてリニアに色が変わるようにする部分の補足です
例えば、Voronoiの値が0で傾きが0の場合は赤色になります
傾きが変わりOffsetが0.5になると水色に変化します
また、1を超えた値は少数点部分を取ることで色相を1周するので、ここも途切れずに色を変化させることができます

実装
Effectをかける最終系
colorEffectを使って任意のViewにEffectをかけます
holographic
という名前のShaderを作成して、voronoiの画像とoffsetの値を渡します
struct ContentView: View {
let voronoi: Image
@State var offset: Float = 0
var body: some View {
VStack {
// EffectをかけるView
effectTargetView
// colorEffect使ってEffectをかける
.colorEffect(ShaderLibrary.holographic(.image(voronoi), .float(offset)))
Slider(value: $offset, in: (-1.0...1.0))
}
}
}
Shaderの実装
新しく.metalファイルを作成し、holographic
shaderを実装します
colorEffectから参照するShaderLibrary.holographic(_:)
はビルド時に自動生成されます
colorEffectがこのような↓形のshaderをサポートするので、これに従って書きます
[[ stitchable ]] half4 name(float2 position, half4 color, args...)
.iamge(_:)引数はtexture2d、.float(_:)引数はfloatでそれぞれ受け取ります
half4 hue2Rgba(half h) {
half hueDeg = h * 360.0;
half x = (1 - abs(fmod(hueDeg / 60.0, 2) - 1));
half4 rgba;
if (hueDeg < 60) rgba = half4(1, x, 0, 1);
else if (hueDeg < 120) rgba = half4(x, 1, 0, 1);
else if (hueDeg < 180) rgba = half4(0, 1, x, 1);
else if (hueDeg < 240) rgba = half4(0, x, 1, 1);
else if ( hueDeg < 300) rgba = half4(x, 0, 1, 1);
else rgba = half4(1, 0, x, 1);
return rgba;
}
// float2 position, half4 colorはcolorEffectに含まれるデフォルトの引数
// texture2d<half> voronoiは .image(voronoi)
// float offsetは .float(offset)
[[ stitchable ]] half4 holographic(float2 position, half4 color, texture2d<half> voronoi, float offset) {
// positionがView上の位置なので、0〜1の値に正規化する(voronoiを3倍(Retina)サイズで作成しているのでx3してます)
float2 coord = float2(position.x / voronoi.get_width() * 3, position.y / voronoi.get_height() * 3);
// voronoiの値
half4 sampled = voronoi.sample(metal::sampler(metal::filter::linear), coord);
// offsetを足して少数部分を取り、色相からRGBに変換する
half4 rgba = hue2Rgba(fract(sampled.x + offset));
// 加算合成
half4 mixed = mix(color, rgba, 0.04);
mixed.a = color.a;
return mixed;
}
Voronoi図の作成
GameplayKitのGKVoronoiNoiseSourceを利用して作成します
Shaderに画像を渡す場合、引数がImageなのでImageに変換します
func makeVoronoi() -> Image {
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()
return Image(cgImage, scale: 1, label: Text(""))
}
まとめ
SwiftUIのcolorEffectを使ってレアカードのホログラムみたいなEffectを作ってみました
これまで、TextなどにEffectをかけようとすると、一度画像にしてCIFilterを使うなどする必要がありましたが、SwiftUIのMetal Shaderの機能を使うことでシンプルに実装できました
また、Textやアニメーションを伴う画面では、CIFilterなどでは都度画像化する必要がありましたが、この方法ではViewに直接Shaderを当てられるため、動的なコンテンツにもEffectをかけることができます
今回はシンプルなEffectでしたが、直接Metalに突っ込むことができるので、今後はよりリッチなEffectをSwiftUIで簡単に実現できそうです