分形扭曲是一类非常有视觉冲击力的图像处理效果,它能让一张普通的照片呈现出流体漩涡、大理石纹理或者熔岩流动般的抽象艺术质感。这类效果在专业的创意编辑类App里相当常见,比如Prisma、Glitch Art风格的图片处理工具都有类似实现。在iOS平台上,实现这类效果最合适的路径就是Core Image框架——它既提供了GPU加速的渲染管线,又允许开发者编写自定义内核来执行逐像素的复杂运算。本文将从分形噪声的原理讲起,逐步实现一个完整的分形扭曲滤镜。

一、分形噪声的数学原理与湍流函数
在动手写代码之前,必须先理解分形噪声到底是什么。普通的随机噪声(比如每个像素随机取黑白值)在视觉上是一团杂乱的噪点,没有任何连续性,不能用来做扭曲场。而分形噪声的核心是连续噪声函数:给定一个坐标点(x, y),函数返回一个平滑变化的值,相邻坐标点的值是连续过渡的。最经典的连续噪声就是Perlin噪声,它通过对网格顶点处的伪随机梯度向量做插值得到平滑的二维噪声场。
单个Perlin噪声的频率是固定的,看起来只有一种“颗粒尺寸”。分形布朗运动(Fractional Brownian Motion,简称fBm)的做法是把多个不同频率、不同幅度的Perlin噪声叠加起来:每叠加一层,频率翻倍、幅度减半,这一层就叫做一个“八度”(octave)。叠加四到五个八度后,噪声既有大尺度的起伏,又有小尺度的细节,呈现出自相似性——放大局部看,纹理结构和整体是相似的,这正是“分形”这个名字的由来。
湍流函数(turbulence)则是fBm的一个变体。标准fBm直接累加各八度的噪声值,结果偏向柔和的云雾状;而湍流函数对每个噪声值先取绝对值再累加,公式大致是 sum += abs(noise(x, y)) * amplitude。取绝对值会在噪声过零的位置产生尖锐的折痕,最终形成大量细密的脊线状纹理,视觉上更接近大理石纹或木纹。在实现扭曲效果时,fBm适合做柔和的流体漩涡,湍流函数适合做锐利的纹理撕裂,两者可以按需求选择或混合使用。
二、用Swift编写自定义Core Image内核
Core Image从iOS 8开始支持用类C语言编写内核,从iOS 12开始支持Metal内核(CIWarpKernel相关API配合Metal编写性能更好)。对于分形扭曲这种逐像素运算,推荐使用 CIColorKernel——它接收源像素坐标和自定义参数,返回处理后的颜色。我们可以在内核里实现一个简化版的二维噪声函数,然后叠加多个八度构成fBm。
下面是一个完整的内核代码示例,注意分形噪声通常直接返回三个通道用于三维坐标偏移,或者返回单通道值用于旋转扭曲:
// 分形扭曲内核(Core Image Kernel Language)
kernel vec4 fractalWarp(sampler src, float time, float scale, float strength) {
vec2 coord = samplerCoord(src);
vec2 dims = samplerSize(src);
// 简易hash函数,生成伪随机值
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
// 平滑插值
float smooth(float t) {
return t * t * (3.0 - 2.0 * t);
}
// 二维值噪声
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = vec2(smooth(f.x), smooth(f.y));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// 分形布朗运动:五个八度叠加
float fbm(vec2 p) {
float value = 0.0;
float amp = 0.5;
for (int i = 0; i < 5; i++) {
value += amp * noise(p);
p *= 2.0;
amp *= 0.5;
}
return value;
}
// 计算噪声梯度作为位移方向,乘以强度得到偏移量
vec2 p = coord / dims * scale;
float n1 = fbm(p + vec2(time * 0.05, 0.0));
float n2 = fbm(p + vec2(0.0, time * 0.07));
vec2 offset = vec2(n1 - 0.5, n2 - 0.5) * strength * dims.y;
// 按偏移量采样源图像,实现扭曲
return sample(src, samplerTransform(src, coord + offset));
}
这段代码的关键点有三处。第一,hash函数用正弦乘大数再取小数的方式生成伪随机值,这是GPU上最常用的低成本随机方案,虽然理论上有可能出现纹理重复,但对视觉效果没有影响。第二,fbm函数里用循环叠加五个八度,每次频率乘2、幅度乘0.5,循环次数越多细节越丰富,但性能开销也线性增长,实际项目中四个八度通常已经足够。第三,最后采样时用samplerTransform把偏移后的目标坐标转换回源图采样坐标,这是自定义内核里最容易出错的地方——直接用目标坐标采样是无效的,必须经过变换。
写好内核字符串后,在Swift侧创建滤镜类并注册参数。需要把内核源码作为字符串常量传给 CIColorKernel 的初始化方法,再用 apply 方法生成输出图像:
class FractalWarpFilter: CIFilter {
var inputImage: CIImage?
var inputTime: Float = 0
var inputScale: Float = 3.0
var inputStrength: Float = 0.15
static var kernel: CIColorKernel = {
let source = """...""" // 上面的内核字符串
return try! CIColorKernel(source: source)
}()
override var outputImage: CIImage? {
guard let input = inputImage else { return nil }
return FractalWarpFilter.kernel.apply(
extent: input.extent,
arguments: [input, inputTime, inputScale, inputStrength]
)
}
}
三、迭代变形:让扭曲效果产生层次感
单次应用fBm位移得到的效果已经不错,但如果想要那种“被反复揉捏”的深度抽象感,就需要用到迭代变形(iterated domain warping)技术。其思路是:把噪声场自己作为输入再做一次位移,即计算 q = fbm(p + fbm(p)),甚至再做一层 r = fbm(p + q)。经过两到三层迭代后,图像会产生剧烈卷曲的结构,纹理从简单的波动变成复杂的涡旋,这是Inigo Quilez在其经典文章中推广的技巧,也是许多抽象艺术滤镜的核心算法。
迭代变形的实现只需要修改内核中的采样逻辑,把偏移量的计算改为级联形式:
// 迭代变形:第一层
vec2 q = vec2(fbm(p), fbm(p + vec2(5.2, 1.3)));
// 第二层:以q作为位移再做fBm
vec2 r = vec2(fbm(p + 4.0 * q + vec2(1.7, 9.2)),
fbm(p + 4.0 * q + vec2(8.3, 2.8)));
// 用r和q加权计算最终位移
vec2 offset = (q.xy + r.yx * 0.5 - 0.5) * strength * dims.y;
迭代次数对性能的影响是乘法级别的:每层迭代都要重新计算完整的fBm,而fBm内部又有五次噪声采样,两层迭代意味着每个像素要做大约四十次噪声运算。在A系列芯片上,一张1000×1000的图像做两层迭代还能保持实时帧率,但如果是4K图像或需要实时预览视频,建议把八度数降到三、四层,或者预先生成噪声贴图存为纹理,在内核中直接采样贴图替代实时计算,这样能把运算量降低一个数量级。
四、参数调节与性能优化建议
实际调参时,scale控制噪声的空间频率:值越小,涡旋越大越舒展;值越大,纹理越细碎。strength控制位移幅度,超过0.3后图像会被拉扯得难以辨认原貌,适合做彻底的抽象化;0.05到0.15之间则保留原始图像的轮廓,只增加流动感。把time参数接入CADisplayLink持续更新,配合 CIContext 复用,就能做出动态流动的分形动画效果,用在Live Wallpaper或短视频特效里非常出彩。
性能方面有几个必须注意的点。首先,CIContext一定要复用,每次渲染都新建context会导致性能暴跌数倍;其次,内核字符串编译一次后应缓存为静态属性,避免重复编译;第三,如果最终输出到 UIImage,建议直接用 UIBezierPath 无关的 CIContext.createCGImage 渲染,或者通过 MetalKit 的 MTKView 渲染到屏幕,绕过CPU侧的位图回读。最后,对于需要发布到App Store的应用,苹果已经逐步淡化Kernel Language,新项目可以直接使用基于Metal的 CIComputeKernel 或自定义 MPSImageKernel,语法上更现代,也更容易和Metal Compute管线集成。
总结一下,Core Image实现分形扭曲的完整链路是:值噪声构造平滑噪声场,多八度叠加构成fBm,绝对值化得到湍流纹理,级联位移实现迭代变形,最后通过自定义内核在GPU上逐像素执行。掌握了这套思路之后,还可以把噪声场换成Voronoi噪声、或者把颜色扭曲替换为亮度扭曲,衍生出完全不同风格的抽象艺术滤镜,这正是分形算法最迷人的地方。
Core Image分形噪声湍流函数修改时间:2026-09-09 08:57:03