Currently vertices are mapped to B-Bone segments without taking the rest pose curve into account. This is very simple and fast, but causes poor deformations in some cases where the rest curvature is significant, e.g. mouth corners in the new Rigify face rig. This patch implements a new mapping mode that addresses this problem. The general idea is to do an orthogonal projection on the curve. However, since there is no analytical solution for bezier curves, it uses the segment approximation: * First, boundaries between segments are used for a binary space partitioning search to narrow down the mapping to one segment. * Then, a position on the segment is chosen via linear interpolation between the BSP planes. * Finally, to remove the sharp discontinuity at the evolute surface a smoothing pass is applied to the chosen position by blending to reduce the slope around the planes previously used in the BSP search. In order to make per-vertex processing faster, a new array with the necessary vectors converted to the pose space, as well as some precomputed coefficients, is built. The new mode is implemented as a per-bone option in order to ensure backward compatibility, and also because the new mode may not be optimal for all cases due to the difference in performance, and complications like the smoothed but still present mapping discontinuities around the evolute surface. Wiki: https://wiki.blender.org/wiki/Source/Animation/B-Bone_Vertex_Mapping Pull Request: https://projects.blender.org/blender/blender/pulls/110758 Pull Request: https://projects.blender.org/blender/blender/pulls/110758
518 lines
16 KiB
Python
518 lines
16 KiB
Python
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import bpy
|
|
from bpy.types import Panel
|
|
from rna_prop_ui import PropertyPanel
|
|
|
|
from bpy.app.translations import contexts as i18n_contexts
|
|
|
|
|
|
class BoneButtonsPanel:
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "bone"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (context.bone or context.edit_bone)
|
|
|
|
|
|
class BONE_PT_context_bone(BoneButtonsPanel, Panel):
|
|
bl_label = ""
|
|
bl_options = {'HIDE_HEADER'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
bone = context.bone
|
|
if not bone:
|
|
bone = context.edit_bone
|
|
|
|
row = layout.row()
|
|
row.label(text="", icon='BONE_DATA')
|
|
row.prop(bone, "name", text="")
|
|
|
|
|
|
class BONE_PT_transform(BoneButtonsPanel, Panel):
|
|
bl_label = "Transform"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if context.edit_bone:
|
|
return True
|
|
|
|
ob = context.object
|
|
return ob and ob.mode == 'POSE' and context.bone
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
ob = context.object
|
|
bone = context.bone
|
|
|
|
col = layout.column()
|
|
|
|
if bone and ob:
|
|
pchan = ob.pose.bones[bone.name]
|
|
col.active = not (bone.parent and bone.use_connect)
|
|
|
|
row = col.row(align=True)
|
|
row.prop(pchan, "location")
|
|
row.use_property_decorate = False
|
|
row.prop(pchan, "lock_location", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
|
|
rotation_mode = pchan.rotation_mode
|
|
if rotation_mode == 'QUATERNION':
|
|
col = layout.column()
|
|
row = col.row(align=True)
|
|
row.prop(pchan, "rotation_quaternion", text="Rotation")
|
|
sub = row.column(align=True)
|
|
sub.use_property_decorate = False
|
|
sub.prop(pchan, "lock_rotation_w", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
sub.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
elif rotation_mode == 'AXIS_ANGLE':
|
|
col = layout.column()
|
|
row = col.row(align=True)
|
|
row.prop(pchan, "rotation_axis_angle", text="Rotation")
|
|
|
|
sub = row.column(align=True)
|
|
sub.use_property_decorate = False
|
|
sub.prop(pchan, "lock_rotation_w", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
sub.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
else:
|
|
col = layout.column()
|
|
row = col.row(align=True)
|
|
row.prop(pchan, "rotation_euler", text="Rotation")
|
|
row.use_property_decorate = False
|
|
row.prop(pchan, "lock_rotation", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
row = layout.row(align=True)
|
|
row.prop(pchan, "rotation_mode", text="Mode")
|
|
row.label(text="", icon='BLANK1')
|
|
|
|
col = layout.column()
|
|
row = col.row(align=True)
|
|
row.prop(pchan, "scale")
|
|
row.use_property_decorate = False
|
|
row.prop(pchan, "lock_scale", text="", emboss=False, icon='DECORATE_UNLOCKED')
|
|
|
|
elif context.edit_bone:
|
|
bone = context.edit_bone
|
|
col = layout.column()
|
|
col.prop(bone, "head")
|
|
col.prop(bone, "tail")
|
|
|
|
col = layout.column()
|
|
col.prop(bone, "roll")
|
|
col.prop(bone, "lock")
|
|
|
|
|
|
class BONE_PT_curved(BoneButtonsPanel, Panel):
|
|
bl_label = "Bendy Bones"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
ob = context.object
|
|
bone = context.bone
|
|
arm = context.armature
|
|
bone_list = "bones"
|
|
|
|
if ob and bone:
|
|
bbone = ob.pose.bones[bone.name]
|
|
elif bone is None:
|
|
bone = context.edit_bone
|
|
bbone = bone
|
|
bone_list = "edit_bones"
|
|
else:
|
|
bbone = bone
|
|
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
layout.prop(bone, "bbone_segments", text="Segments")
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(bone, "bbone_x", text="Display Size X")
|
|
col.prop(bone, "bbone_z", text="Z")
|
|
|
|
topcol = layout.column()
|
|
topcol.active = bone.bbone_segments > 1
|
|
|
|
topcol.prop(bone, "bbone_mapping_mode", text="Vertex Mapping")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_curveinx", text="Curve In X")
|
|
col.prop(bbone, "bbone_curveinz", text="Z")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_curveoutx", text="Curve Out X")
|
|
col.prop(bbone, "bbone_curveoutz", text="Z")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_rollin", text="Roll In")
|
|
col.prop(bbone, "bbone_rollout", text="Out", text_ctxt=i18n_contexts.id_armature)
|
|
col.prop(bone, "use_endroll_as_inroll")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_scalein", text="Scale In")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_scaleout", text="Scale Out")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bbone, "bbone_easein", text="Ease In", text_ctxt=i18n_contexts.id_armature)
|
|
col.prop(bbone, "bbone_easeout", text="Out", text_ctxt=i18n_contexts.id_armature)
|
|
col.prop(bone, "use_scale_easing")
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bone, "bbone_handle_type_start", text="Start Handle")
|
|
|
|
col2 = col.column(align=True)
|
|
col2.active = (bone.bbone_handle_type_start != 'AUTO')
|
|
col2.prop_search(bone, "bbone_custom_handle_start", arm, bone_list, text="Custom")
|
|
|
|
row = col.row(align=True)
|
|
row.use_property_split = False
|
|
split = row.split(factor=0.4)
|
|
split.alignment = 'RIGHT'
|
|
split.label(text="Scale")
|
|
split2 = split.split(factor=0.7)
|
|
row2 = split2.row(align=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_start", index=0, text="X", toggle=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_start", index=1, text="Y", toggle=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_start", index=2, text="Z", toggle=True)
|
|
split2.prop(bone, "bbone_handle_use_ease_start", text="Ease", text_ctxt=i18n_contexts.id_armature, toggle=True)
|
|
row.label(icon='BLANK1')
|
|
|
|
col = topcol.column(align=True)
|
|
col.prop(bone, "bbone_handle_type_end", text="End Handle")
|
|
|
|
col2 = col.column(align=True)
|
|
col2.active = (bone.bbone_handle_type_end != 'AUTO')
|
|
col2.prop_search(bone, "bbone_custom_handle_end", arm, bone_list, text="Custom")
|
|
|
|
row = col.row(align=True)
|
|
row.use_property_split = False
|
|
split = row.split(factor=0.4)
|
|
split.alignment = 'RIGHT'
|
|
split.label(text="Scale")
|
|
split2 = split.split(factor=0.7)
|
|
row2 = split2.row(align=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_end", index=0, text="X", toggle=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_end", index=1, text="Y", toggle=True)
|
|
row2.prop(bone, "bbone_handle_use_scale_end", index=2, text="Z", toggle=True)
|
|
split2.prop(bone, "bbone_handle_use_ease_end", text="Ease", text_ctxt=i18n_contexts.id_armature, toggle=True)
|
|
row.label(icon='BLANK1')
|
|
|
|
|
|
class BONE_PT_relations(BoneButtonsPanel, Panel):
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
bl_label = "Relations"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
ob = context.object
|
|
bone = context.bone
|
|
arm = context.armature
|
|
pchan = None
|
|
|
|
if ob and bone:
|
|
pchan = ob.pose.bones[bone.name]
|
|
elif bone is None:
|
|
bone = context.edit_bone
|
|
|
|
col = layout.column()
|
|
|
|
if context.bone:
|
|
col.prop(bone, "parent")
|
|
else:
|
|
col.prop_search(bone, "parent", arm, "edit_bones")
|
|
|
|
if ob and pchan:
|
|
col.prop(bone, "use_relative_parent")
|
|
|
|
sub = col.column()
|
|
sub.active = (bone.parent is not None)
|
|
sub.prop(bone, "use_connect")
|
|
sub = col.column()
|
|
sub.active = (not bone.parent or not bone.use_connect)
|
|
sub.prop(bone, "use_local_location")
|
|
sub = col.column()
|
|
sub.active = (bone.parent is not None)
|
|
sub.prop(bone, "use_inherit_rotation")
|
|
sub.prop(bone, "inherit_scale")
|
|
|
|
|
|
class BONE_PT_display(BoneButtonsPanel, Panel):
|
|
bl_label = "Viewport Display"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.bone or context.edit_bone
|
|
|
|
def draw(self, context):
|
|
# note. this works ok in edit-mode but isn't
|
|
# all that useful so disabling for now.
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
if context.bone is None:
|
|
self.draw_edit_bone(context, layout)
|
|
else:
|
|
self.draw_bone(context, layout)
|
|
|
|
def draw_bone(self, context, layout):
|
|
bone = context.bone
|
|
|
|
col = layout.column()
|
|
col.prop(bone, "hide", text="Hide", toggle=False)
|
|
|
|
# Figure out the pose bone.
|
|
ob = context.object
|
|
if not ob:
|
|
return
|
|
pose_bone = ob.pose.bones[bone.name]
|
|
|
|
layout.prop(bone.color, 'palette', text='Edit Bone Color')
|
|
self.draw_bone_color_ui(layout, bone.color)
|
|
layout.prop(pose_bone.color, 'palette', text='Pose Bone Color')
|
|
self.draw_bone_color_ui(layout, pose_bone.color)
|
|
|
|
def draw_edit_bone(self, context, layout):
|
|
bone = context.edit_bone
|
|
if bone is None:
|
|
return
|
|
|
|
col = layout.column()
|
|
col.prop(bone, "hide", text="Hide", toggle=False)
|
|
layout.prop(bone.color, 'palette', text='Edit Bone Color')
|
|
self.draw_bone_color_ui(layout, bone.color)
|
|
|
|
def draw_bone_color_ui(self, layout, bone_color):
|
|
if not bone_color.is_custom:
|
|
return
|
|
|
|
layout.use_property_split = False
|
|
split = layout.split(factor=0.4)
|
|
|
|
col = split.column()
|
|
row = col.row()
|
|
row.alignment = 'RIGHT'
|
|
row.label(text="Custom Colors")
|
|
|
|
col = split.column(align=True)
|
|
row = col.row(align=True)
|
|
row.prop(bone_color.custom, "normal", text="")
|
|
row.prop(bone_color.custom, "select", text="")
|
|
row.prop(bone_color.custom, "active", text="")
|
|
|
|
|
|
class BONE_PT_display_custom_shape(BoneButtonsPanel, Panel):
|
|
bl_label = "Custom Shape"
|
|
bl_parent_id = "BONE_PT_display"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.bone
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
ob = context.object
|
|
bone = context.bone
|
|
pchan = None
|
|
|
|
if ob and bone:
|
|
pchan = ob.pose.bones[bone.name]
|
|
elif bone is None:
|
|
bone = context.edit_bone
|
|
|
|
if bone and pchan:
|
|
col = layout.column()
|
|
col.prop(pchan, "custom_shape")
|
|
|
|
sub = col.column()
|
|
sub.active = bool(pchan and pchan.custom_shape)
|
|
sub.separator()
|
|
|
|
sub.prop(pchan, "custom_shape_scale_xyz", text="Scale")
|
|
sub.prop(pchan, "custom_shape_translation", text="Translation")
|
|
sub.prop(pchan, "custom_shape_rotation_euler", text="Rotation")
|
|
|
|
sub.prop_search(pchan, "custom_shape_transform",
|
|
ob.pose, "bones", text="Override Transform")
|
|
sub.prop(pchan, "use_custom_shape_bone_size")
|
|
|
|
sub.separator()
|
|
sub.prop(bone, "show_wire", text="Wireframe")
|
|
|
|
|
|
class BONE_PT_inverse_kinematics(BoneButtonsPanel, Panel):
|
|
bl_label = "Inverse Kinematics"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
ob = context.object
|
|
return ob and ob.mode == 'POSE' and context.bone
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
ob = context.object
|
|
bone = context.bone
|
|
pchan = ob.pose.bones[bone.name]
|
|
|
|
active = pchan.is_in_ik_chain
|
|
|
|
col = layout.column()
|
|
col.prop(pchan, "ik_stretch", slider=True)
|
|
col.active = active
|
|
|
|
layout.separator()
|
|
|
|
col = layout.column(align=True)
|
|
|
|
col.prop(pchan, "lock_ik_x", text="Lock IK X")
|
|
col.prop(pchan, "lock_ik_y", text="Y")
|
|
col.prop(pchan, "lock_ik_z", text="Z")
|
|
|
|
col = layout.column(align=True)
|
|
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_x is False and active
|
|
sub.prop(pchan, "ik_stiffness_x", text="Stiffness X", slider=True)
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_y is False and active
|
|
sub.prop(pchan, "ik_stiffness_y", text="Y", slider=True)
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_z is False and active
|
|
sub.prop(pchan, "ik_stiffness_z", text="Z", slider=True)
|
|
|
|
col = layout.column(align=True)
|
|
|
|
sub = col.column()
|
|
sub.active = pchan.lock_ik_x is False and active
|
|
sub.prop(pchan, "use_ik_limit_x", text="Limit X")
|
|
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_x is False and pchan.use_ik_limit_x and active
|
|
sub.prop(pchan, "ik_min_x", text="Min")
|
|
sub.prop(pchan, "ik_max_x", text="Max")
|
|
|
|
col.separator()
|
|
|
|
sub = col.column()
|
|
sub.active = pchan.lock_ik_y is False and active
|
|
sub.prop(pchan, "use_ik_limit_y", text="Limit Y")
|
|
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_y is False and pchan.use_ik_limit_y and active
|
|
sub.prop(pchan, "ik_min_y", text="Min")
|
|
sub.prop(pchan, "ik_max_y", text="Max")
|
|
|
|
col.separator()
|
|
|
|
sub = col.column()
|
|
sub.active = pchan.lock_ik_z is False and active
|
|
sub.prop(pchan, "use_ik_limit_z", text="Limit Z")
|
|
|
|
sub = col.column(align=True)
|
|
sub.active = pchan.lock_ik_z is False and pchan.use_ik_limit_z and active
|
|
sub.prop(pchan, "ik_min_z", text="Min")
|
|
sub.prop(pchan, "ik_max_z", text="Max")
|
|
|
|
col.separator()
|
|
|
|
if ob.pose.ik_solver == 'ITASC':
|
|
|
|
col = layout.column()
|
|
col.prop(pchan, "use_ik_rotation_control", text="Control Rotation")
|
|
col.active = active
|
|
|
|
col = layout.column()
|
|
|
|
col.prop(pchan, "ik_rotation_weight", text="IK Rotation Weight", slider=True)
|
|
col.active = active
|
|
# not supported yet
|
|
# row = layout.row()
|
|
# row.prop(pchan, "use_ik_linear_control", text="Joint Size")
|
|
# row.prop(pchan, "ik_linear_weight", text="Weight", slider=True)
|
|
|
|
|
|
class BONE_PT_deform(BoneButtonsPanel, Panel):
|
|
bl_label = "Deform"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw_header(self, context):
|
|
bone = context.bone
|
|
|
|
if not bone:
|
|
bone = context.edit_bone
|
|
|
|
self.layout.prop(bone, "use_deform", text="")
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
bone = context.bone
|
|
|
|
if not bone:
|
|
bone = context.edit_bone
|
|
|
|
layout.active = bone.use_deform
|
|
|
|
col = layout.column()
|
|
col.prop(bone, "envelope_distance", text="Envelope Distance")
|
|
col.prop(bone, "envelope_weight", text="Envelope Weight")
|
|
col.prop(bone, "use_envelope_multiply", text="Envelope Multiply")
|
|
|
|
col.separator()
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(bone, "head_radius", text="Radius Head")
|
|
col.prop(bone, "tail_radius", text="Tail")
|
|
|
|
|
|
class BONE_PT_custom_props(BoneButtonsPanel, PropertyPanel, Panel):
|
|
COMPAT_ENGINES = {
|
|
'BLENDER_RENDER',
|
|
'BLENDER_EEVEE',
|
|
'BLENDER_EEVEE_NEXT',
|
|
'BLENDER_WORKBENCH'}
|
|
_property_type = bpy.types.Bone, bpy.types.EditBone, bpy.types.PoseBone
|
|
|
|
@property
|
|
def _context_path(self):
|
|
obj = bpy.context.object
|
|
if obj and obj.mode == 'POSE':
|
|
return "active_pose_bone"
|
|
else:
|
|
return "active_bone"
|
|
|
|
|
|
classes = (
|
|
BONE_PT_context_bone,
|
|
BONE_PT_transform,
|
|
BONE_PT_curved,
|
|
BONE_PT_relations,
|
|
BONE_PT_inverse_kinematics,
|
|
BONE_PT_deform,
|
|
BONE_PT_display,
|
|
BONE_PT_display_custom_shape,
|
|
BONE_PT_custom_props,
|
|
)
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|