OpenGL-HelloWorld-Windows

背景

公司产品需要,探索一些增强现实技术方案的实现,并转化成桌面式虚拟现实设备的内容集成,所以需要基于一些底层的引擎做一些本地化的开发,于是在短时间内重拾了线性代数,并且学习了openGL的引擎使用,并且了解了一些关于OSG的开发。还好,一直对三维内容的实现和展现兴趣依旧,所以将之前探索openGL的学习过程大致记录下来,一方面帮助自己整理思路,另一方面希望通过自己的理解来将整个流程解释的更加清楚。由于时间紧迫,所以一路都是自己摸索,靠着自己的理解和谷歌。
对了,顺便提一嘴,一般大家的思路直接给你将配置什么环境变量,去哪里哪里设置还有一堆添加库的,我这里不会也懒得去说那么详细,我的初衷还是给你一个思路把这些东西的来龙去脉讲清晰,建立起一个整体的工程概念,然后剩下的你自己一步步去实施。

由于是在windows环境下,所以opengl的相关配置相对麻烦了一些,而且整个开发过程你还需要了解windows下编程的一些常用接口和GUI应用。
win32编程接口
openGL
openCV后面项目会用到
unity
进程数据通信

简介

背景知识

openGL是一个多平台的2d3d图形渲染接口。这套接口提供了跟GPU交互的一套东西来实现硬件加速渲染。你要调用这些接口必须要考虑在不同的平台实现,尤其是窗口。win32是其中之一。
开发工具,visualstudio,语言cpp。
这里扯一点题外话,就是cpp的语言。每个人都学过c,但是cpp的设计思路才是每一个技术人应该熟知并且掌握的利器,毫无疑问。这里面涉及两个事情,一个是面向对象编程的模式设计,另外一个就是对内存、算法以及线程进程等。cpp难点包括指针以及其他一些概念如模板类、虚基类、纯虚函数等。平时接触到的纯虚函数应该算比较多,其实可以跟csharp中的抽象方法或者接口中的方法类似,只定义不实现。好处是多态,由派生类决定。
opengl可以用来使用的界面一大堆,比如:
交互界面包括 SDL, Allegro, SFML, FLTK, and Qt等。
wiki官方对opengl支持的图形界面接口的描述:
基础的一些工具:
1 glut 老的窗口处理,不再维护,对应的开源实现是freeglut
2 freeglut 多平台支持,支持键鼠,比glut强大稳定并且一直在更新维护;
3 glfw 基本同glfw,面向游戏。一个轻量级的特性库。提供给开发者管理窗口和上下文比如手柄、键盘以及鼠标。GLFW有何优势呢?glut太老了,最后一个版本还是90年代的。freeglut完全兼容glut,算是glut的代替品,功能齐全,但是bug太多。稳定性也不好(不是我说的啊),GLFW应运而生。
4 glew opengl2.0以后的一个工具函数。
不同的显卡公司,也会发布一些只有自家显卡才支 持的扩展函数,你要想用这数涵数,不得不去寻找最新的glext.h,有了GLEW扩展库,你就再也不用为找不到函数的接口而烦恼,因为GLEW能自动识别你的平台所支持的全部OpenGL高级扩展函数。也就是说,只要包含一个glew.h头文件,你就能使用gl,glu,glext,wgl,glx的全部函数。

多媒体工具库:
1 Allegro 5 面向游戏开发的多平台支持库,有c接口;
2 SDL c接口的多媒体库;
3 SFML 多平台支持cpp接口,支持其他绑定比如cs,java,haskell,go;
控件工具包:
FLTK 多平台cpp小控件库 ;
QT 多平台cpp控件库,提供了很多opengl帮助对象,甚至抽象出OpenGL和opengl ES之间的区别;
wxWidgets 多平台cpp控件库;
扩展加载工具:
glee/glew和glbinding
opengl库函数
opengl每个库函数均有特定的前缀,gl,glu,aux分别对应基于opengl基本库、实用库、辅助库。(aux很大程度上已经被glut取代)
wgl指的是windows专用的库函数。

总结:
windows:Qt,glut,win32
Mac:cocoa,SDL
Linux:X window
话说为什么要用win32来开发呢,因为我们这里要实现跟其他应用程序的交互,以及多窗口等操作,相对来说win32的一些开发文档和支持比较好。

