八、隐藏面消除解决方案

1. OpenGL中的正面、背面(隐藏面)

左侧正面,右侧背面.png
  • 在上图右侧三角形中,线条是从 V0 ->V1 -> V2 -> V0 沿着顶点顺时针方向形成⼀个闭合三角形。 这种顺序与方向结合来指定顶点的⽅式称为环绕
  • 环绕方式由两种:一种是顺时针环绕,一种是逆时针环绕
  • 在 OpenGL 中默认具有逆时针⽅向环绕的多边形为正面。当我们从正面看屏幕上的这个图时,左侧三角形展示的是正面,右侧三角形展示的是背面。假设我们绕到这个图的背面去看时,顺时针和逆时针的方向就变了。 所以如555555t是否为正面,是由环绕方向及观察者方向共同决定的。
  • 这种默认可以更改,更改代码如下。但是由于大家都习惯逆时针方向为正面,所以不建议更改。
glFrontFace(GL_CW);
GL_CW: 设置OpenGL 顺时针环绕的多边形为正面;
GL_CCW: 设置OpenGL 逆时针环绕的多边形为正面

2. 为什么要区分正面、背面

在图形渲染过程中,我们能看到的正面会渲染出颜色,背面黑色。区分了正背面后,可以将背面剔除,以某种方式丢弃这部分数据,不渲染背面,可以提高至少 50% 的渲染性能。

3. 隐藏面消除解决方案

3.1 油画算法

  • 先渲染场景中离观察者较远的图层,再渲染较近的图层。
  • 下图中有多个图层依次叠加在一起,遵循油画算法,先渲染红色图层、再绘制黄色图层、最后渲染灰色图层。这样叠加在一起,可以自然的将背面藏起来,让大家看不到。
图层由远及近绘制.png
3.1.1 油画算法存在的问题
  • 这种方法是非常低效的,因为对于图层重叠的每个像素都要进行多次渲染。
  • 如果图层之间,不是平行的一层一层的叠加,而是交叉叠放时, 没有明确的先后顺序,油画算法就不能解决问题了。如下图这样:


    图层相互交叠.png

3.2 使用正面、背面剔除

//1.开启正背面剔除
glEnable(GL_CULL_FACE);
//2.关闭正背面剔除
glDisable(GL_CULL_FACE);
//3. 指定剔除面:mode 参数的可用值为GL_FRONT、GL_BACK、GL_FRONT_AND_BACK
glCullFace(GLenum mode);

3.2.1 代码实践

我们通过一个 3D 的甜甜圈案例,来看看正背面剔除的效果。

  1. 首先画一个 3D 甜甜圈. OpenGL有一个甜甜圈的批次类,可以直接供给我们使用。
    // 4.创建一个甜甜圈批次类
    //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);
  1. 通过上下左右键位,控制甜甜圈旋转
//键位设置,通过不同的键位对其进行设置
//控制Camera的移动,从而改变视口
void SpecialKeys(int key, int x, int y)
{
    //1.判断方向
    if(key == GLUT_KEY_UP)
        //2.根据方向调整观察者位置
        viewFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_DOWN)
        viewFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f);
    
    if(key == GLUT_KEY_LEFT)
        viewFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f);
    
    if(key == GLUT_KEY_RIGHT)
        viewFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f);
    
    //3.重新刷新
    glutPostRedisplay();
}
  1. 旋转后重绘甜甜圈

//渲染场景
void RenderScene()
{
    //1.清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    
    //2.把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    
    //3.设置绘图颜色
    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);
    
    //5.绘制
    torusBatch.Draw();

    //6.出栈 绘制完成恢复
    modelViewMatix.PopMatrix();
    
    //7.交换缓存区
    glutSwapBuffers();
}
  1. 接下来,看下运行效果:
  • 我们绘制的是红色的甜甜圈,在OpenGL中,正面绘制的是我们设置的颜色,背面绘制的黑色。
  • 最初展示时,我们看到的是正面红色,这是正确的。但是随着观察者旋转,展示出红黑相间的样子,最终转到背面时,展示全黑色。
  • OpenGL 正面背面都绘制时,显示的效果就错乱了。


    未进行背面剔除的甜甜圈.gif
