SDL2 游戏开发之图形渲染(续)

SDL2 图形渲染

我在以前的文章中已经简单的介绍过 SDL 绘图相关的函数,涉及的内容主要是纹理加载与纹理显示的方法,但是 SDL 提供显示相关的函数远远不止这些,这篇文章就对 SDL 库提供的其它绘制方法和方式提供一个简要的说明,希望可以帮助大家对掌握 SDL绘图方法提供一定的帮助。

1. 清屏

SDL 专门为我们提供了清屏相关的函数,在以前的文章其实已经提到了,这里再次强调一下。一般情况下,我们会在代码中这样使用清屏操作:

SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);

这里有两行代码,清屏操作是 SDL_RenderClear 函数,所以理论上仅仅调用它就可以了。但是这里有个地方需要注意,SDL_RenderClear 清屏操作使用的是当前颜色值,换句话说,SDL_RenderClear 做的操作其实是使用当前颜色绘制整个窗口,所以这个当前颜色很重要,特别是在程序中绘制大量的图形之后,你往往已经不确定当前的颜色是什么了,特别是绘制逻辑和清除逻辑分开的时候(例如绘制操作放到用户自己的逻辑中,而清除操作放到框架中)。

所以为了确保我们可以掌控自己的清屏颜色,一般情况下在清屏操作前会重新设置当前的渲染颜色。

为了保证你绘制的结果正确,推荐在任何涉及到颜色绘制之前,最好将当前颜色设置成你想要的结果,特别是对于点、线、面相关的绘制操作。

2. 几何图形绘制

在程序中,有的时候我们需要在界面上绘制一些几何图形,包括点、线、面之类的东西。SDL 专门为我们提供了一套函数:

SDL_RenderDrawLine
SDL_RenderDrawLines

SDL_RenderDrawPoint
SDL_RenderDrawPoints

SDL_RenderDrawRect
SDL_RenderDrawRects

SDL_RenderFillRect
SDL_RenderFillRects

整套函数分为四组,每组两个函数,一个函数负责单一的几何形状,另一个支持同时绘制多个几个形状,简单的就是多了一个复数版本,主要是方便操作而已。

用直线绘制举例,你可以使用 SDL_RenderDrawLine 绘制一条直线:

SDL_RenderDrawLine(renderer, 100, 100, 100, 500);

也可以通过复数的形式同时绘制多条:

#define POINTS_COUNT 4
const SDL_Point points[POINTS_COUNT] = { {100, 100}, {400, 300}, {200, 150}, {200, 300} };
SDL_RenderDrawLines(renderer, points, POINTS_COUNT);

绘制结果如下图:
绘制直线
注意当你使用 SDL_RenderDrawLines 绘制多条支线的时候,这些直线会首尾相连,直线显示的条数是你传入点坐标个数减 1 。

其它几何图形的绘制方法和直线类似,这里就不多介绍了。有一点需要注意,这些集合图形绘制的颜色是由当前渲染颜色所决定的,所以你可以通过 SDL_SetRenderDrawColor 函数来修改它们。

有一些遗憾的是 SDL 提供的几何绘制方法很少,如果你想要绘制椭圆之类的几何体就比较蛋疼了,你可能需要研究椭圆生成的公式,然后使用点或者线之类的绘制方法拼接出来,有点微积分的意思了…… 🤣

3. 像素操作

我们平时直接使用像素操作的机会不多,我能想到的可能动态纹理生成算一个,再有就是游戏中可能会用到的像素级的碰撞检测之类的。

正常情况下,SDL 如果显示一张图片的基本流程是先创建一个表面(SDL_Surface),然后再将其转换为纹理(SDL_Texture),最后你在将纹理显示到窗口上。对应这个显示流程,你有两次像素级操作的机会,一个是对表面(SDL_Surface)操作,一个是对纹理(SDL_Texture)操作。

3.1 表面级像素操作

对于表面的操作,可以使用下面的函数:

SDL_Surface* SDL_CreateRGBSurfaceFrom(void*  pixels,
                                      int    width,
                                      int    height,
                                      int    depth,
                                      int    pitch,
                                      Uint32 Rmask,
                                      Uint32 Gmask,
                                      Uint32 Bmask,
                                      Uint32 Amask);

或者另一个变种:

