分形火焰(Fractal Flame)是一种基于迭代函数系统(IFS)的非线性分形绘图方法,它通过多个仿射变换与变异函数不断迭代点坐标,并将落点累积到二维直方图网格中。原始算法只记录每个像素被命中次数,渲染出来是单调灰度图。要在iOS上获得绚丽的火焰效果,必须引入颜色映射:给每次迭代分配渐变色,再按密度做色调混合。Core Image的CIFilter与CIKernel允许我们用Metal或GLSL风格代码在GPU上完成这一映射,避免主线程卡顿。

分形火焰迭代函数系统与颜色累积原理
迭代函数系统由一组变换组成,每个变换包含仿射矩阵与变异函数(如正弦、球面、漩涡)。算法从一个随机点出发,以一定概率选择某个变换计算新坐标,重复数十万次。传统灰度火焰只统计命中频次,而彩色火焰在每次迭代时,根据所选变换的预设颜色权重,将当前点的RGB值按系数累加到对应像素的颜色缓冲,同时累加频次缓冲。
最终每个像素拥有两个量:总命中次数与颜色总和。真实颜色等于颜色总和除以命中次数,再经过对数色调映射压缩动态范围。这种先累积后归一的方式能让稀疏区域也呈现平滑渐变,而不是被高频点洗白。在iOS中,如果我们用Swift在CPU端做这套统计,1080p画布迭代50万次往往需要数百毫秒,因此必须借助Core Image把映射放到GPU。
下面是一段简化的IFS迭代与颜色权重累积伪代码,用于说明数据形态。实际放入CIKernel时,我们会把直方图存为浮点纹理,在并行着色器中累加。
// 简化IFS迭代示例(CPU参考,非最终GPU内核)
struct Transform {
var a: Float, b: Float, c: Float, d: Float, e: Float, f: Float
var color: (r: Float, g: Float, b: Float)
var prob: Float
}
func iterate(transforms: [Transform], iterations: Int) -> [Pixel] {
var x = 0.0, y = 0.0
var pixels: [Pixel] = []
for _ in 0..<iterations {
let t = pickTransform(transforms)
let nx = t.a * x + t.b * y + t.c
let ny = t.d * x + t.e * y + t.f
x = nx; y = ny
let px = Int((x + 1) * 500)
let py = Int((y + 1) * 500)
pixels[px][py].count += 1
pixels[px][py].r += t.color.r
pixels[px][py].g += t.color.g
pixels[px][py].b += t.color.b
}
return pixels
}
基于Core Image的渐变颜色映射滤镜封装
Core Image并不直接提供分形火焰滤镜,但我们可以继承CIFilter,用CIKernel(或CIColorKernel)编写映射逻辑。常见做法是先用另一个滤镜或Metal计算管线生成密度与颜色累积纹理,再写一个色彩映射内核:输入为密度图与颜色和图,输出为映射后的RGBA。内核中读取像素的颜色和除以密度得到平均色,再用对数公式 brightness = log(1+density)/log(1+maxDensity) 做亮度缩放。
渐变本身可以用一维颜色查找纹理(Gradient LUT)实现。我们将火焰调色板(黑-红-橙-黄-白)写入一张 256x1 的 CGImage,转为CIImage输入内核。内核根据亮度值采样LUT,得到最终火焰色。这样更换调色板只需换图,不需改算法。以下代码展示CIFilter子类的内核调用框架:
import CoreImage
class FlameColorMapFilter: CIFilter {
var inputDensity: CIImage?
var inputColorSum: CIImage?
var inputPalette: CIImage?
static var kernel: CIKernel = {
let src = """
kernel vec4 flameMap(__sample density, __sample colorSum, __sample palette) {
float d = density.r;
if (d < 0.0001) return vec4(0.0);
vec3 avg = colorSum.rgb / d;
float bright = log(1.0 + d) / log(1.0 + 1000.0);
vec3 col = sample(palette, vec2(bright, 0.5)).rgb;
col *= avg;
return vec4(col, 1.0);
}
"""
return CIKernel(source: src)!
}()
override var outputImage: CIImage? {
guard let d = inputDensity, let c = inputColorSum, let p = inputPalette else { return nil }
return FlameColorMapFilter.kernel.apply(extent: d.extent,
arguments: [d, c, p])
}
}
这种封装的好处是滤镜可串联到CIFilterChain中,前面接直方图累积滤镜,后面接CIExposureAdjust做整体提亮。在iPhone上用Metal后端跑 1024x1024 迭代纹理,映射阶段仅耗时约 3-5 毫秒,远快于CPU回读。需要注意的是,内核里除法要避免密度为零,否则出现NaN导致黑屏,务必加极小值保护。
火焰效果渲染优化与常见色彩断层排查
即使算法正确,移动端常出现火焰颜色带断裂或颗粒过重。第一类问题是迭代次数不足,密度图噪声大,对数映射后暗部出现斑驳。解决方法是提升迭代到 100 万次以上,或在累积阶段做多次子帧叠加。第二类问题是Gradient LUT精度不够,256级在平缓过渡区产生可见色阶,可改为 1024 级或改用平滑插值函数。
另一常见误区是直接在 sRGB 空间做颜色平均,导致中间调偏灰。分形火焰应在线性空间累积颜色,输出前再转 sRGB。Core Image默认工作于线性化色彩空间,但如果我们用CGColorSpaceCreateDeviceRGB创建LUT,会被当成非线性,造成火焰发黄。正确做法是明确指定 kCGColorSpaceLinearSRGB 生成调色板图。
最后,Core Image的CIContext建议复用而非每次新建,并开启allowsParallelRendering。若用SwiftUI预览,应将滤镜输出转 UIImage 放在后台调度,避免阻塞。下面代码展示上下文复用与渲染调用:
let context = CIContext(options: [.workingColorSpace: CGColorSpace(name: CGColorSpace.linearSRGB)!])
func renderFlame(density: CIImage, colorSum: CIImage, palette: CIImage) -> UIImage? {
let filter = FlameColorMapFilter()
filter.inputDensity = density
filter.inputColorSum = colorSum
filter.inputPalette = palette
guard let out = filter.outputImage else { return nil }
if let cg = context.createCGImage(out, from: out.extent) {
return UIImage(cgImage: cg)
}
return nil
}
通过上述步骤,我们能在iOS端利用Core Image高效完成分形火焰的颜色映射与渲染。核心在于把IFS密度统计与GPU着色解耦,用累积纹理加LUT内核实现灵活火焰渐变,并注意色彩空间与数值稳定性,便可在手机上实时生成流动般的火焰分形图。
Core_Image分形火焰颜色映射修改时间:2026-08-17 08:06:15