在 swiftui 中使用 metal shader-kb88凯时官网登录

来自:
时间:2024-03-23
阅读:

从 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) { _ 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()
    }
}

运行效果:

在 swiftui 中使用 metal shader

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 
[[ 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()
    }
}

运行效果:

在 swiftui 中使用 metal shader

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()
    }
}

运行效果:

在 swiftui 中使用 metal shader
返回顶部
顶部
网站地图