1.
坐标矩阵变换
在RenderScene
函数中写下如下代码:
BOOL CCOpenGLDemoView::RenderScene()
{
glClearColor(0.0f,0.0f,0.0f,0.0f);
glClearDepth(10.0f);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
glColor3f(1.0f,0.0f,0.0f);
//
绘制多边形
glBegin(GL_QUADS);
glVertex3f(0.5f,0.5f,-2.0f);
glVertex3f(-0.5f,0.5f,-2.0f);
glVertex3f(-0.5f,-0.5f,-2.0f);
glVertex3f(0.5f,-0.5f,-2.0f);
glEnd();
::SwapBuffers(m_pDC->GetSafeHdc());
return TRUE;
}
我们的
本意
是要在窗口上绘制一个矩形,但这个矩形的Z
轴位置在-2.0
处,结果怎么样?什么都看不到。为什么?
和真实世界一样,绘制图形的屏幕上也有一个三维坐标系统,这个坐标系统的原点在屏幕的正中心处,为(0.0f,0.0f,0.0f
),X
轴水平向右为正,Y
轴垂直向上为正,而Z
轴则垂直屏幕向外指向操作者为正。
我们上面绘制的四边形,则是将其绘制在屏幕内部-2.0
处,但结果看不到,如何看到远处的物体呢?这就要考虑投影问题了。
修改OnSize
事件,它发生在每一次窗口大小改变的时候,代码如下:
void CCOpenGLDemoView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
// TODO: Add your message handler code here
GLsizei width,height;
GLdouble aspect;
width=cx;
height=cy;
if (cy==0) //
防被零除
{
aspect=(GLdouble)width;
}
else
{
aspect=(GLdouble)width/(GLdouble)height;
}
::glViewport(0,0,width,height); //
重置当前的视口
glMatrixMode(GL_PROJECTION); //
选择投影矩阵
glLoadIdentity(); //
重置投影矩阵
gluPerspective(45.0f,aspect,0.1f,100.0f); //
计算窗口的比例外观
glMatrixMode(GL_MODELVIEW); //
选择模型观察矩阵
glLoadIdentity(); //
重置模型观察矩阵
}
我们从最后五行谈起,它们透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。此处透视按照基于窗口宽度和高度的45
度视角来计算。0.1f
,100.0f
是我们在场景中所能绘制深度的起点和终点,即如果物体的深度大于100.0
,也还是看不到的。
glMatrixMode(GL_PROJECTION)
指明接下来的两行代码将影响projection matrix(
投影矩阵)
。投影矩阵负责为我们的场景增加透视。 glLoadIdentity()
近似于重置。它将所选的矩阵状态恢复成其原始状态。调用 glLoadIdentity()
之后我们为场景设置透视图。
glMatrixMode(GL_MODELVIEW)
指明任何新的变换将会影响 modelview matrix(
模型观察矩阵)
。模型观察矩阵中存放了我们的物体讯息。最后我们载入模型观察矩阵。
好了,现在我们可以看到物体了。
我们上面使用的是透视投影,除此以外,还有一种正交投影,在透视投影中,远处的物体是变小的,这个倒是符合人的正常感觉,而正交投影的取景器是一个封闭的平行六面体,在这个封闭体内,物体的尺寸不随距离的增加而变小。
更改下投影模式:
glOrtho(-0.8f,0.8f,-0.8f,0.8f,0.1f,100.0f);
然后将多边形的Z
全部改为-99.0f
,是不是和0.0f
处仍然是一样的大小?
下面我们把绘制的图形修改一下,写一个绘制立方体的函数,让它替代上面绘制四边形的代码,这样看起来简洁点:
void CCOpenGLDemoView::DrawQuads()
{
//
绘制立方体
glBegin(GL_QUADS);
glVertex3f(1.0f,1.0f,1.0f);
glVertex3f(-1.0f,1.0f,1.0f);
glVertex3f(-1.0f,-1.0f,1.0f);
glVertex3f(1.0f,-1.0f,1.0f);
glColor3f(1.0f,0.0f,0.0f);
glVertex3f(-1.0f,1.0f,-1.0f);
glVertex3f(1.0f,1.0f,-1.0f);
glVertex3f(1.0f,-1.0f,-1.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);
glColor3f(0.5f,1.5f,0.5f);
glVertex3f(-1.0f,1.0f,1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);
glVertex3f(-1.0f,-1.0f,1.0f);
glColor3f(0.5f,0.0f,1.0f);
glVertex3f(1.0f,1.0f,-1.0f);
glVertex3f(1.0f,1.0f,1.0f);
glVertex3f(1.0f,-1.0f,1.0f);
glVertex3f(1.0f,-1.0f,-1.0f);
glColor3f(1.0f,1.0f,0.0f);
glVertex3f(1.0f,1.0f,-1.0f);
glVertex3f(-1.0f,1.0f,-1.0f);
glVertex3f(-1.0f,1.0f,1.0f);
glVertex3f(1.0f,1.0f,1.0f);
glColor3f(1.0f,0.0f,1.0f);
glVertex3f(1.0f,-1.0f,1.0f);
glVertex3f(-1.0f,-1.0f,1.0f);
glVertex3f(-1.0f,-1.0f,-1.0f);
glVertex3f(1.0f,-1.0f,-1.0f);
glEnd();
}
然后编译程序,运行,呵呵,只看见红红的一片,这是因为窗口的视口的范围就是[-1 -1]
到[1 1]
,而对着屏幕的面的大小就是2*2
,覆盖了整个屏幕。我们需要将这个立方体向后移动,以方便我们能够看到它。
修改的代码如下:
BOOL CCOpenGLDemoView::RenderScene()
{
glClearColor(0.0f,0.0f,0.0f,0.0f);//
清除屏幕颜色
glClearDepth(10.0f); //
清除深度缓存
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);//
进行清除
glLoadIdentity(); //
重设视口
glTranslatef(0.0f,0.0f,-5.0f); //
进行平移变换
DrawQuads();
::SwapBuffers(m_pDC->GetSafeHdc());//
将图像缓存到DC
输出
return TRUE;
}
然后我们编译执行这个程序,在程序的中心处可以看到一个立方体,我们将这个立方体放在Z
轴为-5.0f
的地方,即屏幕内部。
我们可以对这个物体进行三种几何变换,包括:
平移变换
glTranslatef(0.0f,0.0f,-5.0f);
三个参数为平移距离
旋转变换
glRotatef(60.0f,1.0f,0.0f,0.0f)
第一个为角度,后三个为旋转轴
缩放变换
glScalef(1.0f,1.0f,1.0f) XYZ
轴方向上缩放
这些东西我们在后来的例子中都会试验到的。
虽然产生了一个立方体,不过后部的面看起来明显比前方的面大,看起来就像一个锥一样,唉,透视投影嘛。我们在上面的透视投影原理中可以看到,一个物体被投影时,前后面相同尺寸的物体投影后前后面的尺寸是不一样的,后部面的尺寸要大。
为了解决看起来前小后大的问题,我们需要做深度测试,进行消隐处理,即让前面的面挡住后面的面,在InitOpenGL
函数中,即初始化OpengGL
时候,添加下面的代码:
glEnable(GL_DEPTH_TEST); //
启用深度测试
glDepthFunc(GL_LEQUAL); //
所作深度测试的类型
//
高度优化的透视投影计算
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);
return TRUE;
}
再编译看看,就只有一个小小的红色面了。我们做的立方体是正确的嘛?旋转一下角度看看:
glTranslatef(0.0f,0.0f,-5.0f); //
进行平移变换
glRotatef(30.0f,1.0f,0.0f,0.0f); //
绕
X
轴旋转立方体
DrawQuads();
现在我们就可以看到一个三维矩形了,还不错。下面我们就添加一点效果出来看看:
在视图类中添加两个私有变量:
GLfloat m_yRotate;
GLfloat m_xRotate;
在视图类的构造函数中初始化变量:
m_xRotate=0.0f;
m_yRotate=0.0f;
在RenderScene
中修改函数:
glTranslatef(0.0f,0.0f,-5.0f); //
进行平移变换
glRotatef(m_xRotate,1.0f,0.0f,0.0f); //
旋转立方体
glRotatef(m_yRotate,0.0f,1.0f,0.0f);
DrawQuads();
修改OnTimer
函数:
void CCOpenGLDemoView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
m_xRotate+=0.2f;
m_yRotate+=0.2f;
InvalidateRect(NULL,FALSE);
CView::OnTimer(nIDEvent);
}
大家看懂原理了嘛?glRotatef
函数只是将物体在某个轴上转动一个角度,为了达到转动的目的,就需要不停转动,而不停转动可以在一个无限循环中实现。但是我们在OnCreate
中产生了一个Timer
进程,它能够每隔20ms
产生一个OnTimerr
事件,我们就可以利用这个事件来不断改变m_xRotate
和m_yRotate
的值,然后调用InvalidateRect
函数,触发OnDraw
事件,再次绘制图形,但是此时的图形的转动角度已经发生了变化,不断这般,看起来物体就运动起来了。
再绚一点吧,按+
键就转动加速,按-
键就转动减速。再添加两个私有变量:
GLfloat m_xRotateSpeed; //X
转动步进量
GLfloat m_yRotateSpeed; //Y
转动步进量
在视图类的构造函数中初始化:
m_xRotateSpeed=0.0f;
m_yRotateSpeed=0.0f;
添加一个KeyDown
事件,并编写代码:
void CCOpenGLDemoView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Add your message handler code here and/or call default
switch(nChar)
{
case VK_ADD:
m_xRotateSpeed+=0.2f;
m_yRotateSpeed+=0.2f;
break;
case VK_SUBTRACT:
m_xRotateSpeed-=0.2f;
m_yRotateSpeed-=0.2f;
break;
default:
break;
}
CView::OnKeyDown(nChar, nRepCnt, nFlags);
}
在OnTimer
事件中修改代码:
// TODO: Add your message handler code here and/or call default
m_xRotate+=m_xRotateSpeed;
m_yRotate+=m_yRotateSpeed;
InvalidateRect(NULL,FALSE);
好了,编译代码,执行程序,试着按+
和-
两个键,看看怎么样?四面体是不是会加速或减速转动。
下面我再演示一下如何更改视口,所谓视口,可以认为是我们用来观察物体的镜头,我们可以改变这个镜头的位置,其方法如下。
添加两个变量:
GLint m_xViewPort,m_yViewPort; //
视口左下角坐标
将OnSize
函数中的两个局部变量width
和height
变为视图类的变量:
GLsizei width,height; //
窗体的尺寸
在KeyDown
时间中添加以下情况的例子:
case VK_LEFT:
m_xViewPort+=2;
glViewport(m_xViewPort,m_yViewPort,width,height);
InvalidateRect(NULL,FALSE);
break;
case VK_RIGHT:
m_xViewPort-=2;
glViewport(m_xViewPort,m_yViewPort,width,height);
InvalidateRect(NULL,FALSE);
break;
case VK_UP:
m_yViewPort-=2;
glViewport(m_xViewPort,m_yViewPort,width,height);
InvalidateRect(NULL,FALSE);
break;
case VK_DOWN:
m_yViewPort+=2;
glViewport(m_xViewPort,m_yViewPort,width,height);
InvalidateRect(NULL,FALSE);
break;
这样我们在按下某个键的时候就会改变视口左下角的x
或y
坐标,而尺寸不变的情况下,就达到了移动视口的目的。此外我们还可以改变视口的大小,以达到放大或缩小立方体的目的,这个在放大地图中很有用。
我在代码中写上一个改变物体位置的代码,其实就是使用glTranslatef
函数就可以了。代码就不在这里解释了。
下面我们再做点改进工作,把RenderScene
中的:
glClearColor(0.0f,0.0f,0.0f,0.0f);//
清除屏幕颜色
glClearDepth(10.0f); //
清除深度缓存
放到InitOpenGL
函数中去,放在设置RC
语句之后,这是初始化工作,可以在初始化中进行。
谈完了这个例子,我想COPY
一些理论知识在这里,在真实世界里,所有的物体都是三维的。但是,这些三维物体在计算机世界中却必须以二维平面物体的形式表现出来。那么,这些物体是怎样从三维变换到二维的呢?下面我们采用相机(
Camera
)模拟的方式来讲述这个概念,如图所示:
实际上,从三维空间到二维平面,就如同用相机拍照一样,通常都要经历以下几个步骤
(括号内表示的是相应的图形学概念):
第一步,将相机置于三角架上,让它对准三维景物(视点变换, Viewing Transformation )。
第二步,将三维物体放在适当的位置(模型变换, Modeling Transformation )。
第三步,选择相机镜头并调焦,使三维物体投影在二维胶片上(投影变换, Projection Transformation )。
第四步,决定二维像片的大小(视口变换, Viewport Transformation )。
这样,一个三维空间里的物体就可以用相应的二维平面物体表示了,也就能在二维的电脑屏幕上正确显示了。
第一步,将相机置于三角架上,让它对准三维景物(视点变换, Viewing Transformation )。
第二步,将三维物体放在适当的位置(模型变换, Modeling Transformation )。
第三步,选择相机镜头并调焦,使三维物体投影在二维胶片上(投影变换, Projection Transformation )。
第四步,决定二维像片的大小(视口变换, Viewport Transformation )。
这样,一个三维空间里的物体就可以用相应的二维平面物体表示了,也就能在二维的电脑屏幕上正确显示了。
讲完了这些,还有两个东西我没有涉及到,一个是裁减视口,另一个是矩阵堆栈。
裁减视口是通过一个平面来减切视口,一个平面的方程是Ax+By+Cz+D=0
,而A B C D
就可以确定这个平面。Equation
就是ABCD
四个参数的数组。
void glClipPlane(GLenum plane,Const GLdouble *equation);
glEnable(GL_CLIP_PLANEi)
,
//
使减切生效
如果要减切失效,可以使用:
glDisable(GL_CLIP_PLANEi)
我在InitOpenGL
中添加以下代码,注意添加的地方:
glTranslatef(m_xPos,0.0f,-5.0f); //
进行平移变换
GLdouble eqn[4] = {1.0, 1.0, 1.0, -1.0};//
减切平面
glClipPlane(GL_CLIP_PLANE0, eqn); //
减切视口
glEnable(GL_CLIP_PLANE0); //
使减切视口生效
glRotatef(m_xRotate,1.0f,0.0f,0.0f); //
旋转立方体
看看结果:
就好像空中有个幕挡住了一样,其中平面方程是x+y+z-1=0
。(其实这个东西我也有点迷惑的地方,展且不表)
矩阵堆栈对复杂模型运动过程中的多个变换操作之间的联系与独立十分有利。因为所有矩阵操作函数如glLoadMatrix()
、glMultMatrix()
、glLoadIdentity()
等只处理当前矩阵或堆栈顶部矩阵,这样堆栈中下面的其它矩阵就不受影响。堆栈操作函数有以下两个:
void glPushMatrix(void);
void glPopMatrix(void);
第一个函数表示将所有矩阵依次压入堆栈中,顶部矩阵是第二个矩阵的备份;压入的矩阵数不能太多,否则出错。第二个函数表示弹出堆栈顶部的矩阵,令原第二个矩阵成为顶部矩阵,接受当前操作,故原顶部矩阵被破坏;当堆栈中仅存一个矩阵时,不能进行弹出操作,否则出错。由此看出,矩阵堆栈操作与压入矩阵的顺序刚好相反,编程时要特别注意矩阵操作的顺序。
为了更好地理解这两个函数,我们可以形象地认为glPushMatrix()
就是“
记住自己在哪”
,glPopMatrix()
就是“
返回自己原来所在地”
。

相关评论
