Skip to content

Commit

Permalink
Merge Vertex Groups tool, and update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
arocull committed May 14, 2022
1 parent 4fcbf85 commit 572a6a7
Show file tree
Hide file tree
Showing 17 changed files with 257 additions and 48 deletions.
58 changes: 15 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# BoneJuice
Armature utility plugin for Blender, for niche cases I encounter where it would be a lot nicer to have something do the work for me!

Version 0.0.5 which supports Blender 3.0+

Current features include:
- Surface Bone Placer - Place bones on geometric surfaces
- Mark Bone Side - Mark bones as left or right in an armature
- Clean and Combine - Combine multiple meshes, with modifiers, into one export-ready object. Armatures are preserved.
- (Bulk) Set Rotation Mode - Select multiple pose bones and set their rotation mode (Quaternion, XYZ Euler, Axis Angle, etc) with just the click of a button.
- Curl Bones - Offsets euler rotations of all selected pose bones by the given rotation. Only works in Euler rotation mode.
Version 0.0.6 which supports Blender 3.0+

Feature List:
- Object Mode
- **[Clean and Combine](docs/examples/clean_and_combine.md)** - Combine multiple meshes, with modifiers, into one export-ready object. Armatures are preserved.
- **[Merge Vertex Groups](docs/examples/merge_vertex_groups.md)** - Combine two vertex groups on selected meshes using a given operation. Also available in Weight Paint mode.
- Armature Edit Mode
- **[Surface Bone Placer](docs/examples/surface_bone_placer.md)** - Place bones on geometric surfaces with a single click
- **[Mark Bone Side](docs/examples/mark_bone_side.md)** - Mark bones as left or right in an armature. Alternative to built-in method which relies on specific rules.
- Armature Pose Mode
- **[Set Rotation Mode](docs/examples/set_bone_rotation_mode.md)** - Select multiple pose bones and set their rotation mode (Quaternion, XYZ Euler, Axis Angle, etc) with just the click of a button. Alternative to built-in method which can be hit-or-miss (although this will not convert keyframed transforms).
- **[Curl Bones](docs/examples/curl_bones.md)** - Offsets euler rotations of all selected pose bones by the given rotation. Only works in Euler rotation mode.
- Rendering
- **(Work In Progress) Batch Render NLA Tracks** - Individually render out each animated NLA track inside of all selected objects. Renders from all selected cameras. Great for game animation previews, or spritesheets.

# Installation
Download the zip file from the releases area on GitHub, and then go to `Edit > Preferences > Add-ons` and then click `Install` in the top right, and select the zip file. Make sure the plugin it points to is enabled.
Expand All @@ -27,38 +33,4 @@ I also highly recommend these VS Code workspace extensions for editing this proj

The Blender Development plugin is really helpful with it's built-in debugger. Press F1, and run `>Blender: Build and Start` to debug. The workspace configuration should be set up for you already.

