EasyX实现镜面反射倒影效果

最近通关了《闪避刺客》,非常不错的游戏,爽快的战斗、宏大但细节拉满的演出与同品类游戏中让人印象深刻。无论是室外还是室内,游戏场景中多次使用了类似下图所示的静面反射效果,用来烘托氛围和叙事;在《刀剑神域》中,也有类似的倒影地面,本文分享使用EasyX实现镜面反射效果的思路。

闪避刺客(左)/ 刀剑神域(右)

在图形学中,一提到“镜子”,最直接的思路一定是帧缓冲,比较通用且简单的思路就是使用额外的摄像机渲染场景到纹理中,然后再将其贴到场景中的物体上。在EasyX中,我们可以使用IMAGE对象实现帧缓冲纹理,并借助SetWorkingImage函数切换当前的渲染目标,整体流程如下图所示:

镜面反射场景渲染流程

首先是游戏场景的渲染,这里我们使用了itch上Free Sky BackgroundsFree Guns for Cyberpunk Characters作为测试素材,前者包含天空的四层视差效果贴图,后者提供了玩家角色默认、奔跑和跳跃三种状态的序列帧动画。

原始素材资源

四张不同层次的天空背景图是可以实现连续滚动的视差效果的,简单来说的实现思路可以是随着玩家移动,不同图层的背景移动速度不同,这样就可以实现相对立体的效果;而人物角色的三种状态动画可以使用状态机来实现,这部分代码细节与本文主题无关,所以不再详述。

在渲染游戏场景时,我们预留了窗口下方的一部分,用来绘制倒影效果,也就是这个样子:

游戏场景内容

随后对帧缓冲图片进行竖直翻转,实现思路与《EasyX游戏帧动画效果处理》中提到的水平翻转相似,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void FlipImageVertical(IMAGE* img_src, IMAGE* img_dst)
{
int width = img_src->getwidth();
int height = img_src->getheight();
Resize(img_dst, width, height);
DWORD* color_buffer_src = GetImageBuffer(img_src);
DWORD* color_buffer_dst = GetImageBuffer(img_dst);
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int idx_src_img = y * width + x;
int idx_dst_img = (height - y - 1) * width + x;
color_buffer_dst[idx_dst_img] = color_buffer_src[idx_src_img];
}
}
}

这时,将翻转后的帧缓冲贴在窗口的下半区域,也就是这样的画面:
绘制原始倒影

这样倒影的感觉基本上就出来了,但是倒影倒影,本质是“影”,直接翻转图片进行渲染会让下方的倒影和上方的画面本体虚实一致,效果还是差强人意。我们再次观察文章开始时给出的两张图片可以看出,《闪避刺客》中室内的地板倒影,比起上方的实景亮度和饱和度都有所变化,而在《刀剑神域》中,还加上了透明度的渐变(动漫影视作品中的摄像机属性和游戏中的常有不同,我们更多的是借鉴左图游戏画面中的镜面倒影思路)。

用于区分倒影和实景的思路可以有很多种,我们这里可以将翻转后的帧缓冲进行一个竖直方向的线性灰度渐变,原理上与直接覆盖一张透明度渐变的纯白矩形图片是一样的,我们使用region_height参数来控制渐变区域的高度,MAX_RATIO常量控制渐变强度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void MakeImageVerticalLinearGradientGray(IMAGE* img, float region_height)
{
int width = img->getwidth();
int height = img->getheight();
DWORD* color_buffer = GetImageBuffer(img);
static const float MAX_RATIO = 0.85f;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int idx = y * width + x;
DWORD color = color_buffer[y * width + x];
const float RATIO = min(y, region_height) / region_height * MAX_RATIO;
// 注意,由于色彩缓冲中的颜色为 BGR 顺序,所以需要交换 R 和 B 位置
BYTE r = (BYTE)(GetBValue(color) * RATIO + 255 * (1 - RATIO));
BYTE g = (BYTE)(GetGValue(color) * RATIO + 255 * (1 - RATIO));
BYTE b = (BYTE)(GetRValue(color) * RATIO + 255 * (1 - RATIO));
color_buffer[idx] = BGR(RGB(r, g, b));
}
}
}

这时效果就好了很多:

线性灰度处理后的倒影

最后,我们在游戏主循环渲染中不断执行之前提到的流程,便可以实现不错的动态效果:

1
2
3
4
5
6
7
8
9
10
11
static IMAGE img_frame_buffer(WINDOW_WIDTH, 
WINDOW_HEIGHT - MIRROR_HEIGHT); // 帧缓冲
static IMAGE img_frame_buffer_flipped; // 竖直翻转后的帧缓冲

SetWorkingImage(&img_frame_buffer); cleardevice(); // 切换渲染目标为帧缓冲
RenderScene(); // 渲染游戏场景
SetWorkingImage(NULL); cleardevice(); // 切换渲染目标为窗口
putimage(0, 0, &img_frame_buffer); // 将帧缓冲渲染到窗口
FlipImageVertical(&img_frame_buffer, &img_frame_buffer_flipped); // 竖直翻转帧缓冲
MakeImageVerticalLinearGradientGray(&img_frame_buffer_flipped, 100); // 处理帧缓冲线性灰度渐变
putimage(0, WINDOW_HEIGHT - MIRROR_HEIGHT, &img_frame_buffer_flipped); // 渲染处理后的倒影

镜像倒影最终效果

作者

Voidmatrix

发布于

2024-01-03

更新于

2024-01-03

许可协议

评论