我们先看一个例子。先画出OpenGL提供的系统模型——甜甜圈。
绘制过程如下:
1.先定义需要用到的变量
//观察者
GLFrame viewFrame;
//使用视景体类GLFrustum来设置透视投影
GLFrustum viewFrustum;
//甜甜圈批次类(容器)
GLTriangleBatch torusBatch;
//模型视图矩阵堆栈
GLMatrixStack modelViewMatix;
//投影矩阵堆栈
GLMatrixStack projectionMatrix;
//几何变换管线
GLGeometryTransform transformPipeline;
//着色器管理类
GLShaderManager shaderManager;
//标记:背面剔除、深度测试
int iCull = 0;
int iDepth = 0;
2.在main函数中配置渲染环境,开启全局Loop监听屏幕重绘,窗口的改变,特殊键位响应
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;
}
3.在setupRC中,主要设置背景色、初始化着色器管理器、调整观察者与物体距离、创建甜甜圈
void SetupRC()
{
glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
shaderManager.InitializeStockShaders();
//往远离屏幕的方向移动7单位,观察者看到的物体会变小
viewFrame.MoveForward(7.0);
//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);
}
4.在changeSize方法中,去调整视口尺寸,使用GLFrustum类来初始化透视矩阵,并把透视矩阵加载到透视矩阵对阵中,初始化渲染管线,传入模型视图矩阵和投影矩阵。
//窗口改变
void ChangeSize(int w, int h)
{
//1.防止h变为0
if(h == 0)
h = 1;
glViewport(0, 0, w, h);
//3.第一个参数是一个从顶点方向看去的视场角度(用角度值表示)
viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}
5.在RenderScene中
//渲染场景
void RenderScene()
{
//1.清除窗口和深度缓冲区,不清除会导致背景色和物体颜色不可控,残留之前的数据。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//2.把摄像机矩阵压入模型矩阵中,保存初始状态,后面绘制显示完成了要恢复,撤回
modelViewMatix.PushMatrix(viewFrame);
//3.设置绘图颜色
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//4.
//使用平面着色器
//参数1:平面着色器
//参数2:模型视图投影矩阵
//参数3:颜色
// shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
//默认光源着色器,通过光源、阴影效果可以更提现3D立体效果
//参数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();
}
6.在main函数中注册了SpecialKeys的回调,可以通过不同的键位调整观察者位置,从不同视角观察物体。(也可以观察者不动,调整物体的位置,达到同样效果)
void SpecialKeys(int key, int x, int y)
{
//1.判断方向,上下左右方向键
if(key == GLUT_KEY_UP)
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();
}
跑起来可以看到一个甜甜圈就有了。但是发现我们转动甜甜圈的时候,有些部分显示有问题。

问题出现的原因是什么呢?
是首先因为我们用了默认光源着色器,有光的地方就会有阳面和阴面,而我们看到正常的部分就是阳面,出现问题的就是阴面了。而经过我们渲染我们看到了不该看到的面,而OpenGL不知道我们旋转的时候该显示哪一面,所以本身不需要绘制的背光面让我们看到了。但是如果用平面着色器,并不能看到阴面,显示没什么问题,但是根本原因是因为没有光源去区分阳面和阴面,所以我们肉眼看不出来阴面,并不是就不存在阴面。
解决方法
1.油画算法。
油画算法是由远及近的绘制物体,是有个前提条件的,需要层级之间分明,知道层级的远近,近的物体才能把远的物体挡住。而且油画算法在任何发生重叠的地方,虽然底下一层被挡住的部分并不需要显示,但是还是会分别绘制这两个图层,也就是对重叠的像素进行两次写操作,速度会变慢,效率低耗费多。而如果是几个三角形彼此交错的情形,没有孰远孰近,就无法处理了。所以不推荐。
2.采用正背面剔除(隐藏面消除)
这是OpenGL的一种处理3D图形的技巧。这种方法告诉OpenGL只需要绘制观察者看到的面,看不到的就不用绘制出来了。因此它很高效,在渲染的图元装配阶段就丢弃了一些三角形了,节约了片元着色器的性能。一个3D图形,你最多只能看到它的3面,而对于一个立方体来说,如果只需要绘制3面,那相当于提高了50%的效率,何乐而不为呢?
但是使用之前我们得先知道哪些面是需要绘制的,哪些可以不绘制。
OpenGL默认规定了围绕三角形顶点画线的逆时针方向绘制的三角形是正面,当然也提供了方法可以让你改为顺时针为正面,glFrontFace(GLenum mode)
,其中mode有两种:GL_CW
(顺时针),GL_CCW
(逆时针),默认为GL_CCW
,但是一般不建议修改,因为修改正背面设置后会影响到整个项目的环境。
使用的方法是
//开启/关闭正背面剔除功能
if (iCull) {
glEnable(GL_CULL_FACE);
//设置逆时针为正面,可以不用写,默认就是
glFrontFace(GL_CCW);
//设置剔除面为背面,可以不用写,默认就是背面
glCullFace(GL_BACK);
}else
{
glDisable(GL_CULL_FACE);
}
我们可以通过新建菜单栏,手动触发开启和关闭正背面剔除的功能。
//添加右击菜单栏,在ProcessMenu方法中去改变我们一开始定义的iCull变量,根据iCull的0或者非0值,执行`glutPostRedisplay();`去重新渲染。
// Create the Menu
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("Toggle depth test",1);
glutAddMenuEntry("Toggle cull backface",2);
glutAddMenuEntry("Set Fill Mode", 3);
glutAddMenuEntry("Set Line Mode", 4);
glutAddMenuEntry("Set Point Mode", 5);
glutAttachMenu(GLUT_RIGHT_BUTTON);
当我们右键菜单点击2的时候,就会提交重新渲染,触发RanderScene方法执行我们的正背面剔除。

。
run起来之后效果如下:

但是可以看到最后一帧有一块被吃了,显然还有待优化,这就涉及到深度缓冲区和深度测试了,欲知后事如何,且看下节分析OpenGL之深度测试。
网友评论