NO.9 - OpenGL渲染技巧(正背面剔除、深度测试、多边形偏移、混合)

在学习OpenGL的一些渲染技巧之前,我们先用之前学的知识来完成一个小案例:甜甜圈
通过案例顺便回顾项目中核心方法的使用。

甜甜圈绘制

  • main函数:程序入口
  • ChangeSize函数:主要是设置视口及投影方式
  • SetupRC函数:图形数据配置,主要是顶点数据及图元连接方式
  • RenderScene函数:主要用于图形的绘制,可以系统触发,也可以开发者手动触发
  • SpecialKeys函数:对特殊键位的回调处理
main函数

程序入口

int main(int argc, char* argv[])
{
    gltSetWorkingDirectory(argv[0]);
    
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    glutInitWindowSize(800, 600);
    glutCreateWindow("Geometry Test Program");
    glutReshapeFunc(ChangeSize);
    glutSpecialFunc(SpecialKeys);
    glutDisplayFunc(RenderScene);
    
    GLenum err = glewInit();
    if (GLEW_OK != err) {
        fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
        return 1;
    }
    
    SetupRC();
    
    glutMainLoop();
    return 0;
}
SetupRC函数

绘制甜甜圈,我们要使用很多的顶点,计算每个顶点的坐标是很繁琐,这里我们使用系统已经帮我们封装好了一个gltMakeTorus函数,它是GLTriangleBatch 容器帮助类,可以设置外边缘半径和内边缘半径以及主半径和从半径的细分单元数量。同时我们使用观察者动,物体不动的方式来绘制。

void SetupRC()
{
    //设置背景颜色
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
    //初始化着色器管理器
    shaderManager.InitializeStockShaders();
    //将相机向后移动7个单元:肉眼到物体之间的距离
    viewFrame.MoveForward(7.0);
    //创建一个甜甜圈
    //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);
    
    //点的大小(方便点填充时,肉眼观察)
    glPointSize(4.0f);
}
RenderScene函数
//渲染场景
void RenderScene()
{
    //清除窗口和深度缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
   //设置绘图颜色
    GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    
    
    //使用平面着色器
    //参数1:平面着色器
    //参数2:模型视图投影矩阵
    //参数3:颜色
   // shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
    
    //使用默认光源着色器
    //通过光源、阴影效果跟提现立体效果
    //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
    //参数2:模型视图矩阵
    //参数3:投影矩阵
    //参数4:基本颜色值
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    
     // M3DMatrix44f mm;
     // modelViewMatix.GetMatrix(mm);
     //
     // M3DMatrix44f pp;
     // projectionMatrix.GetMatrix(pp);
     //
     // shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,mm,pp, vRed);
    
    //绘制
    torusBatch.Draw();

    //出栈 绘制完成恢复
    modelViewMatix.PopMatrix();
    
   // 交换缓存区
    glutSwapBuffers();
}
SpecialKeys函数

通过上下左右键移动控制观察者角度

//键位设置,通过不同的键位对其进行设置
//控制Camera的移动,从而改变视口
void SpecialKeys(int key, int x, int y)
{
    //判断方向
    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);
    
    //重新刷新
    glutPostRedisplay();
}
ChangeSize函数

窗口改变大小时调用,设置视口,投影模式,初始化投影矩阵和渲染管线

//窗口改变
void ChangeSize(int w, int h)
{
    //防止h变为0
    if(h == 0)
        h = 1;
    
    //设置视口窗口尺寸
    glViewport(0, 0, w, h);
    
    //setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)
    // 设置透视模式,初始化其透视矩阵
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);

    //4.把透视矩阵加载到透视矩阵对阵中
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //5.初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}

这时候我们绘制出了一个甜甜圈


甜甜圈

看起来甜甜圈非常正常,但是当我们进行旋转时,情况出现了


正反面显示问题

