OpenGL深度测试

206 阅读11分钟

在OpenGL中经常胡使用3D图形,那么如果绘制一个3d图形呢?接下来就以甜甜圈为例

如何绘制甜甜圈

  • 导入头文件
* #include "GLTools.h"	
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"

#include <math.h>
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
  • 定义相关属性
////设置角色帧,作为相机
GLFrame             viewFrame;
//使用GLFrustum类来设置透视投影
GLFrustum           viewFrustum;
GLTriangleBatch     torusBatch;
GLMatrixStack       modelViewMatix;
GLMatrixStack       projectionMatrix;
GLGeometryTransform transformPipeline;
GLShaderManager     shaderManager;


GLFrame                cameraFrame;
//标记:背面剔除、深度测试
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.60f, 0.60f, 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();
}
  • 初始化SetupRC
void SetupRC()
{
    //1.设置背景颜色
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
    
    //2.初始化着色器管理器
    shaderManager.InitializeStockShaders();
    
    //3.将相机向后移动7个单元:肉眼到物体之间的距离
   viewFrame.MoveForward(10);
    
    
    //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);
}
  • 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;
}

实现效果如下图

通过键位控制旋转发现图形有黑色填充效果,为什么会出现这种效果,原因是在绘制3D场景的时候,我们需要决定哪些部分是对观察者 可⻅的,或者哪些部分是对观察者不可⻅的.对于不可⻅的 部分,应该及早丢弃.例如在⼀个不透明的墙壁后,就不应该 渲染.这种情况叫做”隐藏⾯消除”(Hidden surface elimination).

那如何将隐藏⾯消除呢下面有两种解决方案

油画算法

  • 油画算法 说到这个问题,我们先看通常情况下油画是怎么绘制 先绘制场景中的离观察者较远的物体,再绘制较近的物体. 例如下⾯的图例: 先绘制红⾊部分,再绘制⻩⾊部分,最后再绘制灰⾊部分,即可解决隐藏⾯消除的问题

  • 油画算法的缺点

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

正背⾯剔除(Face Culling)

  • 背景

⼀个3D图形,你从任何⼀个⽅向去观察,最多可以看到⼏个⾯? 答案是,最多3⾯. 从⼀个⽴⽅体的任意位置和⽅向上看,你⽤过不可能看到多于3个⾯. 那么思考? 我们为何要多余的去绘制那根本看不到的3个⾯? 如果我们能以某种⽅式去丢弃这部分数据,OpenGL 在渲染的性能即可提⾼超过50%.

  • 问题

1、如何知道某个⾯在观察者的视野中不会出现? 2、任何平⾯都有2个⾯,正⾯/背⾯.意味着你⼀个时刻只能看到⼀⾯. 3、 OpenGL 可以做到检查所有正⾯朝向观察者的⾯,并渲染它们.从⽽丢弃背⾯朝向的⾯. 这样可以节约⽚元着⾊器的性能

  • 解决方案

1、分析顶点数据的顺序

正⾯: 按照逆时针顶点连接顺序的三⻆形⾯

背⾯: 按照顺时针顶点连接顺序的三⻆形⾯

2、分析⽴⽅体中的正背⾯

1、左侧三⻆形顶点顺序为: 1—> 2—> 3 ; 右侧三⻆形的顶点顺序为: 1—> 2—> 3

2、当观察者在右侧时,则右边的三⻆形⽅向为逆时针⽅向则为正⾯,⽽左侧的三⻆形为顺时针则为.⾯ 3、当观察者在左侧时,则左边的三⻆形为逆时针⽅向判定为正⾯,⽽右侧的三⻆形为顺时针判定为背⾯

总结: 正⾯和背⾯是有三⻆形的顶点定义顺序和观察者⽅向共同决定的.随着观察者的⻆度⽅向的改变,正⾯背⾯也 逻辑教育 会跟着改变

开启关闭正背面剔除实现代码
//开启表⾯剔除(默认背⾯剔除) 
void glEnable(GL_CULL_FACE); 
//关闭表⾯剔除(默认背⾯剔除) 
void glDisable(GL_CULL_FACE); 
//⽤户选择剔除那个⾯(正⾯/背⾯) 
void glCullFace(GLenum mode); 
//mode参数为: GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK 
//⽤户指定绕序那个为正⾯
void glFrontFace(GLenum mode); 
//mode参数为: GL_CW,GL_CCW,默认值:GL_CCW 
//例如,剔除正⾯实现(1) 
glCullFace(GL_BACK); 
glFrontFace(GL_CW); 
//例如,剔除正⾯实现(2) 
glCullFace(GL_FRONT);

设置正背面剔除之后效果如下

我们发现黑色的颜色没了,但又有缺口,这是为什呢,这个时候引入我深度测试

深度测试

什么是深度?

深度其实就是该像素点在3D世界中距离摄像机的距离,Z值 •

什么是深度缓冲区?

深度缓存区,就是⼀块内存区域,专⻔存储着每个像素点(绘制在屏幕上的)深度值.深度值(Z值)越⼤, 则离摄像机就越远.

为什么需要深度缓冲区?

在不使⽤深度测试的时候,如果我们先绘制⼀个距离⽐较近的物理,再绘制距离较远的物理,则距离 远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么᯿ 要的. 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写⼊到缓冲区中. 除⾮调⽤ glDepthMask(GL_FALSE).来禁⽌写⼊.

Z-buffer⽅法(深度缓冲区Depth-buffer)

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

深度值计算

深度值⼀般由16位,24位或者32位值表示,通常是24位。位数越⾼的话,深度的精确度越 好。深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者。 深度缓冲主要是通过计算深度值来⽐较⼤⼩,在深度缓冲区中包含深度值介于0.0和1.0之间, 从观察者看到其内容与场景中的所有对象的 z 值进⾏了⽐较。这些视图空间中的 z 值可以在投 影平头截体的近平⾯和远平⾯之间的任何值。我们因此需要⼀些⽅法来转换这些视图空间 z 值 到 [0,1] 的范围内,下⾯的 (线性) ⽅程把 z 值转换为 0.0 和 1.0 之间的值

  • 如何使用深度测试
1、深度缓冲区,⼀般由窗⼝管理系统,GLFW创建.深度值⼀般由16位,24位,32位值表示. 通常是24位.位
数越⾼,深度精确度更好. 

2、开启深度测试
glEnable(GL_DEPTH_TEST); 

3、在绘制场景前,清除颜⾊缓存区,深度缓冲
glClearColor(0.0f,0.0f,0.0f,1.0f); 
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
4、清除深度缓冲区默认值为1.0,表示最⼤的深度值,深度值的范围为(0,1)之间. 值越⼩表示越靠近观察者,值越⼤表示越远离观察者
  • 深度测试常见模式

开启深度测试之后效果如下

这样就完美解决了上面一系列问题

ZFighting

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

解决ZFighting闪烁问题的一般方式有

1、启⽤ Polygon Offset ⽅式解决

第一步:让深度值之间产⽣间隔.如果2个图形之间有间隔,是不是意味着就不会产⽣⼲涉.可以理解为在执⾏深度测试前将⽴⽅体的深度值做⼀些细微的增加.于是就能将重叠的2个图形深度值之前有所区分.

//启⽤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 

第二步:指定偏移量

通过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

void glPolygonOffset(Glfloat factor,Glfloat units);
//应⽤到⽚段上总偏移计算⽅程式:
 Depth Offset = (DZ * factor) + (r * units);
//DZ:深度值(Z值)
//r:使得深度缓冲区产⽣变化的最⼩值
//负值,将使得z值距离我们更近,⽽正值,将使得z值距离我们更远,
//对于上节课的案例,我们设置factor和units设置为-1,-1

第三步: 关闭Polygon Offset

glDisable(GL_POLYGON_OFFSET_FILL)
ZFighting闪烁问题预防

1、不要将两个物体靠的太近,避免渲染时三⻆形叠在⼀起。这种⽅式要求对场景中物体插⼊⼀个少量的 偏移,那么就可能避免ZFighting现象。例如上⾯的⽴⽅体和平⾯问题中,将平⾯下移0.001f就可以解 决这个问题。当然⼿动去插⼊这个⼩的偏移是要付出代价的。

2、尽可能将近裁剪⾯设置得离观察者远⼀些。上⾯我们看到,在近裁剪平⾯附近,深度的精确度是很⾼ 的,因此尽可能让近裁剪⾯远⼀些的话,会使整个裁剪范围内的精确度变⾼⼀些。但是这种⽅式会使 离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯参数。

3、使⽤更⾼位数的深度缓冲区,通常使⽤的深度缓冲区是24位的,现在有⼀些硬件使⽤使⽤32位的缓冲 区,使精确度得到提⾼