4.1 对甜甜圈进行背面剔除

对甜甜圈进行背面剔除,背面看不到就不绘制。

    // 开启背面剔除
    glEnable(GL_CULL_FACE);
    
    //5.绘制
    torusBatch.Draw();
    
    // 关闭背面剔除
    glDisable(GL_CULL_FACE);
背面剔除的甜甜圈.gif

不过看起来甜甜圈还有些问题,旋转到侧面时,看起来在上半部分或者下半部分缺少了一个块。为什么呢?

  • 这是因为整个甜甜圈,我们能看到的都是正面。如下图中的ABCD四个区域,都是正面。当甜甜圈旋转到侧面对着我们时,AB区域就重合了。但是这两个都是正面,哪个该显示,哪个不该显呢?
  • 我们直观的看上去,知道应该显示的A区,因为A区离我们更近,所以在重叠时,A区应该挡住B区。
  • 但是OpenGL它不知道哪个离我们近呀。应该怎么让它知道呢?这个时候,我们就需要借助深度测试了。
image.png

3.3 深度测试

3.3.1 深度

  • 表示的是像素在Z轴上距离观察者的距离
  • 取值范围是0~1,默认值是1,通常精度是24位,也可以设置为16位或者32位,位数越大,精度越好。
  • 一般情况下,我们作为观察者,看手机屏幕时,是处于Z轴的负半轴。从这个角度上看,深度值(Z值)越小,离观察者越近
  • 屏幕深度值具有非线性特性(在投影矩阵应用之前是线性的),即深度值 = 0.5 不代表物体 z 值在投影平截头体的中间。

3.3.2 深度缓冲区

是一块内存区域,存储每一个像素点的深度值,一个像素只有一个深度。

3.3.3 深度测试

  • 深度缓冲区与颜色缓冲区是对应的,一个存储像素的深度值,一个存储像素的颜色值。
  • 每当有新的图形图像传入时,先判断它的深度值,如果大于该像素在深度缓存区的深度值则丢弃。反之将新的深度值和颜色值,更新到对应的深度缓冲区及颜色缓冲区。(因为一般情况深度值越小的离观察者越近,这个规则也可以更改)。这个过程,称为深度测试。
