1. 项目概述理解各向异性光照在实时渲染领域光照模型是塑造物体视觉质感的核心。我们熟知的Lambert、Blinn-Phong等模型都基于一个核心假设物体表面是各向同性的。这意味着无论你从哪个方向观察表面在给定光照方向下的明暗表现都是一致的就像一块打磨光滑的塑料或石膏。然而现实世界中存在大量表面并非如此。想象一下拉丝金属、绸缎、CD光盘背面或者一头梳理过的秀发——当你转动视角或移动光源时这些表面会呈现出独特的方向性高光或明暗条纹。这种特性就是各向异性。在Unity的Universal Render Pipeline中实现各向异性光照本质上是在挑战标准光照模型的“均匀”假设。标准PBR工作流中的BRDF双向反射分布函数是为各向同性表面设计的。要实现各向异性效果我们需要引入一个额外的向量来定义表面的“纹路”或“沟槽”方向并让高光的形状、强度甚至颜色随着这个方向与视线/光源方向的相对关系而变化。这不仅仅是加一个法线贴图那么简单它涉及到在Shader中对光照计算进行根本性的修改。对于技术美术、图形程序员或任何希望提升作品视觉独特性的开发者而言掌握URP中的各向异性实现意味着你能创造出更具真实感和艺术表现力的材质比如高级金属材质逼真的拉丝不锈钢、 brushed aluminum拉丝铝、发动机叶片。织物与毛发天鹅绒、绸缎、丝绒的顺滑光泽头发的高光条带。特殊效果光盘、全息图、某些类型的鳞片或羽毛表面。风格化渲染用于创造非真实但视觉上引人注目的方向性光晕或条纹。接下来我将从原理到实践完整拆解在Unity URP中实现各向异性光照的几种主流方案并分享我在实际项目中的踩坑经验和优化技巧。2. 核心原理与方案选型在动手写Shader之前我们必须理解各向异性高光的物理或近似物理基础。核心思想是表面的微观几何不是无序的而是沿着一个主导方向排列的。我们可以用一个额外的向量通常称为切线或各向异性方向来表征这个主导方向。2.1 各向异性光照模型解析主流实现各向异性的方法大多基于对经典光照模型的扩展。这里我们重点分析两种最实用、在URP中最易集成的模型Kajiya-Kay模型和基于法线分布的修改模型。2.1.1 Kajiya-Kay模型经典而直观这个模型最初为头发渲染设计但其思想非常通用。它抛弃了传统的法线概念转而使用切线向量和副切线向量来计算光照。其核心公式简化后可以理解为高光强度不仅依赖于视线方向与反射方向的夹角更依赖于视线方向在由切线和副切线构成的平面上的投影。在Shader中我们通常这样计算获取表面的切线方向。计算视线方向与切线方向的点积。计算光线方向与切线方向的点积。使用一个基于这些点积的数学函数如sqrt(1.0 - dot(T, V)^2) * sqrt(1.0 - dot(T, L)^2) - dot(T, V)*dot(T, L)来得到一个各向异性的高光项。它的优点是概念清晰实现相对简单能产生明显的方向性高光条带。缺点是物理依据相对较弱参数调节需要经验且容易在掠射角产生不自然的效果。2.1.2 修改法线分布函数更PBR的方式现代PBR渲染的核心是微表面理论其中的法线分布函数决定了高光的大小和形状。对于各向同性我们常用GGX或Beckmann分布它们只依赖于法线与半角向量的夹角。为了实现各向异性我们需要修改这个分布函数使其同时依赖于半角向量与切线方向和副切线方向的夹角。以各向异性GGX分布为例其粗糙度参数从一个标量变成了两个沿切线方向的粗糙度和沿副切线方向的粗糙度。公式变得更为复杂但直观理解是高光区域从一个圆形各向同性变成了一个椭圆形各向异性。椭圆的长轴和短轴分别由两个方向的粗糙度控制。当两个粗糙度值不同时高光就会沿着粗糙度更低更光滑的方向被拉长形成各向异性外观。这种方式的优势在于它能更好地融入现有的PBR工作流能量守恒更好视觉效果也更接近真实世界的材质。在URP Lit Shader的基础上进行修改通常采用这种思路。2.2 URP Shader框架下的集成策略URP提供了Lit.shader作为标准PBR模板我们的目标是在其基础上“动手术”而不是从头重写一个管线。这里有几个关键决策点策略一自定义Lit Shader推荐这是最干净、最可控的方式。复制一份URP内置的Lit.shader和对应的Lit.hlsl文件到你的项目重命名如AnisotropicLit.shader。然后在关键的光照计算函数中通常是BRDFData结构体和LightingPhysicallyBased函数将法线分布函数替换为各向异性版本。你需要在Attributes和Varyings结构体中确保切线向量被正确传递。在片元着色器中构建切线空间矩阵TBN矩阵。修改BRDFData增加各向异性参数如anisotropy强度或roughnessT,roughnessB。重写DirectBDRF函数中的分布函数和几何遮挡函数。策略二使用Shader Graph Custom Function如果你更倾向于可视化编程Shader Graph是一个选择。你可以创建一个基于PBR Master节点的Graph然后通过Custom Function节点注入各向异性光照计算的核心HLSL代码。这种方式快速原型但调试复杂逻辑和深度优化时会受到一些限制。你需要自己编写那个计算各向异性高光的HLSL函数块。策略三后处理与屏幕空间技巧这是一种“作弊”但有时很有效的方案。通过渲染时输出世界空间法线和切线到一张G-Buffer如果开启了在后处理Pass中根据屏幕像素的法线、切线和深度信息重新计算或叠加各向异性高光。这种方法不修改物体本身的材质适用于为整个场景添加统一的各向异性“滤镜”效果但精度和正确性不如逐物体计算且与物体本身的粗糙度等属性结合困难。我的选择与理由对于追求高质量和集成的项目我强烈推荐策略一自定义Lit Shader。它虽然前期工作量稍大但能获得最好的性能控制、最正确的光照交互包括阴影、多光源、光照探针并且可以完美继承URP的所有特性如GPU Instancing, SRP Batcher。策略二适合快速验证效果或对Shader编码不熟悉的TA。策略三仅用于特定的全屏风格化效果。3. 基于修改NDF的各向异性Lit实现详解让我们深入策略一一步步构建一个可用的各向异性PBR Shader。我将以修改GGX法线分布函数为例。3.1 准备数据结构与属性扩展首先创建你的Shader文件例如AnisotropicLit.shader。在Properties块中我们需要增加控制各向异性的参数。Properties { // ... 原有的 _BaseColor, _BaseMap, _Metallic, _Smoothness 等属性 ... [Header(Anisotropy)] _Anisotropy(Anisotropy Strength, Range(-1, 1)) 0 _AnisotropyMap(Anisotropy Map (RG as Direction, B as Strength), 2D) white {} _AnisotropyRotation(Anisotropy Rotation, Range(0, 360)) 0 }_Anisotropy: 各向异性强度。正值通常表示高光沿切线方向拉伸负值表示沿副切线方向拉伸。范围-1到1提供了灵活性。_AnisotropyMap: 一张贴图用于逐像素控制各向异性。常见用法是RG通道存储一个方向向量通常是切线空间下的扰动方向B通道存储强度。如果不用贴图可以用常量。_AnisotropyRotation: 一个全局旋转角度用于旋转各向异性方向方便美术调节。接下来我们需要修改BRDFData结构体通常在BRDF.hlsl或你复制的对应文件中添加各向异性数据。struct BRDFData { float3 diffuse; float3 specular; float perceptualRoughness; float roughness; float roughness2; // roughness * roughness float grazingTerm; float normalizationTerm; // roughness^4 * 4.0 2.0 float roughness2MinusOne; // roughness^2 - 1.0 // 新增各向异性参数 float anisotropy; // 各向异性强度 float3 tangentWS; // 世界空间切线 float3 bitangentWS; // 世界空间副切线 };3.2 核心各向异性GGX分布函数实现这是最关键的数学部分。我们将替换标准的D_GGX函数。以下是各向异性GGX分布函数的一种常见实现// 各向异性GGX法线分布函数 float D_GGX_Anisotropic(float NdotH, float TdotH, float BdotH, float at, float ab) { // at: 切线方向的粗糙度平方 ab: 副切线方向的粗糙度平方 float a2 at * ab; float3 v float3(ab * TdotH, at * BdotH, a2 * NdotH); float v2 dot(v, v); float w2 a2 / v2; return a2 * w2 * w2 * INV_PI; // INV_PI 是 1/π } // 根据各向异性强度计算at和ab void GetAnisotropicRoughness(float roughness, float anisotropy, out float at, out float ab) { // 将各向异性强度映射到两个方向的粗糙度上 // 当anisotropy0时atabroughness // 当anisotropy0时沿切线方向更光滑(at减小)沿副切线方向更粗糙(ab增大) float aspect sqrt(1.0 - 0.9 * abs(anisotropy)); // 0.9是一个可调因子控制拉伸程度 at roughness / aspect; ab roughness * aspect; // 确保数值稳定 at max(at, 1e-4); ab max(ab, 1e-4); at at * at; // 转换为平方形式供分布函数使用 ab ab * ab; }然后你需要修改计算直接高光贡献的函数通常是DirectBRDF或BRDF函数。在计算高光项时不再使用D_GGX而是计算半角向量H。计算NdotH,TdotH,BdotH。调用GetAnisotropicRoughness得到at和ab。调用D_GGX_Anisotropic得到分布值。几何遮挡项G项和菲涅尔项F项通常可以暂时沿用各向同性的近似但更精确的做法是使用各向异性版本这会更复杂。3.3 集成修改URP光照循环在URP中主光照循环在Lighting.hlsl的UniversalFragmentPBR函数中。你需要确保在计算每个光源的贡献时使用的是你新的各向异性BRDF数据。主要修改点在于初始化BRDFData的地方和实际计算光照的地方。你需要将世界空间的切线和副切线可以从顶点着色器传递过来并在片元着色器中从法线贴图或各向异性贴图中扰动正确地赋值给brdfData.tangentWS和brdfData.bitangentWS。同时在片元着色器开始你需要根据属性或贴图计算最终的anisotropy值并存入brdfData.anisotropy。// 在片元着色器中 void InitializeBRDFDataAnisotropic(... , out BRDFData outBRDFData) { // ... 初始化原有的diffuse, specular, roughness等 ... // 计算各向异性 float anisotropy _Anisotropy; #if defined(_ANISOTROPYMAP) float4 anisotropySample SAMPLE_TEXTURE2D(_AnisotropyMap, sampler_BaseMap, uv); // 假设贴图B通道是强度RG是方向 anisotropy * anisotropySample.b; // 如果需要用RG通道扰动切线方向 #endif outBRDFData.anisotropy anisotropy; // 构建或获取世界空间切/副切 float3 tangentWS ... ; // 从input结构体或TBN矩阵获取 float3 bitangentWS ... ; // 通常 cross(normalWS, tangentWS) * input.tangent.w outBRDFData.tangentWS tangentWS; outBRDFData.bitangentWS bitangentWS; }最后确保在UniversalFragmentPBR中调用的是你修改过的、支持各向异性的BRDF计算函数。4. 美术控制与资源制作要点一个强大的技术方案必须便于美术人员使用和创作。各向异性材质的美术工作流需要特别设计。4.1 各向异性贴图制作规范一张好的各向异性贴图能极大提升效果和迭代效率。我推荐的格式是RG通道存储切线空间下的方向向量。这比存储单个角度值更直接也避免了在Shader中进行角度到向量的转换。通常(1,0) 表示原始切线方向(0,1) 表示原始副切线方向。你可以用Substance Designer或Photoshop生成这种向量图。例如一个径向刷痕的金属其方向图可能是从中心向外发散的向量。B通道存储各向异性强度。白色1.0表示完全强度黑色0.0表示无各向异性。这允许你在同一个材质上实现各向异性强度的变化比如金属部件边缘磨损处各向异性减弱。A通道可以留作他用比如作为高光染色Specular Tint的遮罩或者与粗糙度贴图配合进行更复杂的控制。在Shader中采样后你需要解码RG通道float2 direction anisotropySample.rg * 2.0 - 1.0; // 从[0,1]映射到[-1,1] float3 modifiedTangent normalize(float3(direction.x, 0, direction.y)); // 假设在切线平面XZ上 // 然后使用这个modifiedTangent去扰动或替换原有的世界空间切线4.2 材质参数调节心法调节各向异性材质时遵循一个顺序可以事半功倍先定基础后调特色首先忽略各向异性强度设为0像调节普通PBR材质一样把基础色、金属度、粗糙度调到满意的程度。确保基础质感是对的。强度由弱到强将各向异性强度从0开始慢慢增加观察高光形状的变化。通常0.2到0.6的范围就能产生明显且自然的效果。过高的值如0.8会导致高光变得极其细长和刺眼除非是做非常风格化的效果。结合粗糙度理解各向异性效果在中等粗糙度表面最为明显。如果表面非常光滑粗糙度接近0高光本身就很锐利各向异性的拉伸效果可能难以察觉如果表面非常粗糙高光弥散各向异性效果也会被模糊掉。尝试在粗糙度0.3-0.5的区间进行主要调节。善用旋转_AnisotropyRotation参数可以快速旋转高光条纹的方向而无需重新制作贴图或修改模型UV。这对于将同一材质应用到不同朝向的模型部件上非常有用。观察掠射角各向异性效果在掠射角视线几乎平行于表面观察时最为强烈。在调节时多旋转摄像机到掠射角度查看效果是否自然避免出现断裂或不连续的高光。实操心得对于拉丝金属我通常将各向异性方向设置为沿UV的U方向即模型表面的主要拉伸方向强度在0.4左右并配合一张轻微噪点的粗糙度贴图来打破重复感。对于头发或毛发各向异性方向通常沿着发丝的方向需要模型提供正确的切线强度可以更高0.6-0.8并且高光颜色有时会略微偏离基础色以模拟凯尔效应。5. 性能考量与移动端适配在URP中增加各向异性计算不可避免地会增加Shader的ALU算术逻辑单元指令数。在性能敏感的平台上如移动端或VR需要进行权衡和优化。5.1 性能开销分析与优化策略主要开销来自额外的向量运算计算TdotH,BdotH以及各向异性分布函数中更复杂的数学运算。可能的贴图采样如果使用各向异性贴图增加了一次纹理采样。更复杂的BRDF计算各向异性的G项和F项如果也实现开销会更大。优化策略质量分级为不同平台或画质等级准备不同的Shader变体。在低端移动端可以完全关闭各向异性回退到标准Lit。在中端可以使用简化版的各向异性模型如一个更廉价的近似公式。简化计算可以考虑使用一个预计算的查找表来近似各向异性分布函数但这会引入纹理采样需要权衡。如果各向异性方向变化不大可以在顶点着色器计算切线/副切线而不是在片元着色器。关闭或简化各向异性对阴影和间接光光照探针的影响。通常直接高光的各向异性是最重要的。贴图优化确保各向异性贴图使用适当的压缩格式如BC5/DXT5NM用于方向BC4/DXT5用于强度并且尺寸合理。对于远处物体可以使用更低分辨率的贴图或完全不用贴图。变体剥离使用shader_feature而不是multi_compile来定义各向异性相关的关键字这样Unity只会编译实际材质用到的变体到游戏包中减少包体和运行时内存。5.2 移动端实战注意事项在Android/iOS的URP项目中使用各向异性Shader要格外小心精度问题移动端GPU尤其是OpenGL ES对half精度中精度的支持更好。确保在片元着色器中对于颜色和HDR值使用half3对于向量点积、法线等使用half精度计算除非必要才用float。不必要的高精度是移动端的性能杀手。分支与循环各向异性计算中尽量避免动态分支。如果基于_Anisotropy强度为0时想跳过计算使用lerp混合可能比if语句在移动端更高效但这取决于具体GPU架构需测试。测试测试再测试必须在目标真机设备上进行性能分析。使用Unity的Frame Debugger和Profiler查看GPU耗时重点关注使用了各向异性材质的Draw Call。如果某个复杂模型使用了各向异性材质导致GPU时间激增考虑是否能用更简单的模型或降低材质复杂度。备用方案准备一个备用的标准Lit材质球。对于中低端机型可以通过代码在运行时动态替换材质确保帧率稳定。6. 常见问题与调试技巧实录即使按照步骤实现你也可能会遇到各种问题。以下是我在实际项目中遇到的典型问题及其解决方法。6.1 高频问题排查表问题现象可能原因排查步骤与解决方案高光方向错误或混乱1. 切线数据错误。2. 各向异性方向贴图编码/解码错误。3. 世界空间切线计算错误。1. 在Shader中输出tangentWS作为颜色在Scene视图检查其方向是否与模型表面走向一致。使用Debug Mode - Tangents检查模型导入设置。2. 检查方向贴图。在PS中查看RG通道确认向量方向。在Shader中先输出原始采样值再输出解码后的方向向量。3. 确保在片元着色器中正确计算了副切线bitangentWS cross(normalWS, tangentWS) * input.tangent.w;这个tangent.w存储了手性非常重要。高光在边缘断裂或不连续1. 各向异性分布函数在数值不稳定区域如NdotH接近0。2. 粗糙度过低导致分母接近0。1. 在分布函数中加入极小值保护max(NdotH, 1e-5)。2. 对计算出的粗糙度参数at和ab进行钳制max(at, 1e-4)。3. 检查几何遮挡项G项是否与各向异性分布匹配不匹配可能导致能量不守恒和边缘artifacts。效果在特定视角下消失1. 视线方向与切线方向平行时某些模型计算失效。2. Shader中基于视角的Fresnel项掩盖了效果。1. 这是数学上的奇点确保所有向量在计算点积前都已归一化normalize。2. 尝试暂时禁用或简化菲涅尔项看各向异性高光本身是否正常。性能开销远超预期1. 使用了未优化的复杂函数。2. 在不需要的物体上使用了高精度计算。3. 变体数量爆炸。1. 使用简化版分布函数或预计算部分项。2. 通过Shader LOD或Quality Settings为不同物体设置不同精度的Shader。3. 审查Shader变体合并或剔除不必要的特性组合。与阴影/间接光不匹配阴影和全局光照GI计算仍使用各向同性模型。这是一个高级问题。对于阴影可以尝试修改阴影投射Pass的深度偏移或使用自定义的阴影接收Shader来模拟方向性。对于GI通常接受这种不匹配因为间接光本身比较柔和各向异性影响较小。如果必须修正需要修改光照贴图烘焙或光照探针采样相关的Shader代码工作量巨大。6.2 调试与可视化技巧当效果不如预期时将中间变量可视化是最高效的调试手段。颜色调试法在Shader的片元输出阶段临时将你想要检查的向量或标量作为颜色输出。// 检查切线方向 return float4(brdfData.tangentWS * 0.5 0.5, 1.0); // 映射到[0,1] // 检查各向异性强度 return float4(brdfData.anisotropy.xxx, 1.0); // 检查方向贴图采样 return float4(anisotropySample.rgb, 1.0);在Game视图或Frame Debugger中查看可以立即发现向量是否归一化、数值范围是否正确。分离计算法暂时屏蔽掉各向异性计算只输出标准PBR结果确认基础光照是正确的。然后逐步启用各向异性计算的不同部分如先只启用方向计算再启用分布函数定位问题出现的具体步骤。参考对比法在同一个场景中并排放置一个使用标准Lit材质的球体和一个使用你自定义的各向异性材质的球体。使用相同的HDR环境贴图和方向光对比两者的高光形状和强度差异这能帮你快速判断各向异性效果是否生效以及强度是否合理。实现各向异性光照是一个从理论到实践不断调试的过程。它没有唯一的标准答案参数和模型的细微调整都会影响最终效果。关键在于理解其物理或艺术意图并掌握一套有效的调试方法。从简单的Kajiya-Kay模型开始尝试理解方向性高光的产生原理再过渡到更复杂的基于NDF修改的PBR方案是一个平滑的学习曲线。在URP的框架下动手实现一遍你对现代实时渲染管线和光照模型的理解一定会更深一层。