iOS中如何用Core Image模拟水晶球玻璃折射效果?

来源:CSS教程作者:沙月恵奈‌头衔:网络博主
导读:本期聚焦于沙月恵奈‌创作的《iOS中如何用Core Image模拟水晶球玻璃折射效果?》,敬请观看详情。玻璃球折射并不是简单的径向模糊,而是光线穿过球体时按入射角重新映射背景像素。这个过程中,球面法线、折射率和观察方向共同决定采样偏移,缺少任何一项都会让球体看起来像普通镜片而非水晶球。iOS的Core Image允许通过自定义CIKernel在GPU上完成逐像素计算,同时叠加环境映射和菲涅尔边缘反射,让普通照片快速呈现通透质感。本文从球面法线和Snell定律出发,给出一次折射近似的内核代码,并说明如何把环境图映射到球面、如何用菲涅尔项增强边缘高光。最后结合Swift封装与实时性能优化,讨论哪些简化不会明显损失观感,哪些场景适合扩展到双重折射。

想让一张普通照片中出现一颗通透的水晶球,系统内置的扭曲滤镜并不够用。球面折射在每个像素上的采样偏移量不是固定的,它取决于光线从球面进入玻璃时的入射角与折射率,因此需要一个逐像素计算的法线。Core Image允许使用自定义CIKernel将这种计算放到GPU上,同时保持与实时相机流或照片编辑链路的兼容。下面从几何模型开始,逐步补齐环境映射和实时性能优化。

iOS中如何用Core Image模拟水晶球玻璃折射效果?

一、球面折射的几何模型与采样偏移

在Core Image内核中,destCoord()会返回当前输出像素在工作空间中的坐标。为了在图像上画出玻璃球,需要先确定球心center和半径radius,这两个参数都要落在图像坐标空间内。对任意输出坐标uv,计算它与球心的二维距离dist,如果距离大于半径,说明该像素不在球体覆盖范围内,直接返回原图即可。对于球内像素,球面的三维法线可以通过normal = normalize(vec3(delta, z))得到,其中z来自球面方程z = sqrt(radius * radius - dist * dist)。这一做法把图像平面放在xy平面,球面朝观察者方向凸起,z轴正方向指向观察者。

光线从观察者眼睛向图像深处传播,方向可以约定为vec3(0.0, 0.0, -1.0)。当这条光线进入玻璃球时,入射方向与球面法线方向相反,因此可以直接交给GLSL内置的refract函数计算折射方向。下面的内核实现了一次折射近似:只计算光线从空气进入玻璃球后方向的变化,再用这个方向的xy分量去偏移采样坐标。虽然忽略后表面二次折射,但它已经能表现出中心放大、边缘压缩和扭曲的水晶球特征。

kernel vec4 glassBallRefraction(sampler image, vec2 center, float radius, float refractionIndex)
{
    vec2 uv = destCoord();
    vec2 delta = uv - center;
    float dist = length(delta);

    if (dist > radius) {
        return sample(image, samplerCoord(image));
    }

    float z = sqrt(radius * radius - dist * dist);
    vec3 normal = normalize(vec3(delta, z));
    vec3 incident = vec3(0.0, 0.0, -1.0);
    float eta = 1.0 / refractionIndex;
    vec3 refracted = refract(incident, normal, eta);

    float offsetScale = 0.12;
    vec2 sampleCoord = uv + refracted.xy * offsetScale;

    return sample(image, sampleCoord);
}

偏移缩放系数offsetScale是一个经验参数,它控制折射后采样点向外偏移的程度。这个值过大会把采样坐标推到球体外部,导致球内出现明显撕裂或黑色区域;过小则折射感太弱,球体看起来只是轻微扭曲。通常把半径设到图像短边的0.3到0.4之间,折射率取玻璃常见值1.5左右,缩放系数取0.08到0.15可以得到比较自然的视觉效果。球心附近的dist很小,法线接近正z方向,折射偏移量弱;越靠近球体边缘,法线与入射方向的夹角越大,偏移越明显,这正是水晶球四周变形强烈的原因。

如果只保留这一次折射近似,球体中心区域的放大倍率会偏低。真实玻璃球的光线会在进入球体后到达后表面,再经历一次从玻璃到空气的折射。双重折射在球体较厚或折射率较高时能明显增强中心凸起感,但计算量也会增加。对于照片编辑和实时相机滤镜场景,一次折射已经可以在观感上达到合格水平,后续再通过环境映射和菲涅尔反射弥补细节。

二、环境映射与菲涅尔反射叠加

仅有折射采样并不能让玻璃球显得通透,因为真实玻璃球边缘会反射周围环境,这种反射随观察角度变化。直白地说,从正面看球心时反射很弱,视线偏到边缘时反射增强。Core Image中可以使用第二个采样器传入环境图,通过球面法线把环境颜色映射到球体表面。对于二维图像滤镜,取法线的xy分量映射到0到1区间已经足够制造环境反射的错觉。

