- OpenGL的状态管理机制
- 视口与视口坐标系概念
- 测试视口设置
- 1 移动视口
- 2 多视口
- 视口小结
1 OpenGL的状态管理机制
从前面的最简单例子可以看出,我们几乎没有进行任何关于颜色和坐标系的配置,OpenGL就已经能够实施渲染了。这是因为OpenGL本身管理了很多渲染时需要的状态数据,并且在初始化时自动设置了合理的默认值。例如,默认的清屏颜色就是黑色,这才是我们看到窗口客户区呈现黑色的原因。
OpenGL的渲染需要很多的状态数据来供其使用,如果把所有的这些数据都作为参数传递给渲染函数的话,那么此函数的样子大概是这样的。
glXXX(渲染上下文,颜色,坐标,视口,光照,雾,.....);
这对于客户端使用来说,简直就是场噩梦啊!由于大多数渲染行为都需要这些状态数据,所以O彭GL干脆把这些大家都使用的数据做成了全局变量,供所有渲染函数使用。这样就可以把渲染函数简化成了
glXXX(坐标);
当然了,全局变量也带来了渲染函数不可重入的问题,还好在OpenGL中,函数重入不算重要。人们习惯称这种使用了大量全局状态数据的程序为状态机。
其实好多库都采取了状态机的机制,如Windows GDI。
2 视口与视口坐标系概念
计算机图形学的本质就是创建三维物体的二维图像,涉及到多个坐标系间的转化。本文从最简单的视口坐标系开始。视口(Viewport)就是最终渲染结果显示的目的地。它是一个矩形的区域,长度单位是像素,视口的位置和大小在视口坐标系中定义。视口坐标系是标准的笛卡尔直角坐标系,其原点位于渲染环境窗口客户区的左下角,横轴(x)向右为正,纵轴(y)向上为正。如下图所示:
注意:视口坐标系与Windows的窗口坐标系是不同的。
OpenGL用来设置视口的函数是:
void WINAPI glViewport(
GLint x,
GLint y,
GLsizei width,
GLsizei height
);
函数参数都是针对视口坐标系的。例如
glViewport(100,50, 200, 150);
设置的视口位置及大小如下图所示:
每一次渲染的执行(glBegin()和glEnd()中间的代码)都是把当前视口作为最终输出目的地。视口也是OpenGL内部维护的状态变量之一,它可以在一帧的渲染中多次改变,OpenGL在执行渲染时都是使用当前的视口。窗口客户区可以被分割为多个视口,但是同一时刻只有一个视口生效。
在初始化OpenGL窗口环境时,视口被设置为布满整个客户区。
3 测试视口设置
3.1 移动视口
我们在WM_LBUTTONDOWN事件的响应中,把视口的左下角坐标设置为鼠标的位置,宽度设置为200像素,高度设置为150像素。然后重新进行OpenGL渲染。
void OnLeftButtonDown(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
RECT clientRect;
::GetClientRect(hWnd, &clientRect);
int xPos = GET_X_LPARAM(lParam); /* 需要包含 <windowsx.h> */
int yPos = GET_Y_LPARAM(lParam);
int x = xPos; /* 把窗口坐标系坐标转换为视口坐标系坐标 */
int y = clientRect.bottom - yPos;
glViewport(x, y, 200, 150);
InvalidateRect(hWnd, NULL, TRUE);
}
在WM_PAINT的事件处理中,通过绘制布满整个可视空间的矩形来标注出当前的视口位置与大小。注意:我们绘制时使用了默认的颜色状态(白色)。
void OnPaint(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
glClear(GL_COLOR_BUFFER_BIT);
glBegin(GL_QUADS);
glVertex2d(-1, -1);
glVertex2d(1, -1);
glVertex2d(1, 1);
glVertex2d(-1, 1);
glEnd();
HDC hdc = ::GetDC(hWnd);
::SwapBuffers(hdc);
::ReleaseDC(hWnd, hdc);
}
这样,每当在客户区点击一下鼠标,当前的视口区域就会被标注出来。
3.2 多视口
OpenGL的视口也是其内部维护的众多状态变量之一,在某一次OpenGL的渲染中(glBegin()和glEnd()之间的渲染代码),只能使用当前视口进行输出。但是在一帧图像可以包含任意多次OpenGL渲染,也就是说,程序中可以多次调用glBengin()和glEnd()。例如如下伪代码:
glViewport(A);
glBegin();
渲染在房子里面看到的场景
glEnd();
glViewport(B);
渲染在外面看房子的场景
glBegin();
glEnd();
这样我们就得到了两个不同的视口A,B。最终A和B组成了一帧完整的图像。
我们的程序现在还不能渲染出这么复杂的场景,那么就用简单的颜色矩形来表示吧。
修改渲染函数如下:
void OnPaint(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1, 0, 0);
glViewport(0, 0, 300, 300);
glBegin(GL_QUADS);
glVertex2d(-1, -1);
glVertex2d(1, -1);
glVertex2d(1, 1);
glVertex2d(-1, 1);
glEnd();
glColor3f(0, 1, 0);
glViewport(250, 250, 300, 300);
glBegin(GL_QUADS);
glVertex2d(-1, -1);
glVertex2d(1, -1);
glVertex2d(1, 1);
glVertex2d(-1, 1);
glEnd();
HDC hdc = ::GetDC(hWnd);
::SwapBuffers(hdc);
::ReleaseDC(hWnd, hdc);
}
4 视口小结
视口是OpenGL里最简单的概念,视口坐标系是唯一一个与窗口环境有关的坐标系。大多数程序,使用一个视口就够了,最可能的情况就是在WM_SIZE的事件响应中自动调整视口大小,使其始终布满整个窗口客户区。
void OnSize(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int cx = LOWORD(lParam);
int cy = HIWORD(lParam);
glViewport(0, 0, cx, cy);
}