Skip to content

About

Play X4: Foundations NPC animations outside the game — check a character-replacement mod's motion without launching the game (.xsm/.xac parsers + realtime viewer)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

16 Commits

Folders and files

Repository files navigation

x4-anim-preview

在游戏外播放《X4:基石》的真实 NPC 动画,用来检查人物替换 mod 的动作是否正常。

做 X4 人物替换 mod 时,每改一版都得进游戏、跑到 NPC 面前、盯着看几秒——慢且看不出细节。 这个工具直接读游戏里的 .xsm 动画和 .xac 网格,在本地把你的 mod 资产和 vanilla 并排播放同一条动画,关节撕裂、四肢弯错方向、猫步、手/脖子错位这些一眼就能看到。

界面

左边两格是 mod 里的两套模型(同一套骨架、同一条动画、同一时刻),右边是 vanilla 基准。 mod 里有多套时全部列出来,点一下切换,多选就分屏并排。

运行方式

方式 怎么做 说明
快捷方式(最稳) python tools/make_shortcut.py --desktop,然后双击生成的快捷方式 直接指向 pythonw.exe,不经过任何文件关联,也不弹控制台
批处理 双击 run_studio.bat 想看到日志和报错时用
命令行 python tools/studio.py 图形界面,终端用户
命令行(无界面) python tools/viewer.py --mod <目录> 脚本化、批处理、出图

为什么推荐生成快捷方式:.py 的文件关联在很多机器上被 Visual Studio 接管, 双击会打开 IDE 而不是运行脚本;.bat 则容易被别的软件抢关联,而且必须是 CRLF 行尾(LF 行尾的 bat 会被 cmd 解析错位,比如把 studio.py 拆成 stud + o.py)。 快捷方式绕开这一切。


1. 为什么能这么做

X4 的 NPC 是换网格、留骨架:libraries/character_components.xml 里的共享 component(比如 character_argon_female_01)持有整套 Biped 骨架和 180 多条动画, macro 只挑 head / torso / props 三个网格槽位。所以:

  • 你的 mod 网格用的就是游戏里那套骨架,游戏的动画可以直接拿来驱动它;
  • 检查"动作对不对"不需要启动游戏,只要把同一条 .xsm 套上去播放。

本仓库自带 .xsm 格式的解析实现(逆向所得,见下),不依赖 3ds Max 或官方插件。

2. 快速开始

pip install -r requirements.txt        # numpy / pillow / pyglet
cp config.example.json config.json     # 按需改 game_dir(也可用环境变量 X4_GAME_DIR)

python tools/make_shortcut.py --desktop   # 生成快捷方式,之后双击即可

图形界面(推荐)

双击生成的快捷方式,或者:

python tools/studio.py

控制面板

左边是控制面板,右边是独立的三维预览窗口:

  1. 选择 mod —— 自动列出工作区(本工具所在目录的上一级,例如 D:\dsh-x4)里 各工程的 mod 输出目录。游戏 extensions/ 不扫(人多眼杂、还拖慢启动),要预览 装好的 mod 就用「浏览…」自己指过去——目录、.cat、.zip 都收。 浏览框默认从工作区根开始;上次的位置只在工作区内才会被记住,想每次都从头 开始就把 config.json 的 browse_remember 设成 false。 勾上「并排对比」后,对比对象可以选 vanilla,也可以选另一个 mod——改一版看一版时, 跟上一版比通常比跟 vanilla 比更有用。改完资产点「重载」即可,不用切来切去。

  2. 选择模型 —— 一个 mod 里往往不止一套模型(build_all.py --outfits a,b 会同时 产出 A/B 两套装扮,有些包干脆是几个角色放一起)。列表里每套都列出来, 点一下切换显示,多选就分屏并排;「全选 / 清空」是快捷方式。 选择会记住,下次打开还是你上次看的那几套。

    怎么判断"哪些文件算一套":先读 mod 的 libraries/character_macros.xml,每条 macro 的 <models> 就是一套(和游戏真正加载的一致);读不到 macro 才退回按 文件名前缀分组(yue_a_head + yue_a_body → yue_a),并且要求 heads/ 与 bodies/ 属于同一个模型目录,免得把"上一版 / 这一版"混成一套。macro 没引用到的 .xac 也会单独列出来,不会从列表里消失。

  3. 播放控制 —— 播放/暂停、逐帧、时间轴拖动、速度、循环、轮播(自动过一遍所有动画)、 网格/骨骼显示、重置相机、一键截图。鼠标拖动是模型跟着鼠标转(和 Blender、 three.js 一致);习惯反过来就用底部的「拖动反向」。

  4. 选择动画 —— 该角色 component 的全部动画(默认 186 条),带搜索框过滤。

