この記事はクラスター 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を選択します。
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)))
}
}
}
時間経過で元の画像の色を変化させることができました!簡単!
時間変化させる代わりに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を指定するだけです。
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 で編集する」です。