diff --git a/FbxReadWriter.py b/FbxReadWriter.py index 7c17411..091fcfe 100644 --- a/FbxReadWriter.py +++ b/FbxReadWriter.py @@ -104,23 +104,23 @@ def addAnimation(self, pkl_filename:str, smpl_params:Dict, verbose:bool = False) # 3. Write smpl_trans to f_avg_root smpl_trans = smpl_params["smpl_trans"] - name = "m_avg_root" + name = "root" node = lRootNode.FindChild(name) lCurve = node.LclTranslation.GetCurve(lAnimLayer, "X", True) if lCurve: - self._write_curve(lCurve, smpl_trans[:, 0]) + self._write_curve(lCurve, smpl_trans[:, 2]) else: print ("Failed to write {}, {}".format(name, "x")) - lCurve = node.LclTranslation.GetCurve(lAnimLayer, "Y", True) + lCurve = node.LclTranslation.GetCurve(lAnimLayer, "Y", True) # Translation on the Y axis (in blender, this is not the vertical axis but one of the axis that forms the horizontal plane) if lCurve: - self._write_curve(lCurve, smpl_trans[:, 1]) + self._write_curve(lCurve, smpl_trans[:, 0]) else: print ("Failed to write {}, {}".format(name, "y")) lCurve = node.LclTranslation.GetCurve(lAnimLayer, "Z", True) if lCurve: - self._write_curve(lCurve, smpl_trans[:, 2]) + self._write_curve(lCurve, smpl_trans[:, 1]) else: print ("Failed to write {}, {}".format(name, "z")) diff --git a/Pkls/sample.pkl b/Pkls/sample.pkl deleted file mode 100644 index a487832..0000000 Binary files a/Pkls/sample.pkl and /dev/null differ diff --git a/SmplObject.py b/SmplObject.py index abb1df4..f3fc11e 100644 --- a/SmplObject.py +++ b/SmplObject.py @@ -9,36 +9,36 @@ from PathFilter import PathFilter class SmplObjects(object): - joints = ["m_avg_Pelvis" - ,"m_avg_L_Hip" - ,"m_avg_R_Hip" - ,"m_avg_Spine1" - - ,"m_avg_L_Knee" - ,"m_avg_R_Knee" - ,"m_avg_Spine2" - - ,"m_avg_L_Ankle" - ,"m_avg_R_Ankle" - ,"m_avg_Spine3" - - ,"m_avg_L_Foot" - ,"m_avg_R_Foot" - ,"m_avg_Neck" - - ,"m_avg_L_Collar" - ,"m_avg_R_Collar" - - ,"m_avg_Head" - ,"m_avg_L_Shoulder" - ,"m_avg_R_Shoulder" - - ,"m_avg_L_Elbow" - ,"m_avg_R_Elbow" - ,"m_avg_L_Wrist" - ,"m_avg_R_Wrist" - ,"m_avg_L_Hand" - ,"m_avg_R_Hand"] + joints = ["Pelvis" + ,"L_Hip" + ,"R_Hip" + ,"Spine1" + + ,"L_Knee" + ,"R_Knee" + ,"Spine2" + + ,"L_Ankle" + ,"R_Ankle" + ,"Spine3" + + ,"L_Foot" + ,"R_Foot" + ,"Neck" + + ,"L_Collar" + ,"R_Collar" + + ,"Head" + ,"L_Shoulder" + ,"R_Shoulder" + + ,"L_Elbow" + ,"R_Elbow" + ,"L_Wrist" + ,"R_Wrist" + ,"L_Hand" + ,"R_Hand"] def __init__(self, read_path): self.files = {} @@ -50,7 +50,7 @@ def __init__(self, read_path): with open(path, "rb") as fp: data = pickle.load(fp) self.files[filename] = {"smpl_poses":data["smpl_poses"], - "smpl_trans":data["smpl_trans"]} + "smpl_trans":data["smpl_trans"] / (data["smpl_scaling"][0]*100)} self.keys = [key for key in self.files.keys()] def __len__(self): diff --git a/load_smpl_addon/__init__.py b/load_smpl_addon/__init__.py new file mode 100644 index 0000000..e3fd686 --- /dev/null +++ b/load_smpl_addon/__init__.py @@ -0,0 +1,177 @@ +bl_info = { + "name": "Load SMPL from pickle", + "author": "Joachim Tesch, Max Planck Institute for Intelligent Systems", + "version": (2023,6,11), + "blender": (2, 80, 0), + "location": "Viewport > Right panel", + "description": "Load SMPL", + "wiki_url": "https://smpl.is.tue.mpg.de/", + "category": "SMPL"} + + +import os +import pickle +import logging + +import bpy +from bpy.props import ( EnumProperty, PointerProperty, StringProperty) +from bpy.types import ( PropertyGroup ) +from bpy_extras.io_utils import ImportHelper + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +formatter = logging.Formatter('[%(asctime)s] %(message)s', '%Y-%m-%d %H:%M:%S') +stream_handler = logging.StreamHandler() +stream_handler.setFormatter(formatter) +logger.addHandler(stream_handler) + +from .helper import InstallScipy, GetAnimation + +#InstallScipy() +#from scipy.spatial.transform import Rotation as R + +# Blender plugins +class SMPLProperties(PropertyGroup): + smpl_gender: EnumProperty( + name = "Model", + description = "SMPL model", + items = [ ("female", "Female", ""), ("male", "Male", "") ] + ) + +class OpenSmplPklOperator(bpy.types.Operator, ImportHelper): + bl_idname = "scene.smpl_open_pkl" + bl_label = "Open Pickle File" + bl_description = ("Load a smpl file stored as a pickle file") + bl_options = {'REGISTER'} + + filename_ext = ".pkl" + filter_glob: StringProperty(default="*.pkl", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + try: + # Enable button only if in Object Mode + if (context.active_object is None) or (context.active_object.mode == 'OBJECT'): + return True + else: + return False + except: return False + + def execute(self, context): + extension = self.filepath.split(".")[-1] + if extension != "pkl": + self.report({'ERROR'}, f"File should be a pickle file with .pkl extension, get {extension} from {self.filepath}") + return {'CANCELLED'} + + # Read smpl file + smpl_params = None + with open(self.filepath, "rb") as fp: + data = pickle.load(fp) + smpl_params = {"smpl_poses":data["smpl_poses"], + "smpl_trans":data["smpl_trans"] / (data["smpl_scaling"][0]*1)} + + logging.info("Read smpl from {}".format(self.filepath)) + + # Load SMPL model. Codes are from smpl blender plugin: https://smpl.is.tue.mpg.de/ + gender = context.window_manager.smpl_loader.smpl_gender + + path = os.path.dirname(os.path.realpath(__file__)) + objects_path = os.path.join(path, "data", "smpl-model-20200803.blend", "Object") + object_name = "SMPL-mesh-" + gender + + bpy.ops.wm.append(filename=object_name, directory=str(objects_path)) + + # Select collection + object_name = context.selected_objects[-1].name + obj = bpy.data.objects[object_name] + obj.select_set(True) + + # Write animation + rotation_euler_xyz, translation_front_up_right = GetAnimation(smpl_params) + for b in obj.pose.bones: + b.rotation_mode = "XYZ" + obj.animation_data_create() + obj.animation_data.action = bpy.data.actions.new(name="SMPL motion") + + for bone_name, bone_data in rotation_euler_xyz.items(): + fcurve_0 = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["{bone_name}"].rotation_euler', index=0 + ) + fcurve_1 = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["{bone_name}"].rotation_euler', index=1 + ) + fcurve_2 = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["{bone_name}"].rotation_euler', index=2 + ) + for frame in range(1, bone_data.shape[0]+1): + k0 = fcurve_0.keyframe_points.insert(frame=frame, value=bone_data[frame-1, 0]) + k1 = fcurve_1.keyframe_points.insert(frame=frame, value=bone_data[frame-1, 1]) + k2 = fcurve_2.keyframe_points.insert(frame=frame, value=bone_data[frame-1, 2]) + + fcurve_x = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["Pelvis"].location', index=0 + ) + fcurve_y = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["Pelvis"].location', index=1 + ) + fcurve_z = obj.animation_data.action.fcurves.new( + data_path=f'pose.bones["Pelvis"].location', index=2 + ) + for frame in range(1, translation_front_up_right.shape[0]+1): + k0 = fcurve_y.keyframe_points.insert(frame=frame, value=translation_front_up_right[frame-1, 0]) # frony + k1 = fcurve_z.keyframe_points.insert(frame=frame, value=translation_front_up_right[frame-1, 1]) # up + k2 = fcurve_x.keyframe_points.insert(frame=frame, value=translation_front_up_right[frame-1, 2]) # right + + obj.animation_data.action.frame_start = 1 + obj.animation_data.action.frame_end = bone_data.shape[0] + + bpy.context.scene.render.fps = 60 + + return {'FINISHED'} + +class SMPL_PT_Panel(bpy.types.Panel): + bl_label = "SMPL Load File" + bl_category = "SMPL_Load_File" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + + def draw(self, context): + + layout = self.layout + col = layout.column(align=True) + + row = col.row(align=True) + col.prop(context.window_manager.smpl_loader, "smpl_gender") + + col.separator() + col.label(text="Read pickle:") + row = col.row(align=True) + col.operator("scene.smpl_open_pkl", text="Open Pickle") + +classes = [ + SMPLProperties, + OpenSmplPklOperator, + SMPL_PT_Panel, +] + +def register(): + #InstallScipy() + from bpy.utils import register_class + for cls in classes: + bpy.utils.register_class(cls) + + # Store properties under WindowManager (not Scene) so that they are not saved in .blend files and always show default values after loading + bpy.types.WindowManager.smpl_loader = PointerProperty(type=SMPLProperties) + +def unregister(): + #UninstallScipy() + from bpy.utils import unregister_class + for cls in classes: + bpy.utils.unregister_class(cls) + + del bpy.types.WindowManager.smpl_loader + +if __name__ == "__main__": + print ("Add SMPL_Load") + register() \ No newline at end of file diff --git a/load_smpl_addon/data/smpl-model-20200803.blend b/load_smpl_addon/data/smpl-model-20200803.blend new file mode 100755 index 0000000..f7a7cab Binary files /dev/null and b/load_smpl_addon/data/smpl-model-20200803.blend differ diff --git a/load_smpl_addon/data/smpl_joint_regressor_female.npz b/load_smpl_addon/data/smpl_joint_regressor_female.npz new file mode 100755 index 0000000..9f4a8c2 Binary files /dev/null and b/load_smpl_addon/data/smpl_joint_regressor_female.npz differ diff --git a/load_smpl_addon/data/smpl_joint_regressor_male.npz b/load_smpl_addon/data/smpl_joint_regressor_male.npz new file mode 100755 index 0000000..0cd3165 Binary files /dev/null and b/load_smpl_addon/data/smpl_joint_regressor_male.npz differ diff --git a/load_smpl_addon/helper.py b/load_smpl_addon/helper.py new file mode 100644 index 0000000..da51008 --- /dev/null +++ b/load_smpl_addon/helper.py @@ -0,0 +1,63 @@ +import sys +import subprocess +import logging +from typing import Dict + +import numpy as np + +SMPL_JOINT_NAMES = { + 0: 'Pelvis', + 1: 'L_Hip', 4: 'L_Knee', 7: 'L_Ankle', 10: 'L_Foot', + 2: 'R_Hip', 5: 'R_Knee', 8: 'R_Ankle', 11: 'R_Foot', + 3: 'Spine1', 6: 'Spine2', 9: 'Spine3', 12: 'Neck', 15: 'Head', + 13: 'L_Collar', 16: 'L_Shoulder', 18: 'L_Elbow', 20: 'L_Wrist', 22: 'L_Hand', + 14: 'R_Collar', 17: 'R_Shoulder', 19: 'R_Elbow', 21: 'R_Wrist', 23: 'R_Hand', +} +smpl_joints = len(SMPL_JOINT_NAMES) + +# Install dependencies +def InstallScipy(): + try: + from scipy.spatial.transform import Rotation as R + except ModuleNotFoundError as e: + python_exe = sys.executable + subprocess.call([python_exe, "-m", "ensurepip"]) + ret = subprocess.run([python_exe, "-m", "pip", "install", "scipy"]) + if ret.returncode != 0: + logging.error(f"Failed to install scipy") + else: + logging.info(f"Install scipy!") + except Exception as e: + raise e + +InstallScipy() +from scipy.spatial.transform import Rotation as R + +def UninstallScipy(): + python_exe = sys.executable + subprocess.call([python_exe, "-m", "ensurepip"]) + ret = subprocess.run([python_exe, "-m", "pip", "uninstall", "-y", "scipy"]) + if ret.returncode != 0: + logging.error(f"Failed to uninstall scipy") + else: + logging.info(f"Uninstall scipy!") + +def GetAnimation(smpl_params:Dict): + names = [SMPL_JOINT_NAMES[k] for k in sorted(SMPL_JOINT_NAMES.keys())] + + # 1. Write smpl_poses + rotation_euler_xyz = {} + smpl_poses = smpl_params["smpl_poses"] + for idx, name in enumerate(names): + rotvec = smpl_poses[:, idx*3:idx*3+3] + _euler = [] + for _f in range(rotvec.shape[0]): + r = R.from_rotvec([rotvec[_f, 0], rotvec[_f, 1], rotvec[_f, 2]]) + euler = r.as_euler('xyz', degrees=False) + _euler.append([euler[0], euler[1], euler[2]]) + euler = np.vstack(_euler) + rotation_euler_xyz[name] = euler + + translation_front_up_right = smpl_params["smpl_trans"] + + return rotation_euler_xyz, translation_front_up_right \ No newline at end of file diff --git a/readme.md b/readme.md index 307ea21..5baf49c 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,26 @@ # SMPL to FBX + ![](Imgs/teaser.gif) +> 🤖 Update 2023: Support blender addons. + I can convert motions in SMPL format into FBX files. +## Blender addons +Blender addon allows you to load SMPL into Blender without setting up FBX SDK and get the best of Blender's power. + +Blender files and part of codes are based on https://smpl.is.tue.mpg.de/. + +### Install blender addon: +(download zip file from Releases) -> `Edit` -> `Preferences` -> `Add-ons` -> `Install` -> (select the zip file) -> search smpl -> check the inventory of `SMPL: Load SMPL from pickle` + +https://github.com/softcat477/SMPL-to-FBX/assets/25975988/f212d1ed-d7b7-4481-be14-5866f4172075 + +### Load a smpl file: +See the screen recording. + +https://github.com/softcat477/SMPL-to-FBX/assets/25975988/e9405fc4-9748-4d67-9118-9e63e04bd027 + ## Steps 1. Install [Python FBX](https://download.autodesk.com/us/fbx/20112/fbx_sdk_help/index.html?url=WS1a9193826455f5ff453265c9125faa23bbb5fe8.htm,topicNumber=d0e8312). 1. Download the [SMPL fbx model](https://smpl.is.tue.mpg.de) for unity. Keep the male model `SMPL_m_unityDoubleBlends_lbs_10_scale5_207_v1.0.0.fbx`.