选择会被记住(config.json),下次打开就是你上次看的 mod、模型和动画。

mod 从哪来:目录 / .cat / .zip 都行

预览器要的是散装的目录:assets/**/*.xac、libraries/character_macros.xml、 libraries/material_library.xml、贴图 *.dds.gz,它按文件名和 xml 去找这些文件。

而 X4 的发布形态是 <mod>/ext_01.cat(文本索引:名字 大小 时间 md5)

  • ext_01.dat(所有 assets 按索引顺序拼在一起),玩家拿到的 .zip 里裹的又是这一对 ——assets 全在 .dat 里,所以把 .cat 或 .zip 直接指给预览器,它什么都找不到。

现在「浏览…」和命令行的 --mod 都能直接吃包:

你给的 会发生什么
散装目录(有 .xac) 原地读,不复制、不写入
ext_01.cat 自己找同名 .dat,解到 work/unpacked/<所在目录>_ext_01/
.zip 发布包 先解压,再把里面裹着的 .cat(通常在 <mod包名>/ 子目录里)一起解到 work/unpacked/<包名>/
只有 ext_01.cat/.dat 的目录 同上摊开(不动你的目录)

摊开是有选择的,不会把整包照抄到硬盘上:

  • 只写预览真正会读的:.xac 网格、.xml(macro / 材质规则)、贴图 .gz / .dds;
  • 网格 + xml 超过 300 MB 就明确报错,而不是默默写满盘;
  • 只有贴图超限时先放弃贴图照常摊开网格(官方 DLC 就是这种:网格 50110 MB、 贴图 200900 MB),状态栏会写明"贴图超限先跳过(模型会没颜色)";
  • ext_01_sig.cat(签名清单,里面只有一堆 md5)不碰。

摊开是缓存的:第二次打开同一个包直接复用(毫秒级),源包更新过(大小/时间戳变了) 才重解。缓存就在工程内的 work/unpacked/(gitignore 已忽略),删掉即可重来。 多套模型的识别、贴图、材质裁剪规则在摊开后的目录上照常工作。

游戏的 extensions/ 不扫:那里面官方 DLC、工具类 mod、别人做的 mod 混在一起 (几十个条目,绝大多数跟人物动作无关),既拖慢启动又刷屏。要预览装好的 mod 就用 「浏览…」自己指过去——包装好的会摊开,官方 DLC 那种大包网格照摊、贴图自动跳过。 确实想让它常驻列表,把路径写进 config.json 的 mod_roots。

命令行

python tools/viewer.py --vanilla                 # 只跑 vanilla,确认工具链正常
python tools/viewer.py --mod /path/to/your/mod   # 与 vanilla 并排对比
python tools/viewer.py --mod <发布包.zip>         # 直接吃 .zip / .cat(自动摊开)
python tools/viewer.py --body a.xac --head b.xac # 指定具体资产
python tools/viewer.py --mod <新版> --compare-mod <上一版>   # 两个 mod 并排(迭代时最有用)

# mod 里有多套模型
python tools/viewer.py --mod <目录> --list-variants    # 先看有几套、都叫什么
python tools/viewer.py --mod <目录> --variant yue_b    # 只看其中一套
python tools/viewer.py --mod <目录> --all-variants     # 每套各占一格

# 不开窗口,批量出图
python tools/viewer.py --mod /path/to/mod --anim anim_stand_idle_05 \
    --shot out.png --frames 0,15,30,45

窗口操作

键 作用
空格 播放 / 暂停
← → 上一条 / 下一条动画
, . 逐帧后退 / 前进(按 15 fps)
↑ ↓ 播放速度
B 叠加骨骼线框(穿透显示,便于核对骨架)
N 只看骨架(隐藏网格)
R 重置相机
鼠标拖动 / 滚轮 旋转(模型跟着鼠标走)/ 缩放
Q / ESC 退出

3. 骨架校验

