OpenGL 编程入门
OpenGL 是一个著名的标准,用于生成强大的、功能多样的 3D 以及 2D 图形。OpenGL 由 OpenGL 架构审查委员会 (ARB) 定义和发布。
本文是对 OpenGL 的入门介绍,旨在帮助您理解如何使用 OpenGL 进行绘图。
在撰写本文时,OpenGL 的最新版本是 4.4,它使用的绘图技术与本文介绍的不同。尽管如此,本文的目的是帮助您理解 OpenGL 的理念,而不是教您如何使用最新的 OpenGL 版本进行编码。因此,本文提供的源代码可以在安装了较旧 OpenGL 版本的 Linux 机器上运行。
安装 OpenGL如果您在 Debian 7 系统上运行以下命令来查找所有包含 “opengl” 单词的软件包,您将获得大量输出(图 1)
$ apt-cache search opengl

图 1. 运行 apt-cache search opengl
Linux 有许多免费的 OpenGL 实现,但您只需要一个。我安装了 FreeGLUT,因为它最新。FreeGLUT 是 OpenGL Utility Toolkit (GLUT) 库的开源替代品。
root@mail:~# apt-get install freeglut3 freeglut3-dev libglew-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following package was automatically installed
and is no longer required:
fonts-liberation
Use 'apt-get autoremove' to remove it.
The following extra packages will be installed:
libgl1-mesa-dev libglu1-mesa-dev libice-dev libpthread-stubs0
libpthread-stubs0-dev libsm-dev libx11-dev libx11-doc
libxau-dev libxcb1-dev libxdmcp-dev libxext-dev libxt-dev
mesa-common-dev x11proto-core-dev x11proto-input-dev
x11proto-kb-dev x11proto-xext-dev xorg-sgml-doctools xtrans-dev
Suggested packages:
libice-doc libsm-doc libxcb-doc libxext-doc libxt-doc
The following NEW packages will be installed:
freeglut3 freeglut3-dev libgl1-mesa-dev libglu1-mesa-dev
libice-dev libpthread-stubs0 libpthread-stubs0-dev libsm-dev
libx11-dev libx11-doc libxau-dev libxcb1-dev libxdmcp-dev
libxext-dev libxt-dev mesa-common-dev x11proto-core-dev
x11proto-input-dev x11proto-kb-dev x11proto-xext-dev
xorg-sgml-doctools xtrans-dev
0 upgraded, 22 newly installed, 0 to remove and 0 not upgraded.
Need to get 7,651 kB of archives.
After this operation, 24.9 MB of additional disk space
will be used.
Do you want to continue [Y/n]?
您还需要一个 C++ 编译器来编译代码。
最后,您可能需要安装 mesa-utils 软件包,以便能够使用 glxinfo 命令。
# apt-get install mesa-utils
glxinfo 命令显示有关您的 OpenGL 安装的有用信息,您可以在以下输出中看到
...
GLX version: 1.4
GLX extensions:
GLX_ARB_get_proc_address, GLX_ARB_multisample,
GLX_EXT_import_context, GLX_EXT_texture_from_pixmap,
GLX_EXT_visual_info, GLX_EXT_visual_rating,
GLX_MESA_copy_sub_buffer, GLX_MESA_multithread_makecurrent,
GLX_OML_swap_method, GLX_SGIS_multisample, GLX_SGIX_fbconfig,
GLX_SGIX_pbuffer, GLX_SGI_make_current_read
OpenGL vendor string: VMware, Inc.
OpenGL renderer string: Gallium 0.4 on llvmpipe
(LLVM 3.4, 128 bits)
OpenGL version string: 2.1 Mesa 10.1.3
OpenGL shading language version string: 1.30
OpenGL extensions:
...
Mesa 是一个 3D 图形库,其 API 与 OpenGL 的 API 非常相似,几乎无法区分。
OpenGL 管线图 2—取自《OpenGL Shading Language》一书(又名“橙皮书”)—展示了带有顶点和片段处理器的可编程 OpenGL 管线。正如您可以想象的那样,OpenGL 管线很复杂,但您不必完全理解它也能使用 OpenGL。管线展示了 OpenGL 在后台如何运作。较新版本的 OpenGL 管线甚至更加复杂!

