OpenGL/ES、Metal

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

2020-07-13  本文已影响0人  z夜流星

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

甜甜圈绘制

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不知道该显示哪些界面,导致本来是观察者不应该看到且该丢弃部分,不仅看到了,而且没有将隐藏部分丢弃。

解决办法

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

同时油画算法也有它的弊端,比如当图层之间相互交叉的时候,油画法就无法区分谁远谁近了,例如下图: 图层交叉

如何区分哪些是用户看得到的和哪部分看不到的呢
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);
    }

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


新的问题

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

深度测试

清除深度缓冲区的默认值是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冲突问题的几种做法
glEnable(GL_POLYGON_OFFSET_FILL);

2)指定偏移量

//应⽤到⽚段上总偏移计算⽅程式
//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);
理解窗口、视口、裁剪区域

混合

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 代表常量颜⾊(默认⿊色)

//strRGB: 源颜色的混合因⼦
//dstRGB: 目标颜⾊的混合因⼦
//strAlpha: 源颜⾊的Alpha因⼦
//dstAlpha: 目标颜⾊的Alpha因⼦
void glBlendFuncSeparate(GLenum strRGB, GLenum dstRGB , GLenum strAlpha, GLenum dstAlpha);
上一篇 下一篇

猜你喜欢

热点阅读