动画能播的前提是 mod 资产带着和 vanilla 一模一样的骨架。单独查这个:

python tools/skeleton_check.py --mod /path/to/your/mod

输出每根骨的 bind 位置 / 旋转 / 缩放偏差与父节点差异,并给出结论。判据是 按骨名配对而不是按出现顺序——原版资产里同一套骨经常重复多次,按顺序比会误报。

4. 给 AI / CI 用:一条命令出报告

GUI 是给人看的,AI 看不见窗口;只丢一张图,AI 还得自己猜哪里有问题。 ai_check.py 把"这版 mod 的动作对不对"翻译成可以比较的数字(全部相对 vanilla 基线),再附上对比图:

python tools/ai_check.py --mod <mod目录> --out report/

产物:

文件 内容
report/report.json 全部指标、mod/vanilla 比值、告警、结论(AI 读这个)
report/summary.txt 同样内容的人类可读版
report/shots/<动画>.png 时间序列:mod(t0) vanilla(t0) mod(t1) vanilla(t1)…
report/shots/<动画>_views.png 环绕视角:同一时刻绕一圈(默认 4 个角度)

图里有什么:mod 侧带 albedo 贴图(能看到颜色错位、纯黑、透明这类外观问题), 两侧都叠绿色骨骼线(穿透显示,可直接核对骨架与几何是否对得上)。 vanilla 侧保持纯色——它只是姿态基准,上贴图没意义还慢一倍。想关掉用 --no-textures / --no-bones。

贴图怎么找到的:xac 材质的 layer 里就记着贴图名(lumine.face → lumine_face_diff), 按它精确匹配,实测 lumine / boru 的网格 100% 命中。比拿材质名去猜准得多—— 一个图集常被十几个材质共用(face / eyebrow / eyelash / teeth 全指向 lumine_face_diff)。

裁剪规则照抄游戏 shader,不是拍脑袋定阈值。mod 的 libraries/material_library.xml 里写着每个材质用哪个 shader、什么 blendmode:

shader blendmode 怎么画
p1_hair ALPHA1 / TWOSIDED alpha < 0.5 丢弃(双面)
p1_character NONE 完全不裁剪(皮肤、衣服)
p1_character TWOSIDED 不裁剪,双面(裙子、飘带、披风)
p1_character ALPHA1 alpha < 0.5 丢弃

阈值 0.5 来自 shadergl/glsl/p1/high/hair.frag.glsl 里的硬编码 if (ColorBaseDiffuse.a < 0.5f) discard;;而 p1/high/character.frag.glsl 一个 discard 都没有。

材质裁剪规则修复前后

左:一律按 0.35 裁剪(半透明发丝边缘被留下,头发成了"白棕相间的噪点")。 中:按材质规则走 p1_hair 的 0.5。右:软光栅同一帧,两边一致。

这是整个项目里最费劲的一个 bug——我一开始靠试阈值(0.02 / 0.1 / 0.2 / 0.35 全试过), 其实该做的是去读游戏 shader 的源码。教训:能查证就别猜。

视角可调——单角度会漏掉背面、侧面、腋下、裙摆内侧:

python tools/ai_check.py --mod <目录> --out report/ \
    --views 8            # 环绕 8 个角度(0 = 关闭)
    --azimuth 200        # 时间序列那张的方位角
    --elevation -20      # 仰角,负值从下往上看(查腋下/裙摆)

命令行出图同样支持:viewer.py --shot out.png --views 6 --elevation -15。

mod 里有多套模型时,ai_check.py 默认只检查第一套,会先把清单打出来;换一套:

python tools/ai_check.py --mod <目录> --variant yue_b --out report/

退出码 0 = ok、1 = warn、2 = fail,可直接接进 CI。

判据(越接近 1 越好):

指标 阈值 含义
joint_tear_cm > 1.35× 顶点到主导骨的距离偏大,蒙皮可疑
feet_gap_cm < 0.85× 两脚间距被收窄 = 猫步(几何间距,不是骨间距)
ground_gap_min_cm 与 vanilla 差 > 3 cm 脚悬空或陷进地板
flipped_face_ratio > 0.1 绕序翻转,实机透明/看到内壳
顶点数 > 15× 实测会让空间站频闪
matched < 0.5 动画节点对不上骨架,动画驱动不了

