一、现象
在OpenGL中先移动后旋转与先旋转后移动的最终效果是并不一定相同的,也就是说在Opengl中如果调用函数glTranslatef和函数glRotatef的次序不同,即使参数一样,效果也可能会不同。下面我们通过两段程序说明该问题。
二、代码
如下所示,在vs2015中新建Win32程序:
输入以下代码:
#include <windows.h>
#include <gl/GL.h>
#include <gl/GLU.h>
#pragma comment(lib,"opengl32.lib")
#pragma comment(lib,"glu32.lib")
//窗口处理函数(回调函数)
LRESULT CALLBACK GLWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM IParam)
{
switch (msg)
{
case WM_CLOSE:
{
PostQuitMessage(0); //该函数向系统表明有个线程有终止请求。如果没有该语句,鼠标点击程序窗口右上方的关闭按钮时会无法关闭。
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, IParam); //该函数确保每一个消息得到处理。如果没有该语句,窗口会卡死。
}
void Init()
{
glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵,声明我们接下来会对投影进行相关的操作。也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投到2维的平面上。这样,接下来的语句可以是跟透视相关的函数,比如glFrustum()或gluPerspective()
gluPerspective(50.0f, 800.0f / 600.0f, 0.1f, 1000.0f); //根据所设置的参数设置当前矩阵。设置垂直方向的视角为50度,画布宽高比为800.0f / 600.0f,最近可以看到的距离为0.1f,最远可以看到的距离为1000.0f
glMatrixMode(GL_MODELVIEW); //切换到模型视图矩阵,这样才能正确画图
glLoadIdentity(); //对当前矩阵进行初始化。无论以前进行了多少次矩阵变换,在该命令执行后,当前矩阵均恢复成一个单位矩阵,即相当于没有进行任何矩阵变换状态
}
void Draw()
{
glClearColor(30.0f / 255.0f, 30.0f / 255.0f, 30.0f / 255.0f, 1.0f); //指定刷新颜色缓冲区时所用的颜色,设置窗口背景颜色为R:0%,G:0%,B:0%,A:100%。切记:此函数仅仅设定颜色,并不执行清除工作
glClear(GL_COLOR_BUFFER_BIT); //清除颜色缓冲。实际完成了把整个窗口清除glClearColor函数设置的颜色的任务
glLoadIdentity(); //对当前矩阵进行初始化
glTranslatef(0.0f, 0.0f, -5.0f); //修改当前的坐标系统,使接下来绘制的所有图形都沿X轴正方向平移0个单位,沿Y轴正方向平移0个单位,沿Z轴正方向平移-5个单位
glRotatef(30.0f, 0.0f, 1.0f, 0.0f); //修改当前的坐标系统(注意,修改的是局部坐标系统而不是世界坐标系统),使接下来绘制的所有图形都沿Y轴旋转30度
glBegin(GL_TRIANGLES); //绘制一个三角形
glColor4ub(255, 0, 0, 255); glVertex3f(-0.5f, -0.25f, 0.0f);
glColor4ub(0, 0, 255, 255); glVertex3f(0.5f, -0.25f, 0.0f);
glColor4ub(0, 255, 0, 255); glVertex3f(0.0f, 0.5f, 0.0f);
glEnd(); //表示此次绘图完成了
}
INT WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
WNDCLASSEX wndclass; //定义窗口类结构体变量wndclass
wndclass.cbClsExtra = 0; //为窗口类的额外信息做记录,初始化为0
wndclass.cbSize = sizeof(WNDCLASSEX); //WNDCLASSEX 的大小
wndclass.cbWndExtra = 0; //记录窗口实例的额外信息,系统初始为0
wndclass.hbrBackground = NULL; //窗口类的背景刷,为背景刷句柄
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); //窗口类的鼠标样式,为鼠标样式资源的句柄
wndclass.hIcon = NULL; //窗口类的图标,为资源句柄,如果设置为NULL,系统将为窗口提供一个默认的图标
wndclass.hIconSm = NULL; //小图标的句柄,在任务栏显示的图标
wndclass.hInstance = hInstance; //本模块的事例句柄
wndclass.lpfnWndProc = GLWindowProc; //指向窗口处理函数(回调函数)。处理窗口事件,像单击鼠标会怎样,右击鼠标会怎样,都是由此函数控制的。
wndclass.lpszClassName = L"GLWindow"; //注册窗口使用的窗口名称
wndclass.lpszMenuName = NULL; //菜单名称,为NULL则为没有菜单
wndclass.style = CS_VREDRAW | CS_HREDRAW; //窗口更新时的重绘方式
ATOM atom = RegisterClassEx(&wndclass); //注册窗口wndclass
if (!atom) //如果注册失败,显示提示框
{
MessageBox(NULL, L"Register Fail", L"Error", MB_OK);
return 0;
}
RECT rect;
rect.left = 0; //指定矩形框左上角的x坐标
rect.right = 800; //指定矩形框右下角的x坐标
rect.top = 0; //指定矩形框左上角的y坐标
rect.bottom = 600; //指定矩形框右下角的y坐标
AdjustWindowRect(&rect,WS_OVERLAPPEDWINDOW,NULL); //用于创建一个客户区所需大小的窗口
int windowWidth = rect.right - rect.left; //得到客户区的宽度
int windowHeight = rect.bottom - rect.top; //得到客户区的高度
HWND hwnd = CreateWindowEx(NULL, L"GLWindow", L"OpenGL Window", WS_OVERLAPPEDWINDOW, 100, 100, windowWidth, windowHeight, NULL, NULL, hInstance, NULL);
HDC dc = GetDC(hwnd); //为一个指定窗口的客户端区域或者整个屏幕从一个设备上下文(DC)中提取一个句柄。返回值为指定窗口客户端区域的DC的句柄
PIXELFORMATDESCRIPTOR pfd; //像素格式结构变量
memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR)); //将pfd的所有成员变量清0
pfd.nVersion = 1; //这里固定设置为1,不要管为什么,微软也没有解释为什么。
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); //pfd的大小
pfd.cColorBits = 32; //32位的颜色深度。即一个像素占4个字节,R占8位,G占8位,B占8位,A占8位
pfd.cDepthBits = 24; //深度缓存设置为24位,这个缓存能解决三维场景的消隐问题
pfd.cStencilBits = 8; //模板缓冲区占用8位
pfd.iPixelType = PFD_TYPE_RGBA; //当前采用RGBA颜色模式
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; //支持windows,支持opengl,支持双缓冲区
int pixelFormat = ChoosePixelFormat(dc, &pfd); //选择一个像素格式,并将像素格式索引号返回给pixelFormat变量
SetPixelFormat(dc, pixelFormat, &pfd); //设置像素格式
HGLRC rc = wglCreateContext(dc); //创建一个新的OpenGL渲染描述表
wglMakeCurrent(dc, rc); //使一个指定的OpenGL渲染上下文调用线程的当前呈现上下文
Init();
ShowWindow(hwnd, SW_SHOW); //该函数设置指定窗口的显示状态。如果没有该语句,执行程序时窗口不会显示
UpdateWindow(hwnd); //绕过消息队列(不进队),直接向窗口客户区发送WM_PAINT消息,使得窗口立即更新
MSG msg;
while (true)
{
if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) //该函数为一个消息检查线程消息队列,并将该消息(如果存在)放于指定的结构
{
if (msg.message == WM_QUIT) //如果关闭消息循环
{
break; //如果没有该语句,鼠标点击程序窗口右上方的关闭按钮时会无法关闭。
}
TranslateMessage(&msg); //将虚拟键消息转换成字符消息
DispatchMessage(&msg); //该函数分发一个消息给窗口程序。如果没有该语句,无法拖动窗口和通过鼠标对窗口进行放大,最小化,关闭等操作。
}
Draw(); //用于画图元。在while(true)的循环中每一次循环都调用Draw函数绘制一次场景。该函数绘制是绘制在后面的缓冲区上的,所以绘制完后,得调用函数SwapBuffers将它交换到前面的缓冲区,这样用户才能看到绘制的东西。
SwapBuffers(dc); //交换opengl前后的两个缓冲区。一个对应的是前面的屏幕的缓存,一个对应的是后面的缓存,之所以用交换的方式,是内部进行了指针的交换,如此速度很快
}
return 0;
}
上述程序的作用是先将坐标系统(局部坐标系统)沿Z轴负方向平移5个单位,然后沿Y轴旋转30度,再绘制一个三角形。编译运行,我们可以得到三角形如下所示:
我们将vs中的代码换成代码如下所示:
#include <windows.h>
#include <gl/GL.h>
#include <gl/GLU.h>
#pragma comment(lib,"opengl32.lib")
#pragma comment(lib,"glu32.lib")
//窗口处理函数(回调函数)
LRESULT CALLBACK GLWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM IParam)
{
switch (msg)
{
case WM_CLOSE:
{
PostQuitMessage(0); //该函数向系统表明有个线程有终止请求。如果没有该语句,鼠标点击程序窗口右上方的关闭按钮时会无法关闭。
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, IParam); //该函数确保每一个消息得到处理。如果没有该语句,窗口会卡死。
}
void Init()
{
glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵,声明我们接下来会对投影进行相关的操作。也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投到2维的平面上。这样,接下来的语句可以是跟透视相关的函数,比如glFrustum()或gluPerspective()
gluPerspective(50.0f, 800.0f / 600.0f, 0.1f, 1000.0f); //根据所设置的参数设置当前矩阵。设置垂直方向的视角为50度,画布宽高比为800.0f / 600.0f,最近可以看到的距离为0.1f,最远可以看到的距离为1000.0f
glMatrixMode(GL_MODELVIEW); //切换到模型视图矩阵,这样才能正确画图
glLoadIdentity(); //对当前矩阵进行初始化。无论以前进行了多少次矩阵变换,在该命令执行后,当前矩阵均恢复成一个单位矩阵,即相当于没有进行任何矩阵变换状态
}
void Draw()
{
glClearColor(30.0f / 255.0f, 30.0f / 255.0f, 30.0f / 255.0f, 1.0f); //指定刷新颜色缓冲区时所用的颜色,设置窗口背景颜色为R:0%,G:0%,B:0%,A:100%。切记:此函数仅仅设定颜色,并不执行清除工作
glClear(GL_COLOR_BUFFER_BIT); //清除颜色缓冲。实际完成了把整个窗口清除glClearColor函数设置的颜色的任务
glLoadIdentity(); //对当前矩阵进行初始化
glRotatef(30.0f, 0.0f, 1.0f, 0.0f); //修改当前的坐标系统(注意,修改的是局部坐标系统而不是世界坐标系统),使接下来绘制的所有图形都沿Y轴旋转30度
glTranslatef(0.0f, 0.0f, -5.0f); //修改当前的坐标系统,使接下来绘制的所有图形都沿X轴正方向平移0个单位,沿Y轴正方向平移0个单位,沿Z轴正方向平移-5个单位
glBegin(GL_TRIANGLES); //绘制一个三角形
glColor4ub(255, 0, 0, 255); glVertex3f(-0.5f, -0.25f, 0.0f);
glColor4ub(0, 0, 255, 255); glVertex3f(0.5f, -0.25f, 0.0f);
glColor4ub(0, 255, 0, 255); glVertex3f(0.0f, 0.5f, 0.0f);
glEnd(); //表示此次绘图完成了
}
INT WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{
WNDCLASSEX wndclass; //定义窗口类结构体变量wndclass
wndclass.cbClsExtra = 0; //为窗口类的额外信息做记录,初始化为0
wndclass.cbSize = sizeof(WNDCLASSEX); //WNDCLASSEX 的大小
wndclass.cbWndExtra = 0; //记录窗口实例的额外信息,系统初始为0
wndclass.hbrBackground = NULL; //窗口类的背景刷,为背景刷句柄
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); //窗口类的鼠标样式,为鼠标样式资源的句柄
wndclass.hIcon = NULL; //窗口类的图标,为资源句柄,如果设置为NULL,系统将为窗口提供一个默认的图标
wndclass.hIconSm = NULL; //小图标的句柄,在任务栏显示的图标
wndclass.hInstance = hInstance; //本模块的事例句柄
wndclass.lpfnWndProc = GLWindowProc; //指向窗口处理函数(回调函数)。处理窗口事件,像单击鼠标会怎样,右击鼠标会怎样,都是由此函数控制的。
wndclass.lpszClassName = L"GLWindow"; //注册窗口使用的窗口名称
wndclass.lpszMenuName = NULL; //菜单名称,为NULL则为没有菜单
wndclass.style = CS_VREDRAW | CS_HREDRAW; //窗口更新时的重绘方式
ATOM atom = RegisterClassEx(&wndclass); //注册窗口wndclass
if (!atom) //如果注册失败,显示提示框
{
MessageBox(NULL, L"Register Fail", L"Error", MB_OK);
return 0;
}
RECT rect;
rect.left = 0; //指定矩形框左上角的x坐标
rect.right = 800; //指定矩形框右下角的x坐标
rect.top = 0; //指定矩形框左上角的y坐标
rect.bottom = 600; //指定矩形框右下角的y坐标
AdjustWindowRect(&rect,WS_OVERLAPPEDWINDOW,NULL); //用于创建一个客户区所需大小的窗口
int windowWidth = rect.right - rect.left; //得到客户区的宽度
int windowHeight = rect.bottom - rect.top; //得到客户区的高度
HWND hwnd = CreateWindowEx(NULL, L"GLWindow", L"OpenGL Window", WS_OVERLAPPEDWINDOW, 100, 100, windowWidth, windowHeight, NULL, NULL, hInstance, NULL);
HDC dc = GetDC(hwnd); //为一个指定窗口的客户端区域或者整个屏幕从一个设备上下文(DC)中提取一个句柄。返回值为指定窗口客户端区域的DC的句柄
PIXELFORMATDESCRIPTOR pfd; //像素格式结构变量
memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR)); //将pfd的所有成员变量清0
pfd.nVersion = 1; //这里固定设置为1,不要管为什么,微软也没有解释为什么。
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR); //pfd的大小
pfd.cColorBits = 32; //32位的颜色深度。即一个像素占4个字节,R占8位,G占8位,B占8位,A占8位
pfd.cDepthBits = 24; //深度缓存设置为24位,这个缓存能解决三维场景的消隐问题
pfd.cStencilBits = 8; //模板缓冲区占用8位
pfd.iPixelType = PFD_TYPE_RGBA; //当前采用RGBA颜色模式
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; //支持windows,支持opengl,支持双缓冲区
int pixelFormat = ChoosePixelFormat(dc, &pfd); //选择一个像素格式,并将像素格式索引号返回给pixelFormat变量
SetPixelFormat(dc, pixelFormat, &pfd); //设置像素格式
HGLRC rc = wglCreateContext(dc); //创建一个新的OpenGL渲染描述表
wglMakeCurrent(dc, rc); //使一个指定的OpenGL渲染上下文调用线程的当前呈现上下文
Init();
ShowWindow(hwnd, SW_SHOW); //该函数设置指定窗口的显示状态。如果没有该语句,执行程序时窗口不会显示
UpdateWindow(hwnd); //绕过消息队列(不进队),直接向窗口客户区发送WM_PAINT消息,使得窗口立即更新
MSG msg;
while (true)
{
if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) //该函数为一个消息检查线程消息队列,并将该消息(如果存在)放于指定的结构
{
if (msg.message == WM_QUIT) //如果关闭消息循环
{
break; //如果没有该语句,鼠标点击程序窗口右上方的关闭按钮时会无法关闭。
}
TranslateMessage(&msg); //将虚拟键消息转换成字符消息
DispatchMessage(&msg); //该函数分发一个消息给窗口程序。如果没有该语句,无法拖动窗口和通过鼠标对窗口进行放大,最小化,关闭等操作。
}
Draw(); //用于画图元。在while(true)的循环中每一次循环都调用Draw函数绘制一次场景。该函数绘制是绘制在后面的缓冲区上的,所以绘制完后,得调用函数SwapBuffers将它交换到前面的缓冲区,这样用户才能看到绘制的东西。
SwapBuffers(dc); //交换opengl前后的两个缓冲区。一个对应的是前面的屏幕的缓存,一个对应的是后面的缓存,之所以用交换的方式,是内部进行了指针的交换,如此速度很快
}
return 0;
}
上述程序的作用是先将坐标系统(局部坐标系统)沿Y轴旋转30度,然后沿Z轴负方向平移5个单位,再绘制一个三角形。编译运行,我们得到三角形如下所示:
三、原因分析
通过对比,我们可以发现两段代码运行得到的三角形不同。两段代码中不同的地方只有平移和旋转的顺序,其它地方都是相同的,但是得到的三角形却是不同。造成该现象的原因是调用函数glTranslatef和函数glRotatef,会令局部坐标系也跟着平移和旋转,而不仅仅只是平移和旋转物体,平移和旋转针对的都是局部坐标系的操作。从矩阵变换的角度去考虑这个问题就是两个矩阵交换位置相乘结果不一样,从而导致了该现象。从数学的角度去分析,我们可以参考链接:https://jingyan.baidu.com/article/414eccf617a9c66b421f0a5e.html
四、代码下载
本博文演示所用的代码和vs工程可以从该链接下载https://download.youkuaiyun.com/download/u014552102/10993374