Metal Shader Effectの細かい話

3.9K Views

January 25, 25

スライド概要

美濃加茂.swift #1での発表資料です

profile-image

岐阜の山中でヒキコモリ系プログラマー WindowsとiOSの間で生きる何か C/C++/Java/C#/Obj-C/Swift/F#/Haskell/Rustで生きている

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Metal Shader Effect の細かい話 2025/01/23 Minokamo.swift

2.

自己紹介 岐阜県出身 岐阜県在住 フリーランスエンジニア エンジニアと人生コミュニティに生息 @ta̲ka̲tsu

3.

趣味で砂鉄から鉄をつくっています

4.

先日、大垣市で開催された Ogaki Mini Maker Faire2024に 出展してきました

5.

Metal Shader Effectとは

6.

Metal Shader Effectとは →公式にはそんな用語はない

7.
[beta]
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
extension View {
nonisolated public func distortionEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View
nonisolated public func colorEffect(
_ shader: Shader,
isEnabled: Bool = true
) -> some View

ff

ff

ff

ff

}

nonisolated public func layerEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View

・distortionE ect

・colorE ect
・layerE ect
本日はこれらを
Metal Shader E ect
と呼ぶことにする

8.
[beta]
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
extension View {
nonisolated public func distortionEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View
nonisolated public func colorEffect(
_ shader: Shader,
isEnabled: Bool = true
) -> some View

ff

ff

ff

ff

}

nonisolated public func layerEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View

・distortionE ect

・colorE ect
・layerE ect
本日はこれらを
Metal Shader E ect
と呼ぶことにする

9.

何をするモディファイアか? x (i, j) y 適用されるViewの 全てのピクセルひとつひとつの 色を変更するモディファイア ピクセル数は多い →GPUで計算させる →MSL(※)で記述 ※MSL:Metal Shading Language

10.

Shaderの作り方 .metalファイル 関数のシグネチャは stitchable属性が必要 使用するモディファイアによって違う [[stitchable]] half4 functionName(float2 position, args...) { ... }

11.

Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル metalファイルの 関数名を指定する let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] )

12.

Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] 必要に応じて ) 引数を追加できる

13.

Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] ) dynamicMemberLookupにより こう書くこともできる let shader = ShaderLibrary.functionName(...)

14.

distortionE ect 入力:ピクセル座標 出力:ピクセル座標 出力のピクセル座標にある色を表示する 第1引数は oat2(暗黙的引数) [[stitchable]] float2 forDistortion(float2 position, args ...) { ... } ff 返り値は oat2 残りは追加引数

15.

colorE ect 入力:ピクセル座標と元の色 出力:出力する色 出力の色を表示する 第1引数は oat2(暗黙的引数) 第2引数はhalf4(暗黙的引数) [[stitchable]] half4 forColor(float2 position, half4 color, args ...) { ... } ff 返り値はhalf4 残りは追加引数

16.
[beta]
layerE ect
入力:ピクセル座標と元の表示レイヤ全部
出力:出力する色
出力の色を表示する

SwiftUI::Layer型を使うために必要

第1引数は oat2(暗黙的引数)
#include <SwiftUI/SwiftUI_Metal.h>

第2引数はSwiftUI::Layer型(暗黙的引数)

ff

[[stitchable]]
half4 forLayer(float2 position, SwiftUI::Layer layer, args ...) {
...
}
返り値はhalf4
残りは追加引数

17.

Demo

18.

参考になるサイト・動画 • How to add Metal shaders to SwiftUI views using layer e ects https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-metal-shaders-to-swiftui-views-usinglayer-e ects • SwiftUI + Metal ‒ Create special e ects by building your own shaders https://www.youtube.com/watch?v=EgzWwgRpUuw • SwiftUI Metal Shader E ects - iOS 17 - WWDC 2023 ff ff ff ff https://www.youtube.com/watch?v=yBdY0UKBIx0

19.

シェーダの基礎・基本テクニックはこちら https://www.youtube.com/watch?v=yU3xzbjMyPU

20.

追加引数に渡せるもの

