Просмотр исходного кода

feat: 添加视差贴图、泛光和 HDR

NiceTry12138 7 месяцев назад
Родитель
Сommit
80cecc0776

BIN
图形学/OpenGL学习/Image/060.png


BIN
图形学/OpenGL学习/Image/061.png


BIN
图形学/OpenGL学习/Image/062.png


BIN
图形学/OpenGL学习/Image/063.png


BIN
图形学/OpenGL学习/Image/064.png


BIN
图形学/OpenGL学习/Image/065.png


BIN
图形学/OpenGL学习/Image/066.png


BIN
图形学/OpenGL学习/Image/067.png


BIN
图形学/OpenGL学习/Image/068.png


BIN
图形学/OpenGL学习/Image/069.png


BIN
图形学/OpenGL学习/Image/070.png


BIN
图形学/OpenGL学习/Image/071.png


BIN
图形学/OpenGL学习/Image/072.png


BIN
图形学/OpenGL学习/Image/073.png


BIN
图形学/OpenGL学习/Image/074.png


BIN
图形学/OpenGL学习/Image/075.png


+ 239 - 0
图形学/OpenGL学习/OpenGLDemo.md

@@ -1356,4 +1356,243 @@ PCF 核心思想就是从深度贴图中多次采样,每一次采样的纹理
 
 #### 点光源阴影
 
