OpenGL3.0(含3.0)之前的规范中存在固定流水线(fixed-functionpipeline),这时的编程风格我称之为“旧风格”,也称为“立即模式(intermediate mode)”。2.4节中的glBegin…glEnd…glFlush这样的代码就是例子。
从OpenGL 3.1开始,规范中删除了固定流水线的内容,这意味着从“支持”的角度来说,旧风格的OpenGL从3.1版开始不受支持了。当然,从编程的角度来说,OpenGL API仍然保持了对旧风格的兼容性支持,但所有旧风格相关的函数都被打上了“过时”的标签。
3.1版之后的OpenGL要求使用着色器(shader),“顶点着色器(Vertex Shader)”和“片段着色器(Fragment Shader)”是必须的,还有其它几种着色器是可选的,具体内容不在这节介绍。
3.1 旧风格
无论底层如何实现,我们可以这样理解旧风格的OpenGL编程:
1)glBegin(图元类型)
通知OpenGL,下面的点用于构造指定的“图元类型”的图元,如2.1例子中的
glBegin(GL_TRIANGLES)
就表示接下来的点用于构造三角形,如果接下来给出3个点,就正好一个三角形,如果给出6个点,就正好两个三角形,如果给出5个点,那么后两个点就没用。
2)glVertex*(…)
给出点的坐标信息,这些点将被用于构造OpenGL指定图元。这里的*(星号)表示存在一组函数,这组函数都是以glVertex开头,*(星号)具体形式为“ntv”:a)n表示维数,可以是2、3、4;b)t表示类型,可以是f,表示float类型;d(double);i(int)……;c)v表示通过数据来构造点
3) glEnd()
指示本次图元构造过程结束。OpenGL不会把两个glBegin…glEnd中给出的点“拼”在一起。例如,第1次给5个点,第2次给1个点并不会得到两个三角形,只会产生一个三角形。
4) 其它控制函数
包括属性(attributes)控制,视角控制(viewing) ……,这些控制函数指示OpenGL如何处理前面给出的图元,如何控制场景……。
OpenGL在得到足够信息后就开始按照固定的流程和固定的方法开始处理整个场景并从显示器上显示出来。
不过随着显卡的GPU时代到来,显卡变得越来越强大,且GPU和CPU一样也是可编程的。简单说,就是你可以为GPU写程序,控制GPU操作图形的方式。这样一来,旧风格显得越来越不适应硬件发展了。为了追求最充分利用显卡能力,OpenGL加入了GLSL,利用用户编程的着色器,即一小段GPU执行的程序,控制GPU的工作,就象编程控制CPU的工作是一样的。这种方式我称之为新风格,也称为“维持模式(retained mode)”
旧风格的例子参考2.4中的示例程序即可,这章不再增加。如果想画很多图形,在glEnd之后和glFlush之前再画就行了。
3.2 新风格
新风格的OpenGL编程分为以下几个步骤:
1) 产生一批点,并传递给GPU
2)通知GPU刚才那一批点应该构造成什么图元
3)通知GPU应该如何控制、显示刚才那个图元
从GPU的角度来看,它至少需要完成以下几个工作:
1)从用户程序获取一批点的坐标
2)根据用户的“顶点着色器”处理这批点
3)根据用户的“片段着色器”处理最终的图元、场景……
很明显的,当GPU和CPU分开来工作以后,就可以形成流水线,再加上GPU内部的流水线及大规模并发处理,新风格的OpenGL编程可以获得非常好的性能。
3.2.1产生点并发送给GPU
新风格下产生点有几种不同的方式,这里介绍最直接的一种。该方式需要以下几个步骤:
1)以数组或等价形式定义一批顶点
2)用glGenVertexArrays指示GPU创建顶点数组对象,该对象可以保存顶点、纹理等多个不同类型的数组。
3) 用glBindVertexArray将2)中创建的对象设置为当前数组对象
4) 用glGenBuffer开辟缓冲区,用于存储顶点数据
5) 用glBindBuffer将4)中的缓冲区与当前数组对象绑定
6)利用glBufferData将1)中定义的顶点数据发送给显卡
3.2.2 编译链接着色器
OpenGL着色器就是一段用GLSL语言编写的程序,提供给GPU运行。具体语法不在此介绍。
OpenGL库自带了编译链接GLSL语言的全部函数:
- glCreateProgram GPU创建程序
- glCreateShader GPU创建着色器
- glShaderSource GPU绑定着色器源代码,源代码应当在内存数组中
- glCompileShader 编译着色器源代码
- glAttachShader 着色器附加到程序
- glLinkProgram 链接产生最后的GPU可执行程序
- glUseProgram 使用程序
3.2.3绘制渲染输出图像
OpenGL新风格下删除了旧风格下的很多“默认”状态,因此,在绘制图像时需要比旧风格多一些步骤:
1)创建(或使用原有的)顶点数组对象,并设置为当前数组对象
2)绑定在3.2.1中创建的顶点缓冲区
3)获取可用的着色器
4)开启顶上属性数组
5)设置3)中获得的着色器为当前顶点属性的着色器
6)要求GPU绘制渲染图像
7)要求GPU在屏幕上输出图像
3.3 Glew新风格示例
这一章的示例是在2.4节示例的基础上进行微小修改得到。程序创建注意事项有以下几点:
1)是win32窗口程序,不是控制台程序
2)在程序前面增加了几个简单的辅助定义
3)着色器以局部数组形式存放在CreateShader函数中
4)数据如果需要在多个函数间使用,都定义成了全局变量
5)为减少内容,省略了调用函数后对返回错误的判断
6)比2.4节示例原始程序增加了InitVertex、SendToGPU、CreateShader、DrawToSreen,4个函数,程序总体结构与2.4节示例完全相同。链接库需要opengl32.lib和glew32.lib。
#include <Windows.h>
#include <iostream>
#include <gl/glew.h>
#include <GL/wglew.h>
typedef GLfloat GLvertex2f[2];
struct GLvertex2fs
{
GLfloat x;
GLfloat y;
};
#define SetGLVertex2f(v,x,y) \
v[0]=x; v[1]=y
GLuint vertexArray1;//用于两个三角形的数组对象
GLuint vertexArray2;//用于line_loop的数组对象
GLuint buffer1; //用于两个三角形的缓冲区
GLuint buffer2; //用于line_loop的缓冲区
GLuint shaderprogram; //由GLSL文件链接得到的程序
//两组点,一组用结构体的数组,一组用数组的数组
GLvertex2fs points[6];//6个点画两个三角形,结构体数组
GLvertex2f points2[4];//3个点画line_loop,数组的数组
void CreateShader()
{
//顶点着色器的源程序
GLchar vertexshader[] ={
"#version 150\n"
"in vec4 vPosition;\n"
"void main()\n"
"{ gl_Position = vPosition; }"
};
//片段着色器的源程序
GLchar fragshader[] ={
"#version 150\n"
"out vec4 fColor;\n"
"void main()\n"
"{ fColor = vec4( 1.0, 0.0, 0.0, 1.0 ); }"
};
//定义内部对象,使后续代码可以使用循环
struct Shader {
GLenum type;
GLchar* source;
} shaders[2] = {
{ GL_VERTEX_SHADER, vertexshader },
{ GL_FRAGMENT_SHADER, fragshader }
};
GLuint program = glCreateProgram();//GPU创建程序对象
for ( int i = 0; i < 2; ++i ) {
Shader& s = shaders[i];
GLuint shader = glCreateShader( s.type ); //GPU创建着色器对象
glShaderSource( shader, 1, (const GLchar**) &s.source, NULL );//着色器绑定对应源程序
glCompileShader( shader );//编译着色器
glAttachShader( program, shader ); //将编译结果存入GPU程序
}
glLinkProgram(program); //将程序中的不同部分链接起来
glUseProgram( program );//应用得到的程序
shaderprogram = program;
}
void InitVertex()
{
//两个三角形的6个点赋值
points[0].x = -0.8f;
points[0].y = -0.8f;
points[1].x = 0.8f;
points[1].y = -0.8f;
points[2].x = 0.0f;
points[2].y = 1.0f;
points[3].x = 0.0f;
points[3].y = -0.8f;
points[4].x = -0.4f;
points[4].y = 0.1f;
points[5].x = 0.4f;
points[5].y = 0.1f;
//一个line_loop的4个点赋值
SetGLVertex2f(points2[0],-0.3f,0.8f);
SetGLVertex2f(points2[1],0.8f,0.8f);
SetGLVertex2f(points2[2],-0.9f,-0.8f);
SetGLVertex2f(points2[3],-0.8f,0.2f);
}
void SendToGPU()
{
//创建一个顶点数组对象,并设置为当前数组对象
//该数组对象保存两个三角形的相关数组
glGenVertexArrays( 1, &vertexArray1 );
glBindVertexArray(vertexArray1);
//创建一个项点缓冲,并设置为当前缓冲,当前缓
//冲总是属于当前数组对象
glGenBuffers( 1, &buffer1 );
glBindBuffer( GL_ARRAY_BUFFER, buffer1 );
//将points中的两个三角形的顶点数据发送给GPU
glBufferData( GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW );
//创建一个顶点数组对象,并设置为当前数组对象
//该数组对象保存line_loop的相关数组
glGenVertexArrays( 1, &vertexArray2 );
glBindVertexArray(vertexArray2);
//创建一个项点缓冲,并设置为当前缓冲,当前缓
//冲总是属于当前数组对象
glGenBuffers( 1, &buffer2 );
glBindBuffer( GL_ARRAY_BUFFER, buffer2 );
//将points2中的line_loop的顶点数据发送给GPU
glBufferData( GL_ARRAY_BUFFER, sizeof(points2), points2, GL_STATIC_DRAW );
glBindVertexArray(0);
}
void DrawToWindow()
{
//用白色清除缓冲区
glClearColor(1.0f,1.0f,1.0f,1.0f);
glClear( GL_COLOR_BUFFER_BIT );
glBindBuffer( GL_ARRAY_BUFFER, buffer2 );
GLuint loc = glGetAttribLocation( shaderprogram, "vPosition" );
glEnableVertexAttribArray( loc );
glVertexAttribPointer( loc, 2, GL_FLOAT, GL_FALSE, 0,(GLvoid*)(0) );
//渲染line_loop
glDrawArrays( GL_LINE_LOOP, 0,4);
glBindBuffer( GL_ARRAY_BUFFER, buffer1);
glVertexAttribPointer( loc, 2, GL_FLOAT, GL_FALSE, 0,(GLvoid*)(0) );
glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);//绘制三角形时不填充,而是绘制边线
glDrawArrays(GL_TRIANGLES , 0, 6 );
glFlush();
}
///////////////////////////////////////////////////////////////////////////////
//窗口回调函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
DrawToWindow();
break;
case WM_CLOSE:
if(MessageBox(hWnd,TEXT("你要关闭窗口吗?"),TEXT("提示!"),MB_OKCANCEL) == IDOK)
DestroyWindow(hWnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
return 0;
}
////////////////////////////////////////////////////////////////////
// 入口函数
int _stdcall WinMain(HINSTANCE hInst,HINSTANCE hPreInstance,LPSTR lpCmdLine,int nShowCmd)
{
HWND hWnd;
HGLRC hRC;
HDC hDC;
HINSTANCE hInstance;
hInstance = hInst;
WNDCLASS windClass;
TCHAR szWindowName[50] = TEXT("OpenGL_Window");
TCHAR szClassName[50] = TEXT("OpenGL_Class");
//初始化窗口结构体
windClass.lpszClassName = szClassName;
windClass.lpfnWndProc = (WNDPROC)WndProc;
windClass.hInstance = hInstance;
windClass.hCursor = LoadCursor(NULL, IDC_ARROW);
windClass.hIcon = LoadIcon(NULL, IDI_WINLOGO);
windClass.hbrBackground = NULL;
windClass.lpszMenuName = NULL;
windClass.style = CS_HREDRAW | CS_OWNDC | CS_VREDRAW;
windClass.cbClsExtra = 0;
windClass.cbWndExtra = 0;
//注册窗口类
if(!RegisterClass( &windClass )) return 1;
//创建窗口
hWnd = CreateWindowEx(0, // 窗体扩展风格
szClassName, // 窗体类名称
szWindowName, // 窗体名称
0, // 窗体风格
0, // 窗体在桌面上的位置,x坐标
0, // 窗体在桌面上的位置, y坐标
0, // 宽
0, // 高
NULL, // 父窗口
NULL, // 菜单
hInstance, // 实例
NULL);
hDC = GetDC(hWnd);
PIXELFORMATDESCRIPTOR pfd;
SetPixelFormat( hDC, 1,&pfd);
hRC = wglCreateContext( hDC );
wglMakeCurrent( hDC, hRC );
//一旦wgl被初始化,可以撤消相关环境
//之后可以利用wgl函数,调用最新版的OpenGL建立真正使用的环境
GLenum ret = glewInit();
if (GLEW_OK != ret)
{
MessageBox(NULL,(LPCSTR)glewGetErrorString(ret),TEXT("glew初始化"),MB_OK);
}
//释放OpenGL环境,wgl函数已经在调用glewInit()时完成了初始化
//释放环境后仍然可以使用
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hRC);
ReleaseDC(hWnd, hDC);
//DestroyWindow(hWnd);
//下面创建真正可用的OpenGL渲染环境
//第1)步:创建一个真正可用的窗口
DWORD dwExtStyle;
DWORD dwWindStyle;
dwExtStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
dwWindStyle = WS_OVERLAPPEDWINDOW;
ShowCursor(TRUE);
// Create the window again
hWnd = CreateWindowEx(dwExtStyle, // 窗体扩展风格
szClassName, // 窗体类名称
szWindowName, // 窗体名称
dwWindStyle | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,// 窗体风格
CW_USEDEFAULT, // 窗体在桌面上的位置,x坐标
CW_USEDEFAULT, // 窗体在桌面上的位置, y坐标
200, // 宽
200, // 高
NULL, // 父窗口
NULL, // 菜单
hInstance, // 实例
NULL);
hDC = GetDC(hWnd);
int nPixCount = 0;
//利用OpenGL查询函数查找最符合要求的OpenGL环境特性
//需要#include <gl/wglew.h>
int pixAttribs[] = { WGL_SUPPORT_OPENGL_ARB, GL_TRUE, //要求支持OpenGL
WGL_DRAW_TO_WINDOW_ARB, GL_TRUE, //可以绘制到某个窗口
WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, //必须完全支持硬件加速
WGL_RED_BITS_ARB, 8, //红色为8位精度
WGL_GREEN_BITS_ARB, 8, //绿色为8位精度
WGL_BLUE_BITS_ARB, 8, //蓝色为8位精度
WGL_DEPTH_BITS_ARB, 16, //深度为16位精度
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, //使用RGBA类型的像素
0}; //必须以0结尾
int nPixelFormat = -1;
//查询系统中是否存在满足要求的环境像素格式
wglChoosePixelFormatARB(hDC, &pixAttribs[0], NULL, 1, &nPixelFormat, (UINT*)&nPixCount);
if(nPixelFormat == -1)
{
//不存在满足要求的环境,则释放数据,结束程序
ReleaseDC(hWnd, hDC);
DestroyWindow(hWnd);
MessageBox(NULL,TEXT("OpenGL不支持查询的格式"),TEXT("错误"),MB_OK);
return 1;
}
else
{
//找到满足要求的格式,则设置其为当前像素格式,并创建OpenGL环境
SetPixelFormat( hDC, nPixelFormat, &pfd );
//指定OpenGL版本为3.30
GLint attribs[] = {WGL_CONTEXT_MAJOR_VERSION_ARB, 1,WGL_CONTEXT_MINOR_VERSION_ARB, 1, 0 };
hRC = wglCreateContextAttribsARB(hDC, 0, attribs);
if (hRC == NULL)
{
MessageBox(NULL,TEXT("无法创建OpenGL环境"),TEXT("错误"),MB_OK);
return 2;
}
//成功则绑定OpenGL渲染环境(hRC)至窗口设备环境(hDC)
wglMakeCurrent( hDC, hRC );
//显示窗口
ShowWindow( hWnd, SW_SHOW );
UpdateWindow(hWnd);
//使用新风格的OpenGL创建一个三角形
//新风格的OpenGL编程需要创建GLSL文件
//为了清晰,为每个步骤创建一个函数
//为了简单,全部使用全局变量
InitVertex();
SendToGPU();
CreateShader();
DrawToWindow();
}
//进入窗口主循环
MSG Msg;
//注意GetMessage第二个参数是NULL
//因为调用程序最终结束需要处理WM_QUIT消息,
//而WM_QUIT消息是进程消息,而不是窗口消息
//所以一旦指定第二个参数为g_hWnd,如果不进行特殊处理,程序将无法正常结束
while(GetMessage(&Msg,NULL,NULL,NULL))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
ReleaseDC(hWnd,hDC);
return 0;
}
3.4 运行结果
图3-1 新风格绘制图形方法
3.5参考资料
本章主要参考了《Interactive Computer Grapics 5/e》(使用了其示例程序的着色器源代码)、《OpenGL Super Bible 8/e》(参考其示例程序的绘图方法)