模型加载与高级 OpenGL
处理模型资源,并继续学习深度、混合、帧缓冲等高级主题。
模型加载
高级OpenGL
深度测试 Depth testing
深度缓冲(Depth Buffer) 深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值,在大部分的系统中,深度缓冲的精度都是24位的。
深度缓冲中储存每一个(片元)屏幕坐标的深度值,会与相同坐标的片段的深度值比较,以此来确定是否更新深度值 -->也就是确定是否通过深度测试,要不要丢弃当前片段,亦即该片段位于其他片段后面,不能被看见
深度测试是在屏幕空间中运行的,是屏幕空间坐标
你会需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。基本上来说,你在使用一个只读的(Read-only)深度缓冲。OpenGL允许我们禁用深度缓冲的写入,只需要设置它的深度掩码(Depth Mask)设置为GL_FALSE就可以了,如在立方体贴图章节有介绍一个使用场景
👍 提示
现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。 片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。当使用提前深度测试时,片段着色器的一个限制是你不能再写入片段的深度值。如果一个片段着色器对它的深度值进行了写入,提前深度测试是不可能的。OpenGL不能提前知道深度值。
- 深度精度:
远处和近处的对于深度值得精度需求不一致,越近的地方,需要的精度越高
重要的是要记住深度缓冲中的值在屏幕空间中不是线性的(在透视矩阵应用之前在观察空间中是线性的)
也就是说,在应用投影矩阵后,会把深度值做一个非线性变换
- 深度冲突
两个面非常近的时候,深度缓冲没有足够的精度来区分两个面的前后关系,会导致其不断的切换前后顺序,产生闪烁效果
解决方法:
- 不要把面离得太近,设置不可观察的小距离
- 把近平面设置的尽量远一点(不影响效果(物体被裁切掉)的前提下),深度缓冲的非线性原理,物体靠近近平面时精度更高。
- 提高深度缓冲的精度(默认24位,可以提高到32位,但是影响性能)
- 总结关于深度测试的内容
模板测试 Stencil Test
和深度测试一样,模板测试也是用来控制像素是否允许写入的一个方式。
来自kimi的通俗解释
✏️ 提示
OpenGL是一个用于渲染2D和3D图形的跨语言、跨平台的应用程序编程接口(API)。在OpenGL中,模板测试(Stencil Test)是一种可选的图形渲染阶段,它允许开发者控制像素是否被写入帧缓冲区(framebuffer)。 通俗来说,模板测试可以想象成一个“模板”,它定义了哪些像素是“允许”被渲染的,哪些是“不允许”的。这在进行复杂的图形效果,比如阴影、反射、轮廓线等时非常有用。 模板测试的工作原理如下:
- 模板缓冲区(Stencil Buffer):这是帧缓冲区的一部分,用于存储模板测试的结果。每个像素都有一个与之对应的模板值。
- 模板函数(Stencil Function):这是一个函数,用于比较当前像素的模板值和预设的参考值。常见的比较函数包括等于、不等于、小于等。
- 模板操作(Stencil Operation):如果模板函数的比较结果为真(即满足条件),则执行的操作。常见的操作包括保持当前模板值、替换为新的模板值、将模板值设置为0等。
- 应用模板测试:在渲染过程中,OpenGL会根据模板测试的结果决定是否将像素写入帧缓冲区。如果模板测试未通过,像素将被丢弃,不会显示在屏幕上。 举个例子,假设你想要渲染一个复杂的场景,其中包含多个透明物体。如果不使用模板测试,这些透明物体可能会相互覆盖,导致渲染效果不理想。通过使用模板测试,你可以先渲染一个物体,然后设置模板测试,使得只有那些未被先前物体覆盖的像素才能被后续物体渲染,这样就可以避免物体之间的相互覆盖。 模板测试是OpenGL中一个非常强大的工具,它提供了高度的灵活性和控制力,使得开发者能够创造出各种复杂的视觉效果。
混合 Blending
OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。透明就是说一个物体(或者其中的一部分)不是纯色(Solid Color)的,它的颜色是物体本身的颜色和它背后其它物体的颜色的不同强度结合。
混合发生在片段着色器以及所有的测试通过之后,按照透明度来混合当前片段输出的颜色与颜色缓冲中(绘制前一个物体的片段输出)的颜色值,更新到颜色缓冲中去
普通混合是绘制次序相关的,距离相机远的物体如果后面绘制,因为深度测试不通过,所以会影响显示的效果。一个解决的方式是绘制前先对物体排序,但是会带来另一个问题,还需要考虑旋转缩放等变换,特殊形状以及增加性能消耗等,
解决方案是 更高级的技术还有次序无关透明度(Order Independent Transparency, OIT) 参考英文版的第八章:Guest Articles——2020——OIT
面剔除 Face Culling
OpenGL能够检查所有面向(Front Facing)观察者的面,并渲染它们,而丢弃那些背向(Back Facing)的面
是指:一个封闭图形的前后两个面
原理:
每组组成三角形图元的三个顶点就包含了一个环绕顺序。OpenGL在渲染图元的时候将使用这个信息来决定一个三角形是一个正向三角形还是背向三角形。默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形
实际上就是法线方向的面是正面
当你定义顶点顺序的时候,你应该想象对应的三角形是面向你的,所以你定义的三角形从正面看去应该是逆时针的。这样定义顶点很棒的一点是,实际的环绕顺序是在光栅化阶段进行的,也就是顶点着色器运行之后。这些顶点就是从观察者视角所见的了。
观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形

