iOS视觉(五) -- 正背面剔除、深度测试与颜色混合

623 阅读12分钟

一、前言

上篇主要是绘制一些简单的图形, 但是全是在平面着色器上进行的. 系统API提供了一个绘制‘🍩’的图案. 如果是在平面着色器上进行绘制, 会感觉没有立体感. 如果使用光照着色器, 就会发现它变得比较立体一些. 但是同时也会产生一些问题. 这里主要依此类问题来进行分析.

二、正背面剔除

使用光照着色器后,效果如下:

感觉就像是出了bug一般, 但是使用平面着色器就不会出现这种情况, 为什么会出现这种情况呢?

因为在不断的旋转过程中, OpenGL不知道那些部分是可以对观察者可见的, 通过旋转后, 我们就看到了本应不该看到的部分(由于观察者不可见,被丢弃掉的部分),此时不存在的部分被加载了就是我们所见的黑色(颜色被丢弃了). 例如在一道不透明的物体之后的东西, 观察者是不可见的, 所以并不会去渲染背面的部分. 这种情况就叫做 隐藏面消除.

2.1、油画算法

油画算法就是先绘制场景中的离观察者较远的物体, 再绘制较近的物体.

这样就可以将不可见的部分也给渲染.

但是油画算法也有一个弊端, 例如:

这个图像中并没有按照观察者远近来排序, 这种叠加情况是油画算法无法处理的.

2.2、正背面剔除

当我们去看一个3D物体时, 从任何一个角度看过去, 我们都不能看到这个物体都全貌, 我们最多只能看到它三个面.

那么我们在绘制的时候, 就可以不用去绘制那些看不到的面. 每次旋转进行重新渲染的时候, 仅仅需要渲染看到的界面. 这将大大的提高了渲染性能.

那么哪些面该定义为我们看到面? 哪些定义为我们看不到的面呢?

我们可以通过分析顶点数据的顺序来决定, 通过逆时针绘制的就为正面, 通过顺时针绘制的就为反面.

当我们观察者角度发生变化的时候, 正面与反面也应该会发生变化:

在OpenGL中, 已经为我们提供了相应的办法来实现这个功能:

我们仅仅需要对其开启这个功能就能达到想要的效果.

为了方便观察, 我们开启一个右击菜单栏, 通过选项来开启与关闭一些功能:

int iCull = 0;
//开始渲染
void RenderScene()
{
    //清除一个或一组特定的缓冲区
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
    
    //开启/关闭正背面剔除功能
    if (iCull) {
        glEnable(GL_CULL_FACE);//开启正背面剔除
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);//剔除背面
    }else
    {
        glDisable(GL_CULL_FACE);//关闭剔除
    }
    ...//省略
}
void ProcessMenu(int value)
{
    switch(value)
    {
        case 1:
            iCull = !iCull;
            break;
    }
    glutPostRedisplay();
}
int main(int argc,char* argv[])
{
    ...//省略
    glutCreateMenu(ProcessMenu);
    glutAddMenuEntry("正背面剔除",1);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    ...//省略
}

来看一下效果:

可以看到解决了正背面问题. 但是又出现了一个新的问题, 会出现一个缺口. 这个问题就在下面分析.

三、深度测试

在旋转过程中我们可以知道:

当我们观察者看向最左边绿色区域到时候, 这两个绿色区域等于我们来说都是正面, 黑色则都是反面. 这样就产生了一个问题. OpenGL并不知道需要向你展示哪一个正面才是对的.

日常生活中我们知道, 当看向两面叠起来的平面的时候, 我们只需要看到我们眼睛最近的平面, 这个平面之后的平面,我们是看不到的.

3.1、深度

在OpenGL中,物体离观察者之间的具体通常的都是通过Z值来知道的,所以OpenGL中就用 ’深度‘来表示像素点在3D世界中具体摄像机的距离(z值). 这里的z值表示的是图形本身的z值, z值与观察者的z值的绝对值越小,越靠近观察者.

3.2、深度缓冲区

深度缓冲区就是一块显存区域, .

深度缓冲区就是距离观察者平面的深度值与窗口中每个像素点一对一进行关联以及存储. 深度值越大, 则离摄像机越远.

一个像素点只会存储一个深度值, 无论有多少个图层.深度值经过OpenGL都会转换变成[0, 1].

为了解决上面都问题, 我们就需要每次渲染的时候去获取当前像素点的深度值, 如果新渲染的像素点在当前的像素点深度下面, 那么我们就没必要进行渲染这个新的像素点,因为他对观察者来说是不可见的. 如果新的像素点在当前像素点深度之上, 我们直接将这个新的像素点都深度进行缓冲区里的替换.

