美术规范
用户给了一张参考图(就是下面这张),内容是一条植物装饰带上的线稿小白兔。 下面所有数值都是从那张图的像素里量出来的,不是凭印象写的。量法见第一节。
和「小农场」的 美术风格(日本清新风)是两条不同的线,不要混用(见第七节)。
一、参考图是什么,以及为什么重做过一次#
第一版参考图是一把机械键盘,我在它最下方那条植物装饰带上量了色板(旧图还留在
art/ref-source.jpg)。按那套做出来的东西,用户看到实物图之后的原话是:
「美术风格完全不对啊,你看看图片中的样式。」 —— 并对,因为第一版有两个硬伤:
| 硬伤 | 实测证据 |
|---|---|
| 画面整体亮了一档 | 旧版小场景平均明度 0.747、暗部(<0.35)占 0.0%;新参考图平均 0.601、暗部占 9.9% |
| 同一画面里有两张参考图的血统 | 旧图是"键盘键帽上的小兔子"(一张实拍照片),新图是装饰花纹边框(一张设计稿)。按实拍照片取色,取到的是照片的环境色;按设计稿取色,取到的才是作者真正用的色 |
结论:画风基准必须取自设计稿,不能取自实拍照片。 照片里的颜色是"被拍到的颜色", 不是"被画出来的颜色",中间隔着环境光、白平衡和相机。 这一条比任何具体色号都重要。
1.1 新参考图(现行基准)#

