简介

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

运行效果:

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

在 SwiftUI 中使用 Metal Shader的更多相关文章

  1. 【玩转cocos2d-x之四十】怎样在Cocos2d-x 3.0中使用opengl shader?

    有小伙伴提出了这个问题.事实上GLProgramCocos2d-x引擎自带了.全然能够直接拿来用. 先上图吧. 使用opengl前后的对照: watermark/2/text/aHR0cDovL2Js ...

  2. Unity5中新的Shader体系简析

    一.Unity5中新的Shader体系简析 Unity5和之前的书写模式有了一定的改变.Unity5时代的Shader Reference官方文档也进一步地变得丰满. 主要需要了解到的是,在原来的Un ...

  3. iOS开发 - 在SwiftUI中显示模态视图

    在SwiftUI中显示模态视图 简介 这里教大家如何弹出一个简单的模态视图.分别有两个页面,ContentView和GCPresentedView,以下对应简称为A和B.我们要做的是在A视图中点击按钮 ...

  4. 【原创翻译】初识Unity中的Compute Shader

    一直以来都想试着自己翻译一些东西,现在发现翻译真的很不容易,如果你直接把作者的原文按照英文的思维翻译过来,你会发现中国人读起来很是别扭,但是如果你想完全利用中国人的语言方式来翻译,又怕自己理解的不到位 ...

  5. unity中使用自定义shader进行光照贴图烘培无法出现透明度的坑爹问题

    最近开发中在对场景进行光照贴图烘焙时发现一个坑爹问题,在使用自定义shader的时候,shader命名中必须包含Transparent路径,否则烘焙的时候不对alpha通道进行计算,烘焙出来都是狗皮膏 ...

  6. 【日常记录】Unity3D 中的 Surface Shader 是不支持在 Pass中使用的,因为自动生成了 Pass

    如题 搞了好久,一直报错: Shader error in 'custom_outline_effect': Parse error: syntax error, unexpected TOK_PAS ...

  7. SwiftUI 中一些和响应式状态有关的属性包装器的用途

    SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...

  8. Unity Diffuse Metal Shader Mod

    Shader "MetalShader" { Properties { _MainTex ("Base (RGB) RefStrGloss (A)", 2D) ...

  9. cocos中lua使用shader实例

    local prog = cc.GLProgram:create("res/shader/light2d.vsh","res/shader/light2d.fsh&quo ...

  10. SwiftUI中多设备运行方法

    https://blog.csdn.net/weixin_42679753/article/details/94465674 https://www.jianshu.com/p/17fc7929fcb ...

随机推荐

  1. VUE2的前端学习笔记

    名词笔记 Node node.js 是一切的基础, 其他的名词都是基于这个运行的, 相当于一个JavaScript的容器或者虚机 NPM 安装node.js时赠送的包管理器, 类似于apt, yum, ...

  2. 【Unity3D】导航系统

    1 导航系统简介 ​ 导航系统用于智能避障并寻找目标物体,如:王者荣耀中,当玩家跑到敌方塔的攻击范围内,敌方塔就会发射火团攻击玩家,当玩家逃跑时,火团会智能跟随玩家,其中智能跟随就使用到了导航系统. ...

  3. Innodb学习之MySQL体系结构

    目录 数据库和数据库实例 MySQL体系结构 MySQL各存储引擎特性 Innodb存储引擎 MyISAM BDB: Memory Archive Federated 数据库和数据库实例 数据库是所有 ...

  4. VS2019 添加三方文件夹遇到的坑

    在开发新项目时需要用到一些三方 API,这些三方 API 没有生成 lib,所以我们在 VS 编译器中添加这些三方文件夹的头文件路径后 会出现 ERROR LNK2019 的错误提示,这些提示通常都是 ...

  5. 彻底搞懂Java中的Runnable和Thread

    写在前面 今天在阅读ThreadPoolExecutor源码的时候觉得有些地方理解起来似是而非,很别扭!最后才猛然发现,原来是我自己的问题:没有真正理解Runnable和Thread的含义! 我之前对 ...

  6. mysql安装及增删改查操作---day35

    # ### mysql ''' 命令可以用tab来补全 d: D:\>cd MySQL5.7 D:\>cd D:\MySQL5.7\mysql-5.7.25-winx64\bin 直接切换 ...

  7. 【Azure 应用服务】添加自定义域时,Domain ownership 验证无法通过 

    问题描述 在Azure App Service添加自定义域名时,遇见了Domain ownership 验证无法通过的问题? 问题解决 因为DNS中配置App Service默认域名和自定义域名的CN ...

  8. Java toString的使用

    1 package com.bytezreo.objectclass; 2 3 import java.util.Date; 4 5 /** 6 * 7 * @Description Object类中 ...

  9. 16 Educational Codeforces Round 142 (Rated for Div. 2)C. Min Max Sort(递归、思维、dp)

    C. Min Max Sort 很不错的一道题目,不过脑电波和出题人每对上,\(qwq.\) 正难则反. 我们考虑最后一步是怎么操作的. 最后一步一定是对\(1\)和\(n\)进行操作 那么上一步呢? ...

  10. Python 初学者容易踩的坑

    哈喽大家好,我是咸鱼. 今天咸鱼列出了一些大家在初学 Python 的时候容易踩的一些坑,看看你有没有中招过. 原文:https://www.bitecode.dev/p/unexpected-pyt ...