OpenGL基础2

本文深入探讨OpenGL的实现机制,包括硬件和软件级别实现的区别。详细解释了OpenGL程序中第一行int main(int argc, int* argv)的原因,以及如何通过argc和argv参数初始化OpenGL配置。此外,文章介绍了OpenGL中的异常获取方式,画图形时正面与背面的默认设定及其调整方法,颜色处理、深度测试模式、背面消隐功能,以及多边形模式。同时,阐述了OpenGL中的视觉变换过程,模型视图矩阵的初始化与矩阵堆栈的使用。最后,简述了OpenGL与MFC整合的过程,并分享了实际项目中的实现效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

OpenGL的实现:

       OpenGL的实现可以分为硬件级别和软件级别两种。

软件级别是程序员编写的OpenGL程序,通过调用系统自带的图形接口实现。

       硬件级别的实现是通过硬件厂商(一般就是显卡厂商)提供的硬件图形接口直接跳过系统输出到显示器。

       我们用的都是软件级别的实现。

 

之前一直纠结OpenGL程序的第一行 int main(intargc,int *argv)为什么要这样写,因为平时都是直接void main()的,这次特地查了下,貌似原来这才是控制台C++的标准写法...argc表示命令行调用时参数的个数,argv则是一个指向字符串数组的指针,用来存放调用时参数的具体内容。与之相应的是main里面第一行的glutInit(argc,argv),这个函数通过命令行里调用程序时给的参数初始化OpenGL的相关配置。就像glutSetWindowPosition(intx,int y)函数一样,如果不需要设置的话可以不调用,这时使用的就是默认的设置,目前来看可以省略掉glutInit()和使用void main(void)。

OpenGL中的异常获取:

       OpenGL中提供了两个函数来获取异常,分别是glGetError()和gluErrorString()。

       GLenum glGetError(void);

       该函数返回GL_INVALID_ENUM(枚举参数超出范围)、GL_INVALID_VALUE(数值参数超出范围)、GL_INVALID_OPERATION(在当前的状态中操作非法)、GL_STACK_OVERFLOW(栈上溢)、GL_STACK_UNDERFLOW(规模下溢)、GL_OUT_OF_MEMORY(内存不足)、GL_TABLE_TOO_LARGE(指定的表太大)、GL_NO_ERROR(没有错误)六种异常值。

       如果同时出现多种异常,则需要调用循环,每次glGetError会按顺序返回一个,直到最后返回NOERROR为止。

       const GLubyte* gluErrorString(GLenumerrorCode);

       该函数的参数是glGetError的返回值,并返回描述错误的字符串。

 

画图形方面:

OpenGL把逆时针环绕画出的多边形默认为正面,把顺时针环绕画出的多边形默认为背面。

要想改变这个默认可以调用glFrontFace(GL_CW);把顺时针环绕认为是正面,用glFrontFace(GL_CCW);把逆时针认为是正面。

这个正面背面的特性在后期可能会有用,我们可以将一个多边形的正面和背面设成不同的材质。

PS:在下文的背面消隐中就用到了这个特性。

OpenGL中的颜色实际上是对单个顶点而言的,而不是多边形。

OpenGL对多边形的着色有两种模式,单调着色和渐变着色。

单调着色:glShadeModel(GL_FLAT);

用多边形绘制时最后一个顶点的颜色对整个多边形进行着色。

渐变着色(也叫平滑着色):glShadeModel(GL_SMOOTH);

根据每个顶点的颜色对多边形进行渐变着色,相邻顶点之间过渡平滑。

注意,这里的着色是对一个图元来说的,比如一个三角形。在画一个三角形扇的时候如果是单调着色模式,那么只要在循环中改变下一个顶点的颜色就可以得到不同颜色的三角形组成的三角形扇,而不是整个扇都是最后一个顶点的颜色。

深度测试模式:

一个很重要的功能。在默认状态下,OpenGL会将后绘制的图形显示在更接近视口的位置(因为是直接画,后画的当然会把前面画的挡住),因此如果背面的图形被后绘制了,那么它会一直出现在前面的图形的更前面,而不是后面。这样的情况下如果将一个三维图形旋转,那么我们将一直看到后绘制的图形在前面挡着本应该在它前面的图形,这是我们不希望看到的,我们应该让图形显示的层次遵循它在裁剪空间中本来应有的物理层次。而打开深度测试模式可以为我们解决这个问题。

glEnable(GL_DEPTH_TEST);

OpenGL是状态机,一旦用glEnable打开一个状态如果不关闭它,它就会一直生效,在每次的渲染中都会生效。

深度测试的原理:深度测试模式下OpenGL会专门为深度测试分配一个缓冲区(称为深度缓冲区)(上一次的总结里也说过OpenGL中有很多这类缓冲区),每绘制一个顶点都会把它的Z轴信息放入缓冲区,绘制下一个顶点时会将该顶点的Z轴信息与缓冲区中的Z轴信息进行对比大的放在更接近视口的层次渲染,也就是放在前面,反之反之。

