SwiftUIでメタルシェーダーのエフェクト使ってみた
この記事は株式会社ガラパゴス(有志) Advent Calendar 2024の23日目の記事です。
前書き
こんにちは、@samechaaaです。
2024年7月からモバイルアプリエンジニアとしてガラパゴスにジョインしました。
初めて技術記事というものを書くので読みづらい点が多々あると思いますが、遠慮なくご指摘いただけると幸いです。
導入
最近、SwiftUIのVisualEffectにてメタルシェーダーを使えるmodifierがある、ということを知りました。
Metalをガッツリ書くとなると敷居が高いですが、シェーダーだけならいけそうなのでちょっと触ってみるか〜という感じで触ってみました。
基本
大体のことは公式で紹介されている通りですね。
- modifierは3つある
- それぞれの効果は
- colorEffectは色効果
- distortionEffectは歪み効果
- layerEffectはレイヤ効果
- シェーダー側の関数は規定のシグネチャがある
実践
どんな効果をつけるか迷いますが、とりあえず簡単なところでDissolve効果を作成してみます。
いい画像がなかったのでiOSDC2024の弊社のブースの画像を使います。
ここにいらすとやさんの迷彩柄の画像を使ってフィルターします。
シェーダーコードは以下の通り。
ビューのピクセル位置に対応する色を迷彩画像からサンプリングし、グレースケールを算出します。
グレースケールが閾値(threshold)を下回ったらそのピクセルは白に置き換える、というコードです。
#include <metal_stdlib>
using namespace metal;
constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
[[ stitchable ]] half4 Dissolve(float2 position, half4 color, texture2d<float> texture, float threshold) {
const float4 colorSample = texture.sample(textureSampler, position);
const float y = 0.299 * colorSample.r + 0.587 * colorSample.g + 0.114 * colorSample.b;
if(y < threshold) {
return half4(0, 0, 0, 0);
} else {
return color;
}
}
UI側では閾値をスライダーで変更できるようにしました。
struct ContentView: View {
@State var threshold: Float = 0.0
let texture = Image(.imageTexture)
var body: some View {
VStack {
Spacer()
Slider(value: $threshold, in: 0.0...1.0)
Image(.imageBooth)
.resizable()
.scaledToFit()
.visualEffect { [threshold] content, proxy in
content.colorEffect(ShaderLibrary.Dissolve(
.image(texture), .float(threshold)))
}
Spacer()
}.padding(.all, 10)
}
}
結果
あれ?
修正
色々調べたりいじったりした結果、シェーダー側に渡っているpositionはピクセル位置であり、texture coodinateのように0.0~1.0ではないということが判明しました。
ということで各コードを修正。
ビューのサイズを使ってピクセル位置からサンプリング位置(0.0~1.0)を求めるように修正しました。
シェーダーコード
#include <metal_stdlib>
using namespace metal;
constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
- [[ stitchable ]] half4 Dissolve(float2 position, half4 color, float2 size, texture2d<float> texture, float threshold) {
+ [[ stitchable ]] half4 Dissolve(float2 position, half4 color, texture2d<float> texture, float threshold) {
+ position.x = position.x / size.x;
+ position.y = position.y / size.y;
const float4 colorSample = texture.sample(textureSampler, position);
const float y = 0.299 * colorSample.r + 0.587 * colorSample.g + 0.114 * colorSample.b;
if(y < threshold) {
return half4(0, 0, 0, 0);
} else {
return color;
}
}
SwiftUI側ではサイズを渡すように修正します。
Image(.imageBooth)
.resizable()
.scaledToFit()
.visualEffect { [threshold] content, proxy in
content.colorEffect(ShaderLibrary.Dissolve(
- .image(texture), .float(threshold)))
+ .float2(proxy.size), .image(texture), .float(threshold)))
}
最終結果
想定通りの効果がつきました!
参考
- How to add Metal shaders to SwiftUI views using layer effects
- Metal in SwiftUI: How to Write Shaders - by Jacob Bartlett
- Metal Shading Language Specification
- 【Unityシェーダ入門】Dissolve(溶けるような)シェーダをつくる - おもちゃラボ
まとめ
GPUやMetalなどのローレベルな領域となると敷居が高いですが、ちょっと豪華なエフェクトつけたいな〜ぐらいであれば少しシェーダーを書くだけで済むので入りやすそうです。
Discussion