绘制一个3D图形——甜甜圈
相关代码
#include <stdio.h>
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"
#include <GLUT/GLUT.h>
GLFrame viewFrame; // 物体
GLFrame cameraFrame; // 观察者
GLFrustum viewFrustum; // 管擦着投影矩阵
GLTriangleBatch torusBatch; // 甜甜圈批次类
GLMatrixStack modelViewMatrix; // 模型视图矩阵
GLMatrixStack projectionMatrix; // 投影矩阵
GLGeometryTransform transformPipeline; // 集合变换管道
GLShaderManager shaderManager; // 着色器管理类
GLfloat vRed[] = { 0.1f, 0.3f, 0.8f, 1.0f };
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);
}
void setupRC() {
// 设置背景颜色
glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
// 初始化着色器管理器
shaderManager.InitializeStockShaders();
// 从初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
// 将相机向后移动7个单元:肉眼到物体之间的距离
viewFrame.MoveForward(10);
// 创建一个甜甜圈
// 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);
// 点的大小(方便点填充时,肉眼观察)
glPointSize(4.0f);
}
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();
}
void specialKeys(int key, int x, int y) {
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);
}
glutPostRedisplay();
}
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("OpenGL渲染方式");
glutReshapeFunc(changeSize);
glutDisplayFunc(renderScene);
glutSpecialFunc(specialKeys);
GLenum status = glewInit();
if (status != GLEW_OK) {
printf("error: %s\n", glewGetString(status));
return 1;
}
setupRC();
glutMainLoop();
return 0;
}
代码解析
- main函数:程序入口
- ChangeSize函数:主要是设置视口及投影方式
- SetupRC函数:图形数据配置,主要是顶点数据及图元连接方式
- RenderScene函数:主要用于图形的绘制,可以系统触发,也可以开发者手动触发
- SpecialKeys函数:对特殊键位的回调处理
main函数
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("OpenGL渲染方式");
//注册响应的回调函数。
glutReshapeFunc(changeSize);
glutDisplayFunc(renderScene);
glutSpecialFunc(specialKeys);
GLenum status = glewInit();
if (status != GLEW_OK) {
printf("error: %s\n", glewGetString(status));
return 1;
}
//执行SetupRC函数。
setupRC();
//开启glutMainLoop。
glutMainLoop();
return 0;
}
- 初始化环境变量。
- 注册响应的回调函数。
- 执行SetupRC函数。
- 开启glutMainLoop。
SetupRC
void setupRC() {
// 设置背景颜色
glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
// 初始化着色器管理器
shaderManager.InitializeStockShaders();
// 从初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
// 将相机向后移动7个单元:肉眼到物体之间的距离
viewFrame.MoveForward(10);
// 创建一个甜甜圈
// 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);
// 点的大小(方便点填充时,肉眼观察)
glPointSize(4.0f);
}
- 设置背景颜色。
- 初始化着色器管理器。
- 设置相机(观察者)的位置。
- 调用gltMakeTorus创建一个甜甜圈。
- 设置点的大小(方便点填充时,肉眼观察)glPointSize。
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
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视图。
- 绘制。
- 绘制完成后将模型视图矩阵恢复。
- 交换缓冲区。
运行代码,得到甜甜圈
甜甜圈.png
看起来甜甜圈正常,当我们进行旋转时就出现了问题
旋转时的甜甜圈.png
情况分析
默认情况下,我们所渲染的每个点、线或三角形都会在屏幕上进行光栅化,并按照在组合图元批次时指定的顺序排列,这在某些情况下会产生问题
在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的.对于不可见的部分,应该及早丢弃.例如在一个不透明的墙壁后,就不应该渲染.这种情况叫做”隐藏面消除”(Hidden surface elimination)
上面出现的问题:
甜甜圈在旋转过程中,OpenGL不知道该显示哪些界面,导致本来是观察者不应该看到且该丢弃部分,不仅看到了,而且没有将隐藏部分丢弃。
没有对不可见部分进行处理,导致渲染了背面的黑暗面,在甜甜圈转动的过程中就会把暗面显示出来,达不到我们想要的效果。
解决办法
1. 油画算法
所谓油画算法先绘制场景中的离观察者较远的物体,再绘制较近的物体。
油画算法.png
先绘制红⾊部分,再绘制⻩色部分,最后再绘制灰色部分,即可解决隐藏⾯消除的问题。
但这种方法在图形处理中效率很低,必须在任何发生重叠的地方对每个像素进行两次写操作,速度会变慢。
油画算法也有它的弊端,比如当图层之间相互交叉的时候,油画法就无法区分谁远谁近了,如下图
图层交叉.png
图中无法界定那个图形远近的问题,因此油画算法就无法解决隐藏面的问题。
2. 正背面剔除
正背面剔除是OpenGL的一种图形绘制技巧,主要用于处理立体图形绘制时,只绘制观察者能看到的部分,看不到的部分就丢弃不绘制。
正背面剔除的主要思想就是:只绘制用户看到的部分,对于看不到的部分直接丢弃。
尝试想象⼀个3D图形,你从任何⼀个方向去观察,最多可以看到⼏个⾯?
3D图形.png
答案是最多3⾯。从⼀个⽴方体的任意位置和方向上看,你不可能看到多于3个⾯。 那么我们为何要多余的去绘制那根本看不到的3个⾯呢? 如果我们能以某种⽅式去丢弃这部分数据,OpenGL 在渲染的性能即可提高超过50%。
那么如何知道某个面在观察者的视口中不会出现?
任何平⾯都有2个⾯,正⾯和背⾯。意味着你一个时刻只能看到一⾯面。OpenGL 可以做到检查所有正面朝向观察者的⾯,并渲染它们.从⽽丢弃背⾯朝向的⾯。这样可以节约⽚元着⾊器的性能。
如何告诉OpenGL 你绘制的图形,哪个⾯是正⾯,哪个面是背⾯?
答案:通过分析顶点数据的顺序。
正⾯背⾯.png
分析立方体中的正背面
立方体中的正背面.png
左侧三⻆形顶点顺序为: 1—> 2—> 3 ;
右侧三⻆形的顶点顺序为: 1—> 2—> 3 .
• 当观察者在右侧时,则右边的三角形⽅向为逆时针方向则为正⾯,⽽左侧的三角形为顺时针则为背面。
• 当观察者在左侧时,则左边的三⻆形为逆时针方向判定为正⾯,⽽右侧的三⻆形为顺时针判定为背⾯。
总结:正面和背面是有三角形的顶点定义顺序和观察者⽅向共同决定的。随着观察者的⻆度⽅向的改变,正面背⾯也会跟着改变。
正背面剔除相关代码
//开启表面剔除(默认背面剔除)
glEnable(GL_CULL_FACE);
//关闭表面剔除(默认背面剔除)
glDisable(GL_CULL_FACE);
//选择剔除那个面(正面/背面)
// mode参数为: GL_FRONT, GL_BACK, GL_FRONT_AND_BACK,默认GL_BACK
glCullFace(GLenum mode);
//用户指定绕序那个为正面
//mode参数为: GL_CW, GL_CCW,默认值:GL_CCW
glFrontFace(GL enum mode);
//剔除正面实现①
glCullFace(GL_BACK);
glFrontFace(GL_CW);
//剔除正面实现②
glCullFace(GL_FRONT);
glFrontFace(GL_CCW);
下面加入到代码里面,首先声明一个全局变量标记
//标记:背面剔除
int iCull = 0;
再来添加监听菜单栏点击方法
// 监听菜单栏点击
void ProcessMenu(int value)
{
switch(value)
{
case 1:
iCull = !iCull;
break;
}
//通过glutPostRedisplay主动触发渲染。
glutPostRedisplay();
}
然后在mian()函数中添加右击菜单栏方法
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);
//添加右击菜单栏
// Create the Menu
glutCreateMenu(ProcessMenu);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
return 1;
}
SetupRC();
glutMainLoop();
return 0;
}
最后在 RenderScene()中添加开启和关闭正背面剔除功能
void RenderScene()
{
//1.清除窗口和深度缓冲区
//可以给学员演示一下不清空颜色/深度缓冲区时.渲染会造成什么问题. 残留数据
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//开启/关闭正背面剔除功能
if (iCull) {
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
}else
{
glDisable(GL_CULL_FACE);
}
//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);
//使用默认光源着色器
//通过光源、阴影效果跟提现立体效果
//参数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();
}
运行代码,通过键盘方向键控制甜甜圈的旋转,可以发现,没有黑色的填充了,但是当甜甜圈的侧面正对着我们的时候,出现了另外一个问题。
另外一个问题.png
出现这样的一个缺口的原因是存在两个重叠的正面。如图所示,正面1和正面2重叠,这时候系统不知道该渲染哪个面,就在重叠部分出现了这样的一个缺口。要解决这个问题可以通过深度测试方法。
缺口的原因.png

油画算法.png
图层交叉.png













网友评论