任天堂 64 缺乏加法混合效果?这项技术让特效无溢出伪影!
任天堂 64 上的加法混合效果
各位是否曾疑惑,为什么原版 PlayStation 上的爆炸和其他特效看起来比任天堂 64(Nintendo 64)上的要酷炫得多?原因就在于加法混合(additive blending),或者更确切地说,是 N64 缺乏这种效果。虽然 N64 实际上支持加法混合,但在实际应用中却几乎无法使用。
PlayStation
PlayStation(PSX)支持 4 种不同的混合模式(除了直接覆盖像素之外),用于控制精灵(sprite)和几何图形如何与现有的帧缓冲区(frame buffer)进行混合:
0: (源颜色 + 目标颜色) / 2
1: 源颜色 + 目标颜色
2: 目标颜色 - 源颜色
3: 目标颜色 + 源颜色 / 4
在《沉默轰炸机》中使用的是概念上最简单的模式:`源颜色 + 目标颜色`。也就是说,颜色会直接添加到帧缓冲区中现有的颜色上。
| R | G | B |
| 源颜色(精灵) | 171 | 42 | 226 |
| + 目标颜色(帧缓冲区) | 63 | 141 | 170 |
| = 结果 | 234 | 183 | 255 |
在场景上绘制精灵只会让画面变得更亮,而不会变暗。这非常适合用于爆炸、等离子束和魔法咒语等特效。值得注意的是,在这个例子中,`B` 值相加为 `396`,但 PSX 的图形处理器(GPU)会将其限制在 `255` 的最大值范围内。(顺便提一下,PSX 的 GPU 实际上只使用 16 位精度,每个颜色分量为 5 位,因此值的范围是 `0 .. 31`,但原理是一样的。)
任天堂 64
N64 的“现实显示处理器”(Reality Display Processor,简称 RDP,一种固定功能的光栅化器)有一种更灵活的方式来控制混合效果:可配置的“颜色组合器”(Color Combiner)。这有点类似于 OpenGL 的 `glBlendFunc()` 函数。
[Libdragon] 通过 `RDPQ_BLENDER((P, A, Q, B))` 宏来实现这一功能,该宏指示 RDP 执行 `(P * A) + (Q * B)` 操作,其中每个“插槽”可以是几种输入之一。
使用这个宏来设置加法混合非常简单:
RDPQ_BLENDER(( IN_RGB, IN_ALPHA, MEMORY_RGB, ONE ))
问题在于,RDP 不会对结果进行限制。
| R | G | B |
| 源颜色(精灵) | 171 | 42 | 226 |
| + 目标颜色(帧缓冲区) | 63 | 141 | 170 |
| = 结果 | 234 | 183 | 140 |
^
溢出了!
这样得到的输出效果并不理想。
当然,你可以选择在 N64 的向量协处理器“现实信号处理器”(Reality Signal Processor,简称 RSP)上绘制这些特效。但如果你想进行旋转、缩放或任何实际的 3D 操作,这很快就会变得复杂起来。而 RDP 更适合做这些事情,显示本来就是它的职责!
虽然 RDP 可以绘制到 32 位缓冲区中,但游戏很少这样做。几乎所有 N64 游戏最终输出都使用 16 位帧缓冲区。不过,考虑到这一点,想出了一个不同的方案:让 RDP 绘制到一个 32 位的 `RGBA 8888`(每个分量 8 位)缓冲区中,但将所有精灵限制在 16 位的 `RGBA 5551`(每个颜色分量 5 位,1 位透明度)范围内。可以通过将 RGB 值除以 `8`(或者右移 `3` 位)来对资源进行预处理。这样做会让所有东西看起来太暗,但反过来也为加法混合提供了很大的空间。
当所有加法混合的精灵结果都小于 255 时,不会发生溢出。
更棒的是,不需要离线进行图像预处理。可以在绘制时让颜色组合器为完成这个任务,而且是免费的!
// 利用雾的透明度值,以 1/8 的强度绘制所有颜色
rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8));
rdpq_mode_blender(RDPQ_BLENDER(( IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE )));
那么,如何将其恢复到正常亮度呢?很简单:使用 16 位帧缓冲区进行显示,并将所有 32 位颜色“复制”到其中。只需要小心地将所有 8 位颜色分量限制在 5 位范围内。
void cpu_rgba_8888_to_5551(uint32_t *rgba32_in, uint16_t *rgba16_out) {
for (int i = 0; i < 320 * 240; i++) {
color_t c = color_from_packed32(rgba32_in[i]);
if (c.r > 31) { c.r = 31; }
if (c.g > 31) { c.g = 31; }
if (c.b > 31) { c.b = 31; }
rgba16_out[i] = (c.r << 11) | (c.g << 6) | (c.b << 1) | 0x1;
}
}当然,在 CPU 上执行这个操作成本非常高。对于一个 320×240 的帧,大约需要 70 毫秒。但这正是 RSP 协处理器发挥优势的地方。现在问题变得简单多了。
RSP 的 128 位向量指令可以一次处理 8 个像素。在 #N64Brew Discord 上的 HailToDodongo 的帮助下,对 GPU 微代码进行了优化,现在整个帧的处理时间约为 3.1 毫秒!(小知识:在 N64 的语境中,通常所说的“GPU 微代码”实际上是在 RSP 上运行的 MIPS 汇编代码,或者就像最近开始称呼的那样,MIPS 加汇编。)
现代的 N64 开发工具非常出色。虽然了解一些汇编知识会有帮助,但不再需要手动编写 MIPS 汇编代码了。HailToDodongo 发明了一种类似 C 语言的语言 [RSPL],它可以直接编译成汇编代码。
所以整个设置如下:
// 使用 16 位帧缓冲区初始化显示
display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, FILTERS_DISABLED);
// 创建一个 32 位的辅助渲染缓冲区,并将其设置为渲染目标
surface_t render32 = surface_alloc(FMT_RGBA32, 320, 240);
rdpq_set_color_image(render32);
// 配置颜色组合器,以 1/8 的强度绘制
rdpq_set_fog_color(RGBA32(0, 0, 0, 256/8));
rdpq_mode_blender(RDPQ_BLENDER((IN_RGB, FOG_ALPHA, MEMORY_RGB, ONE)));
// 绘制包含大量加法混合精灵的场景
render_scene();
// 在 RSP 上启动从 32 位渲染缓冲区到 16 位帧缓冲区的转换
rsp_rgba_8888_to_5551(render32->buffer, screen->buffer);
// 显示 16 位帧缓冲区
display_show(screen);
这样就可以得到大量没有溢出伪影的加法混合精灵特效。
当然,大多数游戏一开始就使用 16 位帧缓冲区是有原因的:N64 的内存吞吐量非常糟糕。与 16 位缓冲区相比,绘制到 32 位缓冲区几乎需要两倍的时间,因为 RDP 必须从存储在 RDRAM 中的帧缓冲区中来回传输两倍的数据。
尽管如此,这种技术的效果比预期的要好。对于某些应用来说,它肯定足够好了。还看到了进一步优化的潜力,比如只将需要加法混合的精灵绘制到 32 位缓冲区中,甚至可以降低分辨率,然后在 RSP 上将其与场景的其他 16 位缓冲区合并……
提醒各位,上述视频的简单演示项目可以在 GitHub 上找到。
