簡介
從 iOS 17/macOS 14 開始,SwiftUI 支援使用 Metal shader 來實現一些特效。主要提供三個 View Modifier:colorEffect
、 distortionEffect
和 layerEffect
。每個 modifier 的第一個引數是傳入的 Shader
例項。
此外,View 例項還新增了一個 visualEffect
modifier,用於暴露修飾內容的佈局資訊。函式簽名為 func visualEffect(_ effect: @escaping (EmptyVisualEffect, GeometryProxy) -> some VisualEffect) -> some View
,在這個閉包中給 EmptyVisualEffect 新增上面的三種 shader modifier,透過 GeometryProxy 引數來獲取所修飾內容的 size 等資訊,可以進一步傳遞給 shader function。
可惜的是,這些 modifier 只適用於 SwiftUI 的 View,不適用於 UIKit/AppKit 包的 View。
用法
Shader Function
Shader 建構函式為 init(function: ShaderFunction, arguments: [Shader.Argument]
,而 ShaderFunction 的建構函式為 init(library: ShaderLibrary, name: String)
。ShaderLibrary 有一個 static 成員 default
,表示 app 的 main bundle 中的 shader library。此外 ShaderLibrary 還提供了 static subscript(dynamicMember _: String) -> ShaderFunction
方法,返回 default shader library 中名字為 name 的 MSL function。
三個 View Modifier 分別操作不同的元素,實現不同的效果,也對 MSL 函式有著各自不同的要求,下面一一介紹。
colorEffect
簽名如下:
func colorEffect(
_ shader: Shader,
isEnabled: Bool = true
) -> some View
該 modifier 用來操作每個單獨的畫素,要求提供的 MSL 函式的簽名必須和下面的匹配:
[[ stitchable ]] half4 name(float2 position, half4 color, args...)
其中 position 和 color 引數在執行 shader 函式的時候會自動傳入,position 表示畫素在 user-space 座標系下的座標(相對的,Metal 的 clip-space 座標系區域為 (-1.0, -1.0) 到 (1.0, 1.0)),color 是當前 position 對應畫素的顏色。我們也可以透過 args… 可變引數傳入自定義的資料。該 shader 函式返回處理後的畫素顏色(Fragment shader)。
示例 Shader:
[[ stitchable ]] half4 colorCircle(float2 position, half4 currentColor, float2 size, float radius, half4 circleColor) {
float2 center = size / 2; // 計算 view 的中心點
if (length(position - center) < radius) {
return circleColor * currentColor.a;
} else {
return currentColor;
}
}
在上面的 shader 函式中,除了會預設提供的兩個引數 position 和 currentColor 外,我們還額外提供了三個引數 size,radius,circleColor,這三個函式需要在SwiftUI 中進行指定,如下所示:
struct ContentView: View {
let start = Date()
var body: some View {
ZStack {
TimelineView(.animation) { _ in
Text("𰻞")
.font(.system(size: 80, weight: .black))
.visualEffect { content, geometryProxy in
content
.colorEffect(ShaderLibrary.colorCircle(
.float2(geometryProxy.size),
.float(abs(start.timeIntervalSinceNow) * 10),
.color(.purple)
))
}
}
}
.padding()
}
}
執行效果:
layerEffect
layerEffect 類似於 colorEffect,也是一個 fragment shader,返回處理後的畫素顏色,但是不同於 colorEffect shader 函式引數只給我們提供 position 位置對應的單個畫素的顏色,layerEffect 給我們提供了被修飾 View 的整個 layer,這樣我們就可以實現一些上下文相關的效果,比如高斯模糊。該 modifier 簽名如下:
func layerEffect(
_ shader: Shader,
maxSampleOffset: CGSize, // 該引數說明見下
isEnabled: Bool = true
) -> some View
要求提供的 MSL 函式的簽名必須和下面的匹配:
[[ stitchable ]] half4 name(float2 position, SwiftUI::Layer layer, args...)
SwiftUI::Layer 只暴露出了一個 half4 sample(float2 p)
函式,返回的是被修飾內容裡,座標 p 處的線性插值顏色值,該函式的實現在標頭檔案裡給出了,程式碼如下:
half4 sample(float2 p) const {
p = metal::fma(p.x, info[0], metal::fma(p.y, info[1], info[2]));
p = metal::clamp(p, info[3], info[4]);
return tex.sample(metal::sampler(metal::filter::linear), p);
}
這裡看起來會對傳入的座標 p 做 clamp,線下試過傳越界值的時候返回的是透明色值,但是因為不知道 info 是什麼資料,也沒用找到明確的文件說明,如果比較謹慎的話可以自己對 p 做越界處理。
回過頭來看 modifier 的 maxSampleOffset 引數,該引數是指在 shader 函式中,對 layer 呼叫 sample 取畫素色值時,如果傳入的座標不是當前的座標 position 而是其他座標,則可以計算出一個相對當前左邊的偏移距離 distance,maxSampleOffset 則是所有呼叫中的 distance 的最大值。(但是線下測試時傳 .zero 卻沒有出現問題,比較奇怪)
Shader,需要引用相關標頭檔案:
#include <SwiftUI/SwiftUI_Metal.h>
[[ stitchable ]] half4 gaussianBlur(float2 position, SwiftUI::Layer layer) {
return
layer.sample(position) * 0.0707355 +
layer.sample(position + float2(-1, -1)) * 0.0453542 +
layer.sample(position + float2(0, -1)) * 0.0566406 +
layer.sample(position + float2(1, -1)) * 0.0453542 +
layer.sample(position + float2(-1, 0)) * 0.0566406 +
layer.sample(position + float2(1, 0)) * 0.0566406 +
layer.sample(position + float2(-1, 1)) * 0.0453542 +
layer.sample(position + float2(0, 1)) * 0.0566406 +
layer.sample(position + float2(1, 1)) * 0.0453542;
}
示例:
struct ContentView: View {
var body: some View {
HStack {
Text("𰻞")
.font(.system(size: 80, weight: .black))
Text("𰻞")
.font(.system(size: 80, weight: .black))
.layerEffect(ShaderLibrary.gaussianBlur(),
maxSampleOffset: .init(width: 3, height: 3))
}
.padding()
}
}
執行效果:
distortionEffect
不同於前兩者,distortionEffect 使用的是一個 vertex shader,即返回的不是一個 half4 型別的顏色值,而是一個 float2 型別的座標值,即改變每一個畫素的位置,從而實現一些扭曲變形的效果。該 modifier 的簽名如下:
func distortionEffect(
_ shader: Shader,
maxSampleOffset: CGSize, // 該引數含義同 layerEffect
isEnabled: Bool = true
) -> some View
要求提供的 MSL 函式的簽名必須和下面的匹配:
[[ stitchable ]] float2 name(float2 position, args...)
示例 Shader:
[[ stitchable ]] float2 stretch(float2 position, float2 size) {
float midY = size.y / 2;
return position + float2(30 * abs((position.y - midY) / midY), 0);
}
示例:
struct ContentView: View {
var body: some View {
ZStack {
Text("𰻞")
.font(.system(size: 80, weight: .black))
.visualEffect { context, proxy in
context
.distortionEffect(
ShaderLibrary.stretch(.float2(proxy.size)),
maxSampleOffset: .init(width: proxy.size.width / 2, height: proxy.size.height / 2))
}
}
.padding()
}
}
執行效果:
References
- How to add Metal shaders to SwiftUI views using layer effects
- https://developer.apple.com/documentation/swiftui/view/coloreffect(_:isenabled:)
- https://developer.apple.com/documentation/swiftui/view/distortioneffect(_:maxsampleoffset:isenabled:)
- https://developer.apple.com/documentation/swiftui/view/layereffect(_:maxsampleoffset:isenabled:)