第36课 - 在SDL中使用OpenGL


更新时间:2013年10月17日


编者注:本教程最后一课由一名德国留学生翻译,详见页脚。
总有些时候,SDL的渲染会满足不了我们的需求。当你需要更强大、更快速、更灵活的图像渲染时,就是OpenGL登场的时间了。这一课会介绍在SDL中使用OpenGL的基础方法。
#include "SDL/SDL.h"
#include "SDL/SDL_opengl.h"
首先,我们得记得包含SDL的OpenGL的头文件(指的是"SDL/SDL_opengl.h"文件——译者注),以便我们可以在SDL中使用OpenGL渲染。
//方形(square)的类
class Square
{
    private:
    //偏移量(offset)
    int x, y;
    
    //方形的速度(velocity)
    int xVel, yVel;
    
    public:
    //初始化
    Square();
    
    //处理键盘按键消息
    void handle_input();
    
    //移动方块
    void move();
    
    //在屏幕上显示方块
    void show();
};
这是一个方块的类,我们将在屏幕上通过按键移动这个方块。Nothing has changed from the overall structure of the class.

在SDL中使用OpenGL进行渲染的时候,消息处理、音频、时间以及线程处理都与平常是一样的,被更换掉的只有渲染的部分而已。
bool init()
{
    //初始化SDL
    if( SDL_Init( SDL_INIT_EVERYTHING ) < 0 )
    {
        return false;    
    }
    
    //创建窗口
    if( SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_OPENGL ) == NULL )
    {
        return false;
    }
    
    //初始化OpenGL
    if( init_GL() == false )
    {
        return false;    
    }
    
    //设置窗口标题
    SDL_WM_SetCaption( "OpenGL Test", NULL );
    
    return true;    
}
这就是我们的init()函数。

首先,就像往常一样我们初始化了SDL,然后我们创建了一个OpenGL的窗口。如果我们需要在SDL中使用OpenGL的话,我们就需要在调用SDL_SetVideoMode()这个函数时,用SDL_OPENGL作为参数替换掉SDL_SWSURFACE。现在,窗口就会使用OpenGL的硬件来加速渲染,而不是使用SDL的软件渲染了。不过这也意味着,我们不能再使用SDL的blit功能对screen的内容进行更改了。

然后,我们调用init_GL()函数(这个函数的实现接下来马上就会详细介绍)来初始化OpenGL。最后,我们设置了窗口的标题。
bool init_GL()
{
    //设置用于清屏的颜色
    glClearColor( 0, 0, 0, 0 );
这是我们用于初始化OpenGL的函数的头几行,我们最最首先要做的事情就是设置了用于清屏的颜色。

我们希望使用黑色进行清屏,所以我们设置了清屏色为黑色(红0,绿0,蓝0,Alpha值0),并且将数据作为参数调用glClearColor()函数。
    //设置投影(projection)
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    glOrtho( 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, -1, 1 );
下一步,我们得设置我们的投影矩阵。投影矩阵控制了所有的物体是如何在屏幕上显示的。

首先,我们调用glMatrixMode()函数,将矩阵的模式设置为投影。然后,我们调用glLoadIdentity()函数来初始化投影矩阵。glLoadIdentity()实际上就是将当前操作的矩阵设置为单位矩阵。

如果你没看懂我在说些什么的话,不要着急,我之后会解释什么是矩阵的。

现在,我们的投影矩阵已经初始化完毕了。接下来就可以设置我们的投影了,我们通过调用glOrtho()将投影的方式设置为正投影(也就是二维的)。而如果我们想使用三维的投影方式的话,可以查阅gluPerspective()这个函数的资料。

现在我们想要一个2D的投影方式,并且让坐标系与使用SDL的渲染时相同。所以我们设置了一个像这样的坐标系:

我们以上图所示的方式设置了左、右、下、上的参数,模拟了SDL渲染的坐标系。如果你想使用普通的坐标系(笛卡尔坐标系)的话,只需要把上和下的参数调换即可。

第5个和第六个参数是用于处理z轴的。那么z轴是个什么东西呢?x轴是左右方向的,而y轴则是上下方向的,那么z轴就是前后方向的。第5个参数表示极近处(near)的z轴坐标为-1,而第6个参数则是设定了极远处(far)的z轴坐标为1。

我们在本篇教程里面不会对z轴进行详细的解释。
    //初始化模型视图(modelview)矩阵
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
接下来,我们像初始化投影矩阵一样,初始化模型视图矩阵。

模型视图矩阵控制了画在屏幕上的物体(的位置、旋转以及大小——译者注)。在方块类的show()成员函数中,我们会看到一个例子。
    //如果有错误的话
    if( glGetError() != GL_NO_ERROR )
    {
        return false;    
    }
    
    //如果所有的初始化都成功了
    return true;
}
在init_GL()函数的最后,我们调用glGetError()函数来检查一下是否有错误。如果没有错误的话,这个函数会返回true。
void Square::show()
{    
    //移动到偏移量处
    glTranslatef( x, y, 0 );
这是我们方块类中show()成员函数的内容。现在,我们可以做一些OpenGL的渲染了。

首先我们先调用glTranslate()函数将方块平移到它的偏移量定义的位置上去。头两个参数分别是x和y的偏移量,最后一个则是z轴上的偏移量。因为我们不需要让方块在z轴上进行平移,所以我们把它设置为0。
    //开始绘制四边形
    glBegin( GL_QUADS );
    
        //将颜色设置为白色
        glColor4f( 1.0, 1.0, 1.0, 1.0 );
现在可以开始绘制我们的方形了。

通过调用glBegin()函数和GL_QUADS参数来开始方形的绘制。我们想绘制一个白色的方形,所以我们将颜色设置为白色。

你可能会觉得奇怪,为什么我们的颜色参数是(红1,绿1,蓝1,Alpha值1),为什么不是(红255,绿255,蓝255,Alpha值255)呢?这是因为glColor4f()这个函数使用浮点数作为颜色值,所以实际上它代表的是(红100%,绿100%,蓝100%,Alpha值100%)。需要指出的是,glClearColor()中使用的也是浮点颜色值。

另外需要指出的是在函数名glColor4f中的“4”。许多OpenGL的函数都有不同的版本,而这些基于参数的类型和数量命名的尾缀可以将不同版本的函数区分开来。

例如,前一段代码的glTranslatef()接受的参数是float类型。我们也可以使用glTranslated(),这样参数就必须是double类型。
        //绘制四边形
        glVertex3f( 0,            0,             0 );
        glVertex3f( SQUARE_WIDTH, 0,             0 );
        glVertex3f( SQUARE_WIDTH, SQUARE_HEIGHT, 0 );
        glVertex3f( 0,            SQUARE_HEIGHT, 0 );
	    
    //结束绘制
    glEnd();
现在就可以绘制方块的顶点了(所谓的顶点其实就是方块的四个角)。

为了绘制每个顶点,我们可以调用glVertex3f()函数。我们根据以下的顺序绘制顶点:左上角、右上角、右下角、左下角。
而在所有顶点绘制结束之后,我们通过调用glEnd()函数来通知OpenGL可以从以上给出的顶点生成四边形了。
    //重置
    glLoadIdentity();
}
在show()函数的最后,我们调用glLoadIdentity()函数来将模型视图矩阵重置为单位矩阵。

