Unity URP/HDRP与UE4局部光照PBR BRDF实现硬核对比

发布时间:2026/9/14 19:55:47
Unity URP/HDRP与UE4局部光照PBR BRDF实现硬核对比 三年前我在团队里遇到一个特别典型的场景美术同事从UE4那边导出一份金属材质参数想在Unity的URP管线里直接用结果调了整整两天目光所及之处全是塑料味。最后我去查了下两边PBR计算的核心代码发现问题根本不是美术手上的参数错了而是两个引擎在局部光照Direct Lighting这一层的BRDF实现本来就有微妙差异。这篇文章就打算把Unity URP、Unity HDRP和UE4三套PBR渲染的代码逻辑放到一起做一次硬核对比聚焦在局部光照这块不碰全局光照和间接光照因为Part1先讲清楚直接光后面才有基础谈环境光和其他高级特性。如果你正在做跨引擎Shader移植、材质效果对标或者想搞明白自己写的PBR Shader为什么在某条管线下表现不一样这篇内容应该能给你省下不少趟坑的时间。1. 三套渲染框架的宏观对比URP、HDRP与UE4各自要解决什么问题1.1 三套引擎管线的设计目标差异很多人把Unity URP、HDRP和UE4放到一起比PBR效果其实这个比较本身就有一个前提性问题这三个东西根本不是同一个维度上的同类产品。URPUniversal Render Pipeline的设计目标是通用性与平台覆盖它要同时兼顾移动端、WebGL、PC、主机所以它底层的PBR算法必须做一个很关键的取舍——在视觉精度和运行性能之间砍掉一部分保证画质下限足够高但上限不能和HDRP比。HDRPHigh Definition Render Pipeline则目标明确就是面向高端PC和主机走的是我要把物理正确性尽可能做满的路线支持更复杂的光照模型和更多特性的叠加。UE4本质上和HDRP的目标更接近但它是一套完整的游戏引擎材质系统、光照系统和后处理链路完全是自家架构不存在通用渲染管线这个中间层概念。这个定位差异直接决定了三者在PBR代码上的走向URP把所有可以预计算的中间结果全部压到最简能省的计算一律省能降到half精度的绝不保留float而HDRP和UE4则更愿意多花计算量去换视觉正确性。1.2 为什么从局部光照下手对比PBR的完整渲染链路很长包括直接光照、间接光照、全局光照GI、光线追踪和各类后处理但所有效果的地基都是从局部光照开始的。局部光照解决的核心问题是给定一个表面点的法线、位置、材质参数和一个光源方向这个点最终反射到观察者眼中的能量是多少。无论URP、HDRP还是UE4局部光照都是整条Shader代码里最高频执行的部分。一个典型的动态光源场景中一个像素可能要执行几十上百次光照运算主光源附加光源高光阴影过滤这里的性能和正确性直接决定最终画面结果。换句话说如果你把三套引擎的局部光照代码拉出来对比就能一眼看出各家在性能与精度的跷跷板上站在什么位置——URP会毫不犹豫地把法线贴图采样精度降为halfHDRP会老老实实用float计算完整的GGX而UE4则会用一套复杂的BxDF封装把各种表面模型统一到一个框架里。另外还有一个实际原因无论你要把Unity项目迁到UE4还是做得反过来第一步要解决的Shader移植问题一定是局部光照因为其它部分的改动基本都是围绕光照核心展开的。所以Part1先啃这块最硬的骨头。2. 直接光照BRDF的代码级拆解从公式到引擎实现2.1 PBR反射率方程与三套引擎的落点差异在用代码对比之前先把理论基石定下来。现代PBR的直接光照几乎都遵循统一反射率方程Reflectance EquationLo (kd * fdiffuse ks * fspecular) * Li * (NdotL)其中fdiffuse是漫反射项fspecular是高光项通常采用Cook-Torrance微表面BRDF模型来定义fspecular D(h) * G(l, v) * F(v, h) / (4 * NdotL * NdotV)这里D是法线分布函数Normal Distribution FunctionG是几何遮蔽项Geometry ShadowingF是菲涅尔项Fresnel。三套引擎对这套公式的态度明显不同。URP在移动端优先的考虑下把整个BRDF分成了两个分支标准PBR计算路径和移动端简化路径移动端路径把D项从GGX换成了Blinn-Phong的变体几何遮蔽项从Smith换成了更廉价的近似。HDRP则在这条方程上做了更科学的扩展加入了能量补偿项来修正多散射带来的能量损失举个例子光滑金属在多次弹射之后仍然保留的微弱高光HDRP会专门用一项来弥补。UE4的Deferred Shading模式下虽然也有DefaultLit模型但它更依赖一套复杂的Shader代码生成框架来管理各类表面模型。我用一张表来总结三套引擎对反射率方程核心项的落地情况引擎/管线漫反射模型D项法线分布G项几何遮蔽F项菲涅尔Unity URPLambert / Disney简化GGX / Blinn-Phong简化Smith / 近似Schlick近似Unity HDRPDisney带粗糙度影响GGXSmith高度相关Schlick近似能量补偿UE4Lambert / BurleyGGXSmith高度相关Schlick近似URP和HDRP、UE4之间的差距比一般人想象得大。差在哪主要差在漫反射项是否随粗糙度变化以及高光项的近似精度上。2.2 漫反射项从Lambert到Disney模型的取舍先看漫反射。Lambert漫反射是游戏渲染界最经典的模型公式简单到令人发指fdiffuse albedo / PIURP在移动端的路径里直接使用这个Lambert模型原因就是它便宜——只需一次纹理采样一次乘法。但在桌面级HDRP和UE4里纯粹的Lambert已经不够看了因为真实世界中粗糙表面的漫反射实际上会随着观察角度变化而变化尤其在半光滑表面上表现得很明显。Disney漫反射模型在Lambert基础上加上了粗糙度相关的修正项fdiffuse (albedo / PI) * (1 (FD90 - 1) * pow(1 - NdotL, 5)) * (1 (FD90 - 1) * pow(1 - NdotV, 5))其中FD90 0.5 2 * roughness * pow(NdotH, 2)。UE4在默认的DefaultLitBRDF中使用了这个模型HDRP同样在Lit Shader里做了类似处理因为这块的计算量虽然比Lambert高但换来了更柔和的边缘过渡和更自然的粗糙度响应。我在实际项目里做过对照测试一个粗糙度为0.6的塑料材质用URP Lambert和HDRP Disney分别渲染差异在高光边缘尤其明显。Lambert版会显得稍微灰一点因为它在掠射角上缺乏能量衰减处理换成Disney之后塑料的质感和边缘的高光衰减就自然多了。URP也不是完全没救。URP的Lit Shader在非移动端路径上使用了改良的漫反射项但代码结构上明显是能把计算塞到宏就一定用宏来切比如用_SPECULAR_SETUP这类宏来切换金属度和高光强度工作流。如果你搭的项目的目标平台是中高端PC可以强制URP走完整PBR路径效果会接近HDRP的水准但性能会明显下降。2.3 高光核心GGX法线分布函数的三种姿势漫反射差异可以通过调参数来弥补但高光项的差距一旦存在基本很难靠参数追平。因为高光的形状、衰减、边缘抖动全部由D和G两项决定。标准的GGX法线分布函数长这样D_GGX(NoH) alpha_sq / (PI * pow(NoH * NoH * (alpha_sq - 1) 1, 2))其中alpha_sq roughness * roughness。三套引擎都采用这个公式但精度的处理各不相同。URP在移动端路径会用TGP近似等廉价方案替代GGX原因是完整GGX需要大量平方根和幂运算移动端GPU的ALU压力会明显增大。HDRP在GGX基础上加了关键修正使用高度相关遮蔽height-correlated masking and shadowing的Smith G项确保高光在掠射角不会出现能量爆发。UE4的DefaultLitBRDF实现里GGX的写法和HDRP并没有本质区别但在代码组织上UE4会把D、G、F拆得非常细然后通过一个BxDFContext结构体在多个函数之间传递数据。这套封装让UE4可以在同一套光照循环框架内切换不同的光照模型代价是要理解它的封装结构需要一点时间。说到关键差异在具体代码上HDRP的做法很有意思。HDRP的GGX实现float D_GGX(float NoH, float roughness) { float a2 roughness * roughness; float d (NoH * a2 - NoH) * NoH 1.0; return a2 * rcp(PI * d * d); }这里用了rcp指令替换除法——直接关于性能的优化。URP的同样功能在移动端路径则会用fastPow或者直接交换成频率更低的简化版本。UE4的GGX实现从数学上讲和这个等价但UE4为了处理各向异性粗糙度会多带两个轴向上的roughness参数。2.4 Fresnel项的Schlick近似与金属度工作流菲涅尔项描述的是观察角度变化时表面反射率如何变化。全精确的菲涅尔公式是用折射率计算的但游戏引擎里统一使用Schlick近似F_Schlick(V, H) F0 (1 - F0) * pow(1 - VdotH, 5)F0是正视角下的基础反射率。这里三套引擎的做法出现了一个有趣的分叉Unity URP和HDRP默认采用金属度工作流金属的F0直接用albedo作为基础反射率即saturate(albedo * metalness)非金属则固定为0.04UE4同样采用金属度工作流但UE4的材质编辑器允许你直接指定高光颜色Specular来绕过金属度推算这一点的灵活性在调整特殊材质比如布料、瓷器时非常实用。实际代码中UE4的DefaultLit会这样写float3 F0 lerp(0.04f, BaseColor.rgb, Metallic);HDRP几乎是同样的逻辑但HDRP额外支持了高光颜色金属度混合模式虽然这对开发者来说增加了选择成本但对追求物理正确的工作流来说可调的维度更多。URP同样有这一区分但在默认情况下Meta Pass和ShadowCaster Pass里的处理比HDRP简化得多。3. 数据结构与参数传递SurfaceData、BSDF与UE4的节点编译链路3.1 URP的SurfaceData结构简单直接暴力传递把光照公式拆完之后下一步要解决的是数据从哪来。URP的做法是定义了一个标准的SurfaceData结构体从材质输入开始到光照计算结束所有需要的数据全部通过这个结构传递。struct SurfaceData { half3 albedo; // 反射率 half3 specular; // 高光颜色高光工作流时使用 half metallic; // 金属度 half smoothness; // 光滑度1-roughness half3 normalTS; // 切线空间法线 half3 emission; // 自发光 half occlusion; // 环境遮蔽 half alpha; // 透明度 half clearCoatMask; // 清漆层遮罩可选 };URP直接用half精度存储这些数据这对移动端友好但也意味着精度上限被卡死了。我在PC端用过URP强跑一些高精度效果输出画面上会有轻微banding就是因为URP内部从采样到计算全程用half量级哪怕你的目标平台完全能承受float计算。InputData结构则承载逐像素的几何数据包括世界空间法线、观察方向、切线坐标、阴影坐标、雾效参数等。URP把SurfaceData和InputData分开是有原因的InputData是要在光照计算前由顶点阶段插值而来的而SurfaceData完全由像素阶段的材质函数决定。这个设计与移动端GPU的渲染架构匹配——移动端更偏好把数据分类存放以减少带宽消耗。3.2 HDRP的BSDF结构为性能与扩展性做的双层封装HDRP的结构设计比URP深不少。它不叫SurfaceData而是叫BSDFData因为HDRP不仅处理PBR材质还要处理各类特殊效果材质毛发、布料、各向异性这些材质的BSDF模型各不相同所以需要一套更通用的数据传递框架。struct BSDFData { float3 diffuseColor; // 漫反射颜色 float3 fresnel0; // 正视角反射率 float3 normalWS; // 世界空间法线 float perceptualRoughness; // 感知粗糙度 float coatRoughness; // 清漆层粗糙度 float3 specularColor; // 高光颜色 float3 transmissionColor; // 透射颜色薄层透射时使用 };注意HDRP直接使用float精度这在PC上是合理的但在移动端支持上HDRP基本不承诺性能所以它也不会刻意去做精度优化。关键区别在于HDRP区分了diffuseColor和fresnel0这为后续再做多散射补偿和透射计算留好了数据入口。而在URP中一旦确定是金属度工作流漫反射颜色就等于albedo * (1 - metallic)你在Shader里看不到这个中间结果的单独存储。我当年从URP切到HDRP时最大的不适应就在这URP里你改一个Metallic值效果立竿见影HDRP里你得同时理解漫反射颜色和高光颜色是怎么从金属度推算出来的以及在不同材质特性如清漆层存在时这两者还会被进一步拆分。3.3 UE4的材质节点编译链路从蓝图到HLSL的转换魔法UE4的PBR代码结构最特殊的点在于它的代码生成机制。你在材质编辑器里连的那些节点最终会被编译成一段HLSL代码插入到引擎的光照循环框架中。而引擎内置的DefaultLit模型本质上是一个模板BxDF其代码路径由材质节点图生成的宏来控制。UE4的材质系统里有一个核心的CalcMaterialParameters环节它会把你在节点面板上连出的所有数据归一化处理成一堆参数float3 DiffuseColor MaterialParameters.DiffuseColor; float3 SpecularColor MaterialParameters.SpecularColor; float Roughness MaterialParameters.Roughness; float3 Normal MaterialParameters.Normal;这些参数在后续的DefaultLitBxDF内部会进一步被套到统一的BRDF函数中。UE4的架构优势在于它的材质系统层提供了比Unity更大的灵活度——比如你可以直接修改材质域为延迟贴花或体积光照代码路径会随之改变而在Unity的URP/HDRP中要改这些路径你必须写完整的Shader。UE4这种代码生成式架构的代价是调试难度急剧上升。当材质节点连错导致画面异常时你很难在引擎的shader中间代码里找到具体问题位置。相比之下Unity在ShaderLab里调试会更直观一些因为你看到的就是着色器源码本身。4. 多光源循环与光照聚合策略各引擎的主光源附加光源处理方式4.1 URP的前向渲染多光源循环与Forward方案局部光照不止是BRDF计算本身还有光照循环的结构。URP默认的前向渲染中UniversalFragmentPBR会被多个光源调用但主光源和其他附加光源的计算路径是拆开的。引擎用UNITY_LIGHT_LOOP宏控制循环次数在逐像素模式下附加光源数量通常控制在4个以内。URP在Standard管线中加入了Forward支持后光照循环从逐对象遍历变为基于Cluster的光源裁剪可以在同一个Shader里处理上百个光源。但这套方案的代码量很大URP为此专门实现了InputData和BRDFData在循环内的多次重建。每个附加光源都有一套完整的BRDF计算只是光源的衰减函数不同——点光源用平方反比衰减聚光灯额外加上角度衰减方向光则没有衰减。我实测过Mobile平台上Forward的效果性能确实比逐光源遍历好很多但在光源密集场景中还是需要谨慎控制Cluster的分配参数。4.2 HDRP的Cluster光照框架把光源数据组织成Tile/ClusterHDRP的光照循环和UE4有一定相似性它所采用的Cluster框架和Forward类似把屏幕分块后建立光源列表但HDRP的Cluster细分更彻底——它在XYZ三个方向上都做了光源范围剔除使得光源列表更紧凑。HDRP的代码里GetCountLightOnCluster来获取当前Cluster的光源数量然后循环内逐光源计算这背后是大量结构化的光源数据DirectionalLightData、PunctualLightData、AreaLightData等。HDRP还支持矩形面光源和圆面光源这两个光源类型在BRDF计算时需要额外处理。HDRP的LightLoop是被精心调过的把光源的衰减计算、阴影采样、BRDF计算拆成了多个独立的函数并且用大量#if宏来区分是否启用了阴影、面积光、光线追踪等特性。这种特性堆叠式设计是有代价的Shader变体数量爆炸HDRP工程编译一次Shader动辄几十分钟这是很多刚接触HDRP的人第一个崩溃点。4.3 UE4的延迟光照循环与Stencil Light分类UE4在默认情况下走的是延迟光照路径而Deferred Shading从根本上改变了局部光照的组织方式。UE4的DeferredLightPixelShaders.usf里会集中处理所有光源的BRDF计算但在最终的shader代码里它会通过StencilBuffer把物体按材质类型分类例如DefaultLit、SubsurfaceProfile、ClearCoat、Hair等每一类走各自的BxDF分支。UE4的延迟光照代码结构中GetDynamicLighting是一个核心函数它在循环内根据光源类型计算出LightColor、Direction、Radiance等参数后统一带入IntegrateBxDF。这个IntegrateBxDF函数是整个UE4光照计算的路由器它根据材质图层Layer分支到相应的BxDF模型。这套架构的最大好处是你在材质节点上切换Shading Model时光照循环框架完全不变只是进入不同的BxDF积分路径。最典型的就是你用ClearCoat模型给车漆材质加一层反射层时引擎会自动在BRDF计算中叠加一个次级高光项而代价则是额外的GPU开销。5. 实操心得与调试技巧Shader移植、参数对标与常见坑点5.1 从Unity URP迁到HDRP时的Shader改造清单三套引擎的PBR代码对比不是用来写论文的真正价值体现在迁移实践中。我梳理一份从URP到HDRP的常见改造清单把SurfaceData改造成BSDFData核心是补上fresnel0和diffuseColor的拆分计算。将所有half精度数据类型改为float避免PC端因为精度截断出现色带。替换光照主函数URP的UniversalFragmentPBR换成HDRP的LitEvaluate或BSDF_Evaluate两者在数据和函数签名上不兼容。重写采样法线的细节。URP里UnpackNormalScale是直接可用的HDRP则要求你自己做TransformWorldToTangent之类的处理。阴影和Ambient Occlusion的采样方式也有变动URP中SampleShadowmap自带PCF过滤HDRP则把过滤方式分散在光照主函数中。从一个刚上手HDRP的同事的实际反馈来看最耗时间的不是光照主函数而是那些看不见的辅助Pass——深度Normal Pass、Prepass、Motion Vector Pass这些在URP里几乎是隐藏的到了HDRP全部都要显式实现。5.2 万能对标方法用灰球金属球场景校准参数做跨引擎效果对标或者说服美术的参数没有问题最好的方法是在同一个场景里放置一套标准材质球——一个纯白漫反射球、一个粗糙金属球、一个光滑塑料球然后在不同引擎里用同样的光照条件渲染出结果并排对比。这时候你会发现三者参数虽然命名不同但在调好映射之后画面表现可以非常接近但不可能完全一致——因为高光形状和边缘衰减总会因为G项差异出现细微不同。具体调参上我习惯先固定UE4的参数基准因为大多数项目的美术以UE4为视觉标准然后到URP里反过来推Smoothness和Metallic。URP里Smoothness的映射和UE4的Roughness是1减去的关系这个大家都知道。但少有人注意的是URP默认把Smoothness直接当作Roughness的反向而UE4的Roughness还会经过一次remapping从线性空间转到感知空间也就是说同样的0.5数值在两边视觉上的高光锐度并不一样。我一般会在URP里做一次Smoothness 1 - Roughness * Roughness的修正才能贴近UE4的效果。5.3 光照模型的常见Bug排查与计算差异定位调试PBR效果时我一般从四个维度来定位问题这个思路可以复用不管你用哪套引擎先确认是否为数据传递问题。检查SurfaceData/BSDFData/MaterialParameters中的中间数据是否符合预期。常见坑是法线方向数据在切线空间和世界空间之间转换出错导致高光完全错位。确认精度问题。移动端经常出现half精度下的BRDF计算出现噪声斑块尤其是GGX中的除以PI操作一旦分母精度不够高光边缘会出现明显的阶梯条纹。试着把相关参数提升到float看问题是否消失。确认循环光照的累积误差。当多个光源叠加时Color和Radiance的归一化处理容易出问题导致过曝或灰蒙。特别是UE4延迟光照中光源的Radiance默认单位是cd/m²如果直接从Unity导过来的灯光强度数值没转换画面会很奇怪。最后检查阴影与光照之间的衔接。有时PBR看起来不对不是BRDF的问题而是阴影Mask传递到BRDF时的参数错误尤其在半影区域噪声会被放大到肉眼可见的程度。6. 常见问题速查表与避坑指南现象可能原因排查方向同一材质在UE4和URP中金属感差异大URP的F0与UE4的Specular处理不同对照双方的BaseColor和F0数值URP可尝试用Specular工作流HDRP高光边缘出现明显噪点half精度不足在HDRP中确认是否意外启用了移动端兼容配置UE4中出现灰尘感漫反射默认漫反射模型为Burley边缘能量偏大尝试把Shading Model改为DefaultLit并检查Roughness来源同样灯光强度下画面亮度差异巨大各引擎灯光强度单位不一致UE4默认使用物理单位Unity使用参考亮度需要按比例换算多光源场景下URP性能骤降Forward未启用或附加光源超过逐像素数量检查是否启用了Forward以及Cluster大小设置金属球在掠射角出现白光爆发G项未做高度相关遮蔽确认是否使用Smith高度相关遮蔽而不是廉价近似法线贴图效果在HDRP中完全失效法线坐标空间不匹配检查采样后是否经过TransformWorldToTangent处理抛开代码细节不谈对我个人来说三套引擎的PBR对比最值得记住的一点是没有绝对正确的PBR只有在不同硬件和不同美术目标下的恰当PBR。URP牺牲精度换来了移动端的可用性HDRP堆计算量换来更高阶的视觉上限UE4则用成熟的材质编排和代码生成机制来管理复杂度。做Shader移植和效果对标时不要指望一套参数通吃所有引擎而是要理解每一条管线在底层做了哪些取舍然后针对性地在参数层做映射补偿。这个逻辑理顺了你下次从UE4搬到Unity或者反过来从URP升到HDRP心里就有底了。局部光照这块搞定之后建议把Global Illumination和间接光照的差异也拉出来做一次对比那就是Part2该聊的内容了。