1 # SPDX-FileCopyrightText: 2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 from typing
import Optional
, List
, Dict
, Tuple
, TYPE_CHECKING
6 from bpy
.types
import Action
, Mesh
, Armature
7 from bl_math
import clamp
9 from .errors
import MetarigError
10 from .misc
import MeshObject
, IdPropSequence
, verify_mesh_obj
11 from .naming
import Side
, get_name_side
, change_name_side
, mirror_name
12 from .bones
import BoneUtilityMixin
13 from .mechanism
import MechanismUtilityMixin
, driver_var_transform
, quote_property
15 from ..base_rig
import RigComponent
, stage
16 from ..base_generate
import GeneratorPlugin
19 from ..operators
.action_layers
import ActionSlot
22 def get_rigify_action_slots(metarig_data
: Armature
) -> IdPropSequence
['ActionSlot']:
23 return metarig_data
.rigify_action_slots
# noqa
27 """Abstract non-RNA base for the action list slots."""
29 action
: Optional
[Action
]
33 transform_channel
: str
40 trigger_action_a
: Optional
[Action
]
41 trigger_action_b
: Optional
[Action
]
43 ############################################
44 # Action Constraint Setup
47 def keyed_bone_names(self
) -> List
[str]:
48 """Return a list of bone names that have keyframes in the Action of this Slot."""
51 for fc
in self
.action
.fcurves
:
52 # Extracting bone name from fcurve data path
53 if fc
.data_path
.startswith('pose.bones["'):
54 bone_name
= fc
.data_path
[12:].split('"]')[0]
56 if bone_name
not in keyed_bones
:
57 keyed_bones
.append(bone_name
)
62 def do_symmetry(self
) -> bool:
63 return self
.symmetrical
and get_name_side(self
.subtarget
) != Side
.MIDDLE
66 def default_side(self
):
67 return get_name_side(self
.subtarget
)
69 def get_min_max(self
, side
=Side
.MIDDLE
) -> Tuple
[float, float]:
70 if side
== -self
.default_side
:
71 # Flip min/max in some cases - based on code of Paste Pose Flipped
72 if self
.transform_channel
in ['LOCATION_X', 'ROTATION_Z', 'ROTATION_Y']:
73 return -self
.trans_min
, -self
.trans_max
74 return self
.trans_min
, self
.trans_max
76 def get_factor_expression(self
, var
, side
=Side
.MIDDLE
):
77 assert not self
.is_corrective
79 trans_min
, trans_max
= self
.get_min_max(side
)
81 if 'ROTATION' in self
.transform_channel
:
82 var
= f
'({var}*180/pi)'
84 return f
'clamp(({var} - {trans_min:.4}) / {trans_max - trans_min:.4})'
86 def get_trigger_expression(self
, var_a
, var_b
):
87 assert self
.is_corrective
89 return f
'clamp({var_a} * {var_b})'
91 ##################################
94 def get_default_channel_value(self
) -> float:
95 # The default transformation value for rotation and location is 0, but for scale it's 1.
96 return 1.0 if 'SCALE' in self
.transform_channel
else 0.0
98 def get_default_factor(self
, side
=Side
.MIDDLE
, *, triggers
=None) -> float:
99 """ Based on the transform channel, and transform range,
100 calculate the evaluation factor in the default pose.
102 if self
.is_corrective
:
103 if not triggers
or None in triggers
:
106 val_a
, val_b
= [trigger
.get_default_factor(side
) for trigger
in triggers
]
108 return clamp(val_a
* val_b
)
111 trans_min
, trans_max
= self
.get_min_max(side
)
113 if trans_min
== trans_max
:
114 # Avoid division by zero
117 def_val
= self
.get_default_channel_value()
118 factor
= (def_val
- trans_min
) / (trans_max
- trans_min
)
122 def get_default_frame(self
, side
=Side
.MIDDLE
, *, triggers
=None) -> float:
123 """ Based on the transform channel, frame range and transform range,
124 we can calculate which frame within the action should have the keyframe
125 which has the default pose.
126 This is the frame which will be read when the transformation is at its default
127 (so 1.0 for scale and 0.0 for loc/rot)
129 factor
= self
.get_default_factor(side
, triggers
=triggers
)
131 return self
.frame_start
* (1 - factor
) + self
.frame_end
* factor
133 def is_default_frame_integer(self
) -> bool:
134 default_frame
= self
.get_default_frame()
136 return abs(default_frame
- round(default_frame
)) < 0.001
139 class GeneratedActionSlot(ActionSlotBase
):
140 """Non-RNA version of the action list slot."""
142 def __init__(self
, action
, *, enabled
=True, symmetrical
=True, subtarget
='',
143 transform_channel
='LOCATION_X', target_space
='LOCAL', frame_start
=0,
144 frame_end
=2, trans_min
=-0.05, trans_max
=0.05, is_corrective
=False,
145 trigger_action_a
=None, trigger_action_b
=None):
147 self
.enabled
= enabled
148 self
.symmetrical
= symmetrical
149 self
.subtarget
= subtarget
150 self
.transform_channel
= transform_channel
151 self
.target_space
= target_space
152 self
.frame_start
= frame_start
153 self
.frame_end
= frame_end
154 self
.trans_min
= trans_min
155 self
.trans_max
= trans_max
156 self
.is_corrective
= is_corrective
157 self
.trigger_action_a
= trigger_action_a
158 self
.trigger_action_b
= trigger_action_b
161 class ActionLayer(RigComponent
):
162 """An action constraint layer instance, applying an action to a symmetry side."""
164 rigify_sub_object_run_late
= True
166 owner
: 'ActionLayerBuilder'
170 def __init__(self
, owner
, slot
, side
):
171 super().__init
__(owner
)
176 self
.name
= self
._get
_name
()
178 self
.use_trigger
= False
180 if slot
.is_corrective
:
181 trigger_a
= self
.owner
.action_map
[slot
.trigger_action_a
.name
]
182 trigger_b
= self
.owner
.action_map
[slot
.trigger_action_b
.name
]
184 self
.trigger_a
= trigger_a
.get(side
) or trigger_a
.get(Side
.MIDDLE
)
185 self
.trigger_b
= trigger_b
.get(side
) or trigger_b
.get(Side
.MIDDLE
)
187 self
.trigger_a
.use_trigger
= True
188 self
.trigger_b
.use_trigger
= True
191 self
.bone_name
= change_name_side(slot
.subtarget
, side
)
193 self
.bones
= self
._filter
_bones
()
195 self
.owner
.layers
.append(self
)
198 def use_property(self
):
199 return self
.slot
.is_corrective
or self
.use_trigger
202 name
= self
.slot
.action
.name
204 if self
.side
== Side
.LEFT
:
206 elif self
.side
== Side
.RIGHT
:
211 def _filter_bones(self
):
212 controls
= self
._control
_bones
()
213 bones
= [bone
for bone
in self
.slot
.keyed_bone_names
if bone
not in controls
]
215 if self
.side
!= Side
.MIDDLE
:
216 bones
= [name
for name
in bones
if get_name_side(name
) in (self
.side
, Side
.MIDDLE
)]
220 def _control_bones(self
):
221 if self
.slot
.is_corrective
:
222 return self
.trigger_a
._control
_bones
() | self
.trigger_b
._control
_bones
()
223 elif self
.slot
.do_symmetry
:
224 return {self
.bone_name
, mirror_name(self
.bone_name
)}
226 return {self
.bone_name
}
228 def configure_bones(self
):
229 if self
.use_property
:
230 factor
= self
.slot
.get_default_factor(self
.side
)
232 self
.make_property(self
.owner
.property_bone
, self
.name
, float(factor
))
235 if self
.slot
.is_corrective
and self
.use_trigger
:
236 raise MetarigError(f
"Corrective action used as trigger: {self.slot.action.name}")
238 if self
.use_property
:
239 self
.rig_input_driver(self
.owner
.property_bone
, quote_property(self
.name
))
241 for bone_name
in self
.bones
:
242 self
.rig_bone(bone_name
)
244 def rig_bone(self
, bone_name
):
245 if bone_name
not in self
.obj
.pose
.bones
:
247 f
"Bone '{bone_name}' from action '{self.slot.action.name}' not found")
249 if self
.side
!= Side
.MIDDLE
and get_name_side(bone_name
) == Side
.MIDDLE
:
254 con
= self
.make_constraint(
256 name
=f
'Action {self.name}',
259 action
=self
.slot
.action
,
260 frame_start
=self
.slot
.frame_start
,
261 frame_end
=self
.slot
.frame_end
,
262 mix_mode
='BEFORE_SPLIT',
266 self
.rig_output_driver(con
, 'eval_time')
268 def rig_output_driver(self
, obj
, prop
):
269 if self
.use_property
:
270 self
.make_driver(obj
, prop
, variables
=[(self
.owner
.property_bone
, self
.name
)])
272 self
.rig_input_driver(obj
, prop
)
274 def rig_input_driver(self
, obj
, prop
):
275 if self
.slot
.is_corrective
:
276 self
.rig_corrective_driver(obj
, prop
)
278 self
.rig_factor_driver(obj
, prop
)
280 def rig_corrective_driver(self
, obj
, prop
):
283 expression
=self
.slot
.get_trigger_expression('a', 'b'),
285 'a': (self
.owner
.property_bone
, self
.trigger_a
.name
),
286 'b': (self
.owner
.property_bone
, self
.trigger_b
.name
),
290 def rig_factor_driver(self
, obj
, prop
):
291 if self
.side
!= Side
.MIDDLE
:
292 control_name
= change_name_side(self
.slot
.subtarget
, self
.side
)
294 control_name
= self
.slot
.subtarget
296 if control_name
not in self
.obj
.pose
.bones
:
298 f
"Control bone '{control_name}' for action '{self.slot.action.name}' not found")
300 channel
= self
.slot
.transform_channel\
301 .replace("LOCATION", "LOC").replace("ROTATION", "ROT")
305 expression
=self
.slot
.get_factor_expression('var', side
=self
.side
),
307 driver_var_transform(
308 self
.obj
, control_name
,
310 space
=self
.slot
.target_space
,
311 rotation_mode
='SWING_TWIST_Y',
317 def rig_child_shape_keys(self
):
318 for child
in self
.owner
.child_meshes
:
319 mesh
: Mesh
= child
.data
322 for key_block
in mesh
.shape_keys
.key_blocks
[1:]:
323 if key_block
.name
== self
.name
:
324 self
.rig_shape_key(key_block
)
326 def rig_shape_key(self
, key_block
):
327 self
.rig_output_driver(key_block
, 'value')
330 class ActionLayerBuilder(GeneratorPlugin
, BoneUtilityMixin
, MechanismUtilityMixin
):
332 Implements centralized generation of action layer constraints.
335 slot_list
: List
[ActionSlotBase
]
336 layers
: List
[ActionLayer
]
337 action_map
: Dict
[str, Dict
[Side
, ActionLayer
]]
338 property_bone
: Optional
[str]
339 child_meshes
: List
[MeshObject
]
341 def __init__(self
, generator
):
342 super().__init
__(generator
)
344 metarig_data
= generator
.metarig
.data
345 self
.slot_list
= list(get_rigify_action_slots(metarig_data
))
348 def initialize(self
):
351 self
.rigify_sub_objects
= []
353 # Generate layers for active valid slots
354 action_slots
= [slot
for slot
in self
.slot_list
if slot
.enabled
and slot
.action
]
356 # Constraints will be added in reverse order because each one is added to the top
357 # of the stack when created. However, Before Original reverses the effective
358 # order of transformations again, restoring the original sequence.
359 for act_slot
in self
.sort_slots(action_slots
):
360 self
.spawn_slot_layers(act_slot
)
363 def sort_slots(slots
: List
[ActionSlotBase
]):
364 indices
= {slot
.action
.name
: i
for i
, slot
in enumerate(slots
)}
366 def action_key(action
: Action
):
367 return indices
.get(action
.name
, -1) if action
else -1
369 def slot_key(slot
: ActionSlotBase
):
370 # Ensure corrective actions are added after their triggers.
371 if slot
.is_corrective
:
372 return max(action_key(slot
.action
),
373 action_key(slot
.trigger_action_a
) + 0.5,
374 action_key(slot
.trigger_action_b
) + 0.5)
376 return action_key(slot
.action
)
378 return sorted(slots
, key
=slot_key
)
380 def spawn_slot_layers(self
, act_slot
):
381 name
= act_slot
.action
.name
383 if name
in self
.action_map
:
384 raise MetarigError(f
"Action slot with duplicate action: {name}")
386 if act_slot
.is_corrective
:
387 if not act_slot
.trigger_action_a
or not act_slot
.trigger_action_b
:
388 raise MetarigError(f
"Action slot has missing triggers: {name}")
390 trigger_a
= self
.action_map
.get(act_slot
.trigger_action_a
.name
)
391 trigger_b
= self
.action_map
.get(act_slot
.trigger_action_b
.name
)
393 if not trigger_a
or not trigger_b
:
394 raise MetarigError(f
"Action slot references missing trigger slot(s): {name}")
396 symmetry
= Side
.LEFT
in trigger_a
or Side
.LEFT
in trigger_b
399 symmetry
= act_slot
.do_symmetry
402 self
.action_map
[name
] = {
403 Side
.LEFT
: ActionLayer(self
, act_slot
, Side
.LEFT
),
404 Side
.RIGHT
: ActionLayer(self
, act_slot
, Side
.RIGHT
),
407 self
.action_map
[name
] = {
408 Side
.MIDDLE
: ActionLayer(self
, act_slot
, Side
.MIDDLE
)
411 def generate_bones(self
):
412 if any(child
.use_property
for child
in self
.layers
):
413 self
.property_bone
= self
.new_bone("MCH-action-props")
417 self
.child_meshes
= [
418 verify_mesh_obj(child
)
419 for child
in self
.generator
.obj
.children_recursive
420 if child
.type == 'MESH'