OpenGL渲染技巧
先引入一个案例:
// 创建一个甜甜圈
//void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
//参数1:GLTriangleBatch 容器帮助类
//参数2:外边缘半径
//参数3:内边缘半径
//参数4、5:主半径和从半径的细分单元数量
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
设置一个甜甜圈,为了体现出阴影效果和立体效果,选取默认光源着色器开始渲染:
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
//参数2:模型视图矩阵
//参数3:投影矩阵
//参数4:基本颜色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
torusBatch.Draw();
效果如下:
ttq00.gif
可以看到,这种方式下,渲染出来的图形是有问题的,因为OpenGL并不知道图形的哪边是正面,导致我们旋转的时候把背面给转出来了,为了解决这个问题,OpenGL引入了”隐藏面消除“的概念:
在绘制3d场景时,我们需要决定哪部分是对观察者可见,哪部分是不可见的,对于不可见的部分,应该及早丢弃
隐藏面消除解决方案
油画算法:
先绘制场景中离观察者较远的物体,再绘制较劲的物体:如下图,先绘制红色部分,再绘制黄色部分,最后绘制灰色部分。
油画算法的弊端:
-
图形相互叠加的场景无法处理,如下图:
image.png - 浪费性能,例如上图,三个图形相交的区域,进行了三次渲染,这是不必要的。
所以,油画算法对于隐藏面消除是不适用的。
正背面剔除
原理分析:一个3d图形,你从任何一个方向去观察,最多只能看到三个面。所以,我们就没必要去渲染看不见的面,如果可以的话,OpenGL的渲染性能能提高最少%50。
OpenGL 可以做到检查所有正面朝向观察者的面,并渲染它们.从⽽丢弃背面朝向的面. 这样可以节约⽚元着⾊器的性能.
那么问题来了:
如何告诉OpenGL哪边是正面,哪边是背面?
解决方案:(分析顶点顺序)
OpenGL默认认为:按照逆时针顶点连接顺序的三角形被称为正面,反之,顺时针进行顶点连接的三角形就是背面
案例分析:
image.png
当观察者在右边时,右边三角形为正面,左边三角形为背面;
当观察者在左边时,右边三角形为背面,左边三角形为正面。
所以,正面和背面是根据三角形的顶点顺序和观察者的位置共同决定的。
正背面剔除的函数示例:
//开启表面剔除(默认是背面剔除)
glEnable(GL_CULL_FACE);
//关闭表面剔除(默认是背面剔除)
glDisable(GL_CULL_FACE);
//用户可以选择剔除哪个面 :GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK
glCullFace(GL_BACK);
//用户指定绕序哪边为正面(可以指定顺时针为正面,不过不建议这么做)GL_CW(顺时针) GL_CCW(逆时针)
glFrontFace(GL_CCW);
在上面的例子中,开启正背面消除后的效果如下:
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
//画完要取消,保证单次渲染的独立性
glDisable(GL_CULL_FACE);
ttq02.gif
看到,背面的黑影已经没有了,但是,仔细看!!!发现有一部分好像被咬掉了:
image.png
那么这个问题是如何产生的?又该如何解决呢?继续往下看。
深度测试
什么是深度?
深度其实就是该像素点在3D世界中距离摄像机的距离,Z值。
什么是深度缓冲区?
深度缓冲区,就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。深度值(Z值)越大, 则离摄像机就越远。
为什么需要深度缓冲区?
在不使⽤深度测试的时候,如果我们先绘制一个距离⽐较近的物体,再绘制距离较远的物体,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写入到缓冲区中. 除⾮调⽤ glDepthMask(GL_FALSE) 来禁止写⼊。
简而言之,一个像素对应一个深度值,如果需要渲染的深度比已经存在的深度还要大,那么就不会进行渲染。这个过程就叫做深度测试。
继续上面的例子:
//开启深度测试
glEnable(GL_DEPTH_TEST);
//取消深度测试
glDisable(GL_DEPTH_TEST);
效果如下:
yjde2-u46bj.gif
我们还可以自定义深度判断测试的模式
:
开启深度测试就一定没问题了吗?实际上还是会有两个问题:
- 如果两个深度值一样或者相差很小以至于OpenGL无法判断该怎么办?(ZFighting)
- 如何处理半透明的颜色?(混合)
ZFighting
怎么出现的?
开启深度测试后,OpenGL 就不会再去绘制模型被遮挡的部分。这样实现的显示更加真实。但是 ,由于深度缓冲区精度的限制对于深度相差⾮常小的情况下,(例如在同一平⾯上进⾏2次绘制),OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测。显示出来的现象是交错闪烁前面2个画面,交错出现。
解决流程:
-
启用 Polygon Offset 方案
让深度值之间产生间隔。如果2个图形之间有间隔,是不是意味着就不会产⽣干涉。可以理解为在执行深度测试前将⽴方体的深度值做一些细微的增加。这样OpenGL就可以进行区分。
/** 启用 polygon offset(多边形偏移)
* 参数列表:
GL_POLYGON_OFFSET_POINT :对应光栅化模式: GL_POINT
GL_POLYGON_OFFSET_LINE : 对应光栅化模式: GL_LINE
GL_POLYGON_OFFSET_FILL : 对应光栅化模式: GL_FILL
*/
glEnable(GL_POLYGON_OFFSET_FILL);
//关闭 polygon offset
glDisable(GL_POLYGON_OFFSET_FILL);
-
指定间隔(不能随意指定,需要有特定的方程式)
使用glPolygonOffset来指定偏移量:
- 每一个像素的深度值都会增加如下所示的偏移量:
Offset = ( m * factor ) + ( r * units);
m : 多边形的深度的斜率的最大值,理解一个多边形越是与近裁剪面平行,m 就越接近于0。
r : 能产生于窗口坐标系的深度值中可分辨的差异最小值。r 是由具体是由具体OpenGL 平台指定的 一个常量。 - ⼀个大于0的Offset 会把模型推到离你(摄像机)更远的位置,相应的⼀个小于0的Offset 会把模型拉近。
- 一般⽽言,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 就基本可以满足需求。
void glPolygonOffset(Glfloat factor,Glfloat units);
应⽤用到⽚片段上总偏移计算⽅方程式:
Depth Offset = (DZ * factor) + (r * units);
DZ:深度值(Z值)
r:使得深度缓冲区产⽣生变化的最⼩小值
- 关闭 polygon offset
glDisable(GL_POLYGON_OFFSET_FILL);
由于ZFighting并不容易随便出现,所以这里就不做演示了。这种方案只是遇到时的一种解决方案。
如何预防ZFighting
- 不要将两个物体靠的太近,避免渲染时三⻆形叠在一起。这种方式要求对场景中物体插⼊一个少量的偏移,那么就可能避免ZFighting现象。当然手动去插入这个⼩小的偏移是要付出代价的。
- 尽可能将近裁剪面设置得离观察者远一些。上面我们看到,在近裁剪平面附近,深度的精确度是很高的,因此尽可能让近裁剪面远一些的话,会使整个裁剪范围内的精确度变⾼一些。但是这种方式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。
- 使⽤更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用32位的缓冲 区,使精确度得到提⾼。
混合
OpenGL 渲染时会把颜色值存在颜色缓冲区,每个片段的深度值也是放在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来颜色缓存区存在的颜⾊色值,当深度缓冲区再次打开时,新的颜色⽚段只是当它们⽐原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。
//开启混合
glEnable(GL_BLEND);
比如我们在处理滤镜的时候,是怎么去做的呢?其实就是将原来的颜色经过一定的算法,得到了目标颜色。这个算法就是 混合方程式:
目标颜色:已经存储在颜色缓冲区的颜色值
源颜色:作为当前渲染命令结果进入颜色缓冲区的颜⾊值
当混合功能被启动时,源颜色和⽬标颜色的组合方式是混合方程式控制的。在默认情况
下,混合方程式如下所示:
Cf = (Cs * S) + (Cd * D)
Cf :最终计算参数的颜色
Cs : 源颜⾊
Cd :⽬标颜色
S :源混合因子
D :⽬标混合因子
设置混合因子:
// S:原混合因子 D:目标混合因子
glBlendFunc(GLenum S,GLenum D);
image.png
表中R、G、B、A 分别代表 红、绿、蓝、alpha。
表中下标S、D,分别代表源、目标。
表中C 代表常量颜色(默认⿊色)。
下⾯通过一个常见的混合函数组合来说明:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
如果颜⾊缓冲区已经有一种颜⾊红色(1.0f,0.0f,0.0f,0.0f),这是目标颜⾊Cd,如果在这上⾯⽤一种alpha为0.6的蓝⾊色(0.0f,0.0f,1.0f,0.6f)
Cd (⽬标颜色) = (1.0f,0.0f,0.0f,0.0f)
Cs (源颜色) = (0.0f,0.0f,1.0f,0.6f)
S = 源alpha值 = 0.6f
D = 1 - 源alpha值= 1-0.6f = 0.4f
混合方程式:Cf = (Cs * S) + (Cd * D)
等价于 = (Blue * 0.6f) + (Red * 0.4f)
总结一下:最终颜色是以原先的红色(⽬标颜色)与后来的蓝色(源颜色)进⾏组合。源颜色的alpha值 越高,添加的蓝⾊颜⾊成分越高,⽬标颜⾊所保留的成分就会越少。 混合函数经常用于实现在其他⼀些不透明的物体前面绘制一个透明物体的效果。
所以,我们在做开发的过程中,有一些常用的优化方案就是尽量不要使用半透明,因为半透明是需要另外做混合的算法,可能影响性能。