每一次进行渲染后我们都需要更新缓冲区,先进行深度缓冲区清除 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

3.3、深度测试

深度缓冲区(DepthBuffer)颜⾊缓存区(ColorBuffer) 是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息.在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较. 如果⼤于深度缓冲区中的值,则丢弃这部分.否则利⽤这个像素对应的深度值和颜⾊值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为” 深度测试

深度测试的开始非常简单:

glEnable(GL_DEPTH_TEST);

深度缓冲区的默认值为1.0, 表示最大的深度值.

我们可以通过 glDepthFunc(GLenum func); 来修改深度测试的规则:

来实现一下深度测试:

int iDepth = 0;
//开始渲染
void RenderScene()
{
    //清除一个或一组特定的缓冲区
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
    
    ...//省略
    //根据设置iDepth标记来判断是否开启深度测试
    if(iDepth) {
        glEnable(GL_DEPTH_TEST);
    } else {
        glDisable(GL_DEPTH_TEST);
    }
    ...//省略
}
void ProcessMenu(int value)
{
    switch(value)
    {
        case 1:
            iCull = !iCull;
            break;
        case 2:
            iDepth = !iDepth;
            break;
    }
    glutPostRedisplay();
}
int main(int argc,char* argv[])
{
    ...//省略
    glutCreateMenu(ProcessMenu);
    glutAddMenuEntry("正背面剔除",1);
    glutAddMenuEntry("深度测试",2);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    ...//省略
}

3.4、深度测试的潜在风险之Z-fighting(Z冲突、闪烁)问题

OpenGL开启深度测试之后, 它就不会去绘制模型被遮挡的部分. 这样实现更加的真是, 但是由于深度缓冲区的精度的限制对于深度相差非常小的情况下,OpenGL就可能出现不能正确判断两者的深度值(例如: 一个像素点深度为0.8000001, 一个为0.8000002),会导致深度测试的结果不可预测,显示出来的现象时而交错时而闪烁.

这种现象主要是发生在同一个位置上的像素点出现重复,且深度值出现精确度很低时,就会发生. 这就表示两个物体非常的近,无法确定谁先谁后, 从而出现歧义.

3.5、Z-fighting的解决 -- 多边形偏移

既然是因为两个图层靠的太近,无法分出先后顺序, 那么此时,就可以在两个图层之间加入一个微妙的间隔. 那么手动添加的话,复杂并且不精确. 所以OpenGL提供来一个解决方案: 多边形偏移

启用多变形偏移也非常简单:

//启⽤Polygon Offset ⽅式
glEnable(GL_POLYGON_OFFSET_FILL)
//关闭
glDisable(GL_POLYGON_OFFSET_FILL);

参数列表:
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT
GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE
GL_POLYGON_OFFSET_FILL 对应光栅化模式: GL_FILL


我们也可以通过glPolygonOffset来指定偏移量. 需要两个参数: factor、units

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

每个Fragment 的深度值都会增加如下所示的偏移量:

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:使得深度缓冲区产⽣变化的最⼩值 负值,将使得z值距离我们更近,⽽正值,将使得z值距离我们更远, 对于上节课的案例,我们设置factor和units设置为-1,-1

四、颜色混合

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

使用开关的方式的话, 是单纯的将两个图层重叠进行混合.在可编程着色器中,我们将操作片元着色器,处理图片的原图颜色, 再进行一个颜色值进行颜色混合计算, 这就是常说的滤镜.

⽬标颜⾊:已经存储在颜⾊缓存区的颜⾊值

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

开启混合功能非常简单:

glEnable(GL_BLEND);

当混合功能被启动时,源颜⾊和⽬标颜⾊的组合⽅式是混合⽅程式控制的。

在默认情况下,混合⽅程式如下所示:

Cf = (Cs * S) + (Cd * D)

Cf :最终计算参数的颜⾊

Cs : 源颜⾊

Cd :⽬标颜⾊

S:源混合因⼦

D:⽬标混合因⼦

设置混合因⼦,需要⽤到glBlendFun函数:

glBlendFunc(GLenum S,GLenum D);

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

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

表中C 代表常量颜⾊(默认⿊⾊)

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

glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

如果颜⾊缓存区已经有⼀种颜⾊红⾊(1.0f,0.0f,0.0f,1.0f),这个⽬标颜⾊Cd,如果在这上⾯⽤⼀种alpha为0.6的蓝⾊(0.0f,0.0f,1.0f,0.6f)

