diff --git a/__init__.py b/__init__.py index 9e0eaa8..cb4bed4 100644 --- a/__init__.py +++ b/__init__.py @@ -2,8 +2,8 @@ "name": "Sequence Loader", "description": "Loader for meshio supported mesh files/ simulation sequences", "author": "Interactive Computer Graphics", - "version": (0, 1, 3), - "blender": (3, 1, 0), + "version": (0, 1, 4), + "blender": (3, 4, 0), "warning": "", "support": "COMMUNITY", "category": "Import-Export", @@ -21,6 +21,7 @@ bpy.context.preferences.filepaths.use_relative_paths = False from bseq import * +from bseq.operators import menu_func_import classes = [ BSEQ_obj_property, @@ -44,6 +45,9 @@ BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all, + BSEQ_OT_refresh_sequences, + WM_OT_batchSequences, + WM_OT_MeshioObject ] @@ -56,6 +60,7 @@ def register(): bpy.types.Object.BSEQ = bpy.props.PointerProperty(type=BSEQ_obj_property) bpy.types.Mesh.BSEQ = bpy.props.PointerProperty(type=BSEQ_mesh_property) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) # manually call this function once # so when addon being installed, it can run correctly @@ -69,6 +74,7 @@ def unregister(): del bpy.types.Scene.BSEQ del bpy.types.Object.BSEQ bpy.app.handlers.load_post.remove(BSEQ_initialize) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) unsubscribe_to_selected() diff --git a/bseq/__init__.py b/bseq/__init__.py index 7740e59..361914c 100644 --- a/bseq/__init__.py +++ b/bseq/__init__.py @@ -1,5 +1,5 @@ from bseq.utils import refresh_obj -from .operators import BSEQ_OT_load, BSEQ_OT_edit, BSEQ_OT_resetpt, BSEQ_OT_resetmesh, BSEQ_OT_resetins, BSEQ_OT_set_as_split_norm, BSEQ_OT_remove_split_norm, BSEQ_OT_disable_selected, BSEQ_OT_enable_selected, BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all +from .operators import BSEQ_OT_load, BSEQ_OT_edit, BSEQ_OT_resetpt, BSEQ_OT_resetmesh, BSEQ_OT_resetins, BSEQ_OT_set_as_split_norm, BSEQ_OT_remove_split_norm, BSEQ_OT_disable_selected, BSEQ_OT_enable_selected, BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all, BSEQ_OT_refresh_sequences, WM_OT_batchSequences, WM_OT_MeshioObject from .properties import BSEQ_scene_property, BSEQ_obj_property, BSEQ_mesh_property from .panels import BSEQ_UL_Obj_List, BSEQ_List_Panel, BSEQ_Settings, BSEQ_Import, BSEQ_Templates, BSEQ_UL_Att_List, draw_template from .messenger import subscribe_to_selected, unsubscribe_to_selected @@ -45,4 +45,7 @@ def BSEQ_initialize(scene): "BSEQ_OT_refresh_seq", "BSEQ_OT_disable_all", "BSEQ_OT_enable_all", + "BSEQ_OT_refresh_sequences", + "WM_OT_batchSequences", + "WM_OT_MeshioObject" ] diff --git a/bseq/importer.py b/bseq/importer.py index 1897ec4..d5dbc93 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -1,7 +1,9 @@ import bpy +import mathutils import meshio import traceback import fileseq +import os from .utils import show_message_box import numpy as np from mathutils import Matrix @@ -51,6 +53,37 @@ def extract_faces(cell: meshio.CellBlock): show_message_box(cell.type + " is unsupported mesh format yet") return np.array([]) +def has_keyframe(obj, attr): + animdata = obj.animation_data + if animdata is not None and animdata.action is not None: + for fcurve in animdata.action.fcurves: + if fcurve.data_path == attr: + return len(fcurve.keyframe_points) > 0 + return False + +def apply_transformation(meshio_mesh, obj, depsgraph): + # evaluate the keyframe animation system + eval_location = obj.evaluated_get(depsgraph).location if has_keyframe(obj, "location") else None + eval_scale = obj.evaluated_get(depsgraph).scale if has_keyframe(obj, "scale") else None + + if has_keyframe(obj, "rotation_quaternion"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_quaternion + elif has_keyframe(obj, "rotation_axis_angle"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_axis_angle + elif has_keyframe(obj, "rotation_euler"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_euler + else: + eval_rotation = None + + eval_transform_matrix = mathutils.Matrix.LocRotScale(eval_location, eval_rotation, eval_scale) + + # evaluate the rigid body transformations (only relevant for .bin format) + rigid_body_transformation = mathutils.Matrix.Identity(4) + if meshio_mesh.field_data.get("transformation_matrix") is not None: + rigid_body_transformation = meshio_mesh.field_data["transformation_matrix"] + + # multiply everything together (with custom transform matrix) + obj.matrix_world = rigid_body_transformation @ obj.BSEQ.initial_transform_matrix @ eval_transform_matrix def update_mesh(meshio_mesh, mesh): # extract information from the meshio mesh @@ -59,7 +92,8 @@ def update_mesh(meshio_mesh, mesh): n_poly = 0 n_loop = 0 n_verts = len(mesh_vertices) - + if n_verts == 0: + return faces_loop_start = np.array([], dtype=np.uint64) faces_loop_total = np.array([], dtype=np.uint64) loops_vert_idx = np.array([], dtype=np.uint64) @@ -136,8 +170,27 @@ def update_mesh(meshio_mesh, mesh): mesh.use_auto_smooth = True mesh.normals_split_custom_set_from_vertices(v) +# function to create a single meshio object +def create_meshio_obj(filepath): + meshio_mesh = None + try: + meshio_mesh = meshio.read(filepath) + except Exception as e: + show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), + "Meshio Loading Error" + str(e), + icon="ERROR") + + # create the object + name = os.path.basename(filepath) + mesh = bpy.data.meshes.new(name) + object = bpy.data.objects.new(name, mesh) + update_mesh(meshio_mesh, object.data) + bpy.context.collection.objects.link(object) + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = object -def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])): + +def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])): current_frame = bpy.context.scene.frame_current filepath = fileseq[current_frame % len(fileseq)] @@ -166,7 +219,8 @@ def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, object.BSEQ.pattern = str(fileseq) object.BSEQ.init = True object.BSEQ.enabled = enabled - object.matrix_world = transform_matrix + # Flatten custom transformation matrix for the property + object.BSEQ.initial_transform_matrix = [transform_matrix[j][i] for i in range(4) for j in range(4)] driver = object.driver_add("BSEQ.frame") driver.driver.expression = 'frame' if enabled: @@ -247,14 +301,5 @@ def update_obj(scene, depsgraph=None): continue update_mesh(meshio_mesh, obj.data) - # force to evaluate the keyframe animation system - obj.location = obj.evaluated_get(depsgraph).location - match obj.rotation_mode: - case "QUATERNION": - obj.rotation_quaternion = obj.evaluated_get(depsgraph).rotation_quaternion - case "AXIS_ANGLE": - obj.rotation_axis_angle = obj.evaluated_get(depsgraph).rotation_axis_angle - case _: - obj.rotation_euler = obj.evaluated_get(depsgraph).rotation_euler - - obj.scale = obj.evaluated_get(depsgraph).scale + apply_transformation(meshio_mesh, obj, depsgraph) + \ No newline at end of file diff --git a/bseq/operators.py b/bseq/operators.py index a469aba..0190329 100644 --- a/bseq/operators.py +++ b/bseq/operators.py @@ -1,9 +1,10 @@ import bpy +from mathutils import Matrix import fileseq from .messenger import * import traceback from .utils import refresh_obj, show_message_box -from .importer import create_obj +from .importer import create_obj, create_meshio_obj import numpy as np @@ -41,7 +42,13 @@ def execute(self, context): show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") return {"CANCELLED"} - create_obj(fs, importer_prop.relative, importer_prop.root_path) + transform_matrix = (Matrix.LocRotScale( + importer_prop.custom_location, + importer_prop.custom_rotation, + importer_prop.custom_scale) + if importer_prop.use_custom_transform else Matrix.Identity(4)) + + create_obj(fs, importer_prop.relative, importer_prop.root_path, transform_matrix=transform_matrix) return {"FINISHED"} @@ -297,3 +304,68 @@ def execute(self, context): if obj.BSEQ.init and not obj.BSEQ.enabled: obj.BSEQ.enabled = True return {"FINISHED"} + +class BSEQ_OT_refresh_sequences(bpy.types.Operator): + '''This operator refreshes all found sequences''' + bl_label = "" #"Refresh Found Sequences" + bl_idname = "bseq.refreshseqs" + bl_options = {"UNDO"} + + def execute(self, context): + scene = context.scene + # call the update function of path by setting it to its own value + scene.BSEQ.path = scene.BSEQ.path + return {"FINISHED"} + +from pathlib import Path +import meshio +from bpy_extras.io_utils import ImportHelper + +class WM_OT_batchSequences(bpy.types.Operator, ImportHelper): + """Batch Import Sequences""" + bl_idname = "wm.seq_import_batch" + bl_label = "Import multiple sequences" + bl_options = {'PRESET', 'UNDO'} + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def execute(self, context): + scene = context.scene + importer_prop = scene.BSEQ + + folder = Path(self.filepath) + used_seqs = set() + + for selection in self.files: + # Check if there exists a matching file sequence for every selection + fp = str(Path(folder.parent, selection.name)) + seqs = fileseq.findSequencesOnDisk(str(folder.parent)) + matching_seqs = [s for s in seqs if fp in list(s) and s not in used_seqs] + + if matching_seqs: + transform_matrix = (Matrix.LocRotScale(importer_prop.custom_location, importer_prop.custom_rotation, importer_prop.custom_scale) + if importer_prop.use_custom_transform else Matrix.Identity(4)) + create_obj(matching_seqs[0], False, importer_prop.root_path, transform_matrix=transform_matrix) + used_seqs.add(matching_seqs[0]) + return {'FINISHED'} + +class WM_OT_MeshioObject(bpy.types.Operator, ImportHelper): + """Batch Import Meshio Objects""" + bl_idname = "wm.meshio_import_batch" + bl_label = "Import multiple Meshio objects" + bl_options = {'PRESET', 'UNDO'} + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def execute(self, context): + folder = Path(self.filepath) + + for selection in self.files: + fp = Path(folder.parent, selection.name) + create_meshio_obj(str(fp)) + return {'FINISHED'} + +def menu_func_import(self, context): + self.layout.operator( + WM_OT_MeshioObject.bl_idname, + text="MeshIO Object") diff --git a/bseq/panels.py b/bseq/panels.py index 43d4f82..c35ec7a 100644 --- a/bseq/panels.py +++ b/bseq/panels.py @@ -191,16 +191,23 @@ def draw(self, context): if importer_prop.use_pattern: col2.prop(importer_prop, "pattern", text="") else: - col2.prop(importer_prop, "fileseq", text="") + split2 = col2.split(factor=0.75) + col3 = split2.column() + col4 = split2.column() + col3.prop(importer_prop, "fileseq", text="") + col4.operator("bseq.refreshseqs", icon="FILE_REFRESH") col1.label(text="Use Relative Path") col2.prop(importer_prop, "relative", text="") - if importer_prop.relative is True: + if importer_prop.relative: col1.label(text="Root Directory") col2.prop(importer_prop, "root_path", text="") layout.operator("sequence.load") + + layout.operator("wm.seq_import_batch") + split = layout.split() col1 = split.column() col2 = split.column() @@ -227,7 +234,19 @@ def draw(self, context): col2.prop(importer_prop, "print", text="") col1.label(text="Auto refresh all the sequence every frame") col2.prop(importer_prop, "auto_refresh", text="") + col1.label(text="Use custom transformation matrix") + col2.prop(importer_prop, "use_custom_transform", text="") + + if importer_prop.use_custom_transform: + box.label(text="Location:") + box.prop(importer_prop, "custom_location", text="") + + box.label(text="Rotation:") + box.prop(importer_prop, "custom_rotation", text="") + box.label(text="Scale:") + box.prop(importer_prop, "custom_scale", text="") + class BSEQ_Templates(bpy.types.Menu): ''' diff --git a/bseq/properties.py b/bseq/properties.py index ed79cdb..d839791 100644 --- a/bseq/properties.py +++ b/bseq/properties.py @@ -1,6 +1,6 @@ import bpy from .callback import * - +from mathutils import Matrix class BSEQ_scene_property(bpy.types.PropertyGroup): path: bpy.props.StringProperty(name="Directory", @@ -22,6 +22,10 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): default=False) pattern: bpy.props.StringProperty(name="Pattern", description="You can specify the pattern here, in case the sequence can't be deteced.") + + file_paths: bpy.props.StringProperty(name="File", + subtype="FILE_PATH", + description="Select a root folder for all relative paths. When not set the current filename is used.") selected_obj_deselectall_flag: bpy.props.BoolProperty(default=True, description="the flag to determine whether call deselect all or not ") @@ -48,7 +52,27 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): auto_refresh: bpy.props.BoolProperty(name='auto refresh', description="whether or not to auto refresh all the sequence every frame", default=False) + + use_custom_transform: bpy.props.BoolProperty(name='Use custom transformation matrix', + description="Whether or not to use a custom transformation matrix", + default=False) + custom_location: bpy.props.FloatVectorProperty(name='Custom Location', + description='Set custom location vector', + size=3, + subtype="TRANSLATION") + + custom_rotation: bpy.props.FloatVectorProperty(name='Custom Rotation', + description='Set custom rotation vector', + size=3, + subtype="EULER", + default=[0,0,0]) + + custom_scale: bpy.props.FloatVectorProperty(name='Custom Scale', + description='Set custom scaling vector', + size=3, + subtype="COORDINATES", + default=[1,1,1]) class BSEQ_obj_property(bpy.types.PropertyGroup): init: bpy.props.BoolProperty(default=False) @@ -59,7 +83,10 @@ class BSEQ_obj_property(bpy.types.PropertyGroup): use_relative: bpy.props.BoolProperty(default=False) pattern: bpy.props.StringProperty() frame: bpy.props.IntProperty() - + initial_transform_matrix: bpy.props.FloatVectorProperty(name='Custom Transformation Matrix', + description='Set custom transformation', + size=16, + subtype="MATRIX") # set this property for mesh, not object (maybe change later?) class BSEQ_mesh_property(bpy.types.PropertyGroup):