(九)OpenGL 渲染技巧

403 阅读11分钟

隐藏面消除(Hidden surface elimination):在渲染过程中可能会产生一些问题,例如:在绘制3D场景的时候,我们需要决定哪些部分是对观察者 可见的,哪些部分是对观察者不可见的.对于不可见的 部分,应该及早丢弃。这种情况叫做“隐藏面消除”.

解决方法:

一.油画算法

  • 先绘制场景中里观察者较远的物体,在绘制较近的物体。
  • 例:先绘制红色部分,在绘制黄色部分,最后绘制灰色部分,既可以解决隐藏面消除的问题。
    但是油画算法也存在一些弊端:如果图片是叠加的情况该如何处理呢。

二.正背面剔除(Face Culling)

在一个立体图像中,我们只能看到一部分,那么看不见的面是不是不需要绘制呢。如果我们能丢弃这部分数据,OpenGL可以做到检查所有观察者朝上的面,这样可以节约片元着色器的性能,渲染性能上可提高50%。 但是我们会不会有这样的疑问,OpenGL是如何分辨出正背面的。

  • 正面:按照逆时针顶点连接顺序的三角形面
  • 背面:按照顺时针顶点连接顺序的三角形面

那么立方体中的正背面是如何区分的呢?

  • 左侧三角形顶点顺序为:1->2->3;右侧三角形的顶点顺序为:1->3->2.
  • 当观察者在右侧时,则右侧的三角形方向为逆时针方向,称为正面;而左侧的三角形方向为顺时针方向,称为背面。
  • 当观察者在左侧时,则左侧的三角形为逆时针方向,称为正面;而右侧的三角形为顺时针时,判定为背面。

正面和背面是由三角形的顶点定义顺序和观察者的方向共同决定的,随着观察者角度方向的改变,正背面也会随之改变。

关于正背面剔除的代码

开启表面剔除(默认背面剔除)
void glEnable(GL_CULL_FACE);

关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);

用户选择剔除那个面(正面/背⾯) 
void glCullFace(GLenum mode);
mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK

⽤户指定绕序那个为正⾯
void glFrontFace(GLenum mode);
mode参数为: GL_CW,GL_CCW,默认值:GL_CCW

例如,剔除正⾯实现(1)
glCullFace(GL_BACK);
glFrontFace(GL_CW);

例如,剔除正面实现(2) 
glCullFace(GL_FRONT);

三.开启深度测试

深度:该像素点在3D世界中距离摄像机的距离,Z值。

深度缓冲区:一块内存区域,用来存储每一个绘制在屏幕上的像素点的深度值,深度值(Z值)越大,则离摄像器越远。

深度测试:深度缓冲区(DepthBuffer)和颜色缓存区(ColorBuffer)是对应的。颜色缓存区存储像素的颜色信息,而深度缓冲区存储像素的深度信息,在决定是否绘制一个物体表面时。首先要将表面对应的像素的深度值进行比较,如果大于深度缓冲区的值,则丢弃,相反更新这个像素的深度缓冲区和颜色缓冲区,这个过程称为“深度测试”。

深度值:一般由16位,24位或者32位值表示,通常位24位。位数越高,深度的精确度越高。深度值的范围在【0,1】之间,值越小越靠近观察者,值越大表示远离观察者。

深度值计算

  • 线性深度缓存:在深度缓冲区中包含深度值介于0.0和0.1之间,从观察者看到其内容与场景中的所有对象的Z值进行了比较。这些视图空间中的Z值可以在投影平头截体的近平面和远平面之间的任何值。我们因此需要一些方法来转换这些视图空间Z值,范围为【0,1】之间,下面的(线性)方程式把Z值转换为0.0和1.0之间的值。

  • 非线性深度缓存: 在实践中是可以减少使用这样的线性深度缓冲区。正确的投影特性非线性深度方程是和1/Z成正比的,由于非线性函数是和1/Z成正比,例如1.0和2.0之间的Z值,将变为1.0到0.5之间,这样,在Z非常小的时候给了我们很高的精度。方程式如下:
    屏幕空间的深度值是非线性如他们在z很小的时候有很⾼的精度,较大的 z 值有较低的精度。该片段的深度值会迅速增加,所以⼏乎所有顶点的深度值接近 1.0。如果我们⼩心的靠近物体,你最终可能会看到的色彩越来越暗,意味着它们的 z 值越来越小,这清楚地表明 深度值的⾮线性特性。近的物体相对远的物体对的深度值比对象较大的影响。只移动几英寸就能让暗⾊完全变亮

