
1. 项目概述为什么丝绸材质是PBR渲染的“试金石”在Unity里做材质尤其是PBR基于物理的渲染材质很多朋友觉得把Metallic、Smoothness调一调再贴张法线贴图就差不多了。但当你真正想还原一些高级质感时比如丝绸、拉丝金属、头发就会发现标准PBR那套有点“力不从心”。它渲染出来的高光总是圆润对称的像个光滑的塑料球而现实世界里很多材质表面的微观结构是有方向的导致高光也是“拉长”或“有流向”的。这就是“各向异性高光”要解决的问题。这次我们聚焦“丝绸”。丝绸材质堪称PBR各向异性效果的经典考题。它不像金属那么硬朗也不像布料那么粗糙。它的魅力在于那种柔顺、丝滑带有独特方向性光泽的质感。用标准的各向同性IsotropicPBR Shader去模拟结果往往就是标题里说的——看起来像一块“塑料布”光泽有但毫无丝绸的灵魂。这个项目的核心就是深入Unity的Shader底层从原理到实践亲手构建一个能真实表现丝绸质感特别是其各向异性高光的PBR材质系统。这不仅是贴图艺术更是一场关于光线、微表面和数学模型的科学实践。2. 核心原理拆解各向异性高光到底“异”在何处要动手造轮子先得明白轮子为什么转。标准PBR的高光模型比如常见的Cook-Torrance BRDF其核心假设是物体表面的微观几何Microfacet分布是均匀且随机的像一个被无数微小镜面朝向完全随机分布的山丘。这样从任何方向看去它对光线的反射都是统计上一致的因此高光呈现完美的圆形或椭圆形取决于观察角度这就是“各向同性”。2.1 微观结构的“方向性”是根源而丝绸、 brushed metal拉丝金属、CD光盘表面它们的微观结构有强烈的方向性。想象一下丝绸的纤维或者拉丝金属表面的无数细小划痕它们基本都是沿着一个主导方向排列的。这种有方向的微观沟槽或纤维导致表面的法线分布不再是随机的而是沿着某个切线方向Tangent Direction被“拉长”了。这个“方向性”直接改变了光线反射的行为。对于沿着沟槽方向即切线方向入射和观察的光线微表面的“斜坡”显得更平缓反射更集中高光就更强、更锐利而对于垂直于沟槽方向即副法线方向Binormal Direction微表面的“斜坡”显得更陡峭反射更分散高光就更弱、更柔和。最终结果就是在材质表面看到一个被拉长的、有清晰方向感的高光条带而不是一个圆斑。2.2 数学模型从各向同性到各向异性的关键扩展在Shader中我们如何描述这种变化关键在于那个描述微表面法线分布的NDFNormal Distribution Function函数。在标准PBR中我们常用GGX/Trowbridge-Reitz模型它的粗糙度参数α是一个标量适用于所有方向。为了实现各向异性我们需要将单一的粗糙度α分解为两个方向上的粗糙度沿切线方向Tangent的粗糙度α_t和沿副法线方向Binormal的粗糙度α_b。这样NDF函数就能根据光线和视线的方向在这两个不同的粗糙度上分别计算影响从而生成方向性的高光。一个常见的简化是我们通常使用一个总的粗糙度Roughness和一个各向异性强度Anisotropy范围-1到1来控制。当Anisotropy为0时α_t α_b退化为各向同性。当Anisotropy为正时表示沿切线方向更光滑α_t 减小沿副法线方向更粗糙α_b 增大高光沿切线方向被拉长。反之亦然。注意切线方向Tangent和副法线方向Binormal共同构成了物体表面的切线空间Tangent Space。这个空间对于法线贴图和各向异性计算都至关重要。确保你的模型拥有正确、一致的切线空间数据是第一步否则一切计算都将失去基准。3. 构建环境与核心工具链准备工欲善其事必先利其器。构建一个自定义的PBR Shader我们需要一个清晰的工作环境。这里我强烈推荐使用Unity的URPUniversal Render Pipeline或HDRP作为渲染管线基础因为它们提供了更现代、更模块化的Shader编写框架并且官方支持越来越好。本文将以URP为例因为它的普及度和性能平衡性更佳。3.1 项目初始化与管线设置首先创建一个新的URP项目或在你现有的项目中通过Package Manager安装“Universal RP”包。接着在Project窗口中右键 - Create - Rendering - Universal Render Pipeline - Pipeline Asset (Forward Renderer)创建必要的渲染管线资源。记得在Project Settings - Graphics中将其指定为Scriptable Render Pipeline Settings。3.2 Shader Graph vs 手写Shader路线选择你有两条路可以走使用Shader Graph可视化工具或手写HLSL/Cg代码。对于学习各向异性原理而言我强烈建议从手写Shader开始。Shader Graph虽然直观但有时会隐藏底层细节而理解这些细节如切线空间变换、自定义函数节点内部的数学正是本项目的关键。当你掌握了原理后再用Shader Graph去复现会事半功倍。我们将创建一个Unlit Shader作为起点因为我们要完全自定义光照然后逐步将其升级为包含各向异性高光的完整PBR Shader。在Project窗口右键 - Create - Shader - Unlit Shader命名为“SilkPBR_Anisotropic”。双击在Shader编辑器中打开清空默认代码我们将从头搭建。3.3 必备输入定义材质属性与纹理在Shader的Properties块和CGPROGRAM内部的变量声明中我们需要定义以下核心输入Properties { _BaseColor (Base Color, Color) (1,1,1,1) _BaseMap (Base Map, 2D) white {} _Metallic (Metallic, Range(0, 1)) 0 _Roughness (Roughness, Range(0, 1)) 0.5 // 各向异性核心参数 _Anisotropy (Anisotropy, Range(-1, 1)) 0.5 // 控制高光拉伸的方向和强度 _AnisotropyRotation (Anisotropy Rotation, Range(0, 1)) 0 // 控制切线方向旋转通常用贴图 _AnisotropyMap (Anisotropy Map (R:Anisotropy, G:Rotation), 2D) white {} // 辅助纹理 _NormalMap (Normal Map, 2D) bump {} _NormalScale (Normal Scale, Range(0, 2)) 1 }这里重点说明_AnisotropyMap。通常我们用一张贴图的两个通道如R和G来分别控制各向异性强度和方向旋转。这比单一标量值控制得更精细可以模拟丝绸上光泽方向随褶皱变化的自然效果。4. 手把手实现各向异性PBR光照模型接下来进入核心环节在片元着色器Fragment Shader中实现计算。4.1 构建切线空间与方向向量首先我们需要获取世界空间下的法线N、切线T、副法线B以及光线方向L和视线方向V。// 从顶点着色器传入的数据 struct VertexOutput { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldTangent : TEXCOORD3; float3 worldBitangent : TEXCOORD4; }; // 在片元着色器中 VertexOutput i ...; // 从顶点着色器传入 // 采样法线贴图并转换到世界空间 float3 normalTS UnpackNormalScale(tex2D(_NormalMap, i.uv), _NormalScale); float3 N normalize(i.worldNormal); float3 T normalize(i.worldTangent); float3 B normalize(i.worldBitangent); // 或者通过 cross(N, T) 计算确保正交 float3x3 TBN float3x3(T, B, N); float3 worldNormal normalize(mul(normalTS, TBN)); float3 V normalize(_WorldSpaceCameraPos - i.worldPos); float3 L normalize(_WorldSpaceLightPos0.xyz); // 对于方向光4.2 实现各向异性NDF函数这里我们采用扩展的GGX各向异性NDF。这是整个效果的灵魂。// 各向异性GGX NDF float D_GGX_Anisotropic(float NoH, float3 H, float3 T, float3 B, float at, float ab) { float ToH dot(T, H); float BoH dot(B, H); float a2 at * ab; float3 v float3(ab * ToH, at * BoH, a2 * NoH); float v2 dot(v, v); float w2 a2 / v2; return a2 * w2 * w2 * (1.0 / UNITY_PI); } // 根据粗糙度和各向异性参数计算 at 和 ab void GetAnisotropicRoughness(float roughness, float anisotropy, out float at, out float ab) { // 将roughness映射到alpha float roughnessSqr roughness * roughness; // 根据anisotropy-1到1拆分 float aspect sqrt(1.0 - 0.9 * abs(anisotropy)); // 0.9是一个可调参数控制强度 at max(roughnessSqr / aspect, 0.001); ab max(roughnessSqr * aspect, 0.001); // 确保at和ab不会为0避免除零错误 }4.3 组合完整的BRDF计算有了NDF我们还需要几何项G和菲涅尔项F。几何项通常可以使用Smith联合阴影遮蔽函数并为其适配各向异性版本虽然有些实现为了简化仍使用各向同性的G项这对结果影响相对较小。菲涅尔项则使用标准的Schlick近似。// 各向异性可见性函数简化版Smith可先使用各向同性版本 float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) { // ... 标准Smith GGX实现 } // 菲涅尔项 Schlick近似 float3 F_Schlick(float u, float3 f0) { return f0 (1.0 - f0) * pow(1.0 - u, 5.0); } // 在片元着色器主循环中 float3 H normalize(V L); float NoV abs(dot(N, V)) 1e-5; float NoL saturate(dot(N, L)); float NoH saturate(dot(N, H)); float VoH saturate(dot(V, H)); // 计算各向异性粗糙度 float at, ab; GetAnisotropicRoughness(_Roughness, _Anisotropy, at, ab); // 计算BRDF三项 float D D_GGX_Anisotropic(NoH, H, T, B, at, ab); float V V_SmithGGXCorrelated(NoV, NoL, sqrt(_Roughness)); // 这里使用sqrt(roughness)作为alpha float3 F F_Schlick(VoH, lerp(0.04, _BaseColor.rgb, _Metallic)); float3 specularBRDF (D * V * F) / (4.0 * NoV * NoL 1e-5); float3 kS F; // 反射部分 float3 kD (1.0 - kS) * (1.0 - _Metallic); // 漫反射部分金属度影响 float3 diffuse kD * _BaseColor.rgb / UNITY_PI; float3 finalColor (diffuse specularBRDF) * _LightColor0.rgb * NoL;4.4 融入环境光照与IBL真实的PBR离不开基于图像的光照IBL。我们需要采样环境贴图如Unity的unity_SpecCube0来提供漫反射和环境高光。对于各向异性高光的环境反射严格来说需要预过滤各向异性的环境贴图但这非常耗时。一个实用的折中方案是在采样环境贴图时根据我们计算出的各向异性粗糙度at, ab对反射向量R进行微扰或者直接使用一个中间值粗糙度去采样虽然不物理精确但视觉上可以接受。// 简化版环境光计算 float3 R reflect(-V, N); // 使用平均粗糙度或主要方向的粗糙度来采样Mipmap级别 float envRoughness (at ab) * 0.5; float mip envRoughness * UNITY_SPECCUBE_LOD_STEPS; float4 envData texCUBElod(_EnvironmentCube, float4(R, mip)); float3 envSpecular envData.rgb * envData.a; // 解码HDR // 将环境高光叠加到finalColor finalColor envSpecular * kS;5. 纹理艺术赋予丝绸灵魂的细节代码实现了物理模型但要让丝绸“活”起来离不开精心制作的纹理。这是“艺术”的部分。5.1 基础颜色与粗糙度贴图Base Color丝绸的底色通常不是纯色而是带有细微的色相变化和纹理感。可以使用一张低饱和度、带有细微噪波或织物纹理的贴图。Roughness Map这是控制质感的关键。丝绸整体光滑但褶皱处、边缘磨损处粗糙度会增高。在PS或Substance Designer中你可以利用法线贴图或高度图生成粗糙度变化原则是凸起褶皱顶部更光滑低粗糙度凹陷褶皱缝隙更粗糙高粗糙度。5.2 法线贴图与各向异性贴图Normal Map用于模拟丝绸表面的织物编织结构和褶皱。即使是很细微的法线变化也能极大地增强立体感。Anisotropy Map这是各向异性的控制核心。通常是一张RGBA贴图R通道Anisotropy Strength白色值1表示各向异性最强黑色值0表示各向同性。你可以根据丝绸的走向来绘制例如沿着布料的经线方向设置为白色纬线方向渐变到灰色。G通道Anisotropy Rotation控制切线方向的旋转。可以用它来模拟丝绸褶皱导致的光泽方向扭曲。0-1的值对应0-360度旋转。B和A通道可以存储其他信息如环境光遮蔽AO。实操心得在Substance Painter中绘制各向异性贴图非常直观。你可以使用“Anisotropy”和“Anisotropy Angle”通道进行绘画。一个技巧是先用法线或高度图生成基础的流向再用手绘笔刷在褶皱转折处进行修饰这样得到的效果非常自然。5.3 在Shader中采样和应用贴图控制在片元着色器中我们需要采样这些贴图并用它们来动态调整参数。float4 anisotropyMap tex2D(_AnisotropyMap, i.uv); // 从贴图读取各向异性强度和旋转并与材质属性混合 float anisotropyStrength anisotropyMap.r * 2.0 - 1.0; // 将[0,1]映射到[-1,1] float anisotropyFinal _Anisotropy * anisotropyStrength; float rotation anisotropyMap.g * 2.0 * UNITY_PI; // 映射到0-2π // 根据rotation旋转切线空间向量T和B float sinRot, cosRot; sincos(rotation, sinRot, cosRot); float3 rotatedT T * cosRot B * sinRot; float3 rotatedB B * cosRot - T * sinRot; // 注意方向 // 在计算NDF时使用旋转后的rotatedT和rotatedB代替原始的T和B6. 性能优化与实用技巧一个功能强大的Shader也必须考虑性能。以下是一些优化点分支优化避免在片元着色器中使用if语句判断Anisotropy 0来切换各向同性和各向异性模型。可以始终使用各向异性公式当at ab时它会自动退化为各向同性。用lerp或数学计算代替条件判断。纹理压缩AnisotropyMap通常不需要很高精度可以使用RGBA DXT5/BC7压缩或者将强度和旋转信息打包到两张不同的单通道贴图中如BC4格式节省带宽。近似计算对于移动平台可以考虑使用更简化的各向异性模型例如在切线空间内对高光进行方向性拉伸的“假”各向异性虽然物理不正确但视觉开销小。Shader变体管理如果你的材质不是所有物体都需要各向异性可以通过Shader变体#pragma shader_feature _ANISOTROPY_ON来编译两个版本为不需要的物体节省计算。7. 常见问题与调试实录在开发过程中我踩过不少坑这里分享几个典型问题和排查思路。7.1 高光方向错乱或闪烁问题描述丝绸的高光条纹方向不随模型旋转而变化或者在特定视角下闪烁、消失。排查步骤检查切线空间这是最常见的原因。在Shader中输出世界空间下的切线T和副法线B向量作为颜色return float4(T * 0.5 0.5, 1.0);在Scene视图查看。它们应该在模型表面连续、一致地变化。如果出现接缝或突变说明模型的切线数据有问题。检查模型导入设置在Unity的模型导入设置中确保“Tangents”选项设置为“Calculate Mikktspace”这是Unity推荐的标准。不要使用“Import”除非你完全信任源文件的切线数据。检查Anisotropy Rotation确保旋转角度的计算是正确的弧度制并且旋转矩阵应用到了正确的向量上T和B要同时旋转且保持正交。7.2 高光强度过强或过弱问题描述调整参数时高光要么白成一片要么几乎看不见。排查步骤检查NDF函数确保D_GGX_Anisotropic函数的返回值在合理范围内。可以单独输出D的值进行可视化。在非常光滑的表面at, ab接近0D值会非常大这是正常的但需要几何项G和分母项来平衡。检查能量守恒确保你的BRDF分母项(4.0 * NoV * NoL)存在。缺少这个分母是导致高光过亮的常见错误。检查线性空间确保你的项目在Player Settings - Other Settings - Color Space中设置为“Linear”。在Gamma空间下进行PBR计算光照会不正确。7.3 性能热点分析问题描述Shader在移动设备上帧率下降明显。排查步骤使用Frame Debugger或RenderDoc分析Draw Call和Shader复杂度。各向异性NDF计算比标准GGX多了点乘和开方运算。简化计算考虑将GetAnisotropicRoughness中的sqrt和除法预先计算或查找表LUT化。对于低端机可以彻底关闭各向异性特性。减少纹理采样检查是否有多余的纹理采样。AnisotropyMap可以和RoughnessMap打包到同一张贴图的不同通道。7.4 与后处理效果的兼容性问题描述应用了屏幕空间环境光遮蔽SSAO或泛光Bloom后丝绸的高光表现异常。排查思路自定义的复杂高光可能不会完全被后处理效果正确识别。确保你的Shader将高光输出到正确的渲染目标如URP中的_ScreenSpaceOcclusionTexture可能需要自定义。对于Bloom可能需要调整阈值或者将特别亮的高光区域单独输出到一个Buffer供Bloom使用。构建一个真实的丝绸材质是一个不断在物理精确度、视觉表现力和运行性能之间寻找平衡的过程。从理解各向异性的微观物理原理开始到在Shader中实现扩展的NDF模型再到用纹理艺术去刻画细节最后解决实际遇到的各种“坑”——这个过程本身就是对计算机图形学中“艺术与科学”结合的最佳诠释。当你看到自己手写的Shader渲染出的丝绸随着光线和视角流动着细腻而方向明确的光泽时那种成就感是直接用现成材质无法比拟的。希望这篇长文能为你打开一扇门不仅仅是学会做丝绸材质更是掌握了一种自定义高级渲染效果的方法论。