From dce3f4e498791400570f13b0c800449f1ef562f4 Mon Sep 17 00:00:00 2001 From: William Herald Snyder Date: Tue, 18 Jan 2022 15:16:49 -0500 Subject: [PATCH] Skeleton related functions separated into msh_skeleton_utilities + CollisionPrimProperties added for imported primitives that don't have proper names + misc changes to avoid circular imports + minor refactors --- addons/io_scene_swbf_msh/__init__.py | 11 +- addons/io_scene_swbf_msh/msh_anim_gather.py | 77 +++--- .../msh_collision_prim_properties.py | 16 ++ addons/io_scene_swbf_msh/msh_material.py | 2 + .../io_scene_swbf_msh/msh_material_gather.py | 4 +- .../msh_material_properties.py | 3 +- .../msh_material_to_blend.py | 2 +- addons/io_scene_swbf_msh/msh_model.py | 6 +- addons/io_scene_swbf_msh/msh_model_gather.py | 21 +- .../io_scene_swbf_msh/msh_model_utilities.py | 19 +- addons/io_scene_swbf_msh/msh_scene.py | 76 +----- addons/io_scene_swbf_msh/msh_scene_read.py | 24 +- addons/io_scene_swbf_msh/msh_scene_save.py | 4 +- .../io_scene_swbf_msh/msh_scene_utilities.py | 85 ++++++ .../msh_skeleton_properties.py | 13 +- .../msh_skeleton_utilities.py | 211 +++++++++++++++ addons/io_scene_swbf_msh/msh_to_blend.py | 242 ++++-------------- 17 files changed, 469 insertions(+), 347 deletions(-) create mode 100644 addons/io_scene_swbf_msh/msh_collision_prim_properties.py create mode 100644 addons/io_scene_swbf_msh/msh_scene_utilities.py create mode 100644 addons/io_scene_swbf_msh/msh_skeleton_utilities.py diff --git a/addons/io_scene_swbf_msh/__init__.py b/addons/io_scene_swbf_msh/__init__.py index bc29a21..7602390 100644 --- a/addons/io_scene_swbf_msh/__init__.py +++ b/addons/io_scene_swbf_msh/__init__.py @@ -56,11 +56,12 @@ import bpy from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy.props import BoolProperty, EnumProperty, CollectionProperty from bpy.types import Operator -from .msh_scene import create_scene +from .msh_scene_utilities import create_scene from .msh_scene_save import save_scene 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 .zaa_to_blend import * @@ -189,6 +190,8 @@ def menu_func_import(self, context): def register(): + bpy.utils.register_class(CollisionPrimitiveProperties) + bpy.utils.register_class(MaterialProperties) bpy.utils.register_class(MaterialPropertiesPanel) @@ -201,11 +204,15 @@ def register(): bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) - bpy.types.Material.swbf_msh = bpy.props.PointerProperty(type=MaterialProperties) + bpy.types.Object.swbf_msh_coll_prim = bpy.props.PointerProperty(type=CollisionPrimitiveProperties) + bpy.types.Material.swbf_msh_mat = bpy.props.PointerProperty(type=MaterialProperties) bpy.types.Armature.swbf_msh_skel = bpy.props.CollectionProperty(type=SkeletonProperties) + def unregister(): + bpy.utils.unregister_class(CollisionPrimitiveProperties) + bpy.utils.unregister_class(MaterialProperties) bpy.utils.unregister_class(MaterialPropertiesPanel) diff --git a/addons/io_scene_swbf_msh/msh_anim_gather.py b/addons/io_scene_swbf_msh/msh_anim_gather.py index 8a4c613..909340c 100644 --- a/addons/io_scene_swbf_msh/msh_anim_gather.py +++ b/addons/io_scene_swbf_msh/msh_anim_gather.py @@ -1,5 +1,4 @@ -""" Gathers the Blender objects from the current scene and returns them as a list of - Model objects. """ +""" Converts currently active Action to an msh Animation """ import bpy import math @@ -11,30 +10,40 @@ from .msh_model_utilities import * from .msh_utilities import * from .msh_model_gather import * +from .msh_skeleton_utilities import * + from .crc import to_crc +''' +Convert the active Action into an Animation. When exported SWBF anims, there is the issue +that all bones in the anim must be in the skeleton/basepose anim. We guarantee this by +only keying bones if they are in the armature's preserved skeleton (swbf_msh_skel) and +adding dummy frames if the bones are not in the armature. +If a preserved skeleton is not present, we include only the keyed bones and add dummy frames for +the root (root_name) +''' def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: + if not armature.animation_data or not armature.animation_data.action: + raise RuntimeError("Cannot export animation data without an active Action on armature!") + action = armature.animation_data.action + # Set of bones to include in SKL2/animation stuff - keyable_bones : Set[str] = set() - - msh_skel = armature.data.swbf_msh_skel + keyable_bones = get_real_BONES(armature) - has_preserved_skel = len(msh_skel) > 0 + # 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): + keyable_bones.add(root_name) - if has_preserved_skel: - for bone in msh_skel: - #print("Adding {} from preserved skel to exported skeleton".format(bone.name)) - keyable_bones.add(bone.name) - elif action: - for group in action.groups: - #print("Adding {} from action groups to exported skeleton".format(group.name)) - keyable_bones.add(group.name) + # Subset of above bones to key with dummy frames (all bones not in armature) + dummy_bones = set([keyable_bone for keyable_bone in keyable_bones if keyable_bone not in armature.data.bones]) anim = Animation(); @@ -51,41 +60,41 @@ def extract_anim(armature: bpy.types.Armature, root_name: str) -> Animation: anim.end_index = num_frames - 1 - anim.bone_frames[root_crc] = ([], []) - for bone in armature.data.bones: - if bone.name in keyable_bones: - anim.bone_frames[to_crc(bone.name)] = ([], []) + + for keyable_bone in keyable_bones: + anim.bone_frames[to_crc(keyable_bone)] = ([], []) + for frame in range(num_frames): frame_time = framerange.x + frame * increment bpy.context.scene.frame_set(frame_time) + for keyable_bone in keyable_bones: - rframe_dummy = RotationFrame(frame, convert_rotation_space(Quaternion())) - tframe_dummy = TranslationFrame(frame, Vector((0.0,0.0,0.0))) + bone_crc = to_crc(keyable_bone) - anim.bone_frames[root_crc][0].append(tframe_dummy) - anim.bone_frames[root_crc][1].append(rframe_dummy) + if keyable_bone in dummy_bones: + rframe = RotationFrame(frame, convert_rotation_space(Quaternion())) + tframe = TranslationFrame(frame, Vector((0.0,0.0,0.0))) - for bone in armature.pose.bones: + else: - if bone.name not in keyable_bones: - continue + bone = armature.pose.bones[keyable_bone] - transform = bone.matrix + transform = bone.matrix - if bone.parent: - transform = bone.parent.matrix.inverted() @ transform - - loc, rot, _ = transform.decompose() + if bone.parent: + transform = bone.parent.matrix.inverted() @ transform + + loc, rot, _ = transform.decompose() - rframe = RotationFrame(frame, convert_rotation_space(rot)) - tframe = TranslationFrame(frame, convert_vector_space(loc)) + rframe = RotationFrame(frame, convert_rotation_space(rot)) + tframe = TranslationFrame(frame, convert_vector_space(loc)) - anim.bone_frames[to_crc(bone.name)][0].append(tframe) - anim.bone_frames[to_crc(bone.name)][1].append(rframe) + anim.bone_frames[bone_crc][0].append(tframe) + anim.bone_frames[bone_crc][1].append(rframe) return anim diff --git a/addons/io_scene_swbf_msh/msh_collision_prim_properties.py b/addons/io_scene_swbf_msh/msh_collision_prim_properties.py new file mode 100644 index 0000000..f5074cb --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_collision_prim_properties.py @@ -0,0 +1,16 @@ +""" IntProperty needed to keep track of Collision Primitives types that are imported without indicitive names. + Not sure I needed a PropertyGroup/what a leaner method would be. The prims shouldn't be renamed on import because + they are often referenced in ODFs. + + Don't see a reason these should be exposed via a panel or need to be changed...""" + +import bpy +from bpy.props import IntProperty +from bpy.types import PropertyGroup + + +class CollisionPrimitiveProperties(PropertyGroup): + prim_type: IntProperty(name="Primitive Type", default=-1) + + + \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_material.py b/addons/io_scene_swbf_msh/msh_material.py index 6b66700..da55a42 100644 --- a/addons/io_scene_swbf_msh/msh_material.py +++ b/addons/io_scene_swbf_msh/msh_material.py @@ -18,6 +18,8 @@ class Rendertype(Enum): NORMALMAPPED_ENVMAPPED = 26 NORMALMAPPED = 27 NORMALMAPPED_TILED_ENVMAP = 29 + + # Placeholders to avoid crashes/import-export inconsistencies OTHER_1 = 1 OTHER_2 = 2 OTHER_4 = 4 diff --git a/addons/io_scene_swbf_msh/msh_material_gather.py b/addons/io_scene_swbf_msh/msh_material_gather.py index 7f058f6..39ea6d4 100644 --- a/addons/io_scene_swbf_msh/msh_material_gather.py +++ b/addons/io_scene_swbf_msh/msh_material_gather.py @@ -24,10 +24,10 @@ def read_material(blender_material: bpy.types.Material) -> Material: result = Material() - if blender_material.swbf_msh is None: + if blender_material.swbf_msh_mat is None: return result - props = blender_material.swbf_msh + props = blender_material.swbf_msh_mat result.specular_color = props.specular_color.copy() result.rendertype = _read_material_props_rendertype(props) diff --git a/addons/io_scene_swbf_msh/msh_material_properties.py b/addons/io_scene_swbf_msh/msh_material_properties.py index cdca6c8..9a35768 100644 --- a/addons/io_scene_swbf_msh/msh_material_properties.py +++ b/addons/io_scene_swbf_msh/msh_material_properties.py @@ -188,6 +188,7 @@ class MaterialProperties(PropertyGroup): "distort the scene behind them. Should be a normal map " "with '-forceformat v8u8' in it's '.tga.option' file.") + # Below props are for yet unsupported render types data_value_0: IntProperty(name="", description="First data value") data_value_1: IntProperty(name="", description="Second data value") @@ -215,7 +216,7 @@ class MaterialPropertiesPanel(bpy.types.Panel): layout = self.layout - material_props = context.material.swbf_msh + material_props = context.material.swbf_msh_mat layout.prop(material_props, "rendertype") diff --git a/addons/io_scene_swbf_msh/msh_material_to_blend.py b/addons/io_scene_swbf_msh/msh_material_to_blend.py index 3ec0367..398abc5 100644 --- a/addons/io_scene_swbf_msh/msh_material_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_material_to_blend.py @@ -1,4 +1,4 @@ -""" For finding textures and assigning material properties from entries in a Material """ +""" For finding textures and assigning MaterialProperties from entries in a Material """ import bpy from typing import Dict diff --git a/addons/io_scene_swbf_msh/msh_model.py b/addons/io_scene_swbf_msh/msh_model.py index e2d624b..ecf7c47 100644 --- a/addons/io_scene_swbf_msh/msh_model.py +++ b/addons/io_scene_swbf_msh/msh_model.py @@ -12,7 +12,11 @@ class ModelType(Enum): CLOTH = 2 BONE = 3 STATIC = 4 - SHADOWVOLUME = 6 + + # Maybe there are only for BF1 models (http://www.secretsociety.com/forum/downloads/BF1/BF1%20Mesh%20File%20Format.txt)? + # According to that link #3 is envelope, not bone, maybe that's for TCW or smthg + # CHILDSKIN = 5 # I didnt bother with these, never encountered one and they might need adjustments to vertex data + SHADOWVOLUME = 6 # Pretty common class CollisionPrimitiveShape(Enum): SPHERE = 0 diff --git a/addons/io_scene_swbf_msh/msh_model_gather.py b/addons/io_scene_swbf_msh/msh_model_gather.py index 90361d6..86764cc 100644 --- a/addons/io_scene_swbf_msh/msh_model_gather.py +++ b/addons/io_scene_swbf_msh/msh_model_gather.py @@ -9,6 +9,7 @@ from itertools import zip_longest from .msh_model import * from .msh_model_utilities import * from .msh_utilities import * +from .msh_skeleton_utilities import * SKIPPED_OBJECT_TYPES = {"LATTICE", "CAMERA", "LIGHT", "SPEAKER", "LIGHT_PROBE"} MESH_OBJECT_TYPES = {"MESH", "CURVE", "SURFACE", "META", "FONT", "GPENCIL"} @@ -59,7 +60,7 @@ def gather_models(apply_modifiers: bool, export_target: str, skeleton_only: bool else: if obj.parent is not None: if obj.parent.type == "ARMATURE": - model.parent = obj.parent.parent.name + model.parent = obj.parent.parent.name if obj.parent.parent else "" transform = obj.parent.matrix_local @ transform else: model.parent = obj.parent.name @@ -298,6 +299,9 @@ def get_collision_primitive(obj: bpy.types.Object) -> CollisionPrimitive: return primitive + + + def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveShape: """ Gets the CollisionPrimitiveShape of an object or raises an error if it can't. """ @@ -311,8 +315,13 @@ def get_collision_primitive_shape(obj: bpy.types.Object) -> CollisionPrimitiveSh if "box" in name or "cube" in name or "cuboid" in name: return CollisionPrimitiveShape.BOX - return CollisionPrimitiveShape.BOX - #raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") + # arc170 fighter has examples of box colliders without proper naming + prim_type = obj.swbf_msh_coll_prim.prim_type + if prim_type in [item.value for item in CollisionPrimitiveShape]: + return CollisionPrimitiveShape(prim_type) + + raise RuntimeError(f"Object '{obj.name}' has no primitive type specified in it's name!") + def check_for_bad_lod_suffix(obj: bpy.types.Object): """ Checks if the object has an LOD suffix that is known to be ignored by """ @@ -373,7 +382,9 @@ def select_objects(export_target: str) -> List[bpy.types.Object]: def expand_armature(armature: bpy.types.Object) -> List[Model]: - + + proper_BONES = get_real_BONES(armature) + bones: List[Model] = [] for bone in armature.data.bones: @@ -398,7 +409,7 @@ def expand_armature(armature: bpy.types.Object) -> List[Model]: local_translation, local_rotation, _ = transform.decompose() - model.model_type = ModelType.BONE + model.model_type = ModelType.BONE if bone.name in proper_BONES else ModelType.NULL model.name = bone.name model.transform.rotation = convert_rotation_space(local_rotation) model.transform.translation = convert_vector_space(local_translation) diff --git a/addons/io_scene_swbf_msh/msh_model_utilities.py b/addons/io_scene_swbf_msh/msh_model_utilities.py index 1b8d314..fb73a40 100644 --- a/addons/io_scene_swbf_msh/msh_model_utilities.py +++ b/addons/io_scene_swbf_msh/msh_model_utilities.py @@ -26,14 +26,17 @@ def inject_dummy_data(model : Model): model.geometry = [dummy_seg] model.model_type = ModelType.STATIC -def convert_vector_space_(vec: Vector) -> Vector: +def convert_vector_space(vec: Vector) -> Vector: return Vector((-vec.x, vec.z, vec.y)) -def convert_rotation_space_(quat: Quaternion) -> Quaternion: +def convert_scale_space(vec: Vector) -> Vector: + return Vector(vec.xzy) + +def convert_rotation_space(quat: Quaternion) -> Quaternion: return Quaternion((-quat.w, quat.x, -quat.z, -quat.y)) def model_transform_to_matrix(transform: ModelTransform): - return Matrix.Translation(convert_vector_space_(transform.translation)) @ convert_rotation_space_(transform.rotation).to_matrix().to_4x4() + return Matrix.Translation(convert_vector_space(transform.translation)) @ convert_rotation_space(transform.rotation).to_matrix().to_4x4() def scale_segments(scale: Vector, segments: List[GeometrySegment]): """ Scales are positions in the GeometrySegment list. """ @@ -145,13 +148,3 @@ def is_model_name_unused(name: str, models: List[Model]) -> bool: return True - -def convert_vector_space(vec: Vector) -> Vector: - return Vector((-vec.x, vec.z, vec.y)) - -def convert_scale_space(vec: Vector) -> Vector: - return Vector(vec.xzy) - -def convert_rotation_space(quat: Quaternion) -> Quaternion: - return Quaternion((-quat.w, quat.x, -quat.z, -quat.y)) - diff --git a/addons/io_scene_swbf_msh/msh_scene.py b/addons/io_scene_swbf_msh/msh_scene.py index 755a365..c803dda 100644 --- a/addons/io_scene_swbf_msh/msh_scene.py +++ b/addons/io_scene_swbf_msh/msh_scene.py @@ -4,17 +4,14 @@ from dataclasses import dataclass, field from typing import List, Dict from copy import copy + import bpy from mathutils import Vector + from .msh_model import Model, Animation, ModelType -from .msh_model_gather import gather_models -from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data -from .msh_model_triangle_strips import create_models_triangle_strips from .msh_material import * -from .msh_material_gather import gather_materials -from .msh_material_utilities import remove_unused_materials from .msh_utilities import * -from .msh_anim_gather import extract_anim + @dataclass class SceneAABB: @@ -47,69 +44,4 @@ class Scene: animation: Animation = None - skeleton: List[int] = field(default_factory=list) - -def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene: - """ Create a msh Scene from the active Blender scene. """ - - scene = Scene() - - scene.name = bpy.context.scene.name - - scene.materials = gather_materials() - - scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only) - scene.models = sort_by_parent(scene.models) - - if generate_triangle_strips: - scene.models = create_models_triangle_strips(scene.models) - else: - for model in scene.models: - if model.geometry: - for segment in model.geometry: - segment.triangle_strips = segment.triangles - - if has_multiple_root_models(scene.models): - scene.models = reparent_model_roots(scene.models) - - scene.materials = remove_unused_materials(scene.materials, scene.models) - - - root = scene.models[0] - - if export_anim: - if armature_obj is not None: - scene.animation = extract_anim(armature_obj, root.name) - else: - raise Exception("Export Error: Could not find an armature object from which to export an animation!") - - if skel_only and root.model_type == ModelType.NULL: - # For ZenAsset - inject_dummy_data(root) - - return scene - - -def create_scene_aabb(scene: Scene) -> SceneAABB: - """ Create a SceneAABB for a Scene. """ - - global_aabb = SceneAABB() - - for model in scene.models: - if model.geometry is None or model.hidden: - continue - - model_world_matrix = get_model_world_matrix(model, scene.models) - model_aabb = SceneAABB() - - for segment in model.geometry: - segment_aabb = SceneAABB() - - for pos in segment.positions: - segment_aabb.integrate_position(model_world_matrix @ pos) - - model_aabb.integrate_aabb(segment_aabb) - - global_aabb.integrate_aabb(model_aabb) - - return global_aabb + skeleton: List[int] = field(default_factory=list) \ No newline at end of file diff --git a/addons/io_scene_swbf_msh/msh_scene_read.py b/addons/io_scene_swbf_msh/msh_scene_read.py index f05267d..c946263 100644 --- a/addons/io_scene_swbf_msh/msh_scene_read.py +++ b/addons/io_scene_swbf_msh/msh_scene_read.py @@ -117,20 +117,10 @@ def read_scene(input_file, anim_only=False, debug=0) -> Scene: if seg.weights: for weight_set in seg.weights: for vweight in weight_set: - if vweight.bone in mndx_remap: vweight.bone = mndx_remap[vweight.bone] else: vweight.bone = 0 - - # So in the new republic boba example, the weights aimed for bone_head instead map to sv_jettrooper... - - - #for key, val in mndx_remap.items(): - #if scene.models[val].name == "bone_head" or scene.models[val].name == "sv_jettrooper": - #print("Key: {} is mapped to val: {}".format(key, val)) - #print("Key: {}, val {} is model: {}".format(key, val, scene.models[val].name)) - return scene @@ -276,10 +266,6 @@ def _read_modl(modl: Reader, materials_list: List[Material]) -> Model: else: modl.skip_bytes(1) - global debug_level - if debug_level > 0: - print(modl.indent + "Read model " + model.name + " of type: " + str(model.model_type)[10:]) - return model @@ -292,10 +278,6 @@ def _read_tran(tran: Reader) -> ModelTransform: xform.rotation = tran.read_quat() xform.translation = tran.read_vec() - global debug_level - if debug_level > 0: - print(tran.indent + "Rot: {} Loc: {}".format(str(xform.rotation), str(xform.translation))) - return xform @@ -341,6 +323,7 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: for _ in range(num_texcoords): geometry_seg.texcoords.append(Vector(uv0l.read_f32(2))) + # TODO: Can't remember exact issue here... elif next_header == "NDXL": with segm.read_child() as ndxl: @@ -399,7 +382,8 @@ def _read_segm(segm: Reader, materials_list: List[Material]) -> GeometrySegment: geometry_seg.triangle_strips = strips - #if segm.read_u16 != 0: #trailing 0 bug https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP + # TODO: Dont know how to handle trailing 0 bug yet: https://schlechtwetterfront.github.io/ze_filetypes/msh.html#STRP + #if segm.read_u16 != 0: # segm.skip_bytes(-2) elif next_header == "WGHT": @@ -439,6 +423,8 @@ def _read_anm2(anm2: Reader) -> Animation: # Dont even know what CYCL's data does. Tried playing # with the values but didn't change anything in zenasset or ingame... + # Besides num_anims, which is never > 1 for any SWBF1/2 mshs I've seen + ''' num_anims = cycl.read_u32() diff --git a/addons/io_scene_swbf_msh/msh_scene_save.py b/addons/io_scene_swbf_msh/msh_scene_save.py index 098aa89..a2930df 100644 --- a/addons/io_scene_swbf_msh/msh_scene_save.py +++ b/addons/io_scene_swbf_msh/msh_scene_save.py @@ -2,7 +2,8 @@ from itertools import islice from typing import Dict -from .msh_scene import Scene, create_scene_aabb +from .msh_scene import Scene +from .msh_scene_utilities import create_scene_aabb from .msh_model import * from .msh_material import * from .msh_writer import Writer @@ -40,6 +41,7 @@ def save_scene(output_file, scene: Scene): with hedr.create_child("SKL2") as skl2: _write_skl2(skl2, scene.animation) + # Def not necessary, including anyways with hedr.create_child("BLN2") as bln2: _write_bln2(bln2, scene.animation) diff --git a/addons/io_scene_swbf_msh/msh_scene_utilities.py b/addons/io_scene_swbf_msh/msh_scene_utilities.py new file mode 100644 index 0000000..3927824 --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_scene_utilities.py @@ -0,0 +1,85 @@ +""" Contains Scene object for representing a .msh file and the function to create one + from a Blender scene. """ + +from dataclasses import dataclass, field +from typing import List, Dict +from copy import copy +import bpy +from mathutils import Vector +from .msh_model import Model, Animation, ModelType +from .msh_scene import Scene, SceneAABB +from .msh_model_gather import gather_models +from .msh_model_utilities import sort_by_parent, has_multiple_root_models, reparent_model_roots, get_model_world_matrix, inject_dummy_data +from .msh_model_triangle_strips import create_models_triangle_strips +from .msh_material import * +from .msh_material_gather import gather_materials +from .msh_material_utilities import remove_unused_materials +from .msh_utilities import * +from .msh_anim_gather import extract_anim + + + +def create_scene(generate_triangle_strips: bool, apply_modifiers: bool, export_target: str, skel_only: bool, export_anim: bool) -> Scene: + """ Create a msh Scene from the active Blender scene. """ + + scene = Scene() + + scene.name = bpy.context.scene.name + + scene.materials = gather_materials() + + scene.models, armature_obj = gather_models(apply_modifiers=apply_modifiers, export_target=export_target, skeleton_only=skel_only) + scene.models = sort_by_parent(scene.models) + + if generate_triangle_strips: + scene.models = create_models_triangle_strips(scene.models) + else: + for model in scene.models: + if model.geometry: + for segment in model.geometry: + segment.triangle_strips = segment.triangles + + if has_multiple_root_models(scene.models): + scene.models = reparent_model_roots(scene.models) + + scene.materials = remove_unused_materials(scene.materials, scene.models) + + + root = scene.models[0] + + if export_anim: + if armature_obj is not None: + scene.animation = extract_anim(armature_obj, root.name) + else: + raise Exception("Export Error: Could not find an armature object from which to export an animation!") + + if skel_only and root.model_type == ModelType.NULL: + # For ZenAsset + inject_dummy_data(root) + + return scene + + +def create_scene_aabb(scene: Scene) -> SceneAABB: + """ Create a SceneAABB for a Scene. """ + + global_aabb = SceneAABB() + + for model in scene.models: + if model.geometry is None or model.hidden: + continue + + model_world_matrix = get_model_world_matrix(model, scene.models) + model_aabb = SceneAABB() + + for segment in model.geometry: + segment_aabb = SceneAABB() + + for pos in segment.positions: + segment_aabb.integrate_position(model_world_matrix @ pos) + + model_aabb.integrate_aabb(segment_aabb) + + global_aabb.integrate_aabb(model_aabb) + + return global_aabb diff --git a/addons/io_scene_swbf_msh/msh_skeleton_properties.py b/addons/io_scene_swbf_msh/msh_skeleton_properties.py index 139e5b0..1bf5530 100644 --- a/addons/io_scene_swbf_msh/msh_skeleton_properties.py +++ b/addons/io_scene_swbf_msh/msh_skeleton_properties.py @@ -1,10 +1,15 @@ -""" Contains Blender properties and UI for .msh materials. """ +""" Keeps track of exact skeleton when imported. Possibly needed for exporting skeleton-compatible animations. Will + probably be needed (with a matrix property) if we: + - add tip-to-tail adjustment and/or omit roots/effectors for imported skeletons to keep track of the original bone transforms + - add some sort of basepose-adjustment animation import option for already imported skeletons + + I guess this might not need a panel, but I included it because the docs might need to reference it and + people may want to exclude certain bones without deleting keyframes. +""" import bpy -from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatVectorProperty, IntProperty +from bpy.props import StringProperty from bpy.types import PropertyGroup -from .msh_material_ui_strings import * -from .msh_model import * class SkeletonProperties(PropertyGroup): diff --git a/addons/io_scene_swbf_msh/msh_skeleton_utilities.py b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py new file mode 100644 index 0000000..7e260ca --- /dev/null +++ b/addons/io_scene_swbf_msh/msh_skeleton_utilities.py @@ -0,0 +1,211 @@ +""" Helpers for SWBF skeleton-armature mapping """ + +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 * + + +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]: + + # First priority, add the names of the skeleton preserved on import + skel_props = armature.data.swbf_msh_skel + + # Second, add all keyed bones + action = armature.animation_data.action if armature.animation_data else None + + # Third, just add all bones in armature + + # Set of bones to include + real_bones : Set[str] = set() + + 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: + for group in armature.animation_data.action.groups: + #print(f"{group.name} is a real BONE") + real_bones.add(group.name) + else: + 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) + 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] + + 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_to_blend.py b/addons/io_scene_swbf_msh/msh_to_blend.py index bde5682..6814fb7 100644 --- a/addons/io_scene_swbf_msh/msh_to_blend.py +++ b/addons/io_scene_swbf_msh/msh_to_blend.py @@ -4,20 +4,23 @@ import bpy import bmesh import math + from enum import Enum from typing import List, Set, Dict, Tuple -from itertools import zip_longest + from .msh_scene import Scene from .msh_material_to_blend import * from .msh_model import * -from .msh_model_utilities import * -from .msh_utilities import * -from .msh_model_gather import * -from .msh_skeleton_properties import * +from .msh_skeleton_utilities import * +from .msh_model_gather import get_is_model_hidden + + from .crc import * import os + + # Extracts and applies anims in the scene to the currently selected armature def extract_and_apply_anim(filename : str, scene : Scene): @@ -100,170 +103,7 @@ def extract_and_apply_anim(filename : str, scene : Scene): - -''' -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) - 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] - - 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 - - It is weighted to - - It has a parent and child that must be in the armature -''' -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 to_crc(model.name) in scene.skeleton: - # print("Skel model {} of type {} has parent {}".format(model.name, model.model_type, model.parent)) - - 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 - #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 - - - - - -# Create the msh hierachy. Armatures are not created here. +# Create the msh hierachy. Armatures are not created here. Much of this could use some optimization... 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 @@ -274,27 +114,32 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) for model in sorted_models: new_obj = None - if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN: + + if model.model_type == ModelType.STATIC or model.model_type == ModelType.SKIN or model.model_type == ModelType.SHADOWVOLUME: new_mesh = bpy.data.meshes.new(model.name) verts = [] faces = [] offset = 0 - mat_name = "" - full_texcoords = [] weights_offsets = {} + face_range_to_material_index = [] + if model.geometry: + + #if model.collisionprimitive is None: + # print(f"On model: {model.name}") + for i,seg in enumerate(model.geometry): - if i == 0: - mat_name = seg.material_name - verts += [tuple(convert_vector_space(v)) for v in seg.positions] + #if model.collisionprimitive is None: + # print("Importing segment with material: {} with and {} verts".format(seg.material_name, len(seg.positions))) + if seg.weights: weights_offsets[offset] = seg.weights @@ -303,6 +148,8 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) else: full_texcoords += [(0.0,0.0) for _ in range(len(seg.positions))] + face_range_lower = len(faces) + if seg.triangles: faces += [tuple([ind + offset for ind in tri]) for tri in seg.triangles] else: @@ -311,13 +158,17 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) face = tuple([offset + strip[j] for j in range(i,i+3)]) faces.append(face) + face_range_upper = len(faces) + face_range_to_material_index.append((face_range_lower, face_range_upper, i)) + offset += len(seg.positions) new_mesh.from_pydata(verts, [], faces) new_mesh.update() new_mesh.validate() - + + # If tex coords are present, add material and UV data if full_texcoords: edit_mesh = bmesh.new() @@ -326,7 +177,12 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) uvlayer = edit_mesh.loops.layers.uv.verify() for edit_mesh_face in edit_mesh.faces: - mesh_face = faces[edit_mesh_face.index] + face_index = edit_mesh_face.index + mesh_face = faces[face_index] + + for frL, frU, ind in face_range_to_material_index: + if face_index >= frL and face_index < frU: + edit_mesh_face.material_index = ind for i,loop in enumerate(edit_mesh_face.loops): @@ -334,7 +190,8 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) loop[uvlayer].uv = tuple([texcoord.x, texcoord.y]) edit_mesh.to_mesh(new_mesh) - edit_mesh.free() + edit_mesh.free() + new_obj = bpy.data.objects.new(new_mesh.name, new_mesh) @@ -348,21 +205,19 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) if index not in vertex_groups_indicies: model_name = scene.models[index].name - #print("Adding new vertex group with index {} and model name {}".format(index, model_name)) vertex_groups_indicies[index] = new_obj.vertex_groups.new(name=model_name) vertex_groups_indicies[index].add([offset + i], weight.weight, 'ADD') - ''' - Assign Materials - will do per segment later... - ''' - if mat_name: - material = materials_map[mat_name] - if new_obj.data.materials: - new_obj.data.materials[0] = material - else: - new_obj.data.materials.append(material) + ''' + Assign Material slots + ''' + if model.geometry: + for seg in model.geometry: + if seg.material_name: + material = materials_map[seg.material_name] + new_obj.data.materials.append(material) else: @@ -380,6 +235,9 @@ def extract_models(scene: Scene, materials_map : Dict[str, bpy.types.Material]) new_obj.rotation_mode = "QUATERNION" new_obj.rotation_quaternion = convert_rotation_space(model.transform.rotation) + if model.collisionprimitive is not None: + new_obj.swbf_msh_coll_prim.prim_type = model.collisionprimitive.shape.value + bpy.context.collection.objects.link(new_obj) @@ -404,7 +262,7 @@ def extract_materials(folder_path: str, scene: Scene) -> Dict[str, bpy.types.Mat texImage.image = bpy.data.images.load(diffuse_texture_path) new_mat.node_tree.links.new(bsdf.inputs['Base Color'], texImage.outputs['Color']) - fill_material_props(material, new_mat.swbf_msh) + fill_material_props(material, new_mat.swbf_msh_mat) extracted_materials[material_name] = new_mat @@ -430,10 +288,10 @@ def extract_scene(filepath: str, scene: Scene): armature = None if not skel else required_skeleton_to_armature(skel, model_map, scene) if armature is not None: - preserved = armature.data.swbf_msh_skel + preserved_skel = armature.data.swbf_msh_skel for model in scene.models: - if to_crc(model.name) in scene.skeleton: - entry = preserved.add() + if to_crc(model.name) in scene.skeleton or model.model_type == ModelType.BONE: + entry = preserved_skel.add() entry.name = model.name