Unity线性空间下移动设备上烘焙变暗问题处理

Unity线性空间下移动设备上烘焙变暗问题处理

不少研发团队曾反映,移动设备上场景感觉比平常开发的PC上要暗很多。为此,笔者通过大量的搜索、实验、方案对比,以及和美术进行频繁深入的沟通讨论,最终得出了行之有效的解决方案。整篇文章详细记录了笔者发现问题到解决问题的过程,非常值得借鉴!


前言

其实针对这个问题已经写了一篇很简单的填坑笔记了,但在整理本文的过程中,又想明白了一些之前没有想明白的技术问题,算是对线性空间下烘焙场景变暗的原因有了一个真正的理解,这也是整理和写博客的收益之一。


一、 问题确认

最近美术同学反馈在移动设备上场景感觉比平常开发的PC上要暗很多。其实之前观察到过这个现象,当时场景的烘焙效果还没完全确定,以为是不同的烘焙版本,或者设备屏幕亮度不同、色差等问题,一直没有怎么关注。

知乎有句名言——“先问是不是,再问为什么”,那么解决问题之前第一步要做的事情就是确认问题。

其实方法很简单,就是排除前面提到的不同烘焙版本、设备色差等问题,使用同一烘焙版本的场景,在移动设备上进行截图操作,然后传输到手机上在同一块屏幕下进行对比,发现场景亮度差别的确很大。
请输入图片描述
手机设备上的截图效果

请输入图片描述
PC设备上的场景截图效果


二、 原因排查

确认问题的确存在,而且很严重之后,我们先做一系列的排查实验来检查问题可能出现的原因。

1)首先在同一屏幕下对截图进行对比,观察是否是全屏变暗,发现使用实时光照的角色没有这个问题,因为对比的关系反而会觉得角色更亮(请忽略上面截图中的角色灰色问题,那个是因为在我的MX4 Pro上材质兼容性存在问题导致的),排除可能是某些错误的后处理导致全屏变暗的可能,基本确认是场景Lightmap相关的问题。

2)检查Unity Editor中切换到安卓平台,直接运行没有问题,但是如果加载AssetBundle包进行运行,就出现了和设备上一样的问题。推测可能和AssetBundle的打包相关,但是无法确认更多细节。

3)制作一个简单的Demo,使用非AssetBundle的方式进行打包,放置到手机上进行截图对比,发现也存在变暗的问题,排除单纯的AssetBundle打包导致的问题;

4)在Demo中,将我们自己的材质替换为Standard材质,进行打包对比测试,依然有变暗的问题,排除我们项目自身材质Bug。

5)因为我们在移动设备上使用了线性空间,因此怀疑可能是线性空间导致的,切换颜色空间进行对比实验,发现伽马空间下烘焙效果也有色差,但是没有线性空间明显。

经过这一系列的实验对照之后,我有点怀疑这是一个Unity的“特性(bug)”了。。。顺便说句题外话,我们使用的是比较新的Unity 5.5.4f版本,之所以从之前较为稳定的5.3.6版本升级上来,很重要的原因之一是我们想要在移动设备上使用线性空间。


三、 资料检索

问题的矛头指向了烘焙,又是Standard材质也会存在的问题,那么猜测网络上应该有不少问题反馈和相关资料,于是照常进行一波相关资料的检索和收集。

3.1 Unity Issue Tracker
首先怀疑是否是Unity的官方“特性(Bug)”,搜索了一下issuetracker,果然找到一个似乎相关的Bug汇报BAKED LIGHTMAP IS DARKER ON ANDROID PLATFORM COMPARED TO PC PLATFORM,说是Fixed in Unity 5.5.5,没提到线性空间的设置,我们7月份要测试也等不到5.5.5版本了,所以暂时先记录下,继续。

3.2 UWA问答
UWA的问答模块有一个帖子问过相关问题:Lightmap在PC上显示正常,但是转到Android平台上存在色差,颜色普遍偏暗。这里引用一下回答的内容:

一般来讲,有两种情况可能会导致色偏和亮度差异。

1)Unity烘焙的Lightmap是32bit的HDR图,而移动设备通常不支持HDR图(32bit per channel),会按照LDR图(8bit per channel)的形式进行处理,因此会出现色偏问题。因此我们建议:

  • 在移动平台下使用Mobile/Diffuse材质,可载入Standard Assets(Mobile) package获得。如果要获得更合适的效果,需要自行修改Lightmap的DecodeLightmap函数,该函数可在Unity\Editor\Data\CGIncludes\UnityCG.cginc文件中找到。需要说明的是,这种方法也不能达到与PC端完全一致的效果。

  • 如果需要PC和移动平台的显示效果一致,可以用图像编辑软体修改Lightmap為LDR格式,例如PNG(8bit per channel)。

  • 为了避免类似问题,请不要使用过于强烈的Light进行烘焙,因為Light的强度(Intensity)越高,色偏问题会越严重。若有阴影丢失时,可以尝试检查一下模型的Lightmapindex、Lightmapscaleoffset、UV2等影响Lightmap采样的一些参数。

2)另一种可能是存在过曝现象,可以尝试将playersettings -> use direct3d 11关闭,看问题是否解决。

这个答案给出了差异产生的原因,即Lightmap贴图是使用exr格式存储的,然后在移动设备上如果按照LDR进行处理产生偏差是很正常的,给出的建议我们看了下,没有对应的问题。自行修改UnityCG.cginc的事情前段时间帮美术干过,但是没有很好的方法可以让团队内部所有成员机器上的效果保证一致,所以一开始不是非常想采用,而且如何修改效果才能正确这个答案给得并不非常明确。

3.3 博客
接着,找到一篇《解决Lightmap在PC上与ios和Android上表现不同的问题》,使用LogLuv这种编码格式对exr文件进行重新的编码。文章中有基本的原理分析,有解决方案,还有源码,看上去很靠谱。做了一个demo尝试走了一遍流程,是可以解决变暗的问题,但是和PC上比,还会变得更亮……依然有偏差,不知道是否是线性空间的问题,而且更加重要的是过程太多繁琐,维护成本很高,所有的材质都需要对应修改,不理想。况且,转换之后的贴图要使用RGBA8的非压缩格式,一张1024的贴图需要占用4-5M左右的包体和内存空间,代价太大,不在走投无路的情况下实在不能忍。

想起不久前看过一篇知乎上的帖子:谈光照图烘焙技巧,里面跨平台的部分有提到相关的问题:

推荐把没有烘焙Lightmap时候的颜色大体定在0.5-0.7左右的亮度,这样烘焙进退都可以,而且这样Lightmap的系数不会特别夸张的大,这样在pc平台和android基本就能保持一致。

检查了一下,做demo用的贴图亮度0.63,光照亮度调整为1,烘焙之前的颜色大约也符合0.5-0.7左右的亮度,不知道是否因为线性空间的问题,反正依然没有解决。但这篇文章直接提到了UnityCG.cginc中的源码,即lightmap的颜色解析过程,去看了一下。

// Decodes HDR textures
// handles dLDR, RGBM formats
// Called by DecodeLightmap when UNITY_NO_RGBM is not defined.
inline half3 DecodeLightmapRGBM (half4 data, half4 decodeInstructions)
{
    // If Linear mode is not supported we can skip exponent part
    #if defined(UNITY_COLORSPACE_GAMMA)
    # if defined(UNITY_FORCE_LINEAR_READ_FOR_RGBM)
        return (decodeInstructions.x * data.a) * sqrt(data.rgb);
    # else
        return (decodeInstructions.x * data.a) * data.rgb;
    # endif
    #else
        return (decodeInstructions.x * pow(data.a, decodeInstructions.y)) * data.rgb;
    #endif
}

