Chapter 5 – Shading Basics 着色基础

5.6 显示编码

当我们计算光照,贴图或者其他操作的效果时,这些值都是假定线性的(linear)。这意味着加法与乘法能够正常工作。但是,为了避免避免显示错误,用于显示的buffer和贴图都使用非线性的编码。我们可以将其理解为:着色器输出的颜色范围是[0,1]并且使用1/2.2次幂,这被称为gamma correction。对于输入的贴图和颜色,则进行相反的操作。大多数情况下,GPU会为你完成这一操作。本小节将会解释如何进行这一操作以及为什么需要这么做。

我们将先学习cathode-ray tube(CRT,阴极射线管)。在早期的数字图像年代,CRT显示是一种标准。这些设备在输入的电压与显示亮度之间使用幂次定律。随着运用到像素上的能量增加,其亮度并不会线性增加而是基于幂次增加。例如,2次幂。一个被设置为50%的像素将会发出四分之一的光线,0.52=0.25(假设,像素设为1,其发出的光线量为1).虽然LCD和其他显示技术有着与CRT不同的响应曲线,但是它们其中的制造原理与CRT的响应类似。

这个幂次函数基本与人类视觉系统对于光强的敏感度成反比。而其造成的结果就是大体上编码与视觉是统一的(perceptually uniform)。也就是说,编码值NN+1不同在显示范围内几乎是不变的。我们可以检测出光线中1%的不同。当颜色被存储在精度有限的buffer中,这个分布能够大大减少显示器上的横纹。其同样会作用于贴图,因为贴图也使用了相同的编码。

显示转换函数(display transfer function)描述了显示buffer中数字值与显示器发射的光线强度等级的关系。因此,其也被称为光学转换函数(electrical optical transfer function,EOTF)。这些属于硬件的范畴,而电脑显示器,电视,电影投影仪在这方面都有着不同的标准。

当对用于显示的线性颜色进行编码时,我们的目标是去除显示转换函数的影响,这样的话,无论我们之前的计算结果是什么都能够有一个对应的亮度等级。例如,如果我们计算的值翻倍了,那么我们希望显示的亮度也能翻倍。因此,我们需要使用显示转换函数的导数来去除它非线性的影响。这个过程也被成为gamma correction。当我们对贴图值进行解码时,需要使用显示转换函数来生成一个线性值,并在之后着色时使用。下图展示了解码与编码在显示过程中的用处。

左侧的图中,一个PNG的贴图在GPU着色器中被读取,其非线性的编码值被转换为线性值。在着色处理之后,最终的颜色值被编码并存储在frame buffer中。显示转换函数将根据frame buffer中的编码值生产光线强度。上图中,绿色以及红色曲线的组合去除了非线性的部分,这样显示的光线强度将与我们计算的线性值成比例。

一般来说,个人电脑的显示转换函数由sRGB定义(color-space specification)。大多数图形API在从贴图读取颜色值或者将颜色值写入颜色buffer时能够自动使用合适的sRGB转换。贴图中的颜色值将会使用双向线性插值,也就是说先将其转换为线性值,之后再进行插值。alpha融合则先将存储的值解码为线性值,再进行融合,之后对结果进行编码。

在渲染的最后阶段运用这一转换是非常重要的,也就是在颜色值被写入用于显示的frame buffer时。如果post-process应用在显示编码之前,那么post-process的计算结果就是非线性的,就会造成显示效果的错误。显示编码的过程可以认为是一种压缩,其保留了颜色值的视觉效果。我们可以认为,在进行实际的计算时,需要使用线性值,当我们需要显示计算的结果或者读取图像时,例如贴图,我们就需要将数据进行编码或者解码。

如果你需要手动去应用sRGB,那么也有一个标准的转换式。在实践中,显示是由每个颜色通道的bit位所控制,例如,民用显示器为8-bit,其范围是[0,255]。这里,我们将显示编码等级的范围设置为[0.0,1.0],并且不考虑bit位。线性值的范围也是[0.0,1.0],由浮点数表示。同时,我们用x表示线性值,用y表示存储在frame buffer中非线性的编码值。为了将线性值转换为sRGB非线性编码值,我们需要使用sRGB显示转换函数的倒数:

其中x表示RGB三个通道中的一个。上述等式需要运用于每一个颜色通道,同时生成的三个值将控制显示的结果。在我们手动运动转换函数时需要格外小心。其中一个错误是使用编码的颜色值而不是其线性形式,另一个就是同时解码或者编码一个颜色值两次。

上述两个表达式,,底部的等式是一个简单的乘法,只是由于数字硬件需要将转换函数变为完美的倒数。上方的表达式则使用幂次运算,基本上在范围[0,1]内的x都会使用该等式。如果再将平移与缩放纳入,这个函数大体上模拟了一个更简单的公式:

其中γ=2.2。而γ就代表了“gamma correction”。

正如同我们计算的颜色值在显示之前需要编码,camera所捕捉的图像在用于计算前是需要解码的。所有你在显示器或者电视机上看见的颜色都是display-encoded(显示编码)的RGB颜色。这些值被存储在文件中,例如PNG,JPEG和GIF,而这些格式的文件可以直接传入frame buffer用于在屏幕上显示,且不需要转换。换句话说,我们在屏幕上看见的都是display-encoded数据。在使用这些颜色进行着色计算前,我们需要将编码值转换为线性值,转换等式如下:

y表示一个标准化的显示通道值,其存储在图像或者frame buffer中,且范围是[0.0,1.0]。而这个解码函数就是之前sRGB转换函数的倒数。这意味着,如果着色器读取一张贴图并且不做任何修改直接输出,贴图会保持原样。解码函数与显示转换函数相同,因为贴图中所存储的值已经被编码用于显示。

更为简洁的gamma显示转换函数如下:

有时,我们会看到一组更为简单的转换式,尤其在移动平台和浏览器中:

上述等式直接使用开根号来表示线性值到sRGB的转换,使用平方表示解码。虽然这是较为粗略的模拟,但这个转换总比没有好。

如果我们不进行gamma correction,那么较低的线性值在屏幕上看上去就会非常暗。假设我们的γ=2.2。我们想要基于线性值,也就是我们计算的着色值,使一个显示像素发光,这意味着我们要对线性值进行(1/2.2)次幂的运算。如果线性值为0.1,那么转换后就是0.351;线性值为0.2,转换后就是0.481;线性值为0.5,转换后就是0.730。如果不进行编码转换,造成的结果就是显示的亮度会小于我们的需求。而0.0和1.0即使经过幂次运算,其值也不会改变。在gamma correction被广泛应用之前,我们会手动增加较暗的表面的亮度。

忽略gamma correction的另一个问题是,线性计算的结果将直接运用于非线性值,如下图所示。

如上图所示,两个重叠的聚集光照亮一个平面。左图中,在我们吧光照值0.4和0.6相加之后没有进行gamma correction。也就是说加法运用在非线性值之上。可以看到,重叠部分的亮度特别高。在右图中,我们计算得出的线性值进行了gamma correction,两个聚集光和重叠部分的亮度是成比例增加的。

忽略gamma correction还会影响物体边界抗锯齿的效果。例如,一个三角形边界覆盖了四个屏幕网格单元(如下图所示)。三角形的标准化亮度为1(白色);背景的亮度为0(黑色)。从左至右,三角形边界对于网格单元的覆盖率分别为1/8,3/8,5/8,7/8。所以,如果我们使用box filter,那么像素的标准化线性亮度为0.125,0.375,0.625,0.875。正确的方法是基于线性值进行抗锯齿,之后再对计算结果进行编码操作。如果我们未进行编码,那么像素的亮度就会过低,造成的结果就是三角形边界的变形,如右侧的图片所示。

上图左侧部分表示,一个白色三角形的边界覆盖了4个像素,背景为黑色。如果我们不进行gamma correction,像素看上去会更暗,便会造成三角形边界的变形,如有图所示。

sRGB的标准在1996年推出,之后成为大多数电脑显示器的统一标准。随着显示技术不断进步,显示器现在能够显示范围更广的颜色,也能更亮。我们将在第八章再讨论颜色的显示,亮度,以及针对HDR的显示编码。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据