系列文章

1 [环境配置-Hello World][]
2 [渲染管线][]
3 [纹理和矩阵知识][]
4 [深度绘制][]
5 [多窗口绘制][]
6 [模型加载][]
7 虚实融合 [][]

win32开发

废话不说,建立win32应用程序,直接上一个案例代码,然后解释部分要点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <Windows.h>
// 必须要进行前导声明
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

// 程序入口点
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
// 类名
WCHAR* cls_Name = L"My Class";
// 设计窗口类
WNDCLASS wc = { };
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
wc.lpfnWndProc = WindowProc;
wc.lpszClassName = cls_Name;
wc.hInstance = hInstance;
// 注册窗口类
RegisterClass(&wc);

// 创建窗口
HWND hwnd = CreateWindow(
cls_Name, //类名,要和刚才注册的一致
L"我的应用程序", //窗口标题文字
WS_OVERLAPPEDWINDOW, //窗口外观样式
38, //窗口相对于父级的X坐标
20, //窗口相对于父级的Y坐标
480, //窗口的宽度
250, //窗口的高度
NULL, //没有父窗口,为NULL
NULL, //没有菜单,为NULL
hInstance, //当前应用程序的实例句柄
NULL); //没有附加数据,为NULL
if(hwnd == NULL) //检查窗口是否创建成功
return 0;

// 显示窗口
ShowWindow(hwnd, SW_SHOW);

// 更新窗口
UpdateWindow(hwnd);

// 消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0)>0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
// 在WinMain后实现
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
switch(uMsg)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

下面按照我的理解和逻辑来解释下win32的基本架构。

程序入口点 WinMain

入口函数,从winmain中跳出时候,程序就结束了。
函数带了一个callback?什么是callback,转到定义,它其实就是stdcall,是和cdec对应的,专门用来调用win api的。所以,callback的意思,函数不是我们调用的,函数只定义了模型没有具体处理,而代码处置权在被调用者手里。如果你知道c#,你可以理解为这是一个委托,只声明了方法的参数和返回值,并没有具体处理代码。
关于参数,其实都特么的是整数,不过换了名字,就这样。
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
分别是
1 当前应用程序实例句柄;
2 前一个实例,比如我运行了两个进程xxx.exe,进程列表中会有两个,第一次的实例号假设为0001,就传递一个参数hInstance,第二次运行的实例号0002,就传给了hPrevInstance参数。
3 lpcmdline 命令行参数,LPSTR或者其他以P/LP开头的参数,实际上就是指针类型。
4 ncmdshow定义了窗口显示方式。参数是操作系统传入的,你可以在app右键属性里看到。

在定义winmain之前声明了WindowProc函数。我们在WinMain设计窗口类时要用到它的指针。所以预先定义下。在WindowProc中返回DefWindowProc是把我们不感兴趣或者没有处理的消息交回给操作系统来处理。名字可以随便改。但是返回值和参数类型必须一致。这个函数带了callback,说明也是操作系统调用的。

设计与注册窗口类

设计窗口类,就是程序主窗口。可以用WNDCLASS或者带EX的。配置好类以后,向系统注册,使用RegisterClass函数,参数就是一个指向刚刚设计好的WNDCLASS结构体的指针。
A Window Class stores information about a type of window, including it’s Window Procedure which controls the window, the small and large icons for the window, and the background color. This way, you can register a class once, and create as many windows as you want from it, without having to specify all those attributes over and over.

创建和显示窗口

创建窗口,返回一个窗口句柄。
然后调用showwindow(para)显示窗口。参数你可以自己传入;或者传入winMain的最后一个参数,由操作系统(用户)指定。

更新窗口

UpdateWindow()只要窗口没有最小化,应用程序会不断地接收WM_PAINT消息。

消息循环和处理

1
2
3
4
5
6
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

理论上来说,你没有循环的话,程序运行完马上就结束了。所以你需要一个消息处理机制,与用户交互,有跳出的条件时程序才跳出结束。这里就需要一个消息队列。
1 MSG定义:
typedef struct tagMSG {
​ HWND hwnd;//窗口句柄。windowProc交给谁处理,hwnd就是谁的句柄。
​ UINT message;//消息代码
​ WPARAM wParam;//消息信息
​ LPARAM lParam;//消息信息
​ DWORD time;
​ POINT pt;
} MSG, PMSG, LPMSG;
一些message定义

