diff --git a/ajc27_freemocap_blender_addon/core_functions/main_controller.py b/ajc27_freemocap_blender_addon/core_functions/main_controller.py index 8ca5881..0fa51d2 100644 --- a/ajc27_freemocap_blender_addon/core_functions/main_controller.py +++ b/ajc27_freemocap_blender_addon/core_functions/main_controller.py @@ -1,7 +1,9 @@ -from pathlib import Path import traceback +from pathlib import Path from .mesh.skelly_mesh.attach_skelly_mesh import attach_skelly_mesh_to_rig +from .rig.save_bone_and_joint_angles_from_rig import save_bone_and_joint_angles_from_rig + from ..core_functions.bones.enforce_rigid_bones import enforce_rigid_bones from ..core_functions.empties.creation.create_freemocap_empties import ( create_freemocap_empties, @@ -39,7 +41,7 @@ def __init__(self, recording_path: str, save_path: str, config: Config): self.save_path = save_path self.recording_name = Path(self.recording_path).stem self.origin_name = f"{self.recording_name}_origin" - self.rig_name = "rig" + self.rig_name = f"{self.recording_name}_rig" self._create_parent_empties() self.freemocap_data_handler = get_or_create_freemocap_data_handler( recording_path=self.recording_path @@ -49,10 +51,12 @@ def __init__(self, recording_path: str, save_path: str, config: Config): def _create_parent_empties(self): self.data_parent_object = create_freemocap_parent_empty(name=self.origin_name) self._empty_parent_object = create_freemocap_parent_empty( - name=f"empties", parent_object=self.data_parent_object + name=f"empties_parent", + parent_object=self.data_parent_object ) self._video_parent_object = create_freemocap_parent_empty( - name=f"videos", parent_object=self.data_parent_object + name=f"videos_parent", + parent_object=self.data_parent_object ) def load_freemocap_data(self): @@ -155,6 +159,21 @@ def add_rig(self): print(e) raise e + def save_bone_and_joint_data_from_rig(self): + try: + print("Saving joint angles...") + csv_file_path = str(Path(self.save_path).parent / "saved_data" / f"{self.recording_name}_bone_and_joint_data.csv") + save_bone_and_joint_angles_from_rig( + rig_name=self.rig_name, + csv_save_path=csv_file_path, + start_frame=0, + end_frame=self.freemocap_data_handler.number_of_frames, + ) + except Exception as e: + print(f"Failed to save joint angles: {e}") + print(e) + raise e + def attach_rigid_body_mesh_to_rig(self): try: print("Adding rigid_body_bone_meshes...") @@ -204,6 +223,7 @@ def run_all(self): self.save_data_to_disk() self.create_empties() self.add_rig() + self.save_bone_and_joint_data_from_rig() self.attach_rigid_body_mesh_to_rig() self.attach_skelly_mesh_to_rig() self.add_videos() @@ -218,7 +238,7 @@ def setup_scene(self): for area in window.screen.areas: # iterate through areas in current screen if area.type == "VIEW_3D": for ( - space + space ) in area.spaces: # iterate through spaces in current VIEW_3D area if space.type == "VIEW_3D": # check if space is a 3D view space.shading.type = "MATERIAL" diff --git a/ajc27_freemocap_blender_addon/core_functions/mesh/rigid_body_mesh/helpers/make_bone_mesh.py b/ajc27_freemocap_blender_addon/core_functions/mesh/rigid_body_mesh/helpers/make_bone_mesh.py index 46be2d7..c458b8a 100644 --- a/ajc27_freemocap_blender_addon/core_functions/mesh/rigid_body_mesh/helpers/make_bone_mesh.py +++ b/ajc27_freemocap_blender_addon/core_functions/mesh/rigid_body_mesh/helpers/make_bone_mesh.py @@ -12,8 +12,8 @@ def make_cone_mesh(name: str = "cone_mesh", emission_strength: float = 1.0, transmittance: float = 0.0, vertices: int = 8, - radius1: float = 0.05, - radius2: float = 0.05, + radius1: float = 0.035, + radius2: float = 0.035, depth: float = 1, end_fill_type: str = 'TRIFAN', align: str = 'WORLD', @@ -74,7 +74,7 @@ def make_cone_mesh(name: str = "cone_mesh", def make_joint_sphere_mesh(name: str = "joint_sphere_mesh", subdivisions: int = 2, - radius: float = 0.1, + radius: float = 0.075, align: str = 'WORLD', location: tuple = (0, 0, 0), scale: tuple = (1, 1, 1), @@ -101,7 +101,7 @@ def make_bone_mesh(name: str = "bone_mesh", joint_color: Union[str, Tuple, List, np.ndarray] = "#aa0055", cone_color: Union[str, Tuple, List, np.ndarray] = "#00FFFF", axis_visible: bool = True, - squish_scale: tuple = (.8, 1, 1), + squish_scale: tuple = (.6, 1, 1), length: float = 1, ) -> bpy.types.Object: cone = make_cone_mesh(name=f"{name}_cone_mesh", diff --git a/ajc27_freemocap_blender_addon/core_functions/rig/save_bone_and_joint_angles_from_rig.py b/ajc27_freemocap_blender_addon/core_functions/rig/save_bone_and_joint_angles_from_rig.py new file mode 100644 index 0000000..19e7fb5 --- /dev/null +++ b/ajc27_freemocap_blender_addon/core_functions/rig/save_bone_and_joint_angles_from_rig.py @@ -0,0 +1,130 @@ +import csv +from pathlib import Path +from typing import Dict +from ajc27_freemocap_blender_addon.data_models.bones.bone_constraints import BONES_CONSTRAINTS + +import bpy + + +def save_bone_and_joint_angles_from_rig(rig_name: str, + csv_save_path: str, + start_frame: int, + end_frame: int): + Path(csv_save_path).parent.mkdir(parents=True, exist_ok=True) + documenation_save_path = Path(csv_save_path).parent / "_BONE_AND_JOINT_DATA_README.md" + + rig = bpy.data.objects[rig_name] + + if rig.type != 'ARMATURE': + raise TypeError(f"Object {rig_name} is not an armature!") + all_bone_data = {} + for frame_number in range(start_frame, end_frame + 1): + bpy.context.scene.frame_set(frame_number) + frame_data = {} + all_bone_data[frame_number] = frame_data + for bone in rig.pose.bones: + if bone.name not in BONES_CONSTRAINTS.keys(): + continue + frame_data[bone.name] = get_bone_data(bone) + + # Save as csv + column_names = [] + for bone_key in all_bone_data[0].keys(): + for data_name in list(next(iter(frame_data.values())).keys()): + column_names.append(f"{bone_key}_{data_name}") + + with open(csv_save_path, 'w', newline='') as file: + writer = csv.DictWriter(file, fieldnames=column_names) + writer.writeheader() + for frame_data in all_bone_data.values(): + row_data = [] + for bone_data in frame_data.values(): + row_data.extend(bone_data.values()) + column_data_mappping = dict(zip(column_names, row_data)) + writer.writerow(column_data_mappping) + + # Save documentation + with open(documenation_save_path, 'w') as file: + file.write(DOCUMENTATION_STRING) + + +def get_bone_data(bone: bpy.types.PoseBone) -> Dict[str, float]: + return { + "head_center_world_x": bone.head.x, + "head_center_world_y": bone.head.y, + "head_center_world_z": bone.head.z, + "tail_center_world_x": bone.tail.x, + "tail_center_world_y": bone.tail.y, + "tail_center_world_z": bone.tail.z, + "rotation_quaternion_x":bone.matrix.to_quaternion().x, + "rotation_quaternion_y":bone.matrix.to_quaternion().y, + "rotation_quaternion_z":bone.matrix.to_quaternion().z, + "rotation_quaternion_w":bone.matrix.to_quaternion().w, + "rotation_euler_x": bone.matrix.to_euler().x, + "rotation_euler_y": bone.matrix.to_euler().y, + "rotation_euler_z": bone.matrix.to_euler().z, + "rotation_euler_order": bone.matrix.to_euler().order, + "matrix_0_0": bone.matrix[0][0], + "matrix_0_1": bone.matrix[0][1], + "matrix_0_2": bone.matrix[0][2], + "matrix_0_3": bone.matrix[0][3], + "matrix_1_0": bone.matrix[1][0], + "matrix_1_1": bone.matrix[1][1], + "matrix_1_2": bone.matrix[1][2], + "matrix_1_3": bone.matrix[1][3], + "matrix_2_0": bone.matrix[2][0], + "matrix_2_1": bone.matrix[2][1], + "matrix_2_2": bone.matrix[2][2], + "matrix_2_3": bone.matrix[2][3], + "matrix_3_0": bone.matrix[3][0], + "matrix_3_1": bone.matrix[3][1], + "matrix_3_2": bone.matrix[3][2], + "matrix_3_3": bone.matrix[3][3], + } + + +DOCUMENTATION_STRING = """ +# Bone data + +This document describes the data that is saved for each bone in the rig. + +All of this data is derived from a Blender Armature object based on a slightly tweaked version of the Rigify rig. + +For multi-part data (e.g. XYZ location), each component of the data is saved in its own column: e.g. {bone_name}_x, {bone_name}_y, {bone_name}_z + +To access the bone data in Blender, use the following command (e.g. to get the Thigh.L bone) in the Blender python console (e.g. in the Scripting Tab): `bone = bpy.context.object["righ"].pose.bone["thigh.L"]` + +Theses are the properties of the bone object that are saved: + +# Bone Properties + +## `bone.head` : location data - X, Y, Z (tuple of floats) +The location of the head of the bone in world space (e.g. (0.0, 0.0, 0.0)) +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.head + +## `bone.tail` : location data - X, Y, Z (tuple of floats) +The location of the tail of the bone in world space (e.g. (0.0, 0.0, 0.0)) +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.tail + + +## `bone.rotation_quaternion['_x', '_y', '_z', '_w']` : quaternion - X, Y, Z, W (tuple of floats) +The rotation of the bone in quaternion space (e.g. (0.0, 0.0, 0.0, 1.0)) +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.rotation_quaternion +https://en.wikipedia.org/wiki/Quaternion + +## `bone.rotation_euler['_x', '_y', '_z']` : euler rotation - X, Y, Z (tuple of floats) +The rotation of the bone in euler space (e.g. (0.0, 0.0, 0.0)) +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.rotation_euler +https://en.wikipedia.org/wiki/Euler_angles + +## `bone.rotation_euler.order` : str +The order of the euler rotation (e.g. 'XYZ') +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.rotation_euler +https://en.wikipedia.org/wiki/Euler_angles + +## `bone.matrix` : 4x4 matrix (tuple of tuples of floats) +The transformation matrix of the bone in world space after constraints and drivers are applied, in the armature object space +https://docs.blender.org/api/current/bpy.types.PoseBone.html#bpy.types.PoseBone.matrix +https://en.wikipedia.org/wiki/Transformation_matrix + +""" diff --git a/ajc27_freemocap_blender_addon/core_functions/rig/save_joint_angles_from_rig.py b/ajc27_freemocap_blender_addon/core_functions/rig/save_joint_angles_from_rig.py deleted file mode 100644 index e69de29..0000000 diff --git a/ajc27_freemocap_blender_addon/core_functions/setup_scene/make_parent_empties.py b/ajc27_freemocap_blender_addon/core_functions/setup_scene/make_parent_empties.py index 68b6737..3ae67e1 100644 --- a/ajc27_freemocap_blender_addon/core_functions/setup_scene/make_parent_empties.py +++ b/ajc27_freemocap_blender_addon/core_functions/setup_scene/make_parent_empties.py @@ -9,7 +9,8 @@ def create_freemocap_parent_empty(name: str = "freemocap_data_parent_empty", parent_object: bpy.types.Object = None): print("Creating freemocap parent empty...") bpy.ops.object.empty_add(type="ARROWS") - parent_empty = bpy.context.active_object + parent_empty = bpy.context.active_object + parent_empty.name = name if parent_object is not None: print(f"Setting parent of {parent_empty.name} to {parent_object.name}") diff --git a/pyproject.toml b/pyproject.toml index c44d522..8399031 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ajc27_freemocap_blender_addon" -version = "0.2.6" +version = "0.2.7" description = "A Blender Add-on for working with Freemocap Data (based on @ajc27's addon)" authors = ["Skelly FreeMoCap "] license = "AGPLv3"