🌻 设计文档小农场 · 野花笔记
← 全部文档/野花笔记/美术

动画与场景设计

提案 #美术#生图#动画#序列帧#场景#背景 docs/野花笔记/动画与场景设计.md
这一批的做法:**画风只写一次,全批共用**

所有提示词都从 tools/art/gen_batch.py 里的同一个 STYLE 常量拼出来 —— 线宽、色板、纹理做法一个字都不改。这是这一批"看起来是一套"的原因。

上一轮的教训是:每张图各写各的提示词,出来的东西色相、线宽、明度全不一样, 拼在一起就是拼贴画。

全部走 图生图(/v1/images/edits + gpt-image-2pro): 动物拿参考图拆出来的那只兔子当风格参考,植物拿元素拆离表当风格参考。 文生图传不过风格,只有图生图能。

一、动物动画#

每一张都是一行 N 个连续姿态(一张图一次生成 → 帧与帧之间风格天然一致)。 下面每个都可以暂停 / 逐帧 / 调速度 —— 判断动画好坏要逐帧看,比看一遍循环有用。

这一节的做法改过,两个坑都修了

① 切件不能用等分格子。 原本用 tools/art/spritesheet.py 按 cols×rows 等分切。这对"姿态乖乖待在自己格子里" 的素材是对的,但生图的姿态会超出格子 —— 实测兔子跳跃那张,6 个姿态里有 2 个左右都顶到格子边界, 也就是兔子被切断了一部分。

而"等分切"本身看不出这件事:它总会切出 6 张图,每张大小一样,只是内容缺一块。 是靠逐帧边界检查(内容贴住单元格最外一列像素)才发现的 —— 修的方法是换 tools/art/pose_sheet.py,走二维连通域切件,跨格子的姿态也能整只切出来。

修之前 修之后
兔子跳跃 2 帧左右贴边 + 3 帧底部贴边 全部正常
兔子奔跑 正常 正常
麻雀 3 帧底部贴边 全部正常
萤火虫 4 帧底部贴边 全部正常
甲虫 / 蝴蝶 正常 正常

② 没有透明底。 spritesheet.py 里的 normalise() 写死了 Image.new("RGB", ...) —— 于是每帧都被刷上生图那张纸的米白色。在文档的浅色底上看不出来, 一旦叠到真实场景上就是一块块米白方块。 现在默认走 RGBA + 抠底(--no-alpha 可关)。11 张精灵表实测 alpha 范围全是 0~255。

教训:RGB 画布在浅色页面上"看起来没问题",这类 bug 只有换背景/叠图才会暴露。

1.1 小兔子 · 奔跑循环(8 帧)#

兔子奔跑 · 8 帧循环
帧 1 / 8

1.2 小兔子 · 跳跃(6 帧)#

兔子跳跃 · 蹲身 → 蹬地 → 上升 → 最高点收腿 → 下落伸腿 → 落地压扁
帧 1 / 6

1.3 小麻雀 · 跳跃扇翅(6 帧)#

小麻雀 · 跳跃与抖翅
帧 1 / 6

1.4 萤火虫 · 飘浮(6 帧)#

萤火虫 · 缓缓飘浮(腹部亮度逐帧变化)
帧 1 / 6

1.5 甲虫 · 爬行(6 帧)#

甲虫 · 六足交替爬行
帧 1 / 6

1.6 蝴蝶 · 扇翅(6 帧)#

蝴蝶 · 翅膀由合到开
帧 1 / 6
视觉复核:这 6 张都过了

「兔子奔跑 8 只、腿姿交替能看出循环、形象一致、无重叠无裁切」; 其余 5 张同样过了"数量对、姿态在变、形象一致、画法统一、无重叠"。

但"能看出在动"和"动得对"是两件事。 真正的验收应该是 逐帧看关节有没有跳、脚有没有穿地 —— 这一条还没做。

二、植物(5 条风景带 × 6 种 = 30 株)#