1
2
3
#define WM_INITDIALOG                   0x0110
#define WM_COMMAND 0x0111
#define WM_LBUTTONDOWN 0x0201

2 GetMessage()函数声明
BOOL WINAPI GetMessage(
Out LPMSG lpMsg,//指向MSG结构体的指针
​ _In_opt_ HWND hWnd,//通常为null,捕捉整个应用程序的消息
​ _In_ UINT wMsgFilterMin,
​ _In_ UINT wMsgFilterMax
);
该函数从呼叫进程的消息队列中检索消息,如果被检索到的消息为可分派消息,则该函数就分派该消息,如果被检索到的消息为不可分派消息,则GetMessage返回非正值,导致消息循环的结束。例如可分派的消息有:WM_PAINT、WM_SIZE、WM_CREATE等,不可分派的消息有WM_QUIT等。
与GetMessage不同,PeekMessage函数不会在返回之前等待发布消息。
3 translatemessage()
将虚拟密钥消息转换为字符消息。字符消息被发布到调用线程的消息队列,以便在线程下次调用GetMessage或PeekMessage函数时读取。
4 Dispatch message()
将消息调度到窗口过程。它通常用于分派由GetMessage函数检索的消息。
消息用GetMessage读入后(注意这个消息可不是WM_QUIT消息),它首先要经过函数TranslateMessage()进行翻译,这个函数会转换成一些键盘消息,它检索匹配的WM_KEYDOWN和WM_KEYUP消息,并为窗口产生相应的ASCII字符消息(WM_CHAR),它包含指定键的ANSI字符.但对大多数消息来说它并不起什么作用,所以现在没有必要考虑它。
下一个函数调用DispatchMessage()要求Windows将消息传送给在MSG结构中为窗口所指定的窗口过程。我们在讲到登记窗口类时曾提到过,登记窗口类时,我们曾指定Windows把函数WindosProc作为咱们这个窗口的窗口过程(就是指处理这个消息的东东)。就是说,Windows会调用函数WindowsProc()来处理这个消息。在WindowProc()处理完消息后,代码又循环到开始去接收另一个消息,这样就完成了一个消息循环。
What DispatchMessage() does is take the message, checks which window it is for and then looks up the Window Procedure for the window. It then calls that procedure, sending as parameters the handle of the window, the message, and wParam and lParam.
Once you have finished processing the message, your windows procedure returns, DispatchMessage() returns, and we go back to the beginning of the loop.
5 其他
DefWindowProc(hwnd, uMsg, wParam, lParam);
你不会处理的消息交给系统去解决。
Well our message loop is responsible for ALL of the windows in our program, this includes things like buttons and list boxes that have their own window procedures, so we need to make sure that we call the right procedure for the window. Since more than one window can use the same window procedure, the first parameter (the handle to the window) is used to tell the window procedure which window the message is intended for.
程序退出:
But what do you do when you want your program to exit? Since we’re using a while() loop, if GetMessage() were to return FALSE (aka 0), the loop would end and we would reach the end of our WinMain() thus exiting the program. This is exactly what PostQuitMessage() accomplishes. It places a WM_QUIT message into the queue, and instead of returning a positive value, GetMessage() fills in the Msg structure and returns 0. At this point, the wParam member of Msg contains the value that you passed to PostQuitMessage() and you can either ignore it, or return it from WinMain() which will then be used as the exit code when the process terminates.
最后一步需要弄清楚的是,退出窗体和程序之间的关系。当窗口被关闭,为窗口所分配的内存会被销毁,同时,我们会收到一条WM_DESTROY消息,因而,我们只要在收到这条消息时调用PostQuitMessage函数,这个函数提交一条WM_QUIT消息,而在消息循环中,WM_QUIT消息使GetMessage函数返回0,这样一来,GetMessage返回FALSE,就可以跳出消息循环了,这样应用程序就可以退出了。
所以,我们要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.

总结

win32,应用程序与用户以及操作系统的交互主要通过消息机制来处理。发生鼠标或者按键响应时,MSG消息会被添加到应用的消息队列里,然后每个窗口对应一个消息处理程序winproc。
根据自己的理解简单画了张流程图[001]Markdown
还有一张网络图,很清晰,可以参考[0011]Markdown

