前阵子调一个Unity演示工程的时候,遇到一个特别典型的问题:场景里放了一块拉丝金属,平行光强度已经调到很高,可金属表面的高光始终“闷闷的”,亮部发灰、暗部糊成一片,怎么调都出不来那种通透感。排查到最后发现,问题根本不在灯光参数上,而是这张金属环境反射用的HDR贴图在URP管线里压根没有正确解码。这里顺手把HDR贴图在URP光照链路里的使用心得完整梳理一遍,涉及数据原理、导入配置、烘焙开关、Shader解码和实际调试,希望对被类似问题卡住的朋友有帮助。
1. HDR贴图在URP光照链路里的真实角色:为什么反光总是灰蒙蒙的
1.1 先从一次灰蒙蒙的材质反光说起
那次调试里,金属球用的是标准PBR流程:BaseMap是一张深色的金属纹理,Smoothness调到0.85左右,环境光通过反射探针提供。我最初怀疑是反射探针没生效,打开Frame Debugger看,反射探针确实参与了渲染,甚至能看到探针捕捉到的Cubemap画面。但金属球高光区域亮度就是上不去,像是被什么东西“掐住”了。
后来检查了工程颜色空间,发现项目用的是Linear没问题,又检查了URP Asset的HDR设置,也是开着的。折腾到最后,我把反射探针的那张Cubemap贴图重新导入了一遍,取消了纹理导入面板里的sRGB勾选,重新拉进场景,高光立刻亮起来了。这个问题的根源很简单:HDR贴图本身是线性数据,sRGB选项默认开着,Unity在采样时把它当成sRGB再解码了一次,相当于把亮度信息做了两次非线性转换,亮部被压缩,高光自然就“闷”了。
这类问题在URP光照链路里并不少见,因为HDR贴图不止用在一个地方。很多人以为HDR贴图只是“一张特别亮的图片”,实际上它是整个基于物理光照模型的数据基础。
1.2 URP中HDR贴图的四个主要落点
在URP光照流程里,HDR贴图通常以四种形态出现:
- 反射探针的Cubemap:天空盒、环境反射、金属表面的高光反射源。
- 自发光贴图:材质Emission通道使用的HDR贴图,依赖HDR颜色输出光照信息。
- 烘焙光照贴图:开启HDR Lightmap后,光照贴图以浮点格式存储入射光照,动态范围远大于LDR。
- IES光轮廓贴图:模拟真实灯具的配光曲线,HDR数据可以让光斑的衰减层次更真实。
另外还有一类容易被忽略的是摄像机HDR渲染目标。URP支持HDR渲染纹理,这个和材质贴图不是同一个东西,但本质思路一致:渲染中间数据用浮点精度保存,避免亮部在中间环节被裁剪。光靠LDR,超过1.0的亮度全变成白色,后期Bloom和Tonemapping都无从谈起。
1.3 超过1.0的亮度才是HDR的灵魂
理解HDR贴图,关键要抓住“动态范围”这个词。普通LDR图片每个通道的值范围是0到1,超过1.0就全白,低于0就全黑。而HDR贴图使用浮点数值,常见格式包括RGBA Half Float、EXR、HDR等,可以记录0.5、1.0、2.0、4.0甚至更高的亮度信息。
PBR算法本身就是吃线性亮度的,例如反射BRDF一定会在某个角度上把入射亮度乘以高光强度系数,如果入射亮度被限制在1.0以内,任何角度的高光都不可能超过1.0,整个画面层次就会非常“平”。HDR贴图的出现,就是为了给PBR的计算过程提供一个足够宽范围的输入信号。后面Tonemapping阶段再把这个宽范围映射到显示器能显示的0到1区间,这个过程中亮部细节才可能被保留或者重新分布。
2. 烘焙光照时的HDR配置与验证:光照贴图不是越亮越好
2.1 URP Lightmapping里的HDR开关与编码方式
URP的光照烘焙走的是Progressive Lightmapper,在Lightmapping Settings面板里有一项Lightmap Encoding,通常默认是Normal Quality或者High Quality,下面还跟着一个Ambient Occlusion选项,再往下就有个Direct/Indirect Lighting强度控制。真正决定光照贴图是不是HDR存储的,是这个Encoding设置。Normal Quality使用Low-Range,High Quality使用Medium-Range,这两种编码本质上是把HDR信息压缩到有限的亮度范围里。
如果你的项目要保留比较强烈的明暗对比,比如户外白天的阳光透过窗户在地板上产生非常亮的光斑,同时暗部阴影又需要保持层次,那就要选High Quality或者直接勾选HDR选项。系统会针对硬件的纹理格式来编码数据,PC平台通常走RGBA Half Float,移动平台则根据设备能力选择ASTC HDR或者精度更低的格式。
2.2 环境光反射探针的HDR处理
烘焙光照时反射探针的HDR设置也容易出问题。选中反射探针后,Inspector面板里有Importance、Intensity、Box Projection这些选项。实时模式反射探针默认捕捉的场景颜色如果是HDR渲染目标输出的,Cubemap会自动包含超过1.0的亮度值。需要注意勾选Realtime Texture,并且把Type设置为Realtime时,URP会在每次相关对象变化时重新捕捉场景,这个过程消耗不小。当场景以烘焙GI为主时,建议直接用Baked模式,探针Capture出来的Cubemap会存成HDR格式,作为材质反射的来源。
环境光部分的调节还有一个点:Environment Reflections的默认来源是Skybox。如果用的是一张LDR天空盒但颜色偏灰,反射效果看起来就会脏脏的。换一张HDR的天空环境贴图,或者直接把反射来源改成自定义Cubemap,实际改善比调半天灯光强度都明显。
2.3 用Frame Debugger验证烘焙HDR数据的真实有效
我自己习惯的验证方式很简单:打开Window > Rendering > Frame Debugger,选中一个使用反射探针的物体,查看它的Draw Call里绑定的是哪张纹理,然后把纹理内容放大,看高光区域有没有超过1.0的像素值。在Frame Debugger的Texture查看器里,可以看到具体数值,如果高光区域像素值都停留在1.0附近,说明数据被裁剪过,要回头检查编码设置或者导入参数。
还有一种更直观的验证方法:在场景里放一个纯白高光球和纯黑哑光球,高光球负责反射环境,哑光球负责展示漫反射。如果两个球的明暗关系在调完HDR贴图后仍然显得平淡,多半是光照贴图编码的精度不够,或者是反射探针的强度设置太低。我通常会把反射探针的Intensity先拉到1.0,再配合HDR天空盒观察高光球,确认基础反射没问题后再调参数。
3. 从导入到解码:HDR贴图在URP材质里真正生效的前提
3.1 线性颜色空间:为什么sRGB勾选必须取消
前面说过的sRGB问题,这里单独展开讲。Unity纹理导入面板里有一个sRGB(Color Texture)选项,默认勾选。这张贴图如果被当成颜色贴图使用,引擎会在采样后就其从sRGB空间转换到线性空间,供光照计算使用。这个转换是必要的,因为纹理文件里存的是Gamma空间编码的颜色,显示器和美术工具都按这套标准工作。
问题是,HDR贴图不是普通颜色贴图。EXR、HDR这类文件里的数据本身就是线性亮度,不经过Gamma编码。如果导入时还勾选sRGB,引擎就会把一个线性数据当成sRGB再去解码,相当于做了多余的运算。结果是亮部被向着暗部压缩,高光区域失去层次,严重的甚至整体偏灰。所以凡是从外部导入的EXR、HDR格式文件,导入面板里务必把sRGB取消掉。
3.2 纹理格式选择:PC平台与移动端的取舍
HDR贴图导入时格式选择也很讲究。PC平台推荐使用RGBA Half Float,兼容性最好,Shader采样出来的是真正的Half精度浮点数据,动态范围足够。其次可以考虑BC6H压缩格式,这种格式专门用于HDR数据压缩,体积比RGBA Half小很多,但压缩会有轻微精度损失,适合环境贴图和光照贴图这种容错性较高的用途。
移动平台的选择思路完全不同。移动端GPU对浮点纹理的支持差异很大,ASTC HDR是目前比较稳妥的方案,设置成ASTC HDR 4x4或6x6都能在画质和性能之间取得平衡。需要注意的是,不是所有移动GPU都支持ASTC HDR,比如一些老的Adreno和Mali显卡支持度有限。在做多机型适配时,一定要在真机上验证,模拟器里看不出带宽消耗,更看不出兼容性问题。
这里整理一个简单的格式对照表:
| 使用场景 | PC推荐格式 | 移动端推荐格式 | 备注 |
|---|---|---|---|
| 反射探针Cubemap | BC6H / RGBA Half | ASTC HDR 4x4 | 体积与画质平衡较好 |
| 光照贴图 | RGBA Half / BC6H | ASTC HDR 6x6 | 精度不够时改用4x4 |
| 自发光贴图 | RGBA Half | ASTC HDR 8x8或RGBA Half | 取决于显存压力 |
| IES光轮廓 | RGBA Half | 降级为LDR也可 | 移动端酌情放弃HDR |
3.3 在Shader里正确采样HDR贴图
URP环境里,Shader Graph是大多数人首选的材质编辑器。采一张HDR贴图,直接拖一个Sample Texture 2D节点,在Texture Type下拉框里要注意:默认是Default,如果没有特别指定,Shader Graph会当成LDR贴图处理。把这一项的Type改成HDR,或者干脆使用Sample Cubemap节点采样反射探针的Cubemap,Shader Graph内部会调用Unity的HDR解码函数,确保数据不被错误处理。
如果是手写Shader,情况就要自己掌控了。URP的Lit shader里自发光通道的处理很简单:
half3 emission = SAMPLE_TEXTURE2D(_EmissionMap, sampler_EmissionMap, uv).rgb * _EmissionIntensity;其中_EmissionIntensity是一个HDR颜色属性,可以超过1.0。关键在于,SAMPLE_TEXTURE2D宏采样出来的数据本身是纹理数据,如果纹理被标记为sRGB,它会在PBS计算前自动转换。对于HDR贴图,导入设置里已经取消了sRGB,采样出来的就是线性数据,直接参与运算就没问题。
自定义反射探针采样时,URP提供内置的采样函数,一般这样写:
float3 reflectVector = reflect(-viewDir, normalWS); half4 envColor = SAMPLE_TEXTURECUBE_LOD(_ReflectionProbe, sampler_ReflectionProbe, reflectVector, 0).rgba; half3 reflection = DecodeHDR(envColor, _ReflectionProbe_HDR);这里的DecodeHDR是Unity内置函数,会根据纹理的HDR解码参数还原真实亮度。很多从Built-in管线迁移过来的团队,手写Shader时忘了这步,直接用.xyz读取,结果反射颜色一直发暗,就是这个原因。
3.4 自发光贴图与HDR强度调制
自发光贴图在URP里的应用同样离不开HDR。美术在Photoshop里画一张自发光贴图,通常每个通道不超过255,也就是数值上限1.0。放到材质里,哪怕EmissionIntensity拉到10,依然能获得较强的HDR效果,因为URP的Lit Shader会在后期把超过1.0的亮度交给Bloom管线处理。
实际项目里自发光和光照的配合往往比想象中复杂。比如一个屏幕发光面板,如果自发光贴图是LDR的,纯色区域亮度一致,扫过的高光层次丢失。我的经验是,自发光贴图尽量保存发光核心的亮度梯度,然后用材质里的EmissionIntensity做整体强度控制。这样既不会让贴图产生大量超白像素,又能保留HDR动态范围的可调性。
4. URP实际使用HDR贴图的坑位:期望和现实之间的差距
4.1 导入面板里那个“HDR”复选框到底管不管用
Unity的Texture Import面板在高版本的Inspector里会有一个HDR选项,很多人看到这个就直接勾上,认为贴图就变成HDR了。实际上这个选项在勾选后,编辑器会在导入阶段把LDR贴图编码成RGBM或者浮点格式,使普通贴图也能携带HDR信息。这个功能用于把普通贴图“升级”成HDR数据,但它没法替代真正的EXR/HDR源文件。
一旦勾选了HDR选项,同时又没有取消sRGB,解码逻辑就很容易错乱。我自己遇到的怪现象是,勾选HDR后贴图变得更暗,取消sRGB后恢复,换了几个组合之后就绕晕了。实际规律不复杂:
- 源文件是EXR/HDR:取消sRGB,不勾HDR,直接以浮点格式导入。
- 源文件是LDR但需要模拟HDR:勾选HDR,取消sRGB,让引擎生成解码参数。
- 源文件是LDR且只是普通颜色贴图:保留sRGB,不勾HDR,正常当LDR处理。
4.2 Tonemapping没设置好,HDR数据反而导致过曝
HDR数据进来之后,Tonemapping没跟上一样会翻车。URP Asset里默认勾选Tonemapping,选择Neutral或ACES。如果不做Tonemapping,HDR亮度超过1.0的部分会直接被显示为纯白,看起来就是一片死白,丢失所有高光层次。有人在场景里换了一张更亮的HDR环境贴图,反射高光瞬间变成一块白色亮斑,就觉得HDR贴图有问题,其实Tonemapping曲线压根没生效。
ACES这个曲线对高光压缩较强,适合多数游戏画面;Neutral相对温和,保留了更多暗部对比。我一般建议:户外场景用ACES,室内暖光场景用Neutral,然后根据目标画面微调Bloom的阈值。URP的Bloom阈值默认0.9,如果场景里HDR数据普遍偏亮,Bloom容易全屏泛白,可以适当提高阈值到1.2或1.5。
4.3 反射探针的LOD偏差问题
反射探针在URP里支持LOD偏差设置,这个参数影响物体表面反射的模糊程度。默认情况下,URP会为反射探针生成一系列LOD Mip,模拟不同粗糙度的反射效果。粗糙度越低的物体,采样越靠前的Mip,反射越清晰;粗糙度越高的物体,采样越靠后的Mip,反射越模糊。
实际项目里这个机制容易出问题:HDR Cubemap在导入时如果关掉了Mipmap生成,URP依然会强行生成Mip,但清晰度表现会很奇怪,金属球反射出现一层一层的断层。检查一下反射探针的Cubemap导入选项,确认Mipmap是开启的。另外,有些团队为了省显存,把反射探针Cubemap的尺寸压到128,结果远处高光反射一片糊,这也是润色时很难察觉但实际很影响质感的点。
4.4 IES光轮廓里的HDR信息被浪费
IES光配置文件本质是一张描述光强度分布的纹理,可以直接导入Unity当HDR贴图用。URP里通过Light组件上的Cookie来采样,配合IES Texture就能模拟真实灯具的光斑。很多人在这个环节容易忽略一个问题:IES贴图如果被压缩成LDR格式,光斑的中心亮度会被限制在1.0以内,边缘的衰减曲线虽然保留,但光比关系就完全错了。
实际项目里,如果IES贴图的格式选成RGB24,中心亮度对周围环境的相对关系会被破坏,看起来像一盏特别平的光。加上HDR编码后,中心区域保持高强度,外围暗部过渡自然,才能模拟出足够真实的体积光感。但这张贴图的尺寸通常不用太大,256x256已经完全够用,重点在于格式和sRGB设置。
5. 调试习惯与性能平衡:正确使用HDR贴图的长期经验
5.1 临时Shader调试Pass观察原始HDR值
遇到HDR贴图问题但一时判断不准时,我习惯写一个极简调试Shader,把采样结果直接输出成颜色,临时挂到材质上观察。这个方法比盯着Frame Debugger猜测更直观。一个调试Pass长这样:
Shader "Debug/HDRPreview" { Properties { _MainTex ("Texture", 2D) = "white" {} _Exposure ("Exposure", Float) = 1.0 } SubShader { Tags { "RenderPipeline" = "UniversalPipeline" } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float _Exposure; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert(Attributes IN) { Varyings OUT; OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); OUT.uv = IN.uv; return OUT; } half4 frag(Varyings IN) : SV_Target { half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv); return half4(c.rgb * _Exposure, 1.0); } ENDHLSL } } }把这个Shader挂到一个平面上,贴图指定为待检查的HDR贴图,调节_Exposure参数,看高光区域能不能通过乘法重新显现细节。如果乘到4.0以后依然一片白,说明贴图数据本身就是裁剪过或者解码错误。这个方法能帮你快速区分“数据问题”和“管线问题”,省下大把排查时间。
5.2 纹理尺寸与Mipmap的平衡控制
HDR贴图的显存消耗比普通LDR贴图高不少,因为浮点格式每个像素占用的位数更多。RGBA Half Float每个像素16字节,相比之下RGBA32每像素也才4字节。一张2048的HDR Cubemap六个面加起来,显存占用约为6 x 2048 x 2048 x 16字节,接近400MB,显然不现实。所以实践中一定要根据反射探针的实际影响范围来控制尺寸。
实时反射探针一般建议不超过512,远处环境反射用256,只有非常核心的展示物体才用到1024。配合Mipmap和LOD偏差,绝大多数画面效果不会被明显影响。烘焙反射探针可以用稍大尺寸,但也要在显存预算内做权衡。URP中反射探针支持LOD偏差,适当调低清晰度的反射细节可以换来可观的性能提升,画面观感主观上也不一定能察觉差别。
5.3 减少HDR纹理带宽消耗的实用组合
移动端性能优化是另一个重点。HDR纹理带宽消耗远高于LDR,尤其是ASTC HDR压缩率不够理想的情况下。减少带宽消耗有几种常用手段:
- 采样频率高的材质,HDR贴图用较小的Mip级别,配合纹理串流功能按需加载。
- 反射探针Cubemap在动态物体上采样频率较高,可以考虑用SharedCubemap共享同一张低分辨率HDR贴图。
- 避免在Fragment Shader里多次采样同一张HDR贴图,提前计算好粗糙度对应的Mip级别,交给采样器处理。
移动端还有一个容易随机闪退的问题:如果贴图格式在目标GPU上不支持HDR压缩,Unity会回退到RGBA Half甚至RGBA32,显存爆炸是小事,更麻烦的是有些机型驱动会直接不显示。严格来说,不是所有设备都支持ASTC HDR,线上之前我遇到一款中端机,反射探针的HDR Cubemap在部分Android机型上花屏,最后是把Cubemap尺寸降到256并换用RGBA Half才稳定。做移动端适配时,这个兼容性测试一定不能省。
5.4 用最朴素的测试场景验证HDR效果
最后分享一个我自己坚持了很久的习惯:工程里永远保留一个朴素的测试场景,里面就放一颗标准高光球、一颗哑光球、一个棋盘格平面,还有一盏可调强度的平行光。每引入一张新的HDR贴图,先在这个测试场景里检查反射高光的层次和明暗关系,再决定要不要放进正式场景。很多HDR贴图从源文件看很惊艳,导入Unity后因为色彩空间、压缩格式或者Tonemapping设置不当,效果直接降低一个档次,先在测试场景里排查一遍,能省掉后续正式场景里的无数返工。
这个测试场景的好处是排除了其他灯光的干扰,能一张贴图对应一套反射表现。用习惯了之后,你基本扫一眼高光球的亮部过渡,就能判断出当前HDR贴图的动态范围是否充足、反射探针是否正确捕捉了环境光照。这套流程不复杂,但确实是反复踩坑之后最有效的防御手段。