OpenGL中的正背面剔除和深度测试
问题场景
在讲正背面剔除和深度测试前,先来使用OpenGL绘制一个甜甜圈。绘制甜甜圈的代码如下:
//
// main.m
// test
//
// Created by macbook pro on 2020/8/13.
// Copyright © 2020 Miss CC. All rights reserved.
//
#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 viewFrustum;
GLMatrixStack projectionMatrix;
GLMatrixStack modelViewMatrix;
GLShaderManager shaderManager;
GLGeometryTransform transform;
GLTriangleBatch torusBatch;
void RenderScence(){
//清除窗口和深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//将视图模型矩阵放入视图模型矩阵堆中
modelViewMatrix.PushMatrix(viewFrame);
//设置一定颜色值
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//使用默认点光源着色器
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transform.GetModelViewMatrix(), transform.GetProjectionMatrix(), vRed);
//绘制
torusBatch.Draw();
//将视图模型堆栈中的视图模型矩阵出栈
modelViewMatrix.PopMatrix();
//恢复绘制点大小
glPointSize(1.0f);
//交换缓冲区
glutSwapBuffers();
}
void SetupRC(){
//使用指定颜色清除背景色
glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
//初始化着色器管理器
shaderManager.InitializeStockShaders();
//将相机向外移动30个单元
viewFrame.MoveForward(7);
//绘制一个甜甜圈
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
//设置绘制时点的大小,便于肉眼观察
glPointSize(4.0f);
}
void SpecialKey(int key, int x, int y){
switch (key) {
case GLUT_KEY_UP:
viewFrame.RotateWorld(m3dDegToRad(-15), 1, 0, 0);
break;
case GLUT_KEY_DOWN:
viewFrame.RotateWorld(m3dDegToRad(15), 1, 0, 0);
break;
case GLUT_KEY_LEFT:
viewFrame.RotateWorld(m3dDegToRad(-15), 0, 1, 0);
break;
case GLUT_KEY_RIGHT:
viewFrame.RotateWorld(m3dDegToRad(15), 0, 1, 0);
break;
default:
break;
}
glutPostRedisplay();
}
void ChangeSize(int w, int h){
//设置视口大小
glViewport(0, 0, w, h);
//设置投影矩阵
viewFrustum.SetPerspective(35, float(w)/float(h), 1, 100);
//将投影矩阵放入投影矩阵堆中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//初始化管线
transform.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
int main(int argc, char * argv[]) {
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("test");
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScence);
glutSpecialFunc(SpecialKey);
GLenum err = glewInit();
if(GLEW_OK != err){
return 1;
}
SetupRC();
glutMainLoop();
return 0;
}
这段代码跑起来后的效果如下图所示:
image.png
这看起来似乎是正常的,但是当图形进行转动的时候就出现如下效果。
image.png
这看起来太不正常了,为什么会出现这种情况呢?由于这里绘制甜甜圈的时候,不再使用是平面着色器,而是改了默认点光源着色器,会不会是因为着色器的原因呢?这里我们改成平面着色器来试试看。修改着色器使用的代码得到如下效果
image.png
看起来似乎正常了,但是,这是真的正常现象么?答案是否定的。点光源着色器会出现黑色部分是因为默认无光照的那面将显示成黑色,而平面着色器其实也出现了同样的问题,但是因为正面和背面的颜色一样肉眼无法直观看出来而已。
问题分析
在3D图形的渲染过程中,我们是需要决定对观察者而言图形的哪部分是可见,哪部分是不可见的。例子:当我们站在一间房子门前的时候,只能看见房子的前面,而无法看到房子的后面。同样,在图形渲染的过程也存在于观察者而言看不见的那面,既然观察者看不见,那也没有必要将它渲染出来,否则就会看见上面那种不正常的情况,即旋转图形时看见了图形的背面。
解决方案
想象一个3D图形,从任何一个方向去观察,我们最多只能看见3个面,那既然只能看见3个面,又为何要去渲染其它看不见的面呢。如果能以某种方式丢弃掉看不见的那几面的数据,是不是不仅解决了看见背面的问题,也使得OpenGL的渲染速度提高了呢。
OpenGL可以做到检查所有正面朝向观察者的面并渲染它们,而丢弃背向观察者那一面的数据。
那OpenGL是如何做到区分正面和背面的呢?答案是,可以通过分析顶点数据的顺序。
- 正面:OpenGL按逆时针进行顶点相连的三角形面为正面。
- 背面:OpenGL按顺时针进行顶点相连的三角形面为背面。
这个顺序也可以由用户指定,但是通常不建议修改。修改的函数为:
//用于修改正面的函数
void glFrontFace(GLenum mode);
//model有两种:GL_CW(顺时针),GL_CCW(逆时针),
//OpenGL中的默认值:GL_CCW
OpenGL内部会通过分析顶点数据的顺序来确定正背面,而开发者只需要告诉OpenGL,是否开启或关闭正背面剔除。
//开启表面剔除 (默认背面剔除)
void glEnable(GL_CULL_FACE);
//关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);
//设置要剔除的面
/*
mode = GL_FRONT 剔除正面
mode = GL_BACK 剔除背面
mode = GL_FRONT_AND_BACK 剔除正背面
*/
void glCullFace(GLenum mode);
此时已经了解了出现黑色背面的原因以及解决方案,因此我们在代码中开启表面剔除再来看看效果。
在RenderScence代码中增加
glEnable(GL_CULL_FACE);
image.png
至此,已经解决了出现黑色隐藏面的问题。
新的问题
但是此时出现了新的问题,如下图所示:
image.png
从图中可以看出,甜甜圈旋转到某一角度时看过去像是被咬了一口似的。这是为什么呢?当甜甜圈旋转到前面两部分有重叠时,于我们而言,需要显示的是前面,而后面的部分需要隐藏(丢弃数据),但是OpenGL并不能清楚的区分两个图层哪个应该在前面,哪个应该在后面,因此当OpenGL隐藏了前面时就导致了甜甜圈产生缺口。
在解决这个问题前,我们先了解几个概念
深度
深度是指在OpenGL坐标系中,像素点在z轴方向与观察者的距离。
在OpenGL中,不能简单的说z越小离观察者越近,深度与图形中像素点的z坐标有如下关系:
- 如果观察者在z轴的正方向时,那么物体的z坐标越大越靠近观察者。
- 如果观察者在z轴的负方向时,那么物体的z坐标越小越靠近观察者。
深度缓冲区
深度缓冲区是指一块专门的内存区域,存储在显示过程中,屏幕上所绘制像素点的深度值。
- 在深度缓冲区中,每个像素点对应于一个深度值。
- 深度缓冲区的数据范围是[0,1],默认值是1.0,表示深度值的最大值。
深度测试
一个物体在绘制的时候,像素点新的深度值需要与深度缓冲区中的深度值进行比较:
- 新值>旧值,则丢弃新值的数据;
- 新值<旧值,则将新的深度值更新到深度缓冲区。
甜甜圈缺口问题解决方案
了解前面几个概率之后,知道需要使用深度测试来解决问题。在需要绘制甜甜圈之前开启深度测试,此时OpenGL就会根据深度值的新旧值来判断当前数据是否需要被丢弃。
开启深度测试的代码如下:
//开启深度测试
glEnable(GL_DEPTH_TEST);
//关闭深度测试
glDisable(GL_DEPTH_TEST);
开启深度测试的效果如下:
image.png
总结
正背面剔除
OpenGL根据顶点数据的顺序判断当前是用户可见的正面还是不可见的背面,如果是正面则绘制,如果是背面则将数据丢弃。
深度测试
当两个图层重叠时,OpenGL根据物体坐标的z轴以及观察者的位置判断当前图层是靠近观察者那面还是远离观察者那面。如果是靠近观察者的那面则绘制,否则则丢弃数据。