mdl_bones.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. #!/usr/bin/env python3
  2. """Dumps a puppet .mdl file's MDLS bone array: parent index, bind-pose matrix determinant
  3. (to spot a mirrored/negative-scale bone), and translation - without needing a debug build.
  4. Mirrors the parsing in CImage.cpp's parsePuppetBones()/readPuppetMeshData(): MDLS header is
  5. 9 bytes, then nextSectionOffset(u32) + boneCount(u32), then per bone: null-terminated name
  6. + type(u32) + parent(i32) + matrixBytes(u32) + [16 floats if matrixBytes==64] + null-terminated
  7. extra string (empty for most rigs, jiggle/physics JSON for some). The 16 floats are the file's
  8. row-major matrix - reshaping them row-major (not glm's column-major convention) gives the actual authored matrix back, which is what this script does.
  9. Usage:
  10. mdl_bones.py <path-to-puppet.mdl>
  11. Needs: pip install numpy
  12. """
  13. import struct
  14. import sys
  15. import numpy as np
  16. def read_cstr(data, offset):
  17. end = data.index(b'\x00', offset)
  18. return data[offset:end].decode('latin1', errors='replace'), end + 1
  19. def main(path):
  20. data = open(path, 'rb').read()
  21. marker_size = 9
  22. mdls_offset = data.find(b'MDLS', marker_size)
  23. if mdls_offset < 0:
  24. raise SystemExit("no MDLS section found - not a puppet mesh, or a layout this script doesn't handle")
  25. print('MDLS at', mdls_offset, 'file size', len(data))
  26. offset = mdls_offset + 9 # skip the MDLS0001-style 9-byte magic
  27. next_section_offset = struct.unpack_from('<I', data, offset)[0]
  28. offset += 4
  29. bone_count = struct.unpack_from('<I', data, offset)[0]
  30. offset += 4
  31. print('nextSectionOffset', next_section_offset, 'boneCount', bone_count)
  32. for i in range(bone_count):
  33. name, offset = read_cstr(data, offset)
  34. offset += 4 # type, unused
  35. parent = struct.unpack_from('<i', data, offset)[0]
  36. offset += 4
  37. matrix_bytes = struct.unpack_from('<I', data, offset)[0]
  38. offset += 4
  39. matrix = None
  40. if matrix_bytes == 64:
  41. values = struct.unpack_from('<16f', data, offset)
  42. offset += 64
  43. matrix = np.array(values, dtype=float).reshape(4, 4) # row-major as authored
  44. elif matrix_bytes > 4096:
  45. raise SystemExit(f"bone {i} has an implausible matrix byte count ({matrix_bytes}), layout desynced")
  46. else:
  47. offset += matrix_bytes
  48. extra, offset = read_cstr(data, offset)
  49. extra_note = f" extra={extra}" if extra else ""
  50. if matrix is None:
  51. print(f"{i} parent={parent} name={name!r} NO MATRIX ({matrix_bytes} bytes){extra_note}")
  52. continue
  53. det = np.linalg.det(matrix[:3, :3])
  54. translation = matrix[3, :3] # row-vector convention: translation lives in the last row
  55. flag = " <-- MIRRORED/NEGATIVE SCALE" if det < 0 else ""
  56. print(f"{i} parent={parent} name={name!r} det3x3={det:.5f} translation={translation}{flag}{extra_note}")
  57. if __name__ == '__main__':
  58. if len(sys.argv) != 2:
  59. print(__doc__)
  60. raise SystemExit(1)
  61. main(sys.argv[1])