SDL中的函数需要先初始化SDL才能用 :

 //Initialize SDL
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
    {
        printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() );
    }

1. 这里SDL_INIT_VIDEO是SDL库中的flag, 因为教程里面只用到了有关于video的部分, 所以只初始化了这一部分.

2. 这里还要注意初始化失败SDL_Init()返回的是-1.

3. SDL_GetError()在之后的教程中很常用,  只要是SDL库中的函数未实现预期的结果, 几乎都可以用这个函数来获取错误信息. 光从这个接口来看的话, SDL库内部应该是用专门用一个指针维护了一块内存, 每次出现错误时, 更新内存中的字符串为最新的错误信息, 而GetError()的作用就是返回这个指针..

 

初始化之后创建窗口:

//Create window
        window = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
        if( window == NULL )
        {
            printf( "Window could not be created! SDL_Error: %s\n", SDL_GetError() );
        }

1. 这儿window是SDL_Window*类型, Window不出意外应该是个结构体, 代表的就是程序窗口.

2. SDL_CreateWindow()接收6个参数, 第一个是窗口的标题; 接下来两个参数定义的是窗口左上角的位置, 如果输入0, 0 的话, 窗口左上角将和屏幕的左上角重合. 这里还要注意这里的坐标系中, 原点是左上角, x轴向右增长, y轴向下增长; 再下面两个是宽和高, 宽就是x轴方向的长度, 高就是y方向的长度. 最后一个是flag, SDL_WINDOW_SHOWN保证窗口一旦创建即可见.

3. 如果有错的话返回空指针, 之后会发现很多函数都是这样, 同样, 这里也用到了SDL_GetError, 其实只要是SDL库中的, 都是靠这个函数打印错误...

 

创建窗口之后, 就要创建窗口的surface:

 screenSurface = SDL_GetWindowSurface( window );

1. 这个screenSurface是SDL_Surface*类型的, SDL_Surface我个人的理解是, 可以当成是一张画布.

2. 同时要知道这张画布比较特殊, 因为它是绑定在该window上的, 你所有对于其他surface的操作, 最终都要弄到这个surface上才有效果.

 

我们暂时忽略一些操作函数不讲, 先来看看最后那些函数.

//Update the surface
            SDL_UpdateWindowSurface( window );

1. 这里一个很重要的概念在于, 你对window所绑定的那个surface做了改变之后, 并不会立即反映在屏幕上, 必须要使用SDL_UpdateWindowSurface(window)来更新该window他才能把你所画在画布上的显示到桌面上.

 

//Destroy window
    SDL_DestroyWindow( window );

1. window其实是一种资源, 当你不使用的时候需要释放掉, 这就是释放窗口的方法.

2. 另外值得一提的是 : 之后你会看到, surface也是需要释放的, 该窗口绑定的那个surface已经被上面这个函数释放掉了, 所以我们不需要释放, 但是对于其他非绑定surface, 需要我们自己释放.

 

//Quit SDL subsystems
    SDL_Quit();

1. 这里只需要记住, 通常来讲要求初始化的最后都要求显式退出.

 

接下来介绍一些操作函数 :

//Load splash image
    gHelloWorld = SDL_LoadBMP( "02_getting_an_image_on_the_screen/hello_world.bmp" );
    if( gHelloWorld == NULL )
    {
        printf( "Unable to load image %s! SDL Error: %s\n", "02_getting_an_image_on_the_screen/hello_world.bmp", SDL_GetError() );
        success = false;
    }

1. gHelloWorld也是SDL_Surface* 类型, 这里SDL_LoadBMP()函数接收一个const char * 的指针, 要求该指针指向的字符串表示图片的路径, 如果成功的话, 将返回一个已经加载好图片的SDL_Surface指针, 这里我们就是用这个gHelloWorld(之前初始化为NULL了)来保存这个指针.

2. 如果不是绝对路径的话, 这个路径的起点一般是你的可执行文件所在的目录, 当然vs中刻意修改过, 这个路径与 vcxproj 文件所在路径相同.

 

 //Deallocate surface
    SDL_FreeSurface( gHelloWorld );
    gHelloWorld = NULL;

