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 贴图的可设计性使得艺术家可以不依赖程序, 自由定制干涉颜色风格,是技术美术实践中的经典范式之一。

相关推荐
魔士于安2 小时前
Unity星球资源,八大星球,带fps显示
游戏·unity·游戏引擎·贴图·模型
张老师带你学4 小时前
unity资源,深空陨石,适合太空背景的游戏开发
游戏·unity·模型
鹿野素材屋6 小时前
Unity动画幅度太大怎么办
unity·游戏引擎
垂葛酒肝汤7 小时前
Unity Sprite Rect 越界问题笔记
笔记·unity·游戏引擎
平行云7 小时前
数字孪生信创云渲染系列(一):混合信创与全国产化架构
unity·ue5·3dsmax·webgl·gpu算力·实时云渲染·像素流送
废嘉在线抓狂.8 小时前
Unity拓展关于阵列物品生成以及物品替换
unity·游戏引擎
电子云与长程纠缠9 小时前
Godot学习01 - HelloWorld
学习·游戏引擎·godot
mxwin9 小时前
Unity Shader · UV 技术 用 UV 坐标打造水波涟漪效果
unity·游戏引擎·shader·uv
野奔在山外的猫18 小时前
【解决】IndexOutOfRangeException: renderPassIndex
unity