+前面介绍的阴影贴图是**定向阴影贴图**,适用于光源指向一个方向的情况
+
+针对点光源,需要使用**万向阴影贴图**,简单理解就是将光照渲染到一个**立方体贴图**中
+
+![](Image/060.png)
+
+当然**点光源阴影**仍然会出现锯齿的情况,原因和之前的**定向阴影贴图**一样,解决方法当然也相似
+
+除了增加深度贴图分辨率之外,仍然可以使用 `PCF`,由于使用立方体贴图,所有 `PCF` 计算采样时需要对三个轴进行采样
+
+> **定向阴影贴图**只用对两个轴采样,最少情况是上下左右 4 个点;**万向阴影贴图**需要对三个轴采样最少 6 个点
+
+#### CSM(Cascaded Shadow Mapping) 级联阴影
+
+### 法线贴图
+
+通常来说,**法线贴图**是一张偏蓝色调的纹理,因为绝大部分法线的指向都偏向 z 轴`(0, 0, 1)`
+
+![](Image/061.png)
+
+再计算时,由于法线贴图指向方向是固定的,所以需要将法线贴图所指的方向转换到当前点的坐标系中,才能进行后续计算
+
+### 视差贴图
+
+和法线贴图一样**视差贴图**能够极大提升表面细节,使之具有深度感
+
+**视差贴图**属于**位移贴图**(`Displacement Mapping`)技术的一种,它对根据储存在纹理中的几何信息对顶点进行位移或偏移
+
+视差贴图背后的思想是修改纹理坐标使一个fragment的表面看起来比实际的更高或者更低,所有这些都根据观察方向和高度贴图
+
+![](Image/062.png)
+
+如上图所示,红色代表高度贴图中数值的立体表达(没有实际物体)。从观察角度触发,如果存在物体,那么观察到的点是 B;不存在物体,那么观察到的点是 A
+
+通过视差贴图,让 A 位置的 Fragment 不在使用点 A 的纹理坐标,而是使用点 B 的。随后用点 B 的纹理坐标采样,观察者就像看到了点 B 一样
+
+视差贴图生成表面位移了的幻觉,当光照不匹配时这种幻觉就被破坏了。法线贴图通常根据高度贴图生成,法线贴图和高度贴图一起用能保证光照能和位移相匹配
+
+![](Image/063.png)
+
+1. 黄色为观察方向,`A` 点表示观察方向与平面的接触点,`H(A)` 表示在视差贴图(高度图)上采样的值
+2. 以 A 点为起点,采样值为值,构建一个与观察方向相反的向量,得到点 `P`,同时从视差贴图(高度图)中得到 `H(P)`
+3. 将 `H(P)` 作为偏移的值进行计算
+4. 可以观察到 `H(P)` 和实际的 `B` 点是有误差的
+
+这个技巧在大多数时候都没问题,但点 `B` 是粗略估算得到的。当表面的高度变化很快的时候,看起来就不会真实,因为向量 `P` 最终不会和 `B` 接近
+
+![](Image/064.png)
+
+除了高度贴图之外,还可以使用**反色高度贴图**,也叫**深度贴图**
+
+![](Image/065.png)
+
+1. 从**观察方向**与**平面**的交点得到 `A` 
+2. 得到 `A` 点在深度贴图中的值 `H(A)`
+3. 将 `A` 点沿着**观察方向**,前进 `H(A)`,得到点 `P`
+4. 那么点 `P` 对应的深度贴图中的值 `H(P)` 就是最终的深度值
+
+> 与高度贴图一样,最终计算得到的深度值是有误差的
+
+#### 陡峭视差映射(Steep Parallax Mapping)
+
+由于视差映射存在误差,只能计算到近似值
+
+通过 **陡峭视察映射** 通过增加采样的数量,来提高精确性
+
+陡峭视差映射的基本思想是将总深度范围划分为同一个深度/高度的多个层。从每个层中我们沿着 `P` 方向移动采样纹理坐标,直到我们找到一个采样低于当前层的深度值
+
+![](Image/066.png)
+
+首先分层,以上图为例,分为 5 层,每层深度 0.2
+
+1. 得到观察视角方向和平面交点 `T0`,此时层为 0,对应的深度贴图值为 `0.7`
+2. 沿着观察视角方向,到达点 `T1`,处于层 1,此时对应的层的深度值为 `0.2`,对应的深度贴图的值为 `1.0`
+3. 沿着观察视角方向,到达点 `T2`,处于层 2,此时对应的层的深度值为 `0.4`,对应的深度贴图的值为 `0.73`
+4. 沿着观察视角方向,到达点 `T3`,处于层 3,此时对应的层的深度值为 `0.6`,对应的深度贴图的值为 `0.38`
+
+此时找到了第一个采样值低于层的点,由此可以确定,真正的交点处于 `T3` 和 `T2` 之间,不过我们直接使用 `T3` 点的值作为高度/深度值 
+
+> 可以简单理解为,`X1` 的坐标是 `(1, 1)`, `X2` 的坐标是 `(2, -1)`,那么直线 x1-x2 与 X 轴的交点一定在 1 ~ 2 之间,因为 X1 在 X 轴之上, X2 在 X 轴之下,那么 X1 和 X2 的连线一定有一个值与 X 轴相交
+
+这个数学定理被称为 **介值定理**
+
+通过 **陡峭视差映射** 可以得到相对之前**普通视差贴图计算**更加精确的值,可以通过增加层数的方式来增加精确度,但是会引入性能问题
+
+#### 浮雕视差贴图
+
+基于 **陡峭视差映射** 计算得到了 `T2` 和 `T3` 两个点,并且知道真实交点在 `T2` 和 `T3` 的两点之间
+
+如果 `T2` 和 `T3` 之间单调递增或者递减,那么可以通过二分法进行计算,每次计算都能使得精度提升,最终可以得到一个更加精确的值
+
+> 需要确定单调性才可以使用二分计算
+
+#### 视差遮蔽映射
+
+视差遮蔽映射(`Parallax Occlusion Mapping`) 简称 POM
+
+POM 基于陡峭视差贴图映射
+
+通过陡峭视差贴图映射得到 `T2` 和 `T3` 两个点,基于介值定理可以知道观察视角方向与视差贴图的交点一定在 `T2` 和 `T3` 两点之间
+
+POM 将 `T2` 和 `T3` 的高度/深度值进行线性插值
+
+![](Image/067.png)
+
+```cpp
+// get texture coordinates before collision (reverse operations)
+vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
+
+// get depth after and before collision for linear interpolation
+float afterDepth  = currentDepthMapValue - currentLayerDepth;
+float beforeDepth = texture(depthMap, prevTexCoords).r - currentLayerDepth + layerDepth;
+
+// interpolation of texture coordinates
+float weight = afterDepth / (afterDepth - beforeDepth);
+vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);
+
+return finalTexCoords;
+```
+
+> 分母 `(afterDepth - beforeDepth)`:总深度变化量(恒为负,因 `afterDepth` < 0, `beforeDepth` > 0)
+
+### HDR
+
+`HDR` 全称 `High Dynamic Range` 高动态范围
+
+存储在帧缓冲中的亮度和颜色的值是默认被限制在 `0 ~ 1.0` 之间的
+
+如果场景中光源非常多、非常亮,使得多个光源数组总和超过了 1.0,最后仍然会被限制在 1.0,进而导致场景混乱难以分辨。由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。这损失了很多的细节,使场景看起来非常假
+
+虽然显示器被限制为只能显示值为 0~1.0 之间的颜色,但是光照方程没有。所以片段的颜色是可以超过 1.0 的,那么我们拥有了更大的**颜色范围**,所以被称为 `HDR` 高动态范围
+
+我们可以让颜色暂时超过 1.0,最后将其转换至 0 ~ 1.0 的区间内,从而防止损失细节
+
+HDR 渲染允许用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)
+
+转换HDR值到LDR值得过程叫做色调映射(Tone Mapping)
+
+最简单的色调映射: `Reinhard` 色调映射算法
+
+```cpp
+void main()
+{             
+    // 1. Gamma值定义
+    const float gamma = 2.2;
+    
+    // 2. HDR颜色采样
+    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
+
+    // 3. Reinhard色调映射核心算法
+    vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
+    
+    // 4. Gamma校正
+    mapped = pow(mapped, vec3(1.0 / gamma));
+
+    // 5. 输出最终颜色
+    color = vec4(mapped, 1.0);
+}   
+```
+
+`hdrColor / (hdrColor + vec3(1.0))` 简单粗暴的将值限制在 0 ~ 1 之间, hdrColor 趋近整无穷大,最后的结果也就是趋近 1
+
+不过存在一些问题,明亮的地方不会损失细节,但是暗的区域会损失一些精度,而且没有区分度
+
+另一个色调映射算法是 **曝光** `Exposure` 的使用
+
+```cpp
+void main()
+{             
+    const float gamma = 2.2;
+    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
+
+    // 曝光色调映射
+    vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
+    // Gamma校正 
+    mapped = pow(mapped, vec3(1.0 / gamma));
+
+    color = vec4(mapped, 1.0);
+}  
+```
+
+通过 `1 - 1/e^(-hdrColor * exposure)` 将值映射到 0 ~ 1 的范围内,通过设置 `exposure` 值来控制映射曲线
+
+- `exposure` 值低,表示低曝光,保留高光细节,整体较暗
+- `exposure` 值搞,表示高曝光,提升暗部细节,可能过曝
+
+![](Image/068.png)
+
+当然还有其他的色调映射算法
+
+### 泛光
+
+明亮的光源和区域经常很难向观察者表达出来,因为显示器的亮度范围是有限的。一种在显示器上区分明亮光源的方式是使它们发出光芒,光芒从光源向四周发散。这有效地给观众一种这些光源或明亮的区域非常亮的错觉
+
+![](Image/069.png)
+
+泛光提供了一种针对物体明亮度的视觉效果。当用优雅微妙的方式实现泛光效果(有些游戏完全没能做到),将会显著增强场景光照并能提供更加有张力的效果
+
+1. 得到这个HDR颜色缓冲纹理,提取所有超出一定亮度的fragment
+2. 将这个超过一定亮度阈限的纹理进行模糊处理。泛光效果的强度很大程度上是由模糊过滤器的范围和强度决定的
+3. 最终的被模糊化的纹理就是我们用来获得发出光晕效果的东西
+
+| 获取超出亮度的 fragment | 模糊处理 | 最终效果 |
+| --- | --- | --- |
+| ![](Image/070.png) | ![](Image/071.png) | ![](Image/072.png) |
+
+泛光本身并不是个复杂的技术,但很难获得正确的效果。它的品质很大程度上取决于所用的模糊过滤器的质量和类型。简单地改改模糊过滤器就会极大的改变泛光效果的品质
+
+### 延迟着色
+
+前面提到的都是前向渲染、正向渲染(`Forward Rendering`)、正向着色法(`Forward Shading`)
+
+前向渲染非常直接,场景中根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推
+
+![](Image/073.png)
+
+如上图所示,在前向渲染中,每个物体的 FS 都需要对每个光照进行处理计算。大部分 FS 的输出都被之后的输出覆盖了,导致极大的性能浪费
+
+![](Image/074.png)
+
+上图展示的是延迟渲染(Deferred Rendering)、延迟着色法(Deferred Shading),它是为了解决前向渲染导致的性能浪费而出现的,大幅度的改变了渲染物体的方式
+
+1. 几何阶段(`Geometry Pass`) 中,正常渲染场景一次,获取对象的各种几何信息,并存储在一系列叫做 G-Buffer 的纹理中
+2. 关照处理阶段(`Lighting Pass`)中,使用 G-Buffer 中的纹理数据,对每一个片段计算场景的光照
+
+![](Image/075.png)
+
+将光照的计算抽离出来,将所需的信息保存在 G-Buffer 中,在最后统一计算
+
+通过 **延迟渲染** 保证每个像素都只计算一次光照,能够省下无用的渲染调用
+
+不过也带来一些问题
+
+- G-Buffer 需要存储较多数据,占用显存
+- 不支持混色,比如半透明、透明物体,因此也不支持 MSAA
+
+**不支持混色**:延迟渲染的光照阶段是全屏后处理,无法像正向渲染那样按深度排序透明物体,因为G-Buffer每个像素只能存储一个表面的数据,深度测试后,只有最靠近相机的表面被保留
+
+**不支持 MSAA**:最简单的 4倍 MSAA 需要 4倍的 G-Buffer 数据,显存压力更大。