├── shared.py ├── readme.rst ├── dualquat.py ├── __init__.py ├── io_md5anim.py ├── import_hudguns.py └── io_md5mesh.py /shared.py: -------------------------------------------------------------------------------- 1 | import re 2 | import math 3 | import bpy 4 | from mathutils import Vector, Matrix, Quaternion 5 | 6 | fmt_row2f = "( {:.6f} {:.6f} )" 7 | fmt_row3f = "( {:.6f} {:.6f} {:.6f} )" 8 | 9 | def construct(*args): 10 | return re.compile("\s*" + "\s+".join(args)) 11 | 12 | def unpack_tuple(mobj, start, end, conv=float, seq=Vector): 13 | return seq(conv(mobj.group(i)) for i in range(start, end + 1)) 14 | 15 | def gather(regex, end_regex, lines): 16 | return gather_multi([regex], end_regex, lines)[0] 17 | 18 | def gather_multi(regexes, end_regex, lines): 19 | results = [[] for regex in regexes] 20 | 21 | for line in lines: 22 | if end_regex.match(line): 23 | break 24 | 25 | for regex, result in zip(regexes, results): 26 | mobj = regex.match(line) 27 | if mobj: 28 | result.append(mobj) 29 | break 30 | 31 | return results 32 | 33 | def skip_until(regex, lines): 34 | for line in lines: 35 | if regex.match(line): 36 | return line 37 | # iterator exhausted 38 | return None 39 | 40 | def restore_quat(rx, ry, rz): 41 | EPS = -5e-2 42 | t = 1.0 - rx*rx - ry*ry - rz*rz 43 | if EPS > t: raise ValueError 44 | if EPS < t < 0.0: return Quaternion(( 0.0, rx, ry, rz)) 45 | else: return -Quaternion((-math.sqrt(t), rx, ry, rz)) 46 | 47 | def is_mesh_object(obj): 48 | return obj.type == "MESH" 49 | 50 | def has_armature_modifier(obj, arm_obj): 51 | for modifier in obj.modifiers: 52 | if modifier.type == "ARMATURE": 53 | if modifier.object == arm_obj: 54 | return True 55 | return False 56 | 57 | def process_match_objects(mobj_list, cls): 58 | for index, mobj in enumerate(mobj_list): 59 | mobj_list[index] = cls(mobj) 60 | 61 | def get_name_to_index_dict(arm_obj): 62 | name_to_index = arm_obj.get("name_to_index") 63 | 64 | if name_to_index is not None: 65 | name_to_index = name_to_index.to_dict() 66 | 67 | else: 68 | name_to_index = {} 69 | root_bones = (pb for pb in arm_obj.pose.bones if pb.parent is None) 70 | index = 0 71 | 72 | for root_bone in root_bones: 73 | name_to_index[root_bone.name] = index 74 | index += 1 75 | for child in root_bone.children_recursive: 76 | name_to_index[child.name] = index 77 | index += 1 78 | 79 | arm_obj["name_to_index"] = name_to_index 80 | 81 | return name_to_index 82 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | ==================================================================================== 2 | Blender Addon - Import and Export `id tech 4`_ ``*.md5mesh`` and ``*.md5anim`` files 3 | ==================================================================================== 4 | 5 | Installation: 6 | ============= 7 | 8 | Download this repository as `zip-archive`_ and rename it to ``md5.zip``. 9 | Go to Blender User Preferences -> Addons and click ``Install from File...`` and 10 | select the zip-archive in the file browser which pops up. 11 | 12 | Afterwards activate the addon under the category Import-Export or User. 13 | 14 | Import: 15 | ======= 16 | 17 | Choose from the Info Menu -> File Import -> ``*.md5mesh``. 18 | In the file browser choose the file you want to import. 19 | 20 | The ``*.md5mesh`` mesh does not store information about the 21 | length of bones. You may edit the length of leaf bones **before** 22 | you import an animation. 23 | 24 | Then select the armature for which you want to import an animation. 25 | Again choose from the Info Menu -> File Import -> ``*.md5anim`` and in 26 | the file browser which pops up select the file you want to import. 27 | 28 | Export: 29 | ======= 30 | 31 | Put all the bones you want to import on one bone layer. 32 | Using the default keymap you can do this by selecting a bone and press M. 33 | 34 | If you plan to export all bones and did not change any layer yet, 35 | you don't have to do anything. 36 | 37 | Select the armature of your model and choose Info Menu -> File Export -> ``*.md5mesh``. 38 | The addon will gather and export all meshes which have an armature deform modifier 39 | with the selected armature as target applied to them and are on an active scene layer. 40 | In the file browser which pops up choose your bone layer. The default layer is 0. 41 | 42 | To export an animation select the armature of your model and assign the action you want to export 43 | to the armature. Afterwards choose Info Menu -> File Menu -> File Export -> ``*.md5anim`` 44 | 45 | If you plan to export an animation for an existing md5mesh, import the mesh and its armature 46 | and use the latter to create your animation. The addon will store the bone indices in a custom 47 | property named ``name_to_index`` of the armature. 48 | 49 | Adding, or removing bones from the armature is **not** supported yet, unless you delete the 50 | custom property and export the new md5mesh and all of its animations. 51 | 52 | Notes: 53 | ====== 54 | 55 | * For now, the addon has only been tested with blender version 2.77a and models of the game 56 | Cube 2: Sauerbraten, which this addon was indented for. 57 | 58 | * This addon originated from the `md5 addon`_ written by Nemyax. 59 | 60 | * md5mesh files only support uv coordinates per vertex, while 61 | blender supports uv coordinates per face loop, which may result in 62 | multiple uv coordinates per vertex. 63 | 64 | The addon will try to split the mesh along the borders of the uv islands so 65 | that each vertex can be assigned multiple uv coordinates if necessary. 66 | This will increase the vertex count of your model. 67 | 68 | * The weights of your mesh should be normalized, which means for each vertice 69 | the sum of its weights should equal 1.0 70 | 71 | * Although the ``*.md5anim`` format supports the export of single components of 72 | the location vectors and rotation quaternions, the addon will always export all 73 | components using mask 63. 74 | 75 | .. _zip-archive: https://github.com/pink-vertex/blender_addon_md5/archive/Release.zip 76 | .. _id tech 4: https://github.com/id-Software/DOOM-3 77 | .. _md5 addon: https://sourceforge.net/p/blenderbitsbobs/wiki/MD5%20exporter/ 78 | -------------------------------------------------------------------------------- /dualquat.py: -------------------------------------------------------------------------------- 1 | import re 2 | import bpy 3 | import math 4 | from mathutils import Vector, Matrix, Quaternion 5 | 6 | QID = Quaternion((1.0, 0.0, 0.0, 0.0)) 7 | 8 | def mat_offset(pose_bone): 9 | bone = pose_bone.bone 10 | mat = bone.matrix.to_4x4() 11 | mat.translation = bone.head 12 | if pose_bone.parent: 13 | mat.translation.y += bone.parent.length 14 | return mat 15 | 16 | class DualQuat: 17 | def __init__(self, r=QID.copy(), d=Quaternion()): 18 | self.r = r 19 | self.d = d 20 | 21 | def copy(self): 22 | return DualQuat( 23 | self.r.copy(), 24 | self.d.copy()) 25 | 26 | def __mul__(self, other): 27 | result = self.copy() 28 | result.r = self.r * other.r 29 | result.d = (self.r * other.d + 30 | self.d * other.r) 31 | return result 32 | 33 | def __add__(self, other): 34 | result = self.copy() 35 | result.r += other.r 36 | result.d += other.d 37 | return result 38 | 39 | def __eq__(self, other): 40 | EPS = 1e-4 41 | return all(abs(rs - ro) < EPS and abs(ds - do) < EPS 42 | for rs, ro, ds, do in zip(self.r, other.r, self.d, other.d)) 43 | 44 | def __str__(self): 45 | tuple_4f = "% .6f % .6f % .6f % .6f" 46 | return " ".join(("Dual Quaternion", 47 | "Real:", tuple_4f % tuple(self.r), 48 | "Img:", tuple_4f % tuple(self.d))) 49 | 50 | def __repr__(self): 51 | return "DualQuat(%s, %s)" % (repr(self.r), repr(self.d)) 52 | 53 | def conjugated(self): 54 | result = self.copy() 55 | result.r.conjugate() 56 | result.d.conjugate() 57 | return result 58 | 59 | def inverted(self): 60 | result = self.copy() 61 | result.r.invert() 62 | result.d = -(result.r * result.d * result.r) 63 | return result 64 | 65 | def transform(self, vec): 66 | p = DualQuat( 67 | QID.copy(), 68 | Quaternion((0.0, *vec))) 69 | 70 | result = self * p * self.conjugated() 71 | return Vector(result.d[1:]) 72 | 73 | def mulorient(self, quat): 74 | self.r = quat * self.r 75 | self.d = quat.inverted() * self.d 76 | 77 | def fixantipodal(self, dq): 78 | if self.r.dot(dq.r) < 0.0: 79 | self.r = -self.r 80 | self.d = -self.d 81 | 82 | def from_loc_rot(self, loc, rot): 83 | self.r = rot.copy() 84 | self.d = 0.5 * Quaternion((0.0, *loc)) * rot 85 | return self 86 | 87 | def to_matrix(self): 88 | mat = Matrix() 89 | r = self.r 90 | d = 2 * (self.d * self.r.conjugated()) 91 | s = r.copy() 92 | for i in range(4): s[i] *= s[i] 93 | 94 | mat[0][0] = s.w + s.x - s.y - s.z 95 | mat[1][1] = s.w - s.x + s.y - s.z 96 | mat[2][2] = s.w - s.x - s.y + s.z 97 | mat[0][1] = 2 * (r.x * r.y - r.w * r.z) 98 | mat[1][0] = 2 * (r.x * r.y + r.w * r.z) 99 | mat[0][2] = 2 * (r.x * r.z + r.w * r.y) 100 | mat[2][0] = 2 * (r.x * r.z - r.w * r.y) 101 | mat[1][2] = 2 * (r.y * r.z - r.w * r.x) 102 | mat[2][1] = 2 * (r.y * r.z + r.w * r.x) 103 | 104 | mat[0][3] = d.x 105 | mat[1][3] = d.y 106 | mat[2][3] = d.z 107 | 108 | return mat 109 | 110 | def to_loc_rot(self): 111 | loc = 2.0 * self.d * self.r.inverted() 112 | return ( 113 | Vector(loc[1:]), 114 | self.r.copy()) 115 | 116 | def translate(self, t): 117 | loc, rot = self.to_loc_rot() 118 | self.from_loc_rot(loc + t, rot) 119 | 120 | class BoneAdjustment: 121 | def __init__(self, name, yaw=0.0, pitch=0.0, roll=0.0, translation=None): 122 | self.name = name 123 | self.yaw = yaw 124 | self.pitch = pitch 125 | self.roll = roll 126 | 127 | if translation is not None: 128 | translation = 0.25 * Vector(translation) 129 | 130 | self.translation = translation 131 | 132 | def adjust(self, dq): 133 | axes = ( 134 | Vector(( 0.0, 0.0, 1.0)), 135 | Vector(( 0.0, -1.0, 0.0)), 136 | Vector((-1.0, 0.0, 0.0)) 137 | ) 138 | angles = self.yaw, self.pitch, self.roll 139 | 140 | for axis, angle in zip(axes, angles): 141 | dq.mulorient(Quaternion(axis, math.radians(angle))) 142 | 143 | if self.translation is not None: 144 | dq.translate(self.translation) 145 | 146 | @staticmethod 147 | def conv_sauerbraten(dq): 148 | loc, rot = dq.to_loc_rot() 149 | rot.w = -rot.w 150 | rot.y = -rot.y 151 | loc.y = -loc.y 152 | dq.from_loc_rot(loc, rot) 153 | 154 | def adjust_sb(self, dq): 155 | self.conv_sauerbraten(dq) 156 | self.adjust(dq) 157 | self.conv_sauerbraten(dq) 158 | 159 | def convert_kf_pts(kf_pts, cls): 160 | return cls(kf.co.y for kf in kf_pts) 161 | 162 | def set_kf_pts(kf_pts, iterable): 163 | for kf, elem in zip(kf_pts, iterable): 164 | kf.co.y = elem 165 | 166 | def create_fcu_dict(action): 167 | re_data_path = re.compile('pose\.bones\["(?P[^"]*)"\]\.(?P.*)') 168 | result = {} 169 | 170 | for fcu in action.fcurves: 171 | mobj = re_data_path.match(fcu.data_path) 172 | if mobj is not None: 173 | attr = mobj.group("attr") 174 | if attr == "location": n = 0 175 | elif attr == "rotation_quaternion": n = 1 176 | else: raise ValueError 177 | 178 | name = mobj.group("name") 179 | data = result.get(name) 180 | if data is None: 181 | data = [None]*3, [None]*4 182 | result[name] = data 183 | 184 | data[n][fcu.array_index] = fcu 185 | 186 | return result 187 | 188 | def adjust_bone_animation(adjustment, pb, fcu_loc, fcu_rot): 189 | loc, rot, scale = mat_offset(pb).decompose() 190 | dq_ofs = DualQuat().from_loc_rot(loc, rot) 191 | dq_ofs_inv = dq_ofs.inverted() 192 | 193 | iter_loc = zip(*(fcu.keyframe_points for fcu in fcu_loc)) 194 | iter_rot = zip(*(fcu.keyframe_points for fcu in fcu_rot)) 195 | 196 | for kf_loc, kf_rot in zip(iter_loc, iter_rot): 197 | loc = convert_kf_pts(kf_loc, Vector) 198 | rot = convert_kf_pts(kf_rot, Quaternion) 199 | 200 | dq_frame = DualQuat().from_loc_rot(loc, rot) 201 | dq_frame = dq_ofs * dq_frame 202 | 203 | adjustment.adjust_sb(dq_frame) 204 | 205 | dq_frame = dq_ofs_inv * dq_frame 206 | loc, rot = dq_frame.to_loc_rot() 207 | 208 | set_kf_pts(kf_loc, loc) 209 | set_kf_pts(kf_rot, rot) 210 | 211 | def adjust_animation(obj, adjustments): 212 | action = obj.animation_data.action 213 | lookup_transform = create_fcu_dict(action) 214 | 215 | for adjustment in adjustments: 216 | name = adjustment.name 217 | pose_bone = obj.pose.bones[name] 218 | fcu_loc, fcu_rot = lookup_transform[name] 219 | adjust_bone_animation(adjustment, pose_bone, fcu_loc, fcu_rot) 220 | 221 | def test(): 222 | obj = bpy.data.objects["Armature_Hands"] 223 | adjustments = ( BoneAdjustment("Root", 11.9, -5.4, 0.0, (0.4, 0.0, 0.0)), ) 224 | adjust_animation(obj, adjustments) 225 | 226 | bpy.context.scene.frame_set(0) 227 | 228 | if __name__ == "__main__": 229 | test() 230 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | bl_info = { 20 | "name": "Id Tech 4 md5mesh and md5anim format", 21 | "author": "pink vertex", 22 | "version": (0, 1), 23 | "blender": (2, 77, 0), 24 | "location": "File -> Import-Export", 25 | "description": "Import-Export *.md5mesh and *.md5anim files", 26 | "warning": "", 27 | "wiki_url": "", 28 | "category": "Import-Export", 29 | } 30 | 31 | if "bpy" in locals(): 32 | import sys 33 | import importlib 34 | importlib.reload(sys.modules['io_md5.io_md5mesh']) 35 | importlib.reload(sys.modules['io_md5.io_md5anim']) 36 | importlib.reload(sys.modules['io_md5.import_hudguns']) 37 | 38 | import os 39 | import bpy 40 | from bpy.props import StringProperty, IntProperty, FloatProperty, EnumProperty, CollectionProperty 41 | from bpy_extras.io_utils import ImportHelper, ExportHelper 42 | 43 | from .io_md5mesh import read_md5mesh, write_md5mesh 44 | from .io_md5anim import read_md5anim, write_md5anim 45 | from .import_hudguns import setup_cam, import_from_config 46 | 47 | class MD5Preferences(bpy.types.AddonPreferences): 48 | bl_idname = __name__ 49 | sb_dir = StringProperty(name="Sauerbraten Directory", default="", subtype="DIR_PATH") 50 | 51 | def draw(self, context): 52 | self.layout.prop(self, "sb_dir") 53 | 54 | class OT_IMPORT_MESH_md5mesh(bpy.types.Operator, ImportHelper): 55 | bl_idname = "import_mesh.md5" 56 | bl_label = "Import *.md5mesh Mesh" 57 | bl_options = {"REGISTER", "UNDO"} 58 | 59 | filename_ext = ".md5mesh" 60 | filter_glob = StringProperty(default="*.md5mesh", options={'HIDDEN'}) 61 | 62 | def execute(self, context): 63 | read_md5mesh(self.filepath) 64 | return {"FINISHED"} 65 | 66 | 67 | class OT_EXPORT_MESH_md5mesh(bpy.types.Operator, ExportHelper): 68 | bl_idname = "export_mesh.md5" 69 | bl_label = "Export *.md5mesh Mesh" 70 | bl_options = {"REGISTER", "UNDO"} 71 | 72 | filename_ext = ".md5mesh" 73 | filter_glob = StringProperty(default="*.md5mesh", options={'HIDDEN'}) 74 | 75 | @classmethod 76 | def poll(cls, context): 77 | return (context.active_object and 78 | context.active_object.type == "ARMATURE") 79 | 80 | def execute(self, context): 81 | write_md5mesh(self.filepath, context.scene, context.active_object) 82 | return {"FINISHED"} 83 | 84 | class OT_IMPORT_ANIM_md5anim(bpy.types.Operator, ImportHelper): 85 | bl_idname = "import_anim.md5" 86 | bl_label = "Import *.md5anim Animation" 87 | bl_options = {'REGISTER', "UNDO"} 88 | 89 | filename_ext = ".md5anim" 90 | filter_glob = StringProperty(default="*.md5anim", options={'HIDDEN'}) 91 | 92 | @classmethod 93 | def poll(cls, context): 94 | return (context.active_object and 95 | context.active_object.type == "ARMATURE") 96 | 97 | def execute(self, context): 98 | bpy.ops.object.mode_set(mode="OBJECT") 99 | read_md5anim(self.filepath) 100 | return {'FINISHED'} 101 | 102 | class OT_EXPORT_ANIM_md5anim(bpy.types.Operator, ExportHelper): 103 | bl_idname = "export_anim.md5" 104 | bl_label = "Export *.md5anim Anim" 105 | bl_options = {"REGISTER", "UNDO"} 106 | 107 | filename_ext = ".md5anim" 108 | filter_glob = StringProperty(default="*.md5anim", options={'HIDDEN'}) 109 | bone_layer = IntProperty(name="BoneLayer", default=0) 110 | 111 | @classmethod 112 | def poll(cls, context): 113 | return (context.active_object and 114 | context.active_object.type == "ARMATURE" and 115 | context.active_object.animation_data and 116 | context.active_object.animation_data.action) 117 | 118 | def execute(self, context): 119 | write_md5anim(self.filepath, context.scene, context.active_object, self.bone_layer) 120 | return {"FINISHED"} 121 | 122 | 123 | class OT_IMPORT_MESH_sb_hudgun(bpy.types.Operator, ImportHelper): 124 | bl_idname = "import_mesh.sb_hudgun" 125 | bl_label = "Import Sauerbraten Hudgun" 126 | bl_options = {"REGISTER", "UNDO"} 127 | 128 | filename_ext = ".cfg" 129 | filter_glob = StringProperty(default="*.cfg", options={'HIDDEN'}) 130 | playermodel = EnumProperty(items=( 131 | ("MRFIXIT", "Mr Fixit", "", 0), 132 | ("SNOUTX10K", "SnoutX10K", "", 1), 133 | ("INKY", "Inky", "", 2), 134 | ("CAPTAINCANNON", "CaptainCannon", "", 3)), 135 | name="Playermodel", 136 | default="SNOUTX10K" 137 | ) 138 | 139 | weapon = EnumProperty(items=( 140 | ("SHOTG", "Shotgun", "", 1), 141 | ("CHAING", "Chaingun", "", 2), 142 | ("ROCKET", "Rocketlauncher", "", 3), 143 | ("RIFLE", "Rifle", "", 4), 144 | ("GL", "Grenadelauncher", "", 5), 145 | ("PISTOL", "Pistol", "", 6)), 146 | name="Weapon", 147 | default="RIFLE" 148 | ) 149 | 150 | fov = FloatProperty(name="Field Of View", default=65.0, subtype="NONE") 151 | res_x = IntProperty(name="Window Width", default=800, subtype="PIXEL") 152 | res_y = IntProperty(name="Window Height", default=600, subtype="PIXEL") 153 | directory = StringProperty(name="Directory", subtype="DIR_PATH", description="Directory of the file") 154 | filename = StringProperty(name="Filename", subtype="FILE_NAME", description="Name of the file", default="", options={'SKIP_SAVE', 'TEXTEDIT_UPDATE'}) 155 | # filesel = CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) 156 | 157 | def execute(self, context): 158 | pref = context.user_preferences.addons[__name__].preferences 159 | 160 | if self.filename == "": 161 | if pref.sb_dir == "": 162 | pref.sb_dir = self.directory 163 | self.filepath = os.path.join( 164 | pref.sb_dir, "packages", "models", 165 | self.playermodel.lower(), "hudguns", self.weapon.lower(), "md5.cfg") 166 | 167 | setup_cam(context.scene, self.fov, self.res_x, self.res_y) 168 | import_from_config(pref.sb_dir, self.filepath, self.weapon.lower()) 169 | context.scene.update() 170 | context.scene.frame_set(0) 171 | return {"FINISHED"} 172 | 173 | def draw(self, context): 174 | pref = context.user_preferences.addons[__name__].preferences 175 | layout = self.layout 176 | if not context.space_data.params.filename == "": 177 | layout.label(text="File selected - Dropdown will be ignored", icon="QUESTION") 178 | enabled = False 179 | else: 180 | enabled = True 181 | col = layout.column() 182 | col.enabled = enabled 183 | col.prop(self, "playermodel") 184 | col.prop(self, "weapon") 185 | layout.prop(self, "fov") 186 | layout.prop(self, "res_x") 187 | layout.prop(self, "res_y") 188 | layout.prop(pref, "sb_dir") 189 | 190 | reg_table = ( 191 | [OT_IMPORT_MESH_md5mesh, bpy.types.INFO_MT_file_import, "MD5 Mesh (.md5mesh)" ], 192 | [OT_IMPORT_ANIM_md5anim, bpy.types.INFO_MT_file_import, "MD5 Animation (.md5anim)"], 193 | [OT_EXPORT_MESH_md5mesh, bpy.types.INFO_MT_file_export, "MD5 Mesh (.md5mesh)" ], 194 | [OT_EXPORT_ANIM_md5anim, bpy.types.INFO_MT_file_export, "MD5 Animation (.md5anim)"], 195 | [OT_IMPORT_MESH_sb_hudgun, bpy.types.INFO_MT_file_import, "Sauerbraten Hudgun (.cfg)"] 196 | ) 197 | 198 | def generate_menu_function(op_cls, description): 199 | def mnu_func(self, context): 200 | self.layout.operator(op_cls.bl_idname, text=description) 201 | return mnu_func 202 | 203 | for row in reg_table: 204 | row[2] = generate_menu_function(row[0], row[2]) 205 | 206 | def register(): 207 | bpy.utils.register_class(MD5Preferences) 208 | for cls, mnu, mnu_func in reg_table: 209 | bpy.utils.register_class(cls) 210 | mnu.append(mnu_func) 211 | 212 | def unregister(): 213 | bpy.utils.unregister_class(MD5Preferences) 214 | for cls, mnu, mnu_func in reg_table: 215 | mnu.remove(mnu_func) 216 | bpy.utils.unregister_class(cls) 217 | -------------------------------------------------------------------------------- /io_md5anim.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from .shared import * # for brevity use star import, also imports modules 3 | 4 | def create_fcurves(action, data_path, dim, group=""): 5 | return tuple(action.fcurves.new(data_path, i, group) for i in range(dim)) 6 | 7 | def insert_keyframe(fcurves, time, values, interpolation="LINEAR"): 8 | for fcu, val in zip(fcurves, values): 9 | kf = fcu.keyframe_points.insert(time, val, {'FAST'}) 10 | kf.interpolation = interpolation 11 | 12 | def mat_offset(pose_bone): 13 | bone = pose_bone.bone 14 | mat = bone.matrix.to_4x4() 15 | mat.translation = bone.head 16 | if pose_bone.parent: 17 | mat.translation.y += bone.parent.length 18 | return mat 19 | 20 | class JointInfo: 21 | fmt = "{: <20s} {:2d} {:6b} {:4d}" 22 | 23 | def __init__(self, mobj=None): 24 | if mobj is not None: 25 | self.from_mobj(mobj) 26 | else: 27 | self.name = "" 28 | self.parent = -1 29 | self.flags = 63 30 | self.start_index = -1 31 | 32 | self.pose_bone = None 33 | self.bf = None 34 | self.fcu_loc = None 35 | self.fcu_rot = None 36 | 37 | self.mat_offset = None 38 | self.mat_rest_inv = None 39 | self.mat_world = None 40 | 41 | def from_mobj(self, mobj): 42 | self.name = mobj.group(1) 43 | self.parent = int(mobj.group(2)) 44 | self.flags = int(mobj.group(3)) 45 | self.start_index = int(mobj.group(4)) 46 | 47 | def from_pose_bone(self, pose_bone): 48 | self.name = pose_bone.name 49 | self.pose_bone = pose_bone 50 | self.mat_offset = mat_offset(pose_bone) 51 | self.mat_rest_inv = pose_bone.bone.matrix_local.inverted() 52 | 53 | def update(self, frame, values): 54 | if self.pose_bone is None: return 55 | 56 | loc = self.bf.loc.copy() 57 | rot = self.bf.rot.copy() 58 | si = self.start_index 59 | j = 0 60 | 61 | for i in range(3): 62 | if self.flags & 1 << i: 63 | loc[i] = values[si + j] 64 | j += 1 65 | 66 | for i in range(3): 67 | if self.flags & 1 << (i + 3): 68 | rot[i] = values[si + j] 69 | j += 1 70 | 71 | rot = restore_quat(*rot) 72 | mat_basis = (Matrix.Translation(loc) * 73 | rot.to_matrix().to_4x4()) 74 | 75 | mat_basis = mat_offset(self.pose_bone).inverted() * mat_basis 76 | loc, rot, scale = mat_basis.decompose() 77 | 78 | insert_keyframe(self.fcu_loc, frame, loc) 79 | insert_keyframe(self.fcu_rot, frame, rot) 80 | 81 | def update_from_scene(self): 82 | self.mat_world = self.pose_bone.matrix.copy() 83 | 84 | def write_hierarchy_data(self, stream): 85 | fmt = "\t\"{name:s}\" {parent:d} {flags:d} {start_index:d}\n" 86 | stream.write(fmt.format( 87 | name = self.name, 88 | parent = self.parent, 89 | flags = self.flags, 90 | start_index = self.start_index 91 | )) 92 | 93 | def write_baseframe(self, stream): 94 | fmt_row3f = "( {:.6f} {:.6f} {:.6f} )" 95 | fmt = "\t{loc:s} {rot:s}\n" 96 | 97 | mat = self.mat_offset * self.pose_bone.matrix_basis 98 | loc, rot, scale = mat.decompose() 99 | rot *= -1.0 100 | 101 | stream.write(fmt.format( 102 | loc = fmt_row3f.format(*loc), 103 | rot = fmt_row3f.format(*rot[1:]) 104 | )) 105 | 106 | def write_frame_data(self, stream): 107 | fmt = "\t{:.6f} {:.6f} {:.6f} {:.6f} {:.6f} {:.6f}\n" 108 | mat = self.mat_offset * self.pose_bone.matrix_basis 109 | loc, rot, scale = mat.decompose() 110 | rot *= -1.0 111 | arg = tuple(loc) + tuple(rot[1:]) 112 | stream.write(fmt.format(*arg)) 113 | 114 | class BaseFrame: 115 | def __init__(self, mobj): 116 | self.loc = unpack_tuple(mobj, 1, 3) 117 | self.rot = unpack_tuple(mobj, 4, 6) 118 | 119 | def read_md5anim(filepath): 120 | obj = bpy.context.active_object 121 | assert obj.type == "ARMATURE" 122 | 123 | if not obj.animation_data: 124 | obj.animation_data_create() 125 | 126 | action = bpy.data.actions.new("test") 127 | obj.animation_data.action = action 128 | pose_bones = obj.pose.bones 129 | data_path_loc = 'pose.bones["{:s}"].location' 130 | data_path_rot = 'pose.bones["{:s}"].rotation_quaternion' 131 | 132 | t_Int = r"(-?\d+)" 133 | t_Float = r"(-?\d+\.\d+)" 134 | t_Word = r"(\S+)" 135 | t_QuotedString = '"([^"]*)"' # does not allow escaping \" 136 | t_Tuple2f = "\\s+".join(("\\(", t_Float, t_Float, "\\)")) 137 | t_Tuple3f = "\\s+".join(("\\(", t_Float, t_Float, t_Float, "\\)")) 138 | 139 | re_end = construct("\\}") 140 | re_num_frames = construct("numFrames", t_Int) 141 | re_framerate = construct("frameRate", t_Int) 142 | re_num_components = construct("numAnimatedComponents", t_Int) 143 | re_hierarchy = construct("hierarchy", "\\{") 144 | re_joint = construct(t_QuotedString, t_Int, t_Int, t_Int) 145 | re_bounds = construct("bounds", "\\{") 146 | re_bbox = construct(t_Tuple3f, t_Tuple3f) 147 | re_baseframe = construct("baseframe", "\\{") 148 | re_bframe = construct(t_Tuple3f, t_Tuple3f) 149 | re_frame = construct("frame", t_Int, "\\{") 150 | re_float = construct(t_Float) 151 | re_endline = construct("\n") 152 | 153 | def read_floats(line): 154 | result = [] 155 | pos = 0 156 | 157 | while True: 158 | if re_endline.match(line, pos): 159 | return result 160 | 161 | mobj = re_float.match(line, pos) 162 | if mobj is None: 163 | raise ValueError 164 | 165 | result.append(float(mobj.group(1))) 166 | pos += len(mobj.group(0)) 167 | 168 | def to_int(x): 169 | return int(x[0].group(1)) 170 | 171 | with open(filepath) as fobj: lines = iter(fobj.readlines()) 172 | 173 | num_frames, framerate, num_components = gather_multi( 174 | [re_num_frames, re_framerate, re_num_components], 175 | re_hierarchy, 176 | lines 177 | ) 178 | 179 | num_frames = to_int(num_frames) 180 | framerate = to_int(framerate) 181 | num_components = to_int(num_components) 182 | 183 | joints = gather(re_joint, re_end, lines) 184 | skip_until(re_bounds, lines) 185 | bboxes = gather(re_bbox, re_end, lines) 186 | skip_until(re_baseframe, lines) 187 | bframes = gather(re_bframe, re_end, lines) 188 | 189 | process_match_objects(joints, JointInfo) 190 | process_match_objects(bframes, BaseFrame) 191 | 192 | for joint, base_frame in zip(joints, bframes): 193 | joint.pose_bone = pose_bones.get(joint.name) 194 | if joint.pose_bone is None: return 195 | 196 | joint.is_valid = True 197 | joint.bf = base_frame 198 | 199 | joint.fcu_loc = create_fcurves( 200 | action, data_path_loc.format(joint.name), 3, joint.name) 201 | joint.fcu_rot = create_fcurves( 202 | action, data_path_rot.format(joint.name), 4, joint.name) 203 | 204 | frames = [[] for i in range(num_frames)] 205 | 206 | while True: 207 | line = skip_until(re_frame, lines) 208 | if line is None: 209 | break 210 | 211 | frame_index = int(re_frame.match(line).group(1)) 212 | parameters = frames[frame_index] 213 | for line in lines: 214 | if re_end.match(line) is not None: 215 | break 216 | parameters.extend(read_floats(line)) 217 | 218 | for fi in range(num_frames): 219 | for ji, joint in enumerate(joints): 220 | joint.update(fi, frames[fi]) 221 | 222 | return action 223 | 224 | #------------------------------------------------------------------------------- 225 | # Write md5anim 226 | #------------------------------------------------------------------------------- 227 | 228 | def vec_cmp(vec, vec_other, func): 229 | return tuple(func(a, b) for a, b in zip(vec, vec_other)) 230 | 231 | def calc_bbox_from_object(mesh_objects): 232 | bbox_iter = (mesh_obj.bound_box for mesh_obj in mesh_objects) 233 | bbox_iter = chain.from_iterable(bbox_iter) 234 | bb_min = bb_max = next(bbox_iter) 235 | 236 | for vec in bbox_iter: 237 | bb_min = vec_cmp(bb_min, vec, min) 238 | bb_max = vec_cmp(bb_max, vec, max) 239 | 240 | return bb_min, bb_max 241 | 242 | def get_parent_index(bone, lut): 243 | if bone.parent is None: return -1 244 | return lut[bone.parent.name] 245 | 246 | def write_md5anim(filepath, scene, arm_obj, bone_layer, skip_bbox=False): 247 | mesh_objects = [] 248 | for mesh_obj in filter(is_mesh_object, scene.objects): 249 | if has_armature_modifier(mesh_obj, arm_obj): 250 | mesh_objects.append(mesh_obj) 251 | 252 | action = arm_obj.animation_data.action 253 | frame_start, frame_end = tuple(map(int, action.frame_range)) 254 | frame_count = frame_end - frame_start + 1 255 | 256 | pose_bones = [pb for pb in arm_obj.pose.bones if pb.bone.layers[bone_layer]] 257 | nof_joints = len(pose_bones) 258 | name_to_index = get_name_to_index_dict(arm_obj) 259 | joint_infos = [JointInfo() for i in range(nof_joints)] 260 | 261 | for pose_bone in pose_bones: 262 | index = name_to_index[pose_bone.name] 263 | joint_info = joint_infos[index] 264 | joint_info.from_pose_bone(pose_bone) 265 | joint_info.parent = get_parent_index(pose_bone, name_to_index) 266 | joint_info.start_index = index * 6 267 | 268 | filler = "\t( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 )\n" 269 | 270 | with open(filepath, "w") as stream: 271 | stream.write("MD5Version 10\n") 272 | stream.write("commandline \"\"\n\n") 273 | 274 | stream.write("numFrames %d\n" % frame_count) 275 | stream.write("numJoints %d\n" % nof_joints) 276 | stream.write("frameRate %d\n" % scene.render.fps) 277 | stream.write("numAnimatedComponents %d\n" % (6 * nof_joints)) 278 | 279 | stream.write("\nhierarchy {\n") 280 | for joint_info in joint_infos: 281 | joint_info.write_hierarchy_data(stream) 282 | stream.write("}\n") 283 | 284 | stream.write("\nbounds {\n") 285 | for frame_index in range(frame_start, frame_end + 1): 286 | if skip_bbox: 287 | stream.write(filler) 288 | continue 289 | 290 | scene.frame_set(frame_index) 291 | bb_min, bb_max = calc_bbox_from_object(mesh_objects) 292 | stream.write("\t{bb_min:s} {bb_max:s}\n".format( 293 | bb_min=fmt_row3f.format(*bb_min), 294 | bb_max=fmt_row3f.format(*bb_max))) 295 | 296 | stream.write("}\n") 297 | 298 | stream.write("\nbaseframe {\n") 299 | scene.frame_set(frame_start) 300 | for joint_info in joint_infos: 301 | joint_info.write_baseframe(stream) 302 | stream.write("}\n") 303 | 304 | for frame_index in range(frame_start, frame_end + 1): 305 | stream.write("\nframe %d {\n" % frame_index) 306 | scene.frame_set(frame_index) 307 | for joint_info in joint_infos: 308 | joint_info.write_frame_data(stream) 309 | stream.write("}\n") 310 | 311 | #------------------------------------------------------------------------------- 312 | # Test 313 | #------------------------------------------------------------------------------- 314 | 315 | def test(): 316 | import os 317 | 318 | filepath = os.path.expanduser( 319 | "~/Downloads/Games" 320 | "/sauerbraten_2013/sauerbraten/packages" 321 | "/models/mrfixit/swim.md5anim") 322 | 323 | output = os.path.expanduser("~/Dokumente/Blender/Scripts/addons/md5/test.md5anim") 324 | 325 | read_md5anim(filepath) 326 | 327 | scene = bpy.context.scene 328 | arm_obj = scene.objects.active 329 | write_md5anim(output, scene, arm_obj, 0) 330 | 331 | read_md5anim(output) 332 | -------------------------------------------------------------------------------- /import_hudguns.py: -------------------------------------------------------------------------------- 1 | """ This python script reads the md5 config files of the hudguns associated with a specific playermodel, 2 | to import their models and adjusted animations. It also sets up a camera for you so you get a preview 3 | of how the hudgun would look like in the game. However it does **not** correctly parse cube script. 4 | It assumes line based commands and greedily parses them and will fail otherwise. Only a selection of 5 | commands is parsed and the rest is ignored. 6 | 7 | After a few tests this seems to be sufficient to get the job done. 8 | Invoke this script from the commandline: 9 | 10 | blender --python import_hudguns.py -- playermodel 11 | 12 | where playermodel is an integer in the range 0..3 13 | 0 - mrfixit 14 | 1 - snoutx10k 15 | 2 - inky 16 | 3 - captaincannon 17 | """ 18 | 19 | import re 20 | import os 21 | import bpy 22 | import math 23 | from mathutils import Vector 24 | from .io_md5mesh import read_md5mesh 25 | from .io_md5anim import read_md5anim 26 | from .dualquat import BoneAdjustment, adjust_animation 27 | 28 | # ------------------------------------------------------------------------------- 29 | 30 | class LineParser: 31 | def __init__(self, tokens, mask=0): 32 | self.tokens = tokens 33 | self.length = len(self.tokens) 34 | self.mask = mask 35 | self.n = 0 36 | self.line = None 37 | self.pos = 0 38 | self.result = None 39 | self.status = 0 40 | 41 | def feed(self, line): 42 | self.n = 0 43 | self.line = line 44 | self.pos = 0 45 | self.result = [] 46 | self.status = 0 47 | 48 | def get_default(self, tk): 49 | if tk is t_Int: return 0 50 | elif tk is t_Float: return 0.0 51 | elif tk is t_Word: return "" 52 | else: return None 53 | 54 | def convert_match_string(self, tk, ms): 55 | if tk is t_Int: return int(ms) 56 | elif tk is t_Float: return float(ms) 57 | 58 | if tk is t_Word: 59 | if ms.startswith('"') and ms.endswith('"'): 60 | ms = ms[1:-1] 61 | return ms 62 | 63 | def consume(self, tk, is_optional): 64 | mobj = tk.match(self.line, self.pos) 65 | if mobj is None: 66 | if not is_optional: 67 | self.status = -1 68 | return None 69 | else: 70 | return self.get_default(tk) 71 | 72 | self.pos = mobj.end() 73 | ms = mobj.group() 74 | 75 | return self.convert_match_string(tk, ms) 76 | 77 | def advance(self): 78 | self.consume(t_Sep, True) 79 | 80 | tk = self.tokens[self.n] 81 | is_optional = self.mask & (1 << self.length - 1 - self.n) 82 | mobj = self.consume(tk, is_optional) 83 | 84 | self.n += 1 85 | if tk in (t_Int, t_Float, t_Word): 86 | self.result.append(mobj) 87 | 88 | if self.n == self.length: 89 | self.status = 1 90 | 91 | def match(self, line): 92 | self.feed(line) 93 | 94 | while self.status == 0: 95 | self.advance() 96 | 97 | if self.status == 1: 98 | return self.result 99 | else: 100 | return None 101 | 102 | t_Int = re.compile(r"-?\d+") 103 | t_Float = re.compile(r"-?\d+(?:\.\d+)?") 104 | t_Word = re.compile(r'(?:"[^"]*")|\S+') 105 | t_Sep = re.compile(r"\s+") 106 | 107 | def t_literal(s): 108 | return re.compile(s) 109 | 110 | lt_exec = LineParser([t_literal("exec"), t_Word]) 111 | lt_md5dir = LineParser([t_literal("md5dir"), t_Word]) 112 | lt_md5load = LineParser([t_literal("md5load"), t_Word]) 113 | lt_md5anim = LineParser([t_literal("md5anim"), t_Word, t_Word, t_Int], 0b0001) 114 | lt_md5adjust = LineParser([t_literal("md5adjust"), t_Word, t_Float, t_Float, t_Float, t_Float, t_Float, t_Float], 0b00111111) 115 | lt_md5tag = LineParser([t_literal("md5tag"), t_Word, t_Word]) 116 | lt_md5link = LineParser([t_literal("md5link"), t_Int, t_Int, t_Word, t_Float, t_Float, t_Float], 0b0000111) 117 | lt_md5skin = LineParser([t_literal("md5skin"), t_Word, t_Word, t_Word]) 118 | lt_mdlscale = LineParser([t_literal("mdlscale"), t_Int]) 119 | lt_mdltrans = LineParser([t_literal("mdltrans"), t_Float, t_Float, t_Float], 0b0111) 120 | 121 | class MD5Part: 122 | def __init__(self): 123 | self.filepath = "" 124 | self.skins = [] 125 | self.adjustments = [] 126 | self.animations = [] 127 | self.tags = {} 128 | self.bobj = None 129 | 130 | class MD5Config: 131 | def __init__(self, sb_dir, cwd): 132 | self.sb_dir = sb_dir 133 | self.parts = [] 134 | self.links = [] 135 | self.scale = 300 136 | self.translation = 0.0, 0.0, 0.0 137 | self.cwd = cwd 138 | 139 | def current_part(self): 140 | return self.parts[-1] 141 | 142 | def readline(self, line): 143 | lt2meth = ( 144 | (lt_exec, MD5Config.on_exec), 145 | (lt_md5dir, MD5Config.on_md5dir), 146 | (lt_md5load, MD5Config.on_md5load), 147 | (lt_md5anim, MD5Config.on_md5anim), 148 | (lt_md5adjust, MD5Config.on_md5adjust), 149 | (lt_md5tag, MD5Config.on_md5tag), 150 | (lt_md5link, MD5Config.on_md5link), 151 | (lt_md5skin, MD5Config.on_md5skin), 152 | (lt_mdlscale, MD5Config.on_mdlscale), 153 | (lt_mdltrans, MD5Config.on_mdltrans)) 154 | 155 | for lt, method in lt2meth: 156 | result = lt.match(line) 157 | if result is not None: 158 | method(self, result) 159 | break 160 | 161 | def read_cfg_file(self, filepath): 162 | with open(filepath) as fobj: 163 | content = fobj.read() 164 | 165 | for line in content.splitlines(): 166 | self.readline(line) 167 | 168 | def on_exec(self, result): 169 | self.read_cfg_file(os.path.join(self.sb_dir, result[0])) 170 | 171 | def on_md5dir(self, result): 172 | self.cwd = os.path.join(self.sb_dir, "packages/models", result[0]) 173 | 174 | def on_md5load(self, result): 175 | part = MD5Part() 176 | part.filepath = os.path.join(self.cwd, result[0]) 177 | self.parts.append(part) 178 | 179 | def on_md5anim(self, result): 180 | result[1] = os.path.join(self.cwd, result[1]) 181 | part = self.current_part().animations.append(result) 182 | 183 | def on_md5adjust(self, result): 184 | bone_name = result[0] 185 | yaw, pitch, roll = result[1:4] 186 | translation = result[4:7] 187 | 188 | ba = BoneAdjustment(bone_name, yaw, pitch, roll, translation) 189 | p = self.current_part() 190 | p.adjustments.append(ba) 191 | 192 | if p.animations: 193 | print("WARNING! - read bone adjustment after animation") 194 | 195 | def on_md5tag(self, result): 196 | self.current_part().tags[result[1]] = result[0] 197 | 198 | def on_md5skin(self, result): 199 | for i in range(1, 3): 200 | if result[i].startswith(""): 201 | result[i] = result[i][5:] 202 | result[i] = os.path.join(self.cwd, result[i]) 203 | 204 | self.current_part().skins.append(result) 205 | 206 | def on_md5link(self, result): 207 | self.links.append(result) 208 | 209 | def on_mdlscale(self, result): 210 | self.scale = result[0] 211 | self.scale = self.scale / 400 212 | 213 | def on_mdltrans(self, result): 214 | self.translation = result 215 | 216 | # ------------------------------------------------------------------------------- 217 | 218 | def get_child(obj, child_name): 219 | for child in obj.children: 220 | if child.name.rsplit(".")[0] == child_name: 221 | return child 222 | 223 | def set_texture(parent, child_name, texture_path): 224 | child = get_child(parent, child_name) 225 | img = bpy.data.images.load(texture_path, check_existing=True) 226 | 227 | for mtp in child.data.uv_textures[0].data: 228 | mtp.image = img 229 | 230 | mat = child.data.materials[0] 231 | if mat.texture_slots[0] is not None: 232 | return 233 | 234 | tex = bpy.data.textures.new(child_name, "IMAGE") 235 | tex.image = img 236 | slot = mat.texture_slots.add() 237 | slot.texture = tex 238 | 239 | def set_scene_layer(scene, layers): 240 | for i in range(20): 241 | scene.layers[i] = scene.layers[i] or layers[i] 242 | 243 | for i in range(20): 244 | scene.layers[i] = layers[i] 245 | 246 | def layer_mask(n): 247 | return tuple(i == n for i in range(20)) 248 | 249 | def set_transform(action, translation, scale): 250 | fcu_loc = create_fcurves(action, "location", 3, "ObjectTransform") 251 | fcu_scale = create_fcurves(action, "scale", 3, "ObjectTransform") 252 | 253 | loc = Vector(translation) 254 | loc.y = -loc.y 255 | loc *= scale 256 | 257 | set_kf(fcu_loc, 0.0, loc) 258 | set_kf(fcu_scale, 0.0, (scale, scale, scale)) 259 | 260 | def create_fcurves(action, data_path, size, group=""): 261 | return [action.fcurves.new(data_path, i, group) for i in range(size)] 262 | 263 | def set_kf(fcurves, time, values, interpolation="CONSTANT"): 264 | for fcu, val in zip(fcurves, values): 265 | kf = fcu.keyframe_points.insert(time, val, {'FAST'}) 266 | kf.interpolation = interpolation 267 | 268 | # ------------------------------------------------------------------------------- 269 | 270 | def import_from_config(sb_dir, cfg_filepath, dn): 271 | md5config = MD5Config(sb_dir, os.path.dirname(cfg_filepath)) 272 | md5config.read_cfg_file(cfg_filepath) 273 | 274 | for i, p in enumerate(md5config.parts): 275 | arm_obj = read_md5mesh(p.filepath) 276 | arm_obj.data.draw_type = "WIRE" 277 | p.bobj = arm_obj 278 | 279 | for skin in p.skins: 280 | set_texture(arm_obj, skin[0], skin[1]) 281 | 282 | for anm in p.animations: 283 | action = read_md5anim(anm[1]) 284 | action.use_fake_user = True 285 | action.name = ("hands_" + dn + "_idle" if i == 0 and anm[0] == "gun idle" else 286 | "hands_" + dn + "_shoot" if i == 0 and anm[0] == "gun shoot" else 287 | "weapon_" + dn + "_idle" if i == 1 and anm[0] == "gun idle" else 288 | "weapon_" + dn + "_shoot" if i == 1 and anm[0] == "gun shoot" else 289 | anm[0]) 290 | 291 | adjust_animation(arm_obj, p.adjustments) 292 | anm[1] = action 293 | 294 | for l in md5config.links: 295 | parent = md5config.parts[l[0]] 296 | child = md5config.parts[l[1]] 297 | tag_name = l[2] 298 | offset = l[3:6] 299 | 300 | bone_name = parent.tags[tag_name] 301 | 302 | child = child.bobj 303 | child.scale *= md5config.scale 304 | child.location = md5config.scale * Vector(offset) 305 | child.location.y = -child.location.y 306 | 307 | con = child.constraints.new("CHILD_OF") 308 | con.use_scale_x = False 309 | con.use_scale_y = False 310 | con.use_scale_z = False 311 | con.target = parent.bobj 312 | con.subtarget = bone_name 313 | 314 | for anm in md5config.parts[0].animations: 315 | set_transform(anm[1], md5config.translation, md5config.scale) 316 | 317 | md5config.parts[0].bobj.name = "Armature_Hands_" + dn 318 | md5config.parts[1].bobj.name = "Armature_" + dn 319 | 320 | # ------------------------------------------------------------------------------ 321 | 322 | def setup_cam(scene, fov, width, height): 323 | cam = bpy.data.objects.get("Camera") 324 | if cam is None: 325 | cam_data = bpy.data.cameras.new("Camera") 326 | cam = bpy.data.objects.new("Camera", cam_data) 327 | scene.objects.link(cam) 328 | 329 | scene.render.resolution_x = width 330 | scene.render.resolution_y = height 331 | scene.camera = cam 332 | 333 | angle = math.radians(65) 334 | aspect = width / height 335 | t = math.tan(angle * 0.5) 336 | hpi = math.pi * 0.5 337 | 338 | cam.location = 0.0, 0.0, 0.0 339 | cam.rotation_euler = hpi, 0.0, -hpi 340 | 341 | cam_data = cam.data 342 | cam_data.angle = 2.0 * math.atan(aspect * t) 343 | 344 | # ------------------------------------------------------------------------------ 345 | 346 | if __name__ == "__main__": 347 | fmt_cfg = "packages/models/{playermodel:s}/hudguns/{weapon:s}/md5.cfg" 348 | sb_dir = os.path.expanduser("~/Downloads/Games/sauerbraten_2013/sauerbraten") 349 | playermodels = "mrfixit", "snoutx10k", "inky", "captaincannon" 350 | scene = bpy.context.scene 351 | 352 | import sys 353 | i = int(sys.argv[-1]) 354 | pm = playermodels[i] 355 | 356 | setup_cam(scene, 65, 1280, 1024) 357 | 358 | for i, dn in enumerate(("shotg", "chaing", "rocket", "rifle", "gl", "pistol")): 359 | scene.update() 360 | 361 | cfg_filepath = os.path.join(sb_dir, fmt_cfg.format(playermodel=pm, weapon=dn)) 362 | print(cfg_filepath) 363 | set_scene_layer(scene, layer_mask(i)) 364 | import_from_config(sb_dir, cfg_filepath, dn) 365 | 366 | scene.frame_set(0) 367 | 368 | for img in bpy.data.images: 369 | img.pack() 370 | img.filepath = "" 371 | -------------------------------------------------------------------------------- /io_md5mesh.py: -------------------------------------------------------------------------------- 1 | import bmesh 2 | import logging 3 | from .shared import * # for brevity use star import, also imports modules 4 | 5 | logging.basicConfig(style="{", level=logging.WARNING) 6 | 7 | #------------------------------------------------------------------------------- 8 | # Classes 9 | #------------------------------------------------------------------------------- 10 | 11 | class Vert: 12 | def __init__(self, mobj=None): 13 | if mobj is not None: 14 | self.from_mobj(mobj) 15 | else: 16 | self.index = -1 17 | self.uv = None 18 | self.fwi = -1 19 | self.nof_weights = 0 20 | self.bmv = None 21 | 22 | def from_mobj(self, mobj): 23 | self.index = int(mobj.group(1)) 24 | self.uv = unpack_tuple(mobj, 2, 3) 25 | self.fwi = int(mobj.group(4)) # first weight index 26 | self.nof_weights = int(mobj.group(5)) 27 | self.bmv = None 28 | 29 | self.uv.y = 1.0 - self.uv.y 30 | 31 | def get_weights(self, weights): 32 | return weights[self.fwi: self.fwi + self.nof_weights] 33 | 34 | def calc_position(self, weights, matrices): 35 | return sum((matrices[weight.joint_index][1] * weight.offset * weight.value 36 | for weight in self.get_weights(weights)), Vector()) 37 | 38 | def serialize(self, stream): 39 | self.uv.y = 1.0 - self.uv.y 40 | fmt = "\tvert {index:d} {uv:s} {fwi:d} {nof_weights:d}\n" 41 | 42 | stream.write(fmt.format( 43 | index = self.index, 44 | uv = fmt_row2f.format(*self.uv), 45 | fwi = self.fwi, 46 | nof_weights = self.nof_weights 47 | )) 48 | 49 | 50 | class Weight: 51 | def __init__(self, mobj=None): 52 | if mobj is not None: 53 | self.from_mobj(mobj) 54 | else: 55 | self.index = -1 56 | self.joint_index = -1 57 | self.value = 0.0 58 | self.offset = None 59 | 60 | def from_mobj(self, mobj): 61 | self.joint_index = int(mobj.group(2)) 62 | self.value = float(mobj.group(3)) 63 | self.offset = unpack_tuple(mobj, 4, 6) 64 | 65 | def serialize(self, stream): 66 | fmt = "\tweight {index:d} {joint_index:d} {value:.6f} {offset:s}\n" 67 | stream.write(fmt.format( 68 | index = self.index, 69 | joint_index = self.joint_index, 70 | value = self.value, 71 | offset = fmt_row3f.format(*self.offset) 72 | )) 73 | 74 | 75 | class Mesh: 76 | def __init__(self, mesh_obj): 77 | mesh = mesh_obj.data 78 | 79 | self.mesh_obj = mesh_obj 80 | self.weights = [] 81 | self.shader = (mesh.materials[0].name if mesh.materials 82 | else "") 83 | 84 | self.bm = bmesh.new() 85 | self.bm.from_mesh(mesh) 86 | self.process_for_export() 87 | self.bm.verts.index_update() 88 | self.tris = [[v.index for v in f.verts] 89 | for f in self.bm.faces] 90 | nof_verts = len(self.bm.verts) 91 | 92 | self.verts = [Vert() for i in range(nof_verts)] 93 | 94 | def process_for_export(self): 95 | bm = self.bm 96 | 97 | def vec_equals(a, b): 98 | return (a - b).magnitude < 5e-2 99 | 100 | # split vertices with multiple uv coordinates 101 | seams = [] 102 | tag_verts = set() 103 | layer_uv = bm.loops.layers.uv.active 104 | 105 | for edge in bm.edges: 106 | if not edge.is_manifold: continue 107 | 108 | uvs = [None] * 2 109 | loops = [None] * 2 110 | 111 | loops[0] = list(edge.link_loops) 112 | loops[1] = [loop.link_loop_next for loop in loops[0]] 113 | 114 | for i in range(2): 115 | uvs[i] = list(map(lambda l: l[layer_uv].uv, loops[i])) 116 | 117 | results = (vec_equals(uvs[0][0], uvs[1][1]), 118 | vec_equals(uvs[0][1], uvs[1][0])) 119 | 120 | if not all(results): 121 | if results[0]: tag_verts.add(loops[0][0].vert) 122 | if results[1]: tag_verts.add(loops[0][1].vert) 123 | seams.append(edge) 124 | 125 | tag_verts = list(tag_verts) 126 | bmesh.ops.split_edges(bm, edges=seams, verts=tag_verts, use_verts=True) 127 | 128 | # triangulate 129 | bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) 130 | 131 | # flip normals 132 | bmesh.ops.reverse_faces(bm, faces=bm.faces[:], flip_multires=False) 133 | 134 | def set_weights(self, joints, lut): 135 | vertex_groups = self.mesh_obj.vertex_groups 136 | layer_deform = self.bm.verts.layers.deform.active 137 | layer_uv = self.bm.loops.layers.uv.active 138 | first_index = 0 139 | nof_weights = 0 140 | 141 | for v, bmv in zip(self.verts, self.bm.verts): 142 | v.index = bmv.index 143 | first_index = first_index + nof_weights 144 | nof_weights = 0 145 | weights = [] 146 | 147 | for key, value in bmv[layer_deform].items(): 148 | if value < 5e-4: 149 | logging.warning("Skipping weight with value %.2f of vertex %d" % (value, bmv.index)) 150 | continue 151 | 152 | vertex_group = vertex_groups[key] 153 | joint_index = lut[vertex_group.name] 154 | 155 | weight = Weight() 156 | weight.index = first_index + nof_weights 157 | weight.joint_index = joint_index 158 | weight.value = value 159 | weights.append(weight) 160 | nof_weights += 1 161 | 162 | v.fwi = first_index 163 | v.nof_weights = nof_weights 164 | self.weights.extend(weights) 165 | 166 | # r = Σ mi wi ri, ensure Σ wi = 1.0 and choose mi^-1 r 167 | co = (1 / sum(weight.value for weight in weights)) * bmv.co 168 | 169 | for weight in weights: 170 | weight.offset = joints[weight.joint_index].mat_inv * co 171 | 172 | for face in self.bm.faces: 173 | for loop in face.loops: 174 | vert = self.verts[loop.vert.index] 175 | vert.uv = loop[layer_uv].uv 176 | 177 | def serialize(self, stream): 178 | stream.write("\nmesh {\n") 179 | stream.write("\t// meshes: %s\n" % self.mesh_obj.name) 180 | stream.write("\n\tshader \"%s\"\n" % self.shader) 181 | stream.write("\n\tnumverts %d\n" % len(self.verts)) 182 | 183 | for vert in self.verts: 184 | vert.serialize(stream) 185 | 186 | stream.write("\n\tnumtris %d\n" % len(self.tris)) 187 | for index, tri in enumerate(self.tris): 188 | stream.write("\ttri {:d} {:d} {:d} {:d}\n".format(index, *tri)) 189 | 190 | stream.write("\n\tnumweights %d\n" % len(self.weights)) 191 | for weight in self.weights: 192 | weight.serialize(stream) 193 | 194 | stream.write("}\n") 195 | 196 | def finish(self): 197 | self.bm.free() 198 | self.verts = None 199 | self.weights = None 200 | self.tris = None 201 | 202 | class Joint: 203 | def __init__(self): 204 | self.name = "" 205 | self.index = -1 206 | self.parent_index = -1 207 | self.parent_name = "" 208 | 209 | self.mat = None 210 | self.mat_inv = None 211 | 212 | self.loc = None 213 | self.rot = None 214 | 215 | def serialize(self, stream): 216 | fmt = "\t\"{name:s}\"\t{pindex:d} {loc:s} {rot:s}\t\t// {pname:s}\n" 217 | stream.write(fmt.format( 218 | name = self.name, 219 | pindex = self.parent_index, 220 | loc = fmt_row3f.format(*self.loc), 221 | rot = fmt_row3f.format(*self.rot[1:]), 222 | pname = self.parent_name 223 | )) 224 | 225 | def from_bone(self, bone, index, lut): 226 | self.name = bone.name 227 | self.index = lut[bone.name] = index 228 | self.parent_index = self.get_parent_index(bone, lut) 229 | self.parent_name = bone.parent.name if bone.parent is not None else "" 230 | self.mat = bone.matrix_local.copy() 231 | self.mat_inv = self.mat.inverted() 232 | self.loc, self.rot, scale = self.mat.decompose() 233 | self.rot *= -1.0 234 | 235 | @classmethod 236 | def get_parent_index(cls, bone, lut): 237 | if bone.parent is None: return -1 238 | return lut[bone.parent.name] 239 | 240 | #------------------------------------------------------------------------------- 241 | # Read md5mesh 242 | #------------------------------------------------------------------------------- 243 | 244 | def read_md5mesh(filepath): 245 | t_Int = r"(-?\d+)" 246 | t_Float = r"(-?\d+\.\d+)" 247 | t_Word = r"(\S+)" 248 | t_QuotedString = '"([^"]*)"' # does not allow escaping \" 249 | t_Tuple2f = "\\s+".join(("\\(", t_Float, t_Float, "\\)")) 250 | t_Tuple3f = "\\s+".join(("\\(", t_Float, t_Float, t_Float, "\\)")) 251 | 252 | re_joint = construct(t_QuotedString, t_Int, t_Tuple3f, t_Tuple3f) 253 | re_vert = construct("vert", t_Int, t_Tuple2f, t_Int, t_Int) 254 | re_tri = construct("tri", t_Int, t_Int, t_Int, t_Int) 255 | re_weight = construct("weight", t_Int, t_Int, t_Float, t_Tuple3f) 256 | re_end = construct("\\}") 257 | re_joints = construct("joints", "\\{") 258 | re_nverts = construct("numverts", t_Int) 259 | re_mesh = construct("mesh", "\\{") 260 | re_shader = construct("shader", t_QuotedString) 261 | re_mesh_label = construct(".*?// meshes: (.*)$") # comment, used by sauerbraten 262 | 263 | with open(filepath, "r") as fobj: 264 | lines = iter(fobj.readlines()) 265 | 266 | skip_until(re_joints, lines) 267 | 268 | arm_obj, matrices = do_joints(lines, re_joint, re_end) 269 | results = [] 270 | reg_exprs = re_shader, re_vert, re_tri, re_weight, re_end, re_nverts, re_mesh_label 271 | n = 0 272 | 273 | while True: 274 | results.append(do_mesh(lines, reg_exprs, matrices)) 275 | n += 1 276 | 277 | if skip_until(re_mesh, lines) is None: 278 | break 279 | 280 | for label, shader, bm in results: 281 | mesh = bpy.data.meshes.new(label) 282 | bm.to_mesh(mesh) 283 | bm.free() 284 | 285 | mesh.auto_smooth_angle = math.radians(45) 286 | mesh.use_auto_smooth = True 287 | 288 | mesh_obj = bpy.data.objects.new(label, mesh) 289 | for joint_name, mat in matrices: 290 | mesh_obj.vertex_groups.new(name=joint_name) 291 | 292 | mesh_obj.parent = arm_obj 293 | arm_mod = mesh_obj.modifiers.new(type='ARMATURE', name="MD5_skeleton") 294 | arm_mod.object = arm_obj 295 | arm_mod.use_deform_preserve_volume = True 296 | 297 | bpy.context.scene.objects.link(mesh_obj) 298 | 299 | mat_name = label 300 | mat = (bpy.data.materials.get(mat_name) or 301 | bpy.data.materials.new(mat_name)) 302 | mesh.materials.append(mat) 303 | 304 | return arm_obj 305 | 306 | def do_joints(lines, re_joint, re_end): 307 | joints = gather(re_joint, re_end, lines) 308 | 309 | arm = bpy.data.armatures.new("MD5") 310 | arm_obj = bpy.data.objects.new("MD5", arm) 311 | arm_obj.select = True 312 | bpy.context.scene.objects.link(arm_obj) 313 | bpy.context.scene.objects.active = arm_obj 314 | 315 | matrices = [] 316 | name_to_index = {} 317 | VEC_Y = Vector((0.0, 1.0, 0.0)) 318 | VEC_Z = Vector((0.0, 0.0, 1.0)) 319 | 320 | bpy.ops.object.mode_set(mode='EDIT') 321 | edit_bones = arm.edit_bones 322 | 323 | for index, mobj in enumerate(joints): 324 | name = mobj.group(1) 325 | parent = int(mobj.group(2)) 326 | loc = unpack_tuple(mobj, 3, 5) 327 | quat = unpack_tuple(mobj, 6, 8, seq=tuple) 328 | name_to_index[name] = index 329 | 330 | eb = edit_bones.new(name) 331 | if parent >= 0: 332 | eb.parent = edit_bones[parent] 333 | 334 | quat = restore_quat(*quat) 335 | mat = Matrix.Translation(loc) * quat.to_matrix().to_4x4() 336 | matrices.append((name, mat)) 337 | 338 | eb.head = loc 339 | eb.tail = loc + quat * VEC_Y 340 | eb.align_roll(quat * VEC_Z) 341 | 342 | for eb in arm.edit_bones: 343 | if len(eb.children) == 1: 344 | child = eb.children[0] 345 | head_to_head = child.head - eb.head 346 | projection = head_to_head.project(eb.y_axis) 347 | if eb.y_axis.dot(projection) > 5e-2: 348 | eb.tail = eb.head + projection 349 | 350 | bpy.ops.object.mode_set() 351 | arm_obj['name_to_index'] = name_to_index 352 | return arm_obj, matrices 353 | 354 | def do_mesh(lines, reg_exprs, matrices): 355 | (re_shader, 356 | re_vert, 357 | re_tri, 358 | re_weight, 359 | re_end, 360 | re_nverts, 361 | re_label) = reg_exprs 362 | 363 | mobjs__label, mobjs_shader = gather_multi([re_label, re_shader], re_nverts, lines) 364 | label = mobjs__label[0].group(1) if len(mobjs__label) > 0 else "md5mesh" 365 | shader = mobjs_shader[0].group(1) if len(mobjs_shader) > 0 else "" 366 | 367 | verts, tris, weights = gather_multi( 368 | [re_vert, re_tri, re_weight], 369 | re_end, 370 | lines 371 | ) 372 | 373 | bm = bmesh.new() 374 | process_match_objects(verts, Vert) 375 | process_match_objects(weights, Weight) 376 | 377 | layer_weight = bm.verts.layers.deform.verify() 378 | layer_uv = bm.loops.layers.uv.verify() 379 | 380 | for index, vert in enumerate(verts): 381 | vert.bmv = bm.verts.new(vert.calc_position(weights, matrices)) 382 | for weight in vert.get_weights(weights): 383 | vert.bmv[layer_weight][weight.joint_index] = weight.value 384 | 385 | for mobj_tri in tris: 386 | vertex_indices = unpack_tuple(mobj_tri, 2, 4, int, list) 387 | bm_verts = [verts[vertex_index].bmv for vertex_index in vertex_indices] 388 | # bm_verts.reverse() - use bmesh operator instead 389 | try: 390 | face = bm.faces.new(bm_verts) 391 | except ValueError: # some models contain duplicate faces 392 | continue 393 | face.smooth = True 394 | 395 | for vert in verts: 396 | for loop in vert.bmv.link_loops: 397 | loop[layer_uv].uv = vert.uv 398 | vert.bmv = None 399 | 400 | # flip normals 401 | bmesh.ops.reverse_faces(bm, faces=bm.faces[:], flip_multires=False) 402 | 403 | return label, shader, bm 404 | 405 | #------------------------------------------------------------------------------- 406 | # Write md5mesh 407 | #------------------------------------------------------------------------------- 408 | 409 | def on_active_layer(scene, obj): 410 | layers_scene = scene.layers 411 | layers_obj = obj.layers 412 | 413 | for i in range(20): 414 | if layers_scene[i] and layers_obj[i]: 415 | return True 416 | return False 417 | 418 | def write_md5mesh(filepath, scene, arm_obj): 419 | meshes = [] 420 | 421 | for mesh_obj in filter(is_mesh_object, scene.objects): 422 | if (on_active_layer(scene, mesh_obj) and 423 | has_armature_modifier(mesh_obj, arm_obj)): 424 | meshes.append(Mesh(mesh_obj)) 425 | 426 | bones = arm_obj.data.bones 427 | joints = [Joint() for i in range(len(bones))] 428 | name_to_index = get_name_to_index_dict(arm_obj) 429 | 430 | for bone in bones: 431 | index = name_to_index[bone.name] 432 | joints[index].from_bone(bone, index, name_to_index) 433 | 434 | for mesh in meshes: 435 | mesh.set_weights(joints, name_to_index) 436 | 437 | with open(filepath, "w") as stream: 438 | stream.write("MD5Version 10\n") 439 | stream.write("commandline \"\"\n\n") 440 | 441 | stream.write("numJoints %d\n" % len(joints)) 442 | stream.write("numMeshes %d\n" % len(meshes)) 443 | 444 | stream.write("\njoints {\n") 445 | for joint in joints: 446 | joint.serialize(stream) 447 | stream.write("}\n") 448 | 449 | for mesh in meshes: 450 | mesh.serialize(stream) 451 | mesh.finish() 452 | 453 | #------------------------------------------------------------------------------- 454 | # Test 455 | #------------------------------------------------------------------------------- 456 | 457 | def test(): 458 | import os 459 | 460 | filepath = os.path.expanduser( 461 | "~/Downloads/Games/sauerbraten_2013" 462 | "/sauerbraten/packages/models" 463 | "/snoutx10k/snoutx10k.md5mesh") 464 | 465 | output = os.path.expanduser("~/Dokumente/Blender/Scripts/addons/md5/test.md5mesh") 466 | 467 | layer_source = tuple(i == 0 for i in range(20)) 468 | layer_reimport = tuple(i == 1 for i in range(20)) 469 | 470 | scene = bpy.context.scene 471 | scene.layers = layer_source 472 | 473 | while bpy.data.objects: 474 | obj = bpy.data.objects[0] 475 | scene.objects.unlink(obj) 476 | obj.user_clear() 477 | bpy.data.objects.remove(obj) 478 | 479 | read_md5mesh(filepath) 480 | write_md5mesh(output, scene, bpy.context.active_object) 481 | 482 | scene.layers = layer_reimport 483 | read_md5mesh(output) 484 | --------------------------------------------------------------------------------