// Decodes doubleLDR encoded lightmaps.
inline half3 DecodeLightmapDoubleLDR( fixed4 color )
{
    return 2.0 * color.rgb;
}

inline half3 DecodeLightmap( fixed4 color, half4 decodeInstructions)
{
#if defined(UNITY_NO_RGBM)
    return DecodeLightmapDoubleLDR( color );
#else
    return DecodeLightmapRGBM( color, decodeInstructions );
#endif
}

half4 unity_Lightmap_HDR;

inline half3 DecodeLightmap( fixed4 color )
{
    return DecodeLightmap( color, unity_Lightmap_HDR );
}

这段代码还比较容易理解,在不同的宏控制下,使用不同的LightmapDecode方案,主要包括DecodeLightmapRGBM和DecodeLightmapDoubleLDR两种。


四、HDR和LDR

这里扩展说明一下HDR和LDR图像的区别,注意,这里说到的HDR不是指High Dynamic Range Rendering,而是仅仅指高动态范围的图像格式,当然在HDR Rendering中肯定要用到高动态范围的图像格式,因为和本文关系不大就不深入讨论了。

前面提到的一篇博客里也说到了,在Unity里,Lightmap是以exr的格式来存储的,即openEXR格式,这是一种开放标准的高动态范围图像格式,在计算机图形学中被广泛应用。

在LDR的图像格式中,比如BMP、PNG、TGA等,一个像素的颜色值可能由RGBA四个通道组成,而每一个通道可以表达的范围是0-255。也就是说8位就可以表示一个像素的一个通道值,RGBA四个通道在不压缩的情况下只需要32位即可。而openEXR格式支持16位浮点数、32位浮点数和32位整数的像素颜色值。Unity中的Lightmap采用这种HDR格式的图像,可以表达的范围当然远比LDR图像的范围要大得多。

在Unity中,exr格式的贴图在导入的时候会被转变为RGBM的格式,因为通常大家Lightmap的导入选项中的TextureImporterSettings.rgbm都是使用默认的“Auto”,即当原始数据为HDR格式的时候使用RGBM格式的编码进行导入。RGBM把[0, 8]范围的值压缩成[0, 1]范围,并且把一个系数存储在Alpha通道中,最终的颜色值为RGBA 8。


五、最终选择的解决方案

在经过这些探究、对比和纠结之后,我决定还是采用UWA的建议,自己修改UnityCG.cginc中的源码。原因是这里是产生不同的根本所在,在不需要对美术制作流程产生任何影响的前提下,也许可以从根本上解决问题。

烘焙场景变暗的原因前文的代码和搜集的博客中已经给出了——在PC平台上,因为是支持RGBM格式的,而且我们开启了线性空间,因此DecodeLightmap最终走了下面代码的逻辑:

return (decodeInstructions.x * pow(data.a, decodeInstructions.y)) * data.rgb;

这里decodeInstructions的值是在外部定义的unity_Lightmap_HDR。关于这个变量我没有从Unity的文档中搜索到相关的设置,只在UnityCG.cginc中看到一个似乎相关定义:

// These constants must be kept in sync with RGBMRanges.h
#define LIGHTMAP_RGBM_SCALE 5.0

在移动设备上,最终会使用这样的逻辑:

// Decodes doubleLDR encoded lightmaps.
inline half3 DecodeLightmapDoubleLDR( fixed4 color )
{
    return 2.0 * color.rgb;
}

也就是值直接乘以了2.0作为Lightmap的颜色值,不知道这个2.0是从科学的计算方法得出的,还是仅仅是一个接近的经验值。而在线性空间下,按照RGBM的计算方法,RGB A 8材质最终的值。由于线性空间的亮度是线性增长的,因此2倍和8倍其实有非常大的差距,明暗差别很大也就可以理解了。当然这里我进行测试,decodeInstructions.x的值并不是8,而似乎是4或者5这样的值。我用Photoshop打开Unity的Lightmap文件,发现其中的alpha通道几乎全部接近纯白色或者纯黑色,只有部分是灰色的,像下图中用于测试的Lightmap图。
请输入图片描述
用于测试的Lightmap原图

