第一课.创建一个OpenGL窗口
创建一个空的OpenGL窗口,关键在于在MFC下配置OpenGL的环境.参考网上的一些配置,主要有三个步骤:设置窗口像素格式;产生RC;设置为当前RC.
1.创建一个空的MFC对话框工程,所有选项都为默认值.
2.与教程中相同,将OpenGL32.lib GLu32.lib 和 GLaux.lib 加入到工程中,方法:在Project->Settings->Link->Object/library modules中输入OpenGL32.lib GLu32.lib GLaux.lib,不要忘记空格.
3.删除对话框资源中的所有按钮,控件.这样对话框看起来比较干净.
4.在StAfx.h文件中增加#include <glew.h> #include<glut.h>包含.这里不需要windows.h,因为afxwin.h中已经包含了windows.h.
至此,对工程的设置以及库的引用都设置完毕.然后根据前面的三个步骤来对OpenGL进行初始化.首先在dlg.h文件中增加以下变量
HDC hDC; // OpenGL渲染描述表句柄
1.设置窗口像素格式 增加函数SetWindowPixelFormat(HDC m_hDc),函数代码如下:
... {
static PIXELFORMATDESCRIPTOR pfd= // /pfd 告诉窗口我们所希望的东东,即窗口使用的像素格式
...{
sizeof(PIXELFORMATDESCRIPTOR), // 上述格式描述符的大小
1, // 版本号
PFD_DRAW_TO_WINDOW | // 格式支持窗口
PFD_SUPPORT_OPENGL | // 格式必须支持OpenGL
PFD_DOUBLEBUFFER, // 必须支持双缓冲
PFD_TYPE_RGBA, // 申请 RGBA 格式
24, // 选定色彩深度
0, 0, 0, 0, 0, 0, // 忽略的色彩位
0, // 无Alpha缓存
0, // 忽略Shift Bit
0, // 无累加缓存
0, 0, 0, 0, // 忽略聚集位
16, // 16位 Z-缓存 (深度缓存)
0, // 无蒙板缓存
0, // 无辅助缓存
PFD_MAIN_PLANE, // 主绘图层
0, // Reserved
0, 0, 0 // 忽略层遮罩
};
int PixelFormat = 0;
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Windows 找到相应的象素格式了吗?
...{
MessageBox("不能设置像素格式","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // 能够设置象素格式么?
...{
MessageBox("不能设置像素格式","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
return TRUE;
}
响应WM_CREATE消息,在OnCreate函数中加入
hDC = ::GetDC(hWnd1);
if ( ! SetWindowPixelFormat(hDC))
... {
return 0 ;
}
2.产生RC
在OnCreate函数中加入
... {
MessageBox("不能创建OpenGL渲染描述表","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
3.设置当前RC
在OnCreate函数中加入
... {
MessageBox("不能激活当前的OpenGL渲然描述表","错误",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
然后,响应WM_SIZE消息,当窗口大小改变时,改变视口大小.
... {
CDialog::OnSize(nType, cx, cy);
// TODO: Add your message handler code here
GLsizei width,height;
width = cx;
height = cy;
if (cy==0) // 防止被零除
...{
height=1; // 将Height设为1
}
glViewport(0,0, width, height); // 重置当前的视口
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
// 设置视口的大小
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW); // 选择模型观察矩阵
glLoadIdentity(); // 重置模型观察矩阵
}
响应WM_DESTROY消息,当窗口关闭时释放HRC与HDC的关联.在这里,大家很明白了.其实所谓的OpenGL的初始化,或者是设置,就是将OpenGL帮定到窗口的绘图区,这样当你进行绘制的时候就知道你要在那里进行绘制了.
... {
CDialog::OnDestroy();
if (hRC) // 我们拥有OpenGL渲染描述表吗?
...{
if (!wglMakeCurrent(NULL,NULL)) // 我们能否释放DC和RC描述表?
...{
MessageBox("释放DC或RC失败。","关闭错误",MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC)) // 我们能否删除RC?
...{
MessageBox("释放RC失败。","关闭错误",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL; // 将RC设为 NULL
}
if (hDC && !::ReleaseDC(this->GetSafeHwnd(),hDC)) // 我们能否释放 DC?
...{
MessageBox("释放DC失败。","关闭错误",MB_OK | MB_ICONINFORMATION);
hDC=NULL; // 将 DC 设为 NULL
}
}
然后,参照NE_HE教程,就可以进行OpenGL的绘制了,将NE_HE的教程移植如下:
初始化函数:
... {
glShadeModel(GL_SMOOTH); // 启用阴影平滑
glClearColor(1.0f, 0.0f, 0.0f, 0.0f); // 黑色背景
glClearDepth(1.0f); // 设置深度缓存
glEnable(GL_DEPTH_TEST); // 启用深度测试
glDepthFunc(GL_LEQUAL); // 所作深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 告诉系统对透视进行修正
return TRUE; // 初始化 OK
}
绘制函数:
... {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存
glLoadIdentity(); // 重置当前的模型观察矩阵
SwapBuffers(hDC);
return TRUE; // 一切 OK
}
这里小鸟也有个疑问,希望高手能帮我解答.为什么我起用新线程绘制的方法,如果把SwapBuffers(hDC)放在绘制线程中,我用FRAPS工具查看,他不会进行重绘.不知道是
在OnCreate函数中调用InitGL函数,在OnPaint函数中调用DrawGLScene函数,如下:
... {
if (IsIconic())
...{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
...{
DrawGLScene();
CDialog::OnPaint();
}
}
最后,就是对场景进行循环绘制了.很多人在MFC中进行循环绘制的时候都喜欢用OnTimer函数定时绘制.虽然方便,但OnTimer函数的精度只有毫秒,对于大型场景的绘制来说,特别是大型3D游戏,这种精度是很不够的.所以在这里,我还是启用一个绘制线程,进行循环绘制.
循环绘制线程:
... {
CLesson1Dlg* lDlg = (CLesson1Dlg*)lparam;
while (!lDlg->GetSafeHwnd())
...{
}
while (TRUE)
...{
lDlg->Invalidate(FALSE);
}
return 0;
}
这里小鸟也有个疑问,希望高手能帮我解答.为什么把SwapBuffers(hDC)放在绘制线程中,我用FRAPS工具查看,他不会进行重绘.所以只有将交换缓存的代码放在绘制函数DrawGLScene中了.
在这里小鸟是按照NE_HE的教程,直接循环不停绘制.并没有对FPS进行绘制.这样程序的FPS是很不稳定的,并且很高.由于MFC中有WM_TIMER事件,处理定时器信息很方便.因此,很多用MFC开发的人都喜欢用WM_TIMER时间来定时绘制,以次来保证FPS.但是WM_TIMER消息的精度是很低的,大概只有55ms(网上说的,小鸟还没有验证过,姑且相信吧),做简单的动画还可以,但一旦绘制的场景比较复杂,绘制耗费的时间比较多的时候,就很难保证FPS了.下面,给出一个精确控制FPS的绘制方法,精确到微秒.不要问小鸟是怎么知道的,在网上去找找资源吧.首先熟悉2个函数:QueryPerformanceFrequency()和
BOOL QueryPerformanceCounter(LARGE_INTEGER *lpCount);
对于_LARGE_INTEGER结构,大家自己去查查申明吧.QueryPerformanceFrequency返回一秒间的计时频率数,即时钟频率,QueryPerformanceCounter()返回CPU运行时长,这里是以CPU运行记数来统计的.,是精确到微秒的哦.好了,先把代码贴下来再解释吧.绘制线程代码如下:
... {
CLesson1Dlg* lDlg = (CLesson1Dlg*)lparam;
while (!lDlg->GetSafeHwnd())
...{
}
_int64 freq, cnt, oldcnt;
QueryPerformanceFrequency((PLARGE_INTEGER)&freq); //取CPU运行频率
QueryPerformanceCounter((PLARGE_INTEGER)&oldcnt); //取开始绘制时CPU运行记数
freq /= 60; //1/60秒内CPU要运行多少下
while (TRUE)
...{
lDlg->Invalidate(FALSE); //重新绘制
do
...{
QueryPerformanceCounter((PLARGE_INTEGER)&cnt); //取当前CPU运行记数
} while (cnt - oldcnt < freq); //判断是否过了1/60秒
oldcnt = cnt; //重新给开始绘制时CPU运行记数赋值
}
return 0;
}
在这里,我是保证FPS为60.啊?什么?FPS为60是什么意思?小鸟要晕了.简单的说,就是一秒内绘制60次了.大家可以到网上去找找更加专业的解释.那就是说,绘制一桢的时间为1/60秒了.所以取了CPU运行频率后,用freq /= 60计算1/60秒内CPU要记多少次数.绘制结束后,就要判断时间间隔了,一定要保证整个过程间CPU的技术是恰好大于或等于刚才得出的CPU在1/60秒内的记数的,这样就可以保证绘制过程总是保持恰好在1/60秒完成.当然,这种方法也不是完全的无误差,因为QueryPerformanceFrequency()和
在OnCreate函数的最后启动线程:
//启动绘制线程
AfxBeginThread(DrawThread,(LPVOID)this);
不知道为什么贴不了代码了,就这样吧.
至此,第一课的内容就完全的移到MFC下了.同时也填加了自己的一些东西,比如说对FPS进行控制.但是,有一点还是没有实现,就是全屏功能.这是因为小鸟暂时也不知道MFC下怎么样实现全屏幕.小鸟会继续努力,把全屏功能补上的.还有,键盘响应我也没有填加.问我为什么?其实大家都知道,MFC有键盘响应消息.但小鸟刚用了高精度的方法控制FPS,所以就觉得MFC的键盘响应的精度不够了.用MFC的键盘消息做键盘响应,相信大家都知道.这里就不多说了。我会把高精度的键盘响应补上的.到这里,其实MFC下OpenGL的框架也就搭好了,可以在这一课的基础上进行下面的学习了.等着小鸟的下一集学习笔记吧