2025-12-01
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
# ##### 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>
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
bl_info = {
|
||||
"name": "Dynamic Parent",
|
||||
"author": "Roman Volodin, roman.volodin@gmail.com",
|
||||
"version": (2, 0, 2),
|
||||
"blender": (4, 0, 0),
|
||||
"location": "View3D > Tool Panel",
|
||||
"description": "Allows to create and disable an animated ChildOf constraint",
|
||||
"category": "Animation",
|
||||
}
|
||||
|
||||
|
||||
def get_rotation_mode(obj):
|
||||
if obj.rotation_mode in ("QUATERNION", "AXIS_ANGLE"):
|
||||
return obj.rotation_mode.lower()
|
||||
return "euler"
|
||||
|
||||
|
||||
def get_selected_objects(context):
|
||||
if context.mode not in ("OBJECT", "POSE"):
|
||||
return
|
||||
|
||||
if context.mode == "OBJECT":
|
||||
active = context.active_object
|
||||
selected = [obj for obj in context.selected_objects if obj != active]
|
||||
|
||||
if context.mode == "POSE":
|
||||
active = context.active_pose_bone
|
||||
selected = [bone for bone in context.selected_pose_bones if bone != active]
|
||||
|
||||
selected.append(active)
|
||||
return selected
|
||||
|
||||
|
||||
def get_last_dynamic_parent_constraint(obj):
|
||||
if not obj.constraints:
|
||||
return
|
||||
const = obj.constraints[-1]
|
||||
if const.name.startswith("DP_") and const.influence == 1:
|
||||
return const
|
||||
|
||||
|
||||
def insert_keyframe(obj, frame):
|
||||
rotation_mode = get_rotation_mode(obj)
|
||||
data_paths = (
|
||||
"location",
|
||||
f"rotation_{rotation_mode}",
|
||||
"scale",
|
||||
)
|
||||
for data_path in data_paths:
|
||||
obj.keyframe_insert(data_path=data_path, frame=frame)
|
||||
|
||||
|
||||
def insert_keyframe_constraint(constraint, frame):
|
||||
constraint.keyframe_insert(data_path="influence", frame=frame)
|
||||
|
||||
|
||||
def dp_keyframe_insert_obj(obj):
|
||||
obj.keyframe_insert(data_path="location")
|
||||
if obj.rotation_mode == "QUATERNION":
|
||||
obj.keyframe_insert(data_path="rotation_quaternion")
|
||||
elif obj.rotation_mode == "AXIS_ANGLE":
|
||||
obj.keyframe_insert(data_path="rotation_axis_angle")
|
||||
else:
|
||||
obj.keyframe_insert(data_path="rotation_euler")
|
||||
obj.keyframe_insert(data_path="scale")
|
||||
|
||||
|
||||
def dp_keyframe_insert_pbone(arm, pbone):
|
||||
arm.keyframe_insert(data_path='pose.bones["' + pbone.name + '"].location')
|
||||
if pbone.rotation_mode == "QUATERNION":
|
||||
arm.keyframe_insert(
|
||||
data_path='pose.bones["' + pbone.name + '"].rotation_quaternion'
|
||||
)
|
||||
elif pbone.rotation_mode == "AXIS_ANGLE":
|
||||
arm.keyframe_insert(
|
||||
data_path='pose.bones["' + pbone.name + '"].rotation_axis_angel'
|
||||
)
|
||||
else:
|
||||
arm.keyframe_insert(data_path='pose.bones["' + pbone.name + '"].rotation_euler')
|
||||
arm.keyframe_insert(data_path='pose.bones["' + pbone.name + '"].scale')
|
||||
|
||||
|
||||
def dp_create_dynamic_parent_obj(op):
|
||||
obj = bpy.context.active_object
|
||||
scn = bpy.context.scene
|
||||
list_selected_obj = bpy.context.selected_objects
|
||||
|
||||
if len(list_selected_obj) == 2:
|
||||
i = list_selected_obj.index(obj)
|
||||
list_selected_obj.pop(i)
|
||||
parent_obj = list_selected_obj[0]
|
||||
|
||||
dp_keyframe_insert_obj(obj)
|
||||
bpy.ops.object.constraint_add_with_targets(type="CHILD_OF")
|
||||
last_constraint = obj.constraints[-1]
|
||||
|
||||
if parent_obj.type == "ARMATURE":
|
||||
last_constraint.subtarget = parent_obj.data.bones.active.name
|
||||
last_constraint.name = (
|
||||
"DP_" + last_constraint.target.name + "." + last_constraint.subtarget
|
||||
)
|
||||
else:
|
||||
last_constraint.name = "DP_" + last_constraint.target.name
|
||||
|
||||
C = bpy.context.copy()
|
||||
C["constraint"] = last_constraint
|
||||
bpy.ops.constraint.childof_set_inverse(
|
||||
constraint=last_constraint.name, owner="OBJECT"
|
||||
)
|
||||
|
||||
current_frame = scn.frame_current
|
||||
scn.frame_current = current_frame - 1
|
||||
obj.constraints[last_constraint.name].influence = 0
|
||||
obj.keyframe_insert(
|
||||
data_path='constraints["' + last_constraint.name + '"].influence'
|
||||
)
|
||||
|
||||
scn.frame_current = current_frame
|
||||
obj.constraints[last_constraint.name].influence = 1
|
||||
obj.keyframe_insert(
|
||||
data_path='constraints["' + last_constraint.name + '"].influence'
|
||||
)
|
||||
|
||||
for ob in list_selected_obj:
|
||||
ob.select_set(False)
|
||||
|
||||
obj.select_set(True)
|
||||
else:
|
||||
op.report({"ERROR"}, "Two objects must be selected")
|
||||
|
||||
|
||||
def dp_create_dynamic_parent_pbone(op):
|
||||
arm = bpy.context.active_object
|
||||
pbone = bpy.context.active_pose_bone
|
||||
scn = bpy.context.scene
|
||||
list_selected_obj = bpy.context.selected_objects
|
||||
|
||||
if len(list_selected_obj) == 2 or len(list_selected_obj) == 1:
|
||||
if len(list_selected_obj) == 2:
|
||||
i = list_selected_obj.index(arm)
|
||||
list_selected_obj.pop(i)
|
||||
parent_obj = list_selected_obj[0]
|
||||
if parent_obj.type == "ARMATURE":
|
||||
parent_obj_pbone = parent_obj.data.bones.active
|
||||
if not parent_obj_pbone.select:
|
||||
op.report({"ERROR"}, "At least two bones must be selected")
|
||||
return
|
||||
else:
|
||||
parent_obj = arm
|
||||
selected_bones = bpy.context.selected_pose_bones
|
||||
selected_bones.remove(pbone)
|
||||
if not selected_bones:
|
||||
op.report({"ERROR"}, "At least two bones must be selected")
|
||||
return
|
||||
parent_obj_pbone = selected_bones[0]
|
||||
|
||||
dp_keyframe_insert_pbone(arm, pbone)
|
||||
bpy.ops.pose.constraint_add_with_targets(type="CHILD_OF")
|
||||
last_constraint = pbone.constraints[-1]
|
||||
|
||||
if parent_obj.type == "ARMATURE":
|
||||
last_constraint.subtarget = parent_obj_pbone.name
|
||||
last_constraint.name = (
|
||||
"DP_" + last_constraint.target.name + "." + last_constraint.subtarget
|
||||
)
|
||||
else:
|
||||
last_constraint.name = "DP_" + last_constraint.target.name
|
||||
|
||||
C = bpy.context.copy()
|
||||
C["constraint"] = last_constraint
|
||||
bpy.ops.constraint.childof_set_inverse(
|
||||
constraint=last_constraint.name, owner="BONE"
|
||||
)
|
||||
|
||||
current_frame = scn.frame_current
|
||||
scn.frame_current = current_frame - 1
|
||||
pbone.constraints[last_constraint.name].influence = 0
|
||||
arm.keyframe_insert(
|
||||
data_path='pose.bones["'
|
||||
+ pbone.name
|
||||
+ '"].constraints["'
|
||||
+ last_constraint.name
|
||||
+ '"].influence'
|
||||
)
|
||||
|
||||
scn.frame_current = current_frame
|
||||
pbone.constraints[last_constraint.name].influence = 1
|
||||
arm.keyframe_insert(
|
||||
data_path='pose.bones["'
|
||||
+ pbone.name
|
||||
+ '"].constraints["'
|
||||
+ last_constraint.name
|
||||
+ '"].influence'
|
||||
)
|
||||
else:
|
||||
op.report({"ERROR"}, "Two objects must be selected")
|
||||
|
||||
|
||||
def disable_constraint(obj, const, frame):
|
||||
if isinstance(obj, bpy.types.PoseBone):
|
||||
matrix_final = obj.matrix
|
||||
else:
|
||||
matrix_final = obj.matrix_world
|
||||
|
||||
insert_keyframe(obj, frame=frame - 1)
|
||||
insert_keyframe_constraint(const, frame=frame - 1)
|
||||
|
||||
const.influence = 0
|
||||
if isinstance(obj, bpy.types.PoseBone):
|
||||
obj.matrix = matrix_final
|
||||
else:
|
||||
obj.matrix_world = matrix_final
|
||||
|
||||
insert_keyframe(obj, frame=frame)
|
||||
insert_keyframe_constraint(const, frame=frame)
|
||||
return
|
||||
|
||||
|
||||
def dp_clear(obj, pbone):
|
||||
dp_curves = []
|
||||
dp_keys = []
|
||||
for fcurve in obj.animation_data.action.fcurves:
|
||||
if "constraints" in fcurve.data_path and "DP_" in fcurve.data_path:
|
||||
dp_curves.append(fcurve)
|
||||
|
||||
for f in dp_curves:
|
||||
for key in f.keyframe_points:
|
||||
dp_keys.append(key.co[0])
|
||||
|
||||
dp_keys = list(set(dp_keys))
|
||||
dp_keys.sort()
|
||||
|
||||
for fcurve in obj.animation_data.action.fcurves[:]:
|
||||
if fcurve.data_path.startswith("constraints") and "DP_" in fcurve.data_path:
|
||||
obj.animation_data.action.fcurves.remove(fcurve)
|
||||
else:
|
||||
for frame in dp_keys:
|
||||
for key in fcurve.keyframe_points[:]:
|
||||
if key.co[0] == frame:
|
||||
fcurve.keyframe_points.remove(key)
|
||||
if not fcurve.keyframe_points:
|
||||
obj.animation_data.action.fcurves.remove(fcurve)
|
||||
|
||||
if pbone:
|
||||
obj = pbone
|
||||
for const in obj.constraints[:]:
|
||||
if const.name.startswith("DP_"):
|
||||
obj.constraints.remove(const)
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_OT_create(bpy.types.Operator):
|
||||
"""Create a new animated Child Of constraint"""
|
||||
|
||||
bl_idname = "dynamic_parent.create"
|
||||
bl_label = "Create Constraint"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
frame = context.scene.frame_current
|
||||
|
||||
if obj.type == "ARMATURE":
|
||||
if obj.mode != "POSE":
|
||||
self.report({"ERROR"}, "Armature objects must be in Pose mode.")
|
||||
return {"CANCELLED"}
|
||||
obj = bpy.context.active_pose_bone
|
||||
const = get_last_dynamic_parent_constraint(obj)
|
||||
if const:
|
||||
disable_constraint(obj, const, frame)
|
||||
dp_create_dynamic_parent_pbone(self)
|
||||
else:
|
||||
const = get_last_dynamic_parent_constraint(obj)
|
||||
if const:
|
||||
disable_constraint(obj, const, frame)
|
||||
dp_create_dynamic_parent_obj(self)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_OT_disable(bpy.types.Operator):
|
||||
"""Disable the current animated Child Of constraint"""
|
||||
|
||||
bl_idname = "dynamic_parent.disable"
|
||||
bl_label = "Disable Constraint"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.mode in ("OBJECT", "POSE")
|
||||
|
||||
def execute(self, context):
|
||||
frame = context.scene.frame_current
|
||||
objects = get_selected_objects(context)
|
||||
counter = 0
|
||||
|
||||
if not objects:
|
||||
self.report({"ERROR"}, "Nothing selected.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
for obj in objects:
|
||||
const = get_last_dynamic_parent_constraint(obj)
|
||||
if const is None:
|
||||
continue
|
||||
disable_constraint(obj, const, frame)
|
||||
counter += 1
|
||||
|
||||
self.report({"INFO"}, f"{counter} constraints were disabled.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_OT_clear(bpy.types.Operator):
|
||||
"""Clear Dynamic Parent constraints"""
|
||||
|
||||
bl_idname = "dynamic_parent.clear"
|
||||
bl_label = "Clear Dynamic Parent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
pbone = None
|
||||
obj = bpy.context.active_object
|
||||
if obj.type == "ARMATURE":
|
||||
pbone = bpy.context.active_pose_bone
|
||||
|
||||
dp_clear(obj, pbone)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_OT_bake(bpy.types.Operator):
|
||||
"""Bake Dynamic Parent animation"""
|
||||
|
||||
bl_idname = "dynamic_parent.bake"
|
||||
bl_label = "Bake Dynamic Parent"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.context.active_object
|
||||
scn = bpy.context.scene
|
||||
|
||||
if obj.type == "ARMATURE":
|
||||
obj = bpy.context.active_pose_bone
|
||||
bpy.ops.nla.bake(
|
||||
frame_start=scn.frame_start,
|
||||
frame_end=scn.frame_end,
|
||||
step=1,
|
||||
only_selected=True,
|
||||
visual_keying=True,
|
||||
clear_constraints=False,
|
||||
clear_parents=False,
|
||||
bake_types={"POSE"},
|
||||
)
|
||||
for const in obj.constraints[:]:
|
||||
if const.name.startswith("DP_"):
|
||||
obj.constraints.remove(const)
|
||||
else:
|
||||
bpy.ops.nla.bake(
|
||||
frame_start=scn.frame_start,
|
||||
frame_end=scn.frame_end,
|
||||
step=1,
|
||||
only_selected=True,
|
||||
visual_keying=True,
|
||||
clear_constraints=False,
|
||||
clear_parents=False,
|
||||
bake_types={"OBJECT"},
|
||||
)
|
||||
for const in obj.constraints[:]:
|
||||
if const.name.startswith("DP_"):
|
||||
obj.constraints.remove(const)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_MT_clear_menu(bpy.types.Menu):
|
||||
"""Clear or bake Dynamic Parent constraints"""
|
||||
|
||||
bl_label = "Clear Dynamic Parent?"
|
||||
bl_idname = "DYNAMIC_PARENT_MT_clear_menu"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("dynamic_parent.clear", text="Clear", icon="X")
|
||||
layout.operator("dynamic_parent.bake", text="Bake and clear", icon="REC")
|
||||
|
||||
|
||||
class DYNAMIC_PARENT_PT_ui(bpy.types.Panel):
|
||||
"""User interface for Dynamic Parent addon"""
|
||||
|
||||
bl_label = "Dynamic Parent {}.{}.{}".format(*bl_info["version"])
|
||||
bl_idname = "DYNAMIC_PARENT_PT_ui"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Dynamic Parent"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
col.operator("dynamic_parent.create", text="Create", icon="KEY_HLT")
|
||||
col.operator("dynamic_parent.disable", text="Disable", icon="KEY_DEHLT")
|
||||
col.menu("DYNAMIC_PARENT_MT_clear_menu", text="Clear")
|
||||
|
||||
|
||||
classes = (
|
||||
DYNAMIC_PARENT_OT_create,
|
||||
DYNAMIC_PARENT_OT_disable,
|
||||
DYNAMIC_PARENT_OT_clear,
|
||||
DYNAMIC_PARENT_OT_bake,
|
||||
DYNAMIC_PARENT_MT_clear_menu,
|
||||
DYNAMIC_PARENT_PT_ui,
|
||||
)
|
||||
|
||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
Reference in New Issue
Block a user