Unity Shader 用 Ramp 贴图实现薄膜干涉效果

从物理光学原理出发,通过 NdotV 驱动彩虹 Ramp 贴图采样,

在 Unity Shader 中还原肥皂泡、彩色金属镀层等薄膜干涉现象。

什么是薄膜干涉?

薄膜干涉(Thin Film Interference)是光在纳米级薄膜 两个界面之间反射后叠加而产生的干涉现象。 当光照射到厚度接近光的波长(几百纳米)的薄膜时,一部分光在膜的上表面反射,另一部分透过薄膜后在下表面反射------ 这两束反射光的光程差决定了它们是相互加强还是相互抵消,从而产生随观察角度变化的彩色条纹。

日常生活中随处可见薄膜干涉现象:肥皂泡的七彩光晕、水面油膜的彩色花纹、CD 光盘的彩虹光泽、 蝴蝶翅膀的结构色,以及各类 PVD 镀膜工艺产生的金属彩色效果。

光程差公式为 Δ = 2n₁d·cosθ ,其中 n₁ 是薄膜折射率,d 是膜厚,θ 是光在薄膜内的折射角。 观察角度(即视线与法线的夹角)越大,cosθ 越小,光程差越小,对应不同波长的光发生加强干涉------ 这正是为什么边缘处颜色与中心不同的原因。

💡

在实时渲染中,我们无法逐像素模拟真实的波动光学计算。 核心技巧是:用 NdotV(法线与视线夹角的余弦值)来近似模拟观察角度变化, 再将该值作为 UV 坐标采样一张预制的彩虹渐变 Ramp 贴图,即可得到逼真的薄膜干涉颜色。

Ramp 贴图:预计算的彩虹光谱

Ramp 贴图(渐变贴图/斜坡贴图)是一张横向的渐变纹理,通常分辨率很低(如 256×1 或 256×4), 其核心作用是将单个 0~1 的数值映射为丰富的颜色输出 。 在薄膜干涉效果中,我们用 NdotV 作为 U 坐标采样这张彩虹渐变贴图。

薄膜干涉 Ramp 贴图的颜色设计

Ramp 贴图的颜色设计可以参考以下策略:

1

模拟真实薄膜干涉颜色序列

按照干涉条纹的物理颜色顺序排列:黑 → 白 → 黄 → 橙红 → 紫蓝 → 青绿,形成"牛顿环"色序。

2

彩虹全光谱循环

将可见光全光谱(红橙黄绿青蓝紫)首尾相连做成循环渐变,配合偏移量 Shader 参数调节相位,适合炫彩金属镀膜效果。

3

风格化设计

不必完全写实,可以根据美术风格设计特定的 2~3 色渐变,如深海蓝到粉红,模拟鱼鳞或甲壳虫翅膀的金属光泽。

⚠️

制作 Ramp 贴图时,在 Unity 中将纹理的 Wrap Mode 设为 Clamp (而非 Repeat), 避免边缘采样出现错误颜色;同时将 Filter Mode 设为 Bilinear 确保颜色平滑过渡。

NdotV:观察角度的数学表达

NdotV(N dot V,法线与视线方向的点积)是菲涅尔效果、边缘光、薄膜干涉等众多视角相关效果的核心量。 其计算极为简单:

NdotV = saturate(dot(normalWS, viewDirWS))

// normalWS:世界空间法线(单位向量)

// viewDirWS:世界空间观察方向(单位向量,指向摄像机)

// saturate 将结果钳制到 [0, 1] 区间

NdotV = 1 时,视线正对法线(正面),NdotV = 0 时,视线与表面平行(边缘掠射角)。 下图展示了不同 NdotV 值对应的球体位置及其采样到的薄膜颜色:

为什么不用角度而用点积?

点积计算比反余弦更廉价,且 GPU 原生支持。由于 Ramp 贴图是非线性的渐变, 即使 NdotV 是 cosθ 而非 θ 本身,视觉上也没有问题------实际上, Ramp 贴图的设计本身就可以补偿这种非线性,甚至利用它制作更好看的颜色分布。

完整 Shader 代码

