1. 从零搞懂PBR为什么它成了现代渲染的标配1.1 传统光照模型的痛点在哪里刚接触渲染那会儿我用的一直是Blinn-Phong那套光照模型。说实话做出来的东西乍一看还行但总觉得哪里不对劲——金属看起来像塑料塑料看起来又像纸片整个场景缺乏一种“真实感”。后来才明白问题出在传统模型对光与物质交互的简化过于粗暴。Blinn-Phong把光照拆成环境光、漫反射、高光三个部分每一项都是拍脑袋的经验公式。比如高光项它只关心半程向量和法线的夹角完全不考虑物体表面的粗糙程度对高光形状的影响。结果就是无论你渲染的是一块打磨过的铜板还是一块粗糙的砖头高光的衰减曲线几乎一样。这显然不符合物理直觉。更麻烦的是传统模型里漫反射和高光反射的能量不守恒。你把高光调亮物体总反射能量就凭空增加了看起来就像物体自己在发光。美术同学调参数的时候经常陷入“调了高光丢了漫反射调了漫反射高光又不对”的死循环。PBRPhysically Based Rendering基于物理的渲染要解决的就是这些问题。它的核心思路很简单用物理学的规律来约束光照计算让渲染结果在数学上就是能量守恒的、在不同光照环境下都是一致的。1.2 PBR的核心思想能量守恒与微表面理论PBR的基石是两个东西能量守恒和微表面理论。能量守恒好理解——一束光打到物体表面要么被反射要么被吸收要么透射过去。反射的能量加上吸收的能量加上透射的能量必须等于入射能量。在渲染里我们主要关心反射部分所以漫反射和高光反射加起来不能超过入射光能量。这就从数学上杜绝了“越调越亮”的问题。微表面理论稍微抽象一点。它假设物体表面在微观尺度上是由无数微小的镜面组成的这些微表面的朝向各不相同。当光线打到表面时实际上是在和这些微表面交互。表面越粗糙微表面朝向越混乱光线反射方向就越分散高光就越模糊表面越光滑微表面朝向越一致光线反射方向就越集中高光就越锐利。这个理论的美妙之处在于它用一个统一的框架解释了从镜面到粗糙表面的所有反射行为。你只需要调整一个粗糙度参数就能平滑地在各种材质之间过渡。1.3 URP管线下的PBR有什么不同Unity的URPUniversal Render Pipeline管线对PBR做了一些工程上的取舍。和内置管线相比URP更注重性能和跨平台兼容性所以在PBR的实现上做了一些简化。最明显的区别是URP默认使用金属度工作流Metallic Workflow而不是高光工作流Specular Workflow。金属度工作流只需要一张Albedo贴图、一张Metallic贴图和一张Smoothness贴图美术资源的管理成本更低。高光工作流虽然控制更精细但需要额外的Specular贴图在移动端上显存开销更大。另一个区别是URP的BRDF实现。内置管线用的是标准的Cook-Torrance BRDF而URP在移动端平台上会切换到一种近似算法牺牲一点精度换取性能。这个切换是自动的你不需要手动干预但了解这一点对排查渲染差异很有帮助。注意如果你在编辑器里看到的材质效果和真机上不一致很可能就是BRDF近似算法导致的。这种情况下不要怀疑自己的Shader写错了先检查一下目标平台的Shader变体。2. PBR核心参数与贴图通道的实战解析2.1 Albedo贴图不只是“颜色”很多人把Albedo贴图简单理解为物体的“颜色”这个理解不够准确。Albedo在物理上应该叫基础反射率Base Reflectance它描述的是物体表面对入射光的漫反射比例。这里有一个新手最容易踩的坑Albedo贴图里不应该包含任何光照信息。我见过不少美术同学直接把照片贴上去当Albedo用结果照片里自带的阴影和高光全被烘进了基础色里渲染出来的物体怎么看怎么脏。正确的做法是Albedo贴图应该是均匀光照下拍摄的、没有明显阴影和高光的“平光”图像。还有一个经验法则现实世界中的物体Albedo值很少超过0.8也很少低于0.04。煤炭大概在0.04左右新鲜雪大概在0.8左右。如果你在贴图里用了纯白1.0或纯黑0.0那基本可以断定是不对的。纯白意味着物体反射了所有入射光这在现实中几乎不存在纯黑意味着物体吸收了所有光那你就什么都看不见了。在URP里Albedo贴图的sRGB选项要勾上因为它是颜色信息需要从sRGB空间转换到线性空间参与光照计算。2.2 Metallic与Smoothness金属度工作流的双核心金属度贴图Metallic Map用单通道存储通常放在R通道或者A通道里。它的值只有0和1两种“极端”情况——要么是纯金属要么是纯非金属。中间值只用于金属边缘的过渡区域比如生锈的铁皮。为什么金属度不能随便取中间值因为金属和非金属的光学行为有本质区别。金属的折射率是复数光进入金属后会被自由电子吸收所以金属没有漫反射只有带颜色的高光反射。非金属则相反漫反射是主要表现高光反射很弱且通常是无色的。你如果把金属度设成0.5渲染器会同时计算金属和非金属的反射结果就是既不金属也不非金属看起来像塑料上镀了一层奇怪的膜。Smoothness贴图控制的是表面的光滑程度它和粗糙度Roughness是反过来的Smoothness越高表面越光滑高光越锐利Smoothness越低表面越粗糙高光越分散。在URP的Lit Shader里Smoothness通常存在Metallic贴图的A通道里这样可以节省一张贴图。这里有一个实操技巧Smoothness的数值不要全图均匀。现实中很少有完全光滑或完全粗糙的表面大多数物体表面都有微小的磨损、灰尘、指纹。你可以在Smoothness贴图里画一些噪点或者手印渲染出来的效果会真实很多。我试过在一个金属杯子上加了一点指纹的Smoothness变化整个杯子的质感立刻从“CG感”变成了“照片感”。2.3 Normal Map让平面“长”出细节法线贴图Normal Map是PBR里性价比最高的贴图。它通过改变表面法线的方向让光照计算以为表面有凹凸而实际上几何体还是平的。法线贴图有两种格式切线空间Tangent Space和对象空间Object Space。URP的Lit Shader默认使用切线空间法线贴图因为切线空间法线贴图可以复用在不同朝向的表面上。对象空间法线贴图虽然计算量稍小但只能用于特定形状的模型通用性差。导入法线贴图时有一个关键设置Texture Type要选Normal Map这样Unity会自动把贴图从sRGB转换到线性空间并且根据平台做适当的压缩。如果你忘了改这个设置法线贴图会被当成普通颜色贴图处理渲染出来的凹凸方向全是错的。还有一个常见问题法线贴图的Y轴方向。不同软件生成的切线空间法线贴图Y轴方向可能不同。Unity使用的是OpenGL风格的Y轴向上而有些软件比如早期的3ds Max生成的是DirectX风格的Y轴向下。如果你发现法线贴图渲染出来的凹凸是反的可以在法线贴图的导入设置里勾选“Flip Green Channel”来修正。提示法线贴图的强度不要调得太高。很多新手为了让凹凸更明显把Normal Scale拉到2.0甚至3.0结果渲染出来的表面像被锤子砸过一样。一般来说Normal Scale在0.5到1.5之间比较合理具体要看你的法线贴图烘焙时的强度设置。2.4 AO贴图环境光遮蔽的妙用AOAmbient Occlusion贴图描述的是表面某一点被周围几何体遮挡了多少环境光。在URP的Lit Shader里AO贴图会影响间接光的强度让缝隙和角落变暗增加模型的体积感。AO贴图在URP里通常存在Occlusion贴图的G通道里。它的值从0到10表示完全被遮挡1表示完全暴露。和Smoothness一样AO也不应该全图均匀否则就失去了意义。一个容易被忽略的细节AO贴图只影响间接光不影响直接光。也就是说如果一束平行光直接照到表面上AO贴图不会让这个表面变暗。这是符合物理的因为直接光没有被遮挡。但很多美术同学期望AO能像阴影一样工作这就产生了误解。2.5 参数速查表贴图通道sRGB典型值范围常见错误AlbedoRGB是0.04-0.8包含光照信息MetallicR否0或1取中间值SmoothnessA否0.1-0.9全图均匀NormalRGB否-1到1未设Normal Map类型AOG否0.3-1.0期望影响直接光3. 手写一个URP下的PBR Shader3.1 Shader骨架搭建URP下写Shader和内置管线最大的区别是你需要引入URP的Shader库并且使用URP提供的函数来获取光照信息。下面是一个最简PBR Shader的骨架Shader Custom/PBR_Lite { Properties { _BaseMap (Albedo, 2D) white {} _BaseColor (Base Color, Color) (1,1,1,1) _MetallicMap (Metallic, 2D) white {} _Metallic (Metallic, Range(0,1)) 0.0 _Smoothness (Smoothness, Range(0,1)) 0.5 _NormalMap (Normal, 2D) bump {} _NormalScale (Normal Scale, Range(0,2)) 1.0 _OcclusionMap (Occlusion, 2D) white {} _OcclusionStrength (Occlusion Strength, Range(0,1)) 1.0 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { 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 // ... ENDHLSL } } }这个骨架里Core.hlsl提供了基础的类型定义和工具函数Lighting.hlsl提供了URP的光照计算函数。注意RenderPipeline标签必须设为UniversalPipeline否则URP不会识别这个Shader。3.2 顶点着色器从模型空间到世界空间顶点着色器的任务是把顶点从模型空间变换到裁剪空间同时计算世界空间的位置、法线和切线struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float4 tangentWS : TEXCOORD3; }; Varyings vert(Attributes input) { Varyings output; VertexPositionInputs posInputs GetVertexPositionInputs(input.positionOS.xyz); VertexNormalInputs normInputs GetVertexNormalInputs(input.normalOS, input.tangentOS); output.positionCS posInputs.positionCS; output.positionWS posInputs.positionWS; output.normalWS normInputs.normalWS; output.tangentWS float4(normInputs.tangentWS, input.tangentOS.w); output.uv input.uv; return output; }这里用了URP提供的GetVertexPositionInputs和GetVertexNormalInputs函数它们会自动处理不同平台上的坐标空间差异。tangentOS.w存储的是副切线的方向符号用于构建切线空间到世界空间的变换矩阵。3.3 片元着色器BRDF计算的核心片元着色器是PBR的核心它要完成以下步骤采样贴图、构建切线空间、计算BRDF、累加光照。half4 frag(Varyings input) : SV_Target { // 采样贴图 half4 baseMap SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); half3 albedo baseMap.rgb * _BaseColor.rgb; half metallic SAMPLE_TEXTURE2D(_MetallicMap, sampler_MetallicMap, input.uv).r * _Metallic; half smoothness SAMPLE_TEXTURE2D(_MetallicMap, sampler_MetallicMap, input.uv).a * _Smoothness; half roughness 1.0 - smoothness; half3 normalTS UnpackNormalScale( SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv), _NormalScale); half ao SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, input.uv).g; ao lerp(1.0, ao, _OcclusionStrength); // 构建切线空间到世界空间的矩阵 half3 normalWS normalize(input.normalWS); half3 tangentWS normalize(input.tangentWS.xyz); half3 bitangentWS cross(normalWS, tangentWS) * input.tangentWS.w; half3x3 TBN half3x3(tangentWS, bitangentWS, normalWS); half3 N normalize(mul(normalTS, TBN)); // 计算视线方向 half3 V normalize(GetWorldSpaceViewDir(input.positionWS)); // 获取主光源 Light mainLight GetMainLight(); half3 L normalize(mainLight.direction); half3 H normalize(L V); // 计算BRDF half NdotL saturate(dot(N, L)); half NdotV saturate(dot(N, V)); half NdotH saturate(dot(N, H)); half VdotH saturate(dot(V, H)); // 漫反射项 half3 diffuseColor albedo * (1.0 - metallic); half3 diffuse diffuseColor / PI; // 高光项Cook-Torrance BRDF half3 F0 lerp(0.04, albedo, metallic); half3 F F0 (1.0 - F0) * pow(1.0 - VdotH, 5.0); half D D_GGX(NdotH, roughness); half G G_Smith(NdotV, NdotL, roughness); half3 specular (D * G * F) / (4.0 * NdotV * NdotL 0.001); // 累加直接光 half3 directLight (diffuse specular) * mainLight.color * NdotL; // 环境光简化版 half3 ambient SampleSH(N) * diffuseColor * ao; half3 finalColor directLight ambient; return half4(finalColor, 1.0); }这段代码里D_GGX和G_Smith是BRDF的两个关键函数下面会详细解释。3.4 BRDF三大核心函数拆解法线分布函数D描述微表面法线朝向的统计分布。GGX分布是目前最常用的因为它的“长尾”特性更符合真实材质的高光衰减half D_GGX(half NdotH, half roughness) { half a roughness * roughness; half a2 a * a; half NdotH2 NdotH * NdotH; half denom NdotH2 * (a2 - 1.0) 1.0; return a2 / (PI * denom * denom); }几何遮蔽函数G描述微表面之间的相互遮挡。Smith-GGX是最常用的组合它把几何遮蔽拆成视线方向和光线方向两部分half G_Smith(half NdotV, half NdotL, half roughness) { half k (roughness 1.0) * (roughness 1.0) / 8.0; half Gv NdotV / (NdotV * (1.0 - k) k); half Gl NdotL / (NdotL * (1.0 - k) k); return Gv * Gl; }菲涅尔项F描述不同角度下反射率的变化。Schlick近似是最常用的half3 F_Schlick(half3 F0, half VdotH) { return F0 (1.0 - F0) * pow(1.0 - VdotH, 5.0); }这三个函数乘在一起再除以4 * NdotV * NdotL做归一化就得到了Cook-Torrance BRDF的高光项。注意分母里的4 * NdotV * NdotL在NdotV或NdotL接近0时会趋向于0导致除零错误。所以我在代码里加了0.001的偏移量。这是一个常见的工程处理虽然会引入微小误差但避免了渲染伪影。4. 性能优化与常见问题排查4.1 移动端PBR的性能取舍在移动端上跑完整的Cook-Torrance BRDF性能压力不小。URP提供了一些优化选项关闭高光抗锯齿在URP Asset里可以设置MSAA级别移动端建议关闭或设为2x。PBR的高光边缘本身就有一定的抗锯齿效果MSAA的收益不如传统光照模型明显。使用近似BRDFURP在移动端会自动切换到一种简化的BRDF用更少的指令数完成计算。你可以在Shader里通过#if defined(SHADER_API_GLES)之类的宏来手动控制。降低贴图精度Metallic和Smoothness贴图可以用半精度甚至更低因为它们只影响光照计算的中间结果对最终颜色的精度要求不高。Albedo贴图建议保持全精度因为它是基础色。减少实时光源数量URP的Forward渲染路径下每个光源都会增加一次Pass。移动端建议只保留一个主光源其他光源用烘焙光照或者顶点光照代替。4.2 常见渲染问题速查表问题现象可能原因排查方法材质看起来像塑料Metallic值不对检查Metallic贴图是否全黑高光位置偏移法线贴图Y轴反了勾选Flip Green Channel表面出现条纹法线贴图压缩过度提高贴图导入质量暗部死黑AO贴图过暗调整AO强度或提亮贴图高光闪烁Smoothness过高降低Smoothness或加噪点颜色偏灰Albedo贴图sRGB未勾选检查导入设置金属边缘发白菲涅尔项计算错误检查F0的lerp参数4.3 实操心得那些文档里不会写的东西关于Smoothness的“反直觉”很多人以为Smoothness越高越好实际上大多数日常材质的Smoothness在0.3到0.6之间。只有抛光金属、玻璃、水面才需要0.8以上。我见过一个场景所有材质的Smoothness都设成0.9结果整个场景像被油泡过一样高光糊成一片。关于法线贴图的“强度”法线贴图的强度不是越大越好。如果你发现法线贴图渲染出来的凹凸感太强不要急着调Normal Scale先检查一下法线贴图本身是不是烘焙得太夸张了。好的法线贴图应该在Normal Scale为1.0时就有合适的凹凸感。关于AO贴图的“滥用”AO贴图不是阴影贴图它只能模拟环境光的遮蔽。如果你想要物体投射阴影应该用Shadow Map或者烘焙阴影。我见过有人在AO贴图里画了投影结果物体一动投影也跟着动穿帮得很明显。关于金属度的“边界情况”金属和非金属之间的过渡区域Metallic值可以取中间值但这个区域应该非常窄。如果你发现金属边缘有一圈奇怪的灰色很可能是Metallic贴图的过渡区域太宽了。建议在Photoshop里用曲线工具把Metallic贴图的对比度拉高让过渡更锐利。关于环境光的“补光”PBR场景里环境光对最终效果的影响非常大。如果环境光太暗金属会看起来像黑色塑料如果环境光太亮整个场景会显得很平。我通常会用一张HDRI贴图作为环境光源这样金属表面能反射出周围的环境真实感会强很多。在URP里你可以在Lighting设置里指定Skybox材质或者用Reflection Probe来提供环境反射。4.4 进阶方向从PBR到IBL直接光的PBR只是第一步。真正让PBR出效果的是IBLImage Based Lighting基于图像的光照。IBL用一张环境贴图来模拟周围环境对物体的光照贡献包括漫反射的辐照度贴图和镜面反射的预过滤贴图。在URP里IBL的漫反射部分通过SampleSH函数采样球谐函数实现镜面反射部分通过采样Reflection Probe实现。如果你想要更高质量的环境反射可以自己生成预过滤的Cubemap然后在Shader里手动采样。IBL的难点在于预计算。漫反射辐照度贴图需要对环境贴图做卷积镜面反射预过滤贴图需要按不同粗糙度做多级模糊。这些计算在运行时做太慢通常是在编辑器里烘焙好存成贴图。Unity的Reflection Probe会自动做这些预计算但如果你想要更精细的控制就需要自己写烘焙工具了。5. 从理论到落地一个完整的材质调优案例5.1 案例背景一把旧铁锤光说理论太干我拿一个实际案例来串一遍。假设我们要渲染一把旧铁锤锤头是生锈的铁锤柄是磨损的木头。锤头的材质分析铁是金属但生锈后表面覆盖了一层氧化铁氧化铁是非金属。所以锤头的Metallic贴图不能全白锈蚀区域要画成黑色。Smoothness方面铁本身比较光滑但锈蚀区域很粗糙所以Smoothness贴图要有明显的明暗变化。Albedo方面铁是银灰色锈是红棕色两者要有清晰的边界。锤柄的材质分析木头是非金属Metallic全黑。Smoothness方面木头本身比较粗糙但握持区域因为长期摩擦会变得光滑一些所以Smoothness贴图要在握持区域提亮。Albedo方面木头有纹理需要一张带木纹的贴图。法线贴图要体现木纹的凹凸和磨损的划痕。5.2 贴图制作的具体步骤第一步在Substance Painter里导入模型烘焙法线、AO、曲率等基础贴图。曲率贴图特别有用它可以自动识别模型的边缘和缝隙方便你画磨损效果。第二步用Fill Layer铺底色。锤头铺铁灰色锤柄铺木色。然后添加Mask用曲率贴图生成边缘磨损的Mask在边缘处露出底层的金属色。第三步画锈蚀。用Noise贴图生成锈蚀的分布然后用Levels调整对比度让锈蚀集中在凹陷处。锈蚀区域的Metallic要刷成0Smoothness要刷低Albedo要刷成红棕色。第四步画木纹。用Wood纹理生成器生成木纹然后叠加Noise做磨损。握持区域的Smoothness要提高模拟长期摩擦的效果。第五步导出贴图。Albedo、MetallicR通道、SmoothnessA通道、Normal、AOG通道分别导出然后在Unity里组装成URP的Lit Shader需要的格式。5.3 在URP里组装材质在Unity里新建一个MaterialShader选Universal Render Pipeline/Lit。然后把导出的贴图拖到对应的槽位Base MapAlbedo贴图勾选sRGBMetallic MapMetallic和Smoothness的合并贴图不勾选sRGBNormal Map法线贴图Texture Type设为Normal MapOcclusion MapAO贴图不勾选sRGB然后调整参数Metallic设为1.0因为贴图里已经画了锈蚀区域Smoothness设为1.0同理Normal Scale设为1.0Occlusion Strength设为1.0。5.4 光照环境搭建材质调好后光照环境也很关键。我通常会用一张室内HDRI作为环境光源强度设为1.0左右。然后加一个方向光作为主光源强度设为1.5角度调成侧逆光这样能突出锤头的轮廓和锈蚀的质感。如果发现锤头的锈蚀区域太暗可以适当提高环境光强度或者在锈蚀区域补一个点光源。但要注意补光不要破坏整体的光照一致性否则会显得很假。提示调材质的时候建议把Game视图的曝光固定住不要用自动曝光。自动曝光会让你的判断失准你以为是材质调好了其实是曝光在帮你“作弊”。在URP里可以在Volume里添加一个Exposure组件把Mode设为Fixed然后手动设置EV值。5.5 最终效果验证验证PBR材质是否调好有一个简单的方法把材质放到不同的光照环境下看。如果它在强光下不过曝在弱光下不死黑在暖光下不偏色在冷光下不发灰那基本就合格了。另一个方法是和参考照片对比。找一张真实铁锤的照片把你的渲染结果和照片并排放在一起看颜色、高光、粗糙度是否接近。如果差距明显就回到贴图里继续调整。我个人的经验是PBR材质调优是一个迭代的过程不要指望一次就能调好。通常需要来回调整三四次每次都在不同的光照环境下验证才能得到一个比较满意的结果。6. 写在最后一些零散但有用的经验PBR这东西理论看起来复杂但真正上手之后会发现核心就那么几个参数。难的是对真实世界的观察——你得知道真实的金属是什么样真实的木头是什么样真实的光照是什么样。我建议做PBR的时候手边常备一些参考照片随时对比。还有一个容易被忽略的点PBR不是万能的。有些风格化的效果比如卡通渲染、手绘风格用PBR反而做不出来。PBR适合写实风格如果你要做风格化渲染可能需要换一套光照模型比如liltoon那种卡通渲染方案。最后说一个调试技巧如果你怀疑BRDF计算有问题可以把最终颜色直接输出成NdotL或者NdotV看看这些中间量的分布是否合理。这个方法帮我排查过好几次法线贴图的问题——如果NdotL的分布看起来像噪声那多半是法线贴图的方向错了。PBR的学习曲线确实有点陡但一旦理解了能量守恒和微表面理论后面的事情就顺理成章了。我当初也是从Blinn-Phong一步步过渡过来的中间踩了不少坑但每次搞明白一个概念都有一种“原来如此”的快感。希望这篇笔记能帮你少走一些弯路。