那我们为什么需要深度缓存区呢?

在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,在绘制距离比较远的物体,因为远的物体后绘制,就会把近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。事实上,只要存在深度缓存区,OpenGL都会把像素的深度值写入到缓存区。

//开启深度测试
glEnable(GL_DEPTH_TEST);   

//关闭深度测试
glDisable(GL_DEPTH_TEST);

//禁止写入(不会存在透明度)
glDepthMask(GL_FALSE)

指定深度测试判断模式

void glDepthFunc(GLEnum mode);
函数 说明
GL_ALWAYS 总是通过测试
GL_NEVER 总是不通过测试
GL_LESS 当前深度值 < 存储的深度值时通过
GL_EQUAL 当前深度值 = 存储的深度值时通过
GL_LEQUAL 当前深度值 <= 存储的深度值时通过
GL_GREATER 当前深度值 > 存储的深度值时通过
GL_NOTEQUAL 当前深度值 != 存储的深度值时通过
GL_GEQUAL 当前深度值 >= 存储的深度值时通过

四.ZFighting 闪烁问题

因为开启深度测试后,OpenGL就不会再去绘制模型被遮挡的部分,这样实现的显示更加真实,但是由于深度缓冲区精度的限制对于深度相差非常小的情况下,(例如在同一平面上进行2次绘制),OpenGL就可出现不能正确判断两者的深度值,会导致深度测试的结果不可预测,显示出来的现象时交错闪烁。

解决闪烁问题

  • 第一步:启用Polygon Offset方式解决 让深度值之间产生间隔,如果2个图像之间有间隔,就不会产生干涉,可以理解为在执行深度测试前将立方体的深度值做一些细微的增加,于是就能将重叠的2个图行深度值之间有所区分。

//启用Polygon Offset 方式

glEnable(GL_POLYGON_OFFSET_FILL)

参数列列表: 
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT 
GL_POLYGON_OFFSET_LINE  对应光栅化模式: GL_LINE 
GL_POLYGON_OFFSET_FILL  对应光栅化模式: GL_FILL
  • 第二步:指定偏移量

1)通过glPolygonOffset 来指定.glPolygonOffset 需要2个参数: factor , units

2)每个Fragment 的深度值都会增加如下所示的偏移量: Offset = ( m * factor ) + ( r * units);

m: 多边形的深度的斜率的最大值,理解一个多边形越是与近裁剪面平行,m就越接近于0.

r: 能产生于窗口坐标系的深度值中可分辨的差异最小值.r 是由具体是由具体OpenGL 平台指定的一个常量. 3)一个大于0的Offset 会把模型推到离你(摄像机)更远的位置,相应的⼀个小于0的Offset 会把模型拉近。 4)一般而言,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 基本可以满⾜需求。

void glPolygonOffset(Glfloat factor,Glfloat units);

应⽤用到⽚片段上总偏移计算⽅方程式:
Depth Offset = (DZ * factor) + (r * units);

DZ:深度值(Z值)
r:使得深度缓冲区产生变化的最小值
负值,将使得z值距离我们更近,而正值,将使得z值距离我们更远, 对于上节课的案例例,我们设置factor和units设置为-1-1
  • 第三步:关闭Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL)

那么如何预防ZFighting闪烁问题预防: 1)不要将两个物体靠的太近,避免渲染时三⻆角形叠在一起。这种方式要求对场景中物体插⼊一个少量的 偏移,那么就可能避免ZFighting现象。例如上面的立方体和平面问题中,将平面下移0.001f就可以解决这个问题。当然手动去插入这个⼩的偏移是要付出代价的。 2)尽可能将近裁剪面设置得离观察者远一些。上⾯我们看到,在近裁剪平面附近,深度的精确度是很高的,因此尽可能让近裁剪面远一些的话,会使整个裁剪范围内的精确度变高一些。但是这种方式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。 3)使用更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用使⽤用32位的缓冲区,使精确度得到提高。

五.裁剪

窗口:就是显示界面

