OpenGL 渲染技巧 正背面剔除、深度测试

甜甜圈效果

先上绘制甜甜圈的代码

#include "GLTools.h"    
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"

#include 
#ifdef __APPLE__
#include 
#else
#define FREEGLUT_STATIC
#include 
#endif

////设置角色帧,作为相机
GLFrame             viewFrame;
//使用GLFrustum类来设置透视投影
GLFrustum           viewFrustum;
GLTriangleBatch     torusBatch;
GLMatrixStack       modelViewMatix;
GLMatrixStack       projectionMatrix;
GLGeometryTransform transformPipeline;
GLShaderManager     shaderManager;

//标记:背面剔除、深度测试
int iCull = 0;
int iDepth = 0;

//渲染场景
void RenderScene()
{
    //1.清除窗口和深度缓冲区
    //可以给学员演示一下不清空颜色/深度缓冲区时.渲染会造成什么问题. 残留数据
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //2.把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    
    //3.设置绘图颜色
    GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
    
    //4.
    //使用平面着色器
    //参数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);
    
    //5.绘制
    torusBatch.Draw();

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

void SetupRC()
{
    //1.设置背景颜色
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
    
    //2.初始化着色器管理器
    shaderManager.InitializeStockShaders();
    
    //3.将相机向后移动7个单元:肉眼到物体之间的距离
    viewFrame.MoveForward(7.0);
    
    //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);
    
    //5.点的大小(方便点填充时,肉眼观察)
    glPointSize(4.0f);
}

//键位设置,通过不同的键位对其进行设置
//控制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();
}

//窗口改变
void ChangeSize(int w, int h)
{
    //1.防止h变为0
    if(h == 0)
        h = 1;
    
    //2.设置视口窗口尺寸
    glViewport(0, 0, w, h);
    
    //3.setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)
    // 设置透视模式,初始化其透视矩阵
    viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
    
    //4.把透视矩阵加载到透视矩阵对阵中
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
    //5.初始化渲染管线
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}


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

当旋转的时候会出现以下问题

正反面显示异常问题

1.根据一个物体在有光源的情况下是有:阳面(被光源照射的面)和阴面(没有光源的面),而上面的图红色的为阳面,黑色的为阴面。
2.但是OpenGL里,是不知道那些面该显示,导致观察者不应该看到的丢弃部分的面,不仅看到,而且没有把隐藏的面丢弃。

物体那个面是隐藏面

  • 根据这问题方案一是画家算法,先远到近的绘制思路来绘制不同图层,如果近的图层挡住了远图层的面,就叫隐藏面。


    画家算法图
  • 画家算法弊端 ---> 只要将场景按照物理距离观察者的距离远近排序,由远及近的绘制即可.那么会出现什么问题? 如果三个三⻆形是叠加的情况,油画算法将⽆法处理.


    三个三⻆形是叠加

解决方案 -----> 正背面剔除(Face Culling)

1.这时OpenGL针对图形绘制的一种技巧,主要用于处理立体图形绘制,只绘制观察者能看到的部分,看不到的部分就会丢弃不绘制,性能即可提⾼超过50%。
2.OpenGL 可以做到检查所有正⾯朝向观察者的⾯,并渲染它们.从⽽丢弃背⾯朝向的⾯. 这样可以节约⽚元着⾊器的性能

OpenGL如何知道哪个面显示,哪些不显示呢?

1.默认规定了逆时针方向绘制的三角形是正面。
2.当然你可以改为顺时针为正面,但是一般不建议这么操作,主要是由于这个设置不仅仅是作用于你的项目,而是作用于OpenGL全局的。我们习惯OpenGL中默认的即可。

//用于修改正面的函数
void glFrontFace(GLenum mode);
//model有两种:GL_CW(顺时针),GL_CCW(逆时针),
//OpenGL中的默认值:GL_CCW

3.OpenGL内部会通过分析顶点数据的顺序来确认正反面,这个并需要开发者,我们只需将正背面剔除,开启/关闭即可

正背面剔除技巧主要涉及三个方法
开启正背面剔除 void glEnable(GL_CULL_FACE);
关闭正背面剔除 void glDisable(GL_CULL_FACE);
设置需要剔除的面 void glCullFace(GLenum mode);

mode主要有3类

枚举值 说明
GL_FRONT 剔除正面
GL_BACK 剔除背面,是默认值
GL_FRONT_AND_BACK 剔除正背面

解析了关于OpenGL如何剔除正背面,现在代码操作如下:

main中注册菜单栏回调函数

glutCreateMenu(ProcessMenu);
glutAddMenuEntry("Toggle depth test",1);
glutAddMenuEntry("Toggle cull backface",2);
glutAddMenuEntry("Set Fill Mode", 3);
glutAddMenuEntry("Set Line Mode", 4);
glutAddMenuEntry("Set Point Mode", 5);
glutAttachMenu(GLUT_RIGHT_BUTTON);

新增 ProcessMenu函数

通过mainloop监听右键菜单触发的消息,收到点击触发消息后,回调该函·数对相应菜单进行的点击操作
void ProcessMenu(int value) {
    switch(value) {  // 添加剔除开关
        case 1:
            iDepth = !iDepth;
            break;
    }
    
    glutPostRedisplay();
}

在RenderScene函数中新增正背面剔除的开启/关闭操作

//开启/关闭正背面剔除功能
    if (iCull) {
        glEnable(GL_CULL_FACE);
        //以下两行是默认的,可以不写
        glFrontFace(GL_CCW);
        glCullFace(GL_BACK);
    }else
    {
        glDisable(GL_CULL_FACE);
    }
甜甜圈

然后出现缺口问题

image.png

什么是深度?

深度就是在openGL坐标系中,像素点的 Z 坐标距离观察者的距离.

深度与图形中像素点的Z坐标有如下关系:
  • 1.如果观察者在Z轴的正方向,Z值越大则越靠近观察者
  • 2.如果观察者在Z轴的负方向,Z值越小则越靠近观察者

深度缓冲区(Depth Buffer)

深度缓存区是指一块专门内存区域,存储在显存中,用于存储屏幕上所绘制图形的每个像素点的深度值

深度缓存区原理

将深度值与屏幕上的每个像素点进行一一对应,然后将深度值存储到深度缓冲区。

在demo里RenderScene函数中,会先清空缓存区,这里的缓冲区就包括深度缓冲区,因为如果缓存区不清空,之前的数据会有残留,会对目前图形的绘制造成影响。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

深度测试

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

为了解决甜甜圈深度(被“咬一口”)的问题

针对甜甜圈缺口问题,我们就需要利用深度测试来解决,需要在甜甜圈绘制前开启深度测试glEnable(GL_DEPTH_TEST);,用于新旧深度值的对比,决定像素点是绘制还是丢弃。

在demo的ProcessMenu方法中

void ProcessMenu(int value) {
    switch(value) {  
        case 1:
            iDepth = !iDepth;// 添加剔除开关
            break;
        case 2:
            iCull = !iCull;// 添加深度开关
            break;
    }
    
    glutPostRedisplay();
}

问题 总结

问题一:使用平面着色器绘制甜甜圈,会出现隐藏面消除吗?

  • 会出现,由于都是红色,因此没有办法区别谁是正面,谁是背面,所以导致肉眼无法察觉

问题二:为什么使用默认光源着色器会出现隐藏面消除?

  • 是因为在默光源着色器中,在光源无法照射的部分会呈现黑色,被照射部分呈现红色,可以非常直观的通过肉眼看出谁是正面,谁是反面

深度测试:可以一次性解决隐藏面消除问题,原理是不管有多少图层,只显示可见图层,剩余不可见的都丢弃

多边形偏移

Z-Fighting(Z冲突,闪烁)问题
通过上面的学习我们知道,深度测试的原理是比较两个像素点的深度值(Z值)来判断在深度缓冲区中到底保存哪个像素去渲染到屏幕上的,那么当两个图层离得非常近,也就是两个像素点的深度值(Z值)非常接近时,那会如何呢?

比如:A、B两个像素点的深度值分别是1.00000000001和1.00000000000时,由于计算机精度的问题,计算机会判断这两个深度值是一样大的,那这个时候就会出现如下现像了:

Z-Fighting(Z冲突,闪烁)问题

Az部分和Bz部分的深度值非常接近,计算机判断它们的深度值是一样大小的,这时候就会出现A、B两种显示效果。这就是Z-Fighting现象.

如何解决Z-Fighting现象?--多边形偏移

其问题产生的主要原因是由于图形靠的太近,导致无法区分出图层先后次序,针对该问题,OpenGL提供了一种多边形偏移(Polygon Offset)方案

使用多边形偏移,主要有以下三个步骤 : 开启多边形偏移

glEnable(GL_POLYGON_OFFSET_FILL)
多边形偏移枚举值 对应的图像填充模式
GL_POLYGON_OFFSET_POINT GL_POINT
GL_POLYGON_OFFSET_LINE GL_LINE
GL_POLYGON_OFFSET_FILL GL_FILL

在绘制完成后,关闭多边形偏移

glDisable(GL_POLYGON_OFFSET_FILL)

预防ZFighting闪烁

1.避免两个物体靠的太近:在绘制时,插入一个小偏移
2.将近裁剪面(设置透视投影时设置)设置的离观察者远一些:提高裁剪范围内的精确度
3.使用更高位数的深度缓冲区:提高深度缓冲区的精确度

你可能感兴趣的:(OpenGL 渲染技巧 正背面剔除、深度测试)