20
9

More than 1 year has passed since last update.

SwiftUIにMetal ShaderでレアカードのホログラムみたいなEffect

Posted at

SwiftUIのViewにMetal Shaderを使ってレアカードのホログラムみたいなEffectをかけてみた

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

View自体にEffectをかけるため、Imageだけではなく、TextやoverlayのborderにもEffectがかかっています
CIFilterを利用する場合は一度画像化する必要がありますが、colorEffectはTextなどViewに直接Effectをかけるため動的なコンテンツで特に便利そうです(今回の場合、"Rotate: X°"の部分)

Frame 286 (1).png

実装方法

ホログラムなどによく用いられるVoronoi図を用意し、0~1のグラデーションを色相に変換し、カラフルにします
この時、Voronoiの値に0~1に変換した傾きの値をOffsetとして足して小数点部分を取ることで、傾きに応じてリニアに色が変わるようします
カラフルにしたVoronoi図を元のViewに加算合成します
これらの処理のうち、色相変換と加算合成をMetal Shaderで実行しています

Frame 287 (2).png

色相変換部分の補足

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でそれぞれ受け取ります

Shader.metal
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で簡単に実現できそうです

20
9
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
20
9