If the debugger is failing to install modules, you may check out this thread [here](https://github.com/JacquesLucke/blender_vscode/issues/99) (I had to run `/home/usrname/.../blender-3.1.0-linux-x64/3.1/python/bin/python3.10 -m ensurepip` before I could use the debugger in Blender 3.1).

# Tools
## Surface Bone Placer
While editing an Armature, go to `Add > Place Surface Bones`. If you would like the bones you place to fall under a parent, make the parent bone active before running this tool.

Once the modal has started, left-click on 3D surfaces to place bones on them. The bone head will fall slightly inside the mesh, and the bone tail will face outward based off the surface normal. Bones should automatically inherit armature and parent bone transforms (essentially they are placed using global space). If there's an issue, let me know.

When you are finished placing bones, hit `Escape` to exit the modal. When using the placed bones, I recommend using Automatic Weights over Envelope weights for inotial weighting of your mesh.

An example of what it does (made center bone active, then used tool to place bones on surface of mesh):

![](docs/images/exmp_surface_bones.png)

## Mark Bone Side
While editing an Armature, go to `Armature > Names > Mark Side`. This works similar to `Auto-Name Left/Right`, but allows you to set bones to one side or another regardless of actual position when not using the "automatic" setting.

## Clean and Combine
In Object mode, select the objects you want to clean and merge, and make the object you want to merge them into Active. Then, go to `Object > Cleanup > Clean and Combine`. Choose the corresponding settings you like. The resulting objects will have modifiers applied or discarded, be joined together, have geometry cleaned using the given settings, and finally have all transforms applied. This operator is great for multi-part characters, or just generally cleaning up messy files before exporting.

Please note that all modifiers are applied or discarded. **THERE WILL BE DATA LOSS**, but **Armatures and bone weights are preserved**. Though the operator can be undone, it is strongly recommended to have a backup of your model, and only use this tool when you are certain that you want to, or are exporting a model.

![](docs/images/exmp_cleancombine_p1.png)
![](docs/images/exmp_cleancombine_p2.png)

## Set Bone Rotation Mode
In Pose mode, select the bones you want to edit, then go to `Pose > Set Rotation Mode`. Choose the rotation mode you want, and all selected pose bones will be set to the new rotation mode. Note that if the bones already have a keyframed rotation, these keyframes will not be converted to the new rotation mode, and you may run into issues with rotation channels clashing.

## Curl Bones
In Pose mode, select the bones you want to edit, then go to `Pose > Curl Bones`. Then input the angle you want to offset the bones' rotation by.

![](docs/images/exmp_curl_bones1.png)
![](docs/images/exmp_curl_bones2.png)

Note that currently this only applies to bones with a Euler rotation mode.
If the debugger is failing to install modules, you may check out this thread [here](https://github.com/JacquesLucke/blender_vscode/issues/99) (I had to run `/home/usrname/.../blender-3.1.0-linux-x64/3.1/python/bin/python3.10 -m ensurepip` before I could use the debugger in Blender 3.1).
7 changes: 7 additions & 0 deletions docs/examples/clean_and_combine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Clean and Combine
In Object mode, select the objects you want to clean and merge, and make the object you want to merge them into Active. Then, go to `Object > Cleanup > Clean and Combine`. Choose the corresponding settings you like. The resulting objects will have modifiers applied or discarded, be joined together, have geometry cleaned using the given settings, and finally have all transforms applied. This operator is great for multi-part characters, or just generally cleaning up messy files before exporting.

Please note that all modifiers are applied or discarded. **THERE WILL BE DATA LOSS**, but **Armatures and bone weights are preserved**. Though the operator can be undone, it is strongly recommended to have a backup of your model, and only use this tool when you are certain that you want to, or are exporting a model.

![](../images/exmp_cleancombine_p1.png)
![](../images/exmp_cleancombine_p2.png)
6 changes: 6 additions & 0 deletions docs/examples/curl_bones.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Curl Bones
In Pose mode, select the bones you want to edit, then go to `Pose > Curl Bones`. Then input the angle you want to offset the bones' rotation by.
Note that currently this only applies to bones with a Euler rotation mode.

![](../images/exmp_curl_bones1.png)
![](../images/exmp_curl_bones2.png)
2 changes: 2 additions & 0 deletions docs/examples/mark_bone_side.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Mark Bone Side
While editing an Armature, go to `Armature > Names > Mark Side`. This works similar to `Auto-Name Left/Right`, but allows you to set bones to one side or another regardless of actual position when not using the "automatic" setting.
28 changes: 28 additions & 0 deletions docs/examples/merge_vertex_groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Merge Vertex Groups
## Accessing
From Object mode, select the objects you want to edit, and go to `Object > Apply > Merge Vertex Groups`.
The operation should apply to all possible objects it can, which is useful for multi-mesh character rigs.

Or, if you're in Weight Paint Mode, simply go to `Weights > Merge Vertex Groups`.
This will allow real-time view of the operation if you make changes after applying it.
You can only do one object at a time with this method.

## Utilization
You should get a menu like this:

![](../images/exmp_mergevertgroups5.png)

From here, fill out the names of the desired vertex groups you want to work with.
If you want to use a constant, select 'Use Constant' and choose where in the formula you want it to be used.
Then set the constant value. The constant value can be any float value--it is not clamped to 0 or 1.

Finally, select your operation. If you want to see what the formula looks like for each one, simply hover your mouse over the option.

![](../images/exmp_mergevertgroups4.png)

## Example
![](../images/exmp_mergevertgroups1.png)

![](../images/exmp_mergevertgroups2.png)

![](../images/exmp_mergevertgroups3.png)
2 changes: 2 additions & 0 deletions docs/examples/set_bone_rotation_mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Set Bone Rotation Mode
In Pose mode, select the bones you want to edit, then go to `Pose > Set Rotation Mode`. Choose the rotation mode you want, and all selected pose bones will be set to the new rotation mode. Note that if the bones already have a keyframed rotation, these keyframes will not be converted to the new rotation mode, and you may run into issues with rotation channels clashing.
10 changes: 10 additions & 0 deletions docs/examples/surface_bone_placer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Surface Bone Placer
While editing an Armature, go to `Add > Place Surface Bones`. If you would like the bones you place to fall under a parent, make the parent bone active before running this tool.

Once the modal has started, left-click on 3D surfaces to place bones on them. The bone head will fall slightly inside the mesh, and the bone tail will face outward based off the surface normal. Bones should automatically inherit armature and parent bone transforms (essentially they are placed using global space). If there's an issue, let me know.

When you are finished placing bones, hit `Escape` to exit the modal. When using the placed bones, I recommend using Automatic Weights over Envelope weights for initial weighting of your mesh.

An example of what it does (made center bone active, then used tool to place bones on surface of mesh):

![](../images/exmp_surface_bones.png)
Binary file added docs/images/exmp_mergevertgroups1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/exmp_mergevertgroups2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/exmp_mergevertgroups3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/exmp_mergevertgroups4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/exmp_mergevertgroups5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author" : "Alan O'Cull",
"description" : "Armature Utility Plugin for Blender",
"blender" : (3, 0, 0),
"version" : (0, 0, 5),
"version" : (0, 0, 6),
"location" : "Edit Armature > Add, Edit Armature > Names, Object > Clean Up",
"warning" : "",
"category" : "Armature, Object"
Expand All @@ -12,15 +12,19 @@
import bpy
from .armature.edit_add import BoneJuice_SurfacePlacer
from .armature.edit_ops import *
from .mesh.object_ops import *
from .mesh.clean_and_combine import *
from .mesh.merge_vertex_groups import *
from .armature.pose_ops import *
from .render.batch import *
from .registrator import registerClass, unregisterClass



def register():
registerClass(BoneJuice_SurfacePlacer, [bpy.types.TOPBAR_MT_edit_armature_add])
registerClass(BoneJuice_MarkSide, [bpy.types.VIEW3D_MT_edit_armature_names, bpy.types.VIEW3D_MT_pose_names], [BoneJuice_MarkSide.button_edit, BoneJuice_MarkSide.button_pose])
registerClass(BoneJuice_CleanAndCombine, [bpy.types.VIEW3D_MT_object_cleanup])
registerClass(BoneJuice_MergeVertexGroups, [bpy.types.VIEW3D_MT_paint_weight, bpy.types.VIEW3D_MT_object_apply])
registerClass(BoneJuice_BulkSetRotationMode, [bpy.types.VIEW3D_MT_pose])
registerClass(BoneJuice_CurlBones, [bpy.types.VIEW3D_MT_pose])
registerClass(BoneJuice_BatchRenderActions, [bpy.types.TOPBAR_MT_render])
Expand All @@ -29,6 +33,7 @@ def unregister():
unregisterClass(BoneJuice_SurfacePlacer, [bpy.types.TOPBAR_MT_edit_armature_add])
unregisterClass(BoneJuice_MarkSide, [bpy.types.VIEW3D_MT_edit_armature_names, bpy.types.VIEW3D_MT_pose_names], [BoneJuice_MarkSide.button_edit, BoneJuice_MarkSide.button_pose])
unregisterClass(BoneJuice_CleanAndCombine, [bpy.types.VIEW3D_MT_object_cleanup])
unregisterClass(BoneJuice_MergeVertexGroups, [bpy.types.VIEW3D_MT_paint_weight, bpy.types.VIEW3D_MT_object_apply])
unregisterClass(BoneJuice_BulkSetRotationMode, [bpy.types.VIEW3D_MT_pose])
unregisterClass(BoneJuice_CurlBones, [bpy.types.VIEW3D_MT_pose])
unregisterClass(BoneJuice_BatchRenderActions, [bpy.types.TOPBAR_MT_render])
Expand Down
File renamed without changes.
173 changes: 173 additions & 0 deletions src/mesh/merge_vertex_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import bpy
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
from bpy.types import Object, Operator, Mesh, VertexGroup
from ..utility import *

class BoneJuice_MergeVertexGroups(Operator):
"""Combine two vertex groups on selected meshes using a given operation."""
bl_idname = "object.bj_merge_vert_groups"
bl_label = "Merge Vertex Groups"
bl_description = "Combine two vertex groups on selected meshes using a given operation."
bl_options = {'REGISTER', 'UNDO'}

## MAPPING
def button(self, context):
self.layout.operator(
BoneJuice_MergeVertexGroups.bl_idname,
text="Merge Vertex Groups",
icon='NONE')

def manual_map():
url_manual_prefix = "https://docs.blender.org/manual/en/latest/"
url_manual_mapping = (
("bpy.ops.object.bj_merge_vert_groups", "scene_layout/object/types.html"),
)
return url_manual_prefix, url_manual_mapping

## PROPERTIES
groupAName: StringProperty(
name = "Group A",
description = "The name of the first vertex group to be used in the operation.",
default = "Group",
)
groupBName: StringProperty(
name = "Group B",
description = "The name of the second vertex group to be used in the operation.",
default = "Group.001",
)
groupCName: StringProperty(
name = "Output Group",
description = "The name of the vertex group to be outputted to. Will create one if not currently present.",
default = "merged_group",
)
groupOperation: EnumProperty(
name = 'Operation',
description = 'What operation to use for the formula.',
items = [
('add', 'Add', 'A + B = C'),
('subtract', 'Subtract', 'A - B = C'),
('multiply', 'Multiply', 'A * B = C'),
('divide', 'Divide', 'A / B = C'),
('modulus', 'Modulus', 'A % B = C'),
('pow', 'Power', 'A ^ B = C'),
('min', 'Minimum', 'min(A, B) = C'),
('max', 'Maximum', 'max(A, B) = C'),
],
default = 'add',
)
useConstant: EnumProperty(
name = 'Use Constant',
description = 'Utilizes the provided constant in place of the given group.',
items = [
('no', 'No', 'Does not use the constant value.'),
('b', 'For B', 'Use constant in place of vertex group B'),
('a', 'For A', 'Use constant in place of vertex group A'),
],
default = 'no',
)
operationConstant: FloatProperty(
name = 'Constant',
description = 'Numberic value to use in place of B, if Use Constant is checked.',
default = 0
)


## functions
def runOperation(self, a: float, b: float) -> float:
if self.groupOperation == 'subtract':
return a - b
elif self.groupOperation == 'multiply':
return a * b
elif self.groupOperation == 'divide':
if (b == 0): # Approach infinity
if (a == 0): # Unless we're also zero
return 0
return 1
return a / b
elif self.groupOperation == 'modulus':
return a % b
elif self.groupOperation == 'pow':
return pow(a, b)
elif self.groupOperation == 'min':
return min(a, b)
elif self.groupOperation == 'max':
return max(a, b)

return a + b
def getVertexGroup(self, obj: bpy.types.Object, grpName: str) -> VertexGroup:
return obj.vertex_groups.get(grpName)

## INVOKE DIALOGUE
def invoke(self, context: bpy.types.Context, event):
return context.window_manager.invoke_props_dialog(self)

## ACTUAL EXECUTION
def execute(self, context: bpy.types.Context):
primary = get_active() # Fetch primary object
objects = bpy.context.selected_objects # Fetch selected objects

# Iterate through all objects so we can work on the meshes
for obj in objects:
set_active(obj) # Set current object as active
if type(get_active().data) is Mesh:

# First, get vertex groups and make sure everything is in order!
groupA: VertexGroup = None
groupB: VertexGroup = None
groupC: VertexGroup = self.getVertexGroup(obj, self.groupCName)

# If we're not using a constant for group a, fetch it
if self.useConstant != 'a':
groupA = self.getVertexGroup(obj, self.groupAName)
if not groupA:
self.report({'WARNING'}, "Could not find vertex group '" + self.groupAName + "' on mesh '" + obj.name + "' Skipping.")
break
if self.useConstant != 'b':
groupB = self.getVertexGroup(obj, self.groupBName)
if not groupB:
self.report({'WARNING'}, "Could not find vertex group '" + self.groupBName + "' on mesh '" + obj.name + "'. Skipping.")
break

if not groupC:
groupC = bpy.context.active_object.vertex_groups.new(name=self.groupCName)

# Then fetch total vertices
numVerts = len(obj.data.vertices)
vertIndices: List[int] = []
vertWeightsA: List[float] = []
vertWeightsB: List[float] = []
# Fill in indices and weights
for i in range(0, numVerts):
vertIndices.append(i)

# Append A weights
if groupA:
try: # Attempt to append the weight of the given vertex if it exists in the group
vertWeightsA.append(groupA.weight(i))
except: # If it isn't present, just append zero instead
vertWeightsA.append(0)
else:
vertWeightsA.append(self.operationConstant)

# Append B weights
if groupB:
try: # Attempt to append the weight of the given vertex if it exists in the group
vertWeightsB.append(groupB.weight(i))
except: # If it isn't present, just append zero instead
vertWeightsB.append(0)
else:
vertWeightsB.append(self.operationConstant)

# Finally, perform operation and add vertices with weights > 0 to the array
for i in range(0, numVerts):
result: float = clampf(self.runOperation(vertWeightsA[i], vertWeightsB[i]), 0, 1)
if (result > 0):
groupC.add([i], result, 'REPLACE')
else:
groupC.remove([i])

self.report({'INFO'}, "Completed operation for mesh object '" + obj.name + "'.")

set_active(primary) # Re-select primary object

return {'FINISHED'}
Loading

0 comments on commit 572a6a7

Please sign in to comment.