找回密码
 立即注册
查看: 362|回复: 0

关于GAMMA和Linear工作流在Unity里应用的一点备忘

[复制链接]
发表于 2022-4-22 16:48 | 显示全部楼层 |阅读模式
前言


前一阵子帮忙别的组查看了一起颜色显示异常的问题,表现是这样的:美术同学输出的UI效果图与程序同学在Unity工程中渲染叠加的UI效果存在出入,工程输出结果整体上泛白,特别是多层叠加的地方。这个问题的根本是gamma工作流与线性工作流冲突所致,我之前对gamma校正相关内容只有皮毛认知,正好借此机会梳理了一番,特此记录备忘。
gamma摘要


为何有gamma空间和gamma校正这些东西?我觉得只需要牢记一点,因为所有显示器在输出颜色信号时,会自动的,不可避免的,在物理层面上对目标颜色做一次映射变换pow(rgb,2.2),也可以记作gamma2.2变换。
对,上面这条原因就是导致后续各种费事费力校正和变换的元凶!因为我们实际上想要的是颜色原本的样子,相当于对原始颜色进行 pow(rgb,1.0) 操作后的结果,也可以叫gamma1.0变换,那么为了让最终从显示器输出的颜色能够停留在gamma1.0空间,人们必须提前对目标颜色做一次gamma2.2的逆变换,既pow(rgb,1/2.2),或者叫gamma0.45变换也可以。目的显而易见,是为了抵消显示器施加的变换影响,让最终呈现在人们眼前的颜色停留在颜色本该在的gamma1.0空间。

curve

结合图例来说明
    gamma2.2 -> pow(rgb, 2.2),是显示器会赋予颜色的变换,对应图中下方红色实线;gamma1/2.2 -> pow(rgb, 0.45),为了弥补显示器变换而施加的逆变换,也叫“gamma correction”,对应上方红色虚线;gamma1.0 -> pow(rgb,1.0) 或什么都不做,对于了图中灰色细实线;图中横轴代表颜色真实值,被归一化到了[0,1]区间;图中纵轴代表了经过映射后的输出值,同样处于[0,1]区间;其中gamma2.2(红色实线)将本色中亮度50%的部分压缩到了21.8%处,这意味着亮度下压,颜色变暗沉;另一方面gamma1/2.2(红色虚线)将原本只占1/5的亮度变换拉伸到了约1/2处,这能明显提亮图片;

补充一点,gamma0.45是sRGB所处的色彩空间,PS等工具输出的美术作品一般都以sRGB格式保存,可以方便的直接送交显示器做出正确显示。

另一种维度的解释如下图:

eye

它反应了人眼对暗部变化敏感,对亮部的变化相对不敏感的特性,在此仅作为补充认识,看太多我感觉反而会混乱记忆。
linear workflow 以及 gamma workflow


workflow

上图尝试将美术工作流和基于Unity的linear和gamma工作流进行了统一。宽泛得说,一切由程序直接生成的纹理都应当处于gamma1.0空间(参考图中左上角);一切由美术同学手K的图像和纹理都应当被视作位于gamma0.45空间(参考图中左小角)。我们认为美术同学利用PS等工具绘制图像的过程本身就可以理解为一种shading,颜色的调配,色调的选取和审美是屏幕与美术家手脑之间的交流,中间无需特意转换到线性空间处理。

在Unity中,特别是勾选了linear工作流之后,事情会稍微复杂一些:首先是为了确保shading计算和颜色混淆发生在线性空间(既gamma1.0空间),Unity会对所有标注为sRGB的图样纹理做一次 “remove gamma correction” 操作,实际发生在采样过程中,且一般由软硬件支持,总之Unity需要将处于sRGB(或者说gamma0.45)空间的数值,通过一次pow(rgb,2.2)变化,使颜色亮度分布重归线性。顺便一说,将科学计算放在线性空间下进行是非常合理的,这点应该没人会反对吧。那么运行和混淆之后所得的颜色缓存区影像如果不经过任何处理,还将处于线性空间,这点也毋庸置疑。 于是在linear工作流下,为了让显示输出正确的结果,Unity会为我们贴心的进行一次 “gamma correction” (如图中右侧节点);而在gamma工作流模式下,因为Unity默认我们的所有运算操作都在gamma0.45空间进行,那么距离最终提交显示器输出前自然就不需要额外的校正操作了(图中间偏下处所示)。