视口:就是窗口中用来显示图形的一块矩形区域,它可以和窗口等大,也可以比窗口大或者小。只有绘 制在视口区域中的图形才能被显示,如果图形有一部分超出了了视⼝区域,那么那一部分是看不不到的。 通过glViewport()函数设置。

裁剪区域(平行投影):就是视口矩形区域的最小最大x坐标(left,right)和最小最大y坐标 (bottom,top),而不是窗口的最小最大x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近 最远z坐标,形成一个⽴立体的裁剪区域。

OpenGL中提高渲染的一种⽅方式.只刷新屏幕上发生变化的部分.OpenGL 允许将要进行渲染的窗口只 去指定一个裁剪框.

基本原理:⽤于渲染时限制绘制区域,通过此技术可以再屏幕(帧缓冲)指定一个矩形区域。启⽤剪裁 测试之后,不在此矩形区域内的片元被丢弃,只有在此矩形区域内的片元才有可能进入帧缓冲区。因此实 际达到的效果就是在屏幕上开辟了一个小窗⼝,可以再其中进行指定内容的绘制。

//1 开启裁剪测试
glEnable(GL_SCISSOR_TEST);
//2.关闭裁剪测试
glDisable(GL_SCISSOR_TEST);
//3.指定裁剪窗⼝
void glScissor(Glint x,Glint y,GLSize width,GLSize height);
x,y:指定裁剪框左下⻆位置;
width , height:指定裁剪尺寸

六.混合

我们把OpenGL 渲染时会把颜色值存在颜色缓存区中,每个⽚片段的深度值也是放在深度缓冲区。当深度 缓冲区被关闭时,新的颜色将简单的覆盖原来颜⾊缓存区存在的颜色值,当深度缓冲区再次打开时,新的颜⾊片段只是当它们比原来的值更接近邻近的裁剪平面才会替换原来的颜色⽚段。

glEnable(GL_BlEND);
  • 组合颜色

目标颜色:已经存储在颜色缓存区的颜色值

源颜色:作为当前渲染命令结果进入颜色缓存区的颜色值

当混合功能被启动时,源颜色和目标颜色的组合⽅式是混合方程式控制的。在默认情况 下,混合方程式如下所示:

Cf = (Cs * S) + (Cd * D)
Cf :最终计算参数的颜⾊色
Cs : 源颜⾊
Cd :⽬标颜⾊ 
S  :源混合因⼦
D  :目标混合因⼦子
  • 设置混合因子 设置混合因子,需要用到glBlendFun函数

glBlendFunc(GLenum S,GLenum D);

S:混合因子

表中R、G、B、A 分别代表 红、绿、蓝、alpha。

表中下标S、D,分别代表源、目标

表中C 代表常量颜色(默认黑色)

  • 改变组合方程式

默认混合⽅方程式: Cf = (Cs*S)+(Cd*D)

实际上远不止这一种混合方程式,我们可以从5个不同的方程式中进行选择

选择混合⽅方程式的函数: glbBlendEquation(GLenum mode);

  • glBlendFuncSeparate 函数

除了能使用glBlendFunc 来设置混合因子,还可以有更灵活的选择。

void glBlendFuncSeparate(GLenum strRGB,GLenum dstRGB ,GLenum strAlpha,GLenum dstAlpha);

strRGB: 源颜⾊的混合因子 
dstRGB: ⽬标颜色的混合因子 
strAlpha: 源颜色的Alpha因子 
dstAlpha: 目标颜色的Alpha因子
  • glBlendFuncSeparate 注意

1)glBlendFunc 指定 源和目标 RGBA值的混合函数;但是glBlendFuncSeparate函数则允许为RGBAlpha 成分单独指定混合函数。

2)在混合因子表中, GL_CONSTANT_COLOR,GL_ONE_MINUS_CONSTANT_COLOR,GL_CONSTANT_ALPHA,GL_ONE_MINUS_CONSTANT值允许混合方程式中引入一个常量混合颜⾊色。

  • 常量量混合颜⾊

常量混合颜色,默认初始化为⿊色(0.0f,0.0f,0.0f,0.0f),但是还是可以修改这个常量混合颜色。

void glBlendColor(GLclampf red ,GLclampf green ,GLclampf blue ,GLclampf alpha );