SwiftUI Metal Shader入門 + GLSL->MSLへの置き換え
はじめに
- これまでMetalを触ろうと思うと、queueやpipelineなどの低レイヤーの話がでてきて、ちょっと手を出すにはハードルが高いものでした。
- そんな折、WWDC2023でSwiftUIで使えるMetal Shaderのmodifierが登場しました。
- これにより低レイヤーを意識せずに実装が可能となり、Metal Shaderを気軽に試せるようになりました。
- ただWWDCでもWhat’s new in SwiftUIで軽く触れられるくらいで、また公式ドキュメントも簡潔にしか書かれていません。
- 幸い技術記事は多いのでこれらを参考に、私の中で体系化した内容や、既存のシェーダー(ShaderToyのGLSL)をMetalへ書き換えるときの話を本記事で行いたいと思います。
参考
-
Metal for SwiftUI
- わかりやすく入門におすすめ
- iOS 17 天気アプリの雨粒演出を作ってみた
- SwiftUIにMetal ShaderでレアカードのホログラムみたいなEffect
- SwiftUI Metal Shader API in iOS 17
- How to add Metal shaders to SwiftUI views using layer effects
-
SwiftUI transitions with distortion effect and Metal Shaders
- Shaderを使った画面遷移
GitHub
-
swiftui-metal-shader-tutorial
- よかったらスターをしていただけると嬉しいです!
Shader modifier入門
colorEffect入門
- まずは実際にShaderをどのように書けるのかを試してみましょう。
- 今回は引数で渡したColorに塗りつぶすようなShaderを作成します。
- Shaderのmodifierは以下のように書けます。
- modifierはいくつか種類があり(後述)、ここではcolorEffect(_:isEnabled:)を使用します。
-
ShaderFunction
で使用するシェーダーを指定します。- 今回は
fillColor
というシェーダーを指定しており、これは後ほど作成するMetalファイル内で実装します。 - またargumentsはShaderに渡す引数で、Shader.Argumentというstructに変換して渡します。
- 今回はSwiftUIの
Color
をShader.Argument
に変換しています。
- 今回は
Rectangle()
.frame(width: 100, height: 100)
.colorEffect(
Shader(function: ShaderFunction(library: .default,name: "fillColor"),
arguments: [.color(Color.blue)])
)
- また以下のようにより簡潔に書くことが出来ます。
- ShaderLibraryは
@dynamicMemberLookup
を実装しているのでこのように書けるとのこと(参考)
- ShaderLibraryは
Rectangle()
.frame(width: 100, height: 100)
.colorEffect(
ShaderLibrary.default.fillColor(.color(Color.blue))
)
- では先程宣言した
fillColor
というMetal Shaderを実装します。 - 下記のように適当な名前でMetalファイルを作成します。
- Shader関数は以下の通りです。
- 言語はMetal Shader Language(以下MSL)を使います。
-
stitchable
はMSL関数を表します(参考) -
half4
はこの関数の返り値の型で、fillColor
が関数名です。 - ここでは引数は3つあり、上2つが
colorEffect
を使うとデフォルトで渡される値で、それぞれ現在のピクセルの位置と色となります。- ピクセル毎にこのMSL関数が呼ばれるので、ピクセル毎の操作が可能となっています。
- 3つ目以降の引数が
arguments
で渡した引数に対応します。 -
colorEffect
の場合、関数の返り値が現在の位置のピクセルの色に反映されるため、今回の場合は元の図形がnewColor
で塗りつぶされることになります。
#include <metal_stdlib>
using namespace metal;
[[ stitchable ]] half4 fillColor
(
float2 position, // 現在のピクセルの位置(デフォルトで与えられる)
half4 color, // 現在のピクセルの色(デフォルトで与えられる)
half4 newColor
) {
return newColor; // r, g, b, a
}
- 以上でビルドをすると以下のように、指定した色で塗りつぶされたRectangleが表示されます。
- また例えば下記のようにx座標が30px未満の場合に元の色を返す、という風にすると以下の通りになります。
#include <metal_stdlib>
using namespace metal;
[[ stitchable ]] half4 fillColor
(
float2 position, // 現在のピクセルの位置(デフォルトで与えられる)
half4 color, // 現在のピクセルの色(デフォルトで与えられる)
half4 newColor
) {
if (position.x < 30 ) {
return color;
}
return newColor; // r, g, b, a
}
- また
colorEffect
の別の例として、画像をモノクロ化するShaderの実装です。 - ここでは元のViewの色を使用しており、現在のピクセルの色からグレーの色を算出しています。
Image(.xcode)
.resizable()
.frame(width: 100, height: 100)
.colorEffect(
ShaderLibrary.default.monochrome()
)
[[ stitchable ]] half4 monochrome
(
float2 position,
half4 color
) {
half v = (color.r + color.g + color.b) / 3.0;
return half4(v, v, v, 1.0);
}
- このようにcolorEffect(_:isEnabled:)は、下記の用途で利用できるmodifierといえます。
- 自由に図形などの描画を行いたい場合
- 元のViewの色を操作したい場合
Shader modifierの種類とその使い分け
- さて先程modifierはいくつか種類があると言いましたが、下記の3つがあります。
-
colorEffect(_:isEnabled:)
- 元のViewの色を操作したい場合(例: モノクロ化・赤色を抜く)
- 自由に図形などの描画を行いたい場合(元のViewからの情報は描画領域だけを採用する)
-
distortionEffect(_:maxSampleOffset:isEnabled:)
- 元の画像を変形させたい場合
- (個人的な考えですが、単体で使う用途が浮かばず、多くの場合でlayerEffectの方が採用されそう)
-
layerEffect(_:maxSampleOffset:isEnabled:)
- 元のViewを操作してエフェクトをつけたい場合(例: ドット化・ページをめくる効果)
- colorEffectとdistortionEffectの両方の機能を併せ持つ(※個人的な印象)
- 実際にこれら2つを使わずにlayerEffectに置き換えられる思います。
distortionEffect入門
- 続いてdistortionEffect(_:maxSampleOffset:isEnabled:)の話です。
- 先述の通りこれは元の画像を変形させることができ、今回は左に10pxずらすようなShaderを作成してみます。
-
distortionEffect
ではcolorEffect
と同様に現在のピクセル位置が引数として渡されます。 - 返り値はピクセル位置を返し、返り値で返したピクセルの色が現在の位置のピクセルの色となります。
- 表現が難しいので今回のShaderで説明すると…
- 各ピクセルの参照する色は、現在位置から10px右にいった位置の色を採用する
- 例えば(x, y) = (10, 10)のピクセルの色は、(20, 10)のピクセルの色とする
- その結果図形としては10px左にずれた描画となる
#include <metal_stdlib>
using namespace metal;
[[ stitchable ]] float2 shiftToLeft
(
float2 position // 現在のピクセル位置
) {
return float2(position.x + 10, position.y);
}
- ※ 見やすいようにいくつか図形を追加しています
ZStack {
Color.yellow.opacity(0.4)
Rectangle()
.stroke(lineWidth: 1)
.frame(width: 100, height: 100)
.zIndex(1)
Rectangle()
.foregroundStyle(.blue)
.frame(width: 100, height: 100)
.distortionEffect(
ShaderLibrary.default.shiftToLeft(),
maxSampleOffset: .zero
)
}
.frame(width: 150, height: 150)
- またこれだとずらした分の描画がされていない状態です。
- そこで下記のようにコードを追加してやると、全てが描画されることになります。
Rectangle()
.foregroundStyle(.blue)
.frame(width: 100, height: 100)
+ .padding(.horizontal, 10)
+ .drawingGroup()
.distortionEffect(
ShaderLibrary.default.shiftToLeft(),
- maxSampleOffset: .zero // 図形がカットされる
+ maxSampleOffset: .init(width: 10, height: 0)
)
- この辺りの話が私の中でまだ消化しきれていないので、参考元の記事を参照いただけたらと思います。
- (私の曖昧な理解)
-
padding
を使ってViewの描画範囲を広げる -
padding
はShaderの対象とならないので、drawingGroup
でSwiftUIの一つのViewとして扱うようにする -
maxSampleOffset
でShader modifierの描画範囲を広げる?- (ドキュメントを見てもよく分からないですね…)
-
distortionEffect(_:maxSampleOffset:isEnabled:)
maxSampleOffset
すべてのソース ピクセルに対する、返されたソース ピクセル位置と宛先ピクセル位置の間の各軸の最大距離。
layerEffect入門
- 最後にlayerEffect(_:maxSampleOffset:isEnabled:)を使ってみます。
colorEffectとdistortionEffectの両方の機能を併せ持つ(※個人的な印象)
- この通り
colorEffect
とdistortionEffect
で実装したShaderを、layerEffect
で実装することができます。
- Shaderの実装は以下の通りです。
- 大きな特徴として引数に現在位置の他に
SwiftUI::Layer layer
を取ることです。-
#include <SwiftUI/SwiftUI_Metal.h>
で使えるようになり、layer.sample(position)
のように任意の位置の色を取得することが出来ます。
-
- この機能が果たす役割は大きく、これによりShaderの色々な表現が可能となります。
- また返り値は
colorEffect
と同様に現在位置の色を返します。
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
[[ stitchable ]] half4 monochromeWithLayerEffect
(
float2 position,
SwiftUI::Layer layer
) {
half4 color = layer.sample(position);
half v = (color.r + color.g + color.b) / 3.0;
return half4(v, v, v, 1.0);
}
[[ stitchable ]] half4 shiftToLeftWithLayerEffect
(
float2 position,
SwiftUI::Layer layer
) {
float2 newPosition = float2(position.x + 10, position.y);
half4 color = layer.sample(newPosition);
return color;
}
- SwiftUI側の実装は以下の通りです。
-
maxSampleOffset
などはdistortionEffect
と同じですね。
Image(.xcode)
.resizable()
.frame(width: 100, height: 100)
.layerEffect(
ShaderLibrary.default.monochromeWithLayerEffect(),
maxSampleOffset: .zero
)
ZStack {
Color.yellow.opacity(0.4)
Rectangle()
.stroke(lineWidth: 1)
.frame(width: 100, height: 100)
.zIndex(1)
Rectangle()
.foregroundStyle(.blue)
.frame(width: 100, height: 100)
.padding(.horizontal, 10)
.drawingGroup()
.layerEffect(
ShaderLibrary.default.shiftToLeftWithLayerEffect(),
maxSampleOffset: .init(width: 10, height: 0)
)
}
.frame(width: 150, height: 150)
- 以上Shader modifierの使い方や使い分けの話でした。
GLSL->MSLへの置き換え
- 実際に1からShaderを書くというよりは、既存のShaderを流用したいことが多いと思いますので、実際にGLSL(ShaderToy)をMSLに置き換える例を見ていきます。
- 話の中でGLSLやMSLの文法の細かい話で不明点があれば、後述の「MSLとGLSLの文法」を参照いただけたらと思います。
関数宣言と型
- まず下記のGLSLで書かれたShaderをMSLに置き換えてみます。
- ShaderToyで与えられる
fragColor
とfragCoord
はそれぞれ現在のピクセルの色と位置です。
#elif TUTORIAL == 3
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec3 color = vec3(0.0, 1.0, 1.0);
float alpha = 1.0;
vec4 pixel = vec4(color, alpha);
fragColor = pixel;
}
- まずは関数の宣言をMSLのものに置き換えます。
[[ stitchable ]] half4 aqua
(
float2 position,
SwiftUI::Layer layer
) {
vec3 color = vec3(0.0, 1.0, 1.0);
float alpha = 1.0;
vec4 pixel = vec4(color, alpha);
fragColor = pixel;
}
- 次に型を置き換えます。
- 例えばvec3はfloat3に置き換えます。
[[ stitchable ]] half4 aqua
(
float2 position,
SwiftUI::Layer layer
) {
- vec3 color = vec3(0.0, 1.0, 1.0);
+ float3 color = float3(0.0, 1.0, 1.0);
float alpha = 1.0;
- vec4 pixel = vec4(color, alpha);
+ float4 pixel = float4(color, alpha);
fragColor = pixel;
}
- GLSLでは
fragColor
に代入した値がそのピクセルの色として反映されます。 - 一方MSLの返り値は先述の通り、returnの返り値がピクセルの色で、型は
half4
の変数なのでそのようにします。-
vec
の置換は基本float
でよく、色に関する場合のみMSL側でhalf
が登場するという認識で良いと思います。 -
float
は32bit、half
が16bitで精度の差があります。
-
[[ stitchable ]] half4 aqua
(
float2 position,
SwiftUI::Layer layer
) {
float3 color = float3(0.0, 1.0, 1.0);
float alpha = 1.0;
float4 pixel = float4(color, alpha);
- fragColor = pixel;
+ return half4(pixel);
}
- 以上でMSLに書き直せました!
- 基本はこのように機械的に置き換えを行っていけばOKです。
boundingRectと座標系
- 続いて下記を置き換えてみます。
- ※ y座標の確認をしたいため、元のコードの
r.x
をr.y
に置き換えています。
#elif TUTORIAL == 7
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// (0, 0) ~ (1, 1)に座標を正規化
vec2 r = vec2(fragCoord.x / iResolution.x,
fragCoord.y / iResolution.y);
vec3 color1 = vec3(0.841, 0.582, 0.594);
vec3 color2 = vec3(0.884, 0.850, 0.648);
vec3 color3 = vec3(0.348, 0.555, 0.641);
vec3 pixel;
if( r.y < 1.0/3.0) {
pixel = color1;
} else if( r.y < 2.0/3.0 ) {
pixel = color2;
} else {
pixel = color3;
}
fragColor = vec4(pixel, 1.0);
}
- まず先程と同様の置き換えを行います。
[[ stitchable ]] half4 tutorial7
(
float2 position,
SwiftUI::Layer layer,
float4 bounds // x,y,z,w = (x, y, width, height)
) {
// (0, 0) ~ (1, 1)に座標を正規化
float2 r = float2(fragCoord.x / iResolution.x,
fragCoord.y / iResolution.y);
float3 color1 = float3(0.841, 0.582, 0.594);
float3 color2 = float3(0.884, 0.850, 0.648);
float3 color3 = float3(0.348, 0.555, 0.641);
float3 pixel;
if( r.y < 1.0/3.0) {
pixel = color1;
} else if( r.y < 2.0/3.0 ) {
pixel = color2;
} else {
pixel = color3;
}
return half4(half3(pixel), 1.0);
}
- また今回元のViewの大きさが必要となるので、Shader.Argumentで定義されているboundingRectをargumentsに渡しています。
Rectangle()
.frame(width: 100, height: 100)
.layerEffect(
ShaderLibrary.default.tutorial7(.boundingRect),
maxSampleOffset: .zero
)
- GLSLに
iResolution
とありますが、これはfloat2
型でViewの大きさの情報です。 - MSLでは先程のboundingRectで渡した
float4 bounds
に該当します。
// Viewの幅
iResolution.x -> bounds.z
// Viewの高さ
iResolution.y -> bounds.w
- また
fragCoord
はposition
に該当します。 - 以上の話を反映するとコードは以下の通りになります。
[[ stitchable ]] half4 tutorial7
(
float2 position,
SwiftUI::Layer layer,
float4 bounds // x,y,z,w = (x, y, width, height)
) {
// (0, 0) ~ (1, 1)に座標を正規化
float2 fragCoord = position;
float2 r = float2(fragCoord.x / bounds.z,
fragCoord.y / bounds.w);
float3 color1 = float3(0.841, 0.582, 0.594);
float3 color2 = float3(0.884, 0.850, 0.648);
float3 color3 = float3(0.348, 0.555, 0.641);
float3 pixel;
if( r.y < 1.0/3.0) {
pixel = color1;
} else if( r.y < 2.0/3.0 ) {
pixel = color2;
} else {
pixel = color3;
}
return half4(half3(pixel), 1.0);
}
- ただしこれをビルドして表示を確認すると、y座標が反転しまっています。
- これはGLSLでは原点が左下、MSLでは原点が左上でy座標が反転しているためです。
- そのため正規化したあとにy座標を反転させるように補正が必要となります。
float2 r = float2(fragCoord.x / bounds.z,
fragCoord.y / bounds.w);
r = float2(r.x, 1.0 - r.y);
- ただ私はしばらくこの方法で書いていたのですが問題点があり、それは座標の正規化の方法が
左下が原点(0, 0) ~ (1, 1)
と中心が原点(-1, -1) ~ (1, 1)
の2種類があることです。 - 後者の場合はy座標に-1をかければいいのですが、正規化の書き方も個人差があり、どちらの座標系か毎回判断するのが手間でした。
- そこで最初から元の座標を反転させればどちらにも対応できるのではという考えに至り、最終的に下記のように補正をすることにしました。
- float2 fragCoord = position;
+ float2 fragCoord = float2(position.x, bounds.z - position.y);
- 最終的なコードとしては以下の通りです。
[[ stitchable ]] half4 tutorial7
(
float2 position,
SwiftUI::Layer layer,
float4 bounds // x,y,z,w = (x, y, width, height)
) {
float2 fragCoord = float2(position.x, bounds.z - position.y);
// (0, 0) ~ (1, 1)に座標を正規化
float2 r = float2(fragCoord.x / bounds.z,
fragCoord.y / bounds.w);
float3 color1 = float3(0.841, 0.582, 0.594);
float3 color2 = float3(0.884, 0.850, 0.648);
float3 color3 = float3(0.348, 0.555, 0.641);
float3 pixel;
if( r.y < 1.0/3.0) {
pixel = color1;
} else if( r.y < 2.0/3.0 ) {
pixel = color2;
} else {
pixel = color3;
}
return half4(half3(pixel), 1.0);
}
その他の置き換えの例
- 以上GLSLからMSLの置き換えの話でした。
- 実際に置き換えを行う中で、座標の正規化処理のその逆を行うなど少しだけ理解しておかないと辛い場面があったので、下記を参考にGLSLの基礎を抑えておくと円滑に進められるかと思います。
- [連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(1)
-
GLSL 2D Tutorials
- 座標系以外にも関数の使い方など参考になります
- 最後に私のMSLに置き換えたものを例として挙げておきます
- (試行錯誤していたコードのため不統一さはご容赦を)
- PageCurl効果
- カーテン効果
- 以上が本題で、以下は細かい話を書いています。
MSLとGLSLの文法
- 私が文法や仕様に関して参考にしたページを下記にまとめます。
GLSLの文法
-
[連載]やってみれば超簡単! WebGL と GLSL で始める、はじめてのシェーダコーディング(1)
- とてもわかり易いGLSL入門
-
GLSLについてのメモ
- 関数の一覧
- GLSL~4.1 基本の型
-
Pi = 3.1415926
- Piの宣言は定数で行う
#define M_PI 3.1415926535897932384626433832795
ShaderToyの仕様
fragCoordが「今から色を決めようとしているピクセルの座標(左下原点)」
fragColorが「xyzw成分にそれぞれそのピクセルのRGBA成分をもったベクトル」です。
よくShadertoyで使われているが、TouchDesignerでやると地味に厄介なのがiMouseです。ShadertoyのiMouseは4つの値を保持しています。
『マウスの左ボタンが押されている間のマウス座標XY』と『マウス左ボタンが押された瞬間のマウス座標XY』です。この後半のが厄介で、マウスがボタンが押されるのをやめた瞬間にXY座標が0になる挙動をします。
MSLの文法
定数定義
#define pi float(3.14159265359)
#define blue half4(0.0, 0.0, 1.0, 1.0)
#define radius float(0.1)
modとfmodの差異
- GLSLの
mod
とMSLのfmod
に差異があるので、下記の通りmodを定義する必要があります。 - これは少数の扱いがそれぞれとfloorでtruncateで差異があるためです。
GLSL
https://qiita.com/edo_m18/items/71f6064f3355be7e4f45
x-y*floor(x/y)MSL
https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
template<typename Tx, typename Ty>
inline Tx mod(Tx x, Ty y)
{
return x - y * floor(x / y);
}
mix
- MSLにmixが定義されていないので関数を定義します。
https://mike-neko.github.io/blog/metal-function/
T mix(T x, T y, T a)
x + (y – x) * a
template<typename Txy, typename Ta>
inline Txy mix(Txy x, Txy y, Ta a)
{
return x + (y - x) * a;
}
textureLod
- こちらの記事がとても参考になります。
iOS 17 天気アプリの雨粒演出を作ってみた
その方によるとSwiftUI::Layerにあるsample関数の実装で、samplerのaddress::clamp_to_edgeというオプションをつけないままsamplerを固定しているためにlayerEffectで画像サイズを取得できないという問題があるようです。 そのため自分でsamplerを用意し、layer.texでtextureに直接アクセスして適用することで問題を解決できるということです。
inout
- GLSLでは参照渡しのinoutが使える。
void disk(vec2 r, vec2 center, float radius, vec3 color, inout vec3 pixel) {
if( length(r-center) < radius) {
pixel = color;
}
}
- 一方MSLではinoutは難しそうなので代わりに返り値を返す関数で代用する
- inout Equivalentの話を見ても難しくて分からずじまいでした。。
half3 disk(float2 r, float2 center, float radius, half3 color, half3 pixel) {
if( length(r-center) < radius) {
return color;
}
return pixel;
}
引数早見表
- よくShaderに時間やドラッグ位置を渡す場合があるのでその早見表です。
経過時間を渡す例
struct TimeSampleView: View {
private let startDate = Date()
var body: some View {
TimelineView(.animation) { context in
let elapsedTime = context.date.timeIntervalSince1970 - self.startDate.timeIntervalSince1970
RoundedRectangle(cornerRadius: 12)
.foregroundStyle(.black)
.frame(width: 100, height: 100)
.layerEffect(
ShaderLibrary.default.timeSample(
.float(elapsedTime)
),
maxSampleOffset: .zero
)
}
}
}
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
[[ stitchable ]] half4 timeSample
(
float2 position,
SwiftUI::Layer layer,
float secs
) {
float r = abs(sin(secs * 1.0));
float g = abs(cos(secs * 1.3));
float b = abs(tan(secs * 1.5));
return half4(r, g, b, 1.0);
}
ドラッグ位置を渡す例
- 余談ですが
withAnimation
で使えないAnimationがいくつかあるような動きをしていて、例えばデフォルトや.spring
はViewの更新が行われませんでした。
struct DragGestureSample: View {
@State private var draggingLocation: CGPoint = .init(x: 50, y: 50)
var body: some View {
RoundedRectangle(cornerRadius: 12)
.foregroundStyle(.black)
.frame(width: 100, height: 100)
.layerEffect(
ShaderLibrary.default.draggingLocationSample(
.boundingRect,
.float2(draggingLocation)
),
maxSampleOffset: .zero
)
.gesture(
DragGesture(coordinateSpace: .local).onChanged { value in
draggingLocation = value.location
}.onEnded { value in
withAnimation(.easeInOut(duration: 0.1)) {
draggingLocation = .init(x: 50, y: 50)
}
}
)
}
}
- GLSL(ShaderToy)ではドラッグ位置は
iMouse
に相当します。 - MSLのロジックは気にしなくて良いですが、
position
と同様に正規化が必要なところがポイントです。
https://note.com/toyoshimorioka/n/nf908ce35d0ea
ShadertoyのiMouseは4つの値を保持しています。『マウスの左ボタンが押されている間のマウス座標XY』と『マウス左ボタンが押された瞬間のマウス座標XY』です。この後半のが厄介で、マウスがボタンが押されるのをやめた瞬間にXY座標が0になる挙動をします。
half3 disk(float2 r, float2 center, float radius, half3 fillColor, half3 backgroundColor) {
if( length(r-center) < radius) {
return fillColor;
}
return backgroundColor;
}
[[ stitchable ]] half4 draggingLocationSample
(
float2 _position,
SwiftUI::Layer layer,
float4 bounds, // x,y,z,w = (x, y, width, height)
float2 _draggingLocation
) {
// 中心を原点とする正規化
float2 position = float2(_position.x, bounds.w - _position.y);
float2 draggingLocation = float2(_draggingLocation.x, bounds.w - _draggingLocation.y);
float2 uv = 2.0 * float2(position.xy - 0.5 * bounds.zw ) / bounds.w;
float2 uv_draggingLocation = 2.0 * float2(draggingLocation.xy - 0.5 * bounds.zw ) / bounds.w;
half3 backgroundColor = half3(0.3);
half3 yellow = half3(1.00, 0.329, 0.298);
half3 pixel = disk(uv, uv_draggingLocation, 0.5, yellow, backgroundColor);
return half4(pixel, 1.0);
}
SwiftUI Metal Shaderの制限
- 現状(2023-11-11現在)SwiftUI Metal Shaderにはいくつか制限があります。
SwiftUIのViewにのみ対応
- 現状SwiftUIのMetal ShaderはSwiftUIのViewにのみの対応で、AppKit/UIKitには適応することができないです。
https://developer.apple.com/documentation/swiftui/view/layereffect(_:maxsampleoffset:isenabled:)
Important
Views backed by AppKit or UIKit views may not render into the filtered layer. Instead, they log a warning and display a placeholder image to highlight the error.
Shaderに渡せるImageは1つだけ
- 現状argumentsでShaderに渡せるImageは1つだけとなっています。
image(_:)
Currently only one image parameter is supported per Shader instance.
Discussion