Qwen-Image2.1 游戏素材工坊:用 ControlNet Union 让角色真正走起来(三)
Qwen-Image2.1游戏素材工坊:让角色真的走起来(三) - 又过二十
Qwen-Image2.1 游戏素材工坊新增 13.3 姿态控制,用程序化 OpenPose 骨架加 ControlNet Union int8 硬控姿势,解决此前 64 格走路表全是同一站姿的问题。改造引入 2.5D 斜 45° 视图锁、三层分布出帧(160 帧仅 82 次出图)和每朝向人工确认闸门,共 11 个提交。目前骨架仅覆盖 idle/walk/run,真机效果尚未验证。
前两篇写的是「素材像不像、能不能用」——分类页签、画幅有据可查、仿真测试、运行时实测。这两天(10-27 ~ 10-29)换了个更硬的问题:角色得动起来。
一张立绘 → 8 个朝向 × 站立/行走/奔跑三段动画 → 一张能直接塞进引擎的精灵图集。中间踩的坑比预想多,最狠的那个是:真机上跑出来的 64 格走路表,64 格是同一个站姿。
2026-10-27 ~ 10-29 · 项目地址
https://github.com/wyfirstname/qwenimage-comic
2.5D 视图锁三层分布出帧人工确认闸门ControlNet Union 姿态控制
0. 两天改了什么
| 版本 | 主要改造 | 一句话 |
|---|---|---|
| 10.0 | 动画帧 + 2.5D 视图锁 | 给所有素材锁一台相机(斜 45°),角色/宠物第一次能走 |
| 11.0 | 美术方向契约 | 八轴风格圣经 + 自动推导,提示词从散句改成八段块结构 |
| 11.1 | 经典整表 | 对齐大话西游式精灵表:行=朝向、列=步态帧 |
| 12.0 | 三段动画 | 站立 / 行走 / 奔跑,帧数按 clip 分开算 |
| 12.1 | 分步生成 | 帧画幅降档 384²、逐朝向落库、增量续跑 |
| 13.0 | 三层分布出帧(换路线) | 身份卡 → 关键姿态 → 插值补帧,160 帧只烧 82 次出图 |
| 13.0a | 续跑不信图 | 已经出过的连身份卡都不再出,直接复用 |
| 13.1 | 出一张显示一张 | 每落一张图冲一次库,一小时的生成过程不再静止 |
| 13.2 | 每朝向人工确认 | 首帧不对就当场停下,只亏 1 张而不是 10 张 |
| 13.3 | 姿态控制 | 程序化 OpenPose 骨架 + ControlNet Union int8 硬控姿势 |
共 11 个提交。下面按「为什么改」而不是「改了什么」来写。
1. 问题的起点:这不叫在走路
用户拿 16G 显存的机器跑了一整组,丢回来一张 8×8 的经典整表,只留一句话:
目前看到的问题依然很严重,角色姿态结果很差。

三件事一眼就能看出来:
- 朝向确实变了,但变得很勉强——每一格都是「正面的近亲」,背面像侧后 30°,侧面像正面歪了一点;
- 姿态完全没变——站立、行走第 1 帧、行走第 8 帧,手脚位置一模一样,只有衣服在抖;
- 这张表不能进引擎。引擎播放时看到的是一个人在原地微微晃动,不是走路。
这不是「生成质量差」,这是动画这件事根本没被建模。整篇改造都是围绕这句展开的。
2. 10.0:先给所有素材一台相机
第一版的想法很朴素:既然要拼成一张表,那就让所有帧共用同一台相机。
散图各自成画的毛病是——你让模型画「侧面走路」,它可能给你一个略微俯视的三分之四侧,下一张又变成平视。单张看都挺好看,排成一行就露馅:地平线不一样高、脚底不一样平、比例不一样大。
所以先写死一个 VIEW_LOCK:
VIEW_LOCK = {
"id": "iso45", "name": "2.5D 斜 45°",
"prompt": "2.5D isometric game art, 45-degree top-down camera, slight downward tilt, "
"consistent orthographic scale, chibi proportions about 3 heads tall, "
"full body visible, feet planted at the bottom of the frame",
"negative": "front orthographic view, straight-on eye-level view, flat top-down view, "
"fisheye, extreme foreshortening, tilted horizon, cast shadow ...",
}
正面平视、纯俯视、鱼眼、极端透视缩短、地平线歪——全部写进负面词。同时定死三件工程约定:
- 192×192 一个 cell,所有帧裁到 alpha 包围盒后按朝向统一缩放(用该朝向 idle 首帧定标尺),不逐帧各算,否则一胀一缩;
- 脚底锚点:每帧人物的脚底对齐到 cell 底部固定基线,不然跑起来会「浮空」;
- 镜像:
W ← E翻转,NW ← NE、SW ← SE。三档里 lite 只画 2 向(正面 + 右侧)就凑出 3 向,40 帧;full 8 向全实拍 160 帧。