下面给出一个基于 Unity Built-in 管线(Surface Shader 变体)和 URP(HLSL)两个版本的实现。 两者核心算法完全相同,差别仅在于渲染管线的接口写法。

版本一:Built-in 管线 Surface Shader

cs 复制代码
Shader "Custom/ThinFilmInterference"
{
    Properties
    {
        _MainTex        ("Albedo Texture", 2D)     = "white" {}
        _RampTex        ("Film Ramp Texture", 2D) = "white" {}
        _BaseColor      ("Base Color", Color)   = (1,1,1,1)
        _FilmStrength   ("Film Strength", Range(0,1))  = 0.8
        _FilmOffset     ("Film UV Offset", Range(0,1)) = 0.0
        _FresnelPow     ("Fresnel Power", Range(0.1,5))= 1.0
        _Smoothness     ("Smoothness", Range(0,1))  = 0.9
        _Metallic       ("Metallic", Range(0,1))   = 0.5
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }

        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _RampTex;
        fixed4    _BaseColor;
        float     _FilmStrength;
        float     _FilmOffset;
        float     _FresnelPow;
        half      _Smoothness;
        half      _Metallic;

        struct Input
        {
            float2 uv_MainTex;
            float3 viewDir;     // Unity 自动填充:世界空间视线方向
            float3 worldNormal; // 世界空间法线
            INTERNAL_DATA
        };

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // 1. 基础纹理颜色
            fixed4 albedo = tex2D(_MainTex, IN.uv_MainTex) * _BaseColor;

            // 2. 计算 NdotV(法线与视线夹角余弦)
            float3 N = WorldNormalVector(IN, o.Normal);
            float3 V = normalize(IN.viewDir);
            float  NdotV = saturate(dot(N, V));

            // 3. 菲涅尔幂次调整(控制颜色在表面上的分布范围)
            float fresnel = pow(NdotV, _FresnelPow);

            // 4. 加入相位偏移,让用户动态调节起始颜色
            float rampU = frac(fresnel + _FilmOffset);

            // 5. 采样 Ramp 贴图(使用固定 V=0.5,水平方向是渐变方向)
            fixed4 filmColor = tex2D(_RampTex, float2(rampU, 0.5));

            // 6. 将薄膜颜色叠加到 Albedo(乘以强度控制)
            o.Albedo     = lerp(albedo.rgb, albedo.rgb * filmColor.rgb * 2.0, _FilmStrength);
            o.Emission   = filmColor.rgb * _FilmStrength * 0.3; // 少量自发光增强通透感
            o.Metallic   = _Metallic;
            o.Smoothness = _Smoothness;
            o.Alpha      = albedo.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

版本二:URP Unlit Shader(HLSL)

cs 复制代码
Shader "Custom/URP/ThinFilmInterference"
{
    Properties
    {
        _MainTex      ("Albedo", 2D)     = "white" {}
        _RampTex      ("Film Ramp", 2D) = "white" {}
        _FilmStrength ("Film Strength",  Range(0,1))  = 0.8
        _FilmOffset   ("Film UV Offset", Range(0,1))  = 0.0
        _FresnelPow   ("Fresnel Power",  Range(0.1,5))= 1.0
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType"     = "Opaque"
            "Queue"          = "Geometry"
        }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            TEXTURE2D(_MainTex);   SAMPLER(sampler_MainTex);
            TEXTURE2D(_RampTex);   SAMPLER(sampler_RampTex);

            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float  _FilmStrength;
                float  _FilmOffset;
                float  _FresnelPow;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
                float3 normalWS    : TEXCOORD1;
                float3 positionWS  : TEXCOORD2;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs posInputs  = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs   normInputs = GetVertexNormalInputs(IN.normalOS);

                OUT.positionHCS = posInputs.positionCS;
                OUT.positionWS  = posInputs.positionWS;
                OUT.normalWS    = normInputs.normalWS;
                OUT.uv          = TRANSFORM_TEX(IN.uv, _MainTex);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                // 基础颜色
                half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);

                // 计算世界空间向量
                float3 N     = normalize(IN.normalWS);
                float3 V     = GetWorldSpaceNormalizeViewDir(IN.positionWS);
                float  NdotV = saturate(dot(N, V));

                // 菲涅尔 + 偏移
                float rampU = frac(pow(NdotV, _FresnelPow) + _FilmOffset);

                // 采样薄膜 Ramp 贴图
                half4 filmColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(rampU, 0.5));

                // 叠加薄膜颜色
                half3 finalColor = lerp(
                    albedo.rgb,
                    albedo.rgb * filmColor.rgb * 1.8,
                    _FilmStrength
                );

                return half4(finalColor, 1.0);
            }
            ENDHLSL
        }
    }
}

