URP ShaderGraph各向异性材质实战:从拉丝金属到头发丝绸高光
2026/9/16 11:27:47 网站建设 项目流程

前阵子接了个保温杯产品展示的项目,甲方把实物寄过来,指着杯子说就要这种“转一个角度、刷地扫过去一条细长光”的不锈钢质感。我拿默认的 URP Lit 调了大半天,金属度直接拉满,光滑度也做了粗糙贴图,高光始终是那种圆乎乎的一团,跟实物照片放一起一眼假。

后来换思路,改用 ShaderGraph 在 URP 里做各向异性(Anisotropic)材质,才终于把那条光丝调出来。拉丝金属、头发、丝绸、光盘背面那种随观察角度变化的高光条纹,本质都和“各向异性”有关。这篇就把我从原理理解、方案选型、实际接线到踩坑填坑的完整过程整理出来,全程用 ShaderGraph,不手写完整 shader,适合刚接触 URP 的 TA 或想给项目加细节的美术同学参考。

1. 先从现象理清原理:拉丝金属的高光为什么是长条

1.1 标准 PBR 高光的物理盲区

默认的 Blinn-Phong、GGX 这类高光模型,底层假设是:微表面法线在宏观法线周围按一个圆形分布随机排列。这个假设对大多数塑料、陶器、喷砂金属成立,所以高光长成一个圆形亮斑。

但拉丝金属完全不同。它的微观表面是无数条平行的微小沟槽,就像一整片细细的拉丝纹理。光线打上去,沿沟槽方向和垂直沟槽方向的散射特性完全不同。圆形的微表面分布假设直接失效,高光被“拉”成了一条细长的光带。

最直观的例子是光盘背面:盘面是同心圆沟槽,你转动光盘,看到的高光是一道道从中心放射出去的光线,而不是一个圆点。这就是典型各向异性反射。

1.2 记住一个方向关系:高光拉丝方向与沟槽方向垂直

这个关系我在项目里反复踩,先写清楚:竖直沟槽的拉丝表面,高光是横向延伸的细长条;横向沟槽,高光则纵向拉长。

原因是沟槽相当于一组微型柱面镜,反射光会沿垂直于沟槽的方向铺开。做材质的时候,脑子里要一直有这条对应关系:你调的是沟槽方向,看到的却是垂直方向的高光。很多教程里 Flow Map 存的是沟槽方向而不是高光方向,就是这个原因。

1.3 ShaderGraph 里实现它的核心依赖:切线

标准高光用“法线 N 和半角向量 H 的点积”控制亮斑形状。各向异性模型的思路很直接:把主角从法线换成切线。

材质网格自带的切线(Tangent)和副切线(Bitangent)构成了表面切平面,沟槽方向就在这个平面里。各向异性高光本质上就是把原本圆形的高光 lobe,在切线方向和副切线方向上分别按不同比例缩放:沿着沟槽方向摊平一点,垂直沟槽方向收窄一点,视觉上就出现了拉丝。

URP 的 Lit Shader 在内部就是这么干的——通过一个标量_Anisotropy控制高光椭圆在切线/副切线方向上的宽窄比例。它没有暴露完整的 Ward 模型或 GGX 各向异性参数,但做 90% 的常规项目效果完全足够了。

2. 两条实现路线,我劝你先走内置 Anisotropy

2.1 路线一:URP Lit 内置 Anisotropy

Unity 在 URP Lit Shader 和 URP Lit ShaderGraph 里都内置了各向异性支持。ShaderGraph 中只需要在 Graph Settings 里勾选一个选项,PBR Master 节点就会多出一个Anisotropy输入端口。

优点非常明显:

  • 光照、阴影、环境光、间接高光全部自动接管,不用自己实现半天下半场。
  • 改动成本低,一个 float 参数就能从各向同性切到各向异性。
  • 性能和管线兼容性有官方保障。

限制也有:

  • 沟槽方向默认跟随网格切线和 UV 布局,美术不能直接在材质里指定“这边是竖直拉丝、那边是横纹”。
  • 无法做到头发那种双高光、带色偏移的复杂效果。
  • 反射探头、平面反射等间接高光不会跟着拉丝,只有直接光高光有各向异性效果。

2.2 路线二:自定义函数重定向切线 / 自定义高光

内置方案的方向限制,可以通过在顶点阶段用自定义函数重算切线来突破。你可以根据 Flow Map 或者顶点色,在切平面内把切线旋转到任意方向,再喂给内置 Lit 的各向异性计算。

更彻底的做法是直接写一个自定义高光函数,比如 Kajiya-Kay 头发模型,在 ShaderGraph 里用 Custom Function 节点调用 HLSL。这个路线的控制力最强,头发、丝绸、CD 都能做,但要自己处理主光源方向、衰减、阴影,工作量是内置方案的数倍。