在顶点数据中,我们将两个三角形都以逆时针顺序定义(正面的三角形是1、2、3,背面的三角形也是1、2、3(如果我们从正面看这个三角形的话))。然而,如果从观察者当前视角使用1、2、3的顺序来绘制的话,从观察者的方向来看,背面的三角形将会是以顺时针顺序渲染的。虽然背面的三角形是以逆时针定义的,它现在是以顺时针顺序渲染的了。这正是我们想要剔除(Cull,丢弃)的不可见面了!


📌 提示
这个地方真的不好理解,这个面剔除居然是和threejs中的双面材质是同一回事,困扰了我非常长的时间!!! 以下是我的理解:
- 为什么开启面剔除(默认开启,渲染front-face)了,进入模型内,就不能显示面了呢? 答: 因为进入模型内,看到的哪个面,就是【背面】了,是会被剔除的,如下图当进入模型内部时能看到【面1】,说明【面2】才是正面,所以这个时候【面1】是会被剔除的!
- 而不是【面1】的正反面,【面1】没有正反面之分
- OpenGL逆时针绘制时,并不会区分正反面,但是观察的视角不同,会影响绘制的顶点的顺序
- 旋转后,面的前后也就反过来了,所以另一面又被剔除了,也就是只能从外部看到模型的mesh和材质,从而看起来就是单面材质了

帧缓冲 Framebuffer
用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲。这些缓冲结合起来叫做帧缓冲(Framebuffer),它被储存在GPU内存中的某处
这个概念还是很重要的,它是实现后处理的关键技术,允许在渲染过程通过不同的步骤加入不同的效果
threejs中的WebGLRenderTarget是一个帧缓冲,所以有时间要去看一下这里的源码实现和关联
就是帧缓冲,用来实现离屏渲染和后期处理
立方体贴图 Cubemaps
立方体贴图
立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体。你可能会奇怪,这样一个立方体有什么用途呢?为什么要把6张纹理合并到一张纹理中,而不是直接使用6个单独的纹理呢?立方体贴图有一个非常有用的特性,它可以通过一个方向向量来进行索引/采样。假设我们有一个1x1x1的单位立方体,方向向量的原点位于它的中心

