从零开始的生化危机4重制版Modding教程 EX1 EMV_Pose2Mot 项目研究结论存根
从零开始的生化危机4重制版Modding教程 EX1 EMV_Pose2Mot 项目研究结论存根
前言
EMV-Engine中有一个方便的Poser插件,这个插件允许用户在RE ENGINE游戏中方便的变更各种建模的姿势,包括人物和物品(只要有骨骼)。本项目的想法是,将这个插件保存的json文件转换为可以被游戏识别和读取的通用mot文件,这样就可以在不需要3ds Max的情况下调整游戏内人物姿势
mot文件简介
可以使用RE_RSZ读取motlist文件。本质上,motlist文件是一个Motion(.mot)文件的集合,其中包含了很多mot片段。游戏在运行过程中会根据状态和条件播放和绘制各种动画。
文件结构简要介绍
下面是对这个文件结构的一个简单解析
名称 值 开始 大小 类型 备注
HEADER 0h 68h struct
Version 663 0h 4h uint32 版本,根据不同游戏版本不同。版本值见1
ID[4] mlst 4h 4h char
padding[0] 0 8h 8h uint64
pointersOffs 50h 10h 8h uint64 指向指针列表的地址
colOffs EFF0h 18h 8h uint64 指向MOT数据结束的位置,即MotionID开始的位置
motListNameOffs 36h 20h 8h uint64 指向motlist文件的名称起始的位置
padding[1] 0 28h 8h uint64
numOffs 3 30h 4h uint32 表明有3个mot数据在本文件里
motListName[10] cha1_wait 36h 14h wstring motlist文件名,字符串以\x00\x00(EOF)结尾,最长不超过10字节
Pointers[3] 50h 18h struct POINTERS mot文件列表的指针
cha1_general_0160_stand_loop[0] 112 50h 8h struct POINTERS 指向第一个mot数据的起始位置,112=0x70
cha1_general_0160_stand_loop[1] 112 58h 8h struct POINTERS
cha1_general_0160_stand_loop[2] 112 60h 8h struct POINTERS
MOT ID: 155 cha1_general_0160_stand_loop (2540 frames) 70h EF80h struct mot Mot数据开始
MOT_HEADER 70h 7Ch struct motHdr mot数据的头部,此处略去了
BONE_HEADERS D960h 168Ch struct 骨骼数据头部
boneHdrOffs D900h D960h 8h uint64
boneHdrCount 59 D968h 8h uint64
BONE_HEADER[59] D970h 1270h struct bnHdr
BONE_HEADER[0] root D970h 50h struct bnHdr
boneNameOffs EB70h D970h 8h uint64
boneName[5] root EBE0h Ah wstring
parentOffs 0h D978h 8h uint64 root无父骨骼
childOffs D950h D980h 8h uint64 root的子骨骼
nextSiblingOffs 0h D988h 8h uint64 兄弟骨骼(树的孩子兄弟表示法)
translation[4] D990h 10h float 位移四元数
quaternion[4] D9A0h 10h float 旋转四元数(这里为什么会有这俩我也不懂)
Index 0 D9B0h 4h uint32
boneHash 2879905340 D9B4h 4h uint
padding 0 D9B8h 8h uint64
BONE_HEADER[1] Hip D9C0h 50h struct bnHdr
// 略去56个骨骼
BONE_HEADER[58] Null_Offset EB90h 50h struct bnHdr
BONE_CLIP_HEADERS F0h 2C4h struct 骨骼的CLIP头(CLIP这里不知道怎么翻译)
bnClipHdr[59] F0h 2C4h struct BONECLIPHEADER
bnClipHdr[0] root F0h Ch struct BONECLIPHEADER root骨骼的头信息
boneIndex 0 F0h 2h ushort
trackFlags T R S F2h 2h trckFlg_t 下面对root骨骼有三个变换,分别是位移、旋转和缩放
boneHash ABA7DE3Ch F4h 4h uint
trackHdrOffs 344h F8h 4h uint32
bnClipHdr[1] Hip FCh Ch struct BONECLIPHEADER
// 略去56个骨骼
bnClipHdr[58] R_Toe 3A8h Ch struct BONECLIPHEADER
CLIP TRACKS 3C0h 508h struct 骨骼详细数据
TRACKS[0] root 3B4h 3Ch struct TRACKS
TRACKS[1] Hip 3F0h 28h struct TRACKS
// 略去骨骼
TRACKS[12] L_Forearm 4F4h 14h struct TRACKS 左前臂骨骼
Rotation 4F4h 14h struct track 旋转的详细参数
flags 00000000 01000010 00100001 00010010,b 4F4h 4h uint32 这个不知道有什么用
keyCount 183 4F8h 4h uint32 有183帧变换了这个骨骼
frameIndOffs 3C90h 4FCh 4h uint32 一些偏移
frameDataOffs 3E00h 500h 4h uint32
unpackDataOffs 3F70h 504h 4h uint32
Rotation Decompression LoadQuaternionsYAxis16Bit 4F4h 4h RotFrameType_t 使用的旋转压缩算法,见2。我认为这个算法应该是Y轴数据,其余几个轴都是0,便于存储和压缩
Frame Data: Rotation 3D00h 300h struct framedatarot
KEYS 3D00h 16Eh struct keys
MaxUnpackX 0.1369131 3FE0h 4h float 压缩算法中的一些参数,似乎是用于解出其他三个数的
MaxUnpackY -0.4289638 3FE4h 4h float
MaxUnpackZ 1.285697e-39 3FE8h 4h float
MaxUnpackW 3.306116e-39 3FECh 4h float
MinUnpackX 4.591834e-39 3FF0h 4h float
MinUnpackY 5.785718e-39 3FF4h 4h float
MinUnpackZ 7.897958e-39 3FF8h 4h float
MinUnpackW 9.551029e-39 3FFCh 4h float
Frames 3E70h 16Eh struct 帧集合
Frame[0] 0 3E70h 2h struct FRAME 第0帧
RotationData 56868 3E70h 2h ushort 第0帧的旋转数据
Frame[1] 11 3E72h 2h struct FRAME
// 略去其他帧
Frame[182] 2540 3FDCh 2h struct FRAME
TRACKS[13] L_Hand 508h 14h struct TRACKS
// 略去其他骨骼
TRACKS[58] R_Toe 8B4h 14h struct TRACKS
CLIP C1B0h 1770h struct MotlistClip
clipOffset[2] C1B0h 10h uint64
Clip[0] chainsaw.IkLeg2FootLockTracks C1C0h 5C8h struct CLIP_ENTRY
Clip[1] chainsaw.SoundJointContactTriggerTracks C790h 1190h struct CLIP_ENTRY
MotionIDs EFF0h D8h struct 动画的ID表,这个表的数组长度和前面的numOffs对应
cha1_general_0160_stand_loop[0] 155 EFF0h 48h struct motIndex
unk[0] 0 EFF0h 4h uint
unk[1] 0 EFF4h 4h uint
motNumber 155 EFF8h 2h ushort 动画ID
Switch 0 EFFAh 2h ushort
data[0] 0 EFFCh 4h uint
data[1] 0 F000h 4h uint
data[2] 1 F004h 4h uint 这里这个数据是1,不知道是什么
data[3] 0 F008h 4h uint
data[4] 0 F00Ch 4h uint
data[5] 0 F010h 4h uint
data[6] 0 F014h 4h uint
data[7] 0 F018h 4h uint
data[8] 0 F01Ch 4h uint
data[9] 0 F020h 4h uint
data[10] 0 F024h 4h uint
data[11] 0 F028h 4h uint
data[12] 0 F02Ch 4h uint
data[13] 0 F030h 4h uint
data[14] 0 F034h 4h uint
cha1_general_0160_stand_loop[1] 160 F038h 48h struct motIndex
cha1_general_0160_stand_loop[2] 162 F080h 48h struct motIndex
End of File 61640 F0C7h 1h ubyte
参考1 : RszTool/RszTool/RszFile/MotlistFile.cs at master · kagenocookie/RszTool
参考2:RszTool/RszTool/RszFile/MotFile.cs at master · kagenocookie/RszTool
创建一个虚构的motlist
def create_motlist_from_mot(motData, baseName):
"""
根据MOT数据和名称创建符合RE Engine规范的motlist结构
参数:
motData (bytes): 原始MOT文件数据
baseName (str): 动画名称(不含扩展名)
返回:
bytes: 构造好的motlist数据
"""
# 1. 计算各部分大小和偏移量
headerSize = 0x50
nameSize = (len(baseName) + 1) * 2 # UTF-16LE包括null终止符
pointersSize = 1 * 8 # 只需要1个8字节指针
paddingAfterPointers = 8 # 指针后填充8字节
motOffset = headerSize + pointersSize + paddingAfterPointers # MOT数据起始位置
motionIDsOffset = motOffset + len(motData) # MotionIDs区域起始
# 2. 构建motlist头部 (0x00-0x50)
fakeMotlistData = bytearray()
# -- 固定头部结构 --
fakeMotlistData.extend(struct.pack("<I", 663)) # 0x00: 版本号663
fakeMotlistData.extend(b'mlst') # 0x04: ID标识
fakeMotlistData.extend(bytes(8)) # 0x08: Padding
fakeMotlistData.extend(struct.pack("<Q", 0x50)) # 0x10: 指针区域偏移
fakeMotlistData.extend(struct.pack("<Q", motionIDsOffset)) # 0x18: MotionIDs偏移
fakeMotlistData.extend(struct.pack("<Q", 0x36)) # 0x20: 名称偏移
fakeMotlistData.extend(bytes(8)) # 0x28: Padding
fakeMotlistData.extend(struct.pack("<I", 1)) # 0x30: 指针数量
fakeMotlistData.extend(bytes(2)) # 0x34: Padding
# -- 名称区域 (0x36-0x4F) --
max_name_length = 0x10 # 最大允许16字节(UTF-16LE下为8个字符)
nameData = baseName.encode('utf-16le')
# 确保名称不超过最大长度且包含终止符
if len(nameData) > max_name_length - 2: # -2是为了保留终止符空间
nameData = nameData[:max_name_length - 2] # 截断超长部分
nameData += b'\x00\x00' # 添加UTF-16LE终止符
# 计算填充长度(确保不会出现负值)
current_pos = len(fakeMotlistData)
padding_needed = max(0, 0x50 - current_pos - len(nameData))
fakeMotlistData.extend(nameData)
fakeMotlistData.extend(bytes(padding_needed)) # 安全填充到0x50
# 3. 指针区域 (0x50-0x57) - 只需要1个指针
fakeMotlistData.extend(struct.pack("<Q", motOffset)) # 单个指针
fakeMotlistData.extend(bytes(8)) # 指针后填充8字节
# 4. MOT数据区域 (从motOffset开始)
fakeMotlistData.extend(bytes(motOffset - len(fakeMotlistData))) # 填充对齐
fakeMotlistData.extend(motData)
# 5. MotionIDs区域(只需要1个48字节结构)
# 单个MotionID结构 (48字节)
fakeMotlistData.extend(struct.pack("<I", 0)) # unk[0]
fakeMotlistData.extend(struct.pack("<I", 0)) # unk[1]
fakeMotlistData.extend(struct.pack("<H", 0)) # motNumber(改为0)
fakeMotlistData.extend(struct.pack("<H", 0)) # Switch
# data数组 (14个uint32)
fakeMotlistData.extend(struct.pack("<I", 0)) # data[0]
fakeMotlistData.extend(struct.pack("<I", 0)) # data[1]
fakeMotlistData.extend(struct.pack("<I", 1)) # data[2] (重要标志)
fakeMotlistData.extend(bytes(12 * 4)) # data[3-14] (全0)
fakeMotlistData.extend(b'\x00') # EOF标记
# 6. 保存到指定路径(可选)
# save_path = r"D:\tmp\tmp.motlist.663"
result_data = bytes(fakeMotlistData)
try:
os.makedirs(os.path.dirname(save_path), exist_ok=True)
with open(save_path, 'wb') as f:
f.write(result_data)
print("Successfully saved motlist to", {save_path})
except Exception as e:
print("Failed to save motlist:", {str(e)})
return result_data
上面的代码可以直接插入noesis的class motlist里,随后只需要更改几个细小的地方就可以让noesis支持载入mot文件。完整的插件文件可以在hed10nes-toolset/vanilla/fmt_RE_MESH.py at main · Holit/hed10nes-toolset下载,不过我添加了很多调试信息,等有人下载了我再删掉。
- 已知问题:生成的motlist中,骨骼名称信息似乎不全
- 另外,这个插件只支持RE4R,
fmt_RE_MESH.py@3022处的代码,即上面的...@22位置写死了663,这里可以根据需要自己改(逃
在编写这个导入插件时,fmt_RE_MESH.py@1505处的代码尤为关键,这里的
fakeMotlist.readBoneHeaders([mot.name for mot in fakeMotlist.mots])
是骨骼信息(KBones等)正常生成的重要步骤,缺少这个就会出现生成的motlist正常,实时加载不正常的问题。
骨骼信息转换
EMV-Poser使用的是_LocalEulerAngle,而为了方便起见,我希望将这个坐标转换到LoadQuaternions3Component,即XYZ形式的坐标。然而,我尝试了多种方法都失败了。
骨骼树
我认为,_LocalEulerAngle应该是基于父骨骼旋转坐标系得到当前骨骼的,也就是说,以root的子骨骼Hips为例,Hips指定的旋转应该是相对于root骨骼的,而不是相对于世界原点。
不过我试着构建了一个树,却没有很好的获得相关的数据,出来的各种模型都十分鬼畜

雕塑:扭曲的作者
在EMV中可以修改代码,让joints输出自己的父骨骼信息来构建一棵树,如下所示
---(replace from reframework\autorun\EMV Engine\init.lua @9336~9366)
if imgui.button("Save Pose") or clear_slot then
local current_name = (poser.new_object_name ~= "" and poser.new_object_name) or poser.current_name
local current_file = json.load_file("EMV_Engine\\Poses\\" .. current_name .. ".json") or {}
local save_name = (poser.save_name ~= "" and poser.save_name) or poser.slot_names[poser.current_slot_idx]
if save_name and not clear_slot then
local pose = {}
for i=1, #poser.prop_names do
if poser.load_all or poser.prop_idx == i then
for j, joint in ipairs((poser.save_only_this and (poser.body_joints or self.joints)) or poser.all_joints or {}) do
local name = joint:get_Name()
local parent = joint:call("get_Parent") --输出父骨骼信息
local parent_name = parent and parent:call("get_Name") or "nil"
print(string.format("[POSE SAVER] Parent of %s is %s", name, parent_name))
-- 正常保存字段
pose[name] = pose[name] or {}
pose[name][poser.prop_names[i]] = jsonify_table({joint:call("get"..poser.prop_names[i])})[1]
end
end
end
elseif current_slot_name then
current_file[current_slot_name] = nil
table.remove(poser.slot_names, poser.current_slot_idx)
poser.current_slot_idx = (poser.current_slot_idx > 1 and poser.current_slot_idx-1) or 1
end
json.dump_file("EMV_Engine\\Poses\\" .. current_name .. ".json", current_file)
json.current_name = current_name
poser.current_name = current_name
poser.names = nil
end
imgui.tooltip("Save frozen joints to a json file as a named pose")
EMV-Poser研究
对于这段代码,我试着跑了joints的相关函数和字段,结果如下
[POSE SAVER] root: joint object type = userdata
[POSE SAVER] root: get_Transform returned nil
[POSE SAVER] root: has no parent
[POSE SAVER] root._LocalEulerAngle = sol.glm::vec<3,float,0>: 000000023CDDA388
[POSE SAVER] Available fields and sample values for joint: root
-> get_Name = root
-> get_NameHash = 2879905340
-> get_Position = (x=-123.6660, y=8.7357, z=21.7007)
-> get_LocalPosition = (x=0.0000, y=0.0000, z=0.0000)
-> get_EulerAngle = (x=-0.0000, y=1.5786, z=-0.0000)
-> get_LocalEulerAngle = (x=-0.0000, y=0.0000, z=0.0000)
-> get_Rotation = (x=0.0000, y=-0.7099, z=0.0000)
-> get_LocalRotation = (x=0.0000, y=0.0000, z=0.0000)
-> get_LocalScale = (x=1.0000, y=1.0000, z=1.0000)
-> get_WorldMatrix = <array or matrix-like userdata>
-> get_LocalMatrix = <array or matrix-like userdata>
-> get_AxisX = (x=-0.0078, y=-0.0000, z=-1.0000)
-> get_AxisY = (x=0.0000, y=1.0000, z=-0.0000)
-> get_AxisZ = (x=1.0000, y=0.0000, z=-0.0078)
-> get_Parent = nil
-> get_Symmetry = sol.REManagedObject*: 000000023CD38FB8
-> get_BaseLocalPosition = (x=0.0000, y=0.0000, z=0.0170)
-> get_BaseLocalRotation = (x=0.0000, y=0.0000, z=0.0000)
-> get_BaseLocalScale = (x=1.0000, y=1.0000, z=1.0000)
-> get_ConstraintJoint = nil
-> get_Valid = true
-> get_Owner = sol.RETransform*: 00000001D4794A28
我尝试了将get_Rotation和get_LocalEulerAngle进行保存并填入mot文件中,都取得了非常失败的成果。
emv保存的数据风格如下:
{
"Ada_A_Pose": {
"AdamsApple": {
"_LocalEulerAngle": "vec:0.11111450195312 -1.8742032370511e-09 0.059826150536537"
},
"B_Knit_s": {
"_LocalEulerAngle": "vec:-0.036831300705671 0.0036867388989776 -0.099788509309292"
},
"C_Chin": {
"_LocalEulerAngle": "vec:0.0351074449718 0.0 0.0"
},
// 还有更多
}
}
这个数据保存在reframework\data\EMV_Engine\Poses\AppResident.Instance.Body.ch3a8z0_body.json里,这里是谁的身体就是谁的后缀。
我又通过本办法实验了root的旋转,结论是:
EMV-Poser
1 : 脚下横杠 X
2 : 穿过身体 Z
3 : 平行于视线 Y
分量1:-1.59(仰面)~1.59(爬在地上) 0到正值向前扑倒,0到负值向后仰倒
分量2:-3.14背对~0(正对)~3.14(背对)逆时针负值转到正值
分量3:-3.14脚底朝上~3.14脚底朝上,0脚底朝下,逆时针(正面看去)自负向正脚底朝下循环
EMV-Poser
正对(T-Pose): 0,0,0
平躺仰面:-1.59,0,0
平躺趴住: 1.59,0,0
脚底朝上,背对: 0,-3.14,-3.14
3ds Max(mot数据)
平躺仰面:-0.7071068,0,0
平躺趴住: 0.7071068,0,0
正对(T-Pose): 0,0,0
脚底朝上,背对: -1,0,0
EMV-Poser
正对(T-Pose): 0,0,0
面向x轴正向(人物逆时针转动90度(z轴逆向观察,从顶部看去)0,1.59,0
背对: 0,-1.59,0
面向x轴逆向:0,3.14,0
3ds Max(mot数据)
(此时更改的是3dsmax里的Z轴数据)
正对(T-Pose): 0,0,0
面向x轴正向(人物逆时针转动90度(z轴逆向观察,从顶部看去)0,0.7071067,0
背对: 0,-1,0
面向x轴逆向:0,-0.7071067,0
EMV-Poser:
人物正面向右倾倒(正面看去,顺时针旋转90度):0,0,-1.59
人物上下颠倒(鞋跟朝上):0,0,-3.14或者0,0,3.14
人物正面向左倾倒:0,0,1.59
3ds Max(mot数据)
(此时更改的是Y轴数据)
人物正面向右倾倒(正面看去,顺时针旋转90度):0,0,-0.7071067
人物上下颠倒(鞋跟朝上):0,0,1
人物正面向左倾倒:0,0,0.7071067
-----------------------------
EMV-Poser:
X数据
0,0,0 正对(T-Pose),数值增大向前扑,直至最大值1.59,数值减小向后扑,直至最小值-1.59
-1.59~1.59,0,0 只对应上半部分坐标系,下半部分坐标系要将YZ设置为-3.14
Y数据
0,0,0 正对(T-Pose),数值增大向右转(顶部观察逆时针转动),直至最大值3.14(背对),然后从-3.14(背对)增大到0
Z数据
0,0,0 正对(T-Pose),数值增大向左倒下(正面观察逆时针转动),直至最大值3.14(鞋底朝上,然后从-3.14(鞋底朝上))增大到0
3ds数据是离散的
X数据
0,0,0 正对(T-Pose),X增大到0.7071068时向前扑倒,鞋底朝上为 1,0,0(同为-1,0,0),随后从-1增大到-0.7071068面朝上仰倒,再增大到0,0,0回到正对
Y数据:修改3dsmax中的绝对旋转变换输入的Y数据时,实际上是变更了mot里的Z数据。
0,0,0 正对(T-Pose),Z减小到-0.7071068从正面看右侧躺倒(正面看顺时针旋转90),继续旋转直到-1,鞋跟朝上,随后从1减小到0回到正对。过程逆时针
Z数据:实际修改的是Y数据
0,0,0 正对(T-Pose),Y自0增大到0.7071068,姿势为面朝右方。转到背面时Y值增大到1,随后从-1增大到0.旋转方向从顶部看时逆时针方向。
经过测试,当同样为向右侧躺倒时,EMV-Poser的数据为0,0,-1.59,而mot的数据为0,0,-0.707说明对应关系正确,不需要换轴
(Never Mind奇怪的叙述方式)
因此,我猜测存在公式
然而这个公式只在root有效,这也是我推断本地欧拉角是相对旋转的原因。
结语
也许哪天我会继续研究这个问题,先写个帖子记录一下。
作者发布、转载的任何文章中所涉及的技术、思路、工具仅供以安全目的的学习交流,并严格遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等网络安全法律法规。
任何人不得将技术用于非法用途、盈利用途。否则作者不对未许可的用途承担任何后果。
本文遵守CC BY-NC-SA 3.0协议,您可以在任何媒介以任何形式复制、发行本作品,或者修改、转换或以本作品为基础进行创作
您必须给出适当的署名,提供指向本文的链接,同时标明是否(对原文)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示作者为您或您的使用背书。
同时,本文不得用于商业目的。混合、转换、基于本作品进行创作,必须基于同一协议(CC BY-NC-SA 3.0)分发。
如有问题, 可发送邮件咨询.

浙公网安备 33010602011771号