WIP: New component type: cloud_curve_custom #160
369
rig_components/cloud_curve_custom.py
Normal file
369
rig_components/cloud_curve_custom.py
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.props import BoolProperty, IntProperty, FloatProperty, EnumProperty
|
||||||
|
from bpy.types import Object, PropertyGroup, BezierSplinePoint
|
||||||
|
|
||||||
|
from ..rig_component_features.bone_info import BoneInfo
|
||||||
|
from .cloud_curve import Component_Curve_Hooked, get_points
|
||||||
|
|
||||||
|
|
||||||
|
class Component_Curve_Custom(Component_Curve_Hooked):
|
||||||
|
"""Create a bezier curve object to drive a bone chain with Spline IK constraint, controlled by Hooks."""
|
||||||
|
|
||||||
|
ui_name = "Curve: Custom"
|
||||||
|
relinking_behaviour = "Constraints will be moved to the Hook controls. Only works when Match Controls to Bones option is enabled."
|
||||||
|
|
||||||
|
forced_params = {
|
||||||
|
'curve.x_axis_symmetry': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize_curve_rig(self):
|
||||||
|
length = self.bone_count
|
||||||
|
subdiv = self.params.spline_ik.subdivide
|
||||||
|
total = length * subdiv
|
||||||
|
if length > 255:
|
||||||
|
self.raise_generation_error(
|
||||||
|
f"Spline IK rig consists of {length} bones but the Spline IK constraint only supports a chain of 255 bones."
|
||||||
|
)
|
||||||
|
if total > 255:
|
||||||
|
old_total = total
|
||||||
|
old_subdiv = subdiv
|
||||||
|
while total > 255:
|
||||||
|
subdiv -= 1
|
||||||
|
total = length * subdiv
|
||||||
|
self.add_log(
|
||||||
|
"Spline IK longer than 255 bones",
|
||||||
|
description=f"Trying to subdivide {length} bones {old_subdiv} times, would result in {old_total} bones. \nThe Spline IK constraint only supports a chain of 255 bones, so subdivisions has been capped at {subdiv} for a new total of {total} bones.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.num_controls = (
|
||||||
|
self.bone_count + 1
|
||||||
|
if self.params.spline_ik.match_hooks
|
||||||
|
else self.params.spline_ik.hooks
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_bone_infos(self, context):
|
||||||
|
# Skip the parent class's create_bone_infos() function, but call the grandparent's.
|
||||||
|
# This is because we need to do things in a different order than cloud_curve:
|
||||||
|
# The curve object is created based on the controls, rather than the other way around.
|
||||||
|
super(Component_Curve_Hooked, self).create_bone_infos(context)
|
||||||
|
self.root_bone = self.bones_org[0].parent # Should be allowed to be None!
|
||||||
|
if self.params.curve.create_root:
|
||||||
|
self.make_curve_root_ctrl()
|
||||||
|
if not self.params.curve.target:
|
||||||
|
self.ensure_curve_obj(context)
|
||||||
|
self.reset_curve_obj(self.params.curve.target)
|
||||||
|
self.make_ctrls_for_curve_points()
|
||||||
|
|
||||||
|
ik_chain = self.bones_org
|
||||||
|
if self.params.spline_ik.deform_setup == 'CREATE':
|
||||||
|
ik_chain = self.make_def_chain()
|
||||||
|
self.add_spline_ik(ik_chain)
|
||||||
|
|
||||||
|
def ensure_curve_obj(self, context):
|
||||||
|
"""Find or create the Bezier Curve that will be used by the rig."""
|
||||||
|
|
||||||
|
curve_ob = self.params.curve.target
|
||||||
|
if curve_ob:
|
||||||
|
return curve_ob
|
||||||
|
|
||||||
|
# Create and name curve object.
|
||||||
|
curve_name = "CUR-" + self.generator.metarig.name.replace("META-", "")
|
||||||
|
curve_name += "_" + (
|
||||||
|
self.params.curve.hook_name
|
||||||
|
if self.params.curve.hook_name != ""
|
||||||
|
else self.base_bone_name.replace("ORG-", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
curve = bpy.data.curves.new(curve_name, 'CURVE')
|
||||||
|
curve_ob = bpy.data.objects.new(curve_name, curve)
|
||||||
|
context.scene.collection.objects.link(curve_ob)
|
||||||
|
self.lock_transforms(curve_ob)
|
||||||
|
self.params.curve.target = curve_ob
|
||||||
|
return curve_ob
|
||||||
|
|
||||||
|
def reset_curve_obj(self, curve_ob):
|
||||||
|
# Store the radii of existing points before removing them.
|
||||||
|
old_radii = [p.radius for p in curve_ob.data.splines[0].bezier_points] if curve_ob.data.splines else []
|
||||||
|
# Remove all splines, then add a new one.
|
||||||
|
for spline in curve_ob.data.splines[:]:
|
||||||
|
curve_ob.data.splines.remove(spline)
|
||||||
|
spline = curve_ob.data.splines.new(type='BEZIER')
|
||||||
|
# Remove all Hook modifiers. They seem to cause an issue where deform bones get created at 0,0,0...
|
||||||
|
# Blows my mind, don't ask me.
|
||||||
|
for m in curve_ob.modifiers[:]:
|
||||||
|
if m.type == 'HOOK':
|
||||||
|
curve_ob.modifiers.remove(m)
|
||||||
|
|
||||||
|
curve_ob.data.dimensions = '3D'
|
||||||
|
sum_bone_length = sum([b.length for b in self.bones_org])
|
||||||
|
length_unit = sum_bone_length / (self.num_controls - 1)
|
||||||
|
handle_length = length_unit * self.params.spline_ik.handle_length
|
||||||
|
|
||||||
|
self.params.curve.target = curve_ob
|
||||||
|
|
||||||
|
# Add the necessary number of curve points to the spline
|
||||||
|
points = get_points(spline)
|
||||||
|
assert len(points) == 1
|
||||||
|
points.add(self.num_controls - len(points))
|
||||||
|
num_points = len(points)
|
||||||
|
|
||||||
|
# Configure control points and reapply radii...
|
||||||
|
for i in range(0, num_points):
|
||||||
|
point_along_chain = i * length_unit
|
||||||
|
p = points[i]
|
||||||
|
|
||||||
|
# Set radius of each point from old curve if available, otherwise use default
|
||||||
|
p.radius = old_radii[i] if i < len(old_radii) else 1.0
|
||||||
|
|
||||||
|
# Place control points
|
||||||
|
index = i if self.params.spline_ik.match_hooks else -1
|
||||||
|
loc, direction = self.vector_along_bone_chain(
|
||||||
|
self.bones_org, point_along_chain, index
|
||||||
|
)
|
||||||
|
p.co = loc
|
||||||
|
p.handle_right = loc + handle_length * direction
|
||||||
|
p.handle_left = loc - handle_length * direction
|
||||||
|
|
||||||
|
return curve_ob
|
||||||
|
|
||||||
|
def make_def_chain(self):
|
||||||
|
segments = self.params.spline_ik.subdivide
|
||||||
|
|
||||||
|
count_def_bone = 0
|
||||||
|
for org_bone in self.bones_org:
|
||||||
|
for i in range(0, segments):
|
||||||
|
## Create Deform bones
|
||||||
|
if self.params.curve.hook_name != "":
|
||||||
|
def_name = self.params.curve.hook_name
|
||||||
|
counter = count_def_bone
|
||||||
|
else:
|
||||||
|
def_name = org_bone.name.replace("ORG-", "")
|
||||||
|
counter = i
|
||||||
|
prefixes, base, suffixes = self.naming.slice_name(def_name)
|
||||||
|
base += "_" + str(counter).zfill(len(str(segments)))
|
||||||
|
prefixes.insert(0, "DEF")
|
||||||
|
def_name = self.naming.make_name(prefixes, base, suffixes)
|
||||||
|
count_def_bone += 1
|
||||||
|
|
||||||
|
unit = org_bone.vector / segments
|
||||||
|
def_bone = self.bone_sets['Curve Deform Bones'].new(
|
||||||
|
name=def_name,
|
||||||
|
source=org_bone,
|
||||||
|
head=org_bone.head + (unit * i),
|
||||||
|
tail=org_bone.head + (unit * (i + 1)),
|
||||||
|
roll=org_bone.roll,
|
||||||
|
bbone_width=0.03,
|
||||||
|
use_deform=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(self.bone_sets['Curve Deform Bones']) > 1:
|
||||||
|
def_bone.parent = self.bone_sets['Curve Deform Bones'][-2]
|
||||||
|
else:
|
||||||
|
def_bone.parent = self.bones_org[0]
|
||||||
|
|
||||||
|
return self.bone_sets['Curve Deform Bones']
|
||||||
|
|
||||||
|
def add_spline_ik(self, bone_chain):
|
||||||
|
# Add constraint to deform chain
|
||||||
|
bone_chain[-1].add_constraint(
|
||||||
|
'SPLINE_IK',
|
||||||
|
target=self.params.curve.target,
|
||||||
|
use_curve_radius=True,
|
||||||
|
chain_count=len(bone_chain),
|
||||||
|
)
|
||||||
|
|
||||||
|
def relink(self):
|
||||||
|
"""Override cloud_curve.
|
||||||
|
Move constraints from ORG to Hook controls and relink them.
|
||||||
|
Only works when params.spline_ik.match_hooks==True.
|
||||||
|
"""
|
||||||
|
if not self.params.spline_ik.match_hooks:
|
||||||
|
return
|
||||||
|
for i, org in enumerate(self.bones_org):
|
||||||
|
for c in org.constraint_infos[:]:
|
||||||
|
if not c.is_from_real:
|
||||||
|
continue
|
||||||
|
to_bone = self.bone_sets['Curve Hooks'][i]
|
||||||
|
to_bone.constraint_infos.append(c)
|
||||||
|
org.constraint_infos.remove(c)
|
||||||
|
c.relink()
|
||||||
|
|
||||||
|
def create_helper_objects(self, context):
|
||||||
|
"""Apply the rest pose of the deform bones, as dictated by
|
||||||
|
the Spline IK constraint."""
|
||||||
|
super().create_helper_objects(context)
|
||||||
|
|
||||||
|
self.target_rig.data.pose_position = 'POSE'
|
||||||
|
bpy.ops.object.mode_set(mode='EDIT')
|
||||||
|
|
||||||
|
for def_bi in self.bone_sets['Curve Deform Bones']:
|
||||||
|
eb = self.target_rig.data.edit_bones.get(def_bi.name)
|
||||||
|
if not eb:
|
||||||
|
continue
|
||||||
|
pb = self.target_rig.pose.bones.get(def_bi.name)
|
||||||
|
eb.head = pb.matrix.to_translation()
|
||||||
|
|
||||||
|
self.target_rig.data.pose_position = 'REST'
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
|
||||||
|
|
||||||
|
def setup_spline(self, context, curve_ob: Object, spline_i: int, hooks: list[BoneInfo]):
|
||||||
|
"""Override cloud_curve.
|
||||||
|
Prevent drivers from being generated
|
||||||
|
"""
|
||||||
|
spline = curve_ob.data.splines[spline_i]
|
||||||
|
points = get_points(spline)
|
||||||
|
num_points = len(points)
|
||||||
|
|
||||||
|
assert num_points == len(
|
||||||
|
hooks
|
||||||
|
), f"Curve object {curve_ob.name} spline has {num_points} points, but {len(hooks)} hooks were passed."
|
||||||
|
|
||||||
|
# Disable all modifiers on the curve object
|
||||||
|
mod_vis_backup = {}
|
||||||
|
for m in curve_ob.modifiers:
|
||||||
|
mod_vis_backup[m.name] = m.show_viewport
|
||||||
|
m.show_viewport = False
|
||||||
|
|
||||||
|
# Disable all constraints on the curve object
|
||||||
|
constraint_vis_backup = {}
|
||||||
|
for c in curve_ob.constraints:
|
||||||
|
constraint_vis_backup[c.name] = c.mute
|
||||||
|
c.mute = True
|
||||||
|
|
||||||
|
context.view_layer.update()
|
||||||
|
|
||||||
|
for point_i in range(0, num_points):
|
||||||
|
hook_b = hooks[point_i]
|
||||||
|
shared_kwargs = {
|
||||||
|
"rig_ob": self.target_rig,
|
||||||
|
"curve_ob": self.params.curve.target,
|
||||||
|
"spline_i": spline_i,
|
||||||
|
"point_i": point_i,
|
||||||
|
"is_bezier": type(points[0]) == BezierSplinePoint,
|
||||||
|
}
|
||||||
|
if not self.params.curve.controls_for_handles:
|
||||||
|
self.make_hook_modifier(
|
||||||
|
bonename=hook_b.name,
|
||||||
|
main_handle=True,
|
||||||
|
left_handle=True,
|
||||||
|
right_handle=True,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.make_hook_modifier(
|
||||||
|
bonename=hook_b.name,
|
||||||
|
main_handle=True,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
if hook_b.left_handle_control:
|
||||||
|
self.make_hook_modifier(
|
||||||
|
bonename=hook_b.left_handle_control.name,
|
||||||
|
left_handle=True,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
if hook_b.right_handle_control:
|
||||||
|
self.make_hook_modifier(
|
||||||
|
bonename=hook_b.right_handle_control.name,
|
||||||
|
right_handle=True,
|
||||||
|
**shared_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Restore modifier visibility on curve object
|
||||||
|
for m in curve_ob.modifiers:
|
||||||
|
if m.name in mod_vis_backup:
|
||||||
|
m.show_viewport = mod_vis_backup[m.name]
|
||||||
|
|
||||||
|
# Restore constraints visibility on the curve object
|
||||||
|
for c in curve_ob.constraints:
|
||||||
|
c.mute = constraint_vis_backup[c.name]
|
||||||
|
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# Parameters
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_bone_sets(cls):
|
||||||
|
super().define_bone_sets()
|
||||||
|
"""Create parameters for this rig's bone sets."""
|
||||||
|
cls.define_bone_set(
|
||||||
|
'Curve Deform Bones', collections=['Deform Bones'], is_advanced=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def curve_selector_ui(cls, layout, context, params):
|
||||||
|
"""Overrides cloud_curve to disable the curve selection."""
|
||||||
|
row = cls.draw_prop(
|
||||||
|
context, layout.row(), params.curve, "target", icon='OUTLINER_OB_CURVE'
|
||||||
|
)
|
||||||
|
if row:
|
||||||
|
row.enabled = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def draw_control_params(cls, layout, context, params):
|
||||||
|
"""Create the ui for the rig parameters."""
|
||||||
|
super().draw_control_params(layout, context, params)
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
cls.draw_control_label(layout, "Spline IK")
|
||||||
|
|
||||||
|
if cls.is_advanced_mode(context):
|
||||||
|
cls.draw_prop(context, layout, params.spline_ik, 'handle_length')
|
||||||
|
|
||||||
|
cls.draw_prop(context, layout, params.spline_ik, 'deform_setup', expand=True)
|
||||||
|
if params.spline_ik.deform_setup == 'CREATE':
|
||||||
|
cls.draw_prop(context, layout, params.spline_ik, 'subdivide')
|
||||||
|
# TODO: When this is false, the directions of the curve points and bones
|
||||||
|
# don't match, and both of them are unsatisfactory. It would be nice if
|
||||||
|
# we would interpolate between the direction of the two bones, using
|
||||||
|
# length_remaining/bone.length as a factor, or something similar to that.
|
||||||
|
cls.draw_prop(context, layout, params.spline_ik, 'match_hooks')
|
||||||
|
if not params.spline_ik.match_hooks:
|
||||||
|
cls.draw_prop(context, layout, params.spline_ik, 'hooks')
|
||||||
|
|
||||||
|
|
||||||
|
class Params(PropertyGroup):
|
||||||
|
match_hooks: BoolProperty(
|
||||||
|
name="Match Controls to Bones",
|
||||||
|
description="Hook controls will be created at each bone, instead of being equally distributed across the length of the chain",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
deform_setup: EnumProperty(
|
||||||
|
name="Deform Setup",
|
||||||
|
items=[
|
||||||
|
(
|
||||||
|
'NONE',
|
||||||
|
'None',
|
||||||
|
"Disable deform flag, so this component won't work with Armature modifiers",
|
||||||
|
),
|
||||||
|
('PRESERVE', 'Preserve', "Preserve deform flag of each bone"),
|
||||||
|
('CREATE', 'Create', "Create deform bones prefixed with DEF-"),
|
||||||
|
],
|
||||||
|
description="How this curve rig component should behave with Armature modifiers",
|
||||||
|
)
|
||||||
|
subdivide: IntProperty(
|
||||||
|
name="Subdivide Bones",
|
||||||
|
description="For each original bone, create this many deform bones in the spline chain (Bendy Bones do not work well with Spline IK, so we create real bones) NOTE: Spline IK only supports 255 bones in the chain",
|
||||||
|
default=3,
|
||||||
|
min=1,
|
||||||
|
max=99,
|
||||||
|
)
|
||||||
|
handle_length: FloatProperty(
|
||||||
|
name="Curve Handle Length",
|
||||||
|
description="Increasing this will result in longer curve handles, resulting in a sharper curve. A value of 1 means the curve handle reaches the neighbouring curve point",
|
||||||
|
default=0.4,
|
||||||
|
min=0.01,
|
||||||
|
max=2.0,
|
||||||
|
)
|
||||||
|
hooks: IntProperty(
|
||||||
|
name="Number of Hooks",
|
||||||
|
description="Number of controls that will be spaced out evenly across the entire chain",
|
||||||
|
default=3,
|
||||||
|
min=3,
|
||||||
|
max=99,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
RIG_COMPONENT_CLASS = Component_Curve_Custom
|
Loading…
Reference in New Issue
Block a user