俗话说,“工欲善其事,必先利其器”,一个好的开发工具能够使你将注意力其中在程序设计本身,做到事半功倍,反之,可能经常需要解决开发工具的问题。我们建议使用微软公司的Visual C++ 6.0,如果使用Visual C++ .NET也可以,当然使用Borland C++、C++ Builder或者Watcom C++也完全可以,这也要看个人的爱好。
之所以把一个“Hello World”程序单独算作一章,是因为在这一章很重要,我们要先奠定OpenGL应用程序的框架,以后的程序将使用这一框架。
在Windows系统中,opengl32.dll、glu32.dll提供对OpenGL的支持,所以应该在编译OpenGL程序时链接相应的库文件。
通常可以在程序的开头加入以下代码来达到链接库文件的目的,如果用到了辅助函数,还需要链接辅助函数库glaux.lib:
#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")
#pragma comment(lib, "glaux.lib")
另一种办法是在VC中设置好工程的参数,如图1-7所示,通过菜单Project->Settings...->Link将opengl32.lib、glu32.lib和glaux.lib(如果需要的话)三个文件加入工程中。
图1-7 VC++静态库设置
开发环境设置完毕后,我们开始第一个OpenGL程序“Hello World”。因为三维图形通常对性能和速度要求比较高,我们就使用Win32 API来写代码,而不使用MFC。实际上,真正的大型高性能三维图形软件和游戏软件是很少会使用MFC来写的。
首先使用VC的Win32 Application Wizard创建一个Hello World的Win32程序,该工程一共包含9个文件,Hello.cpp、Hello.rc、StdAfx.cpp、Hello.h、resource.h、StdAfx.h、Hello.ico、small.ico和Readme.txt。
把StdAfx.cpp、StdAfx.h两个文件删除,再通过菜单Project->Settings...->C/C++将Project Options编译开关中的 /Yu"stdafx.h" 去掉,然后将Hello.cpp按照以下方式进行修改。
#include "windows.h" //Windows标准API的头文件
#include "resource.h" //本程序资源定义文件
//增加opengl的头文件
#include "gl/gl.h" //OpenGL32库的头文件
#include "gl/glu.h" //glu32库的头文件
#include "gl/glaux.h" //辅助库的头文件
#define WIN32_LEAN_AND_MEAN //表示不使用MFC
#define WIDTH 640 //窗口的宽度
#define HEIGHT 480 //窗口的高度
#define BITS 16 //程序的像素格式
然后定义几个全局变量:
HGLRC hRC=NULL; //渲染描述表句柄
HDC hDC=NULL; //设备描述表句柄
HWND hWnd=NULL; //当前窗口句柄
HINSTANCE hInst; //当前程序实例句柄
其中hDC、hWnd、hInst三个变量很容易理解,重要的是第一个变量hRC,它是OpenGL程序的特色,表示渲染描述表(Rendering Context)。它将所有的OpenGL调用全部连接到对应的设备描述表hDC上,hDC通过图形设备接口GDI将窗口联结为一体,通过Windows系统完成显示。
然后再定义一个表示应用是否激活的标志bActive:
BOOL bActive; //当前应用程序是否处于激活状态
下面几行在原来的文件中有,不用修改。
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // The title bar text
// Foward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(int Width, int Height, int Bits);//这里有变化
//原来的函数有两个参数HINSTANCE和nCmdShow,这里改成更加有用的
//窗口大小和像素格式
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
接下来是我们自定义的函数原型说明,用于处理OpenGL的初始化,主流程和OpenGL的关闭。
void glMain(){};
void glShutdown(){};
void glInit(){};
然后进入我们程序的入口WinMain,做一些改动。
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
MSG msg;
//HACCEL hAccelTable;
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_HELLO, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
hInst = hInstance;
// Perform application initialization:
if (!InitInstance (WIDTH, HEIGHT, BITS))
{
return FALSE;
}
//hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_HELLO);
// 下面的消息主循环有变化
//初始化OpenGL
glInit();
while(TRUE)
{
//检查队列是否有消息,若有就取出。这里使用PM_REMOVE表示取出后从队列中删除。
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
//如果是WM_QUIT消息就停止
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
//将消息发送到窗口函数WndProc
DispatchMessage(&msg);
} // end if
//只有程序处于激活状态时才进入OpenGL的主程序进行绘制
if(bActive)
{
glMain();
}
} // end while
//程序结束前关闭OpenGL
glShutdown();
return msg.wParam;
} // end WinMain
在WinMain函数中,我们看到了增加两个函数glMain()和glShutdown()。其中对OpenGl的各种变换、处理、显示都在glMain()中,它是我们的OpenGL应用真正的主函数。当程序收到退出消息时,先关闭OpenGL,这些在glShutdown()中实现。
接下来我们看MyRegusterClass函数,该函数仅仅是注册窗口函数,几乎不用改变。
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_HELLO1);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = NULL; //这里不使用菜单,因此,在对应的资源中,
//菜单资源和About对话框资源都可以删除。
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
return RegisterClassEx(&wcex);
}
再下来是InitInstance函数,这个函数很重要,整个OpenGL就从这里开始。它和VC生成的InitInstance()函数不同。原始的InitInstance有两个参数:程序实例hInstance和窗口显示方式nCmdShow。其中hInstance可以用全局变量hInst代替,nCmdShow可以使用固定的方式来实现。因此可以使用三个参数来创建窗口,这三个参数就是窗口的宽、高以及程序的像素格式。
在InitInstance中,首先根据设定大小使用CreateWindowEx创建一个带有标题栏、可变大小边框、可最大最小化的窗口。然后使用Win32提供的OpenGL扩展函数ChoosePixelFormat选择像素格式,再调用SetPixelFormat设置程序的像素格式。接下来使用调用扩展函数wglMakeCurrent将当前的DC和OpenGL的RC连接起来。如果没有问题,就调用ShowWindow显示窗口。
Windows创建的窗口大小是包括了客户区(Client Area),标题栏的。所以如果以指定的大小(例如640x480)调用CreateWindowEx,那么OpenGL在绘图时有可能会覆盖到标题栏上,这是我们应该避免的。因此,在调用CreateWindowEx创建窗口前,可以先使用AdjustWindowRectEx对窗口的大小进行调整,将得到的大小作为CreateWindowEx的参数,这样,得到的窗口本身已经比预设的值要大,其绘图客户区的大小就是预设的值。OpenGL在作图时就只会在客户区绘图,这正是我们所期望的。
BOOL InitInstance(int width, int height, int bits)
{
uint PixelFormat; //像素格式
DWORD dwExStyle; // 扩展窗口风格
DWORD dwStyle; // 窗口风格
//设置像素格式
static PIXELFORMATDESCRIPTOR pfd=
{
sizeof(PIXELFORMATDESCRIPTOR), //本结构的大小
1, //像素格式描述符的版本号
PFD_DRAW_TO_WINDOW | //该像素格式需要支持窗口
PFD_SUPPORT_OPENGL | //该像素格式需要支持OpenGL
PFD_DOUBLEBUFFER, //需要支持双缓冲
PFD_TYPE_RGBA, //RGBA格式
bits, //像素格式的颜色位数
0, //红色位
0, //RedShift
0, //绿色位
0, //GreenShift
0, //蓝色位
0, //BlueShift
0, //Alpha位
0, //AlphaShift
0, //cAccumBits
0, //cAccumRedBits
0, //cAccumGreenBits
0, //cAccumBlueBits
0, //cAccumAlpha
16, //cDepthBits,16位Z-缓冲
0, //cStencilBits,模板缓冲
0, //cAuxBuffers,辅助缓冲
PFD_MAIN_PLANE, //iLayerType,在主绘图层绘图
0, //保留字节
0, //dwLayerMask
0, //dwVisibleMask
0}; //dwDamageMask
//用一个矩形结构保存窗口的大小,然后根据该矩形的大小调整窗口大小。
RECT Rt;
Rt.left= 0;
Rt.right= width;
Rt.top= 0;
Rt.bottom= height;
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; //
dwStyle=WS_OVERLAPPEDWINDOW; //
//对窗口大小进行调整,达到真正的要求
AdjustWindowRectEx(&Rt, dwStyle, FALSE, dwExStyle);
//开始创建窗口,注意窗口大小使用了调整后的大小
if (!(hWnd=CreateWindowEx( dwExStyle,
szWindowClass,
szTitle,
WS_CLIPSIBLINGS | //避免其他应用在本窗口绘图
WS_CLIPCHILDREN | //避免其他应用在本窗口绘图
dwStyle,
0, 0,
Rt.right-Rt.left,
Rt.bottom-Rt.top,
NULL,
NULL,
hInst,
NULL)))
{
//如果窗口创建失败,则关闭OpenGL使用的资源,并且弹出一个显示错误的对话框。
glShutdown();
MessageBox(NULL,"Fail to Create Window!","ERROR",MB_OK);
return FALSE;
}
//取得当前窗口的设备描述表DC
hDC = GetDC(hWnd);
if(!hDC) //如果失败则释放OpenGL占用的资源,并返回
{
glShutdown();
MessageBox(NULL,"Fail to get DC from window!","ERROR",MB_OK);
return FALSE;
}
//使用Win32的OpenGL扩展函数选择指定的像素格式
PixelFromat = ChoosePixelFormat(hDC, &pfd);
if(!PixelFormat)
{
//如果找不到匹配的像素格式则释放OpenGL占用的资源,并返回
glShutdown();
MessageBox(NULL,"Fail to Choose Pixel Format!","ERROR",MB_OK);
return FALSE;
}
//将设备描述表设置成指定的像素格式
if(!SetPixelFormat(hDC,PixelFormat,&pfd))
{
glShutdown();
MessageBox(NULL,"Fail to Set Pixel Format!","ERROR",MB_OK);
return FALSE;
}
//通过扩展函数创建基于取得的设备描述表hDC的渲染描述表hRC。
if (!(hRC=wglCreateContext(hDC)))
{
glShutdown();
MessageBox(NULL,"Fail to create RC from DC!","ERROR",MB_OK);
return FALSE;
}
//将取得的设备描述表和渲染描述表关联起来
if(!wglMakeCurrent(hDC,hRC))
{
glShutdown();
MessageBox(NULL,"Fail to link RC to DC","ERROR",MB_OK);
return FALSE;
}
//显示窗口,显示方式是SW_NORMAL
ShowWindow(hWnd,SW_SHOW);
return TRUE; //InitInstance初始化成功,返回TRUE
}
在窗口消息处理函数WndProc中,由于没有菜单,我们就把WM_COMMAND的消息处理分支去掉,同时由于我们的绘图由OpenGL控制,因此也把WM_PAINT消息处理分支去掉。然后添加WM_ACTIVATE消息处理分支,以根据程序是否激活来决定是否处理。WndProc函数如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//把不需要的注释掉
//int wmId, wmEvent;
//PAINTSTRUCT ps;
//HDC hdc;
//TCHAR szHello[MAX_LOADSTRING];
//LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
switch (message)
{
//判断窗口是否激活
case WM_ACTIVATE:
{
if (!HIWORD(wParam))
{
bActive=TRUE;
}
else
{
bActive=FALSE;
}
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
把About函数也去掉,然后进行编译,运行之后可以看到只有一个空的窗口,如图1-8所示。
图1-8 OpenGL框架