📌 提示
方向向量的大小并不重要,只要提供了方向,OpenGL就会获取方向向量(最终)所击中的纹素,并返回对应的采样纹理值。
天空盒 Skybox
📚 提示
天空盒实现原理: 利用立方体贴图的采样只需要一个方向向量即可,创建一个小的1x1x1的立方体,然后使用其顶点作为采样的方向向量去对立方体贴图采样,这样即可得到一个所有方向的像素 然后在场景中第一个绘制立方体贴图,并且在绘制立方体贴图的时候关闭深度缓冲写入,这样既能通过了深度测试,又不影响深度缓冲里的值。然后再绘制其他物体,就能保证立方体贴图一直都在所有物体的后面了 但是可以优化,因为背景绘制的时候多跑了一些前面物体遮挡的坐标的立方体贴图片段着色器,浪费性能 改进:
- 最后绘制立方体贴图(它只是一个1x1x1的立方体,意味着距离摄像机的距离也只有1,所以需要后面的步骤,欺骗深度缓冲,让它认为天空盒有着最大的深度值1.0)
- 立方体贴图的顶点gl_Position的坐标位置修改,把z值设置为w相等的值
- 把深度测试函数设置成GL_LEQUAL (即需要保证天空盒在深度值小于或等于深度缓冲时通过深度测试,而不仅是小于) 改进的原理:1. 透视除法是在顶点着色器后执行,会将xyz都除以w分量,然后又从深度测试得知,z分量等于顶点的深度,也就是通过这样的设置后,执行透视除法后z的值会一直等于1(w/w = 1) 尽管如此,threejs中还是先绘制了cubeMap,并关闭了深度测试和深度写入来实现的,同时也设置了w分量与Z分量相等,即深度一直都是1, AI的说法是,即使最后绘制cubemap能节约一定的性能,但是开启了深度测试和深度缓冲也会增加一定的性能消耗,实际可能会相互抵消,以及可能会导致的物体顺序的错误问题(想不明白,应该不会)

环境映射
使用环境立方体贴图的技术叫做环境映射(Environment Mapping),其中最流行的两个是反射(Reflection)和折射(Refraction)
需要考虑物体的折射计算和反射计算
高级数据
讲内存和缓冲的,略过
高级GLSL
介绍一些GLSL的内建变量(built-in variable),有需要可以去看看
几何着色器
暂时略过,较少使用
实例化 Instancing
绘制大量相同顶点,但有不同世界位移的物体时(模型实例)
实例化: 将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体
📌 提示
与**
绘制顶点本身**相比,使用glDrawArrays或glDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以,即便渲染顶点非常快,命令GPU去渲染却未必。
原理就是减少Drawcall,以缩短GPU与CPU之间的通信时间,从而达到性能优化的目的,影响性能的不只是内存占用量,还有通信时间等
一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信
抗锯齿 Anti Aliasing
锯齿: 能够清楚看见形成边缘的像素,是边缘像素采样的原因,包含某个片段或者舍弃某个片段
抗锯齿的的原理,
SSAA 超采样抗锯齿(Super Sample Anti-aliasing) 使用更高的分辨率(即超采样)来渲染场景,当图像输出在帧缓冲中更新时,分辨率会被下采样(Downsample)至正常的分辨率,但是额外的分辨率需要更多的片段着色器,有性能问题
MSAA 多重采样抗锯齿(Multisample Anti-aliasing) 每个像素分成多个子采样点,像素点最终颜色按照三角形覆盖了多少个子采样点的比例来决定,这样片段着色器数量不变,但是边缘更加圆滑
有硬件结构来支持,性能会好一些

高级光照
高级光照
冯氏光照模型计算镜面反射存在的问题:
- 当镜面光的反射光线与观察方向之间的夹角大于90度时,计算得到的镜面光照分量为0
- 这与物理规律不符,即使大于90度我们仍然能够看到反射的光,尽管它可能不如直接反射到观察者眼中的光那么强烈。这是因为光线在表面上的反射并不完全遵循一个严格的几何模型,而是有一定的扩散。
- 光的散射:即使在理想情况下,光线在表面上的反射也会因为表面微小的不规则性而发生散射。这意味着即使在接近90度的视角,也会有一些光线散射到观察者的眼睛中。

Blinn-Phong 模型是对冯氏光照的一个改进,它不再使用反射光线与观察方向的夹角计算镜面分量,而是引入一个半程向量(Halfway Vector),使用半程向量与法线的夹角的余弦值作为镜面分量的系数
半程向量可以使用光线向量与观察向量之和标准化而来