1. 就像我之前所说的, 除了那个window绑定的SDL_Surface指针会在window销毁时随之销毁, 其他的都需要我们手动销毁.

 

//Apply the image
SDL_BlitSurface( gHelloWorld, NULL, gScreenSurface, NULL );

1. SDL_BlitSurface()把这个已经加载好图片的SDL_Surface传输到窗口绑定的Surface上去.

2.  该函数的第一个参数表示源surface, 第三个参数表示目标surface, 当然都是指针而不是surface本身...(暂时不用考虑第二和第四个参数)

3.  就像之前所说的, 完成这一步之后还需要SDL_UpdateWindowSurface()才可以在屏幕上显示. 关于这一点, 是因为目前的大多数渲染系统都是双缓冲的, 显示在桌面的是前缓冲, 而你用SDL_BlitSurface传输的数据其实传输到了后缓冲上, 而这个SDL_UpdateWindowSurface()就是起到了交换缓冲的作用. 双缓冲的好处就在于你从屏幕上看不到图像的绘制过程, 使得图像看起来是在瞬间完成的(实际上是在后缓冲完成后被直接交换到屏幕上).

 

然后来介绍一下SDL中的事件 :

 while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }
                }

1. e是SDL_Event类型, 也就是事件, 比如你移动鼠标, 按下键盘都是事件, 每次操作都将产生SDL_Event类型的结构体存放在事件池中.

2. SDL_PollEvent(&e)接收一个SDL_Event的指针, 然后把最近发生的事件放到这个指针所指向的空间中, 这里就是放到e里面.

3. 这里的SDL_QUI是当用户点击窗口右上角的×的时候触发的事件的类型...

 

 //User presses a key
                    else if( e.type == SDL_KEYDOWN )
                    {
                        //Select surfaces based on key press
                        switch( e.key.keysym.sym )
                        {
                            case SDLK_UP:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_UP ];
                            break;

                            case SDLK_DOWN:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_DOWN ];
                            break;

                            case SDLK_LEFT:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_LEFT ];
                            break;

                            case SDLK_RIGHT:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_RIGHT ];
                            break;

                            default:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_DEFAULT ];
                            break;
                        }

1. 这里可以看到另外一种事件类型SDL_KEYDOWN, 虽然教程上说只要按下键盘上的键就能触发, 但是我实践发现只有类似上, 下, 左, 右, esc, ctrl, alt 等等之类的功能键会触发...

2. e.key里面是一些关于这个键事件的信息, e.key.keysym是关于那个键被按下的信息, e.key.keysym.sym 是关于被按下的键的SDL Keycode的信息, 比如代码中给出了上下左右的Keycode.

 

我们上面对于图片的传输是直接按位进行传输, 效率比较低(当然加载一张图片看不出来),  这里预先改变图片的格式再来进行传输从而达到加速的效果 :

//Convert surface to screen format
        optimizedSurface = SDL_ConvertSurface( loadedSurface, gScreenSurface->format, NULL );
        if( optimizedSurface == NULL )
        {
            printf( "Unable to optimize image %s! SDL Error: %s\n", path.c_str(), SDL_GetError() );
        }

        //Get rid of old loaded surface
        SDL_FreeSurface( loadedSurface );

1. 要理解这种优化我们需要介绍一些背景知识 : 我们所加载的bmp(bitmap)格式的图片通常来讲是24bit的, 然后现代的显示器通常都不是24bit, 例如对于32bit的显示器来讲, 那么我们每次调用上面所提到的SDL_BlitSurface()函数的时候都需要进行一次格式转换, 所以更好的方式是一旦一张图片加载成功就直接先将它格式转换为显示器相符的格式.

2. 这里的SDL_ConvertSurface()就是这个作用, 他接受三个参数, 第一个是源Surface, 第二个是显示器的格式, 这里gScreenSurface和Window是绑定的, 同时在SDL_Surface的结构体内部正好有格式, 所以它代表的就是显示器格式, 第三个参数我也不懂.

