四、OpenGL深度缓冲区、裁剪和混合

1. 在渲染过程中可能产⽣的问题

在绘制3D场景时,我们需要决定哪些部分是对观察者可⻅的,或者哪些部分是不可见的。对于不可⻅的部分,应该及早丢弃,这种做法叫做隐藏⾯消除

1.1 解决方案

1.1.2 油画算法
  • 先绘制场景中的离观察者较远的物体,再绘制较近的物体。

先绘制红色部分,再绘制⻩色部分,最后再绘制灰⾊部分,即可解决隐藏面消除的问题。即将场景按照物理距离和观察者的距离远近排序,由远及近的绘制即可。

油画算法

弊端:如果三个三⻆形是相互重叠的,油画算法将⽆法处理。

油画法弊端
1.1.2 正背⾯剔除(Face Culling)

从任何⼀个⽅向去观察一个立方体,最多可以看到3个⾯。如果我们能以某种⽅式去丢弃这部分数据。OpenGL在渲染的性能即可提高超过50%。

任何平⾯都有2个⾯:正⾯和背面,⼀个时刻我们只能看到一面。通过分析顶点数据的顺序,OpenGL可以做到检查所有正面朝向观察者的面,并渲染它们;从⽽丢弃背面朝向的面。

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

⚠️注意:正⾯和背⾯是有三角形的顶点定义顺序和观察者方向共同决定的。若观察者的观察⽅向发生改变,正⾯和背面也会发生相应的改变。

分析⽴⽅体中的正背面
  • 当观察者在右侧时:右边的三角形为逆时针方向,则为正面;而左侧的三⻆形为顺时针,则为背⾯。
  • 当观察者在左侧时:左边的三⻆形为逆时针⽅向,则为正⾯;而右侧的三⻆形为顺时针,则为背⾯。

弊端:如果前后两个点都是正面或是背面,这时OpenGL无法区分哪个面在前,哪个面在后,就可能出现下图所示的问题。

正背⾯剔除的弊端

1.2 OpenGL中的剔除

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

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

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

//用户指定绕序那个为正面
//mode参数为: GL_CW, GL_CCW,默认值:GL_CCW
glFrontFace(GL enum mode);

//剔除正面实现①
glCullFace(GL_BACK);
glFrontFace(GL_CW); 

//剔除正面实现②
glCullFace(GL_FRONT);
glFrontFace(GL_CCW);

2. 深度

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

2.1 深度缓冲区

深度缓存区,就是⼀块内存区域,专门存储每个像素点的深度值。深度值(Z值)越⼤,则离摄像机就越远。

为什么需要深度缓冲区?

  • 在不使⽤深度测试的时候,如果先绘制⼀个⽐较近的物体,再绘制较远的物体。较远的图像就会像油画一样覆盖掉之前的图像,就会1.1节中最后的问题。
    有了深度缓冲区后,绘制物体的顺序就不那么重要了。
    只要通过开启了深度缓冲区,并允许深度值的写入,OpenGL都会把像素的深度值写入到缓冲区中。

2.2 深度测试

深度缓冲区和颜⾊缓存区是对应的。颜⾊缓存区存储像素的颜⾊信息,而深度缓冲区存储像素的深度信息

在决定是否绘制⼀个物体表⾯时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较。如果大于深度缓冲区中的值,则丢弃这部分;否则利⽤这个像素对应的深度值和颜⾊值,分别更新深度缓冲区和颜色缓存区。这个过程称为深度测试

2.3 深度值的计算

深度值,⼀般由16位、24位或者32位值表示,通常是24位。

  • 位数越高,深度的精确度越好。
  • 深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越大表示远离观察者。

2.4 深度值的使用

  • 开启深度测试
glEnable(GL_DEPTH_TEST);
  • 在绘制场景前,清除颜⾊缓存区,深度缓冲。清除深度缓冲区默认值为1.0。
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  • 指定深度测试判断模式
void glDepthFunc(GLEnum mode);
函数 说明
GL_ALWAYS 总是通过测试
GL_NEVER 总是不通过测试
GL_LESS 在当前深度值 < 存储的深度值时通过
GL_GREATER 在当前深度值 > 存储的深度值时通过
GL_EQUAL 在当前深度值 == 存储的深度值时通过
GL_LEQUAL 在当前深度值 <= 存储的深度值时通过
GL_GEQUAL 在当前深度值 >= 存储的深度值时通过
GL_NOTEQUAL 在当前深度值 != 存储的深度值时通过
  • 深度缓冲区写入开关
