Source code for blenderproc.python.types.ArmatureUtility

""" An armature object, which can be connected with Link objects to other armature objects. """

from typing import Union, List, Optional

import numpy as np
from mathutils import Euler, Vector
import bpy

from blenderproc.python.utility.Utility import Utility
from blenderproc.python.types.EntityUtility import Entity


[docs] class Armature(Entity): """ An armature object, which can be connected with Link objects to other armature objects. It enables the transformation of different objects. """ def __init__(self, bpy_object: bpy.types.Object): super().__init__(bpy_object=bpy_object)
[docs] def set_rotation_euler(self, rotation_euler: Union[float, list, Euler, np.ndarray], frame: Optional[int] = None, mode: str = "absolute"): """ Rotates the armature based on euler angles. Validate values with given constraints. :param rotation_euler: The amount of rotation (in radians). Either three floats for x, y and z axes, or a single float. In the latter case, the axis of rotation is derived based on the rotation constraint. If these are not properly set (i.e., two axes must have equal min/max values) an exception will be thrown. :param frame: Keyframe where to insert the respective rotations. :param mode: One of ["absolute", "relative"]. For absolute rotations we clip the rotation value based on the constraints. For relative, we don't - this will result in inverse motion after the constraint's limits have been reached. """ assert mode in ["absolute", "relative"] bpy.ops.object.select_all(action='DESELECT') bone = self.blender_obj.pose.bones.get('Bone') bone.bone.select = True bone.rotation_mode = 'XYZ' # in absolute mode we overwrite the rotation values of the armature if mode == "absolute": if isinstance(rotation_euler, float): axis = self._determine_rotation_axis() rotation_euler = self._clip_value_from_constraint(value=rotation_euler, constraint_name="Limit Rotation", axis=axis) current_rotation_euler = bone.rotation_euler current_rotation_euler[["X", "Y", "Z"].index(axis)] = rotation_euler bone.rotation_euler = current_rotation_euler print(f"Set rotation_euler of armature {self.get_name()} to {rotation_euler}") else: bone.rotation_euler = Vector([self._clip_value_from_constraint(value=rot_euler, constraint_name="Limit Rotation", axis=axis) for rot_euler, axis in zip(rotation_euler, ["X", "Y", "Z"])]) print(f"Set rotation_euler of armature {self.get_name()} to {rotation_euler}") # in relative mode we add the rotation to the current value elif mode == "relative": if isinstance(rotation_euler, float): axis = self._determine_rotation_axis() bone.rotation_euler.rotate_axis(axis, rotation_euler) print(f"Relatively rotated armature {self.get_name()} around axis {axis} for {rotation_euler} radians") else: for axis, rotation in zip(["X", "Y", "Z"], rotation_euler): bone.rotation_euler.rotate_axis(axis, rotation) print(f"Relatively rotated armature {self.get_name()} for {rotation_euler} radians") Utility.insert_keyframe(bone, "rotation_euler", frame) if frame is not None and frame > bpy.context.scene.frame_end: bpy.context.scene.frame_end += 1
[docs] def _determine_rotation_axis(self): """ Determines the single rotation axis and checks if the constraints are set well to have only one axis of freedom. :return: The single rotation axis ('X', 'Y' or 'Z'). """ c = self.get_constraint(constraint_name="Limit Rotation") assert c is not None, "Tried to determine the single rotation axis but no rotation constraints are set!" axes = ['X', 'Y', 'Z'] if c.use_limit_x and c.min_x == c.max_x: axes.pop(axes.index('X')) if c.use_limit_y and c.min_y == c.max_y: axes.pop(axes.index('Y')) if c.use_limit_z and c.min_z == c.max_z: axes.pop(axes.index('Z')) assert len(axes) == 1, f"Constraints are set wrong for a rotation around a single axis. Only one axis should " \ f"be allowed to move, but found freedom in {len(axes)} axes of armature " \ f"{self.get_name()} (constraint: {c}, uses limits (xyz): " \ f"{c.use_limit_x, c.use_limit_y, c.use_limit_z}, " \ f"values: {c.min_x, c.max_x, c.min_y, c.max_y, c.min_z, c.max_z})." return axes[0]
[docs] def _clip_value_from_constraint(self, value: float, constraint_name: str, axis: str) -> float: """ Checks if an axis is constraint, and clips the value to the min/max of this constraint. If the constraint does not exist, nothing is done. :param value: Value to be clipped. :param constraint_name: Name of the constraint. :param axis: Axis to check. :return: Clipped value if a constraint is set, else the initial value. """ c = self.get_constraint(constraint_name=constraint_name) if c is not None: min_value = {"x": c.min_x, "y": c.min_y, "z": c.min_z}[axis.lower()] max_value = {"x": c.max_x, "y": c.max_y, "z": c.max_z}[axis.lower()] print(f"Clipping {value} to be in range {min_value}, {max_value}") if value < min_value: return min_value if value > max_value: return max_value return value
[docs] def add_constraint_if_not_existing(self, constraint_name: str) -> bpy.types.Constraint: """ Adds a new constraint if it doesn't exist, and returns the specified constraint. :param constraint_name: Name of the desired constraint. """ if constraint_name not in self.blender_obj.pose.bones["Bone"].constraints.keys(): self.blender_obj.pose.bones["Bone"].constraints.new(constraint_name.upper().replace(' ', '_')) return self.blender_obj.pose.bones["Bone"].constraints[constraint_name]
[docs] def set_rotation_constraint(self, x_limits: Optional[List[float]] = None, y_limits: Optional[List[float]] = None, z_limits: Optional[List[float]] = None): """ Sets rotation constraints on the armature's bone. :param x_limits: A list of two float values specifying min/max radiant values along the x-axis or None if no constraint should be applied. :param y_limits: A list of two float values specifying min/max radiant values along the y-axis or None if no constraint should be applied. :param z_limits: A list of two float values specifying min/max radiant values along the z-axis or None if no constraint should be applied. """ if x_limits is None and y_limits is None and z_limits is None: return # add new constraint if it doesn't exist constraint = self.add_constraint_if_not_existing(constraint_name="Limit Rotation") if x_limits is not None: constraint.use_limit_x = True constraint.min_x, constraint.max_x = x_limits if y_limits is not None: constraint.use_limit_y = True constraint.min_y, constraint.max_y = y_limits if z_limits is not None: constraint.use_limit_z = True constraint.min_z, constraint.max_z = z_limits constraint.owner_space = "LOCAL"
[docs] def set_location_constraint(self, x_limits: Optional[List[float]] = None, y_limits: Optional[List[float]] = None, z_limits: Optional[List[float]] = None): """ Sets location constraints on the armature's bone. :param x_limits: A list of two float values specifying min/max values along the x-axis or None if no constraint should be applied. :param y_limits: A list of two float values specifying min/max values along the y-axis or None if no constraint should be applied. :param z_limits: A list of two float values specifying min/max values along the z-axis or None if no constraint should be applied. """ if x_limits is None and y_limits is None and z_limits is None: return # add new constraint if it doesn't exist constraint = self.add_constraint_if_not_existing(constraint_name="Limit Location") if x_limits is not None: constraint.use_min_x = True constraint.use_max_x = True constraint.min_x, constraint.max_x = x_limits if y_limits is not None: constraint.use_min_y = True constraint.use_max_y = True constraint.min_y, constraint.max_y = y_limits if z_limits is not None: constraint.use_min_z = True constraint.use_max_z = True constraint.min_z, constraint.max_z = z_limits constraint.owner_space = "LOCAL"
[docs] def get_constraint(self, constraint_name: str) -> Optional[bpy.types.Constraint]: """ Returns the desired constraint if existing; otherwise None. :param constraint_name: Name of the constraint. :return: Constraint if it exists; else None. """ if constraint_name in self.blender_obj.pose.bones["Bone"].constraints.keys(): return self.blender_obj.pose.bones["Bone"].constraints[constraint_name] return None
[docs] def get_location_constraint(self) -> Optional[bpy.types.Constraint]: """ Returns the location constraint if existing; otherwise None. :return: Location constraint if it exists; else None. """ return self.get_constraint(constraint_name="Limit Location")
[docs] def get_rotation_constraint(self) -> Optional[bpy.types.Constraint]: """ Returns the rotation constraint if existing; otherwise None. :return: Rotation constraint if it exists; else None. """ return self.get_constraint(constraint_name="Limit Rotation")
[docs] def remove_constraint(self, constraint_key: str): """ Removes a specified constraint. :param constraint_key: Key to be removed. """ bone = self.blender_obj.pose.bones["Bone"] bone.constraints.remove(bone.constraints[constraint_key])
[docs] def remove_constraints(self): """ Removes all constraints of the armature. """ bone = self.blender_obj.pose.bones["Bone"] for constraint_key in bone.constraints.keys(): self.remove_constraint(constraint_key=constraint_key)
[docs] def hide(self, hide_object: bool = True): """ Sets the visibility of the object. :param hide_object: Determines whether the object should be hidden in rendering. """ self.blender_obj.hide_render = hide_object