本篇介绍一下布林冯模型,并给出示例代码。
同时本篇补充一下伽马矫正,HDR, 泛光等后处理技术,这些技术可以提供更好的渲染效果。
OpenGL 入门教程(10) 高级光照技术
引言
到目前为止,已经讲完了冯氏光照模型及其常用的材质和投光物的相关知识。
本节讲冯氏模型的进一步优化-布林冯模型,同时补充一下伽马矫正,HDR, 泛光等光照处理技术。
布林冯(Blinn-Phong)模型
冯氏光照在多数情况下表现很好,性能也较高。
然而物体反光度很低时,镜面反射会出现问题,渲染出大片(粗糙的)高光区域。
可以试想光源产生的光与物体表面几乎平行的情况。 对于镜面高光,观察方向与反射角度可能会超过90°使得镜面光为0。 在物体的反光度非常小时,它产生的镜面高光半径足以让这些相反方向的光线对亮度产生足够大的影响。
布林冯模型解决了这个问题,同时计算效率更高了。
半程向量(Halfway Vector)
半程向量(Halfway Vector):光线与视线夹角的一半对应方向上的单位向量。
- 当半程向量与法线向量越接近时,镜面光分量就越大。
- 当视线正好与(现在不需要的)反射向量对齐时,半程向量就会与法线完美契合。
现在,不论观察者向哪个方向看,半程向量与表面法线之间的夹角都不会超过90度(除非光源在表面以下)。
早期固定渲染管线的OpenGL就采用Blinn-Phong着色模型。
通常半程向量与表面法线的夹角会小于观察与反射向量的夹角,因此需要的镜面反光度更高一点。 通常我们会选择冯氏着色时反光度分量的2到4倍。
在渲染的感觉上,布林冯模型的镜面光分量会比冯氏模型更锐利一些。
片段着色器对应修改如下:
1 |
|
运行渲染程序,可以看到布林冯模型的效果。
伽马矫正
伽马矫正(Gamma Correction)是一种非线性校正技术,用于补偿图像在不同显示设备上的亮度失真问题。
伽马矫正的需求有以下两个原因:
- 人眼的非线性感知:人眼对亮度的感知不是线性的。 具体来说,人眼对低亮度区域的变化更为敏感,而对于高亮度区域的变化相对迟钝。 这意味着,如果图像信号是线性编码的,那么在显示时,暗部细节可能会丢失,而亮部则显得过于饱和。
- 显示设备的特性:传统的CRT显示器以及其他一些显示设备,由于电子束激发荧光粉产生光的过程,其亮度输出与输入电压之间存在一种幂律关系(通常近似为2.2的幂次方)。 这意味着,为了在屏幕上获得均匀的亮度感知,输入信号必须先经过预矫正(即伽马编码),以抵消显示设备的非线性响应。
伽马矫正过程:
在图像存储或传输前,对图像数据进行非线性调整,以适应显示设备的特性。 这意味着提高图像中的暗部细节,同时不过分增强亮部,使整个图像在显示时能有更宽的动态范围和更好的视觉效果。
对于显示设备: \(L_{out} = L_{in} ^ {\gamma}\)
因此伽马编码: \(L_{gamma} = L_{origin} ^ {1/\gamma}\)
编码过程中,一般使用γ=2.2的幂函数进行转换。 基于gamma2.2的颜色空间叫做sRGB颜色空间。
注意,通常只有在最后发送给显示器时进行伽马矫正,而不是在中间过程进行。
在 OpenGL 中可以直接开启伽马矫正:
1 |
|
也可以自行在片段着色器中进行伽马矫正:
1 |
|
sRGB纹理
sRGB纹理:纹理并不是在线性空间中,而是在sRGB空间中。
在线性空间中使用sRGB纹理后,再进行伽马矫正就会显得过亮,因为这部分纹理实际上做了两侧伽马矫正。
可以使用 OpenGL 在加载纹理时进行重校:
1 |
|
指定 sRGB 纹理要注意纹理在什么空间。 通常diffuse纹理,这种为物体上色的纹理几乎都是在sRGB空间中的。 而获取光照参数的纹理,像specular贴图和法线贴图几乎都在线性空间中。
sRGB衰减
在sRGB空间中,之前的衰减就会发生变化。
这个变化实质是函数变换的叠加,之前线性项成了gamma次幂。 例如 $1/dinstance$ 会变成 $1/dinstance^2.2$,比线性空间中更真实了。
因此如果使用之前的公式,需要注意调整新的参数。
HDR
通常存储在帧缓冲(Framebuffer)中的亮度和颜色的值是默认被限制在0.0到1.0之间的。
虽然通常不会有问题,但是如果某个区域的光源很多,总和超过了1.0, 这些片段中超过1.0的亮度或者颜色值就会被约束在1.0使整个场景的渲染过曝。
为了解决这个问题,一种办法是实时调节光源强度。然而这个方法实际实施过于困难,或者说很难有通用额方法实现。
另一种就是先暂时允许它超过1.0的限制,在渲染后期调整整幅图像的颜色和亮度,这就是 HDR 技术。
- LDR (Low Dynamic Range): 限制颜色值在0.0到1.0之间,这可能导致在高对比度场景中丢失细节。
- HDR (High Dynamic Range): 允许颜色值超出0.0到1.0范围,从而保存更多细节,特别是当场景中同时存在极端亮度和暗部时。
上一节的帧缓冲在这里就可以使用了。我们可以创建一个可以存超过1.0的颜色的帧缓冲,在这个帧缓冲渲染,最后处理它从 HDR 到 LDR 作为结果。
浮点帧缓冲
当帧缓冲的颜色缓冲被设定成了定点格式(像GL_RGB),OpenGL会在将这些值存入帧缓冲前将其约束到0.0到1.0之间。
而当一个帧缓冲的颜色缓冲的内部格式被设定成了浮点格式,如GL_RGB16F, GL_RGBA16F, GL_RGB32F 或者GL_RGBA32F时,这些帧缓冲为浮点帧缓冲(Floating Point Framebuffer)。 浮点帧缓冲可以存储超过0.0到1.0范围的浮点值,可以用作HDR渲染。
浮点帧缓冲的创建只需要改变颜色缓冲的内部格式参数,示例代码如下:
1 |
|
其他的使用和之前没有区别。
在渲染浮点帧缓冲后,需要映射回 LDR 作为最后的结果。这就是色调映射。
色调映射
色调映射(Tone Mapping)是一种将HDR颜色值转换为LDR颜色值的过程,以适应显示器的显示范围,同时尽量保留原始HDR信息中的细节和对比度。
使用色调映射是为了在不丢失细节的情况下,将超出0.0到1.0范围的颜色值适配到显示器的显示能力上。
Reinhard 色调映射
Reinhard 色调映射是最简单的算法,它将均匀分布HDR值到LDR值,适合大多数情况。
Reinhard 色调映射倾向于保持场景的亮度分布,但可能牺牲暗部细节。
示例着色器如下:
1 |
|
曝光调整(Exposure Adjustment):
为了同时看见亮部和暗部的细节,可以模拟人眼的曝光机制。
具体地说,引入一个可调的曝光参数,允许用户根据场景需求手动调整亮度,模拟人眼在不同光照条件下的适应性。
这样低曝光值增强明亮区域细节,高曝光值提升暗部可见性。
示例着色器如下:
1 |
|
当然还有多种算法可用于优化颜色平衡、对比度和细节呈现,满足特定的视觉风格和场景需求。
总的来说,HDR渲染的优势如下:
- 改善高对比度场景的细节表现,避免亮度饱和和细节丢失。
- 允许使用更真实的光照强度,提高场景的视觉真实性和沉浸感。
- 为高级光照效果和后期处理提供基础,如泛光(Bloom)等。
泛光(Bloom)
在现实世界中,当人眼或相机镜头面对非常亮的光源时,光线会散射并在周围产生一种发光或光晕的效果。 这种效果在摄影和电影制作中很常见,能增加画面的美感和真实感。
泛光和HDR结合使用效果最好。或者说,HDR是最容易判断是否产生泛光的方式。
通常泛光效果这样实现:
- 渲染一个有光场景,提取出场景的HDR颜色缓冲以及只有这个场景明亮区域可见的图像;
- 对提取的亮度图像进行模糊处理;
- 将结果添加到原始HDR场景图像的上面。
这里模糊就是我们上一节讲的,使用卷积,可以盒式模糊,高斯模糊等卷积核。
通常选择高斯模糊,效果更好一些。
高斯模糊
最原始的方式是对每个像素卷积,然而这样的计算量显然是很大的。
由于高斯函数有可分离性,因此可以等价的分解成xy两个轴的一维函数的乘积。
考虑一个二维高斯函数: \(G(x, y) = \frac{1}{2\pi\sigma^2}e^{-\frac{x^2 + y^2}{2\sigma^2}}\)
我们可以将其重写为两个一维高斯函数的乘积: \(G(x, y) = \left(\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{x^2}{2\sigma^2}}\right)\left(\frac{1}{\sqrt{2\pi}\sigma}e^{-\frac{y^2}{2\sigma^2}}\right)\)
因此可以是将二维的高斯模糊操作分解为两个一维的操作:
- 首先沿图像的一个轴(例如,水平轴)执行高斯模糊,
- 然后沿着另一个轴(垂直轴)再次执行高斯模糊。
这就是两步高斯模糊。
对于一个N×N的二维高斯核,那么传统的高斯模糊需要O(N ^2)的复杂度来处理每个像素; 而使用两部高斯模糊,则只需要O(2N)的复杂度。
可以使用两个帧缓冲来进行,可以交替使用来完成多次的高斯模糊。
最终将模糊的图像叠加到HDR图像上,转换LDR即完成泛光效果。
至此,读者已经明白布林冯模型和一些光照相关的后处理方法了。