// 在绘制场景前,清除颜⾊缓存区,深度缓冲
glClearColor(0.0f,0.0f,0.0f,1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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

// 打开/关闭深度缓冲区写入 ,GL_FALSE: 关闭写入 GL_TRUE:打开写入
glDepthMask(GLBool value);

// 指定深度测试判断模式,默认GS_LESS,当前深度值 < 存储深度值时通过
glDepthFunc(GLEnum mode);
深度测试判断模式.png

3.3.4 甜甜圈开启深度测试

  // 开启深度测试
    glEnable(GL_DEPTH_TEST);
    
    //5.绘制
    torusBatch.Draw();
    
    // 关闭深度测试
    glDisable(GL_DEPTH_TEST);
开启深度测试后的甜甜圈.gif

现在甜甜圈的展示,已经非常好了!
其实开启了深度测试后,我们甚至不需要使用背面剔除功能。因为深度测试的原理是一个像素内只绘制离观察者最近的图形图像。甜甜圈的背面离观察者远,自然不会绘制。

3.3.5 深度测试存在 Z-Fighting / Z闪烁 的问题

如果AB两个深度值特别相近,已经超出了深度值的精度位数,就无法比较两个值的大小,会出现可能一会儿显示A的图像,一会儿显示B的图像,造成一种闪烁的现象,这个现象叫做Z-Fighting 或者 Z闪烁。

3.3.6 Z-Fighting / Z闪烁 解决方案

  • 多边形偏移。如果两个相差的很小,可以人为的设置一个偏移值,使得两个值变大,变大后符合精度位数限制,就可以正确的比较大小了。
// 1. 开启多边形偏移
glEnable(GL_POLYGON_OFFSET_FILL)
// 2. 关闭多边形偏移
glDisable(GL_POLYGON_OFFSET_FILL)
参数列列表: 
GL_POLYGON_OFFSET_POINT   对应光栅化模式: GL_POINT
GL_POLYGON_OFFSET_LINE    对应光栅化模式: GL_LINE
GL_POLYGON_OFFSET_FILL    对应光栅化模式: GL_FILL

// 2. 设定偏移量(一般情况下,设置-1, -1)
glPolygonOffset(Glfloat factor,Glfloat units)

3.3.6 Z-Fighting / Z闪烁 预防方案

  • 不要将两个物体靠的太近,避免渲染时三角形叠在⼀一起。因为手动去插⼊这个偏移是要付出代价的。
  • 尽可能将近裁剪⾯设置得离观察者远一些。上面我们看到,在近裁剪平⾯附近,深度的精确度是很高的,因此尽可能让近裁剪面远⼀些的话,会使整个裁剪范围内的精确度变⾼一些。但是这种⽅式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。
  • 使⽤更⾼位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有⼀些硬件使⽤32位的缓冲区,使精确度得到提⾼。

3.4 裁剪测试

  • 裁剪测试是OpenGL提高渲染性能的一种方式。只刷新屏幕上变化的部分。
  • 原理是设置一个裁剪区域,只渲染裁剪区域内的图形图像。也就是只有在范围内的片元,才会到达帧缓冲区,超出范围的将被丢弃。
//1 开启裁剪测试 
glEnable(GL_SCISSOR_TEST);
//2.关闭裁剪测试 
glDisable(GL_SCISSOR_TEST);
//3.指定裁剪窗⼝,  x,y:指定裁剪框左下角位置; width , height:指定裁剪尺⼨
void glScissor(Glint x,Glint y,GLSize width,GLSize height);

3.5 混合

  • 在像素缓冲区中,每一个像素有一个对应的颜色值。
  • 如果未开启深度测试,新的颜色值将会改变颜色缓冲区的颜色值;如果开启了,新的颜色值离观察者更近的才会改变。
  • 当新的颜色透明度为 1 时,新的颜色值需要改变颜色缓冲区的颜色值时,会直接覆盖。 小于 1 时,两种颜色需要混合显示。
// 启用颜色混合
glEnable(GL_BlEND);
// 关闭颜色混合
glDisable(GL_BlEND);

3.5.1 混合方程式

Cf = (Cs * S) + (Cd * D)
Cf :最终计算参数的颜⾊
Cs : 源颜⾊
Cd :⽬标颜色
S:源混合因⼦子
D:⽬标混合因⼦

// 选择混合方程式
void glbBlendEquation(GLenum mode);

// 设置混合因⼦, S:源混合因子 D:⽬标混合因⼦
void glBlendFunc(GLenum S,GLenum D); 
// 常见的混合因子
void glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

// 设置混合因⼦及Alpha因⼦
// strRGB: 源颜⾊的混合因子 
// dstRGB: ⽬标颜色的混合因子 
// strAlpha: 源颜⾊的Alpha因子 
// dstAlpha: ⽬标颜色的Alpha因⼦
void glBlendFuncSeparate(GLenum strRGB,GLenum dstRGB ,GLenum strAlpha,GLenum dstAlpha);

// 设置常量混合颜色,默认黑色
void glBlendColor(GLclampf red ,GLclampf green ,GLclampf blue ,GLclampf alpha );
可选择的混合方程式.png
混合因子.png
  • 表中R、G、B、A 分别代表 红、绿、蓝、alpha。
  • 表中下标S、D,分别代表S:源混合因子 D:⽬标混合因⼦
  • 表中C 代表常量颜色(默认⿊色)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352