1. 举个例子

我们先看一个openGL示例:
这是一个典型的3D场景,包含模型(Model)、相机(Camera)、光照(Lighting)等元素。这里使用默认光照着色器,默认光源着色器会使绘制的的图形产生阴影和光照效果,在没有光照的地方模型的面显示阴影--黑色。

#include "GLTools.h"
#include <glut/glut.h>
#include "GLFrustum.h"
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLGeometryTransform.h"

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wdeprecated-declarations"

GLShaderManager shaderManager;  // 着色管理器,用于管理OpenGL着色器程序
GLFrustum viewFrustum;          // 透视投影对象,用于创建透视投影矩阵
GLFrame viewFrame;              // 观察者位置和方向,视图矩阵的基本框架
GLMatrixStack modelViewMatrix;  // 模型视图矩阵栈,用于存储和管理模型视图矩阵
GLMatrixStack projectionMatrix; // 投影矩阵栈,用于存储和管理投影矩阵

GLGeometryTransform transformPipeline; // 几何变换对象,用于将模型视图和投影矩阵传给着色器

GLTriangleBatch torusBatch; // 环形图形的三角形批次对象

// 改变窗口大小时的回调函数
void onChangeSize(int w, int h) {
    // 设置视口的位置和大小
    glViewport(0, 0, w, h);
    // 设置透视投影
    viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.f);
    // 加载透视投影矩阵到投影矩阵栈
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    // 更新几何变换对象的模型视图矩阵和投影矩阵
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}

// OpenGL初始化设置
void initializeScene() {
    // 设置清空颜色为浅灰色
    glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
    // 初始化着色管理器的默认着色器
    shaderManager.InitializeStockShaders();
    // 更新几何变换对象的模型视图矩阵和投影矩阵
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
    // 将观察者位置向前移动5个单位
    viewFrame.MoveForward(5.0);
    // 创建环形图形的三角形批次
    gltMakeTorus(torusBatch, 1.0, 0.3, 100, 50);
    // 设置点的大小为4个像素
    glPointSize(4.f);
}

// 渲染场景的回调函数
void onRenderScene(void) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // 清空颜色缓冲区、深度缓冲区和模板缓冲区
    
    GLfloat vRed[] = {1.0f, 0.0f, 0.0f, 1.0f};
    
    // 将观察者的视图矩阵压栈【in】
    modelViewMatrix.PushMatrix(viewFrame);
    // 使用默认光照着色器,将模型视图和投影矩阵传给着色器,发的光是红色的
    shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
    // 绘制环形图形
    torusBatch.Draw();
    // 弹出观察者的视图矩阵【out】
    modelViewMatrix.PopMatrix();

    glutSwapBuffers();
}

// 处理特殊按键事件的回调函数
void onSpecialKeysPress(int key, int x, int y) {
    if (key == GLUT_KEY_UP)
        viewFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f); // 向上旋转视图

    if (key == GLUT_KEY_DOWN)
        viewFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f); // 向下旋转视图

    if (key == GLUT_KEY_LEFT)
        viewFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f); // 向左旋转视图

    if (key == GLUT_KEY_RIGHT)
        viewFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f); // 向右旋转视图

    // 标记窗口需要重新绘制
    glutPostRedisplay();
}

// 主函数
int main(int argc, char *argv[]) {
    // 设置工作目录,用于找到资源文件
    gltSetWorkingDirectory(argv[0]);
    // 初始化GLUT库
    glutInit(&argc, argv);
    // 设置显示模式,启用双缓冲、RGBA颜色模式、深度测试和模板缓冲区
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
    // 设置窗口大小
    glutInitWindowSize(800, 600);
    // 设置标题
    glutCreateWindow("一个环示例");

    // 注册窗口大小改变的回调函数
    glutReshapeFunc(onChangeSize);
    // 注册渲染场景的回调函数
    glutDisplayFunc(onRenderScene);
    // 注册特殊按键事件的回调函数
    glutSpecialFunc(onSpecialKeysPress);

    // 初始化GLEW库
    GLenum status = glewInit();
    if (GLEW_OK != status) {
        fprintf(stderr, "glew error: %s\n", glewGetErrorString(status)); // 打印GLEW初始化错误信息
        return 1;
    }

    // 执行OpenGL初始化设置
    initializeScene();
    // 进入GLUT主循环,等待事件触发,处理用户输入和渲染场景
    glutMainLoop();

    return 0;
}

