LoginSignup
1
1

SwiftUIでMetal Shaderを扱う

Last updated at Posted at 2023-12-08

この記事はクラスター Advent Calendar 2023シリーズ2の8日目の記事です。

こんにちは。クラスター株式会社でソフトウェアエンジニアをやっているshindyu(しんじゅ)です。

前日の記事は 獏星(ばくすたー) さんの「オフィスアワーをやってみている話」でした!
普段から気軽に話せるオフィスアワーがあることで業務にも良い影響を与えていそうです。

SwiftUIでMetal Shaderを扱う

さて、clusterのiOSアプリ開発では新しい画面作成時には積極的にSwiftUIを利用しています。
そして今年出たiOS 17からはSwiftUIでも手軽にMetal Shaderを扱えるようになりました。

これまでMetalについて興味はあったものの難しそうだな、と少し敬遠していたのですが
アドベントカレンダー駆動ということでこの機会に試してみます。

iOS 17から追加されたcolorEffect, distortionEffect, layerEffectを使うことで、SwiftUIのViewに対してMetal Shaderを簡単に適用することができます。

SwiftUIでこちらのsample画像に対してcolorEffectを適用してみましょう。

まずはMetal Shaderを作成します。作成する際はFile > NewからMetal Fileを選択します。

スクリーンショット 2023-12-07 21.35.02.png

Metal Fileの中身は以下のようになります。
今回はsampleGradientというShader関数を作成しました。
シェーダー関数がカラーフィルターとして機能するためには、関数シグネチャが以下と一致している必要があります。
[[ stitchable ]] half4 name(float2 position, half4 color, args...)

positionはピクセルの位置、colorはピクセルの色で、その後に続くfloat timeが自分で定義した引数になります。

#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]] half4 sampleGradient(float2 position,
                                    half4 color,
                                    float time) {
    // 元の色(color)を時間経過(time)で変化させた色を返している
    return half4(abs(color.r + cos(time) / 2),
                 abs(color.g + sin(time) / 3),
                 abs(color.b + sin(time) / 5),
                 1.0);
}

次に、作成したsampleGradientをSwiftUIのcolorEffectから利用します。

ShaderLibrary@dynamicMemberLookupを実装しているため、先ほど作成したMetal Shaderを扱う際は以下のように書くことができます。

struct Sample: View {
    private let date = Date()

    var body: some View {
        // TimelineViewは指定したスケジュールで更新を行なってくれるView
        TimelineView(.animation) {
            let time = date.timeIntervalSince1970 -  $0.date.timeIntervalSince1970
            
            Image(.sample)
                .foregroundStyle(Color.white)
                .aspectRatio(contentMode: .fit)
                .colorEffect(ShaderLibrary.sampleGradient(.float(time)))
        }
    }
}

時間経過で元の画像の色を変化させることができました!簡単!

sample_image.gif

時間変化させる代わりにCoreMotionを使えばiPhoneの傾きに応じて見え方が変わるホログラム画像のような表現になります。

import SwiftUI
import CoreMotion

struct Sample: View {
    private let motionManager = CMMotionManager()
    @State private var acceleration: CMAcceleration = .init()

    var body: some View {
        Image(.sample)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .colorEffect(
                ShaderLibrary.sampleGradientMotion(
                    .boundingRect,
                    .float(acceleration.x),
                    .float(acceleration.y),
                    .float(acceleration.z)
                )
            )
            .onAppear() {
                motionManager.accelerometerUpdateInterval = 1/60
                motionManager.startAccelerometerUpdates(to: .main) { data, error in
                    if let data = data {
                        acceleration = data.acceleration
                    }
                }
            }
        }
    }
}

ちなみにTextにもShaderを適用できます。
foregroundStyleでMetal Shaderを指定するだけです。

sample_text.gif

	Text("Sample Effect")
		.font(.largeTitle)
		.foregroundStyle(ShaderLibrary.sampleGradientText(float(time)))
}
[[ stitchable ]] half4 sampleGradientText(float2 position,
                                    float time) {
        return half4(abs(sin(time)),
                     abs(cos(time)),
                     abs(sin(time)),
                     1.0);
}

まとめ

SwiftUIで簡単にMetal Shaderを扱うことができました。
今回Shader内の処理はいずれも簡単な実装になっていますが、Shadertoyなどを参考にすればもっと面白いShaderを作ることもできそうです。

機会があればclusterアプリでもMetal Shaderを使ったリッチな表現にチャレンジしていきたいですね。

明日はchougeさんの「cluster のスクリプトを WSL 上の Vim で編集する」です。

1
1
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
1
1