SDL_Surface* SDL_CreateRGBSurfaceWithFormatFrom(void*  pixels,
                                                int    width,
                                                int    height,
                                                int    depth,
                                                int    pitch,
                                                Uint32 format);

这两个函数都可以从像素直接创建表面,这些像素会被表面直接使用。换句话说,你只要改变这些像素的值,就会间接改变表面的像素。不过有一点需要注意,就是在释放像素数组之前,需要先释放表面。
这里举个例子,你可以使用下面的代码创建一个表面:

const int w = 300;
const int h = 200;
const int depth = 32; //RGBA
const int pitch = 4 * 300; //需要字节对齐
unsigned char *data = NULL;
SDL_Surface* image = NULL;

data = (unsigned char*)malloc(4 * w * h);
memset(data, 0, 4 * w * h);
image = SDL_CreateRGBSurfaceWithFormatFrom(data, w, h, depth, pitch, SDL_PIXELFORMAT_ABGR32);

然后在渲染的时候,你可以将这个表面直接转换为纹理:

SDL_Texture* tex = SDL_CreateTextureFromSurface(pModule->pRenderer, image);
SDL_Rect rt = { 100, 100, w, h };
SDL_RenderCopy(pModule->pRenderer, tex, NULL, &rt);
SDL_DestroyTexture(tex);

最后不要忘记释放操作:

SDL_FreeSurface(image);
free(data);

上面是从像素创建表面到在窗口显示的所有步骤,你可能会说这和原来从图片加载的过程差不多啊,没错,流程差不多,但是有一点是和原来不一样的,那就是你可以在游戏中实时更改像素数组(unsigned char* data)。

例如每次逻辑更新,逐步更改像素颜色:

for (int i = 0; i < w * h; i++) { ((int*)data)[i] = SDL_MapRGB(image->format, total % 255, 0, 0);
}

这里首先做了指针类型转换,因为表面的格式是 RGBA,所以转换为 32 位的 int 。接着就是使用 SDL_MapRGB 合成像素,复制到像素数组中。这样随着程序的运行,total 的值逐步增加,就会导致 R 通道的值不断从 0 到 255 递增,最终的效果会在窗口逐步显示出一个红色的矩形区域。
渐变效果

上面像素级的表面操作真正的用处在于不同格式的图片加载,因为不管是什么编码格式,你都可以将这些图片转为最终的像素格式保存到 SDL_Surface 中,这也是 SDL_Image 实现的基本原理。

还有一个点要提一下,那就是透明像素的问题。透明像素主要是有历史原因的,在过去的图片素材主要是位图,而 32 位一下的位图并没有透明通道,所以为了方便,游戏素材的透明部分会指定一个特殊的像素,这个像素被称为透明像素,主要是用这个像素值来替代透明的部分。

SDL 中可以使用 SDL_SetColorKey 来指定透明像素:

int SDL_SetColorKey(SDL_Surface* surface,
                    int          flag,
                    Uint32       key);

在默认的情况下,理论上你可以随时操作 SDL_Surface 中的像素值,但是 SDL 为了加快效率,为表面提供了一种快捷的编码方式,RLE(Run-Length-Encoding)。你可以使用 SDL_SetSurfaceRLE 来开启这个特性,开启这个特性之后,会大幅度加速透明像素和颜色混合的使用效率。不过因为编码方式的改变,你在操作像素的时候必须执行 SDL_LockSurface 函数来锁定表面,防止在使用 RLE 特性的时候,导致像素操作问题。

如果你想知道在操作像素的时候,是否需要锁定表面,可以使用 SDL_MUSTLOCK 函数进行判断:

SDL_bool SDL_MUSTLOCK(SDL_Surface* surface);

返回 SDL_TURE 表示在执行像素访问的时候必须加锁。

3.2 纹理级像素操作

除了在表面操作像素之外,你还可以更进一步,直接操作纹理的像素。纹理像素操作比较简单,直接锁定即可:

int SDL_LockTexture(SDL_Texture*    texture,
                    const SDL_Rect* rect,
                    void**          pixels,
                    int*            pitch)

函数的前两个参数是输入参数,代表要锁定的纹理以及锁定的区域,后两个参数是返回参数,代表锁定区域的像素指针以及 pitch 长度。

要想该函数执行成功,首要的条件是纹理是 SDL_TEXTUREACCESS_STREAMING 类型,如果你是使用 SDL_CreateTextureFromSurface 从表面创建纹理,则该函数会直接返回错误,因为从表面创建的纹理默认的标记是 SDL_TEXTUREACCESS_STATIC。