3. 另外一点值得注意的是, 这个SDL_ConvertSurface()返回的是转换好格式的Surface的指针, 但这是一个新的Surface, 所以我们现在可以释放掉之前那个Surface的资源了.

 

图片传输还有最后一点, 那就是图片的缩放, 在SDL2中有专门的函数来实现这一目的 :

//Apply the image stretched
                SDL_Rect stretchRect;
                stretchRect.x = 0;
                stretchRect.y = 0;
                stretchRect.w = SCREEN_WIDTH;
                stretchRect.h = SCREEN_HEIGHT;
                SDL_BlitScaled( gStretchedSurface, NULL, gScreenSurface, &stretchRect );

1. 首先我们看到的是SDL_Rect类型, 如果按照我的理解可以把这个类型看作是一个长方形的框架, 其内部有四个属性, 其中x, y代表这个框架在屏幕上的x坐标和y坐标(当然这里的坐标系统和前面一模一样), 之后两个参数就是框架的宽和高(长方形框架), 之后我们会经常碰到这种类型.

2. SDL_BlitScaled()有四个参数, 第一个代表源surface也就是你加载了图片资源的那个Surface,   第三个是目的Surface, 最后一个参数是框架的指针. 接下来SDL_BlitScaled()完成的工作就是按照最后一个参数所指的框架的位置以及大小的格式来将源Surface中的资源缩放到目标Surface, 所以完成之后目标surface中的那张图位置和大小与框架相同, 内容不变.

 

到这里你知道你有没有觉得SDL支持的图片格式有点单一, 这里引出了SDL的拓展库SDL_image.(要到网上额外下载的) :

 //Initialize PNG loading
            int imgFlags = IMG_INIT_PNG;
            if( !( IMG_Init( imgFlags ) & imgFlags ) )
            {
                printf( "SDL_image could not initialize! SDL_image Error: %s\n", IMG_GetError() );
                success = false;
            }

1. 类似于SDL, SDL_image也需要加载, 上面是假设我们只需要PNG的部分, 然后加载函数IMG_Init将返回一个加载结果的flag, 这个flag中如何对于PNG的那一位为0, 则说明PNG加载失败.

2. 因为SDL_image并不在SDL的基本库中, 所以它的报错函数有所不同, 但是使用方法是想通的, IMG_GetError()(之前那个是SDL_GetError()).

3. 这里不知道为什么虽然只加载了png, 但是我实际发现jpg, gif(但是没有动图效果)也是可以用的.

 

//Load image at specified path
    SDL_Surface* loadedSurface = IMG_Load( path.c_str() );

1. 这就是SDL_image中加载图片的函数, 其实和SDL_LoadBMP()用法以及作用都是相同的, 只是扩展了可以加载的图片类型而已.

2. 另外完成这一步之后, 仍然可以使用SDL_ConvertSurface(loadSurface, gScreenSurface->format, NULL) 来预先改变图片格式来进行优化加速.

 

我们上面一直在用Surface, 但是SDL2提供了一套纹理渲染API, 也就是texture rendering, surface和texture有如下几点区别 (区别收集自网络):

1. 一般来说, surface的加载顺序是 :文件 -> 内存 -> 显示,而texture的加载顺序是:文件-> 内存 -> 显存 -> 显示.

2.  从上过程可以看出, surface都是存在系统内存中, 而texture存在显存中, 所以texture的加载比surface多了一步, 但也只有texture可以享受硬件加速的好处.

 

当然要使用texture就要用来到渲染器 :

gRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_ACCELERATED );

1. 这个函数就是用来生成一个与窗口绑定的渲染器, 然后返回指向该渲染器的指针, 所以gRenderer是SDL_Renderer*类型.

2. 渲染器可以理解为画笔, 一般在SDL初始化完成创建好窗口之后进行初始化(也就是生成绑定窗口的surface的位置).

 

接下来是如何将一张加载好的图片渲染到texture上:

newTexture = SDL_CreateTextureFromSurface( gRenderer, loadedSurface );
        if( newTexture == NULL )
        {
            printf( "Unable to create texture from %s! SDL Error: %s\n", path.c_str(), SDL_GetError() );
        }

        //Get rid of old loaded surface
        SDL_FreeSurface( loadedSurface );

1. 虽然说texture好处多多, 但是从上述过程可以看出, 如果要加载图片最开始还是要经过内存的, 而这里使用的就是先用一个临时的surface来加载图片.

2. 加载好之后将surface渲染为texture的方法就是调用SDL_CreateTextureFromSurface(gRenderer, loadedSurface)函数, 这个函数的第一个参数就是渲染器了, 第二个参数则是已经加载好图片的surface, 返回值是一个指向texture的指针.

3. 因为我们最好要显示直接靠texture就可以了, 所以临时的surface在texture生成之后就可以释放了, 同时也因为这一点, 我想应该是没必要使用SDL_ConvertSurface()改变图片格式来优化了.

 

//Free loaded image
    SDL_DestroyTexture( gTexture );
    gTexture = NULL;

    //Destroy window    
    SDL_DestroyRenderer( gRenderer );
    SDL_DestroyWindow( gWindow );
    gWindow = NULL;
    gRenderer = NULL;

1. 当然, 在程序结束的时候, 要记住显式的释放texture和renderer(不同于绑定在窗口上的surface不用显示释放).

 

然后是texture的显示 :

//Initialize renderer color
SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );

//Clear screen
SDL_RenderClear( gRenderer );

1. 之后的教程在显示部分都会使用较新的renderer渲染texture的形式, 所以之前的surface搭配SDL_BlitSurface()的形式会比较少.

2. SDL_SetRenderDrawColor()其实就是设置画笔(gRenderer就是这里的画笔), 后面三个参数分别是RGBA.

3. SDL_RenderClear()使用画笔目前的颜色清屏, 在本例中的效果就是屏幕编程白色(但是现在还不会显示出来).

 

//Render texture to screen
SDL_RenderCopy( gRenderer, gTexture, NULL, NULL );

//Update screen
SDL_RenderPresent( gRenderer );

1. 清屏之后为了把Texture加载上去, 我们就要用到这个SDL_RenderCopy()的函数, 这个函数提供四个参数, 第一个是渲染器, 第二个是要加载的texture, 第三个不清楚, 第四个在后面会提到, 其实我们之前也用过, 如果你记得我之前提过的SDL_Rect, 在改变图片的大小那里使用到的, SDL_BlitScaled函数的最后一个参数, 其实就是用来提供渲染的位置以及大小的, 这里设置为NULL的效果就是让图片填满整个窗口.

2. 最后这个函数作用其实和SDL_UpdateWindowSurface()的作用相同, 只不过这个函数用于renderer, texture这套api, 我之前说SDL_RenderClear()显示不出来就是因为还没有调用该函数.

 

这套texture API 还提供了一些基本的图形绘制函数:

//Clear screen
SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
SDL_RenderClear( gRenderer );

//Render red filled quad
SDL_Rect fillRect = { SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 };
SDL_SetRenderDrawColor( gRenderer, 0xFF, 0x00, 0x00, 0xFF );        
SDL_RenderFillRect( gRenderer, &fillRect );

1. 前两行是先设置背景色, 之前以及提过这里就不再赘述.

2. 这里我们又用到了SDL_Rect, 里面的四个参数分别是x, y, 宽, 高, 主要定义图形的位置和大小, 之前也说过.

3. 这里又使用了SDL_SetRenderDrawColor()来设置颜色, 因为之前设置背景色的时候已经使用了白色, 如果这里不设置的话, 画出了也是白色的, 那就看不出来了.

4. SDL_RenderFillRect()参数很简单, 效果的话就是填充这个长方形里面为当前的画笔颜色. 记住这里是填充, 而不只是用该颜色画出边框.

 

