
URP 里做 PBR说实话最容易被低估的就是 Indirect Specular 的处理。这篇是下篇默认你已经把项目切到 URP、也能调出基础的金属/粗糙度材质了剩下的就是那些真正影响画面质感、又经常在文档里一笔带过的细节间接高光为什么发灰、法线贴图在哪些平台会反、反射探针的平滑度为什么和 roughness 对不上、以及 Shader Graph 里想自定义 PBR 时到底该动哪里。我会把这些展开讲透顺便把我实际项目里趟过的坑一起列出来。1. URP 的渲染路径和光照模式决定了 PBR 的底子1.1 前向渲染路径下PBR 能到几盏灯URP 默认跑的是前向渲染这意味着场景里每个物体的 PBR 计算直接发生在顶点/片元着色器里光源数量会直接决定指令数。URP 里有个很关键的配置项Forward Rendering Path下的Per Object Limit默认是 4 盏逐物体光源。它有什么用就是当一个物体同时被 4 盏以上的实时光照照射时剩下的光源会被 URP 用“逐物体光源列表PerObject Light List”的方式按贡献度砍掉一部分或者走Additional Lights的球谐SH近似。很多人做室内场景时遇到灯光一多墙面就开始出现一块块奇怪的明暗断层就是这个原因。PBR 材质很吃光照数量金属度越高高光对光源位置的敏感度就越高。到了 URP你没法像内置渲染管线老项目那样把一个物体上挂 8 盏点光源还期望它全都能出镜。最常见的补救方式是把Additional Lights模式从Per Vertex改成Per Pixel代价是性能明显下降或者用 Light Probe Group 把静态物体的间接光照提前烘焙好把动态实光留给主角和交互物还有一种做法是关掉部分光源的阴影只保留主光和最近一两盏补光的阴影。建议在项目早期就把光照数量和阴影开关定下来不然后面每加一个光源都要从头排查非常难受。尤其是 PBR 材质反射高光非常明确多一盏不该有的灯金属表面上的高光斑就会直接“穿帮”。1.2 延迟渲染路径URP 12 以后要慎用URP 从 12 版本开始支持延迟渲染。理论上延迟渲染对多光源场景更友好因为光照计算从物体循环改成了屏幕空间逐像素进行光源数量不再是瓶颈。但 URP 延迟渲染有几个限制长期困扰着实际项目不支持 MSAA抗锯齿只能依赖后处理对Decal和Terrain的部分特性支持不够完整半透明物体还是走前向路径等于你要同时维护两套光照设置。我的建议是如果你的目标平台是 PC并且场景光源数量经常超过 8 盏再考虑延迟渲染。移动端或者 WebGL 平台还是老老实实待在前向渲染延迟渲染的 G-Buffer 带宽开销在移动 GPU 上很容易把帧率拖垮PBR 本身计算的指令数已经不低了。1.3 光照模式选择Subtractive 还是 ShadowmaskURP 的 Mixed Lighting 模式里最容易被忽略的是Lighting Mode下拉框。默认的Subtractive模式下静态物体的间接光照会被烘焙但直接光照里的阴影是烘焙出来的硬阴影动态物只能受到主光的实时阴影。这套模式下PBR 金属表面很容易看起来“脏”——因为间接高光几乎全部来自烘焙光照贴图而烘焙图通常分辨率有限金属表面的反射细节基本是糊的。我更推荐Shadowmask模式。它会把阴影信息存到单独的阴影遮罩贴图里动态物体能同时吃到实时阴影和烘焙阴影。代价是多一张纹理采样和一个Shadowmask关键词但换来的 PBR 效果反而干净很多。金属表面的高光轮廓在阴影区域依然能保持清晰不会因为光照模式限制被吃掉一大截这在实际画面中的差异非常明显。2. 拆开 URP 的 PBR 模型金属工作流和高光工作流的取舍2.1 金属工作流为什么是 URP 默认URP 的LitShader 默认是金属工作流Metallic Workflow输入里只有Metallic和Smoothness两张核心贴图。这个工作流的物理基础是金属的反射率在可见光谱内普遍偏高而且反射颜色基本就是表面本身的颜色非金属则反射率很低反射颜色偏灰白。所以 URP 用一个 Metallic 值来描述“这个表面像金属还是像非金属”再用 Smoothness或者 Roughness来描述微表面粗糙度。想理解它在代码层面的行为最简单的思路是Metallic 值越高高光颜色越接近albedo本身的颜色Metallic 值接近 0高光颜色则接近Specular Color的固定默认值 4%。这就是为什么很多人第一次做金属材质发现金属球上看到的“反射颜色”不是环境贴图的颜色而是金属本身的颜色实际上这正是 PBR 正确的行为。金属表面反射的是自身颜色而不是简单地映出天空盒。2.2 高光工作流什么时候值得换URP 的LitShader 其实还内置了一套Specular Workflow只是默认隐藏了需要你在材质面板上把Workflow Mode从Metallic切到Specular。高光工作流多了一个Specular Color输入允许你独立控制非金属高光的颜色。它的优势是美术更容易直观地控制“高光偏黄还是偏白”不用像金属工作流那样通过调金属度间接影响反射颜色。但从物理正确性角度讲高光工作流更容易做出“破坏能量守恒”的材质。比如你把 Specular Color 调成纯白的同时又把 albedo 调到很亮这个材质在灯下就会爆亮。实际项目里我一般不推荐整场景切换高光工作流除非你做的是风格化渲染需要刻意偏离物理规律。正常 PBR 流程金属工作流就够用了。2.3 Smoothness 与 Mip 的隐性关联间接高光的清晰度PBR 里间接高光Image Based LightingIBL在 URP 中的实现核心是反射探针和天空盒被模糊成不同 mip level 的 Cube Map。粗糙度越高的表面采样的 mip level 就越高反射越糊。这个“采样哪个 mip level”在 URP Lit 中是这样处理的half perceptualRoughness SmoothnessToPerceptualRoughness(smoothness); half mip PerceptualRoughnessToMipmapLevel(perceptualRoughness); half4 color SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectDir, mip);问题在于URP 默认生成的反射探针 Cube Map 分辨率往往不高如果粗糙度跨度太大高光从清晰到模糊的过渡会非常生硬出现“分层”感。解决思路有两条把 Reflection Probe 的Resolution调高尤其是面对大面积金属物件时在材质里不要直接用平滑度贴图时让美术把粗糙度色阶过渡处理好避免极端的粗糙度跳变。另外要注意URP 里Smoothness纹理的采样方式默认是Alpha通道。如果你用 PNG 导出粗糙度贴图导出时很容易把透明通道丢掉导致材质全部变成 uniform smoothness。这个坑我见过太多次了所以建议团队统一约定 roughness 贴图必须带 alpha 通道导出或者直接用R通道并通过Smoothness Source改成Metallic Alpha。3. 基于 Shader Graph 自定义 PBR 着色器的实战3.1 为什么要从 Lit Shader 而不是 Unlit 开始最多的情况是项目需要一种 Lit 不支持的效果比如地表距离混合、雪覆盖渐变、或者流沙地面。很多人图省事直接用 Unlit Shader 自己在片元阶段算一堆光照结果做出来的东西在暗部表现特别平还经常和场景其他 PBR 材质不一致。我的建议是尽量在 URP Lit 的基础上做Shader Graph 的 Custom Function 节点或者直接复制 URP 的 Lit.shader 改一个变体。因为 PBR 最麻烦的部分——IBL 采样、阴影衰减、SH 光照、天空盒反射——URP 已经写好了你手动重写的成本非常高而且大概率没有原版稳定。在 Shader Graph 里可以通过Custom Function插入 HLSL 代码块处理一些需要逐像素拿到额外数据的逻辑。比如要做一个粗糙度随视距变化的材质可以在 Custom Function 里写void RoughnessDistance_float(float Smoothness, float3 WorldPos, float3 CameraPos, float DistanceFactor, out float OutSmoothness) { float dist distance(WorldPos, CameraPos); float roughness 1.0 - Smoothness; roughness saturate(roughness dist * DistanceFactor); OutSmoothness 1.0 - roughness; }然后把这个OutSmoothness接到Smoothness端口上。逻辑很简单但如果不走 PBR 节点这样一个简单的距离粗糙度变化你得自己实现所有光照非常不划算。3.2 细节UV 平铺、二次 UV、流图URP Lit 默认只支持一套 UV。做大型地形或者需要贴花混合时你往往需要第二套 UV 来叠加细节贴图或者控制流图方向。Shader Graph 里提供Tiling And Offset节点如果你需要把细节法线和主法线叠到一起可以这样主贴图采样用 UV0细节贴图用UV1模型烘焙的第二套 UV法线混合用Normal Blend节点选Reoriented Normal MappingRNM模式效果比简单的线性插值自然得多。Unity 的Normal Blend节点在 URP 下是支持的但要注意RNM 模式在低端移动设备上开销略高。如果你的目标是中低端手机建议还是用DefaultUnpack模式视觉差异在大多数场景下可以接受。3.3 Cull、深度和阴影的细节禁忌Shader Graph 里创建 PBR 材质时最容易被忽略的是Surface Options里的设置。比如把Surface Type设成Transparent后默认的Render Face是Front意味着模型背面完全不渲染。很多做头发的同学做双面渲染把Render Face改成Both但忘了开Receive Shadows结果头发在阴影里死黑一片。另外透明 PBR 材质默认会受到Depth Write关闭影响在移动端经常出现前后排序错误。建议是除非真的需要半透明玻璃效果否则不要让 PBR 材质走透明队列。PBR 的透明渲染本身在 URP 里就支持薄弱半透明阴影完全依赖 shadow caster pass而 URP 的 shadow caster 对透明物体的支持并不是开箱即用的需要额外处理。还有一个小技巧在 Shader Graph 里做“视差映射”时一句话提醒Parallax Occlusion MappingPOM在 URP 里没有内置节点要么下载第三方库要么通过 Custom Function 自己实现。不要试图用Parallax Mapping期望它和内置渲染管线里的效果一样URP 的那个节点在大多数版本里效果都很弱。4. 反射探针和环境光照的配合4.1 反射探针盲区间隔与缩放URP 的反射探针和内置管线一样基于ReflectionProbe组件但它有一个特别容易翻车的地方探针的Importance和Box Size设置不对会让金属物体反射出完全错误的环境。尤其当场景里多个探针重叠时URP 的混合权重计算方式在某些版本有 bug表现为探针之间出现一条明显的反射 “接缝”。经验做法是严格给每个探针设置Box Size不要让默认的 1000 米范围把整个场景都框进去Importance值只在室内局部使用室外的主探针建议设成最低优先级调试时打开Render Probe确认探针采样到的环境是否是预期的画面。4.2 天空盒对间接高光的决定性影响实际上很多 PBR 材质看起来“闷”不一定是材质问题而是场景天空盒太暗。URP 默认的环境光来自天空盒的Environment Lighting如果你把天空盒调得很暗或者用纯色间接高光也会跟着变暗因为反射探针在未放置时默认从天空盒采样。一个很容易忽略的问题是Skybox Cube Map 的 mip 模糊质量。URP 在运行时生成unity_SpecCube0时会把天空盒的 mip chain 一起生成出来默认采样质量在移动端会自动降级。这时粗糙金属表面上会出现一种“脏脏的噪点感”而不是均匀的模糊反射就是因为 cubemap 的 mip 采样精度太低。如果项目允许建议在场景里放置一个覆盖全图的Reflection ProbeType设为Baked把环境反射烘焙成自定义 cubemap。这样一个探针就能同时承担天空盒反射和局部环境反射质量会比运行时生成的天空盒反射高不少。4.3 Light Probe 对动态物体的 PBR 影响动态物体没有烘焙光照贴图只能靠 Light Probe 提供间接光。很多团队忽略了一点Light Probe 只包含漫反射间接光SH 球谐不含镜面反射的间接光。这意味着一个动态的金属球在亮场景里移动时它的漫反射会随着光照探针变化而变化但它的高光反射只能靠反射探针或者默认天空盒来提供。所以正确做法是动态金属物体必须在运动路径上布置Reflection Probe否则金属球走进室内后高光还是会映出室外的天空非常出戏。5. URP PBR 的性能优化细节5.1 SRP Batcher 对 PBR 材质的限制URP 开启SRP Batcher后材质的MaterialProperty会被批量上传到 GPU减少 CPU 端的绘制调用开销。但 SRP Batcher 对 Shader 有严格限制支持 SRP Batcher 的 Shader 里不能使用内置的MaterialProperty的逐物体修改方式比如material.SetColor后标记 dirty。实际项目里最典型的表现是你用了MaterialPropertyBlock来修改颜色或贴图SRP Batcher 可能会退回到非 batched 状态导致 DC 飙升。排查方式很简单在 Frame Debugger 里看SRP Batch的Break原因如果大量MaterialPropertyBlock被使用考虑把所有动态属性合并到一张纹理或者一个 float4 送到 Shader 里。PBR 材质本身就比 Unlit 贵如果再丢掉 SRP Batcher 的优化移动端很容易掉帧。所以在项目初期就要规范团队对MaterialPropertyBlock的使用频率。5.2 纹理压缩和 MipmapPBR 材质预算的大头全在贴图带宽上。URP 项目里我习惯默认开启纹理 mipmap尤其对带Smoothness通道的贴图没有 mipmap在高光反射时会出现严重的摩尔纹。但要注意Smoothness在 mipmap 下会默认做平均滤波导致远处物体的高光反射变“暗糊”。如果贴图需要保留高频细节建议单独给 Roughness/Smoothness 贴图关闭 mipmap或者采样时用SamplerState_Linear_Clamp代替默认的 Trilinear效果比开 mipmap 更好。移动端纹理压缩格式建议用ASTC 6x6iOS/主流 Android或ETC2兼容老设备不要直接用 RGBA32那种格式下带宽直接爆炸。PBR 材质需要的两张贴图albedo 和 metallic/roughness经常可以通过 RGB 通道打包成一张图来省带宽方法是R 通道存 MetallicG 通道存 RoughnessB 通道存 AOA 通道存 Smoothness 或者 Height。Shader Graph 里写一个Custom Function把Metallic、Roughness、AO从一个纹理里拆出来非常方便。这招在移动端优化里效果立竿见影能从 4 张贴图降到 2 张整体带宽和加载内存都降一半。5.3 不要让透明 PBR 材质进主队列URP 里透明 PBR 材质的 Overdraw 是很大的问题。尤其在粒子系统里如果你用了半透明粒子配合金属度即便粒子本身很小因为 Alpha Blend 需要把整个帧缓冲都读出来混合GPU 的 fillrate 开销会大幅增加。最好是从设计上减少透明 PBR 的数量。比如模拟玻璃效果时可以用一个不透明的模型加上 Cube Map 反射而不是用半透明混合去透出后面的物体如果需要液体表面则尽量用屏幕空间反射或者平面反射代替真正的折射和透射。6. 常见问题速查与排查思路6.1 法线贴图在某些平台是反的URP 在 Android、iOS、WebGL 等平台上法线贴图的 Y 通道方向可能和 PC 上不完全一致。原因是不同平台对UNITY_NO_DXT5nm的宏响应不同。DXT5nm 压缩的法线贴图会把绿色通道存放法线的 Y 分量而未压缩的格式则直接用 RGB 存。一旦方向反了模型表面就像“凹陷”变成“凸起”。排查方式在 Shader Graph 的 Custom Function 里输出法线贴图的 G 通道作为颜色看哪个平台反了。解决方式是在导入设置里把Texture Type设为Normal Map并勾选Create from Grayscale时注意翻转选项。如果跨平台仍不一致可以在 Shader 里对 Y 通道做一次条件翻转#if defined(UNITY_NO_DXT5nm) tangentNormal.y * -1.0; #endif6.2 反射贴图发灰或者完全看不见金属表面反射不到环境最常见原因是场景里没有任何Reflection Probe并且 Skybox 的反射强度被调成了 0。URP 的Environment Reflections默认是启用的但如果你把Source从Skybox改成了Custom而没有正确指定 Cubemap材质就会在间接高光处呈现完全黑色。另一种情况是Metallic值太低。金属工作流中 Metallic 接近 0 时间接高光颜色基本会被 albedo 的漫反射“盖掉”。这不是 bug而是 PBR 的能量守恒表现。6.3 粗糙度变化时高光出现断层前面提到过反射探针 mip 采样是离散的粗糙度平滑变化时如果探针分辨率不够高光过渡就会断层。解决办法是把探针分辨率调到 256 或更高同时把Smoothness贴图的对比度调低让粗糙度范围不要拉满。极端情况下可以考虑用Screen Space ReflectionURP 需要装 Renderer Feature 才能用它能在部分平台补足 PBR 的接触反射细节。6.4 URP 发布 WebGL 时 PBR 材质偏暗WebGL 平台的浮点精度普遍比较低URP 默认会使用mediump精度这会在高动态范围场景中导致 PBR 高光计算出现严重 banding 或暗部发灰。排查方式是在 Player Settings 里打开Use Graphics Settings如果还不行在 URP Asset 里把HDR关闭改用LDR一般能缓解如果项目对画质要求高建议 WebGL 用 URP 的Forward或者延迟管线在支持 WebGL2 的浏览器上运行但兼容性要单独测试。6.5 半透明材质阴影不对URP 对半透明材质的阴影支持本来就弱如果你在 Shader Graph 里启用了Alpha Clip或Alpha Blend阴影投射完全是另一套 pass。经常出现模型投射出完整的方形阴影即使贴图已经镂空得只剩几条线了。解决办法是给材质单独创建一个 ShadowCaster Pass在 Pass 里采样 Alpha 贴图通过clip()丢弃透明像素。Shader Graph 里可以通过Shadow Caster Pass的Alpha输入节点实现但要注意纹理采样坐标需要和主 Pass 保持一致否则阴影会和模型错位。7. 再进一步URP PBR 的扩展思路7.1 Screen Space Reflections 的替代方案URP 官方没有内置 SSR但可以利用Render ObjectsRenderer Feature 做一种“伪接触反射”把场景中高 Metallic 的物体提取出来在Blit阶段做一次屏幕空间求交把反射结果混入颜色缓冲。这套方案做起来复杂但对室内金属地面特别有效。如果你不想那么折腾可以用Planar Reflection做平面反射镜面地面早就够用了。URP 的 Planar Reflection 需要自己写官方没有内置核心思路是在反射平面处翻转相机再渲染一张反射纹理采样时按平面投影坐标取色。7.2 物理渲染的色域与曝光最后想单独提一个容易导致画面“脏”的隐性原因URP 的Post-processing里没有自动做色调映射的话PBR 材质在高光区域很容易过曝成纯白。推荐开ACES色调映射它的高光压缩曲线更适合 PBR 的高动态范围高光不会一刀切曝死而是有一种“柔和滚降”的感觉。曝光和色域的选择往往是团队做 PBR 项目时最后才发现的问题。材质调得再准如果后处理管线的色彩管理不对PBR 的大动态范围反而会变成画面噪点的来源。7.3 团队协作的标准给美术的 PBR 命名规范这不算技术但实际项目中很影响产出。PBR 贴图建议统一命名后缀_AlbedoRGBsRGB_MetallicR 通道_RoughnessG 通道_AOB 通道_NormalRG 压缩或者使用 ORM 打包方案 Occlusion-Roughness-Metallic 一张图。统一命名后可以使用一些自动导入工具根据后缀自动设置Texture Type和sRGB这样材质球不会因为某张贴图格式错了导致 PBR 结果不一致。我在实际操作中最深的体会是URP 下的 PBR 并不缺功能而是它的默认路径和参数更容易让开发者“不知不觉”走到性能或视觉的死角里。很多问题比如反射断层、光滑度过渡不自然、金属反射发灰回头排查一遍几乎全是探针设置、纹理压缩、色调映射这些外围细节在起作用。建议你在动手调材质之前先把项目里的URP Asset、反射探针、后处理链都定好基调这比在一个材质球上死磕半天要高效得多。最后提醒一个很多人忽略的小点Smoothness和Metallic的贴图数值范围建议美术在 DCC 软件里就锁在 0 到 1不要导入 Unity 后再拿Remap节点硬拽不然版本更新或者换管线时这些硬调过的数值会变得非常难维护。