跳到正文

像素调色板 ​

三消的美术不需要画得多细,只要 32 个颜色互相不打架,整屏看起来就成立。

有限调色板 ​

本作的图集用 Dawnbringer 32——一位像素画作者设计的 32 色通用调色板。它覆盖暗部、肤色、草木、金属和几档高饱和点缀,画任何题材都够用。

限定颜色数量有三个直接收益。第一是协调:32 个颜色是被人挑过一遍的,放在一起不会出现刺眼的撞色。第二是决策成本低,可选颜色少,选错的概率就低。第三是可控的文件体积,索引色 PNG 每像素只占几位。

平台 / 方案色彩上限
Game Boy4 档同色
NES主调色板 54 色,每块用 4 色
SNES15 位色,共 32768 色
Dawnbringer 3232 色

老主机的限制不是设计选择,是硬件成本。今天的限制则是主动选择的约束——约束会逼出风格。

真彩色 PNG 存的是每个像素的 RGB,调色板在这里只是美术纪律,不是技术约束。想享受索引色的省空间与秒换色,得在导出时选 PNG-8,之后加载和绘制都需要专门的兼容处理。多数 2D 引擎默认按 RGBA 处理,选前者等于自找麻烦——除非项目真的要跑在老硬件或极小的内存预算里。

抖动 ​

32 色画不出平滑渐变,但可以骗眼睛。抖动(dithering)把两种颜色的像素按固定图案交替排列,退后一步看就是中间色。

  • 棋盘格:两色各占一半,混出居中的第三色
  • 1/4 密度:主色多,用于浅一档的过渡
  • 3/4 密度:辅色多,用于深一档的过渡

密度调细,同一色相就能撑出四档,32 色在观感上够用上百档。

抖动的代价是纹理感。大面积平涂加抖动会显得脏,所以它通常只用在渐变区和阴影边缘。用错地方的典型症状是:远处看是渐变,近看全是噪点。

调色板交换 ​

索引色图像的每个像素存的是「第几号颜色」,不是 RGB 值。换一张颜色表,整张图立刻变成另一套配色,像素数据一个字节都不用改。老主机正是靠这个做敌人变种:同一份精灵数据,指向不同的调色板内存,红史莱姆就变成蓝史莱姆。

存储每像素内容换配色的代价
索引色调色板下标换一张颜色表
真彩色RGB 三个分量改贴图,或用 tint

现代引擎没有硬件调色板,等价做法是准备多份着色器参数或直接用 setColor 乘色。

本作的 18 色 × 6 花样是另一种变体堆叠:不换颜色表,而是把每种颜色的六个花样都画进图集。省掉了运行时逻辑,代价是图集要装下全部 108 格,而索引色方案只需一份形状图加若干颜色表。

代码里的颜色 ​

LÖVE 11 的 setColor 收 0–1 的浮点,所以游戏代码里到处是 /255:

lua
local COLORS = {
    red   = {217/255, 87/255, 99/255},
    blue  = {95/255, 205/255, 228/255},
    yellow = {251/255, 242/255, 54/255},
}

love.graphics.setColor(COLORS.red)

保留 /255 的写法是为了和调色板文件里的十六进制值一一对照,查色时不用心算。写成 0.85 反而对不上原始色号。

setColor 是全局状态,画完必须显式改回来。Tile:render 先设成深靛色画投影,再设回纯白画本体;漏掉第二句,整张棋盘会一起变暗。同理,PlayState 画选中高亮时切到 add 混合模式,画完要切回 alpha,否则后面所有绘制都会叠亮。

标题的循环配色 ​

StartState 用一个 6 项颜色表加 Timer.every(0.075, ...) 做标题变色。每次触发把整张表往后挪一位,最后一项绕回第一项:

lua
self.colors[0] = self.colors[6]
for i = 6, 1, -1 do
    self.colors[i] = self.colors[i - 1]
end

借用下标 0 作为中转,避免额外变量。6 个颜色轮转,每个字母按 letterTable 里的偏移量各自错开,画面就有了流动感。这个计时器在离开标题页时必须 :remove() 掉,否则它会跟着进入下一关继续改一张没人看的表。

做像素美术时可以先从 8 色或 16 色起步,画不下再加。四色的练习尤其有用:Game Boy 只有四档明度,所有形状、层次、可读性都得靠这四档撑住,练完再回到 32 色会觉得空间很大。