#pragma clang diagnostic pop

上述代码运行时的效果如下图:

当使用键盘上下左右键控制模型旋转时,发现模型的一部分是黑色的。这是因为openGL在绘制过程中会将甜甜圈内部的面绘制出来了,内部因为光源无法照射到会绘制成黑色,然而这不是我们希望看到的。

2. 正背面剔除(Face Culling)

如何理解正背面剔除

在现实世界中,如果我们尝试去从某一个角度观察一个立方体盒子,无论如何在这一个角度我们最多只能看到这个盒子的三个面,无法看到其他面。

所以,在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,哪些部分是对观察者不可见的。对于不可见的部分,应该及早丢弃。例如一个不透明的墙壁后的内容,就不应该被渲染这种情况就叫做“隐藏⾯消除”(Hidden Surface Elimination)。

想象一下,在3D图形中,无论我们从哪个方向观察立方体,最多只能看到3个面。而那大于等于3个的不可见的面对于渲染来说是多余的,绘制它们只会浪费计算资源和时间,所以如果我们能以某种方式求掉这部分数据,那么openGL的渲染性能将能提高超过50%。

现实中我们能够通过眼睛来判断物体的某个面是否在视野内,那计算机如何知道某个⾯在观察者的视野中会不会出现?我们知道,任何平面都有2个面,把某一时刻能观察到的面叫做正面,不能观察到的面叫做背面,但是同一时刻我们只能观察到一个面。

我们使用一些方法告诉openGL一个面是正面还是背面,那么openGL就可以在渲染的时候自渲染这些正面,剔除背面的渲染。

这种能够进行隐藏面消除的方法就叫做正背面剔除

正面还是背面

openGL是通过分析这个面的顶点顺序来判断这个面相对观察着是正面还是背面,默认情况下:

  • 正面:按照逆时针顶点连接顺序的三⻆形⾯。
  • 背面:按照顺时针顶点连接顺序的三角形⾯。
GLfloat vertices[] = {
    //顺时针
    vertices[0], // vertex 1 
    vertices[1], // vertex 2 
    vertices[2], // vertex 3 
    // 逆时针
    vertices[0], // vertex 1 
    vertices[2], // vertex 3 
    vertices[1] // vertex 2
};

顺时针
逆时针

通过下面的立方体来了解一下openGL是如何判断一个面是正面和背面的:

  • 左侧三角形顶点顺序为: 1—> 2—> 3
  • 右侧三角形的顶点顺序为: 1—> 2—> 3

正方体

  • 当观察者在右侧时,右边的三⻆形方向为逆时针⽅向,判定为为正⾯;⽽左侧的三角形为顺时针,判断为背⾯。
  • 当观察者在左侧时,左边的三角形为逆时针方向,判定为正面;而右侧的三角形为顺时针,判定为背面。
  • 当观察者在中间时,左边的三角形顺时针方向,判定为正面;而右侧的三角形也是顺时针,也判定正面。

所以 正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的 。随着观察者⻆角度方向的改变,正⾯和背⾯也会跟着改变,这和我们现实生活中的经验是一致的。

openGL对正背面剔除的支持

当然,是否开启正背面剔除技术、剔除正面还是背面、判断面是正面和背面都可以通过openGL的API指定。

  • 开启表⾯面剔除(默认背⾯面剔除)

    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。

    例如,剔除正⾯实现:

    glCullFace(GL_BACK); 
    glFrontFace(GL_CW);
  • 剔除正⾯实现

    glCullFace(GL_FRONT);