到这一步,「能动」实现了。但只是能动。
3. 11.0 / 11.1:让提示词有据可依,让产出能直接进引擎
3.1 美术方向契约
动画一多,提示词就失控了:40 帧 × 每帧一段自由描述,风格必然漂。11.0 把「风格」从一句形容词升级成八轴契约——形状语言、剪影要点、明暗结构、色彩角色、材质笔触、细节密度、禁忌项、参考锚点。八轴可以先自动推导(按题材/背景/风格标签),也可以逐轴手改,改完自动展开进每一条提示词。
同时把提示词重排成八段块结构:
ROLE / PURPOSE → SUBJECT → VIEW → ART DIRECTION → POSE & ACTION
→ FRAME → TECHNICAL → OUTPUT CONTRACT
好处是每段都有明确的职责和固定的位置:换题材只动 SUBJECT 和 ART DIRECTION,VIEW 和 OUTPUT CONTRACT 是模板,不会被自由发挥污染。
配套加了 alpha QA 守门:假透明(边缘一圈白边)、裁边(内容贴到画布边界)、色数异常(<16 色警告,说明模型塌成了色块)。这些不是审美判断,是能自动判的,所以能进闸门重试。
3.2 经典整表
11.1 把整表形态对齐到老式 2D 游戏的做法:行=朝向、列=步态帧,行走一张、奔跑一张,图集旁边配同名的 .json(cell 尺寸、帧序、fps、锚点)。到这一步,「图集」才真的是能塞进引擎的图集,而不是一堆按命名规则排好的 png。
4. 12.0 / 12.1:三段动画,以及「别让一次失败报废一小时」
12.0 把动画从「一段行走」扩成三段:idle 站立呼吸、walk 行走、run 奔跑,帧数按 clip 分开记(站立 4、走 8、跑 8),主角和宠物都走同一套。
12.1 解决的是工程可行性,两个改动都很土但很有效:
- 帧画幅降档:动画帧原来跟着立绘走 512×768,单帧 2.7 倍像素。40 帧 × 最多 3 次重试能把单个角色拖到半小时以上(用户实测就是「主角久不出图」)。降到 384×384——落进 192 cell 是 2 倍采样,肉眼看不出区别,速度差别很大;
- 逐朝向落库 + 增量续跑:以前是「整组跑完才写盘」,中途任何一张失败,一小时白跑。改成每出一个朝向就先把该朝向的帧存下来并记账,重跑时只补缺的朝向。
第 4 节这两个改动后来救了很多次——包括 13.3 调骨架的时候。
5. 13.0:换路线——三层分布出帧
这是整个两天的转折点。
5.1 逐帧文生图的死结
12.x 的做法是「40 帧 = 40 次独立出图,每帧带同一张参考图」。问题是:
你在用一个概率模型去对抗「同一个人」。
每帧独立采样,模型每次都会重新「理解」这个人该长什么样:脸型漂、发色漂、服装细节漂、身材比例漂。守门程序能挡住黑图和跑色,但量不出「不像」——两个完全不同的美女,色调和 alpha 都是合格的。
5.2 三层:身份卡 → 关键姿态 → 插值
13.0 把身份从「每帧重新猜」改成「一次定死,之后只做姿态变换」:
- 身份卡:一张图里并排画同一角色的 3~8 个朝向。全朝向共用,只出 1~2 次。这张图同时还是一份「这个人到底是什么样」的定稿。
- lite 一张三栏(s/se/e);standard 一张四栏(s/se/e/n,
ne由n那一栏派生,省一栏);full 两张四栏。
- lite 一张三栏(s/se/e);standard 一张四栏(s/se/e/n,
- 关键姿态:每向只出
站立 2 + 行走 4 + 奔跑 4 = 10张。图生图的参考图 = 该朝向那一栏 + 基准图,姿态只在这一层发生。 - 插值补帧:关键帧之间的中间帧不烧显存,按「接触帧压低、过渡帧抬高」整帧垂直位移补:
KEY_POSES = {"idle": [0, 2], "walk": [0, 2, 4, 6], "run": [0, 2, 4, 6]}
TWEENS = { # 帧号 → (源关键帧, 垂直位移 px)
"walk": {1: (0, +3), 3: (2, -3), 5: (4, +3), 7: (6, -3)},
"run": {1: (0, +4), 3: (2, -6), 5: (4, +4), 7: (6, -6)},
}
账一下就变了:
| 档位 | 朝向 | 帧数合计 | 关键姿态 | 插值 | 真正出图次数 | 老办法 |
|---|---|---|---|---|---|---|
| lite 3 向 | 2 实拍 + 1 镜像 | 40 | 20 | 20 | 21(1 身份卡 + 20) | 40 |
| standard 5 向 | 5 | 100 | 50 | 50 | 51 | 100 |
| full 8 向 | 8 全实拍 | 160 | 80 | 80 | 82(2 身份卡 + 80) | 160 |
full 档从 160 次直接砍到 82 次,而且身份不再漂——因为身份不来自采样,来自那张身份卡。
5.3 13.0a:续跑连身份卡都不再出
原本「续跑」只跳过帧,身份卡每次都重画,而重画的身份卡会和上次不一样——第二趟的身份和前一趟对不上,等于白续。改成连身份卡都复用,续跑才是真的续跑。
6. 13.1 / 13.2:让它看得见,让它停得下来
6.1 出一张显示一张
13.1 之前,点下「生成」后界面完全静止一小时。用户的判断只有一种:这系统卡死了。
改成每落一张图就冲一次库:点下去立刻出现卡片并写明「正在画身份卡」,身份卡一出来就带图,之后每张关键姿态都更新缩略图、进度行(正面 · 行走 2/4 · 已 11 分钟)、小图墙(封顶 16 张)和阶段条(身份卡 3/21 张)。
顺带修了三个只有真实使用才暴露的毛病:_url_of 被重复定义第二份盖掉第一份(传字符串直接炸)、阶段条长文案把按钮挤成竖排、条目收尾不清 stages.live 导致「当前项」永久挂起。
6.2 每朝向人工确认
全自动的问题不是慢,是错得没机会刹车:一个朝向 10 张关键姿态,首帧人物就歪了,后面 9 张都以它为参考,只会沿着歪的方向继续歪。
13.2 加了一道闸门:每个朝向只先出 1 张确认帧,在卡片上停下等人点。三档:
| 档位 | 行为 | 适用 |
|---|---|---|
off |
全自动,一路跑完 | 已经调稳、批量刷 |
dir(默认) |
每个朝向停一次 | 日常 |
design |
身份卡也停一次,再逐朝向 | 新角色第一次跑 |
驳回可以写「哪里不对」,修正词会拼进下一张的提示词;还有「重画身份卡」——身份卡错了后面全错,得连多栏一起重出。等待期间阶段条显示「待确认」而不是假装在转,取消和超时(默认 6 小时)都走取消语义,不留点不动的按钮。

7. 13.3:姿势不是提示词能管的事
13.0~13.2 解决的是「像不像同一个人」,一个字都没解决「姿势对不对」。第 1 节那张 64 格同姿态的表,就是这一块欠的账。
7.1 自查:两条根因
一开始的想法是「上 ControlNet 就好了」,但先自查了一遍,发现两条原因,第二条比第一条致命:
根因一:融合编辑的提示词是软约束。 denoise=1 时输出几乎完全复刻 image_1 的构图与姿态,写 left foot forward 在像素层面几乎没有发言权。
根因二:参考图顺序把基准图当成了画布。 这是真 bug。拼参考图的代码是:
refs = [master_path] + extra_refs # 基准图排在第一位
refs = list(extra_refs or []) + [master_path] # 改成:该朝向那一栏在前
而 Qwen-Image 2.1 融合编辑的官方约定是 image_1 = 画布,输出尺寸和构图跟随它。也就是说:每帧都在拿那张正面站姿立绘当画布,去「画」背面和侧面。 方向根本没机会变——这跟接不接 ControlNet 无关,顺序不修,控制图喂进去也只是在正面站姿上描一遍骨架。
这条写进断言:refs[0] 必须是该朝向参考栏、refs[1] 才是基准图。
7.2 骨架自己画:不装预处理器,不占显存
姿势要硬控,就得先有「姿势已知」的控制图。常规做法是装 dwpose 之类的预处理器,但那要额外的模型、额外的显存、额外的推理时间——16G 的机器上每一份都贵。
所以直接在程序里算(pose.py,纯 Pillow):
- 18 个 OpenPose 关键点(鼻/颈/双肩肘腕/双髋膝踝 + 双眼双耳)+ 17 条肢体 + 脚尖线;
- 姿势来自关节角表:站立呼吸、走路四拍、奔跑四拍,每段 4 个关键角表,镜像出 8 帧;
- 局部空间 → 按朝向 yaw 旋转 → 20° 俯角正交投影 → 脚底锚点取景。
俯角这个参数是试出来的,而且值得单说:
CAM_PITCH = 20.0 # 10° 时摆动腿在屏幕上只位移 16px,和站着一样;
# 20° 时 35px,迈腿才看得出来
正面走路时,前后脚的落差全靠俯角压出来。10° 时两脚在屏幕上只差 31px(384² 画布),看着像站着微晃;20° 时差 52px,一眼能读出「在走」。

朝向由 yaw 决定,跟姿势完全解耦——同一套关节角,转一下就得到 8 个朝向:

走路的 8 帧里只有 4 帧需要出图,中间 4 帧靠整帧位移补:

7.3 通路探测:三条路,缺件要说清楚缺什么
骨架有了,还要能喂进 ComfyUI。这里不写死,运行时探测:
# 首选:上游插件的 Union 分支(一份权重支持 Canny/Depth/Pose/Lineart…)
QwenImage21UnionLoader → QwenImage21UnionApply(control_mode="Pose")
→ QwenImage21UnionReferenceEncode(reference_image/_2/_3)
# 兜底:ComfyUI 内置 Control-LoRA 通路
ModelPatchLoader → QwenImageDiffsynthControlnet
# 都没有:明确报「缺哪个权重、放进哪个目录」
Union int8 的好处是一份权重管多种控制,不用为 Pose 单独再下一个几 G 的模型;代价是按 2.1 基座训练的,量化档(-UC)能不能吃还需要实测。
anim_control 三档,语义刻意做得不一样:
| 档位 | 行为 |
|---|---|
auto(默认) |
有 Union 就用;没有就照旧画,并在卡片上标「姿态无控制」 |
pose |
缺权重直接报错,不悄悄降级——批量跑之前先用它自检 |
off |
不加骨架,姿势全交给提示词 |
强度默认 0.8、采样区间 0.0→0.9:最后 10% 放开,让模型补衣褶和发丝,不然骨架会把飘带也拉成棍子。

7.4 三种结局都要记账
这是这一版最花时间的部分,不是写功能,是不给「看起来做了」留空间。
anim.control 里固定记 mode / requested / strength / frames / applied / reason:
- 成功:
applied=true、frames=N,卡片显示🦴 骨架 N; - 降级:
mode=off但requested=auto保留意图、reason写明原因,日志留痕,且只试一次(不重复踩同一个坑); - 关闭:连骨架文件都不落盘,不白占磁盘。
中间出过一个很典型的 bug:降级之后记录里仍然是 applied=True——因为记账用的是「请求里有控制图」而不是「这帧真的被骨架管住了」。改成 _gen_frame() 返回第二个值 ctrl_used,只按它计数。

8. 六个坑
| # | 坑 | 怎么发现的 | 怎么修的 |
|---|---|---|---|
| 1 | 首帧歪了,后面 10 张跟着歪 | 用户实测:一个朝向整组歪 | 13.2 每朝向人工闸门 |
| 2 | image_1 被当成基准图 |
自查参考图拼接顺序 | extra_refs + [master] |
| 3 | 融合分支提前 return,控制链没接上 |
断言 KeyError: '30' |
抽 _patch_control() 两处调用 |
| 4 | 俯角 10° 迈腿看不出来 | 量摆动腿屏幕位移只有 16px | 提到 20° |
| 5 | 竖长画布把迈开的腿切了 | 320×640 上 15 张 bbox 触边 | 整段 8 帧并集取景 + 双向兜底 |
| 6 | 降级后仍记 applied=True |
对账发现「说做了但其实没做」 | 按 ctrl_used 计数 |
第 5 条值得多说一句:取景原来是逐帧各自算 scale,结果 320×640 的竖画布上,run 的某一帧腿迈太开、横向溢出被裁掉。改成整段 8 帧取并集算一个 scale,再加一条「按宽度兜底」(FILL_W=0.92),256 张复验 0 越界。同一段动画必须共用一个取景,这和 10.0 的视图锁是同一个道理,只是从「相机」下沉到了「取景框」。

9. 数字收尾
| 项 | 数字 |
|---|---|
| 提交 | 11 个(10.0 → 13.3) |
| 动画帧 | lite 40 / standard 100 / full 160 帧每角色 |
| 出图次数 | 21 / 51 / 82(老办法 = 帧数) |
| 骨架 | 18 关键点、17 肢体、8 朝向 × 3 段 × 8 帧 = 32 张两两不同 |
| 断言 | 13.3 新增 71 条,全绿 |
| 回归 | 13.0/13.1/13.2/12.1/11.2 = 54/26/40/21/30 全过 |
| 便携包 | F:\qwenimage 同步后哈希 0 差异,包内 runtime 冒烟通过 |
13.3 的 71 条断言里,比较有意思的几条:
- 8 朝向 × 4 关键姿态 = 32 张骨架两两不同(哈希去重);
- 摆动腿位移 > 28px、支撑脚位移 < 25px(走路得有一条腿在动、一条腿在撑);
- 用两耳距离塌缩判朝向(正面 78px、侧面 29px),比看脸可靠;
- 320×640 竖画布 256 张 0 越界;
- 骨架图尺寸必须等于参考栏尺寸(不等就会「劈叉」);
off档零控制图、零骨架文件(不白占磁盘);- 降级后
reason非空、日志有痕、且之后不再塞控制图。
10. 还差什么
诚实列一下没解决的:
- 真机未验证。Union 权重按 2.1 基座训练,本项目用的是
-UC量化档,理论上兼容(只 patch 分支,不动主权重),但必须实拍一张才知道姿势真被拉动。README 3.5 写清了目录和自检入口:装好后参数弹窗那行会从「占位引擎」变成「Union」。 - 骨架只覆盖 idle / walk / run。攻击、受击、施法、死亡这些还没有关节角表,得逐个编。
- 非刚体管不了。披风、飘带、长发、手持武器——骨架能拉动四肢,拉不动这些东西的物理;它们只能靠提示词和强度让位。
- 插值是整帧位移,不是形变。8 帧够用,但要更顺需要真形变或光流,那是另一件事。
- 左右不对称的角色必须选 full 档全实拍,镜像会把单边披风翻到另一边。
- 身份卡上限 8 栏。再多就得拆成多张,拆了之后栏与栏之间的一致性会掉。
下一步优先做第 1 条:先跑一张,确认姿势真被拉动,再整组批量跑。 在那之前,13.3 只能算「通路打通、骨架可用、断言全绿」,不能算「效果已验证」。
来源:博客园 · cnblogs.com