T.TAO
Back to Blog
/16 min read/Graphics Engine

Metal #22 SwiftUI における Metal

#ComputerGraphics#GraphicsAPI
Metal #22 SwiftUI における Metal

マーク:

Graphics & API Content List

  • Graphics

#TechnicalArt#ComputerGraphics#GraphicsAPI#GameEngine

このノートは主に、SwiftUI で Metal Shading Language を使用して view に提供するマテリアルを記述する方法について説明します。

SwiftUI における shader

iOS 17 以降、SwiftUI は view に対して3種類の異なる視覚効果をサポートしており、それぞれ colorEffect、layerEffect、distortEffect と呼ばれます。これら3つの Effect は、2D 平面ベースの Metal shader を現在の view に適用することができます。

しかし、公式ドキュメント [1] でさえ、SwiftUI で使用できる shader に関する説明の多くは非常に少なく、いくつかの制限事項も実際には明確に提示されていません。このノートは、より多くの人が SwiftUI で Metal shader 開発を行えるよう、重要な詳細をいくつか提示することを目的としています。

また、この記事で言及されているすべての shader およびコードリポジトリはオープンソース化されています。興味のある読者は GitHub リポジトリをご自身で確認してください:https://github.com/Lockbrains/SwiftUI-2D-Shader-Assets。

使用方法

SwiftUI 側での呼び出し

SwiftUI では、1つの view に対して直接1つ(または同時に複数)の .colorEffect.layerEffect、または .distortEffect 修飾子を使用できます。例えば、

ここでの3種類の shader Effect は、それぞれ以下の場合に対応します:

  1. .colorEffect:対応する shader が現在のピクセルのカラー情報のみを必要とする場合は、Color Effect を使用する必要があります。.colorEffect は fragment shader と見なすことができます。
  2. .layerEffect:対応する shader が現在のピクセルのカラー情報だけでなく、修飾された view の layer 全体を必要とする場合、layerEffect はそれを提供してくれます。これにより、ガウスぼかしなどのコンテキストに依存する効果を実装できます。
  3. .distortEffect:対応する shader が vertex の位置を変更する場合は、Distort Effect を使用する必要があります。.distortEffect は vertex shader と見なすことができます。

上記のコードにおいて、colorEffect() の内容は、私が別のファイルで定義したサンプル shader(読みやすさのため)であり、以下のような形式を持っています:

ここで、関数 dissolveEffect は以下のような形式を持っています:

ここでは2層のカプセル化を提供していますが、これもコードの可読性のため、そしてサードパーティが shader を使用する際により直感的にするためです。その考え方については後のセクションで詳しく説明します。ただし、一時的に使用する shader を単独で記述する必要がある場合は、.colorEffect 内で直接 ShaderLibrary.shaderName() を使用し、後述のデータ伝達のセクションで言及されている方法を使用してパラメータを shader に渡すだけで十分です。

MSL の構文

各 Effect は、要件を満たす MSL 関数を使用して shader の内容を提供する必要があります。異なる Effect の関数は、異なる関数シグネチャに従わなければなりません。

Color Effect

Color Effect の場合、提供する必要があるシグネチャは以下の通りです:

ここで、第0パラメータの float2 position について、公式の説明では position はユーザースペースのピクセル座標であるとされています。SwiftUI の shader を開発する前に、ユーザースペース座標の意味を必ず理解しておく必要があります。不明な場合は、後述のユーザースペースのセクションを参照してください。

また、第1パラメータの位置に half4 color も提供する必要があります。これは、view のその論理位置における現在のカラーです。

ただし、ここでの「提供」という言葉は実際にはあまり正確ではありません。position と color は MSL の shader シグネチャで提供するだけでよく、SwiftUI の ShaderLibrary.shaderName ではこれら2つのプロパティを提供する必要はありません。これら2つのプロパティは自動的に渡されます。しかし、その他のシグネチャについては、1対1で対応し、順序通りになります。例えば、私が書いた dissolveEffect では、その MSL シグネチャは以下のようになります:

対応して、SwiftUI でこの shader を呼び出す際に使用する順序は以下のようになります:

それらの順序の1対1の対応に注意してください。SwiftUI の API では各パラメータの名前を見ることができないため、複数の入力を持つ shader の開発において debug の難易度が確実に上がります。したがって、呼び出し時には十分な comment を追加することを強くお勧めします。

Layer Effect

Layer Effect の場合、提供する必要があるシグネチャは以下の通りです:

MSL では、layer.sample(position) の方法を通じて、現在の view の position 位置におけるカラーを取得できます。そして、layer 全体のグローバル情報を持っているため、明らかに layer.sample(f(position)) の方法を通じて、現在の位置に関連する他の位置の情報を取得することも可能です。これにより、ぼかしなどの操作を記述する可能性が生まれます。

例えば、このシンプルなガウスぼかしでは、position 付近の合計9つの論理点をサンプリングすることでガウスぼかしを行うことができます。

SwiftUI において、データを渡す方法は Color Effect と同じであるため詳細は割愛しますが、追加で maxSampleOffset を提供する必要があることに注意してください:

公式ドキュメントの情報によると、

例えば上記のガウスぼかしでは、周囲の9ピクセルに対してぼかしを行う可能性があるため、ここで width と height を 3 に設定するのは合理的です。この maxSampleOffset は、現在の位置以外のピクセル情報を使用する可能性があるものの、絶対にこの範囲を超えないことを shader に伝えることに相当し、これは Apple 公式が Layer Effect を最適化する方法でもあります。