Blinn-Phong着色模型正是早期固定渲染管线时代时OpenGL所采用的光照模型
冯氏模型与Blinn-Phong模型也有一些细微的差别:半程向量与表面法线的夹角通常会小于观察与反射向量的夹角。所以,如果你想获得和冯氏着色类似的效果,就必须在使用Blinn-Phong模型时将镜面反光度设置更高一点。通常我们会选择冯氏着色时反光度分量的2到4倍
解释:
cos值在0-90度之间是单调递减的,但是计算镜面因子是计算的次方,而点乘的结果小于1,所以 Blinn-Phong的因子更大一些,需要的次数就更高,如下图
float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = lightColor * spec;
Gamma校正 Gamma Correction
📌 提示
目前仅理解了gamma矫正的原因和概念,应用需要后续再去理解
根本原因:人眼对光线的亮度感知与实际的物理亮度存在差异,需要进行转换

我的理解:
所有的颜色值,经过显示显示时,都会做一个gamma变换(也就是做2.2次幂运算),把颜色调暗到人眼感知亮度
OpenGL计算的颜色值可以认为是物理亮度,输出到显示器时也会做一个显示器gamma变换,所以需要一个gamma矫正(做1/2.2次幂运算),抵消掉显示器gamma的影响

红色实线时显示器gamma,这个是显示器自动进行的操作,红色虚线是gamma矫正曲线
硬盘储存的图片是经过gamma矫正的,也就是做了一个(1/2.2次幂运算,2.2或者其他gamma值),所以实际会更亮
注意点:
- diffuse贴图需要使用sRBG纹理
- specular、法线贴图不能使用sRGB纹理,会破坏光照
- 光照的衰减方程 (后续再深入理解一遍)
阴影
阴影是光线被阻挡的结果
阴影映射 Shadow Mapping
阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了
点阴影
CSM
法线贴图
法线贴图
作用: 让每个片段(fragment)都能有自己单独的法线
使用使用一个2D纹理来储存法线数据,使用采样的方式来获取每个fragment的法线数据,这样的贴图称为法线贴图
注意点1:因为法线的方向几乎都是偏向Z轴(意味着b数值大一点),所以图片会呈现一个蓝色调 ,朝向x会偏红,朝向y轴会偏绿
**注意点2:**法线数值取值范围是[-1,1],rgb颜色范围[0,1],所以存储和读取的时候都要经过一个转换,
读取时,从 [0,1] 法线向量转换为范围[-1,1] normal *2-1
储存时,从 [-1,1] 转换至 [0,1] normal/2+0.5
缺点:
法线贴图出现的问题: 默认基于贴图面法线方向是的Z轴正方向,存储的法线的值(储存的是固定的值,读取也是固定的值),如果贴图贴到物体上的贴图的方向旋转了,也就是贴图面的法线(不再朝向正Z轴),这时采样得到的法线方向就会出错(仍然是指向正Z面的值)
这时候就需要使用切线空间来解决这个问题。
切线空间:
📌 提示
相当于把法线定义在一个局部的空间中,在这个空间中的贴图面永远朝向Z轴正方向
使用贴图的与贴图面固定为Z轴方向得到的一个矩阵,两个方案:
- 把矩阵传给片段着色器,再把采样得到的法向量转换到世界空间,来计算光照(简单直观)
- 把光照的的坐标转换进去,减少片段着色器使用矩阵变换,(性能优化)
视差贴图
HDR (High Dynamic Range, 高动态范围)
可以允许光线强度暂时大于1,然后把它转换回[0,1]的范围内,也就是说能利用更大范围的的颜色值从而能获取更大范围的场景细节
泛光
延迟着色法
思路是把前面的顶点着色器的输出信息全部存储到一系列的G-buffer上,然后再统一进行光照,这样可以避免运行一些深度测试不通过的片段,以达到提升性能的效果【是否和提前深度测试 带来的性能优化相当,未找到答案】