那么什么是矩阵呢?看下面这一组数据:
[ 1, 0, 0, 0 ]
[ 0, 1, 0, 0 ]
[ 0, 0, 1, 0 ]
[ 0, 0, 0, 1 ]

这只是4×4的一组数字,但是它又是非常重要的。它可以量化地控制顶点进行平移、旋转以及缩放。

只要我们调用了glTranslate()函数,我们实际上就是更改了矩阵。如果我们没有通过调用glLoadIdentity()函数来重置矩阵的话,当我们再次调用glTranslate()时,我们所作的变换就是基于上一次变换的基础之上,而不是基于最初始的位置进行变换。
        //当有事件需要处理时
        while( SDL_PollEvent( &event ) )
        {
            //处理按键消息
            square.handle_input();
            
            //处理用户退出消息
            if( event.type == SDL_QUIT )
            {
                quit = true;
            }
        }
	    
        //移动方块
        square.move();
	    
        //清屏
        glClear( GL_COLOR_BUFFER_BIT );
	    
        //显示方块
        square.show();
	    
        //刷新屏幕
        SDL_GL_SwapBuffers();
	    
        //调整帧率
        if( fps.get_ticks() < 1000 / FRAMES_PER_SECOND )
        {
            SDL_Delay( ( 1000 / FRAMES_PER_SECOND ) - fps.get_ticks() );
        }
    }
这就是我们的主循环(Main Loop)。

首先,像我们经常做的那样,我们处理消息,然后移动方块。然后使用glClear()函数来清屏,紧接着我们将方块显示在屏幕上。为了刷新屏幕中的内容,我们需要调用SDL_GL_SwapBuffers()函数,而不是SDL_Flip()函数,因为我们用的是OpenGL的渲染。最后我们通过SDL_Delay()函数的调用调整帧率,并且继续主循环。
呼~我们做了很多的工作,仅仅为了在屏幕上显示一个方块。这样你也许也就明白了为什么初学者的教程要使用SDL的渲染了。OpenGL既拥有强大的渲染能力,但是相较于SDL渲染来说,它也更加难以掌握并使用。

到此为止,我们的教程就全部结束了。现在我仍然在学习OpenGL,一旦我掌握了OpenGL的使用方法,我还会发一大批OpenGL的教程。但是你要是问我什么时候能发布这些教程,我只能说你得等好长一段时间了。我会在新闻页面上发布教程的更新情况,所以请你不要轰炸我的邮箱/(´Д`)/

你也可以去看看 NeHe这篇教程。NeHe中的教程文章虽然使用的是Win32API(并非SDL),但是实际的OpenGL的代码大多数都是一样的。NeHe中大多数的教程页面下方也有使用SDL的实现方法,它们叫"Linux/SDL" ports。由于SDL具有跨平台的特性,所以这些SDL源代码应该也能在Windows下面或者OS X下面跑起来。不过SDL的实现方法已经有一段时间没有更新了,所以想要保留原来的样子就直接编译的话,很可能编译不通过。

现在我已经发布了一个 学习指导,你也可以去看一下。它并不如我的教程深,但是也希望它足够带你入门了。

这里下载这篇教程的源代码与所需的媒体。

记住,先读一下readme.txt,以确定你需要链接哪些库文件。