五带植物总览 · 每行一条风景带,每行 6 株
五带植物总览 · 每行一条风景带,每行 6 株
风景带 植物形态
苔原草坡 低矮垫状植物、细硬短草、微小高山花、灰色地衣、小型莎草
石楠台地 石楠状灌木(密生小铃花)、低矮木质茎、细针叶
溪谷林地 蕨类、阔叶林下植物、苔藓团、细高的耐荫草
砾石滩 微小景天、匍匐垫状植物、细弱草丛、灰叶小植物
高地野花田 高杆野花、种子头、三叶草、带刺蓟状植物、羽毛状草

出图原稿(每带一张,6 株一排,由程序切开):

带 原稿
苔原草坡 苔原草坡 6 株原稿
石楠台地 石楠台地 6 株原稿
溪谷林地 溪谷林地 6 株原稿
砾石滩 砾石滩 6 株原稿
高地野花田 高地野花田 6 株原稿
蕨类和禾草"没有花头"是对的,不是缺陷

视觉复核对这批的意见是:「多数有茎、叶和花头,但蕨类、叶状植物及部分禾草的花头不明显或缺失」。 这一条不是问题 —— 蕨类和草本来就不开花。提示词里写的是"有种子的地方要有种子头", 模型按植物学常识处理是对的。不要为了"每株都要有花头"去改,那会做出假的植物。

2.1 植物摇摆动画(连续变形,不是生图序列帧)#

五条带各挑一株做摇摆,8 帧:

苔原草坡 · 植株摇摆
帧 1 / 8
石楠台地 · 植株摇摆
帧 1 / 8
溪谷林地 · 植株摇摆
帧 1 / 8
砾石滩 · 植株摇摆
帧 1 / 8
高地野花田 · 植株摇摆
帧 1 / 8
摇摆方向写反过,一行代码的事

第一版是 t = 1 - y/(h-1)(顶=1)之后又算 bend = (1-t)**1.6 —— 两次取反正好抵消,结果变成底部摆得最大、顶端不动, 整个像地里长出来的东西在扭腰。正确的写法是 bend = t**1.8(t 就是"离根部有多远")。

验法也不能靠肉眼:逐帧的像素差会把"哪儿植物多"和"哪儿在动"混在一起 (内容多的地方差值天然大)。要量每 10% 高度带的水平重心位移:

高度带 位移
顶 0~10% +7.7 px
10~20% +13.2 px(最大)
50~60% +4.1 px
80~90% +0.5 px
底 90~100% 0.00 px(根部锁定)

现在脚本落在 tools/art/plant_sway.py。

做法: 绕根部旋转 + 按行水平位移 + 相位滞后 —— 底部固定,越靠上摆幅越大, 再叠一层两倍频的小抖动让叶子不是整体硬摆。

为什么不生图:一株植物只有一个姿态可参考,让它"再摆一个姿势", 出来的就是另一株植物了(生图序列帧的老毛病:帧间形变)。 连续变形是同一张图在动,帧与帧之间只差一个显式参数 —— 改一次摆幅,8 帧一起变。

这条和 小农场 那边"走路动画在图是一坨的时候用连续变形"是同一套办法 (tools/art/walkers.py)。

三、场景背景#

背景一共做了**三轮**才拿到可用结果 —— 前两轮全部报废

三轮的失败方向正好相反,这是这一节最值得记的东西:

轮次 平均明度 色度 元素内暗部 结论
第一轮 0.738 ~ 0.838 4 ~ 12 — 太亮(基准 0.601),发白像蒙雾
第二轮 0.374 ~ 0.485 14 ~ 26 27 ~ 48% 太暗,色度和暗部对了但整体压黑
第三轮(gamma 标定) 0.588 ~ 0.597 9 ~ 17 3.4 ~ 21% 3/5 达标

第一轮为什么亮:我在提示词里写了「远处更亮更朦胧」,却没说整幅该压在哪个明度上, 它就把天空和白雾一路推到接近纯白。另外第一轮还是横构图, 而这是个竖屏 9:16 的游戏 —— 一起改成竖构图重出。

第二轮为什么暗:我为了修"太亮",加了一段硬性明度约束 (天空最多占上面 30%、约 40% 的像素暗于 30% 明度、明确写"发白的高调画面视为失败")—— 然后模型就往反方向冲过头了。