如果想要创建流式纹理,你需要使用 SDL_CreateTexture 函数,人工指定纹理的类型为 SDL_TEXTUREACCESS_STREAMING ,这样,你就可以在以后的逻辑中随时更改纹理的像素值了。

3.3 其它

除了上述的像素操作之外,SDL 还提供了一个函数 SDL_RenderReadPixels,使用它可以直接从渲染目标中读取指定区域的像素数据:

int SDL_RenderReadPixels(SDL_Renderer*   renderer,
                         const SDL_Rect* rect,
                         Uint32          format,
                         void*           pixels,
                         int             pitch)

不过文档上说这种操作效率很慢,请谨慎使用。

4. 颜色与混合

4.1 Alpha 通道

SDL 中包含多种像素格式,不过无外乎 RGBA 四个通道的不同组合而已。你可以通过 Αlpha 相关的函数来操作图片或纹理的显示效果:

int SDL_GetSurfaceAlphaMod(SDL_Surface* surface, Uint8* alpha);
int SDL_GetTextureAlphaMod(SDL_Texture* texture, Uint8* alpha);
int SDL_SetSurfaceAlphaMod(SDL_Surface* surface, Uint8 alpha);
int SDL_SetTextureAlphaMod(SDL_Texture* texture, Uint8 alpha);

如果像素格式支持 Alpha 通道,则最终颜色显示效果计算公式为:

srcA = srcA * (alpha / 255)

4.2 颜色调制

和 Alpha 通道类似,其它通道颜色也可以通过函数来进行调整:

int SDL_GetSurfaceColorMod(SDL_Surface* surface, Uint8* r, Uint8* g, Uint8* b);
int SDL_SetSurfaceColorMod(SDL_Surface* surface, Uint8 r, Uint8 g, Uint8 b);
int SDL_GetTextureColorMod(SDL_Surface* texture, Uint8* r, Uint8* g, Uint8* b);
int SDL_SetTextureColorMod(SDL_Texture* texture, Uint8 r, Uint8 g, Uint8 b);

r、g、b 分别对应的不同通道。最终的效果和 Alpha 类似:

srcC = srcC * (color / 255)

4.3 颜色混合

官方支持三种类型的混合模式,分别对应一种函数:

int SDL_SetRenderDrawBlendMode(SDL_Renderer* renderer, SDL_BlendMode blendMode)
int SDL_SetSurfaceBlendMode(SDL_Surface* surface, SDL_BlendMode blendMode)
int SDL_SetTextureBlendMode(SDL_Texture* texture, SDL_BlendMode blendMode)

三种函数的语义类似,唯一不同的地方在于混合的目标不同,从上到下分别对应渲染设备、表面以及纹理。而第二个参数,混合模式的类型是一样的,如下表所示:

SDL_BLENDMODE_NONEno blending
 dstRGBA = srcRGBA
SDL_BLENDMODE_BLENDalpha blending
 dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA))
 dstA = srcA + (dstA * (1-srcA))
SDL_BLENDMODE_ADDadditive blending
 dstRGB = (srcRGB * srcA) + dstRGB
 dstA = dstA
SDL_BLENDMODE_MODcolor modulate
 dstRGB = srcRGB * dstRGB
 dstA = dstA

对于 SDL_SetRenderDrawBlendMode 函数而言,设置混合模式之后对应使用的函数主要是 SDL_RenderDraw~以及 SDL_RenderFill~ 之类的函数。

对于 SDL_SetSurfaceBlendMode 函数而言,设置混合模式之后对应使用的函数主要是 SDL_BlitSurface 以及 SDL_BlitScaled 之类的函数。

对于 SDL_SetTextureBlendMode 函数而言,设置混合模式之后对应使用的函数主要是 SDL_RenderCopy 以及 SDL_RenderCopyEx 之类的函数。

具体怎么选择,还需要看你具体的程序逻辑,目前来说在程序中使用颜色混合的情况还是比较少见的。

大佬,给点反馈?

平均评分 / 5. 投票数:

很抱歉,这篇文章不能帮助到你

请让我们改进这篇文章

告诉我们我们如何改善这篇文章?

发表评论

邮箱地址不会被公开。 必填项已用*标注