1. 项目概述为什么PBR是渲染的“通用语言”如果你在Unity里做过一段时间的渲染从卡通到写实从风格化到次世代最终大概率会撞上“PBR”这个词。它就像一个渲染领域的“普通话”无论是AAA大作还是独立精品无论是角色皮肤还是生锈铁皮大家都在用这套标准来沟通“这个材质看起来应该是什么样”。但说实话很多教程要么一上来就扔给你一堆复杂的BRDF公式要么直接让你拖一个Standard Shader了事中间那层窗户纸始终没捅破。我自己也是从Standard Shader的“玄学调参”阶段过来的总觉得金属度、光滑度这些滑块背后有某种神秘力量。直到后来硬啃理论、动手写Shader才真正理解PBR不是魔法而是一套严谨的、基于物理规律的“翻译规则”。它把现实世界中的光线与材质交互比如光的能量守恒、微表面散射翻译成计算机能理解的几个核心参数如基础色、金属度、粗糙度。掌握这套规则你就不再是材质的“调参赌徒”而是能精准预测和创造视觉效果的“材质设计师”。这篇内容就是带你从Shader代码的层面亲手搭建这套“翻译规则”。我们不依赖Unity内置的Standard Shader黑盒而是从一个最基础的漫反射Shader开始一步步添加高光、引入微表面理论、实现能量守恒最终组装成一个功能完整的PBR Shader。目标是让你看完之后不仅能写出自己的PBR Shader更能透彻理解每一个参数、每一行代码背后的物理意义和设计逻辑真正从“会用”变成“懂为什么这么用”。2. 核心理论拆解PBR的三大支柱在动手写代码之前我们必须把地基打牢。PBR不是一个单一技术而是由几个核心物理原理共同支撑的体系。理解它们你才能明白后续每一个计算步骤的意义。2.1 微表面理论一切细节的根源现实世界的表面即使用肉眼看起来光滑在微观尺度上也是凹凸不平的。微表面理论的核心思想就是把一个宏观的表面看作是无数个朝向随机分布的、理想光滑的微小镜面微表面的集合。宏观粗糙度 vs 微观法线我们平时在材质球上调整的“粗糙度”或“光滑度”本质上控制的是这些微表面法线方向的混乱程度。粗糙度越高微表面法线越散乱反射的光线也就越分散形成模糊的高光粗糙度越低微表面法线越一致反射光线方向越集中形成锐利的高光。微表面与几何遮蔽微表面之间会互相遮挡。当光线以一个倾斜角入射时部分微表面会被其他微表面挡住而无法被照亮阴影Shadowing同样反射出去的光线也可能被其他微表面挡住而无法进入人眼遮蔽Masking。忽略这个效应在掠射角观察时物体边缘会显得不自然得亮。注意微表面理论是PBR高光部分镜面反射的计算基础。后续我们要实现的法线分布函数NDF、几何函数G都是为了描述微表面的统计特性。2.2 能量守恒光不会凭空产生或消失这是物理渲染的铁律也是PBR Shader看起来“正确”的关键。对于任何一点入射的光线能量不考虑自发光必须等于出射的能量即被反射的漫反射镜面反射加上被吸收的转化为热或其他形式。在Shader中我们主要关注反射部分的能量守恒漫反射能量 镜面反射能量 ≤ 1.0这意味着如果一个表面有非常强烈、明亮的镜面反射比如光滑的金属那么它的漫反射部分就必须相应减弱因为光线能量主要被反射了没多少进入内部进行散射。传统的Phong或Blinn-Phong模型经常违反这个原则导致材质看起来像“自发光”一样不真实。在我们的PBR实现中我们会明确地用金属度等参数来控制这两部分能量的分配。2.3 菲涅尔效应角度的魔法菲涅尔效应描述了反射率随观察角度变化的规律视线与表面法线夹角越大即掠射角观察反射越强。你可以在湖边观察水面正下方看能看到水底反射弱远处看则主要看到天空的倒影反射强。在PBR中菲涅尔效应至关重要基础反射率在0度角垂直入射时材质的反射率称为F0。对于电介质非金属如塑料、木头、石材F0值很低约0.02-0.05。对于导体金属F0值很高0.5-0.9。金属工作流这正是Unity的Standard Shader所采用的“金属度-粗糙度工作流”的理论依据。我们用一个Metallic滑块0到1和基础色Albedo来共同确定F0。当Metallic0非金属时F0使用一个近似常量如0.04当Metallic1金属时F0直接取自Albedo颜色因为金属的反射是带有颜色的例如金子的反射是金色的。理解了这三大支柱我们就知道PBR Shader要计算什么对于一个给定的入射光方向、观察方向、表面法线、粗糙度和金属度我们需要基于微表面模型计算符合能量守恒和菲涅尔效应的最终出射光亮度。3. 工具与准备搭建你的Shader实验室工欲善其事必先利其器。我们不依赖Surface Shader或URP/HDRP的复杂框架而是从最基础的Unlit Shader模板开始构建这能让你看清每一个环节。3.1 创建基础Shader文件在Unity项目中创建一个新的Standard Surface Shader并删掉重写是一个方法但我更推荐从Create Shader Unlit Shader开始。把它命名为MyPBR.shader。然后清空SubShader里的Pass我们将从头编写。首先定义关键的属性块这是我们在材质检视面板中调节的入口Properties { _MainTex (Albedo (RGB), 2D) white {} _Color (Color, Color) (1,1,1,1) _Metallic (Metallic, Range(0,1)) 0.0 _Roughness (Roughness, Range(0,1)) 0.5 // 注意为了更符合物理直觉我们使用Roughness粗糙度而非Smoothness光滑度。 // 后续计算中可能需要将Roughness平方以得到α参数这能更好地分布高光。 }3.2 理解CG/HLSL代码结构我们的计算将在片元着色器Fragment Shader中完成。核心函数的结构大致如下fixed4 frag (v2f i) : SV_Target { // 1. 采样纹理获取基础信息 fixed4 albedo tex2D(_MainTex, i.uv) * _Color; float metallic _Metallic; float roughness _Roughness; // 2. 准备向量数据世界空间或切线空间 float3 worldNormal normalize(i.worldNormal); float3 worldViewDir normalize(_WorldSpaceCameraPos - i.worldPos); float3 worldLightDir normalize(_WorldSpaceLightPos0.xyz); // 对于平行光 // 3. 计算漫反射部分Lambert或Disney Diffuse float3 diffuse ...; // 4. 计算镜面反射部分Cook-Torrance BRDF // 4.1 计算半角向量H // 4.2 法线分布函数 D (GGX/Trowbridge-Reitz) // 4.3 几何函数 G (Smith/Schlick-GGX) // 4.4 菲涅尔方程 F (Schlick近似) // 4.5 组合成 specular (D * G * F) / (4 * NdotV * NdotL) // 5. 组合最终颜色应用能量守恒 // 非金属 finalColor (1 - F) * diffuse specular // 金属 finalColor specular (因为金属没有漫反射) // 通过 metallic 插值 return fixed4(finalColor, 1.0); }这个框架就是我们的施工蓝图。接下来我们逐一填充每个部分。4. 实战构建手写Cook-Torrance BRDF现在进入最核心的环节我们将把理论公式转化为实际的HLSL代码。我们选择业界最主流的GGX模型组合。4.1 实现漫反射项Disney Diffuse传统的Lambert漫反射diffuse albedo / π * NdotL在物理上并不完全准确尤其是在掠射角或与高光配合时。Disney现为BRDF领域的标杆提出了一种更优的漫反射模型能更好地与镜面反射项协同工作。我们在Shader中实现它float DisneyDiffuse(float NdotV, float NdotL, float LdotH, float roughness) { float fd90 0.5 2 * LdotH * LdotH * roughness; // 两个视角下的菲涅尔式衰减 float lightScatter (1 (fd90 - 1) * pow(1 - NdotL, 5)); float viewScatter (1 (fd90 - 1) * pow(1 - NdotV, 5)); return lightScatter * viewScatter; } // 在 frag 函数中使用 float3 diffuseTerm albedo.rgb * DisneyDiffuse(NdotV, NdotL, LdotH, roughness) * NdotL / UNITY_PI;这个模型考虑了光线入射角和观察角以及粗糙度的影响使得漫反射在边缘和粗糙表面有更自然的表现。4.2 实现法线分布函数GGX/Trowbridge-Reitz这是描述微表面法线分布的函数决定了高光的大小和形状。GGX模型能产生从锐利核心到长尾拖影的高光非常接近真实材质。float D_GGX(float NdotH, float roughness) { float a roughness * roughness; // 粗糙度平方得到α float a2 a * a; float NdotH2 NdotH * NdotH; float denom (NdotH2 * (a2 - 1.0) 1.0); denom UNITY_PI * denom * denom; return a2 / max(denom, 0.0000001); // 防止除零 }NdotH是宏观表面法线与半角向量光线与视线中间向量的点积。roughness越小函数峰值越尖锐高光越集中。4.3 实现几何函数Smith-Schlick-GGX几何函数用于模拟微表面间的遮蔽阴影效应。我们使用Schlick-GGX近似它需要分别计算光线方向和视线方向的几何衰减。float GeometrySchlickGGX(float NdotV, float roughness) { float r roughness 1.0; float k (r * r) / 8.0; // 对于直接光照的近似k值 float denom NdotV * (1.0 - k) k; return NdotV / denom; } float GeometrySmith(float NdotV, float NdotL, float roughness) { float ggx1 GeometrySchlickGGX(NdotV, roughness); float ggx2 GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; }这个函数在NdotV或NdotL很小时掠射角会返回较小的值从而减弱边缘过亮的现象。4.4 实现菲涅尔项Schlick近似菲涅尔项计算反射率。Schlick近似是计算F0和观察角度的函数效率高且足够精确。float3 F_Schlick(float cosTheta, float3 F0) { return F0 (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }这里cosTheta可以是LdotH一些推导中或VdotH更常见。F0是基础反射率这是关键。如何确定F0这就是金属工作流的精髓// 在 frag 函数中 float3 F0 float3(0.04, 0.04, 0.04); // 非金属的近似F0 F0 lerp(F0, albedo.rgb, metallic); // 金属则使用albedo颜色作为F0 float3 F F_Schlick(max(VdotH, 0.0), F0);当metallic0时F0是0.04当metallic1时F0就是albedo。albedo对于金属来说表示的是其反射颜色例如铜是(0.95, 0.64, 0.54)左右。4.5 组装BRDF与能量守恒合成现在我们把所有部分组合起来计算Cook-Torrance镜面反射BRDF并最终合成颜色。// 计算 Cook-Torrance BRDF 的镜面反射部分 float NDF D_GGX(NdotH, roughness); float G GeometrySmith(NdotV, NdotL, roughness); float3 F F_Schlick(max(dot(i.viewDir, halfDir), 0.0), F0); // 使用VdotH float3 numerator NDF * G * F; float denominator 4.0 * max(NdotV, 0.0000001) * max(NdotL, 0.0000001); float3 specularBRDF numerator / max(denominator, 0.0000001); // 能量守恒合成 float3 kS F; // 反射部分的比例就是菲涅尔项 float3 kD (1.0 - kS) * (1.0 - metallic); // 折射部分的比例且金属没有漫反射 float3 diffuseContrib kD * albedo.rgb * DisneyDiffuse(NdotV, NdotL, LdotH, roughness) / UNITY_PI; float3 specularContrib specularBRDF * NdotL; // 乘以光强和NdotL // 最终颜色假设白色平行光强度为1 float3 finalColor (diffuseContrib specularContrib) * _LightColor0.rgb;这里kD的计算是关键(1.0 - kS)确保了反射和折射部分之和不超过1能量守恒再乘以(1.0 - metallic)则彻底关闭了金属的漫反射。金属的albedo只参与镜面反射菲涅尔F0不参与漫反射。5. 光照与环境集成从单一光源到真实世界我们目前只处理了单个平行光。但真实场景包含环境光天空盒、间接光照等。一个完整的PBR Shader需要处理这些。5.1 处理环境镜面反射IBL间接光的高光部分通常使用Image-Based Lighting。简单实现是采样一个预过滤的环境立方体贴图Cubemap和一张BRDF积分查找贴图LUT。Unity内置的UNITY_SAMPLE_TEXCUBE和UNITY_SAMPLE_TEXCUBE_LOD可以帮助我们。// 在顶点着色器或片元着色器计算反射方向 float3 reflectDir reflect(-worldViewDir, worldNormal); // 根据粗糙度选择Mipmap级别 float mip roughness * UNITY_SPECCUBE_LOD_STEPS; float4 encodedIrradiance UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, reflectDir, mip); float3 envSpecular DecodeHDR(encodedIrradiance, unity_SpecCube0_HDR).rgb; // 结合菲涅尔项 float3 F FresnelSchlickRoughness(max(NdotV, 0.0), F0, roughness); float3 kS F; float3 specularIBL envSpecular * kS; // 最终颜色加入环境高光 finalColor specularIBL;FresnelSchlickRoughness是一个考虑了粗糙度的菲涅尔变体因为粗糙表面在掠射角的菲涅尔效应会减弱。5.2 处理环境漫反射辐照度图间接光的漫反射部分可以采样一张从环境贴图卷积得到的辐照度图Irradiance Map或者使用Unity提供的ShadeSH9函数来近似球谐光照。// 使用球谐函数计算环境漫反射 float3 ambientSH ShadeSH9(float4(worldNormal, 1)); float3 kD (1.0 - kS) * (1.0 - metallic); float3 diffuseIBL kD * albedo.rgb * ambientSH; finalColor diffuseIBL;ShadeSH9是Unity内置函数它根据法线方向返回预计算的球谐光照值这是一个非常高效的环境漫反射近似。5.3 多光源支持一个完整的Forward渲染Shader需要在Pass中使用#pragma multi_compile_fwdbase和#pragma multi_compile_fwdadd等编译指令并为每个额外光源执行一次相同的BRDF计算在额外的Pass中Blend One One进行叠加。这是体力活但框架和主光计算完全一致。6. 进阶优化与常见问题排查理论正确后性能和质量优化是下一个挑战。这里分享几个实战中的关键点。6.1 性能优化技巧近似计算在移动平台可以用half或fixed精度变量替代float。对于非关键的中间计算如某些pow运算可以寻找更廉价的近似公式。纹理优化将Metallic、Roughness和Occlusion环境光遮蔽打包到一张纹理的R、G、B通道中减少纹理采样次数。这就是常见的MRT或ORM贴图。分支优化避免在片元着色器中使用动态分支如if语句判断metallic 0。尽量使用lerp或数学运算来平滑过渡。我们之前用lerp(F0, albedo, metallic)就是很好的例子。查找表对于复杂的、计算昂贵的函数如完整的BRDF积分可以预计算结果并存储在一张2D查找纹理LUT中用(NdotV, roughness)作为UV去采样。Unity Standard Shader的BRDF LUT就是这么做的。6.2 常见视觉问题与调试当你写的Shader看起来不对劲时可以按以下步骤隔离问题问题现象可能原因调试方法整体过暗或过亮能量不守恒或未乘以π因子分别输出diffuseContrib和specularContrib检查值域。确保漫反射项除以了π。检查kD kS是否超过1。高光形状奇怪/有黑点NDF或G函数分母为0在max(denominator, VERY_SMALL_NUMBER)中加入极小保护值。检查NdotV、NdotL、NdotH是否做了max(dot(...), 0.0)保护。金属看起来像塑料F0设置错误输出F0的值。确保非金属时是~0.04纯金属时是albedo值应较高如(0.95, 0.64, 0.54)铜。检查lerp逻辑。边缘有亮边或黑边几何函数G不正确或菲涅尔项有误单独输出G项在掠射角观察其值是否平滑衰减至0。检查菲涅尔计算用的cosTheta是否正确。粗糙度变化不自然粗糙度参数未做重映射尝试将传入NDF和G的粗糙度进行平方roughness roughness * roughness这能使滑块调节更线性。无光照时全黑未添加环境光检查是否加入了ShadeSH9或环境贴图采样代码。一个非常有效的调试方法是分通道可视化。在return语句前临时用以下代码替换可以直观看到每个组成部分// 可视化法线 return fixed4(worldNormal * 0.5 0.5, 1.0); // 可视化粗糙度 return fixed4(roughness.xxx, 1.0); // 可视化菲涅尔项 F return fixed4(F, 1.0); // 可视化几何项 G return fixed4(G.xxx, 1.0);6.3 从理论到艺术的桥梁材质制作心得写好了Shader只是拥有了精准的“画笔”。要画出好画还需要好的“颜料”——即PBR材质贴图。这里有几个制作和使用的核心心得Albedo基础色贴图这是最重要的贴图。务必去除光照信息它应该是材质在均匀、无阴影的纯白光下的颜色。金属的Albedo是其反射色而非“固有色”。非金属的Albedo通常饱和度较低。粗糙度Roughness贴图它定义了表面的微观不规则程度。划痕、磨损、污渍、潮湿区域通常更光滑粗糙度低而灰尘、锈迹、未加工表面则更粗糙。制作时可以把它理解为高光“模糊度”的蒙版。金属度Metallic贴图这是一张黑白掩码图。纯白1表示该像素是纯金属如干净的刀锋纯黑0表示非金属如塑料手柄。中间灰度值应谨慎使用通常只用于金属表面薄薄的灰尘或氧化物过渡。纹理一致性Albedo、Roughness、Metallic这几张贴图在空间上必须对齐。例如Albedo上的一块锈迹在Roughness贴图上对应区域应该更粗糙在Metallic贴图上对应区域应该更黑金属性降低。最后再分享一个调试材质真实性的小技巧在场景中放置一个强度适中、颜色纯白的平行光关闭所有环境光和后期效果。观察你的材质球。一个正确的PBR材质在只有单一平行光的情况下其明暗对比、高光形状就应该看起来非常扎实、自然。如果这时看起来就很“平”或很“假”那么问题大概率出在BRDF计算或贴图本身上而不是复杂的光照环境。从简到繁一步步验证这是掌握PBR渲染最踏实的方法。