图 2. OpenGL 架构
OpenGL 是一个大型状态机。大多数对 OpenGL 函数的调用都会修改您无法直接访问的全局状态。
旨在 OpenGL 可编程处理器之一上执行的 OpenGL Shading Language 代码称为着色器 (Shader)。OpenGL Shading Language 源于 C 语言(本文的范围不包括介绍 OpenGL Shading Language)。
OpenGL 没有定义窗口层,因为它试图保持平台中立,并将此功能留给操作系统。操作系统必须提供一个接受命令的渲染上下文,以及一个保存绘图命令结果的帧缓冲区。
矩阵代数广泛应用于 3D 图形中,因此了解如何加、乘、减和除矩阵对您有好处,尽管您不需要自己编写此类操作的代码。熟悉 3D 坐标并能够在纸上草绘您尝试绘制的 3D 场景也很有用。
绘制三角形现在是时候编写一些真正的 OpenGL 代码了。列表 1 中的代码在执行时,会使用 OpenGL 在屏幕上绘制一个三角形。
列表 1. triangle.cc
// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a triangle.
#include "GL/freeglut.h"
#include "GL/gl.h"
void drawTriangle()
{
glClearColor(0.4, 0.4, 0.4, 0.4);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);
glBegin(GL_TRIANGLES);
glVertex3f(-0.7, 0.7, 0);
glVertex3f(0.7, 0.7, 0);
glVertex3f(0, -1, 0);
glEnd();
glFlush();
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE);
glutInitWindowSize(500, 500);
glutInitWindowPosition(100, 100);
glutCreateWindow("OpenGL - Creating a triangle");
glutDisplayFunc(drawTriangle);
glutMainLoop();
return 0;
}
列表 1 中用于设置 OpenGL 的代码量很大,但您只需要学习一次。
在 Debian 7 系统上,以下命令编译了 triangle.cc OpenGL 程序,没有任何错误消息
$ g++ triangle.cc -lglut -o triangle
在 Ubuntu Linux 系统上,相同的命令产生了以下错误消息
/usr/bin/ld: /tmp/ccnBP5li.o: undefined reference to symbol
↪'glOrtho'
//usr/lib/x86_64-linux-gnu/mesa/libGL.so.1: error adding
//symbols: DSO missing from command line
collect2: error: ld returned 1 exit status
解决方案是通过将可执行文件链接到额外的库 (-lGL) 来编译 triangle.cc 程序
mtsouk@mtsouk-VirtualBox:/media/sf_OpenGL.LJ/code$ g++
↪triangle.cc -lglut -lGL -o triangle
libGL.so 库接受 OpenGL 命令,并确保它们以某种方式显示在屏幕上。如果您的显卡没有 3D 加速,libGL 包含一个软件渲染器,它将 2D 图像作为输出提供给 X Window System。Mesa 就是这种情况。如果存在 GLX 扩展,libGL 也可以将 OpenGL 信息传递给 X Window System。然后,X Window System 可以借助 Mesa 进行软件渲染,或者使用硬件加速。
可执行文件的输出将生成图 3 中所示的三角形。triangle.cc 的正确编译证明您的 Linux 系统可以用于开发 OpenGL 应用程序。

