代码改变世界

线性空间光照(即Gamma)

2016-04-17 17:58  风恋残雪  阅读(3556)  评论(1编辑  收藏  举报

在渲染管线中正确使用Gamma校正是决定渲染效果的一个非常重要的因素,现在商业引擎包括很多国内引擎都已经是在线性空间计算光照。当然也包括我们之前公司设计的引擎。关于gamma校正的定义可以查看wikipedia或者看知乎的这篇文章

 

原文地址:http://filmicgames.com/archives/299

对于图形程序员和技术美术来说,线性空间光照 是最重要的东西。它并不是看上去那么难,但是不知道是什么原因,没有人真正教过它。就我自己而言,我从Georgia Tech获得了本科和硕士学位,学习了基本上所有的图形学知识,但是并没有听到关于Gamma的知道,直到我从George Borshukov那里学到。这扁文章是我GDC演讲的一个简化版。你可以查看幻灯片来获取更多的细节。

我经常以下面这张图开头:

    

希望你的浏览器没有改变它的大小,左边和右边是黑白交替显示的水平线。如果你有一个正确校正的显示器,中上的方块应该会比较暗,中下部的方块应该和交替的线有一样的强度。在左右两边,你会得到差不多0~255中间的值。中间的值 是127/128,对吗?

不。实际上187差不多是0~255之间的中间值。128会比一半暗很多。怎么回事?

这里有一个0~255的灰阶,128并不是0~255的中间值,而是187。为什么?这就是所谓的gamma在搞鬼。

当把一个值显示到屏幕时,我自然地假设光子的数量跟发送的值是线性增长的关系。例如50发送光子的数量应该比25发送光子的数量多一倍。但是,现实中大多数显示器遵循一个gamma 2.2的曲线。如果 我们假设输出的值在0~1之间而不是0~255,那么光子的数量应该跟pow(x,2.2)成正比。

不同条件下可能会不同。如果你听说Mac有一个不同的gamma,那是因为gamma默认值为1.8(虽然最近我听说他们已经修改了)。sRGB曲线也非常相似,但是实际上跟gamma 2.2的有一些小差别。

上面是一张我穿着一个很小且很贵的貂皮外套的照片。左边的图是一个gamma 1/2.2 (接近0.45)的照片。中间是线性的,或者说是gamma 1.0。右边图片的gamma值为2.2。

我们实际上可以通过一个pow操作在gamma空间中转换。从左边的亮照片说起,我们可以通过 pow(x,2.2)来得到 蹭的图片,再通过一个pow(x,2.2)得到右边的图片。反过来,我们也可以通过 pow(x,1/2.2)来得到。

假设我们在不知道gamma的条件下制造了一个相机。我们会建造一个感光元件用来接收进入的光线(左边)并且把每个像素分到255个灰阶中。这个图片会存放到硬盘上(中间图片)。但是,用户会在它们的屏幕上来查看图片,显示器会应用一个gamma 2.2来校正 图片(右边图片)。如果我们这样做,用户会说我们的相机很烂,因为屏幕上的照片跟现实世界的不一样。

你可以尝试向他们解释相机是正常的,只是所有的显示器有问题(包括你相机背面的预览LCD)。祝你好运。相反,每一个消费级相机老师把照片存储在gamma 空间。当光线到达感光器时,相机 会应用一个pow(x,1/2.2)操作,然后再把它映射到255个灰阶上,并且把最后结果存储到硬盘上。傻瓜相机如此,网络摄像头也是如此,iPhone也是如此,单反也是如此 。唯一的例外可能是计算机视觉相机 。因此图片都存储在gamma空间,它看上去会比较亮且柔和。但是当用户在屏幕上看到图片时,它看上去是正确的。

这是关于gamma你需要了解的最重要的东西。每当你看到屏幕上的一张照片时(像右边的这张),存储在硬盘上的其实是更亮、更柔和,就像上图中左边的一样。那么 它怎么影响计算机图形学呢?

左边的图片所有的光照计算都是在gamma空间做的,而右边的是在线性空间计算的。如果是在PC/PS3/360平台上,那么光照计算应该是线性空间内。如果 是在Wii/PSP/iPhone平台上,你必须做你必须做的事。

左边的图片错在了几个地方。首先,它有一个非常柔软的衰减,这样看上去不正确。你也会看到很多的色调偏移,尤其是在高光处,白色到黄色和黑色到黄色的偏移。当你看右边的图片时,它看起来像一个漫反射表面加高光,它更符合光照模型描述的样子。最后,我不是在讨论什么看起来是好了,我是在讨论什么是正确的。这是一个非常不同的讨论。

如果你不关心gamma,你基本上是遵循上面的光照流程。假设你有一个shader如下所示:

1 float specular = ...;
2 float3 color=tex2D(samp,uv.xy);
3 float diffuse = saturate(dot(N,L));
4 float3 finalColor = color * diffuse + specular;
5 return finalColor;

 

首先读取纹理,但是在你硬盘上的纹理是存储在gamma空间的。纹理看起来比较亮且饱和度比较低。所以你使用了这张太亮的纹理,快些 基础上应用你的光照模型得到蹭的图像。最后显示器应用了pow(2.2)减小了它的亮度信息得到最终的结果。如何纠正它得到正确的结果?

下面是修改后的shader:

1 float specular = ...;
2 float3 color=pow(tex2D(samp,uv.xy),2.2);
3 float diffuse = saturate(dot(N,L));
4 float3 finalColor = pow(color * diffuse + specular,1/2.2);
5 return finalColor;

 

唯一的不同之处是上面的两个pow指令。当纹理由硬件读取时,它是在gamma空间的(第一张图)。但是pow(x,2.2)把这张纹理转换线性空间中(第二张图)。然后我们计算光照(第三张图)。现在我们得到了想要的结果,再应用pow(x,1/2.2)把它转换到gamma空间中。Gamma空间的图片传输到显示器,这时会对像素应用pow(x,2.2)来显示最终的图片。

当然,这些pow函数并不是没有开销的。但是它们实际上是免费的如果你使用硬件采样状态。对于纹理读取来说 ,你可以使用D3DSAMP_SRGBTEXTURE,对于要写入的帧缓冲区,可以使用D3DRS_SRGBWRITEENABLE。所以你的shader代码跟原来一样了,并且硬件状态来帮助你免费转换。

这里有一张排球的真实照片。上面的图片是在线性空间而下面是在gamma空间。注意线性的照片是如何在分明的衰减上与真实照片吻合的,但是gamma空间计算的就不是这个样子的。

那么这就是我们如何使用我们的图片看起来正确的。怎么让它变好看留给读者自己当练习解决。