1.各种缓冲区的作用
颜色缓冲区:就是帧缓冲区(图形设备的内存),需要渲染的场景的每一个像素都最终写入该缓冲区,然后由他渲染到屏幕上显示。
深度缓冲区:与帧缓冲区对应,用于记录上面每个像素的深度值,通过深度缓冲区,我们可以进行深度测试,从而确定像素的遮挡关系,保证渲染正确。(注意区分深度测试和背面剔除)
模板缓冲区:与深度缓冲区类似,通过设置模板,缓冲每个像素的值,我们可以在渲染的时候只渲染,后写入模板缓存对应的值,从而在后续的其他绘制可以通过配置模板缓存的参数来决定是否丢弃该片元,以及如何操作对应该片段对应的模板缓存值。
模板缓冲区可以为屏幕上的每个像素点保存一个无符号的整数值,在渲染过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。模板测试发生在透明度测试之后,深度测试之前,如果模板测试通过,则相应的像素点更新,否则不更新。
累积缓冲区:允许在渲染到颜色缓冲区之后,不是把结果显示到窗口上,而是把内容复制到累积缓冲区,这样就可以把颜色缓冲区与累积缓冲区中的内容反复进行混合,可以用来进行模糊处理和抗锯齿。
2.渲染管线的四个阶段
渲染管线通常被分为四个阶段:应用阶段,几何阶段,光栅化阶段,像素处理阶段。
(1) 应用阶段:应用阶段是指在CPU端进行处理的阶段,包括物理碰撞检测、物理模拟、动画计算等任务,对于3D游戏来说,游戏中包含大量的模型,3D模型中保存着模型的顶点坐标,法线,切线,颜色等数据,这些数据一般通过向量进行存储,CPU从模型中获取这些顶点信息数据,并将这些数据传送给GPU作为最开始的输入数据。然后将数据送到渲染管线中。
(2) 几何阶段:几何阶段主要执行顶点.坐标变换、顶点处理、坐标裁剪等操作,计算对象为顶点数据,即模型的顶点数据,在这个阶段,做的最多的操作就是顶点坐标变换,从模型空间变换到世界空间,然后再从世界空间变换至相机空间。这样在相机坐标系下就能方便地进行裁剪,裁剪的作用是判断顶点是否可见。
(3) 光栅化阶段:相比于屏幕上的像素点,顶点数据要少很多,这就有一个问题:无法实现从顶点数据到屏幕像素上的映射。而为了达到这一目的,就出现了顶点插值运算,即将两个顶点之间的空缺部分通过插值的形式进行填充,以达到能实现从顶点数据到像素上的一一映射,最后将渲染的结果显示到屏幕上。
(4) 像素处理阶段:像素处理阶段包括像素颜色计算、像素变换,透明度混合等操作,处理物体渲染顺序及深度测试等。这个阶段要处理的内容比较多,可以在此阶段中利用一些算法实现非常多的屏幕特效,比如高斯模糊,景深等,此阶段是游戏渲染中应用非常广泛的阶段,但因为计算对象是像素点,因此实现相关特效时比较耗费性能。
流程如下:
![image-20220616094241525](/Users/wujianqiang/Library/Application Support/typora-user-images/image-20220616094241525.png)
顶点数据:一个模型或者图形是由点线面构成的,为了让计算机绘制出这个图形,就必须告诉计算机这些数据的值,顶点数据包括顶点坐标,坐标的切线,法线,颜色等信息,对于OpenGL,这些数据一般都是向量(Vector)结构体,对于游戏引擎,这些数据来自导入的模型当中,在开始渲染之前,CPU会获取这些数据,然后将其传递给GPU,作为最原始数据,做好计算准备。
顶点着色器:顶点着色器(Vertex Shader)在渲染管线中的作用非常大,是渲染管线的第一个可编.程着色器,处理单元是顶点数据。顶点着色器的主要功能是对坐.标进行.变换。将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标。除此之外当然也可以进行光照着色,但是着色效果远不如在片元着色器中进行光照着色,因为计算量较小。
图元装配:图元装配(Primitive Assembly)是对传入的顶点数据进行重新组装,将顶点着色器的输出作为输入,这一点正验证了渲染的过程是以流水线的形式进行的,图元装配会将顶点装配成指定的图形,与此同时,会进行裁剪、被面剔除等操作,以减少不必要的计算,加速渲染过程。
几何着色器:几何着色器(Geometry Shader)会将图元装配阶段输出的数据作为输入数据。几何着色器不属于可编程阶段,由硬件设备自动完成,其主要作用是对顶点数据进行重构,可以在此阶段产生新的顶点数据,来弥补之前存在的一些问题。以便为接下来要进行的操作做好充分的准备工作。
光栅化:光栅化阶段(Rasterization Stage)的输入数据来自几何着色器的输出数据,光.栅化的意思很容易理解,到目前为止,渲染所处理的数据对象为顶点,无法通过一一对应的方式映射到屏幕上,而为了实现顶点到屏幕像素的一一映射,就出现了光栅化。换而言之,光栅.化的作用就是将两个顶点直接缺少的像素点通过插值的形式进行补充,生成片元着.色器可以处理的片段。此阶段.人为不可干涉,由硬件完成插值计算。在插值的过程中,会将不可见的顶点进行剔除。
片元着色器:片元着色器处理的对象将是像素点的颜色信息,也是最终显示在屏幕上的像素点,在这个过程中,可以处理光照和阴影计算,将处理完的值保存至缓冲区当中。
混合处理阶段:混合处理阶段属于屏幕后期处理范围,这意味着此阶段主要做的任务为屏幕优化操作,通过片元着色器得到的像素,有些不能被显示出来,比如透明度为0的像素点,对于这类像素点,我们需要进行测试,测试的范围包括Alpha测试、模板测试和深度测试等。不能通过测试的像素点将会被丢弃,就不会参与接下来的操作;通过测试的像素点会进入混合阶段。混合阶段主要是要处理透明物体,物体的透明度通过Alpha值来表示,范围是从0.至.1, alpha=1表示完全不透明,alpha=0表示完全透明。测试混合阶段基本不需要进行编程,但是常见的渲染管线接口会开放出一些参数给开发者.做调整。
3.顶点着色器和片元着色器的作用
顶点着色器:顶点着色器(Vertex Shader)在渲染管线中的作用非常大,是渲染管线的第一个可编程着色器,处理单元是顶点数据。顶点着色器的主要功能是对坐标进行变换。将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标。除此之外当然也可以进行光照着色,但是着色效果远不如在片元着色器中进行光照着色,因为计算量较小。
片元着色器:片元着色器处理的对象是像素点的颜色信息,也是最终显示在屏幕上的像素点,在这个过程中,可以处理光照和阴影计算,将处理完的值保存至缓冲区当中。
4.深度测试问题
为什么需要进行深度测试?
当我们渲染多个物体时,这多个物体之间存在互相遮挡的关系,被遮挡的物体的部分将不可见,也就是它离相机更远,为了告诉计算机被遮挡的物体不需要渲染,我们就需要对物体上的点做深度测试,检测它是否需要渲染。
为了实现上述的检测,就需要深度缓冲,简单而言就是存储物体上点深度值的数组,这个数组一开始值为0,当目前渲染的物体的深度值大于缓冲区中存储的深度值时,就将这个值写入缓冲区,同时通过深度测试,如果与此相反,深度值小于缓冲区中的深度值,就是未通过深度测试,且不写入缓冲区,淘汰掉这个点(不做渲染)。
其中:深度测试函数可以自己定义
函数 | 描述 |
---|---|
GL_ALWAYS | 永远通过深度测试 |
GL_NEVER | 永远不通过深度测试 |
GL_LESS | 在片段深度值小于缓冲的深度值时通过测试 |
GL_EQUAL | 在片段深度值等于缓冲区的深度值时通过测试 |
GL_LEQUAL | 在片段深度值小于等于缓冲区的深度值时通过测试 |
GL_GREATER | 在片段深度值大于缓冲区的深度值时通过测试 |
GL_NOTEQUAL | 在片段深度值不等于缓冲区的深度值时通过测试 |
GL_GEQUAL | 在片段深度值大于等于缓冲区的深度值时通过测试 |
深度测试主要的的两个步骤:
- 对比缓冲区中的值,判断是否通过测试(Z Test)
- 是否将该值写入缓冲区(Z Write)
注意:每次渲染一遍之后必须将缓存值清空,不然可能会出现画面错误情况。
清除方式和清除颜色缓存一样:
1 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
深度测试精确度及深度冲突
计算深度值的函数:
深度测试出现的冲突问题:
- 当大量物体离摄像机极远时,会出现深度值错误的现象,因为太过集中在1这个值处(深度值往往有16位,24位和32位,其值位于0-1)
- 当大量物体在同一平面时,会出现深度值相同(因为是float,所以可能每次渲染的结果都不同,造成抖动现象)
对于以上两类问题,应该设置其渲染顺序,相机的近平面和远平面,来达到最佳效果 。
5.透明物体和不透明物体的渲染顺序问题
注意:在OpenGL中,透明物体不能写入深度(一个透明物体在一个不透明物体之前,那么理论上,如果写入深度值,那么后面的不透明物体将不可见,但实际是我们是可以通过透明物体看到后面的东西)
这就意味着透明物体要关闭深度写入。如果开启深度测试,则要同时开启混合算法,这样才能看到透明物体后面的其他物体。
进行混合计算的物体要求:必须带有Alpha值的物体(Alpha取值范围:0~1),即颜色类型为RGBA(因此jpg格式的图片不行,因为jpg图片只有rgb值)
混合函数:
result=source∗Fsource+destination∗Fdestination
source:源颜色向量。这是源自纹理的颜色向量。
destination:目标颜色向量。这是当前储存在颜色缓冲中的颜色向量。
Fsource: 源因子值。指定了alpha值对源颜色的影响。实例中为红色(1)
Fdestination: 目标因子值。指定了alpha值对目标颜色的影响。实例中为绿色(0.6)
1 | glBlendFunc(GLenum sfactor, GLenum sfactor) |
Fsource和Fdestination的值为以下选项:
选项 | 值 |
---|---|
GL_ZERO | 因子等于0,舍去对应的贴图 |
GL_ONE | 因子等于1,全取 |
GL_SRC_COLOR | 因子等于源颜色向量source |
GL_ONE_MINUS_SRC_COLOR | 因子等于1−C¯source |
GL_DST_COLOR | 因子等于目标颜色向量C¯destination |
GL_ONE_MINUS_DST_COLOR | 因子等于1−C¯destination |
GL_SRC_ALPHA | 因子等于C¯source的alpha分量 |
GL_ONE_MINUS_SRC_ALPHA | 因子等于1− C¯source的alpha分量 |
GL_DST_ALPHA | 因子等于C¯destination的alpha分量 |
GL_ONE_MINUS_DST_ALPHA | 因子等于1− C¯destination的alpha分量 |
GL_CONSTANT_COLOR | 因子等于常数颜色向量C¯constant |
GL_ONE_MINUS_CONSTANT_COLOR | 因子等于1−C¯constant |
GL_CONSTANT_ALPHA | 因子等于C¯constant的alpha分量 |
GL_ONE_MINUS_CONSTANT_ALPHA | 因子等于1− C¯constant的alpha分量 |
一般选则为:
1 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); |
也可以使用glBlendFuncSeparate为RGB和alpha通道分别设置不同的选项
1 | glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); |
渲染顺序原则:
不透明物体:从前向后的顺序渲染,因为深度值越大,越容易不被遮挡,这里涉及Early-Z技术
透明物体:从后向前渲染,因为透明物体需要和后面的物体进行叠加,同时不写入深度值
⚠️:要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。
普通不需要混合的物体仍然可以使用深度缓冲正常绘制,因此它们不需要排序。但我们仍要保证它们在绘制(排序的)透明物体之前已经绘制完毕了。
当绘制场景中同时存在透明和不透明物体时:
- 先绘制所有不透明的物体。从近到远
- 对所有透明的物体排序。从远到近
- 按顺序绘制所有透明的物体
举例常见的性能优化:
优化主要从三个方面入手:CPU端优化,数据传输过程和GPU端优化。
在游戏性能遇到瓶颈时,首先要明确是谁拖了后腿,在CPU端还是GPU端,游戏逻辑由CPU执行,如果是UE4蓝图或者C++代码执行效率低,则是CPU端需要优化,如果是渲染速度慢,则需要GPU端进行优化,如果是CPU向GPU发送指令过程慢,则说明要减少Draw Call(CPU向GPU发送渲染指令数量)数量。
游戏有“帧”的概念, 一帧的运行时间并不等于CPU渲染时间+CPU向GPU发送渲染指令时间+GPU渲染时间,因为这三项是由三个线程执行的,基本等于其中耗时最长的那一项,因此只要降低耗时最长的一项,就能有效提升帧速率。
由于上述三者之间执行由不同线程执行,但是只有CPU端处理完成后GPU才能进行渲染,因此有以下的执行顺序:
在第0帧时什么也没做,第一帧时CPU处理逻辑,第二帧时CPU线程继续处理逻辑,同时CPU开辟一个线程向GPU发送渲染指令(Draw Call),第三帧时GPU开始处理CPU第二帧发送过来的数据,因此GPU每一帧处理的数据都是CPU前一帧发送过来的数据,这样就达到了看似顺序执行的目的。
CPU端主要处理逻辑,对性能影响最大的便是Tick,如果在Tick里做复杂逻辑,就会使帧速率变低,因此能用事件触发就用事件触发,毕竟游戏中大部分的逻辑都可以通过事件触发,不必每一帧都去刷新,如果必须在每一帧都刷新,比如旋转这种简单的逻辑,可以尝试使用Shader实现。
CPU端常见优化点:
1 减少tick的使用,大部分逻辑都是事件驱动的,能不在tick/update里刷就不要在里面刷
2 尽量减少使用get all class方法,性能较低
3 不要频繁创建销毁物体,对象池了解一下
数据传输(DrawCall)优化
1 尽量使用共享材质
2 减少渲染的物体数量,可以减少模型顶点数量,或者尽量使用LOD以及遮挡剔除
3 尽量使用图集
4 动态/静态批处理
⚠️:数据传输:在渲染物体之前,物体模型顶点数据保存在内存中,CPU通过向GPU发送渲染指令后,数据会复制到显存中,然后进行渲染。在这个过程中,CPU向GPU发送渲染指令的过程,名为Draw Call。OpenGL中的渲染指令是指: glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);函数
⚠️:批处理:当我们在渲染一个场景时,该场景中包含非常多的简单模型,比如星星,草等,这些模型的顶点数据极少,但是每次单独渲染一个简单模型时都会调用一次Draw Call,渲染的速度很快,但是发送指令的过程是很慢的(CPU告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的),这就会使渲染整个场景的速度变得非常慢,但如果我们能将这些星星或者草一次性全部发给GPU去渲染,速度就会非常快,这就是批处理。批处理能节省Draw Call的数量,极大提升渲染速度。
GPU端优化(主要就是简化shader运算):
1 注意渲染顺序,尤其是透明物体
2 尽量减少光照和阴影的开销,尽量使用烘焙光
3 使用mipmap优化纹理寻址
4 尽量简化shader计算量
5.能在顶点着色器计算,就不在片元着色器计算
6.模版测试(Stencil Test)与模版缓冲(Stencil Buffer)
模板缓冲和我们经常听到的颜色缓冲、深度缓冲几乎是一类东西。如果开启了模板测试,GPU会首先读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值(reference value)进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃。不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作,例如,在失败时模板缓冲区保持不变,通过时将模板缓冲区中对应位置的值加1等。模板测试通常用于限制渲染的区域。另外,模板测试还有一些更高级的用法,如渲染阴影、轮廓渲染等。
7.深度测试(Depth Test)
如果一个片元幸运地通过了模板测试,那么它会进行下一个测试——深度测试(Depth Test)。相信很多读者都听到过这个测试。这个测试同样是可以高度配置的。如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不需要出现在屏幕上。如果这个片元没有通过这个测试,该片元就会被舍弃。和模板测试有些不同的是,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值,这是通过开启/关闭深度写入来做到的。我们在后面的学习中会发现,透明效果和深度测试以及深度写入的关系非常密切。
GBuffer Float[ 720 * 1280 * 4] 中转区:合格通过测试的点会把RGBA值、模板值、深度值等存入GBuffer。
关于GBuffer、FrontBuffer、FrameBuffer:
GBuffer选出来的像素,FrontBuffer只写入GBuffer的RGBA,推到显示器显示,之后退到后台变为FrameBuffer;
FrameBuffer同样也会只写入GBuffer的RGBA,推到显示器显示,之后退到后台变为FrontBuffer。如此交替显示,一帧一帧渲染显示,有了动画。
8.延迟渲染和前向渲染问题
正向渲染(Forward Rendering)
正向渲染是指每执行一次渲染命令,就会对需要渲染的物体进行光照着色,那么当场景中有m个光源,n个需要渲染的物体,那么时间复杂度就是O(m*n)。多光源时不适合使用正向渲染。
延迟渲染(Deferred Rendering)
为了解决正向渲染的不足之处,把渲染物体的光照着色分开计算,即先把所有是物体都渲染完成保存至GBuffer中,然后对GBuffer中的内容进行光照着色,这样只需要执行O(n)+O(m)次,因此适合处理多光源。
延迟渲染的不足之处
- 显而易见,延迟渲染需要一个临时缓冲区GBuffer,这样就会多浪费空间
- 又因为GBuffer的存在,延迟渲染不适合MSAA抗锯齿算法,因为多重采样需要使用更大的缓存空间,是GBuffer的四倍或更高,这样一般显卡都没办法提供那么大的空间,因此不适应MSAA。
- 延迟渲染无法处理混合,即渲染透明物体,因为G缓冲中所有的数据都是从一个单独的片段中来的,而混合需要对多个片段的组合进行操作。
- 延迟渲染只能对所有模型做统一的光照着色,使用一套光照算法。
9.阴影渲染的原理
阴影绘制的实现基础是Shadow Map算法
Shadow Map算法伪代码:
输入:顶点位置坐标
输出:在灯光空间下是否可见
1 | bool ShadowMap(){ |
具体绘制过程如下:
1)在顶点坐标转换到灯光空间下,需要乘变换矩阵
将物体顶点数据通过矩阵运算转移到灯光空间下。
从局部空间转移到世界空间,在转移到灯光空间下,对于不同的灯光,有着不同的投影矩阵,这里涉及两种投影方式:正交投影和透视投影。两种投影方式分别对应着不同的投影矩阵及视觉效;
正交投影往往应用于2D游戏中,透视投影应用于3D游戏中,但并不绝对。在灯光坐标下渲染物体阴影:
2)计算该顶点在灯光空间下的深度
在灯光空间下创建一张深度纹理贴图,用于保存物体在灯光空间下的深度信息,此深度贴图则是摄像机的渲染目标。灯管空间下的物体深度信息将被保留在该深度贴图中。
渲染深度贴图的过程比较简单,首先在该视口下的顶点信息进行判断,如果某个顶点的深度值比深度贴图中的值小,就更新贴图数据,反之丢弃掉该数据,直到遍历一遍所有像素点为止。遍历完成后,便得到了深度信息图。
3)判断顶点的深度值与深度贴图中对应点点深度值的大小关系
获取到深度贴图后,如果某个点在光源视角下的深度值大于深度贴图中对应位置的深度值,就说明它被某个物体遮挡,因此是在阴影中的;反之,深度值小于深度贴图中的值,则不在阴影之中。
判断完成该顶点是否需要接收阴影后,需要做的最后一步就是对该顶点乘上阴影的颜色,遍历完一遍该物体的所有顶点后,便可得到该物体接收阴影的效果。
正交投影和透视投影有什么区别
摄像机在渲染管线中作用非常大,在游戏世界里,摄像机担任的角色类似于人的眼睛,摄像机所看到的内容最终会被绘制到屏幕上。为了模拟人的视野,摄像机具有近远平面和视野等属性,通过这些属性,可以绘制出一个视锥体,所有在视锥体内的物体初步判定为视野可见。
摄像机具有的最重要的属性是投影方式:即正交投影和透视投影。正交投影和透视投影两种投影方式的本质区别是投影矩阵的差异以及视锥体的不同。正交投影一般应用于平面或者2D游戏当中,使用正交投影矩阵进行平面映射。而透视投影一般应用于空间或者3D游戏。使用透视投影可以模拟人的视野,物体之间将具备更加贴近现实的遮挡关系,透视投影进行投影的方式是通过透视投影矩阵。
![image-20220616170114129](/Users/wujianqiang/Library/Application Support/typora-user-images/image-20220616170114129.png)
正交投影矩阵应用于一个立方体视口下,不在这个立方体之内的顶点都会被剔除掉。因为正交投影矩阵围成一个立方体,因此为了生成正交矩阵,就需要指定对应的长度、宽和高。当顶点坐标在这个立方体内,则视为视口可见,不会被剔除。
![image-20220616170131906](/Users/wujianqiang/Library/Application Support/typora-user-images/image-20220616170131906.png)
透视投影矩阵定义了一个锥体空间,其中最.重要的参数是FOV(Field of View),就是相机的视野角度。通过该角度和进平面远平面围成一个封闭的锥体,在该锥体范围内的物体将不会被裁剪掉。透视投影广泛应用于3D空间,因为它能模拟人的视角去观察物体。因此在透视矩阵下,物体能表现出深度信息与遮挡关系。
10.多级渐远纹理(mipmap)性能快的原因
在计算机渲染一个物体的时候,对于透视投影情况下,当该物体距离摄像机非常远时,映射到屏幕上的像素点非常少,这就导致采样命中率降低,这样纹理寻址速度会变慢,性能会降低。同时对于高分辨率的物体来说,也会造成不必要的内存浪费。
为了解决上述采样率过低的问题,出现了多级渐远纹理(mipmap)多级渐远纹理是以空间换时间的方案,通过保存原尺寸贴图的四分之一大小,直到最小尺寸为2*2为止。在纹理寻址的过程中,会选择与映射到屏幕时对应的像素点数最接近的一张纹理,然后使用该纹理做纹理寻址。
使用mipmap会造成多用33%的空间,但是使用它的好处也非常明显,换句话说就是使用空间换时间,利大于弊。对于阴影贴图来说,使用mipmap在做纹理寻址时也是一项重要优化。通过mipmap加快阴影映射,在性能上还能有一定程度的提升,但是当场景中物体离摄像机极远时,最好不要使用mipmap,因为这样会导致阴影极度模糊,造成阴影不真实的现象。
OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。
11.采样和走样问题(抗锯齿)
场景的定义在三维空间中是连续的,而最终显示的像素则是一个离散的二维数组,这是计算机屏幕产生锯齿的原因。在计算机处理图形的过程中(渲染管线)有一个非常重要的阶段,就是光栅化,光栅化主要的作用是:将顶点数据的不连续性通过插值计算,将两个顶点之间不存在的点进行弥补,然后实现到屏幕像素点上的一一映射。
抗锯齿算法
⚠️:抗锯齿算法应用于硬件,且在光栅化阶段执行。
SSAA:超级采样抗锯齿 (Super-Sampling Anti-aliasing),也叫SSAA,其原理比较好理解,就是直接“放大屏幕分辨率”。假设屏幕分辨率为800x600,那么4xSSAA会将屏幕渲染到1600x1200的缓冲区上,然后在下采样到800x600,SSAA可以得到非常好的抗锯齿效果,不过SSAA需要的计算量是非常大的,光栅化和片段着色器都是原来的4倍,渲染缓存的大小也是原来的4倍。4xSSAA就是放大4倍的意思。因为SSAA要浪费大量内存,因此基本没有大量应用。
MSAA:多重采样抗锯齿 (Multi-Sampling Anti-aliasing),MSAA是对SSAA的改进,放弃了扩大像素分辨率,使用多重采样的方式,既然有多重采样,就有“单重采样”,而传统的采样方式就可以理解为“单重采样”,当然并不存在“单重采样”这个东西了。姑且这样叫它。“单重采样”就是一个像素点与一个采样点进行对应,而“多重采样”则是一个像素点与多个采样点进行对应。⚠️:MSAA不适合延迟渲染,这里说的不适合并不是不支持,延迟渲染也能支持MSAA,但是支持他就会大量浪费空间
PS:延迟渲染是什么,在这里不做过多介绍,总之他为了优化光照着色,需要一个GBuffer,这个就是临时渲染出的结果的缓存因为需要GBuffer,所以MSAA也需要进行临时保存,那么保存临时数据的后果则是浪费大量显存。
前面可知4x MSAA的显存消耗是4x的关系,假设我们按照2k的原生分辨率来做,一个RT就是64 MB,用来做MSAA的那个RT就会是不低于256MB。又因为GBuffer至少存depth,normal,basecolor3张RT,那三个MSAA RT就是768MB,这种消耗非常巨大,因此可以理解为“不支持”
CSAA:覆盖采样抗锯齿(Coverage Sampling Anti-Aliasing,简称CSAA)是NVIDIA在G80及其衍生产品首次推向实用化的AA技术,也是目前NVIDIA GeForce 8/9/G200系列独享的AA技术。CSAA就是在MSAA基础上更进一步的节省显存使用量及带宽,简单说CSAA就是将边缘多边形里需要取样的子像素坐标覆盖掉,把原像素坐标强制安置在硬件和驱动程序预先算好的坐标中。这就好比取样标准统一的MSAA,能够最高效率的执行边缘取样,效能提升非常的显著。比方说16xCSAA取样性能下降幅度仅比4xMSAA略高一点,处理效果却几乎和8xMSAA一样。8xCSAA有着4xMSAA的处理效果,性能消耗却和2xMSAA相同。
FSAA:FSAA(Fast Approximate AA)的核心思想是正常采样一个三角形,如果该三角形存在锯齿,则通过边缘检测查找出边界区域,然后使用没有锯齿的线段去替换有锯齿的边界,该方法和采样无关,因此速度很快。
TAA:TAA(Temporal AA),该方法的核心思想是复用上一帧渲染出的
包围盒问题(常出算法题)
12.三种旋转表达(四元数,欧拉角,矩阵)各自的优缺点
一、矩阵旋转:
优点:旋转轴可以是任意向量
缺点:旋转其实只需要知道一个向量+一个角度(共4个信息值),但矩阵却用了16个元素(矩阵法消耗时间和内存)
二、欧拉角旋转
优点:容易理解,形象直观;表示更方便,只需要三个值(分别对应x、y、z轴的旋转角度)
缺点:欧拉角这种方法是要按照一个固定的坐标轴的顺序旋转的,因此不同的顺序会造成不同结果;欧拉角旋转会造成万向锁现象,这种现象的发生就是由于上述固定的坐标轴旋转顺序造成的。理论上,欧拉角旋转可以靠这种顺序让一个物体旋转到任何一个想要的方向,但如果在旋转中不幸让某些坐标轴重合,就会发生万向锁现象,这时就会丢失一个方向上的旋转能力(两个旋转轴(环)重叠),也就是说在这种状态下,我们无论怎么旋转(还是按照原先的旋转顺序),都不可能得到某些想要的结果,除非打破原先的旋转顺序或者同时旋转三个轴。
由于万向锁的存在,欧拉旋转无法实现球面平滑插值。
万向锁的简单解决办法:构造一个不同的旋转层级顺序,但是万向锁总是会在某一个顺序发生,调整旋转顺序不是根本解决办法。(Unity使用的是Z-X-Y顺规,即旋转顺序为z轴、x轴、y轴,虽然某些情况下会出现万向锁,但是这种顺规出现万向锁的概率最小)
万向锁解决办法:将欧拉角转换为四元数,对四元数进行Slerp插值,再将这一系列四元数转换为对应的欧拉角,然后作用于需要进行旋转的对象。这种做法缺点在于消耗内存,但是可以使物体任意旋转,灵活度高。
使用欧拉旋转出现旋转路径偏移的根本原因:在万向锁情况下对欧拉角的插值不是线性的。(突变)
静态欧拉角:其旋转轴使用的是静止不同的参考系。
动态欧拉角:使用object本身的坐标系,因而会随着object旋转而旋转。(局部坐标系会随着对象的旋转而旋转)
三、四元数旋转
优点:可以避免万向锁;只需要一个4维的四元数就可以执行绕任意过原点的向量的旋转,方便快捷,在某些实现下比旋转矩阵效率更高;而且四元数旋转可以提供平滑插值。
缺点:比欧拉旋转稍微复杂了一点,因为多了一个维度,理解更困难,不直观。
https://zhuanlan.zhihu.com/p/79894982
13.OpenGL中VA、VAO、VBO和EBO的区别
顶点数组(Vertex Array)
VA,顶点数组也是收集好所有的顶点,一次性发送给GPU。不过数据不是存储于GPU中的,绘制速度上没有显示列表快,优点是可以修改数据。
VAO(Vertex Array Object)顶点数组对象
VAO的全名是Vertex Array Object,首先,它不是Buffer-Object,所以不用作存储数据;其次,它针对“顶点”而言,也就是说它跟“顶点的绘制”息息相关。(VAO和VA没有任何关系)
VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。
OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。
VBO(Vertex Buffer Object)顶点缓冲区对象
VBO,全称为Vertex Buffer Object,与FBO,PBO并称,但它实际上老不少。就某种意义来说,他就是VA(Vertex Array)的升级版。
通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
EBO(Element Buffer Object)索引缓冲对象
在渲染顶点这一话题上我们还有最有一个需要讨论的东西——索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。
和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。
图元装配的过程
图元装配(Primitive Assembly)是对传入的顶点数据进行重新组装,将顶点着色器的输出作为输入,这一点正验证了渲染的过程是以流水线的形式进行的,图元装配会将顶点装配成指定的图形,与此同时,会进行裁剪、被面剔除等操作,以减少不必要的计算,加速渲染过程。
glDrawArrays与glDrawElements的功能和区别
glDrawArrays 和 glDrawElements 的作用都是从一个数据数组中提取数据渲染基本图元。(render primitives from array data )
他们只是用不同的方式来将客户端中的数据传送到服务器的地址空间中,OpenGL支持3种方式来完成这个操作:
(1)访问单独的数据元素(随机存储)
(2)创建一个单独数组元素的列表(系统存取)
(3)线性的处理数组元素。具体选用的数据访问方式取决于需要处理的问题类型。
glArrayElements()、glDrawElements()和glDrawRangeElements()能够对数据数组进行随机存取,但是glDrawArrays()只能按顺序访问它们。因为前者支持顶点索引的机制。
简单的说来,顶点索引就是把输入的顶点坐标值从0开始编号,并在一个单独的无符号类型数组中保存多个索引值组成的图元信息,从而进一步避免了重复指定顶点数据造成的冗余。