//value: GL_TURE,开启深度缓冲区写入; GL_FALSE,关闭深度缓冲区写⼊
void glDepthMask(GLBool value);

2.5 总结

使⽤正⾯/背面剔除法和深度测试法解决了OpenGL的渲染效率问题。

3. ZFighting闪烁问题

3.1 原因

由于精度的限制,对于相差非常小的深度值(比如在同一个深度进行2次渲染),就可能出现不能正确区分两个深度值的问题,导致测试的结果随机出现。所以,显示时2个画⾯交错出现,就会出现闪烁问题。

3.2 解决办法

  • 步骤1:启用Polygon Offset

增大重叠或深度值接近的2个图形的深度值差距,使得OpenGL可以区分两个深度值。

Polygon Offset模式 对应的光栅化模式
GL_POLYGON_OFFSET_FILL GL_FILL
GL_POLYGON_OFFSET_LINE GL_LINE
GL_POLYGON_OFFSET_POINT GL_POINT
glEnable(GL_POLYGON_OFFSET_FILL);
  • 步骤二:指定偏移量

    • 通过glPolygonOffset来指定2个参数 factor和units
    //应⽤到⽚段上总偏移计算⽅程式
    //Depth Offset = (DZ * factor) + (r * units);
    //DZ:深度值(Z值)
    //r:使得深度缓冲区产⽣变化的最⼩值,是由具体OpenGL平台指定的⼀个常量
    void glPolygonOffset(Glfloat factor, Glfloat units);
    
    • offset为负值,将使得z值距离摄像机更近;⽽正值,将使得z值距离摄像机更远。 一般而言,我们设置factor和units设置为-1.0和-1.0。

步骤三:关闭Polygon Offset

// 参数和开启的参数相同
glDisable(GL_POLYGON_OFFSET_FILL);

3.3 问题的预防

  • 1. 不要将两个物体靠的太近
    避免渲染时三⻆形叠在一起。
  • 2. 尽可能将近裁剪面设置得离观察者远一些
    如果观察者离近裁剪平⾯很近,那么深度测试对精确度要求很⾼。因此,可以适当推远近裁剪平⾯的位置来避免这个问题,但是可能导致离观察者较近的物体被裁减掉,使用时需要小心。
  • 3. 使用更高位数的深度缓冲区
    默认的深度缓冲区精度是24位的,如果硬件支持32位的缓冲区,就可以提供更高的精度。

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);

4.1 理解窗口、视口、裁剪区域

  • 窗口
    显示界面。就相当于iOS里面的window。
  • 视口
    窗口中⽤来显示图形的一块矩形区域,它可以和窗⼝等⼤,也可以⽐窗口⼤或者小。只有绘制在视口区域中的图形才能被显示,如果图形有一部分超出了视口区域,那么那一部分是看不到的。通过glViewport()函数设置。就相当于View。
  • 裁剪区域(平⾏投影)
    视口矩形区域的最小最大x坐标(left,right)和最小最⼤y坐标 (bottom,top),⽽不是窗口的最小最大x坐标和y坐标。通过glOrtho()函数设置,这个函数还需指定最近最远z坐标,形成一个立体的裁剪区域。 就相当于设置一个frame。
窗口和视口

5. 混合

OpenGL渲染时会把颜色值存在颜⾊缓存区中,每个⽚段的深度值也是放在深度缓冲区。

  • 当深度缓冲区被关闭时,新的颜色将简单地覆盖原来颜色缓存区存在的颜色值。
  • 当深度缓冲区再次打开时,新的颜⾊片段只是当它们比原来的值更接近邻近的裁剪平⾯才会替换原来的颜⾊片段。
//开启混合
gl_Enable(GL_BIEND);

