From 85651f9ebe8c3271529d5d92168fd563ae5b4142 Mon Sep 17 00:00:00 2001 From: HENDRIX-ZT2 Date: Wed, 20 Dec 2023 12:30:07 +0100 Subject: [PATCH] plugin - import mdl2s as collections, not scenes --- generated/formats/ms2/compounds/PcMeshData.py | 3 + plugin/export_banis.py | 2 +- plugin/export_manis.py | 2 +- plugin/export_ms2.py | 39 +- plugin/import_banis.py | 2 +- plugin/import_manis.py | 2 +- plugin/import_ms2.py | 75 +- plugin/modules_export/armature.py | 35 +- plugin/modules_export/collision.py | 4 +- plugin/modules_export/geometry.py | 4 +- plugin/modules_import/armature.py | 22 +- plugin/modules_import/collision.py | 46 +- plugin/modules_import/material.py | 1 + plugin/utils/matrix_util.py | 5 +- plugin/utils/object.py | 44 +- plugin/utils/shell.py | 924 +++++++++--------- source/formats/ms2/compounds/PcMeshData.py | 3 + version.txt | 2 +- 18 files changed, 602 insertions(+), 613 deletions(-) diff --git a/generated/formats/ms2/compounds/PcMeshData.py b/generated/formats/ms2/compounds/PcMeshData.py index 6fc89921a..489c92bf1 100644 --- a/generated/formats/ms2/compounds/PcMeshData.py +++ b/generated/formats/ms2/compounds/PcMeshData.py @@ -144,6 +144,9 @@ def write_pc_array(self, arr): self.buffer_info.verts.write(padding) offset = self.buffer_info.verts.tell() self.buffer_info.verts.write(arr.tobytes()) + # to be safe, also write padding at the end + padding = get_padding(self.buffer_info.verts.tell(), alignment=16) + self.buffer_info.verts.write(padding) return offset // 16 def read_verts(self): diff --git a/plugin/export_banis.py b/plugin/export_banis.py index 18a79b012..da6d568b7 100644 --- a/plugin/export_banis.py +++ b/plugin/export_banis.py @@ -36,7 +36,7 @@ def save(filepath=""): corrector = Corrector(False) scene = bpy.context.scene bones_data = {} - b_armature_ob = get_armature(scene) + b_armature_ob = get_armature(scene.objects) if not b_armature_ob: logging.warning(f"No armature was found in scene '{scene.name}' - did you delete it?") return "Failed, no armature" diff --git a/plugin/export_manis.py b/plugin/export_manis.py index 98b140574..de4fd9c75 100644 --- a/plugin/export_manis.py +++ b/plugin/export_manis.py @@ -106,7 +106,7 @@ def save(filepath=""): folder, manis_name = os.path.split(filepath) scene = bpy.context.scene bones_data = {} - b_armature_ob = get_armature(scene) + b_armature_ob = get_armature(scene.objects) if not b_armature_ob: logging.warning(f"No armature was found in scene '{scene.name}' - did you delete it?") else: diff --git a/plugin/export_ms2.py b/plugin/export_ms2.py index 086558f12..d6c9338f4 100644 --- a/plugin/export_ms2.py +++ b/plugin/export_ms2.py @@ -94,42 +94,43 @@ def save(filepath='', backup_original=True, apply_transforms=False, update_rig=F logging.info(f"Exporting {filepath}...") ms2.read_editable = True - found_scenes = 0 + found_mdl2s = 0 model_info_lut = {model_info.name: model_info for model_info in ms2.model_infos} - for scene in bpy.data.scenes: + scene = bpy.context.scene + for mdl2_coll in scene.collection.children: if from_scratch: set_game(ms2.context, scene.cobra.game) set_game(ms2.info, scene.cobra.game) model_info = ModelInfo(ms2.context) - model_info.name = scene.name + model_info.name = mdl2_coll.name model_info.bone_info = BoneInfo(ms2.context) model_info.model = Model(ms2.context, model_info) ms2.model_infos.append(model_info) else: - if scene.name not in model_info_lut: - logging.warning(f"Scene '{scene.name}' was not found in the MS2 file, skipping") + if mdl2_coll.name not in model_info_lut: + logging.warning(f"Collection '{mdl2_coll.name}' was not found in the MS2 file, skipping") continue - model_info = model_info_lut[scene.name] + model_info = model_info_lut[mdl2_coll.name] - found_scenes += 1 - logging.info(f"Exporting scene {scene.name}") + found_mdl2s += 1 + logging.info(f"Exporting {mdl2_coll.name}") - # make active scene - bpy.context.window.scene = scene + # todo - account for nesting, share with lod gen # make all collections visible in view_layer to ensure applying modifiers works view_collections = bpy.context.view_layer.layer_collection.children + view_states = [coll.exclude for coll in view_collections] for coll in view_collections: coll.exclude = False - model_info.render_flag._value = get_property(scene, "render_flag") + model_info.render_flag._value = get_property(mdl2_coll, "render_flag") # ensure that we have objects in the scene - if not has_objects_in_scene(scene): - raise AttributeError(f"No objects in scene '{scene.name}', nothing to export!") + if not has_objects_in_scene(mdl2_coll.objects): + raise AttributeError(f"No objects in collection '{mdl2_coll.name}', nothing to export!") - b_armature_ob = get_armature(scene) + b_armature_ob = get_armature(mdl2_coll.objects) if not b_armature_ob: - logging.warning(f"No armature was found in scene '{scene.name}' - did you delete it?") + logging.warning(f"No armature was found in collection '{mdl2_coll.name}' - did you delete it?") else: # clear pose for pbone in b_armature_ob.pose.bones: @@ -145,7 +146,7 @@ def save(filepath='', backup_original=True, apply_transforms=False, update_rig=F bounds = [] lod_collections = [] for lod_i in range(6): - lod_coll = get_collection_endswith(scene, f"_LOD{lod_i}") + lod_coll = get_collection_endswith(scene, f"{mdl2_coll.name}_L{lod_i}") if not lod_coll: break lod_collections.append(lod_coll) @@ -214,15 +215,15 @@ def save(filepath='', backup_original=True, apply_transforms=False, update_rig=F # write ms2, backup should have been created earlier ms2.save(filepath) # print(ms2) - if found_scenes: + if found_mdl2s: messages.add(f"Finished MS2 export in {time.time() - start_time:.2f} seconds") else: mdl2_names = sorted(model_info_lut.keys()) mdl2_names_str = '\n'.join(mdl2_names) raise AttributeError( - f"Found no scenes matching MDL2s in MS2:\n" + f"Found no collections matching MDL2s in MS2:\n" f"{mdl2_names_str}\n" - f"Rename your scenes to match the MDL2s") + f"Rename your collections to match the MDL2s") return messages diff --git a/plugin/import_banis.py b/plugin/import_banis.py index ec5de6b0e..67784c162 100644 --- a/plugin/import_banis.py +++ b/plugin/import_banis.py @@ -20,7 +20,7 @@ def load(files=[], filepath="", set_fps=False): use_armature = True scene = bpy.context.scene - b_armature_ob = get_armature(scene) + b_armature_ob = get_armature(scene.objects) bones_table, p_bones = get_bones_table(b_armature_ob) bone_names = [tup[1] for tup in bones_table] diff --git a/plugin/import_manis.py b/plugin/import_manis.py index eb59e8456..30ed6d8a3 100644 --- a/plugin/import_manis.py +++ b/plugin/import_manis.py @@ -82,7 +82,7 @@ def load(files=[], filepath="", set_fps=False): scene = bpy.context.scene bones_data = {} - b_armature_ob = get_armature(scene) + b_armature_ob = get_armature(scene.objects) if not b_armature_ob: logging.warning(f"No armature was found in scene '{scene.name}' - did you delete it?") b_cam_data = bpy.data.cameras.new("ManisCamera") diff --git a/plugin/import_ms2.py b/plugin/import_ms2.py index 1d0ac1396..9074558cd 100644 --- a/plugin/import_ms2.py +++ b/plugin/import_ms2.py @@ -4,7 +4,6 @@ import bpy # import bmesh -import numpy as np from generated.formats.ms2.compounds.packing_utils import has_nan from plugin.modules_import.armature import import_armature, append_armature_modifier, import_vertex_groups, \ @@ -12,7 +11,7 @@ from plugin.utils.hair import add_psys from plugin.modules_import.material import import_material from plugin.utils.shell import is_fin, num_fur_as_weights, is_shell, gauge_uv_scale_wrapper -from plugin.utils.object import create_ob, create_scene, create_collection, mesh_from_data, set_collection_visibility +from plugin.utils.object import create_ob, create_scene, create_collection, set_collection_visibility from generated.formats.ms2 import Ms2File from generated.formats.ms2.enums.MeshFormat import MeshFormat @@ -21,25 +20,26 @@ def load(filepath="", use_custom_normals=False, mirror_mesh=False): messages = set() start_time = time.time() in_dir, ms2_name = os.path.split(filepath) + ms2_basename = os.path.splitext(ms2_name)[0] ms2 = Ms2File() ms2.load(filepath, read_editable=True) + scene = create_scene(ms2_basename, len(ms2.modelstream_names), ms2.context.version) + bpy.context.window.scene = scene # print(ms2) created_materials = {} - for mdl2_name, model_info in zip(ms2.mdl_2_names, ms2.model_infos): - scene = create_scene(mdl2_name, int(model_info.render_flag), len(ms2.modelstream_names), ms2.context.version) - bpy.context.window.scene = scene - + for model_info in ms2.model_infos: + mdl2_coll = create_collection(scene, model_info.name) + mdl2_coll["render_flag"] = int(model_info.render_flag) bone_names = get_bone_names(model_info) - b_armature_obj = import_armature(scene, model_info, bone_names) + b_armature_obj = import_armature(scene, model_info, bone_names, mdl2_coll) mesh_dict = {} ob_dict = {} # print(model_info) # print(model_info.model) - # print("mdl2.mesh.meshes",mdl2.mesh.meshes) for lod_i, m_lod in enumerate(model_info.model.lods): logging.info(f"Importing LOD{lod_i}") - lod_coll = create_collection(scene, f"LOD{lod_i}") + lod_coll = create_collection(scene, f"{model_info.name}_L{lod_i}", mdl2_coll) # skip other shells for JWE2 obs = [] for m_ob in m_lod.objects: @@ -53,19 +53,15 @@ def load(filepath="", use_custom_normals=False, mirror_mesh=False): for ob_i, m_ob in enumerate(obs): mesh = m_ob.mesh # print(mesh) - # lod_i = mesh.lod_index # logging.debug(f"flag {mesh.flag}") - mesh_name = f"{mdl2_name}_model{m_ob.mesh_index}" - # continue + mesh_name = f"{model_info.name}_model{m_ob.mesh_index}" if m_ob.mesh_index in mesh_dict: b_me = mesh_dict[m_ob.mesh_index] - # create object and mesh from data else: b_me = bpy.data.meshes.new(mesh_name) # cast array to prevent truth check in from_pydata b_me.from_pydata(mesh.vertices, [], tuple(mesh.tris)) - # print(mesh.vertices, [], tuple(mesh.tris)) try: # store mesh unknowns # cast the bitfield to int @@ -87,49 +83,35 @@ def load(filepath="", use_custom_normals=False, mirror_mesh=False): # link material to mesh import_material(created_materials, in_dir, b_me, m_ob.material) - if m_ob.mesh_index not in ob_dict: - b_ob = create_ob(scene, f"{mdl2_name}_lod{lod_i}_ob{ob_i}", b_me, coll=lod_coll) + if m_ob.mesh_index in ob_dict: + b_ob = ob_dict[m_ob.mesh_index] + else: + b_ob = create_ob(scene, f"{model_info.name}_ob{ob_i}_L{lod_i}", b_me, coll=lod_coll) b_ob.parent = b_armature_obj - + ob_dict[m_ob.mesh_index] = b_ob try: import_vertex_groups(b_ob, mesh, bone_names) - # import_face_maps(b_ob, mesh) import_shapekeys(b_ob, mesh) # link to armature, only after mirror so the order is good and weights are mirrored append_armature_modifier(b_ob, b_armature_obj) if mirror_mesh: append_bisect_modifier(b_ob) ob_postpro(b_ob, mirror_mesh, use_custom_normals) + # from plugin.modules_import.tangents import visualize_tangents + # ob2, me2 = visualize_tangents(b_ob.name, mesh.vertices, mesh.normals, mesh.tangents) except: logging.exception("Some mesh data failed") - ob_dict[m_ob.mesh_index] = b_ob - # from plugin.modules_import.tangents import visualize_tangents - # ob2, me2 = visualize_tangents(b_ob.name, mesh.vertices, mesh.normals, mesh.tangents) - # if mesh.flag == 517: - # ob2, me2 = visualize_foliage_field(b_ob.name, mesh.vertices, mesh.colors) - else: - b_ob = ob_dict[m_ob.mesh_index] - # we can't assume that the first ob referencing this mesh has it already + # we can't assume that the first ob referencing this mesh has fur already if ms2.context.version > 32 and is_shell(b_ob): logging.debug(f"{b_ob.name} has shells, adding psys") add_psys(b_ob, mesh.fur_length) - coll_name = f"{scene.name}_LOD{lod_i}" # show lod 0, hide the others - set_collection_visibility(scene, coll_name, lod_i != 0) + set_collection_visibility(scene, lod_coll.name, lod_i != 0) gauge_uv_scale_wrapper() messages.add(f"Imported {ms2_name} in {time.time() - start_time:.2f} seconds") return messages -def import_face_maps(b_ob, mesh): - if hasattr(mesh, "face_maps"): - for map_name, face_indices in mesh.face_maps.items(): - b_face_map = b_ob.face_maps.new(name=map_name) - b_face_map.add(face_indices) - # for ind in face_indices: - # b_face_map.add(ind) - - def per_loop(b_me, per_vertex_input): return [c for col in [per_vertex_input[l.vertex_index] for l in b_me.loops] for c in col] @@ -229,22 +211,3 @@ def append_bisect_modifier(b_ob): # mod.use_x = True mod.use_axis = (True, False, False) mod.merge_threshold = 0.001 - - -def unpack_swizzle_vectorized_col(arr): - arr[:] = arr[:, (0, 2, 1, 3)] - arr[:, (0, 1)] *= -1.0 - - -def visualize_foliage_field(name, vertices, colors): - colors *= 2.0 - colors -= 1.0 - unpack_swizzle_vectorized_col(colors) - out_verts = [] - out_faces = [] - v_len = 0.1 - for i, (v, n) in enumerate(zip(vertices, colors)): - out_verts.append(v) - out_verts.append(v+v_len*n[:3]*n[3]) - out_faces.append((i * 2, i*2 + 1,)) - return mesh_from_data(bpy.context.scene, f"{name}_vecs", out_verts, out_faces, wireframe=False) diff --git a/plugin/modules_export/armature.py b/plugin/modules_export/armature.py index 439d9a31b..00d2589a8 100644 --- a/plugin/modules_export/armature.py +++ b/plugin/modules_export/armature.py @@ -23,8 +23,8 @@ def assign_p_bone_indices(b_armature_ob): p_bone["index"] = i -def get_armature(scene): - src_armatures = [ob for ob in scene.objects if type(ob.data) == bpy.types.Armature] +def get_armature(objects): + src_armatures = [ob for ob in objects if type(ob.data) == bpy.types.Armature] # do we have armatures? if src_armatures: # see if one of these is selected @@ -89,7 +89,7 @@ def export_bones_custom(b_armature_ob, model_info): bone_info.zeros_padding.arg = bone_info.zeros_count # paddings are taken care of automatically during writing export_ik(b_armature_ob, bone_info) - export_joints(bone_info, corrector) + export_joints(bone_info, corrector, b_armature_ob) def add_parents(bones_with_ik, p_bone, count): @@ -153,14 +153,14 @@ def check_ik_name(name): ik_link.matrix.set_rows(def_mat.transposed()) -def export_joints(bone_info, corrector): - logging.info("Exporting joints") - scene = bpy.context.scene - joint_coll = get_collection_endswith(scene, "_joints") - if not joint_coll: +def export_joints(bone_info, corrector, b_armature_ob): + logging.info(f"Exporting joints for {b_armature_ob.name}") + joint_obs = [ob for ob in b_armature_ob.children if ob.type == "EMPTY"] + if not joint_obs: return + b_armature_basename = b_armature_ob.name.split("_armature")[0] joints = bone_info.joints - bone_info.joint_count = joints.joint_count = len(joint_coll.objects) + bone_info.joint_count = joints.joint_count = len(joint_obs) joints.reset_field("joint_transforms") joints.reset_field("rigid_body_pointers") joints.reset_field("rigid_body_list") @@ -170,9 +170,8 @@ def export_joints(bone_info, corrector): # reset bone -> joint mapping since we don't catch them all if we loop over existing joints joints.bone_to_joint[:] = -1 bone_lut = {bone.name: bone_index for bone_index, bone in enumerate(bone_info.bones)} - for joint_i, joint_info in enumerate(joints.joint_infos): - b_joint = joint_coll.objects[joint_i] - joint_info.name = bone_name_for_ovl(get_joint_name(b_joint)) + for joint_i, (joint_info, b_joint) in enumerate(zip(joints.joint_infos, joint_obs)): + joint_info.name = bone_name_for_ovl(get_joint_name(b_armature_basename, b_joint)) joint_info.index = joint_i joint_info.bone_name = bone_name_for_ovl(b_joint.parent_bone) try: @@ -202,8 +201,8 @@ def export_joints(bone_info, corrector): else: hitcheck.surface_name = surface_name hitcheck.classification_name = classification_name - hitcheck.name = get_joint_name(b_hitcheck) - export_hitcheck(b_hitcheck, hitcheck, corrector) + hitcheck.name = get_joint_name(b_armature_basename, b_hitcheck) + export_hitcheck(b_hitcheck, hitcheck, corrector, b_armature_basename) rb = joints.rigid_body_list[joint_i] if b_joint.children: @@ -224,14 +223,14 @@ def export_joints(bone_info, corrector): # update ragdoll constraints, relies on previously updated joints corrector_rag = CorrectorRagdoll(False) j_map = {j.name: j for j in joints.joint_infos} - joints_with_ragdoll_constraints = [b_joint for b_joint in joint_coll.objects if b_joint.rigid_body_constraint] + joints_with_ragdoll_constraints = [b_joint for b_joint in joint_obs if b_joint.rigid_body_constraint] joints.num_ragdoll_constraints = len(joints_with_ragdoll_constraints) joints.reset_field("ragdoll_constraints") for rd, b_joint in zip(joints.ragdoll_constraints, joints_with_ragdoll_constraints): rbc = b_joint.rigid_body_constraint # get the joint empties, which are the parents of the respective rigidbody objects - child_joint_name = bone_name_for_ovl(get_joint_name(rbc.object1.parent)) - parent_joint_name = bone_name_for_ovl(get_joint_name(rbc.object2.parent)) + child_joint_name = bone_name_for_ovl(get_joint_name(b_armature_basename, rbc.object1.parent)) + parent_joint_name = bone_name_for_ovl(get_joint_name(b_armature_basename, rbc.object2.parent)) rd.child.joint = j_map[child_joint_name] rd.parent.joint = j_map[parent_joint_name] rd.child.index = rd.child.joint.index @@ -273,7 +272,7 @@ def export_joints(bone_info, corrector): # find the root joint, assuming the first one with least parents parents_map = [] - for joint_i, b_joint in enumerate(joint_coll.objects): + for joint_i, b_joint in enumerate(joint_obs): b_bone = b_joint.parent.data.bones[b_joint.parent_bone] num_parents = len(b_bone.parent_recursive) parents_map.append((num_parents, joint_i)) diff --git a/plugin/modules_export/collision.py b/plugin/modules_export/collision.py index 4bd3d349e..4c277d8d4 100644 --- a/plugin/modules_export/collision.py +++ b/plugin/modules_export/collision.py @@ -41,8 +41,8 @@ def get_bounds(bounds, swizzle_func=pack_swizzle): return bounds_max, bounds_min -def export_hitcheck(b_obj, hitcheck, corrector): - hitcheck.name = get_joint_name(b_obj) +def export_hitcheck(b_obj, hitcheck, corrector, b_armature_basename): + hitcheck.name = get_joint_name(b_armature_basename, b_obj) b_rb = b_obj.rigid_body if not b_rb: raise AttributeError(f"No rigid body on {b_obj.name} - can't identify collision type.") diff --git a/plugin/modules_export/geometry.py b/plugin/modules_export/geometry.py index fca87d923..068e43fca 100644 --- a/plugin/modules_export/geometry.py +++ b/plugin/modules_export/geometry.py @@ -11,7 +11,7 @@ from plugin.modules_export.armature import handle_transforms from plugin.modules_export.mesh_chunks import DYNAMIC_ID, ChunkedMesh, NO_BONES_ID, DISCARD_STATIC_TRIS from plugin.utils.matrix_util import ensure_tri_modifier, evaluate_mesh -from plugin.utils.shell import num_fur_as_weights, is_fin, is_shell +from plugin.utils.shell import num_fur_as_weights, is_fin, is_shell, FUR_VGROUPS def export_model(model_info, b_lod_coll, b_ob, b_me, bones_table, bounds, apply_transforms, use_stock_normals_tangents, m_lod, shell_index, shell_count): @@ -305,7 +305,7 @@ def validate_vertex_groups(b_ob, bones_table): for v_group in b_ob.vertex_groups: if v_group.name in bones_table: continue - elif v_group.name in ("fur_width", "fur_length"): + elif v_group.name in FUR_VGROUPS: continue else: logging.warning(f"Ignored extraneous vertex group {v_group.name} on mesh {b_ob.name}") diff --git a/plugin/modules_import/armature.py b/plugin/modules_import/armature.py index ec1d0737f..1b91376a1 100644 --- a/plugin/modules_import/armature.py +++ b/plugin/modules_import/armature.py @@ -7,7 +7,7 @@ from generated.formats.ms2.versions import is_ztuac, is_dla from plugin.modules_import.collision import import_collider, parent_to -from plugin.utils.object import create_ob, link_to_collection, set_collection_visibility +from plugin.utils.object import create_ob, link_to_collection, set_collection_visibility, create_collection from plugin.utils import matrix_util from plugin.utils.matrix_util import mat3_to_vec_roll, CorrectorRagdoll, vectorisclose @@ -15,7 +15,7 @@ vec_y = mathutils.Vector((0.0, 1.0, 0.0)) -def import_armature(scene, model_info, b_bone_names): +def import_armature(scene, model_info, b_bone_names, mdl2_coll): """Scans an armature hierarchy, and returns a whole armature.""" is_old_orientation = any((is_ztuac(model_info.context), is_dla(model_info.context))) # print(f"is_old_orientation {is_old_orientation}") @@ -31,7 +31,7 @@ def import_armature(scene, model_info, b_bone_names): b_armature_data = bpy.data.armatures.new(bone_info.name) b_armature_data.display_type = 'STICK' # b_armature_data.show_axes = True - armature_ob = create_ob(scene, bone_info.name, b_armature_data) + armature_ob = create_ob(scene, bone_info.name, b_armature_data, coll=mdl2_coll) armature_ob.show_in_front = True # make armature editable and create bones bpy.ops.object.mode_set(mode='EDIT', toggle=False) @@ -108,7 +108,7 @@ def import_armature(scene, model_info, b_bone_names): bone = armature_ob.pose.bones[short_name] bone["index"] = i try: - import_joints(scene, armature_ob, bone_info, b_bone_names, corrector) + import_joints(scene, armature_ob, bone_info, b_bone_names, corrector, mdl2_coll) except: logging.exception("Importing joints failed") try: @@ -116,8 +116,8 @@ def import_armature(scene, model_info, b_bone_names): except: logging.exception("Importing IK failed") - set_collection_visibility(scene, f"{scene.name}_joints", True) - set_collection_visibility(scene, f"{scene.name}_hitchecks", True) + set_collection_visibility(scene, f"{model_info.name}_joints", True) + set_collection_visibility(scene, f"{model_info.name}_hitchecks", True) return armature_ob @@ -331,15 +331,19 @@ def get_name(n): b_ik.chain_count = len(parents) -def import_joints(scene, armature_ob, bone_info, b_bone_names, corrector): +def import_joints(scene, armature_ob, bone_info, b_bone_names, corrector, mdl2_coll): logging.info("Importing joints") j = bone_info.joints joint_map = {} + if bone_info.joint_count: + joint_coll = create_collection(scene, f"{mdl2_coll.name}_joints", mdl2_coll) + # if joint_info.hitchecks: + # joint_coll = create_collection(scene, f"{mdl2_coll.name}_joints", mdl2_coll) for joint_i, (bone_index, joint_info, joint_transform) in enumerate(zip( j.joint_to_bone, j.joint_infos, j.joint_transforms)): logging.debug(f"joint {joint_info.name}") # create an empty representing the joint - b_joint = create_ob(scene, f"{bpy.context.scene.name}_{joint_info.name}", None, coll_name="joints") + b_joint = create_ob(scene, f"{mdl2_coll.name}_{joint_info.name}", None, coll=joint_coll) b_joint["long_name"] = joint_info.name joint_map[joint_info.name] = b_joint b_joint.empty_display_type = "ARROWS" @@ -349,7 +353,7 @@ def import_joints(scene, armature_ob, bone_info, b_bone_names, corrector): if hasattr(joint_info, "hitchecks"): for hitcheck in joint_info.hitchecks: - b_collider = import_collider(hitcheck, b_joint, corrector) + b_collider = import_collider(hitcheck, b_joint, corrector, joint_coll) # not used by PC if j.rigid_body_list: rb = j.rigid_body_list[joint_i] diff --git a/plugin/modules_import/collision.py b/plugin/modules_import/collision.py index 08b891b16..42e4dc8ad 100644 --- a/plugin/modules_import/collision.py +++ b/plugin/modules_import/collision.py @@ -11,23 +11,23 @@ from plugin.utils.quickhull import qhull3d -def import_collider(hitcheck, b_joint, corrector): +def import_collider(hitcheck, b_joint, corrector, collection): # logging.debug(f"{hitcheck.name} type {hitcheck.dtype}") - hitcheck_name = f"{bpy.context.scene.name}_{hitcheck.name}" + hitcheck_name = f"{collection.name}_{hitcheck.name}" coll = hitcheck.collider # print(hitcheck) if hitcheck.dtype == CollisionType.SPHERE: - ob = import_spherebv(coll, hitcheck_name) + ob = import_spherebv(coll, hitcheck_name, collection) elif hitcheck.dtype == CollisionType.BOUNDING_BOX: - ob = import_boxbv(coll, hitcheck_name, corrector) + ob = import_boxbv(coll, hitcheck_name, corrector, collection) elif hitcheck.dtype == CollisionType.CAPSULE: - ob = import_capsulebv(coll, hitcheck_name) + ob = import_capsulebv(coll, hitcheck_name, collection) elif hitcheck.dtype == CollisionType.CYLINDER: - ob = import_cylinderbv(coll, hitcheck_name) + ob = import_cylinderbv(coll, hitcheck_name, collection) elif hitcheck.dtype == CollisionType.MESH_COLLISION: - ob = import_meshbv(coll, hitcheck_name, corrector) + ob = import_meshbv(coll, hitcheck_name, corrector, collection) elif hitcheck.dtype in (CollisionType.CONVEX_HULL, CollisionType.CONVEX_HULL_P_C): - ob = import_hullbv(coll, hitcheck_name, corrector) + ob = import_hullbv(coll, hitcheck_name, corrector, collection) else: logging.warning(f"Unsupported collider type {hitcheck.dtype}") return @@ -35,8 +35,6 @@ def import_collider(hitcheck, b_joint, corrector): # store the strings on the right enum property ob.cobra_coll.set_value(bpy.context, "surface", hitcheck.surface_name) ob.cobra_coll.set_value(bpy.context, "classification", hitcheck.classification_name) - # h = HitCheck() - # print(export_hitcheck(ob, h)) return ob @@ -66,7 +64,7 @@ def set_b_collider(b_obj, radius, bounds_type='BOX', display_type='BOX'): b_r_body.type = "PASSIVE" -def box_from_extents(b_name, minx, maxx, miny, maxy, minz, maxz, coll_name="hitchecks", coll=None): +def box_from_extents(b_name, minx, maxx, miny, maxy, minz, maxz, coll=None): verts = [] for x in [minx, maxx]: for y in [miny, maxy]: @@ -74,7 +72,7 @@ def box_from_extents(b_name, minx, maxx, miny, maxy, minz, maxz, coll_name="hitc verts.append((x, y, z)) faces = [[0, 1, 3, 2], [6, 7, 5, 4], [0, 2, 6, 4], [3, 1, 5, 7], [4, 5, 1, 0], [7, 6, 2, 3]] scene = bpy.context.scene - return mesh_from_data(scene, b_name, verts, faces, coll_name=coll_name, coll=coll) + return mesh_from_data(scene, b_name, verts, faces, coll_name=None, coll=coll) def center_origin_to_matrix(n_center, n_dir): @@ -88,9 +86,9 @@ def center_origin_to_matrix(n_center, n_dir): return rot -def import_spherebv(sphere, hitcheck_name): +def import_spherebv(sphere, hitcheck_name, collection): r = sphere.radius - b_obj, b_me = box_from_extents(hitcheck_name, -r, r, -r, r, -r, r) + b_obj, b_me = box_from_extents(hitcheck_name, -r, r, -r, r, -r, r, collection) b_obj.location = unpack_swizzle((sphere.center.x, sphere.center.y, sphere.center.z)) set_b_collider(b_obj, r, bounds_type="SPHERE", display_type="SPHERE") return b_obj @@ -113,17 +111,17 @@ def import_collision_quat(q, corrector): # return corrector.nif_bind_to_blender_bind(mat) -def import_boxbv(box, hitcheck_name, corrector): +def import_boxbv(box, hitcheck_name, corrector, collection): mat = import_collision_matrix(box.rotation, corrector) y, x, z = unpack_swizzle((box.extent.x / 2, box.extent.y / 2, box.extent.z / 2)) - b_obj, b_me = box_from_extents(hitcheck_name, -x, x, -y, y, -z, z) + b_obj, b_me = box_from_extents(hitcheck_name, -x, x, -y, y, -z, z, collection) mat.translation = unpack_swizzle((box.center.x, box.center.y, box.center.z)) b_obj.matrix_local = mat set_b_collider(b_obj, (x+y+z)/3) return b_obj -def import_capsulebv(capsule, hitcheck_name): +def import_capsulebv(capsule, hitcheck_name, collection): # positions of the box verts minx = miny = -capsule.radius maxx = maxy = +capsule.radius @@ -131,14 +129,14 @@ def import_capsulebv(capsule, hitcheck_name): maxz = +(capsule.extent + 2 * capsule.radius) / 2 # create blender object - b_obj, b_me = box_from_extents(hitcheck_name, minx, maxx, miny, maxy, minz, maxz) + b_obj, b_me = box_from_extents(hitcheck_name, minx, maxx, miny, maxy, minz, maxz, collection) # apply transform in local space b_obj.matrix_local = center_origin_to_matrix(capsule.offset, capsule.direction) set_b_collider(b_obj, capsule.radius, bounds_type="CAPSULE", display_type="CAPSULE") return b_obj -def import_cylinderbv(cylinder, hitcheck_name): +def import_cylinderbv(cylinder, hitcheck_name, collection): # positions of the box verts minx = miny = -cylinder.radius maxx = maxy = +cylinder.radius @@ -146,14 +144,14 @@ def import_cylinderbv(cylinder, hitcheck_name): maxz = cylinder.extent / 2 # create blender object - b_obj, b_me = box_from_extents(hitcheck_name, minx, maxx, miny, maxy, minz, maxz) + b_obj, b_me = box_from_extents(hitcheck_name, minx, maxx, miny, maxy, minz, maxz, collection) # apply transform in local space b_obj.matrix_local = center_origin_to_matrix(cylinder.offset, cylinder.direction) set_b_collider(b_obj, cylinder.radius, bounds_type="CYLINDER", display_type="CYLINDER") return b_obj -def import_meshbv(coll, hitcheck_name, corrector): +def import_meshbv(coll, hitcheck_name, corrector, collection): # print(coll) # print(coll.data) scene = bpy.context.scene @@ -177,7 +175,7 @@ def import_meshbv(coll, hitcheck_name, corrector): else: # cast array to list for blender good_tris = list(tris) - b_obj, b_me = mesh_from_data(scene, hitcheck_name, [unpack_swizzle_collision(v) for v in coll.data.vertices], good_tris, coll_name="hitchecks") + b_obj, b_me = mesh_from_data(scene, hitcheck_name, [unpack_swizzle_collision(v) for v in coll.data.vertices], good_tris, coll=collection) mat = import_collision_matrix(coll.rotation, corrector) mat.translation = unpack_swizzle((coll.offset.x, coll.offset.y, coll.offset.z)) b_obj.matrix_local = mat @@ -185,10 +183,10 @@ def import_meshbv(coll, hitcheck_name, corrector): return b_obj -def import_hullbv(coll, hitcheck_name, corrector): +def import_hullbv(coll, hitcheck_name, corrector, collection): # print(coll) scene = bpy.context.scene - b_obj, b_me = mesh_from_data(scene, hitcheck_name, *qhull3d([unpack_swizzle_collision(v) for v in coll.vertices]), coll_name="hitchecks") + b_obj, b_me = mesh_from_data(scene, hitcheck_name, *qhull3d([unpack_swizzle_collision(v) for v in coll.vertices]), coll=collection) mat = import_collision_matrix(coll.rotation, corrector) # this is certainly needed for JWE2 as of 2023-06-12 mat.translation = unpack_swizzle((coll.offset.x, coll.offset.y, coll.offset.z)) diff --git a/plugin/modules_import/material.py b/plugin/modules_import/material.py index e8e950fe3..c16344dc5 100644 --- a/plugin/modules_import/material.py +++ b/plugin/modules_import/material.py @@ -434,6 +434,7 @@ def create_material(in_dir, matname): tree.links.new(emissive.outputs[0], principled.inputs["Emission"]) for alpha in shader.get_tex(shader.alpha_slots): + alpha.image.colorspace_settings.name = "Raw" alpha_pass = alpha.outputs[0] b_mat.blend_method = "CLIP" b_mat.shadow_method = "CLIP" diff --git a/plugin/utils/matrix_util.py b/plugin/utils/matrix_util.py index a1a467dfd..8b1f988dc 100644 --- a/plugin/utils/matrix_util.py +++ b/plugin/utils/matrix_util.py @@ -151,9 +151,8 @@ def ensure_tri_modifier(ob): blender_name_suffix_re = re.compile(r'\.\d+$') -def get_joint_name(b_ob): - scene = bpy.context.scene - ob_name = b_ob.name[len(scene.name)+1:] +def get_joint_name(b_armature_basename, b_ob): + ob_name = b_ob.name[len(b_armature_basename)+1:] long_name = b_ob.get("long_name", None) if not long_name: # logging.warning(f"Custom property 'long_name' is not set for {b_ob.name}") diff --git a/plugin/utils/object.py b/plugin/utils/object.py index d980ccb20..bf91ed92b 100644 --- a/plugin/utils/object.py +++ b/plugin/utils/object.py @@ -35,12 +35,11 @@ def get_lod(ob): # @TODO: Create appropriate defaults -def create_scene(name, render_flag=0, num_streams=0, version=0): +def create_scene(name, num_streams=0, version=0): logging.debug(f"Adding scene {name} to blender") if name not in bpy.data.scenes: scene = bpy.data.scenes.new(name) # store scene properties - scene["render_flag"] = render_flag scene.cobra.num_streams = num_streams context = Ms2Context() context.version = version @@ -49,13 +48,16 @@ def create_scene(name, render_flag=0, num_streams=0, version=0): return bpy.data.scenes[name] -def create_collection(scene, coll_name): +def create_collection(scene, coll_name, parent_coll=None): # turn any relative collection names to include the scene prefix - if not coll_name.startswith(f"{scene.name}_"): - coll_name = f"{scene.name}_{coll_name}" + # if not coll_name.startswith(f"{scene.name}_"): + # coll_name = f"{scene.name}_{coll_name}" if coll_name not in bpy.data.collections: coll = bpy.data.collections.new(coll_name) - scene.collection.children.link(coll) + if parent_coll: + parent_coll.children.link(coll) + else: + scene.collection.children.link(coll) return coll return bpy.data.collections[coll_name] @@ -74,11 +76,11 @@ def link_to_collection(scene, ob, coll_name): return coll_name -def has_objects_in_scene(scene): - if scene.objects: +def has_objects_in_scene(objects): + if objects: # operator needs an active object, set one if missing (eg. user had deleted the active object) if not bpy.context.view_layer.objects.active: - bpy.context.view_layer.objects.active = scene.objects[0] + bpy.context.view_layer.objects.active = objects[0] # now enter object mode on the active object, if we aren't already in it bpy.ops.object.mode_set(mode="OBJECT") return True @@ -94,14 +96,22 @@ def get_property(ob, prop_name, default=None): raise KeyError(f"Custom property '{prop_name}' missing from {ob.name} (data: {type(ob).__name__}). Add it!") +def find_collection(layer_collection, collection): + # adapted from https://devtalk.blender.org/t/unique-identifier-for-layer-collections/23966 + if layer_collection.collection == collection: + yield layer_collection + for child_collection in layer_collection.children: + yield from find_collection(child_collection, collection) + + def set_collection_visibility(scene, coll_name, hide): - # get view layer if it exists - view_collections = bpy.context.view_layer.layer_collection.children - if coll_name in view_collections: - view_collections[coll_name].hide_viewport = hide - scene_collections = scene.collection.children - if coll_name in scene_collections: - scene_collections[coll_name].hide_render = hide + if coll_name in bpy.data.collections: + coll = bpy.data.collections[coll_name] + coll.hide_render = hide + # get view layer collection if it exists + view_colls = list(find_collection(bpy.context.view_layer.layer_collection, coll)) + if view_colls: + view_colls[0].hide_viewport = hide def get_bones_table(b_armature_ob): @@ -119,4 +129,4 @@ def get_p_index(pbone): def get_parent_map(p_bones): parent_index_map = [get_p_index(pbone.parent) for pbone in p_bones] - return parent_index_map \ No newline at end of file + return parent_index_map diff --git a/plugin/utils/shell.py b/plugin/utils/shell.py index 94410b8f7..008696d92 100644 --- a/plugin/utils/shell.py +++ b/plugin/utils/shell.py @@ -20,6 +20,7 @@ FUR_FIN = "_fur_fin" FUR = "_fur" FUR_SHELL = "_fur_shell" +FUR_VGROUPS = ("fur_length", "fur_width", "fur_clump") def add_vgroup(ob, group_name, weight): @@ -71,470 +72,479 @@ def create_lods(): """Automatic LOD generator by NDP. Generates LOD objects and automatically decimates them for LOD0-LOD5""" msgs = [] logging.info(f"Generating LOD objects") - - # Get active scene and root collection - scn = bpy.context.scene - col = scn.collection - col_list = bpy.types.Collection(col).children + scene = bpy.context.scene # enforce inclusion of all lod_collections [tick box] and their objects to avoid error view_layer = bpy.context.view_layer for layer_collection in view_layer.layer_collection.children: layer_collection.exclude = False - # Make list of all LOD collections - lod_collections = [col for col in col_list if col.name[:-1].endswith("LOD")] - # Setup default lod ratio values - lod_ratios = np.linspace(1.0, 0.05, num=len(lod_collections)) - - # Deleting old LODs - for lod_coll in lod_collections[1:]: - for ob in lod_coll.objects: - # delete old target - bpy.data.objects.remove(ob, do_unlink=True) - shape_keyed = [] decimated = [] - for lod_index, (lod_coll, ratio) in enumerate(zip(lod_collections, lod_ratios)): - if lod_index > 0: - for ob_index, ob in enumerate(lod_collections[0].objects): - # additional skip condition for JWE2, as shell is separate from base fur here - if scn.cobra.game == "Jurassic World Evolution 2": - if is_shell(ob) and lod_index > 1: + for mdl2_coll in scene.collection.children: + # Make list of all LOD collections + lod_collections = [col for col in mdl2_coll.children if col.name[:-1].endswith("_L")] + # Setup default lod ratio values + lod_ratios = np.linspace(1.0, 0.05, num=len(lod_collections)) + + # Deleting old LODs + for lod_coll in lod_collections[1:]: + for ob in lod_coll.objects: + # delete old target + bpy.data.objects.remove(ob, do_unlink=True) + + for lod_index, (lod_coll, ratio) in enumerate(zip(lod_collections, lod_ratios)): + if lod_index > 0: + for ob_index, ob in enumerate(lod_collections[0].objects): + # additional skip condition for JWE2, as shell is separate from base fur here + if scene.cobra.game == "Jurassic World Evolution 2": + if is_shell(ob) and lod_index > 1: + continue + # check if we want to copy this one + if is_fin(ob) and lod_index > 1: continue - # check if we want to copy this one - if is_fin(ob) and lod_index > 1: - continue - obj1 = copy_ob(ob, f"{scn.name}_LOD{lod_index}") - obj1.name = f"{scn.name}_lod{lod_index}_ob{ob_index}" - b_me = obj1.data - - # Can't create automatic LODs for models that have shape keys - if ob.data.shape_keys: - shape_keyed.append(ob) - else: - decimated.append(ob) - if len(b_me.polygons) > 3: - # Decimating duplicated object - decimate = obj1.modifiers.new("Decimate", 'DECIMATE') - decimate.ratio = ratio - - # remove additional shell material from LODs after LOD1 - if is_shell(ob) and lod_index > 1: - # toggle the flag on the bitfield to maintain the other bits, but fins seems to be always 565 - b_me["flag"] = 565 - # flag = ModelFlag.from_value(b_me["flag"]) - # flag.repeat_tris = True - # flag.fur_shells = False - # b_me["flag"] = int(flag) - # remove shell material - b_me.materials.pop(index=1) + obj1 = copy_ob(ob, lod_coll) + obj1.name = f"{mdl2_coll.name}_ob{ob_index}_L{lod_index}" + b_me = obj1.data + + # Can't create automatic LODs for models that have shape keys + if ob.data.shape_keys: + shape_keyed.append(ob) + else: + decimated.append(ob) + if len(b_me.polygons) > 3: + # Decimating duplicated object + decimate = obj1.modifiers.new("Decimate", 'DECIMATE') + decimate.ratio = ratio + + # remove additional shell material from LODs after LOD1 + if is_shell(ob) and lod_index > 1: + # toggle the flag on the bitfield to maintain the other bits, but fins seems to be always 565 + b_me["flag"] = 565 + # flag = ModelFlag.from_value(b_me["flag"]) + # flag.repeat_tris = True + # flag.fur_shells = False + # b_me["flag"] = int(flag) + # remove shell material + b_me.materials.pop(index=1) if decimated: msgs.append(f"{len(decimated)} LOD objects generated successfully") if shape_keyed: - msgs.append(f"Can't create automatic LODs for {len(shape_keyed)} models with shape keys. Decimate those manually") + msgs.append( + f"Can't create automatic LODs for {len(shape_keyed)} models with shape keys. Decimate those manually") return msgs # Main rig editing function def generate_rig_edit(**kwargs): - """Automatic rig edit generator by NDP. Detects posed bones and automatically generates nodes and offsets them.""" - # Initiate logging - msgs = [] - #logging.info(f"-------------------------------------------------------------") - logging.info(f"generating rig edit from pose") - - # Function settings - mergenodes = kwargs.get('mergenodes', True) - applyarmature = kwargs.get('applyarmature', False) - errortolerance = 0.0001 - - # Log settings - logging.info(f"function settings:") - logging.info(f"merge identical nodes = {mergenodes}") - logging.info(f"apply armature modifiers = {applyarmature}") - logging.info(f"error tolerance = {errortolerance}") - - # Check if the active object is a valid armature - if bpy.context.active_object.type != 'ARMATURE': - # Object is not an armature. Cancelling. - msgs.append(f"No armature selected.") - return msgs - - # Get the armature - armature = bpy.context.object - logging.info(f"armature: {armature.name}") - - # Apply armature modifiers of children objects - if applyarmature == True: - logging.info(f"Applying armature modifiers of children objects") - armaturechildren = [] - - # Go over every object in the scene - for ob in bpy.data.objects: - # Check if they are parented to the armature, are a mesh, and have modifiers - if ob.parent == armature and ob.type == 'MESH' and ob.modifiers: - modifier_list = [] - #Create a list of current armature modifiers in the object - for modifier in ob.modifiers: - if modifier.type == 'ARMATURE': - modifier_list.append(modifier) - for modifier in modifier_list: - # Apply the armature modifier - bpy.ops.object.modifier_copy({"object": ob}, modifier=modifier.name) - bpy.ops.object.modifier_apply({"object": ob}, modifier=modifier.name) - - # Store current mode - original_mode = bpy.context.mode - # For some reason it doesn't recognize edit_armature as a valid mode to switch to so we change it to just edit. Blender moment - if original_mode == 'EDIT_ARMATURE': - original_mode = 'EDIT' - - # Store number of edits done - editnumber = 0 - - # Force Switch to pose mode. - bpy.ops.object.mode_set(mode='POSE') - - # --------------------------------------------------------------------------------------------------------------- - - # Creating list of posed bones and storing data - - # Initiate list of all posed bones - posebone_list = [] - # Initiate dictionary of matrix data - posebone_data = {} - - # We iterate over every pose bone and detetect which ones have been posed, and create a list of them. - logging.info(f"evaluating posed bones:") - for p_bone in armature.pose.bones: - # We check if vectors have miniscule transforms, and just consider them rounding errors and clear them. - # Check location - if vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) and p_bone.location != Vector((0, 0, 0)): - # Warn the user - logging.info(f"{p_bone.name} had miniscule location transforms, assuming it is an error and clearing") - # Clear transforms - p_bone.location = Vector((0, 0, 0)) - - # Check rotation - if vectorisclose(Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance) and Vector(p_bone.rotation_quaternion) != Vector((1, 0, 0, 0)): - # Warn the user - logging.info(f"{p_bone.name} had miniscule rotation transforms, assuming it is an error and clearing") - # Clear rotation - p_bone.rotation_quaternion = Quaternion((1, 0, 0, 0)) - - # Check scale - if vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance) and p_bone.scale != Vector((1, 1, 1)): - # Warn the user - logging.info(f"{p_bone.name} had miniscule scale transforms, assuming it is an error and clearing") - # Clear scale - p_bone.scale = Vector((1, 1, 1)) - - # Check if any bones have major scale transform, and warn the user. - if not vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance): - logging.info(f"{p_bone.name} had scale. Value = {repr(p_bone.scale)}, difference: {(p_bone.scale - Vector((1, 1, 1))).length}") - msgs.append(f"Warning: {str(p_bone.name)} had scale transforms. Reset scale for all bones and try again.") - return msgs - - # We check for NODE bones with transforms and skip them. - if (not vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) or not vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance) or not vectorisclose(Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance)) and p_bone.name.startswith("NODE_"): - # Ignore posed NODE bones and proceed to the next, their offsets can be applied directly. - editnumber = editnumber + 1 - logging.info(f"rig edit number {editnumber}") - logging.info(f"NODE with offsets detected. Applying offsets directly.") - continue - - # We append any remaining bones that have been posed. - if (not vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) or not vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance) or not vectorisclose(Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance)): - # Append the bones to the list of posed bones. - # posebone_list.append(p_bone) - # bonebase values - logging.info(f"p_bone: {p_bone.name}") - #logging.info(f"p_bone head (global rest): {p_bone.bone.head_local}") - #logging.info(f"p_bone head (global pose): {p_bone.head}") - #logging.info(f"p_bone head (+difference): {p_bone.head - p_bone.bone.head_local}") - if p_bone.parent == None: - posebone_data[p_bone.name] = [p_bone.matrix.copy(), p_bone.bone.matrix_local.copy(), Matrix(((0.0, 1.0, 0.0, 0.0),(-1.0, 0.0, 0.0, 0.0),(0.0, 0.0, 1.0, 0.0),(0.0, 0.0, 0.0, 1.0)))] - else: - posebone_data[p_bone.name] = [p_bone.matrix.copy(), p_bone.bone.matrix_local.copy(), p_bone.parent.bone.matrix_local.copy()] - - # --------------------------------------------------------------------------------------------------------------- - - # Apply pose as rest pose - bpy.ops.pose.armature_apply() - - # --------------------------------------------------------------------------------------------------------------- - - # Creating the nodes - # We switch to edit mode to use edit bones, as they do not exist outside of edit mode. - # Switch to edit mode - bpy.ops.object.mode_set(mode='EDIT') - - for bone_name, (base_posed, base_armature_space, parent_armature_space) in posebone_data.items(): - # Get edit bones - bonebase = armature.data.edit_bones.get(bone_name) - - if bonebase.parent == None: - logging.info(f"{bonebase.name} has no parent, creating a blank node") - # Create the node - bonenode = armature.data.edit_bones.new(f"NODE_{bone_name}") - - bonenode.matrix = Matrix(((0.0, 1.0, 0.0, 0.0),(-1.0, 0.0, 0.0, 0.0),(0.0, 0.0, 1.0, 0.0),(0.0, 0.0, 0.0, 1.0))) - - # Does length matter? It was just dissapearing for some reason - bonenode.length = 1 - - # Set parent of bonebase to bonenode - bonebase.parent = bonenode - - - - #Set the parent - boneparent = bonebase.parent - - # Detect if the bone already has a node. - if boneparent.name.startswith("NODE_"): - logging.info(f"{bone_name} has a pre-existing NODE. Using it instead.") - bonenode = bonebase.parent - - else: - #Set the parent - boneparent = bonebase.parent - - # Creating the node bone - bonenode = armature.data.edit_bones.new(f"NODE_{bone_name}") - - # Set parent of bonenode to boneparent - bonenode.parent = boneparent - - # Set parent of bonebase to bonenode - bonebase.parent = bonenode - - # Copy parent length to node - bonenode.length = boneparent.length - - - # Define the matrix - bonenode_matrix_local = base_posed @ (base_armature_space.inverted() @ parent_armature_space) - #logging.info(f"calculated node matrix") - #logging.info(f"{bonenode_matrix_local}") - - # Remember length - nodelength = bonenode.length - - # Set node matrix - # bonenode.matrix = bonenode_matrix_local - set_transform4(bonenode_matrix_local, bonenode) - - # Restore length - bonenode.length = nodelength - - # Node completed. Log number of edits. - editnumber = editnumber + 1 - # Node creation complete - logging.info(f"created node: {bonenode.name}") - #logging.info(f"node matrix:") - #logging.info(f"{bonenode.matrix}") - - bpy.ops.object.mode_set(mode='POSE') - - # --------------------------------------------------------------------------------------------------------------- - - # Creating node groups to delete - - if mergenodes == True: - # Checking for duplicates to merge - logging.info(f"creating node_groups to merge") - # Initiate list - node_list = [] - - # We create a list of all NODES - for p_bone in armature.pose.bones: - if p_bone.name.startswith("NODE_"): - node_list.append(p_bone) - - # Create NODE parent dictionary - node_groups = {} - - # Create and sort NODE groups, we create a dictionary with a tuple of: parent,rounded matrix as the key. This way we can group identical nodes. - for p_bone in node_list: - #We create a rounded matrix to create leeway for miniscule variation - rounded_matrix = tuple(tuple(round(element, 5) for element in row) for row in p_bone.bone.matrix_local) - #logging.info(f"node matrix: {p_bone.bone.matrix_local}") - #logging.info(f"rounded matrix: {rounded_matrix}") - - #We use the parent and rounded matrix as a key to sort all identical nodes into groups - keytuple = (p_bone.parent, rounded_matrix) - if keytuple in node_groups: - node_groups[keytuple].append(p_bone) - else: - node_groups[keytuple] = [p_bone] - - # Log node groups - for nodegroup in node_groups: - logging.info(f"node group: {nodegroup}") - for node in node_groups[nodegroup]: - logging.info(f"node: {node.name}") - - # store number of deleted nodes - deletednodes = 0 - - # Merge node groups - for nodegroup in node_groups: - # Rename NODE to indicate it owns more than one bone - if len(node_groups[nodegroup]) > 1: - # Renaming to be more descriptive - if node_groups[nodegroup][0].parent == None: - node_groups[nodegroup][0].name = f"NODE_{len(node_groups[nodegroup])}GROUP_ROOTNODE" - else: - node_groups[nodegroup][0].name = f"NODE_{len(node_groups[nodegroup])}GROUP_{node_groups[nodegroup][0].parent.name}" - - # Log group organization - #logging.info(f"first node: {node_groups[nodegroup][0].name}") - #logging.info(f"first child: {node_groups[nodegroup][0].children[0].name}") - - #Delete all extra nodes - for node in node_groups[nodegroup]: - if node != node_groups[nodegroup][0]: - #logging.info(f"secondary node: {node.name}") - #logging.info(f"secondary child: {node.children[0].name}") - - # Switch to edit mode to edit the parents - bpy.ops.object.mode_set(mode='EDIT') - - # Reparent duplicate basebones to the first node - armature.data.edit_bones.get(node.children[0].name).parent = armature.data.edit_bones.get( - node_groups[nodegroup][0].name) - - # Delete the duplicate nodes - deletednodes = deletednodes + 1 - armature.data.edit_bones.remove(armature.data.edit_bones.get(node.name)) - - # Switch back to pose mode - bpy.ops.object.mode_set(mode='POSE') - - # Report amount of deleted nodes - if deletednodes > 0: - logging.info(f"merged {deletednodes} nodes into node groups") - # Node groups completed - - # --------------------------------------------------------------------------------------------------------------- - - # Switch back to original mode. - bpy.ops.object.mode_set(mode=original_mode) - - # Finalize - logging.info(f"total number of edits: {editnumber}") - totalnodes = len([p_bone for p_bone in armature.pose.bones if p_bone.name.startswith("NODE_")]) - totalbones = len(armature.pose.bones) - logging.info(f"total nodes: {totalnodes}") - logging.info(f"total bones: {totalbones}") - - if totalbones > 254: - msgs.append(f"Warning: Total amount of bones exceeds 254 after rig edit, game will crash. Please undo, reduce the number of edits, and try again.") - return msgs - - # Return count of succesfull rig edits - msgs.append(f"{str(editnumber)} rig edits generated succesfully") - return msgs + """Automatic rig edit generator by NDP. Detects posed bones and automatically generates nodes and offsets them.""" + # Initiate logging + msgs = [] + # logging.info(f"-------------------------------------------------------------") + logging.info(f"generating rig edit from pose") + + # Function settings + mergenodes = kwargs.get('mergenodes', True) + applyarmature = kwargs.get('applyarmature', False) + errortolerance = 0.0001 + + # Log settings + logging.info(f"function settings:") + logging.info(f"merge identical nodes = {mergenodes}") + logging.info(f"apply armature modifiers = {applyarmature}") + logging.info(f"error tolerance = {errortolerance}") + + # Check if the active object is a valid armature + if bpy.context.active_object.type != 'ARMATURE': + # Object is not an armature. Cancelling. + msgs.append(f"No armature selected.") + return msgs + + # Get the armature + armature = bpy.context.object + logging.info(f"armature: {armature.name}") + + # Apply armature modifiers of children objects + if applyarmature: + logging.info(f"Applying armature modifiers of children objects") + armaturechildren = [] + + # Go over every object in the scene + for ob in bpy.data.objects: + # Check if they are parented to the armature, are a mesh, and have modifiers + if ob.parent == armature and ob.type == 'MESH' and ob.modifiers: + modifier_list = [] + # Create a list of current armature modifiers in the object + for modifier in ob.modifiers: + if modifier.type == 'ARMATURE': + modifier_list.append(modifier) + for modifier in modifier_list: + # Apply the armature modifier + bpy.ops.object.modifier_copy({"object": ob}, modifier=modifier.name) + bpy.ops.object.modifier_apply({"object": ob}, modifier=modifier.name) + + # Store current mode + original_mode = bpy.context.mode + # For some reason it doesn't recognize edit_armature as a valid mode to switch to so we change it to just edit. Blender moment + if original_mode == 'EDIT_ARMATURE': + original_mode = 'EDIT' + + # Store number of edits done + editnumber = 0 + + # Force Switch to pose mode. + bpy.ops.object.mode_set(mode='POSE') + + # --------------------------------------------------------------------------------------------------------------- + + # Creating list of posed bones and storing data + + # Initiate list of all posed bones + posebone_list = [] + # Initiate dictionary of matrix data + posebone_data = {} + + # We iterate over every pose bone and detetect which ones have been posed, and create a list of them. + logging.info(f"evaluating posed bones:") + for p_bone in armature.pose.bones: + # We check if vectors have miniscule transforms, and just consider them rounding errors and clear them. + # Check location + if vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) and p_bone.location != Vector((0, 0, 0)): + # Warn the user + logging.info(f"{p_bone.name} had miniscule location transforms, assuming it is an error and clearing") + # Clear transforms + p_bone.location = Vector((0, 0, 0)) + + # Check rotation + if vectorisclose(Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance) and Vector( + p_bone.rotation_quaternion) != Vector((1, 0, 0, 0)): + # Warn the user + logging.info(f"{p_bone.name} had miniscule rotation transforms, assuming it is an error and clearing") + # Clear rotation + p_bone.rotation_quaternion = Quaternion((1, 0, 0, 0)) + + # Check scale + if vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance) and p_bone.scale != Vector((1, 1, 1)): + # Warn the user + logging.info(f"{p_bone.name} had miniscule scale transforms, assuming it is an error and clearing") + # Clear scale + p_bone.scale = Vector((1, 1, 1)) + + # Check if any bones have major scale transform, and warn the user. + if not vectorisclose(p_bone.scale, Vector((1, 1, 1)), errortolerance): + logging.info( + f"{p_bone.name} had scale. Value = {repr(p_bone.scale)}, difference: {(p_bone.scale - Vector((1, 1, 1))).length}") + msgs.append(f"Warning: {str(p_bone.name)} had scale transforms. Reset scale for all bones and try again.") + return msgs + + # We check for NODE bones with transforms and skip them. + if (not vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) or not vectorisclose( + p_bone.scale, Vector((1, 1, 1)), errortolerance) or not vectorisclose( + Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance)) and p_bone.name.startswith( + "NODE_"): + # Ignore posed NODE bones and proceed to the next, their offsets can be applied directly. + editnumber = editnumber + 1 + logging.info(f"rig edit number {editnumber}") + logging.info(f"NODE with offsets detected. Applying offsets directly.") + continue + + # We append any remaining bones that have been posed. + if (not vectorisclose(p_bone.location, Vector((0, 0, 0)), errortolerance) or not vectorisclose(p_bone.scale, + Vector(( + 1, 1, 1)), + errortolerance) or not vectorisclose( + Vector(p_bone.rotation_quaternion), Vector((1, 0, 0, 0)), errortolerance)): + # Append the bones to the list of posed bones. + # posebone_list.append(p_bone) + # bonebase values + logging.info(f"p_bone: {p_bone.name}") + # logging.info(f"p_bone head (global rest): {p_bone.bone.head_local}") + # logging.info(f"p_bone head (global pose): {p_bone.head}") + # logging.info(f"p_bone head (+difference): {p_bone.head - p_bone.bone.head_local}") + if not p_bone.parent: + posebone_data[p_bone.name] = [p_bone.matrix.copy(), p_bone.bone.matrix_local.copy(), Matrix( + ((0.0, 1.0, 0.0, 0.0), (-1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0)))] + else: + posebone_data[p_bone.name] = [p_bone.matrix.copy(), p_bone.bone.matrix_local.copy(), + p_bone.parent.bone.matrix_local.copy()] + + # --------------------------------------------------------------------------------------------------------------- + + # Apply pose as rest pose + bpy.ops.pose.armature_apply() + + # --------------------------------------------------------------------------------------------------------------- + + # Creating the nodes + # We switch to edit mode to use edit bones, as they do not exist outside of edit mode. + # Switch to edit mode + bpy.ops.object.mode_set(mode='EDIT') + + for bone_name, (base_posed, base_armature_space, parent_armature_space) in posebone_data.items(): + # Get edit bones + bonebase = armature.data.edit_bones.get(bone_name) + + if bonebase.parent == None: + logging.info(f"{bonebase.name} has no parent, creating a blank node") + # Create the node + bonenode = armature.data.edit_bones.new(f"NODE_{bone_name}") + + bonenode.matrix = Matrix( + ((0.0, 1.0, 0.0, 0.0), (-1.0, 0.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))) + + # Does length matter? It was just dissapearing for some reason + bonenode.length = 1 + + # Set parent of bonebase to bonenode + bonebase.parent = bonenode + + # Set the parent + boneparent = bonebase.parent + + # Detect if the bone already has a node. + if boneparent.name.startswith("NODE_"): + logging.info(f"{bone_name} has a pre-existing NODE. Using it instead.") + bonenode = bonebase.parent + + else: + # Set the parent + boneparent = bonebase.parent + + # Creating the node bone + bonenode = armature.data.edit_bones.new(f"NODE_{bone_name}") + + # Set parent of bonenode to boneparent + bonenode.parent = boneparent + + # Set parent of bonebase to bonenode + bonebase.parent = bonenode + + # Copy parent length to node + bonenode.length = boneparent.length + + # Define the matrix + bonenode_matrix_local = base_posed @ (base_armature_space.inverted() @ parent_armature_space) + # logging.info(f"calculated node matrix") + # logging.info(f"{bonenode_matrix_local}") + + # Remember length + nodelength = bonenode.length + + # Set node matrix + # bonenode.matrix = bonenode_matrix_local + set_transform4(bonenode_matrix_local, bonenode) + + # Restore length + bonenode.length = nodelength + + # Node completed. Log number of edits. + editnumber = editnumber + 1 + # Node creation complete + logging.info(f"created node: {bonenode.name}") + # logging.info(f"node matrix:") + # logging.info(f"{bonenode.matrix}") + + bpy.ops.object.mode_set(mode='POSE') + + # --------------------------------------------------------------------------------------------------------------- + + # Creating node groups to delete + + if mergenodes: + # Checking for duplicates to merge + logging.info(f"creating node_groups to merge") + # Initiate list + node_list = [] + + # We create a list of all NODES + for p_bone in armature.pose.bones: + if p_bone.name.startswith("NODE_"): + node_list.append(p_bone) + + # Create NODE parent dictionary + node_groups = {} + + # Create and sort NODE groups, we create a dictionary with a tuple of: parent,rounded matrix as the key. This way we can group identical nodes. + for p_bone in node_list: + # We create a rounded matrix to create leeway for miniscule variation + rounded_matrix = tuple(tuple(round(element, 5) for element in row) for row in p_bone.bone.matrix_local) + # logging.info(f"node matrix: {p_bone.bone.matrix_local}") + # logging.info(f"rounded matrix: {rounded_matrix}") + + # We use the parent and rounded matrix as a key to sort all identical nodes into groups + keytuple = (p_bone.parent, rounded_matrix) + if keytuple in node_groups: + node_groups[keytuple].append(p_bone) + else: + node_groups[keytuple] = [p_bone] + + # Log node groups + for nodegroup in node_groups: + logging.info(f"node group: {nodegroup}") + for node in node_groups[nodegroup]: + logging.info(f"node: {node.name}") + + # store number of deleted nodes + deletednodes = 0 + + # Merge node groups + for nodegroup in node_groups: + # Rename NODE to indicate it owns more than one bone + if len(node_groups[nodegroup]) > 1: + # Renaming to be more descriptive + if not node_groups[nodegroup][0].parent: + node_groups[nodegroup][0].name = f"NODE_{len(node_groups[nodegroup])}GROUP_ROOTNODE" + else: + node_groups[nodegroup][ + 0].name = f"NODE_{len(node_groups[nodegroup])}GROUP_{node_groups[nodegroup][0].parent.name}" + + # Log group organization + # logging.info(f"first node: {node_groups[nodegroup][0].name}") + # logging.info(f"first child: {node_groups[nodegroup][0].children[0].name}") + + # Delete all extra nodes + for node in node_groups[nodegroup]: + if node != node_groups[nodegroup][0]: + # logging.info(f"secondary node: {node.name}") + # logging.info(f"secondary child: {node.children[0].name}") + + # Switch to edit mode to edit the parents + bpy.ops.object.mode_set(mode='EDIT') + + # Reparent duplicate basebones to the first node + armature.data.edit_bones.get(node.children[0].name).parent = armature.data.edit_bones.get( + node_groups[nodegroup][0].name) + + # Delete the duplicate nodes + deletednodes = deletednodes + 1 + armature.data.edit_bones.remove(armature.data.edit_bones.get(node.name)) + + # Switch back to pose mode + bpy.ops.object.mode_set(mode='POSE') + + # Report amount of deleted nodes + if deletednodes > 0: + logging.info(f"merged {deletednodes} nodes into node groups") + # Node groups completed + + # --------------------------------------------------------------------------------------------------------------- + + # Switch back to original mode. + bpy.ops.object.mode_set(mode=original_mode) + + # Finalize + logging.info(f"total number of edits: {editnumber}") + totalnodes = len([p_bone for p_bone in armature.pose.bones if p_bone.name.startswith("NODE_")]) + totalbones = len(armature.pose.bones) + logging.info(f"total nodes: {totalnodes}") + logging.info(f"total bones: {totalbones}") + + if totalbones > 254: + msgs.append( + f"Warning: Total amount of bones exceeds 254 after rig edit, game will crash. Please undo, reduce the number of edits, and try again.") + return msgs + + # Return count of succesfull rig edits + msgs.append(f"{str(editnumber)} rig edits generated succesfully") + return msgs + # Function for converting scale to visual location transforms in pose mode def convert_scale_to_loc(): - """Automatically convert scaled bones into equivalent visual location transforms""" - # Initiate logging - msgs = [] - logging.info(f"converting scale transforms to visual location") - - # Check if the active object is a valid armature - if bpy.context.active_object.type != 'ARMATURE': - # Object is not an armature. Cancelling. - msgs.append(f"No armature selected.") - return msgs - - # Store current mode - original_mode = bpy.context.mode - # For some reason it doesn't recognize edit_armature as a valid mode to switch to so we change it to just edit. Blender moment - if original_mode == 'EDIT_ARMATURE': - original_mode = 'EDIT' - #Set to pose mode - bpy.ops.object.mode_set(mode='POSE') - - # Get the armature - armature = bpy.context.object - logging.info(f"armature: {armature.name}") - - # Initiate logging variable - editnumber = 0 - - # Initiate list of any bones that are not at their armaturespace rest location - posebone_list = [] - posebone_data = {} - - # We get a list of all bones not in their rest positions in armaturespace - for p_bone in armature.pose.bones: - - posebone_rotation = p_bone.rotation_quaternion.copy() - - p_bone.rotation_quaternion = (1,0,0,0) - bpy.context.view_layer.update() - - #We copy all data in case we need parent data - posebone_data[p_bone] = [p_bone.bone.head_local.copy(),p_bone.head.copy(),p_bone.location.copy(),posebone_rotation] - posebone_list.append(p_bone) - logging.info(f"{p_bone.name} rest pos: {p_bone.bone.head_local}") - logging.info(f"{p_bone.name} pose pos: {p_bone.head}") - - - #Clear scale of all bones - for p_bone in armature.pose.bones: - p_bone.scale = (1,1,1) - p_bone.location = (0,0,0) - - - #Clear scale and set location - logging.info(f"Setting location of pose bones:") - for p_bone in posebone_list: - logging.info(f"posed bone: {p_bone}") - # Update positions - bpy.context.view_layer.update() - - if p_bone.parent != None: - # Bone_rest offset from Parent_rest - rest_offset = posebone_data[p_bone][0] - posebone_data[p_bone.parent][0] - - # Bone_pose offset from Parent_pose - pose_offset = posebone_data[p_bone][1] - posebone_data[p_bone.parent][1] - - calc_offset = pose_offset - rest_offset - - p_bone.matrix.translation = p_bone.matrix.translation + calc_offset - - else: - p_bone.matrix.translation = p_bone.matrix.translation + (posebone_data[p_bone][1] - posebone_data[p_bone][0]) - - editnumber = editnumber + 1 - - for p_bone in posebone_list: - p_bone.rotation_quaternion = posebone_data[p_bone][3] - - - if editnumber > 0: - msgs.append(f"Moved {editnumber} bones to their visual locations and reset scales") - else: - msgs.append(f"No bones required movement.") - - #Return to original mode - bpy.ops.object.mode_set(mode=original_mode) - - return msgs - - -def copy_ob(src_obj, lod_group_name): + """Automatically convert scaled bones into equivalent visual location transforms""" + # Initiate logging + msgs = [] + logging.info(f"converting scale transforms to visual location") + + # Check if the active object is a valid armature + if bpy.context.active_object.type != 'ARMATURE': + # Object is not an armature. Cancelling. + msgs.append(f"No armature selected.") + return msgs + + # Store current mode + original_mode = bpy.context.mode + # For some reason it doesn't recognize edit_armature as a valid mode to switch to so we change it to just edit. Blender moment + if original_mode == 'EDIT_ARMATURE': + original_mode = 'EDIT' + # Set to pose mode + bpy.ops.object.mode_set(mode='POSE') + + # Get the armature + armature = bpy.context.object + logging.info(f"armature: {armature.name}") + + # Initiate logging variable + editnumber = 0 + + # Initiate list of any bones that are not at their armaturespace rest location + posebone_list = [] + posebone_data = {} + + # We get a list of all bones not in their rest positions in armaturespace + for p_bone in armature.pose.bones: + posebone_rotation = p_bone.rotation_quaternion.copy() + + p_bone.rotation_quaternion = (1, 0, 0, 0) + bpy.context.view_layer.update() + + # We copy all data in case we need parent data + posebone_data[p_bone] = [p_bone.bone.head_local.copy(), p_bone.head.copy(), p_bone.location.copy(), + posebone_rotation] + posebone_list.append(p_bone) + logging.info(f"{p_bone.name} rest pos: {p_bone.bone.head_local}") + logging.info(f"{p_bone.name} pose pos: {p_bone.head}") + + # Clear scale of all bones + for p_bone in armature.pose.bones: + p_bone.scale = (1, 1, 1) + p_bone.location = (0, 0, 0) + + # Clear scale and set location + logging.info(f"Setting location of pose bones:") + for p_bone in posebone_list: + logging.info(f"posed bone: {p_bone}") + # Update positions + bpy.context.view_layer.update() + + if not p_bone.parent: + # Bone_rest offset from Parent_rest + rest_offset = posebone_data[p_bone][0] - posebone_data[p_bone.parent][0] + + # Bone_pose offset from Parent_pose + pose_offset = posebone_data[p_bone][1] - posebone_data[p_bone.parent][1] + + calc_offset = pose_offset - rest_offset + + p_bone.matrix.translation = p_bone.matrix.translation + calc_offset + + else: + p_bone.matrix.translation = p_bone.matrix.translation + ( + posebone_data[p_bone][1] - posebone_data[p_bone][0]) + + editnumber = editnumber + 1 + + for p_bone in posebone_list: + p_bone.rotation_quaternion = posebone_data[p_bone][3] + + if editnumber > 0: + msgs.append(f"Moved {editnumber} bones to their visual locations and reset scales") + else: + msgs.append(f"No bones required movement.") + + # Return to original mode + bpy.ops.object.mode_set(mode=original_mode) + + return msgs + + +def copy_ob(src_obj, coll=None): new_obj = src_obj.copy() new_obj.data = src_obj.data.copy() new_obj.name = src_obj.name + "_copy" new_obj.animation_data_clear() - plugin.utils.object.link_to_collection(bpy.context.scene, new_obj, lod_group_name) + if coll is not None: + coll.objects.link(new_obj) bpy.context.view_layer.objects.active = new_obj return new_obj @@ -542,13 +552,14 @@ def copy_ob(src_obj, lod_group_name): def ob_processor_wrapper(func): msgs = [] for lod_i in range(6): - coll = get_collection_endswith(bpy.context.scene, f"_LOD{lod_i}") - if coll is None: - return msgs - src_obs = [ob for ob in coll.objects if is_shell(ob)] - trg_obs = [ob for ob in coll.objects if is_fin(ob)] - if src_obs and trg_obs: - msgs.append(func(src_obs[0], trg_obs[0])) + for mdl2_coll in bpy.context.scene.collection.children: + coll = get_collection_endswith(bpy.context.scene, f"{mdl2_coll.name}_L{lod_i}") + if coll is None: + continue + src_obs = [ob for ob in coll.objects if is_shell(ob)] + trg_obs = [ob for ob in coll.objects if is_fin(ob)] + if src_obs and trg_obs: + msgs.append(func(src_obs[0], trg_obs[0])) return msgs @@ -564,7 +575,7 @@ def gauge_uv_scale_wrapper(): def get_collection_endswith(scene, suffix): # get collections in scene root collection - for coll in scene.collection.children: + for coll in bpy.data.collections: if coll.name.endswith(suffix): return coll @@ -603,8 +614,7 @@ def build_fins_geom(shell_ob): except: raise AttributeError(f"{shell_ob.name} has no UV scale properties. Run 'Gauge UV Scales' first!") - lod_group_name = plugin.utils.object.get_lod(shell_ob) - ob = copy_ob(shell_ob, lod_group_name) + ob = copy_ob(shell_ob, shell_ob.users_collection[0]) me = ob.data # data is per loop @@ -648,7 +658,7 @@ def build_fins_geom(shell_ob): bm.free() # free and prevent further access # remove fur_length vgroup - for vg_name in ("fur_length", "fur_width"): + for vg_name in FUR_VGROUPS: if vg_name in ob.vertex_groups: vg = ob.vertex_groups[vg_name] ob.vertex_groups.remove(vg) @@ -931,10 +941,10 @@ def gauge_uv_factors(shell_ob, fin_ob): base_fur_length = vertex_group.weight * hair_length uv_heights.append(uv_height) base_fur_lengths.append(base_fur_length) - # if vgroup_name == "fur_width": - # base_fur_width = vertex_group.weight - # if i == 20: - # break + # if vgroup_name == "fur_width": + # base_fur_width = vertex_group.weight + # if i == 20: + # break uv_scale_x = np.mean(uv_lens) / np.mean(v_lens) uv_scale_y = np.mean(uv_heights) / np.mean(base_fur_lengths) # store on mesh for consistency @@ -1011,7 +1021,6 @@ def num_fur_as_weights(mat_name): def extrude_fins(): - ob = bpy.context.active_object if not ob: raise AttributeError("No object in context") @@ -1031,7 +1040,7 @@ def extrude_fins(): # normal = b_loop.normal hair_len = Y_START - fins_layer[loop_index].uv.y directions[loop_index] = normal * hair_len - # print(hair_len, normal) + # print(hair_len, normal) verts = set() for loop_index, direction in enumerate(directions): b_loop = me.loops[loop_index] @@ -1044,7 +1053,6 @@ def extrude_fins(): def intrude_fins(): - ob = bpy.context.active_object if not ob: raise AttributeError("No object in context") diff --git a/source/formats/ms2/compounds/PcMeshData.py b/source/formats/ms2/compounds/PcMeshData.py index cc682bdd6..21b55d30d 100644 --- a/source/formats/ms2/compounds/PcMeshData.py +++ b/source/formats/ms2/compounds/PcMeshData.py @@ -57,6 +57,9 @@ def write_pc_array(self, arr): self.buffer_info.verts.write(padding) offset = self.buffer_info.verts.tell() self.buffer_info.verts.write(arr.tobytes()) + # to be safe, also write padding at the end + padding = get_padding(self.buffer_info.verts.tell(), alignment=16) + self.buffer_info.verts.write(padding) return offset // 16 def read_verts(self): diff --git a/version.txt b/version.txt index 801c52eaf..19fad7068 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -710a010a0 - Mon Dec 18 19:19:03 2023 +0100 \ No newline at end of file +ae732fd12 - Mon Dec 18 19:26:20 2023 +0100 \ No newline at end of file