我认为在使用OpenGL绘制三维空间的时候,这个模式应该是一直被开着的。另外在开启深度测试模式后面应该加上一句glClear(GL_DEPTH_BUFFER_BIT);来清除深度缓冲区里已经存有的信息,避免冲突。

背面消隐:

       虽然用深度测试可以防止多边形不应该被显示的部分被显示,但是每次渲染顶点的时候都需要跟深度缓冲区里的顶点Z轴进行比较是会消耗计算资源的。在一些情况下,我们知道有些部分是绝对不需要被显示出来的,那我们就可以直接不渲染它,而不是渲染的时候根据Z轴的前后来判断是否需要显示。

       比如一个闭合圆锥体的内表面,在外面观察的时候它们是永远不需要显示的,这时候我们就可以用背面消隐直接不渲染它们。

       glEnable(GL_CULL_FACE);

       与深度测试一样,这个功能也是个状态量,可以叫背面消隐模式吧。

       背面消隐的原理:那么OpenGL是如何判断哪些多边形是内表面(也就是背面),哪些是外表面的呢?这里就要用到上文所说的顺逆时针环绕了。在设顺时针环绕生成的多边形为正面的时候,我们画外表面的时候就必须保证都用顺时针环绕画,这样由于OpenGL把顺时针判断为正面,那么自然它的另一面就是背面了,也就可以被直接不渲染了。如果画好了一整个闭合多边形打开了背面消隐而其中一块外表面不被显示也不要紧,因为我们可以在画那块表面的代码前加上glFrontFace(GL_CCW);来开启逆时针环绕为正面状态,画完加上glFrontFace(GL_CW);恢复顺时针环绕为正面状态。

       背面消隐不像深度测试,它应该在需要被打开的时候打开,而在需要显示双面的时候,比如我在上一次做的旋转三角形程序中需要被打开。

在上一次做的旋转三角形中(由于我是沿Z轴旋转的,所以一直显示的是其中一个面),我在初始化时加语句打开了背面消隐,就不显示任何东西了,因为三角形面向视口的面被认为是背面,而当我在背面消隐之前开启了顺时针环绕为正面的时候(相当于把背面变成了正面)(这里也可以看出默认是逆时针环绕为正面),就可以显示了。而关闭背面消隐的时候无论设置顺还是逆时针环绕为正面都能正常显示。

多边形模式:

OpenGL默认将多边形渲染成“实心的”,这也是可以变的。

我们可以用glPolygonMode();这个函数来对多边形的正背面分别进行“点、线、面”级别的渲染。多边形模式同样是状态量。

glPolygonMode(GL_BACK,GL_POINT);

这是一条开启背面点级别渲染的语句。

GL_BACK指定设置对象为背面,GL_POINT设置其为点级别渲染。

与GL_BACK对应的是有GL_FRONT、GL_FRONT_AND_BACK,指定设置对象为正面或两者兼。

与GL_POINT对应的有GL_LINE、GL_FILL,指定设置对象渲染级别为线级别或面级别。

上两张效果图

 

线级别

点级别(这里为了让点更明显用glPointSize()把点的大小变大了)

 

矩阵部分:

先得再说下OpenGL的几种视觉变换,这里直接引用上篇总结里的内容就够了:

“首先是视图变换,就是变换camera的角度和坐标,也就是改变观察者的位置朝向,从而改变视口中观察到的裁剪区的内容。

然后是模型变换,这个类似于3D建模里的Local模式、局部模式,也就是变换模型本身的角度和位置,这样也能实现视口中内容的变换。

在制作模型较多的场景时显然模型变换是太麻烦了,应该用视图的变换。

投影变换,与其说是变换不如说是投影的模式,这个在上文中有提过,有透视和正投影,区别就是有没有透视效果,不多说了。

视口变换,这个我觉得其实就是个光栅化的过程,我们不需要特别的去控制它。“

默认的视口是在Z轴正方向正对Z轴负方向的,也就是从裁剪区的顶上往下看。

下面顶点通往屏幕的过程会详细理解哪些变换在哪些时候被调用。

顶点通往屏幕的过程:(这块是我自己理解的,不知道对不对)

       1)顶点保存为一个1*4的矩阵,前三个值为顶点在裁剪区的XYZ坐标,第四个为缩放量W。

       此时还不存在观察者,也就是视图变换和模型变换还没有起作用。

       2)顶点矩阵与模型视图矩阵相乘,得到视觉坐标。

这时候视图变换和模型变换会起作用,得到从观察者角度看过去的坐标,也就是视觉坐标。这里也说明模型变换和视图变换的本质是一样的,只是参照物不同。同时也可以理解为模型变换和视图变换的本质都是修改模型视图矩阵。

模型视图矩阵(MODELVIEW)就是描述视口和裁剪区相对位置关系的矩阵。顶点矩阵与模型视图矩阵相乘就可以得到观察者角度的视觉坐标。

模型视图矩阵只有一个,变换只是对它做修改而已。

3)视觉坐标与投影矩阵相乘,得到裁剪坐标。

这时候投影模式(正投影或者透视投影)会起作用

4)裁剪坐标除以W值,产生设备坐标。

视口变换起作用,光栅化,3D变2D。

