四、Gamma矫正
4.1、人的视觉特性
和很多错视图一样,对于下面这张灰阶图,如果1表示纯白,0表示纯黑,那么这张图片的哪个位置代表的是0.5,也就是自然界的平均亮度(中灰)呢?
我想看到这里的你可能会毫不犹豫的选中间,并心想:这个问题不是zz嘛?
然而事实上,自然界的平均亮度0.5在这张图3/4的位置而并非正中间,也就是说,平均色在人眼中是偏白的,尽管这确实看上去有点颠覆认知、不可思议
正如上图,上面是我们所真实感受的灰度变化,而下方才是物理世界真实的灰度变化
为了更适应环境提高生存能力,人类的视觉系统随着进化出了一个特性:黑暗环境下的辨识能力要强于明亮环境,这可能有助于我们及时发现黑暗中隐藏的危险,因此,人类的黑色的感知能力要远高于对白色的感知
换句话说,假设当前真实颜色是偏黑色(0.1亮度),这个时候环境发生了变化,亮度提高了0.01至0.11,尽管这个变化很细微,但是人仍能察觉的出来环境“变亮了”,但若当前真实颜色是偏白色(0.9亮度),此时环境又发生了变化,亮度提高了0.02至0.92,尽管这个变化量是之前的两倍,但这个时候人眼就察觉不出颜色变化了
一句话总结:人类对黑色的敏感程度高于对白色的敏感程度,认为的平均色(中灰)其实不在灰度为0.5的地方,而是在大约灰度为0.18~0.22的地方
4.2、Gamma神话
现在已经是21世纪了,聪明的科学家们当然早就发现了这一点:人眼对自然亮度感知是非线性的,既然这样,如果说现实世界的亮度为自然亮度,而人们的视觉感受为主观灰阶感受,那么这两者之间是否存在着关系?
在此,Gamma就出现了:
其中横轴代表着自然界线性增长的亮度,竖轴表示人实际感受到均匀亮度,红色的虚线就是Gamma矫正曲线,图中0.218到0.5的箭头,就代表着屏幕实际显示的亮度为0.5,其对应着自然环境颜色就为0.218
是的,存储在你硬盘上的图像,都是矫正过的,也就是对于显示器的实际亮度数值,其2.2次方才等于自然亮度数值
那么问题又来了,Gamma值为什么是2.2?其实这个值就是约定俗成而已,如果硬要是拿2.3或者2.1,其实差距也不大,过去大多数监视器是阴极射线管显示器(CRT),这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度,输入电压产生约为输入电压的2.2次幂的亮度,这叫做监视器Gamma,而选择2.2的倒数进行图像矫正,正是为了迎合这个特性
一句话结论:计算机显示的颜色正是迎合我们的主观灰阶感受,而不是自然亮度,想要映射到对应的自然亮度需要经过Gamma校准
4.3、Gamma与颜色向量
为什么计算机显示的图象颜色要用校正后的主观灰阶感受,而不是自然亮度呢?
其实主要也不是为了迎合人眼,因为理论上只要计算机能表示出所有的颜色段,那么其实是否要进行Gamma校验根本不重要,但问题就出现在这里:《OpenGL基础17:颜色》已经提到过了计算机的显示颜色的规范,目前主流(99%)都是RGB-8bit,也就是三原色都由0~255来描述。就和像素一样,计算机总共也只能展现出 种不同的颜色,是离散的。如果只考虑灰阶(RGB三原色值相等),那么计算机总共就只能描述出256种不同的灰阶
不过现在科技发达,已经有支持10-bit的显示器(能展现出更多种不同的颜色)
这样的话,这256个灰阶就显得非常宝贵,为了能展示更丰富的颜色,在这种情况下就必须要迎合人的主观灰阶感受。否则就会出现如下情况:亮度高的两个相邻灰度【例如#RGB(220, 220, 220)和#RGB(221, 221, 221)】,人们从中根本看不出区别,而亮度低的两个灰度【例如#RGB(18, 18, 18)和#RGB(19, 19, 19)】,它们在人眼中的差别又显得不小,这样如果想表示比前者颜色更亮,却比后者暗的亮度就是一件完全不可能的事
一句话结论:归根结底就是精度问题,因为计算机能表示的颜色数量是有限的,而人类又对黑暗更为敏感,因此一定要在表示黑暗片段上使用更高精度
4.4、Gamma矫正和渲染的关系
所有的光照计算(之前的漫反射和镜面反射等)都一定是在线性空间计算的,这也意味着,如果你在计算物体最终颜色之前就进行过了Gamma矫正(进入了非线性空间),那么这个计算结果肯定就不对了(特别是颜色之间的混合),毕竟次方运算根本不是线性运算,更不可能遵循交换律,遗憾的是,这种错误在很多地方都有……
很多纹理,都已经进行过了gamma矫正!除非能确保纹理制作者是在线性空间中进行创作的,然而事实上就是:大多数纹理制作者并不知道什么是gamma校正,它们都是在sRGB空间中进行创作的(上面提过,sRGB空间定义的gamma接近于2.2,不属于线性空间)
这样的话,对于sRGB空间制作的纹理,我们就必须要在进行光照计算前进行重校,也就是拉回线性空间,不过还好,如果在glTexImage2D()时指定纹理格式为GL_SRGB或GL_SRGB_ALPHA,那么openGL内部就会自动帮你重校
注意!在指定纹理格式前一定要确定纹理类型,不是所有纹理都进行过gamma矫正的,举个例子:对于专业的美术,制作specular贴图和法线贴图几乎肯定是在线性空间中的,这主要是为了确定更准确的光照参数
不过如果转到了线性空间,那么在最后输出的时候就需要进行一次Gamma矫正了,有两种方法:
- glEnable(GL_FRAMEBUFFER_SRGB)
- 在着色器中手动计算,记得一定要在最后进行
图为线性空间计算光照的结果,会显得比之前亮,代码就不放了