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