| … | … | @@ -456,6 +456,15 @@ def calc_pose_mats(iqmodel, iqpose, bone_axis): |
| 456 | 456 | |
| 457 | 457 | return loc_pose_mat, abs_pose_mat |
| 458 | 458 | |
| 459 | def compute_bind_mats(iqmodel, bone_axis): |
| 460 | # Derive the bind-pose matrices used to convert per-frame poses into bone |
| 461 | # space. Stored on the model so both armature creation and (later) |
| 462 | # animation application can reuse them. |
| 463 | loc_bind_mat, abs_bind_mat = calc_pose_mats(iqmodel, iqmodel.bindpose, bone_axis) |
| 464 | iqmodel.abs_bind_mat = abs_bind_mat |
| 465 | iqmodel.loc_bind_mat = loc_bind_mat |
| 466 | iqmodel.inv_loc_bind_mat = [m.inverted() for m in loc_bind_mat] |
| 467 | |
| 459 | 468 | def make_armature(iqmodel, bone_axis): |
| 460 | 469 | |
| 461 | 470 | if len(iqmodel.bones) == 0: return None |
| … | … | @@ -475,10 +484,8 @@ def make_armature(iqmodel, bone_axis): |
| 475 | 484 | amt.name = iqmodel.name |
| 476 | 485 | obj.name = iqmodel.name + ".amt" |
| 477 | 486 | |
| 478 | | loc_bind_mat, abs_bind_mat = calc_pose_mats(iqmodel, iqmodel.bindpose, bone_axis) |
| 479 | | iqmodel.abs_bind_mat = abs_bind_mat |
| 480 | | iqmodel.loc_bind_mat = loc_bind_mat |
| 481 | | iqmodel.inv_loc_bind_mat = [m.inverted() for m in loc_bind_mat] |
| 487 | compute_bind_mats(iqmodel, bone_axis) |
| 488 | abs_bind_mat = iqmodel.abs_bind_mat |
| 482 | 489 | |
| 483 | 490 | for n in range(len(iqmodel.bones)): |
| 484 | 491 | iqbone = iqmodel.bones[n] |
| … | … | @@ -517,7 +524,9 @@ def make_pose(iqmodel, frame, amtobj, bone_axis, tick): |
| 517 | 524 | loc_pose_mat, _ = calc_pose_mats(iqmodel, frame, bone_axis) |
| 518 | 525 | for n in range(len(iqmodel.bones)): |
| 519 | 526 | name = iqmodel.bones[n].name |
| 520 | | pose_bone = amtobj.pose.bones[name] |
| 527 | pose_bone = amtobj.pose.bones.get(name) |
| 528 | if pose_bone is None: |
| 529 | continue |
| 521 | 530 | pose_bone.matrix_basis = matmul(iqmodel.inv_loc_bind_mat[n], loc_pose_mat[n]) |
| 522 | 531 | pose_bone.keyframe_insert(group=name, frame=tick, data_path='location') |
| 523 | 532 | pose_bone.keyframe_insert(group=name, frame=tick, data_path='rotation_quaternion') |
| … | … | @@ -1073,14 +1082,89 @@ def make_model(iqmodel, bone_axis, dir): |
| 1073 | 1082 | |
| 1074 | 1083 | print("all done.") |
| 1075 | 1084 | |
| 1076 | | def import_iqm(filename, bone_axis='Y'): |
| 1085 | # |
| 1086 | # Animation-only files (no joints, no meshes — e.g. OctaForge per-clip IQMs) |
| 1087 | # carry pose/frame data that indexes the base model's skeleton by order. We |
| 1088 | # can't build them standalone, but we can apply them onto an already-imported |
| 1089 | # armature: reload the base model's skeleton + bind pose, graft the clip's |
| 1090 | # frames on, and reuse the normal action-building path. |
| 1091 | # |
| 1092 | |
| 1093 | base_skeleton_cache = {} |
| 1094 | |
| 1095 | def load_model(filename): |
| 1077 | 1096 | if filename.endswith(".iqm") or filename.endswith(".IQM"): |
| 1078 | | iqmodel = load_iqm(filename) |
| 1097 | return load_iqm(filename) |
| 1098 | return load_iqe(filename) |
| 1099 | |
| 1100 | def is_animation_only(iqmodel): |
| 1101 | return len(iqmodel.bones) == 0 and len(iqmodel.meshes) == 0 and len(iqmodel.anims) > 0 |
| 1102 | |
| 1103 | def find_base_skeleton(anim_filename, nbones): |
| 1104 | # Look for a sibling/parent model file whose skeleton matches the clip's |
| 1105 | # bone count (OctaForge keeps animation/<clip>.iqm next to <model>.iqm). |
| 1106 | anim_dir = os.path.dirname(anim_filename) |
| 1107 | for search_dir in [os.path.dirname(anim_dir), anim_dir]: |
| 1108 | if not search_dir: |
| 1109 | continue |
| 1110 | for cand in sorted(glob.glob(os.path.join(search_dir, "*.iqm")) + |
| 1111 | glob.glob(os.path.join(search_dir, "*.iqe"))): |
| 1112 | if os.path.abspath(cand) == os.path.abspath(anim_filename): |
| 1113 | continue |
| 1114 | key = os.path.abspath(cand) |
| 1115 | if key not in base_skeleton_cache: |
| 1116 | try: |
| 1117 | base_skeleton_cache[key] = load_model(cand) |
| 1118 | except Exception: |
| 1119 | base_skeleton_cache[key] = None |
| 1120 | base = base_skeleton_cache[key] |
| 1121 | if base and len(base.bones) == nbones and nbones > 0: |
| 1122 | print("using base skeleton", cand) |
| 1123 | return base |
| 1124 | return None |
| 1125 | |
| 1126 | def find_target_armature(nbones): |
| 1127 | cands = [o for o in bpy.context.scene.objects if o.type == 'ARMATURE'] |
| 1128 | matching = [o for o in cands if len(o.data.bones) == nbones] |
| 1129 | act = bpy.context.active_object |
| 1130 | if act in matching: return act |
| 1131 | if matching: return matching[0] |
| 1132 | if act in cands: return act |
| 1133 | return cands[0] if cands else None |
| 1134 | |
| 1135 | def apply_animation(iqmodel, filename, bone_axis): |
| 1136 | nbones = len(iqmodel.anims[0].frames[0]) if iqmodel.anims[0].frames else 0 |
| 1137 | amtobj = find_target_armature(nbones) |
| 1138 | if amtobj is None: |
| 1139 | raise Exception("No armature in the scene to apply this animation to. " |
| 1140 | "Import the base model (with the skeleton) first.") |
| 1141 | base = find_base_skeleton(filename, nbones) |
| 1142 | if base is None: |
| 1143 | raise Exception("Could not find the base model (%d-bone skeleton) for this " |
| 1144 | "animation. Keep the base .iqm next to it and import it first." % nbones) |
| 1145 | base.anims = iqmodel.anims |
| 1146 | compute_bind_mats(base, bone_axis) |
| 1147 | print("applying %d animation(s) onto armature '%s'" % (len(base.anims), amtobj.name)) |
| 1148 | make_actions(base, amtobj, bone_axis) |
| 1149 | |
| 1150 | def dispatch_iqmodel(iqmodel, filename, bone_axis): |
| 1151 | if is_animation_only(iqmodel): |
| 1152 | apply_animation(iqmodel, filename, bone_axis) |
| 1079 | 1153 | else: |
| 1080 | | iqmodel = load_iqe(filename) |
| 1081 | | dir = os.path.dirname(filename) |
| 1082 | | make_model(iqmodel, bone_axis, dir) |
| 1083 | | bpy.ops.screen.frame_jump() |
| 1154 | make_model(iqmodel, bone_axis, os.path.dirname(filename)) |
| 1155 | bpy.ops.screen.frame_jump() |
| 1156 | |
| 1157 | def import_iqm(filename, bone_axis='Y'): |
| 1158 | dispatch_iqmodel(load_model(filename), filename, bone_axis) |
| 1159 | |
| 1160 | def import_iqm_files(filenames, bone_axis='Y'): |
| 1161 | # Load everything, then process models (skeleton/mesh) before animation-only |
| 1162 | # clips so the armature exists when the clips are applied. |
| 1163 | loaded = [(f, load_model(f)) for f in filenames] |
| 1164 | models = [(f, m) for f, m in loaded if not is_animation_only(m)] |
| 1165 | anims = [(f, m) for f, m in loaded if is_animation_only(m)] |
| 1166 | for f, m in models + anims: |
| 1167 | dispatch_iqmodel(m, f, bone_axis) |
| 1084 | 1168 | |
| 1085 | 1169 | # |
| 1086 | 1170 | # Register addon |
| … | … | @@ -1093,6 +1177,10 @@ class ImportIQM(bpy.types.Operator, ImportHelper): |
| 1093 | 1177 | filename_ext = ".iqe, .iqm" |
| 1094 | 1178 | filter_glob = StringProperty(default="*.iq[em]", options={'HIDDEN'}) |
| 1095 | 1179 | filepath = StringProperty(name="File Path", maxlen=1024, default="") |
| 1180 | # Allow selecting several files at once (e.g. a model + its animation clips); |
| 1181 | # models are imported first so clips can attach to the new armature. |
| 1182 | files = CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) |
| 1183 | directory = StringProperty(subtype='DIR_PATH') |
| 1096 | 1184 | |
| 1097 | 1185 | bone_axis = EnumProperty(name="Bone Axis", |
| 1098 | 1186 | description="Flip bones to extend along the Y axis", |
| … | … | @@ -1104,11 +1192,15 @@ class ImportIQM(bpy.types.Operator, ImportHelper): |
| 1104 | 1192 | default='Y') |
| 1105 | 1193 | |
| 1106 | 1194 | def execute(self, context): |
| 1107 | | import_iqm(self.properties.filepath, self.bone_axis) |
| 1195 | if self.files and self.directory: |
| 1196 | paths = [os.path.join(self.directory, f.name) for f in self.files if f.name] |
| 1197 | else: |
| 1198 | paths = [self.properties.filepath] |
| 1199 | import_iqm_files(paths, self.bone_axis) |
| 1108 | 1200 | return {'FINISHED'} |
| 1109 | 1201 | |
| 1110 | 1202 | def menu_func(self, context): |
| 1111 | | self.layout.operator(ImportIQM.bl_idname, text="Inter-Quake Model (.iqm, .iqe)") |
| 1203 | self.layout.operator(ImportIQM.bl_idname, text="Inter-Quake Model + Anims (.iqm, .iqe)") |
| 1112 | 1204 | |
| 1113 | 1205 | def register(): |
| 1114 | 1206 | if bpy.app.version >= (2, 80, 0): |