Win32 OpenGL编程(7) 3D视图变换——真3D的关键
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie
提要
本文从照相机比喻开始,引入视图变换的概念,然后讲解了视图变换的关键函数并用例子演示了参数变化时图形显示效果响应的动态变化,然后再补充了3D图形绘制时的多边形正面背面识别及处理。可算作是真正的3D入门。
照相机比喻
在《 OpenGL Programming Guide 》中将所有的3D变换统一到一个有意思的现实世界模型,照相机比喻。
1.确定照相机的位置的过程对应于“视图变换”(Viewing Transformations)
2.确定物体位置的过程对应于“模型变换”(Modeling Transformations)
3.确定照相机放大倍数的过程对应于“投影变换”(Projection Transformations)
4.确定照片大小的过程对应于“视口变换”(Viewport Transformations)
实际的照相过程遵循这个过程,在我们处理3D图形的时候也遵循这个过程,其中,上述的4个变换就是我今天准备介绍的。
视图变换——确定视角
现实生活中的物体,从不同的角度观察,我们看到的东西是不一样的,这还引发了历史上著名的“金银盾事件”(实际上是寓言-_-!),从一面看,是金盾,一面看是银盾,从中间看是一面金,一面银。。。。。。
以《 Win32 OpenGL编程(6) 踏入3D世界 》(以后简称XO6,该系列文章类似)一例中最后最复杂的三角锥为例,此例中是三角锥本身在旋转,我们的观察角度并没有变。现在我们反过来,三角锥不动,我们自己移动自己的位置,看看三角锥不同方向的样子。现实生活中你看雕塑可不是总能让别人扛着雕塑旋转吧-_-!这时候,总得自己走动走动。这个时候,我们变化的是观察的角度,但是看到的确是雕塑不同的侧面。见下例,我们可以从不同的观察角度来观察这个简单的三角锥。见下例:
// 观察者位置
GLfloat gViewPosX;
GLfloat gViewPosY;
GLfloat gViewPosZ = 1.0;
// 观察者视角方向
GLfloat gViewDirX = 0.0;
GLfloat gViewDirY = 0.0;
GLfloat gViewDirZ = 0.0;
GLfloat gViewUpDirX = 0.0;
GLfloat gViewUpDirY = 1.0;
GLfloat gViewUpDirZ = 0.0;
// 是改变位置还是视角
bool gbChangePos = true;
//这里进行所有的绘图工作
void SceneShow(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 0.0, 0.0);
glPushMatrix();
DrawSmoothColorPyramid(0.5);
glPopMatrix();
glLoadIdentity();
gluLookAt(gViewPosX, gViewPosY, gViewPosZ, gViewDirX, gViewDirY, gViewDirZ, gViewUpDirX, gViewUpDirY, gViewUpDirZ);
glFlush();
}
///////////////////////////////////////////////////////////
int Game_Main( void *parms = NULL, int num_parms = 0)
{
DWORD dwStartTime;
dwStartTime = GetTickCount();
// this is the main loop of the game, do all your processing
// here
// for now test if user is hitting ESC and send WM_CLOSE
if (KEYDOWN(VK_ESCAPE))
SendMessage(ghWnd,WM_CLOSE,0,0);
if (gbChangePos)
{
if (KEYDOWN(VK_UP))
{
gViewPosY += 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosY * gViewPosY);
}
if (KEYDOWN(VK_DOWN))
{
gViewPosY -= 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosY * gViewPosY);
}
if (KEYDOWN(VK_LEFT))
{
gViewPosX += 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosX * gViewPosX);
}
if (KEYDOWN(VK_RIGHT))
{
gViewPosX -= 0.01;
gViewPosZ = sqrt( 1.0 - gViewPosX * gViewPosX);
}
}
else
{
if (KEYDOWN(VK_UP))
{
gViewDirY += 0.01;
}
if (KEYDOWN(VK_DOWN))
{
gViewDirY -= 0.01;
}
if (KEYDOWN(VK_LEFT))
{
gViewDirX += 0.01;
}
if (KEYDOWN(VK_RIGHT))
{
gViewDirX -= 0.01;
}
}
if (KEYDOWN(VK_NUMPAD8))
{
gViewUpDirY += 0.01;
}
if (KEYDOWN(VK_NUMPAD2))
{
gViewUpDirY -= 0.01;
}
if (KEYDOWN(VK_NUMPAD6))
{
gViewUpDirX += 0.01;
}
if (KEYDOWN(VK_NUMPAD4))
{
gViewUpDirX -= 0.01;
}
SceneShow();
// 控制帧率
while (GetTickCount() - dwStartTime < TIME_IN_FRAME)
{
Sleep(1);
}
// return success or failure or your own return code here
return (1);
} // end Game_Main
gluLookAt — define a viewing transformation
C Specification
void gluLookAt( GLdouble eyeX,
GLdouble eyeY,
GLdouble eyeZ,
GLdouble centerX,
GLdouble centerY,
GLdouble centerZ,
GLdouble upX,
GLdouble upY,
GLdouble upZ);
ParameterseyeX, eyeY, eyeZ
Specifies the position of the eye point.
centerX, centerY, centerZSpecifies the position of the reference point.
upX, upY, upZSpecifies the direction of the up vector.
此例中,通过全局变量保存观察者的位置,并通过上下左右键改变方向,以从不同的角度观察三角锥,作为演示,我开始时锁定了观察的方向,一直是朝向观察的三角锥的,并且通过计算,让观察者总是保持与观察物品的距离相等,这样最能看到观察位置改变带来的效果,但是目前只实现了在物体正面时的方向改变,当越过正面后,由于象限的改变,实际上增加的数值需要编程减小,然后将另外一个辅助变量的正负号更改。比如,当一直向上按时,Y坐标不停增长,但是到Y=1.0的时候就需要变成减小了,Z的坐标也变成了负值,因为此时已经到了物体的后方。
当按下INSERT键后,再按上下左右改变的就是看哪里的方向了,尝试一下会发现当观察方向向右时,物体向左移动,直到移出屏幕以外,这点很像你开始面对着屏幕,然后不停的将头向右偏,那么屏幕也就向左移动,你头偏的角度足够大的时候,已经就看不见屏幕了。
而当通过小键盘的8426方向控制时,改变的是观察的向上的角度,就像你看屏幕时偏着脑袋看一样,具体的演示效果就大家自己去看了,这里提供一个截图
还是那句老话,为节省篇幅仅贴出关键片段,完整源代码见我博客源代码的2009-10-25/glViewingTrans 目录,获取方式见文章最后关于获取博客完整源代码的说明。
此例强烈建议通过运行程序去感受一下,也算是体会在游戏中3D的转换是怎么回事儿,其实也就是这么回事儿。既然都已经是3D图形了,那为什么有的游戏要限制你的观察角度呢?还冒出了2.5D,2.8D等新鲜词汇(其实我也不太懂怎么命名的),但是学了视图变换后就会明白,限制角度对程序处理的简化作用了,就像我此处一样,虽然已经是个真正的3D三角锥了,我还是没有提供720度的随意旋转,因为当移动到物体的另一面时,处理的方式有些改变,老是变来变去会比较麻烦,要知道,2D平面有4个象限,而3D呢?8个象限,可不少了,每个象限在视图变换时的计算方式可是不太一样的。我的程序因此而简化了很多,而那些2.XD的游戏程序自然简化的更多罗。
多边形表面显示方式
在确定了视角后,会发现原来的三角锥有问题了,背面都显示出来了-_-!见上面的截图,这个可是不行的,问题出在我们一没有告诉OpenGL那个面是背面,那个面是正面,而且OpenGL默认的现实方式是不管正面背面一样处理,自然我们得告诉OpenGL按照我们想要处理的方式处理才行,比如,我们想要正面显示,背面不显示(这是多么正常的需求啊),那么我们可以通过glPolygonMode函数指明。
glPolygonMode — select a polygon rasterization mode
C Specification
void glPolygonMode( GLenum face,
GLenum mode);
Parametersface
Specifies the polygons that mode applies to.
Must be
GL_FRONT for front-facing polygons,
GL_BACK for back-facing polygons,
or GL_FRONT_AND_BACK for front- and back-facing polygons.
modeSpecifies how polygons will be rasterized.
Accepted values are
GL_POINT,
GL_LINE, and
GL_FILL.
The initial value is GL_FILL for both front- and back-facing polygons.
要达到我们的要求只需要这样调用此函数:
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
这样指示以后,背面就不会填充了,但是,OpenGL怎么知道哪个是正面,哪个是背面呢?通过glFrontFace可以指定,
glFrontFace — define front- and back-facing polygons
C Specification
void glFrontFace( GLenum mode);
Parametersmode
Specifies the orientation of front-facing polygons.
GL_CW and GL_CCW are accepted.
The initial value is GL_CCW.
OpenGL自然不是傻到要你具体一个一个面去指定哪个面是正面,哪个面是方面,它是通过顶点绘制的方向来决定的,OpenGL通过此函数指定了什么方向时多边形表示正反面,默认是是逆时针为正面,顺时针为方面,想想这样的好处,指定起来还是方便点,绘制顶点的时候考虑好就行了,不用额外再次指定,然后,的确还有效,正面看的是逆时针的东西,到了反面还真是顺时针-_-!(废话)
如下例所示:
void DrawSmoothColorPyramid(GLfloat adSize)
{
static GLfloat fPyramidDatas[] = { 0.0, 1.0, 0.0, // 三角锥上顶点
-1.0, 0.0, 1.0, // 底面左前顶点
1.0, 0.0, 1.0, // 底面右前下顶点
0.0, 0.0, -1.0}; // 底面后下顶点
GLfloat fPyramidSizeDatas[ sizeof (fPyramidDatas)/ sizeof (GLfloat)] = {0};
// 计算大小
for ( int i = 0; i < 12; ++i)
{
fPyramidSizeDatas[i] = fPyramidDatas[i] * adSize;
}
static GLfloat fPyramidColors[] = { 0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0};
//static GLubyte ubyIndices[] = { 0, 1, 2, // 正面
// 0, 1, 3, // 左侧面
// 0, 2, 3, // 右侧面
// 1, 2, 3}; // 底面
static GLubyte ubyIndices[] = { 0, 1, 2, // 正面
0, 3, 1, // 左侧面
0, 2, 3, // 右侧面
1, 3, 2}; // 底面
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, fPyramidSizeDatas);
glColorPointer(3, GL_FLOAT, 0, fPyramidColors);
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
for ( int i = 0; i < 4; ++i)
{
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_BYTE, ubyIndices+i*3);
}
}
为节省篇幅仅贴出关键片段,完整源代码见我博客源代码的2009-10-25/glPolygonFace 目录,获取方式见文章最后关于获取博客完整源代码的说明。
多边形表面剔除
另外,事实上,我们这里是指定了背面绘制轮廓(即GL_LINE),但是既然是背面,为啥我们一定要轮廓呢?完全可以剔除之,glCullFace就是干这个的。
glCullFace — specify whether front- or back-facing facets can be culled
C Specification
void glCullFace( GLenum mode);
Parametersmode
Specifies whether front- or back-facing facets are candidates for culling.
Symbolic constants
GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK are accepted.
The initial value is GL_BACK.
既然背面都指定好了,此函数使用起来就非常简单了,无非就是调用glCullFace(GL_BACK)表示剔除背面的多边形(虽然你可以剔除正面或者全部都剔除了),并且,需要注意的是,剔除时需要用glEnable(GL_CULL_FACE)开启剔除功能。为了速度,像这种功能OpenGL一般都是默认关闭的,我们也习惯了。需要添加的代码实在也就简单了:
//OpenGL初始化开始
void SceneInit( int w, int h)
{
GLenum err = glewInit();
if (err != GLEW_OK)
{
MessageBox(NULL, _T( "Error" ), _T( "Glew init failed." ), MB_OK);
exit(-1);
}
glClearColor(0.0, 0.0, 0.0, 0.0);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
但是,需要说明的是,背面剔除后,在转换视角,到背面变成正面的那一瞬间,会需要重新渲染背面,此时效率较低,事实上,这么一个简单的程序,再没有开启背面剔除时,从正面到背面的转换非常流畅,开启后,转换会有明显的小小迟缓,但是好处就是背面没有需要显示的时候是不占用资源的了,孰优孰劣,开启与否,那就只能看情况把握了。
为节省篇幅仅贴出关键片段,完整源代码见我博客源代码的2009-10-25/glCullFace 目录,获取方式见文章最后关于获取博客完整源代码的说明。
参考资料
1. 《 OpenGL Reference Manual 》,OpenGL参考手册
2. 《OpenGL 编程指南》(《 OpenGL Programming Guide 》),Dave Shreiner,Mason Woo,Jackie Neider,Tom Davis 著,徐波译,机械工业出版社
3. 《Nehe OpenGL Tutorials》,Nehe著,在 http://nehe.gamedev.net/ 上可以找到教程及相关的代码下载,(有PDF版本教程下载)Nehe自己还做了一个面向对象的框架,作为演示程序来说,这样的框架非常合适。也有 中文版 ,各取所需吧。
4. 《OpenGL入门学习》 ,eastcowboy著,这是我在网上找到的一个比较好的教程,较为完善,而且非常通俗。这是第一篇的地址: http://bbs.pfan.cn/post-184355.html
本系列下一篇《 Win32 OpenGL编程(8) 3D模型变换及其组合应用 》
本OpenGL系列其他文章
1. 《 Win32 OpenGL 编程(1)Win32下的OpenGL编程必须步骤 》
2. 《 Win32 OpenGL编程(2) 寻找缺失的OpenGL函数 》
3. 《 Win32 OpenGL编程(3) 基本图元(点,直线,多边形)的绘制 》
4. 《 Win32 OpenGL编程(4) 2D图形基础(颜色及坐标体系进阶知识) 》
5. 《 Win32 OpenGL编程(5)顶点数组详细介绍 》
6.《 Win32 OpenGL编程(6) 踏入3D世界 》
应用举例:《 Win32 OpenGL编程系列 2D例子 -- 七巧板图形绘制 》
完整源代码获取说明
由于篇幅限制,本文一般仅贴出代码的主要关心的部分,代码带工程(或者makefile)完整版(如果有的话)都能用Mercurial在Google Code中下载。文章以博文发表的日期分目录存放,请直接使用Mercurial克隆下库:
https://blog-sample-code.jtianling.googlecode.com/hg/
Mercurial使用方法见《 分布式的,新一代版本控制系统Mercurial的介绍及简要入门 》
要是仅仅想浏览全部代码也可以直接到google code上去看,在下面的地址:
http://code.google.com/p/jtianling/source/browse?repo=blog-sample-code
原创文章作者保留版权 转载请注明原作者 并给出链接
write by 九天雁翎(JTianLing) -- blog.csdn.net/vagrxie