2011-06-25 23:50:50 +00:00
|
|
|
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU General Public License
|
|
|
|
# as published by the Free Software Foundation; either version 2
|
|
|
|
# of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software Foundation,
|
|
|
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
#
|
|
|
|
# ##### END GPL LICENSE BLOCK #####
|
|
|
|
|
|
|
|
# <pep8 compliant>
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
import bpy
|
|
|
|
|
|
|
|
from bpy.props import *
|
|
|
|
from bpy import *
|
2011-07-04 11:35:29 +00:00
|
|
|
import mocap_constraints
|
|
|
|
import retarget
|
|
|
|
import mocap_tools
|
|
|
|
### reloads modules (for testing purposes only)
|
|
|
|
from imp import reload
|
|
|
|
reload(mocap_constraints)
|
|
|
|
reload(retarget)
|
|
|
|
reload(mocap_tools)
|
|
|
|
|
2011-07-02 18:24:05 +00:00
|
|
|
from mocap_constraints import *
|
2011-06-24 17:05:53 +00:00
|
|
|
|
2011-06-29 14:29:52 +00:00
|
|
|
# MocapConstraint class
|
|
|
|
# Defines MocapConstraint datatype, used to add and configute mocap constraints
|
|
|
|
# Attached to Armature data
|
|
|
|
|
|
|
|
|
|
|
|
class MocapConstraint(bpy.types.PropertyGroup):
|
2011-07-02 18:24:05 +00:00
|
|
|
name = bpy.props.StringProperty(name="Name",
|
2011-07-29 18:23:16 +00:00
|
|
|
default="Mocap Fix",
|
|
|
|
description="Name of Mocap Fix",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-07-02 18:24:05 +00:00
|
|
|
constrained_bone = bpy.props.StringProperty(name="Bone",
|
|
|
|
default="",
|
|
|
|
description="Constrained Bone",
|
2011-07-03 21:25:54 +00:00
|
|
|
update=updateConstraintBoneType)
|
2011-07-02 18:24:05 +00:00
|
|
|
constrained_boneB = bpy.props.StringProperty(name="Bone (2)",
|
|
|
|
default="",
|
|
|
|
description="Other Constrained Bone (optional, depends on type)",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-07-02 18:24:05 +00:00
|
|
|
s_frame = bpy.props.IntProperty(name="S",
|
2011-08-03 18:17:33 +00:00
|
|
|
default=0,
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Start frame of Fix",
|
2011-07-14 13:26:23 +00:00
|
|
|
update=setConstraint)
|
2011-07-02 18:24:05 +00:00
|
|
|
e_frame = bpy.props.IntProperty(name="E",
|
2011-08-03 18:17:33 +00:00
|
|
|
default=100,
|
2011-07-29 18:23:16 +00:00
|
|
|
description="End frame of Fix",
|
2011-07-14 13:26:23 +00:00
|
|
|
update=setConstraint)
|
2011-07-03 21:25:54 +00:00
|
|
|
smooth_in = bpy.props.IntProperty(name="In",
|
|
|
|
default=10,
|
|
|
|
description="Amount of frames to smooth in",
|
2011-07-14 13:26:23 +00:00
|
|
|
update=setConstraint,
|
2011-07-03 21:25:54 +00:00
|
|
|
min=0)
|
|
|
|
smooth_out = bpy.props.IntProperty(name="Out",
|
|
|
|
default=10,
|
|
|
|
description="Amount of frames to smooth out",
|
2011-07-14 13:26:23 +00:00
|
|
|
update=setConstraint,
|
2011-07-03 21:25:54 +00:00
|
|
|
min=0)
|
2011-07-02 18:24:05 +00:00
|
|
|
targetMesh = bpy.props.StringProperty(name="Mesh",
|
|
|
|
default="",
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Target of Fix - Mesh (optional, depends on type)",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-07-02 18:24:05 +00:00
|
|
|
active = bpy.props.BoolProperty(name="Active",
|
|
|
|
default=True,
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Fix is active",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-07-22 18:46:13 +00:00
|
|
|
show_expanded = bpy.props.BoolProperty(name="Show Expanded",
|
|
|
|
default=True,
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Fix is fully shown")
|
2011-07-02 18:24:05 +00:00
|
|
|
targetPoint = bpy.props.FloatVectorProperty(name="Point", size=3,
|
|
|
|
subtype="XYZ", default=(0.0, 0.0, 0.0),
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Target of Fix - Point",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-07-16 13:36:47 +00:00
|
|
|
targetDist = bpy.props.FloatProperty(name="Offset",
|
|
|
|
default=0.0,
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Distance and Floor Fixes - Desired offset",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-06-29 14:29:52 +00:00
|
|
|
targetSpace = bpy.props.EnumProperty(
|
2011-07-04 11:35:29 +00:00
|
|
|
items=[("WORLD", "World Space", "Evaluate target in global space"),
|
|
|
|
("LOCAL", "Object space", "Evaluate target in object space"),
|
2011-07-02 18:24:05 +00:00
|
|
|
("constrained_boneB", "Other Bone Space", "Evaluate target in specified other bone space")],
|
|
|
|
name="Space",
|
|
|
|
description="In which space should Point type target be evaluated",
|
2011-07-09 21:52:25 +00:00
|
|
|
update=setConstraint)
|
2011-06-29 14:29:52 +00:00
|
|
|
type = bpy.props.EnumProperty(name="Type of constraint",
|
2011-07-02 18:24:05 +00:00
|
|
|
items=[("point", "Maintain Position", "Bone is at a specific point"),
|
2011-06-29 14:29:52 +00:00
|
|
|
("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
|
|
|
|
("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
|
|
|
|
("distance", "Maintain distance", "Target bones maintained specified distance")],
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Type of Fix",
|
2011-07-03 21:25:54 +00:00
|
|
|
update=updateConstraintBoneType)
|
2011-07-02 18:24:05 +00:00
|
|
|
real_constraint = bpy.props.StringProperty()
|
|
|
|
real_constraint_bone = bpy.props.StringProperty()
|
2011-06-29 14:29:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
bpy.utils.register_class(MocapConstraint)
|
|
|
|
|
|
|
|
bpy.types.Armature.mocap_constraints = bpy.props.CollectionProperty(type=MocapConstraint)
|
|
|
|
|
2011-08-08 11:09:56 +00:00
|
|
|
|
|
|
|
class AnimationStitchSettings(bpy.types.PropertyGroup):
|
|
|
|
first_action = bpy.props.StringProperty(name="Action 1",
|
|
|
|
description="First action in stitch")
|
|
|
|
second_action = bpy.props.StringProperty(name="Action 2",
|
|
|
|
description="Second action in stitch")
|
|
|
|
blend_frame = bpy.props.IntProperty(name="Stitch frame",
|
|
|
|
description="Frame to locate stitch on")
|
|
|
|
blend_amount = bpy.props.IntProperty(name="Blend amount",
|
|
|
|
description="Size of blending transitiion, on both sides of the stitch",
|
|
|
|
default=10)
|
|
|
|
|
|
|
|
bpy.utils.register_class(AnimationStitchSettings)
|
|
|
|
|
|
|
|
|
|
|
|
class MocapNLATracks(bpy.types.PropertyGroup):
|
|
|
|
name = bpy.props.StringProperty()
|
|
|
|
active = bpy.props.BoolProperty()
|
|
|
|
base_track = bpy.props.StringProperty()
|
|
|
|
auto_fix_track = bpy.props.StringProperty()
|
|
|
|
manual_fix_track = bpy.props.StringProperty()
|
|
|
|
stride_action = bpy.props.StringProperty()
|
|
|
|
|
|
|
|
bpy.utils.register_class(MocapNLATracks)
|
|
|
|
|
|
|
|
bpy.types.Armature.stitch_settings = bpy.props.PointerProperty(type=AnimationStitchSettings)
|
|
|
|
|
|
|
|
bpy.types.Armature.mocapNLATracks = bpy.props.CollectionProperty(type=MocapNLATracks)
|
|
|
|
|
2011-06-29 14:29:52 +00:00
|
|
|
#Update function for IK functionality. Is called when IK prop checkboxes are toggled.
|
|
|
|
|
|
|
|
|
2011-06-27 12:46:53 +00:00
|
|
|
def toggleIKBone(self, context):
|
|
|
|
if self.IKRetarget:
|
|
|
|
if not self.is_in_ik_chain:
|
|
|
|
print(self.name + " IK toggled ON!")
|
|
|
|
ik = self.constraints.new('IK')
|
2011-06-29 14:29:52 +00:00
|
|
|
#ik the whole chain up to the root, excluding
|
2011-07-04 11:35:29 +00:00
|
|
|
chainLen = 0
|
|
|
|
for parent_bone in self.parent_recursive:
|
2011-07-05 10:57:29 +00:00
|
|
|
chainLen += 1
|
2011-07-04 11:35:29 +00:00
|
|
|
if hasIKConstraint(parent_bone):
|
|
|
|
break
|
2011-07-05 10:57:29 +00:00
|
|
|
deformer_children = [child for child in parent_bone.children if child.bone.use_deform]
|
2011-08-03 18:17:33 +00:00
|
|
|
#~ if len(deformer_children) > 1:
|
|
|
|
#~ break
|
2011-06-27 12:46:53 +00:00
|
|
|
ik.chain_count = chainLen
|
|
|
|
for bone in self.parent_recursive:
|
|
|
|
if bone.is_in_ik_chain:
|
|
|
|
bone.IKRetarget = True
|
|
|
|
else:
|
|
|
|
print(self.name + " IK toggled OFF!")
|
2011-08-03 18:17:33 +00:00
|
|
|
cnstrn_bones = []
|
|
|
|
newChainLength = []
|
2011-06-27 12:46:53 +00:00
|
|
|
if hasIKConstraint(self):
|
2011-08-03 18:17:33 +00:00
|
|
|
cnstrn_bones = [self]
|
2011-06-27 12:46:53 +00:00
|
|
|
elif self.is_in_ik_chain:
|
2011-08-03 18:17:33 +00:00
|
|
|
cnstrn_bones = [child for child in self.children_recursive if hasIKConstraint(child)]
|
|
|
|
for cnstrn_bone in cnstrn_bones:
|
|
|
|
newChainLength.append(cnstrn_bone.parent_recursive.index(self) + 1)
|
|
|
|
if cnstrn_bones:
|
2011-06-27 12:46:53 +00:00
|
|
|
# remove constraint, and update IK retarget for all parents of cnstrn_bone up to chain_len
|
2011-08-03 18:17:33 +00:00
|
|
|
for i, cnstrn_bone in enumerate(cnstrn_bones):
|
|
|
|
print(cnstrn_bone.name)
|
|
|
|
if newChainLength:
|
|
|
|
ik = hasIKConstraint(cnstrn_bone)
|
|
|
|
ik.chain_count = newChainLength[i]
|
|
|
|
else:
|
|
|
|
ik = hasIKConstraint(cnstrn_bone)
|
|
|
|
cnstrn_bone.constraints.remove(ik)
|
|
|
|
cnstrn_bone.IKRetarget = False
|
2011-06-27 12:46:53 +00:00
|
|
|
for bone in cnstrn_bone.parent_recursive:
|
|
|
|
if not bone.is_in_ik_chain:
|
|
|
|
bone.IKRetarget = False
|
2011-06-24 17:05:53 +00:00
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
|
|
|
|
class MocapMapping(bpy.types.PropertyGroup):
|
|
|
|
name = bpy.props.StringProperty()
|
|
|
|
|
|
|
|
bpy.utils.register_class(MocapMapping)
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
bpy.types.Bone.map = bpy.props.StringProperty()
|
2011-07-09 21:52:25 +00:00
|
|
|
bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping)
|
2011-07-06 13:31:13 +00:00
|
|
|
bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot",
|
|
|
|
description="Marks this bone as a 'foot', which determines retargeted animation's translation",
|
|
|
|
default=False)
|
2011-07-02 18:24:05 +00:00
|
|
|
bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK",
|
|
|
|
description="Toggles IK Retargeting method for given bone",
|
|
|
|
update=toggleIKBone, default=False)
|
2011-06-29 14:29:52 +00:00
|
|
|
|
2011-06-27 12:46:53 +00:00
|
|
|
|
|
|
|
def updateIKRetarget():
|
2011-06-29 14:29:52 +00:00
|
|
|
# ensures that Blender constraints and IK properties are in sync
|
|
|
|
# currently runs when module is loaded, should run when scene is loaded
|
|
|
|
# or user adds a constraint to armature. Will be corrected in the future,
|
|
|
|
# once python callbacks are implemented
|
2011-06-27 12:46:53 +00:00
|
|
|
for obj in bpy.data.objects:
|
|
|
|
if obj.pose:
|
|
|
|
bones = obj.pose.bones
|
|
|
|
for pose_bone in bones:
|
|
|
|
if pose_bone.is_in_ik_chain or hasIKConstraint(pose_bone):
|
|
|
|
pose_bone.IKRetarget = True
|
|
|
|
else:
|
|
|
|
pose_bone.IKRetarget = False
|
|
|
|
|
|
|
|
updateIKRetarget()
|
|
|
|
|
2011-07-05 10:57:29 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class MocapPanel(bpy.types.Panel):
|
2011-06-29 14:29:52 +00:00
|
|
|
# Motion capture retargeting panel
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_label = "Mocap tools"
|
|
|
|
bl_space_type = "PROPERTIES"
|
|
|
|
bl_region_type = "WINDOW"
|
|
|
|
bl_context = "object"
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def draw(self, context):
|
|
|
|
self.layout.label("Preprocessing")
|
|
|
|
row = self.layout.row(align=True)
|
|
|
|
row.alignment = 'EXPAND'
|
|
|
|
row.operator("mocap.samples", text='Samples to Beziers')
|
|
|
|
row.operator("mocap.denoise", text='Clean noise')
|
2011-07-06 13:31:13 +00:00
|
|
|
row.operator("mocap.rotate_fix", text='Fix BVH Axis Orientation')
|
2011-07-18 18:44:54 +00:00
|
|
|
row.operator("mocap.scale_fix", text='Auto scale Performer')
|
2011-06-24 17:05:53 +00:00
|
|
|
row2 = self.layout.row(align=True)
|
|
|
|
row2.operator("mocap.looper", text='Loop animation')
|
|
|
|
row2.operator("mocap.limitdof", text='Constrain Rig')
|
2011-08-03 18:17:33 +00:00
|
|
|
row2.operator("mocap.removelimitdof", text='Unconstrain Rig')
|
2011-06-24 17:05:53 +00:00
|
|
|
self.layout.label("Retargeting")
|
2011-06-25 23:50:50 +00:00
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj]
|
|
|
|
if enduser_obj is None or len(performer_obj) != 1:
|
|
|
|
self.layout.label("Select performer rig and target rig (as active)")
|
|
|
|
else:
|
2011-07-20 21:03:06 +00:00
|
|
|
self.layout.operator("mocap.guessmapping", text="Guess Hiearchy Mapping")
|
|
|
|
row3 = self.layout.row(align=True)
|
|
|
|
column1 = row3.column(align=True)
|
|
|
|
column1.label("Performer Rig")
|
|
|
|
column2 = row3.column(align=True)
|
|
|
|
column2.label("Enduser Rig")
|
2011-06-25 23:50:50 +00:00
|
|
|
performer_obj = performer_obj[0]
|
2011-06-26 19:54:29 +00:00
|
|
|
if performer_obj.data and enduser_obj.data:
|
|
|
|
if performer_obj.data.name in bpy.data.armatures and enduser_obj.data.name in bpy.data.armatures:
|
|
|
|
perf = performer_obj.data
|
|
|
|
enduser_arm = enduser_obj.data
|
|
|
|
perf_pose_bones = enduser_obj.pose.bones
|
|
|
|
for bone in perf.bones:
|
2011-06-27 12:46:53 +00:00
|
|
|
row = self.layout.row()
|
2011-07-06 13:31:13 +00:00
|
|
|
row.prop(data=bone, property='foot', text='', icon='POSE_DATA')
|
2011-06-26 19:54:29 +00:00
|
|
|
row.label(bone.name)
|
|
|
|
row.prop_search(bone, "map", enduser_arm, "bones")
|
|
|
|
label_mod = "FK"
|
|
|
|
if bone.map:
|
|
|
|
pose_bone = perf_pose_bones[bone.map]
|
|
|
|
if pose_bone.is_in_ik_chain:
|
2011-06-27 12:46:53 +00:00
|
|
|
label_mod = "ik chain"
|
|
|
|
if hasIKConstraint(pose_bone):
|
|
|
|
label_mod = "ik end"
|
|
|
|
row.prop(pose_bone, 'IKRetarget')
|
2011-07-06 13:31:13 +00:00
|
|
|
row.label(label_mod)
|
|
|
|
else:
|
|
|
|
row.label(" ")
|
|
|
|
row.label(" ")
|
2011-07-14 13:26:23 +00:00
|
|
|
mapRow = self.layout.row()
|
|
|
|
mapRow.operator("mocap.savemapping", text='Save mapping')
|
|
|
|
mapRow.operator("mocap.loadmapping", text='Load mapping')
|
2011-08-08 11:09:56 +00:00
|
|
|
self.layout.prop(data=performer_obj.animation_data.action, property='name', text='Action Name')
|
2011-06-26 19:54:29 +00:00
|
|
|
self.layout.operator("mocap.retarget", text='RETARGET!')
|
2011-06-25 23:50:50 +00:00
|
|
|
|
|
|
|
|
2011-06-29 14:29:52 +00:00
|
|
|
class MocapConstraintsPanel(bpy.types.Panel):
|
|
|
|
#Motion capture constraints panel
|
2011-07-29 18:23:16 +00:00
|
|
|
bl_label = "Mocap Fixes"
|
2011-06-29 14:29:52 +00:00
|
|
|
bl_space_type = "PROPERTIES"
|
|
|
|
bl_region_type = "WINDOW"
|
|
|
|
bl_context = "object"
|
|
|
|
|
|
|
|
def draw(self, context):
|
|
|
|
layout = self.layout
|
|
|
|
if context.active_object:
|
|
|
|
if context.active_object.data:
|
|
|
|
if context.active_object.data.name in bpy.data.armatures:
|
|
|
|
enduser_obj = context.active_object
|
|
|
|
enduser_arm = enduser_obj.data
|
2011-07-22 18:46:13 +00:00
|
|
|
layout.operator_menu_enum("mocap.addmocapfix", "type")
|
2011-08-05 08:44:16 +00:00
|
|
|
layout.operator("mocap.updateconstraints", text='Update Fixes')
|
2011-07-22 18:46:13 +00:00
|
|
|
bakeRow = layout.row()
|
2011-08-05 08:44:16 +00:00
|
|
|
bakeRow.operator("mocap.bakeconstraints", text='Bake Fixes')
|
|
|
|
bakeRow.operator("mocap.unbakeconstraints", text='Unbake Fixes')
|
2011-06-29 14:29:52 +00:00
|
|
|
layout.separator()
|
|
|
|
for i, m_constraint in enumerate(enduser_arm.mocap_constraints):
|
|
|
|
box = layout.box()
|
2011-07-22 18:46:13 +00:00
|
|
|
headerRow = box.row()
|
|
|
|
headerRow.prop(m_constraint, 'show_expanded', text='', icon='TRIA_DOWN' if m_constraint.show_expanded else 'TRIA_RIGHT', emboss=False)
|
|
|
|
headerRow.prop(m_constraint, 'type', text='')
|
|
|
|
headerRow.prop(m_constraint, 'name', text='')
|
2011-08-02 17:10:01 +00:00
|
|
|
headerRow.prop(m_constraint, 'active', icon='MUTE_IPO_ON' if not m_constraint.active else'MUTE_IPO_OFF', text='', emboss=False)
|
2011-07-22 18:46:13 +00:00
|
|
|
headerRow.operator("mocap.removeconstraint", text="", icon='X', emboss=False).constraint = i
|
|
|
|
if m_constraint.show_expanded:
|
|
|
|
box.separator()
|
|
|
|
box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones", icon='BONE_DATA')
|
|
|
|
if m_constraint.type == "distance" or m_constraint.type == "point":
|
|
|
|
box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones", icon='CONSTRAINT_BONE')
|
|
|
|
frameRow = box.row()
|
|
|
|
frameRow.label("Frame Range:")
|
|
|
|
frameRow.prop(m_constraint, 's_frame')
|
|
|
|
frameRow.prop(m_constraint, 'e_frame')
|
|
|
|
smoothRow = box.row()
|
|
|
|
smoothRow.label("Smoothing:")
|
|
|
|
smoothRow.prop(m_constraint, 'smooth_in')
|
|
|
|
smoothRow.prop(m_constraint, 'smooth_out')
|
|
|
|
targetRow = box.row()
|
|
|
|
targetLabelCol = targetRow.column()
|
|
|
|
targetLabelCol.label("Target settings:")
|
|
|
|
targetPropCol = targetRow.column()
|
|
|
|
if m_constraint.type == "floor":
|
|
|
|
targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects")
|
|
|
|
if m_constraint.type == "point" or m_constraint.type == "freeze":
|
|
|
|
box.prop(m_constraint, 'targetSpace')
|
|
|
|
if m_constraint.type == "point":
|
|
|
|
targetPropCol.prop(m_constraint, 'targetPoint')
|
|
|
|
if m_constraint.type == "distance" or m_constraint.type == "floor":
|
|
|
|
targetPropCol.prop(m_constraint, 'targetDist')
|
|
|
|
layout.separator()
|
2011-06-29 14:29:52 +00:00
|
|
|
|
|
|
|
|
2011-08-03 22:26:59 +00:00
|
|
|
class ExtraToolsPanel(bpy.types.Panel):
|
|
|
|
# Motion capture retargeting panel
|
|
|
|
bl_label = "Extra Mocap Tools"
|
|
|
|
bl_space_type = "PROPERTIES"
|
|
|
|
bl_region_type = "WINDOW"
|
|
|
|
bl_context = "object"
|
|
|
|
|
|
|
|
def draw(self, context):
|
|
|
|
layout = self.layout
|
|
|
|
layout.operator('mocap.pathediting', text="Follow Path")
|
2011-08-08 11:09:56 +00:00
|
|
|
layout.label("Animation Stitching")
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
if activeIsArmature:
|
|
|
|
enduser_arm = context.active_object.data
|
|
|
|
settings = enduser_arm.stitch_settings
|
|
|
|
layout.prop_search(settings, "first_action", enduser_arm, "mocapNLATracks")
|
|
|
|
layout.prop_search(settings, "second_action", enduser_arm, "mocapNLATracks")
|
|
|
|
layout.prop(settings, "blend_frame")
|
|
|
|
layout.prop(settings, "blend_amount")
|
|
|
|
layout.operator('mocap.animstitch', text="Stitch Animations")
|
2011-08-03 22:26:59 +00:00
|
|
|
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class OBJECT_OT_RetargetButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Retarget animation from selected armature to active armature '''
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_idname = "mocap.retarget"
|
|
|
|
bl_label = "Retargets active action from Performer to Enduser"
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def execute(self, context):
|
2011-08-08 11:09:56 +00:00
|
|
|
scene = context.scene
|
|
|
|
s_frame = scene.frame_start
|
|
|
|
e_frame = scene.frame_end
|
2011-07-09 21:52:25 +00:00
|
|
|
enduser_obj = context.active_object
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj]
|
|
|
|
if enduser_obj is None or len(performer_obj) != 1:
|
|
|
|
print("Need active and selected armatures")
|
|
|
|
else:
|
|
|
|
performer_obj = performer_obj[0]
|
2011-08-08 11:09:56 +00:00
|
|
|
s_frame, e_frame = performer_obj.animation_data.action.frame_range
|
|
|
|
s_frame = int(s_frame)
|
|
|
|
e_frame = int(e_frame)
|
2011-07-09 21:52:25 +00:00
|
|
|
retarget.totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class OBJECT_OT_SaveMappingButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Save mapping to active armature (for future retargets) '''
|
2011-07-09 21:52:25 +00:00
|
|
|
bl_idname = "mocap.savemapping"
|
|
|
|
bl_label = "Saves user generated mapping from Performer to Enduser"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
|
|
|
|
retarget.createDictionary(performer_obj.data, enduser_obj.data)
|
2011-06-24 17:05:53 +00:00
|
|
|
return {"FINISHED"}
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-07-14 13:26:23 +00:00
|
|
|
class OBJECT_OT_LoadMappingButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Load saved mapping from active armature'''
|
2011-07-14 13:26:23 +00:00
|
|
|
bl_idname = "mocap.loadmapping"
|
|
|
|
bl_label = "Loads user generated mapping from Performer to Enduser"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
|
|
|
|
retarget.loadMapping(performer_obj.data, enduser_obj.data)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Convert active armature's sampled keyframed to beziers'''
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_idname = "mocap.samples"
|
|
|
|
bl_label = "Converts samples / simplifies keyframes to beziers"
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def execute(self, context):
|
2011-08-03 22:26:59 +00:00
|
|
|
mocap_tools.fcurves_simplify(context, context.active_object)
|
2011-06-24 17:05:53 +00:00
|
|
|
return {"FINISHED"}
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
return context.active_object.animation_data
|
|
|
|
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class OBJECT_OT_LooperButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Trim active armature's animation to a single cycle, given a cyclic animation (such as a walk cycle)'''
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_idname = "mocap.looper"
|
|
|
|
bl_label = "loops animation / sampled mocap data"
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def execute(self, context):
|
|
|
|
mocap_tools.autoloop_anim()
|
|
|
|
return {"FINISHED"}
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
return context.active_object.animation_data
|
|
|
|
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class OBJECT_OT_DenoiseButton(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Denoise active armature's animation. Good for dealing with 'bad' frames inherent in mocap animation'''
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_idname = "mocap.denoise"
|
|
|
|
bl_label = "Denoises sampled mocap data "
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def execute(self, context):
|
2011-07-05 10:57:29 +00:00
|
|
|
mocap_tools.denoise_median()
|
2011-06-24 17:05:53 +00:00
|
|
|
return {"FINISHED"}
|
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
return context.active_object
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
return context.active_object.animation_data
|
|
|
|
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
class OBJECT_OT_LimitDOFButton(bpy.types.Operator):
|
2011-08-03 18:17:33 +00:00
|
|
|
'''Create limit constraints on the active armature from the selected armature's animation's range of motion'''
|
2011-06-24 17:05:53 +00:00
|
|
|
bl_idname = "mocap.limitdof"
|
|
|
|
bl_label = "Analyzes animations Max/Min DOF and adds hard/soft constraints"
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def execute(self, context):
|
2011-08-03 18:17:33 +00:00
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object][0]
|
|
|
|
mocap_tools.limit_dof(context, performer_obj, context.active_object)
|
2011-06-24 17:05:53 +00:00
|
|
|
return {"FINISHED"}
|
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2011-07-07 20:46:35 +00:00
|
|
|
|
2011-08-03 18:17:33 +00:00
|
|
|
class OBJECT_OT_RemoveLimitDOFButton(bpy.types.Operator):
|
|
|
|
'''Removes previously created limit constraints on the active armature'''
|
|
|
|
bl_idname = "mocap.removelimitdof"
|
|
|
|
bl_label = "Removes previously created limit constraints on the active armature"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
mocap_tools.limit_dof_toggle_off(context, context.active_object)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
activeIsArmature = False
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
return activeIsArmature
|
|
|
|
|
|
|
|
|
2011-07-06 13:31:13 +00:00
|
|
|
class OBJECT_OT_RotateFixArmature(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Realign the active armature's axis system to match Blender (Commonly needed after bvh import)'''
|
2011-07-06 13:31:13 +00:00
|
|
|
bl_idname = "mocap.rotate_fix"
|
|
|
|
bl_label = "Rotates selected armature 90 degrees (fix for bvh import)"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
mocap_tools.rotate_fix_armature(context.active_object.data)
|
|
|
|
return {"FINISHED"}
|
2011-07-07 20:46:35 +00:00
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
2011-07-06 13:31:13 +00:00
|
|
|
|
2011-06-25 23:50:50 +00:00
|
|
|
|
2011-07-18 18:44:54 +00:00
|
|
|
class OBJECT_OT_ScaleFixArmature(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Rescale selected armature to match the active animation, for convienence'''
|
2011-07-18 18:44:54 +00:00
|
|
|
bl_idname = "mocap.scale_fix"
|
|
|
|
bl_label = "Scales performer armature to match target armature"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
|
|
|
|
mocap_tools.scale_fix_armature(performer_obj, enduser_obj)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2011-07-22 18:46:13 +00:00
|
|
|
class MOCAP_OT_AddMocapFix(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Add a post-retarget fix - useful for fixing certain artifacts following the retarget'''
|
2011-07-22 18:46:13 +00:00
|
|
|
bl_idname = "mocap.addmocapfix"
|
2011-07-29 18:23:16 +00:00
|
|
|
bl_label = "Add Mocap Fix to target armature"
|
|
|
|
type = bpy.props.EnumProperty(name="Type of Fix",
|
2011-07-22 18:46:13 +00:00
|
|
|
items=[("point", "Maintain Position", "Bone is at a specific point"),
|
|
|
|
("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"),
|
|
|
|
("floor", "Stay above", "Bone does not cross specified mesh object eg floor"),
|
|
|
|
("distance", "Maintain distance", "Target bones maintained specified distance")],
|
2011-07-29 18:23:16 +00:00
|
|
|
description="Type of fix")
|
2011-06-29 14:29:52 +00:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
enduser_arm = enduser_obj.data
|
2011-07-02 18:24:05 +00:00
|
|
|
new_mcon = enduser_arm.mocap_constraints.add()
|
2011-07-22 18:46:13 +00:00
|
|
|
new_mcon.type = self.type
|
2011-06-29 14:29:52 +00:00
|
|
|
return {"FINISHED"}
|
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
|
2011-06-29 14:29:52 +00:00
|
|
|
|
|
|
|
class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Remove this post-retarget fix'''
|
2011-06-29 14:29:52 +00:00
|
|
|
bl_idname = "mocap.removeconstraint"
|
2011-07-29 18:23:16 +00:00
|
|
|
bl_label = "Removes fixes from target armature"
|
2011-06-29 14:29:52 +00:00
|
|
|
constraint = bpy.props.IntProperty()
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
enduser_arm = enduser_obj.data
|
2011-07-02 18:24:05 +00:00
|
|
|
m_constraints = enduser_arm.mocap_constraints
|
|
|
|
m_constraint = m_constraints[self.constraint]
|
|
|
|
if m_constraint.real_constraint:
|
|
|
|
bone = enduser_obj.pose.bones[m_constraint.real_constraint_bone]
|
|
|
|
cons_obj = getConsObj(bone)
|
|
|
|
removeConstraint(m_constraint, cons_obj)
|
|
|
|
m_constraints.remove(self.constraint)
|
2011-06-29 14:29:52 +00:00
|
|
|
return {"FINISHED"}
|
|
|
|
|
2011-07-09 21:52:25 +00:00
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
|
2011-06-29 14:29:52 +00:00
|
|
|
|
2011-07-14 13:26:23 +00:00
|
|
|
class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Bake all post-retarget fixes to the Retarget Fixes NLA Track'''
|
2011-07-14 13:26:23 +00:00
|
|
|
bl_idname = "mocap.bakeconstraints"
|
2011-07-29 18:23:16 +00:00
|
|
|
bl_label = "Bake all fixes to target armature"
|
2011-07-14 13:26:23 +00:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
bakeConstraints(context)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
|
|
|
|
|
|
|
|
class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator):
|
2011-07-19 16:33:28 +00:00
|
|
|
'''Unbake all post-retarget fixes - removes the baked data from the Retarget Fixes NLA Track'''
|
2011-07-14 13:26:23 +00:00
|
|
|
bl_idname = "mocap.unbakeconstraints"
|
2011-07-29 18:23:16 +00:00
|
|
|
bl_label = "Unbake all fixes to target armature"
|
2011-07-14 13:26:23 +00:00
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
unbakeConstraints(context)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
|
|
|
|
|
2011-08-05 08:44:16 +00:00
|
|
|
class OBJECT_OT_UpdateMocapConstraints(bpy.types.Operator):
|
|
|
|
'''Updates all post-retarget fixes - needed after changes to armature object or pose'''
|
|
|
|
bl_idname = "mocap.updateconstraints"
|
|
|
|
bl_label = "Updates all fixes to target armature - neccesary to take under consideration changes to armature object or pose"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
updateConstraints(context.active_object, context)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
return isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
|
|
|
|
|
2011-07-20 21:03:06 +00:00
|
|
|
class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator):
|
|
|
|
'''Attemps to auto figure out hierarchy mapping'''
|
|
|
|
bl_idname = "mocap.guessmapping"
|
|
|
|
bl_label = "Attemps to auto figure out hierarchy mapping"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
enduser_obj = bpy.context.active_object
|
|
|
|
performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0]
|
|
|
|
mocap_tools.guessMapping(performer_obj, enduser_obj)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
performer_obj = [obj for obj in context.selected_objects if obj != context.active_object]
|
|
|
|
if performer_obj:
|
|
|
|
return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2011-08-03 22:26:59 +00:00
|
|
|
class OBJECT_OT_PathEditing(bpy.types.Operator):
|
|
|
|
'''Sets active object (stride object) to follow the selected curve'''
|
|
|
|
bl_idname = "mocap.pathediting"
|
|
|
|
bl_label = "Sets active object (stride object) to follow the selected curve"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
path = [obj for obj in context.selected_objects if obj != context.active_object][0]
|
|
|
|
mocap_tools.path_editing(context, context.active_object, path)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
if context.active_object:
|
2011-08-05 08:44:16 +00:00
|
|
|
selected_objs = [obj for obj in context.selected_objects if obj != context.active_object and isinstance(obj.data, bpy.types.Curve)]
|
2011-08-03 22:26:59 +00:00
|
|
|
return selected_objs
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2011-08-08 11:09:56 +00:00
|
|
|
class OBJECT_OT_AnimationStitchingButton(bpy.types.Operator):
|
|
|
|
'''Stitches two defined animations into a single one via alignment of NLA Tracks'''
|
|
|
|
bl_idname = "mocap.animstitch"
|
|
|
|
bl_label = "Stitches two defined animations into a single one via alignment of NLA Tracks"
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
|
|
mocap_tools.anim_stitch(context, context.active_object)
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def poll(cls, context):
|
|
|
|
activeIsArmature = False
|
|
|
|
if context.active_object:
|
|
|
|
activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature)
|
|
|
|
if activeIsArmature:
|
|
|
|
stitch_settings = context.active_object.data.stitch_settings
|
|
|
|
return (stitch_settings.first_action and stitch_settings.second_action)
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def register():
|
2011-06-25 23:50:50 +00:00
|
|
|
bpy.utils.register_module(__name__)
|
|
|
|
|
|
|
|
|
2011-06-24 17:05:53 +00:00
|
|
|
def unregister():
|
|
|
|
bpy.utils.unregister_module(__name__)
|
2011-06-25 23:50:50 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2011-07-05 10:57:29 +00:00
|
|
|
register()
|