diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..175c9cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Gustavo Jaruga Cruz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..18dbf29 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Bones To Mesh Addon + +Blender addon to help bake physics simulations to bones inside armatures. + + + +## Description + +Select both a mesh object and armature and use the side panel 'BonesToMesh'. +Select 'Create constrained bones' will create vertex groups for the mesh and assign IK constraints for the bones. +Putting the closest vertex to the bone tail with weight of 1 in its vertex group. + +Select the armature and press 'Bake bones' to bake all bone constraints. +Note that you need to bake your physics baked. +This is only a shortcut to Pose->Animation->Bake with all bones selected. Using visual keying. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ffc6b59 --- /dev/null +++ b/__init__.py @@ -0,0 +1,37 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +bl_info = { + "name" : "BonesToMesh", + "author" : "Gustjc", + "description" : "", + "blender" : (2, 80, 0), + "version" : (0, 0, 1), + "location" : "View3D", + "warning" : "", + "category" : "Object" +} + +import bpy + +from . btm_op import GDEV_OT_RemoveConstraintsOperator, GDEV_OT_CreateVertexGroupsOperator, GDEV_OT_RemoveVertexOperator, GDEV_OT_BakeConstraintsOperator +from . btm_panel import GDEV_PT_Panel +classes = (GDEV_OT_RemoveConstraintsOperator, GDEV_PT_Panel, GDEV_OT_CreateVertexGroupsOperator, GDEV_OT_RemoveVertexOperator, GDEV_OT_BakeConstraintsOperator) + +def register(): + for c in classes: + bpy.utils.register_class(c) + +def unregister(): + for c in classes: + bpy.utils.unregister_class(c) \ No newline at end of file diff --git a/btm_op.py b/btm_op.py new file mode 100644 index 0000000..62150d0 --- /dev/null +++ b/btm_op.py @@ -0,0 +1,175 @@ +import bpy +import numpy as np +from bpy.types import Operator + +class GDEV_OT_CreateVertexGroupsOperator(bpy.types.Operator): + """Create a vertex group for each bone in the selected armature""" + bl_idname = "object.create_bones_vertex_groups" + bl_label = "Create Vertex Groups" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.object + + if obj.mode == "OBJECT" and len(context.selected_objects) == 2: + if context.selected_objects[0].type == 'MESH' and \ + context.selected_objects[1].type == 'ARMATURE': + return True + if context.selected_objects[1].type == 'MESH' and \ + context.selected_objects[0].type == 'ARMATURE': + return True + + return False + + def execute(self, context): + # Assuming the mesh is active object + if context.selected_objects[0].type == 'MESH': + mesh_obj = context.selected_objects[0] + armature_obj = context.selected_objects[1] + else: + mesh_obj = context.selected_objects[1] + armature_obj = context.selected_objects[0] + + bone_names = [bone.name for bone in armature_obj.data.bones] + vertices = np.array(mesh_obj.data.vertices) + + for bone_name in bone_names: + bone_tail = armature_obj.pose.bones[bone_name].tail + vertex_group = mesh_obj.vertex_groups.new(name=bone_name) + + closest_vertex_idx = np.argmin([np.linalg.norm(v.co - bone_tail) for v in vertices]) + vertex_group.add([int(closest_vertex_idx)], 1.0, 'REPLACE') + + # Add IK constraint + bone_constraint = armature_obj.pose.bones[bone_name].constraints.new(type='IK') + bone_constraint.target = mesh_obj + bone_constraint.subtarget = bone_name + bone_constraint.chain_count = 1 + + return {'FINISHED'} + +class GDEV_OT_BakeConstraintsOperator(Operator): + bl_idname = "object.bake_all_pose" + bl_label = "Bake pose constrains" + bl_description = "Bake all pose constrains" + bl_options = {'REGISTER', 'UNDO'} + + frame_start: bpy.props.IntProperty( + name="Start Frame", + description="Start frame for baking", + min=0, max=300000, + default=1, + ) + frame_end: bpy.props.IntProperty( + name="End Frame", + description="End frame for baking", + min=1, max=300000, + default=250, + ) + step: bpy.props.IntProperty( + name="Frame Step", + description="Frame Step", + min=1, max=120, + default=1, + ) + use_current_action: bpy.props.BoolProperty( + name="Overwrite Current Action", + description="Bake animation into current action, instead of creating a new one " + "(useful for baking only part of bones in an armature)", + default=False, + ) + clean_curves: bpy.props.BoolProperty( + name="Clean Curves", + description="After baking curves, remove redundant keys", + default=False, + ) + + @classmethod + def poll(cls, context): + obj = context.object + + if obj.mode == "OBJECT" and obj.type == "ARMATURE": + return True + + return False + + def execute(self, context): + # Active object is ARMATURE + + bpy.ops.nla.bake(only_selected=False, visual_keying=True, clear_constraints=True,bake_types={'POSE'}, + clean_curves=self.clean_curves, + frame_start=self.frame_start, + frame_end=self.frame_end, + step=self.step, + use_current_action=self.use_current_action) + + return {'FINISHED'} + + def invoke(self, context, _event): + scene = context.scene + if scene.use_preview_range: + self.frame_start = scene.frame_preview_start + self.frame_end = scene.frame_preview_end + else: + self.frame_start = scene.frame_start + self.frame_end = scene.frame_end + + wm = context.window_manager + return wm.invoke_props_dialog(self) + + +class GDEV_OT_RemoveConstraintsOperator(Operator): + bl_idname = "object.remove_all_bone_constrains" + bl_label = "Remove pose constrains" + bl_description = "Remove all pose constrains" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.object + + if obj.mode == "OBJECT" and obj.type == "ARMATURE": + return True + + return False + + def execute(self, context): + # Active object is ARMATURE + armature_obj = context.active_object + + bone_names = [bone.name for bone in armature_obj.data.bones] + + for bone_name in bone_names: + + # Add IK constraint + constraint = armature_obj.pose.bones[bone_name].constraints.get("IK") + if constraint: + armature_obj.pose.bones[bone_name].constraints.remove(constraint) + + return {'FINISHED'} + + +class GDEV_OT_RemoveVertexOperator(Operator): + bl_idname = "object.remove_all_vertexgroups" + bl_label = "Remove vertexgroups" + bl_description = "Remove all vertexgroups" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obj = context.object + + if obj.mode == "OBJECT" and obj.type == "MESH": + return True + + return False + + def execute(self, context): + # Active object is ARMATURE + mesh_obj = context.active_object + + mesh_obj.vertex_groups.clear() + + return {'FINISHED'} + diff --git a/btm_panel.py b/btm_panel.py new file mode 100644 index 0000000..57ab4fd --- /dev/null +++ b/btm_panel.py @@ -0,0 +1,34 @@ + +from bpy.types import Panel + +class GDEV_PT_Panel(Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_label = "Bones to mesh" + bl_category = "BonesToMesh" + + @classmethod + def poll(cls, context): + obj = context.object + + if obj.mode == "OBJECT": + return True + + return False + + def draw(self, context): + layout = self.layout + + # 2 Column buttons + row = layout.row() + row.operator("object.create_bones_vertex_groups", text="Create constrained bones") + + row = layout.row() + row.operator("object.bake_all_pose", text="Bake bones") + + #col = row.column() + row = layout.row() + row.operator("object.remove_all_bone_constrains", text="Remove IK constraints") + + row = layout.row() + row.operator("object.remove_all_vertexgroups", text="Remove vertex groups") \ No newline at end of file