Shader 参数说明

参数 类型 说明 推荐值
_RampTex Texture2D 彩虹渐变 Ramp 贴图,水平方向为颜色渐变,需设置 Wrap Mode = Clamp 256×4 PNG
_FilmStrength Range(0,1) 薄膜颜色的混合强度,0 = 无效果,1 = 完全薄膜颜色 0.6 ~ 0.9
_FilmOffset Range(0,1) Ramp UV 偏移,改变起始颜色相位,可用于动画驱动 0.0 ~ 1.0
_FresnelPow Range(0.1,5) 菲涅尔幂次,控制颜色边缘聚集程度,值越大颜色越集中在边缘 1.0 ~ 2.0
_Smoothness Range(0,1) 表面光滑度,影响高光大小,薄膜材质通常较高 0.85 ~ 0.95
_Metallic Range(0,1) 金属度,影响反射颜色,金属镀膜设高,肥皂泡设低 0.3 ~ 0.8

进阶技巧与优化

技巧一:多层薄膜叠加(双 Ramp)

真实薄膜干涉往往有多层结构。可以准备两张不同相位的 Ramp 贴图,分别采样后用加法或屏幕混合叠加, 模拟更复杂的干涉条纹,如蝶翅的多层薄膜结构色。

cs 复制代码
// 双层薄膜叠加
float rampU1 = frac(pow(NdotV, _FresnelPow) + _FilmOffset);
float rampU2 = frac(pow(NdotV, _FresnelPow * 1.5) + _FilmOffset2);

half4 film1 = SAMPLE_TEXTURE2D(_RampTex,  sampler_RampTex,  float2(rampU1, 0.5));
half4 film2 = SAMPLE_TEXTURE2D(_RampTex2, sampler_RampTex2, float2(rampU2, 0.5));

// Screen 混合模式:1-(1-a)*(1-b),颜色更亮且饱和
half3 filmBlended = 1.0 - (1.0 - film1.rgb) * (1.0 - film2.rgb);

技巧二:_Time 驱动动态流动

_FilmOffset 用 Unity 内置的 _Time.y 驱动,即可实现颜色随时间缓慢流动的动态效果,非常适合宝石、魔法能量体等特效。

cs 复制代码
// 在 CBUFFER 中添加速度参数
float _FilmFlowSpeed;

// frag shader 中
float timeOffset = _Time.y * _FilmFlowSpeed;
float rampU     = frac(pow(NdotV, _FresnelPow) + _FilmOffset + timeOffset);
half4 filmColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(rampU, 0.5));

技巧三:法线贴图扰动

加入法线贴图后,表面细节会造成法线方向的随机扰动,NdotV 也随之变化, 从而让薄膜颜色在曲面上更自然地流动与变化,避免颜色过于均匀。

cs 复制代码
// 法线贴图采样(切线空间 → 世界空间)
float3 normalTS  = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv));
float3 normalWS  = TransformTangentToWorld(
                      normalTS,
                      half3x3(IN.tangentWS, IN.bitangentWS, IN.normalWS));
float3 N         = normalize(normalWS);

// 后续使用扰动后的 N 计算 NdotV
float NdotV     = saturate(dot(N, V));

技巧四:Shader Graph 可视化实现

在 Shader Graph 中,操作步骤完全一致:Normal Vector → View Direction → Dot Product → Power → Add(加偏移)→ Sample Texture 2D(_RampTex),将输出颜色接到 Emission 或 Base Color 即可。

