【RTR4笔记】 第八章 光与颜色

光的量化、立体角、Tone Mapping

Posted Kongouuu's Blog on February 5, 2022

8.1 Light Quantities

在我们要进一步的去处理一个更真实的画面的时候,就需要考虑到怎么样去形容我们模型里面的光。当我们能正确的测量我们的光的能量,才可以达到更好的一个渲染的效果。

8.1.1 辐射度量学 Radiometry

辐射度量学主要是用于测量电磁辐射的强度。我们眼睛能看到的光的颜色在本质上都是处于一个特定的波长区间的电磁波。在基于电磁波的思路上,我们可以想到下面的几个单位用于衡量我们的能量

8.1.1.1 辐射通量 Radiant flux (Φ)

辐射通量是最基础的单位,用来形容一定时间内的辐射能量。

\[Radiant\ flux ≡\frac{辐射能量}{时间}≡\frac{dQ}{dt}=W\]

所以使用 瓦特(W) 来计量。

8.1.1.2 辐照度 Irradiance (E)

辐照度是辐射通量在一定面积内的密度。

\[Irradiance≡\frac{Radiant\ flux}{Area}≡\frac{W}{m^2}\]

所以使用 W/平方米 来计量。

8.1.1.3 立体角 Solid Angle (ω)

立体角表现着在三维空间中的角度,使用 steradian 来计算。我们可以从二维的角度去简单的推一下立体角代表什么:

\[二维:\\ \theta=\frac{l}{r}\\\ 因为圆形的周长为2\pi r,因此\theta范围为[0,2\pi]\\ \\ 三维:\\ \omega=\frac{A}{r^2}\\ 因为球面积为4\pi r^2,因此\omega范围为[0,4\pi]\]

这是一个比较简单的思考方式,往细了看是这样的:

\[d\omega = \frac{dA}{r^2}=\frac{(rd\theta)*(r\sin{\theta}d\phi)}{r^2}\\ =\sin{\theta}d\theta d\phi\]

面积微元本质

从本质上来看我们的面积是先用 围绕x轴画出来一个弧长,并且让这个弧长围绕着y轴旋转。我们二维的旋转一定要围绕着特定的轴才能进行,我们一开始的弧长是基于原点来生成的,所以可以直接使用半径。我们后面跟φ使用的是让弧长围绕y轴旋转的宽度,这时候必须使用每个点和y轴的距离,而不是对原点距离,所以是r*sinθ

假设我们打算通过φ去生成第一个弧长,那么我们的公式则会变成:

\[dA=(rd\phi)*(r\sin{\phi} d\theta)\]

实际上是差别不太大的,视觉上来看就是头转个90度去看上面的图,所以我们的球面面积就是先根据原点和一个轴画弧长,再围绕另一个轴旋转这样绘制出来的。

8.1.1.4 辐射强度 Radiant Intensity (I)

辐射强度算的是每个方向的辐射强度,而方向我们使用的是单位立体角。

\[Radiant\ Intensity ≡ \frac{Radiant\ flux}{Solid\ Angle}≡\frac{W}{sr}\]

8.1.1.5 辐射率 Radiance (L)

辐射率是我们光照计算的一个核心的单位。它是用于形容单个光线的辐射的能量的单位。形容的是在特定的面积上的特定的立体角方向的辐射通量,也就是:

\[Radiance≡\frac{Radiance\ flux}{(Solid\ Angle)(Area)}≡\frac{W}{m^2sr}\\ L(p,\omega)≡\frac{d^2Φ(p,\omega)}{d\omega dA\cos{\theta}}\\ dAcos\theta为垂直于光线方向的面积\]

辐射率是我们后续在计算BRDF的时候的核心。对于一个点来说,它可以用来形容一个特定光源给我们一个点多少能量(从特定方向打到我们点的特定面积),也可以形容我们反射网不同方向的光的强度。

通过这样的计算我们可以更好的去形容画面里面的光。

8.1.2 光度学 Photometry

辐射度量学是完全的依照物理去考量的计算方式,而光度学在那之上纳入了人眼对不同波长的光的反应。光度学本身在所有的计算上都和辐射度量学一模一样,虽然使用的单位名字有些许的差异。他们主要的不同点在于光度学在辐射度量学的计算结果上,会根据人眼对波长的接受度来进行调整。使用的曲线如下,大概在波长为555nm的时候我们的能量会跟辐射度量学计算出的保持一致。

8.1.3 比色法 Colorimetry

这一部分主要讲的是从人眼看到的三原色一直到XYZ空间上对颜色的形容。主要还是围绕着XYZ空间的颜色做讲解吧。

8.1.4 使用RGB渲染

严格地说,RGB色彩空间是更感官的形容,并没有很物理。如果要做PBR 应该要使用光谱的计量去计算所有的光照值,并且在最后在转成RGB。直接的使用RGB会有很多物理上的错误,例如在计算反射的时候理论上应该要考虑到入射光谱光谱功率里对于不同波长的反射率来计算实际的反射光谱功率,之后再把光谱功率转换成RGB。而我们一半做的则是把光和物体的RGB值承在一起,得到一个偏离物理的计算结果。

虽然话是这么说,光谱的计算是非常复杂的,在实时渲染中RGB还算是非常够用的。

8.2 场景到屏幕

8.2.2 色调映射 Tone Mapping

