From aa8a61175dc70cb5f1746afb804389af5abe0544 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Fri, 30 Sep 2022 07:04:49 -0400 Subject: [PATCH] Skeleton import code moved to separate file + other renaming --- addons/io_scene_swbf_msh/__init__.py | 2 +- addons/io_scene_swbf_msh/msh_anim_gather.py | 2 +- ...{msh_to_blend.py => msh_scene_to_blend.py} | 5 +- .../msh_skeleton_to_blend.py | 178 +++++++++++++++++ .../msh_skeleton_utilities.py | 183 +----------------- 5 files changed, 186 insertions(+), 184 deletions(-) rename addons/io_scene_swbf_msh/{msh_to_blend.py => msh_scene_to_blend.py} (98%) create mode 100644 addons/io_scene_swbf_msh/msh_skeleton_to_blend.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index 3ba0636..a62b65c 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -62,8 +62,8 @@ from .msh_scene_read import read_scene from .msh_material_properties import * from .msh_skeleton_properties import * from .msh_collision_prim_properties import * -from .msh_to_blend import * from .msh_material_operators import * +from .msh_scene_to_blend import * from .zaa_to_blend import * diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 909340c..f10c514 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -39,7 +39,7 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: # If it doesn't have a preserved skeleton, then we add the scene root. # If it does have a preserved skeleton, any objects not animatable by blender (i.e. objects above the skeleton, scene root) # will be included in the preserved skeleton - if not has_preserved_skeleton(armature): + if len(armature.data.swbf_msh_skel): keyable_bones.add(root_name) # Subset of above bones to key with dummy frames (all bones not in armature) diff --git a/addons/io_scene_swbf_msh/msh_to_blend.py b/addons/io_scene_swbf_msh/msh_scene_to_blend.py similarity index 98% rename from addons/io_scene_swbf_msh/msh_to_blend.py rename to addons/io_scene_swbf_msh/msh_scene_to_blend.py index 24ae749..efbfb62 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_scene_to_blend.py @@ -12,6 +12,7 @@ from .msh_scene import Scene from .msh_material_to_blend import * from .msh_model import * from .msh_skeleton_utilities import * +from .msh_skeleton_to_blend import * from .msh_model_gather import get_is_model_hidden from .msh_mesh_to_blend import model_to_mesh_object @@ -108,9 +109,7 @@ def extract_and_apply_anim(filename : str, scene : Scene): -# Create the msh hierachy. Armatures are not created here. Much of this could use some optimization... - -# TODO: Replace with an approach informed by existing Blender addons (io_scene_obj e.g.) +# Create the msh hierachy. Armatures are not created here. def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) -> Dict[str, bpy.types.Object]: # This will be filled with model names -> Blender objects and returned diff --git a/addons/io_scene_swbf_msh/msh_skeleton_to_blend.py b/addons/io_scene_swbf_msh/msh_skeleton_to_blend.py new file mode 100644 index 0000000..04b1faa --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_skeleton_to_blend.py @@ -0,0 +1,178 @@ +""" SWBF skeleton-armature mapping functions. By skeleton, we simply +mean models that will end up in an armature. Literal SWBF skeletons (zafbins) +are not relevant as of now. """ + +import bpy +import math + +from typing import List, Set, Dict, Tuple + +from .msh_scene import Scene +from .msh_model import * +from .msh_model_utilities import * + +from .crc import * + + +''' +Creates armature from the required nodes. +Assumes the required_skeleton is already sorted by parent. + +Uses model_map to get the world matrix of each bone (hacky, see NOTE) +''' +def required_skeleton_to_armature(required_skeleton : List[Model], model_map : Dict[str, bpy.types.Object], msh_scene : Scene) -> bpy.types.Object: + + armature = bpy.data.armatures.new("skeleton") + armature_obj = bpy.data.objects.new("skeleton", armature) + armature_obj.matrix_world = Matrix.Identity(4) + bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj) + + + bones_set = set([model.name for model in required_skeleton]) + + armature_obj.select_set(True) + bpy.context.view_layer.objects.active = armature_obj + bpy.ops.object.mode_set(mode='EDIT') + + for bone in required_skeleton: + + edit_bone = armature.edit_bones.new(bone.name) + + if bone.parent and bone.parent in bones_set: + edit_bone.parent = armature.edit_bones[bone.parent] + + ''' + NOTE: I recall there being some rare issue with the get_world_matrix utility func. + Never bothered to figure it out and referencing the bone object's world mat always works. + Bone objects will be deleted later. + ''' + bone_obj = model_map[bone.name] + + # TODO: This will lead to mistranslated bones when armature is reparented! + edit_bone.matrix = bone_obj.matrix_world + edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0)) + + bone_children = [b for b in get_model_children(bone, required_skeleton)] + + ''' + Perhaps we'll add an option for importing bones tip-to-tail, but that would + require preserving their original transforms as changing the tail position + changes the bones' transform... + ''' + tail_pos = Vector() + if bone_children: + for bone_child in bone_children: + tail_pos += bone_obj.matrix_world.translation + tail_pos = tail_pos / len(bone_children) + edit_bone.length = .5 #(tail_pos - edit_bone.head).magnitude + else: + bone_length = .5# edit_bone.parent.length if edit_bone.parent is not None else .5 + edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,bone_length,0.0)) + + bpy.ops.object.mode_set(mode='OBJECT') + armature_obj.select_set(True) + bpy.context.view_layer.update() + + return armature_obj + + + + +''' +Ok, so this method is crucial. What this does is: + 1) Find all nodes that are weighted to by skinned segments. + 2) A node must be included in the armature if it: + - is in SKL2 and is not the scene root + - has model_type == BONE + - is weighted to + - has a parent and child that must be in the armature + +This may need a lot of adjustments, don't think I can prove it's validity but it has worked very well +and handles all stock + ZETools + Pandemic XSI exporter models I've tested +''' +def extract_required_skeleton(scene: Scene) -> List[Model]: + + # Will map Model names to Models in scene, for convenience + model_dict : Dict[str, Model] = {} + + ''' + Will contain hashes of all models that definitely need to be in the skeleton/armature. + We initialize it with the contents of SKL2 i.e. the nodes that are animated. + For now this includes the scene root, but that'll be excluded later. + ''' + skeleton_hashes = set(scene.skeleton) + + ''' + We also need to add all nodes that are weighted to. These are not necessarily in + SKL2, as SKL2 seems to only reference nodes that are keyframed. + However, sometimes SKL2 is not included when it should be, but it can be mostly recovered + by checking which models are BONEs. + ''' + for model in scene.models: + model_dict[model.name] = model + + if model.model_type == ModelType.BONE: + skeleton_hashes.add(to_crc(model.name)) + + elif model.geometry: + for seg in model.geometry: + if seg.weights: + for weight_set in seg.weights: + for weight in weight_set: + model_weighted_to = scene.models[weight.bone] + + if to_crc(model_weighted_to.name) not in skeleton_hashes: + skeleton_hashes.add(to_crc(model_weighted_to.name)) + + # The result of this function (to be sorted by parent) + required_skeleton_models = [] + + # Set of nodes to be included in required skeleton/were visited + visited_nodes = set() + + ''' + Here we add all skeleton nodes (except root) and any necessary ancestors to the armature. + - e.g. in bone_x/eff_x/eff_y, the effectors do not have to be in armature, as they are not ancestors of a bone + - but in bone_x/eff_x/eff_y/bone_y, they do. + ''' + for bone in sort_by_parent(scene.models): + + # make sure we exclude the scene root and any nodes irrelevant to the armature + if not bone.parent or to_crc(bone.name) not in skeleton_hashes: + continue + + potential_bones = [bone] + visited_nodes.add(bone.name) + + # Stacked transform will be needed if we decide to include an option for excluding effectors/roots or + # connecting bones tip-to-tail + #stacked_transform = model_transform_to_matrix(bone.transform) + + curr_ancestor = model_dict[bone.parent] + + while True: + + # If we hit a non-skin scene root, that means we just add the bone we started with, no ancestors. + if not curr_ancestor.parent and curr_ancestor.model_type != ModelType.SKIN: + required_skeleton_models.append(bone) + visited_nodes.add(bone.name) + break + + # If we encounter another bone, a skin, or a previously visited object, we need to add the bone and its + # ancestors. + elif to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.model_type == ModelType.SKIN or curr_ancestor.name in visited_nodes: + for potential_bone in potential_bones: + required_skeleton_models.append(potential_bone) + visited_nodes.add(potential_bone.name) + break + + # Add ancestor to potential bones, update next ancestor + else: + if curr_ancestor.name not in visited_nodes: + potential_bones.insert(0, curr_ancestor) + curr_ancestor = model_dict[curr_ancestor.parent] + + #stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform + + return required_skeleton_models + diff --git a/addons/io_scene_swbf_msh/msh_skeleton_utilities.py b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py index 15b63e6..13fe193 100644 --- a/addons/io_scene_swbf_msh/msh_skeleton_utilities.py +++ b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py @@ -1,4 +1,4 @@ -""" Helpers for SWBF skeleton-armature mapping """ +""" Armature -> SWBF skeleton mapping functions """ import bpy import math @@ -19,12 +19,6 @@ def get_bone_world_matrix(armature: bpy.types.Object, bone_name: str) -> Matrix: return None - -def has_preserved_skeleton(armature : bpy.types.Armature): - return len(armature.data.swbf_msh_skel) > 0 - - - '''Returns all bones that should be marked as BONE''' def get_real_BONES(armature: bpy.types.Armature) -> Set[str]: @@ -41,182 +35,13 @@ def get_real_BONES(armature: bpy.types.Armature) -> Set[str]: if len(skel_props) > 0: for bone in skel_props: - #print(f"{bone.name} is a real BONE") real_bones.add(bone.name) - elif action: + if action: for group in armature.animation_data.action.groups: - #print(f"{group.name} is a real BONE") real_bones.add(group.name) - else: + + if len(skel_props) == 0 and action is None: for bone in armature.data.bones: - #print(f"{bone.name} is a real BONE") real_bones.add(bone.name) return real_bones - - - - - -''' -Creates armature from the required nodes. -Assumes the required_skeleton is already sorted by parent. - -Uses model_map to get the world matrix of each bone (hacky, see NOTE) -''' -def required_skeleton_to_armature(required_skeleton : List[Model], model_map : Dict[str, bpy.types.Object], msh_scene : Scene) -> bpy.types.Object: - - armature = bpy.data.armatures.new("skeleton") - armature_obj = bpy.data.objects.new("skeleton", armature) - armature_obj.matrix_world = Matrix.Identity(4) - bpy.context.view_layer.active_layer_collection.collection.objects.link(armature_obj) - - - bones_set = set([model.name for model in required_skeleton]) - - armature_obj.select_set(True) - bpy.context.view_layer.objects.active = armature_obj - bpy.ops.object.mode_set(mode='EDIT') - - for bone in required_skeleton: - - edit_bone = armature.edit_bones.new(bone.name) - - if bone.parent and bone.parent in bones_set: - edit_bone.parent = armature.edit_bones[bone.parent] - - ''' - NOTE: I recall there being some rare issue with the get_world_matrix utility func. - Never bothered to figure it out and referencing the bone object's world mat always works. - Bone objects will be deleted later. - ''' - bone_obj = model_map[bone.name] - - # TODO: This will lead to mistranslated bones when armature is reparented! - edit_bone.matrix = bone_obj.matrix_world - edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,1.0,0.0)) - - bone_children = [b for b in get_model_children(bone, required_skeleton)] - - ''' - Perhaps we'll add an option for importing bones tip-to-tail, but that would - require preserving their original transforms as changing the tail position - changes the bones' transform... - ''' - tail_pos = Vector() - if bone_children: - for bone_child in bone_children: - tail_pos += bone_obj.matrix_world.translation - tail_pos = tail_pos / len(bone_children) - edit_bone.length = .5 #(tail_pos - edit_bone.head).magnitude - else: - bone_length = .5# edit_bone.parent.length if edit_bone.parent is not None else .5 - edit_bone.tail = bone_obj.matrix_world @ Vector((0.0,bone_length,0.0)) - - bpy.ops.object.mode_set(mode='OBJECT') - armature_obj.select_set(True) - bpy.context.view_layer.update() - - return armature_obj - - - - -''' -Ok, so this method is crucial. What this does is: - 1) Find all nodes that are weighted to by skinned segments. - 2) A node must be included in the armature if it: - - is in SKL2 and is not the scene root - - has model_type == BONE - - is weighted to - - has a parent and child that must be in the armature - -This may need a lot of adjustments, don't think I can prove it's validity but it has worked very well -and handles all stock + ZETools + Pandemic XSI exporter models I've tested -''' -def extract_required_skeleton(scene: Scene) -> List[Model]: - - # Will map Model names to Models in scene, for convenience - model_dict : Dict[str, Model] = {} - - ''' - Will contain hashes of all models that definitely need to be in the skeleton/armature. - We initialize it with the contents of SKL2 i.e. the nodes that are animated. - For now this includes the scene root, but that'll be excluded later. - ''' - skeleton_hashes = set(scene.skeleton) - - ''' - We also need to add all nodes that are weighted to. These are not necessarily in - SKL2, as SKL2 seems to only reference nodes that are keyframed. - However, sometimes SKL2 is not included when it should be, but it can be mostly recovered - by checking which models are BONEs. - ''' - for model in scene.models: - model_dict[model.name] = model - - if model.model_type == ModelType.BONE: - skeleton_hashes.add(to_crc(model.name)) - - elif model.geometry: - for seg in model.geometry: - if seg.weights: - for weight_set in seg.weights: - for weight in weight_set: - model_weighted_to = scene.models[weight.bone] - - if to_crc(model_weighted_to.name) not in skeleton_hashes: - skeleton_hashes.add(to_crc(model_weighted_to.name)) - - # The result of this function (to be sorted by parent) - required_skeleton_models = [] - - # Set of nodes to be included in required skeleton/were visited - visited_nodes = set() - - ''' - Here we add all skeleton nodes (except root) and any necessary ancestors to the armature. - - e.g. in bone_x/eff_x/eff_y, the effectors do not have to be in armature, as they are not ancestors of a bone - - but in bone_x/eff_x/eff_y/bone_y, they do. - ''' - for bone in sort_by_parent(scene.models): - - # make sure we exclude the scene root and any nodes irrelevant to the armature - if not bone.parent or to_crc(bone.name) not in skeleton_hashes: - continue - - potential_bones = [bone] - visited_nodes.add(bone.name) - - # Stacked transform will be needed if we decide to include an option for excluding effectors/roots or - # connecting bones tip-to-tail - #stacked_transform = model_transform_to_matrix(bone.transform) - - curr_ancestor = model_dict[bone.parent] - - while True: - - # If we hit a non-skin scene root, that means we just add the bone we started with, no ancestors. - if not curr_ancestor.parent and curr_ancestor.model_type != ModelType.SKIN: - required_skeleton_models.append(bone) - visited_nodes.add(bone.name) - break - - # If we encounter another bone, a skin, or a previously visited object, we need to add the bone and its - # ancestors. - elif to_crc(curr_ancestor.name) in scene.skeleton or curr_ancestor.model_type == ModelType.SKIN or curr_ancestor.name in visited_nodes: - for potential_bone in potential_bones: - required_skeleton_models.append(potential_bone) - visited_nodes.add(potential_bone.name) - break - - # Add ancestor to potential bones, update next ancestor - else: - if curr_ancestor.name not in visited_nodes: - potential_bones.insert(0, curr_ancestor) - curr_ancestor = model_dict[curr_ancestor.parent] - - #stacked_transform = model_transform_to_matrix(curr_ancestor.transform) @ stacked_transform - - return required_skeleton_models -