几个win32消息机制的参考阅读

0 [外网很清晰的解释][http://www.winprog.org/tutorial/message_loop.html]
1 [win32主函数流程][https://blog.csdn.net/tcjiaan/article/details/8497535]
2 [消息机制][https://blog.csdn.net/feixiaoxing/article/details/78787876]
3 [消息机制][https://blog.csdn.net/hyman_c/article/details/70144952][2][https://blog.csdn.net/u013777351/article/details/49522219]

环境配置

glut配置

[下载glut][https://www.opengl.org/resources/libraries/glut/glut_downloads.php]
解压后,5个文件。glut.h/glut.dll/glut32.dll/glut.lib/glut32.lib
1 找到vs2017安装的目录,路径为 (D:\Program)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\include ,创建一个名为gl的文件夹,并将解压到的glut.h文件复制其中。
2 再找到路径为 (D:\Program)\Microsoft Visual Studio\2017\Community\VC\Tools\MSVC\14.11.25503\lib\x86 ,将解压到的glut.lib,glut32.lib复制其中。
3 最后把解压到的glut.dll和glut32.dll复制到C:\Windows\System32文件夹内(32位系统)或C:\Windows\SysWOW64(64位系统)。

win32+opengl配置

基本同上,如果不安装glfw和glew等库的话。安装的话这里再详细展开。目前的配置够了。
如果你是Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了。由于这篇教程用的是VS编译器,并且是在Windows操作系统上,我们只需将opengl32.lib添加进连接器设置里就行了。

OpenGL mac 环境配置

因为mac系统集成了opengl,所以你只需要包含相应的头文件进行开发即可。但是要注意的是,mac平台已经在最新的几版系统里建议使用metal/vulkan开发了,尤其在游戏开发方面,windows+dxd独孤求败。多的不展开,配置平台还是比较简单,而且可选的方式也比较多。
首先,Xcode是必须安装的,mac下开发的大器;
其次窗口gui你可以选择glut,glfw或者cocoa等:
glut:
只需要饱含glut头文件即可简单创建opengl窗口程序,但是要注意的是mac os 10.9以后官方不建议使用glut等库,你可以强行这样使用:

在仅仅进行一些小的测试方面还是很方便的。
glfw:

HelloWorld

console应用

建立控制台应用程序,头文件只需要包含#include <GL/glut.h> 即可
进行一个矩形的简单绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <GL/glut.h>
void Show()
{
glClear(GL_COLOR_BUFFER_BIT);
glRectf(-0.1f, -0.1f, 0.5f, 0.5f);
glFlush();
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
glutInitWindowPosition(100, 100);
glutInitWindowSize(800, 600);
glutCreateWindow("OpenGL-ONE");
glutDisplayFunc(Show);
glutMainLoop();
return 0;
}

Done!
基本就是这么简单
参考图002Markdown

win32 窗口应用

嗯,如果只用glut的话比较好折腾。用win32就比较麻烦了,坑比较多,尤其对于win32或者opengl编程新手来说,稍微麻烦一点。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/* An example of the minimal Win32 & OpenGL program.  It only works in
16 bit color modes or higher (since it doesn't create a
palette). */
#include <windows.h> /* must include this before GL/gl.h */
#include <GL/gl.h> /* OpenGL header file */
#include <GL/glu.h> /* OpenGL utilities header file */
#include <stdio.h>
#pragma comment ( lib , "opengl32.lib" )
#pragma comment ( lib , "glu32.lib" )
void display()
{
/* rotate a triangle around */
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_TRIANGLES);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex2i(0, 1);
glColor3f(0.0f, 1.0f, 0.0f);
glVertex2i(-1, -1);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex2i(1, -1);
glEnd();
glFlush();
}

LONG WINAPI WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static PAINTSTRUCT ps;

switch (uMsg) {
case WM_PAINT:
display();
BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
return 0;

case WM_SIZE:
glViewport(0, 0, LOWORD(lParam), HIWORD(lParam));
PostMessage(hWnd, WM_PAINT, 0, 0);
return 0;

case WM_CHAR:
switch (wParam) {
case 27: /* ESC key */
PostQuitMessage(0);
break;
}
return 0;

case WM_CLOSE:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

HWND CreateOpenGLWindow(char* title, int x, int y, int width, int height,
BYTE type, DWORD flags)
{
int pf;
HDC hDC;
HWND hWnd;
WNDCLASS wc;
PIXELFORMATDESCRIPTOR pfd;
static HINSTANCE hInstance = 0;

/* only register the window class once - use hInstance as a flag. */
if (!hInstance) {
hInstance = GetModuleHandle(NULL);
wc.style = CS_OWNDC;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL";

if (!RegisterClass(&wc)) {
MessageBox(NULL, "RegisterClass() failed: "
"Cannot register window class.", "Error", MB_OK);
return NULL;
}
}

hWnd = CreateWindow("OpenGL", title, WS_OVERLAPPEDWINDOW |
WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
x, y, width, height, NULL, NULL, hInstance, NULL);

if (hWnd == NULL) {
MessageBox(NULL, "CreateWindow() failed: Cannot create a window.",
"Error", MB_OK);
return NULL;
}

hDC = GetDC(hWnd);

/* there is no guarantee that the contents of the stack that become
the pfd are zeroed, therefore _make sure_ to clear these bits. */
memset(&pfd, 0, sizeof(pfd));
pfd.nSize = sizeof(pfd);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | flags;
pfd.iPixelType = type;
pfd.cColorBits = 32;

pf = ChoosePixelFormat(hDC, &pfd);
if (pf == 0) {
MessageBox(NULL, "ChoosePixelFormat() failed: "
"Cannot find a suitable pixel format.", "Error", MB_OK);
return 0;
}

if (SetPixelFormat(hDC, pf, &pfd) == FALSE) {
MessageBox(NULL, "SetPixelFormat() failed: "
"Cannot set format specified.", "Error", MB_OK);
return 0;
}

DescribePixelFormat(hDC, pf, sizeof(PIXELFORMATDESCRIPTOR), &pfd);

ReleaseDC(hWnd,hDC );

return hWnd;
}

int APIENTRY WinMain(HINSTANCE hCurrentInst, HINSTANCE hPreviousInst,
LPSTR lpszCmdLine, int nCmdShow)
{
HDC hDC; /* device context */
HGLRC hRC; /* opengl context */
HWND hWnd; /* window */
MSG msg; /* message */

hWnd = CreateOpenGLWindow("minimal", 0, 0, 256, 256, PFD_TYPE_RGBA, 0);
if (hWnd == NULL)
exit(1);

hDC = GetDC(hWnd);
hRC = wglCreateContext(hDC);
wglMakeCurrent(hDC, hRC);

ShowWindow(hWnd, nCmdShow);

while (GetMessage(&msg, hWnd, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

wglMakeCurrent(NULL, NULL);
ReleaseDC(hWnd, hDC);
wglDeleteContext(hRC);
DestroyWindow(hWnd);

return msg.wParam;
}

如果OK,你会得到图片003Markdown

调试时候的坑

1 const.char 类型形参与LPWSTR 类型的实参不兼容
右键项目-属性-常规-字符集,将unicode改为多字符集,为了向下兼容
2 如果没有配置环境并且没有
//#pragma comment ( lib , “opengl32.lib” )
//#pragma comment ( lib , “glu32.lib” )
这两行的话,你会得到error LNK1120 等类似的错误提示 无法解析的外部命令.原因就是这些库文件没有成功包含进项目。链接时没有找到对应的OpenGL库。
为了方便可以直接通过上面语句完成(vc++的特性)。

代码解释

后续的章节再说吧。

参考

[1 nehe的参考][https://www.opengl.org/archives/resources/code/samples/win32_tutorial/]
[2 其他的参考][https://blog.csdn.net/vagrxie/article/details/4602961]

参考

1 [opengl wiki][https://en.wikipedia.org/wiki/OpenGL#Context_and_window_toolkits]
2 [两个配置参考]
https://blog.csdn.net/wangwei19951128/article/details/78410869
https://blog.csdn.net/qq_19003345/article/details/76098781
https://blog.csdn.net/AvatarForTest/article/details/79199807

3 关于opengl 的背景,可以参考

http://openglbook.com/chapter-0-preface-what-is-opengl.html