ユーザースペース

ユーザースペース座標(User Space Coordinates, USC)は、デバイスや画面の物理ピクセル座標ではなく、アプリケーションロジックで使用される座標系です。この座標空間は通常正規化されており、コンテンツのレイアウトやデザインをより便利にするために、アプリケーションの要件に応じてスケーリング、平行移動、または回転が行われます。

SwiftUI では、通常、物理ピクセル座標を直接使用する必要はなく、論理座標に基づいています。ユーザースペース座標により、デバイスに依存しない方法でグラフィックコンテンツをレイアウトできます。

300 × 300 の SwiftUI の Rectangle コンポーネントで shader を使用したと仮定すると、position パラメータはユーザースペースの座標で提供されます。例えば、中心にあるときの位置は (150, 150) になる可能性があります。Rectangle を拡大または縮小(.scaleEffect 修飾子を使用)しても、position の座標値は依然として 300 x 300 の範囲に基づいており、具体的なピクセル数としては反映されません。

ここでの 300 x 300 や (150, 150) の単位はすべて論理ポイントであり、ピクセルではないことに注意してください。SwiftUI のポイントは画面解像度に依存しない論理単位であり、例えば高解像度の Retina 画面では、1つの論理ポイントが複数の物理ピクセルに対応する可能性があります。

増加方向

SwiftUI では、通常、左上角が (0,0) であり、右下角が最大値(例えば上記の例では (300, 300))であると考えます。簡単な shader を通じてこのことを検証できます。

シンプルな shader を使用して最もシンプルな Gradient を返し、ある view に対して .colorEffect を使用します。

ここでの size は現在の view の論理サイズを提供する必要があります。この gradient は確かに左上角が (0,0) であり、右下角が最大値であるという特徴を反映しています。

UV

以上の基礎を理解した上で、依然として正規化の手法(つまりよく言われる uv 座標系)を使用して shader を記述したい場合、MSL の入力に float2 size を追加することをお勧めします。この size は現在の view の論理サイズです。この論理サイズを通じて、上記の trivialGradient で行ったように、正規化された uv を簡単に計算することができます:

そして SwiftUI で、論理サイズを .float2(x, y) を使用して MSL に渡すことで、正規化座標に基づいた効果を作成できます。

データ伝達

この部分については、Apple 公式が確かに説明を提供しているとは言えますが、実際に見つけるのに非常に時間がかかります。とにかく、Metal と SwiftUI のデータ型には違いがあります。簡単な例を挙げると、Metal では float2、half2 などは非常に一般的なデータ型ですが、SwiftUI には Float しかありません。データを渡すには、両側にどのようなデータ型があるか、どの API を通じてデータを渡すかをそれぞれ知る必要があります。

MSL 側

MSL 側でサポートされているデータについては、Metal Shading Language Specification を参照することをお勧めします。ただし、ほとんどの場合、シンプルな float、floatn、half、halfn、texture2d<half> 型のみを使用します。特に、カラーを記述・記録する際は、常に half4 を使用する必要があります。

注意すべき点として、MSL では halfn と floatn の間で暗黙の型変換を行うことはできません。halfn * float や floatn*half を使用することはできますが、halfn と floatn を掛け合わせることはできません。

SwiftUI 側

SwiftUI では、以下のインターフェースを通じてデータを Metal に渡す必要があります:

  • .color(Color):括弧内に SwiftUI の Color 型を入力します。このデータは half4 型に変換されます。Metal では、half4 型はデフォルトでカラーを記述するために使用されることに注意してください。half4 を color4 と見なすことができます。
  • .float(T):括弧内に BinaryFloatingPoint プロトコルに準拠する SwiftUI 型を入力します。このデータは float 型に変換されます。
  • .float2(T, T):括弧内に BinaryFloatingPoint プロトコルに準拠する2つの SwiftUI 型を入力します。このデータは float2 型に変換されます。同様に .float3、.float4 もありますが、詳細は割愛します。
  • .image(Image): 括弧内に SwiftUI の Image 型を入力します。このデータは texture2d<half> 型に変換されます。特に注意すべき点として、現在 SwiftUI の shader は最大で1つの texture 型の入力しか持つことができません。2つの texture をサンプリングする必要がある shader を書いた場合、非常に残念ですが効果はありません。

上記以外にも、Array を渡す方法がいくつかあります:

  • .colorArray([Color]):括弧内に SwiftUI の Color 配列を入力します。この配列はペア、つまり (device const half4 *, count) に変換されます。
  • .floatArray([T]):括弧内に BinaryFloatingPoint プロトコルに準拠する SwiftUI 型の配列を入力します。この配列はペア、つまり (device const float *, count) に変換されます。
  • .data(Data):括弧内に SwiftUI の Data データを入力します。このデータはペア、つまり (device const void *, size_in_bytes) に変換されます。

他のプラットフォームの shader を変換する方法

ShaderToy からの shader

Unity からの shader

参考資料:

  1. Apple 開発者ドキュメント(Shader)、https://developer.apple.com/documentation/swiftui/shader
  2. Apple 開発者ドキュメント(Shader.Argument)、https://developer.apple.com/documentation/swiftui/shader/argument
  3. How to add Metal shaders to SwiftUI views using layer effects、https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-metal-shaders-to-swiftui-views-using-layer-effects
  4. SwiftUI で Metal shader を使用する、https://www.cnblogs.com/jerrywossion/p/18090457