甜甜圈的绘制
main
-
初始化环境变量。
-
注册响应的回调函数。
-
执行SetupRC函数。
-
开启glutMainLoop。
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; }
SetupRC
-
设置背景颜色。
-
初始化着色器管理器。
-
设置相机(观察者)的位置。
-
调用
gltMakeTorus
创建一个甜甜圈。//参数1:GLTriangleBatch 容器帮助类 //参数2:外边缘半径 //参数3:内边缘半径 //参数4、5:主半径和从半径的细分单元数量 gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
-
设置点的大小(方便点填充时,肉眼观察)
glPointSize
。void SetupRC() { //1.设置背景颜色 glClearColor(0.3f, 0.3f, 0.3f, 1.0f ); //2.初始化着色器管理器 shaderManager.InitializeStockShaders(); //3.将相机向后移动7个单元:肉眼到物体之间的距离 viewFrame.MoveForward(7.0); //4.创建一个甜甜圈 gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26); //5.点的大小(方便点填充时,肉眼观察) glPointSize(4.0f); }
ChangeSize
-
设置视口。
-
设置透视模式,初始化其透视矩阵。
-
把透视矩阵加载到透视矩阵对阵中。
-
初始化渲染管线。
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.0f, float(w)/float(h), 1.0f, 100.0f); //4.把透视矩阵加载到透视矩阵对阵中 projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix()); //5.初始化渲染管线 transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix); }
RenderScene
-
清除窗口和深度缓冲区。
-
把摄像机矩阵压入模型矩阵中。
-
设置绘图颜色。
-
使用默认光源着色器来渲染3D视图。
//使用默认光源着色器 //通过光源、阴影效果跟提现立体效果 //参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器 //参数2:模型视图矩阵 //参数3:投影矩阵 //参数4:基本颜色值 shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
-
绘制。
-
绘制完成后将模型视图矩阵恢复。
-
交换缓冲区。
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: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(); }
经过上面的步骤,我们就可以看到如下图所示的3D的甜甜圈了。
![](https://img.haomeiwen.com/i1398407/b709f28d80c66ec8.jpg)
这是看起来都是没什么问题的。
接下来我们通过键盘来移动观察者的位置来看看甜甜圈会发生什么变化。
SpecialKeys
SpecialKeys函数是OpenGL接收到键盘控制信号后的回调函数,这个函数我们在main
函数中已经注册过了。我们来看看该如何移动观察者。
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.重新刷新,这个时候会重新出发RenderScene函数来渲染视图。
glutPostRedisplay();
}
发现问题
当我们通过键位改变观察者之后我们发现,原本好好的甜甜圈,变成了下面这个样子,这并不是我们想要的样子。
![](https://img.haomeiwen.com/i1398407/3f18b4d71ab7761c.jpg)
因为我们使用的是默认光源着色器,你可以把这个着色器理解为太阳,正对着太阳的一面为正面,背对着太阳的为背面。上图中红色的部分也就是我们的正面,黑色的为背面,但是我们发现,原本应该显示为正面的地方却显示为了背面。这是为什么呢?
简单概括下出现的问题:甜甜圈在旋转过程中,OpenGL不知道该显示哪些界面,导致本来是观察者不应该看到且该丢弃部分,不仅看到了,而且没有将隐藏部分丢弃。
解决方案
画家算法
那什么是画家算法呢?
画家算法就是有远及近依次绘制不同的图层。近的图层可以将远的图层的背面覆盖掉。就像下面这样。
![](https://img.haomeiwen.com/i1398407/25e2f8f8cd154d66.jpg)
弊端
但是画家算法也仅仅只能解决有远近次序的图形,针对无法区分出谁远谁近(例如三个三角形叠加)的情况,油画算法就无法满足需求了
正背面剔除(Face Culling)
尝试想象一个3D图像,你从任何一个方向去观察,最多可以看到几个画面?
![](https://img.haomeiwen.com/i1398407/0cc79d5bc6d46cb2.jpg)
正如上面的正方体一样,我们最多只能看到3个面。
那我们为什么要去绘制那根本看不到的另外3个面呢?如果我们能以某种方式丢弃这部分数据,OPenGL在渲染时的性能即可提高超过50%。
那我们该如何知道什么是正面什么是背面呢?
分析顶点的顺序
![](https://img.haomeiwen.com/i1398407/e66abf478ad3e8db.jpg)
正/背面区分
- 正面:按照逆时针顶点顺序连接的三角形面。
- 背面:按照顺时针顶点顺序连接的三角形面。
分析立方体中的正背面
![](https://img.haomeiwen.com/i1398407/b277730e12379784.jpg)
-
分析
- 左侧三角形的顶点顺序为:1->2->3;右侧三角形的顶点顺序为:1->2->3
- 当观察者在右侧时,则右边的三角形方向为逆时针,则为正面,而左侧的三角形则为顺时针,则为背面。
- 当观察者在左侧时,则左边的三角形方向为逆时针,则为正面,而右侧的三角形则为顺时针,则为背面。
-
总结
正面和背面是由三角形的顶点定义顺序和观察者的方向共同决定的,随着观察者的角度方向的改变,正面背面也会随着改变。
正背面剔除技巧主要涉及三个方法
-
开启正背面剔除
//开启表面剔除 (默认背面剔除) void glEnable(GL_CULL_FACE);
-
关闭正背面剔除
//关闭表面剔除(默认背面剔除) void glDisable(GL_CULL_FACE);
-
设置需要剔除的面
void glCullFace(GLenum mode);
枚举值 说明 GL_FRONT 剔除正面 GL_BACK 剔除背面,是默认值 GL_FRONT_AND_BACK 剔除正背面
接下来我们实现这个方案
方案实现
新增 ProcessMenu
函数
通过mainloop监听右键菜单触发的消息,收到点击触发消息后,回调该函数对相应菜单进行的点击操作。
void ProcessMenu(int value)
{
switch(value)
{
case 1:
iCull = !iCull;
break;
}
glutPostRedisplay();
}
通过glutPostRedisplay主动触发渲染。
在原来RenderScene
的开头增加如下代码.
//开启/关闭正背面剔除功能
if (iCull) {
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
} else {
glDisable(GL_CULL_FACE);
}
到此,甜甜圈的正背面绘制异常问题就解决了,但是当我们在旋转的过程中,新的问题又出现了,如下图所示。
![](https://img.haomeiwen.com/i1398407/8d5f65a790c37500.jpg)
这是我们发现甜甜圈在旋转的过程中出现了一个缺口。这个问题将留在下篇文章中来讲解。方案为深度测试。
网友评论