Inferno 是一个为 SwiftUI 应用程序设计的开源片段着色器集合。这些着色器设计得易于阅读和理解,即使对于相对初学者也是如此。因此,你会发现每一行代码都用简洁的英语重新表述,并且在每个文件的顶部都有对所用算法的总体解释。
如果您已经熟悉着色器,请下载一个或多个您感兴趣的着色器并开始使用。如果不是,本 README 的其余部分大部分充当在 SwiftUI 中使用着色器的入门指南。
此仓库包含一个跨平台示例项目,演示了所有着色器的实际效果。该示例项目使用 SwiftUI 构建,需要 iOS 17 和 macOS 14。
示例项目包含许多额外的辅助代码,用于以各种方式演示所有着色器。要在您自己的项目中使用这些着色器,您只需要复制相关的 Metal 文件,如果您使用过渡着色器,还可以选择复制 Transitions.swift。
如果您使用 SwiftUI,您可以添加 Inferno 的特殊效果,以添加水波纹、旋转黑洞、闪光灯、浮雕、噪点、渐变等等——所有这些都在 GPU 上完成,以实现最大速度。
要使用此处的着色器,请将相应的 .metal 文件复制到您的项目中,然后从下面显示的该着色器的示例代码开始。如果您正在使用 Inferno 过渡效果,您还应该将 Transitions.swift 复制到您的项目中。
要了解更多信息,请点击下方观看我的 YouTube 视频,了解如何构建用于 SwiftUI 的着色器。
片段着色器是在 SwiftUI 图层的各个元素上运行的小型程序。它们有时被称为“像素着色器”——这个名称并不完全准确,但确实使它们更容易理解。
实际上,片段着色器在 SwiftUI 视图中的每个像素上运行,并且可以随意转换该像素。这听起来可能很慢,但事实并非如此——这里的所有片段着色器在所有支持 iOS 17 的手机上都以 60fps 运行,在所有 ProMotion 设备上都以 120fps 运行。
转换过程可以随意重新着色像素。用户可以通过将各种参数传递到每个着色器来自定义该过程,并且 SwiftUI 还为我们提供了一些值来使用,例如正在修改的像素的坐标及其当前颜色。
着色器使用 Metal Shading Language (MSL) 编写,这是一种基于 C++ 的简单、快速且极其高效的语言,针对高性能 GPU 操作进行了优化。Metal 着色器在构建时编译并链接到 .metallib
文件中。当您在应用程序中激活着色器时,相应的 Metal 函数将从 metallib
加载,然后用于创建要在 GPU 上执行的程序。
SwiftUI 能够与各种 Metal 着色器一起工作,具体取决于您尝试创建的效果类型。
MSL 附带各种内置数据类型和函数,其中许多函数可以对多种数据类型进行操作。Inferno 中使用的数据类型非常简单
bool
:布尔值,即 true 或 false。float
:浮点数。float2
:一个二分量浮点向量,用于保存 X 和 Y 坐标或宽度和高度等内容。half
:半精度浮点数。half2
:一个二分量半精度浮点数。half3
:一个三分量浮点向量,用于保存 RGB 值。half4
:一个四分量浮点向量,用于保存 RGBA 值。uint2
:一个二分量整数向量,用于保存 X 和 Y 坐标或宽度和高度等内容。着色器通常在 float
、float2
、half3
和 half4
之间根据需要流畅地移动。例如,如果您从 float
创建 half4
,则该数字将仅在向量中的每个分量中重复。您还会经常看到通过使用 half3
作为前三个值(通常为 RGB)并指定第四个值作为 float
来创建 half4
的代码。在 half
和 float
之间转换是免费的。
在选择变量类型时请注意:GPU 经过大量优化,可以执行浮点运算,尤其是(在 iOS 上)半精度浮点运算。这意味着您应该尽可能在精度要求允许的情况下首选使用 half
数据类型。这也将节省寄存器空间并提高着色器程序的所谓“占用率”,从而有效地让更多 GPU 核心同时运行您的着色器。查看 了解 Metal 着色器的性能最佳实践 技术讲座以获取更多详细信息。
此外,请注意着色器代码中的标量数字。确保为操作使用正确的数字类型。例如,float y = (x - 1) / 2
可以工作,但 1
和 2
在这里是 int
,并且在运行时不必要地转换为 float
。相反,请编写 float y = (x - 1.0) / 2.0
。相应类型的数字字面量如下所示
float
:0.5
、0.5f
或 0.5F
half
:0.5h
或 0.5H
int
:42
uint
:42u
或 42U
以下是 Inferno 中使用的函数
abs()
计算数字的绝对值,即其非负值。因此,1、5 和 500 等正值保持不变,但 -3 或 -3000 等负值会删除其符号,使其变为 3 或 3000。如果您传递给它一个向量(例如 float2
),则将对每个分量执行此操作。ceil()
将数字向上舍入到最接近的整数。如果您传递给它一个向量(例如 float2
),则将对每个分量执行此操作。cos()
计算弧度值的余弦值。余弦值始终介于 -1 和 1 之间。如果您为 cos()
提供向量(例如 vec3
),它将计算向量中每个分量的余弦值,并返回一个包含结果的相同大小的向量。distance()
计算两个值之间的距离。例如,如果您为它提供一对 vec2
,您将获得通过从另一个向量中减去一个向量而创建的向量的长度。无论您给它什么数据类型,这始终返回一个数字。dot()
计算两个值的点积。这意味着将第一个值的每个分量乘以第二个值中相应的分量,然后将结果相加。floor()
将数字向下舍入到最接近的整数。如果您传递给它一个向量(例如 float2
),则将对每个分量执行此操作。fmod()
计算除法运算的余数。例如,fmod(10.5, 3.0)
为 1.5。fract()
返回值的小数部分。例如,fract(12.5)
为 0.5。如果您为此传递向量,则操作将按分量执行,并将返回一个包含结果的新向量。min()
用于查找两个值中较小的值。如果您传递向量,则按分量完成此操作,这意味着生成的向量将评估向量中的每个分量,并将最小的分量放置在生成的向量中。max()
用于查找两个值中较大的值。如果您传递向量,则按分量完成此操作,这意味着生成的向量将评估向量中的每个分量,并将最大的分量放置在生成的向量中。mix()
基于第三个介于 0 和 1 之间的值,在两个值之间平滑插值,提供线性曲线。pow()
计算一个值升为另一个值的幂,例如 pow(2.0, 3.0)
的计算结果为 2 * 2 * 2,即 8。除了对 float
进行操作外,pow()
还可以计算按分量的指数——它将第一个向量中的第一个项提升为第二个向量中的第一个项的幂,依此类推。sin()
计算弧度值的正弦值。正弦值始终介于 -1 和 1 之间。如果您为 sin()
提供向量(例如 float2
),它将计算向量中每个分量的正弦值,并返回一个包含结果的相同大小的向量。smoothstep()
基于第三个介于 0 和 1 之间的值,在两个值之间插值,提供 S 曲线形状。也就是说,插值开始缓慢(接近 0.0 的值),加速(接近 0.5 的值),然后在接近尾声时减速(接近 1.0 的值)。sample()
提供 SwiftUI 图层在特定位置的颜色值。这最常用于读取当前像素的颜色。有关所有这些的更多信息,请参见 Metal Shading Language Specification。
许多着色器无需用户的任何特殊输入即可运行——它可以操作 SwiftUI 发送给它的数据,然后发回新数据。
因为 SwiftUI 使用动态成员查找在运行时查找着色器函数,这意味着可以像这样应用简单的着色器
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.yourShaderFunction()
)
但是,通常您会想要自定义着色器的工作方式,有点像将参数传递给函数。着色器有点复杂,因为这些值需要上传到 GPU,但原理是相同的。
SwiftUI 使用辅助方法处理此数据传输,这些方法将常见的 Swift 和 SwiftUI 数据类型转换为其 Metal 等效项。例如,如果您想将 Float
、CGFloat
或 Double
从 Swift 传递到 Metal,您可以这样做
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.yourShaderFunction(
.float(someNumber)
)
)
SwiftUI 提供了三个修饰符,使我们可以将 Metal 着色器应用于视图层次结构。每个修饰符都为您的着色器函数提供不同的输入,但每个修饰符也可以接受任意数量的进一步值来自定义着色器的工作方式。
colorEffect()
修饰符传入用户空间中当前像素的位置(即,基于图层的实际大小,以点为单位测量)及其当前颜色。distortionEffect()
修饰符仅传入用户空间中当前像素的位置。layerEffect()
修饰符传入用户空间中当前像素的位置,以及 SwiftUI 图层本身,以便您可以自由地从那里读取值。在下面的文档中,着色器参数在列出时不包括 SwiftUI 自动传入的参数——您只看到您实际需要自己传递的参数。
提示: 在编写更复杂的着色器时,您通常会发现自己需要优化代码以获得最大效率。开始优化代码的最佳位置之一是研究着色器 uniform 变量:与其计算着色器内每个片段都相同的值,不如在 CPU 上预先计算值并将其直接传递到着色器中。这意味着此类计算是每次绘制执行一次,而不是每个片段执行一次。
Inferno 中的所有着色器都是专门为可读性而编写的。具体来说,它们
代码做什么(行间注释)和代码意味着什么(算法介绍)的结合应该有望使所有人都能理解这些着色器。
一个小提示:您通常会看到最终颜色值乘以原始颜色的 alpha 值,只是为了确保我们在存在透明度的位置获得非常平滑的边缘。
Inferno 提供了一系列着色器,其中大多数允许使用输入参数进行一些自定义。
一个 colorEffect()
着色器,生成一个不断循环的颜色渐变,以输入视图为中心。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.visualEffect { content, proxy in
content
.colorEffect(
ShaderLibrary.animatedGradientFill(
.float2(proxy.size),
.float(elapsedTime)
)
)
}
}
}
}
一个 colorEffect()
着色器,用棋盘格图案替换当前图像,在原始颜色和替换颜色之间切换。
参数
replacement
:用于棋盘格的替换颜色。size
:棋盘格正方形的大小。示例代码
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.checkerboard(
.color(.red),
.float(50)
)
)
一个 colorEffect()
着色器,生成向外或向内移动的圆形波浪,具有不同的尺寸、亮度、速度、强度等等。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。brightness
:颜色应该有多亮。0 到 5 的范围效果最佳;尝试从 0.5 开始并进行实验。speed
:波浪应该传播多快。-2 到 2 的范围效果最佳,其中负数会导致波浪向内传播;尝试从 1 开始。strength
:波浪应该有多强烈。0.02 到 5 的范围效果最佳;尝试从 2 开始。density
:每个波浪应该有多大。20 到 500 的范围效果最佳;尝试从 100 开始。center
:效果的中心,其中 0.5/0.5 是正中心circleColor
:用于波浪的颜色。使用较深的颜色可以创建强度较低的核心。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.drawingGroup()
.visualEffect { content, proxy in
content
.colorEffect(
ShaderLibrary.circleWave(
.float2(proxy.size),
.float(elapsedTime),
.float(0.5),
.float(1),
.float(2),
.float(100),
.float2(0.5, 0.5),
.color(.blue)
)
)
}
}
}
}
一个 layerEffect()
着色器,分离像素的 RGB 值并偏移它们以创建故障风格效果。当通过加速度计数据提供 offset
值时,这在 iOS 上尤其有效。
参数
offset
:颜色偏移多少。示例代码
struct ContentView: View {
@State private var touchLocation = CGSize.zero
var body: some View {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black)
.drawingGroup()
.layerEffect(
ShaderLibrary.colorPlanes(
.float2(touchLocation)
),
maxSampleOffset: .zero
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { touchLocation = $0.translation }
)
}
}
一个 layerEffect()
着色器,通过从一个方向的像素添加亮度,并从另一个方向的像素减去亮度来创建浮雕效果。
参数
strength
:我们应该读取多远像素来创建效果。示例代码
struct ContentView: View {
@State private var embossAmount = 0.0
var body: some View {
VStack {
Text("🏳️🌈")
.font(.system(size: 300))
.layerEffect(
ShaderLibrary.emboss(
.float(embossAmount)
),
maxSampleOffset: .zero
)
Slider(value: $embossAmount, in: 0...20)
.padding()
}
}
}
一个 colorEffect()
着色器,生成渐变填充
参数
示例代码
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.gradientFill()
)
通过将较亮的对象着色为红色,将较暗的对象着色为蓝色来模拟红外摄像机。
参数
示例代码
Text("👩💻")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.infrared()
)
一个 colorEffect()
着色器,应用隔行扫描效果,其中原始颜色的水平线被另一种颜色的线分隔开。
参数
width
:隔行扫描线的宽度。1 到 4 的范围效果最佳;尝试从 1 开始。replacement
:用于隔行扫描线的颜色。尝试从黑色开始。strength
:将隔行扫描线与颜色混合的程度。指定 0(完全不混合)到 1(完全混合)。示例代码
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.red)
.colorEffect(
ShaderLibrary.interlace(
.float(2),
.color(.black),
.float(1)
)
)
一个 colorEffect()
着色器,反转图像的 alpha 值,用提供的颜色替换透明颜色。
参数
replacement
:用于像素的替换颜色。示例代码
Text("🤷♂️")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.invertAlpha(
.color(.red)
)
)
一个 colorEffect()
着色器,生成多条扭曲和转动的线条,这些线条循环显示颜色。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数density
:要创建多少行和列。1 到 50 的范围效果良好;尝试从 8 开始。speed
:使灯光颜色变化的速度有多快。较高的值会导致灯光闪烁更快,颜色变化更多。1 到 20 的范围效果良好;尝试从 3 开始。groupSize
:每个组中放置多少个灯光。1 到 8 的范围效果良好,具体取决于您的密度;从 1 开始。brightness
:使灯光有多亮。0.2 到 10 的范围效果良好;尝试从 3 开始。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.drawingGroup()
.visualEffect { content, proxy in
content
.colorEffect(
ShaderLibrary.lightGrid(
.float2(proxy.size),
.float(elapsedTime),
.float(8),
.float(3),
.float(1),
.float(3)
)
)
}
}
}
}
一个 colorEffect()
着色器,发回现有的颜色数据,不进行任何更改。
参数
示例代码
Text("🏳️🌈")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.passthrough()
)
一个 colorEffect()
着色器,生成动态的、多色的噪点。
参数
time
:自着色器创建以来经过的秒数。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.rainbowNoise(
.float(elapsedTime)
)
)
}
}
}
一个 colorEffect()
着色器,将输入颜色更改为替换颜色,同时保持当前 alpha 值不变。
参数
replacement
:用于像素的替换颜色。示例代码
Text("💪")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.recolor(
.color(.blue)
)
)
一个 distortionEffect()
着色器,生成波浪效果,其中输入项的左侧不应用任何效果,而右侧应用完整效果。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。speed
:使波浪起伏的速度有多快。尝试从值 5 开始。smoothing
:平滑波纹的程度,值越大,效果越平滑。尝试从值 20 开始。strength
:使波纹效果有多明显。尝试从值 5 开始。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.visualEffect { content, proxy in
content
.distortionEffect(
ShaderLibrary.relativeWave(
.float2(proxy.size),
.float(elapsedTime),
.float(5),
.float(20),
.float(5)
),
maxSampleOffset: .zero
)
}
}
}
}
一个 visualEffect()
和 colorEffect()
着色器,生成微光效果,其中输入颜色被从左到右动画的对角渐变照亮。
参数
position
:当前像素的用户空间坐标。color
:像素的当前颜色。size
:整个视图的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。animationDuration
:微光动画的单个循环的持续时间,以秒为单位。gradientWidth
:UV 空间中微光渐变的宽度。maxLightness
:渐变峰值处的最大亮度。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.visualEffect { content, proxy in
content
.colorEffect(
InfernoShaderLibrary[dynamicMember: "shimmer"](
.float2(proxy.size),
.float(elapsedTime),
.float(3.0),
.float(0.3),
.float(0.9)
)
)
}
}
}
}
一个 layerEffect()
着色器,在精确位置上方创建圆形缩放效果。
参数
size
:整个图像的大小,以用户空间为单位。touch
:用户触摸的位置,缩放应以此为中心。maxDistance
:使缩放区域有多大。尝试从 0.05 开始。zoomFactor
:放大镜内容的缩放倍数。示例代码
struct ContentView: View {
@State private var touchLocation = CGPoint.zero
var body: some View {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.visualEffect { content, proxy in
content
.layerEffect(
ShaderLibrary.simpleLoupe(
.float2(proxy.size),
.float2(touchLocation),
.float(0.05),
.float(2)
),
maxSampleOffset: .zero
)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { touchLocation = $0.location }
)
}
}
一个 colorEffect()
着色器,生成多条扭曲和转动的线条,这些线条循环显示颜色。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Rectangle()
.visualEffect { content, proxy in
content
.colorEffect(
ShaderLibrary.sinebow(
.float2(proxy.size),
.float(elapsedTime)
)
)
}
}
}
}
一个 layerEffect()
着色器,在精确位置上方创建圆形缩放效果,触摸区域周围具有可变缩放,以创建类似玻璃球的效果。
参数
size
:整个图像的大小,以用户空间为单位。touch
:用户触摸的位置,缩放应以此为中心。maxDistance
:使缩放区域有多大。尝试从 0.05 开始。zoomFactor
:放大镜内容的缩放倍数。示例代码
struct ContentView: View {
@State private var touchLocation = CGPoint.zero
var body: some View {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.visualEffect { content, proxy in
content
.layerEffect(
ShaderLibrary.warpingLoupe(
.float2(proxy.size),
.float2(touchLocation),
.float(0.05),
.float(2)
),
maxSampleOffset: .zero
)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { touchLocation = $0.location }
)
}
}
一个 layerEffect()
着色器,在精确位置上方创建简单的肥皂泡效果。
参数
uiSize
:整个图像的大小,以用户空间为单位。uiPosition
:气泡应以此为中心的位置,以用户空间为单位。uiRadius
:气泡区域应有多大,以用户空间为单位。示例代码
struct ContentView: View {
@State private var touchLocation = CGPoint.zero
var body: some View {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.visualEffect { content, proxy in
content
.layerEffect(
ShaderLibrary.bubble(
.float2(proxy.size),
.float2(touchLocation),
.float(50)
),
maxSampleOffset: .zero
)
}
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { touchLocation = $0.location }
)
}
}
一个 distortionEffect()
着色器,生成水波纹效果。
参数
size
:整个图像的大小,以用户空间为单位。time
:自着色器创建以来经过的秒数。speed
:使水波纹起伏的速度有多快。0.5 到 10 的范围效果最佳;尝试从 3 开始。strength
:波纹效果应该有多明显。1 到 5 的范围效果最佳;尝试从 3 开始。frequency
:应该多久创建波纹。5 到 25 的范围效果最佳;尝试从 10 开始。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.visualEffect { content, proxy in
content
.distortionEffect(
ShaderLibrary.water(
.float2(proxy.size),
.float(elapsedTime),
.float(3),
.float(3),
.float(10)
),
maxSampleOffset: .zero
)
}
}
}
}
一个 distortionEffect()
着色器,生成波浪效果,其中输入项的左侧不应用任何效果,而右侧应用完整效果。
参数
time
:自着色器创建以来经过的秒数。speed
:使波浪起伏的速度有多快。尝试从值 5 开始。smoothing
:平滑波纹的程度,值越大,效果越平滑。尝试从值 10 开始。strength
:使波纹效果有多明显。尝试从值 5 开始。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.padding()
.background(.background)
.drawingGroup()
.distortionEffect(
ShaderLibrary.wave(
.float(elapsedTime),
.float(5),
.float(10),
.float(5)
),
maxSampleOffset: .zero
)
}
}
}
一个 colorEffect()
着色器,生成动态的、灰度噪点。
参数
time
:自着色器创建以来经过的秒数。示例代码
struct ContentView: View {
@State private var startTime = Date.now
var body: some View {
TimelineView(.animation) { timeline in
let elapsedTime = startTime.distance(to: timeline.date)
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.colorEffect(
ShaderLibrary.whiteNoise(
.float(elapsedTime)
)
)
}
}
}
除了上面列出的着色器外,Inferno 还提供了一系列专门设计用作过渡效果的着色器。尽管这些着色器在内部仍然是 Metal 着色器,但您将通过 AnyTransition
扩展来使用它们,这使得该过程无缝衔接。
提示: 除了将一个特定的着色器复制到您的项目中之外,您还应该添加 Transitions.swift 以包含
AnyTransition
扩展。
一种过渡效果,其中许多圆形向上增长以显示新内容。
参数
size
:使圆形有多大。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.circles(size: 20))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.circles(size: 20))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,其中许多圆形向上增长以显示新内容,圆形从左上角边缘向外移动。
参数
size
:使圆形有多大。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.circleWave(size: 20))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.circleWave(size: 20))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,从左边缘开始拉伸和淡化像素。
参数
示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.crosswarpLTR)
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.crosswarpLTR)
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,从右边缘开始拉伸和淡化像素。
参数
示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.crosswarpRTL)
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.crosswarpRTL)
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,使各种菱形同时在屏幕上放大。
参数
size
:使菱形有多大。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.diamonds(size: 20))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.diamonds(size: 20))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,使各种圆形根据其 X/Y 位置在屏幕上放大。
参数
size
:使菱形有多大。默认为 20。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.diamondWave(size: 20))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.diamondWave(size: 20))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,导致传入和传出的视图变得越来越像素化,然后恢复到其正常状态。在此过程中,旧视图淡出,新视图淡入。
参数
squares
:像素应该有多大。默认为 20。steps
:动画要使用的步数。较低的值会使像素以更明显的尺寸增量跳跃,从而产生非常有趣的复古效果。默认为 60。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.pixellate(squares: 20, steps: 20))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.pixellate(squares: 20, steps: 20))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,创建旧式的径向擦除,从正上方开始。
参数
示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.radial)
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.radial)
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,逐渐扭曲传入和传出的视图的内容,然后解开扭曲以完成过渡。在此过程中,两个视图淡入淡出以平滑地从一个视图移动到另一个视图。
参数
radius
:漩涡相对于其过渡的视图应有多大。默认为 0.5。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.swirl(radius: 0.5))
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.swirl(radius: 0.5))
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,使其看起来像一个图像的像素正在水平吹走。
参数
size
:风条纹相对于视图宽度应有多大。默认为 0.2。示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.wind())
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.wind())
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
一种过渡效果,使其看起来像视图正在被吸入角落。
参数
示例代码
struct ContentView: View {
@State private var showingFirstView = true
var body: some View {
VStack {
if showingFirstView {
Image(systemName: "figure.walk.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.blue)
.drawingGroup()
.transition(.genie())
} else {
Image(systemName: "figure.run.circle")
.font(.system(size: 300))
.foregroundStyle(.white)
.padding()
.background(.indigo)
.drawingGroup()
.transition(.genie())
}
Button("Toggle Views") {
withAnimation(.easeIn(duration: 1.5)) {
showingFirstView.toggle()
}
}
}
}
}
此仓库内部是一个 macOS 的示例 SwiftUI 项目,演示了每个着色器以及一些示例值——如果您好奇每个着色器的外观或性能,请尝试运行它们。
如果您修改了其中一个着色器并想查看其外观,沙箱是最佳场所。如果您单击“切换不透明度”工具栏按钮,预览内容将在不透明度 0 和 1 之间交替,因此您可以确保您的修改正确混合。
这里的所有着色器在所有支持 macOS Sonoma 和协调发布的设备(包括 iOS 17)上都能很好地工作。
提示
尽管沙箱对于预览着色器很有帮助,但几乎所有代码都不是在您自己的项目中使用 Inferno 着色器所必需的——您只需要复制相关的 Metal 文件,如果您使用过渡着色器,还可以选择复制 Transitions.swift。
我创建 Inferno 是因为没有足够的人知道着色器是向您的应用程序添加特殊效果的强大而简单的方法。如果您想贡献您自己的着色器或修改现有着色器,那太棒了!但请先阅读以下内容
MIT 许可证。
版权所有 (c) 2023 Paul Hudson 和其他作者。
特此授予任何获得本软件和相关文档文件(“软件”)副本的人员免费许可,以不受限制地处理本软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本的权利,并允许向为其提供软件的人员这样做,但须符合以下条件
上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。
本软件按“原样”提供,不提供任何形式的明示或暗示保证,包括但不限于适销性、特定用途的适用性和不侵权的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是在合同诉讼、侵权诉讼或其他诉讼中,由于软件或软件的使用或其他交易而产生、源于或与之相关的任何索赔、损害或其他责任。
Inferno 由 Paul Hudson 制作,他撰写了 Hacking with Swift 上的免费 Swift 教程。它在 MIT 许可证下可用,该许可证允许商业用途、修改、分发和私人用途。
一些着色器是由我从其他同样在 MIT 许可证下发布的开源示例移植到 Metal 的。他们的代码的所有功劳归于其原始作者;所有错误和类似问题显然是我的责任!
如果您热衷于了解更多关于 Metal 着色器的信息,以下是我推荐的资源