export_face.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. # Usage: Run in Blender with the correct scene setup to produce a face.bin file:
  2. # - A collection named "Visual"
  3. # - An Armature with Bones and an Animation
  4. # - One of the Bones should be called Neck and will be used to parent the hair to
  5. # - A mesh skinned to those Bones
  6. # - A collection named "Collision"
  7. # - One or more meshes parent constrained to one of the Bones
  8. import bpy
  9. import mathutils
  10. import struct
  11. export_path = "C:\\Users\\jrouw\\Documents\\Code\\JoltPhysics\\Assets\\"
  12. head_joint = "Neck"
  13. scale = 0.00254
  14. basis = mathutils.Matrix([[0, scale, 0, 0], [0, 0, scale, 0], [scale, 0, 0, 0], [0, 0, 0, 1]])
  15. num_weights = 3
  16. def apply_basis(m):
  17. return basis @ m @ basis.inverted()
  18. def export_scene_meshes_to_bin(filepath):
  19. with open(filepath, "wb") as f:
  20. bpy.context.scene.frame_set(1)
  21. # Find the skinned mesh in the "Visual" collection
  22. visual_coll = bpy.data.collections.get("Visual")
  23. assert visual_coll, "No 'Visual' collection found"
  24. visual_meshes = [obj for obj in visual_coll.objects if obj.type == 'MESH']
  25. assert len(visual_meshes) == 1, "There must be exactly one mesh in the 'Visual' collection"
  26. obj = visual_meshes[0]
  27. mesh = obj.data
  28. # Head joint index
  29. armature = next((m for m in obj.modifiers if m.type == 'ARMATURE'), None)
  30. assert armature
  31. bones = armature.object.data.bones
  32. head_joint_idx = [b.name for b in bones].index(head_joint)
  33. f.write(struct.pack("<I", head_joint_idx))
  34. # Vertices
  35. vertices = [basis @ obj.matrix_world @ v.co for v in mesh.vertices]
  36. f.write(struct.pack("<I", len(vertices)))
  37. for v in vertices:
  38. f.write(struct.pack("<3f", v[0], v[1], v[2]))
  39. # Indices (triangles)
  40. indices = [tuple(p.vertices) for p in mesh.polygons]
  41. f.write(struct.pack("<I", len(indices)))
  42. for t in indices:
  43. f.write(struct.pack("<3I", t[0], t[1], t[2]))
  44. # Inverse Bind Matrices
  45. inv_bind_matrices = [apply_basis(b.matrix_local).inverted() for b in bones]
  46. f.write(struct.pack("<I", len(inv_bind_matrices)))
  47. for m in inv_bind_matrices:
  48. # Write 16 floats (column major)
  49. for i in range(4):
  50. for j in range(4):
  51. f.write(struct.pack("<f", m[j][i]))
  52. # Skin Weights
  53. weights = []
  54. for v in mesh.vertices:
  55. vw = []
  56. total_weight = 0
  57. for g in sorted(v.groups, key=lambda g: g.weight, reverse=True)[:num_weights]:
  58. bone_index = [b.name for b in bones].index(obj.vertex_groups[g.group].name)
  59. vw.append((bone_index, g.weight))
  60. total_weight += g.weight
  61. # Pad to 1
  62. if len(vw) < 1:
  63. vw.append((0, 1.0))
  64. elif total_weight <= 0:
  65. vw = [[v[0], 1.0] for v in vw]
  66. else:
  67. vw = [[v[0], v[1] / total_weight] for v in vw]
  68. weights.append(vw)
  69. f.write(struct.pack("<I", num_weights))
  70. for vw in weights:
  71. # Always write 'num_weights' weights per vertex (pad with zeros if needed)
  72. for i in range(num_weights):
  73. if i < len(vw):
  74. bone_index, weight = vw[i]
  75. else:
  76. bone_index, weight = 0, 0.0
  77. f.write(struct.pack("<If", bone_index, weight))
  78. # Animation (per frame, per joint)
  79. armature_obj = armature.object
  80. if armature_obj.animation_data and armature_obj.animation_data.action:
  81. action = armature_obj.animation_data.action
  82. frame_start = int(action.frame_range[0])
  83. frame_end = int(action.frame_range[1])
  84. num_frames = frame_end - frame_start + 1
  85. f.write(struct.pack("<I", num_frames))
  86. for frame in range(frame_start, frame_end + 1):
  87. bpy.context.scene.frame_set(frame)
  88. for bone in bones:
  89. pose_bone = armature.object.pose.bones[bone.name]
  90. mat = apply_basis(pose_bone.matrix)
  91. # Translation from matrix
  92. t = mat.to_translation()
  93. # Rotation quaternion from matrix
  94. q = mat.to_quaternion().normalized()
  95. # Ensure unique quaternion sign: make W positive
  96. if q.w < 0.0:
  97. q = -q
  98. # Export translation (x,y,z) + quaternion real part (x,y,z)
  99. f.write(struct.pack("<3f", t.x, t.y, t.z))
  100. f.write(struct.pack("<3f", q.x, q.y, q.z))
  101. else:
  102. print("No animation data found on the armature object.")
  103. bpy.context.scene.frame_set(1)
  104. # Export collision hulls
  105. collision_coll = bpy.data.collections.get("Collision")
  106. assert collision_coll, "No 'Collision' collection found"
  107. collision_meshes = [obj for obj in collision_coll.objects if obj.type == 'MESH']
  108. f.write(struct.pack("<I", len(collision_meshes)))
  109. for col_obj in collision_meshes:
  110. # Find parent bone name and index
  111. parent = col_obj.parent
  112. assert parent and parent.type == 'ARMATURE', f"Collision mesh '{col_obj.name}' must be parented to a bone"
  113. bone_name = col_obj.parent_bone
  114. try:
  115. joint_index = [b.name for b in bones].index(bone_name)
  116. except ValueError:
  117. joint_index = 0xffffffff
  118. f.write(struct.pack("<I", joint_index))
  119. # Write vertices
  120. verts = [basis @ col_obj.matrix_world @ v.co for v in col_obj.data.vertices]
  121. f.write(struct.pack("<I", len(verts)))
  122. for v in verts:
  123. f.write(struct.pack("<3f", v[0], v[1], v[2]))
  124. export_scene_meshes_to_bin(export_path + "face.bin")