第八节—关于隐藏面消除的问题
本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。
在绘制3D场景的时候,立体图形对于观察者来说,并不是所有的面都要在同一时间全部都看见,立体图形的一些部分或者面在很多的时候是不需要或者说看不到的,而对于这部分图形的渲染,并不是一个即时需求。比如一个不透明墙面的后面有什么东西,站在墙前面的我们不是立即就要看到的,所以这里就存在了一个隐藏面消除的问题。
都用文字来叙述很片面,也不直观,那么下面我将通过绘制一个甜甜圈,来阐释问题的关键,还有解决问题的方法都有哪些,从而进行讨论。
一、绘制一个甜甜圈
直接上代码。
#pragma mark - 引用类
//GLTool的工具类,里面包含了大多数GLTool类似C语言的函数
#include "GLTools.h"
//矩阵工具类。用于加载矩阵/单元矩阵/矩阵相乘/压栈/出栈/旋转/平移/缩放
#include "GLMatrixStack.h"
//工具类。用它可以设置vForward,vOrigin,vUp
#include "GLFrame.h"
//矩阵工具类。用它可以快速的设置正投影/透视投影。里面还有将3D到2D的映射功能
#include "GLFrustum.h"
//管道。用来管理模型视图矩阵/投影矩阵
#include "GLGeometryTransform.h"
//数学类
#include <math.h>
//宏定义判断系统类别。根据不同的系统导入相应的头文件
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
#pragma mark - 公共变量
//观察者/相机坐标(坐标矩阵)
GLFrame viewFrame;
//容器类
GLTriangleBatch torusBatch;
//设置投影方式类
GLFrustum viewFrustum;
//模型视图矩阵堆栈
GLMatrixStack modelViewMatrix;
//投影矩阵堆栈
GLMatrixStack projectionMatrix;
//变换管道。存储模型视图矩阵/投影矩阵
GLGeometryTransform transformPipeline;
//存储着色器管理器
GLShaderManager shaderManager;
#pragma mark - 函数
//设置视口
void ChangeSize(int w , int h )
{
//因为一会要取得宽高比,这里防止h作为除数出现0的情况
if (h == 0) {
h = 1;
}
//设置视口大小
glViewport(0, 0, w, h);
//设置投影方式
viewFrustum.SetPerspective(36.f, float(w)/float(h), 1.f, 100.f);
//将投影矩阵加载到投影矩阵堆栈当中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
//设置渲染环境
void SetUpRC()
{
//设置清屏颜色
glClearColor(0.3f, 0.3f, 0.3f, 1.f);
//初始化存储着色器管理器
shaderManager.InitializeStockShaders();
//设置相机/观察者的位置
viewFrame.MoveForward(7.f);
//创建一个甜甜圈,glut有绘制环形,直接用
//gltMakeTorus(GLTriangleBatch &torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor)
//参数:
//1.GLTriangleBatch torusBatch:容器类
//2.GLfloat majorRadius:外圈圆半径
//3.GLfloat minorRadius:内圈圆半径
//4.GLint numMajor:外圈细分单元数量
//5.GLint numMinor:内圈细分单元数量
//注:细分单元数量:数值越大,绘制的圈越圆,大概的意思
//注:这里的外圈圆半径一定要大于内圈圆半径
gltMakeTorus(torusBatch, 1.f, 0.3f, 60, 36);
//因为画的就一个图形,也没什么其他的,所以就在环境里直接设置点大小了
glPointSize(4.f);
}
//渲染
void RenderScene()
{
//清空缓冲区。不清除会有之前绘制的残留
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//把相机矩阵压栈到模型视图矩阵中去
modelViewMatrix.PushMatrix(viewFrame);
//设置绘图的颜色
GLfloat vRed[] = {1.f,0.f,0.f,1.f};
//设置存储着色器管理器
//这里选用默认光源着色器,这样才可以显示出立体的效果和阴影效果
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT,transformPipeline.GetModelViewMatrix(),transformPipeline.GetProjectionMatrix(),vRed);
//绘制
torusBatch.Draw();
//绘制完成。出栈
modelViewMatrix.PopMatrix();
//交换缓冲区
glutSwapBuffers();
}
//特殊键位设置
void SpecialKeys (int key , int x , int y)
{
switch (key) {
case GLUT_KEY_UP:
viewFrame.RotateWorld(m3dDegToRad(5.f), 1.f, 0.f, 0.f);
break;
case GLUT_KEY_DOWN:
viewFrame.RotateWorld(m3dDegToRad(-5.f), 1.f, 0.f, 0.f);
break;;
case GLUT_KEY_RIGHT:
viewFrame.RotateWorld(m3dDegToRad(5.f), 0.f, 1.f, 0.f);
break;;
case GLUT_KEY_LEFT:
viewFrame.RotateWorld(m3dDegToRad(-5.f), 0.f, 1.f, 0.f);
break;
}
glutPostRedisplay();
}
#pragma mark - main函数
int main(int argc , char *argv[])
{
//设置工作目录到可执行程序的文件夹下
//GLUT的优先级已经设置过了,这里再设置是做保险
gltSetWorkingDirectory(argv[0]);
//初始化GLUT
glutInit(&argc, argv);
//初始化显示模式
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
//初始化窗口大小
glutInitWindowSize(800, 600);
//创建窗口并命名
glutCreateWindow("Geometry Test Program");
//注册各类函数
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
glutSpecialFunc(SpecialKeys);
//初始化一个Glew库,确保OpenGL的API对程序都可用
GLenum status = glewInit();
if (status != GLEW_OK) {
printf("glew init error : %s \n",glewGetErrorString(status));
return 1;
}
//设置渲染环境
SetUpRC();
//建立OpenGL本地消息循环
glutMainLoop();
return 0;
}
执行效果下图1.1:

这么看是没有任何问题的样子,但是当使用方向键进行旋转之后,如下图1.2:

出现了如图1.2所示的情况,这种情况就是系统并不能分辨立体图形的哪个面才是正面展示,从而将本可以看到的正面还有不应被看到的背面还有内部全都渲染出来了,也就是渲染的时候像素点发生了混乱,渲染顺序出现了问题,这不仅仅是增加了系统不必要的开销,还因此产生了显示的BUG。
对此,我们就需要让系统知道,什么面该渲染展示,什么面不需要渲染展示。
这也就是标题所说的隐藏面消除。
二、对于隐藏面消除问题的解决思路中的油画算法
油画算法是什么意思?简单的说,就是先绘制场景中离观察者远的图形,以从远到近的方式逐一绘制图形。如下图2.1.1所示:

先画红色矩形,再画黄色圆形,最后画灰色的圆角矩形。
那是不是我们通过计算要绘制的图形距离观察者的距离,从而由远向近的渲染就没有问题了呢?
还是有两个问题的:
(1)重复绘制导致性能浪费
如图2.1.1所示,这张图的确可以将红黄灰三个图形重叠的地方重复复置,也不会有很大的性能开销,但是当图元变得复杂,那么多次无用的绘制将势必造成性能浪费,甚至出现BUG。
(2)但是如果要绘制的图像没有办法理清顺序呢
比如下图2.1.2所示:

三个颜色根本无法说明哪一个应该先绘制。这就无法利用油画算法解决隐藏面消除的问题。
所以油画算法并不能解决我们的核心问题,并不是可取的。
那么到底如何利用OpenGL来解决这样的问题呢?利用深度缓冲区,具体的介绍因为内容较多,并且有正确的甜甜圈的画法,所以将在第九节详细写明。