kernel vec4 glassBallWithFresnel(sampler image, sampler envMap, vec2 center, float radius, float refractionIndex, float envStrength)
{
    vec2 uv = destCoord();
    vec2 delta = uv - center;
    float dist = length(delta);

    if (dist > radius) {
        return sample(image, samplerCoord(image));
    }

    float z = sqrt(radius * radius - dist * dist);
    vec3 normal = normalize(vec3(delta, z));
    vec3 viewDir = vec3(0.0, 0.0, 1.0);
    vec3 incident = vec3(0.0, 0.0, -1.0);
    float eta = 1.0 / refractionIndex;
    vec3 refracted = refract(incident, normal, eta);

    vec2 sampleCoord = uv + refracted.xy * 0.12;
    vec4 refractedColor = sample(image, sampleCoord);

    float fresnel = pow(1.0 - dot(normal, viewDir), 3.0);
    vec2 envCoord = normal.xy * 0.5 + 0.5;
    vec4 envColor = sample(envMap, envCoord);

    return mix(refractedColor, envColor, fresnel * envStrength);
}

这里的菲涅尔项使用pow(1.0 - dot(normal, viewDir), 3.0)近似。球心处法线与观察方向几乎平行,点积接近1,菲涅尔值接近0,折射颜色占主导;球体边缘法线与观察方向夹角增大,菲涅尔值快速上升,环境颜色覆盖上来,形成边缘反射。指数3是一个常用近似,也可以改成5来让反射区域更贴边,或者改小让反射更扩散。

如果没有专门的环境贴图,可以用固定颜色模拟天空反射,例如vec4(0.75, 0.85, 1.0, 1.0)代替envColor。这种方式成本极低,而且在小尺寸球体上很难看出与环境图的差别。若想增加更集中的高光点,可以在菲涅尔基础上继续计算pow(fresnel, 8.0),只保留非常靠近边缘的亮带,让球体轮廓更清晰。

三、Swift封装与实时性能优化

在iOS工程中,可以把内核逻辑封装成CIFilter子类,方便接入相机管线或照片编辑流程。下面的Swift代码使用CIColorKernel加载内核字符串并输出处理后的CIImage。球心、半径、折射率和环境强度都通过属性暴露,外部只需要修改属性并重新读取outputImage即可,不会触发内核重编译。

import CoreImage

class GlassBallRefractionFilter: CIFilter {
    @objc dynamic var inputImage: CIImage?
    @objc dynamic var inputCenter: CIVector?
    @objc dynamic var inputRadius: CGFloat = 120.0
    @objc dynamic var inputRefractionIndex: CGFloat = 1.52
    @objc dynamic var inputEnvironmentImage: CIImage?

    static var kernel: CIColorKernel? = {
        let source = """
        kernel vec4 glassBallWithFresnel(sampler image, sampler envMap, vec2 center, float radius, float refractionIndex, float envStrength)
        {
            vec2 uv = destCoord();
            vec2 delta = uv - center;
            float dist = length(delta);
            if (dist > radius) {
                return sample(image, samplerCoord(image));
            }
            float z = sqrt(radius * radius - dist * dist);
            vec3 normal = normalize(vec3(delta, z));
            vec3 viewDir = vec3(0.0, 0.0, 1.0);
            vec3 incident = vec3(0.0, 0.0, -1.0);
            float eta = 1.0 / refractionIndex;
            vec3 refracted = refract(incident, normal, eta);
            vec2 sampleCoord = uv + refracted.xy * 0.12;
            vec4 refractedColor = sample(image, sampleCoord);
            float fresnel = pow(1.0 - dot(normal, viewDir), 3.0);
            vec2 envCoord = normal.xy * 0.5 + 0.5;
            vec4 envColor = sample(envMap, envCoord);
            return mix(refractedColor, envColor, fresnel * envStrength);
        }
        """
        return CIColorKernel(source: source)
    }()

    override var outputImage: CIImage? {
        guard let inputImage = inputImage else { return nil }
        let center = inputCenter ?? CIVector(x: inputImage.extent.midX, y: inputImage.extent.midY)
        let environment = inputEnvironmentImage ?? inputImage
        let envStrength: CGFloat = 0.65

        return Self.kernel?.apply(
            extent: inputImage.extent,
            arguments: [inputImage, environment, center, inputRadius, inputRefractionIndex, envStrength]
        )
    }
}

实时预览时,这个内核在每个输出像素上执行一次,球外像素直接返回原图,耗时主要集中在球体覆盖区域。如果图像尺寸很大,球体占比又小,最直接的优化方式是先把输入图像裁剪到球体包围盒,再交给内核处理,最后与原始图像合成。但由于Core Image的延迟渲染机制,多一步裁剪和合成也会增加纹理往返开销,所以只有在明显卡顿时才值得这样做。对于相机视频流,通常保持单次全图内核更简单稳定。

如果后续要升级到双重折射,可以在第一次折射得到方向后,求该方向与球体后表面的交点,再对交点处法线执行第二次折射,最后用新的出射方向与背景平面求交来确定采样坐标。这个计算在数学上并不复杂,但对每个球内像素都要多一次平方根和折射函数调用。对于静态照片处理完全可接受,对于实时视频流则需要根据机型表现决定是否开启。无论采用哪种方案,把复杂计算保留在GPU上用CIKernel完成,是Core Image实现这类像素级效果的正确思路。

Core Image球面折射环境映射修改时间:2026-10-03 02:03:26

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