16 色夜景调色板:为什么不是把颜色调暗

一张 16 色的夜色调色板,以及背后的三条规则:色相偏移、明度分层、局部光源。附配色表与在 Godot 里做调色板替换的写法。

美术调色板像素

新手做夜晚场景,第一反应是把白天画面整体调暗。结果通常是:灰、脏、分不清层次。夜晚不是「亮度更低」,而是光源变了——只有一盏灯、一个月亮,其他东西靠反射和轮廓活下来。

16 色,够用

这套色板用在像素猫小屋里,从地板到灯笼全靠它。不是标准答案,但它有一个好处:任意两色放在一起都不会脏。

#色值名字主要用途
1#0B0F1C深夜 0最暗的背景、场景外框
2#161B2E深夜 1墙面阴影、家具暗面
3#232A44深夜 2地板、夜间描边
4#3B4368蓝灰远处的物体、冷反射
5#5A648F雾蓝月光照到的冷面
6#8C9BC4月白高光边缘、玻璃反光
7#0E2A28暗绿植物暗部
8#1E5C4A松绿叶子、猫眼底色
9#37A57E薄荷主强调色,健康与成功
10#7CE0B8荧光薄荷灯光核心、选中态
11#4A2A2E砖红木头暗部
12#8F4A3C陶红箱子、地毯
13#FF9FBE樱花粉猫耳内、腮红、第二个强调色
14#FFC85C灯黄唯一的暖光源,灯笼与月光反射
15#FFF3D6奶油猫的毛、纸、最高光
16#120D1A紫黑描边最深一档,替代纯黑

规则一:色相跟着光走

同一个物体,亮面和暗面的色相要朝光源方向和环境方向各自偏移,只改明度就会变成灰阶。

  • 暖光(灯黄 #FFC85C)照射:向黄橙偏移
  • 冷环境(月白 #8C9BC4):向蓝紫偏移
  • 交界处:降低饱和度,而不是加黑

举例,木箱的三个面:暗面 #4A2A2E、中间 #8F4A3C、受光面往 #FFC85C 的方向走。注意受光面并不是「更亮的红」,而是偏黄的橙。

规则二:明度分三层,不超过三层

像素画在 48×48 以内,一个物体最多三层明度就够。第四层会让轮廓消失。大物体(比如地板)可以破例到四层,但要保证最亮层只占很小面积。

规则三:光要有来源,并且只给一个

整张图只允许一个主光源。我的场景里是右下角那盏灯笼,addLight() 只调用两次:一次给灯笼核心,一次给地面的大范围衰减。

js
// 简化版:距离平方衰减 + 暖色混合
function addLight(buf, lx, ly, radius, tint, strength) {
  for (let y = 0; y < buf.h; y++) {
    for (let x = 0; x < buf.w; x++) {
      const d = Math.hypot(x - lx, y - ly) / radius;
      if (d >= 1) continue;
      const k = (1 - d) * (1 - d) * strength;
      blend(buf, x, y, tint, k);
    }
  }
}

用平方衰减而不是线性衰减,边缘会更自然。半径不要超过画面短边的 60%,否则整张图会发灰。

抖动:只在渐变处用

两种颜色之间需要过渡时,用 2×2 棋盘抖动,不要用第三色去「和」。抖动密度最好只有两档(25%、50%),三档以上在低分辨率下会变成噪点。

在 Godot 里换色板

如果你用索引色画图,可以在 shader 里做运行时换色板:

glsl
shader_type canvas_item;
uniform sampler2D palette : filter_nearest;
uniform float palette_size = 16.0;

void fragment() {
    vec4 c = texture(TEXTURE, UV);
    float idx = c.r * 255.0 / 16.0;   // 把 R 通道当作索引
    COLOR = texture(palette, vec2((idx + 0.5) / palette_size, 0.5));
}

这样白天、夜晚、中毒、回忆等状态只需要换一张 16×1 的色板贴图,不用重画所有素材。

最后一条经验

把画面转成灰度看一眼。如果灰度下依然能分清「猫、地板、灯笼、门」,这套配色就成立;如果灰度下一团糊,说明你只是在靠颜色区分物体,而颜色在夜里是最先丢失的东西。