5.1 颜色混合

  • ⽬标颜色:已经存储在颜色缓存区的颜色值 (已经存在的颜色,旧颜色)
  • 源颜色:作为当前渲染命令结果进入颜色缓存区的颜⾊值 (新进来的颜色 ,新颜色)

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

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

混合函数经常用于实现在其他一些不透明的物体前面绘制一个透明物体的效果。

5.2 混合方程式

实际上不止一种颜色混合方程式,OpenGL有5个不同的方程式进行选择。

glbBlendEquation(GLenum mode);
模式 函数
GL_FUNC_ADD Cf = (Cs * S) + (Cd * D)
GL_FUNC_SUBTRACT Cf = (Cs * S) - (Cd * D)
GL_FUNC_REVERSE_SUBTRACT Cf = (Cd * D) - (Cs * S)
GL_MIN Cf = min(Cs, Cd)
GL_MAX Cf = max(Cs, Cd)

5.3 设置混合因⼦

//S:源混合因⼦
//D:⽬标混合因子
glBlendFunc(GLenum S, GLenum D);
函数 RGB混合因子 Alpha混合因子
GL_ZERO (0, 0, 0) 0
GL_ONE (1, 1, 1) 1
GL_SRC_COLOR (Rs, Gs, Bs) As
GL_ONE_MINUS_SCR_COLOR (1, 1, 1) - (Rs, Gs, Bs) 1 - As
GL_DST_COLOR (Rd, Gd, Bd) Ad
GL_ONE_MINUS_DST_COLOR (1, 1, 1) - (Rd, Gd, Bd) 1 - Ad
GL_SRC_ALPHA (As, As, As) As
GL_ONE_MINUS_SCR_ALPHA (1, 1, 1) - (As, As, As) 1- As
GL_DST_ALPHA (Ad, Ad, Ad) Ad
GL_ONE_MINUS_DST_ALPHA (1, 1, 1) - (Ad, Ad, Ad) 1- Ad
GL_CONSTANT_COLOR (Rc, Gc, Bc) Ac
GL_ONE_MINUS_CONSTANT_ALPHA (1, 1, 1) - (Ac, Ac, Ac) 1- Ac
GL_CONSTANT_ALPHA (Ac, Ac, Ac) Ac
GL_ONE_MINUS_CONSTANT_ALPHA (1, 1, 1) - (Ac, Ac, Ac) 1- Ac
GL_SRC_ALPHA_SATURATE (f, f, f)* f = min(As, 1 - Ad) 1

表中R、G、B、A 分别代表 红、绿、蓝、Alpha
表中下标S、D,分别代表源、⽬标
表中C 代表常量颜⾊(默认⿊色)

  • 常量混合颜色
    默认初始化为⿊色(0, 0, 0, 0),通过下面的函数可以修改这个颜色。
void glBlendColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha );
  • glBlendFuncSeparate函数
    除了能使⽤OpenGL内置的混合因⼦,还可以有更灵活的选择。
    //strRGB: 源颜色的混合因⼦
    //dstRGB: 目标颜⾊的混合因⼦
    //strAlpha: 源颜⾊的Alpha因⼦
    //dstAlpha: 目标颜⾊的Alpha因⼦
    void glBlendFuncSeparate(GLenum strRGB, GLenum dstRGB , GLenum strAlpha, GLenum dstAlpha);
    
5.3.1 举例

下⾯通过一个常见的混合函数组合来说明问题:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

如果颜色缓存区已经有一种颜⾊红色(1, 0, 0, 0)
在这上面混合一种alpha为0.6的蓝色(0, 0, 1, 0.6)

Cd (目标颜色) = (1, 0, 0, 0)
Cs (源颜色) = (0, 0, 1, 0.6)
S = 源alpha值 = 0.6
D = 1 - 源alpha值= 1-0.6 = 0.4
⽅程式Cf = (Cs * S) + (Cd * D)
等价于 = (Blue * 0.6) + (Red * 0.4)

最终颜色是以原先的红色(⽬标颜色)与后来的蓝色(源颜色)进⾏组合。源颜色的alpha值越高,添加的蓝色颜色成分越高,⽬标颜⾊所保留的成分就会越少。

对应demo代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容