Skip to content

Commit

Permalink
Merge pull request #26 from softcat477/blender-addon
Browse files Browse the repository at this point in the history
Support blender addon
  • Loading branch information
softcat477 authored Jul 2, 2023
2 parents 4109fce + da7af62 commit c5e64bc
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 36 deletions.
10 changes: 5 additions & 5 deletions FbxReadWriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down
Binary file removed Pkls/sample.pkl
Binary file not shown.
62 changes: 31 additions & 31 deletions SmplObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand All @@ -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):
Expand Down
177 changes: 177 additions & 0 deletions load_smpl_addon/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file added load_smpl_addon/data/smpl-model-20200803.blend
Binary file not shown.
Binary file not shown.
Binary file not shown.
63 changes: 63 additions & 0 deletions load_smpl_addon/helper.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down

0 comments on commit c5e64bc

Please sign in to comment.