总结Unity关于色彩空间的工作流如下:
1)Gamma -> 重点是shader运算和混淆等过程在gamma0.45空间进行,为此需遵循:
    采样纹理时,不对任何sRGB格式的图片进行remove gamma correction操作输出到FrameBuffer时,不执行gamma correction操作

2)Linear -> 重点是shader运算和混淆始终处于gamma1.0这个线性空间中,为此必须:
    对勾选sRGB的纹理,在采样颜色时,对rgb分量进行remove gamma corre操作输出到FrameBuffer时,执行gamma correction操作,调整rgb分量到sRGB空间
Unity如何处理alpha?


参考网上找的文献资料:
The alpha channel is generally linear. The alpha channel doesn't get displayed, but it is generally a non-color term used for transparency (or whatever else). Because they don't need to display on a monitor, there's no reason to store in in gamma space. If you did, you would unnecessarily lose precision at the lower end of the alpha values. link
结论是不做处理。
开篇处所述问题的成因分析


有了上面积累的知识,我们就可以大胆假设,细心求证了。首先我们知道美术同学使用PS输出和叠加内容,而且往往是在默认的gamma空间下处理和混合图层,查看效果的,这是典型的gamma工作流。当出产的图层纹理导入Unity,变成sRGB格式的精灵贴图或者RawImage,如果Unity是工作在Linear模式下且不做任何处理的话,那么在透明度混合的环节会引发问题,使得在Unity里叠加混合后会显得更加明亮一些,具体原有用文字说明不方便,可以直接看下面的公式和拟合曲线:

混合公式在不同色彩空间下的定义:
    Gamma空间下的alpha混合公式是:color = A.rgb * A.a + B.rgb * (1 - A.a)  其中A是src, B是已经渲染在RT上的targetLinear空间下的ALpha混合公式:color = ( A.rgb ^ 2.2 * A.a + B.rgb ^ 2.2 * (1 - A.a) )^ 0.45

混合公式作用在不同亮度颜色上的效果对比

curve2

显而易见,由于Linear操作的混合是放在了gamma1.0空间的,待混合完成再反变换回去的过程,使得数值上产生了扭曲,由图所示,同样一套资源,Unity在linear流下输出的颜色整体偏亮(蓝线在绿线之上)。
尝试耦合这2种工作流,修补问题


我们保证美术同学仍然在Gamma空间中使用Photoshop等工具生成和迭代资源纹理和最终效果图(需要叠加各种UI,对alpha各种混淆),同时在Unity中,我们也要保持之前选定的Linear工作流,但是为了能再现美术同学输出的UI叠加效果,我们必须针对UI渲染做专门处理,关键的第一步就是确保所有UI图层在Unity中的混合计算发生在gamma0.45空间,也就是sRGB空间,这点可以通过取消图片资源上的sRGB勾选框实现。随后我们再想办法将这些特殊处理的UI结果与Linear渲染出来的3D层融合起来,如下Code作为Blit操作的shader代码,作用在UI层与3D层结果混合的时候:
sampler2D _CameraColorTexture;  //全局变量,URP会把main camera的颜色输出存放此处sampler2D _MainTex;             //Blit操作时,会把第一个入参附加给 _MainTex,存放的是UI Camera的输出RTfloat4 frag (v2f i) : SV_Target{    //第一步是采样UI Camera的输出,注意需要取消所有UI相关额贴图纹理的sRGB勾选项(有图集的要取消图集的相关设定)    //所有UI将会在Gamma空间进行计算和混淆:rgb保持原样,混淆时使用原始a,这样可以保持和美术家在Photoshop中的叠加混合一致。    float4 uicol = tex2D(_MainTex, i.uv); //ui in lighter color    //对UI RT的alpha作Pow(alpha, 0.45)操作,借用了转换到Gamma空间的方法,其实是为了提高alpha的数值大小,使得UI的总体不透明度增加。    //注意uicol.a是UI shader对alpha通道混淆后输出的最终值,受到Blend算法的影响。    uicol.a = LinearToGammaSpace(uicol.a); //make ui alpha in lighter color     //所有3D相机的内容都是基于Linear space进行渲染的,请确保ProjectSetting->Player->Other Render->col space的设置是Linear    //此外请确保所有艺术家输出的纹理的sRGB勾选项是勾选的状态,这样Unity才会在采样时自动做remove gamma correction操作。    float4 col = tex2D(_CameraColorTexture, i.uv); //3d in normal color    //3D层的内容需要转换到Gamma空间,与UI输出的RT保持在一个空间里,此后才能做混淆。    col.rgb = LinearToGammaSpace(col.rgb); //make 3d in lighter color    float4 result;    //按alpha值混淆3D层和UI层输出,混淆是发生在Gamma空间里的。    result.rgb = lerp(col.rgb,uicol.rgb,uicol.a); //do linear blending    //最后转换到Linear空间,后续仍然会有计算(Post process?),需要继续保持Linear    result.rgb = GammaToLinearSpace(result.rgb); //make result normal color    result.a = 1;    return result;  //输出到Blit方法的第二个入参上,既 renderer.cameraColorTarget(主摄像机ColorTarget)}总结一些让美术输出的效果图与工程渲染结果一致的经验

    checkpoint 1:确保Unity渲染UI的原始纹理是在Gamma0.45空间下的(同PS等工具使用的Gamma流一致);checkpoint 2:确保UI渲染过程中的图层叠加(Blend)方式与Photoshop等美术输出预览图时的叠加方式一致(经典、正片叠底还是柔和相加等等);checkpoint 3:确保将UI最终结果叠加到3D层时,所采用的混淆方式,与美术同学生成预览图时,所使用的UI层与3D层“截图”的混淆的方式是一致的;checkpoint 4:确保将所有构成UI相机输出的内容都来自于UI,如果不是(如将其他相机的渲染结果Blit到了UI相机输出纹理上的某个区域),那么这些部分要单独处理。处理方式 -> 如果输出贴图在Gamma空间,则无需处理,而如果在Linear空间,需要变换到Gamma空间后再Blit到UI相机的RT上;checkpoint 5:确保3D层摄像机渲染的纹理sRGB不受影响(任然勾选),且Unity处于Linear工作流中。