默认情况下,我们所渲染的每个点、线或三角形都会再屏幕上进行光栅化,并按照在组合图元批次时指定的顺序排列,这在某些情况下会产生问题
在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的.对于不可见的部分,应该及早丢弃.例如在一个不透明的墙壁后,就不应该渲染.这种情况叫做”隐藏⾯面消除”(Hidden surface elimination)
上面出现的问题:甜甜圈在旋转过程中,OpenGL不知道该显示哪些界面,导致本来是观察者不应该看到且该丢弃部分,不仅看到了,而且没有将隐藏部分丢弃。

解决办法
  • 1)油画算法


    油画法

由远及近的绘制不同图层,近的图层就可以将远的图层的隐藏面覆盖掉。但这种方法在图形处理中效率很低,必须在任何发生重叠的地方对每个像素进行两次写操作,速度会变慢。

同时油画算法也有它的弊端,比如当图层之间相互交叉的时候,油画法就无法区分谁远谁近了,例如下图:
图层交叉
  • 2)正背面剔除
    正背面剔除的主要思想就是:只绘制用户看到的部分,对于看不到的部分直接丢弃。

如何区分哪些是用户看得到的和哪部分看不到的呢
OpenGL中默认规定了逆时针方向绘制的三角形是正面,顺时针方向绘制的三角形为背面。

立体正背面

• 左侧三⻆形顶点顺序为: 1—> 2—> 3 ; 右侧三⻆形的顶点顺序为: 1—> 2—> 3
• 当观察者在右侧时,则右边的三⻆形⽅向为逆时针⽅向则为正⾯,⽽左侧的三⻆形为顺时针则为背⾯。
• 当观察者在左侧时,则左边的三⻆形为逆时针⽅向判定为正⾯,⽽右侧的三⻆形为顺时针判定为背⾯。

注意:正⾯和背⾯是由三⻆形的顶点定义顺序和观察者⽅向共同决定的.随着观察者的⻆度⽅向的改变,正⾯背⾯也会跟着改变。

关于正别面剔除相关代码
//开启表面剔除(默认背面剔除)
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);

在OpenGL中我们只要选择性的调用这么代码就能实现正背面消除了。

//标记:背面剔除
int iCull = 0;
// 然后在mian()函数中
//添加右击菜单栏
glutCreateMenu(ProcessMenu);
// 监听菜单栏点击
void ProcessMenu(int value)
{
    switch(value)
    {
        case 1:
            iCull = !iCull;
            break;
    }
    
    glutPostRedisplay();
}

  //最后在 RenderScene()中添加开启和关闭正背面剔除功能
  //开启/关闭正背面剔除功能
    if (iCull) {
        glEnable(GL_CULL_FACE);
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }else
    {
        glDisable(GL_CULL_FACE);
    }

这样我们就解决了上面的问题。但同时我们又有了新的问题出现,如图:


新的问题

造成这个问题的出现是因为没有开启深度测试。

深度测试
  • 深度:
    深度就是在openGL坐标系中,像素点的 Z 坐标距离观察者的距离。观察者可能放在坐标系的任何位置,那么,就不能简单的说 Z 数值越大或越小,就是越靠近观察者。
    如果观察者在Z轴的正方向,Z 值大的靠近观察者,如果是在Z轴的反方向,则 Z 值小的更靠近观察者。

  • 深度缓冲区(DepthBuffer):
    深度缓冲区原理就是把一个距离观察平面(近裁剪面)的深度值(或距离)与窗口中的每个像素相关联。
    首先,使用glClear(GL_DEPTH_BUFFER_BIT),把所有像素的深度值设置为最大值。
    如果启用了深度缓冲区,在绘制每个像素之前,OpenGL会把它的深度值和已经存储在这个像素的深度值进行比较。如果,新像素深度值 < 原先像素深度值,则新像素值会取代原先的;反之,新像素值被遮挡,它的颜色值和深度将被丢弃。
    这个比较、丢弃的过程就叫做 深度测试,深度测试是另一种高效消除隐藏面的技术。

清除深度缓冲区的默认值是1.0,表示最大的深度值,深度值的范围在[0,1]之间。
用户通过glDepthFunc(GLenum func)函数指定深度测试的规则,这个函数包括一个参数,如下表:


同样的我们只需要调用一些代码就能实现深度测试