第三轮为什么只过 3/5:我用 gamma 把明度拉到基准 0.601, 但 gamma 提亮是把暗部一起提掉的 —— 石楠台地原本暗部 47.5%, 提亮后只剩 3.8%。于是"明度"和"暗部"两条判据互相打架。

解法是换掉提亮曲线:改成分段曲线,knee(本次取 0.08~0.16)以下的像素 完全不动,只提亮中高调。这样暗部保住了,明度也能上去。 溪谷林地靠这一条拿到 暗部 8.9% / 色度 14 / 明度 0.611,全部达标。

[!warning] 这里踩的是我自己在 4.1.6 写过的坑,而且踩了两次 第一次:只有"不许加饱和"这条上限,没有色度下限 —— 于是第二轮出来的背景色度掉到 3(要求 ≥13),画面是纯中性灰, 而明度和暗部全部合格。"太灰"和"太艳"是两个方向,各要一条判据 —— 这条是项目里白纸黑字记着的,我还是漏了。 第二次:"太亮"和"太暗"也是两个方向,我修第一个的时候直接把第二个做出来了。

3.1 五条风景带(竖屏)#

第三轮成品(竖构图 + 明度标定):

苔原草坡 石楠台地 溪谷林地
苔原草坡背景 石楠台地背景 溪谷林地背景
砾石滩 高地野花田
砾石滩背景 高地野花田背景

五带横向拼在一起,检查"是不是同一个世界":

