Pose Shape Keys: Major Update #321
@ -1 +1,41 @@
|
|||||||
<!--@include: ../../scripts-blender/addons/pose_shape_keys/README.md-->
|
# Pose Shape Keys
|
||||||
|
|
||||||
|
This add-on enables a workflow where you can continue iterating on your vertex weights and bone constraints after you've already created your shape keys, without having to re-sculpt those shape keys. To put another way, you can think of shape keys as a final shape rather than as deltas on some deformation.
|
||||||
|
|
||||||
|
The only limitation is that there is some precision loss when using bendy bone deformations.
|
||||||
|
|
||||||
|
It also lets you manage multiple copies of a shape key together. Each copy can have a different vertex group mask, or be applied mirrored around the X axis.
|
||||||
|
|
||||||
|
You can find a video tutorial and more detailed explanation of how it works [here](https://studio.blender.org/blog/rig-with-shape-keys-like-never-before/).
|
||||||
|
|
||||||
|
## Basic Workflow:
|
||||||
|
- Create a pose whose deformation you want to correct. A pose is defined as an Action and a frame number.
|
||||||
|
- Create a Pose Key on the deformed mesh. Assign the action and the frame number.
|
||||||
|
- Press "Store Evaluated Mesh". This will create a copy of your mesh with all deformations applied.
|
||||||
|
- Sculpt this mesh into the desired shape.
|
||||||
|
- Go back to the deformed mesh, and assign one or more Shape Keys to the Pose Key.
|
||||||
|
- Press "Set Pose" to ensure that the rig is in the pose you created and specified earlier.
|
||||||
|
- Press "Overwrite Shape Keys".
|
||||||
|
- When you activate your shape key, your deformed mesh should now look identical to your sculpted shape.
|
||||||
|
- If you have more than one shape key, the same data will be pushed into each.
|
||||||
|
The purpose of this is that each copy of the shape key can have a different mask assigned to it.
|
||||||
|
This can streamline symmetrical workflows, since you can push to a left and a right-side shape key in a single click.
|
||||||
|
|
||||||
|
# Example use cases:
|
||||||
|
|
||||||
|
### 1. Sculpted facial expressions applied directly on top of a bone deformation based rig:
|
||||||
|
- A character artist can sculpt facial expressions to great quality and detail
|
||||||
|
- You pose the rig to be as close to this sculpted shape as possible, and create a rig control that blends into this pose using Action Constraints.
|
||||||
|
- Using the add-on, create corrective shape keys that blend your posed mesh into the shape of the sculpt.
|
||||||
|
- Hook up those corrective shape keys to the rig via drivers
|
||||||
|
- You now have the precise result of the sculpted facial expression, while retaining the freedom of bone-based controls that can move, scale and rotate!
|
||||||
|
|
||||||
|
### 2. Author finger correctives 24-at-a-time:
|
||||||
|
- Create a fist pose where all finger bones (4x2x3=24) are bent by around 90 degrees.
|
||||||
|
- Create a Pose Key and a storage object, and sculpt the desired deformation result.
|
||||||
|
- On the rigged mesh, create the 24 shape keys within the PoseKey; One for each section of each finger.
|
||||||
|
- Assign vertex groups to them that mask the affected areas.
|
||||||
|
- Normalize the vertex masks.
|
||||||
|
- Now you can push the sculpted fist shape into all 24 shape keys at the same time.
|
||||||
|
- Create drivers so each shape key is driven by the corresponding finger bone.
|
||||||
|
- You can now tweak and iterate on the sculpted shape, and update all 24 shape keys with the click of a single button.
|
@ -46,13 +46,13 @@ class EASYWEIGHT_OT_force_apply_mirror(bpy.types.Operator):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if not obj or obj.type != 'MESH':
|
if not obj or obj.type != 'MESH':
|
||||||
cls.set_poll_message("There must be an active mesh object deformed by an Armature.")
|
cls.poll_message_set("There must be an active mesh object deformed by an Armature.")
|
||||||
return False
|
return False
|
||||||
for mod in obj.modifiers:
|
for mod in obj.modifiers:
|
||||||
if mod.type == 'MIRROR':
|
if mod.type == 'MIRROR':
|
||||||
return True
|
return True
|
||||||
|
|
||||||
cls.set_poll_message("This mesh is not deformed by an Armature modifier.")
|
cls.poll_message_set("This mesh is not deformed by an Armature modifier.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
@ -139,7 +139,7 @@ class EASYWEIGHT_OT_toggle_weight_paint(Operator):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
obj = context.active_object
|
obj = context.active_object
|
||||||
if not obj and obj.type == 'MESH':
|
if not obj and obj.type == 'MESH':
|
||||||
cls.set_poll_message("Active object must be a mesh.")
|
cls.poll_message_set("Active object must be a mesh.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
## 0.0.4 - 2024-02-23
|
|
||||||
|
|
||||||
|
|
||||||
### CHANGED
|
|
||||||
- Change name separator from . to -
|
|
||||||
- Format w/ Black + an icon API breakage
|
|
||||||
- Use consistent registration pattern
|
|
||||||
|
|
||||||
## 0.0.3 - 2023-08-02
|
|
||||||
|
|
||||||
### FIXED
|
|
||||||
- Fix Changelog Rendering (#125)
|
|
||||||
- Fix Typo in README
|
|
||||||
- Fix line ends from DOS to UNIX (#68)
|
|
||||||
|
|
||||||
## 0.0.2 - 2023-06-02
|
|
||||||
|
|
||||||
## DOCUMENTED
|
|
||||||
- Initial release
|
|
@ -1,51 +1,5 @@
|
|||||||
# Pose Shape Keys
|
# Pose Shape Keys
|
||||||
|
|
||||||
## Table of Contents
|
This add-on enables a workflow where you can think of shape keys as a final shape rather than as deltas on some deformation. This means you can continue iterating on your vertex weights and bone constraints after you've already created your shape keys, without having to re-sculpt those shape keys.
|
||||||
- [Installation](#installation)
|
|
||||||
- [Basic Workflow](#basic-workflow)
|
|
||||||
- [Example Use Cases](#example-use-cases)
|
|
||||||
|
|
||||||
This addon lets you create shape keys that blend already deformed meshes into a previously stored shape.
|
|
||||||
It also lets you manage multiple copies of a shape key together. Each copy can have a different vertex group mask, or be applied mirrored around the X axis.
|
|
||||||
|
|
||||||
You can find a detailed video tutorial on how to download, install and use the addon [here](https://studio.blender.org/training/blender-studio-rigging-tools/pose-shape-keys/).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Find installation instructions [here](https://studio.blender.org/pipeline/addons/overview).
|
|
||||||
|
|
||||||
## Basic Workflow:
|
|
||||||
- Create a pose whose deformation you want to correct. A pose is defined as an Action and a frame number.
|
|
||||||
- Create a Pose Key on the deformed mesh. Assign the action and the frame number.
|
|
||||||
- Press "Store Evaluated Mesh". This will create a copy of your mesh with all deformations applied.
|
|
||||||
- Sculpt this mesh into the desired shape.
|
|
||||||
- Go back to the deformed mesh, and assign one or more Shape Keys to the Pose Key.
|
|
||||||
- Press "Set Pose" to ensure that the rig is in the pose you created and specified earlier.
|
|
||||||
- Press "Overwrite Shape Keys".
|
|
||||||
- When you activate your shape key, your deformed mesh should now look identical to your sculpted shape.
|
|
||||||
- If you have more than one shape key, the same data will be pushed into each.
|
|
||||||
The purpose of this is that each copy of the shape key have a different mask assigned to it.
|
|
||||||
This can streamline symmetrical workflows, since you can push to a left and a right-side shape key in a single click.
|
|
||||||
|
|
||||||
# Example use cases:
|
|
||||||
|
|
||||||
### 1. Sculpted facial expressions applied directly on top of a bone deformation based rig:
|
|
||||||
- A character artist can sculpt facial expressions to great quality and detail
|
|
||||||
- You pose the rig to be as close to this sculpted shape as possible, and create
|
|
||||||
a rig control that blends into this pose using Action Constraints.
|
|
||||||
- Using the addon, create corrective shape keys that blend your posed mesh into
|
|
||||||
the shape of the sculpt.
|
|
||||||
- Hook up those corrective shape keys to the rig via drivers
|
|
||||||
- You now have the precise result of the sculpted facial expression, while retaining
|
|
||||||
the freedom of bone-based controls that can move, scale and rotate!
|
|
||||||
|
|
||||||
### 2. Author finger correctives 24 at a time:
|
|
||||||
- Create a pose where all fingers are bent by 90 degrees at the first joint.
|
|
||||||
- Create a Pose Key and a storage object, and sculpt the desired deformation result.
|
|
||||||
- On the rigged mesh, create 24 shape keys within the PoseKey; One for each section of each finger.
|
|
||||||
- Assign vertex groups to them that mask each finger.
|
|
||||||
- Normalize the vertex masks.
|
|
||||||
- Now you can push the sculpted hand shape into all 24 shape keys at the same time.
|
|
||||||
- Create drivers so each shape key is driven by the corresponding finger bone.
|
|
||||||
- You can now tweak and iterate on the sculpted shape, and update all 24 shape keys
|
|
||||||
with the click of a single button.
|
|
||||||
|
|
||||||
|
You can find the documentation [here](https://studio.blender.org/pipeline/addons/pose_shape_keys).
|
@ -1,43 +1,29 @@
|
|||||||
# Pose Shape Keys addon for Blender
|
|
||||||
# Copyright (C) 2022 Demeter Dzadik
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Pose Shape Keys",
|
"name": "Pose Shape Keys",
|
||||||
"author": "Demeter Dzadik",
|
"author": "Demeter Dzadik",
|
||||||
"version": (0, 0, 4),
|
"version": (1, 0, 0),
|
||||||
"blender": (3, 1, 0),
|
"blender": (3, 1, 0),
|
||||||
"location": "Properties -> Mesh Data -> Shape Keys -> Pose Keys",
|
"location": "Properties -> Mesh Data -> Shape Keys -> Pose Keys",
|
||||||
"description": "Create shape keys that blend deformed meshes into a desired shape",
|
"description": "Create shape keys that blend deformed meshes into a desired shape",
|
||||||
"category": "Rigging",
|
"category": "Rigging",
|
||||||
"doc_url": "",
|
"doc_url": "https://studio.blender.org/pipeline/addons/pose_shape_keys",
|
||||||
"tracker_url": "",
|
"tracker_url": "https://projects.blender.org/studio/blender-studio-pipeline/src/branch/main/scripts-blender/addons/pose_shape_keys",
|
||||||
}
|
}
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from . import ui
|
from . import (
|
||||||
from . import pose_key
|
props,
|
||||||
from . import ui_list
|
ui,
|
||||||
from . import reset_rig
|
ops,
|
||||||
from . import symmetrize_shape_key
|
ui_list,
|
||||||
from . import prefs
|
symmetrize_shape_key,
|
||||||
|
prefs,
|
||||||
|
)
|
||||||
|
|
||||||
# Each module can have register() and unregister() functions and a list of classes to register called "registry".
|
# Each module can have register() and unregister() functions and a list of classes to register called "registry".
|
||||||
modules = [prefs, ui, pose_key, ui_list, reset_rig, symmetrize_shape_key]
|
modules = [props, prefs, ui, ops, ui_list, symmetrize_shape_key]
|
||||||
|
|
||||||
|
|
||||||
def register_unregister_modules(modules, register: bool):
|
def register_unregister_modules(modules, register: bool):
|
||||||
|
19
scripts-blender/addons/pose_shape_keys/blender_manifest.toml
Normal file
19
scripts-blender/addons/pose_shape_keys/blender_manifest.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "pose_shape_keys"
|
||||||
|
version = "0.0.2"
|
||||||
|
name = "Pose Shape Keys"
|
||||||
|
tagline = "Preserve your shape keys through weight changes, and much more"
|
||||||
|
maintainer = "Demeter Dzadik <demeter@blender.org>"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://studio.blender.org/pipeline/addons/pose_shape_keys"
|
||||||
|
tags = ["Rigging"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
|
||||||
|
license = [
|
||||||
|
"SPDX:GPL-3.0-or-later",
|
||||||
|
]
|
||||||
|
copyright = [
|
||||||
|
"2019-2024 Demeter Dzadik & Blender Studio",
|
||||||
|
]
|
1125
scripts-blender/addons/pose_shape_keys/ops.py
Normal file
1125
scripts-blender/addons/pose_shape_keys/ops.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,854 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from typing import List
|
|
||||||
from bpy.types import PropertyGroup, Object, Operator, Action, ShapeKey, VertexGroup, MeshVertex
|
|
||||||
from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty
|
|
||||||
from mathutils import Vector
|
|
||||||
from math import sqrt
|
|
||||||
from .symmetrize_shape_key import mirror_mesh
|
|
||||||
|
|
||||||
# When saving or pushing shapes, disable any modifier NOT in this list.
|
|
||||||
DEFORM_MODIFIERS = [
|
|
||||||
'ARMATURE',
|
|
||||||
'CAST',
|
|
||||||
'CURVE',
|
|
||||||
'DISPLACE',
|
|
||||||
'HOOK',
|
|
||||||
'LAPLACIANDEFORM',
|
|
||||||
'LATTICE',
|
|
||||||
'MESH_DEFORM',
|
|
||||||
'SHRINKWRAP',
|
|
||||||
'SIMPLE_DEFORM',
|
|
||||||
'SMOOTH',
|
|
||||||
'CORRECTIVE_SMOOTH',
|
|
||||||
'LAPLACIANSMOOTH',
|
|
||||||
'SURFACE_DEFORM',
|
|
||||||
'WARP',
|
|
||||||
'WAVE',
|
|
||||||
]
|
|
||||||
GOOD_MODIFIERS = ['ARMATURE']
|
|
||||||
|
|
||||||
|
|
||||||
def get_addon_prefs(context):
|
|
||||||
return context.preferences.addons[__package__].preferences
|
|
||||||
|
|
||||||
|
|
||||||
class PoseShapeKeyTarget(PropertyGroup):
|
|
||||||
def update_name(self, context):
|
|
||||||
if self.block_name_update:
|
|
||||||
return
|
|
||||||
ob = context.object
|
|
||||||
if not ob.data.shape_keys:
|
|
||||||
return
|
|
||||||
sk = ob.data.shape_keys.key_blocks.get(self.shape_key_name)
|
|
||||||
if sk:
|
|
||||||
sk.name = self.name
|
|
||||||
self.shape_key_name = self.name
|
|
||||||
|
|
||||||
def update_shape_key_name(self, context):
|
|
||||||
self.block_name_update = True
|
|
||||||
self.name = self.shape_key_name
|
|
||||||
self.block_name_update = False
|
|
||||||
|
|
||||||
name: StringProperty(
|
|
||||||
name="Shape Key Target",
|
|
||||||
description="Name of this shape key target. Should stay in sync with the displayed name and the shape key name, unless the shape key is renamed outside of our UI",
|
|
||||||
update=update_name,
|
|
||||||
)
|
|
||||||
mirror_x: BoolProperty(
|
|
||||||
name="Mirror X",
|
|
||||||
description="Mirror the shape key on the X axis when applying the stored shape to this shape key",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
block_name_update: BoolProperty(
|
|
||||||
description="Flag to help keep shape key names in sync", default=False
|
|
||||||
)
|
|
||||||
shape_key_name: StringProperty(
|
|
||||||
name="Shape Key",
|
|
||||||
description="Name of the shape key to push data to",
|
|
||||||
update=update_shape_key_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key_block(self) -> List[ShapeKey]:
|
|
||||||
mesh = self.id_data
|
|
||||||
if not mesh.shape_keys:
|
|
||||||
return
|
|
||||||
return mesh.shape_keys.key_blocks.get(self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class PoseShapeKey(PropertyGroup):
|
|
||||||
target_shapes: CollectionProperty(type=PoseShapeKeyTarget)
|
|
||||||
|
|
||||||
def update_active_sk_index(self, context):
|
|
||||||
ob = context.object
|
|
||||||
if not ob.data.shape_keys:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name
|
|
||||||
except IndexError:
|
|
||||||
ob.active_shape_key_index = len(ob.data.shape_keys.key_blocks) - 1
|
|
||||||
return
|
|
||||||
key_block_idx = ob.data.shape_keys.key_blocks.find(sk_name)
|
|
||||||
if key_block_idx > -1:
|
|
||||||
ob.active_shape_key_index = key_block_idx
|
|
||||||
|
|
||||||
# If in weight paint mode and there is a mask vertex group,
|
|
||||||
# also set that vertex group as active.
|
|
||||||
if context.mode == 'PAINT_WEIGHT':
|
|
||||||
key_block = ob.data.shape_keys.key_blocks[key_block_idx]
|
|
||||||
vg_idx = ob.vertex_groups.find(key_block.vertex_group)
|
|
||||||
if vg_idx > -1:
|
|
||||||
ob.vertex_groups.active_index = vg_idx
|
|
||||||
|
|
||||||
active_target_shape_index: IntProperty(update=update_active_sk_index)
|
|
||||||
|
|
||||||
action: PointerProperty(
|
|
||||||
name="Action",
|
|
||||||
type=Action,
|
|
||||||
description="Action that contains the frame that should be used when applying the stored shape as a shape key",
|
|
||||||
)
|
|
||||||
frame: IntProperty(
|
|
||||||
name="Frame",
|
|
||||||
description="Frame that should be used within the selected action when applying the stored shape as a shape key",
|
|
||||||
default=0,
|
|
||||||
)
|
|
||||||
storage_object: PointerProperty(
|
|
||||||
type=Object,
|
|
||||||
name="Storage Object",
|
|
||||||
description="Specify an object that stores the vertex position data",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_deforming_armature(mesh_ob: Object) -> Object:
|
|
||||||
for m in mesh_ob.modifiers:
|
|
||||||
if m.type == 'ARMATURE':
|
|
||||||
return m.object
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_Create_ShapeKey_For_Pose(Operator):
|
|
||||||
"""Create and assign a Shape Key"""
|
|
||||||
|
|
||||||
bl_idname = "object.create_shape_key_for_pose"
|
|
||||||
bl_label = "Create Shape Key"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
bl_property = "sk_name"
|
|
||||||
|
|
||||||
def update_sk_name(self, context):
|
|
||||||
def set_vg(vg_name):
|
|
||||||
ob = context.object
|
|
||||||
vg = ob.vertex_groups.get(vg_name)
|
|
||||||
if vg:
|
|
||||||
self.vg_name = vg.name
|
|
||||||
return vg
|
|
||||||
|
|
||||||
ob = context.object
|
|
||||||
vg = set_vg(self.sk_name)
|
|
||||||
if not vg and self.sk_name.endswith(".L"):
|
|
||||||
vg = set_vg("Side.L")
|
|
||||||
if not vg and self.sk_name.endswith(".R"):
|
|
||||||
vg = set_vg("Side.R")
|
|
||||||
|
|
||||||
sk_name: StringProperty(
|
|
||||||
name="Name",
|
|
||||||
description="Name to set for the new shape key",
|
|
||||||
default="Key",
|
|
||||||
update=update_sk_name,
|
|
||||||
)
|
|
||||||
vg_name: StringProperty(
|
|
||||||
name="Vertex Group",
|
|
||||||
description="Vertex Group to assign as the masking group of this shape key",
|
|
||||||
default="",
|
|
||||||
)
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
ob = context.object
|
|
||||||
if ob.data.shape_keys:
|
|
||||||
self.sk_name = f"Key {len(ob.data.shape_keys.key_blocks)}"
|
|
||||||
else:
|
|
||||||
self.sk_name = "Key"
|
|
||||||
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
if pose_key.name:
|
|
||||||
self.sk_name = pose_key.name
|
|
||||||
|
|
||||||
return context.window_manager.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.prop(self, 'sk_name')
|
|
||||||
ob = context.object
|
|
||||||
layout.prop_search(self, 'vg_name', ob, "vertex_groups")
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
ob = context.object
|
|
||||||
|
|
||||||
# Ensure Basis shape key
|
|
||||||
if not ob.data.shape_keys:
|
|
||||||
basis = ob.shape_key_add()
|
|
||||||
basis.name = "Basis"
|
|
||||||
ob.data.update()
|
|
||||||
|
|
||||||
# Add new shape key
|
|
||||||
new_sk = ob.shape_key_add()
|
|
||||||
new_sk.name = self.sk_name
|
|
||||||
new_sk.value = 1
|
|
||||||
if self.vg_name:
|
|
||||||
new_sk.vertex_group = self.vg_name
|
|
||||||
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
target = pose_key.target_shapes[pose_key.active_target_shape_index]
|
|
||||||
target.name = new_sk.name
|
|
||||||
|
|
||||||
self.report({'INFO'}, f"Added shape key {new_sk.name}.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class SaveAndRestoreState:
|
|
||||||
def disable_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object):
|
|
||||||
# Disable non-deforming modifiers
|
|
||||||
self.disabled_mods_storage = []
|
|
||||||
self.disabled_mods_rigged = []
|
|
||||||
self.disabled_fcurves = []
|
|
||||||
for ob, lst in zip(
|
|
||||||
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
|
||||||
):
|
|
||||||
if not ob:
|
|
||||||
continue
|
|
||||||
for m in ob.modifiers:
|
|
||||||
if m.type not in GOOD_MODIFIERS and m.show_viewport:
|
|
||||||
lst.append(m.name)
|
|
||||||
m.show_viewport = False
|
|
||||||
if m.show_viewport:
|
|
||||||
data_path = f'modifiers["{m.name}"].show_viewport'
|
|
||||||
fc = ob.animation_data.drivers.find(data_path)
|
|
||||||
if fc:
|
|
||||||
fc.mute = True
|
|
||||||
self.disabled_fcurves.append(data_path)
|
|
||||||
m.show_viewport = False
|
|
||||||
|
|
||||||
def restore_non_deform_modifiers(self, storage_ob: Object, rigged_ob: Object):
|
|
||||||
# Re-enable non-deforming modifiers
|
|
||||||
for ob, m_list in zip(
|
|
||||||
[storage_ob, rigged_ob], [self.disabled_mods_storage, self.disabled_mods_rigged]
|
|
||||||
):
|
|
||||||
if not ob:
|
|
||||||
continue
|
|
||||||
for m_name in m_list:
|
|
||||||
ob.modifiers[m_name].show_viewport = True
|
|
||||||
for data_path in self.disabled_fcurves:
|
|
||||||
fc = ob.animation_data.drivers.find(data_path)
|
|
||||||
if fc:
|
|
||||||
fc.mute = False
|
|
||||||
|
|
||||||
def save_state(self, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
storage_ob = pose_key.storage_object
|
|
||||||
|
|
||||||
# Non-Deforming modifiers
|
|
||||||
self.disable_non_deform_modifiers(storage_ob, rigged_ob)
|
|
||||||
|
|
||||||
# Active Shape Key Index
|
|
||||||
self.orig_sk_index = rigged_ob.active_shape_key_index
|
|
||||||
rigged_ob.active_shape_key_index = 0
|
|
||||||
|
|
||||||
# Shape Keys
|
|
||||||
self.org_sk_toggles = {}
|
|
||||||
for target_shape in pose_key.target_shapes:
|
|
||||||
key_block = target_shape.key_block
|
|
||||||
if not key_block:
|
|
||||||
self.report({'ERROR'}, f"Shape key not found: {key_block.name}")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
self.org_sk_toggles[key_block.name] = key_block.mute
|
|
||||||
key_block.mute = True
|
|
||||||
|
|
||||||
def restore_state(self, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
storage_ob = pose_key.storage_object
|
|
||||||
self.restore_non_deform_modifiers(storage_ob, rigged_ob)
|
|
||||||
|
|
||||||
rigged_ob.active_shape_key_index = self.orig_sk_index
|
|
||||||
for kb_name, kb_value in self.org_sk_toggles.items():
|
|
||||||
rigged_ob.data.shape_keys.key_blocks[kb_name].mute = kb_value
|
|
||||||
|
|
||||||
|
|
||||||
class OperatorWithWarning:
|
|
||||||
def invoke(self, context, event):
|
|
||||||
addon_prefs = get_addon_prefs(context)
|
|
||||||
if addon_prefs.no_warning:
|
|
||||||
return self.execute(context)
|
|
||||||
|
|
||||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout.column(align=True)
|
|
||||||
|
|
||||||
warning = self.get_warning_text(context)
|
|
||||||
for line in warning.split("\n"):
|
|
||||||
row = layout.row()
|
|
||||||
row.alert = True
|
|
||||||
row.label(text=line)
|
|
||||||
|
|
||||||
addon_prefs = get_addon_prefs(context)
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.prop(addon_prefs, 'no_warning', text="Disable Warnings (Can be reset in Preferences)")
|
|
||||||
|
|
||||||
def get_warning_text(self, context):
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
|
|
||||||
def set_pose_of_active_pose_key(context):
|
|
||||||
bpy.ops.object.posekey_reset_rig()
|
|
||||||
|
|
||||||
rigged_ob = context.object
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
|
|
||||||
arm_ob = get_deforming_armature(rigged_ob)
|
|
||||||
if pose_key.action:
|
|
||||||
# Set Action and Frame to get the right pose
|
|
||||||
arm_ob.animation_data.action = pose_key.action
|
|
||||||
context.scene.frame_current = pose_key.frame
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Set_Pose(Operator):
|
|
||||||
"""Set the rig pose to the specified action and frame (Reset any other posing)"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_set_pose"
|
|
||||||
bl_label = "Set Pose"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
arm_ob = get_deforming_armature(rigged_ob)
|
|
||||||
if not arm_ob:
|
|
||||||
return False
|
|
||||||
if rigged_ob.type != 'MESH' or not rigged_ob.data.shape_keys:
|
|
||||||
return False
|
|
||||||
if len(rigged_ob.data.pose_keys) == 0:
|
|
||||||
return True
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
if not pose_key.action:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
set_pose_of_active_pose_key(context)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
def get_active_pose_key(ob):
|
|
||||||
if ob.type != 'MESH':
|
|
||||||
return
|
|
||||||
if len(ob.data.pose_keys) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
return ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
|
|
||||||
|
|
||||||
def verify_pose(context):
|
|
||||||
"""To make these operators foolproof, there are a lot of checks to make sure
|
|
||||||
that the user gets to see the effect of the operator. The "Set Pose" operator
|
|
||||||
can be used first to set the correct state and pass all the checks here.
|
|
||||||
"""
|
|
||||||
ob = context.object
|
|
||||||
|
|
||||||
pose_key = get_active_pose_key(ob)
|
|
||||||
if not pose_key:
|
|
||||||
return False
|
|
||||||
|
|
||||||
arm_ob = get_deforming_armature(ob)
|
|
||||||
|
|
||||||
# Action must exist and match.
|
|
||||||
if not pose_key.action:
|
|
||||||
return False
|
|
||||||
if not arm_ob.animation_data or arm_ob.animation_data.action != pose_key.action:
|
|
||||||
return False
|
|
||||||
if pose_key.frame != context.scene.frame_current:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Save(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
||||||
"""Save the current evaluated mesh vertex positions into the Storage Object"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_save"
|
|
||||||
bl_label = "Overwrite Storage Object"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
ob = context.object
|
|
||||||
# We can guess the action and frame number
|
|
||||||
arm_ob = get_deforming_armature(ob)
|
|
||||||
pose_key = get_active_pose_key(ob)
|
|
||||||
if (
|
|
||||||
pose_key
|
|
||||||
and not pose_key.storage_object
|
|
||||||
and not pose_key.action
|
|
||||||
and arm_ob.animation_data
|
|
||||||
and arm_ob.animation_data.action
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return verify_pose(context)
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
ob = context.object
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
if pose_key.storage_object:
|
|
||||||
return super().invoke(context, event)
|
|
||||||
return self.execute(context)
|
|
||||||
|
|
||||||
def get_warning_text(self, context):
|
|
||||||
ob = context.object
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
return f'This will overwrite "{pose_key.storage_object.name}".\n Are you sure?'
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
storage_ob = pose_key.storage_object
|
|
||||||
already_existed = storage_ob != None
|
|
||||||
self.disable_non_deform_modifiers(storage_ob, rigged_ob)
|
|
||||||
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
|
||||||
rigged_ob_eval = rigged_ob.evaluated_get(depsgraph)
|
|
||||||
rigged_ob_eval_mesh = rigged_ob_eval.data
|
|
||||||
|
|
||||||
storage_ob_name = rigged_ob.name + "-" + pose_key.name
|
|
||||||
storage_ob_mesh = bpy.data.meshes.new_from_object(rigged_ob)
|
|
||||||
storage_ob_mesh.name = storage_ob_name
|
|
||||||
|
|
||||||
if not already_existed:
|
|
||||||
storage_ob = bpy.data.objects.new(storage_ob_name, storage_ob_mesh)
|
|
||||||
context.scene.collection.objects.link(storage_ob)
|
|
||||||
pose_key.storage_object = storage_ob
|
|
||||||
storage_ob.location = rigged_ob.location
|
|
||||||
storage_ob.location.x -= rigged_ob.dimensions.x * 1.1
|
|
||||||
|
|
||||||
# Set action and frame number to the current ones, in case the user
|
|
||||||
# is already in the desired pose for this pose key.
|
|
||||||
arm_ob = get_deforming_armature(rigged_ob)
|
|
||||||
if arm_ob and arm_ob.animation_data and arm_ob.animation_data.action:
|
|
||||||
pose_key.action = arm_ob.animation_data.action
|
|
||||||
pose_key.frame = context.scene.frame_current
|
|
||||||
else:
|
|
||||||
old_mesh = storage_ob.data
|
|
||||||
storage_ob.data = storage_ob_mesh
|
|
||||||
bpy.data.meshes.remove(old_mesh)
|
|
||||||
|
|
||||||
if len(storage_ob.data.vertices) != len(rigged_ob.data.vertices):
|
|
||||||
self.report(
|
|
||||||
{'WARNING'},
|
|
||||||
f'Vertex Count did not match between storage object {storage_ob.name}({len(storage_ob.data.vertices)}) and current ({len(rigged_ob.data.vertices)})!',
|
|
||||||
)
|
|
||||||
storage_ob_mesh = bpy.data.meshes.new_from_object(rigged_ob_eval)
|
|
||||||
storage_ob.data = storage_ob_mesh
|
|
||||||
storage_ob.data.name = storage_ob_name
|
|
||||||
|
|
||||||
storage_ob.use_shape_key_edit_mode = True
|
|
||||||
storage_ob.shape_key_add(name="Basis")
|
|
||||||
target = storage_ob.shape_key_add(name="Morph Target")
|
|
||||||
adjust = storage_ob.shape_key_add(name="New Changes", from_mix=True)
|
|
||||||
target.value = 1
|
|
||||||
adjust.value = 1
|
|
||||||
storage_ob.active_shape_key_index = 2
|
|
||||||
|
|
||||||
# Fix material assignments in case any material slots are linked to the
|
|
||||||
# object instead of the mesh.
|
|
||||||
for i, ms in enumerate(rigged_ob.material_slots):
|
|
||||||
if ms.link == 'OBJECT':
|
|
||||||
storage_ob.material_slots[i].link = 'OBJECT'
|
|
||||||
storage_ob.material_slots[i].material = ms.material
|
|
||||||
|
|
||||||
# Set the target shape to be the evaluated mesh.
|
|
||||||
for target_v, eval_v in zip(target.data, rigged_ob_eval_mesh.vertices):
|
|
||||||
target_v.co = eval_v.co
|
|
||||||
|
|
||||||
# Copy some symmetry settings from the original
|
|
||||||
storage_ob.data.use_mirror_x = rigged_ob.data.use_mirror_x
|
|
||||||
|
|
||||||
# Nuke vertex groups, since we don't need them.
|
|
||||||
storage_ob.vertex_groups.clear()
|
|
||||||
|
|
||||||
self.restore_non_deform_modifiers(storage_ob, rigged_ob)
|
|
||||||
|
|
||||||
# If new shape is visible and it already existed, set it as active.
|
|
||||||
if already_existed and storage_ob.visible_get():
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
context.view_layer.objects.active = storage_ob
|
|
||||||
storage_ob.select_set(True)
|
|
||||||
|
|
||||||
self.report({'INFO'}, f'The deformed mesh has been stored in "{storage_ob.name}".')
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Push(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
||||||
"""Let the below shape keys blend the current deformed shape into the shape of the Storage Object"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_push"
|
|
||||||
bl_label = "Load Vertex Position Data into Shape Keys"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
pose_matches = verify_pose(context)
|
|
||||||
if not pose_matches:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# No shape keys to push into
|
|
||||||
ob = context.object
|
|
||||||
pose_key = get_active_pose_key(ob)
|
|
||||||
for target_shape in pose_key.target_shapes:
|
|
||||||
if target_shape.key_block:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_warning_text(self, context):
|
|
||||||
ob = context.object
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
target_shape_names = [target.name for target in pose_key.target_shapes if target]
|
|
||||||
return (
|
|
||||||
"This will overwrite the following Shape Keys: \n "
|
|
||||||
+ "\n ".join(target_shape_names)
|
|
||||||
+ "\n Are you sure?"
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
"""
|
|
||||||
Load the active PoseShapeKey's mesh data into its corresponding shape key,
|
|
||||||
such that the shape key will blend from whatever state the mesh is currently in,
|
|
||||||
into the shape stored in the PoseShapeKey.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.save_state(context)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.push_active_pose_key(context, set_pose=False)
|
|
||||||
except:
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
self.restore_state(context)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def push_active_pose_key(self, context, set_pose=False):
|
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
|
||||||
scene = context.scene
|
|
||||||
|
|
||||||
rigged_ob = context.object
|
|
||||||
|
|
||||||
pose_key = rigged_ob.data.pose_keys[rigged_ob.data.active_pose_key_index]
|
|
||||||
|
|
||||||
storage_object = pose_key.storage_object
|
|
||||||
if storage_object.name not in context.view_layer.objects:
|
|
||||||
self.report({'ERROR'}, f'Storage object "{storage_object.name}" must be in view layer!')
|
|
||||||
raise Exception
|
|
||||||
|
|
||||||
if set_pose:
|
|
||||||
set_pose_of_active_pose_key(context)
|
|
||||||
|
|
||||||
# The Pose Key stores the vertex positions of a previous evaluated mesh.
|
|
||||||
# This, and the current vertex positions of the mesh are subtracted
|
|
||||||
# from each other to get the difference in their shape.
|
|
||||||
storage_eval_verts = pose_key.storage_object.evaluated_get(depsgraph).data.vertices
|
|
||||||
rigged_eval_verts = rigged_ob.evaluated_get(depsgraph).data.vertices
|
|
||||||
|
|
||||||
# Shape keys are relative to the base shape of the mesh, so that delta
|
|
||||||
# will be added to the base mesh to get the final shape key vertex positions.
|
|
||||||
rigged_base_verts = rigged_ob.data.vertices
|
|
||||||
|
|
||||||
# The CrazySpace provides us the matrix by which each vertex has been
|
|
||||||
# deformed by modifiers and shape keys. This matrix is necessary to
|
|
||||||
# calculate the correct delta.
|
|
||||||
rigged_ob.crazyspace_eval(depsgraph, scene)
|
|
||||||
|
|
||||||
for i, v in enumerate(storage_eval_verts):
|
|
||||||
if i > len(rigged_base_verts) - 1:
|
|
||||||
break
|
|
||||||
storage_eval_co = Vector(v.co)
|
|
||||||
rigged_eval_co = rigged_eval_verts[i].co
|
|
||||||
|
|
||||||
delta = storage_eval_co - rigged_eval_co
|
|
||||||
|
|
||||||
delta = rigged_ob.crazyspace_displacement_to_original(
|
|
||||||
vertex_index=i, displacement=delta
|
|
||||||
)
|
|
||||||
|
|
||||||
base_v = rigged_base_verts[i].co
|
|
||||||
for target_shape in pose_key.target_shapes:
|
|
||||||
key_block = target_shape.key_block
|
|
||||||
if not key_block:
|
|
||||||
continue
|
|
||||||
key_block.data[i].co = base_v + delta
|
|
||||||
|
|
||||||
# Mirror shapes if needed
|
|
||||||
for target_shape in pose_key.target_shapes:
|
|
||||||
if target_shape.mirror_x:
|
|
||||||
key_block = target_shape.key_block
|
|
||||||
if not key_block:
|
|
||||||
continue
|
|
||||||
mirror_mesh(
|
|
||||||
reference_verts=rigged_ob.data.vertices,
|
|
||||||
vertices=key_block.data,
|
|
||||||
axis='X',
|
|
||||||
symmetrize=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
rigged_ob.crazyspace_eval_clear()
|
|
||||||
|
|
||||||
if len(storage_eval_verts) != len(rigged_eval_verts):
|
|
||||||
self.report(
|
|
||||||
{'WARNING'},
|
|
||||||
f'Mismatching topology: Stored shape "{pose_key.storage_object.name}" had {len(storage_eval_verts)} vertices instead of {len(rigged_eval_verts)}',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Push_All(Operator, OperatorWithWarning, SaveAndRestoreState):
|
|
||||||
"""Go through all Pose Keys, set their pose and overwrite the shape keys to match the storage object shapes"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_push_all"
|
|
||||||
bl_label = "Push ALL Pose Keys into Shape Keys"
|
|
||||||
bl_options = {'UNDO', 'REGISTER'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
ob = context.object
|
|
||||||
if not ob or ob.type != 'MESH':
|
|
||||||
return False
|
|
||||||
return len(ob.data.pose_keys) > 0
|
|
||||||
|
|
||||||
def get_warning_text(self, context):
|
|
||||||
ob = context.object
|
|
||||||
target_shape_names = []
|
|
||||||
for pk in ob.data.pose_keys:
|
|
||||||
target_shape_names.extend([t.name for t in pk.target_shapes if t])
|
|
||||||
return (
|
|
||||||
"This will overwrite the following Shape Keys: \n "
|
|
||||||
+ "\n ".join(target_shape_names)
|
|
||||||
+ "\n Are you sure?"
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
for i, pk in enumerate(rigged_ob.data.pose_keys):
|
|
||||||
rigged_ob.data.active_pose_key_index = i
|
|
||||||
bpy.ops.object.posekey_set_pose()
|
|
||||||
bpy.ops.object.posekey_push()
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Clamp_Influence(Operator):
|
|
||||||
"""Clamp the influence of this pose key's shape keys to 1.0 for each vertex, by normalizing the vertex weight mask values of vertices where the total influence is greater than 1"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_clamp_influence"
|
|
||||||
bl_label = "Clamp Vertex Influences"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_affected_vertex_group_names(object: Object) -> List[str]:
|
|
||||||
pose_key = object.data.pose_keys[object.data.active_pose_key_index]
|
|
||||||
|
|
||||||
vg_names = []
|
|
||||||
for target_shape in pose_key.target_shapes:
|
|
||||||
kb = target_shape.key_block
|
|
||||||
if not kb:
|
|
||||||
continue
|
|
||||||
if kb.vertex_group and kb.vertex_group in object.vertex_groups:
|
|
||||||
vg_names.append(kb.vertex_group)
|
|
||||||
|
|
||||||
return vg_names
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return cls.get_affected_vertex_group_names(context.object)
|
|
||||||
|
|
||||||
def normalize_vgroups(self, o, vgroups):
|
|
||||||
"""Normalize a set of vertex groups in isolation"""
|
|
||||||
""" Used for creating mask vertex groups for splitting shape keys """
|
|
||||||
for v in o.data.vertices:
|
|
||||||
# Find sum of weights in specified vgroups
|
|
||||||
# set weight to original/sum
|
|
||||||
sum_weights = 0
|
|
||||||
for vg in vgroups:
|
|
||||||
w = 0
|
|
||||||
try:
|
|
||||||
sum_weights += vg.weight(v.index)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
for vg in vgroups:
|
|
||||||
if sum_weights > 1.0:
|
|
||||||
try:
|
|
||||||
vg.add([v.index], vg.weight(v.index) / sum_weights, 'REPLACE')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
ob = context.object
|
|
||||||
vg_names = self.get_affected_vertex_group_names(ob)
|
|
||||||
self.normalize_vgroups(ob, [ob.vertex_groups[vg_name] for vg_name in vg_names])
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Place_Objects_In_Grid(Operator):
|
|
||||||
"""Place the storage objects in a grid above this object"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_object_grid"
|
|
||||||
bl_label = "Place ALL Storage Objects in a Grid"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_storage_objects(context) -> List[Object]:
|
|
||||||
ob = context.object
|
|
||||||
pose_keys = ob.data.pose_keys
|
|
||||||
return [pk.storage_object for pk in pose_keys if pk.storage_object]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
"""Only available if there are any storage objects in any of the pose keys."""
|
|
||||||
return cls.get_storage_objects(context)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def place_objects_in_grid(context, objs: List[Object]):
|
|
||||||
x = max([o.dimensions.x for o in objs])
|
|
||||||
y = max([o.dimensions.y for o in objs])
|
|
||||||
z = max([o.dimensions.z for o in objs])
|
|
||||||
scalar = 1.2
|
|
||||||
dimensions = Vector((x * scalar, y * scalar, z * scalar))
|
|
||||||
|
|
||||||
grid_rows = round(sqrt(len(objs)))
|
|
||||||
for i, ob in enumerate(objs):
|
|
||||||
col_i = (i % grid_rows) - int(grid_rows / 2)
|
|
||||||
row_i = int(i / grid_rows) + scalar
|
|
||||||
offset = Vector((col_i * dimensions.x, 0, row_i * dimensions.z))
|
|
||||||
ob.location = context.object.location + offset
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
storage_objects = self.get_storage_objects(context)
|
|
||||||
self.place_objects_in_grid(context, storage_objects)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Jump_To_Shape(Operator):
|
|
||||||
"""Place the storage object next to this object and select it"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_jump_to_storage"
|
|
||||||
bl_label = "Jump To Storage Object"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_storage_object(context):
|
|
||||||
ob = context.object
|
|
||||||
pose_key = ob.data.pose_keys[ob.data.active_pose_key_index]
|
|
||||||
return pose_key.storage_object
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
"""Only available if there is a storage object in the pose key."""
|
|
||||||
return cls.get_storage_object(context)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
storage_object = self.get_storage_object(context)
|
|
||||||
|
|
||||||
storage_object.location = context.object.location
|
|
||||||
storage_object.location.x -= context.object.dimensions.x * 1.1
|
|
||||||
|
|
||||||
if storage_object.name not in context.view_layer.objects:
|
|
||||||
self.report({'ERROR'}, "Storage object must be in view layer.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
storage_object.select_set(True)
|
|
||||||
storage_object.hide_set(False)
|
|
||||||
context.view_layer.objects.active = storage_object
|
|
||||||
|
|
||||||
# Put the other storage objects in a grid
|
|
||||||
prefs = get_addon_prefs(context)
|
|
||||||
if prefs.grid_objects_on_jump:
|
|
||||||
storage_objects = OBJECT_OT_PoseKey_Place_Objects_In_Grid.get_storage_objects(context)
|
|
||||||
storage_objects.remove(storage_object)
|
|
||||||
OBJECT_OT_PoseKey_Place_Objects_In_Grid.place_objects_in_grid(context, storage_objects)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_PoseKey_Copy_Data(Operator):
|
|
||||||
"""Copy Pose Key data from active object to selected ones"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_copy_data"
|
|
||||||
bl_label = "Copy Pose Key Data"
|
|
||||||
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
"""Only available if there is a selected mesh and the active mesh has pose key data."""
|
|
||||||
selected_meshes = [ob for ob in context.selected_objects if ob.type == 'MESH']
|
|
||||||
if len(selected_meshes) < 2:
|
|
||||||
return False
|
|
||||||
if context.object.type != 'MESH' or not context.object.data.pose_keys:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
source_ob = context.object
|
|
||||||
targets = [ob for ob in context.selected_objects if ob.type == 'MESH' and ob != source_ob]
|
|
||||||
|
|
||||||
for target_ob in targets:
|
|
||||||
target_ob.data.pose_keys.clear()
|
|
||||||
|
|
||||||
for src_pk in source_ob.data.pose_keys:
|
|
||||||
new_pk = target_ob.data.pose_keys.add()
|
|
||||||
new_pk.name = src_pk.name
|
|
||||||
new_pk.action = src_pk.action
|
|
||||||
new_pk.frame = src_pk.frame
|
|
||||||
new_pk.storage_object = src_pk.storage_object
|
|
||||||
for src_sk_slot in src_pk.target_shapes:
|
|
||||||
new_sk_slot = new_pk.target_shapes.add()
|
|
||||||
new_sk_slot.name = src_sk_slot.name
|
|
||||||
new_sk_slot.mirror_x = src_sk_slot.mirror_x
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
|
||||||
PoseShapeKeyTarget,
|
|
||||||
PoseShapeKey,
|
|
||||||
OBJECT_OT_PoseKey_Save,
|
|
||||||
OBJECT_OT_PoseKey_Set_Pose,
|
|
||||||
OBJECT_OT_PoseKey_Push,
|
|
||||||
OBJECT_OT_PoseKey_Push_All,
|
|
||||||
OBJECT_OT_Create_ShapeKey_For_Pose,
|
|
||||||
OBJECT_OT_PoseKey_Clamp_Influence,
|
|
||||||
OBJECT_OT_PoseKey_Place_Objects_In_Grid,
|
|
||||||
OBJECT_OT_PoseKey_Jump_To_Shape,
|
|
||||||
OBJECT_OT_PoseKey_Copy_Data,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def update_posekey_index(self, context):
|
|
||||||
# Want to piggyback on update_active_sk_index() to also update the active
|
|
||||||
# shape key index when switching pose keys.
|
|
||||||
mesh = context.object.data
|
|
||||||
if mesh.pose_keys:
|
|
||||||
pk = mesh.pose_keys[mesh.active_pose_key_index]
|
|
||||||
# We just want to fire the update func.
|
|
||||||
pk.active_target_shape_index = pk.active_target_shape_index
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
bpy.types.Mesh.pose_keys = CollectionProperty(type=PoseShapeKey)
|
|
||||||
bpy.types.Mesh.active_pose_key_index = IntProperty(update=update_posekey_index)
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
del bpy.types.Mesh.pose_keys
|
|
||||||
del bpy.types.Mesh.active_pose_key_index
|
|
@ -1,5 +1,16 @@
|
|||||||
from bpy.types import AddonPreferences
|
from bpy.types import AddonPreferences
|
||||||
from bpy.props import BoolProperty
|
from bpy.props import BoolProperty
|
||||||
|
from . import __package__ as base_package
|
||||||
|
|
||||||
|
|
||||||
|
def get_addon_prefs(context=None):
|
||||||
|
if not context:
|
||||||
|
context = bpy.context
|
||||||
|
if base_package.startswith('bl_ext'):
|
||||||
|
# 4.2
|
||||||
|
return context.preferences.addons[base_package].preferences
|
||||||
|
else:
|
||||||
|
return context.preferences.addons[base_package.split(".")[0]].preferences
|
||||||
|
|
||||||
|
|
||||||
class PoseShapeKeysPrefs(AddonPreferences):
|
class PoseShapeKeysPrefs(AddonPreferences):
|
||||||
@ -11,7 +22,7 @@ class PoseShapeKeysPrefs(AddonPreferences):
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
no_warning: BoolProperty(
|
no_warning: BoolProperty(
|
||||||
name="No Warning",
|
name="No Danger Warning",
|
||||||
description="Do not show a pop-up warning for dangerous operations",
|
description="Do not show a pop-up warning for dangerous operations",
|
||||||
)
|
)
|
||||||
grid_objects_on_jump: BoolProperty(
|
grid_objects_on_jump: BoolProperty(
|
||||||
|
127
scripts-blender/addons/pose_shape_keys/props.py
Normal file
127
scripts-blender/addons/pose_shape_keys/props.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import bpy
|
||||||
|
from bpy.types import PropertyGroup, Object, Action, ShapeKey
|
||||||
|
from bpy.props import PointerProperty, IntProperty, CollectionProperty, StringProperty, BoolProperty
|
||||||
|
|
||||||
|
|
||||||
|
class PoseShapeKeyTarget(PropertyGroup):
|
||||||
|
def update_name(self, context):
|
||||||
|
if self.block_name_update:
|
||||||
|
return
|
||||||
|
obj = context.object
|
||||||
|
if not obj.data.shape_keys:
|
||||||
|
return
|
||||||
|
sk = obj.data.shape_keys.key_blocks.get(self.shape_key_name)
|
||||||
|
if sk:
|
||||||
|
sk.name = self.name
|
||||||
|
self.shape_key_name = self.name
|
||||||
|
|
||||||
|
def update_shape_key_name(self, context):
|
||||||
|
self.block_name_update = True
|
||||||
|
self.name = self.shape_key_name
|
||||||
|
self.block_name_update = False
|
||||||
|
|
||||||
|
name: StringProperty(
|
||||||
|
name="Shape Key Target",
|
||||||
|
description="Name of this shape key target. Should stay in sync with the displayed name and the shape key name, unless the shape key is renamed outside of our UI",
|
||||||
|
update=update_name,
|
||||||
|
)
|
||||||
|
mirror_x: BoolProperty(
|
||||||
|
name="Mirror X",
|
||||||
|
description="Mirror the shape key on the X axis when applying the stored shape to this shape key",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
block_name_update: BoolProperty(
|
||||||
|
description="Flag to help keep shape key names in sync", default=False
|
||||||
|
)
|
||||||
|
shape_key_name: StringProperty(
|
||||||
|
name="Shape Key",
|
||||||
|
description="Name of the shape key to push data to",
|
||||||
|
update=update_shape_key_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_block(self) -> list[ShapeKey]:
|
||||||
|
mesh = self.id_data
|
||||||
|
if not mesh.shape_keys:
|
||||||
|
return
|
||||||
|
return mesh.shape_keys.key_blocks.get(self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class PoseShapeKey(PropertyGroup):
|
||||||
|
target_shapes: CollectionProperty(type=PoseShapeKeyTarget)
|
||||||
|
|
||||||
|
def update_active_sk_index(self, context):
|
||||||
|
obj = context.object
|
||||||
|
if not obj.data.shape_keys:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sk_name = self.target_shapes[self.active_target_shape_index].shape_key_name
|
||||||
|
except IndexError:
|
||||||
|
obj.active_shape_key_index = len(obj.data.shape_keys.key_blocks) - 1
|
||||||
|
return
|
||||||
|
key_block_idx = obj.data.shape_keys.key_blocks.find(sk_name)
|
||||||
|
if key_block_idx > -1:
|
||||||
|
obj.active_shape_key_index = key_block_idx
|
||||||
|
|
||||||
|
# If in weight paint mode and there is a mask vertex group,
|
||||||
|
# also set that vertex group as active.
|
||||||
|
if context.mode == 'PAINT_WEIGHT':
|
||||||
|
key_block = obj.data.shape_keys.key_blocks[key_block_idx]
|
||||||
|
vg_idx = obj.vertex_groups.find(key_block.vertex_group)
|
||||||
|
if vg_idx > -1:
|
||||||
|
obj.vertex_groups.active_index = vg_idx
|
||||||
|
|
||||||
|
active_target_shape_index: IntProperty(update=update_active_sk_index)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_target(self):
|
||||||
|
return self.target_shapes[self.active_target_shape_index]
|
||||||
|
|
||||||
|
def update_name(self, context):
|
||||||
|
if self.name == "":
|
||||||
|
self.name = "Pose Key"
|
||||||
|
|
||||||
|
name: StringProperty(name="Name", update=update_name)
|
||||||
|
|
||||||
|
action: PointerProperty(
|
||||||
|
name="Action",
|
||||||
|
type=Action,
|
||||||
|
description="Action that contains the frame that should be used when applying the stored shape as a shape key",
|
||||||
|
)
|
||||||
|
frame: IntProperty(
|
||||||
|
name="Frame",
|
||||||
|
description="Frame that should be used within the selected action when applying the stored shape as a shape key",
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
storage_object: PointerProperty(
|
||||||
|
type=Object,
|
||||||
|
name="Storage Object",
|
||||||
|
description="Specify an object that stores the vertex position data",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
registry = [
|
||||||
|
PoseShapeKeyTarget,
|
||||||
|
PoseShapeKey,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def update_posekey_index(self, context):
|
||||||
|
# Want to piggyback on update_active_sk_index() to also update the active
|
||||||
|
# shape key index when switching pose keys.
|
||||||
|
mesh = context.object.data
|
||||||
|
if mesh.pose_keys:
|
||||||
|
pk = mesh.pose_keys[mesh.active_pose_key_index]
|
||||||
|
# We just want to fire the update func.
|
||||||
|
pk.active_target_shape_index = pk.active_target_shape_index
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
bpy.types.Mesh.pose_keys = CollectionProperty(type=PoseShapeKey)
|
||||||
|
bpy.types.Mesh.active_pose_key_index = IntProperty(update=update_posekey_index)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
del bpy.types.Mesh.pose_keys
|
||||||
|
del bpy.types.Mesh.active_pose_key_index
|
@ -1,76 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from bpy.props import BoolProperty
|
|
||||||
from .pose_key import get_deforming_armature
|
|
||||||
|
|
||||||
|
|
||||||
class CK_OT_reset_rig(bpy.types.Operator):
|
|
||||||
"""Reset all bone transforms and custom properties to their default values"""
|
|
||||||
|
|
||||||
bl_idname = "object.posekey_reset_rig"
|
|
||||||
bl_label = "Reset Rig"
|
|
||||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
||||||
|
|
||||||
reset_transforms: BoolProperty(
|
|
||||||
name="Transforms", default=True, description="Reset bone transforms"
|
|
||||||
)
|
|
||||||
reset_props: BoolProperty(
|
|
||||||
name="Properties", default=True, description="Reset custom properties"
|
|
||||||
)
|
|
||||||
selection_only: BoolProperty(
|
|
||||||
name="Selected Only",
|
|
||||||
default=False,
|
|
||||||
description="Affect selected bones rather than all bones",
|
|
||||||
)
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
wm = context.window_manager
|
|
||||||
return wm.invoke_props_dialog(self)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
rigged_ob = context.object
|
|
||||||
rig = get_deforming_armature(rigged_ob)
|
|
||||||
bones = rig.pose.bones
|
|
||||||
if self.selection_only:
|
|
||||||
bones = context.selected_pose_bones
|
|
||||||
for pb in bones:
|
|
||||||
if self.reset_transforms:
|
|
||||||
pb.location = (0, 0, 0)
|
|
||||||
pb.rotation_euler = (0, 0, 0)
|
|
||||||
pb.rotation_quaternion = (1, 0, 0, 0)
|
|
||||||
pb.scale = (1, 1, 1)
|
|
||||||
|
|
||||||
if self.reset_props and len(pb.keys()) > 0:
|
|
||||||
rna_properties = [
|
|
||||||
prop.identifier for prop in pb.bl_rna.properties if prop.is_runtime
|
|
||||||
]
|
|
||||||
|
|
||||||
# Reset custom property values to their default value
|
|
||||||
for key in pb.keys():
|
|
||||||
if key.startswith("$"):
|
|
||||||
continue
|
|
||||||
if key in rna_properties:
|
|
||||||
continue # Addon defined property.
|
|
||||||
|
|
||||||
ui_data = None
|
|
||||||
try:
|
|
||||||
ui_data = pb.id_properties_ui(key)
|
|
||||||
if not ui_data:
|
|
||||||
continue
|
|
||||||
ui_data = ui_data.as_dict()
|
|
||||||
if not 'default' in ui_data:
|
|
||||||
continue
|
|
||||||
except TypeError:
|
|
||||||
# Some properties don't support UI data, and so don't have a default value. (like addon PropertyGroups)
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not ui_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if type(pb[key]) not in (float, int):
|
|
||||||
continue
|
|
||||||
pb[key] = ui_data['default']
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
registry = [CK_OT_reset_rig]
|
|
@ -1,7 +1,4 @@
|
|||||||
# This script expects a mesh whose base shape is symmetrical, and symmetrize the
|
from bpy.types import Operator
|
||||||
# active shape key based on the symmetry of the base mesh.
|
|
||||||
|
|
||||||
from typing import List, Tuple
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import BoolProperty, EnumProperty, FloatProperty
|
from bpy.props import BoolProperty, EnumProperty, FloatProperty
|
||||||
from mathutils.kdtree import KDTree
|
from mathutils.kdtree import KDTree
|
||||||
@ -9,12 +6,12 @@ from mathutils.kdtree import KDTree
|
|||||||
|
|
||||||
def mirror_mesh(
|
def mirror_mesh(
|
||||||
*,
|
*,
|
||||||
reference_verts: List,
|
reference_verts: list,
|
||||||
vertices: List,
|
vertices: list,
|
||||||
axis: str,
|
axis: str,
|
||||||
symmetrize=False,
|
symmetrize=False,
|
||||||
symmetrize_pos_to_neg=True,
|
symmetrize_pos_to_neg=True,
|
||||||
) -> Tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Symmetrize vertices around any axis in any direction based on a set of
|
Symmetrize vertices around any axis in any direction based on a set of
|
||||||
reference vertices which share the same vertex order and are known to be
|
reference vertices which share the same vertex order and are known to be
|
||||||
@ -95,8 +92,10 @@ def mirror_mesh(
|
|||||||
return good_counter, bad_counter
|
return good_counter, bad_counter
|
||||||
|
|
||||||
|
|
||||||
class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
class OBJECT_OT_symmetrize_shape_key(Operator):
|
||||||
"""Symmetrize shape key by matching vertex pairs by proximity in the original mesh"""
|
"""Symmetrize shape key by matching vertex pairs by proximity in the original mesh"""
|
||||||
|
# NOTE: This script expects a mesh whose base shape is symmetrical, and symmetrize the
|
||||||
|
# active shape key based on the symmetry of the base mesh.
|
||||||
|
|
||||||
bl_idname = "object.symmetrize_shape_key"
|
bl_idname = "object.symmetrize_shape_key"
|
||||||
bl_label = "Symmetrize Shape Key"
|
bl_label = "Symmetrize Shape Key"
|
||||||
@ -133,8 +132,8 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
|||||||
layout.prop(self, 'threshold', slider=True)
|
layout.prop(self, 'threshold', slider=True)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
ob = context.object
|
obj = context.object
|
||||||
mesh = ob.data
|
mesh = obj.data
|
||||||
|
|
||||||
if 'X' in self.direction:
|
if 'X' in self.direction:
|
||||||
axis = 'X'
|
axis = 'X'
|
||||||
@ -145,10 +144,10 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
|||||||
|
|
||||||
pos_to_neg = not self.direction.startswith('NEG')
|
pos_to_neg = not self.direction.startswith('NEG')
|
||||||
|
|
||||||
key_blocks = [ob.active_shape_key]
|
key_blocks = [obj.active_shape_key]
|
||||||
if self.all_keys:
|
if self.all_keys:
|
||||||
# TODO: This could be more optimized, right now we re-build the kdtree for each key block unneccessarily.
|
# TODO: This could be more optimized, right now we re-build the kdtree for each key block unneccessarily.
|
||||||
key_blocks = ob.data.shape_keys.key_blocks[:]
|
key_blocks = obj.data.shape_keys.key_blocks[:]
|
||||||
|
|
||||||
for kb in key_blocks:
|
for kb in key_blocks:
|
||||||
good_counter, bad_counter = mirror_mesh(
|
good_counter, bad_counter = mirror_mesh(
|
||||||
@ -175,12 +174,12 @@ class OBJECT_OT_Symmetrize_Shape_Key(bpy.types.Operator):
|
|||||||
def draw_symmetrize_buttons(self, context):
|
def draw_symmetrize_buttons(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.separator()
|
layout.separator()
|
||||||
op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize Active")
|
op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize Active")
|
||||||
op = layout.operator(OBJECT_OT_Symmetrize_Shape_Key.bl_idname, text="Symmetrize All")
|
op = layout.operator(OBJECT_OT_symmetrize_shape_key.bl_idname, text="Symmetrize All")
|
||||||
op.all_keys = True
|
op.all_keys = True
|
||||||
|
|
||||||
|
|
||||||
registry = [OBJECT_OT_Symmetrize_Shape_Key]
|
registry = [OBJECT_OT_symmetrize_shape_key]
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
@ -1,15 +1,94 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Object, Panel, UIList, Menu
|
from bpy.types import Panel, UIList, Menu
|
||||||
from .ui_list import draw_ui_list
|
|
||||||
from bpy.props import EnumProperty
|
|
||||||
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
|
from bl_ui.properties_data_mesh import DATA_PT_shape_keys
|
||||||
|
from bpy.props import EnumProperty
|
||||||
|
|
||||||
|
from .ui_list import draw_ui_list
|
||||||
|
from .ops import get_deforming_armature, poll_correct_pose_key_pose
|
||||||
|
from .prefs import get_addon_prefs
|
||||||
|
|
||||||
|
|
||||||
def get_addon_prefs(context):
|
class MESH_PT_pose_keys(Panel):
|
||||||
return context.preferences.addons[__package__].preferences
|
bl_space_type = 'PROPERTIES'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_context = 'data'
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
bl_label = "Pose Shape Keys"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return context.object and context.object.type == 'MESH'
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
obj = context.object
|
||||||
|
mesh = obj.data
|
||||||
|
layout = self.layout.column()
|
||||||
|
|
||||||
|
layout.row().prop(mesh, 'shape_key_ui_type', expand=True)
|
||||||
|
|
||||||
|
if mesh.shape_key_ui_type == 'DEFAULT':
|
||||||
|
return DATA_PT_shape_keys.draw(self, context)
|
||||||
|
|
||||||
|
arm_ob = get_deforming_armature(obj)
|
||||||
|
if not arm_ob:
|
||||||
|
layout.alert = True
|
||||||
|
layout.label(text="Object must be deformed by an Armature to use Pose Keys.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if mesh.shape_keys and not mesh.shape_keys.use_relative:
|
||||||
|
layout.alert = True
|
||||||
|
layout.label("Relative Shape Keys must be enabled!")
|
||||||
|
return
|
||||||
|
|
||||||
|
list_row = layout.row()
|
||||||
|
|
||||||
|
groups_col = list_row.column()
|
||||||
|
draw_ui_list(
|
||||||
|
groups_col,
|
||||||
|
context,
|
||||||
|
class_name='POSEKEYS_UL_pose_keys',
|
||||||
|
list_context_path='object.data.pose_keys',
|
||||||
|
active_idx_context_path='object.data.active_pose_key_index',
|
||||||
|
menu_class_name='MESH_MT_pose_key_utils',
|
||||||
|
add_op_name='object.posekey_add',
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
if len(mesh.pose_keys) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = context.object.data.active_pose_key_index
|
||||||
|
active_posekey = context.object.data.pose_keys[idx]
|
||||||
|
|
||||||
|
action_split = layout.row().split(factor=0.4, align=True)
|
||||||
|
action_split.alignment = 'RIGHT'
|
||||||
|
action_split.label(text="Action")
|
||||||
|
row = action_split.row(align=True)
|
||||||
|
icon = 'FORWARD'
|
||||||
|
if active_posekey.action:
|
||||||
|
icon = 'FILE_REFRESH'
|
||||||
|
row.operator('object.posekey_auto_init', text="", icon=icon)
|
||||||
|
row.prop(active_posekey, 'action', text="")
|
||||||
|
layout.prop(active_posekey, 'frame')
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
row = layout.row(align=True)
|
||||||
|
text = "Save Posed Mesh"
|
||||||
|
if active_posekey.storage_object:
|
||||||
|
text = "Overwrite Posed Mesh"
|
||||||
|
row.operator('object.posekey_save', text=text, icon="FILE_TICK")
|
||||||
|
row.prop(active_posekey, 'storage_object', text="")
|
||||||
|
row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF')
|
||||||
|
|
||||||
|
|
||||||
class CK_UL_pose_keys(UIList):
|
class POSEKEYS_UL_pose_keys(UIList):
|
||||||
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
||||||
pose_key = item
|
pose_key = item
|
||||||
|
|
||||||
@ -21,13 +100,101 @@ class CK_UL_pose_keys(UIList):
|
|||||||
|
|
||||||
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
|
icon = 'SURFACE_NCIRCLE' if pose_key.storage_object else 'CURVE_NCIRCLE'
|
||||||
name_row = split.row()
|
name_row = split.row()
|
||||||
|
if not pose_key.name:
|
||||||
|
name_row.alert = True
|
||||||
|
split = name_row.split()
|
||||||
|
name_row = split.row()
|
||||||
|
split.label(text="Unnamed!", icon='ERROR')
|
||||||
name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon)
|
name_row.prop(pose_key, 'name', text="", emboss=False, icon=icon)
|
||||||
|
|
||||||
|
|
||||||
class CK_UL_target_keys(UIList):
|
class MESH_MT_pose_key_utils(Menu):
|
||||||
|
bl_label = "Pose Key Utilities"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME')
|
||||||
|
layout.operator('object.posekey_push_all', icon='WORLD')
|
||||||
|
layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES')
|
||||||
|
layout.operator('object.posekey_copy_data', icon='PASTEDOWN')
|
||||||
|
|
||||||
|
|
||||||
|
class MESH_PT_shape_key_subpanel(Panel):
|
||||||
|
bl_space_type = 'PROPERTIES'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_context = 'data'
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
bl_label = "Shape Key Slots"
|
||||||
|
bl_parent_id = "MESH_PT_pose_keys"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
obj = context.object
|
||||||
|
if not (obj and obj.data and obj.data.shape_key_ui_type=='POSE_KEYS'):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return poll_correct_pose_key_pose(cls, context, demand_pose=False)
|
||||||
|
except AttributeError:
|
||||||
|
# Happens any time that function tries to set a poll message,
|
||||||
|
# since panels don't have poll messages, lol.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
obj = context.object
|
||||||
|
mesh = obj.data
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
|
idx = context.object.data.active_pose_key_index
|
||||||
|
active_posekey = context.object.data.pose_keys[idx]
|
||||||
|
|
||||||
|
layout.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT")
|
||||||
|
|
||||||
|
draw_ui_list(
|
||||||
|
layout,
|
||||||
|
context,
|
||||||
|
class_name='POSEKEYS_UL_target_shape_keys',
|
||||||
|
list_context_path=f'object.data.pose_keys[{idx}].target_shapes',
|
||||||
|
active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index',
|
||||||
|
add_op_name='object.posekey_shape_add',
|
||||||
|
remove_op_name='object.posekey_shape_remove',
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(active_posekey.target_shapes) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
active_target = active_posekey.active_target
|
||||||
|
row = layout.row()
|
||||||
|
if not mesh.shape_keys:
|
||||||
|
return
|
||||||
|
row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks')
|
||||||
|
if not active_target.key_block:
|
||||||
|
add_shape_op = row.operator('object.posekey_shape_add', icon='ADD', text="")
|
||||||
|
add_shape_op.create_slot=False
|
||||||
|
sk = active_target.key_block
|
||||||
|
if not sk:
|
||||||
|
return
|
||||||
|
addon_prefs = get_addon_prefs(context)
|
||||||
|
icon = 'HIDE_OFF' if addon_prefs.show_shape_key_info else 'HIDE_ON'
|
||||||
|
row.prop(addon_prefs, 'show_shape_key_info', text="", icon=icon)
|
||||||
|
if addon_prefs.show_shape_key_info:
|
||||||
|
layout.prop(active_target, 'mirror_x')
|
||||||
|
split = layout.split(factor=0.1)
|
||||||
|
split.row()
|
||||||
|
col = split.column()
|
||||||
|
col.row().prop(sk, 'value')
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(sk, 'slider_min', text="Range")
|
||||||
|
row.prop(sk, 'slider_max', text="")
|
||||||
|
col.prop_search(sk, "vertex_group", obj, "vertex_groups", text="Vertex Mask")
|
||||||
|
col.row().prop(sk, 'relative_key')
|
||||||
|
|
||||||
|
|
||||||
|
class POSEKEYS_UL_target_shape_keys(UIList):
|
||||||
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
def draw_item(self, context, layout, data, item, _icon, _active_data, _active_propname):
|
||||||
obj = context.object
|
obj = context.object
|
||||||
pose_key = data # I think?
|
|
||||||
pose_key_target = item
|
pose_key_target = item
|
||||||
key_block = pose_key_target.key_block
|
key_block = pose_key_target.key_block
|
||||||
|
|
||||||
@ -51,6 +218,7 @@ class CK_UL_target_keys(UIList):
|
|||||||
):
|
):
|
||||||
name_row.active = value_row.active = False
|
name_row.active = value_row.active = False
|
||||||
|
|
||||||
|
value_row.operator('object.posekey_magic_driver', text="", icon='DECORATE_DRIVER').key_name = key_block.name
|
||||||
value_row.prop(key_block, "value", text="")
|
value_row.prop(key_block, "value", text="")
|
||||||
|
|
||||||
mute_row = split.row()
|
mute_row = split.row()
|
||||||
@ -58,169 +226,6 @@ class CK_UL_target_keys(UIList):
|
|||||||
mute_row.prop(key_block, 'mute', emboss=False, text="")
|
mute_row.prop(key_block, 'mute', emboss=False, text="")
|
||||||
|
|
||||||
|
|
||||||
def ob_has_armature_mod(ob: Object) -> bool:
|
|
||||||
for m in ob.modifiers:
|
|
||||||
if m.type == 'ARMATURE':
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MESH_PT_pose_keys(Panel):
|
|
||||||
bl_space_type = 'PROPERTIES'
|
|
||||||
bl_region_type = 'WINDOW'
|
|
||||||
bl_context = 'data'
|
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
|
||||||
bl_label = "Shape/Pose Keys"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return context.object and context.object.type == 'MESH'
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
ob = context.object
|
|
||||||
mesh = ob.data
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.prop(mesh, 'shape_key_ui_type', text="List Type: ", expand=True)
|
|
||||||
|
|
||||||
if mesh.shape_key_ui_type == 'DEFAULT':
|
|
||||||
return DATA_PT_shape_keys.draw(self, context)
|
|
||||||
|
|
||||||
if not ob_has_armature_mod(ob):
|
|
||||||
layout.alert = True
|
|
||||||
layout.label(text="Object must have an Armature modifier to use Pose Keys.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if mesh.shape_keys and not mesh.shape_keys.use_relative:
|
|
||||||
layout.alert = True
|
|
||||||
layout.label("Relative Shape Keys must be enabled!")
|
|
||||||
return
|
|
||||||
|
|
||||||
list_row = layout.row()
|
|
||||||
|
|
||||||
groups_col = list_row.column()
|
|
||||||
draw_ui_list(
|
|
||||||
groups_col,
|
|
||||||
context,
|
|
||||||
class_name='CK_UL_pose_keys',
|
|
||||||
list_context_path='object.data.pose_keys',
|
|
||||||
active_idx_context_path='object.data.active_pose_key_index',
|
|
||||||
menu_class_name='MESH_MT_pose_key_utils',
|
|
||||||
)
|
|
||||||
|
|
||||||
layout.use_property_split = True
|
|
||||||
layout.use_property_decorate = False
|
|
||||||
|
|
||||||
if len(mesh.pose_keys) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
idx = context.object.data.active_pose_key_index
|
|
||||||
active_posekey = context.object.data.pose_keys[idx]
|
|
||||||
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.prop(active_posekey, 'action')
|
|
||||||
if active_posekey.action:
|
|
||||||
col.prop(active_posekey, 'frame')
|
|
||||||
|
|
||||||
if active_posekey.storage_object:
|
|
||||||
row = layout.row()
|
|
||||||
row.prop(active_posekey, 'storage_object')
|
|
||||||
row.operator('object.posekey_jump_to_storage', text="", icon='RESTRICT_SELECT_OFF')
|
|
||||||
else:
|
|
||||||
layout.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
|
||||||
row = layout.row()
|
|
||||||
row.operator('object.posekey_save', text="Store Evaluated Mesh", icon="FILE_TICK")
|
|
||||||
row.prop(active_posekey, 'storage_object', text="")
|
|
||||||
return
|
|
||||||
|
|
||||||
layout.separator()
|
|
||||||
col = layout.column(align=True)
|
|
||||||
col.operator('object.posekey_set_pose', text="Set Pose", icon="ARMATURE_DATA")
|
|
||||||
col.separator()
|
|
||||||
|
|
||||||
row = col.row()
|
|
||||||
row.operator('object.posekey_save', text="Overwrite Storage Object", icon="FILE_TICK")
|
|
||||||
row.operator('object.posekey_push', text="Overwrite Shape Keys", icon="IMPORT")
|
|
||||||
|
|
||||||
|
|
||||||
class MESH_PT_shape_key_subpanel(Panel):
|
|
||||||
bl_space_type = 'PROPERTIES'
|
|
||||||
bl_region_type = 'WINDOW'
|
|
||||||
bl_context = 'data'
|
|
||||||
bl_options = {'DEFAULT_CLOSED'}
|
|
||||||
bl_label = "Shape Key Slots"
|
|
||||||
bl_parent_id = "MESH_PT_pose_keys"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
ob = context.object
|
|
||||||
return (
|
|
||||||
ob.data.shape_key_ui_type == 'POSE_KEYS'
|
|
||||||
and len(ob.data.pose_keys) > 0
|
|
||||||
and ob.data.pose_keys[ob.data.active_pose_key_index].storage_object
|
|
||||||
and ob_has_armature_mod(ob)
|
|
||||||
)
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
ob = context.object
|
|
||||||
mesh = ob.data
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.use_property_split = True
|
|
||||||
layout.use_property_decorate = False
|
|
||||||
|
|
||||||
idx = context.object.data.active_pose_key_index
|
|
||||||
active_posekey = context.object.data.pose_keys[idx]
|
|
||||||
|
|
||||||
draw_ui_list(
|
|
||||||
layout,
|
|
||||||
context,
|
|
||||||
class_name='CK_UL_target_keys',
|
|
||||||
list_context_path=f'object.data.pose_keys[{idx}].target_shapes',
|
|
||||||
active_idx_context_path=f'object.data.pose_keys[{idx}].active_target_shape_index',
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(active_posekey.target_shapes) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
active_target = active_posekey.target_shapes[active_posekey.active_target_shape_index]
|
|
||||||
row = layout.row()
|
|
||||||
if not mesh.shape_keys:
|
|
||||||
row.operator('object.create_shape_key_for_pose', icon='ADD')
|
|
||||||
return
|
|
||||||
row.prop_search(active_target, 'shape_key_name', mesh.shape_keys, 'key_blocks')
|
|
||||||
if not active_target.name:
|
|
||||||
row.operator('object.create_shape_key_for_pose', icon='ADD', text="")
|
|
||||||
sk = active_target.key_block
|
|
||||||
if not sk:
|
|
||||||
return
|
|
||||||
addon_prefs = get_addon_prefs(context)
|
|
||||||
icon = 'HIDE_OFF' if addon_prefs.show_shape_key_info else 'HIDE_ON'
|
|
||||||
row.prop(addon_prefs, 'show_shape_key_info', text="", icon=icon)
|
|
||||||
if addon_prefs.show_shape_key_info:
|
|
||||||
layout.prop(active_target, 'mirror_x')
|
|
||||||
split = layout.split(factor=0.1)
|
|
||||||
split.row()
|
|
||||||
col = split.column()
|
|
||||||
col.row().prop(sk, 'value')
|
|
||||||
row = col.row(align=True)
|
|
||||||
row.prop(sk, 'slider_min', text="Range")
|
|
||||||
row.prop(sk, 'slider_max', text="")
|
|
||||||
col.prop_search(sk, "vertex_group", ob, "vertex_groups", text="Vertex Mask")
|
|
||||||
col.row().prop(sk, 'relative_key')
|
|
||||||
|
|
||||||
|
|
||||||
class MESH_MT_pose_key_utils(Menu):
|
|
||||||
bl_label = "Pose Key Utilities"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.operator('object.posekey_object_grid', icon='LIGHTPROBE_VOLUME')
|
|
||||||
layout.operator('object.posekey_push_all', icon='WORLD')
|
|
||||||
layout.operator('object.posekey_clamp_influence', icon='NORMALIZE_FCURVES')
|
|
||||||
layout.operator('object.posekey_copy_data', icon='PASTEDOWN')
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def shape_key_panel_new_poll(cls, context):
|
def shape_key_panel_new_poll(cls, context):
|
||||||
engine = context.engine
|
engine = context.engine
|
||||||
@ -229,8 +234,8 @@ def shape_key_panel_new_poll(cls, context):
|
|||||||
|
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
CK_UL_pose_keys,
|
POSEKEYS_UL_pose_keys,
|
||||||
CK_UL_target_keys,
|
POSEKEYS_UL_target_shape_keys,
|
||||||
MESH_PT_pose_keys,
|
MESH_PT_pose_keys,
|
||||||
MESH_PT_shape_key_subpanel,
|
MESH_PT_shape_key_subpanel,
|
||||||
MESH_MT_pose_key_utils,
|
MESH_MT_pose_key_utils,
|
||||||
@ -244,7 +249,7 @@ def register():
|
|||||||
('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"),
|
('DEFAULT', 'Shape Keys', "Show a flat list of shape keys"),
|
||||||
(
|
(
|
||||||
'POSE_KEYS',
|
'POSE_KEYS',
|
||||||
'Pose Keys',
|
'Pose Shape Keys',
|
||||||
"Organize shape keys into a higher-level concept called Pose Keys. These can store vertex positions and push one shape to multiple shape keys at once, relative to existing deformation",
|
"Organize shape keys into a higher-level concept called Pose Keys. These can store vertex positions and push one shape to multiple shape keys at once, relative to existing deformation",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -118,6 +118,10 @@ def draw_ui_list(
|
|||||||
list_context_path='object.data.vertex_groups',
|
list_context_path='object.data.vertex_groups',
|
||||||
active_idx_context_path='object.data.vertex_groups.active_index',
|
active_idx_context_path='object.data.vertex_groups.active_index',
|
||||||
insertion_operators=True,
|
insertion_operators=True,
|
||||||
|
add_op_name=None,
|
||||||
|
add_kwargs={},
|
||||||
|
remove_op_name=None,
|
||||||
|
remove_kwargs={},
|
||||||
move_operators=True,
|
move_operators=True,
|
||||||
menu_class_name='',
|
menu_class_name='',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -148,13 +152,19 @@ def draw_ui_list(
|
|||||||
|
|
||||||
col = row.column()
|
col = row.column()
|
||||||
if insertion_operators:
|
if insertion_operators:
|
||||||
add_op = col.operator('ui.list_entry_add', text="", icon='ADD')
|
op_name = add_op_name or 'ui.list_entry_add'
|
||||||
|
add_op = col.operator(op_name, text="", icon='ADD')
|
||||||
|
for key, value in add_kwargs.items():
|
||||||
|
setattr(add_op, key, value)
|
||||||
add_op.list_context_path = list_context_path
|
add_op.list_context_path = list_context_path
|
||||||
add_op.active_idx_context_path = active_idx_context_path
|
add_op.active_idx_context_path = active_idx_context_path
|
||||||
|
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.enabled = len(my_list) > 0
|
row.enabled = len(my_list) > 0
|
||||||
remove_op = row.operator('ui.list_entry_remove', text="", icon='REMOVE')
|
op_name = remove_op_name or 'ui.list_entry_remove'
|
||||||
|
remove_op = row.operator(op_name, text="", icon='REMOVE')
|
||||||
|
for key, value in remove_kwargs.items():
|
||||||
|
setattr(remove_op, key, value)
|
||||||
remove_op.list_context_path = list_context_path
|
remove_op.list_context_path = list_context_path
|
||||||
remove_op.active_idx_context_path = active_idx_context_path
|
remove_op.active_idx_context_path = active_idx_context_path
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user