效果对比展示

下图展示了使用标准 PBR 材质与添加薄膜干涉效果后的视觉对比:

不同 Ramp 贴图的风格对比

如何制作 Ramp 贴图

方法一:在 Photoshop / GIMP 中手绘

新建 256×4 像素的画布,用渐变工具绘制从左到右的彩虹渐变(红→橙→黄→绿→青→蓝→紫→红), 导出为无压缩 PNG。在 Unity 中导入后,设置 Wrap Mode = Clamp,Filter Mode = Bilinear, 关闭 Generate Mip Maps。

方法二:在 Unity Editor 中用脚本生成

cs 复制代码
using UnityEngine;
using UnityEditor;

public class ThinFilmRampGenerator
{
    [MenuItem("Tools/Generate ThinFilm Ramp Texture")]
    static void GenerateRamp()
    {
        int width  = 256;
        int height = 4;

        Texture2D ramp = new Texture2D(width, height,
                                     TextureFormat.RGBA32, false);
        ramp.wrapMode   = TextureWrapMode.Clamp;
        ramp.filterMode = FilterMode.Bilinear;

        for (int x = 0; x < width; x++)
        {
            // 将 x 映射到 Hue (0~1 循环彩虹)
            float hue   = (float)x / width;
            Color  color = Color.HSVToRGB(hue, 0.85f, 1.0f);

            for (int y = 0; y < height; y++)
                ramp.SetPixel(x, y, color);
        }

        ramp.Apply();

        // 保存为 PNG
        byte[] png  = ramp.EncodeToPNG();
        string path = "Assets/Textures/ThinFilmRamp.png";
        System.IO.File.WriteAllBytes(path, png);

        AssetDatabase.Refresh();
        Debug.Log($"Ramp 贴图已生成:{path}");
    }
}

💡

脚本运行后在 Unity 菜单栏选择 Tools → Generate ThinFilm Ramp Texture , 即可自动在 Assets/Textures/ 目录生成一张彩虹渐变 Ramp 贴图,拖拽到 Shader 的 _RampTex 槽即可使用。

适用的材质与应用场景

薄膜干涉 Shader 的适用范围极为广泛,以下是常见的游戏和影视应用场景:

总结

使用 Ramp 贴图实现薄膜干涉是一个 高性价比 的渲染技巧------ 仅需一次额外的贴图采样,即可显著提升材质的视觉丰富度和物理真实感。 在此基础上,可以进一步扩展为双层薄膜叠加、法线扰动、时间驱动动画等效果, 满足从写实渲染到风格化卡通的各类需求。Ramp 贴图的可设计性使得艺术家可以不依赖程序, 自由定制干涉颜色风格,是技术美术实践中的经典范式之一。

相关推荐
摄影图1 天前
科技企业研发宣传图片素材 适配多场景宣传使用需求
大数据·人工智能·科技·aigc·贴图·插画
为你写首诗ge1 天前
【Unity知识分享】Mirror实现房间等待功能(创建房间 / 搜索房间、加入房间、房间准备、房间内角色设置、返回房间)
unity·mirror·房间等待功能
游乐码1 天前
Unity坦克案例疑难记录(二)
unity·游戏引擎
摄影图1 天前
AI设计实用图片素材 适配多元创作推广需求
人工智能·科技·智能手机·aigc·贴图
小白学鸿蒙1 天前
Funplay Unity MCP 接入 trae 实战
unity·游戏引擎·mcp
相信神话20211 天前
3.5《酒魂》体验与失败设计
游戏引擎·godot·godot4
游乐码1 天前
Unity基础(一)游戏中的数学Mathf函数
游戏·unity·游戏引擎
地狱为王2 天前
Unity实现猫脸关键点检测
unity·游戏引擎·猫脸关键点检测
598866753@qq.com2 天前
Unity Job System笔记
unity
winlife_2 天前
Funplay Unity MCP 与 Unity AI Assistant 详细对比:开源 MCP 工具集 vs 官方全栈 AI 产品
人工智能·unity·开源·ai编程·claude·mcp