图 3. 使用 OpenGL 绘制三角形
使用 OpenGL 绘制三角形的方法不止一种,主要取决于您使用的 OpenGL 版本。虽然本文介绍的方法是一种较旧的 OpenGL 应用程序编程方式,但我发现它简单易懂。请记住,无论您使用哪种方法,三角形的坐标都将是相同的。
注意:请记住,本文最重要的部分是代码!
使用 OpenGL 绘制立方体接下来,让我们编写一个使用 OpenGL 绘制立方体的应用程序。您需要使用三角形来构建立方体。一个立方体有六个面,每个面至少需要定义两个三角形。我说“至少”的原因是,一般来说,如果您想获得更平滑、更精确的形状,可以使用更多三角形,但在绘制立方体时,这是不必要的。我相信您已经意识到,您总共需要 12 个三角形。
一个立方体也有八个顶点。每个三角形都需要定义三个不同的顶点。
列表 2 显示了 cube.cc 文件的完整源代码。
列表 2. cube.cc
// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a colorful cube
// that rotates as you move the arrow keys!
//
// g++ cube.cc -lm -lglut -lGL -lGLU -o cube
#define GL_GLEXT_PROTOTYPES
#ifdef __APPLE__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include <math.h>
// Rotate X
double rX=0;
// Rotate Y
double rY=0;
// The coordinates for the vertices of the cube
double x = 0.6;
double y = 0.6;
double z = 0.6;
void drawCube()
{
// Set Background Color
glClearColor(0.4, 0.4, 0.4, 1.0);
// Clear screen
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Reset transformations
glLoadIdentity();
// Rotate when user changes rX and rY
glRotatef( rX, 1.0, 0.0, 0.0 );
glRotatef( rY, 0.0, 1.0, 0.0 );
// BACK
glBegin(GL_TRIANGLES);
glColor3f(0.4, 0.3, 0.5);
glVertex3f(x, y, z);
glVertex3f(x, -y, z);
glVertex3f(-x, y, z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.5, 0.3, 0.2);
glVertex3f(-x, -y, z);
glVertex3f(x, -y, z);
glVertex3f(-x, y, z);
glEnd();
// FRONT
// Using 4 trianges!
glBegin(GL_TRIANGLES);
glColor3f(0.1, 0.5, 0.3);
glVertex3f(-x, y, -z);
glVertex3f(0, 0, -z);
glVertex3f(-x, -y, -z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.0, 0.5, 0.0);
glVertex3f(-x, -y, -z);
glVertex3f(0, 0, -z);
glVertex3f(x, -y, -z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.1, 0.3, 0.3);
glVertex3f(-x, y, -z);
glVertex3f(x, y, -z);
glVertex3f(0, 0, -z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.2, 0.2, 0.2);
glVertex3f(0, 0, -z);
glVertex3f(x, y, -z);
glVertex3f(x, -y, -z);
glEnd();
// LEFT
glBegin(GL_TRIANGLES);
glColor3f(0.3, 0.5, 0.6);
glVertex3f(-x, -y, -z);
glVertex3f(-x, -y, z);
glVertex3f(-x, y, -z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.5, 0.5, 0.5);
glVertex3f(-x, y, z);
glVertex3f(-x, -y, z);
glVertex3f(-x, y, -z);
glEnd();
// RIGHT
glBegin(GL_TRIANGLES);
glColor3f(0.2, 0.2, 0.2);
glVertex3f(x, y, z);
glVertex3f(x, y, -z);
glVertex3f(x, -y, z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.0, 0.0, 0.0);
glVertex3f(x, -y, -z);
glVertex3f(x, y, -z);
glVertex3f(x, -y, z);
glEnd();
// TOP
glBegin(GL_TRIANGLES);
glColor3f(0.6, 0.0, 0.0);
glVertex3f(x, y, z);
glVertex3f(x, y, -z);
glVertex3f(-x, y, -z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.6, 0.1, 0.2);
glVertex3f(-x, y, z);
glVertex3f(x, y, z);
glVertex3f(-x, y, -z);
glEnd();
// BOTTOM
glBegin(GL_TRIANGLES);
glColor3f(0.4, 0.0, 0.4);
glVertex3f(-x, -y, -z);
glVertex3f(-x, -y, z);
glVertex3f(x, -y, z);
glEnd();
glBegin(GL_TRIANGLES);
glColor3f(0.3, 0.0, 0.3);
glVertex3f(x, -y, -z);
glVertex3f(-x, -y, -z);
glVertex3f(x, -y, z);
glEnd();
glFlush();
glutSwapBuffers();
}
void keyboard(int key, int x, int y)
{
if (key == GLUT_KEY_RIGHT)
{
rY += 15;
}
else if (key == GLUT_KEY_LEFT)
{
rY -= 15;
}
else if (key == GLUT_KEY_DOWN)
{
rX -= 15;
}
else if (key == GLUT_KEY_UP)
{
rX += 15;
}
// Request display update
glutPostRedisplay();
}
int main(int argc, char **argv)
{
// Initialize GLUT and process user parameters
glutInit(&argc, argv);
// Request double buffered true color window with Z-buffer
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(700,700);
glutInitWindowPosition(100, 100);
// Create window
glutCreateWindow("Linux Journal OpenGL Cube");
// Enable Z-buffer depth test
glEnable(GL_DEPTH_TEST);
// Callback functions
glutDisplayFunc(drawCube);
glutSpecialFunc(keyboard);
// Pass control to GLUT for events
glutMainLoop();
return 0;
}
图 4 显示了 cube.cc 应用程序的两个不同屏幕截图。该程序的左侧屏幕截图仅显示一个正方形,因为您正对着立方体的正面,看不到立方体的其他五个面。因此,您在屏幕上看到的形状看起来像是 2D 的。当您开始使用箭头键旋转立方体时,您可以看出它是一个 3D 形状。

图 4. cube.cc 的图形输出
代码解释每个三角形由三个顶点定义。每个顶点借助单个点 (x,y,z) 定义。每个点都带有三个数字(坐标),因为 OpenGL 使用 3D 空间。一个立方体需要定义八个顶点。
作为练习,立方体的正面使用四个三角形制成。图 5 显示了使用四个三角形定义立方体正面时的坐标。点 (0,0,-0.6) 是任意选择的。您只需要一个属于立方体正面的点。