2.3 怎么选

维度内置 Anisotropy自定义函数
上手成本极低,勾选即用较高,要写 HLSL
方向控制跟随网格切线Flow Map / 顶点色自由控制
光照阴影自动需自己取主光并计算
典型用途拉丝金属、磨砂面、常规道具头发、丝绸、光盘、特殊镜面
移动端风险需注意顶点采样限制

我的建议很明确:先把内置方案调出想要的感觉,确认“我就是要这种拉丝高光”,再决定要不要上 Flow Map 自定义函数。不要一上来就从数学公式写起。

3. 第一版效果:URP Lit 内置各向异性从勾选到出效果

3.1 环境要求与 Graph 初始化

  • Unity 2021.3 LTS 或 2022.3 LTS,URP 12/14 及以上。
  • 工程必须切换成 URP 管线资产,否则创建不了 URP Shader Graph。

操作路径:Create > Shader Graph > URP > Lit Shader Graph,命名比如SG_Aniso_Brush

双击打开后,在 Graph Inspector 的Graph Settings面板中:

  • Surface Type:Opaque
  • Workflow:Metallic
  • 往下拉到底部的 Inputs 区域,勾选 Anisotropy

勾选后返回主预览,会看到 PBR Master 节点上多出来一个Anisotropy输入口。这一步在不同 Unity 版本里位置略有不同,但基本都在 Surface Options/Inputs 这一类目下,找不到就按 F 全局搜一下。

3.2 用 Float 滑块先看清方向

创建 Float 属性,命名_AnisoStrength,Slider 范围-1 ~ 1,默认 0.5。直接连到 Master 的Anisotropy端口。

创建材质,赋给一个圆柱体或者带弧面的金属杯模型。把场景里主光源转一圈看:高光开始从圆斑变成一条环绕杯身的亮线。

这时候一定要做两个调试动作:

  1. 把值拉到 0,确认高光回到普通圆斑,说明开关生效。
  2. 把值拉到 -0.5,再看一次。正负值会切换高光优先沿切线还是副切线拉伸。至于是横的还是竖的,取决于你模型的 UV 和切线导出方式,没有统一答案,所以每次在项目里都要实测一遍,别凭记忆猜方向。

3.3 用噪声或纹理打破单调的“塑料拉丝感”

一个全模型统一的正值各向异性,做出来的拉丝是均匀的,像塑料片上压了细纹,缺少真实金属那种忽强忽弱的闪光。

处理办法是用一个噪声或拉丝纹理去调制Anisotropy,而不是给固定值。我喜欢用 ShaderGraph 自带的Gradient Noise

  • Gradient Noise节点,Scale 设为 (15, 15);
  • Power节点,指数 3 左右压暗中间调;
  • 再乘_AnisoStrength,整体接Anisotropy

这样高光在某些区域被拉得很开,有些区域接近圆形,模拟了拉丝不一致的粗糙感。

如果你手里有真实拉丝不锈钢的灰度纹理,就更简单:采样后取 R 通道,Saturate一下,直接乘_AnisoStrength。注意这里不要做0~1 到 -1~1的重映射,否则纹理暗部会让高光反向拉丝,画面会花掉。

3.4 快速可视化切线方向的小技巧

内置方案的方向完全由模型切线决定。你要确认当前模型的切线方向,最土的办法是临时把 BaseColor 接Tangent Vector节点,输出一张“方向色图”。

具体做法:在片元阶段拖入Tangent Vector(输出的是 Object 空间切线),接Transform转 World 空间,再乘 0.5 加 0.5 后接到 BaseColor。预览时材质会呈现红绿蓝渐变,颜色变化方向就是切线的走向。看清楚后删掉调试连线,重新接回正式贴图。

4. 让拉丝方向听话:Flow Map 重定向切线的完整做法

内置方案什么都好,就是方向固定。真实场景里,一个杯子可能是杯身纵向拉丝、杯口边缘横向拉丝、底部漩涡纹,这就必须让每个像素的沟槽方向可控制。

4.1 Flow Map 到底存了什么

Flow Map 是一张线性纹理,RG 通道记录了切平面内的一个方向向量:R 对应切线方向贡献,G 对应副切线方向贡献,中间值 0.5 表示“该方向没有贡献”。采样后做一次* 2 - 1的映射,就得到 -1 到 1 的方向值。

用 Photoshop 就能画:

  • 纯横纹:R=1,G=0.5,代表方向沿切线;
  • 纯竖纹:R=0.5,G=1,代表方向沿副切线;
  • 倾斜 45°:R=0.75,G=0.75。

Substance Painter 里更方便,直接用 Fiber 或 Brush Direction 这类生成器烘焙方向图,导出时务必取消 sRGB,设置为 Linear、Clamp。

4.2 顶点阶段自定义函数:完整可复制的 HLSL

