10、OpenGL - 正背面剔除、深度缓冲区

OpenGL 正背面剔除、深度缓冲区 

 

详细代码参见Demo  甜甜圈 demo

Demo地址 -> OpenGLDemos -> 3.donuts

 

1、隐藏面消除

在绘制3D场景时,我们需要决定哪些部分是对观察者可见的,或者哪些部分是不可见的。对于不可见的部分,应该丢弃。这种丢弃不可见部分的做法叫 -- 隐藏面消除


方案:

1.1、油画算法

油画算法的思路是,先绘制离观察者最远的物体,然后依次类推,直到绘制完毕。
如下图,先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分。
这样就可以解决隐藏面消除的问题了

但是油画算法并不能解决所有的隐藏面消除问题。如下图所示

 

1.2、正背面剔除(Face Culling)

我们要观察一个3D 图形,例如:从任何一个方向去观察一个立方体,最多可以看到3个面。那些我们看不到的面,如果能以某种方式丢弃这部分数据。OpenGL在渲染的性能即可提高超过50%。

任何平面都有2个面:正面和背面,一个时刻我们只能看到一面。

通过分析顶点数据的顺序,OpenGL可以做到检查所有正面朝向观察者的面,并渲染他们。从而丢弃背面朝向的面。

正 / 背面的区别:

  • 正面:按照逆时针顶点连接顺序的三角形面
  • 背面:按照顺时针顶点连接顺序的三角形面

注意:正面和背面是有三角形的顶点定义顺序和观察者方向共同决定的。若观察者方向发生变化,正面和背面也会发生相应的改变。

  • 当观察者在右侧时:右侧的三角形为逆时针方向,则为正面;而左侧的三角形为顺时针,则为背面。
  • 当观察者在左侧时:左边的三角形为逆时针方向,则为正面;而右侧的三角形为顺时针,则为背面。

缺点:如果当前后两个点都是正面或是背面,这时OpenGL 无法区分哪个面在前,哪个面在后,就可能出现如下图所示问题

 

1.3、立方体中的正背面剔除

//    开启表面剔除(默认背面剔除)
    glEnable(GL_CULL_FACE);
    
//    关闭表面剔除(默认背面剔除)
    glDisable(GL_CULL_FACE);
    
//    用户选择剔除哪个面(正面 / 背面)
    void glCullFace(GLenum mode);
//    mode 参数为GL_FRONT,GL_BACK,GL_FRONT_AND_BACK ,默认GL_BACK

//    用户指定绕序哪个为正面
    void glFrontFace(GLenum mode)
//    modea 参数为:GL_CW,GL_CCW,默认值:GL_CCW
    
//    剔除正面实现(1)
    glCullFace(GL_BACK);
    glFrontFace(GL_CW);
    
//    剔除正面实现(2)
    glCullFace(GL_FRONT);

 

2、深度

深度,就是像素点在3D世界中距离摄像机的距离,即Z值。

 

2.1、深度缓冲区

深度缓冲区,是一块内存区域,专门存储每个像素点的深度值。深度值(Z值)越大,则离摄像机就越远。

为什么需要深度缓冲区?
在不使用深度测试的时候,如果先绘制一个比较近的物体,再绘制较远的物体,较远的图像就会想油画一样覆盖掉之前的图像,就会出现1.2最后的问题
有了深度缓冲区,绘制物体的顺序就不那么重要了。
只要通过开启了深度缓冲区,并允许深度值的写入,OpenGL都会把像素的深度值写到缓冲区中。

 

2.2、深度测试

深度缓冲区和颜色缓冲区是对应的。颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像像素的深度信息

在决定是否绘制一个物体表面时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区中的值,则丢弃这部分;否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这个过程称为深度测试。

 

2.3、深度值的计算

深度值,一般由16位、24位或者32位值表示,通常是24位。
位数越高,深度的精确度越好
深度值的范围在 [0, 1] 之间,值越小表示越靠近观察者,值越大表示越远离观察者。

 

2.4、深度值的使用

开启深度测试

glEnable(GL_DEPTH_TEST);

在绘制场景前,清除颜色缓存区,深度缓冲。清除深度缓冲区默认值为1.0(表示最大的深度值,深度值的范围是(0-1),值越小表示越靠近观察者,值越大,表示越原理观察者)

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

指定深度测试判断模式

