Unity URP丝绸材质各向异性高光实现与Shader详解

发布时间:2026/9/19 8:18:41
Unity URP丝绸材质各向异性高光实现与Shader详解 做Unity渲染的朋友多多少少都遇到过这种尴尬美术给过来的丝绸面料放到场景里怎么调都像塑料桌布粗糙度拉到最低也只是把高光调亮完全出不来那种一缕一缕的反光。老实说这不是美术参数没调好而是默认PBR模型的一个先天短板——它假设表面微结构是各向同性的而丝绸恰恰是典型的各向异性材质。今天这篇就从思路到落地把Unity里丝绸材质那套“拉丝高光”完整讲一遍包括底层的BRDF原理、Shader实现、参数调整以及我踩过的坑尽量让没有写过自定义渲染管线的朋友也能跟下来。这套方案适合谁如果你在做服装展示、虚拟试穿、数字人、CG过场或者只是想把场景里某块布料做得更像样一点都可以直接参考。我会采用URPUniversal Render Pipeline作为主环境手写HLSL自定义Shader同时也会提一下Shader Graph的对应思路方便不同习惯的开发者上手。1. 先从物理聊起丝绸高光为什么是“拉丝”的1.1 各向异性到底是个啥在PBR体系里我们通常把物体表面想象成无数个微小平面组成的这就是微表面模型。光打到表面上每个微表面会各自反射光线最终我们看到的高光就是这些微表面反射结果叠加在一起的样子。如果这些微表面像沙滩上的沙子一样杂乱无章地排列那么高光反射就会向各个方向均匀散开得到一个圆形、柔和的光斑。这种表面就是我们说的“各向同性”默认PBR里的GGX高光就是这个假设。但丝绸完全不是这样。丝绸表面本质上是一根根极细的、朝同一个方向排列的纤维每根纤维又近似一个小圆柱体。圆柱体表面的法线是绕圆周连续变化的当一束平行光打上去反射方向会沿着圆柱面的“圆周方向”散开同时沿着纤维轴向保持比较强的连续性。结果就是高光不再是一个圆形光斑而是被拉成一条垂直于纤维走向的长条也就是丝绸上那种肉眼能看到的一缕一缕亮光。我用个生活化的类比你拿一块普通不锈钢镜面板和一块拉丝金属板做对比普通镜面反射的灯光是圆形清晰光斑拉丝板的反射光会沿拉丝方向被拉长成一条亮线。丝绸跟拉丝板的原理类似只是纤维更细、更密集高光更有“丝感”。1.2 从Blinn-Phong到PBR高光模型怎么演化先看传统的高光模型。Blinn-Phong假设高光分布只跟法线N和半向量H的夹角有关公式就是常见的pow(dot(N, H), glossiness)。它没有任何方向概念光线反射出来永远是圆形分布。放到丝绸上效果就是高光在丝绸表面转来转去都是圆的看起来更像塑料。后来图形学社区提出了专门处理各向异性高光的模型最经典的是Kajiya-Kay模型。它的核心不是看N和H的夹角而是看切线方向T和半向量H的关系。因为圆柱面沿轴向的法线变化最剧烈所以用切线方向去计算高光分布就能得到沿纤维方向拉伸的细长高光。到了PBR时代做法更精细。GGX微表面分布本身也支持各向异性扩展思路是把原本单一的各向同性粗糙度alpha拆成沿切线方向的alphaX和沿副切线方向的alphaY。当这两个值相等时各向异性GGX会退化回普通GGX。当它们不一样的时候高光分布就会在某个方向上被拉长或者压缩。所以你要理解的核心是各向异性高光不是额外加一个贴花或者发光特效而是把BRDF里的高光分布函数换成支持方向差异的版本。搞明白这件事后面读代码就有底了。2. 动手前的准备渲染管线与方案选型2.1 渲染管线确认URP、HDRP还是内置管线Unity里做自定义Shader第一件事是确认项目用的渲染管线因为Shader的语法、Pass标签、包含文件全都跟着管线走。内置管线的Surface Shader写法相对简单很多老教程都在用但它在URP里完全不可用。如果你的项目是新建的或者已经用了URP建议直接写URP的HLSL Shader或者用Shader Graph。HDRP当然也能做各向异性材质它的Lit Shader本身就自带Anisotropy选项等于帮我们做了一半工作。但HDRP对硬件要求高移动端基本不考虑而且自定义渲染管线的代码复杂得多。我个人推荐在URP环境下操作移动端和PC端都能跑性价比最高。Unity版本方面建议用Unity 2021.3 LTS或更新的版本对应URP 12以上。老版URP的API接口有差异部分内置函数名对不上会绕一些弯路。2.2 手写Shader与Shader Graph的取舍很多同学纠结到底用Shader Graph还是手写HLSL。我的建议是分情况如果你主要做美术不太熟悉代码Shader Graph确实能快速出效果。你可以在Graph里拼一个Custom Function节点把各向异性高光算法写成一个HLSL文件挂进去这样既能可视化调节其他参数又不用从零搭整套BRDF。但Shader Graph对整个Pass的掌控力偏弱比如你想灵活控制SRP Batcher的兼容性、或者按平台分支优化就不太方便。如果你是技术美术或者程序建议直接手写HLSL。原因很直接你能看到每一行代码出问题时能断点调试移动端性能有问题时能逐行优化。而且手写Shader对理解渲染管线有不可替代的帮助这次写完丝绸下次做其他自定义材质就会快很多。素材准备方面除了常规的Albedo贴图、法线贴图还需要准备一张“方向控制贴图”。这张贴图的核心作用是告诉Shader每个像素的纤维朝哪个方向排列。如果没有单独的切线贴图也可以用UV的U方向近似表示纤维走向但遇到UV有扭曲或者镜像时高光方向就会出错。这个问题我在第五节会详细说。3. 核心实现把各向异性高光写进PBR管线3.1 哪里有“方向”切线空间的准备工作各向异性高光离不开切线方向这个方向在Shader里通常用顶点切线T来表示。Unity在导入模型时会自动为每个顶点计算切线所以理论上你Mesh里有tangent顶点着色器就能拿到。但这里有个坑如果模型是从某些软件导出的或者由程序化生成的切线可能缺失或方向翻转。你可以用Mesh.RecalculateTangents()强制重算也可以在Shader里从UV导数推导切线。前者适合编辑器处理后者适合运行时动态网格。我的做法是在顶点着色器里先把切线变换到世界空间float3 worldTangent TransformObjectToWorldDir(v.tangent.xyz);注意Unity的顶点切线是一个float4w分量表示副切线方向1或-1一般在计算副切线时需要用到。如果你只用各向异性高光不计算副切线w可以暂时忽略但后续如果想完整接入各向异性GGX副切线方向是少不了的。如果材质是布料UV的U方向通常对应织物的纬向V方向对应经向。你会发现大部分丝绸材质高光应该沿V方向拉长也就是织物纤维的垂直走向。你可以用Texture的Tiling和Offset去对UV但在Shader里建议直接暴露一个“各向异性角度”参数方便美术微调。3.2 核心算法Kajiya-Kay高光与各向异性GGX我先用Kajiya-Kay模型做一层基础高光因为它容易理解调参也直观。原理是已知切线方向T和半向量HH在T方向的分量越小说明H越接近“垂直”于纤维轴向此时高光沿纤维方向的散射最强。所以先用点积计算float TdotH dot(worldTangent, halfDir); float sinT sqrt(1 - TdotH * TdotH);再用sinT做幂次运算float spec pow(sinT, _Shininess);这个spec就是各向异性高光的亮度因子。当_Shininess很大时高光会被压缩成一条细线当_Shininess较小时高光会铺开。Kajiya-Kay模型像一个“方向版Blinn-Phong”优点是直观缺点是物理精度一般。如果追求更好的效果可以换成各向异性GGX。它的做法是把粗糙度按切线方向拆成两个值一个控制沿切线分布一个控制沿副切线分布。计算时需要先求出H在切线和副切线上的投影然后代入GGX分布函数。从视觉上看各向异性GGX在低粗糙度下表现更柔和丝绸那种细腻的丝光感更自然。关于参数范围我做丝绸材质时的经验是_Anisotropy各向异性强度在0.3到0.6之间视觉最自然超过0.8会变成类似金属拉丝的效果丝绸没那么“硬”_Shininess或者roughness的取值范围按URP的PBR常规用光滑度0.6到0.9太低了会显得像湿抹布太高了则像硬塑料。3.3 完整Shader示例URP下的丝绸材质下面是一个我实际用过的浓缩版各向异性丝绸Shader基于URP的Lit模板改的。它包含主纹理采样、法线贴图、Glossiness、各向异性强度、高光颜色的控制核心高光用Kajiya-Kay计算便于新手理解。Shader Custom/AnisotropicSilk { Properties { _BaseMap (Albedo, 2D) white {} _NormalMap (Normal Map, 2D) bump {} _Glossiness (Smoothness, Range(0,1)) 0.7 _Anisotropy (Anisotropy, Range(-1,1)) 0.4 _SpecColor (Specular Color, Color) (1,1,1,1) _FresnelPower (Fresnel Power, Range(0,10)) 5 } SubShader { Tags { RenderPipelineUniversalPipeline QueueTransparent RenderTypeTransparent } Pass { Tags { LightModeUniversalForward } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 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 struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float4 tangentWS : TEXCOORD3; }; TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap); CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float4 _NormalMap_ST; float _Glossiness; float _Anisotropy; float4 _SpecColor; float _FresnelPower; CBUFFER_END Varyings vert(Attributes v) { Varyings o (Varyings)0; o.positionWS TransformObjectToWorld(v.positionOS.xyz); o.positionCS TransformWorldToHClip(o.positionWS); o.uv TRANSFORM_TEX(v.uv, _BaseMap); o.normalWS TransformObjectToWorldNormal(v.normalOS); o.tangentWS float4(TransformObjectToWorldDir(v.tangentOS.xyz), v.tangentOS.w); return o; } half4 frag(Varyings i) : SV_Target { half4 albedo SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); half3 normalWS normalize(i.normalWS); half3 tangentWS normalize(i.tangentWS.xyz); Light mainLight GetMainLight(); half3 lightDir normalize(mainLight.direction); half3 viewDir normalize(GetWorldSpaceViewDir(i.positionWS)); half3 halfDir normalize(lightDir viewDir); // 各向异性高光Kajiya-Kay float TdotH dot(tangentWS, halfDir); float sinT sqrt(saturate(1 - TdotH * TdotH)); float spec pow(sinT, _Glossiness * 128); half3 specular _SpecColor.rgb * spec * mainLight.color; // 简单菲涅尔让掠射角反射更强 float NdotV saturate(dot(normalWS, viewDir)); float fresnel pow(1 - NdotV, _FresnelPower); half3 color albedo.rgb * mainLight.color * mainLight.shadowAttenuation specular * fresnel; return half4(color, albedo.a); } ENDHLSL } } FallBack Universal Render Pipeline/Unlit }这段代码的价值在于能跑通核心逻辑但它不是完整的物理PBR环境反射和多重光源都没处理。如果要做更精细的材质建议对照URP的Lit源码在它的镜面反射BRDF函数里传入不同的roughnessU和roughnessV达到真正的各向异性GGX效果。不过从视觉感知上Kajiya-Kay加菲涅尔已经能骗过大部分人眼做服装展示的原型足够了。Shader Graph里怎么处理你在Lit Graph的基础上把Smoothness拆成两个方向或者用一个Custom Function节点把上边的核心高光函数接进去再把Anisotropy暴露给美术调节就行。底层逻辑跟手写一样只是换了个组装方式。4. 细节打磨让丝绸材质看起来更真实4.1 粗糙度贴图与环境反射的配合丝绸的效果不是单独靠高光公式撑起来的。真正的丝绸织物不同区域纤维的排列密度和凹凸程度不一样粗糙度、高光强度、反射方向会有变化。如果只用单一参数整块布料会显得过于均匀一看就是程序生成的假材质。我建议主纹理之外的配套贴图至少有两张一张粗糙度贴图Roughness Map控制不同区域的高光收敛程度一张方向扰动贴图让高光方向有微小的偏移。方向扰动贴图听起来很玄其实就是在PS或SD里做一张灰度图用它的R通道去干扰各向异性角度。这样丝织品那种“纤维不是绝对笔直”“不同区块有轻微编织纹理”的细节就出来了。环境反射同样重要。丝绸在暗环境下很显“廉价”因为没有环境光来展示高光的拉丝形状。我的做法是在场景里放一个反射探针并把探针的渐变模糊调到符合丝绸的粗糙度范围。高光方向的拉伸感在反射探针里看得很清楚——如果反射高光还是一团圆形说明你的BRDF没有真正生效更多是探针的Smoothness没配合好。4.2 半透明纱质与多层高光的叠加思路很多时候要做的不是一整块厚缎子而是纱质、半透明的丝绸。这时候不能再拿不透明PBR硬做需要把材质切换到透明队列用Alpha值控制布料透过程度。我常用的方法是分层底层用一个漫反射Shader处理布料的固有色和微纹理顶层用一个各向异性高光Shader叠加丝光。两层之间通过菲涅尔或Alpha贴图做过渡这样既保留了纱的透光感又能在丝光区看到明显的拉丝高光。注意性能问题。如果每个层都单独开一个PassGPU的Overdraw会变大特别是在移动端远处看材质几乎没法看。推荐的做法还是合并成一个Pass在片元函数里同时计算漫反射和环境高光再用Alpha混合输出。这样效果差别不大但性能更稳。纱质丝绸还有个特殊点透光之后高光的饱和度会下降。我会把高光颜色的Alpha值乘以透明度否则会出现一束极亮的高光浮在透明布料上的违和感。简单说透明度越低高光越弱、越淡。4.3 灯光摆放与烘焙参数的一些经验很多人在调丝绸材质时会在默认的平行光下观察结果高光长条根本出不来。问题不是Shader而是灯光类型。平行光属于方向光它的高光分布非常“硬”适合表现金属不适合表现丝绸那种柔和纤维反光。丝绸更适合用面光源Area Light或者环境光来打亮因为面光源能提供大面积渐变光照高光在纤维方向会形成渐变拉丝。在URP里如果实时面光源开销太大可以用多个点光源模拟面光源的覆盖范围或者直接依靠反射探针和IBLImage Based Lighting来营造高光氛围。烘焙方面我的建议是别把希望寄托在Lightmap上。丝绸高光是视角相关的随摄像机移动而变化Lightmap只能记录静态漫反射对高光的贡献很小。正确做法是漫反射走烘焙高光和反射走实时探针。以前在项目里吃过亏把丝绸地面烘焙到Lightmap里结果旋转视角时地面高光纹丝不动看起来像一张贴纸。后来改成探针反射才正常。5. 常见问题与排查实录5.1 高光方向不对像普通高光或者横竖反了这是第一次写各向异性材质最常见的问题。整个Shader都运行了高光也确实存在但它依然是圆形的或者明明布料纹理是竖向的高光光条却很横向。先排除最基础的原因切线数据是否传入。如果你在顶点着色器里把tangentWS设成了常量或者用了法线贴图的切线空间但没正确变换那么T的方向就是错的。检查方式是在调试视图里输出tangentWS的颜色如果颜色变化剧烈且不符合UV分布规律说明切线有问题。如果切线方向基本正确但高光稍微偏转可以用“方向角度”参数去旋转切线。比如float angle radians(_AnisoDir); float2x2 rot float2x2(cos(angle), -sin(angle), sin(angle), cos(angle)); float3 rotatedTangent float3(mul(rot, tangentWS.xy), tangentWS.z);建议把_AnisoDir暴露给美术让他们在材质面板直接旋转高光方向效率会高很多。另外UV镜像问题也会让切线方向翻转。建模软件里面如果对UV做了镜像Unity的自动切线计算会出错。解决办法一是回到DCC软件里修复UV避免镜像二是在Shader里增加一个修正系数让特定区域的切线方向乘以-1。后者更适合那种不想动模型的场景但需要美术标记区域略麻烦。5.2 高光过亮、糊成一团、或者出现黑斑高光过亮很多时候不是Shader效果太强而是没有做合法的BRDF归一化。你直接用pow(sinT, shininess)乘上一个高光颜色值域可以轻松超过1导致曝光溢出。解决思路是给镜面反射项乘上1/PI或者类似系数把能量控制回合理范围。黑斑问题比较隐蔽。当TdotH大于1或者小于-1时sqrt(1 - pow(TdotH, 2))里会出现负数sqrt对负数返回NaN在GPU上表现不稳定然后pow(NaN, 0)也有未知结果。视觉上就是部分像素突然变黑或者出现杂色块。所以在sqrt前必须做saturate钳制float sinT sqrt(saturate(1 - TdotH * TdotH));这句话看着简单但能避免大量显示异常。我在支持Vulkan的安卓机上专门验证过不saturate的话黑斑概率很高改了之后就干净了。高光“糊成一团”则说明锐度不够。可以加一个“高光压缩”参数或者提高Glossiness值。不过要小心过于锐利会变成金属感丝绸和金属的区别在于丝绸高光边缘是柔和的渐晕金属高光则偏硬。保持低Glossiness的同时用方向性把高光拉长才是丝绸的正确打开方式。5.3 URP下材质不生效、颜色全黑、或移动端闪烁如果你把Shader放到了URP项目里却完全没反应先检查SubShader标签和Pass标签有没有写对。URP要求SubShader有Tags { RenderPipelineUniversalPipeline }Pass里必须有Tags { LightModeUniversalForward }。缺少后者URP会把它当无效Pass直接跳过。这是新写URP Shader最容易踩的坑。颜色全黑还有一个常见原因没有包含Lighting.hlsl也没有调用GetMainLight()。URP默认主光源信息不会自动注入必须通过头文件获取。如果你只采样了贴图颜色没有计算光照那结果当然是一团黑。移动端闪烁或不稳定多半是精度问题。在GPU上float和half的精度不同很多移动GPU对half的处理很激进。如果各向异性高光里的pow和sqrt用了float某些芯片会出问题。我倾向于在关键计算用half存储最后输出阶段再转float但pow函数建议用half版本或者用pow的half重载能明显减少闪屏。另外一个性能优化如果你的材质要跑移动端避免在片元着色器里对每个像素都计算完整的切线矩阵和三角函数。把能放顶点着色器计算的量尽量提前算比如把方向和半向量的点积拆到顶点上。不过注意半向量依赖视角随便提前算会出问题最好还是老老实实保留在片元但可以缓存一些重复计算项。6. 一些偏门的实用技巧6.1 用噪波纹理增强“丝缕感”有一次做晚礼服材质单靠各向异性高光怎么都缺一点真实感后来发现是表面太干净了。真实丝绸会有极其细微的纤维束它们的光反射方向并不完全一致。解决方式是在默认高光之上加一层噪声扰动让各向异性方向在每帧或者每个像素上有微小抖动。我用PerlinNoise或者FBM噪声作为扰动源叠加到切线的旋转角上肉眼看到的效果是丝光里带一种微微的“呼吸感”非常像真实织物。这里要注意扰动幅度不宜过大否则高光会像“金属碎片闪光”一样杂乱。一般角度扰动控制在正负2度到5度之间就够了。6.2 用贴花或顶点色做区域差异真实服装有褶皱、有拼接缝、有不同纹理方向的裁片。如果整个模型用同一套UV方向高光会很呆板。如果你的模型有顶点色可以在Shader里用顶点色的R通道作为“方向混合权重”——某个区域的切线方向偏转角度更大另一个区域基本保持原状。这样不额外加贴图就能做出区域差异灵活性很高。如果没有顶点色也可以从UV的局部坐标导出一个方向场。最简单的办法是取float2 dir float2(ddx(i.uv.x), ddy(i.uv.y))作为该像素的切线方向。这个方法在连续UV区域效果很好但碰到UV接缝就会断。我只在烘焙静态布料时用这个方法动画布料还是手动维护顶点色更靠谱。6.3 配合动画布料飘动后高光要跟着“流动”如果你做的是丝绸动画比如裙子摇曳、旗帜飘动高光不仅要跟随面片移动还要跟随顶点法线变化。这种情况下如果只用静态切线方向布料一变形高光方向就乱了。推荐的做法是在顶点着色器里根据当前的切线方向实时重算高光方向向量让T和B都跟随形态变化。Unity的SkinnedMeshRenderer提供顶点动画但切线不会自动更新。你需要手动在Shader里根据蒙皮矩阵变换切线或者从邻接三角形计算切线。这个专题比较大这里先提个方向如果只是想让飘动布料看起来不穿帮可以在顶点着色器里对切线方向做轻微的随波整形也就是把当前顶点位置偏移的方向混入切线方向。这个方法在视觉上能隐藏部分变形僵硬感。7. 最后分享一点我做丝绸材质的心得做了几年渲染每次拿到新材质需求我都习惯先问自己一个问题现实里这个东西它的微观结构到底长什么样金属是有晶体和划痕的皮肤有一层半透明的表皮丝绸则是一束束纤维。搞清楚微观结构很多参数和贴图的需求就自然浮现出来了。不要急着堆参数先把材质的基础方向定好。具体到丝绸我的个人经验是高光方向永远比高光强度重要方向错了亮度再对也是错的粗糙度贴图和方向扰动贴图的价值超过一个复杂的BRDF函数不要让美术去调一大堆数学意义上的参数尽量暴露“高光方向角度”“高光锐度”“丝缕感强度”这种语义明确的滑块。另外调试这类材质时别忘了把环境调成纯黑只留一盏平行光或者一个方向探针。这样能快速看到高光形态排除环境光的干扰。等你把高光方向调得干干净净再开场景全局光照效果基本不会翻车。希望这篇文章能帮你在丝绸材质这条路上少走点弯路。如果你在做项目时遇到了其他诡异问题欢迎在评论区和同行交流很多时候一个问题背后是另一种材质思路的开启。