#!/usr/bin/env python3 """ Convert a text-based multi-mesh bone animation file to the BSMF binary format. Usage: python convert_anim_to_binary_new.py Binary format (BSMF v2) -- all values little-endian: HEADER 4 bytes magic "BSMF" uint32 version (2) ARMATURE MATRIX 16 x float 4x4 matrix (row-major) BONES uint32 numBones per bone: 3 x float boneStartWorld (from HEAD_LOCAL) float boneLength 9 x float 3x3 rotation matrix (row-major) int32 parentIndex (-1 if none) uint32 numChildren numChildren x int32 childIndices BONE NAMES per bone: uint32 nameLen nameLen bytes UTF-8 name (no terminator) MESHES uint32 numMeshes per mesh: uint32 nameLength nameLength x char meshName (UTF-8, no null terminator) VERTICES uint32 numVertices numVertices x 3 x float positions UV COORDINATES uint32 numFaces numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2) NORMALS numVertices x 3 x float normals TRIANGLES uint32 numTriangles numTriangles x 3 x int32 vertex indices VERTEX WEIGHTS per vertex (numVertices): uint32 numGroups numGroups x (int32 boneIndex, float weight) ANIMATION KEYFRAMES uint32 numKeyframes per keyframe: int32 frameNumber per bone (numBones, in index order 0..N-1): 3 x float location 16 x float 4x4 matrix (row-major) """ import struct import re import sys def parse_floats(line): return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)] def parse_first_int(line): m = re.search(r'\d+', line) if m: return int(m.group()) raise ValueError(f"No integer found in: {line}") def parse_children(line): return re.findall(r"'([^']+)'", line) def convert(input_path, output_path): with open(input_path, 'r', encoding='utf-8', errors='replace') as f: lines = f.readlines() idx = 0 def next_line(): nonlocal idx line = lines[idx].rstrip() idx += 1 return line # --- Armature matrix header + 4 rows --- next_line() # "=== Armature Matrix ===" armature_matrix = [] for _ in range(4): armature_matrix.extend(parse_floats(next_line())[:4]) # --- Bone count --- line = next_line() # "=== Armature Bones: 65" num_bones = parse_first_int(line) bone_names = [] bones = [] bone_parent_names = [] bone_children_names = [] for _ in range(num_bones): bone = {} # "Bone: mixamorig:Hips" line = next_line() bone_name = line[6:] bone_names.append(bone_name) # " HEAD_LOCAL: " line = next_line() bone['head'] = parse_floats(line)[:3] # " TAIL_LOCAL: ..." -- skip next_line() # " Length: 0.123" line = next_line() bone['length'] = parse_floats(line)[0] # 3x3 matrix (3 rows) mat = [] for _ in range(3): mat.extend(parse_floats(next_line())) bone['matrix_3x3'] = mat # " Parent: None" or " Parent: boneName" line = next_line() if line == " Parent: None": bone_parent_names.append(None) else: bone_parent_names.append(line[10:]) # " Children: ['a', 'b'] or []" line = next_line() bone_children_names.append(parse_children(line)) bones.append(bone) # Build name -> index map name_to_idx = {name: i for i, name in enumerate(bone_names)} # Resolve parent / child indices for i in range(num_bones): if bone_parent_names[i] is None: bones[i]['parent'] = -1 else: bones[i]['parent'] = name_to_idx[bone_parent_names[i]] bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]] # --- Multi-mesh header --- line = next_line() # "=== TOTAL MESHES TO EXPORT: 7 ===" num_meshes = parse_first_int(line) meshes = [] for _ in range(num_meshes): # "=== Mesh Object: Name ===" line = next_line() m = re.match(r"===\s*Mesh Object:\s*(.+?)\s*===$", line) if not m: raise ValueError(f"Invalid mesh header: {line}") mesh_name = m.group(1) # --- Vertices --- line = next_line() # "===Vertices: N" num_vertices = parse_first_int(line) vertices = [] for _ in range(num_vertices): vertices.append(parse_floats(next_line())[:3]) # --- UV Coordinates --- next_line() # "===UV Coordinates:" line = next_line() # "Face count: M" num_faces = parse_first_int(line) uvs = [] for _ in range(num_faces): next_line() # "Face N" next_line() # "UV Count: 3" face_uvs = [] for _ in range(3): face_uvs.extend(parse_floats(next_line())[:2]) uvs.append(face_uvs) # --- Normals --- next_line() # "===Normals:" normals = [] for _ in range(num_vertices): normals.append(parse_floats(next_line())[:3]) # --- Triangles --- line = next_line() # "===Triangles: M" num_triangles = parse_first_int(line) triangles = [] for _ in range(num_triangles): line = next_line() ints = [int(x) for x in re.findall(r'[-]?\d+', line)] triangles.append(ints[:3]) # --- Vertex Weights --- next_line() # "=== Vertex Weights (Max 5 bones per vertex) ===" vertex_weights = [] for _ in range(num_vertices): next_line() # "Vertex N:" line = next_line() # "Vertex groups: K" num_groups = parse_first_int(line) groups = [] for _ in range(num_groups): line = next_line() m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line) bone_name = m.group(1) weight = float(m.group(2)) groups.append((name_to_idx[bone_name], weight)) vertex_weights.append(groups) meshes.append({ 'name': mesh_name, 'num_vertices': num_vertices, 'vertices': vertices, 'num_faces': num_faces, 'uvs': uvs, 'normals': normals, 'num_triangles': num_triangles, 'triangles': triangles, 'vertex_weights': vertex_weights, }) # --- Animation Keyframes --- next_line() # "=== Animation Keyframes ===" next_line() # "=== Bone Transforms per Keyframe ===" line = next_line() # "Keyframes: N" num_keyframes = parse_first_int(line) keyframes = [] for _ in range(num_keyframes): line = next_line() # "Frame: N" frame_number = parse_first_int(line) bone_data = {} for _ in range(num_bones): line = next_line() # " Bone: mixamorig:Hips" bone_name = line.strip() if bone_name.startswith("Bone: "): bone_name = bone_name[6:] bone_idx = name_to_idx[bone_name] # Location location = parse_floats(next_line())[:3] # Rotation (skip) next_line() # " Matrix:" (skip header) next_line() # 4 rows of 4 floats matrix = [] for _ in range(4): matrix.extend(parse_floats(next_line())) bone_data[bone_idx] = { 'location': location, 'matrix': matrix, } keyframes.append((frame_number, bone_data)) # ================================================================ # Write binary file # ================================================================ with open(output_path, 'wb') as out: # Header out.write(b'BSMF') out.write(struct.pack(' {output_path} ({output_size:,} bytes binary)") print(f" Bones: {num_bones}, Meshes: {num_meshes}, Keyframes: {num_keyframes}") for md in meshes: print(f" - {md['name']}: {md['num_vertices']} verts, " f"{md['num_faces']} faces, {md['num_triangles']} tris") if __name__ == '__main__': if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} ") sys.exit(1) convert(sys.argv[1], sys.argv[2])