# Subset normalization / weight splatting script # # Purpose # ------- # # Let's say you have a fully rigged model with auto-applied bone weights. # You've taken much care to have smooth and good-looking deforms. # # But now you want to add a small detail deformation to this model, such as # a biceps bump, or a pouch in their pants, or maybe you're making porn and # need a breast deformation that, when moved together with the breast bone, # keeps the existing shape intact. # # With this add-on, you can simply paint the deformation area (disable # auto normalization in Blender!) and then it a subset of another bone. # # Installation # ------------ # # 1. Find your Python scripts directory: # * Gentoo: /usr/share/blender/2.79/scripts/addons/ # * Linux (downloaded): /opt/blender-2.79/2.79/scripts/addons/ # * Windows (installer): %ProgramFiles%\Blender Foundation\Blender 2.79\2.79\scripts\addons\ # # 2. Copy this script into that directory # # 3. In Blender, enable it under File -> User Preferences -> Add Ons # (enter 'subset' in the search box to find it easily) # # Usage # ----- # # Let's assume the breast deformation case from above. # # 1. Create two detail deformation bones. Their parent should be the breast bone so # that the detail bones move together with it. # # 2. Weight paint their deformation areas onto the breast with auto normalization # disabled (so that the existing weights aren't disturbed) # # 3. In weight paint mode, look for this add-on in the 'T' (tools) panel on # the left side. Select the Armature, use the breast bone as limit bone and # select the two detail bones as subset bones. # # 4. Click the 'Normalize Subset' button # # The detail bones will now have taken over some of the breast bone's weights, # but stay within the limits of the breast. If the breast bone moves, you will not # see any indication that the detail bones exist. # bl_info = { "name": "Normalize Subset of Weights", "author": "Markus Ewald", "version": (1, 1), "blender": (2, 7, 9), "location": "Object -> Tools -> Misc", "description": "Normalizes the weights of a subset of bones", #"warning": "20 Nov 2016", "wiki_url": "", "tracker_url": "", "category": "Mesh" } import os import bpy import bmesh import mathutils # ----------------------------------------------------------------------------------------------- # class NormalizeBoneSubsetOperator(bpy.types.Operator): """Normalizes the weights between a subset of bones within one or two limit bones""" bl_idname = "mesh.normalize_bone_subset" bl_label = "Normalize Bone subset" def _get_limit_bone_names(self, context): """Reads the limit bone names transmitted through the scene properties and stores them in a plain Python array for easier processing.""" limit_bone_names = [] limit_bone_count = context.scene.normalize_subset_limit_bone_count if limit_bone_count == 'ONE': limit_bone_names.append(context.scene.normalize_subset_first_limit_bone_name) elif limit_bone_count == 'TWO': limit_bone_names.append(context.scene.normalize_subset_first_limit_bone_name) limit_bone_names.append(context.scene.normalize_subset_second_limit_bone_name) return limit_bone_names def _get_subset_bone_names(self, context): """Reads the limit bone names transmitted through the scene properties and stores them in a plain Python array for easier processing.""" subset_bone_names = [] if True: subset_bone_count = context.scene.normalize_subset_bone_count if subset_bone_count == 'TWO': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) elif subset_bone_count == 'THREE': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) elif subset_bone_count == 'FOUR': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) subset_bone_names.append(context.scene.normalize_subset_fourth_bone_name) elif subset_bone_count == 'FIVE': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) subset_bone_names.append(context.scene.normalize_subset_fourth_bone_name) subset_bone_names.append(context.scene.normalize_subset_fifth_bone_name) elif subset_bone_count == 'SIX': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) subset_bone_names.append(context.scene.normalize_subset_fourth_bone_name) subset_bone_names.append(context.scene.normalize_subset_fifth_bone_name) subset_bone_names.append(context.scene.normalize_subset_sixth_bone_name) elif subset_bone_count == 'SEVEN': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) subset_bone_names.append(context.scene.normalize_subset_fourth_bone_name) subset_bone_names.append(context.scene.normalize_subset_fifth_bone_name) subset_bone_names.append(context.scene.normalize_subset_sixth_bone_name) subset_bone_names.append(context.scene.normalize_subset_seventh_bone_name) elif subset_bone_count == 'EIGHT': subset_bone_names.append(context.scene.normalize_subset_first_bone_name) subset_bone_names.append(context.scene.normalize_subset_second_bone_name) subset_bone_names.append(context.scene.normalize_subset_third_bone_name) subset_bone_names.append(context.scene.normalize_subset_fourth_bone_name) subset_bone_names.append(context.scene.normalize_subset_fifth_bone_name) subset_bone_names.append(context.scene.normalize_subset_sixth_bone_name) subset_bone_names.append(context.scene.normalize_subset_seventh_bone_name) subset_bone_names.append(context.scene.normalize_subset_eighth_bone_name) return subset_bone_names def execute(self, context): """Performs the subset splat/normalization""" ob = bpy.context.object assert ob is not None and ob.type == 'MESH', "active object invalid" # Put the bone names in an array for easier processing limit_bone_names = self._get_limit_bone_names(context) subset_bone_names = self._get_subset_bone_names(context) # Pick up any vertex groups that already exist for the selected bones limit_groups = [None] * len(limit_bone_names) subset_groups = [None] * len(subset_bone_names) for group in ob.vertex_groups: for limit_bone_index in range(0, len(limit_bone_names)): if group.name == limit_bone_names[limit_bone_index]: limit_groups[limit_bone_index] = group for subset_bone_index in range(0, len(subset_bone_names)): if group.name == subset_bone_names[subset_bone_index]: subset_groups[subset_bone_index] = group # If any of the bones had no vertex groups, create new vertex groups # so we can work with them (just to simplify the code) for limit_bone_index in range(0, len(limit_bone_names)): if limit_groups[limit_bone_index] is None: limit_groups[limit_bone_index] = ob.vertex_groups.new( name = limit_bone_names[limit_bone_index] ) for subset_bone_index in range(0, len(subset_bone_names)): if subset_groups[subset_bone_index] is None: subset_groups[subset_bone_index] = ob.vertex_groups.new( name = subset_bone_names[subset_bone_index] ) # Find the affected bones, too limit_bones = [None] * len(limit_bone_names) subset_bones = [None] * len(subset_bone_names) armature = bpy.data.armatures[context.scene.normalize_subset_armature] for bone in armature.bones: for limit_bone_index in range(0, len(limit_bone_names)): if bone.name == limit_bone_names[limit_bone_index]: limit_bones[limit_bone_index] = bone for subset_bone_index in range(0, len(subset_bone_names)): if bone.name == subset_bone_names[subset_bone_index]: subset_bones[subset_bone_index] = bone # Verify we've got everything for limit_bone_index in range(0, len(limit_bone_names)): assert limit_groups[limit_bone_index] is not None, 'limit vertex group missing' assert limit_bones[limit_bone_index] is not None, 'limit bone missing' for subset_bone_index in range(0, len(subset_bone_names)): assert subset_groups[subset_bone_index] is not None, 'subset vertex group missing' assert subset_bones[subset_bone_index] is not None, 'subset bone missing' self.report( {'INFO'}, "Normalizing " + str(len(subset_bones)) + " bones " + "as subset of " + str(len(limit_bones)) + " limit bones." ) # Go over all selected vertices and normalize the weights of the selected # bones amongst them, then make them a subset of their limit bones selected_verts = [v for v in ob.data.vertices if v.select] for v in selected_verts: # Check the group associations this vertex has (each vertex has its own # list of weights, or at least that how it's presented in the Python interface) vertex_limit_groups = [None] * len(limit_bone_names) vertex_subset_groups = [None] * len(subset_bone_names) for g in v.groups: for limit_bone_index in range(0, len(limit_bone_names)): if g.group == limit_groups[limit_bone_index].index: vertex_limit_groups[limit_bone_index] = g for subset_bone_index in range(0, len(subset_bone_names)): if g.group == subset_groups[subset_bone_index].index: vertex_subset_groups[subset_bone_index] = g # Calculate the combined weight of all limit bones on this vertex total_limit_weight = 0.0 if not (vertex_limit_groups[0] is None): total_limit_weight = vertex_limit_groups[0].weight if len(limit_bone_names) == 2: if not (vertex_limit_groups[1] is None): total_limit_weight += vertex_limit_groups[1].weight total_subset_weight = 0.0 for subset_bone_index in range(0, len(subset_bone_names)): if not (vertex_subset_groups[subset_bone_index] is None): total_subset_weight += vertex_subset_groups[subset_bone_index].weight self.report( {'INFO'}, "Vertex limit weight " + str(total_limit_weight) + " subset weight " + str(total_subset_weight) ) # Factor by which subset weights are multiplied to normalize them if their # sum goes above 1. It's fine if they're less than one because we're just # mixing them into the limit bone(s) factor = 1.0 if total_subset_weight > 1.0: factor = 1.0 / total_subset_weight #total_subset_weight = 1.0 # Subtract the sum of weight from the limit bones # This integrates weights layered on top of body parts their parent bone weights. if len(limit_bone_names) == 2: if (vertex_limit_groups[0] is None): if not (vertex_limit_groups[1] is None): limit_groups[1].add( [v.index], max(vertex_limit_groups[1].weight - total_subset_weight, 0.0), 'REPLACE' ) elif (vertex_limit_groups[1] is None): limit_groups[0].add( [v.index], max(vertex_limit_groups[0].weight - total_subset_weight, 0.0), 'REPLACE' ) else: first_factor = vertex_limit_groups[0].weight / total_limit_weight second_factor = vertex_limit_groups[1].weight / total_limit_weight first_portion = total_subset_weight * first_factor second_portion = total_subset_weight * second_factor limit_groups[0].add( [v.index], max(vertex_limit_groups[0].weight - first_portion, 0.0), 'REPLACE' ) limit_groups[1].add( [v.index], max(vertex_limit_groups[1].weight - second_portion, 0.0), 'REPLACE' ) else: if not (vertex_limit_groups[0] is None): limit_groups[0].add( [v.index], max(vertex_limit_groups[0].weight - total_subset_weight, 0.0), 'REPLACE' ) # Normalize between the subset bones, if neccessary for subset_bone_index in range(0, len(subset_bone_names)): if not (vertex_subset_groups[subset_bone_index] is None): portion = factor * total_limit_weight subset_groups[subset_bone_index].add( [v.index], vertex_subset_groups[subset_bone_index].weight * portion, 'REPLACE' ) ob.data.update() return {'FINISHED'} # ----------------------------------------------------------------------------------------------- # class NormalizeSubsetPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" bl_idname = "OBJECT_PT_normalize_subset" bl_label = "Normalize Bone Subset" bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_context = 'weightpaint' # 'objectmode' # posemode bl_category = 'Blending' def draw(self, context): layout = self.layout scene = context.scene col = layout.column() # Armature in which the bones will be selected (all bones must share this) col.prop_search(scene, "normalize_subset_armature", bpy.data, "armatures") arma = bpy.data.armatures.get(scene.normalize_subset_armature) # Let the user choose how many limit bones he has as parents to the subset if not (arma is None): col.separator() col.prop(scene, "normalize_subset_limit_bone_count") if scene.normalize_subset_limit_bone_count == 'ONE': col.prop_search(scene, "normalize_subset_first_limit_bone_name", arma, "bones") else: col.prop_search(scene, "normalize_subset_first_limit_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_limit_bone_name", arma, "bones") # Let the user choose how many bones he wants to normalize within their parents if not (arma is None): col.separator() col.prop(scene, "normalize_subset_bone_count") if scene.normalize_subset_bone_count == 'TWO': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'THREE': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'FOUR': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fourth_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'FIVE': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fourth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fifth_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'SIX': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fourth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fifth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_sixth_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'SEVEN': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fourth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fifth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_sixth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_seventh_bone_name", arma, "bones") if scene.normalize_subset_bone_count == 'EIGHT': col.prop_search(scene, "normalize_subset_first_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_second_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_third_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fourth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_fifth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_sixth_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_seventh_bone_name", arma, "bones") col.prop_search(scene, "normalize_subset_eighth_bone_name", arma, "bones") if not (arma is None): col.separator() col.operator("mesh.normalize_bone_subset", text='Normalize Subset') # ----------------------------------------------------------------------------------------------- # def register(): """Registers extra scene properties when the add-in is loaded in Blender""" bpy.utils.register_module(__name__) bpy.types.Scene.normalize_subset_armature = bpy.props.StringProperty( name = "Armature", description = "Armature from which bones can be selected" ) # Limit bone count and names bpy.types.Scene.normalize_subset_limit_bone_count = bpy.props.EnumProperty( name = "Limit Bone Count", description = "Number of bones to limit the subset total by", items = [ ( 'ONE', 'One', '', 1), ( 'TWO', 'Two', '', 2) ], default = 'ONE' ) bpy.types.Scene.normalize_subset_first_limit_bone_name = bpy.props.StringProperty( name = "First Limit Bone", description = "First bone used to limit the total weight" ) bpy.types.Scene.normalize_subset_second_limit_bone_name = bpy.props.StringProperty( name = "Second Limit Bone", description = "Second bone used to limit the total weight" ) # Subset bone count and names bpy.types.Scene.normalize_subset_bone_count = bpy.props.EnumProperty( name = "Subset Bone Count", description = "Number of bones to normalize to a subset of the limit bone", items = [ ( 'TWO', 'Two', '', 2), ( 'THREE', 'Three', '', 3), ( 'FOUR', 'Four', '', 4), ( 'FIVE', 'Five', '', 5), ( 'SIX', 'Six', '', 6), ( 'SEVEN', 'Seven', '', 7), ( 'EIGHT', 'Eight', '', 8) ], default = 'SIX' ) bpy.types.Scene.normalize_subset_first_bone_name = bpy.props.StringProperty( name = "First Bone", description = "First bone that will be normalized" ) bpy.types.Scene.normalize_subset_second_bone_name = bpy.props.StringProperty( name = "Second Bone", description = "Second bone that will be normalized" ) bpy.types.Scene.normalize_subset_third_bone_name = bpy.props.StringProperty( name = "Third Bone", description = "Third bone that will be normalized" ) bpy.types.Scene.normalize_subset_fourth_bone_name = bpy.props.StringProperty( name = "Fourth Bone", description = "Fourth bone that will be normalized" ) bpy.types.Scene.normalize_subset_fifth_bone_name = bpy.props.StringProperty( name = "Fifth Bone", description = "Fifth bone that will be normalized" ) bpy.types.Scene.normalize_subset_sixth_bone_name = bpy.props.StringProperty( name = "Sixth Bone", description = "Sixth bone that will be normalized" ) bpy.types.Scene.normalize_subset_seventh_bone_name = bpy.props.StringProperty( name = "Seventh Bone", description = "Seventh bone that will be normalized" ) bpy.types.Scene.normalize_subset_eighth_bone_name = bpy.props.StringProperty( name = "Eighth Bone", description = "Eighth bone that will be normalized" ) # ----------------------------------------------------------------------------------------------- # def unregister(): """Unregisters all scene properties when the add-in is unloaded in Blender""" del bpy.types.Scene.normalize_subset_eighth_bone_name del bpy.types.Scene.normalize_subset_seventh_bone_name del bpy.types.Scene.normalize_subset_sixth_bone_name del bpy.types.Scene.normalize_subset_fifth_bone_name del bpy.types.Scene.normalize_subset_fourth_bone_name del bpy.types.Scene.normalize_subset_third_bone_name del bpy.types.Scene.normalize_subset_second_bone_name del bpy.types.Scene.normalize_subset_first_bone_name del bpy.types.Scene.normalize_subset_bone_count del bpy.types.Scene.normalize_subset_second_limit_bone_name del bpy.types.Scene.normalize_subset_first_limit_bone_name del bpy.types.Scene.normalize_subset_limit_bone_count del bpy.types.Scene.normalize_subset_armature bpy.utils.unregister_module(__name__) # ----------------------------------------------------------------------------------------------- # # This allows you to run the script directly from blenders text editor # to test the addon without having to install it if __name__ == "__main__": register() # ----------------------------------------------------------------------------------------------- #