图 5. 立方体正面的坐标
定义顶点
图 6 显示了 x=0.6、y=0.6 和 z=0.6 时立方体顶点的坐标。请注意,定义边的顶点,其三个坐标中有两个坐标完全相同。

图 6. 立方体的顶点
如图 6 所示,定义正面的四个顶点的坐标和定义背面的四个顶点的相应坐标仅在 z 轴坐标的值上有所不同。
定义三角形
三角形基于顶点。每个三角形都需要定义三个顶点。定义立方体的每个面都需要两个三角形,除了正面使用了四个三角形。以下命令基于 x、y 和 z 的值创建一个彩色三角形
glBegin(GL_TRIANGLES);
glColor3f(0.4, 0.0, 0.4);
glVertex3f(-x, -y, -z);
glVertex3f(-x, -y, z);
glVertex3f(x, -y, z);
glEnd();
更改颜色
您可以使用 glColor3f(...)
命令更改形状的颜色。glColor3f(...)
命令接受三个参数,这些参数表示所需颜色的 RGB 值。
更改视角
您可以使用以下命令更改场景的视角
glRotatef(rX, 1.0, 0.0, 0.0);
glRotatef(rY, 0.0, 1.0, 0.0);
当用户按下四个箭头键之一时,视角会相应地改变。
绘制立方体
绘制立方体是逐面进行的。除了正面使用了四个三角形外,每个面都需要绘制两个三角形。
一旦您正确获得了三角形的坐标,绘制就非常容易了。每个三角形的绘制都以 glBegin(GL_TRIANGLES)
命令开始,以 glEnd()
命令结束。GL_TRIANGLES
是一个 OpenGL 图元。其他图元类型包括 GL_POINTS
、GL_LINES
、GL_LINE_STRIP
、GL_LINE_LOOP
、GL_TRIANGLE_STRIP
、GL_TRIANGLE_FAN
、GL_QUADS
、GL_QUAD_STRIP
和 GL_POLYGON
。最终,OpenGL 中的每个图元形状都由一个或多个三角形表示。
如果您使用三角形 (GL_TRIANGLES
) 绘制,则顶点放置的顺序并不重要。如果您使用矩形 (GL_POLYGON
) 绘制,则矩形的四个顶点必须按正确的顺序绘制,尽管顺时针 (CW) 或逆时针 (CCW) 绘制都没关系。如果绘制顺序错误,您尝试绘制的矩形将有一个大洞。
使用箭头键
使用箭头键非常简单。以下代码检查箭头键并做出相应的操作
void keyboard(int key, int x, int y)
{
if (key == GLUT_KEY_RIGHT)
{
rY += 15;
}
else if (key == GLUT_KEY_LEFT)
{
rY -= 15;
}
else if (key == GLUT_KEY_DOWN)
{
rX -= 15;
}
else if (key == GLUT_KEY_UP)
{
rX += 15;
}
// Request display update
glutPostRedisplay();
}
keyboard(...)
函数在 main(...)
函数中使用以下代码行注册
glutSpecialFunc(keyboard);
自动旋转立方体
作为奖励,让我们看看如何使立方体自动旋转(图 7)。
这次,立方体使用矩形绘制。由于立方体有六个面,因此只需要六个矩形。使用矩形绘制更容易,需要的代码更少,但当使用三角形时,复杂的 OpenGL 代码运行速度更快。
注意:每个对象都可以拆分为三角形,但三角形不能拆分为三角形以外的任何东西。

