├── omtk ├── ui │ ├── __init__.py │ ├── widget_list_meshes.ui │ ├── widget_list_modules.ui │ ├── widget_list_influences.ui │ ├── widget_component_wizard_parts.ui │ ├── widget_list_modules.py │ ├── preferences_window.py │ ├── widget_list_meshes.py │ ├── widget_list_influences.py │ ├── preferences_window.ui │ ├── widget_component_wizard_parts.py │ ├── widget_logger.ui │ ├── pluginmanager_window.ui │ ├── pluginmanager_window.py │ ├── widget_create_component.ui │ ├── widget_logger.py │ └── widget_create_component.py ├── vendor │ ├── __init__.py │ └── libSerialization │ │ ├── .gitignore │ │ ├── __init__.py │ │ ├── plugin_yaml.py │ │ ├── plugin_json.py │ │ ├── decorators.py │ │ ├── cache.py │ │ └── plugin_maya_json.py ├── patches │ ├── __init__.py │ ├── patch_0.2.0_missing_rig_attr.py │ ├── patch_0.3.0_convert_facelids_to_faceeyelids.py │ ├── patch_0_4_7_add_leg_auto_roll_threshold.py │ ├── patch_0_4_32_support_multiple_root_ctrls.py │ └── patch_0.2.0_missing_spaceswitch_targets.py ├── modules │ ├── __init__.py │ ├── rigHead.py │ ├── template │ ├── rigFaceSquint.py │ ├── rigFaceBrow.py │ ├── rigFaceNose.py │ ├── rigNeck.py │ ├── rigArm.py │ ├── rigSplineIK.py │ ├── rigFaceEyeLids.py │ ├── rigFaceJaw.py │ ├── rigFK.py │ └── rigFKAdditive.py ├── rigs │ └── __init__.py ├── animation │ ├── __init__.py │ └── ikfkTools.py ├── core │ ├── macros.py │ ├── __init__.py │ ├── utils.py │ ├── preferences.py │ ├── classModuleMap.py │ └── classModuleCompound.py ├── macros │ ├── __init__.py │ ├── modules_build_selected.py │ ├── modules_unbuild_selected.py │ ├── ctrls_calibrate_selected.py │ ├── launch_create_component_wizard.py │ ├── ctrls_mirror_selected.py │ ├── ctrls_match_selected.py │ ├── ctrls_interactive_swap_mesh.py │ ├── ctrls_bake_selected.py │ └── avar_selection_toggle.py ├── libs │ ├── __init__.py │ ├── libStringMap.py │ ├── libQt.py │ ├── libDagTraversal.py │ ├── libUtils.py │ ├── libHistory.py │ ├── libSkeleton.py │ └── libCtrlMatch.py ├── models │ ├── __init__.py │ ├── model_avar_linear.py │ └── model_avar_base.py ├── __init__.py ├── constants.py ├── preferences_window.py ├── ui_shared.py ├── widget_create_component.py ├── widget_list_meshes.py ├── pluginmanager_window.py ├── widget_list_influences.py └── widget_create_component_wizard_parts.py ├── .gitattributes ├── images ├── omtk_rig_show.png ├── omtk_rig_show.xcf ├── reload_scene.png ├── reload_scene.xcf ├── omtk_anm_switch_fk.png ├── omtk_anm_switch_fk.xcf ├── omtk_anm_switch_ik.png ├── omtk_anm_switch_ik.xcf ├── omtk_rig_build_all.png ├── omtk_rig_build_all.xcf ├── omtk_rig_ctrl_bake.png ├── omtk_rig_ctrl_bake.xcf ├── omtk_rig_ctrl_swap.png ├── omtk_rig_ctrl_swap.xcf ├── omtk_anm_ctrl_mirror.png ├── omtk_anm_ctrl_mirror.xcf ├── omtk_rig_ctrl_mirror.png ├── omtk_rig_ctrl_mirror.xcf ├── omtk_rig_rebuild_all.png ├── omtk_rig_rebuild_all.xcf ├── omtk_rig_select_avar.png ├── omtk_rig_select_avar.xcf ├── omtk_rig_unbuild_all.png ├── omtk_rig_unbuild_all.xcf ├── omtk_rig_ctrl_calibrate.png ├── omtk_rig_ctrl_calibrate.xcf ├── omtk_rig_build_selection.png ├── omtk_rig_build_selection.xcf ├── omtk_rig_ictrl_swap_mesh.png ├── omtk_rig_ictrl_swap_mesh.xcf ├── omtk_rig_rebuild_selection.png ├── omtk_rig_rebuild_selection.xcf ├── omtk_rig_unbuild_selection.png └── omtk_rig_unbuild_selection.xcf ├── tests ├── run.sh ├── test_simple.py ├── run.py ├── doctest_rigSqueeze.txt ├── test_nomenclature.py ├── test_nomenclature_squeeze.py ├── test_libFormula.py └── test_animtools.py ├── package.py ├── .gitignore ├── test.sh ├── LICENSE ├── README.md └── shelf_omtk_anim.mel /omtk/ui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /omtk/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /omtk/patches/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ma -diff 2 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | *.pyc 3 | -------------------------------------------------------------------------------- /images/omtk_rig_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_show.png -------------------------------------------------------------------------------- /images/omtk_rig_show.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_show.xcf -------------------------------------------------------------------------------- /images/reload_scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/reload_scene.png -------------------------------------------------------------------------------- /images/reload_scene.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/reload_scene.xcf -------------------------------------------------------------------------------- /omtk/modules/__init__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | __path__ = pkgutil.extend_path(__path__, __name__) 3 | -------------------------------------------------------------------------------- /omtk/rigs/__init__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | __path__ = pkgutil.extend_path(__path__, __name__) 3 | -------------------------------------------------------------------------------- /images/omtk_anm_switch_fk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_switch_fk.png -------------------------------------------------------------------------------- /images/omtk_anm_switch_fk.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_switch_fk.xcf -------------------------------------------------------------------------------- /images/omtk_anm_switch_ik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_switch_ik.png -------------------------------------------------------------------------------- /images/omtk_anm_switch_ik.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_switch_ik.xcf -------------------------------------------------------------------------------- /images/omtk_rig_build_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_build_all.png -------------------------------------------------------------------------------- /images/omtk_rig_build_all.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_build_all.xcf -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_bake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_bake.png -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_bake.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_bake.xcf -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_swap.png -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_swap.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_swap.xcf -------------------------------------------------------------------------------- /images/omtk_anm_ctrl_mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_ctrl_mirror.png -------------------------------------------------------------------------------- /images/omtk_anm_ctrl_mirror.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_anm_ctrl_mirror.xcf -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_mirror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_mirror.png -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_mirror.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_mirror.xcf -------------------------------------------------------------------------------- /images/omtk_rig_rebuild_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_rebuild_all.png -------------------------------------------------------------------------------- /images/omtk_rig_rebuild_all.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_rebuild_all.xcf -------------------------------------------------------------------------------- /images/omtk_rig_select_avar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_select_avar.png -------------------------------------------------------------------------------- /images/omtk_rig_select_avar.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_select_avar.xcf -------------------------------------------------------------------------------- /images/omtk_rig_unbuild_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_unbuild_all.png -------------------------------------------------------------------------------- /images/omtk_rig_unbuild_all.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_unbuild_all.xcf -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_calibrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_calibrate.png -------------------------------------------------------------------------------- /images/omtk_rig_ctrl_calibrate.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ctrl_calibrate.xcf -------------------------------------------------------------------------------- /images/omtk_rig_build_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_build_selection.png -------------------------------------------------------------------------------- /images/omtk_rig_build_selection.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_build_selection.xcf -------------------------------------------------------------------------------- /images/omtk_rig_ictrl_swap_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ictrl_swap_mesh.png -------------------------------------------------------------------------------- /images/omtk_rig_ictrl_swap_mesh.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_ictrl_swap_mesh.xcf -------------------------------------------------------------------------------- /images/omtk_rig_rebuild_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_rebuild_selection.png -------------------------------------------------------------------------------- /images/omtk_rig_rebuild_selection.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_rebuild_selection.xcf -------------------------------------------------------------------------------- /images/omtk_rig_unbuild_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_unbuild_selection.png -------------------------------------------------------------------------------- /images/omtk_rig_unbuild_selection.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudll/omtk/HEAD/images/omtk_rig_unbuild_selection.xcf -------------------------------------------------------------------------------- /omtk/animation/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | if re.match('maya.*', sys.executable, re.IGNORECASE): 5 | import ikfkTools 6 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find . -name "*.pyc" -delete 3 | cd `dirname $0` # ensure 'tests' is the current directory 4 | /usr/autodesk/maya2016/bin/mayapy run.py $* -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pymel.core as pymel 3 | 4 | class TestCase(unittest.TestCase): 5 | def test_simple(self): 6 | pymel.createNode('transform') 7 | -------------------------------------------------------------------------------- /omtk/core/macros.py: -------------------------------------------------------------------------------- 1 | """ 2 | A macro is a simple action that can be in a shelf or in a keyboard shortcut. Nothing fancy. 3 | """ 4 | 5 | 6 | class BaseMacro(object): 7 | def run(self): 8 | raise NotImplementedError 9 | -------------------------------------------------------------------------------- /omtk/macros/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contain various tools that can be used to facilitate the rigging workflow with omtk. 3 | Previously they were in omtk.core.api, however the increasing number of macros necessitate a dedicated directory. 4 | """ 5 | 6 | -------------------------------------------------------------------------------- /package.py: -------------------------------------------------------------------------------- 1 | name = 'omtk' 2 | 3 | version = '0.6.0' 4 | 5 | 6 | def commands(): 7 | env.PYTHONPATH.append('{root}/python') 8 | 9 | if system.platform == 'windows': 10 | env.XBMLANGPATH.append("{root}/images/") 11 | else: 12 | env.XBMLANGPATH.append("{root}/images/%B") 13 | -------------------------------------------------------------------------------- /omtk/macros/modules_build_selected.py: -------------------------------------------------------------------------------- 1 | from omtk.core import api 2 | from omtk.core.macros import BaseMacro 3 | 4 | 5 | class BuildSelectedModulesMacro(BaseMacro): 6 | def run(self): 7 | api.build_selected() 8 | 9 | 10 | def register_plugin(): 11 | return BuildSelectedModulesMacro 12 | -------------------------------------------------------------------------------- /omtk/macros/modules_unbuild_selected.py: -------------------------------------------------------------------------------- 1 | from omtk.core import api 2 | from omtk.core.macros import BaseMacro 3 | 4 | 5 | class UnbuildSelectedModulesMacro(BaseMacro): 6 | def run(self): 7 | api.unbuild_selected() 8 | 9 | 10 | def register_plugin(): 11 | return UnbuildSelectedModulesMacro 12 | -------------------------------------------------------------------------------- /omtk/macros/ctrls_calibrate_selected.py: -------------------------------------------------------------------------------- 1 | from omtk.core import api 2 | from omtk.core.macros import BaseMacro 3 | 4 | 5 | class CalibrateSelectedModulesMacro(BaseMacro): 6 | def run(self): 7 | api.calibrate_selected() 8 | 9 | 10 | def register_plugin(): 11 | return CalibrateSelectedModulesMacro 12 | -------------------------------------------------------------------------------- /omtk/macros/launch_create_component_wizard.py: -------------------------------------------------------------------------------- 1 | from omtk.core.macros import BaseMacro 2 | 3 | 4 | class LaunchCreateComponentWizard(BaseMacro): 5 | def run(self): 6 | from omtk import widget_create_component 7 | widget_create_component.show() 8 | 9 | 10 | def register_plugin(): 11 | return LaunchCreateComponentWizard 12 | -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage: 3 | /usr/autodesk/maya2016/bin/mayapy ~/packages/omtk/9.9.9/tests/run.py 4 | """ 5 | import os 6 | import sys 7 | import mayaunittest 8 | 9 | path_module_omtk = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts')) 10 | sys.path.append(path_module_omtk) 11 | 12 | mayaunittest.run_tests_from_commandline(directories=[os.path.dirname(__file__)]) 13 | -------------------------------------------------------------------------------- /omtk/macros/ctrls_mirror_selected.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | from omtk.core import macros 3 | from omtk.libs import libCtrlMatch 4 | 5 | 6 | class MacroMirrorSelectedCtrls(macros.BaseMacro): 7 | def run(self): 8 | libCtrlMatch.controller_matcher(selection=pymel.selected(), mirror_prefix=["L_", "R_"], flip=True) 9 | 10 | 11 | def register_plugin(): 12 | return MacroMirrorSelectedCtrls 13 | -------------------------------------------------------------------------------- /omtk/libs/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | if re.match('maya.*', os.path.basename(sys.executable), re.IGNORECASE): 6 | pass 7 | 8 | # Reload libs 9 | import libAttr 10 | import libCtrlShapes 11 | import libFormula 12 | import libPython 13 | import libQt 14 | import libPymel 15 | import libSkeleton 16 | import libRigging 17 | import libSkinning 18 | import libStringMap 19 | import libUtils 20 | import libHistory 21 | import libComponent 22 | -------------------------------------------------------------------------------- /omtk/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Models provides the link between a controller and an influence in OMTK. 3 | 4 | By changing models, we can change how a certain entity controller something. 5 | For example, the facial modules exposes 'avars' that mights be controlled by multiple type of setup depending on the flavor. 6 | - LinearCtrl 7 | - InteractiveCtrl (ctrls directly on the face) 8 | - FaceBoard 9 | 10 | Currently this is only used to link avars to ctrl/faceboard/sliders/etc. 11 | """ 12 | -------------------------------------------------------------------------------- /omtk/macros/ctrls_match_selected.py: -------------------------------------------------------------------------------- 1 | """ 2 | Original code by Jimmy Goulet (https://github.com/goujin), thanks for the contribution! 3 | """ 4 | import pymel.core as pymel 5 | from omtk.core import macros 6 | from omtk.libs import libCtrlMatch 7 | 8 | 9 | class CtrlMatchSelected(macros.BaseMacro): 10 | def run(self): 11 | libCtrlMatch.controller_matcher(selection=pymel.selected(), mirror_prefix=None, flip=False) 12 | 13 | 14 | def register_plugin(): 15 | return CtrlMatchSelected 16 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/__init__.py: -------------------------------------------------------------------------------- 1 | from core import * 2 | 3 | try: 4 | from plugin_maya import * 5 | except ImportError, e: # will raise when executed outside maya 6 | pass 7 | 8 | try: 9 | from plugin_maya_json import * 10 | except ImportError, e: # will raise when executed outside maya 11 | pass 12 | 13 | try: 14 | from plugin_json import * 15 | except ImportError, e: 16 | pass 17 | 18 | try: 19 | from plugin_yaml import * 20 | except ImportError, e: 21 | pass 22 | -------------------------------------------------------------------------------- /omtk/modules/rigHead.py: -------------------------------------------------------------------------------- 1 | from omtk.modules import rigFK 2 | 3 | 4 | class CtrlHead(rigFK.CtrlFk): 5 | pass 6 | 7 | 8 | class Head(rigFK.FK): 9 | """ 10 | Simple FK setup customized for head rigging. Mandatory when using facial modules. 11 | """ 12 | _CLS_CTRL = CtrlHead 13 | _NAME_CTRL_MERGE = True # By default we only expect one controller for the head. (Head_Ctrl > than Head_Head_Ctrl) 14 | _NAME_CTRL_ENUMERATE = True # If we find additional influences, we'll use enumeration. 15 | 16 | 17 | def register_plugin(): 18 | return Head 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pycharm files 2 | .idea/* 3 | 4 | *.egg-info/* 5 | cpp/includes/ 6 | 7 | /*cpp-related exclude*/ 8 | cpp/build/* 9 | Mac OS X 10 | *.DS_Store 11 | 12 | # Xcode 13 | *.pbxuser 14 | *.mode1v3 15 | *.mode2v3 16 | *.perspectivev3 17 | *.xcuserstate 18 | project.xcworkspace/ 19 | xcuserdata/ 20 | 21 | # Generated files 22 | *.o 23 | *.pyc 24 | 25 | #Python modules 26 | MANIFEST 27 | dist/ 28 | build/ 29 | 30 | # Backup files 31 | *~.nib 32 | 33 | # Configuration file 34 | config.json 35 | 36 | # NFS junk 37 | .nfs* 38 | 39 | # py.test files 40 | .cache 41 | .coverage 42 | -------------------------------------------------------------------------------- /omtk/libs/libStringMap.py: -------------------------------------------------------------------------------- 1 | class StringMap(object): 2 | def __init__(self, content, **kwargs): 3 | self.content = content 4 | self.set_fields(**kwargs) 5 | 6 | def get_fields(self): 7 | return self.fields 8 | 9 | def set_fields(self, **kwargs): 10 | new_fields = {} 11 | for key, val in kwargs.iteritems(): 12 | new_fields['{0}'.format(key)] = val 13 | self.fields = kwargs 14 | 15 | def __str__(self): 16 | return self.content.format(**self.fields) 17 | 18 | def __repr__(self): 19 | return repr(self.fields) 20 | -------------------------------------------------------------------------------- /omtk/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .core import * 4 | import constants 5 | import pymel.core as pymel 6 | 7 | # HACK: Load matrixNodes.dll 8 | pymel.loadPlugin('matrixNodes', quiet=True) 9 | 10 | 11 | def _reload(): 12 | """ 13 | Reload all module in their respective order. 14 | """ 15 | # Hack: prevent a crash related to loosing our OpenMaya.MSceneMessage events. 16 | try: 17 | pymel.deleteUI('OpenRiggingToolkit') 18 | except: 19 | pass 20 | 21 | import omtk 22 | libPython.rreload(omtk) 23 | 24 | 25 | def show(): 26 | import main_window 27 | main_window.show() 28 | -------------------------------------------------------------------------------- /omtk/libs/libQt.py: -------------------------------------------------------------------------------- 1 | def get_all_QTreeWidgetItem(widget, qt_item=None): 2 | """ 3 | Iterate through all items of a provided QTreeWidgetItem. 4 | :param widget: The QTreeWidgetItem to iterate through. 5 | :param qt_item: The starting point of the iteration. If nothing is provided the invisibleRootItem will be used. 6 | :return: 7 | """ 8 | if qt_item is None: 9 | qt_item = widget.invisibleRootItem() 10 | 11 | num_child = qt_item.childCount() 12 | for i in reversed(range(num_child)): 13 | qt_sub_item = qt_item.child(i) 14 | yield qt_sub_item 15 | for x in get_all_QTreeWidgetItem(widget, qt_sub_item): 16 | yield x 17 | -------------------------------------------------------------------------------- /tests/doctest_rigSqueeze.txt: -------------------------------------------------------------------------------- 1 | >>> from omtk.rigs.rigSqueeze import SqueezeNomenclature 2 | 3 | # Construct a naming from scratch 4 | >>> n = SqueezeNomenclature(tokens=['Eye', 'Jnt'], side=SqueezeNomenclature.SIDE_L) 5 | >>> n.resolve() 6 | 'L_Eye_Jnt' 7 | 8 | # Construct a naming from another existing naming 9 | >>> n = SqueezeNomenclature('L_Eye_Jnt') 10 | >>> n.prefix is None 11 | True 12 | >>> n.suffix is None 13 | True 14 | >>> n.side is None 15 | False 16 | 17 | # Adding of tokens using suffix 18 | >>> n = SqueezeNomenclature(tokens=['Eye'], side=SqueezeNomenclature.SIDE_L, suffix='Jnt') 19 | >>> n.resolve() 20 | 'L_Eye_Jnt' 21 | >>> n.tokens.append('Micro') 22 | >>> n.resolve() 23 | 'L_Eye_Micro_Jnt' -------------------------------------------------------------------------------- /omtk/libs/libDagTraversal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions that traverse the DAG tree in search of specific networks/serialized objects. 3 | """ 4 | from omtk.vendor import libSerialization 5 | 6 | 7 | def get_avar_network_by_name(avar_name): 8 | """Find a serialized avar with a provided name in the scene.""" 9 | for network in libSerialization.iter_networks_from_class('AbstractAvar'): 10 | if network.hasAttr('name') and network.attr('name').get() == avar_name: 11 | return network 12 | 13 | 14 | def get_avar_by_name(avar_name): 15 | """Find an avar with a provided name in the scene.""" 16 | network = get_avar_network_by_name(avar_name) 17 | if network: 18 | avar = libSerialization.import_network(network) 19 | if avar: 20 | return avar 21 | -------------------------------------------------------------------------------- /omtk/patches/patch_0.2.0_missing_rig_attr.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.vendor import libSerialization 4 | 5 | def run(): 6 | nets = pymel.ls(type='network') 7 | net_rig = next(iter(net for net in nets if libSerialization.is_network_from_class(net, 'Rig')), None) 8 | 9 | for net in nets: 10 | if not libSerialization.is_network_from_class(net, 'Module'): 11 | continue 12 | if not net.hasAttr('rig'): 13 | print("Adding attribute 'rig' on {0}".format(net)) 14 | pymel.addAttr(net, longName='rig', niceName='rig', attributeType='message') 15 | if not net.rig.isDestination(): 16 | print("Connecting attribute 'rig' on {0}".format(net)) 17 | pymel.connectAttr(net_rig.message, net.rig) 18 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Linux bash script to simplify testing omtk with pytest 3 | 4 | # Expose omtk 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | echo "[setup] Adding to PYTHONPATH: ${DIR}" 7 | PYTHONPATH="${DIR}:{$PYTHONPATH}" 8 | 9 | # Expose mayapy 10 | _MAYA_BIN=$(readlink -f `command -v maya`) 11 | _MAYA_DIR=$(dirname "${_MAYA_BIN}") 12 | 13 | echo "[setup] Adding to PATH: ${_MAYA_DIR}" 14 | PATH="${PATH}:${_MAYA_DIR}" 15 | 16 | # Export pytest 17 | _PYTEST_BIN=$(python -c "import pytest; print pytest.__file__") 18 | _PYTEST_DIR=$(dirname "${_PYTEST_BIN}") 19 | 20 | echo "[setup] Adding to PYTHONPATH: ${_PYTEST_DIR}" 21 | PYTHONPATH="${PYTHONPATH}:${_PYTEST_DIR}" 22 | 23 | # Run unit tests 24 | mayapy -m "py.test" $@ --cov=omtk--cov-branch --cov-report term-missing 25 | -------------------------------------------------------------------------------- /tests/test_nomenclature.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mayaunittest 3 | from maya import cmds 4 | import omtk 5 | 6 | class NomenclatureTestCase(mayaunittest.TestCase): 7 | 8 | def test(self): 9 | from omtk.core.className import BaseName 10 | 11 | # Construct a naming from scratch 12 | n = BaseName(tokens=['eye', 'jnt'], side=BaseName.SIDE_L) 13 | self.assertEqual(n.resolve(), 'l_eye_jnt') 14 | 15 | # Construct a naming from another existing naming 16 | n = BaseName('l_eye_jnt') 17 | self.assertEqual(n.prefix, None) 18 | self.assertEqual(n.suffix, None) 19 | self.assertEqual(n.side, n.SIDE_L) 20 | 21 | # Adding of tokens using suffix 22 | n = BaseName(tokens=['eye'], side=BaseName.SIDE_L, suffix='jnt') 23 | self.assertEqual(n.resolve(), 'l_eye_jnt') 24 | n.tokens.append('micro') 25 | self.assertEqual(n.resolve(), 'l_eye_micro_jnt') 26 | -------------------------------------------------------------------------------- /tests/test_nomenclature_squeeze.py: -------------------------------------------------------------------------------- 1 | import mayaunittest 2 | from omtk.rigs.rigSqueeze import SqueezeNomenclature 3 | 4 | class NomenclatureTestCase(mayaunittest.TestCase): 5 | 6 | def test(self): 7 | # Construct a naming from scratch 8 | n = SqueezeNomenclature(tokens=['Eye', 'Jnt'], side=SqueezeNomenclature.SIDE_L) 9 | self.assertEqual(n.resolve(), 'L_Eye_Jnt') 10 | 11 | # Construct a naming from another existing naming 12 | n = SqueezeNomenclature('L_Eye_Jnt') 13 | self.assertEqual(n.prefix, None) 14 | self.assertEqual(n.suffix, 'Jnt') 15 | self.assertEqual(n.side, n.SIDE_L) 16 | 17 | # Adding of tokens using suffix 18 | n = SqueezeNomenclature(tokens=['Eye'], side=SqueezeNomenclature.SIDE_L, suffix='Jnt') 19 | self.assertEqual(n.resolve(), 'L_Eye_Jnt') 20 | n.tokens.append('Micro') 21 | self.assertEqual(n.resolve(), 'L_Eye_Micro_Jnt') 22 | -------------------------------------------------------------------------------- /omtk/core/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | import logging 4 | import os 5 | 6 | import utils 7 | import classCtrl 8 | import classCtrlModel 9 | import classModule 10 | import classModuleMap 11 | import classModuleCompound 12 | import className 13 | import classNode 14 | import classRig 15 | from api import * 16 | from . import plugin_manager 17 | 18 | log = logging.getLogger('omtk') 19 | log.setLevel(logging.DEBUG) 20 | 21 | # Load configuration file 22 | # Currently this only allow the default rig class from being used. 23 | config = {} 24 | config_dir = os.path.abspath(os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) 25 | config_path = os.path.join(config_dir, 'config.json') 26 | if os.path.exists(config_path): 27 | with open(config_path) as fp: 28 | config = json.load(fp) 29 | 30 | # Load plugins 31 | plugin_manager.plugin_manager.get_plugins() # force evaluating lazy singleton (todo: remove it?) -------------------------------------------------------------------------------- /omtk/core/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class decorator_uiexpose(object): 5 | """ 6 | Use this decorator to expose instance method to the GUI. 7 | """ 8 | 9 | def __init__(self, flags=None): 10 | if flags is None: 11 | flags = [] 12 | self.flags = flags 13 | 14 | def __call__(self, fn, *args, **kwargs): 15 | def wrapped_f(*args, **kwargs): 16 | return fn(*args, **kwargs) 17 | 18 | wrapped_f.__can_show__ = self.__can_show__ 19 | wrapped_f._flags = self.flags 20 | return wrapped_f 21 | 22 | def __get__(self, inst, owner): 23 | fn = functools.partial(self.__call__, inst) 24 | fn.__can_show__ = self.__can_show__ # todo: necessary? 25 | fn._flags = self.flags # todo: necessary? 26 | return fn 27 | 28 | def __can_show__(self): 29 | """ 30 | This method is used for duck-typing by the interface. 31 | """ 32 | return True 33 | -------------------------------------------------------------------------------- /omtk/constants.py: -------------------------------------------------------------------------------- 1 | class Axis: 2 | """ 3 | Fake enum as class with constant variable to represent the axis value that could change 4 | """ 5 | x = 'X' 6 | y = 'Y' 7 | z = 'Z' 8 | 9 | 10 | class SpaceSwitchReservedIndex: 11 | """ 12 | Fake enum as class with constant variable to represent the reserved index in controller space switch 13 | """ 14 | world = -3 15 | local = -2 16 | root = -1 17 | 18 | 19 | class UIExposeFlags: 20 | """ 21 | Flags used when exposing Rig or Module functionality in the ui. 22 | """ 23 | trigger_network_export = 1 24 | 25 | 26 | class EnvironmentVariables: 27 | """ 28 | Customize the behavior of OMTK by defining thoses environment variables. 29 | """ 30 | # Force a specific rig type as default. 31 | # Usefull for project-specific configurations. 32 | OMTK_DEFAULT_RIG = 'OMTK_DEFAULT_RIG' 33 | 34 | # Define additional location on disk to search for plugins. 35 | OMTK_PLUGINS = 'OMTK_PLUGINS' 36 | -------------------------------------------------------------------------------- /omtk/patches/patch_0.3.0_convert_facelids_to_faceeyelids.py: -------------------------------------------------------------------------------- 1 | """ 2 | In omtk 0.3.0, the FaceLids module was renamed to FaceEyeLids to prevent 3 | confusion with the FaceLips module. 4 | Run this script to convert a pre-0.3.0 scene. 5 | """ 6 | 7 | from omtk.vendor import libSerialization 8 | 9 | cls_rename_map = ( 10 | ('FaceLids', 'FaceEyeLids'), 11 | ('CtrlLidUpp', 'CtrlEyeLidUpp'), 12 | ('CtrlLidLow', 'CtrlEyeLidLow'), 13 | ('AvarGrpAreaOnSurface', 'AvarGrpOnSurface') 14 | ) 15 | affected_attr_names = ('_class', '_class_namespace') 16 | 17 | for old_name, new_name in cls_rename_map: 18 | for net in libSerialization.get_networks_from_class(old_name): 19 | for attr_name in affected_attr_names: 20 | if not net.hasAttr(attr_name): 21 | print("Missing attribute {} on {}".format(attr_name, net)) 22 | continue 23 | print("Renaming {}".format(net)) 24 | attr = net.attr(attr_name) 25 | attr.set('.'.join((token.replace(old_name, new_name) for token in attr.get().split('.')))) -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/plugin_yaml.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | import core 4 | 5 | __all__ = ( 6 | 'export_yaml', 7 | 'export_yaml_file', 8 | 'import_yaml', 9 | 'import_yaml_file' 10 | ) 11 | 12 | 13 | def export_yaml(data, **kwargs): 14 | dicData = core.export_dict(data) 15 | return yaml.dump(dicData, **kwargs) 16 | 17 | 18 | def export_yaml_file(data, path, mkdir=True, **kwargs): 19 | if mkdir: 20 | dirname = os.path.dirname(path) 21 | if not os.path.exists(dirname): 22 | os.makedirs(path) 23 | 24 | dicData = core.export_dict(data) 25 | 26 | with open(path, 'w') as fp: 27 | yaml.dump(dicData, fp) 28 | 29 | return True 30 | 31 | 32 | def import_yaml(str_, **kwargs): 33 | data = yaml.load(str_) 34 | return core.import_dict(data) 35 | 36 | 37 | def import_yaml_file(path, **kwargs): 38 | if not os.path.exists(path): 39 | raise Exception("Can't importFromYamlFile, file does not exist! {0}".format(path)) 40 | 41 | with open(path, 'r') as fp: 42 | data = yaml.load(fp) 43 | return core.import_dict(data) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Renaud Lessard Larouche 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/plugin_json.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import core 4 | 5 | __all__ = ( 6 | 'export_json', 7 | 'export_json_file', 8 | 'import_json', 9 | 'import_json_file' 10 | ) 11 | 12 | # 13 | # Json Support 14 | # 15 | 16 | 17 | def _make_dir(path): 18 | path_dir = os.path.dirname(path) 19 | 20 | # Create destination folder if needed 21 | if not os.path.exists(path_dir): 22 | os.makedirs(path_dir) 23 | 24 | 25 | def export_json(data, indent=4, **kwargs): 26 | data = core.export_dict(data) 27 | return json.dumps(data, indent=indent, **kwargs) 28 | 29 | 30 | def export_json_file(data, path, mkdir=True, indent=4, **kwargs): 31 | if mkdir: 32 | _make_dir(path) 33 | 34 | data_dict = core.export_dict(data) 35 | 36 | with open(path, 'w') as fp: 37 | json.dump(data_dict, fp, indent=indent, **kwargs) 38 | 39 | return True 40 | 41 | 42 | def import_json(str_, **kwargs): 43 | data = json.loads(str_, **kwargs) 44 | return core.import_dict(data) 45 | 46 | 47 | def import_json_file(path, **kwargs): 48 | if not os.path.exists(path): 49 | raise Exception("Can't importFromJsonFile, file does not exist! {0}".format(path)) 50 | 51 | with open(path, 'r') as fp: 52 | data = json.load(fp, **kwargs) 53 | return core.import_dict(data) 54 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/decorators.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | 4 | # src: https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize 5 | # modified to support kwargs 6 | class memoized(object): 7 | '''Decorator. Caches a function's return value each time it is called. 8 | If called later with the same arguments, the cached value is returned 9 | (not reevaluated). 10 | ''' 11 | 12 | def __init__(self, func): 13 | self.func = func 14 | self.cache = {} 15 | 16 | def __call__(self, *args, **kwargs): 17 | if not isinstance(args, collections.Hashable): 18 | # uncacheable. a list, for instance. 19 | # better to not cache than blow up. 20 | return self.func(*args) 21 | 22 | # Include kwargs 23 | # src: http://stackoverflow.com/questions/6407993/how-to-memoize-kwargs 24 | key = (args, frozenset(kwargs.items())) 25 | if key in self.cache: 26 | return self.cache[key] 27 | else: 28 | value = self.func(*args, **kwargs) 29 | self.cache[key] = value 30 | return value 31 | 32 | def __repr__(self): 33 | """Return the function's docstring.""" 34 | return self.func.__doc__ 35 | 36 | def __get__(self, obj, objtype): 37 | """Support instance methods.""" 38 | return functools.partial(self.__call__, obj) -------------------------------------------------------------------------------- /omtk/macros/ctrls_interactive_swap_mesh.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple utility to swap the mesh used by an InteractiveCtrl in case the wrong mesh was used. 3 | """ 4 | from omtk.core.macros import BaseMacro 5 | 6 | 7 | class CtrlInteractiveSwapMesh(BaseMacro): 8 | def _iter_ictrl(self): 9 | from omtk import api 10 | from omtk.modules import rigFaceAvarGrps 11 | from omtk.modules import rigFaceAvar 12 | from omtk.models import model_ctrl_linear 13 | rig, modules = api._get_modules_from_selection() 14 | # Build selected modules 15 | for module in modules: 16 | if isinstance(module, rigFaceAvarGrps.AvarGrp): 17 | for avar in module._iter_all_avars(): 18 | if isinstance(avar, rigFaceAvar.AbstractAvar) and \ 19 | avar.model_ctrl and \ 20 | isinstance(avar.model_ctrl, model_ctrl_linear.ModelCtrlLinear): 21 | yield avar.model_ctrl 22 | 23 | def run(self): 24 | import pymel.core as pymel 25 | from omtk.libs import libPymel 26 | 27 | mesh = next((obj for obj in pymel.selected() if libPymel.isinstance_of_shape(obj, cls=pymel.nodetypes.Mesh)), None) 28 | if not mesh: 29 | pymel.warning("Please select a mesh to follow.") 30 | return 31 | 32 | for ctrl_model in self._iter_ictrl(): 33 | ctrl_model.swap_mesh(mesh) 34 | 35 | 36 | def register_plugin(): 37 | return CtrlInteractiveSwapMesh 38 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_meshes.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 316 10 | 295 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Update 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | false 38 | 39 | 40 | 41 | 1 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Select Meshes Grp 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /omtk/preferences_window.py: -------------------------------------------------------------------------------- 1 | from ui import preferences_window 2 | from omtk.core import plugin_manager 3 | from omtk.core import preferences 4 | 5 | from omtk.vendor.Qt import QtCore, QtWidgets 6 | 7 | 8 | class PreferencesWindow(QtWidgets.QDialog): 9 | searchQueryChanged = QtCore.Signal(str) 10 | 11 | def __init__(self, parent=None): 12 | super(PreferencesWindow, self).__init__(parent=parent) 13 | 14 | # Initialize GUI 15 | self.ui = preferences_window.Ui_Dialog() 16 | self.ui.setupUi(self) 17 | 18 | # Fill the QComboBox 19 | self.rig_plugins = sorted(plugin_manager.plugin_manager.get_loaded_plugins_by_type('rigs')) 20 | rig_plugins_names = [plugin.cls.__name__ for plugin in self.rig_plugins if plugin] 21 | labels = ['Default'] + rig_plugins_names 22 | 23 | self.ui.comboBox.addItems(labels) 24 | 25 | default_rig_type_name = preferences.preferences.get_default_rig_class().__name__ 26 | if default_rig_type_name in rig_plugins_names: 27 | self.ui.comboBox.setCurrentIndex(rig_plugins_names.index(default_rig_type_name) + 1) 28 | 29 | # Connect events 30 | self.ui.comboBox.currentIndexChanged.connect(self.on_default_rig_changed) 31 | 32 | def on_default_rig_changed(self, index): 33 | if index == 0: 34 | preferences.preferences.default_rig = None 35 | else: 36 | preferences.preferences.default_rig = self.rig_plugins[index - 1].cls.__name__ 37 | 38 | preferences.preferences.save() 39 | 40 | 41 | gui = PreferencesWindow() 42 | 43 | 44 | def show(): 45 | global gui 46 | gui.show() 47 | -------------------------------------------------------------------------------- /omtk/modules/template: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from omtk.core.classCtrl import BaseCtrl 4 | from omtk.core.classModule import Module 5 | from omtk.libs import libRigging, libCtrlShapes 6 | 7 | 8 | class BaseAttHolder(BaseCtrl): 9 | def __createNode__(self, size=None, refs=None, **kwargs): 10 | # Resolve size automatically if refs are provided. 11 | ref = next(iter(refs), None) if isinstance(refs, collections.Iterable) else refs 12 | if size is None and ref is not None: 13 | size = libRigging.get_recommended_ctrl_size(ref) 14 | else: 15 | size = 1.0 16 | 17 | node = libCtrlShapes.create_shape_attrholder(size=size, **kwargs) 18 | 19 | # Hide default keyable attributes 20 | node.t.set(channelBox=False) 21 | node.r.set(channelBox=False) 22 | node.s.set(channelBox=False) 23 | 24 | return node 25 | 26 | 27 | class MyCtrl(BaseCtrl): 28 | """ 29 | If you need specific ctrls for you module, you can inherit from BaseCtrl directly. 30 | """ 31 | pass 32 | 33 | 34 | class MyModule(Module): 35 | def __init__(self, *args, **kwargs): 36 | """ 37 | Pre-declare here all the used members. 38 | """ 39 | super(MyModule, self).__init__(*args, **kwargs) 40 | 41 | def build(self, *args, **kwargs): 42 | super(MyModule, self).build(*args, **kwargs) 43 | 44 | nomenclature_anm = self.get_nomenclature_anm() 45 | nomenclature_rig = self.get_nomenclature_rig() 46 | 47 | raise NotImplementedError 48 | 49 | def unbuild(self): 50 | """ 51 | If you are using sub-modules, you might want to clean them here. 52 | :return: 53 | """ 54 | super(MyModule, self).unbuild() 55 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_modules.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 316 10 | 295 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Update 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::StrongFocus 38 | 39 | 40 | Qt::CustomContextMenu 41 | 42 | 43 | true 44 | 45 | 46 | QAbstractItemView::ExtendedSelection 47 | 48 | 49 | false 50 | 51 | 52 | 53 | 1 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /omtk/modules/rigFaceSquint.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.libs import libCtrlShapes 4 | from omtk.libs import libRigging 5 | from omtk.modules import rigFaceAvar 6 | from omtk.modules import rigFaceAvarGrps 7 | 8 | 9 | class CtrlSquint(rigFaceAvar.BaseCtrlFace): 10 | def __createNode__(self, **kwargs): 11 | return libCtrlShapes.create_triangle_upp() 12 | 13 | 14 | class FaceSquint(rigFaceAvarGrps.AvarGrpOnSurface): 15 | """ 16 | AvarGrp setup customized for squint rigging. 17 | """ 18 | _CLS_CTRL = CtrlSquint 19 | IS_SIDE_SPECIFIC = False 20 | SHOW_IN_UI = True 21 | CREATE_MACRO_AVAR_HORIZONTAL = True 22 | CREATE_MACRO_AVAR_VERTICAL = False 23 | CREATE_MACRO_AVAR_ALL = False 24 | 25 | def get_default_name(self): 26 | return 'squint' 27 | 28 | def _create_avar_macro_l_ctrls(self, ctrl_tm=None, **kwargs): 29 | # Find the middle of l eyebrow. 30 | pos = libRigging.get_average_pos_between_vectors(self.get_jnts_l()) 31 | ctrl_tm = pymel.datatypes.Matrix( 32 | 1, 0, 0, 0, 33 | 0, 1, 0, 0, 34 | 0, 0, 1, 0, 35 | pos.x, pos.y, pos.z, 1 36 | ) 37 | 38 | super(FaceSquint, self)._create_avar_macro_l_ctrls(ctrl_tm=ctrl_tm) 39 | 40 | def _create_avar_macro_r_ctrls(self, ctrl_tm=None, **kwargs): 41 | # Find the middle of l eyebrow. 42 | pos = libRigging.get_average_pos_between_vectors(self.get_jnts_r()) 43 | ctrl_tm = pymel.datatypes.Matrix( 44 | 1, 0, 0, 0, 45 | 0, 1, 0, 0, 46 | 0, 0, 1, 0, 47 | pos.x, pos.y, pos.z, 1 48 | ) 49 | 50 | super(FaceSquint, self)._create_avar_macro_r_ctrls(ctrl_tm=ctrl_tm) 51 | 52 | 53 | def register_plugin(): 54 | return FaceSquint 55 | -------------------------------------------------------------------------------- /omtk/patches/patch_0_4_7_add_leg_auto_roll_threshold.py: -------------------------------------------------------------------------------- 1 | """ 2 | In omtk 0.4.7, the Leg module now preserve the auto-roll threshold value. 3 | Run this script to convert a pre-0.4.7 scene. 4 | """ 5 | 6 | import omtk; reload(omtk); omtk._reload() 7 | from omtk.modules import rigLeg 8 | import pymel.core as pymel 9 | 10 | NET_ATTR_NAME = 'attrAutoRollThreshold' 11 | 12 | 13 | def _module_need_patch(module): 14 | # Verify module type 15 | if not isinstance(module, rigLeg.Leg): 16 | return False 17 | 18 | # Verify module state 19 | if not module.is_built(): 20 | return False 21 | if not (module.sysIK and module.sysIK.is_built()): 22 | return False 23 | 24 | # Verify module version 25 | major, minor, patch = [int(val) for val in getattr(module, 'version', '0.0.0').split('.')] 26 | if major > 0 or minor > 4 or patch > 7: 27 | return False 28 | 29 | # Verify the module is not already patched. 30 | net = module.sysIK._network 31 | if net.hasAttr(NET_ATTR_NAME): 32 | return False 33 | 34 | return True 35 | 36 | 37 | def _patch_module(module): 38 | net = module.sysIK._network 39 | ctrl = module.sysIK.ctrl_ik 40 | attr_anm = ctrl.rollAutoThreshold 41 | if not net.hasAttr('attrAutoRollThreshold'): 42 | print("Fixing {}".format(net)) 43 | pymel.addAttr(net, longName='attrAutoRollThreshold') 44 | pymel.connectAttr(attr_anm, net.attrAutoRollThreshold) 45 | 46 | 47 | def run(): 48 | rig = omtk.find_one() 49 | modules = [module for module in rig.modules if _module_need_patch(module)] 50 | 51 | if not modules: 52 | print("Nothing to patch.") 53 | return 54 | 55 | for module in modules: 56 | print("Patching {}".format(module)) 57 | _patch_module(module) 58 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_influences.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 316 10 | 295 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Update 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Hide Assigned 38 | 39 | 40 | true 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::CustomContextMenu 48 | 49 | 50 | QAbstractItemView::ExtendedSelection 51 | 52 | 53 | false 54 | 55 | 56 | 57 | 1 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /omtk/models/model_avar_linear.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.core import classNode 4 | from omtk.libs import libAttr 5 | from omtk.libs import libRigging 6 | 7 | from .model_avar_base import AvarInflBaseModel 8 | 9 | 10 | class AvarLinearModel(AvarInflBaseModel): 11 | """ 12 | A deformation point on the face that move accordingly to nurbsSurface. 13 | """ 14 | SHOW_IN_UI = False 15 | 16 | _ATTR_NAME_MULT_LR = 'multiplierLr' 17 | _ATTR_NAME_MULT_UD = 'multiplierUd' 18 | _ATTR_NAME_MULT_FB = 'multiplierFb' 19 | 20 | def __init__(self, *args, **kwargs): 21 | super(AvarLinearModel, self).__init__(*args, **kwargs) 22 | 23 | # Reference to the object containing the bind pose of the avar. 24 | self._obj_offset = None 25 | 26 | def _build(self): 27 | nomenclature_rig = self.get_nomenclature_rig() 28 | 29 | grp_output = pymel.createNode( 30 | 'transform', 31 | name=nomenclature_rig.resolve('output'), 32 | parent=self.grp_rig, 33 | ) 34 | 35 | attr_get_t = libRigging.create_utility_node( 36 | 'multiplyDivide', 37 | input1X=self._attr_inn_lr, 38 | input1Y=self._attr_inn_ud, 39 | input1Z=self._attr_inn_fb, 40 | input2X=self.multiplier_lr, 41 | input2Y=self.multiplier_ud, 42 | input2Z=self.multiplier_fb, 43 | ).output 44 | 45 | pymel.connectAttr(attr_get_t, grp_output.translate) 46 | pymel.connectAttr(self._attr_inn_pt, grp_output.rotateX) 47 | pymel.connectAttr(self._attr_inn_yw, grp_output.rotateY) 48 | pymel.connectAttr(self._attr_inn_rl, grp_output.rotateZ) 49 | pymel.connectAttr(self._attr_inn_sx, grp_output.scaleX) 50 | pymel.connectAttr(self._attr_inn_sy, grp_output.scaleY) 51 | pymel.connectAttr(self._attr_inn_sz, grp_output.scaleZ) 52 | 53 | return grp_output.matrix 54 | -------------------------------------------------------------------------------- /omtk/modules/rigFaceBrow.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | from omtk.modules import rigFaceAvarGrps 3 | from omtk.libs import libCtrlShapes 4 | from omtk.libs import libRigging 5 | from omtk.modules import rigFaceAvar 6 | 7 | 8 | class CtrlBrow(rigFaceAvar.BaseCtrlFace): 9 | def __createNode__(self, **kwargs): 10 | return libCtrlShapes.create_triangle_low() 11 | 12 | 13 | class FaceBrow(rigFaceAvarGrps.AvarGrpOnSurface): 14 | """ 15 | AvarGrp customized for Brow rigging. 16 | """ 17 | _CLS_CTRL = CtrlBrow 18 | 19 | SHOW_IN_UI = True 20 | IS_SIDE_SPECIFIC = False 21 | CREATE_MACRO_AVAR_HORIZONTAL = True 22 | CREATE_MACRO_AVAR_VERTICAL = False 23 | CREATE_MACRO_AVAR_ALL = False 24 | 25 | def _create_avar_macro_l_ctrls(self, ctrl_tm=None, **kwargs): 26 | # Find the middle of l eyebrow. 27 | pos = libRigging.get_average_pos_between_vectors(self.get_jnts_l()) 28 | ctrl_tm = pymel.datatypes.Matrix( 29 | 1, 0, 0, 0, 30 | 0, 1, 0, 0, 31 | 0, 0, 1, 0, 32 | pos.x, pos.y, pos.z, 1 33 | ) 34 | 35 | super(FaceBrow, self)._create_avar_macro_l_ctrls(ctrl_tm=ctrl_tm) 36 | 37 | def _create_avar_macro_r_ctrls(self, ctrl_tm=None, **kwargs): 38 | # Find the middle of l eyebrow. 39 | # We expect the right-side influence to be mirrored in behavior. 40 | # However this should be applied to to influence tm, not the ctrl tm. Clean this please. 41 | pos = libRigging.get_average_pos_between_vectors(self.get_jnts_r()) 42 | ctrl_tm = pymel.datatypes.Matrix( 43 | 1, 0, 0, 0, 44 | 0, -1.0, 0, 0, 45 | 0, 0, -1.0, 0, 46 | pos.x, pos.y, pos.z, 1 47 | ) 48 | 49 | super(FaceBrow, self)._create_avar_macro_r_ctrls(ctrl_tm=ctrl_tm) 50 | 51 | def get_default_name(self): 52 | return 'brow' 53 | 54 | 55 | def register_plugin(): 56 | return FaceBrow 57 | -------------------------------------------------------------------------------- /omtk/ui/widget_component_wizard_parts.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | QAbstractItemView::SelectRows 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Add 33 | 34 | 35 | 36 | 37 | 38 | 39 | Remove 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Connect 51 | 52 | 53 | 54 | 55 | 56 | 57 | Disconnect 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /omtk/modules/rigFaceNose.py: -------------------------------------------------------------------------------- 1 | from omtk.modules import rigFaceAvarGrps 2 | from omtk.modules import rigFaceAvar 3 | from omtk.models import model_ctrl_linear 4 | 5 | 6 | class CtrlNose(rigFaceAvar.CtrlFaceMicro): 7 | pass 8 | 9 | 10 | class ModelMicroAvarNose(model_ctrl_linear.ModelCtrlLinear): 11 | def connect(self, avar, avar_grp, ud=True, fb=True, lr=True, yw=True, pt=True, rl=True, sx=True, sy=True, sz=True): 12 | avar_tweak = avar_grp._get_micro_tweak_avars_dict().get(avar, None) 13 | if avar_tweak: 14 | super(ModelMicroAvarNose, self).connect(avar, avar_grp, ud=ud, fb=fb, lr=False, yw=False, pt=False, 15 | rl=False, sx=False, sy=False, sz=False) 16 | super(ModelMicroAvarNose, self).connect(avar_tweak, avar_grp, ud=False, fb=False, lr=lr, yw=yw, pt=pt, 17 | rl=rl, sx=sx, sy=sy, sz=sz) 18 | else: 19 | super(ModelMicroAvarNose, self).connect(avar, avar_grp, ud=ud, fb=fb, lr=lr, yw=yw, pt=pt, rl=rl, sx=sx, 20 | sy=sy, sz=sz) 21 | 22 | 23 | class FaceNose(rigFaceAvarGrps.AvarGrpOnSurface): 24 | """ 25 | The Nose is composed of two zones. The uppernose and the lower nose. 26 | The uppernose is user specifically for it's yaw and pitch rotation. 27 | Everything under is considered a nostril. 28 | 29 | Note that this was done reallllly quickly and cleanup may be needed in the future. 30 | """ 31 | # _DEFORMATION_ORDER = 'post' 32 | # _CLS_AVAR = AvarJaw 33 | SHOW_IN_UI = True 34 | IS_SIDE_SPECIFIC = False 35 | _CLS_CTRL = CtrlNose 36 | _CLS_MODEL_CTRL_MICRO = ModelMicroAvarNose 37 | CREATE_MACRO_AVAR_ALL = True 38 | CREATE_MACRO_AVAR_HORIZONTAL = False 39 | CREATE_MACRO_AVAR_VERTICAL = False 40 | 41 | def get_default_name(self): 42 | return 'nose' 43 | 44 | 45 | def register_plugin(): 46 | return FaceNose 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open Maya Rigging Toolkit 2 | 3 | Omtk is a lightweight suite of production rigging tools for Maya. 4 | This include an object-oriented modular autorig solution. 5 | Using metadata, generated rigs can be unbuilt and rebuilt if modifications need to be made. 6 | 7 | ### Installation 8 | 9 | 1. Clone the repository or download a release 10 | 11 | 2. Add this directory to your `PYTHONPATH` environment variable 12 | 13 | 3. Open Maya 14 | 15 | Alternatively, if you are using [rez](https://github.com/nerdvegas/rez), a `package.py` file is included. 16 | 17 | ### Rules 18 | 19 | The following rules simplify the implementation of the system and must be respected for Omtk to work properly. 20 | 21 | - All influences are in a separated hierarchy. 22 | - All joints point toward the X axis, Z is their up axis. This mean the roll axis is always x and the primary axis is z. 23 | - The character should look toward the positive Z axis. 24 | - Feets orientation and straight, always. 25 | - All python code should respect the PEP8 standards. 26 | - All nodes and attributes created in a Maya scene are in camelCase for better integration in Maya. 27 | 28 | ### Third parties 29 | 30 | Omtk vendor the following third party libraries: 31 | 32 | ### libFormula 33 | A lightweight programming language that create maya utility nodes setup by parsing mathematical formulas. 34 | 35 | Read the [documentation](http://github.com/renaudll/omtk/wiki/omtk.libs.libFormula). 36 | 37 | ### libSerialization 38 | An IO module that allow serialization/deserialisation of Python objects to Maya networks. 39 | 40 | Read the [documentation](https://github.com/renaudll/libSerialization). 41 | 42 | ### Qt.py 43 | Minimal Python 2 & 3 shim around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5. 44 | 45 | Read the [documentation](https://github.com/mottosso/Qt.py) 46 | 47 | ### Testing 48 | 49 | If you are on Linux and have `pytest` installed you can run the unittests with the `test.sh` script. 50 | 51 | ### Support 52 | 53 | If you are in need of support, please create a GitHub issue. -------------------------------------------------------------------------------- /omtk/animation/ikfkTools.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import pymel.core as pymel 4 | 5 | from omtk.vendor import libSerialization 6 | 7 | 8 | def _get_module_networks_from_selection(module_name): 9 | """ 10 | Search for specific module networks recursively, starting at selection. 11 | Note that to help with performances, we'll skip any Rig network we encounter. 12 | :param module_name: The name of the module to search for. 13 | :return: A list of pymel.nodetypes.Network. 14 | """ 15 | 16 | def fn_skip(network): 17 | return libSerialization.is_network_from_class(network, 'Rig') 18 | 19 | def fn_key(network): 20 | return libSerialization.is_network_from_class(network, module_name) 21 | 22 | sel = pymel.selected() 23 | networks = libSerialization.get_connected_networks(sel, recursive=True, key=fn_key, key_skip=fn_skip) 24 | 25 | modules = [libSerialization.import_network(network, fn_skip=fn_skip) for network in networks] 26 | modules = filter(None, modules) # Filter any invalid networks that libSerialization doesn't protect us from. 27 | return modules 28 | 29 | 30 | def _call_on_networks_by_class(fn_name, module_name): 31 | modules = _get_module_networks_from_selection(module_name) 32 | for module in modules: 33 | # Resolve function 34 | if not hasattr(module, fn_name): 35 | logging.warning("Can't find attribute {0} in {1}".format(fn_name, module)) 36 | continue 37 | fn = getattr(module, fn_name) 38 | if not hasattr(fn, '__call__'): 39 | logging.warning("Can't execute {0} in {1}, not callable!".format(fn_name, module)) 40 | continue 41 | 42 | # Execute function 43 | try: 44 | fn() 45 | except Exception, e: 46 | logging.warning("Error excecuting {0} in {1}! {2}".format(fn_name, module, str(e))) 47 | 48 | 49 | switchToIk = functools.partial(_call_on_networks_by_class, 'switch_to_ik', 'Limb') 50 | switchToFk = functools.partial(_call_on_networks_by_class, 'switch_to_fk', 'Limb') 51 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_modules.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_list_modules.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:54 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(316, 295) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Form) 17 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 18 | self.verticalLayout.setObjectName("verticalLayout") 19 | self.horizontalLayout = QtWidgets.QHBoxLayout() 20 | self.horizontalLayout.setObjectName("horizontalLayout") 21 | self.lineEdit_search = QtWidgets.QLineEdit(Form) 22 | self.lineEdit_search.setObjectName("lineEdit_search") 23 | self.horizontalLayout.addWidget(self.lineEdit_search) 24 | self.btn_update = QtWidgets.QPushButton(Form) 25 | self.btn_update.setObjectName("btn_update") 26 | self.horizontalLayout.addWidget(self.btn_update) 27 | self.verticalLayout.addLayout(self.horizontalLayout) 28 | self.treeWidget = QtWidgets.QTreeWidget(Form) 29 | self.treeWidget.setFocusPolicy(QtCore.Qt.StrongFocus) 30 | self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 31 | self.treeWidget.setAutoFillBackground(True) 32 | self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) 33 | self.treeWidget.setObjectName("treeWidget") 34 | self.treeWidget.headerItem().setText(0, "1") 35 | self.treeWidget.header().setVisible(False) 36 | self.verticalLayout.addWidget(self.treeWidget) 37 | 38 | self.retranslateUi(Form) 39 | QtCore.QMetaObject.connectSlotsByName(Form) 40 | 41 | def retranslateUi(self, Form): 42 | Form.setWindowTitle(QtCompat.translate("Form", "Form", None, -1)) 43 | self.btn_update.setText(QtCompat.translate("Form", "Update", None, -1)) 44 | 45 | -------------------------------------------------------------------------------- /omtk/ui/preferences_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/preferences_window.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Dialog(object): 13 | def setupUi(self, Dialog): 14 | Dialog.setObjectName("Dialog") 15 | Dialog.resize(383, 229) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) 17 | self.verticalLayout.setObjectName("verticalLayout") 18 | self.gridLayout = QtWidgets.QGridLayout() 19 | self.gridLayout.setObjectName("gridLayout") 20 | self.label = QtWidgets.QLabel(Dialog) 21 | self.label.setObjectName("label") 22 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 23 | self.comboBox = QtWidgets.QComboBox(Dialog) 24 | self.comboBox.setObjectName("comboBox") 25 | self.gridLayout.addWidget(self.comboBox, 0, 1, 1, 1) 26 | self.verticalLayout.addLayout(self.gridLayout) 27 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) 28 | self.verticalLayout.addItem(spacerItem) 29 | self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) 30 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal) 31 | self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) 32 | self.buttonBox.setObjectName("buttonBox") 33 | self.verticalLayout.addWidget(self.buttonBox) 34 | 35 | self.retranslateUi(Dialog) 36 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), Dialog.accept) 37 | QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), Dialog.reject) 38 | QtCore.QMetaObject.connectSlotsByName(Dialog) 39 | 40 | def retranslateUi(self, Dialog): 41 | Dialog.setWindowTitle(QtCompat.translate("Dialog", "OMTK - Preferences", None, -1)) 42 | self.label.setText(QtCompat.translate("Dialog", "Default Rig Class", None, -1)) 43 | 44 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_meshes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_list_meshes.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(316, 295) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Form) 17 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 18 | self.verticalLayout.setObjectName("verticalLayout") 19 | self.horizontalLayout = QtWidgets.QHBoxLayout() 20 | self.horizontalLayout.setObjectName("horizontalLayout") 21 | self.lineEdit_search = QtWidgets.QLineEdit(Form) 22 | self.lineEdit_search.setObjectName("lineEdit_search") 23 | self.horizontalLayout.addWidget(self.lineEdit_search) 24 | self.btn_update = QtWidgets.QPushButton(Form) 25 | self.btn_update.setObjectName("btn_update") 26 | self.horizontalLayout.addWidget(self.btn_update) 27 | self.verticalLayout.addLayout(self.horizontalLayout) 28 | self.treeWidget = QtWidgets.QTreeWidget(Form) 29 | self.treeWidget.setObjectName("treeWidget") 30 | self.treeWidget.headerItem().setText(0, "1") 31 | self.treeWidget.header().setVisible(False) 32 | self.verticalLayout.addWidget(self.treeWidget) 33 | self.pushButton_selectGrpMeshes = QtWidgets.QPushButton(Form) 34 | self.pushButton_selectGrpMeshes.setObjectName("pushButton_selectGrpMeshes") 35 | self.verticalLayout.addWidget(self.pushButton_selectGrpMeshes) 36 | 37 | self.retranslateUi(Form) 38 | QtCore.QMetaObject.connectSlotsByName(Form) 39 | 40 | def retranslateUi(self, Form): 41 | Form.setWindowTitle(QtCompat.translate("Form", "Form", None, -1)) 42 | self.btn_update.setText(QtCompat.translate("Form", "Update", None, -1)) 43 | self.pushButton_selectGrpMeshes.setText(QtCompat.translate("Form", "Select Meshes Grp", None, -1)) 44 | 45 | -------------------------------------------------------------------------------- /omtk/core/preferences.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide a Preference class to store the user preferences of the local installation. 3 | """ 4 | import os 5 | import inspect 6 | import json 7 | import logging 8 | 9 | log = logging.getLogger('omtk') 10 | 11 | from omtk import constants 12 | 13 | CONFIG_FILENAME = 'config.json' 14 | 15 | 16 | def get_path_preferences(): 17 | """ 18 | :return: The search path of the configuration file. 19 | """ 20 | current_dir = os.path.dirname(inspect.getfile(inspect.currentframe())) 21 | config_dir = os.path.abspath(os.path.join(current_dir, '..', '..')) 22 | config_path = os.path.join(config_dir, CONFIG_FILENAME) 23 | return config_path 24 | 25 | 26 | class Preferences(object): 27 | def __init__(self): 28 | self.default_rig = None 29 | 30 | def save(self, path=None): 31 | if path is None: 32 | path = get_path_preferences() 33 | 34 | data = self.__dict__ 35 | with open(path, 'w') as fp: 36 | json.dump(data, fp) 37 | 38 | def load(self, path=None): 39 | if path is None: 40 | path = get_path_preferences() 41 | 42 | if not path or not os.path.exists(path): 43 | log.warning("Can't find config file. Using default config.") 44 | return 45 | 46 | with open(path, 'r') as fp: 47 | data = json.load(fp) 48 | self.__dict__.update(data) 49 | 50 | def get_default_rig_class(self): 51 | from omtk.core import plugin_manager 52 | 53 | # Listen to an environment variable to drive the default rig for specific projects. 54 | default_rig = self.default_rig 55 | 56 | default_rig_override = os.environ.get(constants.EnvironmentVariables.OMTK_DEFAULT_RIG, None) 57 | if default_rig_override: 58 | default_rig = default_rig_override 59 | 60 | if default_rig: 61 | for plugin in plugin_manager.plugin_manager.iter_loaded_plugins_by_type('rigs'): 62 | if plugin.cls.__name__ == default_rig: 63 | return plugin.cls 64 | log.warning("Can't find default rig type {0}.".format(default_rig)) 65 | 66 | # If no match is found, return the base implementation 67 | from omtk.core import classRig 68 | return classRig.Rig 69 | 70 | 71 | preferences = Preferences() 72 | preferences.load() 73 | -------------------------------------------------------------------------------- /omtk/ui/widget_list_influences.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_list_influences.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(316, 295) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Form) 17 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 18 | self.verticalLayout.setObjectName("verticalLayout") 19 | self.horizontalLayout = QtWidgets.QHBoxLayout() 20 | self.horizontalLayout.setObjectName("horizontalLayout") 21 | self.lineEdit_search = QtWidgets.QLineEdit(Form) 22 | self.lineEdit_search.setObjectName("lineEdit_search") 23 | self.horizontalLayout.addWidget(self.lineEdit_search) 24 | self.btn_update = QtWidgets.QPushButton(Form) 25 | self.btn_update.setObjectName("btn_update") 26 | self.horizontalLayout.addWidget(self.btn_update) 27 | self.verticalLayout.addLayout(self.horizontalLayout) 28 | self.checkBox_hideAssigned = QtWidgets.QCheckBox(Form) 29 | self.checkBox_hideAssigned.setChecked(True) 30 | self.checkBox_hideAssigned.setObjectName("checkBox_hideAssigned") 31 | self.verticalLayout.addWidget(self.checkBox_hideAssigned) 32 | self.treeWidget = QtWidgets.QTreeWidget(Form) 33 | self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 34 | self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) 35 | self.treeWidget.setObjectName("treeWidget") 36 | self.treeWidget.headerItem().setText(0, "1") 37 | self.treeWidget.header().setVisible(False) 38 | self.verticalLayout.addWidget(self.treeWidget) 39 | 40 | self.retranslateUi(Form) 41 | QtCore.QMetaObject.connectSlotsByName(Form) 42 | 43 | def retranslateUi(self, Form): 44 | Form.setWindowTitle(QtCompat.translate("Form", "Form", None, -1)) 45 | self.btn_update.setText(QtCompat.translate("Form", "Update", None, -1)) 46 | self.checkBox_hideAssigned.setText(QtCompat.translate("Form", "Hide Assigned", None, -1)) 47 | 48 | -------------------------------------------------------------------------------- /omtk/ui/preferences_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 383 10 | 229 11 | 12 | 13 | 14 | OMTK - Preferences 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Default Rig Class 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Qt::Vertical 35 | 36 | 37 | 38 | 20 39 | 40 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Horizontal 48 | 49 | 50 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | buttonBox 60 | accepted() 61 | Dialog 62 | accept() 63 | 64 | 65 | 248 66 | 254 67 | 68 | 69 | 157 70 | 274 71 | 72 | 73 | 74 | 75 | buttonBox 76 | rejected() 77 | Dialog 78 | reject() 79 | 80 | 81 | 316 82 | 260 83 | 84 | 85 | 286 86 | 274 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /omtk/patches/patch_0_4_32_support_multiple_root_ctrls.py: -------------------------------------------------------------------------------- 1 | """ 2 | In omtk 0.4.32, we added support for multiple root ctrls. 3 | Some facial systems were using the root ctrl local transform which caused issues when having an hyerarchy of root ctrls. 4 | """ 5 | import logging 6 | import pymel.core as pymel 7 | import omtk 8 | from omtk.modules import rigFaceAvar 9 | from omtk.modules import rigFaceAvarGrps 10 | from omtk.models import model_ctrl_linear 11 | from omtk.libs import libRigging 12 | 13 | 14 | def _get_ctrl_model_parent_grp(ctrl_model): 15 | if ctrl_model and ctrl_model.is_built(): 16 | grp_rig = ctrl_model.grp_rig 17 | for child in grp_rig.getChildren(): 18 | if '_parent' in child.nodeName().lower(): 19 | return child 20 | 21 | 22 | def _grp_parent_need_fix(grp_parent): 23 | # We expect the grp_parent inputs to be a a decomposeMatrix node. 24 | obj_input = next(iter(grp_parent.translate.inputs()), None) 25 | return not isinstance(obj_input, pymel.nodetypes.DecomposeMatrix) 26 | 27 | 28 | def _patch_grp_parent(grp_parent, rig): 29 | u = libRigging.create_utility_node('decomposeMatrix', inputMatrix=rig.grp_anm.worldMatrix) 30 | pymel.connectAttr(u.outputTranslate, grp_parent.translate, force=True) 31 | pymel.connectAttr(u.outputRotate, grp_parent.rotate, force=True) 32 | pymel.connectAttr(u.outputScale, grp_parent.scale, force=True) 33 | 34 | 35 | def _iter_all_ctrl_models(): 36 | rigs = omtk.find() 37 | for rig in rigs: 38 | for module in rig.modules: 39 | if isinstance(module, rigFaceAvarGrps.AvarGrp) and module.get_version() < ( 40 | 0, 4, 32): # AvarGrp contain avars 41 | for avar in module._iter_all_avars(): 42 | if isinstance(avar, rigFaceAvar.AvarSimple): # AvarSimple introduced the ctrl_model attribute 43 | ctrl_model = avar.model_ctrl 44 | if isinstance(ctrl_model, model_ctrl_linear.ModelCtrlLinear): 45 | yield rig, ctrl_model 46 | 47 | 48 | def run(): 49 | for rig, ctrl_model in _iter_all_ctrl_models(): 50 | grp_parent = _get_ctrl_model_parent_grp(ctrl_model) 51 | if not grp_parent: 52 | logging.warning("Could not find a grp_parent for {0}".format(ctrl_model)) 53 | continue 54 | if _grp_parent_need_fix(grp_parent): 55 | logging.info("Patching {0}".format(grp_parent)) 56 | _patch_grp_parent(grp_parent, rig) 57 | 58 | -------------------------------------------------------------------------------- /tests/test_libFormula.py: -------------------------------------------------------------------------------- 1 | import mayaunittest 2 | from omtk.libs import libFormula 3 | import pymel.core as pymel 4 | 5 | class SampleTests(mayaunittest.TestCase): 6 | 7 | def _create_pymel_node(self, val_tx=1, val_ty=2, val_tz=3, val_rx=4, val_ry=5, val_rz=6, val_sx=7, val_sy=8, val_sz=9): 8 | t = pymel.createNode('transform') 9 | t.tx.set(val_tx) 10 | t.ty.set(val_ty) 11 | t.tz.set(val_tz) 12 | t.rx.set(val_rx) 13 | t.ry.set(val_ry) 14 | t.rz.set(val_rz) 15 | t.sx.set(val_sx) 16 | t.sy.set(val_sy) 17 | t.sz.set(val_sz) 18 | return t 19 | 20 | def _create_pymel_attrs(self, *args, **kwargs): 21 | t = self._create_pymel_node(*args, **kwargs) 22 | return t.tx, t.ty, t.tz, t.rx, t.ry, t.rz, t.sx, t.sy, t.sz 23 | 24 | def test_add(self): 25 | v = libFormula.parse('2+3') 26 | self.assertEqual(v, 5) 27 | 28 | def test_sub(self): 29 | v = libFormula.parse('5-3') 30 | self.assertEqual(v, 2) 31 | 32 | def test_mul(self): 33 | v = libFormula.parse('2*3') 34 | self.assertEqual(v, 6) 35 | 36 | def test_div(self): 37 | v = libFormula.parse('6/2') 38 | self.assertEqual(v, 3) 39 | 40 | def test_operation_priority(self): 41 | v = libFormula.parse("a+3*(6+(3*b))", a=4, b=7) 42 | self.assertEqual(v, 85) 43 | 44 | # usage of '-' 45 | v = libFormula.parse("-2^1.0*-1.0+3.3") 46 | self.assertEqual(v, 5.3) 47 | 48 | # usage of '-' 49 | v = libFormula.parse("-2*(1.0-(3^(3*-1.0)))") 50 | self.assertAlmostEqual(v, -1.925925925925926) 51 | 52 | def test_add_pymel(self): 53 | a, b, c, _, _, _, _, _, _ = self._create_pymel_attrs(1, 2, 3) 54 | result = libFormula.parse('a+b+c', a=a, b=b, c=c) 55 | self.assertEqual(result.get(), 6) 56 | 57 | def test_sub_pymel(self): 58 | a, b, c, _, _, _, _, _, _ = self._create_pymel_attrs(8, 2, 1) 59 | result = libFormula.parse('a-b-c', a=a, b=b, c=c) 60 | self.assertEqual(result.get(), 5) 61 | 62 | def test_mul_pymel(self): 63 | a, b, c, _, _, _, _, _, _ = self._create_pymel_attrs(2, 3, 4) 64 | result = libFormula.parse('a*b*c', a=a, b=b, c=c) 65 | self.assertEqual(result.get(), 24) 66 | 67 | # def test_add3D_pymel(self): 68 | # t = self._create_pymel_node(1, 2, 3, 4, 5, 6, 7, 8, 9) 69 | # a = t.t 70 | # b = t.r 71 | # c = t.s 72 | # result = libFormula.parse('a+b+c', a=a, b=b, c=c) 73 | # print(result.get()) 74 | 75 | -------------------------------------------------------------------------------- /omtk/ui_shared.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.vendor import libSerialization 4 | from omtk.vendor.Qt import QtGui 5 | 6 | 7 | # todo: Move to a shared location 8 | class MetadataType: 9 | """ 10 | Used to quickly determine what metadata have been monkey-patched to a QWidget. 11 | """ 12 | Rig = 0 13 | Module = 1 14 | Influence = 2 15 | Mesh = 3 16 | 17 | 18 | # http://forums.cgsociety.org/archive/index.php?t-1096914.html 19 | # Use the intern maya ressources icon 20 | _STYLE_SHEET = \ 21 | """ 22 | 23 | QTreeView::item::selected 24 | { 25 | background-color: highlight; 26 | color: rgb(40,40,40); 27 | } 28 | 29 | QTreeView::branch 30 | { 31 | selection-background-color: highlight; 32 | background-color: rgb(45,45,45); 33 | } 34 | 35 | QTreeView::branch:has-children:!has-siblings:closed, 36 | QTreeView::branch:closed:has-children:has-siblings 37 | { 38 | border-image: none; 39 | image: url(:/openObject.png); 40 | } 41 | 42 | QTreeView::branch:open:has-children:!has-siblings, 43 | QTreeView::branch:open:has-children:has-siblings 44 | { 45 | border-image: none; 46 | image: url(:/closeObject.png); 47 | } 48 | 49 | QTreeView::indicator:checked 50 | { 51 | image: url(:/checkboxOn.png); 52 | } 53 | 54 | QTreeView::indicator:unchecked 55 | { 56 | image: url(:/checkboxOff.png); 57 | } 58 | """ 59 | 60 | 61 | def set_icon_from_type(obj, qItem): 62 | if isinstance(obj, pymel.nodetypes.Joint): 63 | qItem.setIcon(0, QtGui.QIcon(":/pickJointObj.png")) 64 | elif isinstance(obj, pymel.nodetypes.Transform): 65 | set_icon_from_type(obj.getShape(), qItem) 66 | elif isinstance(obj, pymel.nodetypes.NurbsCurve): 67 | qItem.setIcon(0, QtGui.QIcon(":/nurbsCurve.svg")) 68 | elif isinstance(obj, pymel.nodetypes.NurbsSurface): 69 | qItem.setIcon(0, QtGui.QIcon(":/nurbsSurface.svg")) 70 | elif isinstance(obj, pymel.nodetypes.Mesh): 71 | qItem.setIcon(0, QtGui.QIcon(":/mesh.svg")) 72 | else: 73 | qItem.setIcon(0, QtGui.QIcon(":/question.png")) 74 | 75 | 76 | def _update_network(module, item=None): 77 | if hasattr(module, "_network"): 78 | pymel.delete(module._network) 79 | new_network = libSerialization.export_network(module) # TODO : Automatic update 80 | # If needed, update the network item net property to match the new exported network 81 | if item: 82 | item.net = new_network 83 | return new_network 84 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/cache.py: -------------------------------------------------------------------------------- 1 | from decorators import memoized 2 | from .core import get_class_namespace, get_class_module_root 3 | 4 | def iter_subclasses_recursive(cls): 5 | yield cls 6 | 7 | try: 8 | for sub_cls in cls.__subclasses__(): 9 | for x in iter_subclasses_recursive(sub_cls): 10 | yield x 11 | except TypeError: # This will fail when encountering the 'type' datatype. 12 | pass 13 | 14 | def iter_module_subclasses_recursive(module_root, cls): 15 | for sub_cls in iter_subclasses_recursive(cls): 16 | cur_module_root = get_class_module_root(sub_cls) 17 | if module_root == cur_module_root: 18 | yield sub_cls 19 | 20 | class Cache(object): 21 | def __init__(self): 22 | self.classes = None 23 | self._cache_import_by_id = {} 24 | self._cache_networks_by_id = {} # todo: merge with _cache_import_by_id 25 | 26 | @memoized 27 | def _get_cls_cache_by_module(self, module_name, base_class=object): 28 | i = iter_module_subclasses_recursive(module_name, base_class) 29 | result = {} 30 | for cls in i: 31 | result[cls.__name__] = cls 32 | return result 33 | 34 | @memoized 35 | def _get_cls_cache(self, base_class=object): 36 | i = iter_subclasses_recursive(base_class) 37 | result = {} 38 | for cls in i: 39 | result[cls.__name__] = cls 40 | return result 41 | 42 | def get_class_by_name(self, cls_name, module_name=None, base_class=object): 43 | if module_name is None: 44 | cache = self._get_cls_cache(base_class=base_class) 45 | else: 46 | cache = self._get_cls_cache_by_module(module_name=module_name, base_class=base_class) 47 | return cache.get(cls_name, None) 48 | 49 | def get_class_by_namespace(self, cls_namespace, module_name=None, base_class=object): 50 | if module_name is None: 51 | cache = self._get_cls_cache(base_class=base_class) 52 | else: 53 | cache = self._get_cls_cache_by_module(base_class=base_class) 54 | for cls in cache.values(): 55 | cur_namespace = get_class_namespace(cls) 56 | if cls_namespace == cur_namespace: 57 | return cls 58 | 59 | def get_import_value_by_id(self, id, default=None): 60 | return self._cache_import_by_id.get(id, default) 61 | 62 | def set_import_value_by_id(self, id, val): 63 | self._cache_import_by_id[id] = val 64 | 65 | def get_network_by_id(self, id, default=None): 66 | return self._cache_networks_by_id.get(id, default) 67 | 68 | def set_network_by_id(self, id, net): 69 | self._cache_networks_by_id[id] = net -------------------------------------------------------------------------------- /omtk/ui/widget_component_wizard_parts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_component_wizard_parts.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(400, 300) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Form) 17 | self.verticalLayout.setObjectName("verticalLayout") 18 | self.tableView = QtWidgets.QTableView(Form) 19 | self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 20 | self.tableView.setObjectName("tableView") 21 | self.tableView.horizontalHeader().setStretchLastSection(True) 22 | self.verticalLayout.addWidget(self.tableView) 23 | self.horizontalLayout = QtWidgets.QHBoxLayout() 24 | self.horizontalLayout.setObjectName("horizontalLayout") 25 | self.pushButton_add = QtWidgets.QPushButton(Form) 26 | self.pushButton_add.setObjectName("pushButton_add") 27 | self.horizontalLayout.addWidget(self.pushButton_add) 28 | self.pushButton_remove = QtWidgets.QPushButton(Form) 29 | self.pushButton_remove.setObjectName("pushButton_remove") 30 | self.horizontalLayout.addWidget(self.pushButton_remove) 31 | self.verticalLayout.addLayout(self.horizontalLayout) 32 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 33 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 34 | self.pushButton_connect = QtWidgets.QPushButton(Form) 35 | self.pushButton_connect.setObjectName("pushButton_connect") 36 | self.horizontalLayout_2.addWidget(self.pushButton_connect) 37 | self.pushButton_disconnect = QtWidgets.QPushButton(Form) 38 | self.pushButton_disconnect.setObjectName("pushButton_disconnect") 39 | self.horizontalLayout_2.addWidget(self.pushButton_disconnect) 40 | self.verticalLayout.addLayout(self.horizontalLayout_2) 41 | 42 | self.retranslateUi(Form) 43 | QtCore.QMetaObject.connectSlotsByName(Form) 44 | 45 | def retranslateUi(self, Form): 46 | Form.setWindowTitle(QtCompat.translate("Form", "Form", None, -1)) 47 | self.pushButton_add.setText(QtCompat.translate("Form", "Add", None, -1)) 48 | self.pushButton_remove.setText(QtCompat.translate("Form", "Remove", None, -1)) 49 | self.pushButton_connect.setText(QtCompat.translate("Form", "Connect", None, -1)) 50 | self.pushButton_disconnect.setText(QtCompat.translate("Form", "Disconnect", None, -1)) 51 | 52 | -------------------------------------------------------------------------------- /omtk/libs/libUtils.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | import logging 3 | from omtk.libs import libSkinning 4 | 5 | ''' 6 | This method will create a locator at the center of the selection. 7 | Support : Transform objects, vertices, faces and edges selection 8 | ''' 9 | 10 | 11 | def get_center(objs): 12 | pos = pymel.datatypes.Point() 13 | count = 0 14 | for obj in objs: 15 | if isinstance(obj, pymel.general.MeshVertex): 16 | for vert in obj: 17 | pos += vert.getPosition(space='world') 18 | count += 1 19 | elif isinstance(obj, pymel.nodetypes.Transform): 20 | pos += obj.getTranslation(space="world") 21 | count += 1 22 | elif isinstance(obj, pymel.general.MeshEdge): 23 | pos += obj.getPoint(0, space="world") 24 | pos += obj.getPoint(1, space="world") 25 | count += 2 26 | elif isinstance(obj, pymel.general.MeshFace): 27 | aPointVtx = obj.getPoints(space="world") 28 | for oPointVtx in aPointVtx: 29 | pos += oPointVtx 30 | count += 1 31 | elif isinstance(obj, pymel.general.NurbsCurveCV): 32 | pos += obj.getPosition(space="world") 33 | count += 1 34 | else: 35 | logging.warning("Unsupported data type ({0}), will be skipped".format(type(obj))) 36 | if count != 0: 37 | pos /= count 38 | return pos 39 | 40 | 41 | def createLocToCenter(): 42 | old_selection = pymel.selected() 43 | p3Pos = get_center(pymel.selected(flatten=True)) 44 | pPoint = pymel.general.spaceLocator() 45 | pPoint.setTranslation(p3Pos, space='world') 46 | if old_selection: 47 | pymel.select(old_selection) 48 | 49 | 50 | '''Snap two or more objects with the last selected using their world matrix''' 51 | 52 | 53 | def snapObj(): 54 | aSelection = pymel.selected() 55 | if len(aSelection) < 2: 56 | pymel.error("Select at least two objects") 57 | 58 | aSources = aSelection[:-1] 59 | oTarget = aSelection[-1] 60 | 61 | for oCurSource in aSources: 62 | pMatrixToMatch = oTarget.getMatrix(worldSpace=True) 63 | oCurSource.setMatrix(pMatrixToMatch, worldSpace=True) 64 | 65 | 66 | '''Get skin cluster attach to an objects''' 67 | 68 | 69 | def getSkinCluster(_oObj): 70 | for oCurHistory in pymel.listHistory(_oObj): 71 | if isinstance(oCurHistory, pymel.nodetypes.SkinCluster): 72 | return oCurHistory 73 | return None 74 | 75 | 76 | '''Get bone included in a skin''' 77 | 78 | 79 | def getSkinBones(): 80 | aInfluences = [] 81 | for oCurObj in pymel.selected(): 82 | oSkinCluster = getSkinCluster(oCurObj) 83 | if oSkinCluster is not None: 84 | aInfluences += libSkinning.get_skin_cluster_influence_objects(oSkinCluster) 85 | pymel.select(aInfluences) 86 | -------------------------------------------------------------------------------- /omtk/modules/rigNeck.py: -------------------------------------------------------------------------------- 1 | from omtk.modules import rigFK 2 | from omtk.modules import rigTwistbone 3 | from omtk.core.utils import decorator_uiexpose 4 | 5 | 6 | class CtrlNeck(rigFK.CtrlFk): 7 | pass 8 | 9 | 10 | class Neck(rigFK.FK): 11 | """ 12 | Simple FK setup with twistbone support. 13 | """ 14 | 15 | def __init__(self, *args, **kwarg): 16 | super(Neck, self).__init__(*args, **kwarg) 17 | self.create_twist = True 18 | self.sys_twist = None 19 | 20 | _CLS_CTRL = CtrlNeck 21 | _CLASS_SYS_TWIST = rigTwistbone.Twistbone 22 | _NAME_CTRL_MERGE = True # By default we only expect one controller for the head. (Head_Ctrl > than Head_Head_Ctrl) 23 | _NAME_CTRL_ENUMERATE = True # If we find additional influences, we'll use enumeration. 24 | 25 | def build(self, *args, **kwargs): 26 | super(Neck, self).build(create_grp_rig=True, *args, **kwargs) 27 | 28 | # Create twistbone system if needed 29 | if self.create_twist: 30 | jnt_s = self.jnt 31 | jnt_e = self.get_head_jnt() 32 | 33 | twist_nomenclature = self.get_nomenclature().copy() 34 | twist_nomenclature.add_tokens('bend') 35 | 36 | self.sys_twist = self.init_module(self._CLASS_SYS_TWIST, self.sys_twist, inputs=[jnt_s, jnt_e]) 37 | self.sys_twist.name = twist_nomenclature.resolve() 38 | self.sys_twist.build(num_twist=3, create_bend=True) 39 | if self.sys_twist.grp_anm: 40 | self.sys_twist.grp_anm.setParent(self.grp_anm) 41 | self.sys_twist.grp_rig.setParent(self.grp_rig) 42 | 43 | def unbuild(self): 44 | if self.sys_twist: 45 | self.sys_twist.unbuild() 46 | 47 | super(Neck, self).unbuild() 48 | 49 | def validate(self): 50 | """ 51 | Allow the ui to know if the module is valid to be builded or not 52 | :return: True or False depending if it pass the building validation 53 | """ 54 | super(Neck, self).validate() 55 | num_jnts = len(self.jnts) 56 | 57 | if num_jnts != 1: 58 | raise Exception("Expected only one influences, got {}".format(num_jnts)) 59 | 60 | head_jnt = self.get_head_jnt() 61 | if not head_jnt: 62 | raise Exception("Cannot resolve Head influence from {}".format(self.jnt)) 63 | 64 | return True 65 | 66 | @decorator_uiexpose() 67 | def assign_twist_weights(self): 68 | for module in self.sys_twist: 69 | if isinstance(module, rigTwistbone.Twistbone) and module.is_built(): 70 | module.assign_twist_weights() 71 | 72 | @decorator_uiexpose() 73 | def unassign_twist_weights(self): 74 | for module in self.sys_twist: 75 | if isinstance(module, rigTwistbone.Twistbone) and module.is_built(): 76 | module.unassign_twist_weights() 77 | 78 | 79 | def register_plugin(): 80 | return Neck 81 | -------------------------------------------------------------------------------- /omtk/widget_create_component.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper UI to create "Component". 3 | Component are a concept of OMTK2 backported to OMTK1 for convinience. 4 | """ 5 | 6 | from ui import widget_create_component 7 | 8 | reload(widget_create_component) 9 | from omtk.vendor import libSerialization 10 | from omtk.libs import libComponent 11 | 12 | import pymel.core as pymel 13 | 14 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets 15 | 16 | 17 | def create_offset_grp(obj, suffix='offset'): 18 | offset = pymel.createNode('transform', name=(obj.name() + '_' + suffix)) 19 | offset.setMatrix(obj.getMatrix(worldSpace=True), worldSpace=True) 20 | offset.setParent(obj.getParent()) 21 | obj.setParent(offset) 22 | return offset 23 | 24 | 25 | class WidgetCreateComponent(QtWidgets.QMainWindow): 26 | def __init__(self): 27 | super(WidgetCreateComponent, self).__init__() 28 | 29 | self.ui = widget_create_component.Ui_MainWindow() 30 | self.ui.setupUi(self) 31 | 32 | self._wizard_network = None 33 | self._wizard = None 34 | wizard = self.import_() 35 | if not wizard: 36 | wizard = libComponent.ComponentWizard() 37 | wizard.initialize() 38 | 39 | self.set_wizard(wizard) 40 | 41 | self.export() 42 | 43 | self.ui.widget_view_ctrl.onNetworkChanged.connect(self.on_network_changed) 44 | self.ui.widget_view_infl.onNetworkChanged.connect(self.on_network_changed) 45 | self.ui.widget_view_guid.onNetworkChanged.connect(self.on_network_changed) 46 | 47 | def set_wizard(self, wizard): 48 | # type: (libComponent.ComponentWizard) -> None 49 | assert (isinstance(wizard, libComponent.ComponentWizard)) 50 | self._wizard = wizard 51 | self.ui.widget_view_ctrl.set_entries(wizard, libComponent.ComponentPartCtrl, wizard.parts_ctrl) 52 | self.ui.widget_view_infl.set_entries(wizard, libComponent.ComponentPartInfluence, wizard.parts_influences) 53 | self.ui.widget_view_guid.set_entries(wizard, libComponent.ComponentPartGuide, wizard.parts_guides) 54 | 55 | def reset(self): 56 | self.model_ctrl.reset() 57 | self.model_guid.reset() 58 | self.model_infl.reset() 59 | 60 | def on_network_changed(self): 61 | self.export() 62 | 63 | def import_(self): 64 | networks = libSerialization.get_networks_from_class(libComponent.ComponentWizard.__name__) 65 | if not networks: 66 | return 67 | network = networks[0] 68 | self._wizard_network = network 69 | wizard = libSerialization.import_network(network) 70 | return wizard 71 | 72 | def export(self): 73 | if self._wizard_network: 74 | pymel.delete(self._wizard_network) 75 | self._wizard_network = libSerialization.export_network(self._wizard) 76 | 77 | 78 | _gui = None 79 | 80 | 81 | def show(): 82 | global _gui 83 | _gui = WidgetCreateComponent() 84 | _gui.show() 85 | -------------------------------------------------------------------------------- /omtk/ui/widget_logger.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 435 10 | 300 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 25 | Logs 26 | 27 | 28 | 29 | 30 | 31 | 32 | Qt::Horizontal 33 | 34 | 35 | 36 | 40 37 | 20 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Search 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Level: 56 | 57 | 58 | 59 | 60 | 61 | 62 | 1 63 | 64 | 65 | 66 | Error 67 | 68 | 69 | 70 | 71 | Warning 72 | 73 | 74 | 75 | 76 | Info 77 | 78 | 79 | 80 | 81 | Debug 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Save 90 | 91 | 92 | 93 | 94 | 95 | 96 | Clear 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /tests/test_animtools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mayaunittest 3 | from maya import cmds 4 | import pymel.core as pymel 5 | import omtk 6 | import omtk 7 | from omtk.vendor import libSerialization 8 | from omtk.libs import libRigging 9 | from omtk.modules.rigArm import Arm 10 | from omtk.animation import ikfkTools 11 | from omtk.animation import ikfkTools 12 | 13 | class SampleTests(mayaunittest.TestCase): 14 | 15 | def _create_simple_arm_build_and_export(self): 16 | jnt_1 = pymel.joint(position=[0,0,0]) 17 | jnt_2 = pymel.joint(position=[10,0,10]) 18 | jnt_3 = pymel.joint(position=[20,0,0]) 19 | 20 | rig = omtk.create() 21 | module = Arm([jnt_1, jnt_2, jnt_3]) 22 | rig.add_module(module) 23 | rig.build() 24 | libSerialization.export_network(rig) 25 | 26 | return rig, module 27 | 28 | def _verify_ik_fk_match(self, module): 29 | ctrl_ik = module.sysIK.ctrl_ik.node 30 | ctrl_swivel = module.sysIK.ctrl_swivel.node 31 | ctrl_fk_01 = module.sysFK.ctrls[0].node 32 | ctrl_fk_02 = module.sysFK.ctrls[1].node 33 | ctrl_fk_03 = module.sysFK.ctrls[2].node 34 | 35 | ctrl_ik_pos = ctrl_ik.getTranslation(space='world') 36 | ctrl_swivel_pos = ctrl_swivel.getTranslation(space='world') 37 | ctrl_fk_01_pos = ctrl_fk_01.getTranslation(space='world') 38 | ctrl_fk_02_pos = ctrl_fk_02.getTranslation(space='world') 39 | ctrl_fk_03_pos = ctrl_fk_03.getTranslation(space='world') 40 | 41 | # Verify IK ctrl 42 | self.assertAlmostEqual((ctrl_ik_pos - ctrl_fk_03_pos).length(), 0, places=3) 43 | 44 | # Verify IK swivel 45 | look_vec = ctrl_fk_03_pos - ctrl_fk_01_pos 46 | upp_vec = ctrl_fk_02_pos - ctrl_fk_01_pos 47 | tm = libRigging.get_matrix_from_direction(look_vec, upp_vec, 48 | look_axis=pymel.datatypes.Vector.xAxis, 49 | upp_axis=pymel.datatypes.Vector.zAxis 50 | ) 51 | distorsion = (ctrl_swivel_pos * tm.inverse()).y 52 | self.assertAlmostEqual(distorsion, 0) 53 | 54 | def test_ikfk_switch(self): 55 | rig, module = self._create_simple_arm_build_and_export() 56 | 57 | ctrl_ik = module.sysIK.ctrl_ik.node 58 | 59 | ctrl_fk_01 = module.sysFK.ctrls[0].node 60 | ctrl_fk_02 = module.sysFK.ctrls[1].node 61 | ctrl_fk_03 = module.sysFK.ctrls[2].node 62 | 63 | # Do a brainless IK->FK->IK switch to ensure nothing is moving. 64 | pymel.select(ctrl_ik) 65 | ikfkTools.switchToFk() 66 | self._verify_ik_fk_match(module) 67 | ikfkTools.switchToIk() 68 | self._verify_ik_fk_match(module) 69 | 70 | # Modify IK pose and perform an IK-FK switch 71 | ctrl_ik.setTranslation([10,0,0], space='world') 72 | pymel.select(ctrl_ik) 73 | ikfkTools.switchToFk() 74 | self._verify_ik_fk_match(module) 75 | 76 | # Modify FK pose and perform an FK->IK switch 77 | ctrl_fk_01.rotateX.set(45) 78 | ctrl_fk_02.rotateY.set(-45) 79 | pymel.select(ctrl_fk_02) 80 | ikfkTools.switchToIk() 81 | self._verify_ik_fk_match(module) 82 | -------------------------------------------------------------------------------- /omtk/modules/rigArm.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk import constants 4 | from omtk.modules import rigIK 5 | from omtk.modules import rigLimb 6 | from omtk.libs import libCtrlShapes 7 | from omtk.libs import libRigging 8 | 9 | 10 | class CtrlIkArm(rigIK.CtrlIk): 11 | def __createNode__(self, *args, **kwargs): 12 | return libCtrlShapes.create_shape_box_arm(*args, **kwargs) 13 | 14 | 15 | def get_spaceswitch_targets(self, module, *args, **kwargs): 16 | targets, labels, indexes = super(CtrlIkArm, self).get_spaceswitch_targets(module, *args, **kwargs) 17 | jnt_head = module.get_head_jnt() 18 | if jnt_head: 19 | targets.append(jnt_head) 20 | labels.append(None) 21 | indexes.append(self.get_bestmatch_index(jnt_head)) 22 | return targets, labels, indexes 23 | 24 | 25 | class ArmIk(rigIK.IK): 26 | _CLASS_CTRL_IK = CtrlIkArm 27 | SHOW_IN_UI = False 28 | 29 | def _get_ik_ctrl_bound_refs_raycast(self): 30 | """ 31 | Resolve what objects to use for computing the bound of the ik ctrl using raycasts. 32 | This also use the first phalanges to have a more precise bound height. 33 | :return: An array of pymel.general.PyNode instances. 34 | """ 35 | jnt_hand = self.input[self.iCtrlIndex] 36 | return [jnt_hand] + jnt_hand.getChildren() 37 | 38 | def _get_ik_ctrl_tms(self): 39 | """ 40 | Compute the desired rotation for the ik ctrl. 41 | If the LEGACY_ARM_IK_CTRL_ORIENTATION is set, we'll simply align to the influence. 42 | :return: A two-size tuple containing the transformation matrix for the ctrl offset and the ctrl itself. 43 | """ 44 | if self.rig.LEGACY_ARM_IK_CTRL_ORIENTATION: 45 | return super(ArmIk, self)._get_ik_ctrl_tms() 46 | 47 | inf_tm = self.input[self.iCtrlIndex].getMatrix(worldSpace=True) 48 | side = self.get_side() 49 | 50 | # Resolve offset_tm 51 | offset_tm = pymel.datatypes.Matrix() 52 | 53 | # Resolve ctrl_tm 54 | axis_dir = constants.Axis.x 55 | axis_upp = self.rig.DEFAULT_UPP_AXIS # normally the z axis 56 | inn_tm_dir = libRigging.get_matrix_axis(inf_tm, axis_dir) 57 | inn_tm_upp = libRigging.get_matrix_axis(inf_tm, axis_upp) 58 | 59 | ctrl_tm = libRigging.get_matrix_from_direction( 60 | inn_tm_dir, 61 | inn_tm_upp, 62 | look_axis=pymel.datatypes.Vector(1, 0, 0), 63 | upp_axis=pymel.datatypes.Vector(0, -1, 64 | 0) if side == self.rig.nomenclature.SIDE_R else pymel.datatypes.Vector(0, 1, 65 | 0) 66 | ) 67 | ctrl_tm.translate = inf_tm.translate 68 | 69 | return offset_tm, ctrl_tm 70 | 71 | 72 | class Arm(rigLimb.Limb): 73 | """ 74 | IK/FK Setup customized for Arm riging. 75 | """ 76 | _CLASS_SYS_IK = ArmIk 77 | 78 | def __init__(self, *args, **kwargs): 79 | super(Arm, self).__init__(*args, **kwargs) 80 | self.sysFootRoll = None 81 | 82 | 83 | def register_plugin(): 84 | return Arm 85 | -------------------------------------------------------------------------------- /omtk/vendor/libSerialization/plugin_maya_json.py: -------------------------------------------------------------------------------- 1 | # 2 | # Define a custom JSON Encoder that can be used to export PyNode and Attribute. 3 | # 4 | from maya import cmds 5 | import pymel.core as pymel 6 | import json 7 | from plugin_json import export_json 8 | from plugin_json import export_json_file 9 | from plugin_json import import_json 10 | from plugin_json import import_json_file 11 | __all__ = ( 12 | 'export_json_maya', 13 | 'export_json_file_maya', 14 | 'import_json_maya', 15 | 'import_json_file_maya' 16 | ) 17 | 18 | 19 | # TODO: Add support for matrix and vector datatypes 20 | class PymelJSONEncoder(json.JSONEncoder): 21 | def default(self, o): 22 | if isinstance(o, pymel.PyNode): 23 | return {'_class_pymel':'pymel.PyNode', '__melobject__': o.__melobject__()} 24 | elif isinstance(o, pymel.Attribute): 25 | return {'_class_pymel':'pymel.Attribute', '__melobject__': o.__melobject__()} 26 | elif isinstance(o, pymel.datatypes.Matrix): 27 | return {'_class_pymel':'pymel.datatypes.Matrix', '__melobject__': o.__melobject__()} 28 | elif isinstance(o, pymel.datatypes.Vector): 29 | return {'_class_pymel':'pymel.datatypes.Vector', '__melobject__': [o.x, o.y, o.z]} 30 | elif isinstance(o, pymel.datatypes.Point): 31 | return {'_class_pymel':'pymel.datatypes.Point', '__melobject__': [o.w, o.x, o.y, o.z]} 32 | else: 33 | return super(PymelJSONEncoder, self).default(o) 34 | 35 | 36 | # TODO: Add support for matrix and vector datatypes 37 | class PymelJSONDecoder(json.JSONDecoder): 38 | def __init__(self, *args, **kwargs): 39 | super(PymelJSONDecoder, self).__init__(object_hook=self.object_hook, *args, **kwargs) 40 | 41 | def object_hook(self, val): 42 | if isinstance(val, dict): 43 | cls = val.get('_class_pymel', None) 44 | if cls == 'pymel.PyNode': 45 | dagpath = val.get('__melobject__') 46 | val = pymel.PyNode(dagpath) if cmds.objExists(dagpath) else None # TODO: add warning? 47 | elif cls == 'pymel.Attribute': 48 | dagpath = val.get('__melobject__') 49 | val = pymel.Attribute(dagpath) if cmds.objExists(dagpath) else None # TODO: add warning? 50 | elif cls == 'pymel.datatypes.Matrix': 51 | melval = val.get('__melobject__') 52 | val = pymel.datatypes.Matrix(melval) 53 | elif cls == 'pymel.datatypes.Vector': 54 | coords = val.get('__melobject__') 55 | val = pymel.datatypes.Vector(coords) 56 | elif cls == 'pymel.datatypes.Point': 57 | coords = val.get('__melobject__') 58 | val = pymel.datatypes.Point(coords) 59 | return val 60 | 61 | def export_json_maya(*args, **kwargs): 62 | return export_json(cls=PymelJSONEncoder, *args, **kwargs) 63 | 64 | def export_json_file_maya(*args, **kwargs): 65 | return export_json_file(cls=PymelJSONEncoder, *args, **kwargs) 66 | 67 | def import_json_maya(*args, **kwargs): 68 | return import_json(cls=PymelJSONDecoder, *args, **kwargs) 69 | 70 | def import_json_file_maya(*args, **kwargs): 71 | return import_json_file(cls=PymelJSONDecoder, *args, **kwargs) -------------------------------------------------------------------------------- /omtk/ui/pluginmanager_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | mainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 485 10 | 391 11 | 12 | 13 | 14 | OMTK - Plugin Manager 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | QAbstractItemView::SelectRows 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Reload 37 | 38 | 39 | 40 | 41 | 42 | 43 | Qt::Horizontal 44 | 45 | 46 | 47 | 40 48 | 20 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 0 62 | 485 63 | 28 64 | 65 | 66 | 67 | 68 | 69 | 70 | Reload 71 | 72 | 73 | 74 | 75 | SearchQueryChanged 76 | 77 | 78 | 79 | 80 | 81 | 82 | pushButton_reload 83 | released() 84 | actionReload 85 | trigger() 86 | 87 | 88 | 52 89 | 347 90 | 91 | 92 | -1 93 | -1 94 | 95 | 96 | 97 | 98 | lineEdit_search 99 | textChanged(QString) 100 | actionSearchQueryChanged 101 | trigger() 102 | 103 | 104 | 242 105 | 52 106 | 107 | 108 | -1 109 | -1 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /omtk/modules/rigSplineIK.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.core.classModule import Module 4 | from omtk.libs import libRigging, libPymel 5 | 6 | 7 | # Todo: Support more complex IK limbs (ex: 2 knees) 8 | class SplineIK(Module): 9 | """ 10 | Generic ik setup on a spline. 11 | """ 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(SplineIK, self).__init__(*args, **kwargs) 15 | self.bStretch = True 16 | self.iCtrlIndex = 2 17 | self.ikEffector = None 18 | self.ikHandle = None 19 | 20 | def build(self, stretch=True, squash=False, *args, **kwargs): 21 | # TODO: Use self.chain_jnt 22 | self._joints = [input for input in self.input if libPymel.isinstance_of_transform(input, pymel.nodetypes.Joint)] 23 | self._curves = [input for input in self.input if 24 | libPymel.isinstance_of_shape(input, pymel.nodetypes.CurveShape)] 25 | 26 | if len(self._joints) < 2: 27 | raise Exception("Can't build SplineIK. Expected at least two joints, got {0}".format(self._joints)) 28 | if len(self._curves) < 1: 29 | raise Exception("Can't build SplineIK. Expected at least one nurbsCurve, got {0}".format(self._curves)) 30 | 31 | super(SplineIK, self).build(*args, **kwargs) 32 | 33 | nomenclature_rig = self.get_nomenclature_rig() 34 | 35 | # todo: handle multiple curves? 36 | curve = next(iter(self._curves), None) 37 | curve_shape = next((shape for shape in curve.getShapes() if isinstance(shape, pymel.nodetypes.NurbsCurve)), 38 | None) 39 | 40 | # Create ik solver 41 | handle_name = nomenclature_rig.resolve('ikHandle') 42 | eff_name = nomenclature_rig.resolve('ikEffector') 43 | self.ikHandle, self.ikEffector = pymel.ikHandle( 44 | solver="ikSplineSolver", 45 | curve=curve, 46 | startJoint=self._joints[0], 47 | endEffector=self._joints[-1], 48 | createCurve=False, 49 | name=handle_name, 50 | parentCurve=False, 51 | snapCurve=False) 52 | self.ikHandle.setParent(self.grp_rig) 53 | self.ikEffector.rename(eff_name) 54 | 55 | # Create stretch 56 | # Todo: use shape instead of transform as curve input? 57 | if stretch: 58 | stretch_attr = libRigging.create_strech_attr_from_curve(curve_shape) 59 | for jnt in self._joints: 60 | pymel.connectAttr(stretch_attr, jnt.sx, force=True) 61 | 62 | # Create squash 63 | if squash: 64 | num_joints = len(self._joints) 65 | squash_attrs = libRigging.create_squash_atts(stretch_attr, num_joints) 66 | # Todo: Find correct axis orient 67 | for jnt, squash in zip(self._joints, squash_attrs): 68 | pymel.connectAttr(squash, jnt.sy, force=True) 69 | pymel.connectAttr(squash, jnt.sz, force=True) 70 | 71 | def unbuild(self): 72 | # hack: the ikEffector is parented to the bone chain and need to be deleted manually 73 | if libPymel.is_valid_PyNode(self.ikEffector): 74 | pymel.delete(self.ikEffector) 75 | 76 | super(SplineIK, self).unbuild() 77 | 78 | 79 | def register_plugin(): 80 | return SplineIK 81 | -------------------------------------------------------------------------------- /shelf_omtk_anim.mel: -------------------------------------------------------------------------------- 1 | global proc shelf_omtk_anim () { 2 | global string $gBuffStr; 3 | global string $gBuffStr0; 4 | global string $gBuffStr1; 5 | 6 | shelfButton 7 | -enableCommandRepeat 1 8 | -enable 1 9 | -width 35 10 | -height 35 11 | -manage 1 12 | -visible 1 13 | -preventOverride 0 14 | -annotation "from omtk.animation import ikfkTools\nikfkTools.switchToIk()" 15 | -enableBackground 0 16 | -align "center" 17 | -label "from omtk.animation import ikfkTools\nikfkTools.switchToIk()" 18 | -labelOffset 0 19 | -rotation 0 20 | -flipX 0 21 | -flipY 0 22 | -useAlpha 1 23 | -font "plainLabelFont" 24 | -overlayLabelColor 0.8 0.8 0.8 25 | -overlayLabelBackColor 0 0 0 0.2 26 | -image "omtk_anm_switch_ik.png" 27 | -image1 "omtk_anm_switch_ik.png" 28 | -style "iconOnly" 29 | -marginWidth 1 30 | -marginHeight 1 31 | -command "from omtk.animation import ikfkTools\nikfkTools.switchToIk()" 32 | -sourceType "python" 33 | -commandRepeatable 1 34 | -flat 1 35 | ; 36 | shelfButton 37 | -enableCommandRepeat 1 38 | -enable 1 39 | -width 35 40 | -height 35 41 | -manage 1 42 | -visible 1 43 | -preventOverride 0 44 | -annotation "from omtk.animation import ikfkTools\nikfkTools.switchToFk()" 45 | -enableBackground 0 46 | -align "center" 47 | -label "from omtk.animation import ikfkTools\nikfkTools.switchToFk()" 48 | -labelOffset 0 49 | -rotation 0 50 | -flipX 0 51 | -flipY 0 52 | -useAlpha 1 53 | -font "plainLabelFont" 54 | -overlayLabelColor 0.8 0.8 0.8 55 | -overlayLabelBackColor 0 0 0 0.2 56 | -image "omtk_anm_switch_fk.png" 57 | -image1 "omtk_anm_switch_fk.png" 58 | -style "iconOnly" 59 | -marginWidth 1 60 | -marginHeight 1 61 | -command "from omtk.animation import ikfkTools\nikfkTools.switchToFk()" 62 | -sourceType "python" 63 | -commandRepeatable 1 64 | -flat 1 65 | ; 66 | shelfButton 67 | -enableCommandRepeat 1 68 | -enable 1 69 | -width 35 70 | -height 35 71 | -manage 1 72 | -visible 1 73 | -preventOverride 0 74 | -annotation "from omtk.animation import mirrorPose; reload(mirrorPose)\nimport pymel.core as pymel\nmirrorPose.mirror_objs(pymel.selected())" 75 | -enableBackground 0 76 | -align "center" 77 | -label "from omtk.animation import mirrorPose; reload(mirrorPose)\nimport..." 78 | -labelOffset 0 79 | -rotation 0 80 | -flipX 0 81 | -flipY 0 82 | -useAlpha 1 83 | -font "plainLabelFont" 84 | -overlayLabelColor 0.8 0.8 0.8 85 | -overlayLabelBackColor 0 0 0 0.2 86 | -image "omtk_anm_ctrl_mirror.png" 87 | -image1 "omtk_anm_ctrl_mirror.png" 88 | -style "iconOnly" 89 | -marginWidth 1 90 | -marginHeight 1 91 | -command "from omtk.animation import mirrorPose; reload(mirrorPose)\nimport pymel.core as pymel\nmirrorPose.mirror_objs(pymel.selected())" 92 | -sourceType "python" 93 | -commandRepeatable 1 94 | -flat 1 95 | ; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /omtk/ui/pluginmanager_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/pluginmanager_window.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_mainWindow(object): 13 | def setupUi(self, mainWindow): 14 | mainWindow.setObjectName("mainWindow") 15 | mainWindow.resize(485, 391) 16 | self.centralwidget = QtWidgets.QWidget(mainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) 19 | self.verticalLayout.setObjectName("verticalLayout") 20 | self.lineEdit_search = QtWidgets.QLineEdit(self.centralwidget) 21 | self.lineEdit_search.setObjectName("lineEdit_search") 22 | self.verticalLayout.addWidget(self.lineEdit_search) 23 | self.tableView = QtWidgets.QTableView(self.centralwidget) 24 | self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 25 | self.tableView.setObjectName("tableView") 26 | self.tableView.horizontalHeader().setStretchLastSection(True) 27 | self.verticalLayout.addWidget(self.tableView) 28 | self.horizontalLayout = QtWidgets.QHBoxLayout() 29 | self.horizontalLayout.setObjectName("horizontalLayout") 30 | self.pushButton_reload = QtWidgets.QPushButton(self.centralwidget) 31 | self.pushButton_reload.setObjectName("pushButton_reload") 32 | self.horizontalLayout.addWidget(self.pushButton_reload) 33 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 34 | self.horizontalLayout.addItem(spacerItem) 35 | self.verticalLayout.addLayout(self.horizontalLayout) 36 | mainWindow.setCentralWidget(self.centralwidget) 37 | self.menubar = QtWidgets.QMenuBar(mainWindow) 38 | self.menubar.setGeometry(QtCore.QRect(0, 0, 485, 28)) 39 | self.menubar.setObjectName("menubar") 40 | mainWindow.setMenuBar(self.menubar) 41 | self.statusbar = QtWidgets.QStatusBar(mainWindow) 42 | self.statusbar.setObjectName("statusbar") 43 | mainWindow.setStatusBar(self.statusbar) 44 | self.actionReload = QtWidgets.QAction(mainWindow) 45 | self.actionReload.setObjectName("actionReload") 46 | self.actionSearchQueryChanged = QtWidgets.QAction(mainWindow) 47 | self.actionSearchQueryChanged.setObjectName("actionSearchQueryChanged") 48 | 49 | self.retranslateUi(mainWindow) 50 | QtCore.QObject.connect(self.pushButton_reload, QtCore.SIGNAL("released()"), self.actionReload.trigger) 51 | QtCore.QObject.connect(self.lineEdit_search, QtCore.SIGNAL("textChanged(QString)"), self.actionSearchQueryChanged.trigger) 52 | QtCore.QMetaObject.connectSlotsByName(mainWindow) 53 | 54 | def retranslateUi(self, mainWindow): 55 | mainWindow.setWindowTitle(QtCompat.translate("mainWindow", "OMTK - Plugin Manager", None, -1)) 56 | self.pushButton_reload.setText(QtCompat.translate("mainWindow", "Reload", None, -1)) 57 | self.actionReload.setText(QtCompat.translate("mainWindow", "Reload", None, -1)) 58 | self.actionSearchQueryChanged.setText(QtCompat.translate("mainWindow", "SearchQueryChanged", None, -1)) 59 | 60 | -------------------------------------------------------------------------------- /omtk/ui/widget_create_component.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 819 10 | 416 11 | 12 | 13 | 14 | Component Creation Wizard 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Ctrls: 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Influences: 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 0 53 | 0 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Guides: 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 0 74 | 0 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 0 87 | 0 88 | 819 89 | 19 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | WidgetCreateComponentWizardParts 98 | QWidget 99 |
..widget_create_component_wizard_parts.h
100 | 1 101 |
102 |
103 | 104 | 105 |
106 | -------------------------------------------------------------------------------- /omtk/modules/rigFaceEyeLids.py: -------------------------------------------------------------------------------- 1 | from omtk.modules import rigFaceAvarGrps 2 | from omtk.modules import rigFaceAvar 3 | from omtk.libs import libCtrlShapes 4 | from omtk.libs import libPymel 5 | import pymel.core as pymel 6 | 7 | 8 | class CtrlEyeLidUpp(rigFaceAvar.BaseCtrlFace): 9 | def __createNode__(self, size=1.0, **kwargs): 10 | return libCtrlShapes.create_triangle_upp(size=size) 11 | 12 | 13 | class CtrlEyeLidLow(rigFaceAvar.BaseCtrlFace): 14 | def __createNode__(self, size=1.0, **kwargs): 15 | return libCtrlShapes.create_triangle_low(size=size) 16 | 17 | 18 | class FaceEyeLids(rigFaceAvarGrps.AvarGrpOnSurface): 19 | """ 20 | AvarGrp setup customized for Eyelids rigging. 21 | FaceLids behave the same a a standard AvarGrp with an Upp and Low section. 22 | However to adapt ourself to non-symetrical eyes, we use two surface to slide on. 23 | """ 24 | _CLS_CTRL_UPP = CtrlEyeLidUpp 25 | _CLS_CTRL_LOW = CtrlEyeLidLow 26 | IS_SIDE_SPECIFIC = True 27 | CREATE_MACRO_AVAR_HORIZONTAL = True 28 | CREATE_MACRO_AVAR_VERTICAL = True 29 | CREATE_MACRO_AVAR_ALL = True 30 | 31 | SHOW_IN_UI = True 32 | 33 | def __init__(self, *args, **kwargs): 34 | super(FaceEyeLids, self).__init__(*args, **kwargs) 35 | self.surface_upp = None 36 | self.surface_low = None 37 | 38 | def handle_surface(self): 39 | """ 40 | Create a separated surface for the upper and lower lids. 41 | This allow the rigger to easily tweak how each lids react which may be necessary with hyper-realistic characters. 42 | 43 | If the user provided it's own surface (via the input property), we'll use it for the upper AND lower lids. 44 | This is mainly to support custom surface like spheres for very cartoony characters. 45 | """ 46 | 47 | # Get all surfaces provided by the input property. 48 | def get_surface(obj): 49 | if libPymel.isinstance_of_shape(obj, pymel.nodetypes.NurbsSurface): 50 | return obj 51 | 52 | surfaces = filter(None, map(get_surface, self.input)) 53 | 54 | # If the user provided surface, pop them out of the input property and store them in the surface_[upp/low] property. 55 | if surfaces: 56 | surface = next(iter(surfaces), None) 57 | for surface in surfaces: 58 | self.input.remove(surface) 59 | self.surface_upp = surface 60 | self.surface_low = surface 61 | 62 | # Create surfaces if they were not provided 63 | if self.surface_upp is None: 64 | pymel.warning("Can't find surface for {0}, creating one.".format(self)) 65 | self.surface_upp = self.create_surface(name='SurfaceUpp') 66 | 67 | if self.surface_low is None: 68 | pymel.warning("Can't find surface for {0}, creating one.".format(self)) 69 | self.surface_low = self.create_surface(name='SurfaceLow') 70 | 71 | # We still provide the surface property in case we need a controller to be connected directly to it. 72 | self.surface = self.surface_upp 73 | 74 | def configure_avar(self, avar): 75 | inf = avar.jnt 76 | if inf in self.get_jnts_upp(): 77 | avar.surface = self.surface_upp 78 | else: 79 | avar.surface = self.surface_low 80 | 81 | def get_default_name(self): 82 | return 'eyelid' 83 | 84 | def get_multiplier_u(self): 85 | # Since the V go all around the sphere, it is too much range. 86 | # We'll restrict ourself only to a single quadrant. 87 | return 0.25 88 | 89 | 90 | def register_plugin(): 91 | return FaceEyeLids 92 | -------------------------------------------------------------------------------- /omtk/macros/ctrls_bake_selected.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use this macro to 'fix' any calibrated facial shape that have been manually adjusted. 3 | """ 4 | import pymel.core as pymel 5 | from omtk.libs import libRigging 6 | from omtk.core import api 7 | from omtk.core.macros import BaseMacro 8 | 9 | 10 | def _fix_ctrl_shape(ctrl): 11 | """ 12 | When the rigger want to resize an InteractiveCtrl, he will modify the ctrl shape 'controlPoints' attributes. 13 | This can be problematic since the shape 'create' attribute is feed from a transformGeometry node 14 | to compensate the non-uniform scaling caused by the calibration. This will 'skew' the shape which we don't want. 15 | We always want to make sure that there's only data in the orig shape 'controlPoints' attributes. 16 | This method will create a temporary shape that will receive the 'local' attribute from the ctrl shape (which 17 | contain the deformation from the 'controlPoints' attribute). The 'local' attribute of that shape will then be 18 | fed back to the orig shape. Finally, all the original 'controlPoints' will be set to zero. 19 | """ 20 | grp_offset = ctrl.getParent() 21 | 22 | def get_orig_shape(shape): 23 | return next((hist for hist in shape.listHistory() 24 | if isinstance(hist, pymel.nodetypes.NurbsCurve) 25 | and hist != shape 26 | and hist.intermediateObject.get()), None) 27 | 28 | def get_transformGeometry(shape): 29 | return next((hist for hist in shape.listHistory() 30 | if isinstance(hist, pymel.nodetypes.TransformGeometry)), None) 31 | 32 | for shape in ctrl.getShapes(noIntermediate=True): 33 | # Resolve orig shape 34 | shape_orig = get_orig_shape(shape) 35 | if not shape_orig: 36 | pymel.warning("Skipping {}. Cannot find orig shape.".format(shape)) 37 | continue 38 | 39 | # Resolve compensation matrix 40 | util_transform_geometry = get_transformGeometry(shape) 41 | if not util_transform_geometry: 42 | pymel.warning("Skipping {}. Cannot find transformGeometry.".format(shape)) 43 | continue 44 | attr_compensation_tm = next(iter(util_transform_geometry.transform.inputs(plugs=True)), None) 45 | if not attr_compensation_tm: 46 | pymel.warning("Skipping {}. Cannot find compensation matrix.".format(shape)) 47 | continue 48 | 49 | tmp_shape = pymel.createNode('nurbsCurve') 50 | tmp_shape.getParent().setParent(grp_offset) 51 | 52 | # Apply the inverted compensation matrix to access the desired orig_shape 'create' attr. 53 | tmp_transform_geometry = libRigging.create_utility_node( 54 | 'transformGeometry', 55 | inputGeometry=shape.local, 56 | transform=attr_compensation_tm, 57 | invertTransform=True 58 | ) 59 | attr_output_geometry = tmp_transform_geometry.outputGeometry 60 | pymel.connectAttr(attr_output_geometry, tmp_shape.create) 61 | pymel.disconnectAttr(tmp_shape.create) 62 | 63 | pymel.connectAttr(tmp_shape.local, shape_orig.create) 64 | 65 | # Remove any extraneous controlPoints coordinates. 66 | for attr_cp in shape.cp: 67 | attr_cp.set(0, 0, 0) 68 | for attr_cp in shape_orig.cp: 69 | attr_cp.set(0, 0, 0) 70 | 71 | # Cleanup 72 | pymel.disconnectAttr(shape_orig.create) 73 | pymel.delete(tmp_shape.getParent()) 74 | pymel.delete(tmp_transform_geometry) 75 | 76 | 77 | class CtrlBakeSelectedMacro(BaseMacro): 78 | def run(self): 79 | for obj in pymel.selected(): 80 | _fix_ctrl_shape(obj) 81 | 82 | 83 | def register_plugin(): 84 | return CtrlBakeSelectedMacro 85 | -------------------------------------------------------------------------------- /omtk/libs/libHistory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions to deal with construction history. 3 | Please learn the following used nomenclature: 4 | - mesh: A polygonal geometry shape 5 | - surface: A nurbs geometry shape 6 | - shape: Any geometry shape, including mesh and surface. 7 | """ 8 | import pymel.core as pymel 9 | 10 | from omtk.libs.libRigging import _filter_shape 11 | 12 | 13 | def _is_mesh(shape): 14 | return isinstance(shape, pymel.nodetypes.Mesh) 15 | 16 | 17 | def _is_surface(shape): 18 | return isinstance(shape, pymel.nodetypes.NurbsSurface) 19 | 20 | 21 | # 22 | # Utility functions that compliment pymel.listHistory. 23 | # 24 | 25 | def _iter_history(shape, key=None, fn_stop=None, stop_at_shape=False, **kwargs): 26 | """ 27 | Go through the history of the provided shapes and yield interesting elements. 28 | :param shape: The starting po 29 | :param key: 30 | :param stop_at_shape: If True, we will stop iterating as soon as we encounter a shape. Note that if you provided a 31 | transform, it will stop at the first encounter of the main shape. 32 | :param kwargs: 33 | :return: 34 | """ 35 | # Determine what condition make us stop iterating though history. 36 | if stop_at_shape: 37 | shape_start = shape.getShape() if isinstance(shape, pymel.nodetypes.Transform) else shape 38 | if fn_stop: 39 | fn_stop = lambda shape: isinstance(shape, pymel.nodetypes.Shape) and shape != shape_start or fn_stop(shape) 40 | else: 41 | fn_stop = lambda shape: isinstance(shape, pymel.nodetypes.Shape) and shape != shape_start 42 | 43 | for hist in shape.listHistory(**kwargs): 44 | if hist == shape: 45 | continue 46 | if fn_stop and fn_stop(hist): 47 | return 48 | if key and key(hist): 49 | yield hist 50 | 51 | 52 | def iter_history_foward(shape, **kwargs): 53 | for hist in _iter_history(shape, future=True, **kwargs): 54 | yield hist 55 | 56 | 57 | def iter_history_backward(shape, **kwargs): 58 | for hist in _iter_history(shape, **kwargs): 59 | yield hist 60 | 61 | 62 | def get_history_farthest_sibling(shape, **kwargs): 63 | i = iter_history_foward(shape, **kwargs) 64 | return next(reversed(list(i)), None) 65 | 66 | 67 | def get_history_previous_sibling(shape, **kwargs): 68 | i = iter_history_backward(shape, **kwargs) 69 | return next(i, None) 70 | 71 | 72 | # 73 | # Utility functions to resolve skinning shapes from influences. 74 | # 75 | 76 | def iter_affected_shapes(objs, key=None): 77 | """ 78 | :param obj: A reference object, generally a pymel.nodetypes.Joint. 79 | :return: The geometries affected by the object. 80 | """ 81 | known_skinClusters = [] 82 | 83 | # Ensure objs is iterable 84 | if not isinstance(objs, (tuple, list)): 85 | objs = [objs] 86 | 87 | for obj in objs: 88 | if isinstance(obj, pymel.nodetypes.Joint): 89 | # Collect all geometries affected by the joint. 90 | for hist in obj.worldMatrix.outputs(): 91 | if isinstance(hist, pymel.nodetypes.SkinCluster) and not hist in known_skinClusters: 92 | known_skinClusters.append(hist) 93 | for geometry in hist.getOutputGeometry(): 94 | if key is None or key(geometry): 95 | yield geometry 96 | 97 | 98 | def get_affected_shapes(objs, key=None): 99 | return list(iter_affected_shapes(objs, key=key)) 100 | 101 | 102 | def get_affected_meshes(objs): 103 | return list(iter_affected_shapes(objs, key=_is_mesh)) 104 | 105 | 106 | def get_affected_surface(objs): 107 | return list(iter_affected_shapes(objs, key=_is_surface)) 108 | -------------------------------------------------------------------------------- /omtk/ui/widget_logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_logger.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:53 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(435, 300) 16 | self.verticalLayout = QtWidgets.QVBoxLayout(Form) 17 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 18 | self.verticalLayout.setObjectName("verticalLayout") 19 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout() 20 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 21 | self.label_2 = QtWidgets.QLabel(Form) 22 | self.label_2.setObjectName("label_2") 23 | self.horizontalLayout_4.addWidget(self.label_2) 24 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 25 | self.horizontalLayout_4.addItem(spacerItem) 26 | self.label_4 = QtWidgets.QLabel(Form) 27 | self.label_4.setObjectName("label_4") 28 | self.horizontalLayout_4.addWidget(self.label_4) 29 | self.lineEdit_log_search = QtWidgets.QLineEdit(Form) 30 | self.lineEdit_log_search.setObjectName("lineEdit_log_search") 31 | self.horizontalLayout_4.addWidget(self.lineEdit_log_search) 32 | self.label_3 = QtWidgets.QLabel(Form) 33 | self.label_3.setObjectName("label_3") 34 | self.horizontalLayout_4.addWidget(self.label_3) 35 | self.comboBox_log_level = QtWidgets.QComboBox(Form) 36 | self.comboBox_log_level.setObjectName("comboBox_log_level") 37 | self.comboBox_log_level.addItem("") 38 | self.comboBox_log_level.addItem("") 39 | self.comboBox_log_level.addItem("") 40 | self.comboBox_log_level.addItem("") 41 | self.horizontalLayout_4.addWidget(self.comboBox_log_level) 42 | self.pushButton_logs_save = QtWidgets.QPushButton(Form) 43 | self.pushButton_logs_save.setObjectName("pushButton_logs_save") 44 | self.horizontalLayout_4.addWidget(self.pushButton_logs_save) 45 | self.pushButton_logs_clear = QtWidgets.QPushButton(Form) 46 | self.pushButton_logs_clear.setObjectName("pushButton_logs_clear") 47 | self.horizontalLayout_4.addWidget(self.pushButton_logs_clear) 48 | self.verticalLayout.addLayout(self.horizontalLayout_4) 49 | self.tableView_logs = QtWidgets.QTableView(Form) 50 | self.tableView_logs.setObjectName("tableView_logs") 51 | self.verticalLayout.addWidget(self.tableView_logs) 52 | 53 | self.retranslateUi(Form) 54 | self.comboBox_log_level.setCurrentIndex(1) 55 | QtCore.QMetaObject.connectSlotsByName(Form) 56 | 57 | def retranslateUi(self, Form): 58 | Form.setWindowTitle(QtCompat.translate("Form", "Form", None, -1)) 59 | self.label_2.setText(QtCompat.translate("Form", "Logs", None, -1)) 60 | self.label_4.setText(QtCompat.translate("Form", "Search", None, -1)) 61 | self.label_3.setText(QtCompat.translate("Form", "Level:", None, -1)) 62 | self.comboBox_log_level.setItemText(0, QtCompat.translate("Form", "Error", None, -1)) 63 | self.comboBox_log_level.setItemText(1, QtCompat.translate("Form", "Warning", None, -1)) 64 | self.comboBox_log_level.setItemText(2, QtCompat.translate("Form", "Info", None, -1)) 65 | self.comboBox_log_level.setItemText(3, QtCompat.translate("Form", "Debug", None, -1)) 66 | self.pushButton_logs_save.setText(QtCompat.translate("Form", "Save", None, -1)) 67 | self.pushButton_logs_clear.setText(QtCompat.translate("Form", "Clear", None, -1)) 68 | 69 | -------------------------------------------------------------------------------- /omtk/macros/avar_selection_toggle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Toogle selection between the facial animation layer and the rigging layer. 3 | """ 4 | import pymel.core as pymel 5 | from omtk.vendor import libSerialization 6 | from omtk.core import macros 7 | 8 | 9 | def get_avars_networks_from_selection(objs): 10 | """ 11 | Return all avars associated with the current selection. 12 | :param objs: A list of pymel.PyNode. If nothing is provided the selection is used. 13 | :return: A list of Avar instances. 14 | """ 15 | 16 | def fn_skip(network): 17 | return libSerialization.is_network_from_class(network, 'Rig') 18 | 19 | def fn_filter(network): 20 | return libSerialization.is_network_from_class(network, 'AbstractAvar') 21 | 22 | if objs is None: 23 | objs = pymel.selected() 24 | 25 | networks = libSerialization.get_connected_networks(objs, key=fn_filter, key_skip=fn_skip) 26 | 27 | # avars = filter(None, [libSerialization.import_network(network) for network in networks]) 28 | 29 | return networks 30 | 31 | 32 | def iter_connected_ctrl_models_networks(objs): 33 | def fn_skip(network): 34 | return libSerialization.is_network_from_class(network, 'Module') and not libSerialization.is_network_from_class( 35 | network, 'BaseCtrlModel') 36 | 37 | def fn_filter(network): 38 | return libSerialization.is_network_from_class(network, 'BaseCtrlModel') 39 | 40 | if objs is None: 41 | objs = pymel.selected() 42 | 43 | for network in libSerialization.iter_connected_networks(objs, key=fn_filter, key_skip=fn_skip): 44 | yield network 45 | 46 | 47 | def get_connected_ctrl_models_networks(objs): 48 | return list(iter_connected_ctrl_models_networks(objs)) 49 | 50 | 51 | def iter_connected_ctrl_models(objs): 52 | for network in iter_connected_ctrl_models_networks(objs): 53 | ctrl_model = libSerialization.import_network(network) 54 | if ctrl_model: 55 | yield ctrl_model 56 | 57 | 58 | def get_connected_ctrl_models(objs): 59 | return list(iter_connected_ctrl_models(objs)) 60 | 61 | 62 | class ToogleAvarSelection(macros.BaseMacro): 63 | def run(self): 64 | sel = pymel.selected() 65 | 66 | avar_networks = get_avars_networks_from_selection(sel) 67 | if not avar_networks: 68 | pymel.warning("Please select an avar controller or grp.") 69 | return 70 | 71 | # Since this is a toogle, we'll want to know if we are currently in 'animation' mode or 'rigging' mode. 72 | 73 | # If there's any controllers selected, we are in animation mode and want to select avar_grp. 74 | is_animation_mode = bool(get_connected_ctrl_models_networks(sel)) 75 | if is_animation_mode: 76 | result = set() 77 | for network in avar_networks: 78 | if network.hasAttr('grp_rig'): 79 | grp_rig = next(iter(network.attr('grp_rig').inputs()), None) 80 | if grp_rig and grp_rig.hasAttr('avar_ud'): # we duck type avars by checking the UD avar 81 | result.add(grp_rig) 82 | pymel.select(result) 83 | 84 | # If there aren't any controllers selected, we are in rigging mode and want to select avar ctrls. 85 | else: 86 | result = set() 87 | 88 | def fn_filter(network): 89 | return isinstance(network, pymel.nodetypes.Network) and \ 90 | libSerialization.is_network_from_class(network, 'BaseCtrl') 91 | 92 | for avar_network in avar_networks: 93 | if avar_network.hasAttr('ctrl'): 94 | for hist in avar_network.attr('ctrl').listHistory(): 95 | if fn_filter(hist): 96 | ctrl = next(iter(hist.attr('node').inputs()), None) if hist.hasAttr('node') else None 97 | if ctrl: 98 | result.add(ctrl) 99 | pymel.select(result) 100 | 101 | 102 | def register_plugin(): 103 | return ToogleAvarSelection 104 | -------------------------------------------------------------------------------- /omtk/libs/libSkeleton.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various utility methods that help the job of laying out the skeletton for a Rig. 3 | """ 4 | import pymel.core as pymel 5 | from omtk.libs import libPymel 6 | from maya import OpenMaya 7 | import math 8 | 9 | 10 | def mirror_obj(obj_src, obj_dst=None): 11 | """ 12 | Method to mirror joints in behavior. 13 | This use existing joint and doesn't break the skin or the network associated with the joints. 14 | """ 15 | from omtk.animation import mirrorPose 16 | if obj_dst is None: 17 | obj_dst = mirrorPose.get_ctrl_friend(obj_src) 18 | if obj_src is obj_dst: 19 | return False 20 | tm = obj_src.getMatrix(worldSpace=True) 21 | new_tm = mirrorPose.mirror_matrix(tm, mirror_x=True, flip_rot_x=True, flip_rot_y=True, flip_rot_z=True) 22 | obj_dst.setMatrix(new_tm, worldSpace=True) 23 | return obj_dst 24 | 25 | 26 | def transfer_rotation_to_joint_orient(obj): 27 | """ 28 | In Maya it is not possible to do a "makeIdentity" command on a joint that is bound to a skinCluster. 29 | This method bypass this limitation. 30 | """ 31 | mfn = obj.__apimfn__() 32 | 33 | rotation_orig = OpenMaya.MEulerRotation() 34 | mfn.getRotation(rotation_orig) 35 | rotation_xyz = rotation_orig.reorder(OpenMaya.MEulerRotation.kXYZ) 36 | 37 | # Apply existing jointOrient values 38 | orientation_orig = OpenMaya.MEulerRotation() 39 | mfn.getOrientation(orientation_orig) 40 | rotation_xyz *= orientation_orig 41 | 42 | def is_attr_accessible(attr): 43 | return not attr.isFreeToChange() == OpenMaya.MPlug.kFreeToChange 44 | 45 | if is_attr_accessible(obj.rotateX) or is_attr_accessible(obj.rotateY) or is_attr_accessible(obj.rotateZ): 46 | pymel.warning("Can't transfer rotation to joint orient. {0} rotation is locked.".format(obj.name())) 47 | return 48 | 49 | if is_attr_accessible(obj.jointOrientX) or is_attr_accessible(obj.jointOrientY) or is_attr_accessible( 50 | obj.jointOrientZ): 51 | pymel.warning("Can't transfer rotation to joint orient. {0} jointOrient is locked.".format(obj.name())) 52 | return 53 | 54 | obj.jointOrientX.set(math.degrees(rotation_xyz.x)) 55 | obj.jointOrientY.set(math.degrees(rotation_xyz.y)) 56 | obj.jointOrientZ.set(math.degrees(rotation_xyz.z)) 57 | obj.rotateX.set(0) 58 | obj.rotateY.set(0) 59 | obj.rotateZ.set(0) 60 | 61 | 62 | def mirror_jnt(obj_src, handle_joint_orient=True, create_missing=True): 63 | from omtk.animation import mirrorPose 64 | obj_dst = mirrorPose.get_ctrl_friend(obj_src) 65 | if obj_dst is None: 66 | src_name = obj_src.name() 67 | dst_name = mirrorPose.get_name_friend(src_name) 68 | if src_name == dst_name: 69 | return False 70 | 71 | obj_dst = pymel.createNode('joint') 72 | obj_dst.rename(dst_name) 73 | 74 | obj_src_parent = obj_src.getParent() 75 | if obj_src_parent: 76 | obj_dst_parent = mirrorPose.get_ctrl_friend(obj_src_parent) 77 | if obj_dst_parent: 78 | obj_dst.setParent(obj_dst_parent) 79 | 80 | mirror_obj(obj_src, obj_dst) 81 | if handle_joint_orient and isinstance(obj_src, pymel.nodetypes.Joint) and isinstance(obj_dst, 82 | pymel.nodetypes.Joint): 83 | transfer_rotation_to_joint_orient(obj_dst) 84 | obj_dst.radius.set(obj_src.radius.get()) 85 | return obj_dst 86 | 87 | 88 | def mirror_jnts(objs, **kwargs): 89 | # Sort objects by hyerarchy so we mirror parents before their children. 90 | objs = sorted(objs, key=libPymel.get_num_parents) 91 | with pymel.UndoChunk(): 92 | for obj in objs: 93 | mirror_jnt(obj, **kwargs) 94 | 95 | 96 | def freeze_selected_joints_rotation(): 97 | jnts = [obj for obj in pymel.selected() if isinstance(obj, pymel.nodetypes.Joint)] 98 | for jnt in jnts: 99 | if not isinstance(jnt, pymel.nodetypes.Joint): 100 | pymel.warning("Skipping non-joint {0}".format(jnt)) 101 | continue 102 | 103 | transfer_rotation_to_joint_orient(jnt) 104 | -------------------------------------------------------------------------------- /omtk/core/classModuleMap.py: -------------------------------------------------------------------------------- 1 | from .classModule import Module 2 | 3 | 4 | class ModuleMap(Module): 5 | """ 6 | Define a Module that use a CtrlModel to control each inputs. 7 | # todo: Use this new class in the AvarGrp class!!!!!! 8 | """ 9 | _CLS_CTRL_MODEL = None # please redefine! 10 | _CLS_CTRL = None # please redefine! 11 | DEFAULT_NAME_USE_FIRST_INPUT = True 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(ModuleMap, self).__init__(*args, **kwargs) 15 | self.models = [] 16 | 17 | def get_influences(self): 18 | return self.jnts 19 | 20 | def init_model(self, model, inputs, cls_model=None, cls_ctrl=None): 21 | """ 22 | Initialize a new CtrlModel instance, reuse existing data as much as necessary. 23 | :param model: The current definition. If never defined, the value is None. 24 | :param inputs: The inputs to use. 25 | :param cls: The desired CtrlModel datatype. 26 | :param cls_ctrl: The desired Ctrl datatype. 27 | :return: A CtrlModel instance. 28 | """ 29 | if cls_model is None: 30 | cls_model = self._CLS_CTRL_MODEL 31 | if cls_ctrl is None: 32 | cls_ctrl = self._CLS_CTRL 33 | # todo: validate inputs from existing model? 34 | 35 | # Use existing model if possible. 36 | if not isinstance(model, cls_model): 37 | if model: 38 | self.warning("Unexpected Model type for {0}. Expected {1}, got {2}.".format( 39 | model, cls_model.__name__, type(model).__name__ 40 | )) 41 | model = cls_model( 42 | inputs, 43 | rig=self.rig 44 | ) 45 | 46 | # Hack: Ensure a model has a name. 47 | if not model.name: 48 | model.name = model.get_default_name() 49 | 50 | # Hack: Force a certain ctrl type to the model. 51 | model._CLS_CTRL = cls_ctrl 52 | 53 | return model 54 | 55 | def init_models(self): 56 | new_models = [] 57 | influences = self.get_influences() 58 | known_influences = set() 59 | 60 | # Check existing models 61 | for model in self.models: 62 | # Remove any unrecognized model 63 | if model.jnt not in influences: 64 | self.warning("Unexpected Model {0} will be deleted.".format(model.name)) 65 | continue 66 | 67 | model_inputs = [model.jnt] 68 | model = self.init_model(model, model_inputs) 69 | new_models.append(model) 70 | known_influences.update(model_inputs) 71 | 72 | for influence in influences: 73 | if influence not in known_influences: 74 | model = self.init_model(None, [influence]) 75 | new_models.append(model) 76 | 77 | return new_models 78 | 79 | def build_model(self, model, parent_grp_anm=True, parent_grp_rig=True, **kwargs): 80 | model.build(self, **kwargs) 81 | # todo: reduce cluttering by using direct connection and reducing grp_anm count 82 | 83 | if parent_grp_anm and model.grp_anm and self.grp_anm: 84 | model.grp_anm.setParent(self.grp_anm) 85 | 86 | if parent_grp_rig and model.grp_rig and self.grp_rig: 87 | model.grp_rig.setParent(self.grp_rig) 88 | 89 | def build_models(self, **kwargs): 90 | for model in self.models: 91 | self.build_model(model, **kwargs) 92 | 93 | def build(self, create_grp_anm=True, create_grp_rig=True, connect_global_scale=True, 94 | parent=True, **model_kwargs): 95 | super(ModuleMap, self).build( 96 | create_grp_anm=create_grp_anm, 97 | create_grp_rig=create_grp_rig, 98 | connect_global_scale=connect_global_scale, 99 | parent=parent 100 | ) 101 | 102 | self.models = self.init_models() 103 | 104 | self.build_models( 105 | **model_kwargs 106 | ) 107 | 108 | def unbuild(self, **kwargs): 109 | for model in self.models: 110 | model.unbuild() 111 | 112 | super(ModuleMap, self).unbuild(**kwargs) 113 | -------------------------------------------------------------------------------- /omtk/widget_list_meshes.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pymel.core as pymel 3 | from ui import widget_list_meshes 4 | 5 | from omtk.libs import libSkinning 6 | from omtk.libs import libQt 7 | 8 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets 9 | 10 | import ui_shared 11 | 12 | 13 | class WidgetListMeshes(QtWidgets.QWidget): 14 | def __init__(self, parent=None): 15 | super(WidgetListMeshes, self).__init__(parent=parent) 16 | 17 | self._rig = None 18 | 19 | self.ui = widget_list_meshes.Ui_Form() 20 | self.ui.setupUi(self) 21 | 22 | # Connect events 23 | self.ui.treeWidget.itemSelectionChanged.connect(self.on_mesh_selection_changed) 24 | self.ui.lineEdit_search.textChanged.connect(self.on_meshes_query_changed) 25 | self.ui.pushButton_selectGrpMeshes.pressed.connect(self.on_SelectGrpMeshes) 26 | self.ui.btn_update.pressed.connect(self.update) 27 | 28 | def set_rig(self, rig, update=True): 29 | self._rig = rig 30 | if update: 31 | self.update_list() 32 | 33 | def update_list(self): 34 | self.ui.treeWidget.clear() 35 | 36 | if self._rig is None: 37 | return 38 | 39 | # Hack: force cache to invalidate 40 | # try: 41 | # self._rig.get_meshes.func.im_self.cache.clear() 42 | # except Exception, e: 43 | # pass 44 | all_meshes = self._rig.get_shapes() 45 | 46 | if all_meshes: 47 | widget_root = self.ui.treeWidget.invisibleRootItem() 48 | 49 | for mesh in all_meshes: 50 | influences = None 51 | 52 | skincluster = libSkinning.get_skin_cluster(mesh) 53 | if skincluster: 54 | influences = sorted(libSkinning.get_skin_cluster_influence_objects(skincluster)) 55 | 56 | self._fill_widget_meshes(widget_root, mesh, influences) 57 | 58 | self.update_list_visibility() 59 | 60 | def _fill_widget_meshes(self, qt_parent, mesh, influences): 61 | textBrush = QtGui.QBrush(QtCore.Qt.white) 62 | 63 | # Add mesh 64 | item_mesh = QtWidgets.QTreeWidgetItem(0) 65 | item_mesh.setText(0, str(mesh)) 66 | item_mesh.setForeground(0, textBrush) 67 | ui_shared.set_icon_from_type(mesh.getParent(), item_mesh) 68 | qt_parent.addChild(item_mesh) 69 | 70 | # Monkey-patch mesh QWidget 71 | item_mesh.metadata_type = ui_shared.MetadataType.Mesh 72 | item_mesh.metadata_data = mesh 73 | 74 | # Add influences 75 | if influences: 76 | for influence in influences: 77 | item = QtWidgets.QTreeWidgetItem(0) 78 | item.setText(0, str(influence)) 79 | item.setForeground(0, textBrush) 80 | ui_shared.set_icon_from_type(influence, item) 81 | item_mesh.addChild(item) 82 | 83 | # Monkey-patch influence QWidget 84 | item.metadata_type = ui_shared.MetadataType.Influence 85 | item.metadata_data = influence 86 | 87 | def update_list_visibility(self, query_regex=None): 88 | if query_regex is None: 89 | query_raw = self.ui.lineEdit_search.text() 90 | query_regex = ".*{0}.*".format(query_raw) if query_raw else ".*" 91 | 92 | def fn_can_show(qItem, query_regex): 93 | if qItem.metadata_type == ui_shared.MetadataType.Influence: # Always show influences 94 | return True 95 | 96 | return not query_regex or re.match(query_regex, qItem.text(0), re.IGNORECASE) 97 | 98 | for qt_item in libQt.get_all_QTreeWidgetItem(self.ui.treeWidget): 99 | can_show = fn_can_show(qt_item, query_regex) 100 | qt_item.setHidden(not can_show) 101 | 102 | def get_selection(self): 103 | result = [] 104 | for item in self.ui.treeWidget.selectedItems(): 105 | if item.metadata_data.exists(): 106 | result.append(item.metadata_data.getParent()) 107 | return result 108 | 109 | # 110 | # Events 111 | # 112 | 113 | def on_mesh_selection_changed(self): 114 | pymel.select(self.get_selection()) 115 | 116 | def on_meshes_query_changed(self, *args, **kwargs): 117 | self.update_list_visibility() 118 | 119 | def on_SelectGrpMeshes(self): 120 | grp = self._rig.grp_geo 121 | if not grp or not grp.exists(): 122 | pymel.warning("Can't find influence grp!") 123 | else: 124 | pymel.select(grp) 125 | -------------------------------------------------------------------------------- /omtk/modules/rigFaceJaw.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.libs import libCtrlShapes 4 | from omtk.libs import libRigging 5 | from omtk.libs import libPython 6 | from omtk.modules import rigFaceAvar 7 | from omtk.modules import rigFaceAvarGrps 8 | from omtk.models import model_ctrl_linear 9 | 10 | 11 | class CtrlJaw(rigFaceAvar.BaseCtrlFace): 12 | def __createNode__(self, **kwargs): 13 | node = libCtrlShapes.create_triangle_low() 14 | # node.r.lock() 15 | node.s.lock() 16 | return node 17 | 18 | 19 | class ModelCtrlJaw(model_ctrl_linear.ModelCtrlLinear): 20 | def connect(self, avar, avar_grp, ud=True, fb=True, lr=True, yw=True, pt=True, rl=True, sx=True, sy=True, sz=True): 21 | libRigging.connectAttr_withLinearDrivenKeys( 22 | self.ctrl.translateX, avar.attr_lr 23 | ) 24 | libRigging.connectAttr_withLinearDrivenKeys( 25 | self.ctrl.translateY, avar.attr_ud, 26 | ) 27 | libRigging.connectAttr_withLinearDrivenKeys( 28 | self.ctrl.translateZ, avar.attr_fb 29 | ) 30 | libRigging.connectAttr_withLinearDrivenKeys( 31 | self.ctrl.rotateX, avar.attr_pt 32 | ) 33 | libRigging.connectAttr_withLinearDrivenKeys( 34 | self.ctrl.rotateY, avar.attr_yw 35 | ) 36 | libRigging.connectAttr_withLinearDrivenKeys( 37 | self.ctrl.rotateZ, avar.attr_rl 38 | ) 39 | # todo: connect jaw ratio 40 | 41 | def get_default_tm_ctrl(self): 42 | """ 43 | Find the chin location using raycast. This is the prefered location for the jaw doritos. 44 | If raycast don't return any information, use the default behavior. 45 | """ 46 | ref = self.jnt.getMatrix(worldSpace=True) 47 | pos_s = pymel.datatypes.Point(self.jnt.getTranslation(space='world')) 48 | pos_e = pymel.datatypes.Point(1, 0, 0) * ref 49 | dir = pos_e - pos_s 50 | result = self.rig.raycast_farthest(pos_s, dir) 51 | if not result: 52 | return super(ModelCtrlJaw, self).get_default_tm_ctrl() 53 | 54 | tm = pymel.datatypes.Matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, result.x, result.y, result.z, 1]) 55 | return tm 56 | 57 | 58 | class AvarJaw(rigFaceAvar.AvarSimple): 59 | """ 60 | This avar is not designed to use any surface. 61 | """ 62 | SHOW_IN_UI = False 63 | IS_SIDE_SPECIFIC = False 64 | 65 | def get_default_name(self): 66 | return 'Jaw' 67 | 68 | 69 | class FaceJaw(rigFaceAvarGrps.AvarGrpOnSurface): 70 | """ 71 | AvarGrp customized for jaw rigging. Necessary for some facial modules. 72 | The Jaw is a special zone since it doesn't happen in pre-deform, it happen in the main skinCluster. 73 | The Jaw global avars are made 74 | """ 75 | _CLS_AVAR = AvarJaw 76 | _CLS_AVAR_MACRO = rigFaceAvar.AvarSimple # todo: use AbstractAvar??? 77 | CREATE_MACRO_AVAR_ALL = True 78 | CREATE_MACRO_AVAR_HORIZONTAL = False 79 | CREATE_MACRO_AVAR_VERTICAL = False 80 | _CLS_CTRL_ALL = CtrlJaw 81 | _CLS_CTRL_MICRO = None 82 | _CLS_MODEL_CTRL_ALL = ModelCtrlJaw 83 | _CLS_MODEL_CTRL_MICRO = None 84 | SHOW_IN_UI = True 85 | SINGLE_INFLUENCE = True 86 | 87 | def handle_surface(self): 88 | pass # todo: better class schema! 89 | 90 | @libPython.memoized_instancemethod 91 | def _get_avar_macro_all_ctrl_tm(self): 92 | """ 93 | If the rigger provided an extra influence (jaw_end), we'll use it to define the ctrl and influence position. 94 | """ 95 | jnt_jaw = self.jnt 96 | if len(self.jnts) > 1: 97 | self.info('Using {} as the ctrl position reference.'.format(jnt_jaw.name())) 98 | jnt_ref = self.jnts[-1] 99 | p = jnt_ref.getTranslation(space='world') 100 | else: 101 | ref = jnt_jaw.getMatrix(worldSpace=True) 102 | pos_s = pymel.datatypes.Point(self.jnt.getTranslation(space='world')) 103 | pos_e = pymel.datatypes.Point(1, 0, 0) * ref 104 | dir = pos_e - pos_s 105 | p = self.rig.raycast_farthest(pos_s, dir) 106 | if p is None: 107 | self.warning("Raycast failed. Using {} as the ctrl position reference.".format(jnt_jaw)) 108 | p = pos_s 109 | 110 | result = pymel.datatypes.Matrix( 111 | 1, 0, 0, 0, 112 | 0, 1, 0, 0, 113 | 0, 0, 1, 0, 114 | p.x, p.y, p.z, 1 115 | ) 116 | return result 117 | 118 | 119 | def register_plugin(): 120 | return FaceJaw 121 | -------------------------------------------------------------------------------- /omtk/ui/widget_create_component.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '/home/rlessard/packages/omtk/0.4.999/python/omtk/ui/widget_create_component.ui' 4 | # 5 | # Created: Tue Feb 20 10:34:52 2018 6 | # by: pyside2-uic running on Qt 2.0.0~alpha0 7 | # 8 | # WARNING! All changes made in this file will be lost! 9 | 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets, QtCompat 11 | 12 | class Ui_MainWindow(object): 13 | def setupUi(self, MainWindow): 14 | MainWindow.setObjectName("MainWindow") 15 | MainWindow.resize(819, 416) 16 | self.centralwidget = QtWidgets.QWidget(MainWindow) 17 | self.centralwidget.setObjectName("centralwidget") 18 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.centralwidget) 19 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 20 | self.verticalLayout = QtWidgets.QVBoxLayout() 21 | self.verticalLayout.setObjectName("verticalLayout") 22 | self.label_ctrl = QtWidgets.QLabel(self.centralwidget) 23 | self.label_ctrl.setObjectName("label_ctrl") 24 | self.verticalLayout.addWidget(self.label_ctrl) 25 | self.widget_view_ctrl = WidgetCreateComponentWizardParts(self.centralwidget) 26 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding) 27 | sizePolicy.setHorizontalStretch(0) 28 | sizePolicy.setVerticalStretch(0) 29 | sizePolicy.setHeightForWidth(self.widget_view_ctrl.sizePolicy().hasHeightForWidth()) 30 | self.widget_view_ctrl.setSizePolicy(sizePolicy) 31 | self.widget_view_ctrl.setObjectName("widget_view_ctrl") 32 | self.verticalLayout.addWidget(self.widget_view_ctrl) 33 | self.horizontalLayout_4.addLayout(self.verticalLayout) 34 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 35 | self.verticalLayout_2.setObjectName("verticalLayout_2") 36 | self.label_infl = QtWidgets.QLabel(self.centralwidget) 37 | self.label_infl.setObjectName("label_infl") 38 | self.verticalLayout_2.addWidget(self.label_infl) 39 | self.widget_view_infl = WidgetCreateComponentWizardParts(self.centralwidget) 40 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding) 41 | sizePolicy.setHorizontalStretch(0) 42 | sizePolicy.setVerticalStretch(0) 43 | sizePolicy.setHeightForWidth(self.widget_view_infl.sizePolicy().hasHeightForWidth()) 44 | self.widget_view_infl.setSizePolicy(sizePolicy) 45 | self.widget_view_infl.setObjectName("widget_view_infl") 46 | self.verticalLayout_2.addWidget(self.widget_view_infl) 47 | self.horizontalLayout_4.addLayout(self.verticalLayout_2) 48 | self.verticalLayout_3 = QtWidgets.QVBoxLayout() 49 | self.verticalLayout_3.setObjectName("verticalLayout_3") 50 | self.label_guid = QtWidgets.QLabel(self.centralwidget) 51 | self.label_guid.setObjectName("label_guid") 52 | self.verticalLayout_3.addWidget(self.label_guid) 53 | self.widget_view_guid = WidgetCreateComponentWizardParts(self.centralwidget) 54 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding) 55 | sizePolicy.setHorizontalStretch(0) 56 | sizePolicy.setVerticalStretch(0) 57 | sizePolicy.setHeightForWidth(self.widget_view_guid.sizePolicy().hasHeightForWidth()) 58 | self.widget_view_guid.setSizePolicy(sizePolicy) 59 | self.widget_view_guid.setObjectName("widget_view_guid") 60 | self.verticalLayout_3.addWidget(self.widget_view_guid) 61 | self.horizontalLayout_4.addLayout(self.verticalLayout_3) 62 | MainWindow.setCentralWidget(self.centralwidget) 63 | self.menubar = QtWidgets.QMenuBar(MainWindow) 64 | self.menubar.setGeometry(QtCore.QRect(0, 0, 819, 19)) 65 | self.menubar.setObjectName("menubar") 66 | MainWindow.setMenuBar(self.menubar) 67 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 68 | self.statusbar.setObjectName("statusbar") 69 | MainWindow.setStatusBar(self.statusbar) 70 | 71 | self.retranslateUi(MainWindow) 72 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 73 | 74 | def retranslateUi(self, MainWindow): 75 | MainWindow.setWindowTitle(QtCompat.translate("MainWindow", "Component Creation Wizard", None, -1)) 76 | self.label_ctrl.setText(QtCompat.translate("MainWindow", "Ctrls:", None, -1)) 77 | self.label_infl.setText(QtCompat.translate("MainWindow", "Influences:", None, -1)) 78 | self.label_guid.setText(QtCompat.translate("MainWindow", "Guides:", None, -1)) 79 | 80 | from ..widget_create_component_wizard_parts import WidgetCreateComponentWizardParts 81 | -------------------------------------------------------------------------------- /omtk/pluginmanager_window.py: -------------------------------------------------------------------------------- 1 | from omtk.core import plugin_manager 2 | 3 | from omtk.vendor.Qt import QtCore, QtWidgets 4 | 5 | from ui import pluginmanager_window 6 | 7 | 8 | class PluginListModel(QtCore.QAbstractTableModel): 9 | """ 10 | QTableModel that list attributes. 11 | # TODO: Benchmark pymel.Attribute vs lazy proxy class. 12 | """ 13 | HEADER = ('Name', 'Type', 'Status', 'Location', 'Description') 14 | 15 | ROW_NAME = 0 16 | ROW_TYPE = 1 17 | ROW_STAT = 2 18 | ROW_LOCA = 3 19 | ROW_DESC = 4 20 | 21 | def __init__(self, parent, data, *args): 22 | super(PluginListModel, self).__init__(parent, *args) 23 | self.items = data 24 | self.header = self.HEADER 25 | 26 | def rowCount(self, parent): 27 | return len(self.items) 28 | 29 | def columnCount(self, parent): 30 | return len(self.header) 31 | 32 | def data(self, index, role): 33 | if not index.isValid(): 34 | return None 35 | 36 | if role == QtCore.Qt.DisplayRole: 37 | plugin = self.items[index.row()] 38 | col_index = index.column() 39 | if col_index == self.ROW_NAME: 40 | return plugin.name 41 | elif col_index == self.ROW_TYPE: 42 | return plugin.type_name.title() 43 | elif col_index == self.ROW_STAT: 44 | return plugin.status 45 | elif col_index == self.ROW_LOCA: 46 | return plugin.module.__file__ if plugin.module else 'n/a' 47 | elif col_index == self.ROW_DESC: 48 | return plugin.description 49 | return '' 50 | elif role == QtCore.Qt.BackgroundColorRole: 51 | plugin = self.items[index.row()] 52 | if plugin.status == plugin_manager.PluginStatus.Failed: 53 | return QtCore.Qt.red 54 | return None 55 | 56 | return None 57 | 58 | def headerData(self, col, orientation, role): 59 | if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: 60 | return self.header[col] 61 | return None 62 | 63 | def reset(self): 64 | """Backport of Qt4 .reset method()""" 65 | self.beginResetModel() 66 | self.endResetModel() 67 | 68 | 69 | class PluginListFilterProxyModel(QtCore.QSortFilterProxyModel): 70 | def __init__(self, parent): 71 | super(PluginListFilterProxyModel, self).__init__(parent) 72 | self._search_query = None 73 | 74 | def set_search_query(self, search_query, update=True): 75 | self._search_query = search_query 76 | if update: 77 | self.reset() 78 | 79 | def filterAcceptsRow(self, row, index): 80 | if not self._search_query: 81 | return True 82 | 83 | model = self.sourceModel() 84 | item = model.items[row] 85 | return self._search_query in item.module_name 86 | 87 | def reset(self): 88 | """Backport of Qt4 .reset method()""" 89 | self.beginResetModel() 90 | self.endResetModel() 91 | 92 | 93 | class PluginManagerWindow(QtWidgets.QMainWindow): 94 | searchQueryChanged = QtCore.Signal(str) 95 | 96 | def __init__(self, parent=None): 97 | super(PluginManagerWindow, self).__init__(parent=parent) 98 | 99 | # Initialize GUI 100 | self.ui = pluginmanager_window.Ui_mainWindow() 101 | self.ui.setupUi(self) 102 | 103 | # Initialize MVC 104 | self._data = plugin_manager.plugin_manager.get_plugins() 105 | self._model = PluginListModel(self, self._data) 106 | self._proxy_model = PluginListFilterProxyModel(self) 107 | self._proxy_model.setSourceModel(self._model) 108 | self._proxy_model.setDynamicSortFilter(False) 109 | self.ui.tableView.setModel(self._proxy_model) 110 | 111 | # Connect actions 112 | self.ui.actionReload.triggered.connect(self.on_reload) 113 | self.ui.actionSearchQueryChanged.triggered.connect(self.on_searchquery_changed) 114 | 115 | def iter_selected_plugins(self): 116 | for row in self.ui.tableView.selectionModel().selectedRows(): 117 | plugin = self._data[row.row()] 118 | yield plugin 119 | 120 | def get_selected_plugins(self): 121 | return list(self.iter_selected_plugins()) 122 | 123 | def on_reload(self): 124 | for plugin in self.iter_selected_plugins(): 125 | plugin.load(force=True) 126 | self._proxy_model.reset() 127 | 128 | def on_searchquery_changed(self, *args, **kwargs): 129 | query = self.ui.lineEdit_search.text() 130 | self._proxy_model.set_search_query(query) 131 | 132 | 133 | gui = PluginManagerWindow() 134 | 135 | 136 | def show(): 137 | global gui 138 | gui.show() 139 | -------------------------------------------------------------------------------- /omtk/patches/patch_0.2.0_missing_spaceswitch_targets.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pymel.core as pymel 4 | 5 | from omtk.modules import rigDpSpine 6 | 7 | from omtk.vendor import libSerialization 8 | 9 | log = logging.getLogger('omtk') 10 | 11 | 12 | def patch_spaceswitch_data_into_network(sel=None): 13 | if sel is None: 14 | sel = pymel.selected() 15 | 16 | networks = libSerialization.get_connected_networks(sel, key=lambda 17 | net: libSerialization.is_network_from_class(net, 'BaseCtrl')) 18 | 19 | for net in networks: 20 | ctrl_instance = libSerialization.import_network(net) 21 | data = ctrl_instance.get_spaceswitch_enum_targets() 22 | 23 | if data: 24 | # Create missing attributes if needed 25 | if not net.hasAttr('targets_indexes'): 26 | log.info('targets_indexes attribute is missing on network {0}. It will be created'.format(net)) 27 | pymel.addAttr(net, longName='targets_indexes', at='long', multi=True) 28 | if not net.hasAttr('targets'): 29 | log.info('targets attribute is missing on network {0}. It will be created'.format(net)) 30 | pymel.addAttr(net, longName='targets', at='message', multi=True) 31 | if not net.hasAttr('local_index'): 32 | log.info('targets_indexes attribute is missing on network {0}. It will be created'.format(net)) 33 | pymel.addAttr(net, longName='local_index', at='long') 34 | 35 | # Add data if needed 36 | for index, (name, obj) in data.items(): 37 | target_data = net.targets.get() 38 | indexes_data = net.targets_indexes.get() 39 | if obj not in target_data: 40 | if index not in indexes_data: 41 | if obj is not None: 42 | if isinstance(obj, pymel.nt.Joint): 43 | log.info('Patching network {0} space switch data named {1} - Index {2} on object {3}' 44 | .format(net.name(), name, index, obj)) 45 | pymel.connectAttr(obj.message, net.targets[len(target_data)]) 46 | elif isinstance(obj, pymel.nt.Transform): 47 | key = lambda network: libSerialization.is_network_from_class(network, 'Node') 48 | ctrl_net = libSerialization.get_connected_networks(obj, key) 49 | if ctrl_net > 0: 50 | log.info('Patching network {0} space switch data named {1} - ' 51 | 'Index {2} on object {3}'.format(net.name(), name, index, ctrl_net[0])) 52 | pymel.connectAttr(ctrl_net[0].message, net.targets[len(target_data)]) 53 | net.targets_indexes[len(target_data)].set(index) 54 | elif name == 'Local': 55 | log.info('Patching network {0} space switch data named {1} - Index {2} as local index' 56 | .format(net.name(), name, index)) 57 | net.local_index.set(index) 58 | else: 59 | if obj is not None: 60 | log.warning('Object {0} have not been found in attribute targets but index {1} yes.' 61 | 'This object will not be patched to prevent any index conflict. ' 62 | 'Look at your data to understand'.format(obj, index)) 63 | else: 64 | log.info('Space Switch object {0} is already included in the targets attributes'.format(obj)) 65 | 66 | 67 | def patch_spaceswitch_dpspine_objects(): 68 | """ 69 | This function is not clean, but it can be used to generate spaceswitch object for certain module and after look at 70 | all space switch target information to replace to space switch object target that could used a ctrl to the new space 71 | switch object that will not be removed after an unbuild 72 | """ 73 | 74 | # Get the rig instance in the scene (Now support only the first one found 75 | rig_net = libSerialization.get_networks_from_class('Rig')[0] 76 | rig_instance = libSerialization.import_network(rig_net) 77 | 78 | # Get all the module that we could need to patch 79 | to_patch = [module for module in rig_instance.modules if isinstance(module, rigDpSpine.DpSpine)] 80 | 81 | for module in to_patch: 82 | module_to_patch = module 83 | if isinstance(module_to_patch, rigDpSpine.DpSpine): 84 | # Find all connection that have from the ctrl ik down (COG) and replace it 85 | connected_to_fk_dwn = module_to_patch.ctrl_fk_dwn._network.message.outputs(s=False, d=True, p=True) 86 | 87 | for connection in connected_to_fk_dwn: 88 | attr_name = connection.shortName() 89 | if attr_name.find('targets') >= 0: 90 | log.info('Reconnecting {0} from {1} to space switch node {2}' 91 | .format(connection, module_to_patch.ctrl_ik_dwn, module_to_patch.ctrl_ik_dwn._network)) 92 | connection.disconnect() 93 | pymel.connectAttr(module_to_patch.ctrl_ik_dwn._network.message, connection, force=True) -------------------------------------------------------------------------------- /omtk/modules/rigFK.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | from omtk.core.classCtrl import BaseCtrl 3 | from omtk.core.classModule import Module 4 | from omtk.libs import libPython 5 | from omtk.libs import libRigging 6 | 7 | 8 | class CtrlFk(BaseCtrl): 9 | def __createNode__(self, *args, **kwargs): 10 | ''' 11 | if 'shoulder' in name.lower(): 12 | node = libCtrlShapes.create_shape_double_needle(size=size*0.04, normal=(0, 0, 1), *args, **kwargs) 13 | else: 14 | ''' 15 | node = super(CtrlFk, self).__createNode__(multiplier=1.1, *args, **kwargs) 16 | 17 | make = next(iter(node.inputs()), None) 18 | if make: 19 | # TODO: Multiply radius??? 20 | # make.radius.set(size) 21 | make.degree.set(1) 22 | make.sections.set(8) 23 | 24 | return node 25 | 26 | 27 | class FK(Module): 28 | """ 29 | A Simple FK with support for multiple hyerarchy. 30 | 31 | Note that there's multiple way to name the ctrls. 32 | 1) Use the inputs as reference ( _NAME_CTRL_ENUMERATE = False ) 33 | ex: (module name is arm_l) 34 | jnt_upperarm_l -> ctrl_arm_upperarm_l 35 | jnt_forearm_l -> ctrl_arm_forearm_l 36 | 2) Use enumeration ( _NAME_CTRL_ENUMERATE = True ) 37 | ex: (module name is arm_l) 38 | jnt_upperarm_l -> ctrl_arm_01_l 39 | jnt_forearm_l -> ctrl_arm_02_l 40 | ex: 41 | """ 42 | DEFAULT_NAME_USE_FIRST_INPUT = True 43 | _NAME_CTRL_ENUMERATE = False # If set to true, the ctrl will use the module name. 44 | _FORCE_INPUT_NAME = False # Force using the name of the input in the name of the ctrl 45 | # Otherwise they will use their associated input name. 46 | _CLS_CTRL = CtrlFk 47 | 48 | def __init__(self, *args, **kwargs): 49 | super(FK, self).__init__(*args, **kwargs) 50 | self.ctrls = [] 51 | self.sw_translate = False 52 | self.create_spaceswitch = True 53 | 54 | # 55 | # libSerialization implementation 56 | # 57 | def __callbackNetworkPostBuild__(self): 58 | """ 59 | Cleaning routine automatically called by libSerialization after a network import. 60 | """ 61 | # Ensure there's no None value in the .ctrls array. 62 | # This can happen if the rigging delete the stored shape before rebuilding. 63 | try: 64 | self.ctrls = filter(None, self.ctrls) 65 | except (AttributeError, TypeError): 66 | pass 67 | super(FK, self).__callbackNetworkPostBuild__() 68 | 69 | def build(self, constraint=True, parent=True, create_grp_anm=True, create_grp_rig=False, *args, **kwargs): 70 | super(FK, self).build(create_grp_rig=create_grp_rig, *args, **kwargs) 71 | nomenclature_anm = self.get_nomenclature_anm() 72 | nomenclature_rig = self.get_nomenclature_rig() 73 | 74 | # Initialize ctrls 75 | libPython.resize_list(self.ctrls, len(self.jnts)) 76 | for i, ctrl in enumerate(self.ctrls): 77 | self.ctrls[i] = self.init_ctrl(self._CLS_CTRL, ctrl) 78 | 79 | for i, chain in enumerate(self.chains): 80 | # Build chain ctrls 81 | chain_ctrls = [] 82 | for j, jnt in enumerate(chain): 83 | jnt_index = self.jnts.index(jnt) # todo: optimize performance by created a map? 84 | ctrl = self.ctrls[jnt_index] 85 | chain_ctrls.append(ctrl) 86 | 87 | # Resolve ctrl name. 88 | # TODO: Validate with multiple chains 89 | nomenclature = nomenclature_anm + self.rig.nomenclature(jnt.stripNamespace().nodeName()) 90 | if not self._FORCE_INPUT_NAME: 91 | if len(self.jnts) == 1 and len(self.chains) == 1: 92 | ctrl_name = nomenclature_anm.resolve() 93 | elif len(self.chains) == 1 or self._NAME_CTRL_ENUMERATE: 94 | ctrl_name = nomenclature_anm.resolve('{0:02d}'.format(j)) 95 | else: 96 | ctrl_name = nomenclature.resolve() 97 | else: 98 | ctrl_name = nomenclature.resolve() 99 | 100 | ctrl.build(name=ctrl_name, refs=jnt, geometries=self.rig.get_meshes()) 101 | ctrl.setMatrix(jnt.getMatrix(worldSpace=True)) 102 | 103 | # Build space-switch for first chain ctrl 104 | if j == 0: 105 | if self.create_spaceswitch: 106 | if self.sw_translate: 107 | ctrl.create_spaceswitch(self, self.parent, add_world=True) 108 | else: 109 | ctrl.create_spaceswitch(self, self.parent, skipTranslate=['x', 'y', 'z'], add_world=True) 110 | 111 | if chain_ctrls: 112 | chain_ctrls[0].setParent(self.grp_anm) 113 | libRigging.create_hyerarchy(chain_ctrls) 114 | 115 | # Constraint jnts to ctrls if necessary 116 | if constraint is True: 117 | for jnt, ctrl in zip(self.jnts, self.ctrls): 118 | pymel.parentConstraint(ctrl, jnt, maintainOffset=True) 119 | pymel.connectAttr(ctrl.scaleX, jnt.scaleX) 120 | pymel.connectAttr(ctrl.scaleY, jnt.scaleY) 121 | pymel.connectAttr(ctrl.scaleZ, jnt.scaleZ) 122 | 123 | 124 | def register_plugin(): 125 | return FK 126 | -------------------------------------------------------------------------------- /omtk/modules/rigFKAdditive.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import pymel.core as pymel 3 | from omtk import constants 4 | from omtk.core.classCtrl import BaseCtrl 5 | from omtk.core.classModule import Module 6 | from omtk.libs import libRigging 7 | from omtk.libs import libCtrlShapes 8 | from omtk.libs import libPython 9 | from omtk.libs import libAttr 10 | from omtk.modules import rigFK 11 | 12 | 13 | class CtrlFkAdd(BaseCtrl): 14 | def __createNode__(self, size=None, refs=None, *args, **kwargs): 15 | # Resolve size automatically if refs are provided. 16 | ref = next(iter(refs), None) if isinstance(refs, collections.Iterable) else refs 17 | if size is None and ref is not None: 18 | size = libRigging.get_recommended_ctrl_size(ref) 19 | else: 20 | size = 1.0 21 | 22 | node = libCtrlShapes.create_shape_needle(size=size, *args, **kwargs) 23 | 24 | return node 25 | 26 | 27 | class AdditiveFK(rigFK.FK): 28 | """ 29 | An AdditiveFK chain is a standard FK chain that have one or many additional controllers to rotate the entire chain. 30 | """ 31 | _CLASS_CTRL_IK = CtrlFkAdd 32 | 33 | def __init__(self, *args, **kwargs): 34 | super(AdditiveFK, self).__init__(*args, **kwargs) 35 | self.num_ctrls = 1 36 | self.additive_ctrls = [] 37 | # Deactivate additive fk ctrl to prevent anybody to use it 38 | self.enable_addfk_ctrl = True 39 | 40 | def build(self, *args, **kwargs): 41 | super(AdditiveFK, self).build(*args, **kwargs) 42 | 43 | nomenclature_anm = self.get_nomenclature_anm() 44 | nomenclature_rig = self.get_nomenclature_rig() 45 | 46 | # Ensure to create the finger ctrl in the good orientation 47 | if nomenclature_anm.side == self.rig.nomenclature.SIDE_L: 48 | normal_data = {constants.Axis.x: (1, 0, 0), constants.Axis.y: (0, 1, 0), constants.Axis.z: (0, 0, 1)} 49 | else: 50 | normal_data = {constants.Axis.x: (-1, 0, 0), constants.Axis.y: (0, -1, 0), constants.Axis.z: (0, 0, -1)} 51 | 52 | self.additive_ctrls = filter(None, self.additive_ctrls) 53 | if not self.additive_ctrls: 54 | ctrl_add = CtrlFkAdd() 55 | self.additive_ctrls.append(ctrl_add) 56 | 57 | # HACK - Temp since we don't support multiple ctrl for the moment 58 | ctrl_add = self.additive_ctrls[0] 59 | for i, ctrl in enumerate(self.additive_ctrls): 60 | # Resolve ctrl name 61 | nomenclature = nomenclature_anm + self.rig.nomenclature(self.jnt.stripNamespace().nodeName()) 62 | if not self._FORCE_INPUT_NAME: 63 | if len(self.additive_ctrls) == 1 and len(self.chains) == 1: 64 | ctrl_name = nomenclature_anm.resolve("addFk") 65 | elif len(self.chains) == 1 or self._NAME_CTRL_ENUMERATE: 66 | ctrl_name = nomenclature_anm.resolve("addFk", "{0:02d}".format(i)) 67 | else: 68 | ctrl_name = nomenclature.resolve("addFk") 69 | else: 70 | ctrl_name = nomenclature.resolve("addFk") 71 | 72 | ctrl.build(name=ctrl_name, refs=self.chain.start, normal=normal_data[self.rig.DEFAULT_UPP_AXIS]) 73 | ctrl.offset.setMatrix(self.chain.start.getMatrix(worldSpace=True)) 74 | ctrl.setParent(self.grp_anm) 75 | # In case we don't want to see addFk ctrl, like in a hand. 76 | # TODO - In this case, maybe the hand would be best to switch it's finger to fk 77 | if not self.enable_addfk_ctrl: 78 | ctrl.visibility.set(False) 79 | libAttr.lock_hide_trs(ctrl) 80 | 81 | for i, ctrl in enumerate(self.ctrls): 82 | # HACK Add a new layer if this is the first ctrl to prevent Gimbal lock problems 83 | if i == 0: 84 | ctrl.offset = ctrl.append_layer("gimbal") 85 | attr_rotate_x = libRigging.create_utility_node('addDoubleLinear', 86 | input1=ctrl.offset.rotateX.get(), 87 | input2=ctrl_add.rotateX 88 | ).output 89 | attr_rotate_y = libRigging.create_utility_node('addDoubleLinear', 90 | input1=ctrl.offset.rotateY.get(), 91 | input2=ctrl_add.rotateY 92 | ).output 93 | attr_rotate_z = libRigging.create_utility_node('addDoubleLinear', 94 | input1=ctrl.offset.rotateZ.get(), 95 | input2=ctrl_add.rotateZ 96 | ).output 97 | pymel.connectAttr(attr_rotate_x, ctrl.offset.rotateX) 98 | pymel.connectAttr(attr_rotate_y, ctrl.offset.rotateY) 99 | pymel.connectAttr(attr_rotate_z, ctrl.offset.rotateZ) 100 | 101 | # Constraint the fk ctrls in position to the additive fk ctrls 102 | pymel.pointConstraint(ctrl_add, self.ctrls[0].offset) 103 | 104 | def iter_ctrls(self): 105 | for ctrl in super(AdditiveFK, self).iter_ctrls(): 106 | yield ctrl 107 | for ctrl in self.additive_ctrls: 108 | yield ctrl 109 | 110 | 111 | def register_plugin(): 112 | return AdditiveFK 113 | -------------------------------------------------------------------------------- /omtk/models/model_avar_base.py: -------------------------------------------------------------------------------- 1 | import pymel.core as pymel 2 | 3 | from omtk.core import classModule 4 | from omtk.libs import libAttr 5 | from omtk.libs import libRigging 6 | 7 | 8 | def _connect_with_blend(attrs, attr_dst, attr_amount): 9 | """Quick function that create two attributes with a blend factor.""" 10 | attr_blended = libRigging.create_utility_node( 11 | 'blendTwoAttr', 12 | attributesBlender=attr_amount, 13 | input=attrs, 14 | ).output 15 | pymel.connectAttr(attr_blended, attr_dst) 16 | 17 | 18 | class AvarInflBaseModel(classModule.Module): 19 | """ 20 | A deformation point on the face that move accordingly to nurbsSurface. 21 | """ 22 | SHOW_IN_UI = False 23 | 24 | _ATTR_NAME_MULT_LR = 'multiplierLr' 25 | _ATTR_NAME_MULT_UD = 'multiplierUd' 26 | _ATTR_NAME_MULT_FB = 'multiplierFb' 27 | 28 | default_multiplier_lr = 1.0 29 | default_multiplier_ud = 1.0 30 | default_multiplier_fb = 1.0 31 | 32 | support_no_inputs = True 33 | 34 | def __init__(self, *args, **kwargs): 35 | super(AvarInflBaseModel, self).__init__(*args, **kwargs) 36 | 37 | # An AvarModel will receive the avar values 38 | self._attr_inn_lr = None 39 | self._attr_inn_ud = None 40 | self._attr_inn_fb = None 41 | self._attr_inn_yw = None 42 | self._attr_inn_pt = None 43 | self._attr_inn_rl = None 44 | self._attr_inn_sx = None 45 | self._attr_inn_sy = None 46 | self._attr_inn_sz = None 47 | 48 | # The original transform of the influence 49 | self._attr_inn_offset_tm = None 50 | 51 | # The output transform of the system 52 | self._attr_out_tm = None 53 | 54 | # Reference to the object containing the bind pose of the avar. 55 | self._obj_offset = None 56 | 57 | # How much are we moving around the surface for a specific avar. 58 | self.multiplier_lr = self.default_multiplier_lr 59 | self.multiplier_ud = self.default_multiplier_ud 60 | self.multiplier_fb = self.default_multiplier_fb 61 | 62 | def _create_interface(self): 63 | # Create avar inputs 64 | self._attr_inn_lr = libAttr.addAttr(self.grp_rig, 'innAvarLr') 65 | self._attr_inn_ud = libAttr.addAttr(self.grp_rig, 'innAvarUd') 66 | self._attr_inn_fb = libAttr.addAttr(self.grp_rig, 'innAvarFb') 67 | self._attr_inn_yw = libAttr.addAttr(self.grp_rig, 'innAvarYw') 68 | self._attr_inn_pt = libAttr.addAttr(self.grp_rig, 'innAvarPt') 69 | self._attr_inn_rl = libAttr.addAttr(self.grp_rig, 'innAvarRl') 70 | self._attr_inn_sx = libAttr.addAttr(self.grp_rig, 'innAvarSx') 71 | self._attr_inn_sy = libAttr.addAttr(self.grp_rig, 'innAvarSy') 72 | self._attr_inn_sz = libAttr.addAttr(self.grp_rig, 'innAvarSz') 73 | 74 | self.multiplier_lr = libAttr.addAttr(self.grp_rig, 'innMultiplierLr', defaultValue=self.multiplier_lr) 75 | self.multiplier_ud = libAttr.addAttr(self.grp_rig, 'innMultiplierUd', defaultValue=self.multiplier_ud) 76 | self.multiplier_fb = libAttr.addAttr(self.grp_rig, 'innMultiplierFb', defaultValue=self.multiplier_fb) 77 | 78 | # Create influences 79 | _avar_filter_kwargs = { 80 | 'hasMinValue': True, 81 | 'hasMaxValue': True, 82 | 'minValue': 0.0, 83 | 'maxValue': 1.0, 84 | 'keyable': True 85 | } 86 | 87 | self._attr_inn_offset_tm = libAttr.addAttr(self.grp_rig, 'innOffset', dt='matrix') 88 | self._attr_out_tm = libAttr.addAttr(self.grp_rig, 'outTm', dataType='matrix') 89 | 90 | def build(self, **kwargs): 91 | """ 92 | The dag stack is a chain of transform nodes daisy chained together that computer the final transformation of the influence. 93 | The decision of using transforms instead of multMatrix nodes is for clarity. 94 | Note also that because of it's parent (the offset node) the stack relative to the influence original translation. 95 | """ 96 | super(AvarInflBaseModel, self).build(create_grp_anm=False, disconnect_inputs=False, **kwargs) 97 | 98 | self._create_interface() 99 | attr_tm = self._build() 100 | pymel.connectAttr(attr_tm, self._attr_out_tm) 101 | 102 | def unbuild(self): 103 | # Cleanup old deprecated properties to prevent invalid pynode warning. 104 | self.grp_offset = None 105 | 106 | # Save the current uv multipliers. 107 | # It is very rare that the rigger will tweak this advanced setting manually, 108 | # however for legacy reasons, it might be useful when upgrading an old rig. 109 | if isinstance(self.multiplier_lr, pymel.Attribute) and self.multiplier_lr.exists(): 110 | self.multiplier_lr = self.multiplier_lr.get() 111 | if isinstance(self.multiplier_ud, pymel.Attribute) and self.multiplier_ud.exists(): 112 | self.multiplier_ud = self.multiplier_ud.get() 113 | if isinstance(self.multiplier_fb, pymel.Attribute) and self.multiplier_fb.exists(): 114 | self.multiplier_fb = self.multiplier_fb.get() 115 | 116 | super(AvarInflBaseModel, self).unbuild() 117 | 118 | def _build(self): 119 | """ 120 | :return: A matrix pymel.Attribute containing the resulting transform. 121 | """ 122 | raise NotImplementedError 123 | 124 | def connect_avar(self, avar): 125 | pymel.connectAttr(avar.attr_lr, self._attr_inn_lr) 126 | pymel.connectAttr(avar.attr_ud, self._attr_inn_ud) 127 | pymel.connectAttr(avar.attr_fb, self._attr_inn_fb) 128 | pymel.connectAttr(avar.attr_yw, self._attr_inn_yw) 129 | pymel.connectAttr(avar.attr_pt, self._attr_inn_pt) 130 | pymel.connectAttr(avar.attr_rl, self._attr_inn_rl) 131 | pymel.connectAttr(avar.attr_sx, self._attr_inn_sx) 132 | pymel.connectAttr(avar.attr_sy, self._attr_inn_sy) 133 | pymel.connectAttr(avar.attr_sz, self._attr_inn_sz) 134 | pymel.connectAttr(avar.grp_offset.matrix, self._attr_inn_offset_tm) 135 | -------------------------------------------------------------------------------- /omtk/widget_list_influences.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pymel.core as pymel 3 | from ui import widget_list_influences 4 | 5 | from omtk.libs import libQt 6 | from omtk.libs import libPython 7 | from omtk.libs import libPymel 8 | 9 | from omtk.vendor import libSerialization 10 | from omtk.vendor.Qt import QtCore, QtGui, QtWidgets 11 | 12 | import ui_shared 13 | 14 | 15 | class WidgetListInfluences(QtWidgets.QWidget): 16 | onRightClick = QtCore.Signal() 17 | 18 | def __init__(self, parent=None): 19 | super(WidgetListInfluences, self).__init__(parent=parent) 20 | 21 | self._rig = None 22 | 23 | self.ui = widget_list_influences.Ui_Form() 24 | self.ui.setupUi(self) 25 | 26 | # Tweak gui 27 | self.ui.treeWidget.setStyleSheet(ui_shared._STYLE_SHEET) 28 | 29 | # Connect signals 30 | self.ui.treeWidget.customContextMenuRequested.connect(self.onRightClick) 31 | 32 | # Connect events 33 | self.ui.treeWidget.itemSelectionChanged.connect(self.on_influence_selection_changed) 34 | self.ui.lineEdit_search.textChanged.connect(self.on_query_changed) 35 | self.ui.checkBox_hideAssigned.stateChanged.connect(self.on_query_changed) 36 | self.ui.btn_update.pressed.connect(self.update) 37 | 38 | def set_rig(self, rig, update=True): 39 | self._rig = rig 40 | if update: 41 | self.update() 42 | 43 | @libPython.log_execution_time('update_ui_jnts') 44 | def update(self, *args, **kwargs): 45 | self.ui.treeWidget.clear() 46 | 47 | if self._rig is None: 48 | return 49 | 50 | all_potential_influences = self._rig.get_potential_influences() 51 | 52 | if all_potential_influences: 53 | data = libPymel.get_tree_from_objs(all_potential_influences, sort=True) 54 | 55 | self._fill_widget_influences(self.ui.treeWidget.invisibleRootItem(), data) 56 | self.ui.treeWidget.sortItems(0, QtCore.Qt.AscendingOrder) 57 | 58 | self.update_list_visibility() 59 | 60 | def _fill_widget_influences(self, qt_parent, data): 61 | obj = pymel.PyNode(data.val) if data.val else None 62 | if obj: 63 | obj_name = obj.name() 64 | 65 | fnFilter = lambda x: libSerialization.is_network_from_class(x, 'Module') 66 | networks = libSerialization.get_connected_networks(obj, key=fnFilter, recursive=False) 67 | 68 | textBrush = QtGui.QBrush(QtCore.Qt.white) 69 | 70 | if self._is_influence(obj): # todo: listen to the Rig class 71 | qItem = QtWidgets.QTreeWidgetItem(0) 72 | qItem.obj = obj 73 | 74 | # Monkey-patch mesh QWidget 75 | qItem.metadata_type = ui_shared.MetadataType.Influence 76 | qItem.metadata_data = obj 77 | 78 | qItem.networks = networks 79 | qItem.setText(0, obj_name) 80 | qItem.setForeground(0, textBrush) 81 | ui_shared.set_icon_from_type(obj, qItem) 82 | qItem.setCheckState(0, QtCore.Qt.Checked if networks else QtCore.Qt.Unchecked) 83 | if qItem.flags() & QtCore.Qt.ItemIsUserCheckable: 84 | qItem.setFlags(qItem.flags() ^ QtCore.Qt.ItemIsUserCheckable) 85 | qt_parent.addChild(qItem) 86 | qt_parent = qItem 87 | 88 | for child_data in data.children: 89 | self._fill_widget_influences(qt_parent, child_data) 90 | 91 | def _is_influence(self, obj): 92 | """ 93 | Supported influences are joints and nurbsSurface. 94 | :return: 95 | """ 96 | return libPymel.isinstance_of_transform(obj, pymel.nodetypes.Joint) or \ 97 | libPymel.isinstance_of_shape(obj, pymel.nodetypes.NurbsSurface) 98 | 99 | def update_list_visibility(self, query_regex=None): 100 | if query_regex is None: 101 | query_raw = self.ui.lineEdit_search.text() 102 | query_regex = ".*{0}.*".format(query_raw) if query_raw else ".*" 103 | 104 | unselectableBrush = QtGui.QBrush(QtCore.Qt.darkGray) 105 | selectableBrush = QtGui.QBrush(QtCore.Qt.white) 106 | for qt_item in libQt.get_all_QTreeWidgetItem(self.ui.treeWidget): 107 | can_show = self._can_show_QTreeWidgetItem(qt_item, query_regex) 108 | qt_item.setHidden(not can_show) 109 | if can_show: 110 | qt_item.setForeground(0, selectableBrush) 111 | flags = qt_item.flags() 112 | if not flags & QtCore.Qt.ItemIsSelectable: # Make selectable 113 | flags ^= QtCore.Qt.ItemIsSelectable 114 | qt_item.setFlags(flags) 115 | self._show_parent_recursive(qt_item.parent()) 116 | else: 117 | qt_item.setForeground(0, unselectableBrush) 118 | flags = qt_item.flags() 119 | if flags & QtCore.Qt.ItemIsSelectable: # Make selectable 120 | flags ^= QtCore.Qt.ItemIsSelectable 121 | qt_item.setFlags(flags) 122 | 123 | self.ui.treeWidget.expandAll() 124 | 125 | def _can_show_QTreeWidgetItem(self, qItem, query_regex): 126 | obj = qItem.obj # Retrieve monkey-patched data 127 | obj_name = obj.name() 128 | # print obj_name 129 | 130 | if not re.match(query_regex, obj_name, re.IGNORECASE): 131 | return False 132 | 133 | if self.ui.checkBox_hideAssigned.isChecked(): 134 | if qItem.networks: 135 | return False 136 | 137 | return True 138 | 139 | def _show_parent_recursive(self, qt_parent_item): 140 | if qt_parent_item is not None: 141 | if qt_parent_item.isHidden: 142 | qt_parent_item.setHidden(False) 143 | self._show_parent_recursive(qt_parent_item.parent()) 144 | 145 | def get_selection(self): 146 | result = [] 147 | for item in self.ui.treeWidget.selectedItems(): 148 | if item.obj.exists(): 149 | result.append(item.obj) 150 | return result 151 | 152 | # 153 | # Events 154 | # 155 | 156 | def on_influence_selection_changed(self): 157 | pymel.select(self.get_selection()) 158 | 159 | def on_query_changed(self): 160 | self.update_list_visibility() 161 | -------------------------------------------------------------------------------- /omtk/widget_create_component_wizard_parts.py: -------------------------------------------------------------------------------- 1 | from omtk.vendor.Qt import QtCore, QtWidgets 2 | from ui import widget_component_wizard_parts 3 | 4 | import pymel.core as pymel 5 | 6 | 7 | class ComponentPartModel(QtCore.QAbstractTableModel): 8 | ID_COLUMN_NAME = 0 9 | ID_COLUMN_ATTR_NAME = 1 10 | 11 | onNetworkChanged = QtCore.Signal() 12 | 13 | def __init__(self, entries): 14 | super(ComponentPartModel, self).__init__() 15 | self._entries = entries 16 | 17 | def rowCount(self, index): 18 | return len(self._entries) 19 | 20 | def columnCount(self, index): 21 | return 2 22 | 23 | def data(self, index, role): 24 | col = index.column() 25 | row = index.row() 26 | if col == self.ID_COLUMN_NAME: 27 | if role == QtCore.Qt.DisplayRole: 28 | return str(self._entries[row]) 29 | if role == QtCore.Qt.CheckStateRole: 30 | entry = self._entries[row] 31 | return entry.is_connected() 32 | elif col == self.ID_COLUMN_ATTR_NAME: 33 | if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: 34 | entry = self._entries[row] 35 | return entry.attr_name 36 | 37 | def headerData(self, section, orientation, role): 38 | if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: 39 | if section == self.ID_COLUMN_NAME: 40 | return 'Node' 41 | elif section == self.ID_COLUMN_ATTR_NAME: 42 | return 'Attr Name' 43 | 44 | def setData(self, index, value, role): 45 | if not value: 46 | return False 47 | 48 | if role == QtCore.Qt.CheckStateRole: 49 | index = index.row() 50 | is_checked = bool(value) 51 | entry = self._entries[index] 52 | if is_checked: 53 | entry.connect() 54 | else: 55 | entry.disconnect() 56 | return True 57 | 58 | if role == QtCore.Qt.EditRole: 59 | index = index.row() 60 | entry = self._entries[index] 61 | entry.rename_attr(value) 62 | self.onNetworkChanged.emit() 63 | 64 | return False 65 | 66 | def flags(self, index): 67 | flags = super(ComponentPartModel, self).flags(index) 68 | col = index.column() 69 | if col == self.ID_COLUMN_NAME: 70 | flags |= QtCore.Qt.ItemIsUserCheckable 71 | if col == self.ID_COLUMN_ATTR_NAME: 72 | flags |= QtCore.Qt.ItemIsEditable 73 | return flags 74 | 75 | def reset(self): 76 | self.beginResetModel() 77 | self.endResetModel() 78 | 79 | def add_entries(self, entries, update=False): 80 | need_update = False 81 | for entry in entries: 82 | # assert (isinstance(entry, ComponentPart)) 83 | if not entry in self._entries: 84 | self._entries.append(entry) 85 | entry.initialize() 86 | entry.connect() 87 | need_update = True 88 | 89 | if need_update and update: 90 | self.reset() 91 | 92 | def have_entry_for_node(self, obj): 93 | for entry in self._entries: 94 | for entry_obj in entry.iter_nodes(): 95 | if obj == entry_obj: 96 | return True 97 | return False 98 | 99 | 100 | class WidgetCreateComponentWizardParts(QtWidgets.QWidget): 101 | _cls = None 102 | 103 | # Triggered when the scene changed 104 | onNetworkChanged = QtCore.Signal() 105 | 106 | def __init__(self, parent): 107 | self._wizard = None 108 | self._cls = None 109 | 110 | super(WidgetCreateComponentWizardParts, self).__init__(parent) 111 | 112 | self.ui = widget_component_wizard_parts.Ui_Form() 113 | self.ui.setupUi(self) 114 | 115 | self.model = ComponentPartModel([]) 116 | self.model.onNetworkChanged.connect(self.onNetworkChanged.emit) 117 | 118 | self.ui.tableView.setModel(self.model) 119 | 120 | self.ui.pushButton_add.pressed.connect(self.on_added) 121 | self.ui.pushButton_remove.pressed.connect(self.on_remove) 122 | self.ui.pushButton_connect.pressed.connect(self.on_connect) 123 | self.ui.pushButton_disconnect.pressed.connect(self.on_disconnect) 124 | 125 | def get_selected_entries(self): 126 | indexes = set(qindex.row() for qindex in self.ui.tableView.selectedIndexes()) 127 | entries = [self.model._entries[index] for index in indexes] 128 | return entries 129 | 130 | def can_add(self, obj): 131 | # Prevent adding the same object twice. 132 | return not self.model.have_entry_for_node(obj) 133 | 134 | def on_added(self): 135 | assert (self._cls is not None) # ensure the class have been set 136 | new_entries = [] 137 | for obj in pymel.selected(): 138 | if self.can_add(obj): 139 | entry = self._cls(self._wizard, obj) 140 | new_entries.append(entry) 141 | 142 | if new_entries: 143 | self.model.add_entries(new_entries, update=True) 144 | self.onNetworkChanged.emit() 145 | 146 | def on_remove(self): 147 | need_update = False 148 | entries_to_remove = self.get_selected_entries() 149 | for entry in entries_to_remove: 150 | if entry.is_connected(): 151 | entry.disconnect() 152 | entry.delete() # todo: implement 153 | self.model._entries.remove(entry) 154 | need_update = True 155 | if need_update: 156 | self.model.reset() 157 | 158 | def on_connect(self): 159 | need_update = False 160 | entries = self.get_selected_entries() 161 | for entry in entries: 162 | if not entry.is_connected(): 163 | entry.connect() 164 | need_update = True 165 | if need_update: 166 | self.model.reset() 167 | 168 | def on_disconnect(self): 169 | need_update = False 170 | entries = self.get_selected_entries() 171 | for entry in entries: 172 | if entry.is_connected(): 173 | entry.disconnect() 174 | need_update = True 175 | if need_update: 176 | self.model.reset() 177 | 178 | def set_entries(self, wizard, cls, entries): 179 | self._wizard = wizard 180 | self._cls = cls 181 | self.model._entries = entries 182 | self.model.reset() 183 | -------------------------------------------------------------------------------- /omtk/core/classModuleCompound.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | 4 | from omtk.core.classModule import Module 5 | from omtk.libs import libRigging 6 | 7 | import pymel.core as pymel 8 | from maya import cmds 9 | 10 | 11 | def _connect_matrix_attr_to_transform(attr_tm, node): 12 | util_decompose = libRigging.create_utility_node( 13 | 'decomposeMatrix', 14 | inputMatrix=attr_tm 15 | ) 16 | pymel.connectAttr(util_decompose.outputTranslate, node.translate) 17 | pymel.connectAttr(util_decompose.outputRotate, node.rotate) 18 | pymel.connectAttr(util_decompose.outputScale, node.scale) 19 | 20 | 21 | class CompoundModule(Module): 22 | def __init__(self, *args, **kwargs): 23 | super(CompoundModule, self).__init__(*args, **kwargs) 24 | self.grp_inn = None 25 | self.grp_out = None 26 | self.grp_dag = None 27 | self.grp_guides = None 28 | self.grp_influences = None 29 | 30 | def __get_compounnd_path__(self): 31 | """Return the path to the .ma containing the compound.""" 32 | cls_path = inspect.getfile(self.__class__) 33 | dirname = os.path.dirname(cls_path) 34 | filename = os.path.basename(cls_path) 35 | basename = os.path.splitext(filename)[0] 36 | path = os.path.join(dirname, basename + '.ma') 37 | if not os.path.exists(path): 38 | raise Exception("Cannot find {0}".format(path)) 39 | return path 40 | 41 | def _import_component(self, path, namespace): 42 | """ 43 | Simple implement of the component workflow in omtk. 44 | This will import a .ma file and resolve the public interface. 45 | This take a lot of things in consideration like how objects are named, and more. 46 | """ 47 | cmds.file(path, i=True, namespace=namespace) 48 | 49 | def _get_compound_namespace(self): 50 | """ 51 | Resolve the namespace associated with the Compound. 52 | Keeping the namespace is highly efficient in keep the scene clean. 53 | """ 54 | return self.grp_inn.namespace().strip(':') 55 | 56 | def create_compound_ctrl(self, cls, inst, suffix, attr_inn_name, attr_out_name, **kwargs): 57 | """ 58 | Initialize and build and connect a controller used in the Compound. 59 | :param cls: The desired class for the controller. 60 | :param inst: The current instance. 61 | :param suffix: A str that identify this controller. 62 | :param attr_inn_name: The name of the network attribute that receive the controller local matrix. 63 | :param attr_out_name: The name of the network attribute that will drive the controller offset world matrix. 64 | :param kwargs: Any keyword argument will be passed to the controller .build() method. 65 | :return: A controller instance of the desired type. 66 | """ 67 | attr_inn = self.grp_inn.attr(attr_inn_name) 68 | attr_out = self.grp_out.attr(attr_out_name) 69 | nomenclature_anm = self.get_nomenclature_anm() 70 | 71 | inst = self.init_ctrl(cls, inst) 72 | ref_tm = attr_out.get() 73 | 74 | inst.build( 75 | name=nomenclature_anm.resolve(suffix), 76 | geometries=self.rig.get_meshes(), 77 | refs=[ref_tm], 78 | **kwargs 79 | ) 80 | inst.setParent(self.grp_anm) 81 | 82 | pymel.connectAttr(inst.matrix, attr_inn) 83 | _connect_matrix_attr_to_transform(attr_out, inst.offset) 84 | return inst 85 | 86 | def create_compound_influence(self, suffix, attr_name, target=None, **kwargs): 87 | """ 88 | Initialize and build and connect an influence used in the Compound. 89 | :param suffix: A str that identify this influence. 90 | :param attr_name: The name of the network attribute that will drive the influence world matrix. 91 | :param target: An optional pymel.nodetypes.Transform that will be constrained to the influence. 92 | :param kwargs: Any keyword argument will be passed to pymel.joint(). 93 | :return: A pymel.nodetypes.Joint. 94 | """ 95 | inst = pymel.createNode('joint', name=self.get_nomenclature_jnt().resolve(suffix)) 96 | inst.setParent(self.grp_influences) 97 | attr = self.grp_out.attr(attr_name) 98 | _connect_matrix_attr_to_transform(attr, inst) 99 | 100 | if target: 101 | pymel.parentConstraint(inst, target, maintainOffset=True) 102 | pymel.scaleConstraint(inst, target, maintainOffset=True) 103 | 104 | return inst 105 | 106 | def build(self, create_grp_inf=True, **kwargs): 107 | nomenclature_rig = self.get_nomenclature_rig() 108 | 109 | # Hack: Need to create self.grp_influences first since parent_to is called by .build() ... 110 | if create_grp_inf: 111 | self.grp_influences = pymel.createNode('transform', name=nomenclature_rig.resolve('influences')) 112 | 113 | super(CompoundModule, self).build(**kwargs) 114 | 115 | path = self.__get_compounnd_path__() 116 | namespace = '_{0}'.format(self.name) # we prefix with an underscore to ensure that the namespace is recognized 117 | self._import_component(path, namespace) 118 | 119 | # Resolve grp_inn (mandatory) 120 | grp_inn_dagpath = '{0}:inn'.format(namespace) 121 | if not cmds.objExists(grp_inn_dagpath): 122 | raise Exception("Cannot find {0}".format(grp_inn_dagpath)) 123 | self.grp_inn = pymel.PyNode(grp_inn_dagpath) 124 | self.grp_inn.setParent(self.grp_rig) 125 | 126 | # Resolve grp_out (mandatory) 127 | grp_out_dagpath = '{0}:out'.format(namespace) 128 | if not cmds.objExists(grp_out_dagpath): 129 | raise Exception("Cannot find {0}".format(grp_out_dagpath)) 130 | self.grp_out = pymel.PyNode(grp_out_dagpath) 131 | self.grp_out.setParent(self.grp_rig) 132 | 133 | # Resolve grp_dag (optional) 134 | grp_dag_dagpath = '{0}:dag'.format(namespace) 135 | if cmds.objExists(grp_dag_dagpath): 136 | self.grp_dag = pymel.PyNode(grp_dag_dagpath) 137 | self.grp_dag.setParent(self.grp_rig) 138 | self.grp_dag.visibility.set(False) 139 | else: 140 | self.grp_dag = None 141 | 142 | # Resolve grp_guides (optional) 143 | grp_guides_dagpath = '{0}:guides'.format(namespace) 144 | if cmds.objExists(grp_guides_dagpath): 145 | self.grp_guides = pymel.PyNode(grp_guides_dagpath) 146 | self.grp_guides.setParent(self.grp_rig) 147 | self.grp_guides.visibility.set(False) 148 | else: 149 | self.grp_guides = None 150 | 151 | if self.grp_influences: 152 | self.grp_influences.setParent(self.grp_rig) 153 | 154 | def parent_to(self, parent): 155 | super(CompoundModule, self).parent_to(parent) 156 | if self.grp_influences: 157 | pymel.parentConstraint(parent, self.grp_influences) 158 | pymel.scaleConstraint(parent, self.grp_influences) 159 | -------------------------------------------------------------------------------- /omtk/libs/libCtrlMatch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Library that allow facial ctrls to be matched/mirrored/baked. 3 | Original code by Jimmy Goulet (https://github.com/goujin), thanks for the contribution! 4 | todo: Use className to find sibling. 5 | """ 6 | import pymel.core as pymel 7 | from omtk.libs import libRigging 8 | 9 | 10 | def get_orig_shape(shape): 11 | return next((hist for hist in shape.listHistory() 12 | if isinstance(hist, pymel.nodetypes.NurbsCurve) 13 | and hist != shape 14 | and hist.intermediateObject.get()), None) 15 | 16 | 17 | def safety_check(check_object=None, check_curve_list=None): 18 | if check_object: 19 | if not isinstance(check_object, pymel.nodetypes.Transform): 20 | return False 21 | elif check_curve_list: 22 | for each in check_curve_list: 23 | if not isinstance(each, pymel.nodetypes.NurbsCurve): 24 | return False 25 | 26 | 27 | def get_previous_controller_info(previous_controler): 28 | # this implementation assumes your only using one shape or that the the first shape of children shapes is representative of the lot 29 | assumed_only_shape = previous_controler.getShape() 30 | 31 | if assumed_only_shape.overrideEnabled.get(): # will return False if it isn't activated 32 | if assumed_only_shape.overrideRGBColors.get(): 33 | rgb_color = assumed_only_shape.overrideColorRGB.get() 34 | color_info = [True, True, rgb_color] 35 | else: 36 | index_color = assumed_only_shape.overrideColor.get() 37 | color_info = [True, False, index_color] 38 | else: 39 | color_info = [False, False, []] 40 | 41 | if assumed_only_shape.visibility.isConnected(): 42 | visibility_connection_info = assumed_only_shape.visibility.connections(plugs=True)[0] 43 | 44 | else: 45 | visibility_connection_info = False 46 | 47 | return (color_info, visibility_connection_info) 48 | 49 | 50 | def adapt_to_orig_shape(source, target): 51 | """ 52 | :param source: source shape to transfer 53 | :param target: target to transfer to 54 | This is based out of Renaud's code on shape to orig when building and unbuilding with omtk to preserve shape info. 55 | """ 56 | 57 | def get_transformGeometry(shape): 58 | return next((hist for hist in shape.listHistory() 59 | if isinstance(hist, pymel.nodetypes.TransformGeometry)), None) 60 | 61 | # Resolve orig shape 62 | shape_orig = get_orig_shape(target) 63 | 64 | # Resolve compensation matrix 65 | util_transform_geometry = get_transformGeometry(target) 66 | if not util_transform_geometry: 67 | target.warning("Skipping {}. Cannot find transformGeometry.".format(target)) 68 | return 69 | attr_compensation_tm = next(iter(util_transform_geometry.transform.inputs(plugs=True)), None) 70 | 71 | if not attr_compensation_tm: 72 | target.warning("Skipping {}. Cannot find compensation matrix.".format(target)) 73 | return 74 | 75 | tmp_transform_geometry = libRigging.create_utility_node( 76 | 'transformGeometry', 77 | inputGeometry=source.local, 78 | transform=attr_compensation_tm, 79 | invertTransform=True 80 | ) 81 | 82 | # source.getParent().setParent(grp_offset) JG modification source should already be in place 83 | pymel.connectAttr(tmp_transform_geometry.outputGeometry, shape_orig.create) 84 | 85 | # Cleanup 86 | pymel.refresh(force=True) # but why do I have to refresh^! 87 | pymel.disconnectAttr(shape_orig.create) 88 | pymel.delete(tmp_transform_geometry) 89 | 90 | 91 | def controller_matcher(selection=None, mirror_prefix=None, flip=True): 92 | """it will try to find it's match on the other side of the rig 93 | Select controls curves (ex. 'leg_front_l_ik_ctrl'), and set the mirror prefix ('_l_', '_r_') 94 | flip is for flipping on the X axis the shapes of the ctrl""" 95 | if selection is None: 96 | selection = pymel.selected() 97 | 98 | if not mirror_prefix: 99 | if len(selection) != 2: 100 | msg = """The only supported behavior when no mirror_prefix is given, is to have only two controlers selected. 101 | It will match the first controller to the second one.""" 102 | pymel.warning(msg) 103 | return "Error" 104 | transfer_shape(*selection, flip=flip) 105 | 106 | else: 107 | 108 | for selected_object in selection: 109 | _possible_sides = list(mirror_prefix) 110 | skip_mechanism = False # This is in place to protect from possible controller having no mirror prefix at all 111 | 112 | if mirror_prefix[0] in selected_object.name(): 113 | current_side = _possible_sides.pop(0) 114 | 115 | elif mirror_prefix[1] in selected_object.name(): 116 | current_side = _possible_sides.pop(1) 117 | 118 | else: 119 | skip_mechanism = True 120 | 121 | if skip_mechanism: 122 | pass 123 | else: 124 | target_name = selected_object.name().replace(current_side, _possible_sides[0]) 125 | if pymel.objExists(target_name): 126 | target = pymel.PyNode(selected_object.name().replace(current_side, _possible_sides[0])) 127 | transfer_shape(selected_object, target, flip=True) 128 | 129 | 130 | def transfer_shape(source, target, flip=True): 131 | """it will replace the shape of selected number 2 with the shapes of selected number 1""" 132 | 133 | target_shape = target.getShape(noIntermediate=True) 134 | target_shape_orig = get_orig_shape(target_shape) 135 | 136 | dup = pymel.duplicate(source, rc=1)[0] 137 | tmp = pymel.createNode('transform') 138 | pymel.parent(tmp, dup) 139 | pymel.xform(tmp, t=(0, 0, 0), ro=(0, 0, 0), scale=(1, 1, 1)) 140 | pymel.parent(tmp, w=1) 141 | for sh in dup.getShapes(noIntermediate=True): 142 | pymel.parent(sh, tmp, r=1, s=1) 143 | 144 | pymel.delete(dup) 145 | temp_grp_negScale = pymel.createNode('transform') 146 | pymel.parent(tmp, temp_grp_negScale) 147 | if flip: 148 | temp_grp_negScale.scaleX.set(-1) 149 | 150 | pymel.parent(tmp, target) 151 | pymel.delete(temp_grp_negScale) 152 | 153 | pymel.makeIdentity(tmp, t=True) # this brings translate values at 0 before scale freezing 154 | pymel.makeIdentity(tmp, apply=True, t=True, r=True, s=True) 155 | pymel.parent(tmp, w=1) 156 | 157 | color_info, vis_master = get_previous_controller_info(target) 158 | 159 | shapes_has_been_deleted = False 160 | for sh in tmp.getShapes(): 161 | if target_shape_orig: 162 | adapt_to_orig_shape(sh, target.getShape()) 163 | else: 164 | if not shapes_has_been_deleted: 165 | shapesDel = target.getShapes() 166 | if shapesDel: pymel.delete(shapesDel) 167 | shapes_has_been_deleted = True 168 | 169 | pymel.parent(sh, target, r=1, s=1) 170 | pymel.rename(sh.name(), target.name() + "Shape") 171 | 172 | if color_info[0]: 173 | if color_info[1]: 174 | sh.overrideEnabled.set(True) 175 | sh.overrideRGBColors.set(1) 176 | sh.overrideColorRGB.set(color_info[2]) 177 | 178 | else: 179 | sh.overrideEnabled.set(True) 180 | sh.overrideRGBColors.set(0) 181 | sh.overrideColor.set(color_info[2]) 182 | 183 | else: 184 | sh.overrideEnabled.set(False) 185 | 186 | if vis_master: 187 | vis_master.connect(sh.visibility) 188 | 189 | pymel.delete(tmp) 190 | --------------------------------------------------------------------------------