多提一嘴,如果能在项目开工前提前统一好美术和程序的颜色空间工作流,那么上述所有问题都可以得到缓解和消除。
补充:Photoshop默认是如何混合图层的


首先必须说明,以下内容来自于实验和反推的结果,如果找到可靠的官方公式会后续修正。

PS的图层混合计算公式中RGB部分采用了经典模式,既:
src * srcAlpha + target * (1 - srcAlpha)
但是Alpha部分采用了预乘透明度的方式,既:
srcAlpha * one + tarAlpha * (1 - srcAlpha)

Unity默认UI shader 对于Alpha部分的处理,2020版是和PS一致的,但是之前2018版的处理方式是 srcAlpha* srcAlpha + tarAlpha* (1 - srcAlpha)

然后说说PS合并图层后的输出结果,RGB和A:
先说Alpha,PS默认有一个完全透明的大底板,既initAlpha = 0,然后依据公式
srcAlpha * one + tarAlpha * (1 - srcAlpha)
计算与当前层的混合结果,替换到工作RT上作为新的tarAlpha。依照图层先后顺序混合完全部图层后所得的结果就是最终输出finalAlpha值。

然后是RGB,PS依然会默认有一个全黑的大底板(0,0,0),使用公式
src * srcAlpha + target * (1 - srcAlpha)
以及上一层混合后所得的target输出,依次计算下一层结果,所得的最终值假设是x,然后拿
x * (1/finalAlpha) = y
这个经过处理的y值才是PS最终输出的颜色分量(这一步也叫去除预乘透明度)。

理解最后一步除法操作的意义很简单,假设只有一张图层,那么就不存在混合过程,输出值理应是这个图层的固有值,我们假设是 (yyya)。 由于我们会预设一个全黑的大底(0000)作为工作RT,并使用公式src * srcAlpha + target * (1 - srcAlpha)对当前这个唯一的图层依次混合,经过唯一的一次迭代后可计算出输出数值为: (y*a,y*a,y*a, a) 最后获得我们期望的“正确”rgb输出(yyya),我们要对计算结果再乘以 (1/a):
(y*a,y*a,y*a, a) * (1/a) = (y,y,y,a)
参考

    Linear or gamma workflow浅谈色彩空间我理解的伽马校正聊聊Unity的Gamma校正以及线性工作流3D scene need Linear but UI need Gamma

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 14:24 , Processed in 0.091573 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表