导读:本期聚焦于永濑创作的《iOS如何用Core Image实现图像波浪扭曲和水波纹效果?正弦余弦波与动态扰动详解》,敬请观看详情。给一张静态图片加上逼真的水波荡漾效果,是iOS开发中常见又容易踩坑的需求。Core Image框架内置的CIRippleTransition和自定义CIFilter结合GPU渲染,可以低成本实现正弦波扭曲、余弦波扭曲以及持续动画的水波纹。本文先拆解波浪扭曲背后的数学原理,讲清楚位移、波长、振幅与相位之间的关系,再通过自定义CIFilter和CIWarpKernel写出自绘波纹滤镜,最后结合CADisplayLink驱动相位变化,做出随时间流动的动态扰动效果,并附上性能优化与Metal兼容建议。

图像的波浪扭曲本质上是按照某种周期函数对像素坐标做位移映射,Core Image提供了GPU加速的渲染管线,非常适合实现这类逐像素计算的效果。无论是做照片编辑App的创意滤镜,还是给游戏背景加一层水波荡漾,理解正弦波、余弦波扭曲的数学模型和CIFilter的用法都十分关键。本文将从原理、内置滤镜、自定义Kernel以及动态动画四个层面,完整实现一套水波纹扭曲方案。

iOS如何用Core Image实现图像波浪扭曲和水波纹效果?正弦余弦波与动态扰动详解

一、波浪扭曲的数学原理:位移映射是怎么回事

波浪扭曲属于几何变换(Warp)的一种。普通滤镜计算的是某个像素输出什么颜色,而几何扭曲计算的是某个位置的像素应该从原图哪个坐标取样。Core Image的Warp类滤镜(如CIBumpDistortion、CITwirlDistortion)都是这个思路,波浪扭曲也一样。

以正弦波为例,对于输出图像上的一点(x, y),我们希望它取样自原图中沿某个方向发生周期性偏移的坐标。水平方向的波浪映射可以写成:

// 正弦波扭曲的坐标映射公式
newX = x + amplitude * sin(2 * PI * y / wavelength + phase)
newY = y

其中amplitude(振幅)决定波峰波谷的幅度,wavelength(波长)决定波纹的密度,phase(相位)决定波的起始位置。当相位随时间递增时,波纹就会看起来在移动,这就是动态水波的核心。如果把sin换成cos,波形整体错开四分之一个周期,视觉效果上波的起点不同,常用于与正弦波叠加制造更自然的随机感。

真实的水波纹通常不是单一方向的波,而是从某个中心向外扩散的同心圆。此时映射公式改为基于半径计算:对每个像素求它到波心的距离r,再用sin(r / wavelength - phase)作为径向偏移量,沿半径方向推拉采样点。振幅还应随距离衰减,否则整个画面都在抖动,只有波心附近的扰动才像真实水面。

二、使用内置滤镜快速实现波纹过渡

Core Image自带的CIRippleTransition是最省事的方案,它模拟一粒石子落入水面产生的涟漪,并用在两张图片的过渡动画中。它暴露了inputCenter(波心坐标)、inputWidth(波纹宽度)、inputScale(缩放强度)和inputTime(时间参数)等属性,把inputTime从0递增到1即可看到涟漪从中心扩散。

import CoreImage

let context = CIContext()
guard let inputImage = CIImage(image: UIImage(named: "lake")!) else { return }

if let filter = CIFilter(name: "CIRippleTransition") {
    filter.setValue(inputImage, forKey: kCIInputImageKey)
    // 波心归一化到0~1再乘以尺寸
    filter.setValue(CIVector(x: inputImage.extent.width * 0.5,
                             y: inputImage.extent.height * 0.5),
                    forKey: kCIInputCenterKey)
    filter.setValue(10.0, forKey: "inputWidth")
    filter.setValue(30.0, forKey: "inputScale")

    if let output = filter.outputImage,
       let cgImage = context.createCGImage(output, from: inputImage.extent) {
        imageView.image = UIImage(cgImage: cgImage)
    }
}

需要注意的是,CIRippleTransition本质是过渡滤镜,它会同时消费inputImageinputTargetImage,如果只想要单纯扭曲而不做图片切换,把target设置成同一张图即可。此外它的波纹样式固定,无法自定义衰减曲线,只适合简单场景。

内置滤镜还有CITwirlDistortion、CIBumpDistortion等几何变形可以与波纹叠加,比如先做轻微的Bump让画面有局部隆起,再叠加涟漪,层次感会更强。但要做真正可控的正弦波扭曲,还是得走自定义滤镜的路线。

三、自定义CIFilter与CIWarpKernel实现正弦波扭曲

自定义滤镜的核心是CIWarpKernel,它只负责坐标映射,不需要自己写取样逻辑,Core Image会在后台用GPU执行。对于波浪扭曲这种纯几何效果,WarpKernel比通用的CIKernel更简洁高效。