Cd (⽬标颜⾊) = (1.0f,0.0f,0.0f,0.1f);

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值越⾼,添加的蓝⾊颜⾊成分越⾼,⽬标颜⾊所保留的成分就会越少。混合函数经常⽤于实现在其他⼀些不透明的物体前⾯绘制⼀个透明物体的效果。

默认混合⽅程式:

Cf = (Cs * S) + (Cd * D)

实际上远不⽌这⼀种混合⽅程式,我们可以从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 = (Cs * D) - (Cd * S)
GL_FUNC_MIN Cf = min(Cs,Cd)
GL_FUNC_MAX Cf = max(Cs,Cd)

除了能使⽤glBlendFunc 来设置混合因⼦,还可以有更灵活的选择:

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

strRGB: 源颜⾊的混合因⼦

dstRG: ⽬标颜⾊的混合因⼦

strAlpha: 源颜⾊的Alpha因⼦

dstAlpha: ⽬标颜⾊的Alpha因⼦

glBlendFunc 指定 源和⽬标RGBA值的混合函数;但是glBlendFuncSeparate函数则允许为RGB 和Alpha 成分单独指定混合函数。

在混合因⼦表中,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 );

五、补充:关于平面着色器下边的绘制

在上篇中演示来一些图元绘制 (文章地址), 在对一些多边形对绘制中做了一些处理.

平面着色器中, 并没有一些3D相关的渲染, 所以为了在平面着色器上体现出一个立体效果的话, 我们就对渲染的多边形添加了一些黑色的边.

为了体现这样的效果需要进行这几步:

  1. 重新绘制绿色的面
  2. 边框的存在是需要一定空间的,所以将每个面进行多边形偏移出一些距离用于绘制黑边
  3. 开启抗锯齿,颜色混合(用于黑边与面的颜色的混合)
  4. 绘制线框
  5. 重新渲染
void DrawWireFramedBatch(GLBatch* pBatch)
{
    /*------------画绿色部分----------------*/
    /* GLShaderManager 中的Uniform 值——平面着色器
     参数1:平面着色器
     参数2:运行为几何图形变换指定一个 4 * 4变换矩阵
          --transformPipeline 变换管线(指定了2个矩阵堆栈)
     参数3:颜色值
    */
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
    pBatch->Draw();
    
    /*-----------边框部分-------------------*/
    /*
        glEnable(GLenum mode); 用于启用各种功能。功能由参数决定
        参数列表:http://blog.csdn.net/augusdi/article/details/23747081
        注意:glEnable() 不能写在glBegin() 和 glEnd()中间
        GL_POLYGON_OFFSET_LINE  根据函数glPolygonOffset的设置,启用线的深度偏移
        GL_LINE_SMOOTH          执行后,过虑线点的锯齿
        GL_BLEND                启用颜色混合。例如实现半透明效果
        GL_DEPTH_TEST           启用深度测试 根据坐标的远近自动隐藏被遮住的图形(材料
        
        glDisable(GLenum mode); 用于关闭指定的功能 功能由参数决定
     */
    
    //画黑色边框
    glPolygonOffset(-1.0f, -1.0f);// 偏移深度,在同一位置要绘制填充和边线,会产生z冲突,所以要偏移
    glEnable(GL_POLYGON_OFFSET_LINE);
    
    // 画反锯齿,让黑边好看些
    glEnable(GL_LINE_SMOOTH);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    //绘制线框几何黑色版 三种模式,实心,边框,点,可以作用在正面,背面,或者两面
    //通过调用glPolygonMode将多边形正面或者背面设为线框模式,实现线框渲染
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    //设置线条宽度
    glLineWidth(2.5f);
    
    /* GLShaderManager 中的Uniform 值——平面着色器
     参数1:平面着色器
     参数2:运行为几何图形变换指定一个 4 * 4变换矩阵
         --transformPipeline.GetModelViewProjectionMatrix() 获取的
          GetMatrix函数就可以获得矩阵堆栈顶部的值
     参数3:颜色值(黑色)
     */
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
    pBatch->Draw();

    // 复原原本的设置
    //通过调用glPolygonMode将多边形正面或者背面设为全部填充模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glDisable(GL_POLYGON_OFFSET_LINE);
    glLineWidth(1.0f);
    glDisable(GL_BLEND);
    glDisable(GL_LINE_SMOOTH);
    
}