前阵子在弄一个金属箔纸质感的道具材质,美术给的需求参考图是那种皱巴巴的金箔纸:高光细碎、锐利、分布不均匀,稍微转动视角就哗啦啦地闪,跟普通金属那种一大块连续高光完全不同。当时第一反应是调高金属度、压低粗糙度,结果调了半天,出来的是不锈钢灯具,不是金箔。后来把突破口放到法线贴图上——不是改光照模型,而是直接扭曲法线的采样坐标,让法线在表面上出现高频、不规则的空间扰动,金箔那种“碎金块在灯下乱闪”的质感才真正出来。
这个思路在Unity的SurfaceShader里实现成本很低,效果却非常明显。核心就一句话:用一张噪声/细节贴图去扰动法线贴图采样的UV坐标,让光照方向跟表面微法线之间的关系被打乱,高光自然就碎开了。这篇文章就以SurfaceShader为载体,把法线UV扭曲做金箔纸效果的原理、代码、参数调节、踩坑记录全部讲透,适合正在做材质效果、尤其是需要做金属质感变体的人直接参考。
1. 先弄清楚"金箔纸"画面感的成因
1.1 金箔纸和普通金属高光的不同
正常金属高光的观感是连续的:表面越光滑,高光越集中成一个亮斑;表面越粗糙,高光越散开成一个柔和的带状区域。这是PBR模型里微表面分布决定的,粗糙度决定了微法线在宏观法线周围的散布范围,而法线贴图只能改变宏观方向,不能让微观朝向在相邻像素之间产生快速随机变化。
金箔纸为什么看起来不一样?因为它的微表面不是“均匀粗糙”的,而是被机械压皱、折叠之后形成的密集小平面。每个小平面的朝向跟周围的邻居几乎没有关联,有的朝左,有的朝右,有的几乎垂直朝天。这种强随机性让高光在很小的空间范围内不停切换:这个像素亮一下,旁边像素可能立刻暗掉,转一下视角又全部重新排布。视觉结果就是“闪烁的碎屑感”。
普通PBR金属没法模拟这种效果,因为微表面模型假设粗糙度在一个宏表面范围内是统计均匀的。法线贴图可以模拟朝向,但前提是你在像素级别提供了足够杂乱的法线数据;如果贴图本身的法线变化频率低于屏幕像素密度,高光就会连成片。金箔纸需要的不是更低的粗糙度,而是更高频率、更无序的法线变化。
1.2 法线UV扭曲在这个效果里到底做了什么
这里要引入一个关键认知:金箔纸表面的法线不是静态的,它会因为底层纸张的褶皱、金属箔自身的弯折而在空间上产生偏移。也就是说,同样一张法线贴图,如果你能把它“贴”得歪歪扭扭、这里凸起来那里凹下去,就相当于把原本规整的微法线分布打乱了。
法线UV扭曲做的就是这件事。它不直接改法线数据,而是修改采样法线贴图时的坐标:
float2 newUV = originalUV + distortion; float3 normal = UnpackNormal(tex2D(_BumpMap, newUV));这里的distortion如果是一张高频随机图(噪音、细节法线、Voronoi图等),那采样出来的法线序列就不再是原本贴图里那个有序排列了,而像是把法线贴图“揉皱”了一样。微观法线朝向在屏幕空间快速变化,高光跟着被打散成碎块。
从信号处理角度看,原来一张法线贴图在UV空间里的自相关性是确定的——相邻UV点大概率有相近的法线走向。扭曲之后,自相关性被破坏,变成一个短程无序场。短程无序场最典型的视觉特征就是“表面闪烁但又不失去整体方向”,金箔纸正是这种观感。
这里有个容易误解的地方:很多人以为金箔纸效果是改高光模型改成GGX窄高光、把粗糙度调到极低炒出来的。其实粗糙度极低只会让高光成一个极亮的点,转动视角时是镜面反射的连续滑动,而不是金箔那种随机闪现。真正的高频无秩序来自法线空间的变化速度,不是粗糙度本身。
2. 方案设计:为什么选SurfaceShader,而不是纯手写Shader
2.1 SurfaceShader适合解决什么问题
SurfaceShader是Unity内置渲染管线里为“快速实现受光照材质”而设计的一套着色器模板。它对开发者隐藏了顶点片元细节,只保留一个surf函数,你负责输出表面属性(Albedo、Normal、Metallic、Smoothness等),光照逻辑由Unity根据Standard或StandardSpecular光照模型自动生成。
对金箔纸这个需求来说,SurfaceShader非常合适,原因有三个:
- 法线贴图的解码、平滑、世界坐标转换这些底层细节Unity都处理好了,不用手写
normalFromTangentToWorld矩阵变换。 - PBR光照模型需要金属度、光滑度、反射率等多个通道配合,手写片元着色器得自己把BRDF算一遍,SurfaceShader直接复用Standard模型,调整金属度和光滑度参数就够了。
- 调试场景里要频繁切换光照环境观察高光表现,SurfaceShader默认支持完整光照(主方向光、点光源、反射探针),不用自己维护光照列表循环。
当然它也有局限:SurfaceShader只在内置管线正常工作,URP/HDRP下需要转成Shader Graph或者手写的Lit Shader;而且它生成的代码相对冗余,移动端性能敏感时可能不是最优解。如果是做PC端、主机端、或者内置管线的项目,SurfaceShader完全够用。
2.2 两种"扭曲"路线:扰动物理坐标还是扰动UV坐标
做法线UV扭曲有两条路线,思路完全不同,这里必须分清楚。
路线一:扰动法线贴图的采样UV坐标。这是这篇文章的重点。操作上就是先用一张单位化贴图生成偏移量,把这个偏移加到原来的UV上,再拿新UV去采样法线贴图。好处是效果可控,偏移量的范围可以直接映射到UV的数值,适合在模型UV接缝不明显的材质上使用。
路线二:不碰UV,在输出法线的阶段直接对法线方向做空间扰动。例如采样两张法线贴图,把其中一张的xy分量加到另一张上,然后归一化。这种做法的本质是改变法线方向本身,而不是改变采样位置。它实现简单,但效果通常不够“碎”,因为两张贴图各自仍是连续场,合成的结果只是方向上的叠加,没有破坏各自内部的空间相关性。
金箔纸要的是那种“被打碎、被揉皱”的感觉,所以必须走路线一,让我再强调一次:效果来源不是法线方向变了,而是法线贴图的采样位置发生了局部错位,破坏了法线场的空间连续性。
2.3 贴图资源准备与工具选择
法线UV扭曲需要两类贴图:一类是主法线贴图,负责定义表面整体的微观轮廓;另一类是扰动源贴图,负责生成高频偏移数据。
主法线贴图可以用Substance/Photoshop生成,常规做法是从灰度高度图转成法线图,高度图的细节越多,出来的法线变化频率越高。金箔纸建议使用带细小皱褶纹理的高度图生成,而不是完全平铺的纯噪声法线图。
扰动源贴图的选择更关键。它的质量直接决定了“碎屑感”的真实度。我推荐三种:
| 扰动源类型 | 视觉效果 | 适合场景 |
|---|---|---|
| 灰度噪声(Perlin/Simplex) | 随机但不撕裂,高光碎块大小均匀 | 通用的金箔、磨砂金属 |
| Voronoi碎片图 | 高光分成明显块状区域,块与块之间有边界 | 强烈折痕感的金箔纸、铝箔 |
| 细节法线贴图(Detail Normal) | 自带方向性,扰动看起来有“流动感” | 布纹金属、颗粒压花表面 |
如果手里没有现成的扰动图,直接从Substance里导出一张“碎纸噪声”灰度图,然后转成RGBA通道即可。扰动源贴图采样时要把颜色值减到0.5附近,否则会产生一个恒定向一个方向的UV偏移,导致整个材质看起来像是偏移了而不是扭曲了。
实际生产里,我更推荐直接把扰动源打包进主法线贴图的R和G通道。因为主法线贴图本身通常有A通道存高度信息、G通道存y方向法线,这些通道在渲染时往往被浪费掉;把扰动量塞进去可以减少一次贴图采样,对移动端尤其友好。这个后面参数调优部分会展开。
3. 核心实现:可直接复制的SurfaceShader代码
3.1 基础版:单张扰动图+扭曲主法线
先把最精简的版本贴出来,这个版本只有一张扰动图和一张主法线贴图,它的作用就是验证“扭曲UV”这件事是否成立了:
Shader "Custom/FoilNormalDistortion_Basic" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _BumpMap ("Normal Map", 2D) = "bump" {} _DistortMap ("Distort Map", 2D) = "gray" {} _DistortStrength ("Distort Strength", Range(0, 0.1)) = 0.02 _DistortTiling ("Distort Tiling", Float) = 8 _Metallic ("Metallic", Range(0, 1)) = 0.8 _Smoothness ("Smoothness", Range(0, 1)) = 0.5 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; sampler2D _DistortMap; float _DistortStrength; float _DistortTiling; half _Metallic; half _Smoothness; struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; void surf (Input IN, inout SurfaceOutputStandard o) { // 扰动源的采样坐标:乘tiling把频率提上去 float2 distortUV = IN.uv_BumpMap * _DistortTiling; // 取r通道作为扰动值,减去0.5让偏移可以双向进行 float2 distortion = tex2D(_DistortMap, distortUV).rr - 0.5; distortion *= _DistortStrength; // 关键:用扰动过后的UV去采样法线贴图 float2 finalBumpUV = IN.uv_BumpMap + distortion; float3 normal = UnpackNormal(tex2D(_BumpMap, finalBumpUV)); o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb; o.Normal = normal; o.Metallic = _Metallic; o.Smoothness = _Smoothness; } ENDCG } FallBack "Diffuse" }这套代码能直接在Material上跑起来。把扰动图设成一张高频噪声图,强度先给0.02,把金属度拉到0.8,光滑度0.4左右,基本就能看到那种碎闪效果了。
需要注意一个细节:这里用的是_DistortMap的r通道(.rr),没有把它当成法线贴图去Unpack,因为这里只需要一维的偏移数据,不需要方向信息。如果你用的是detail normal贴图,应该用rg两个通道作为二维偏移向量:
float2 distortion = tex2D(_DistortMap, distortUV).rg - 0.5;这两个写法的差异视觉上还是明显的。用单通道偏移,高光碎块是沿固定方向拉伸的;用双通道偏移,碎块会在二维方向随机扩散,金箔纸会更自然。推荐双通道,用.rg - 0.5。
3.2 进阶版:双法线叠加+细节扰动
基础版的问题在于高频碎屑有了,但表面缺少一个基础的法线轮廓,整体看起来像一张完全随机噪点的金属皮,缺少“箔纸折叠起来的立体感”。所以进阶版加入一张细节法线贴图,同时作为扰动源和细节法线使用:
Shader "Custom/FoilNormalDistortion_Advanced" { Properties { _MainTex ("Albedo", 2D) = "white" {} _BumpMap ("Normal Map", 2D) = "bump" {} _DetailBumpMap ("Detail Normal Map", 2D) = "bump" {} _DistortStrength ("Distort Strength", Range(0, 0.1)) = 0.03 _DetailTiling ("Detail Tiling", Float) = 6 _Metallic ("Metallic", Range(0, 1)) = 0.85 _Smoothness ("Smoothness", Range(0, 1)) = 0.45 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; sampler2D _BumpMap; sampler2D _DetailBumpMap; float _DistortStrength; float _DetailTiling; half _Metallic; half _Smoothness; struct Input { float2 uv_MainTex; float2 uv_BumpMap; float2 uv_DetailBumpMap; }; void surf (Input IN, inout SurfaceOutputStandard o) { // 高频采样细节法线贴图 float2 detailUV = IN.uv_DetailBumpMap * _DetailTiling; float4 detailTex = tex2D(_DetailBumpMap, detailUV); // 从细节法线贴图提取扰动向量(rg通道) float2 distortion = (detailTex.rg - 0.5) * 2.0 * _DistortStrength; // 扰动主法线采样坐标 float2 finalBumpUV = IN.uv_BumpMap + distortion; float3 mainNormal = UnpackNormal(tex2D(_BumpMap, finalBumpUV)); // 把细节法线按一定权重叠加到主法线上 float3 detailNormal = UnpackNormal(detailTex); float3 combinedNormal = normalize(float3( mainNormal.xy + detailNormal.xy * 0.5, mainNormal.z )); o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb; o.Normal = combinedNormal; o.Metallic = _Metallic; o.Smoothness = _Smoothness; } ENDCG } FallBack "Standard" }这个版本在实际项目里更实用。主法线贴图提供大尺度褶皱走向,细节法线贴图通过高频采样提供微观碎屑,同时它的rg通道又被当作扰动源去破坏主法线的采样连续性。一个贴图干了三件事:提供细节法线、生成扰动向量、增强碎屑闪烁。
细节法线叠加的权重0.5不是固定值,它是“细节强度”的简易方案。如果想让碎屑更明显,可以把这个系数提上去到0.7;如果只想让主褶皱更突出,就降到0.2。不过叠加系数太高会导致法线出现禁忌边缘(normal flip),产生一种“凹陷看起来像凸起”的错误视觉,控制在0.5以下比较稳。
3.3 关键代码逐行行为解析
这里把核心三行代码拆开讲清楚,因为很多人不理解为什么“只是改变了UV偏移”就能产生这么剧烈的视觉差异。
第一行:扰动源的采样坐标。
float2 detailUV = IN.uv_DetailBumpMap * _DetailTiling;这里乘_DetailTiling的本质是改变采样频率。比如物体表面一个UV单位在屏幕上对应100个像素,原来细节贴图一个纹素对应1个像素,那乘以8之后,一个纹素对应8个像素,也就是扰动点的空间密度提高了8倍。这个频率要跟屏幕实际像素密度匹配,否则会出现严重的摩尔纹。金箔纸的碎块感很多时候不是强度不够,而是频率不够——碎块的尺寸必须小于你期望的“一块金箔碎片”的尺寸。
第二行:扰动向量的范围控制。
float2 distortion = (detailTex.rg - 0.5) * 2.0 * _DistortStrength;细节法线贴图的rg范围理论上在0到1之间,减去0.5后变成-0.5到0.5,再乘2变成-1到1。这里的2.0是补偿系数,为了让“零扰动”落在中性值的正中间。如果不乘2,同样的_DistortStrength实际扰动幅度会打对折,调参时容易产生“加强度没反应”的错觉。
真正控制扰动范围的变量是_DistortStrength。UV自身的范围是0到1,0.01的强度意味着扰动可以偏移UV的1%,乘以8倍的采样频率后,等效于原始法线贴图里8%幅度的空间偏移。做一个简单的换算:一张2048分辨率的法线贴图,一个纹理像素的实际大小是1/2048≈0.0005个UV单位,那么0.01强度的扰动相当于偏移了约20个纹理像素。这是相当剧烈的变化,足以让相邻像素采样到的法线来自完全不同的位置。所以实际参数给的区间我一般控制在0.005到0.03之间,超过0.05就会让材质看起来像水面波动而不是金属箔。
第三行:扰动后的采样。
float2 finalBumpUV = IN.uv_BumpMap + distortion; float3 mainNormal = UnpackNormal(tex2D(_BumpMap, finalBumpUV));这里如果用原始UV采样,主法线的分布是贴图作者预设好的,空间连续;扰动后UV跳变到邻近甚至更远的纹素,主法线场被打乱。但注意:扰动幅度不能大到让UV点落到法线贴图的完全无关区域,否则面片之间的法线过渡会断裂,出现金属表面被“撕裂”的观感。
4. 参数调优:从"塑料高光"调到"真金箔"
4.1 五个关键参数速查表
做这个效果最容易的误区是只调Smoothness和Metallic,UV扭曲强度随便给一个就完事。下面是控制金箔纸视觉效果最关键的五个参数,我整理成速查表:
| 参数 | 取值范围 | 视觉效果 | 调节策略 |
|---|---|---|---|
_DistortStrength | 0.002 - 0.03 | 碎片尺寸和扰动幅度 | 先从0.01开始,过糊调小,过碎调大 |
_DetailTiling | 3 - 12 | 碎屑密度 | 增加密度时同步减小强度,避免高频噪声刺眼 |
_Smoothness | 0.2 - 0.6 | 高光锐利度 | 越低高光越大越柔和,越高高光越碎越亮 |
_Metallic | 0.6 - 1.0 | 反射强度和金属质感 | 金箔通常0.8以上,低于0.6会偏向丝绸 |
| 扰动源对比度 | - | 碎片边界锐度 | 对比度越高,高光碎块边界越清晰 |
扰动源对比度这个参数不在Shader里,而是在贴图资源本身。如果你用的扰动图是灰度噪声,可以用Photoshop或Substance的Levels工具把对比度拉高,让暗部更暗、亮部更亮。这样产生的扰动向量大部分接近0或接近最大值,中间过渡少,出来的高光碎块会有明确的边界,像真的金属薄片互相压出了棱角。如果扰动图对比度低,扰动向量大量落在中间值,效果会像一层雾。
4.2 典型组合与效果对照
调试的时候可以直接套用下面这几种组合,它们是验证过的基准值:
- 轻微磨砂金:强度0.008,Tiling 4,光滑度0.35,金属度0.7。表面有细闪但不刺眼,适合饰品、雕塑表面。
- 标准金箔纸:强度0.02,Tiling 8,光滑度0.45,金属度0.85。高光碎块明显,转动视角闪烁强烈,最接近参考图里的艺术效果。
- 强烈折痕箔纸:强度0.03,Tiling 12,光滑度0.5,金属度0.95。再加一张Voronoi扰动源,出现大块的高光区块,适合做那种揉成一团再展开的锡箔/铝箔效果。
这里有一个调参的黄金顺序问题:我习惯先定扰动强度和Tiling,再调Smoothness,最后调Metallic。因为前两个参数决定碎屑的形状特征,Smoothness决定高光在碎屑内部的扩散程度,Metallic只影响整体的金属反射底色,放在最后微调不至于互相干扰。如果顺序反过来,经常会出现“看到一个奇怪的高光,不知道是光滑度问题还是扭曲强度问题”的情况。
4.3 处理世界坐标UV时的注意事项
前面所有代码都是基于模型自带的UV坐标。这有一个隐患:如果模型是程序化生成的、或者UV展开了很多个chart,金箔纸效果会在UV接缝处出现明显断裂——原本连续的碎屑分布沿着接缝突然错位。
这时候可以把扰动源的坐标源换成世界坐标。在SurfaceShader里,只需要在Input结构里声明float3 worldPos;,Unity会自动填充世界位置:
struct Input { float2 uv_MainTex; float2 uv_BumpMap; float3 worldPos; }; // 在surf里 float2 worldUV = worldPos.xz * _DetailTiling; float2 distortion = tex2D(_DistortMap, worldUV).rg - 0.5;注意扰动源的采样坐标换成世界坐标后,主法线贴图仍然用模型UV采样。这样扰动信息在世界空间连续,但主法线的褶皱走向仍然跟随模型UV,视觉上既能保持材质贴图设计的褶皱结构,又不会因为UV接缝导致碎屑断裂。
用世界坐标有一个坑:物体本身的缩放会影响worldPos的间隔密度。同一个材质给一个单位立方体和给一个十米的大箱子,采样密度会差十倍。解决办法是把_DetailTiling暴露出来做成材质面板参数,然后在处理不同尺寸物体时手动调整。更工程化的方案是取worldPos时除以物体的LossyScale(注意Batch后没法取非均匀scale),但这样做烘焙时会失真,多数项目就直接按需调Tiling了,所以没有黑科技。
5. 踩坑记录:常见问题与排查实录
5.1 高光糊成一团:扰动看起来完全没生效
这是第一个容易翻车的现象:材质加上去以后,高光还是一整块,完全没有碎。排查步骤按顺序走:
- 先检查
_DistortMap导入设置有没有把Texture Type设为Default,不要设成Normal Map。如果设成Normal Map,Unity在导入时会做线性转Gamma的处理,采样出来的rg会主动往0.5收敛,扰动范围被压缩得几乎为零。 - 检查采样结果是否接近0。可以在surf函数末尾临时输出
o.Albedo = distortion * 1000;来可视化扰动向量。如果画面几乎全黑,说明扰动值没取到;如果画面是灰色的云状噪声,说明值取到了但强度太小。 - 检查
_BumpMap是否真的设置为Normal Map类型。如果主法线贴图设置错误,UnpackNormal会解出错误数据,表现为模型表面一个巨大凹坑的亮暗差异。
5.2 撕裂与边缘恶化:接缝处法线突变
现象是模型UV接缝位置出现一条明显的闪现亮线,或者高光在接缝两侧突然断开。这个问题的根源不是Shader代码本身,而是扰动后的UV越过了原本应该在接缝处“镜像”的边界。UV接缝两侧的纹素本来是孪生关系(一个纹素的坐标被拉伸到另一个chart的连续位置),但扰动向量会把坐标推过头,采样到了完全无关的纹素。
解决方案按推荐程度排序:
- 用世界坐标作为扰动源的坐标,不依赖模型UV。
- 使用Triplanar采样主法线贴图,但实现复杂,SurfaceShader里需要自己写。
- 把扰动强度降低,让扰动向量在UV空间里的位移小于单个UV chart的最小宽度。这种方法只适合UV按等距展开的模型,不适合有机网格。
实际项目里遇到接缝问题,我优先考虑换世界坐标,如果因为物体运动导致世界坐标扰动看起来“漂移”,再考虑在顶点着色器阶段对UV进行预扰动的折中方案——但那样做会丢失像素级频率,效果质量会下降,仅作应急。
5.3 奇怪的闪烁:屏幕空间摩尔纹和时序闪烁
最头大的问题是静态截图看一切正常,一旦相机移动或物体转动,高光就开始一帧帧乱闪,像电视雪花。这是法线高频信息与屏幕像素采样率不匹配引发的混叠失真。
解决手段有两个维度:
- 调低扰动源频率或者降低扰动强度,让碎屑尺寸大于至少2个屏幕像素。如果碎屑小于单个像素大小,就必然出现闪烁,因为每个像素的采样位置在高频噪声场里不断跳变。
- 给扰动源贴图开启Mipmap并且把纹理的过滤模式设为Trilinear,然后利用Texture2D的SampleBias函数在像素密度低时采样更模糊的mip层级。如果不想改代码,直接在材质面板上把扰动强度做成动态LOD:距离摄像机超过N米时逐渐把强度减到原来的1/4,这是生产项目里最常见的优化手段。
还有一个容易被忽略的元凶:细节法线贴图重复时的无缝性。很多噪声图从图库拿来直接用,左右边界不连续,在Tiling大于1时就出现周期性撕裂条纹,表现为远处高光呈现规律性的亮暗波动而不是随机闪烁。判断方法是把细节Tiling调大到50,如果画面出现规则的网格状图案,说明扰动图本身没有做无缝平铺处理,需要换一张可平铺噪声图。
5.4 法线叠加后的“凹陷错觉”
这是细节法线叠加时最容易出现的视觉bug。主法线和细节法线合成时,如果细节法线的xy分量跟主法线的xy分量相反且绝对值接近,合成的法线z分量会变成接近0甚至负值。z分量接近0意味着法线朝切线平面方向,光线掠射会异常,看起来像是表面有深坑;如果z是负值,法线直接穿到曲面下方,就变成了凹陷。
常规做法里float3(mainNormal.xy + detailNormal.xy * 0.5, mainNormal.z)之所以用主法线的z分量而不是重新归一化计算z,就是为了避免z被减掉。千万不要图省事写成normalize(mainNormal + detailNormal * 0.5),那样等于把z分量也做了加法,在强扰动下翻转概率大增。这点很多教程不会提,但实际调试时能少走半天弯路。
6. 扩展思路:这套法线扭曲打法还能用在哪些地方
6.1 从金箔纸扩展到其他材质变体
这个方法本质上是“破坏微法线空间连续性”的通用工具,不只是金箔纸能用。把扰动源从随机噪声换成对应纹理的结构图,就能泛化出很多材质。
比如使用织物纱线方向图作为扰动源,同样强度的UV扭曲能把光滑皮革变成有织纹的帆布质感;把扰动强度调低到0.003,配合微纹理图,能让塑料表面多出一层细腻颗粒,而不是简单依赖粗糙度贴图。我曾经在一个演示项目里,用一张布料折痕法线贴图作为扰动源,对一张标准金属法线贴图做扭曲,结果出了一个非常有说服力的碳纤维质感。
从实现原理上理解,这套方法还能跟Tessellation结合。通过顶点位移加表面法线扰动,金箔纸能更进一步做出物理起伏;但SurfaceShader里做Tessellation需要额外写hull和domain shader,已经超出普通SurfaceShader的范围,调研时要注意成本。
6.2 如果项目迁移到URP,这版代码怎么改
URP用的是Lit Shader,SurfaceShader在这个管线里已经不工作了。迁移思路分两种:
- 保留效果流程,用Shader Graph重建:在顶点片元阶段把同样的操作放到Custom Function节点里,输入法线贴图坐标和扰动强度,输出扭曲后的法线,再接到Material中。Shader Graph的好处是可视化调整容易,但需要注意Unity的Shader Graph里
UnpackNormal节点有内置Normal Strength,需要绕开默认强度单独控制,否则叠加细节法线会双重缩放。 - 手写HLSL Shader,替换
surface surf Standard为Pass中自定义光照。这时需要自己算BRDF或者引用URP的Lit函数,工作量比SurfaceShader大不少,但换来的是对采样次数和变量精度的完全控制。性能敏感场景推荐这条路。
6.3 我再分享一个实用的细节
做金箔纸这类材质时,一定记得把采样噪声压到half精度而不是默认的float。移动端GPU的半精度运算比全精度快一倍,在大量高频采样的shader里,性能差距肉眼可见。我在实际项目里把struct Input里的float2 uv改成half2,_DistortStrength改成half,其他不动,一次改动省下了大概0.2ms的GPU耗时,视觉上没有任何可感知差异。
另外,如果你用的是基础版那套代码,建议把扰动向量的计算放到顶点着色器里预计算一次吗?答案是不建议。UV扰动必须保持在像素着色器里逐像素执行,才能在屏幕空间产生足够高频的碎屑变化。如果在顶点着色器预计算,扰动向量会被线性插值平滑掉,金箔纸效果等于被强制开了一个低通滤波,碎屑全变成大块亮斑,那就不是金箔纸而是珠光漆了。这个坑我亲身踩过,当时为了省几次贴图采样把计算上移到顶点阶段,结果效果立刻垮掉,不得不改回来。
最后提醒一下,法线UV扭曲虽然能在视觉上模拟出金箔的随机闪动,但它不会改变物体的轮廓阴影——金箔纸真的在光照下,边缘和凹陷处会因为几何起伏产生自阴影,而在平面上用普通Mesh做出来的金箔纸效果永远只有高光变化,没有几何起伏。要做出连轮廓都皱巴巴的金箔纸,还需要配合顶点位移或者置换贴图,等到模型细分和Tessellation就绪之后,再让这套法线扰动方案去叠加细节,才算完整的金箔纸材质方案。