两个容易踩的坑,已经在实现里绕开了:

  • 两脚间距必须用几何顶点算。用骨位置算的话,因为 mod 与 vanilla 骨架逐字节相同, 两边会得出完全一样的假结论——而猫步本来就是"关节落在几何内侧"的几何问题。
  • 绕序别用"面法线背离质心"那套。实测同一个 mod 躯干 0.63、头部 0.76,vanilla 反而 0.76、0.61,完全不可比,会把正常资产判成绕序翻转。改用带符号的 dot(面法线, 顶点法线) 为负的比例,两边都是 0.000,干净。

工作区根还有一份 AGENTS.md,写明让 AI 改完 mod 后先跑这个自查再进游戏。

5. 资产与动画从哪来

  • 网格:--mod 目录(或 .cat / .zip 包,见上面"mod 从哪来")下的 .xac。 哪些算一套模型优先看 macro 的 <models> (每个 <macro> 一套),读不到 macro 才按文件名里的 head / body 分槽位。 一个目录里有好几套时全部列出:GUI 是"模型"列表,命令行是 --list-variants。
  • 动画:默认取 macro 实际引用的 component(character_argon_female_01)。 Argon / Terran 等人类种族共用这套骨架和动画,所以 Terran 的 mod 也用它。 python tools/animations.py <component名> 可以列出任意 component 的动画清单。
  • 想单独核对"多套模型有没有被认全",跑 python examples/variants_check.py [mod目录…]: 不开窗口、不需要游戏,逐个列出每套模型的槽位与文件,并检查有没有 .xac 漏掉。

6. .xsm 格式(逆向结果)

"XSM " + u32 版本 + 头部字段 + 导出器/源文件/日期三个字符串
随后 N 条节点记录首尾相接:
    [100 字节记录头][u32 名字长度][名字][位置关键帧段][旋转关键帧段]

记录头里与帧数有关的部分固定在 名字长度字段 - 0x50:

偏移 含义
+0x00 u32 位置关键帧数
+0x04 u32 旋转关键帧数
+0x08 / +0x0c 附加段帧数(老格式才有意义)
−0x04 单精度浮点(骨骼半径一类,未使用)

关键帧有两种编码,同一文件内一致:

布局 记录头 位置段 旋转段 附加
新(X4 9.00 常见) 100 B 16 B/帧 f32 x,y,z,t 12 B/帧 i16 x,y,z,w(Q15) + f32 t —
老(2008–2011 导出) 100 B 同上 20 B/帧 f32 x,y,z,w,t 20 B/帧的同构轨道 + 32 B 尾
  • 坐标系与 .xac 一致:Y 轴向上、单位厘米,旋转是相对父骨的局部旋转。
  • 旋转段存的是绝对局部旋转(第 0 帧实测与 .xac 的绑定旋转一致),直接套用即可。
  • 关键帧时间是稀疏的(不保证等间隔),按时间插值;四元数用相邻帧 nlerp。
  • 现代资产的节点名与 .xac 骨架同名(Bip01 ...);老资产用 root/spine1/l_upleg 旧命名,和现役骨架对不上,只能作参考。

实现里对上述差异做了自适应:先在文件头部定位第一条记录并判定布局,之后按记录长度 链式推进(不扫全文件),落后时在 ±24 字节内重新对齐。

顺带记录一个 .xac 的坑:材质 chunk 的 length 不包含 layer 数据 (实测 length=129 的 chunk 实际占 191 字节),照着 length 跳会立刻错位。

7. 参数速查

--mod PATH             mod 目录,或 .cat / .zip 包(包会摊到 work/unpacked/ 再预览)
--variant NAME         只用 mod 里的某一套模型(名字见 --list-variants)
--all-variants         mod 里每套模型各占一格并排显示
--list-variants        列出 mod 目录里的模型套,然后退出
--body / --head FILE   直接指定资产(磁盘路径或游戏包内路径)
--vanilla              只加载 vanilla 基准
--vanilla-body/--head  换对照用的 vanilla 槽位资产
--component NAME       取哪套动画(默认 character_argon_female_01)
--anim NAME            只播某条动画(逻辑名或资产名)
--delta                实验性增量姿态模式,见下
--shot OUT.png         不开窗口,直接出图;配合 --frames/--anims
--views N              环绕 N 个角度出图(0 = 只用 --azimuth)
--azimuth / --elevation  视角角度;elevation 为负是从下往上看
--width/--height       窗口或出图尺寸