| 项 | 实测 |
|---|---|
| 载体 | 一条横向装饰花纹边框,918×133,宽高比 6.90 |
| 内容 | 从左到右:花朵 → 叶片 → 中央的兔子 → 右侧花叶(视觉识别原话:"一条偏暗色调的装饰花纹边框,画面从左到右主要是花朵、叶片、中央的兔子和右侧花叶") |
| 背景 | 深色。三段底色分别是 #222328 / #3D4756 / #5F6B76(左→右逐渐变浅) |
| 描边 | 有,深灰,约 1~2px,不是纯黑 |
| 上色 | 平涂为主 + 轻微明暗,带纸面纹理 |
1.2 色板怎么量出来的#
不是凭感觉调的,是量化 + 直方图两层量出来的:
# ① 24 阶中值切分量化 → 按面积排序拿主色
pal = im.quantize(colors=20, method=Image.MEDIANCUT).convert('RGB')
# ② 明度直方图:确认画面的**重心**落在哪一档(这一条决定了"亮一档"这种硬伤)
g = np.asarray(im.convert('L')).astype(float) / 255
for i in range(10):
print(i/10, ((g >= i/10) & (g < (i+1)/10)).mean())
# ③ 取最暗的 3% 和最亮的 3%,分别拿"描边色"和"高光色"
实测结果 —— 明度直方图的峰值在 0.5~0.6(占 25.5%):
| 明度档 | 占比 | 参考图实测色值 |
|---|---|---|
| 0.1~0.2 | 1.9% | #2A2B2D |
| 0.2~0.3 | 4.5% | #313234 ← 描边色 |
| 0.3~0.4 | 7.6% | #4C4C4F |
| 0.4~0.5 | 13.4% | #6E6E74 |
| 0.5~0.6 | 25.5% ← 峰值 | #7D818C / #888890 |
| 0.6~0.7 | 17.5% | #8E9099 / #9C9A9E |
| 0.7~0.8 | 11.1% | #BCB7B5 |
| 0.8~0.9 | 8.8% | — |
| 0.9~1.0 | 9.5% | #F8F1EB |
最暗只到
#2A2B2D,没有纯黑。 但暗部(<0.35)确实占 9.9% —— 所以"不要纯黑"和"必须有暗部"这两件事同时成立,不矛盾。 旧版把"不要纯黑"执行成了"整幅不用深色",暗部直接归零,这才是错的地方。峰值在 0.5
0.6,不是 0.70.8。 画面重心在中间调。 旧版重心在 0.75,整幅亮一档。全图主色
#8E9099饱和度只有 7.2%,#9C9A9E只有 2.5%。结论:新图是中性灰,不是"蓝灰"。[!danger] 上面这句结论读错了,已在 2.0 纠正 用"全图统计"读一张元素只占 18.4% 的图,读到的是背景纸底的颜色, 不是作者用的颜色。按元素口径重量: 有色元素色相 P50 = 220°(正蓝灰)、饱和 P90 = 23.2%、色度 P50 = 12。
所以它就是蓝灰,而且比我当时以为的艳得多。 这个误读直接导致了第二版「
#8E9099中性灰」色板, 也就是用户说的「灰度过重、对比不足」。 教训:统计口径选错,会把"蓝灰"读成"中性灰",然后照着错的基准做一整套素材。
二、色板(按新参考图实测,这一版是按"色度"定的)#
| 版本 | 主色 | 问题 | 用户看到的结果 |
|---|---|---|---|
| 第一版 | #A8ACB5 |
取自旧参考图(机械键盘实拍照片) —— 取到的是"被拍到的颜色" | 「美术风格完全不对啊」 |
| 第二版 | #8E9099 |
取自新参考图,但我把它读成了「中性灰」 | 「灰度过重、对比不足」 |
| 第三版(现行) | #8C97A8 |
按实测色相 220° / 色度恒定重做 | — |
第一版错在取材,第二版错在判读。 第二版的失误在于:
我只量了参考图全图的主色(#8E9099,饱和 7.2%),
却没量元素内部的色相分布。分区域一量就很清楚:
2.0 参考图到底"是什么色"(实测,这一节的地基)#
把参考图按元素(与高斯模糊差异 > 8 的像素)挑出来,只统计有颜色的部分:
| 量 | 实测值 | 说明 |
|---|---|---|
| 有色元素的色相 P50 | 220° | 正蓝灰,不是中性灰 |
| 有色元素色相 P10 / P90 | 27° / 240° | P10 是暖色(花心那撮) |
| 有色元素饱和 P50 / P90 | 12.1% / 23.2% | 比我第二版整幅的 3.9% 高得多 |
| 元素内色度(max−min)P50 | 12 | —— |
最关键的一条:把参考图按明度分 8 档,每档都量色度:
| 明度档 | 0.1~0.2 | 0.2~0.3 | 0.3~0.4 | 0.4~0.5 | 0.5~0.6 | 0.6~0.7 | 0.7~0.8 | 0.8~0.9 |
|---|---|---|---|---|---|---|---|---|
| 色度 P50 | 7 | 5 | 11 | 14 | 12 | 11 | 11 | 8 |
参考图的色度在各明度档基本不变(P50 都在 11~14)。意思是它的画法是 「每个明度档都有自己的色」,不是「越暗越往灰里退」。
而我第二版的色板色度是 11 → 15 → 8 → 6 → 4 → 3 → 3 —— 暗部全退成了中性灰。所以整幅看着发灰、发闷,而每一项检查都写着 ✓。
修正做法:色相钉在 215~225°,色度逐档保持;暗色也要有颜色,不许退成灰。
[!danger] 顺带纠一个判据错误:对暗色不能用 HSV 饱和度做上限
HSV 饱和 = (max−min)/max,max(明度)越小,分母越小,
于是暗色的 HSV 饱和会自动虚高。
实测:#292D35 明度只有 0.18、HSV 饱和 24.5%,看着"超标 4.5 个点",
但它的色度只有 12 —— 和参考图暗部完全一致。
拿 HSV 饱和当暗色的上限,会逼着我把本来就对的暗色改坏。
判"有没有颜色"用色度,判"艳不艳"才用饱和度。
2.1 色板(现行,tools/art/notes_art.py 的 P)#
| 档 | 色值 | 色度 | 明度 | 用在 |
|---|---|---|---|---|
| 亮 | #F6F1EC 纸白 |
10 | 0.95 | 兔子身体、花瓣、纸底 |
#EDE8E5 暖白 · #DFDFE6 雾 · #D3D6DE 浅 |
8/7/11 | 0.91/0.88/0.84 | 受光面、远景、云 | |
| 中间调 | #B8BCC6 · #9FA5B2 |
14/19 | 0.74/0.65 | 中景、灰面 |
| 主体 | #8C97A8 ★ 主色(色相 216°) · #808D9F |
28/31 | 0.59/0.55 | 画面主体 |
| 暗 | #878C98 · #6E7480 · #5D6370 · #4C525D |
17/18/19/17 | 0.55/0.45/0.39/0.32 | 暗面、地面、中景剪影 |
| 最深 | #32373F ← 描边色 · #292D35 |
13/12 | 0.21/0.18 | 所有轮廓线、最深的暗部 |
- 最暗到
#292D35为止,没有纯黑。(参考图暗部占 9.9%) - 主色是偏蓝的灰蓝
#8C97A8(色相 216°),不是中性灰。 想压冷就往蓝灰加,不要把颜色抽掉变成灰 —— 抽掉颜色就是第二版的老毛病。 - 每个明度档都要有色度(≥12)。暗部退成中性灰 = 画面发灰,这是硬禁。
[!important] 饱和度与色度是两级约束,别混成一个数
| 对象 | 判据 | 上限/下限 | 依据 |
|---|---|---|---|
| 色板色号 | 色度 | 8 ~ 31 | 参考图元素内色度 12~28 |
| 成品像素 | 饱和 P99.9 | ≤ 30% | 参考图自己 P99.9 = 32.3% |
| 场景整幅 | 饱和 P90 | ≤ 22% | 参考图实测 15.2% |
| 场景整幅 | 色度 P50 | ≥ 13 | 拦"太灰"。旧版 6 / 新版 15~22 / 基准 12 |
[!danger] 这条阈值我定错过两次,两次都是比基准还严或比基准还松 第一次(第二版):饱和度上限写 10%,而基准图元素内是 16.2% —— 于是我的场景只有 3.9%(基准的 1/4、用户的评价是"灰度过重")却一路显示 ✓。 第二次(本轮):只加"饱和上限",没加"色度下限" —— 但饱和上限拦不住"太灰",它只拦"太艳"。
规矩:拦"太灰"必须有一条下限判据,而下限必须能被旧版触发。 判据是否有效,用三个已知样本验:旧版色度 6(必须拦下)/ 新版 15~22(必须通过)/ 基准 12。三个一眼可分,判据才算立住。 色度下限取 13 比基准的 12 还高一档是有意的:基准是装饰边框(元素占 73%、 几乎全是笔触),游戏场景大部分是平涂天空,色度天然被拉低,套同一个下限两边都不合适。
2.2 描边色#
| 名称 | 色值 | 明度 | 用在哪 |
|---|---|---|---|
| 墨灰 | #32373F |
0.21 | 所有外轮廓 |
| 次墨灰 | #4A4F5A |
0.29 | 内部结构线、浅一档的轮廓 |
外轮廓用 #32373F(最深),内部结构线用 #4A4F5A。
两者不能一样重 —— 全画同一种线重,会让身体像"被黑线切成零件"(见第三节)。
2.3 暖色(只出现在"芯"和"果"上)#
| 名称 | 色值 | 用在哪 |
|---|---|---|
| 暖金 | #C4A05E |
极稀有植物的花心 —— 唯一允许的高饱和色,面积 < 3% |
| 浅粉棕 | #BCAEA6 |
兔子内耳 |
① 色值在容差 ±22 以内(水彩后期会把颜色挪 10~15 个数,容差太紧会误判); ② 面积 < 3%。检查脚本两条都量,面积是独立量的。
极稀有植物不能做成"整朵金花",暖金只能进花心。 实测极稀有那一种暖色面积 1.08%。
2.4 植物的稀有度色相#
稀有度靠色相拉开,不靠明度。分布与 收集手册 二一致: 常见 18 / 少见 8 / 稀有 3 / 极稀有 1。
| 稀有度 | 花瓣色 | 种数 | 色度 | 备注 |
|---|---|---|---|---|
| 常见 | #94979F 灰蓝 |
18 | 11 | 与主色同族 |
| 少见 | #728C74 苔绿 |
8 | 26 | |
| 稀有 | #8D9CB4 野花青 |
3 | 39 | 色度偏高,靠混色压回;成品 P99.9 实测 20.0%,在 30% 以内 |
| 极稀有 | 花瓣仍是灰蓝,花心是暖金 | 1 | — | 暖金 52% 靠暖色例外放行 |
三、造型:那只兔子#
3.1 新参考图的视觉识别结果(照抄,不要"发挥")#
对着新参考图中央那只兔子,视觉模型的描述:
兔子是白色偏米色、带浅棕色斑纹的卡通造型,整体有细深色描边。 耳朵很长,竖直尖耳,内耳为浅粉棕色;眼睛画成黑色小椭圆。 身体圆润,直立坐着,前腿垂在胸前,后腿收拢;尾巴基本看不见。
| 部位 | 特征 | 实现 |
|---|---|---|
| 毛色 | 白色偏米色 + 浅棕斑纹 | 底 #F8F1EB;背上 3 块浅棕斑 |
| 耳朵 | 很长、竖直、尖;内耳浅粉棕 | leaf_shape 画叶片形(根部窄、中间宽、顶部尖),长度 25/29px |
| 眼睛 | 黑色小椭圆 | 2.5×3.3 实心椭圆,不加高光点 |
| 身体 | 圆润 | 竖向胶囊,30×34px |
| 四肢 | 前腿垂在胸前、后腿收拢 | 侧面跑姿需要改,见 3.3 |
| 尾巴 | 基本看不见 | 半径 5px 的小球 |
| 描边 | 有,细深色 | 见 4.1「连续外轮廓」 |
3.2 这条兔子改了三轮,每轮改的都不是颜色#
拿并排图给专业复核,原话是: 「右边和左边的核心差异:左边是手绘感、细线、柔和明暗、竖向兔子造型; 右边是粗描边、硬边、局部平涂、横向机器人造型。」 并给出「右边最该改的三点」,顺序是: ① 先改成竖直、完整、像兔子的身体轮廓;② 补出两只宽而尖的兔耳; ③ 减轻统一粗黑描边,把阴影从"零件灰块"改成连续的体积明暗。 注意:三点里没有一条是"换颜色"。
| 轮次 | 改了什么 | 复核结论 |
|---|---|---|
| 一 | 换色板 + 加描边 | "像四足小机器人 / 关节玩偶" |
| 二 | 竖向躯干 + 叶片形耳 + 加明暗 | "还是'兔子头部特征 + 机械身体'" |
| 三 | 改成"整体剪影 → 连续外轮廓 → 内部细节" | 暗部 0.2% → 10.9%(参考图 8.2%),明度 0.894 → 0.666(参考图 0.584) |
第二轮复核的关键一句是: 「不是描边重,是关节分块把身体切开了 —— 头/颈/胸像几个球体堆在一起, 前后腿都有明显的分段和独立块面,像机械接口。」
所以第三轮把做法从"给每个零件描边"改成
"把所有零件画成一张实心剪影 → 由剪影生成一条连续外轮廓 → 再叠内部细节"。
这样头、脖子、胸口、腿之间不会再有闭合的深色圈线 ——
分件感的根源就是那些内部圈线,而不是线宽。
实现见 tools/art/notes_art.py 的 draw_rabbit() 与 outline_pass()。
3.3 坐姿 vs 跑姿#
参考图里是坐姿,但游戏里的主角要跑。所以:
- 坐姿:直接照参考图(用于立绘、手账封面、主视觉)
- 跑姿:在坐姿的基础上改成侧面偏四分之三,只允许动腿和身体倾角, 耳朵、眼睛、内耳、斑纹、描边规则一律不许变


3.4 四条不许动的#
- 耳朵是叶片形,不是细圆柱。 细圆柱画出来是"天线"(第一轮就是这个毛病)。
- 眼睛是实心小黑椭圆,不加高光点。 加了高光就变成另一只兔子了。
- 描边只在外轮廓上。 内部不许有闭合的深色圈线 —— 那是"零件感"的来源。
- 背上有浅棕斑纹。 这是新参考图的特征,去掉就变成一只普通白兔了。
四、线条、上色与纸感#
4.1 线条:连续外轮廓,不是逐零件描边#
| 元素 | 原生 1080 下线宽 | 渲染 540 下线宽 | 颜色 |
|---|---|---|---|
| 外轮廓(所有主体) | 3~4px | 1.5~2px | #32373F |
| 内部结构线 | 2px | 1px | #4C4C4F(必须比外轮廓浅) |
| 小图标(单朵花、手账格) | 2px | 1px | #4C4C4F |
| 极小(花蕊、粒子) | 不描边 | — | — |
病根解释见 3.2。做法是 outline_pass():
① 把主体所有部件画成一张**实心剪影**(只看 alpha,不看颜色)
② 剪影膨胀一圈、取差集 → 得到**一条连续外轮廓**
③ 轮廓柔化 0.6px(去硬锯齿),贴上去
④ 最后画内部细节(**细节不带外描边**)
这样头、颈、胸、腿之间不会出现闭合的深色圈线。 那只"像机械接口"的分件感,来源就是这些内部圈线。
[!warning] 线宽必须写明在多大画布上量 只写"3px"是没用的:同一条线在 540 宽的图上和 1080 宽的图上差一倍。 这份的规矩是:先在 1080×1920 上量,再整体缩到 540×960, 而且缩放只对最终合成帧做一次,不许每个零件各自缩放(接缝会各糊一圈)。
4.1.1 毛发边缘要有轻微起伏#
完美椭圆 + 粗描边 = "玩具感"。用 fur_ellipse() 把轮廓做成
1px 级的自然起伏多边形,看起来才像手绘的毛边。
4.2 上色#
这套风格的核心是"平":
| 规则 | 数值 |
|---|---|
| 填充 | 平涂,同一块颜色内部明度变化 ≤ 6% |
| 明暗 | 最多两级(本色 + 一级暗面),没有环境光、没有反射光 |
| 暗面位置 | 统一在右下(光从左上来) |
| 边缘 | 轮廓处柔化 1~2px(渗化),平涂区严格不柔化,见 4.4 |
| 纹理 | 全图压一层结构化纸纹(不是逐像素噪点),见 4.4 |
4.4 水彩后期:四个参数与实测#
上面说"这套风格的核心是平"——那指的是色块内部要平。 但整幅图如果只有平涂,出来的就是扁平插画,不是水彩。 实测复核原话:"更接近低饱和扁平插画,水彩感中等——轮廓整齐、块面化, 缺少水彩渗化、笔触方向和纸张吸水边缘"。所以平涂之后还有一道后期:
| 参数 | 作用 | 做法 | 实测贡献 |
|---|---|---|---|
bleed |
边缘渗化 | 只在轮廓附近混一层模糊(用边缘 mask 限定范围) | 强边 31310 → 29010 |
edge |
吸水沉积边 | 沿轮廓压一圈略深的 #737782 |
色数 +46,加一圈深边 |
mottle |
透明叠色(水渍) | 撒极淡的同色椭圆斑块,模糊 9px | 柔化边 3% |
grain |
纸纹纤维 | 多尺度:粗纤维(低分辨率噪声放大)+ 细颗粒,只动亮度不动色相 | 噪点幅度 3.42 → 1.53 |
坑一:bleed 曾经是空转的。 老版膨胀的是 alpha 通道,
而底图 alpha 全是 255(画布不透明)——MaxFilter 对恒定值就是恒等。
所以"渗化"一次都没生效,写着 bleed=1.8 实际等于 0。
归因实测:色数 / 噪点 / 强边三项和"无任何后期"完全相同。
现在改成在 RGB 上做。
坑二:纸纹曾经是"电视雪花"。 老版是 px[x,y] += randint(-s, s) ——
逐像素白噪声。量出来它把噪点幅度从 1.26 冲到 3.42、色数 377 → 884,
而"水彩感"一点没涨:白噪声做出来的是雪花,不是纸。
视觉复核独立给出了同一个判断——把老版那一段评为
"最像电视雪花噪点:布满细密颗粒,边缘仍清楚,色块发灰、通透感弱",
新版评为 "最像水彩画:纸张纹理与朦胧斑驳明显,边缘柔和,色块半透明、相互晕染"。
指标和眼睛对上了,才确认改的是真问题。
真正的纸有两点白噪声没有的东西:① 纤维(一个方向长、一个方向短) ② 尺度(纸纹比像素大得多,放大要能看见"纹路"不是"沙子")。 现在用"低分辨率噪声放大 6 倍 + 很轻的细颗粒",并且三通道同加同一个亮度值, 所以色相和饱和度完全不动。
[!warning] 渗化不能整幅混,只能限定在轮廓附近
试错两次,都记下来:
① 逐像素按局部差异混 → 色数 377 → 2277(平涂区被轻微混合,干净色块碎成渐变)
② Image.blend(im, blurred, 0.30) 整幅恒定 alpha → 色数 377 → 1786
(恒定 alpha 仍然是"每个像素都变",平涂区也变了)
正确做法是先做一张 mask:只在有轮廓的地方允许混,平涂区 mask=0 →
像素值原样保留。"平"和"水彩"不冲突,冲突的是"整幅一起动"。
4.3 阴影#
| 用途 | 颜色 | 做法 |
|---|---|---|
| 地面接触投影 | #8C9EAE @ 26% |
压扁的椭圆,模糊 2px |
| 跳跃时的投影 | 同上 @ 12% | 同时缩小 40%(离地越高越小越淡) |
| 角色自投影 | #AEBAC6 @ 30% |
一块,硬边但很浅 |
| 主体锚 | 深石青 #4E5A66 @ 16% |
兔子正后方一圈极淡的径向暗角,见下 |
兔子跳起来的时候,没有地面投影就看不出脚有没有离地。
主体锚是实测加上的:整套色板都是浅色、又禁止加黑描边, 结果兔子(米白)压在天空/远山(也是浅色)上时会糊掉。 两轮视觉复核都指出"兔子和背景融色"。 解决办法不是加黑线(违反规范),而是在兔子正后方压一圈 16% 的暗角—— 它把主体"顶"出来,又在色板之内。
五、序列帧动画规范#
5.1 为什么是"生成器逐帧画",而不是切图或生图#
| 做法 | 问题 |
|---|---|
| 生图序列帧 | 帧间形变(形状每帧都在变,像融化)、帧间漂移、帧数不够 |
| 从整图上切零件 | 参考图那只头身连成一体,切不开;硬切会露出一条直边 |
| 生成器逐帧画 ✅ | 帧与帧之间只有显式参数在变,不存在形变和漂移;改一次腿长,全部 42 帧一起变 |
工具:tools/art/notes_art.py(PIL,无外部依赖)。它同时出序列帧、场景物件、小场景预览三样。
python3 tools/art/notes_art.py --only all # 全出
python3 tools/art/notes_art.py --only rabbit # 只出 5 组序列帧
python3 tools/art/notes_art.py --only check # 只跑脚底校验(触地帧必须钉在地面线)
| 参数 | 出什么 |
|---|---|
rabbit |
5 组序列帧 → docs/野花笔记/_art/anim/*.webp + manifest.json |
props |
17 个场景物件 → docs/野花笔记/_art/props/*.webp |
scene |
小场景拼接预览 → art/scene-preview.png |
sheet |
序列帧总览 + 物件总览 → art/frames-contact.png、art/props-sheet.png |
check |
逐组报触地帧最大偏移与最高离地 |
5.2 五组动画#
| 组 | 帧数 | fps | 什么时候播 |
|---|---|---|---|
stand |
8 | 8 | 待机(呼吸 + 耳朵摆动 + 眨眼) |
hop |
10 | 12 | 小跳(障碍 ≤ 30px) |
leap |
12 | 11 | 大跳(障碍 30~72px / 跨缺口) |
land |
6 | 14 | 落地缓冲(重量感的全部来源) |
look |
6 | 7 | 跑到岔路口/观察点时停下的张望 |
| 组 | 总帧 | 触地帧 | 腾空帧 | foot_lift |
|---|---|---|---|---|
stand |
8 | 8 | 0 | 0 |
hop |
10 | 3 | 7 | 身体抬升 × 0.92 |
leap |
12 | 2 | 10 | 身体抬升 × 0.80 |
land |
6 | 6 | 0 | 0 |
look |
6 | 6 | 0 | 0 |
画布 128×128,地面线 GY = 112,兔子中心 CX = 64。
实测的脚底离地量(正数=离地,地面线 y=112):
| 组 | 逐帧离地量 | 峰值 | 触地帧最大偏移 |
|---|---|---|---|
stand |
0,0,0,0,0,0,0,0 | 0 | 0px |
hop |
−2, 3, 15, 28, 38, 38, 30, 15, 1, −2 | 38px | 2px |
leap |
0, 6, 18, 34, 49, 62, 68, 64, 51, 34, 14, 0 | 68px | 0px |
land |
1, −2, −1, 1, 2, 1 | 2 | 2px |
look |
0,0,0,0,0,0 | 0 | 0px |
① foot_lift 对脚底不是恒等映射,所以不能"量出偏移就直接补"。
髋、膝、脚三处耦合,膝盖还有个夹取值会跟着变 —— 实测给 foot_lift 加 −3,
脚只动了 1px。调这个参数调了 7 轮都没调对,因为偏移根本不是它造成的。
② 更关键的一条:hipy 漏了 foot_lift。
原来写的是 hipy = GY - 30 * squash + body_y,少了 foot_lift:
身体升上去、髋部留在原地 → 腿被拉长去够地面 → 脚永远踩在地上,腾空帧也踩着。
这一个漏项同时造成了三个假象:hop 只离地 8px、leap 完全不离地、land 脚陷地下 7px。
修掉之后 hop 立刻变成 40px。
"腿改了、髋没改"是这类骨架代码最典型的漏改,靠肉眼看帧根本发现不了。
③ 最后靠"扫描 + 自动标定"收口。 calibrate() 会扫描补偿量,
取"触地帧最大偏移最小"的那个,不依赖任何关于"谁在动"的假设。
迭代逼近和手推公式都试过,前者会过冲(+7 → +10),后者符号还容易搞反。
[!danger] 关于影子,这里有两个都踩过的坑
① 腾空时影子不许消失。 原来 alpha = 74 × shadow,而 shadow 在最高点掉到 0.12,
算出来 alpha ≈ 9、半径又只有 1.8px —— 影子的 alpha 直接变成 0,整块没了。
表现是"跳起来的时候脚下什么都没有",反而更看不出离地。现在 alpha 有地板值(≥30),
半径也有下限(≥0.5):离地靠"影子变小变淡"表达,不是靠"影子没了"。
② 校验脚本曾经量的是影子,不是脚。 影子画在最下面,所以"最下面一个不透明像素"
永远是影子的底边,不是脚。于是 --probe 一直在量影子,报出来的"漂移 2px"
其实是影子的漂移,跟脚一点关系都没有——脚本在量错的东西,却一直是绿的。
现在 foot_y() 会按颜色先把影子剔除再扫。
合计 42 帧 / 128×128px / 约 462 KB(WebP 无损,5 张精灵表)。
兔子在游戏里显示约 62px 高,按 128px 出图有 2 倍余量(高清屏够用), 又不至于浪费包体 —— 序列帧体积随尺寸平方缩放,按 300px 出图会白白多 5 倍体积。
5.3 三条硬规矩#
- 地面锁定——但它只管"触地帧":
站着、走着、落地缓冲这些帧,脚底必须画在同一条线(画布 y=112)上,
身体怎么动、腿怎么收都不许改这条线。
校验方式是硬的——
--probe逐帧扫出脚底像素的 y,实测最大漂移 2px。 超过 3px 就说明某帧的姿态把脚画飞了,必须回去改参数。
第一版把它用在了所有帧上——包括腾空帧。结果视觉复核的结论是 "双脚仍像着地,跳跃感不足":兔子身体明明抬到 66px 高了, 脚却还钉在地面上,看起来像被拉长而不是跳起来。
正确做法是给绘制函数加一个 foot_lift 参数:
触地帧传 0(脚钉在地面线),腾空帧传正数(脚跟着身体抬起来)。
实测小跳取 抬升 × 0.62、大跳取 × 0.80(大跳抬得更多、更快,
最高点的剪影是"腿悬在身下")。
一句话:踩在地上的时候不许漂,飞在天上的时候必须离地。
还有一个连带的坑:--probe 自己一开始是错的。
它对所有帧都要求"脚底 = 地面线",所以腾空帧一改对就报"漂移 51px"——
把正确的行为当成了错误。现在它按组分开报(触地帧报漂移、腾空帧报离地量)。
校验脚本写错比不写更危险:它会让你把对的改回错的。
触地帧的 foot_lift 还不许为负:leap 的最后一帧身体下压(抬升 −3px),
负的 foot_lift 会让脚沉到地面线以下,实测漂移 4px(穿模)。加 max(0, ...) 修掉。
跳的幅度要能一眼看出区别(实测踩过):第一版的
hop和leap视觉复核的结论是"第 2、3 行看不出区别"。修法是让两档在姿态上分开,不是只差高度:- 小跳 = 身体前倾 + 腿往肚子底下收
- 大跳 = 后腿蹬直往后伸 + 鼻子先朝上再朝下(一次明显的仰头到俯冲)
前腿和后腿不许黏在一起:近侧腿粗一档、往前错开,远侧腿细一档、 往后下方错开。第一版两条腿几乎并线,复核的结论是"腿部看不清、粘连"。
5.4 帧图集#

视觉复核前后看过四轮,前两轮的结论都是"第 2、3 行看不出区别"。最后靠三件事分开: ① 小跳身体前倾、腿往肚子下收;大跳后腿蹬直往后伸; ② 大跳的鼻子先朝上再朝下(一次明显的仰头到俯冲),小跳没有; ③ 大跳中间有 2~3 帧几乎停在最高点(滞空长)。 只把高度调高是不够的——高度差在连续播放时几乎看不出来。
六、五段风景带配色(现行,与 notes.js 的 BANDS 逐值一致)#
每带的天空 / 远山 / 中景 / 地面 / 土壤都从第二节的色板里取,不许新调色。
| 带 | 天空 | 远山(亮而糊) | 中景树剪影 | 地面(舞台) | 土壤剖面 |
|---|---|---|---|---|---|
| 苔原草坡 | #A8AEBB |
#8E97A8 |
#4F5764 |
#3C424B |
#474D59 |
| 石楠台地 | #A6ACBA |
#8C95A7 |
#555F70 |
#383E48 |
#474D59 |
| 溪谷林地 | #A4AAB4 |
#808A9B |
#484F5A |
#343941 |
#3C424B |
| 砾石滩 | #A8AABA |
#7B8093 |
#575D6D |
#3A3E4A |
#464C5A |
| 高地野花田 | #A7ADBD |
#8892A1 |
#555F70 |
#3A404C |
#474D59 |
第一版(表里原来那些 #E8E3D5 / #A88763 / #7E9B93)是从小农场那套清新风
直接搬过来的 —— 暖米白天空 + 土黄土壤 + 小清新绿,和灰蓝水彩毫无关系。
它甚至不在第二节的色板里,那句"不许新调色"当时就是一句空话。
第二版我按"色相要区分"改了,但只看色板值、没看屏幕值: 按色板算"天空与远山差 0.19",渲染出来只有 0.043 —— 因为天空有渐变、远山被混色、地面被提亮。整整一轮调色都没生效, 而我还以为"改了颜色数值却完全不动"是缓存问题。
现在这张表是按渲染后的实测值反推出来的,并且每一带都过了 check_tone.py --scene(见 8.5.1)。
[!important] 为什么五带的色号看起来"差不多"
这是同一个世界的一条跑道,不是五个关卡。五带的明度、饱和、色度都压在同一档
(实测明度 0.6030.626、饱和 14.617.5%、色度 19~22),
靠色相的小偏移(砾石滩略偏紫灰、溪谷偏冷)和中景剪影的疏密来区分。
不要为了"换带要明显"去拉大色差。拉大之后五带会变成五个游戏,
而且必然有一带因为掉出色度区间而不达标(砾石滩第一版就是这样:
饱和 8.2%、色度 7,其余四带 1618% / 1722,一眼就是"这关没做完")。
[!tip] 换带的时候只动环境,不动角色 过渡靠天空带整体渐变(6 秒),兔子和花的颜色不变。 "换了个地方"的感觉靠环境给,不靠给角色换皮 —— 省素材,也更像真的走到了另一个地方。
同理,纵向分四层,相邻层明度差要 >0.05: 天空 0.67 / 远山 0.55 / 中景树 0.47 / 地面 0.59 / 土壤 0.36。 远山必须比中景树亮(空气透视)—— 这一条我两个方向都错过: 先是画得比树还暗(深度反了),改完又和树撞在一起(0.43 vs 0.47,分不开)。
七、和「小农场」的关系(别混用)#
| 维度 | 小农场 · 日本清新风 | 野花笔记(这份) |
|---|---|---|
| 上色 | 水彩淡彩,有晕染浓淡 | 平涂,明度差 ≤6% |
| 线条 | 蓝灰 #7C8B99,2~3px |
同色系但要标基准画布;兔子轮廓压深到 #5C6874 |
| 主色 | 薄荷绿 #A8D8B9 / 奶油白 |
石青 / 米白 / 苔绿,更冷更哑 |
| 情绪 | 通透、治愈、想待着 | 安静、克制、像一本植物图谱 |
| 亮色 | 樱粉、淡橙、暖金 | 几乎不用,暖色占比 ≤3% |
| 动画 | 木偶(代码驱动骨骼) | 序列帧(生成器逐帧画) |
两套不许混在同一个画面里。
八、素材预览(现行素材全部由 gpt-image-2 生图产出)#
旧版的做法是 tools/art/notes_art.py 用 PIL 画椭圆和矩形拼出兔子和植物。
那是根本性的路线错误 —— 用户看到成品后的原话是
「你是在逗我吗 你生图使用的是生图工具吗 风格完全不对 完全不是一样的东西」。
8.1 现行画风基准:参考图拆出来的元素#

这 12 个元素是新素材的风格来源 —— 所有动物、植物、场景元素都拿它当参考图喂进生图模型, 所以它们看起来是一套。
其中 7 号是一只兔子,它定义了这只兔子该长什么样: 正面坐姿、头占整体高度约 45%(婴儿比例)、耳朵占身高约 1/3、内耳浅棕带斑点、 毛用短线和点表现且轮廓被毛尖打断、胸前有棕色短线和小点、夸张的长胡须。

8.2 兔子(按参考图重做)#

| 我按自己想的"可爱"做的 | 参考图给的做法 | |
|---|---|---|
| 头身比 | 头 ≈ 身的 1/3 | 头占整体高度 45% |
| 毛的质感 | 平滑连续的轮廓线 | 轮廓被一簇簇毛尖打断,内部是短线和点 |
| 眼睛 | 一个黑点 | 带细小浅色高光的小圆眼 |
| 细节 | 无 | 胸口的棕色短线与小点、夸张长胡须 |
这是一条画法差异,不是形容词差异。按形容词做出来的东西不像参考图, 按画法做出来的像。
兔子:从"透明的骨架"到最终定稿#

这一节记三件事:"透明"的真根因、一次我自己做出来的假对比、最终定稿的参数。
一、"透明"的真根因:抠底把身体挖空了#
anim_sheets.py 的 cut() 原本是全图一刀切阈值(拿角落一个像素当纸底,
|a−bg|.sum() < 76 判为背景)。而实测:
| 到纸底中位的曼哈顿距离 | |
|---|---|
| 纯纸底(四边) | p50 1~4,max 12~18 |
| 兔子毛的身体(画面中心) | p50 2~10 |
兔子的毛和纸底是同一个颜色,整个身体被判成背景挖掉,只剩骨架。 每帧包围盒内填充率只有 8.0~11.4%(正常 40~70%)。调容差救不了: 两个分布重叠,不存在能分开它们的阈值。
修法:cut_background() 先按紧容差(>26)找轮廓 → 膨胀 3 像素把毛尖缺口糊上 →
再从封闭轮廓外面 flood fill。画风要求轮廓"被毛尖打断",那些缺口就是 fill 的入口。
二、一次我自己做出来的假对比(比 bug 更值得记)#
用户说「这质感一样吗???现在的毛茸茸的可爱的感觉都没了」。 我去做了一个"生图原始 vs 成品"的对比图,结论是成品糊了、有锯齿、有白边 —— 然后照着这个结论去改了三轮。
那个对比图本身是假的:
| 原图高度 | 我展示时 | 等效缩放 | |
|---|---|---|---|
| 生图原始 | 400 px | 显示 430 px | 1.08×,接近 1:1 → 锐 |
| 成品 | 218 px | 显示 430 px | 1.97×,放大一倍 → 必然糊 |
我拿一张放大两倍的去和一张原尺寸的比,然后怪自己的处理糊。 改成"两边都缩到同样的最终像素高度、再同样放大"之后,结论直接反转: 成品的清晰度和生图原始是同一水平,甚至因为暖调显得层次更足。

否则你看到的"差异"全是缩放比造出来的。 这和之前"量兔子要量内部像素""量接缝要量画布不要量截图"是同一条: 先确认你量的/看的对象,是不是你要评价的那个东西。
实测佐证:成品相对生图原始的高频能量保留 91%(127 vs 139)—— 笔触本来就没丢,那三轮改动改的是不存在的问题。
(那个"厚白边"倒是真的,见下。)
三、真被修掉的三处#
| # | 问题 | 原因 | 修法 |
|---|---|---|---|
| ① | 一圈米白厚边 | cut_background 输出的透明像素RGB 仍是米白纸底;resize 时 LANCZOS 把米白混进边缘 |
resize_rgba():预乘 alpha 再缩放,反预乘回来 |
| ② | 膨胀进了输出 | alpha = ~seen 把封缺口用的 3px 膨胀也算成前景,轮廓外胖了 3px、毛尖被糊 |
fg = far | body:膨胀只当填充的墙,不进输出 |
| ③ | 边缘锯齿 | 缩放后一刀二值化 alpha | 改为保留抗锯齿(中位 alpha 244~252,是干净的 1px 边,不是雾) |
另外把归一化和体型收窄合成了一次重采样(原来分两步,等于对同一张图做两次 LANCZOS), WEBP 质量 92 → 97。
四、最终定稿的参数#
| 项 | 值 | 说明 |
|---|---|---|
| 毛色 | 暖白 (250,249,238) | 用户的定稿色,R−B −12 偏暖 |
| 体型 | 不收窄(SLIM_X = 1.0) | 圆润 |
| 占屏高 | 15~16% | sc = 0.70 |
| 填充率 | 38~52% | 实心 |
| 半透明 | 中位 alpha 244~252 | 干净的 1px 抗锯齿 |
用户先说「蓝色基调 瘦瘦的」,我做成冷蓝灰 + 横向挤 0.78, 结果反馈是「可爱的感觉没了」。问过之后确认要回暖白、回圆润。
可爱感很大一部分来自「圆 + 暖」:冷灰产生距离感、瘦长削弱圆润, 两条一起上,"可爱"就没了。 用户提"改成某个属性"时,既要把方向问清,也要把"度"问清 —— 我按字面把两条都做到了极端,反而把原稿最好的东西做掉了。
现在 SLIM_X 这个常数留在代码里:以后要调体型,动这一个数就行。
8.3 动物动画(6 种,可逐帧播)#
| 动画 | 帧数 | fps |
|---|---|---|
| 兔子奔跑 | 8 | 12 |
| 兔子跳跃 | 6 | 10 |
| 麻雀 | 6 | 10 |
| 萤火虫 | 6 | 7 |
| 甲虫 | 6 | 9 |
| 蝴蝶 | 6 | 13 |
① 必须透明底。 旧版 spritesheet.py 里 normalise() 写死 Image.new("RGB", ...),
每帧被刷上生图的米白色 —— 在浅色文档页上看不出来,叠到真实场景就是一块块米白方块。
② 不能用等分格子切件。 生图的姿态会超出格子,等分切会把角色切断,
而且等分切看不出这件事(总能切出对的数量,每张大小一样,只是内容缺一块)。
改用二维连通域切(tools/art/pose_sheet.py)。
8.4 植物:30 株(5 带 × 6 株)#

每条带 6 种形态,见 素材总表 第二节。其中 5 株有摇摆动画:
让生图"再摆一个姿势",出来的就是另一株植物了。
变形做法:绕根部旋转 + 按行水平位移 + 相位滞后。
方向必须"根稳梢摆" —— 第一版把 bend 写反了(两次取反正好抵消),
结果底部摆得最大、顶端不动。验法是量每 10% 高度带的水平重心位移:
底 10% 必须是 0.00 px。
8.5 场景元素(36 件)与拼接场景#


场景是拼出来的,不是整张生成的。 整图生成有两个问题: ① 每次出来的不一样,改一处要重生成整张;② 背景前景焊死,做不了视差滚动。
8.6 可玩的测试场景#
页面里已经落地的五条(都踩过,见 11.5):
| 项 | 现状 | 说明 |
|---|---|---|
| 地面 | 地平线 y=856、可走线 y=884,地面带只占画面 11% |
侧视游戏的路面是底部一条窄带,不是一大片地(原来 41% 是俯视的地) |
| 尺度 | 树 300 |
全部放大过一轮,原来花草比兔子还小 |
| 跳跃点 | 小石头 x=950(小跳)、大木头 x=1850(大跳) |
不用玩家按键,跑到就起跳;每处带 done 标记,否则落地后还在触发范围内会连跳两次 |
| 调色 | 烘焙进图层图片(grade() + grain()) |
不要用 CSS filter,理由见 11.5 |
| 缩放 | 吸附到整数比 540x960 / 486x864 / 450x800 / 432x768 / 405x720 / 360x640 |
自动算出的任意小数(如 484x861)会出问题,见 11.5 |
跳跃是「触发器 + 抛物线」,不是「播一段跳跃动画」。 动画只管姿态、位移由引擎算,
所以腾空时锁同一帧、跳多远都成立。姿态按进度 t 取:
t<0.18 蹲 → t<0.30 蹬 → t∈[0.30,0.86) 恒为腾空帧 → t≥0.86 落地。
九、十二条可执行的验收标准#
旧版八条全部通过,但用户看到实物图的原话是「美术风格完全不对啊」。 原因很直接:旧八条里一条都没量"明暗关系" —— 它只量了"线够不够细""有没有纯黑" "有没有高饱和",而翻车翻在"整幅亮了一档、暗部为零"。 标准漏了维度,全过也没有意义。
新加的 1~3 条就是补这个维度。补完之后又发现第二个问题: 单件素材和整幅场景不能共用一个口径。
9.1 两个口径(这一步不能省)#
| 口径 | 适用 | 为什么不一样 |
|---|---|---|
| 场景口径 | 整幅画面(scene-preview.png、shot-*.png、ref-decorative-2x.png) |
画面被背景填满,暗部占比是有意义的整体指标 |
| 单件口径 | 带透明背景的素材(props/*.webp、flowers/*.webp、anim/*.webp) |
只用不透明像素统计。合成到纸色底再算整幅,透明区会变成亮像素把暗部稀释掉 |
按场景口径量 bush-0.webp,结果是「平均明度 0.561、暗部 0.0%」——
看着像"灌木的轮廓没画出来"。按单件口径重量的真值是
平均明度 0.482、暗部 10.8%,轮廓完全正常。
差的那 10.8% 全是透明区被合成成纸色造成的。
一个口径错误可以凭空造出一个不存在的问题。
9.2 判据(三个口径,tools/art/check_tone.py)#
第一轮:拿"全图暗部 ≥5%"当判据 → 全部场景不达标。 查下来它量的是构图密度:参考图是一条花纹边框,元素填满 67.5% 画幅; 我的场景只有 18.4% 是元素,其余是天空 —— 全图暗部永远到不了 5%。 而按元素口径看,我的画法比参考图还暗一档(13.8% vs 10.5%)。 拿一个量构图的指标去判画法,会得出"画法不合格"这个完全错误的结论。
第二轮:阈值定 8% → 参考图自己只有 7.8%,基准图被判不合格。 阈值必须低于基准图的实测值,否则就是自相矛盾。改成 6%。
场景口径(整幅画面,--scene)
| 判据 | 阈值 | 基准图实测 | 我的场景实测(五带) |
|---|---|---|---|
| 整体平均明度 | 0.55 ~ 0.72 | 0.601 | 0.571 ~ 0.595 |
| 饱和(P90) | ≤ 22% | 15.2% | 14.4 ~ 17.8% |
| 色度(P50) | ≥ 13 | 12 | 15 ~ 22 |
| 元素内暗部占比 | ≥ 6% | 7.8% | 22.0 ~ 24.2% |
| 元素内平均明度 | ≤ 0.72 | 0.642 | 0.601 ~ 0.618 |
现场回放:用户说「灰度过重、对比不足」,而我跑完 check_tone 得到的是一排 ✓。 为什么?因为当时只有饱和度的上限(10%),没有下限 —— 而上限拦的是"太艳",根本拦不住"太灰"。 我的场景当时 P90 只有 3.9%(基准 15.2%),色度只有 6(基准 12), 一路绿灯。
判据有效性的验法(这条以后照做):拿三个已知样本跑一遍, 旧版必须被拦下、新版必须通过、基准在中间 ——
| 样本 | 饱和 P90 | 色度 P50 | 色度≥13 的判定 |
|---|---|---|---|
| 旧版(用户说太灰那版) | 3.9% | 6 | ✗ 拦下 ✓ 正确 |
| 基准图 | 15.2% | 12 | ✗(基准不参与判定,只比对) |
| 新版(现行) | 14.4~17.8% | 15~22 | ✓ 通过 |
三个样本一眼可分,判据才算立住。只能"全过"的判据等于没有判据。
[!warning] 反过来也要防:主观评价不构成判据,除非它对基准图有区分度 这一轮修完之后,我把图送去视觉复核,回答仍然是「整体明显发灰发闷」。 看起来像是"改了个寂寞",但同一套提问拿去问基准图,回答是「这张图整体偏灰」—— 参考图自己也是灰的。
所以"发灰"这个评价对基准图同样成立,它不区分"做对了"和"做过灰了"。 这种评价不能当判据用,只能用来看"有没有硬伤"(错位、糊成一团、元素丢失)。
能不能当判据,验法是一样的:拿基准图跑一遍。 基准图也会被判"不合格"的判据,一律不能用 —— 无论它是脚本还是人。 (这和"阈值必须低于基准值"是同一条规矩的两个面: 一个是数值面,一个是主观面。)
[!note] 为什么是「元素内」而不是「全图」
elem_mask() 用局部对比(与高斯模糊的差 >0.05)把元素圈出来 ——
平滑的天空和地面底色不会被算进去。这样量出来的才是画法,
不受"这一关天空留了多少"影响。
单件口径(带透明背景的素材,--sprite)
| 判据 | 阈值 | 说明 |
|---|---|---|
| 最低明度 | ≤ 0.45 | = 描边真的画出来了 |
| 平均明度 | 0.30 ~ 0.85 | 草偏暗(0.39)、石头偏亮(0.68)都正常 |
「暗部占比」量的是【线条面积 / 总填充面积】,偏向"线条多"的素材:
| 素材 | 暗部占比 | 实际情况 |
|---|---|---|
grass-0 |
33.1% | 全是竖线 |
tree-pine-0 |
10.1% | 密实剪影 |
tree-broad-0 |
2.9% | 亮灰叶团 |
rock-0 |
2.2% | 描边明明画出来了 |
石头只有 2.2%,但它的描边完全正常。用占比判它,会得出"石头没有描边" 这个错误结论,然后去改一个根本没问题的函数。
面板口径(UI 占大半画幅的截图,--panel)
线索卡 / 小游戏 / 手账这三张截图,UI 面板占了画幅大半。 面板本来就该比画面亮(可读性优先),所以只判「整体明度 ≤ 0.78 + 饱和 ≤ 22% + 色度 ≥ 13」, 不判元素暗部 —— 否则等于要求"对话框也要够暗"。
拼接图口径(--composite,只给 scene-preview.png 这一张静态拼接图)
「元素内暗部」靠 elem_mask() 圈元素,判据是局部对比(与高斯模糊差 >0.05)。
静态拼接图(先铺底、再叠物件)里树和灌木是一整块平涂,模糊值和它自己一样,
根本圈不进来;能圈进来的只有细描边。
于是这一条量的变成【描边面积 / 总填充面积】,不是画法。
| 图 | 元素内暗部 | 说明 |
|---|---|---|
运行时截图 band-1~5 |
20.7 ~ 22.5% | ✓ |
静态拼接 scene-preview |
1.3% | ✗ 同一套色板、同一套画法 |
差 16 倍,差的是图的做法,不是画法。 单件素材当时踩过同一个坑(石头 2.2% 被判"没描边"),那时改判"最低明度"; 拼接图没有 alpha,改不了最低明度,所以直接不判这一条, 只判它对得上的三条:整体明度、饱和(防艳)、色度(防灰)。
它真正该验的是"色调统不统一、前后景分不分得开",那两条看第八节的图,不靠脚本。
基准比对(--bench <参考图>)
参考图是基准,不是被判定的对象。拿"给场景定的规则"去判基准图, 会得出"基准图自己不合格"(第一版就出了这个结果)。 基准只打印实测值供比对,不计入通过数。
9.3 脚本#
# 单件素材
python3 tools/art/check_tone.py --sprite "docs/野花笔记/_art/props/*.webp"
python3 tools/art/check_tone.py --sprite "docs/野花笔记/_art/anim/*.webp"
# 场景(运行时截图,判全部五条)
python3 tools/art/check_tone.py --scene docs/野花笔记/art/band-*.png
# 静态拼接图(不判元素内暗部,理由见上)
python3 tools/art/check_tone.py --composite docs/野花笔记/art/scene-preview.png
# 基准比对
python3 tools/art/check_tone.py --bench docs/野花笔记/art/ref-decorative-2x.png docs/野花笔记/art/band-1.png
# 面板(线索卡 / 小游戏 / 手账)
python3 tools/art/check_tone.py --panel docs/野花笔记/art/shot-branch.png docs/野花笔记/art/shot-mini*.png
# 饱和度(第 9、10 条)
python3 tools/art/check_sat.py docs/野花笔记/art/*.png docs/野花笔记/_art/props/*.webp
- 口径混用:按场景口径量
bush-0.webp得出"暗部 0.0%、轮廓没画出来", 按单件口径真值是 10.8%、完全正常。一个口径错误可以凭空造出问题。 - 判据量错了对象:全图暗部量的是构图,不是画法。见上面第一轮。
- 阈值高于基准值:定 8%,基准图 7.8% —— 自相矛盾。
- 暗部占比偏向线条多的素材:石头 2.2% 被判"没描边"。
- 过期截图:
shot-band2.png是 12:15 的旧文件(生成它的逻辑早删了), 带着旧色板混进验收,伪造出两张"不达标"。验收前先确认截图是本轮生成的。 - UI 面板按场景判:线索卡截图被判"太亮",而面板本来就该亮。
- 判据的方向搞反了:饱和度上限拦的是"太艳",拦不住"太灰"。 我拿一个上限去对付"灰度过重",于是用户看得见的问题在验收里是隐形的。 "太灰"和"太艳"是两个方向,各要一条判据。
- 阈值比基准还严:色板色号饱和定 ≤20%,而 HSV 饱和在暗色上会自动虚高
(
#292D35明度 0.18 → HSV 饱和 24.5%,色度却只有 12、和基准一致)。 差点为了迁就一条错判据,把本来就对的暗色改坏。对暗色用色度,不用 HSV 饱和。 - 色板值 ≠ 屏幕值:我按色板算"天空与远山差 0.19",实测屏幕上只有 0.043 ——
因为天空有渐变、远山被混色、地面被提亮。
同时树带画在 y 47%~69%(
SKY_H+4起、高 160),我却一直量 43%~51% —— 那段全是天空。量之前先确认元素到底在哪个区段。
十、演示里的实测截图#




演示里的地形、花、背景全是 canvas 代码画的(tools/art/scene/notes.js),
只有兔子是序列帧(tools/art/notes_art.py 生成的那 5 组)。
目的是先把构图、比例、节奏、配色验掉;正式素材仍然按第五节的规范做。
配色与线条规则是同一份(都取自第二节),所以这三张可以用来验第八节那八条。
十一、判据的失败记录:同一类错误在生图上连摔四次#
第九节那十二条判据是对的。这一节记的是"我拿着对的判据,还是把同一件事做错四次"。
11.1 事件经过#
用生图做五带背景,一共做了三轮,失败方向正好相反:
| 轮次 | 平均明度 | 色度 | 元素内暗部 | 饱和 P90 | 判定 |
|---|---|---|---|---|---|
| 第一轮 | 0.738 ~ 0.838 | 4 ~ 12 | — | — | 太亮 |
| 第二轮 | 0.374 ~ 0.485 | 14 ~ 26 | 27 ~ 48% | 17.6 ~ 21.9% | 太暗 |
| 第三轮(联合标定) | 0.590 ~ 0.614 | 13 ~ 17 | 6.9 ~ 21.4% | 13.0 ~ 14.8% | ✓ 全部达标 |
| 判据 | 0.55~0.72 | ≥13 | ≥6% | ≤22% |
每一次都是"修一个方向,冲到相反方向":
| 我修的问题 | 加的手段 | 结果 |
|---|---|---|
| 太亮(0.84) | 加"约 40% 像素暗于 30% 明度"的硬约束 | → 太暗(0.38) |
| 太暗(0.38) | 用 gamma 提亮到基准 | → 暗部被提没了(47.5% → 3.8%) |
| 太灰(色度 3) | 加"必须有可辨认的蓝绿倾向" | → 太艳(饱和 32%,上限 22%) |
| 太艳(饱和 32%) | 向亮度混合降饱和 | → 才能同时满足四条 |
11.2 这四次摔的是同一个坑#
「"太灰"和"太艳"是两个方向,各要一条判据」
我嘴上引用它,手上每一次都只推一个方向。而且不止"灰/艳"这一对 —— "亮/暗"也是两个方向、"暗部多/暗部少"也是两个方向,全都犯了同一个错。
真正的教训不是"下次小心",是一条操作方法:
加任何一条约束时,必须同时问一句「它的反方向会不会冲过头」, 并把反方向的界一起写上。
只写一边(无论是写进提示词,还是写进调参循环),结果必然冲到另一边。
11.3 附带发现的三个技术点#
① 提亮会吃掉暗部,所以提亮曲线要分段。
普通 gamma 是整条曲线一起抬,暗部跟着上去,元素内暗部 直接从 47.5% 掉到 3.8%。
改成分段曲线:knee 以下原样不动,只提中高调 —— 暗部保住了,明度也能上去。
溪谷林地靠这一条拿到 暗部 8.9% / 色度 14 / 明度 0.610。
② 降饱和和提亮要一起解,不能分开调。
石楠台地最后用的参数是联合解:k=0.62(向亮度混合的降饱和系数)g=0.52 knee=0.10。
先降饱和再提亮,或反过来,都试不出解。
③ 验收脚本的调用方式也会骗人。
有一次扫描找不到解,查下来是筛选条件用文件名匹配 heath,
而候选文件存成了 /tmp/cand-h4.png —— 条件从来没匹配上。
"无解"和"筛选写错"在输出上长得一模一样,都是空结果。
11.4 一条更早的同类记录(背景之外的)#
第四轮修"灰度过重"时也踩过同一个坑,那次记在 4.1.6:
- 当时只有「饱和 ≤10%」这一条上限,而上限拦的是"太艳"。 于是场景 P90 只有 3.9%(基准 15.2%)却一路 ✓。 用户看得见的问题,在验收里是隐形的。
只有单边的判据 = 没有判据。 加新判据时,先问反方向在哪。
11.5 追一条"竖向接缝",最后发现它根本不在画里#
场景页上一直有一条贯穿整幅高度的竖向分界线,视觉复核连着三轮都报"有接缝"。 追了很久,最后证明是无头截图通道的伪影,画布内容本身从头到尾都是干净的。
决定性的一步是同一瞬间、两种取法(cam=1858):
| 取法 | 中点处的台阶 | |
|---|---|---|
cv.toDataURL()(实际显示的内容) |
0.57 | ✓ 干净 |
Page.captureScreenshot()(CDP 截图) |
4.47 | ✗ 有台阶 |
前面三轮我一直在优化画布,但报出问题的那个数是截图量出来的。 中间一度把已经改对的东西又改坏(为了压这一条,把纸纹颗粒从 0.010 加到 0.018)。 任何一个量出来的异常,先确认量的对象是不是用户真正看到的那个。
追的过程中排除掉的(都留下了有价值的结论):
| 怀疑对象 | 结论 |
|---|---|
| 平铺回绕接缝 | 已修,且确实该修。paste_wrap() 环绕粘贴 + 改用 createPattern 平铺(手动画三份会在交界处出问题)。四层里 l1 的环绕差从 12.42 降到可忽略 |
drawImage 偏移没取整 |
无关。取整之后台阶一点没动,说明不是子像素相位 |
CSS filter 加在 canvas 上 |
无关,但顺手改掉了。调色改烘焙进图层(grade()),页面一个 filter 都不用加 |
| 平铺/缩放的尺寸比例 | 无关,但顺手改掉了。缩放吸附到整数比(任意小数比更差) |
顺手捞到的两条:
- 无头浏览器里 rAF 被节流,改完参数画布根本不重绘,截到的是旧帧。 我有一轮跑 18 组调色参数,结果18 组数字完全相同 —— 当时差点当成"调色对画面没影响"。 改完必须强制重绘一帧(或者干脆重新加载页面)再截。
- 逐层排查要用画布快照,不要用截图。
off.toDataURL()/cv.toDataURL()拿到的是真实像素,不受合成通道影响。这个手段一次就把范围缩到了"画布之外"。