diff --git a/__init__.py b/__init__.py index 883d026..93e9cd2 100644 --- a/__init__.py +++ b/__init__.py @@ -21,8 +21,8 @@ bl_info = { "name": "Mixamo Rig", "author": "Mixamo", - "version": (1, 11, 11), - "blender": (2, 80, 0), + "version": (2, 0, 0), + "blender": (4, 2, 0), "location": "3D View > Mixamo> Control Rig", "description": "Generate a control rig from the selected Mixamo Fbx skeleton", "category": "Animation"} diff --git a/lib/addon.py b/lib/addon.py new file mode 100644 index 0000000..1116e62 --- /dev/null +++ b/lib/addon.py @@ -0,0 +1,15 @@ +import bpy, sys, linecache, ast + +def get_error_message(): + exc_type, exc_obj, tb = sys.exc_info() + f = tb.tb_frame + lineno = tb.tb_lineno + filename = f.f_code.co_filename + linecache.checkcache(filename) + line = linecache.getline(filename, lineno, f.f_globals) + error_message = 'Error in ({}\nLine {} "{}"): {}'.format(filename, lineno, line.strip(), exc_obj) + return error_message + + +def get_addon_preferences(): + return bpy.context.preferences.addons[__package__].preferences \ No newline at end of file diff --git a/lib/animation.py b/lib/animation.py new file mode 100644 index 0000000..95d0864 --- /dev/null +++ b/lib/animation.py @@ -0,0 +1,203 @@ +import bpy +from .maths_geo import * +from .bones_pose import * +from .version import * + +def bake_anim(frame_start=0, frame_end=10, only_selected=False, bake_bones=True, bake_object=False, ik_data=None): + scn = bpy.context.scene + obj_data = [] + bones_data = [] + armature = bpy.data.objects.get(bpy.context.active_object.name) + + def get_bones_matrix(): + matrix = {} + for pbone in armature.pose.bones: + if only_selected and not pbone.bone.select: + continue + + bmat = pbone.matrix + + # IK poles + if pbone.name.startswith("Ctrl_ArmPole") or pbone.name.startswith("Ctrl_LegPole"): + b1 = b2 = None + src_arm = ik_data["src_arm"] + type = "" + if "Leg" in pbone.name: + type = "Leg" + elif "Arm" in pbone.name: + type = "Arm" + + name_split = pbone.name.split('_') + side = name_split[len(name_split)-1] + b1_name = ik_data[type+side][0] + b2_name = ik_data[type+side][1] + b1 = src_arm.pose.bones.get(b1_name) + b2 = src_arm.pose.bones.get(b2_name) + + _axis = None + if type == "Leg": + _axis = (b1.z_axis*0.5) + (b2.z_axis*0.5)#b1.z_axis# + elif type == "Arm": + if side == "Left": + _axis = b2.x_axis + elif side == "Right": + _axis = -b2.x_axis + + pole_pos = get_ik_pole_pos(b1, b2, method=2, axis=_axis) + #pole_pos = b2.head + (b2.z_axis.normalized() * (b2.tail-b2.head).magnitude) + bmat = Matrix.Translation(pole_pos) + + # Child Of constraints are preserved after baking + # need to compensate the matrix with the Child Of transformation + child_of_cns = pbone.constraints.get("Child Of") + if child_of_cns: + if child_of_cns.influence == 1.0 and child_of_cns.mute == False: + bmat = get_pose_bone(child_of_cns.subtarget).matrix_channel.inverted() @ bmat + + matrix[pbone.name] = armature.convert_space(pose_bone=pbone, matrix=bmat, from_space="POSE", to_space="LOCAL") + + return matrix + + def get_obj_matrix(): + parent = armature.parent + matrix = armature.matrix_world + if parent: + return parent.matrix_world.inverted_safe() @ matrix + else: + return matrix.copy() + + # store matrices + current_frame = scn.frame_current + + for f in range(int(frame_start), int(frame_end+1)): + scn.frame_set(f) + bpy.context.view_layer.update() + + if bake_bones: + bones_data.append((f, get_bones_matrix())) + if bake_object: + obj_data.append((f, get_obj_matrix())) + + # set new action + action = bpy.data.actions.new("Action") + anim_data = armature.animation_data_create() + anim_data.action = action + + def store_keyframe(bn, prop_type, fc_array_index, fra, val): + fc_data_path = 'pose.bones["' + bn + '"].' + prop_type + fc_key = (fc_data_path, fc_array_index) + if not keyframes.get(fc_key): + keyframes[fc_key] = [] + keyframes[fc_key].extend((fra, val)) + + + # set transforms and store keyframes + if bake_bones: + for pb in armature.pose.bones: + if only_selected and not pb.bone.select: + continue + + euler_prev = None + quat_prev = None + keyframes = {} + + for (f, matrix) in bones_data: + pb.matrix_basis = matrix[pb.name].copy() + + for arr_idx, value in enumerate(pb.location): + store_keyframe(pb.name, "location", arr_idx, f, value) + + rotation_mode = pb.rotation_mode + + if rotation_mode == 'QUATERNION': + if quat_prev is not None: + quat = pb.rotation_quaternion.copy() + quat.make_compatible(quat_prev) + pb.rotation_quaternion = quat + quat_prev = quat + del quat + else: + quat_prev = pb.rotation_quaternion.copy() + + for arr_idx, value in enumerate(pb.rotation_quaternion): + store_keyframe(pb.name, "rotation_quaternion", arr_idx, f, value) + + elif rotation_mode == 'AXIS_ANGLE': + for arr_idx, value in enumerate(pb.rotation_axis_angle): + store_keyframe(pb.name, "rotation_axis_angle", arr_idx, f, value) + + else: # euler, XYZ, ZXY etc + if euler_prev is not None: + euler = pb.rotation_euler.copy() + euler.make_compatible(euler_prev) + pb.rotation_euler = euler + euler_prev = euler + del euler + else: + euler_prev = pb.rotation_euler.copy() + + for arr_idx, value in enumerate(pb.rotation_euler): + store_keyframe(pb.name, "rotation_euler", arr_idx, f, value) + + for arr_idx, value in enumerate(pb.scale): + store_keyframe(pb.name, "scale", arr_idx, f, value) + + + # Add keyframes + for fc_key, key_values in keyframes.items(): + data_path, index = fc_key + fcurve = action.fcurves.find(data_path=data_path, index=index) + if fcurve == None: + fcurve = action.fcurves.new(data_path, index=index, action_group=pb.name) + + num_keys = len(key_values) // 2 + fcurve.keyframe_points.add(num_keys) + fcurve.keyframe_points.foreach_set('co', key_values) + + if blender_version._float >= 290:# internal error when doing so with Blender 2.83, only for Blender 2.90 and higher + linear_enum_value = bpy.types.Keyframe.bl_rna.properties['interpolation'].enum_items['LINEAR'].value + fcurve.keyframe_points.foreach_set('interpolation', (linear_enum_value,) * num_keys) + else: + for kf in fcurve.keyframe_points: + kf.interpolation = 'LINEAR' + + + if bake_object: + euler_prev = None + quat_prev = None + + for (f, matrix) in obj_data: + name = "Action Bake" + armature.matrix_basis = matrix + + armature.keyframe_insert("location", index=-1, frame=f, group=name) + + rotation_mode = armature.rotation_mode + if rotation_mode == 'QUATERNION': + if quat_prev is not None: + quat = armature.rotation_quaternion.copy() + quat.make_compatible(quat_prev) + armature.rotation_quaternion = quat + quat_prev = quat + del quat + else: + quat_prev = armature.rotation_quaternion.copy() + armature.keyframe_insert("rotation_quaternion", index=-1, frame=f, group=name) + elif rotation_mode == 'AXIS_ANGLE': + armature.keyframe_insert("rotation_axis_angle", index=-1, frame=f, group=name) + else: # euler, XYZ, ZXY etc + if euler_prev is not None: + euler = armature.rotation_euler.copy() + euler.make_compatible(euler_prev) + armature.rotation_euler = euler + euler_prev = euler + del euler + else: + euler_prev = armature.rotation_euler.copy() + armature.keyframe_insert("rotation_euler", index=-1, frame=f, group=name) + + armature.keyframe_insert("scale", index=-1, frame=f, group=name) + + + # restore current frame + scn.frame_set(current_frame) diff --git a/lib/armature.py b/lib/armature.py new file mode 100644 index 0000000..b232b97 --- /dev/null +++ b/lib/armature.py @@ -0,0 +1,19 @@ +import bpy + +def restore_armature_layers(layers_select): + # restore the armature layers visibility + for i in range(0, 32): + bpy.context.active_object.data.layers[i] = layers_select[i] + + +def enable_all_armature_layers(): + # enable all layers + # and return the list of each layer visibility + _layers = bpy.context.active_object.data.layers + layers_select = [] + for i in range(0, 32): + layers_select.append(_layers[i]) + for i in range(0, 32): + bpy.context.active_object.data.layers[i] = True + + return layers_select \ No newline at end of file diff --git a/lib/bones_data.py b/lib/bones_data.py new file mode 100644 index 0000000..f7a5826 --- /dev/null +++ b/lib/bones_data.py @@ -0,0 +1,17 @@ +import bpy + +def get_data_bone(name): + return bpy.context.active_object.data.bones.get(name) + + +def set_bone_layer(databone, layer_idx, multi=False): + if databone == None: + return + + databone.layers[layer_idx] = True + if multi: + return + + for i, lay in enumerate(databone.layers): + if i != layer_idx: + databone.layers[i] = False \ No newline at end of file diff --git a/lib/bones_edit.py b/lib/bones_edit.py new file mode 100644 index 0000000..4a03525 --- /dev/null +++ b/lib/bones_edit.py @@ -0,0 +1,19 @@ +import bpy + +def get_edit_bone(name): + return bpy.context.object.data.edit_bones.get(name) + + +def copy_bone_transforms(bone1, bone2): + # copy editbone bone1 transforms to bone 2 + bone2.head = bone1.head.copy() + bone2.tail = bone1.tail.copy() + bone2.roll = bone1.roll + + +def create_edit_bone(bone_name, deform=False): + b = get_edit_bone(bone_name) + if b == None: + b = bpy.context.active_object.data.edit_bones.new(bone_name) + b.use_deform = deform + return b \ No newline at end of file diff --git a/lib/bones_pose.py b/lib/bones_pose.py new file mode 100644 index 0000000..6d4aa65 --- /dev/null +++ b/lib/bones_pose.py @@ -0,0 +1,104 @@ +import bpy +from .objects import * +from .version import * + +def get_custom_shape_scale(pbone, uniform=True): + if blender_version._float >= 300: + if uniform: + # uniform scale + val = 0 + for i in range(0,3): + val += pbone.custom_shape_scale_xyz[i] + return val/3 + # array scale + else: + return pbone.custom_shape_scale_xyz + # pre-Blender 3.0 + else: + return pbone.custom_shape_scale + + +def get_selected_pbone_name(): + try: + return bpy.context.selected_pose_bones[0].name#.active_pose_bone.name + except: + return + + +def get_pose_bone(name): + return bpy.context.active_object.pose.bones.get(name) + + +def lock_pbone_transform(pbone, type, list): + for i in list: + if type == "location": + pbone.lock_location[i] = True + elif type == "rotation": + pbone.lock_rotation[i] = True + elif type == "scale": + pbone.lock_scale[i] = True + + +def set_bone_custom_shape(pbone, cs_name): + cs = get_object(cs_name) + if cs == None: + append_cs(cs_name) + cs = get_object(cs_name) + + pbone.custom_shape = cs + + +def set_bone_color_group(obj, pb, grp_name): + # mixamo required color + orange = (0.969, 0.565, 0.208) + orange_light = (0.957, 0.659, 0.416) + blue_dark = (0.447, 0.682, 1.0) + blue_light = (0.365, 0.851, 1.0) + + # base color + green = (0.0, 1.0, 0.0) + red = (1.0, 0.0, 0.0) + blue = (0.0, 0.9, 1.0) + + grp_color_master = orange_light + grp_color_neck = orange_light + grp_color_root_master = orange + grp_color_head = orange + grp_color_body_mid = green + grp_color_body_left = blue_dark + grp_color_body_right = blue_light + + grp = obj.pose.bone_groups.get(grp_name) + if grp == None: + grp = obj.pose.bone_groups.new(name=grp_name) + grp.color_set = 'CUSTOM' + + grp_color = None + if grp_name == "body_mid": + grp_color = grp_color_body_mid + elif grp_name == "body_left": + grp_color = grp_color_body_left + elif grp_name == "body_right": + grp_color = grp_color_body_right + elif grp_name == "master": + grp_color = grp_color_master + elif grp_name == "neck": + grp_color = grp_color_head + elif grp_name == "head": + grp_color = grp_color_neck + elif grp_name == "root_master": + grp_color = grp_color_root_master + + # set normal color + grp.colors.normal = grp_color + + # set select color/active color + for col_idx in range(0,3): + grp.colors.select[col_idx] = grp_color[col_idx] + 0.2 + grp.colors.active[col_idx] = grp_color[col_idx] + 0.4 + + pb.bone_group = grp + + +def update_transform(): + bpy.ops.transform.rotate(value=0, orient_axis='Z', orient_type='VIEW', orient_matrix=((0.0, 0.0, 0), (0, 0.0, 0.0), (0.0, 0.0, 0.0)), orient_matrix_type='VIEW', mirror=False) \ No newline at end of file diff --git a/lib/constraints.py b/lib/constraints.py new file mode 100644 index 0000000..3111787 --- /dev/null +++ b/lib/constraints.py @@ -0,0 +1,19 @@ +import bpy + +def add_copy_transf(p_bone, tgt, subtgt): + cns_transf = p_bone.constraints.get("Copy Transforms") + if cns_transf == None: + cns_transf = p_bone.constraints.new("COPY_TRANSFORMS") + cns_transf.name = "Copy Transforms" + cns_transf.target = tgt + cns_transf.subtarget = subtgt + + +def set_constraint_inverse_matrix(cns): + # set the inverse matrix of Child Of constraint + tar_obj = cns.target + subtarget_pbone = tar_obj.pose.bones.get(cns.subtarget) + if subtarget_pbone: + #cns.inverse_matrix = tar_obj.matrix_world.inverted() @ subtarget_pbone.matrix_basis.inverted() + print("reset child of cns", cns.name, cns.subtarget) + cns.inverse_matrix = subtarget_pbone.bone.matrix_local.to_4x4().inverted() \ No newline at end of file diff --git a/lib/context.py b/lib/context.py new file mode 100644 index 0000000..a3718bf --- /dev/null +++ b/lib/context.py @@ -0,0 +1,10 @@ +import bpy + +def get_current_mode(): + return bpy.context.mode + + +def restore_current_mode(current_mode): + if current_mode == 'EDIT_ARMATURE': + current_mode = 'EDIT' + bpy.ops.object.mode_set(mode=current_mode) \ No newline at end of file diff --git a/lib/cs.blend b/lib/cs.blend new file mode 100644 index 0000000..04431e7 Binary files /dev/null and b/lib/cs.blend differ diff --git a/lib/custom_props.py b/lib/custom_props.py new file mode 100644 index 0000000..794ea37 --- /dev/null +++ b/lib/custom_props.py @@ -0,0 +1,60 @@ +import bpy +from .version import blender_version + + +def get_prop_setting(node, prop_name, setting): + if blender_version._float >= 300: + return node.id_properties_ui(prop_name).as_dict()[setting] + else: + return node['_RNA_UI'][prop_name][setting] + + +def set_prop_setting(node, prop_name, setting, value): + if blender_version._float >= 300: + ui_data = node.id_properties_ui(prop_name) + if setting == 'default': + ui_data.update(default=value) + elif setting == 'min': + ui_data.update(min=value) + elif setting == 'max': + ui_data.update(max=value) + elif setting == 'soft_min': + ui_data.update(soft_min=value) + elif setting == 'soft_max': + ui_data.update(soft_max=value) + elif setting == 'description': + ui_data.update(description=value) + + else: + if not "_RNA_UI" in node.keys(): + node["_RNA_UI"] = {} + node['_RNA_UI'][prop_name][setting] = value + + +def create_custom_prop(node=None, prop_name="", prop_val=1.0, prop_min=0.0, prop_max=1.0, prop_description="", soft_min=None, soft_max=None, default=None): + if soft_min == None: + soft_min = prop_min + if soft_max == None: + soft_max = prop_max + + if blender_version._float < 300: + if not "_RNA_UI" in node.keys(): + node["_RNA_UI"] = {} + + node[prop_name] = prop_val + + if default == None: + default = prop_val + + if blender_version._float < 300: + node["_RNA_UI"][prop_name] = {'use_soft_limits':True, 'min': prop_min, 'max': prop_max, 'description': prop_description, 'soft_min':soft_min, 'soft_max':soft_max, 'default':default} + else: + set_prop_setting(node, prop_name, 'min', prop_min) + set_prop_setting(node, prop_name, 'max', prop_max) + set_prop_setting(node, prop_name, 'description', prop_description) + set_prop_setting(node, prop_name, 'soft_min', soft_min) + set_prop_setting(node, prop_name, 'soft_max', soft_max) + set_prop_setting(node, prop_name, 'default', default) + + # set as overridable + node.property_overridable_library_set('["'+prop_name+'"]', True) \ No newline at end of file diff --git a/lib/drivers.py b/lib/drivers.py new file mode 100644 index 0000000..31b8bda --- /dev/null +++ b/lib/drivers.py @@ -0,0 +1,23 @@ +import bpy + +def add_driver_to_prop(obj, dr_dp, tar_dp, array_idx=-1, exp="var"): + if obj.animation_data == None: + obj.animation_data_create() + + drivers_list = obj.animation_data.drivers + dr = drivers_list.find(dr_dp, index=array_idx) + + if dr == None: + dr = obj.driver_add(dr_dp, array_idx) + + dr.driver.expression = exp + + var = dr.driver.variables.get('var') + + if var == None: + var = dr.driver.variables.new() + + var.type = 'SINGLE_PROP' + var.name = 'var' + var.targets[0].id = obj + var.targets[0].data_path = tar_dp \ No newline at end of file diff --git a/lib/maths_geo.py b/lib/maths_geo.py new file mode 100644 index 0000000..6219111 --- /dev/null +++ b/lib/maths_geo.py @@ -0,0 +1,151 @@ +from math import * +from mathutils import * + + +def mat3_to_vec_roll(mat): + vec = mat.col[1] + vecmat = vec_roll_to_mat3(mat.col[1], 0) + vecmatinv = vecmat.inverted() + rollmat = vecmatinv @ mat + roll = atan2(rollmat[0][2], rollmat[2][2]) + return roll + + +def vec_roll_to_mat3(vec, roll): + target = Vector((0, 0.1, 0)) + nor = vec.normalized() + axis = target.cross(nor) + if axis.dot(axis) > 0.0000000001: # this seems to be the problem for some bones, no idea how to fix + axis.normalize() + theta = target.angle(nor) + bMatrix = Matrix.Rotation(theta, 3, axis) + else: + updown = 1 if target.dot(nor) > 0 else -1 + bMatrix = Matrix.Scale(updown, 3) + bMatrix[2][2] = 1.0 + + rMatrix = Matrix.Rotation(roll, 3, nor) + mat = rMatrix @ bMatrix + return mat + + +def align_bone_x_axis(edit_bone, new_x_axis): + new_x_axis = new_x_axis.cross(edit_bone.y_axis) + new_x_axis.normalize() + dot = max(-1.0, min(1.0, edit_bone.z_axis.dot(new_x_axis))) + angle = acos(dot) + edit_bone.roll += angle + dot1 = edit_bone.z_axis.dot(new_x_axis) + edit_bone.roll -= angle * 2.0 + dot2 = edit_bone.z_axis.dot(new_x_axis) + if dot1 > dot2: + edit_bone.roll += angle * 2.0 + + +def align_bone_z_axis(edit_bone, new_z_axis): + new_z_axis = -(new_z_axis.cross(edit_bone.y_axis)) + new_z_axis.normalize() + dot = max(-1.0, min(1.0, edit_bone.x_axis.dot(new_z_axis))) + angle = acos(dot) + edit_bone.roll += angle + dot1 = edit_bone.x_axis.dot(new_z_axis) + edit_bone.roll -= angle * 2.0 + dot2 = edit_bone.x_axis.dot(new_z_axis) + if dot1 > dot2: + edit_bone.roll += angle * 2.0 + + +def signed_angle(u, v, normal): + nor = normal.normalized() + a = u.angle(v) + + c = u.cross(v) + + if c.magnitude == 0.0: + c = u.normalized().cross(v) + if c.magnitude == 0.0: + return 0.0 + + if c.angle(nor) < 1: + a = -a + return a + + +def project_point_onto_plane(q, p, n): + n = n.normalized() + return q - ((q - p).dot(n)) * n + + +def get_pole_angle(base_bone, ik_bone, pole_location): + pole_normal = (ik_bone.tail - base_bone.head).cross(pole_location - base_bone.head) + projected_pole_axis = pole_normal.cross(base_bone.tail - base_bone.head) + return signed_angle(base_bone.x_axis, projected_pole_axis, base_bone.tail - base_bone.head) + + +def get_pose_matrix_in_other_space(mat, pose_bone): + rest = pose_bone.bone.matrix_local.copy() + rest_inv = rest.inverted() + + if pose_bone.parent and pose_bone.bone.use_inherit_rotation: + par_mat = pose_bone.parent.matrix.copy() + par_inv = par_mat.inverted() + par_rest = pose_bone.parent.bone.matrix_local.copy() + + else: + par_mat = Matrix() + par_inv = Matrix() + par_rest = Matrix() + + smat = rest_inv @ (par_rest @ (par_inv @ mat)) + + return smat + + +def get_ik_pole_pos(b1, b2, method=1, axis=None): + + if method == 1: + # IK pole position based on real IK bones vector + plane_normal = (b1.head - b2.tail) + midpoint = (b1.head + b2.tail) * 0.5 + prepole_dir = b2.head - midpoint#prepole_fk.tail - prepole_fk.head + pole_pos = b2.head + prepole_dir.normalized()# * 4 + pole_pos = project_point_onto_plane(pole_pos, b2.head, plane_normal) + pole_pos = b2.head + ((pole_pos - b2.head).normalized() * (b2.head - b1.head).magnitude * 1.7) + + elif method == 2: + # IK pole position based on bone2 Z axis vector + pole_pos = b2.head + (axis.normalized() * (b2.tail-b2.head).magnitude) + + return pole_pos + + +def rotate_point(point, angle, origin, axis): + rot_mat = Matrix.Rotation(angle, 4, axis.normalized()) + # rotate in world origin space + offset_vec = -origin + offset_knee = point + offset_vec + # rotate + rotated_point = rot_mat @ offset_knee + # bring back to original space + rotated_point = rotated_point -offset_vec + return rotated_point + + +def dot_product(x, y): + return sum([x[i] * y[i] for i in range(len(x))]) + + +def norm(x): + return sqrt(dot_product(x, x)) + + +def normalize(x): + return [x[i] / norm(x) for i in range(len(x))] + + +def project_vector_onto_plane(x, n): + d = dot_product(x, n) / norm(n) + p = [d * normalize(n)[i] for i in range(len(n))] + vec_list = [x[i] - p[i] for i in range(len(x))] + return Vector((vec_list[0], vec_list[1], vec_list[2])) + \ No newline at end of file diff --git a/lib/mixamo.py b/lib/mixamo.py new file mode 100644 index 0000000..0644bc5 --- /dev/null +++ b/lib/mixamo.py @@ -0,0 +1,39 @@ +import bpy +from .bones_pose import * +from .bones_data import * +from ..definitions import naming + +def get_mixamo_prefix(): + p = "" + rig = bpy.context.active_object + + if 'mixamo_prefix' in rig.data.keys(): + p = rig.data["mixamo_prefix"] + + else: + for dbone in rig.data.bones: + if dbone.name.startswith("mixamorig") and ':' in dbone.name: + p = dbone.name.split(':')[0]+':' + break + + try: + rig.data["mixamo_prefix"] = p + except:# context error + pass + + return p + + +def get_mix_name(name, use_prefix): + if not use_prefix: + return name + else: + p = get_mixamo_prefix() + return p+name + + +def get_bone_side(bone_name): + if bone_name.endswith("_Left"): + return "Left" + elif bone_name.endswith("_Right"): + return "Right" \ No newline at end of file diff --git a/lib/objects.py b/lib/objects.py new file mode 100644 index 0000000..00e4248 --- /dev/null +++ b/lib/objects.py @@ -0,0 +1,81 @@ +import bpy, os + +def delete_object(obj): + bpy.data.objects.remove(obj, do_unlink=True) + + +def duplicate_object(): + try: + bpy.ops.object.duplicate(linked=False, mode='TRANSLATION') + except: + bpy.ops.object.duplicate('TRANSLATION', False) + + +def get_object(name): + return bpy.data.objects.get(name) + + +def set_active_object(object_name): + bpy.context.view_layer.objects.active = bpy.data.objects[object_name] + bpy.data.objects[object_name].select_set(state=True) + + +def hide_object(obj_to_set): + obj_to_set.hide_set(True) + obj_to_set.hide_viewport = True + + +def is_object_hidden(obj_to_get): + if obj_to_get.hide_get() == False and obj_to_get.hide_viewport == False: + return False + else: + return True + + +def append_cs(names=[]): + context = bpy.context + scene = context.scene + addon_directory = os.path.dirname(os.path.abspath(__file__)) + filepath = addon_directory + "\cs.blend" + + # load the objects data in file + with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): + data_to.objects = [name for name in data_from.objects if name in names] + + # Add the objects in the scene + for obj in data_to.objects: + if obj: + # link in collec + scene.collection.objects.link(obj) + + cs_grp = bpy.data.objects.get("cs_grp") + if cs_grp == None: + cs_grp = bpy.data.objects.new(name="cs_grp", object_data=None) + bpy.context.collection.objects.link(cs_grp) + cs_grp.location = [0,0,0] + cs_grp.rotation_euler = [0,0,0] + cs_grp.scale = [1,1,1] + + # parent the custom shape + obj.parent = cs_grp + + # assign to new collection + assigned_collections = [] + for collec in cs_grp.users_collection: + try: + collec.objects.link(obj) + assigned_collections.append(collec) + except:# already in collection + pass + + if len(assigned_collections): + # remove previous collections + for i in obj.users_collection: + if not i in assigned_collections: + i.objects.unlink(obj) + # and the scene collection + try: + scene.collection.objects.unlink(obj) + except: + pass + \ No newline at end of file diff --git a/lib/version.py b/lib/version.py new file mode 100644 index 0000000..d8f38d1 --- /dev/null +++ b/lib/version.py @@ -0,0 +1,35 @@ +import bpy + +class ARP_blender_version: + _string = bpy.app.version_string + blender_v = bpy.app.version + _float = blender_v[0]*100+blender_v[1]+blender_v[2]*0.01 + _char = bpy.app.version_char + +blender_version = ARP_blender_version() + + +def convert_drivers_cs_to_xyz(armature): + # Blender 3.0 requires Vector3 custom_shape_scale values + # convert single uniform driver to vector3 array drivers + drivers_armature = [i for i in armature.animation_data.drivers] + + for dr in drivers_armature: + if 'custom_shape_scale' in dr.data_path: + if not 'custom_shape_scale_xyz' in dr.data_path: + for i in range(0, 3): + new_dr = armature.animation_data.drivers.from_existing(src_driver=dr) + new_dr.data_path = new_dr.data_path.replace('custom_shape_scale', 'custom_shape_scale_xyz') + new_dr.array_index = i + new_dr.driver.expression += ''# update hack + + armature.driver_remove(dr.data_path, dr.array_index) + + print("Converted custom shape scale drivers to xyz") + + +def get_custom_shape_scale_prop_name(): + if blender_version._float >= 300: + return 'custom_shape_scale_xyz' + else: + return 'custom_shape_scale' \ No newline at end of file