事先声明:此MOD逆向工具仅用于学习,由于涉及官方以及二创问题,因此不放出完整代码和发行版工具。
工具名称:WWMI___MOD逆向工具
注意:这个帖子是在软件中编辑好粘贴过来的,可能格式有点问题,将就着看吧,谢谢理解。
逆向思路
初期查看了mod的输出文件夹,包含Meshes,Textures,mod.ini(部分包含disabledmod.ini)
Meshes文件夹下包含了所有的.buf文件(简单理解为一种缓存文件),如:position.buf,index.buf等文件,这些文件包含了所有的模型信息,简单来说就是二进制读取这些然后按照WWMI封装的顺序反向解除封装就可以了。
接下来去找有关排列顺序的部分,然后查看了WWMITools的源代码,因为这个blender配套工具本身是开源的,就直接下载源码看了,在exoprt_mod这块里面总共有大概四个python文件,这里面就包含了封装的过程。
当然,这样逆向出来的东西会变成一个整块,不是真的作者编辑出来的样子,那要怎么处理?这里找到了mod.ini文件,里面存储了mod的配置,主要包括了设置Component这种的,把模型分组件了,因为我本身也做过mod所以了解一点,这个模型都是按照Component划分的,最终采用读取mod.ini的组件配置来分割模型。
当然,我本人并不懂关于3d模型格式转换问题,所以实际做转换功能的时候是配合了各种工具和资料完成的。
代码区
前置条件:
@staticmethod
def read_half_float(data):
u_half = (data[0] << 8) | data[1]
v_half = (data[2] << 8) | data[3]
def half_to_float(h):
s = int((h >> 15) & 0x1)
e = int((h >> 10) & 0x1f)
f = int(h & 0x3ff)
if e == 0:
if f == 0:
return -0.0 if s else 0.0
else:
return (-1 if s else 1) * pow(2, -14) * (f / 1024)
elif e == 31:
return float('-inf' if s else 'inf')
return (-1 if s else 1) * pow(2, e - 15) * (1 + f / 1024)
return (half_to_float(u_half), 1.0 - half_to_float(v_half))
buf文件读取:
def read_buffer_file(self, file_path: str, element_size: int,
unpack_format: str, process_func=None) -> list:
file_name = os.path.basename(file_path)
if not os.path.exists(file_path):
print(f"找不到文件: {file_name}")
self.converted_files[file_name] = False
return []
print(f"\n读取文件: {file_name}")
self.debug_file_header(file_path)
results = []
try:
with open(file_path, 'rb') as f:
file_size = os.path.getsize(file_path)
expected_elements = file_size // element_size
print(f"预期元素数量: {expected_elements}")
count = 0
while True:
try:
data = f.read(element_size)
if not data or len(data) < element_size:
break
values = struct.unpack(unpack_format, data)
if process_func:
values = process_func(values)
results.append(values)
count += 1
if count <= 5 or count % 10000 == 0:
print(f"元素 {count}: {values}")
except struct.error as e:
print(f"解析第{count}个元素时出错: {str(e)}")
break
except Exception as e:
print(f"处理第{count}个元素时出错: {str(e)}")
break
print(f"成功读取了 {len(results)} 个元素")
self.converted_files[file_name] = True
except Exception as e:
print(f"读取文件时出错: {str(e)}")
self.converted_files[file_name] = False
return results
模型转换
def convert_mod_to_model(self, mod_folder: str, output_path: str, format='FBX'):
self._ensure_blender_available()
print(f"\n开始转换mod到{format}...")
print(f"Mod文件夹: {mod_folder}")
print(f"输出路径: {output_path}")
try:
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
meshes_dir = os.path.join(mod_folder, 'Meshes')
if not os.path.exists(meshes_dir):
raise Exception(f"找不到Meshes文件夹: {meshes_dir}")
print(f"\n读取buffer文件...")
positions = self.read_buffer_file(
os.path.join(meshes_dir, 'Position.buf'),
element_size=12,
unpack_format='fff'
)
vectors = self.read_buffer_file(
os.path.join(meshes_dir, 'Vector.buf'),
element_size=8,
unpack_format='8b',
process_func=lambda x: (
(x[0] / 127.0, x[1] / 127.0, x[2] / 127.0),
(x[4] / 127.0, x[5] / 127.0, x[6] / 127.0, x[7] / 127.0)
)
)
uvs = self.read_buffer_file(
os.path.join(meshes_dir, 'TexCoord.buf'),
element_size=16,
unpack_format='16B',
process_func=lambda x: self.read_half_float(x[:4])
)
colors = self.read_buffer_file(
os.path.join(meshes_dir, 'Color.buf'),
element_size=4,
unpack_format='4B',
process_func=lambda x: tuple(v / 255.0 for v in x)
)
blend_data = self.read_buffer_file(
os.path.join(meshes_dir, 'Blend.buf'),
element_size=8,
unpack_format='8B',
process_func=lambda x: (
(x[0], x[1], x[2], x[3]),
tuple(w / 255.0 for w in x[4:8])
)
)
indices = self.read_buffer_file(
os.path.join(meshes_dir, 'Index.buf'),
element_size=4,
unpack_format='I',
process_func=lambda x: x[0]
)
print("\n读取形态键数据...")
shape_key_offsets = self.read_buffer_file(
os.path.join(meshes_dir, 'ShapeKeyVertexOffset.buf'),
element_size=12,
unpack_format='fff'
)
shape_key_vertex_ids = self.read_buffer_file(
os.path.join(meshes_dir, 'ShapeKeyVertexId.buf'),
element_size=4,
unpack_format='I',
process_func=lambda x: x[0]
)
shape_key_ranges = self.read_buffer_file(
os.path.join(meshes_dir, 'ShapeKeyOffset.buf'),
element_size=4,
unpack_format='I',
process_func=lambda x: x[0]
)
components = self.parse_mod_ini(mod_folder)
if not components:
print("警告:无法从mod.ini获取组件信息,将创建单个模型")
components = [{
"name": "SingleMesh",
"start_idx": 0,
"count": len(indices),
"bone_start": 0,
"bone_count": max(max(indices) for indices, _ in blend_data) + 1
}]
print("\n开始根据组件信息分割模型...")
# 为每个组件创建顶点组
vertex_groups = {}
vertex_to_group = {}
for comp in components:
start_idx = comp["start_idx"]
count = comp["count"]
used_vertices = set()
faces = set()
if "sub_components" in comp:
for sub in comp["sub_components"]:
sub_start = sub["start_idx"]
sub_count = sub["count"]
for i in range(sub_start, sub_start + sub_count, 3):
if i + 2 >= len(indices):
break
face = indices[i:i+3]
faces.add(tuple(face))
used_vertices.update(face)
else:
for i in range(start_idx, start_idx + count, 3):
if i + 2 >= len(indices):
break
face = indices[i:i+3]
faces.add(tuple(face))
used_vertices.update(face)
vertex_groups[comp["name"]] = {
"vertices": sorted(list(used_vertices)),
"vertex_map": {v: i for i, v in enumerate(sorted(list(used_vertices)))},
"faces": faces,
"bone_range": (comp["bone_start"], comp["bone_start"] + comp["bone_count"])
}
for v in used_vertices:
vertex_to_group[v] = comp["name"]
# 省略---------------------------------------------------------------------
部分说明
转换核心在于.buf文件的读取和实际转换时顶点位置记录
读取后需要需要建立网格(3d建模里面有这个概念),在网格基础上设置顶点映射等信息
ku和mi的mod基础都是3dmigoto,它主要用ib和vb文件记录模型信息,万变不离其宗,只要获取到模型实际信息,最后转换成什么模型格式都是可以的,当然,作者仍然处于小白阶段,大佬们看看就好,别当真。
最后给大家的提醒:知识无罪,但也不要随意侵害他人权益。