试一试
让我们先来看看我们的第一个OpenGL程序。下载“basic shapes”测试程序,编译并运行它(怎样用Visual Studio编译看第一节课程)。程序的运行样例如下图,点击ESC可以退出程序。
程序是怎样运行起来的
程序是怎样运行起来的呢?最基本的思想就是我们告知OpenGL每个图形顶点的3D坐标。OpenGL使用标准的x,y坐标系,x水平指向右方,y竖直指向上方。但在3D中我们还需要一个z坐标系,它指向屏幕。

OpenGL是怎样利用3D坐标的呢?它模仿人类眼睛的工作机理,看下面这张图片。

OpenGL在画图前都将所有的3D点转化为一个像素坐标,正如上面这张图片所描述的,它为了将3D点转化为一个像素坐标,其通过将此3D点与眼睛联系,然后会与屏幕有一个焦点,此点就转化为了屏幕上的像素坐标。所以,当OpenGL要画一个三角形的时候,它将3D坐标转化为像素坐标,然后画一个“2D”的三角形。
用户的“眼睛”总位于原点,看向z轴的负方向。当然,OpenGL不会画出任何在“眼睛”后面的图像的。
屏幕离我们的眼睛到底有多远呢?其实这个真的不重要,无聊屏幕离我们有多远,一个给定的3D点都会定位到固定的像素点上,关键是我们眼睛的角度。
源代码
关于像素坐标的事情都是上面所说的,但是作为程序员,我们更想看看代码,让我们看看main.cpp吧。
你关注的第一件事就是指明此代码的License,此网站上的所有代码都是免费的,你甚至可以将其应用到商业工程上去。
第二件事就是你会发现代码中有许多注释,虽然有些碍眼,但这是第一节课。后面的课程中的代码注释将会减少。
让我们看一下代码,看看我们是否已经弄懂他们了。
#include <iostream>
#include <stdlib.h> //Needed for "exit" function
//Include OpenGL header files, so that we can use OpenGL
#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
首先,包含我们的头文件。标准C++的头文件。如果我们使用的是MAC,那么我们的程序还需要包含“OpenGL/OpenGL.h”和“GLUT/glut.h”两个头文件;否则我们只包含"GL/glut.h"一个头文件
using namespace std;
这一行代码位于main.cpp的顶端。它仅仅是让我们不用在写代码的过程中写太多的std::
//Called when a key is pressed
void handleKeypress(unsigned char key, //The key that was pressed
int x, int y) { //The current mouse coordinates
switch (key) {
case 27: //Escape key
exit(0); //Exit the program
}
}
这段代码处理用户的键盘触发事件。这段代码所做的就是当用户点击ESC时,程序将会退出。此函数还会传递鼠标坐标的信息,但我们并没有进行处理。
//Initializes 3D rendering
void initRendering() {
//Makes 3D drawing work when something is in front of something else
glEnable(GL_DEPTH_TEST);
}
initRendering函数初始化我们的渲染参数。现在,它做的很少。当我们初始化初始化渲染时,我们总是用glEnable(GL_DEPTH_TEST)。此函数表面某物体会出现在已经画过的物体之前,这也是我们希望的。
注意glEnable,就像OpenGL函数一样,它也是gl开头。
//Called when the window is resized
void handleResize(int w, int h) {
//Tell OpenGL how to convert from coordinates to pixel values
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION); //Switch to setting the camera perspective
//Set the camera perspective
glLoadIdentity(); //Reset the camera
gluPerspective(45.0, //The camera angle
(double)w / (double)h, //The width-to-height ratio
1.0, //The near z clipping coordinate
200.0); //The far z clipping coordinate
}
当窗口改变大小的时候,handleResize函数就会被调用。w和h就是新的宽度和高度。handleResize的内容在我们的其他工程中改变很小,我们不需要关系太多。
这里有很多事情需要关注。45.0表示用户的“眼睛可以看到的角度”。1.0指明当物体的z坐标大于-1时,物体无法成像到屏幕上;200.0表示到物体的z坐标小于-200时,也无法成像。
那么,为什么gltPerspective是以“glu”开头的而不是以“gl”开头的那?那是因为技术上面他是GLU(GL Utility)函数。另外还有“gl”和“glu”,有些函数将“glut”作为前缀。其实我们无需但系OpenGL,GLU和GLUT的不同。
//Draws the 3D scene
void drawScene() {
//Clear information from last draw
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene函数是3D画图真正起作用的地方。首先,我们调用glClear去清除上次我们的画图信息。在大多数OpenGL程序中,我们都这样使用。
glMatrixMode(GL_MODELVIEW); //Switch to the drawing perspective
glLoadIdentity(); //Reset the drawing perspective
现在,我们先不去管这两行代码,下一节课我们将讨论到,它们涉及到了转化。
glBegin(GL_QUADS); //Begin quadrilateral coordinates
//Trapezoid
glVertex3f(-0.7f, -1.5f, -5.0f);
glVertex3f(0.7f, -1.5f, -5.0f);
glVertex3f(0.4f, -0.5f, -5.0f);
glVertex3f(-0.4f, -0.5f, -5.0f);
glEnd(); //End quadrilateral coordinates
这里我们开始了程序的主要部分。这部分画了一个梯形,我们调用glBegin(GL_QUADS)告诉OpenGL我们要画一个四边形,之后我们调用glVertex3f函数标定了4个3D坐标。当我们调用glVertex3f时,我们将3个浮点型值输入。当我们画完一个四边形的时,我们调用glEnd()。注意glBegin和glEnd必须配对。
在左右顶点坐标数值后的f都是强制编译器将其视为浮点型。技术上我并不觉得这样做有必要,但写代码时还是继续在做。
glBegin(GL_TRIANGLES); //Begin triangle coordinates
//Pentagon
glVertex3f(0.5f, 0.5f, -5.0f);
glVertex3f(1.5f, 0.5f, -5.0f);
glVertex3f(0.5f, 1.0f, -5.0f);
glVertex3f(0.5f, 1.0f, -5.0f);
glVertex3f(1.5f, 0.5f, -5.0f);
glVertex3f(1.5f, 1.0f, -5.0f);
glVertex3f(0.5f, 1.0f, -5.0f);
glVertex3f(1.5f, 1.0f, -5.0f);
glVertex3f(1.0f, 1.5f, -5.0f);
OpenGL会自动将三个坐标组成一组,每一组构成一个三角形。
//Triangle
glVertex3f(-0.5f, 0.5f, -5.0f);
glVertex3f(-1.0f, 1.5f, -5.0f);
glVertex3f(-1.5f, 0.5f, -5.0f);
最后,我们再画一个三角形。我们并没有调用glEnd()来告知OpenGL我们已经画完了,所以我们可以再画一个三角形。
glEnd(); //End triangle coordinates
我们已经画完三角形了,所以可以调用glEnd()。
其实我们可以通过调用四次glBegin()和glEnd()函数来画出四个三角形,但这样效率会很低,我们不应当这样做。
除了GL_TRIANGLES和GL_QUADS,我们还可以像更多其它的参数,但这两个变量是最常用的。
glutSwapBuffers(); //Send the 3D scene to the screen
}
这段代码使OpenGL将结果显示在屏幕上,我们画完图基本上都要调用此代码。
int main(int argc, char** argv) {
//Initialize GLUT
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(400, 400); //Set the window size
//Create the window
glutCreateWindow("Basic Shapes - videotutorialsrock.com");
initRendering(); //Initialize rendering
这是程序的主功能,我们一开始初始化GLUT,一些相似的东西将会在我们的程序中出现,我们不需要关心它们。当调用glutInitWindowSize(400,400),我们指明窗口的大小是400×400的,当我们调用glutCreateWindow函数,我们告知窗口的名称。initRendering函数则是我们上面介绍的初始化渲染的函数。
//Set handler functions for drawing, keypresses, and window resizes
glutDisplayFunc(drawScene);
glutKeyboardFunc(handleKeypress);
glutReshapeFunc(handleResize);
现在我们将原来写的处理按键、画图和屏幕改变的函数指针赋值给GLUT,有一件是必须注意:在drawScene之外我们不能调用任何的画图的代码。
glutMainLoop(); //Start the main loop. glutMainLoop doesn't return.
return 0; //This line is never reached
}
最后我们调用glutMainLoop,此函数告知GLUT做它该做的事情。