//Render green outlined quad
SDL_Rect outlineRect = { SCREEN_WIDTH / 6, SCREEN_HEIGHT / 6, SCREEN_WIDTH * 2 / 3, SCREEN_HEIGHT * 2 / 3 };
SDL_SetRenderDrawColor( gRenderer, 0x00, 0xFF, 0x00, 0xFF );        
SDL_RenderDrawRect( gRenderer, &outlineRect );

1. 这里和上面唯一的区别就在于最后一个函数是SDL_RenderDrawRect(), 这个是画而不是填充, 效果是只画出边框, 里面还是背景色, 要特别遇上边那个函数区别开来.

 

//Draw blue horizontal line
                SDL_SetRenderDrawColor( gRenderer, 0x00, 0x00, 0xFF, 0xFF );        
                SDL_RenderDrawLine( gRenderer, 0, SCREEN_HEIGHT / 2, SCREEN_WIDTH, SCREEN_HEIGHT / 2 );

1. 这个函数的效果从名字就可以看出来, 就是画线.

2. 前两个参数给出了画笔的起点位置, 后两个参数是画笔终点的位置, 这条线会把这两个点连起来.

 

SDL_RenderDrawPoint( gRenderer, SCREEN_WIDTH / 2, i );

1. 除了画线当然还可以画点, 后两个参数是点的左边.

 

当然你不是任何时候都希望在整个窗口中上只显示一张图片, 所以下面引入了viewport的概念:

//Top left corner viewport
                SDL_Rect topLeftViewport;
                topLeftViewport.x = 0;
                topLeftViewport.y = 0;
                topLeftViewport.w = SCREEN_WIDTH / 2;
                topLeftViewport.h = SCREEN_HEIGHT / 2;
                SDL_RenderSetViewport( gRenderer, &topLeftViewport );
                
                //Render texture to screen
                SDL_RenderCopy( gRenderer, gTexture, NULL, NULL );

1. 前面几行换汤不换药, SDL_Rect而已.

2. SDL_RenderSetViewport()函数的效果简单来说就是把你的render的操作对象暂时变成你在SDL_Rect中设置的那一块区域, 使用后如果你在后面再使用SDL_RenderCopy(), 虽然最后一个参数是NULL, 但是他并不会默认全屏显示该图片, 而是按照你viewport中设置的的位置和大小进行显示.

 

最后是Color Keying, 翻译过来叫抠图, 意思就是把指定的颜色吸收掉, 露出图层的下一层的颜色, 比如你想在一个背景是蓝色的天空上弄一个太阳的图片, 但是无奈太阳图片中有黑色的背景, 为了让太阳自然地挂在蓝色的天空上, 使得只有太阳而没有黑色的背景, 就必须使用color Keying 技术.

 

//Color key image
SDL_SetColorKey( loadedSurface, SDL_TRUE, SDL_MapRGB( loadedSurface->format, 0, 0xFF, 0xFF ) );

1. SDL_SetColorKey()函数有三个参数, 第一个表示加载有图片的源surface(所以surface虽然不直接在窗口显示上使用, 但是在该库的使用过程中还是挺实用的), 第二个表示我们是否激活抠图(渣英语, 这里不太理解), 最后一个是我们需要抠掉的像素点.

2. 要想创造出我们需要的扣掉的像素点可以使用 SDL_MapRGB(), 该函数的第一个参数是这个这些像素点所处的格式, 因为图片加载在loadedSurface中, 所以自然是由loadedSurface来提供, 后面三个组成RGB, 这里代表的是青色, 那个图中所有青色的部分都会变得透明.(所以其实这里假设的是背景是青色的)

3. 函数产生的更改是直接在loadedSurface上进行的, 所以不会产生额外的Surface, 只需要调用SDL_CreateTextureFromSurface()来把这个surface渲染成texture就好.

 

最后要提醒的是如果真的要在蓝天上挂一个太阳, 那么应该先用SDL_RenderCopy(gRenderer, texture, NULL, &renderQuad); 渲染蓝天, 再渲染太阳, 如果顺序反了那么太阳就会被蓝天盖住, 也就显示不出来了.

posted on 2016-07-28 14:59  内脏坏了  阅读(1299)  评论(0编辑  收藏  举报