在 ShaderGraph 里,URP Lit 的内置各向异性使用网格顶点切线作为方向基准。我们可以在顶点阶段把切线重算成 Flow Map 指定的方向,PBR Master 内的光照代码完全不需要改动。

新建一个 Text 文件,改名AnisoFlowTangent.hlsl,写入:

void AnisoFlowTangent_float( float3 TangentWS, float3 BitangentWS, float3 NormalWS, float2 FlowXY, float Strength, out float3 OutTangentWS) { // 纹理存的是 0~1,方向需要 -1~1 float2 dir = FlowXY * 2.0 - 1.0; // 长度极短说明这个像素没有明确沟槽方向,保留原切线 float len = length(dir); if (len > 1e-5) dir /= len; // 在切平面内按 Flow 方向重新组合切线 float3 flowTangent = TangentWS * dir.x + BitangentWS * dir.y; // 去掉法线分量,防止切线被带倾斜 flowTangent = normalize(flowTangent - NormalWS * dot(NormalWS, flowTangent)); // 用 Strength 控制原始切线与 Flow 切线之间的混合比例 OutTangentWS = normalize(lerp(TangentWS, flowTangent, saturate(Strength))); }

我在项目里一般先让美术确认流水方向,再决定 Strength 是 1 全量替换,还是 0.3~0.7 之间做混合。混合的好处是 UV 接缝附近过度自然,不会从“强拉丝”突然跳到“无拉丝”。

4.3 接线步骤:在 Vertex 阶段把新切线喂给 Master

  1. 在 ShaderGraph 的 Blocks 列表里,找到Vertex > Tangent块并添加进主 stack。
  2. 拖入Normal Vector节点和Tangent Vector节点,分别用Transform节点转成 World 空间,得到NormalWSTangentWS
  3. 新建Custom Function节点,源文件设为AnisoFlowTangent.hlsl
  4. Cross Product节点计算BitangentWS = cross(NormalWS, TangentWS)。这里注意:我默认副切线方向乘的是正号,镜像 UV 的模型会出现方向反了的问题,后面踩坑部分细说。
  5. Sample Texture 2D采样_FlowMapSplit后取 RG,Combine成 Vector2 接入FlowXY
  6. 用一个 Float 属性控制Strength
  7. 函数输出OutTangentWS,接回Vertex Tangent块。

完成后,转动物体或光照,你会看到高光拉丝方向随 Flow Map 变化,不再死板地跟着模型原始切线走。

4.4 顶点采样的代价与替代方案

把纹理采样用在顶点阶段,最大的隐患是方向细节受限于网格顶点密度。圆柱分段数低时,拉丝方向会呈现明显的折线感,不像片元采样那么细腻。解决办法有两个:

  • 给关键模型加细分,让顶点密度足够表现方向变化;
  • 把方向信息烘焙到顶点色,用顶点色 RG 替代 Flow Map。移动端我强烈建议用顶点色方案,省一张纹理,也规避了部分平台对顶点纹理采样支持不佳的问题。

另一个容易忽略的点:Flow Map 的 UV 尽量跟网格 UV0 一致,不要用 Lightmap UV2 去采样方向图,否则贴图对齐会乱。

5. 同一套逻辑改出头发、丝绸和光盘效果

5.1 头发:双高光加切线偏移

头发的各向异性跟拉丝金属不太一样。发丝是细长圆柱体,高光不是一条简单的拉丝,而是沿发丝方向有一道主高光、再带一道偏蓝或偏冷的副高光。经典方法是 Kajiya-Kay 模型加切线偏移:把切线沿法线方向偏移一定量,让高光环发生位移。

偏移的核心代码:

void HairAnisoSpecular_float( float3 TangentWS, float3 NormalWS, float3 HalfDir, float Exponent, float Shift1, float Shift2, float Strength1, float Strength2, out float3 SpecColor) { float3 shifted1 = normalize(TangentWS + NormalWS * Shift1); float3 shifted2 = normalize(TangentWS + NormalWS * (Shift1 + Shift2)); float d1 = saturate(dot(shifted1, HalfDir)); float d2 = saturate(dot(shifted2, HalfDir)); float lobe1 = pow(sqrt(1.0 - d1 * d1), Exponent); float lobe2 = pow(sqrt(1.0 - d2 * d2), Exponent * 0.5); float3 col1 = float3(1.0, 0.75, 0.5) * lobe1 * Strength1; float3 col2 = float3(0.2, 0.35, 0.9) * lobe2 * Strength2; SpecColor = col1 + col2; }

HalfDir需要通过半角向量计算:normalize(LightDir + ViewDir)。ShaderGraph 里主光源方向和颜色不能直接拿,需要用一个小 HLSL 函数:

void GetMainLightInfo_float(float3 WorldPos, out float3 LightDir, out float3 LightColor) { #if defined(SHADERGRAPH_PREVIEW) LightDir = float3(0.5, 0.5, 0.5); LightColor = float3(1, 1, 1); #else Light mainLight = GetMainLight(); LightDir = mainLight.direction; LightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation); #endif }

Custom Function 节点的 Include 里要加上:

Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl

这样在 ShaderGraph 里就能取到主光方向,再把两个函数组合起来,输出接到 Emission 上做叠加。

5.2 丝绸和天鹅绒:善用负各向异性

丝绸的亮面经常出现一条边缘变亮、中间偏暗的高光带,这是丝绸纤维的柱面反射特性。很多情况下把Anisotropy调成负值,配合一张缎纹灰度图控制强度,就能出现类似丝绸的两侧亮、中间暗的过渡。

天鹅绒就更特殊,边缘光比中心亮,本质上是一种“倒置”的高光。URP 内置版本做不了真正意义的倒置 lobe,但你可以把 Anisotropy 拉到一个较大的负值,边缘处用菲涅尔加强,观感上已经能骗过大部分人的眼睛。

5.3 光盘和干涉色:伪频谱色

光盘背面那种彩虹色,是沟槽间距对可见光不同波长做衍射干涉的结果,严格做需要按波长算。项目里一般是偷懒:用各向异性高光拉出一条径向光带,再根据切线方向和观察角度的点积去采样一张彩虹渐变贴图,乘到高光上。

这种方法在电竞外设、赛博朋克道具上特别好用,参数少、效果抢眼,性能也便宜。核心思路就是:各向异性负责形状,颜色交给渐变纹理贴片。

6. 这些坑我花了三天才填平:接线、压缩与版本细节

6.1 镜像 UV 导致高光方向拧成麻花

做 Flow Map 版本时,杯子一侧拉丝方向正确,另一侧高光却旋转了 180°,看起来像被拧过。原因是模型 UV 有镜像摆放,导致副切线方向需要乘 -1 的切线符号,我的Cross Product节点默认按正号计算。

排查办法:在模型导入器的 Tangent 设置里,把 Tangent Space 从默认的Calculate改成LegacyNone对比,或者直接关掉一半 UV 看看问题是不是出在镜像区域。

根治手段是改模型 UV,不让 UV 岛出现镜像。如果美术实在改不了,就把方向烘焙到顶点色,在顶点色里直接修正镜像区域的方向。

6.2 纹理格式和线性空间不对,效果几乎消失

Flow Map 如果忘了取消 sRGB,色彩空间转换会把 0.5 推到一个不等距的位置,导致*2-1映射后中性方向不再是 0,拉丝方向会整体偏掉,高光看起来又糊又脏。

纹理设置记住三件事:

  • sRGB 勾选取消,保证线性采样;
  • Wrap Mode 设 Clamp,防止 UV 边缘采样环绕产生突兀方向跳变;
  • 压缩格式优先选择支持 R、G 通道独立的格式,最好不要用带 BC 通道依赖的压缩给方向图。

6.3 URP 版本差异

Unity 2019 时代的老 URP 里,Lit ShaderGraph 没有 Anisotropy 选项。遇到老工程,要么升级 URP 包,要么直接走自定义函数方案。

Unity 2021.3 之后,Anisotropy 选项都在 Graph Settings 底部。Unity 6(6000.0)里位置没变,但 ShaderGraph 对新节点的命名做了调整,老工程升级后会有一些节点报错,建议升级前先备份。

6.4 性能和高光可见性

URP 内置各向异性主要增加的是高光计算里的几个三角函数和分支,桌面平台随便用,移动端控制在场景里两三个核心材质内比较稳妥。大范围地面、墙体等次要物体不建议开,高光拉丝在远景下基本看不清,只会白白增加 GPU 消耗。

还有一个容易误会的点:URP 内置各向异性只作用于直接光,反射探针和光探头的间接高光不会跟着拉丝。产品展示场景必须保证有一个方向明确的主光源,否则拉丝效果会“若隐若现”,看起来以为是 shader 坏了。

另外,移动端关闭 MSAA 时拉丝高光边缘容易出现闪烁,可以把 Anisotropy 控制在-0.5 ~ 0.5范围,别拉太满。

最后说几句

调这玩意儿调多了会发现,各向异性材质绝大多数情况下不需要从公式开始造轮子。URP 内置的 Anisotropy 加上一张方向可控的 Flow Map,已经能覆盖拉丝金属、磨砂玻璃、甚至大部分丝绸类需求。真正需要写 Kajiya-Kay 自定义函数的,只有角色头发这种对高光轮廓要求苛刻的场景。先勾选、再连图、最后才考虑写函数,这个顺序能替我省掉很多无效加班。如果你在项目里卡在方向控制上,建议直接跳到 Flow Map 那一节,把贴图线性化先检查一遍,大概率问题就出在那儿。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询