关于 --delta:本意是用"相对 .xsm 记录头里绑定姿态的增量"去抹平动画文件与 网格资产的版本差。实测不该当默认:同一动画在不同资产上会给出不同姿态。 默认是直接套用动画值。

8. 已知限制

  • 贴图只上 albedo(diffuse):normal / smooth / metal 通道和法线贴图没做, 所以"光照看起来不对"这类问题看不出来。透明只做了 alpha 裁剪(头发镂空能出来), 没做真正的半透明混合。
  • 贴图只覆盖 mod 侧;vanilla 侧始终纯色。
  • ai_check.py 的指标是筛子不是判决:长裙、大袖、披风这类远离骨骼的服装几何, 关节撕裂天然偏高,报警后要对着 shots/ 的图确认再下结论。
  • 只播骨骼动画:morph / 表情(viseme、blendshape)还没做,脸部细节看不到。
  • props 槽位(头发、胡子等)可以当普通资产用 --body/--head 传,但没有自动配对。
  • 同屏格数是按宽度均分的:同时看 4 套以上时每格会很窄(GUI 里"全选"也一样)。 默认只在模型不多时全部选中,超过就只选第一套。
  • 摊开只覆盖预览要用的那几类文件,且先网格后贴图:贴图超限会被跳过(模型没颜色, 姿态照样能看)。缓存放在 work/unpacked/,不用了直接删。
  • .dat 单独给没用——它只是数据,索引在同名 .cat 里(会明确提示)。
  • 自动发现只扫工作区(本工具上一级),不扫游戏的 extensions/;要常驻别的根 目录就写 config.json 的 mod_roots。
  • 一个目录里塞了好几个 mod(比如 work/dist)时,重名的模型靠目录提示区分 (lumine · x4_lumine_terran_add);同一套模型被复制到多处时,取离 macro 最近的那份。
  • 老格式动画(root/spine1 命名)不能驱动现役骨架,播放时会提示匹配率。
  • 软光栅出图是 CPU 实现,一张 400×660 约 0.25 s;实时窗口走 OpenGL,不吃这个开销。

9. 目录

run_studio.bat      双击启动图形界面(CRLF 行尾,别改成 LF)
tools/
  studio.py         图形界面(tkinter 控制面板 + 独立 3D 窗口)   <- 推荐入口
  viewer.py         命令行预览 / 批量出图
  modpack.py        .cat/.dat 与 .zip 就地摊开成目录(带缓存)
  scene.py          资产定位、场景组装、自动发现 mod 目录、识别 mod 里的多套模型
  make_shortcut.py  生成 Windows 快捷方式(绕开文件关联问题)
  console.py        统一 UTF-8 控制台(GBK 下打印 ✓ 会直接崩)
  glview.py         OpenGL 视图层:轨道相机、绘制、可手动步进的窗口
  x4game.py         游戏目录探测 + .cat/.dat 随机读取
  xac.py            .xac 解析(骨架 / 网格 / UV / 骨骼权重)
  xsm.py            .xsm 解析(关键帧曲线)
  rig.py            正向运动学 + 线性混合蒙皮
  animations.py     component -> 动画清单
  ai_check.py       给 AI/CI 的成果检查:一条命令出 JSON 判据 + 对比图
  metrics.py        量化判据(关节撕裂 / 两脚间距 / 离地 / 绕序 / 法线)
  ddstex.py         DDS + BC1/BC3 解码(mod 贴图是 .gz 包着的 DDS)
  skeleton_check.py 骨架一致性校验
  render.py         纯 numpy 软光栅(离线出图用)
examples/
  offline_check.py  不开窗口跑一遍并出图的最小示例
  variants_check.py 核对"mod 里的多套模型有没有被认全"(不需要游戏)

图形界面用的是 tkinter(Python 自带)做控制面板、pyglet 单独开三维窗口, 两者共享一个线程:tkinter 的 after 循环每帧调用 PreviewWindow.pump() 手动步进 pyglet,因此不需要两套事件循环,也不用加锁。

About

Play X4: Foundations NPC animations outside the game — check a character-replacement mod's motion without launching the game (.xsm/.xac parsers + realtime viewer)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages