实时薄膜干涉渲染: MatCap 与彩虹色材质的 Unity实现与UE转移
在肥皂泡、甲壳虫外壳、镭射纸等表面,能观察到随视角变化的五彩斑斓的干涉条纹,这在物理上称为“薄膜干涉”。实时渲染中要高效模拟这种效果,不能直接使用复杂的波动光学,而需要借助图形学的技巧。本文就是来讲解基于 MatCap(材质捕捉)与菲涅尔渐变映射的组合方案,能在移动端等性能受限平台上实时产生虹彩质感,但是它有其局限性,要在案例基础上做出一定的改良。
本文主要的技术包括:相机空间法线重映射到视角跟随的高光抓取
菲涅尔效应(NdotV)驱动到一维彩虹渐变图再到模拟光谱干涉
多层叠加 + ACES 色调映射来提升蜡质光泽与色彩动态
一、从光学现象到实时模拟
1、薄膜干涉的物理本质
- 现象:光照射到透明薄膜(就像肥皂膜、油膜、氧化层)时,在薄膜上表面和下表面分别发生反射,两束反射光因光程差产生干涉。不同波长的光在不同角度下干涉增强或相消,从而形成随视角变化的彩色条纹。
- 在渲染中的启示:要实现类似效果,材质颜色必须依赖view-dependent,且色彩应按光谱顺序变化(红→橙→黄→绿→蓝→紫),也可以按照自己喜欢的来。
2、MatCap(材质捕捉)的原理与局限
- 原理:预先渲染一个材质球(或拍摄真实材质球),得到一张圆形光照贴图。在实时渲染时,将模型的相机空间法线的 xy 分量作为 UV 坐标,直接采样这张贴图。因为相机空间法线总是相对于当前视角,所以采样结果会自然跟随视角旋转(但并不会改变高光形状,只是固定贴图映射)。
- 优点:性能极低(一次纹理采样),适合早期移动平台,现在适用的领域不多,但是能快速模拟复杂反射。
- 局限:贴图上的高光区域始终固定在模型“上方”,无法根据表面粗糙度产生菲涅尔边缘效应。相机边缘会出现采样越界(灰色区域),需要额外 clamp。缺少物理上的视角渐变,不适合单独模拟薄膜干涉。
3、 本片文章的技术路线
为了在 MatCap 廉价的基础上增加“视角依赖的色彩变化”,我们就引入菲涅尔渐变映射(又见面了,上篇文章刚刚讲过):
- MatCap 层 : 提供基础的金属质感与动态高光。
- 菲涅尔层 : 计算 N·V,边缘值大,中心值小,用来采样彩虹渐变图。
- 叠加混合 :(漫反射 + MatCap)× 渐变颜色,实现虹彩边缘。
- 后处理 ACES :解决色彩溢出,提升饱和度与动态范围。
二、其中核心数学原理详解
1、法线空间转换:从世界空间到相机空间
(1)为什么需要相机空间法线(上一篇有类似知识点,可以对比看一下)?
- 世界空间法线是固定的,一直是物体本身的方向。
- 在实现效果时希望 MatCap 贴图上的高光点始终随着相机视角旋转(比如你从正面看,高光在中心;从侧面看,高光移到边缘)。
- 相机空间下的法线 (x, y, z),其 xy 分量正是“法线在屏幕平面上的投影”,用它作 UV,就能让贴图区域跟随视角。
// 世界空间法线(归一化) float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 视图矩阵(从世界到相机空间) float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal); // 映射到 [0,1] 范围,作为 UV float2 matcapUV = viewNormal.xy * 0.5 + 0.5;
- UNITY_MATRIX_V 是 Unity 内置的视图矩阵,需要确保在 UnityCG.cginc 包含后可用。
- 实际使用中,由于法线是方向向量,只需用矩阵的 3x3 部分(可以忽略平移)。
2、 菲涅尔效应与 NdotV
- 菲涅尔(Fresnel)效应(自己翻上一篇看一下吧哈哈)
- 计算公式:
float3 viewDir = normalize(_WorldSpaceCameraPos - worldPos); float NdotV = dot(worldNormal, viewDir); float fresnel = 1.0 - NdotV; // 边缘 → 1,中心 → 0
- 在薄膜干涉中,边缘颜色往往更鲜艳,所以直接用 fresnel 驱动渐变映射非常契合。
3、 渐变映射
- 整体的思路就是用一个一维纹理(宽度为 256 或 512,高度为 1)存储彩虹色。采样时,用 float2(fresnel, 0.5) 作为 UV。因为纹理的高度方向恒定,相当于只依赖 X 轴。
- 图像制作:在 Photoshop 中新建 512×512 图像,从左到右依次填充光谱色(红→橙→黄→绿→蓝→紫),按照自己喜欢的来即可,可重复一次来加强周期感。
- 代码采样:
float rampUV = saturate(fresnel * _RampScale); // 可调范围,防止越界 float3 rampColor = tex2D(_RampTex, float2(rampUV, 0.5)).rgb;
- _RampScale 控制色彩变化的速率,比如 2.0 会让边缘更早进入紫色区域。也可使用 1.0 - fresnel 翻转渐变位置。
三、Shader 代码逐步实现(基于 Unity URP / Built-in)
1、顶点着色器的关键计算
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
2 、片元着色器:法线变换与 MatCap 采样
float3 worldNormal = normalize(i.worldNormal); float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos); // 相机空间法线 float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal); float2 matcapUV = viewNormal.xy * 0.5 + 0.5; // MatCap 贴图采样(圆形区域) float3 matcapColor = tex2D(_MatCapTex, matcapUV).rgb; matcapColor *= _MatCapIntensity; // 强度参数,可调至 5~10
注意点:
- MatCap 贴图通常是一张圆形光照图,边缘黑色,中心有高光。采样时若 UV 超出 [0,1],会得到黑色(如果贴图设置为 clamp)或重复采样(repeat),建议使用 tex2D(_MatCapTex, saturate(matcapUV)) 避免越界灰色。
3、 菲涅尔渐变映射代码
float NdotV = dot(worldNormal, viewDir); float fresnel = 1.0 - NdotV; // 边缘为大值 // 控制渐变范围,防止采样超出纹理边缘 float rampPos = saturate(fresnel * _RampScale); float3 rampColor = tex2D(_RampTex, float2(rampPos, 0.5)).rgb; // 可选:增加强度控制 rampColor *= _RampIntensity;
4 、多通道颜色混合
方案 A(叠加法,适合金属感强)
float3 diffuse = tex2D(_MainTex, i.uv).rgb; float3 final = (diffuse + matcapColor) * rampColor;
方案 B(增强高光层,适合蜡质光泽):
float3 final = diffuse * rampColor + matcapColor; // 再额外叠加一个 add 层 float3 addColor = tex2D(_AddTex, matcapUV).rgb * _AddIntensity; final += addColor;
我使用了 makeup 与 add 两层 MatCap,乘法与加法组合,最终通过亮度参数 intensity * 10 强化虹彩表现。
5、 参数面板设计
在 Properties 块中定义:
Properties {
_MainTex ("Diffuse", 2D) = "white" {}
_MatCapTex ("MatCap (RGB)", 2D) = "black" {}
_RampTex ("Ramp (RGB)", 2D) = "rainbow" {}
_MatCapIntensity ("MatCap Intensity", Range(0,10)) = 2.0
_RampScale ("Ramp Scale", Range(0,3)) = 1.0
_RampIntensity ("Ramp Intensity", Range(0,2)) = 1.0
_Saturation ("Final Saturation", Range(0,2)) = 1.0
}
四、后处理增强:ACES 色调映射与色彩校正
1、 为什么需要后处理
- MatCap 和渐变映射叠加后,高光区域极易过曝,色彩饱和度不稳定。
- 色调映射可将高动态范围的色彩压缩到显示器范围,同时保留细节和对比度。
- ACES(Academy Color Encoding System)是一种电影级色调映射曲线,能显著提升画面色彩鲜活度。
2、 在 Unity 中启用后处理(详细步骤如下,如果还是不会就去问问AI或者私信)
步骤(Unity 2020+):
- 导入 Post Processing 包(Package Manager → 搜 Post Processing)。
- 主相机添加 Post-process Layer 组件,Layer 选择 “PostProcess”。
- 场景中新建空物体,添加 Post-process Volume 组件,勾选 Is Global。
- 在该 Volume 中点击 Add Override → Color Grading。将 Mode 设为 ACES。调节 Post-exposure 增加亮度(如 +0.5)。调节 Saturation 微调整体饱和度。
3、代码中内置简易 LUT(可选)
如果你不想依赖后处理包,也可以在 Shader 最后阶段对颜色进行简单校正:
hlsl
final = pow(final, 1/2.2); // Gamma 校正 final = lerp(luminance(final), final, _Saturation);
五、完整代码实现
以下是一个完整的 Unity Unlit Shader,整合了上述所有技术(不含后处理,但可在材质面板调节参数):
Shader "Custom/MatCapFresnelRamp"
{
Properties
{
_MainTex ("Diffuse", 2D) = "white" {}
_MatCapTex ("MatCap", 2D) = "black" {}
_RampTex ("Ramp", 2D) = "rainbow" {}
_MatCapIntensity ("MatCap Intensity", Range(0, 10)) = 2
_RampScale ("Ramp Scale", Range(0, 3)) = 1.2
_RampIntensity ("Ramp Intensity", Range(0, 2)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
sampler2D _MainTex;
sampler2D _MatCapTex;
sampler2D _RampTex;
float _MatCapIntensity;
float _RampScale;
float _RampIntensity;
v2f vert (appdata v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target {
float3 worldNormal = normalize(i.worldNormal);
float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
// MatCap
float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNormal);
float2 matcapUV = viewNormal.xy * 0.5 + 0.5;
float3 matcapColor = tex2D(_MatCapTex, matcapUV).rgb * _MatCapIntensity;
// Fresnel
float NdotV = dot(worldNormal, viewDir);
float fresnel = 1.0 - NdotV;
float rampUV = saturate(fresnel * _RampScale);
float3 rampColor = tex2D(_RampTex, float2(rampUV, 0.5)).rgb * _RampIntensity;
// Diffuse
float3 diffuse = tex2D(_MainTex, i.uv).rgb;
// Combine
float3 final = (diffuse + matcapColor) * rampColor;
return fixed4(final, 1.0);
}
ENDCG
}
}
}
六、补充知识点
1、 MatCap 贴图的制作规范
- 尺寸:建议 512×512 或 1024×1024,圆形区域居中,背景黑色(保证 uv 采样到边缘时是黑色,不影响)。
- 可使用 3D 软件渲染一个标准材质球,采用三点布光,或者手绘卡通高光。
- 如果希望虹彩更强,可以直接在 MatCap 贴图中预置部分彩色,但会牺牲视角变化的自由度。
2、相机边缘 UV 越界修正
MatCap 的固有缺陷是当模型位于屏幕边缘时,相机空间法线的 xy 分量可能超出 [-1,1],导致 UV 超出 [0,1] 范围,产生灰色(因为默认采样超出会返回边界像素,若贴图边缘是黑色则没问题,但若贴图边缘为彩色就会错误)。
修正方法:
float2 matcapUV = viewNormal.xy * 0.5 + 0.5; matcapUV = saturate(matcapUV); // 强制钳位,但会导致边缘高光消失
更佳方法:对越界的 UV 返回纯黑,或使用 tex2Dlod 并设置采样器的 clamp 状态。
3、 性能优化建议
- 使用 half 精度代替 float(法线、方向、颜色均可)。
- 减少纹理采样:如果渐变图可以直接用数学公式生成彩虹色(就像 hsv2rgb(fresnel, 1, 1)),可省去一次纹理读取,但对移动端而言纹理采样通常更快。
- 对于不移动的物体,可以预计算视角相关的 MatCap UV 并存入顶点色,但会损失动态性。
4、 与其他方案的对比(AI总结)
本文的 MatCap+菲涅尔渐变 | 极低(2-3次纹理采样) | 良好,有虹彩边缘 | 移动端、卡通渲染、特效 |
PBR + Clearcoat + 薄膜 BRDF | 高(多次采样+计算) | 物理准确 | PC/主机高品质写实 |
纯纹理动画(不依赖视角) | 极低 | 静态色彩,效果差 | 不推荐 |
5、 扩展:使用厚度纹理模拟复杂干涉
真实薄膜干涉还会受到膜厚分布影响。可以在模型上烘焙一张厚度贴图(Thickness map),替代菲涅尔值作为渐变 UV,这样就能表现不均匀的彩色条纹(比如沾了油污的玻璃等等复杂表现的材质)。代码修改很简单:
hlsl
float thickness = tex2D(_ThicknessMap, i.uv).r; float rampUV = saturate(thickness * _ThicknessScale); float3 rampColor = tex2D(_RampTex, float2(rampUV, 0.5)).rgb;
七、技术局限与改进方向
现有局限:
- MatCap 本身无法模拟真实物理 BRDF,高光形状固定。
- 菲涅尔渐变只是一维的,无法表现多个干涉级次(重复光谱)。
- 相机边缘的采样错误仍需 clamp 解决。
改进优化的方向:
- 升级为 Sphere Mapping:类似 MatCap,但使用纹理包裹方式,减少越界。
- 使用各向异性法线扰动:叠加细节法线贴图,让虹彩边缘更自然。
- 实现双频渐变:将菲涅尔值乘以 2π,并取小数部分,可实现彩虹的重复周期。
- 结合 HDR 渲染:薄膜干涉中高光极亮,可在 HDR 管线中输出超过 1 的颜色,再用后处理 Bloom 增强光晕感。
八、UE5移植概述
将薄膜干涉材质从Unity移植到UE5,核心理念与数学原理高度一致,差异主要体现在引擎的实现方式和工作流程上
- 材质编辑器 vs Shader代码:在Unity中可能需要手写ShaderLab,而在UE5中大部分逻辑通过材质编辑器节点完成。需要复杂逻辑时,可使用Custom节点注入HLSL代码。
- 纹理空间约定:UE5的UV空间原点在左上角(DirectX系),需要对Y值进行翻转处理。
- 色彩空间管线:UE5默认采用ACEScg工作流,无需手动导入后处理包,直接在Post Process Volume启用ACES色调映射即可获得电影级校色效果。
1、场景准备与贴图导入
- 在Content Browser中右键创建Material,命名为M_MatCapFresnel。
- 导入准备好的贴图(T_MatCap光影球贴图、T_Ramp彩虹渐变图及可选的T_Normal法线贴图)。
2、:MatCap核心逻辑构建
- 空间转换:拖入TransformVector节点,Source设为World Space,Destination设为View Space。连线成Normal Vector (View Space)。
- 翻转Y轴:因为UE5的UV原点在左上角,而我们的视图空间原点在左下角,需拖入ComponentMask提取R和G通道,对G通道执行*-1,再用AppendVector重组。
- 范围重映射:原向量范围是[-1,1],UV范围是[0,1]。通过Append和Add (+1)及Divide (/2)节点转换。
- 采样贴图:拖入TextureSample,将Tex设为T_MatCap,UVs连上生成的坐标。
3、菲涅尔渐变逻辑构建
- 菲涅尔节点:拖入菲涅尔节点Fresnel,如需要,可将ExponentIn设为1.0获得线性渐变。输出端会自动给出N·V的计算结果。
- 控制亮度:拖入乘法节点Multiply与一个命名为RampIntensity的ScalarParameter相乘。
- 采样渐变图:拖入TextureSample,Tex设T_Ramp。拖入AppendVector,A端连上强度值,B端输入常数0.5(因为是U向一维采样)。
- 最终叠加:通过Multiply节点将MatCapColor、RampColor与BaseColor (TextureSample)混合。这对应于你的笔记中提到的lamp与diffuse混合。
4、法线贴图
- 拖入TextureSample,Tex设法线贴图(纹理设置中Texture Type必须设为Normal Map),UVs连模型的TextureCoordinate。
- 将采样器输出直接连到材质节点的Normal输入端口。
5、 Custom HLSL优化(可选)
对于需要高强度优化的移动端项目,或需要编写更复杂逻辑(如循环采样高斯模糊)时,使用Custom节点可以将节点网络精简,并提高性能,这是想进大厂进大项目的需要。
- 在材质编辑器中右键,搜索并添加Custom节点。
- 在Details面板中设置:Output Type:占位符Inputs:依次定义MatCapTex,RampTex,ViewNormal等。
- 在Code框中注入HLSL代码(代码示例见下方)。
- UE5会将输入的参数自动映射为HLSL中的变量。
HLSL代码示例:
这段代码将与之前构建的节点网络等效,但封装成了一个高性能的“黑盒”。
cpp
float3 viewNormal = ViewNormalInput; // 输入的相机空间法线 float2 matCapUV = viewNormal.xy * 0.5f + 0.5f; float3 matcapSample = Texture2DSample(MatCapTex, MatCapTexSampler, matCapUV); float ndotv = FresnelInput; float rampPos = saturate( ndotv * FresnelIntensity); float3 rampColor = Texture2DSample(RampTex, RampTexSampler, float2(rampPos, 0.5f)); float3 finalColor = (DiffuseColor + matcapSample) * rampColor; return finalColor;
6、后处理与色彩校正(ACES)
这最后一步能统一画面的色彩风格并提升动态范围。
- 添加后处理: 在场景的Post Process Volume中,找到Color Grading下的Tonemapper。
- 启用ACES: 将Tonemapper中的Film选项设置为ACES。
- 微调参数:Exposure / Gain:非Exposure Compensation,直接提升画面的整体亮度,避免过曝。Saturation:如需进一步浓郁色彩,微调饱和度。
7、材质实例化与参数暴露
从父材质生成多个参数不同的实例,以提升工作效率。
- 标量参数:按住S键鼠标左键创建 ScalarParameter(浮点数滑块),暴露Ramp强度、亮度等。
- 向量参数:按住V键鼠标左键创建 VectorParameter(RGB颜色值)。
- 纹理参数:拖入TextureSampleParameter2D暴露,便于后续直接替换贴图。
8、扩展方案拓展实现
8.1基础版:多层干涉(利用纹理通道)
可在甲壳虫材质案例上叠加多层效果,混合出更丰富的质感。
- 实施方案:增加第二层MatCap(如使用高光更锐利的贴图),通过加法或屏幕混合模式与基础MatCap混合。你的笔记中所记的“Add图层”即是这一思想的体现。
- 效果:模拟昆虫外壳、釉面陶瓷等更复杂的光泽感。
82进阶版:基于厚度的薄膜干涉(基于厚度贴图)
想要实现真实世界油膜那种不均匀的七彩效果(而非仅基于边缘),需要引入基于纹理的模拟。
- 额外需求:一张代表表面“厚度”的灰度贴图 (Thickness Map)。
- 实施方案:获取厚度贴图的R通道强度thickness,替代菲涅尔节点作为渐变贴图的U向坐标。cpp
- 示例:透明玻璃酒瓶上的油污、肥皂泡表面随意的彩色漩涡。
8.3 基于厚度的多层干涉(连续多层叠加)
将“基于厚度的薄膜干涉”进行多层连续叠加,可以更真实地模拟光线在薄膜内多次反射和折射的复杂物理现象。
- 多层光程差累加:在前一层 Color1 = Ramp(thickness * Scale1) 基础上,增加第二层 Color2 = Ramp(thickness * Scale2) 并偏移相位。通过等比数列累加(如 1x, 2x, 4x 倍频),可以得到更丰富的光谱干涉条纹。
- 在材质中采样3到4次厚度值,乘以不同的缩放系数后查表,最后乘以衰减系数累加。公式为:text
最终实现效果就像下面这样,我是直接用ASE链接的,不会的话直接写代码就行