请输入图片描述
对应的Alpha通道颜色

检查了下我们自己用的场景中的Lightmap贴图,无论是室外场景还是室内场景,光照贴图的Alpha通道都是接近纯白色或者纯黑色,部分地方是灰色,那么如果我仿照对于RGBM格式贴图的处理,舍弃掉对于alpha的乘法,结果应该只会丢失掉一些局部细节而已,不会对整体的明暗效果产生影响。于是,我选择了最终的解决方案——将DecodeLightmapDoubleLDR函数中的2.0修改为unity_Lightmap_HDR.x的值。
总结一下最终的解决方案:

1)修改BuildinShader中的UnityCG.cginc中的DecodeLightmapDoubleLDR函数为:

// Decodes doubleLDR encoded lightmaps.
inline half3 DecodeLightmapDoubleLDR( fixed4 color )
{
#if defined(UNITY_COLORSPACE_GAMMA)
 // Gamma空间下依然使用之前的计算方法
 return 2.0 * color.rgb;
#else
 // 线性空间下使用和RGBM近似的方式
 return unity_Lightmap_HDR.x * color.rgb;
#endif
}

2)重新替换Unity Editor的UnityCG.cginc文件,删除工程中Libery下缓存的编译好的Shader,即整个ShaderCache目录,重启Unity让它重新编译所有Shader。

3)编写资源导入的后处理脚本,设置Android和iOS设备下的Lightmap的导入格式为ETC2_RGB4和PVRTC_RGB,重新导入所有光照贴图。这里舍弃掉alpha通道,在安卓设备上,让之前一张1024*1024大小的贴图从1.3M变为0.7M,意外的收获,开心~(另外我在安卓设备上尝试了ETC2_RGBA8的格式,结果反而全黑掉了,因为是在中间尝试的步骤,因此没有深究。这里提醒一下,如果使用同样方法修改shader的同学可能需要注意下光照贴图压缩格式的问题。)

4)重新生成所有相关的AssetBundle文件,包括Shader、光照贴图,打包到安卓设备上,然后截图传到PC同屏对比,美术看完之后表示——“很完美,看上去和PC上的完全一样!”。

放一张修改之后在PC上使用安卓平台运行AssetBundle版本的游戏截图。
请输入图片描述
最终修复后的效果图


六、总结

这个问题的根本原因在整理这篇文章的时候我也进行了一些思考,应该是2.0这个经验值是针对伽马空间下的光照贴图亮度调整的经验值,伽马空间的亮度叠加是非线性的,2.0只能是一个接近,因此无法做到各个平台下的效果一致。在移动平台支持线性空间之后,UnityCG.cginc并没有更新这个值在线性空间下的表现问题,导致烘焙的结果在设备上会变暗。

这个问题的解决花费了我大约两天的时间,中间进行各种资料的搜索、实验、方案对比,以及和美术进行问题的讨论和最终解决方案的效果确认。最终的解决方案虽然只修改了几行代码,但是寻找解决方案的过程却涉及到了各种知识点。工程中,解决问题的过程总是伴随着各种猜测和不解,在最后问题解决了之后,有些疑问解开了,有些疑问可能仍然没解开,比如我依然不知道Unity在哪里可以设置unity_Lightmap_HDR的值,或者说这个变量的值是由什么来决定的。最终方案的选择是基于时间成本、维护成本、最终效果一致等各个方面的考虑,我也很明确地知道这个解决方案并不一定适合所有的项目,抑或这个解决方案的背后是否还隐藏着别的坑。但是如美术看到结果时所说——“很完美,看上去和PC上的完全一样!”正应了计算机图学上的那句名言:如果看上去是对的,那它就是对的。


七、问题解决后的安利:线性空间的工作流

