实时薄膜干涉渲染: 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+):

  1. 导入 Post Processing 包(Package Manager → 搜 Post Processing)。
  2. 主相机添加 Post-process Layer 组件,Layer 选择 “PostProcess”。
  3. 场景中新建空物体,添加 Post-process Volume 组件,勾选 Is Global。
  4. 在该 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;

七、技术局限与改进方向

现有局限

  1. MatCap 本身无法模拟真实物理 BRDF,高光形状固定。
  2. 菲涅尔渐变只是一维的,无法表现多个干涉级次(重复光谱)。
  3. 相机边缘的采样错误仍需 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核心逻辑构建

  1. 空间转换:拖入TransformVector节点,Source设为World Space,Destination设为View Space。连线成Normal Vector (View Space)。
  2. 翻转Y轴:因为UE5的UV原点在左上角,而我们的视图空间原点在左下角,需拖入ComponentMask提取R和G通道,对G通道执行*-1,再用AppendVector重组。
  3. 范围重映射:原向量范围是[-1,1],UV范围是[0,1]。通过Append和Add (+1)及Divide (/2)节点转换。
  4. 采样贴图:拖入TextureSample,将Tex设为T_MatCap,UVs连上生成的坐标。

3、菲涅尔渐变逻辑构建

  1. 菲涅尔节点:拖入菲涅尔节点Fresnel,如需要,可将ExponentIn设为1.0获得线性渐变。输出端会自动给出N·V的计算结果。
  2. 控制亮度:拖入乘法节点Multiply与一个命名为RampIntensity的ScalarParameter相乘。
  3. 采样渐变图:拖入TextureSample,Tex设T_Ramp。拖入AppendVector,A端连上强度值,B端输入常数0.5(因为是U向一维采样)。
  4. 最终叠加:通过Multiply节点将MatCapColor、RampColor与BaseColor (TextureSample)混合。这对应于你的笔记中提到的lamp与diffuse混合。

4、法线贴图

  • 拖入TextureSample,Tex设法线贴图(纹理设置中Texture Type必须设为Normal Map),UVs连模型的TextureCoordinate。
  • 将采样器输出直接连到材质节点的Normal输入端口。

5、 Custom HLSL优化(可选)

对于需要高强度优化的移动端项目,或需要编写更复杂逻辑(如循环采样高斯模糊)时,使用Custom节点可以将节点网络精简,并提高性能,这是想进大厂进大项目的需要。

  1. 在材质编辑器中右键,搜索并添加Custom节点。
  2. 在Details面板中设置:Output Type:占位符Inputs:依次定义MatCapTex,RampTex,ViewNormal等。
  3. 在Code框中注入HLSL代码(代码示例见下方)。
  4. 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)

这最后一步能统一画面的色彩风格并提升动态范围。

  1. 添加后处理: 在场景的Post Process Volume中,找到Color Grading下的Tonemapper。
  2. 启用ACES: 将Tonemapper中的Film选项设置为ACES。
  3. 微调参数: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链接的,不会的话直接写代码就行

全部评论

相关推荐

万物DP:目前可以说没机会了,offer在4月都发完了。把实习用AI改一改投中小厂吧
点赞 评论 收藏
分享
03-26 12:00
已编辑
门头沟学院 Java
offer魅魔_oc...:100-200每天,你还要倒贴100
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务