21.
[beta]
Shader.Argument
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
public struct Shader : Equatable, Sendable {
public struct Argument : Equatable, Sendable {
public static func float<T>(_ x: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2<T>(_ x: T, _ y: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float3<T>(_ x: T, _ y: T, _ z: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float4<T>(_ x: T, _ y: T, _ z: T, _ w: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2(_ point: CGPoint) -> Shader.Argument
public static func float2(_ size: CGSize) -> Shader.Argument
public static func float2(_ vector: CGVector) -> Shader.Argument
public static func floatArray(_ array: [Float]) -> Shader.Argument
public static var boundingRect: Shader.Argument { get }
public static func color(_ color: Color) -> Shader.Argument
public static func colorArray(_ array: [Color]) -> Shader.Argument
public static func image(_ image: Image) -> Shader.Argument
public static func data(_ data: Data) -> Shader.Argument
public static func == (a: Shader.Argument, b: Shader.Argument) -> Bool
}
}

参考:https://developer.apple.com/documentation/swiftui/shader/argument

22.
[beta]
Shader.Argument
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
public struct Shader : Equatable, Sendable {
public struct Argument : Equatable, Sendable {
public static func float<T>(_ x: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2<T>(_ x: T, _ y: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float3<T>(_ x: T, _ y: T, _ z: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float4<T>(_ x: T, _ y: T, _ z: T, _ w: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2(_ point: CGPoint) -> Shader.Argument
public static func float2(_ size: CGSize) -> Shader.Argument
ビューの原点位置とサイズが渡せる
public static func float2(_ vector: CGVector) -> Shader.Argument
public static func floatArray(_ array: [Float]) -> Shader.Argument
public static var boundingRect: Shader.Argument { get }
public static func color(_ color: Color) -> Shader.Argument
public static func colorArray(_ array: [Color]) -> Shader.Argument
public static func image(_ image: Image) -> Shader.Argument
public static func data(_ data: Data) -> Shader.Argument
public static func == (a: Shader.Argument, b: Shader.Argument) -> Bool
}
}

23.
[beta]
Shader.Argument
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
public struct Shader : Equatable, Sendable {
public struct Argument : Equatable, Sendable {
public static func float<T>(_ x: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2<T>(_ x: T, _ y: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float3<T>(_ x: T, _ y: T, _ z: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float4<T>(_ x: T, _ y: T, _ z: T, _ w: T) -> Shader.Argument where T : BinaryFloatingPoint
public static func float2(_ point: CGPoint) -> Shader.Argument
public static func float2(_ size: CGSize) -> Shader.Argument
public static func float2(_ vector: CGVector) -> Shader.Argument
public static func floatArray(_ array: [Float]) -> Shader.Argument
public static var boundingRect: Shader.Argument { get }
public static func color(_ color: Color) -> Shader.Argument
public static func colorArray(_ array: [Color]) -> Shader.Argument
public static func image(_ image: Image) -> Shader.Argument
public static func data(_ data: Data) -> Shader.Argument
public static func == (a: Shader.Argument, b: Shader.Argument) -> Bool
}
}

画像も渡せる
ただし現時点では1つのShaderにつき1つまで

24.

maxSampleOffsetの意味

25.
[beta]
@available(iOS 17.0, macOS 14.0, tvOS 17.0, *)
@available(watchOS, unavailable)
extension View {
nonisolated public func distortionEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View
nonisolated public func colorEffect(
_ shader: Shader,
isEnabled: Bool = true
) -> some View

}

nonisolated public func layerEffect(
_ shader: Shader,
maxSampleOffset: CGSize,
isEnabled: Bool = true
) -> some View

26.

maxSampleO setとは x ff y

27.

maxSampleO setとは x サンプルを取るピクセルを 上下左右に増やす y maxSampleOffset.height ff maxSampleOffset.width

28.

振幅10の正弦波で上下方向に移動するdistortionE ect ff ff maxSampleO set: .zero

29.

maxSampleO set: ff CGSize(width: 0, height: 10)

30.

HitTestに影響は?

31.

タップできる箇所をプロットしてみる ff distortionE ectなし

32.

distortionE ectなし ff ff distortionE ectで 上下にずらしたもの

33.

適用できるViewとできないView

35.

何もしないdistortionE ect用のShaderで検証 ff [[stitchable]] float2 doNothing(float2 position) { return position; }

36.
[beta]
struct LabelUIKit: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let label = UILabel()
label.text = "Text"
return label
}
}

func updateUIView(_ uiView: UIView, context: Context) {}

struct ApplyToUIViewRepresentable: View {
var body: some View {
LabelUIKit()
.distortionEffect(
ShaderLibrary.doNothing(),
maxSampleOffset: .zero
)
}
}

38.
[beta]
struct LabelUIKit: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let label = UILabel()
label.text = "Text"
return label
}
}

func updateUIView(_ uiView: UIView, context: Context) {}

struct ApplyToUIViewRepresentable: View {
var body: some View {
LabelUIKit()
.distortionEffect(
ShaderLibrary.wave(.float(10)),
maxSampleOffset: .zero
)
}
}

40.

struct ApplyToToggle: View { @State private var isOn = true } var body: some View { Toggle("Toggle", isOn: $isOn) }

41.

struct ApplyToToggle: View { @State private var isOn = true } struct ApplyToToggle: View { @State private var isOn = true var body: some View { Toggle("Toggle", isOn: $isOn) } } var body: some View { Toggle("Toggle", isOn: $isOn) .distortionEffect( ShaderLibrary.doNothing(), maxSampleOffset: .zero ) }

42.

struct ApplyToToggle: View { @State private var isOn = true } var body: some View { Toggle("Toggle", isOn: $isOn) .toggleStyle(.button) .distortionEffect( ShaderLibrary.doNothing(), maxSampleOffset: .zero ) }

43.

Metal Shader E ectが適用できるもの ff •Text •Label •Link •NavigationLink •Image •Toggle (.buttonスタイル) •Picker (.navigationスタイル) •DisclosureGroup •EditButton •Gauge •ShareLink •Divider •Shape(Rectangle, Circle, ...)

44.

Metal Shader E ectが適用できないもの •UIViewRepresentable •NavigationStack •NavigationSplitView •NavigationView •List •Menu •Toggle (.switchスタイル) •Picker (.navigationスタイル以外) •ColorPicker •DatePicker •MultiDatePicker •ProgressView •SecureField •SignInWithAppleButton •Slider •Stepper •TabView •Table •TextEditor •Map (MapKit) •RealityView (visionOS) •Model3D (visionOS) ff ※黄色はラベル部分には適用されるもの

45.

中身のコンテンツに適用されるもの •HStack •VStack •ZStack •LazyVGrid •LazyHGrid •LabeledContent •Section •GeometryReader •Group •GroupBox •OutlineGrup

46.

どちらでもないもの •ScrollView •PasteButton

47.

ScrollView { ForEach(cities, id: \.self) { city in Text(city).font(.subheadline) } }

48.

ScrollView { ForEach(cities, id: \.self) { city in Text(city).font(.subheadline) } } ScrollView { ForEach(cities, id: \.self) { city in Text(city).font(.subheadline) } } .distortionEffect( ShaderLibrary.doNothing(), maxSampleOffset: .zero )

49.

実はShaderはShapeStyle

50.

ShaderはShapeStyleに準拠している https://developer.apple.com/documentation/swiftui/shader

51.

ShapeStyleとして 入力:ピクセル座標 出力:出力する色 出力の色を表示する(※描画されるべきピクセルのみ) 第1引数は oat2(暗黙的引数) [[stitchable]] half4 forDistortion(float2 position, args ...) { ... } 返り値はhalf4 残りは追加引数

52.

[[stitchable]] half4 redBlueCheck(float2 position) { uint2 blockUV(position.x / 5, position.y / 5); float mask = (blockUV.x + blockUV.y) % 2; return mix(half4(1.0, 0.0, 0.0, 1.0), half4(0.0, 0.0, 1.0, 1.0), mask); } Text(“Welcome to Minokamo") .font(.largeTitle) .bold() .foregroundStyle( ShaderLibrary.redBlueCheck() )

53.

Thank you! Enjoy Gifu!