mdl_bones.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  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: 1 padding byte + type(u32)
  6. + parent(i32) + matrixBytes(u32) + [16 floats if matrixBytes==64] + null-terminated name. The
  7. 16 floats are the file's row-major matrix - reshaping them row-major (not glm's column-major
  8. 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. offset += 1 # padding byte
  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. else:
  45. offset += matrix_bytes
  46. name, offset = read_cstr(data, offset)
  47. if matrix is None:
  48. print(f"{i} parent={parent} name={name!r} NO MATRIX ({matrix_bytes} bytes)")
  49. continue
  50. det = np.linalg.det(matrix[:3, :3])
  51. translation = matrix[3, :3] # row-vector convention: translation lives in the last row
  52. flag = " <-- MIRRORED/NEGATIVE SCALE" if det < 0 else ""
  53. print(f"{i} parent={parent} name={name!r} det3x3={det:.5f} translation={translation}{flag}")
  54. if __name__ == '__main__':
  55. if len(sys.argv) != 2:
  56. print(__doc__)
  57. raise SystemExit(1)
  58. main(sys.argv[1])