色调映射是将我们场景的辐射值转换到屏幕的辐射值的一个过程;一般来说我们会把这个过程当作 通过屏幕完全复现场景的光照 的过程, 或者说 把HDR颜色空间压到LDR空间, 但目标并不是这样的。

要说目标,色调映射的主要目标是: 尝试复现人眼看到场景的画面。 压缩HDRLDR当然是需要的,但并不是这部分的主要目标,只是一个我们达到目标所使用的手段。不过对于我们普通的图形学萌新来说,可以当作是压HDRLDR。(

实际上我们只需要使用不到场景1%的辐射度就可以复现出类似的效果到屏幕,因为人眼有相对的补偿机制。不过由于我们使用的光强确实比较低,再加上屏幕外的影响,有很大的概率我们看到的画面会有过低的对比度,所以进行 场景->屏幕 的转换中,一定要提高对比度。提高对比度有衍生出了一个问题,那就是我们的显示设备实际上的色彩范围是比较低的,所以很难达到一个理想的效果。

色调映射的主要过程就是选定一个色彩/亮度范围,并且把场景映射到这个范围内。可以说从HDR到LDR也是把大的范围的色彩压到特定范围内。为了达到这个效果有非常多的方法。

8.2.2.1 Reinhard Tone Reproduction

这是一个比较简单的色调映射的函数:

\[c_{adapted}=c_{input}*\frac{用户定义的灰色}{场景的平均亮度}\\ c_{output}=\frac{c_{adapted}}{1+c_{adapted}}\]

这个经验模型是把一个超出范围的颜色输入压缩到LDR空间的一个映射方式。

映射的第一阶段是设置一个我们假设的中间值颜色 灰色。 如果我们的颜色达到了平均的亮度值,那么我们的颜色的亮度就等同于灰色的亮度。这个阶段还没有把颜色压到 [0,1] 的空间。 进行完上面的操作之后我们的色彩范围会置中于我们定义的灰色;之后我们进行下面那一侧的操作把所有的数字放到 [0,1] 的范围;越小的颜色值受到的影响越小,并且越大的颜色值会越接近白色。

在很多的计算中,我们会把用户定义的黑色和场景平均亮度设置成相同的值,那么我们就不需要进行上面的计算(更不需要计算场景的亮度)

8.2.2.2 CryEngine2和曝光

CryEngine2为了做到色调映射,使用了下面的方法:

\[c_{output}=1-e^{\frac{-c}{l}}\\ l: 场景平均亮度\]

这样我们也可以画出一个还过得去的曲线来映射我们的颜色。通过使用场景的平均亮度的参数,我们一样是可以把画面亮度根据场景设置到一个合适的区间。不过这个参数可以做到更多艺术上的效果。

假设我们的场景亮度低,那么大部分颜色会被映射到比较小的值,如果场景亮度高,那么都会被映射到更高的值。如果我们不使用场景的平均亮度,而是为了艺术效果自定义设置这个值,我们就可以做到曝光的效果。

也就是说,我们如果在渲染一个实际平均亮度为0.10的较暗的场景,并且希望能看到更多的细节,那么我们可以把平均亮度设置成0.05,这样就可以做到曝光的一个效果。LearnOpenGL里面的曝光效果的函数就是使用的CryEngine2的色调映射函数,并且把平均亮度的倒数当成曝光度的参数。

值得一提的是上面的Reinhard参数在把颜色映射到特定亮度范围时同时参考了场景亮度以及灰色的亮度,所以并不能直接用曝光度当作参数来替代平均亮度,要改的话自定义的灰色的值也得更改。

8.2.2.3 ACES Tone Mapping

UE4采用的是ACES的编码,过程如下:

1
2
3
4
5
6
7
8
9
10
float3 ACES(float3 color)
{
    float a = 2.51f;
    float b = 0.03f;
    float c = 2.43f;
    float d = 0.59f;
    float e = 0.14f;
    color *= adaptive_lum;
    return (color*(a*color+b))/(color*(c*color+d)+e);
}

这里跟上面一样也使用了场景亮度去做一些自定义的计算。当然这个值也可以用来做曝光度的计算。不过更多时候我们不会在函数中直接考虑这个,而是使用一个额外的函数来做曝光度上的调整。

8.2.2.4 小总结

色调映射的主要思路就是把大范围的数字映射到[0,1]的区间。 为了达到更好的效果,大部分的映射函数都会考虑到场景目前的平均亮度,并且根据平均亮度来决定所有颜色应该被映射到什么区间。

大部分的色调映射函数都有一个调整值,可以是平均亮度,或者是适应性亮度(曝光度)。他们的关系也很简单,我们的曝光度一般就是我们的平均亮度的倒数。

是这样的,我们假设场景的平均亮度为0.2,那么这时候代表我们的映射在HDR空间的0.2附近有着较大的梯度,就是说会把0.2映射到LDR0.5附近,因此画面会变亮,得到曝光度为5的效果。

8.2.3 Color Grading

在色调映射之后我们可以把场景复现到一个屏幕上,主要处理的是一个亮度的问题。但有的时候我们会想要更多的艺术上的效果,所以我们会去对颜色进行操作。我们可以去调整整体的饱和度、对比度、曝光度等去更改我们希望看到的颜色的样貌。同时也可以考虑直接把一个颜色,或者说我们原始的RGB,映射到别的颜色上面。

这些操作可以在场景空间操作,也可以在屏幕上操作。我们一般可以把这个当作后处理的艺术效果来看待。