在 SwiftUI 中使用 Metal Shader

news/发布时间2024/5/16 22:59:01

简介

从 iOS 17/macOS 14 开始,SwiftUI 支持使用 Metal shader 来实现一些特效。主要提供三个 View Modifier:colorEffectdistortionEffectlayerEffect 。每个 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) { _ inText("𰻞").font(.system(size: 80, weight: .black)).visualEffect { content, geometryProxy incontent.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) {returnlayer.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, // 该参数含义同 layerEffectisEnabled: 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 incontext.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:)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/35136403.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

STM32G431RBT6之LCD03

导入三个文件lcd.c && lcd.h && fonts.h 初始化 && 界面显示LCD_Init();LCD_Clear(Black); LCD_Clear(Black); LCD_SetBackColor(Black); LCD_SetTextColor(White); char temp[20]; LCD_DisplayStringLine(Line1,(u8)" DATA "); spr…

【MMD x EEVEE教程】导出60FPS ABC

在MMD桥左上角找到 这里模型是导入到blender里面,如果是其它软件,c4d什么的,可以选对应的脚本, 导出范围,0为开始帧,100为结束帧率,100 = mmd动作总帧率 * 2, mmd默认帧率都是30帧 为了导出快一些,可以调低一些导出的分辨率,不能调太低,10 x 10之类的 导出视频 …

ICMP协议

Internet控制消息协议ICMP (Internet Control Message Protocol)是IP协议的辅助协议 ICMP协议用来在网络设备间传递各种差错和控制信息,对于收集各种网络信息、诊断和排除各种网络故障等方面起着至关重要的作用。 icmp作用: 检测网络的双向联通性 ping 的格式: ping 空格 …