// 申请一个颜色缓冲区和一个深度缓冲区:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
// 开启深度测试
glEnable(GL_DEPTH_TEST);
// 关闭深度测试
glDisable(GL_DEPTH_TEST);
// 如果没有深度缓冲区,那么启动深度测试的命令将被忽略
//在绘制场景前,清除颜色缓冲区和深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

下面我们在对之前代码进行补充:

//标记:深度测试
int iDepth = 0;

// 监听菜单栏点击
void ProcessMenu(int value)
{
    switch(value)
    {
        case 1:
            iDepth = !iDepth;
            break;
       case 2:
            iCull = !iCull;
            break;
    }
    
    glutPostRedisplay();
}
  // RenderScene()中添加开启和关闭正背面深度测试
  //根据设置iDepth标记来判断是否开启深度测试
    if(iDepth)
        glEnable(GL_DEPTH_TEST);
    else
        glDisable(GL_DEPTH_TEST);

开启了深度测试后,我们终于得到了一个我们想要的甜甜圈


完美的甜甜圈

z-fighting(z冲突、闪烁)问题

闪烁问题
造成闪烁的原因:

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

避免深度值相同造成的z-fighting冲突问题的几种做法
  • 在第二次绘制时,插入一个少量的偏移。
  • 使用更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用使用32位的缓冲区,使精确度得到提高。
  • 使用 glPolygonOffset 函数调节片段的深度值,使得深度值偏移而不产生重叠。
    1)启用Polygon Offset
glEnable(GL_POLYGON_OFFSET_FILL);

2)指定偏移量

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

3)关闭Polygon Offset

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

剪裁

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

混合

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

  • 当深度缓冲区被关闭时,新的颜色将简单地覆盖原来颜色缓存区存在的颜色 * 值。
  • 当深度缓冲区再次打开时,新的颜⾊片段只是当它们比原来的值更接近邻近的裁剪平⾯才会替换原来的颜⾊片段。
//开启混合
gl_Enable(GL_BIEND);
  • 颜色混合
  • ⽬标颜色:已经存储在颜色缓存区的颜色值 (已经存在的颜色,旧颜色)
  • 源颜色:作为当前渲染命令结果进入颜色缓存区的颜⾊值 (新进来的颜色 ,新颜色)
    当混合功能被开启时,源颜色和⽬标颜色的组合方式是混合方程式控制的。在默认情况下,混合方程式如下所示:
//Cf: 最终计算参数的颜⾊
//Cs: 源颜⾊
//Cd: 目标颜⾊
//S: 源混合因⼦,源Alpha混合因子
//D: ⽬标混合因⼦,⽬标Alpha混合因子
Cf = (Cs * S) + (Cd * D);

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

混合方程式

glbBlendEquation(GLenum mode);

OpenGL有5个不同的方程式:


混合方程式

设置混合因⼦

//S:源混合因⼦
//D:⽬标混合因子
glBlendFunc(GLenum S, GLenum D);
混合因⼦

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

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

  • glBlendFuncSeparate函数
    除了能使⽤OpenGL内置的混合因⼦,还可以有更灵活的选择

//strRGB: 源颜色的混合因⼦
//dstRGB: 目标颜⾊的混合因⼦
//strAlpha: 源颜⾊的Alpha因⼦
//dstAlpha: 目标颜⾊的Alpha因⼦
void glBlendFuncSeparate(GLenum strRGB, GLenum dstRGB , GLenum strAlpha, GLenum dstAlpha);
  • glBlendFuncSeparate注意
  • glBlendFunc 指定源和目标 RGBA值的混合函数;但是glBlendFuncSeparate函数则允许为RGB 和 Alpha 成分单独指定混合函数。
  • 在混合因子表中,GL_CONSTANT_COLOR,GL_ONE_MINUS_CONSTANT_COLOR,GL_CONSTANT_ALPHA,GL _ONE_MINUS_CONSTANT只允许混合方程式中引⼊一个常量混合颜⾊。

你可能感兴趣的:(NO.9 - OpenGL渲染技巧(正背面剔除、深度测试、多边形偏移、混合))