Wave Warp的Kernel代码用Core Image Kernel Language编写,这种语言类似GLSL,运行时会被编译为Metal或OpenGL着色器。下面实现一个支持方向、波长、振幅、相位的通用波浪扭曲:

// SineWaveWarp.swift
import CoreImage

class SineWaveWarpFilter: CIFilter {
    var inputImage: CIImage?
    var inputAmplitude: CGFloat = 12
    var inputWavelength: CGFloat = 100
    var inputPhase: CGFloat = 0
    var inputAngle: CGFloat = 0  // 波浪方向

    static var kernel: CIWarpKernel = { () -> CIWarpKernel in
        let url = Bundle.main.url(forResource: "default", withExtension: "metallib")!
        do {
            return try CIWarpKernel(functionName: "sineWaveWarp", fromMetalLibraryData: try Data(contentsOf: url))
        } catch {
            fatalError("内核加载失败: \(error)")
        }
    }()

    override var outputImage: CIImage? {
        guard let input = inputImage else { return nil }
        return SineWaveWarpFilter.kernel.apply(
            extent: input.extent,
            roiCallback: { _, rect in rect.insetBy(dx: -30, dy: -30) },
            image: input,
            arguments: [input, inputAmplitude, inputWavelength,
                        inputPhase, inputAngle]
        )
    }
}

对应的Metal内核如下,注意坐标系原点在左下角,方向角通过旋转坐标实现任意角度的波浪:

#include <CoreImage/CoreImage.h>

extern "C" float4 sineWaveWarp(coreimage::destination dest,
                               coreimage::sampler src,
                               float amplitude, float wavelength,
                               float phase, float angle)
{
    // 将坐标旋转到波浪方向
    float dx = dest.coord().x - src.extent().z * 0.5;
    float dy = dest.coord().y - src.extent().w * 0.5;
    float ca = cos(angle), sa = sin(angle);
    float u =  dx * ca + dy * sa;
    float v = -dx * sa + dy * ca;

    // 沿垂直于波浪方向做正弦位移
    float offset = amplitude * sin(2.0 * M_PI * v / wavelength + phase);
    float2 newCoord = dest.coord() + float2(offset * ca, offset * sa);

    return src.sample(newCoord);
}

这里有一个非常关键的细节:roiCallback必须把感兴趣区域向外扩,扩展量至少等于振幅。因为扭曲后某个输出像素可能要从更远的位置取样,如果ROI没有放大,画面边缘会出现黑边或采样越界。余弦波扭曲只需把sin换成cos,或者简单地给phase加上M_PI / 2

四、结合CADisplayLink实现动态水波纹扰动

静态扭曲只是一张图,真正的水波纹要持续流动。做法是用CADisplayLink驱动每一帧更新相位,让inputPhase随时间线性递增,然后把渲染结果交给CAAnimatableLayer或直接绘制到MetalKit.MTKView上。

class RippleViewController: UIViewController {
    let filter = SineWaveWarpFilter()
    let ciContext = CIContext()

    func startAnimation() {
        let displayLink = CADisplayLink(target: self, selector: #selector(tick))
        displayLink.add(to: .main, forMode: .common)
        displayLink.preferredFramesPerSecond = 30
    }

    @objc private func tick(_ link: CADisplayLink) {
        let t = CGFloat(link.timestamp)
        filter.inputPhase = t * 4.0          // 波速
        filter.inputAmplitude = 10 + 3 * sin(t * 0.7)  // 振幅呼吸
        if let output = filter.outputImage,
           let cg = ciContext.createCGImage(output, from: output.extent) {
            imageView.layer.contents = cg
        }
    }
}

如果想更接近真实水面的扩散涟漪,可以把Kernel改造成径向版本:以波心为原点计算半径r,位移量改为amplitude * sin(r / wavelength - phase) * exp(-r * decay),其中exp(-r * decay)是距离衰减项,让远离波心的区域保持稳定。再叠加两三个不同波长、不同相位的波,随机感会明显提升。

性能方面有几点建议:第一,尽量复用同一个CIContext,它的GPU命令队列创建成本很高;第二,滤镜链上的outputImage只是描述图,真正渲染发生在createCGImageMTKView绘制时,避免中间多余的UIImage转换;第三,纹理大小超过4096时注意设备Metal纹理上限,必要时分块渲染;第四,帧率控制在30帧足够,水波动画在人眼看来24帧以上就流畅了,降低帧率能显著省电。

最后提醒一点兼容性问题:CIWarpKernel在旧设备上会回退到OpenGL ES路径,Kernel Language代码是通用的,但如果直接用Metal函数名方式加载,要确保工程里正确打包了metallib资源。真机上务必测试iPhone和iPad两种屏幕尺寸,因为以点为单位的波长参数在不同像素密度下视觉密度不同,必要时按screen.scale做换算,保证波纹粗细在所有设备上观感一致。

Core Image波浪扭曲水波纹效果修改时间:2026-09-02 07:54:38

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。