从零开始的生化危机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骨骼的,而不是相对于世界原点。

不过我试着构建了一个树,却没有很好的获得相关的数据,出来的各种模型都十分鬼畜

image

雕塑:扭曲的作者

在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_Rotationget_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奇怪的叙述方式)

因此,我猜测存在公式

\[\text{RotationAngle} = \sin(\text{\_LocalEulerAngle} / 2) \]

然而这个公式只在root有效,这也是我推断本地欧拉角是相对旋转的原因。

结语

也许哪天我会继续研究这个问题,先写个帖子记录一下。

posted @ 2025-06-10 15:24  二氢茉莉酮酸甲酯  阅读(240)  评论(0)    收藏  举报