void glDepthFunc(GLEnum mode);
函数说明
GL_ALWAYS总是通过测试
GL_NEVER 总是不通过测试
GL_LESS  在当前深度值 < 存储的深度值时通过
GL_GREATER   在当前深度值 > 存储的深度值时通过
GL_EQUAL 在当前深度值 == 存储的深度值时通过
GL_LEQUAL 在当前深度值 <= 存储的深度值时通过
GL_QEQUAL在当前深度值 >= 存储的深度值时通过
GL_NOTEQUAL在当前深度值 != 存储的深度值时通过

深度缓冲区写入开关
//value:

  • GL_TURE,开启深度缓冲区写入;
  • GL_FALSE,关闭深度缓冲区写入
void glDepthcMask(GLBOOL value);

总结:
使用正面 / 背面剔除法 和 深度测试法 解决了OpenGL 的渲染效率问题。

 

3、ZFighting 闪烁问题

3.1、原因

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

同一个位置上,出现的图层,且深度值出现精度很低时,就会容易引起ZFighthing 现象。表示2个物体靠的非常近,无法确定谁在前,谁在后

 

3.2、解决闪烁

分析原因既然是因为靠的太近,无法区分图层先后,那么可以通过给2个图层之间一个适当的间隔,来解决ZFighting 的问题。手动添加的话非常复杂且不精确,OpenGL提供了一个解决方案,“多边形偏移”。

步骤
1、启用Polygon Offset(多边形偏移)

让深度值之间产生间隔,如果2个图形之间有间隔,是不是意味着就不会产生干涉。可以理解为在执行深度测试前将立方体的深度值做一些细微的增加。于是就能将重叠的2个图形深度值之前有所区别。
增大重叠或深度值接近的2个图形的深度值差距,使得OpenGL可以区分两个深度值。

Polygon Offset模式对应的光栅化模式
GL_POLYGON_OFFSET_FILLGL_FILL
GL_POLYGON_OFFSET_LINEGL_LINE
GL_POLYGON_OFFSET_POINT GL_POINT

//启用 Polygon Offset 方式

glEnable(GL_POLYGON_OFFSET_FILL);

2、指定偏移量

通过glPolygonOffset 来指定 glPolygonOffset需要的 2个参数:factor(因素) 和 units(单位)
每个 Fragment 的深度值都会增加如下所示的偏移量

Offset = (m * factor) + (r * units);

m:多边形的深度的斜率的最大值,理解一个多边形越是近裁剪面平行,m 就越接近与0.

r:能产生窗口坐标系的深度值中可分辨的差异最小值.r 是由具体OpenGL 平台指定的一个常量

一个大于0 的Offset 会把模型推到离你(观察者 / 摄像机)更远的位置,相应的一个小于0 的Offset 会把模型拉近.
一般而言,只需要将 -1 和 -1 这样简单赋值给glPolygonOffset 基本可以满足需求

void glPolygonOffset(Glfloat factor, Glfloat units);

应用到片段上总偏移计算方程式
Depth offset = (DZ * factor) + (r * units);

DZ:深度值(z值)
r:使得深度缓冲区产生变化的最小值,是由具体OpenGL 平台指定的一个常量

Offset 为负值,将使得z值距离观察者更近;而正值,将使得z值距离观察者更远。一般而言,我们设置factor 和 units 设置为 (-1.0, -1.0)

 

3、关闭Polygon Offset(多边形偏移)

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

 

3.3、闪烁问题的预防

1、不要将两个物体靠的太近,避免渲染时三角形叠在一起。
对场景中物体插入一个少量的偏移,避免ZZFighting现象。

2、尽可能将近裁剪面设置得离观察者远一些。
如果观察者离近裁剪面很近,那么深度测试对精确度要求很高。因此,可以适当推远近裁剪平面的位置来避免这个问题,但是可能导致离观察者较近的物体被裁剪掉,使用时需要小心。

3、使用更高位数的深度缓冲区
默认的深度缓冲区精度是24位的,如果硬件支持32/64位的缓冲区,就可以使用更高的精度。

 

4、裁剪

在OpenGL 中提高渲染效率的一种方式。只刷新屏幕上发生变化的部分。

基本原理
用于渲染时限制绘制区域,通过此技术可以在屏幕(帧缓冲区)指定一个矩形区域。启用裁剪测试之后,不在此矩形区域内的片元被丢弃,只有在此矩形区域内的片元才有可能进入帧缓冲区。因此,实际达到的效果就是在屏幕上开辟了一个小窗口,可以在其中进行制定内容的绘制。

//1、开启裁剪测试

glEnable(GL_SCISSOR_TEST);

//2、关闭裁剪测试

glDisable(GL_SCISSOR_TEST);

//3、制定裁剪窗口
//x,y:制定裁剪框左下角位置;width,height:指定裁剪尺寸

void glScissor(Glint x, GLiint y, GLSize width, GLSize height);

 

4.1、理解窗口、视口、裁剪区域

窗口
显示界面。相当于iOS里面的window

视口
窗口中用来显示图形的一块矩形区域,他可以和窗口等大,也可以比窗口大或者小。只有绘制在视口区域中的图形才能被显示,如果图形有一部分超出了视口区域,那么那一部分是看不到的。通过 glViewport() 函数设置。相当于View

裁剪区域(平行投影)
视口矩形区域的最小最大x坐标(left, right)和最小最大y坐标(bottom, top),而不是窗口的最小最大x 坐标 和 y 坐标。通过glOrtho() 函数设置,这个函数还需要指定最近最远 z 坐标,形成一个立体的裁剪区域。相当于设置一个frame

 

5、甜甜圈demo分析

5.1、引入头文件

#include <stdio.h>
//演示了OpenGL背面剔除,深度测试,和多边形模式
#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

5.2、创建需要的变量

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

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

5.3、渲染场景

1、记得要清除一下缓冲区

//    1、清除窗口和深度缓冲区
//    如果不清理会有残留数据(如图1)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

不清除的话会有残留数据

 

2、开启表面剔除

开启之后在进行翻转的时候,就不会出现背面的黑色了,但是会出现缺失。

这是因为甜甜圈这个时候有两个正面向上(想着照相机),渲染完第一个去渲染第二个,计算机是不知道哪个是应该被遮挡的,所以渲染第二个就会把上面的剔除。

//    开启表面剔除
 glEnable(GL_CULL_FACE);

3、通过正背面剔除不能解决我们甜甜圈全部的问题,所以用深度测试

//    开启深度测试
    glEnable(GL_DEPTH_TEST); 

如果不开启深度测试 和 正背面剔除的话,系统分不清正反向翻转的时候效果如下

 

1、系统自动触发

2、开发者手动调用函数触发

处理:

1、清理缓冲区(颜色、深度、模板缓冲区等)

2、使用存储着色器

3、绘制图形

void RenderScene()
{
//    1、清除窗口和深度缓冲区
//    如果不清理会有残留数据(如图1)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
//    开启表面剔除
//    glEnable(GL_CULL_FACE);
    
//    开启深度测试
    glEnable(GL_DEPTH_TEST);
    
//    2、把摄像机矩阵压入模型矩阵中
    modelViewMatix.PushMatrix(viewFrame);
    
//    modelViewMatix.PushMatrix();
//    M3DMatrix44f mCamera;
//    cameraFrame.GetCameraMatrix(mCamera);
//
//    modelViewMatix.MultMatrix(mCamera);
//
//    M3DMatrix44f mObjectFrame;
//    viewFrame.GetMatrix(mObjectFrame);
//    modelViewMatix.MultMatrix(mObjectFrame);
    
//    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();

}

 手动main 函数触发

处理:

1、设置窗口背景颜色

2、初始化存储着色器

3、设置图形顶点数据 shaderManager

4、利用 GLBatch 三角形批次类,将数据传递到着色器


void SetupRC()
{
//    1、用当前颜色清除背景颜色,如果不清除的话在动的时候会有残影
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    
//    2、初始化着色器管理器
    shaderManager.InitializeStockShaders();
    
//    3、将相机向后移动10个单元:肉眼到物体之间的距离
    viewFrame.MoveForward(10.0f);
//    viewFrame.MoveForward(-10.0f);//设置成负值就看不到圆环了示意
//    圆环  相机  我们的方向,圆环在我们相机看的后面是看不到的
    
//    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();
}

 触发:

1、新建窗口

2、窗口尺寸发生调整

处理:

1、设置OpenGL 视口

2、设置OpenGL 投影方式等。

//窗口改变
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.f, float(w)/float(h), 1.0f, 100.0f);
    
//4、把透视矩阵加载到透视矩阵堆栈中
    projectionMatix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    
//    5、初始化渲染管道
    transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatix);
}
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、案例 03:金字塔、六边形、圆环的绘制

2、OpenGL之 甜甜圈与背面剔除

3、四、OpenGL深度缓冲区、裁剪和混合

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值