lighting - 一些光照模型以及高级光照
1 phon的光照和blinn-phon的光照
直接看这个shader:
1 |
|
2 gamma矫正
一句话理解他: 物理显示器显示(线性空间)的颜色亮度为0.5,人看到的亮度会为0.5^2.2,也就是更暗了,于是需要先做1/2.2次幂的拔高
Gamma校正(Gamma Correction)的思路是在最终的颜色输出上应用监视器Gamma的倒数。回头看前面的Gamma曲线图,你会有一个短划线,它是监视器Gamma曲线的翻转曲线。我们在颜色显示到监视器的时候把每个颜色输出都加上这个翻转的Gamma曲线,这样应用了监视器Gamma以后最终的颜色将会变为线性的。我们所得到的中间色调就会更亮,所以虽然监视器使它们变暗,但是我们又将其平衡回来了。
我们来看另一个例子。还是那个暗红色(0.5,0.0,0.0)。在将颜色显示到监视器之前,我们先对颜色应用Gamma校正曲线。线性的颜色显示在监视器上相当于降低了2.2次幂的亮度,所以倒数就是1/2.2次幂。Gamma校正后的暗红色就会成为(0.5,0.0,0.0)1/2.2=(0.5,0.0,0.0)0.45=(0.73,0.0,0.0)。校正后的颜色接着被发送给监视器,最终显示出来的颜色是(0.73,0.0,0.0)2.2=(0.5,0.0,0.0)。你会发现使用了Gamma校正,监视器最终会显示出我们在应用中设置的那种线性的颜色
总而言之,gamma校正使你可以在线性空间中进行操作。因为线性空间更符合物理世界,大多数物理公式现在都可以获得较好效果,比如真实的光的衰减。你的光照越真实,使用gamma校正获得漂亮的效果就越容易。这也正是为什么当引进gamma校正时,建议只去调整光照参数的原因。
在使用了gamma校正之后,另一个不同之处是光照衰减(Attenuation)。真实的物理世界中,光照的衰减和光源的距离的平方成反比。但是由于本身有gamma矫正,所以我们就用双曲线函数衰减就行了,因为最后会乘以2.2次幂!约等于距离平方反比
1 |
|
3 阴影映射(定向阴影贴图技术)(基于光照空间的深度缓冲和正常渲染空间的深度缓冲做比较来实现阴影效果)
一句话理解:对于场景的每个顶点转换到光源为中心的坐标系里,然后渲染场景得到的z值就是光源能看到它的深度,然后借用原本渲染场景时,会有一个深度值z‘,比较这两个值,就知道在这个像素是否能够直面光源
- 效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。它也叫定向阴影映射,深度(阴影)贴图生成自定向光的视角。
1 | // 1. 首选渲染深度贴图 |
然后利用这个光照空间深度buffer,得到阴影是否该渲染,直接看shader:
1 |
|
4 点光源阴影(万向阴影贴图(omnidirectional shadow maps)技术)
算法本身:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
1 | 万向阴影贴图有两个渲染阶段:首先我们生成深度贴图,然后我们正常使用深度贴图渲染,在场景中创建阴影。帧缓冲对象和立方体贴图的处理看起是这样的: |
由于万向阴影贴图基于传统阴影映射的原则,它便也继承了由解析度产生的非真实感。如果你放大就会看到锯齿边了。PCF或称Percentage-closer filtering允许我们通过对fragment位置周围过滤多个样本,并对结果平均化。
1 | float shadow = 0.0; |
5 法线贴图(模拟光照)
一句话:为每个fragment生成一个法向,更真实地模拟光照
考虑一个问题,当光照在z轴,然后墙面法向也是z轴,那么法线贴图的每个法线都指向z轴,者能够正常工作,但是当墙面指向正y方向,法向应该能随着墙面旋转而旋转,然后我们没有改动法向,那么就会产生错误的光照!
一个稍微有点难的解决方案是,在一个不同的坐标空间中进行光照,这个坐标空间里,法线贴图向量总是指向这个坐标空间的正z方向;所有的光照向量都相对与这个正z方向进行变换。这样我们就能始终使用同样的法线贴图,不管朝向问题。这个坐标空间叫做切线空间(tangent space)。
方法就是,纹理的相邻两边叉乘得到法向量得到TBN矩阵(切线、副切线、法向),有两种方式使用:
- 1 法线坐标左乘上TBN矩阵,转换到世界坐标空间中,这样所有法线和其他光照变量就在同一个坐标系中了。(在着色器里传入这个向量即可,然后对于着色器里的法向向量乘以TBN矩阵)
- 2 TBN矩阵的逆矩阵,这个矩阵可以把世界坐标空间的向量转换到切线坐标空间。因此我们使用这个矩阵左乘其他光照变量,把他们转换到切线空间,这样法线和其他光照变量再一次在一个坐标系中了。(正交矩阵(每个轴既是单位向量同时相互垂直)的一大属性是一个正交矩阵的置换矩阵与它的逆矩阵相等。所有我们对正交矩阵求逆一般都是直接transpose,而不是inverse)
第二种方法看似要做的更多,它还需要在像素着色器中进行更多的乘法操作,所以为何还用第二种方法呢?(将lightpos viewpos等等都在顶点着色器就转换到了切线空间,避免了在像素着色器阶段做这件事)
将向量从世界空间转换到切线空间有个额外好处,我们可以把所有相关向量在顶点着色器中转换到切线空间,不用在像素着色器中做这件事。这是可行的,因为lightPos和viewPos不是每个fragment运行都要改变,对于fs_in.FragPos,我们也可以在顶点着色器计算它的切线空间位置。基本上,不需要把任何向量在像素着色器中进行变换,而第一种方法中就是必须的,因为采样出来的法线向量对于每个像素着色器都不一样。
所以现在不是把TBN矩阵的逆矩阵发送给像素着色器,而是将切线空间的光源位置,观察位置以及顶点位置发送给像素着色器。这样我们就不用在像素着色器里进行矩阵乘法了。这是一个极佳的优化,因为顶点着色器通常比像素着色器运行的少。这也是为什么这种方法是一种更好的实现方式的原因。
使用法线贴图的优势
- 1 更漂亮
- 2 保持细节,高精度网格和使用法线贴图的低精度网格几乎区分不出来。所以法线贴图不仅看起来漂亮,它也是一个将高精度多边形转换为低精度多边形而不失细节的重要工具。
对于网格渲染,共享顶点的TBN法向会被平均用于平滑效果,这样做有个问题,就是TBN向量可能会不能互相垂直,这意味着TBN矩阵不再是正交矩阵了。法线贴图可能会稍稍偏移,但这仍然可以改进。使用叫做格拉姆-施密特正交化过程(Gram-Schmidt process)的数学技巧,我们可以对TBN向量进行重正交化,这样每个向量就又会重新垂直了。在顶点着色器中我们这样做:
1 | vec3 T = normalize(vec3(model * vec4(tangent, 0.0))); |
6 视差贴图(模拟深度)
一句话:视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图。
1 | vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir) |
陡峭视差贴图(viewDircection多次采样,得到更精确的视差)
一句话:上面的直接用高度h在viewDirection方向采样去模拟偏移p,不够精确,那么对viewDirection方向上做很多个layer的采样,通过每个采样点和真实高度相比较,直到找到第一个比真实高度低的采样点作为结果即可!
上面我们可以知道,这个p只是我们利用viewDir乘以高度得到的偏移,那么我们可以考虑在viewDir多采样几个长度,会得到若干深度,有些大于目标深度,有些小于,那么采样的个数我们把它叫做层数,层数越高就越能逼近真实值
而且这种情况你很容易知道,随着采样层数的增多,砖体上凹下去的横纹会渐渐消失(用1280测试过),因为采样层数少了以后,高度相近的fragment(实际不相同)会最终偏移到同一个纹理坐标,导致横纹
1 |
|
视差遮蔽映射
一句话:相比较与陡峭视差映射,我们采用和真实高度最相近的两个layer线性差值得到最终结果
视差遮蔽映射(Parallax Occlusion Mapping)和陡峭视差映射的原则相同,但不是用触碰的第一个深度层的纹理坐标(本来的过程不是说:从最高layer每个采样点去比较,直到遇到第一个比他小的,然后就作为最终的偏移结果嘛),而是在触碰之前和之后这两个layer,在深度层之间进行一次线性插值。
1 | [...] // steep parallax mapping code here |
7 HDR高动态范围
一句话:我们能做的是用一个不同的方程与/或曲线来转换这些HDR(渲染过程中的连读)值到LDR(真实渲染的亮度)值,从而给我们对于场景的亮度完全掌控,这就是之前说的色调变换,也是HDR渲染的最终步骤。
1 |
|
8 泛光
一句话:对于高亮的东西先取出来,然后blur掉,然后再和原来的combine得到泛光
1 | // 具体算法流程: |
9 延迟着色法
一句话:通常用的正向渲染(forward shading)对于每一个光源和每一个渲染片段都进行了迭代,计算量很大!而且大部分片段着色器输出之后会被之后的输出覆盖,很多时间浪费,于是我们把法向,镜像贴图颜色等等都先放到gBuffer,然后fragmentShader从gbuffer中读取数据渲染即可
有缺点:
- 1 不能进行混合(Blending),因为G缓冲中所有的数据都是从一个单独的片段中来的,而混合需要对多个片段的组合进行操作·
- 2 它迫使你对大部分场景的光照使用相同的光照算法
为了克服这些缺点(特别是混合),我们通常分割我们的渲染器为两个部分:一个是延迟渲染的部分,另一个是专门为了混合或者其他不适合延迟渲染管线的着色器效果而设计的的正向渲染的部分(比如光照物体,需要gbuffer中的场景物体的深度,那么我们会把这个gbuffer的深度信息在渲染光照物体之前copy出来,然后渲染光照物体之前绑定,让光照物体有这些深度信息)。为了展示这是如何工作的,我们将会使用正向渲染器渲染光源为一个小立方体,因为光照立方体会需要一个特殊的着色器(会输出一个光照颜色)。
延迟渲染一直被称赞的原因就是它能够渲染大量的光源而不消耗大量的性能。然而,延迟渲染它本身并不能支持非常大量的光源,因为我们仍然必须要对场景中每一个光源计算每一个片段的光照分量。真正让大量光源成为可能的是我们能够对延迟渲染管线引用的一个非常棒的优化:光体积(Light Volumes)(计算每个光源的可照明半径,仅渲染球体内部像素,超出部分不渲染)
1 |
|
仅仅是延迟着色法它本身(没有光体积)已经是一个很大的优化了,每个像素仅仅运行一个单独的片段着色器,然而对于正向渲染,我们通常会对一个像素运行多次片段着色器。当然,延迟渲染确实带来一些缺点:大内存开销,没有MSAA和混合(仍需要正向渲染的配合)。
10 SSAO(sscreen-space ambient occlusion)屏幕空间环境光遮蔽
一句话:给环境光照加上一个遮蔽因子,决定环境光照的强弱,简单的来说,在凹下去的地方要暗一点,就这个需求,对!
算法核心:若一个点周围的深度都比他高,那么我们增加遮蔽因子,在目标周围的法向半球型附近随机采样即可。
很明显,渲染效果的质量和精度与我们采样的样本数量有直接关系。如果样本数量太低,渲染的精度会急剧减少,我们会得到一种叫做波纹(Banding)的效果;如果它太高了,反而会影响性能。我们可以通过引入随机性到采样核心(Sample Kernel)的采样中从而减少样本的数目。通过随机旋转采样核心,我们能在有限样本数量中得到高质量的结果。然而这仍然会有一定的麻烦,因为随机性引入了一个很明显的噪声图案,我们将需要通过模糊结果来修复这一问题。
因为核心中一半的样本都会在墙这个几何体上。下面这幅图展示了孤岛危机的SSAO,它清晰地展示了这种灰蒙蒙的感觉,由于这个原因,我们将不会使用球体的采样核心,而使用一个沿着表面法向量的半球体采样核心。通过在法向半球体(Normal-oriented Hemisphere)周围采样,我们将不会考虑到片段底部的几何体.它消除了环境光遮蔽灰蒙蒙的感觉,从而产生更真实的结果。
如下是大体流程以及shader中的某些实现:
1 | // render循环: |