本文到最后,我突然理解了为什么网上那么多解决方案在建议规定贴图亮度范围,又或者如UWA的问答中所说的“需要自行修改Lightmap的DecodeLightmap函数,该函数可在Unity\Editor\Data\CGIncludes\UnityCG.cginc文件中找到。需要说明的是,这种方法也不能达到与PC端完全一致的效果。 ”

而我们在解决这个问题之后,做到了动态光影场景效果接近最终烘焙效果,PC预览烘焙效果几乎等于移动设备上忽略色差和亮度差别之后的效果,因为我们使用的是线性空间的工作流啊~~

之前在网易做端游项目《无尽战区》的时候,就经历过一次从伽马空间到线性空间制作方案的改变,当时已经了解到线性空间对于美术制作和最终效果的影响力。因此在了解到Unity 5.5开始在移动设备上原生支持线性空间的时候,我们就在5.5.1f版本的时候在工作室内推行了线性空间的工作流方法。由于之前角色制作已经是在用Substance和线性空间来做了(角色材质中模拟的线性方案),因此只在场景制作这边遇到了一些阻力,但当时为了解决烘焙效果几乎无法预览,全靠猜、重试和经验的问题,顶住压力和主美一起推行了完整的线性空间工作流。除了效果之外,这次问题的解决也让我再次体会到了线性空间的优势——虽然问题很可能是由于Unity官方在材质中对于设备上的结果计算没有考虑线性空间导致的,但是我们自己修复之后,可以做到设备上的效果与PC预览的效果基本一致,这对于美术的效果调整有很大的信心提升。虽然安卓设备上屏幕参数各种不同,但是我们可以保证在色准较好的设备上效果是稳定的。

关于线性空间的原理和优劣晚上有大把TA、或者程序的文章在讲,包括Unity官方文档也有详细的说明,这里只放一张官方的对比图,有兴趣的朋友可以自己搜索。
请输入图片描述
线性空间和伽马空间对比

当然,线性空间也有代价,就是一点额外的性能消耗。Unity 5.5版本开始支持移动设备上线性空间,需要项目组付出的代价是只支持OpenGLES 3.0以上的设备,对应最低的安卓系统版本是4.3。

一方面,支持OpenGLES 3.0的设备占比越来越高,另外一方面,安卓模拟器貌似还都大都是只支持2.0的版本。这也是我们项目后面可能要面临的问题,但是有舍才有得,从项目整体的收益来看,目前使用线性空间还是利大于弊。

最后,安利最近立项的Unity手游项目组,可以考虑使用线性空间工作流,你们的美术会爱你的。


这是侑虎科技第263篇原创文章,感谢作者贾伟昊供稿,欢迎转发分享,未经作者授权请勿转载。当然,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者简书:http://www.jianshu.com/u/5ccd10a133d9,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!

  • 出来混迟早要胖的 发表在 2017年11月28日 回复

    看了下2017.2.0p1下的UnityCG.cginc,代码已经改为了
    inline half3 DecodeLightmapDoubleLDR( fixed4 color )
    {
    float multiplier = IsGammaSpace() ? 2.0f : GammaToLinearSpace(2.0f).x;
    return multiplier * color.rgb;
    }
    inline half3 GammaToLinearSpace (half3 sRGB)
    {
    // Approximate version from http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
    return sRGB * (sRGB * (sRGB * 0.305306011h + 0.682171111h) + 0.012522878h);

    // Precise version, useful for debugging.
    //return half3(GammaToLinearSpaceExact(sRGB.r), GammaToLinearSpaceExact(sRGB.g), GammaToLinearSpaceExact(sRGB.b));
    }

    还给了引用的网址

  • 蒙占志 发表在 2017年11月01日 回复

    inline half3 DecodeLightmapDoubleLDR( fixed4 color )
    {
    #if UNITY_COLORSPACE_GAMMA
    return 2.0 * color.rgb;
    #else
    return 4.59479380 * color.rgb;
    #endif
    }
    感觉这样写好理解一点。 pow(2,2.2)-> 4.59479380