3.9K Views
January 25, 25
スライド概要
美濃加茂.swift #1での発表資料です
岐阜の山中でヒキコモリ系プログラマー WindowsとiOSの間で生きる何か C/C++/Java/C#/Obj-C/Swift/F#/Haskell/Rustで生きている
Metal Shader Effect の細かい話 2025/01/23 Minokamo.swift
自己紹介 岐阜県出身 岐阜県在住 フリーランスエンジニア エンジニアと人生コミュニティに生息 @ta̲ka̲tsu
趣味で砂鉄から鉄をつくっています
先日、大垣市で開催された Ogaki Mini Maker Faire2024に 出展してきました
Metal Shader Effectとは
Metal Shader Effectとは →公式にはそんな用語はない
@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
と呼ぶことにする
@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
と呼ぶことにする
何をするモディファイアか? x (i, j) y 適用されるViewの 全てのピクセルひとつひとつの 色を変更するモディファイア ピクセル数は多い →GPUで計算させる →MSL(※)で記述 ※MSL:Metal Shading Language
Shaderの作り方 .metalファイル 関数のシグネチャは stitchable属性が必要 使用するモディファイアによって違う [[stitchable]] half4 functionName(float2 position, args...) { ... }
Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル metalファイルの 関数名を指定する let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] )
Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] 必要に応じて ) 引数を追加できる
Shaderの作り方 .metalファイル [[stitchable]] half4 functionName(float2 position, args...) { ... } .swiftファイル let shader = Shader( function: .init(library: .default, name: "functionName"), arguments: [...] ) dynamicMemberLookupにより こう書くこともできる let shader = ShaderLibrary.functionName(...)
distortionE ect 入力:ピクセル座標 出力:ピクセル座標 出力のピクセル座標にある色を表示する 第1引数は oat2(暗黙的引数) [[stitchable]] float2 forDistortion(float2 position, args ...) { ... } ff 返り値は oat2 残りは追加引数
colorE ect 入力:ピクセル座標と元の色 出力:出力する色 出力の色を表示する 第1引数は oat2(暗黙的引数) 第2引数はhalf4(暗黙的引数) [[stitchable]] half4 forColor(float2 position, half4 color, args ...) { ... } ff 返り値はhalf4 残りは追加引数
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
残りは追加引数
Demo
参考になるサイト・動画 • 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
シェーダの基礎・基本テクニックはこちら https://www.youtube.com/watch?v=yU3xzbjMyPU
追加引数に渡せるもの
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
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
}
}
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つまで
maxSampleOffsetの意味
@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
maxSampleO setとは x ff y
maxSampleO setとは x サンプルを取るピクセルを 上下左右に増やす y maxSampleOffset.height ff maxSampleOffset.width
振幅10の正弦波で上下方向に移動するdistortionE ect ff ff maxSampleO set: .zero
maxSampleO set: ff CGSize(width: 0, height: 10)
HitTestに影響は?
タップできる箇所をプロットしてみる ff distortionE ectなし
distortionE ectなし ff ff distortionE ectで 上下にずらしたもの
適用できるViewとできないView
何もしないdistortionE ect用のShaderで検証 ff [[stitchable]] float2 doNothing(float2 position) { return position; }
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
)
}
}
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
)
}
}
struct ApplyToToggle: View { @State private var isOn = true } var body: some View { Toggle("Toggle", isOn: $isOn) }
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 ) }
struct ApplyToToggle: View { @State private var isOn = true } var body: some View { Toggle("Toggle", isOn: $isOn) .toggleStyle(.button) .distortionEffect( ShaderLibrary.doNothing(), maxSampleOffset: .zero ) }
Metal Shader E ectが適用できるもの ff •Text •Label •Link •NavigationLink •Image •Toggle (.buttonスタイル) •Picker (.navigationスタイル) •DisclosureGroup •EditButton •Gauge •ShareLink •Divider •Shape(Rectangle, Circle, ...)
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 ※黄色はラベル部分には適用されるもの
中身のコンテンツに適用されるもの •HStack •VStack •ZStack •LazyVGrid •LazyHGrid •LabeledContent •Section •GeometryReader •Group •GroupBox •OutlineGrup
どちらでもないもの •ScrollView •PasteButton
ScrollView { ForEach(cities, id: \.self) { city in Text(city).font(.subheadline) } }
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 )
実はShaderはShapeStyle
ShaderはShapeStyleに準拠している https://developer.apple.com/documentation/swiftui/shader
ShapeStyleとして 入力:ピクセル座標 出力:出力する色 出力の色を表示する(※描画されるべきピクセルのみ) 第1引数は oat2(暗黙的引数) [[stitchable]] half4 forDistortion(float2 position, args ...) { ... } 返り値はhalf4 残りは追加引数
[[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() )
Thank you! Enjoy Gifu!