forked from LeenkxTeam/LNXSDK
283 lines
11 KiB
Python
283 lines
11 KiB
Python
import bpy
|
|
import textwrap
|
|
from bpy.props import *
|
|
|
|
import lnx.props_ui
|
|
|
|
if lnx.is_reload(__name__):
|
|
lnx.props_ui = lnx.reload_module(lnx.props_ui)
|
|
else:
|
|
lnx.enable_reload(__name__)
|
|
|
|
class LnxRetargetActions(bpy.types.Operator):
|
|
bl_idname = 'lnx.retarget_action'
|
|
bl_label = 'Retarget action data'
|
|
bl_description = 'Retargets action data from one bone to another for root motion'
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.object
|
|
if obj.type != 'ARMATURE':
|
|
return False
|
|
if obj.mode != 'OBJECT':
|
|
return False
|
|
wrd = bpy.data.worlds['Lnx']
|
|
if wrd.lnx_retarget_from == wrd.lnx_retarget_to:
|
|
return False
|
|
if context.space_data.type == 'DOPESHEET_EDITOR':
|
|
ds_mode = context.space_data.mode
|
|
if ds_mode in {'DOPESHEET', 'ACTION'}:
|
|
return bool(context.active_action)
|
|
if context.space_data.type == 'NLA_EDITOR':
|
|
if context.active_nla_strip:
|
|
if context.active_nla_strip.action:
|
|
return True
|
|
return False
|
|
|
|
def execute(self, context):
|
|
if context.space_data.type == 'DOPESHEET_EDITOR':
|
|
ds_mode = context.space_data.mode
|
|
if ds_mode in {'DOPESHEET', 'ACTION'}:
|
|
self.action = context.active_action
|
|
if context.space_data.type == 'NLA_EDITOR':
|
|
self.action = context.active_nla_strip.action
|
|
wrd = bpy.data.worlds['Lnx']
|
|
obj = context.object
|
|
# Create helper object
|
|
helper1 = bpy.data.objects.new(name="lnx_helper_1", object_data=None)
|
|
helper1.rotation_mode = 'QUATERNION'
|
|
helper2 = bpy.data.objects.new(name="lnx_helper_2", object_data=None)
|
|
helper2.rotation_mode = 'QUATERNION'
|
|
# Get bones
|
|
pose = obj.pose
|
|
from_bone = pose.bones.get(wrd.lnx_retarget_from)
|
|
to_bone = pose.bones.get(wrd.lnx_retarget_to)
|
|
if from_bone is None or to_bone is None:
|
|
self.report({'ERROR'}, "Actions not retargeted. Armature or bones do not exist.")
|
|
return{'CANCELLED'}
|
|
# Copy selected transform bake to helper1
|
|
helper1_scl = helper1.constraints.new('COPY_SCALE')
|
|
helper1_scl.target = obj
|
|
helper1_scl.subtarget = wrd.lnx_retarget_from
|
|
helper1_rot = helper1.constraints.new('COPY_ROTATION')
|
|
helper1_rot.target = obj
|
|
helper1_rot.subtarget = wrd.lnx_retarget_from
|
|
helper1_rot.use_x = wrd.lnx_action_retarget_rot_x
|
|
helper1_rot.use_y = wrd.lnx_action_retarget_rot_y
|
|
helper1_rot.use_z = wrd.lnx_action_retarget_rot_z
|
|
helper1_loc = helper1.constraints.new('COPY_LOCATION')
|
|
helper1_loc.target = obj
|
|
helper1_loc.subtarget = wrd.lnx_retarget_from
|
|
helper1_loc.use_x = wrd.lnx_action_retarget_pos_x
|
|
helper1_loc.use_y = wrd.lnx_action_retarget_pos_y
|
|
helper1_loc.use_z = wrd.lnx_action_retarget_pos_z
|
|
# Remove selected transform and bake to helper2
|
|
helper2_scl = helper2.constraints.new('COPY_SCALE')
|
|
helper2_scl.target = obj
|
|
helper2_scl.subtarget = wrd.lnx_retarget_from
|
|
helper2_rot = helper2.constraints.new('COPY_ROTATION')
|
|
helper2_rot.target = obj
|
|
helper2_rot.subtarget = wrd.lnx_retarget_from
|
|
helper2_rot.use_x = not wrd.lnx_action_retarget_rot_x
|
|
helper2_rot.use_y = not wrd.lnx_action_retarget_rot_y
|
|
helper2_rot.use_z = not wrd.lnx_action_retarget_rot_z
|
|
helper2_loc = helper2.constraints.new('COPY_LOCATION')
|
|
helper2_loc.target = obj
|
|
helper2_loc.subtarget = wrd.lnx_retarget_from
|
|
helper2_loc.use_x = not wrd.lnx_action_retarget_pos_x
|
|
helper2_loc.use_y = not wrd.lnx_action_retarget_pos_y
|
|
helper2_loc.use_z = not wrd.lnx_action_retarget_pos_z
|
|
# Select helper only
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
bpy.context.scene.collection.objects.link(helper1)
|
|
bpy.context.scene.collection.objects.link(helper2)
|
|
helper1.select_set(True)
|
|
helper2.select_set(True)
|
|
bpy.context.view_layer.objects.active = helper1
|
|
obj.animation_data.action = self.action
|
|
framerange = self.action.frame_range
|
|
# Set helper locations
|
|
bpy.context.scene.frame_set(1)
|
|
world_loc = obj.matrix_world @ from_bone.head
|
|
helper1.location = world_loc
|
|
helper2.location = world_loc
|
|
# Bake to helper
|
|
bpy.ops.nla.bake(frame_start=int(framerange[0]), frame_end=int(framerange[1]), step=1, only_selected=True, visual_keying=True,
|
|
clear_constraints=True, clean_curves=True, clear_parents=False, use_current_action=False, bake_types={'OBJECT'})
|
|
# Copy transform to new root bone
|
|
copy_transform1 = to_bone.constraints.new('COPY_TRANSFORMS')
|
|
copy_transform1.target = helper1
|
|
# Copy transform from old root bone
|
|
copy_transform2 = from_bone.constraints.new('COPY_TRANSFORMS')
|
|
copy_transform2.target = helper2
|
|
# Select from and to bones only
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
for bone in obj.data.bones:
|
|
bone.select = False
|
|
to_bone.bone.select = True
|
|
from_bone.bone.select = True
|
|
obj.data.bones.active = to_bone.bone
|
|
framerange = self.action.frame_range
|
|
obj.animation_data.action = self.action
|
|
overwrite = wrd.lnx_retarget_overwrite
|
|
if not overwrite:
|
|
new_action = self.action.copy()
|
|
new_action.name = self.action.name + '_retarget'
|
|
obj.animation_data.action = new_action
|
|
# Bake to from and to bones
|
|
bpy.ops.nla.bake(frame_start=int(framerange[0]), frame_end=int(framerange[1]), step=1, only_selected=True, visual_keying=True,
|
|
clear_constraints=True, clean_curves=True, clear_parents=False, use_current_action=True, bake_types={'POSE'})
|
|
# Clean up
|
|
bpy.data.actions.remove(helper1.animation_data.action)
|
|
bpy.data.actions.remove(helper2.animation_data.action)
|
|
bpy.data.objects.remove(helper1)
|
|
bpy.data.objects.remove(helper2)
|
|
self.report({'INFO'}, "Action retargeted successfully")
|
|
return{'FINISHED'}
|
|
|
|
class LnxDrawRetargetPanel:
|
|
|
|
@classmethod
|
|
def check_bone(cls, armature, bone):
|
|
if bone is not None:
|
|
if armature is not None:
|
|
return bone in armature.bones
|
|
return False
|
|
|
|
@classmethod
|
|
def draw(cls, context, action, layout):
|
|
layout.label(text='Action: ' + action.name)
|
|
wrd = bpy.data.worlds['Lnx']
|
|
# Armature Object
|
|
layout.prop(wrd, 'lnx_retarget_lnxature')
|
|
con = False
|
|
# From, To bones
|
|
if wrd.lnx_retarget_lnxature:
|
|
if wrd.lnx_retarget_lnxature.type == 'ARMATURE':
|
|
con = True
|
|
sub = layout.row(align=True)
|
|
sub.label(text="From Bone:")
|
|
sub.prop_search(wrd, 'lnx_retarget_from',
|
|
wrd.lnx_retarget_lnxature.data, "bones", text="")
|
|
con = cls.check_bone(wrd.lnx_retarget_lnxature.data, wrd.lnx_retarget_from)
|
|
sub = layout.row(align=True)
|
|
sub.label(text="To Bone:")
|
|
sub.prop_search(wrd,'lnx_retarget_to',
|
|
wrd.lnx_retarget_lnxature.data, "bones", text="")
|
|
con = cls.check_bone(wrd.lnx_retarget_lnxature.data, wrd.lnx_retarget_to)
|
|
# Check if bones exist
|
|
condition = con and bool(wrd.lnx_retarget_from) and bool(wrd.lnx_retarget_to)
|
|
section = layout.column()
|
|
section.enabled = condition
|
|
# Position
|
|
sub = section.row(align=True)
|
|
sub.label(text="Position:")
|
|
sub.prop(wrd, 'lnx_action_retarget_pos_x', text='X', toggle=True)
|
|
sub.prop(wrd, 'lnx_action_retarget_pos_y', text='Y', toggle=True)
|
|
sub.prop(wrd, 'lnx_action_retarget_pos_z', text='Z', toggle=True)
|
|
# Rotation
|
|
sub = section.row(align=True)
|
|
sub.label(text="Rotation:")
|
|
sub.prop(wrd, 'lnx_action_retarget_rot_x', text='X', toggle=True)
|
|
sub.prop(wrd, 'lnx_action_retarget_rot_y', text='Y', toggle=True)
|
|
sub.prop(wrd, 'lnx_action_retarget_rot_z', text='Z', toggle=True)
|
|
# Retarget Operator
|
|
sub = section.row()
|
|
sub.prop(wrd, 'lnx_retarget_overwrite')
|
|
box = section.box()
|
|
text='Retargeting will apply all constraints for the selected objects and then baked.'
|
|
'The constraints are removed after retarget.'
|
|
textwrap_width = int(bpy.context.region.width//2)
|
|
col = lnx.props_ui.draw_multiline_with_icon(box, textwrap_width, 'ERROR', text)
|
|
sub = section.row(align=True)
|
|
sub.operator('lnx.retarget_action', text='Retarget', icon='FILE_REFRESH')
|
|
|
|
class LNX_PT_DSRootMotionRetargetPanel(bpy.types.Panel):
|
|
bl_label = 'Leenkx Root Motion Retarget'
|
|
bl_idname = 'LNX_PT_DSRootMotionRetargetPanel'
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_context = 'data'
|
|
bl_category = 'Leenkx'
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
ds_mode = context.space_data.mode
|
|
if ds_mode in {'DOPESHEET', 'ACTION'}:
|
|
return bool(context.active_action)
|
|
|
|
def draw(self, context):
|
|
LnxDrawRetargetPanel.draw(context, context.active_action, self.layout)
|
|
|
|
class LNX_PT_NLARootMotionRetargetPanel(bpy.types.Panel):
|
|
bl_label = 'Leenkx Root Motion Retarget'
|
|
bl_idname = 'LNX_PT_NLARootMotionRetargetPanel'
|
|
bl_space_type = 'NLA_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_context = 'data'
|
|
bl_category = 'Leenkx'
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return bool(context.active_nla_strip)
|
|
|
|
def draw(self, context):
|
|
LnxDrawRetargetPanel.draw(context, context.active_nla_strip.action, self.layout)
|
|
|
|
class LNX_PT_DopeSheetRootMotionPanel(bpy.types.Panel):
|
|
bl_label = 'Leenkx Root Motion'
|
|
bl_idname = 'LNX_PT_DopeSheetRootMotionPanel'
|
|
bl_space_type = 'DOPESHEET_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_context = 'data'
|
|
bl_category = 'Leenkx'
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
ds_mode = context.space_data.mode
|
|
if ds_mode in {'DOPESHEET', 'ACTION'}:
|
|
return bool(context.active_action)
|
|
|
|
def draw(self, context):
|
|
action = context.active_action
|
|
layout = self.layout
|
|
layout.label(text='Action: ' + action.name)
|
|
layout.prop(action, 'lnx_root_motion_pos')
|
|
layout.prop(action, 'lnx_root_motion_rot')
|
|
|
|
class LNX_PT_NLARootMotionPanel(bpy.types.Panel):
|
|
bl_label = 'Leenkx Root Motion'
|
|
bl_idname = 'LNX_PT_NLARootMotionPanel'
|
|
bl_space_type = 'NLA_EDITOR'
|
|
bl_region_type = 'UI'
|
|
bl_context = 'data'
|
|
bl_category = 'Leenkx'
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return bool(context.active_nla_strip)
|
|
|
|
def draw(self, context):
|
|
action = context.active_nla_strip.action
|
|
layout = self.layout
|
|
layout.label(text='Action: ' + action.name)
|
|
layout.prop(action, 'lnx_root_motion_pos')
|
|
layout.prop(action, 'lnx_root_motion_rot')
|
|
|
|
__REG_CLASSES = (
|
|
LnxRetargetActions,
|
|
LNX_PT_DSRootMotionRetargetPanel,
|
|
LNX_PT_NLARootMotionRetargetPanel,
|
|
LNX_PT_DopeSheetRootMotionPanel,
|
|
LNX_PT_NLARootMotionPanel,
|
|
)
|
|
__reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES)
|
|
|
|
def register():
|
|
__reg_classes()
|
|
|
|
def unregister():
|
|
__unreg_classes()
|