五带背景拼接条
五带背景拼接条
背景的构图约定(写死在提示词里的三条)
  1. 天空最多占上面 30%,而且天空本身是灰蓝(#808090~#B8B8BE),不能是白/米白;
  2. 景深只用明度做:远处更亮更糊、近处更暗更实;不用强透视,地平线不在正中;
  3. 底部约 25% 必须是简洁干净的地面带 —— 角色要站在上面,那一带不能有杂物, 否则角色读不出来。这条和场景演示里"动物道必须在地平线以下"是同一个道理。

四、验收结果(跑的是项目自己的脚本,不是我目测)#

python3 tools/art/check_tone.py --scene docs/野花笔记/art/bg2/*.png
图 平均明度 色度 元素内暗部 饱和 P90 判定
苔原草坡 0.591 15 21.4% 13.8% ✓
石楠台地 0.614 13 10.9% 14.8% ✓
溪谷林地 0.610 14 8.9% 13.0% ✓
砾石滩 0.597 13 6.9% 14.4% ✓
高地野花田 0.590 17 11.3% 14.6% ✓
五带拼接条 0.600 14 8.6% 14.1% ✓
判据 0.55~0.72 ≥13 ≥6% ≤22%
基准参考图 0.601 12 9.9% 15.2%

6/6 全部达标。

石楠台地是**重出一张**才过的,而且它暴露了一个更值钱的问题

第一版石楠台地色度只有 10、暗部只有 3.8%(要 ≥13 / ≥6%)—— 图本身画得太平, 没有真正的暗块也没有可辨认的冷色调。这一条调 gamma 修不了: 提亮只会让明度合格而把暗部打得更低(实测提亮后暗部从 47.5% 掉到 3.8%)。

重出了一张之后,它又冲到了另一个极端:色度 22(对)、暗部 25%(对), 但饱和 P90 = 32%,超过 ≤22% 上限 —— 从"太灰"直接跳到了"太艳"。

最后是联合标定(先向亮度混合降饱和,再分段提亮)才同时满足四条: k=0.62(降饱和系数)g=0.52 knee=0.10。

[!danger] 这一轮我一共在同一个坑里摔了四次,全是同一个错误 4.1.6 写着「"太灰"和"太艳"是两个方向,各要一条判据」。 我嘴上引用它,手上却每一次都只推一个方向:

方向 第一次 反方向 结果
明度 太亮 0.84 太暗 0.38 两个都要判据
彩色 太灰 色度 3 太艳 饱和 32% 两个都要判据

修"太亮"的时候把"太暗"做出来了;修"太灰"的时候把"太艳"做出来了。 教训不是"下次小心",是加约束时必须同时把反向的界也写上 —— 只写一边,模型(或你自己调参)必然冲到另一边。

[!note] 这 6 张的明度都是标定出来的,不是生图一次命中 生图原始明度在 0.374~0.481,全部偏暗。用分段 gamma + 降饱和后处理拉到基准。 严格说这 6 张的明度是"我调的"而不是"模型给的"。 记在这里是为了下次不要以为一次生图就能命中。

五、场景元素素材(场景是拼出来的)#

场景**不再整张生成**,改成用元素拼

整图生成有两个问题: ① 每次出来的东西不一样,改一处要重生成整张; ② 背景和前景焊死,没法做视差滚动 —— 而视差是横版游戏的核心。

所以背景拆成两层来做: 远景用整张画(云、天光这种没法拼的),中近景全部用元素拼。

5.1 六类元素,每类 6 个变体 = 36 件#

场景元素总览 · 石头 / 树木 / 灌木 / 草丛 / 枯木 / 远山,每行 6 个变体
场景元素总览 · 石头 / 树木 / 灌木 / 草丛 / 枯木 / 远山,每行 6 个变体
类别 6 个变体
石头 圆巨石 / 平板石 / 棱角中石 / 卵石堆 / 半埋的长苔石 / 磨平的踏石
树木 高窄针叶 / 圆阔叶 / 分层塔形 / 幼树苗 / 枯树 / 低矮灌木树
灌木 圆密灌丛 / 散枝矮丛 / 匍匐垫状 / 带小浆果 / 发白霜的 / 带刺棘丛
草丛 高细草丛 / 短硬草簇 / 宽叶莎草 / 苔藓块 / 小蕨丛 / 匍匐地被
枯木 倒木带断枝 / 树桩 / 扭曲枯枝 / 小枝堆 / 空心木段 / 翻起的根盘
远山 圆缓丘 / 尖脊 / 崖边 / 远处树线 / 缓坡 / 平顶台地

每个类别里的 6 个是同一族的不同变体(提示词里明写"look like they belong to the same landscape — a set of variants of the same family of objects"), 所以它们摆在一起不会互相打架。

远山那一组是**故意画得更亮更淡**的

它是视差的最远层,提示词里要求"FLAT SILHOUETTES only, LIGHT hazy blue-grey, clearly LIGHTER in value than the other props"。摆进场景时还会再压一层透明度。

5.2 可玩的走路测试场景(先玩这个)#

这是一个能操作的真场景,不是截图

按住屏幕左半边 → 兔子转身往左走;按住右半边 → 往右走;松开就停。 手机和电脑都能用(电脑也可以按 ← → 键)。

场景里是四层视差 + 角色:远山几乎不动、中景树慢慢移、地面和兔子同速、前景草比兔子快。 兔子用的是真实的 8 帧奔跑精灵表(左走时水平翻转)。

野花笔记 · 走路测试场景(按住左右半屏)

搭建方式:tools/art/build_walk_demo.py,用第 5.1 节那 36 件元素拼出来, 四层各导出成一张宽 2 屏的透明图,运行时无缝平铺 + 按视差因子滚动。

层 视差 内容
L0 0.20 天空渐变 + 远山
L1 0.45 中景树 + 灌木(整体压暗到 0.74)
L2 1.00 地面 + 碎石 + 小径 + 近处石头枯木
L3 1.35 前景草(最高点不超过走线)

每次改完必须在无头浏览器里实测,不能只看代码:

node /tmp/walk_check.mjs      # 起 Chrome + CDP,按住半屏 1.4s,截图 + 读状态
调试时踩的两个坑
  1. 截图视口高度不是窗口高度。窗口设 600×1000,实际视口只有 600×861(少了浏览器 chrome), 于是 CSS 缩放系数从 1.11 变成 0.897。按错的系数去裁剪"兔子在哪", 裁到的是背景 —— 然后就会得出"兔子没画出来"这种错误结论。
  2. 视觉模型对整图的评价会偏严,对局部放大的评价才准。 整图它说"兔子被遮挡 60~70%",放大裁剪后说"轮廓清晰,没有和背景糊在一起"。 判断"某个东西清不清楚"要放大看,不要看全景。

5.3 拼出来的五带场景#

用 tools/art/compose_scene.py 拼,540×960 竖屏:

苔原草坡 石楠台地 溪谷林地
苔原草坡 石楠台地 溪谷林地
砾石滩 高地野花田
砾石滩 高地野花田

图层顺序(从远到近):天空渐变 → 远山 → 中景树 → 近景灌木 → 地面 → 石头/枯木 → 前景草 → 角色

每层都带自己那一档的透明度和明度衰减(远山 0.55、中景树 0.78、近景 1.0), 景深是用透明度+明度做的,不是用颜色做的 —— 这样换配色时层次不会塌。

拼接的"假"是怎么暴露的 —— 视觉复核的三轮反馈

第一轮:「底部横向硬切线明显;下方留白过大,像一整块空黑地面;前中后景层次偏平」。

第二轮(加了地面渐变 + 碎石点之后):「横切线仍很直、明暗跳变明显; 还是偏"一大块空黑",纹理不足」。

第三轮(改起伏地平线 + 密铺地被之后):「基本不是直线了;不再是空的了; 能读成自然场景」。

三处修改分别是:

  1. 地平线改成起伏的。直线地平线是"拼贴感"最大的来源 —— 一眼就能看出地面是一块贴上去的矩形。用三个频率叠加的正弦做边缘(振幅 9 / 5 / 2.5 px)。
  2. 地面自身要有明度层次。上段暗、下段亮(0.84 → 1.18),否则就是一块死黑。
  3. 地被要密铺到地面上,不能只摆在地平线上。 原来的元素全都贴着地平线站,够不到下面,所以下半幅是空的。 现在把草丛和碎石缩小了铺 46 个到地面带里,越靠下越小越淡(近大远小的简易版)。

[!tip] 这一条是可以推广的 "拼贴感"通常不是元素画得不好,是接缝太规则。 直线边界、均匀间距、同一尺寸 —— 这三样才是"一看就是贴上去的"的原因。

剩下的问题(已记录、暂未改):「前景土层仍偏平且纹理均匀; 部分植物和山体像贴上去的,尺度/透视统一性不足」。

六、验收结果(跑项目自己的脚本,不是目测)#

python3 tools/art/check_tone.py --scene docs/野花笔记/art/bg2/*.png

6/6 全部达标(5 张背景 + 1 张拼接条)。判定口径:明度 0.55~0.72 / 色度 ≥13 / 元素内暗部 ≥6% / 饱和 P90 ≤22%。

背景是"调"出来的,不是生图一次命中

生图原始明度在 0.374~0.481,全部偏暗。用分段 gamma + 降饱和后处理拉到基准 0.601。 严格说这 6 张的明度是"我调的"而不是"模型给的"。 记在这里是为了下次不要以为一次生图能命中。

本节详细的失败数据已移到 docs/野花笔记/美术规范.md 第十节, 因为那是画风基准文档,判据本身该记在那里。

七、还没做的#

不要把这些当成已完成
  1. 帧数偏少。奔跑只有 8 帧、其余 6 帧。1013fps 下能看, 但要更顺需要 1216 帧 —— 生图一次出不了那么多(多了每格会糊)。
  2. 植物只挑了 5 株做摇摆(每条带 1 株),剩下 25 株还是静态的。
  3. 背景还没合成进运行时场景。演示页 notes.html 里的场景仍然是代码画的几何图形 —— 这批生图素材目前只出现在本文档的图片里。
  4. --align 用的是 none。奔跑和跳跃这两组位置会漂(生图的帧间位置本来就不一致), 没做 feet 对齐 —— 因为这两组的姿态本身就在上下移动,feet 对齐会把跳跃抹掉。 正确的做法是手动给每组一个对齐基准,还没做。
  5. 动物素材还没过 check_sat.py / check_tone.py,只过了视觉复核。 背景是过了的(见第四节,6/6 达标)。
  6. 植物摇摆脚本还在 /tmp,按工程约定应该挪进 tools/art/ 并提交。