这时,顶点到了视口(Viewport),也就是屏幕。

 

模型视图矩阵的初始化(单位矩阵):

       由于视图变换和模型变换都是对模型视图矩阵的修改,而模型视图矩阵又是一直都存在的,所以视图变换和模型变换都是带有叠加效应的。

       也就是说,如果裁剪区内有两个物体,我想让他们一个沿Z轴一个 沿X轴分别移动5个单位,那我用glTranslatef(0.0f,0.0f,5.0f)来先沿Z轴正方向移动5个单位,再画第一个物体,接着用同样方法沿X轴正方向移5个单位,画第二个物体。这时候画出来第二个物体实际上是先沿着Z轴移动又沿着X轴移动,两次变换都会对它生效。

       因此,在很多有多个物体而我们需要分别对它们进行操作的情况下,我们需要初始化模型视图矩阵到最初的状态。实现的方法就是把模型视图矩阵设为一个单位矩阵,任何顶点矩阵乘以单位矩阵得到的还是原来的顶点矩阵,也就是顶点没有发生变换,达到了初始化模型视图矩阵的效果。

       glMatrixMode(GL_MODELVIEW);

       glLoadIdentity();

       第一句指定矩阵操作对象为模型视图矩阵,矩阵操作对象同样是状态量,这样设置以后在下次设置矩阵操作对象之前,操作对象会一直是模型视图矩阵。

       第二句是初始化目前的操作对象,这里就是初始化模型视图矩阵,本质上就是把它变成一个4*4的单位矩阵。、

 

矩阵堆栈:

       很多时候,虽然我们需要单独对每个物体进行操作,但是初始化模型视图矩阵会丢失之前操作的结果,我们不可能在每次操作的时候重复一遍之前所有的操作,OpenGL提供了矩阵堆栈来解决这个问题。

       OpenGL的矩阵堆栈可以存放模型视图矩阵和投影矩阵,在我们需要操作另一个物体的时候,我们可以把当前的矩阵压入栈内,相当于把它保存起来,在操作完别的物体后,再把这个矩阵弹出来,相当于读取保存的内容。这样就实现了灵活的多物体独立变换。

       用glPushMatrix()可以将当前的状态矩阵入栈,glPopMatrix()则是出栈最顶上的矩阵并设为当前的矩阵。

glGetFloatv(GL_MAX_PROJECTION_STACK_DEPTH,&m);

或glGetFloatv(GL_MAX_MODELVIEW_STACK_DEPTH,&m);可以获得矩阵堆栈可以容纳的最大深度到GLfloat变量m。

       如果栈溢出会触发GL_STACK_OVERFLOW异常,如果弹出空栈会触发GL_STACK_UNDERGLOW异常。

 

上面都是对OpenGL系统性的学习,下面是尝试实践,用到了些上面矩阵的知识。

OpenGL与MFC的整合:

       关于OpenGL与MFC整合我没有找到系统性的教程,所以整个框架我是根据群邮件资料里的那个word文档搭建的,那些设置的部分还没有深入理解,这个暂时没时间。     

       程序里面绘图、动画和消息响应的部分是自己写的。这边提几点。

       默认设置下观察点是的原点的,因此如果画的图形Z轴坐标等于零的话,不设置是看不到的,所以需要在绘图语句里面先把观察点“拉远一点”再画图。但是不能每次都拉远,因此在绘图语句里面应该先把当前模型视图矩阵入栈,拉远,再绘图,再出栈。这样就可以保证整个过程中只拉远了一次。

       在控制台下的OpenGL中动画是通过   glutTimerFunc()这个函数来设置定时刷新的,但是在MFC下的话就可以用MFC的定时器来实现,

在On_Timer的定时器响应函数里面加上绘图的语句就行了。

       改变窗口大小的这类的函数用MFC自带的OnSize这类函数重载就可以了。

       对于一些经常要用到的变量坐标什么的可以放在Doc类里面,在OnNewDocument里初始化。

上一个实现效果:

         对于放大缩小的实现有两种方法:1是直接改变绘图时三个顶点的Z轴值。2是改变观察点的位置,也就是改变模型视图矩阵。

       第一种方法很简单直接在键盘的响应函数里面加上z的增加或者减少语句就行了。

上一张放大了以后的效果图:

       用第二种方法实现的时候遇到了很大的困难…

       我觉得明明只要在响应函数里把关于z减少的语句改成glTranslatef(0.0f,0.0f,-0.5f);就可以了,但是这样做就是没有用…

       进一步尝试之后发现如果glTranslatef语句放在绘图的函数里就能生效,而如果放在绘图函数以外的任何函数里都不会生效。

       更进一步的尝试是我定义了两个数组分别用来在响应函数和绘图函数里通过glGetDoublev(GL_MODELVIEW_MATRIX,matrix2);来获得当前活动的模型视图矩阵的值,然后单步跟踪,结果居然发现绘图函数里发生改变的矩阵和响应函数里面的矩阵不是同一个对象…这也就解释了为什么绘图函数外对模型视图矩阵的操作都会无效(因为操作的不是绘图函数用的那个矩阵)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值