图 7. rotateCube.cc 的输出
列表 3 显示了 rotateCube.cc 的源代码。
列表 3. rotateCube.cc
// Programmer: Mihalis Tsoukalos
// Date: Wednesday 04 June 2014
//
// A simple OpenGL program that draws a triangle
// and automatically rotates it.
//
// g++ rotateCube.cc -lm -lglut -lGL -lGLU -o rotateCube
#include <iostream>
#include <stdlib.h>
// the GLUT and OpenGL libraries have to be linked correctly
#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
using namespace std;
// The coordinates for the vertices of the cube
double x = 0.6;
double y = 0.6;
double z = 0.6;
float angle = 0.0;
void drawCube()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Reset transformations
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0, 0.0, -5.0);
// Add an ambient light
GLfloat ambientColor[] = {0.2, 0.2, 0.2, 1.0};
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientColor);
// Add a positioned light
GLfloat lightColor0[] = {0.5, 0.5, 0.5, 1.0};
GLfloat lightPos0[] = {4.0, 0.0, 8.0, 1.0};
glLightfv(GL_LIGHT0, GL_DIFFUSE, lightColor0);
glLightfv(GL_LIGHT0, GL_POSITION, lightPos0);
glTranslatef(0.5, 1.0, 0.0);
glRotatef(angle, 1.0, 1.0, 1.0);
glRotatef( angle, 1.0, 0.0, 1.0 );
glRotatef( angle, 0.0, 1.0, 1.0 );
glTranslatef(-0.5, -1.0, 0.0);
// Create the 3D cube
// BACK
glBegin(GL_POLYGON);
glColor3f(0.5, 0.3, 0.2);
glVertex3f(x, -y, z);
glVertex3f(x, y, z);
glVertex3f(-x, y, z);
glVertex3f(-x, -y, z);
glEnd();
// FRONT
glBegin(GL_POLYGON);
glColor3f(0.0, 0.5, 0.0);
glVertex3f(-x, y, -z);
glVertex3f(-x, -y, -z);
glVertex3f(x, -y, -z);
glVertex3f(x, y, -z);
glEnd();
// LEFT
glBegin(GL_POLYGON);
glColor3f(0.5, 0.5, 0.5);
glVertex3f(-x, -y, -z);
glVertex3f(-x, -y, z);
glVertex3f(-x, y, z);
glVertex3f(-x, y, -z);
glEnd();
// RIGHT
glBegin(GL_POLYGON);
glColor3f(0.0, 0.0, 0.0);
glVertex3f(x, -y, -z);
glVertex3f(x, -y, z);
glVertex3f(x, y, z);
glVertex3f(x, y, -z);
glEnd();
// TOP
glBegin(GL_POLYGON);
glColor3f(0.6, 0.0, 0.0);
glVertex3f(x, y, z);
glVertex3f(-x, y, z);
glVertex3f(-x, y, -z);
glVertex3f(x, y, -z);
glEnd();
// BOTTOM
glBegin(GL_POLYGON);
glColor3f(0.3, 0.0, 0.3);
glVertex3f(-x, -y, -z);
glVertex3f(-x, -y, z);
glVertex3f(x, -y, z);
glVertex3f(x, -y, -z);
glEnd();
glFlush();
glutSwapBuffers();
}
// Function for increasing the angle variable smoothly,
// keeps it <=360
// It can also be implemented using the modulo operator.
void update(int value)
{
angle += 1.0f;
if (angle > 360)
{
angle -= 360;
}
glutPostRedisplay();
glutTimerFunc(25, update, 0);
}
// Initializes 3D rendering
void initRendering()
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_COLOR_MATERIAL);
// Set the color of the background
glClearColor(0.7f, 0.8f, 1.0f, 1.0f);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_NORMALIZE);
}
// Called when the window is resized
void handleResize(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0, (double)w / (double)h, 1.0, 200.0);
}
int main(int argc, char **argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(700, 700);
glutInitWindowPosition(100, 100);
glutCreateWindow("OpenGL - Rotating a Cube");
initRendering();
glutDisplayFunc(drawCube);
glutReshapeFunc(handleResize);
// Add a timer for the update(...) function
glutTimerFunc(25, update, 0);
glutMainLoop();
return 0;
}
请注意,triangle.cc、cube.cc 和 rotateCube.cc 的 main(...)
函数非常相似,尽管这三个程序执行不同的任务。
这里的关键是 glutTimerFunc(...)
函数的用法。它为 update(...)
函数注册一个定时器回调,该回调将在指定的毫秒数后触发。update(...)
函数每次被调用时都会更改场景的角度。
OpenGL 编程并不容易,但本文应该可以帮助您快速开始编写 OpenGL 应用程序。如果您想精通 OpenGL,请坚持练习编写更多的 OpenGL 程序。OpenGL 入门容易,但精通难。
致谢我要感谢 Nikos Platis 博士与我分享了他的一小部分 OpenGL 知识。
资源OpenGL: http://www.opengl.org
学习现代 3D 图形编程: http://www.arcsynthesis.org/gltut
OpenGL 超级宝典,第 6 版,Graham Sellers, Richard S. Wright 和 Nicholas Haemel 著,Addison Wesley 出版,ISBN: 0321902947
GLEW: http://glew.sourceforge.net
Mesa 3D 图形库: http://www.mesa3d.org