3. 深度测试

深度定义

某个像素点的深度定义为像素点在3D世界中距离摄像机的距离, 即Z值。

深度缓冲区

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

为什么需要深度缓冲区

在不使⽤深度测试的时候,如果我们要绘制两个远近不同的物体,首先绘制一个距离比较近的物体,然后再绘制距离较远的物体,那么距离远的物体因为后绘制,会把距离近的物体覆盖掉。

有了深度缓冲区后,绘制物体的顺序就变得不那么重要了。 实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写⼊到缓冲区中,除⾮主动调⽤ glDepthMask(GL_FALSE)来禁⽌openGL写入深度缓冲区。

如何使用深度缓冲区---深度测试

上面说到深度缓冲区和颜色缓冲区是一一对应的,颜⾊缓冲区存储像素点的颜⾊值,深度缓冲区存储对应像素点的深度值。

如果决定将一个新的像素绘制在屏幕上,首先需要将像素的深度值与原来屏幕上此位置的深度值比较,如果新像素的深度值大于屏幕上像素的深度值,那么放弃该绘制;
如果新像素的深度值小于屏幕上像素的深度值,那么执行该绘制,同时更新屏幕该位置颜色缓冲区为新像素的颜色值,深度缓冲区为新像素的深度值。

openGL对深度测试的支持

深度缓冲区,一般由openGL的窗⼝管理系统,GLFW创建。深度值⼀般是16位、24位、32位值表示的,通常使用24位。当然位数越高,深度精确度越好。

  • 开启深度测试

    glEnable(GL_DEPTH_TEST);
  • 一般在绘制场景前,需要清除颜色缓存区和深度缓冲区

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

    清除深度缓冲区默认值为1.0,表示最大的深度值,openGL支持的深度值的范围为(0,1)之间,值越⼩表示像素点越靠近观察者,值越⼤表示像素点越远离观察者。

  • 指定深度测试判断式

    可以通过制定深度测试判断式来改变深度测试通过的条件。

    // 指定深度测试判断模式
    void glDepthFunc(GLEnum mode);
    判断模式(mode)描述
    GL_ALWAYS总是通过测试
    GL_NEVER总是不通过测试
    GL_LESS当前深度值 < 存储的深度值 --> 通过测试
    GL_EQUAL当前深度值 = 存储的深度值 --> 通过测试
    GL_LEQUAL当前深度值 <= 存储的深度值 --> 通过测试
    GL_GREATER当前深度值 > 存储的深度值 --> 通过测试
    GL_NOTEQUAL当前深度值 != 存储的深度值 --> 通过测试
    GL_GEQUAL当前深度值 >= 存储的深度值 --> 通过测试
  • 打开/阻断深度缓存区写⼊

    void glDepthMask(GLBool value);
    value :
    GL_TURE 开启深度缓冲区写⼊入;
    GL_FALSE 关闭深度缓冲区写⼊入;

4. ZFighting问题

出现问题

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

ZFighting

ZFighting

如何解决

第一步:启⽤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 和 0.0 简单赋值给glPolygonOffset就可以满足需求。

第三步:关闭Polygon Offset

glDisable(GL_POLYGON_OFFSET_FILL)

如何避免ZFighting问题

  • 不要将两个物体靠的太近,避免渲染时三⻆形重叠在一起。这种方式需要对场景中物体插入一个少量的偏移,就可能避免ZFighting现象。
  • 因为手动插⼊⼩小的偏移量是有代价的,所以尽可能将近裁剪面设置得离观察者远一些。在近裁剪平面附近,深度的精确度是很高的,尽可能让近裁剪⾯远一些的话,会使整个裁剪范围内的精确度变⾼一些。但是这种⽅式会把离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯面参数。
  • 使⽤更⾼位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用32位的缓冲区,使精确度更高。
分类: 音视频开发扫盲 标签: OpenGL正背面剔除深度测试

评论

暂无评论数据

暂无评论数据

目录