diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/genindex.html b/Scripts/Animation/epic_pose_wrangler/docs/site/html/genindex.html new file mode 100644 index 0000000..c1b68fd --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/genindex.html @@ -0,0 +1,310 @@ + + + + + + Index — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Index
  • +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | C + | D + | E + | G + | I + | L + | M + | O + | P + | R + | S + | U + | V + +
+

A

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

G

+ + + +
+ +

I

+ + +
+ +

L

+ + +
+ +

M

+ + + +
+ +

O

+ + +
+ +

P

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/index.html b/Scripts/Animation/epic_pose_wrangler/docs/site/html/index.html new file mode 100644 index 0000000..3c56d0d --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/index.html @@ -0,0 +1,168 @@ + + + + + + + Pose Wrangler Documentation — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Pose Wrangler Documentation

+ +
+
+

Overview

+

PoseWrangler is a tool for interfacing with Epic’s MayaUERBFPlugin. The plugin is distributed by Epic Games and installed via Quixel Bridge.

+
+

Versions

+
+

1.0.0:

+
    +
  • Supports legacy scenes created with the UE4RBFSolverNode

  • +
  • Supports Maya 2018-2022

  • +
  • Provides upgrade workflow to migrate to V2

  • +
+
+
+

2.0.0:

+
    +
  • Supports scenes created with the UERBFSolverNode

  • +
  • Multiple Driver Support

  • +
  • Initial blendshape support (WIP)

  • +
  • Supports Maya 2018-2022

  • +
  • Support for custom mirror mappings to allow for rigs with naming conventions that deviate from the default UE5 conventions

  • +
  • Fully automatable via Python and MayaPy

  • +
  • Serialization/deserialization to dictionary or JSON file

  • +
  • Support for custom extensions and context menu actions

  • +
+
+
+
+

Contributors

+
    +
  • Chris Evans

  • +
  • Judd Simantov

  • +
  • David Corral

  • +
  • Borna Berc

  • +
  • Chris Theodosius

  • +
+
+
+
+

Indices and tables

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/mirror_mapping.html b/Scripts/Animation/epic_pose_wrangler/docs/site/html/mirror_mapping.html new file mode 100644 index 0000000..965287b --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/mirror_mapping.html @@ -0,0 +1,168 @@ + + + + + + + Mirror Mapping — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Mirror Mapping

+

Mirror mapping enables Pose Wrangler to support custom skeletons when mirroring RBF solvers and poses.

+

Below is the mirror mapping for characters following the MetaHuman naming conventions:

+
+
{
+    "solver_expression": "(?P<prefix>[a-zA-Z0-9]+)?(?P<side>_[lr]{1}_)(?P<suffix>[a-zA-Z0-9_]+)",
+    "transform_expression": "(?P<prefix>[a-zA-Z0-9_]+)?(?P<side>_[lr]{1}_)(?P<suffix>[a-zA-Z0-9_]+)",
+    "left": {
+        "solver_syntax": "_l_",
+        "transform_syntax": "_l_"
+        },
+    "right": {
+        "solver_syntax": "_r_",
+        "transform_syntax": "_r_"
+    }
+}
+
+
+
+ + ++++ + + + + + + + + + + + + + + + + + + + +
Title

Key

Definition

solver_expression

A regular expression used to match against the solver node name

transform_expression

A regular expression used to match against the transform (joint) node name

left

dictionary containing the expected match for the solver_expression and
transform_expression

right

dictionary containing the expected match for the solver_expression and
transform_expression

+
+

Note

+

As an example if we wanted to mirror a solver called calf_l_UERBFSolver we first try to match the name +of the solver against the solver_expression. If the match fails, you’ll see an error appear in the output +log that the solver name doesn’t match the settings defined in mirror mapping.

+

If the match succeeds, it will then iterate through the groups in the matchdict to determine if one of the groups +matches the solver_syntax for the left hand side. If it does, it will replace it with the right and vice versa.

+

In this case, we will end up with a matchdict that looks something like this:
+{"prefix": "calf", "side": "_l_", "suffix": "UERBFSolver"}
+The mapping will iterate through this dictionary and attempt to match each value against the solver_syntax of +the left and right group defined in mirror mapping. If it matches the left group it will replace the +side value with the solver_syntax specified in the right group resulting in the following dictionary:
+{"prefix": "calf", "side": "_r_", "suffix": "UERBFSolver"}

+

The same thing then happens for the drivers and driven transforms associated with the solver, using transform_expression +and transform_syntax in place of the solver.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/objects.inv b/Scripts/Animation/epic_pose_wrangler/docs/site/html/objects.inv new file mode 100644 index 0000000..7ca6758 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/objects.inv @@ -0,0 +1,7 @@ +# Sphinx inventory version 2 +# Project: Pose Wrangler +# Version: +# The remainder of this file is compressed using zlib. +xڵMo0 k6,rhvd "v`C,fPC5EI+ p0}Z  +װm‰sUR\qԶDC p30԰=H켓)#l>g%H+a'uƀt6Jq +(J`0/XwF c|> (sK|d2%mF4pمP z im은D*lc)T1c~W E,1`Afh%1=˝c9ԀP{H7Z&_W,ɒ?ǁP84ٿPO=ZE˶7zi}Nߥű5^Tt8@Saᯮ4qE5ݕ}-E:HUVc; .0q!-LR895og1%#6KOQ\ |%6mWJy_R?P?QI-?=zIbGm + + + + + + 2.0.0 Overview — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

2.0.0 Overview

+

Version 2.0.0 is a refactor of the original Pose Wrangler. The goal of this version has been to separate the UI +logic from the backend, enabling users to automate RBF setup via Python and create custom extensions to add new +functionality.

+
+

UI Overview

+UI Overview +
+

1) Solver View

+

Here is where you can find a list of all the RBF solvers currently available in the scene and can edit, create, delete +or mirror solvers.

+

In order to make changes to the poses, drivers or driven transforms/blendshapes you need to toggle a solver into edit +mode via the Edit Selected Solver button.

+
+

Note

+

Only one solver can be edited at a single time.

+
+
+
+

2) Pose View

+

Here you can find a list of all the poses. In order to make any changes to the poses, you need to edit the corresponding solver.

+
+

Warning

+

Most of these are self-explanatory, but is it worth noting that Mute Pose will enable and disable the pose on the +solver via the targetEnable attribute. This means that if the RBF solver node is set to use automatic radius +then the radius will be calculated without the influence of the muted poses.

+
+
+
+

3) Driver View

+

2.0.0 supports multiple drivers. Here you can add/remove driver transforms.

+
+
+

4) Driven Transforms / Blendshape View

+

Here you can add/remove driven transforms and create/add blendshapes.

+
+

Note

+

The blendshape workflow is currently a work in progress and may change in the future.

+
+
+
+

5) Extensions and Settings View

+

Pose Wrangler now supports writing custom extensions for the tool that can be embedded into the UI here. +Core Extensions are default extensions that ship with Pose Wrangler. Pose Exporter is an example of a custom +extension used by the MetaHuman team. For more information on how these extensions work and how to create your own, +please refer to the Extensions page.

+

You can also change the mirror mapping, which will allow you to specify a custom mapping if your skeleton does not +match the default MetaHuman skeleton and the mirror solver/pose functionality is causing unexpected results.

+
+
+

6) Top Menu

+

Here you can import/export RBF solvers to/from a JSON file, change the theme and view the help.

+
+
+

7) Output Log

+

Displays debug, info, warning and error messages.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/search.html b/Scripts/Animation/epic_pose_wrangler/docs/site/html/search.html new file mode 100644 index 0000000..0d13311 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/search.html @@ -0,0 +1,119 @@ + + + + + + Search — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Search
  • +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/searchindex.js b/Scripts/Animation/epic_pose_wrangler/docs/site/html/searchindex.js new file mode 100644 index 0000000..25bb901 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["api","extensions","index","mirror_mapping","overview","upgrading"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":5,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["api.rst","extensions.rst","index.rst","mirror_mapping.rst","overview.rst","upgrading.rst"],objects:{"pose_wrangler.v2.main":[[0,0,1,"","UERBFAPI"]],"pose_wrangler.v2.main.UERBFAPI":[[0,1,1,"","VERSION"],[0,2,1,"","add_blendshape"],[0,2,1,"","add_driven_transforms"],[0,2,1,"","add_drivers"],[0,2,1,"","create_blendshape"],[0,2,1,"","create_pose"],[0,2,1,"","create_rbf_solver"],[0,3,1,"","current_solver"],[0,2,1,"","delete_blendshape"],[0,2,1,"","delete_pose"],[0,2,1,"","delete_rbf_solver"],[0,2,1,"","deserialize"],[0,2,1,"","deserialize_from_file"],[0,2,1,"","edit_blendshape"],[0,2,1,"","edit_solver"],[0,3,1,"","extensions"],[0,2,1,"","get_context"],[0,2,1,"","get_extension_by_type"],[0,2,1,"","get_rbf_solver_by_name"],[0,2,1,"","get_solver_edit_status"],[0,2,1,"","get_ui_context"],[0,2,1,"","go_to_pose"],[0,2,1,"","isolate_blendshape"],[0,2,1,"","load"],[0,3,1,"","mirror_mapping"],[0,2,1,"","mirror_pose"],[0,2,1,"","mirror_rbf_solver"],[0,2,1,"","mute_pose"],[0,3,1,"","rbf_solvers"],[0,2,1,"","remove_driven"],[0,2,1,"","remove_drivers"],[0,2,1,"","rename_pose"],[0,2,1,"","serialize"],[0,2,1,"","serialize_to_file"],[0,2,1,"","set_mirror_mapping"],[0,2,1,"","update_pose"],[0,3,1,"","view"]],"pose_wrangler.v2.model.base_extension":[[1,0,1,"","PoseWranglerExtension"]],"pose_wrangler.v2.model.base_extension.PoseWranglerExtension":[[1,3,1,"","api"],[1,2,1,"","execute"],[1,2,1,"","on_context_changed"],[1,3,1,"","view"]]},objnames:{"0":["py","class","Python class"],"1":["py","attribute","Python attribute"],"2":["py","method","Python method"],"3":["py","property","Python property"]},objtypes:{"0":"py:class","1":"py:attribute","2":"py:method","3":"py:property"},terms:{"0":0,"1":3,"2":0,"2018":2,"2022":2,"9":3,"9_":3,"case":[0,3],"class":[0,1],"default":[0,2,4],"export":4,"function":4,"import":[0,1,4],"new":[0,1,4,5],"return":[0,1],"true":0,"try":3,A:3,And:[],As:3,For:4,If:[0,3,5],In:[3,4],The:[2,3,4],To:1,_:3,_l_:3,_r_:3,abil:[],action:2,ad:1,add:[0,4],add_blendshap:0,add_driv:0,add_driven_transform:0,addit:5,affect:0,against:3,all:[0,1,4,5],allow:[2,4],almost:5,also:4,an:[0,3,4,5],ani:[1,4],api:[1,2],appear:3,ar:4,associ:[0,1,3],attempt:3,attribut:4,autom:4,automat:[0,1,2,4],avail:[0,4,5],awai:0,backend:4,base:[0,1],base_extens:[0,1],base_mesh:0,been:4,befor:1,belong:[],below:3,berc:2,blender:0,blendshap:[0,2],bool:0,borna:2,bridg:2,button:[4,5],calcul:4,calf:3,calf_l_uerbfsolv:3,call:[1,3],can:[1,4],carbon:[],caus:4,chang:[0,4],charact:3,check:0,chri:2,class_ref:0,clean:5,code:1,complet:[],connect:0,contain:[0,1,3,5],context:[0,1,2],convent:[2,3],convert:[],copi:[],core:[1,4],corral:2,correspond:4,creat:[0,1,2,4],create_blendshap:0,create_pos:0,create_rbf_solv:0,creation:0,current:[0,1,4],current_solv:0,custom:[1,2,3,4],data:0,david:2,debug:4,defin:3,definit:3,delet:[0,4],delete_blendshap:0,delete_pos:0,delete_rbf_solv:0,deriv:0,deseri:[0,2],deserialize_from_fil:0,detect:1,determin:3,develop:[],deviat:2,dict:0,dictionari:[2,3],disabl:[0,4],disconnect:0,displai:4,display_view:1,distribut:2,doe:[3,4],doesn:3,driven:[0,3],driven_nod:0,driver:[0,2,3],dynam:1,each:3,edit:[0,1,4],edit_blendshap:0,edit_solv:0,emb:1,embed:4,enabl:[0,3,4],end:3,entri:0,entrypoint:1,epic:2,error:[3,4],evan:2,event:1,exampl:[1,3,4],examplesolv:0,execut:[1,5],exist:0,expect:3,explanatori:4,express:3,extend:1,extens:[0,2],fail:3,fals:[0,1],featur:1,file:[0,2,4],file_path:0,find:4,finish:0,first:3,follow:[1,3],found:[0,1],from:[0,1,2,4],fulli:2,futur:4,game:2,gener:1,get:[0,1],get_context:0,get_extension_by_typ:0,get_rbf_solver_by_nam:0,get_solver_edit_statu:0,get_ui_context:0,github:5,given:0,go_to_pos:0,goal:4,greet:5,group:3,ha:4,hand:3,happen:3,have:[1,5],help:4,here:4,how:4,ident:5,index:2,influenc:[0,4],info:4,inform:4,inherit:1,initi:2,insensit:0,instal:2,instanc:0,interact:0,interfac:[1,2],introduc:1,isol:0,isolate_blendshap:0,iter:3,joint:3,json:[0,2,4],judd:2,kei:3,kwarg:1,left:[3,5],leg_l:0,legaci:[2,5],like:3,linger:5,list:[0,4],ll:3,load:[0,1,5],log:[3,5],logic:4,loiter:[],look:3,lr:3,made:[],mai:4,main:[1,2],make:[1,4],map:[0,2,4],match:[3,4],matchdict:3,maya:2,mayapi:2,mayauerbfplugin:2,mean:4,menu:2,mesh:0,mesh_nam:0,messag:4,metahuman:[3,4],migrat:2,mirror:[0,2,4],mirror_map:0,mirror_pos:0,mirror_rbf_solv:0,mirrormap:0,mode:[0,4],model:[0,1],modul:2,more:4,most:4,move:0,multipl:[2,4],mute:[0,4],mute_pos:0,name:[0,2,3],need:4,new_context:1,new_pose_nam:0,newli:0,node:[0,3,4,5],none:[0,1],note:[0,4],now:[1,4],object:[0,1],off:0,old:5,on_context_chang:1,one:[0,3,4],onli:4,option:[0,5],order:4,origin:4,other:0,output:[0,3],overridden:1,own:4,p:3,page:[2,4],param:1,paramet:[0,1],parent:0,path:0,place:3,pleas:[1,4],plugin:[2,5],point:0,pose:[0,1,3,5],pose_nam:0,pose_wrangl:[1,2],posewrangl:[1,2,5],posewranglercontext:[0,1],posewranglerextens:[0,1],posewrangleruicontext:0,prefix:3,present:5,press:5,previous:5,progress:4,properti:[0,1],provid:2,pysid:1,python:[2,4],qtwidget:0,quixel:2,qwidget:[0,1],radiu:[0,4],rbf:[0,3,4],rbf_api:0,rbf_solver:0,rbfapi:0,rbfnode:0,recogn:[],reconnect:0,redund:5,ref:0,refactor:4,refer:[0,1,4],regular:3,remov:[0,4],remove_driv:0,remove_driven:0,renam:0,rename_pos:0,replac:[3,5],result:[3,4],rig:2,right:3,s:2,same:3,scene:[0,2,4,5],script:5,search:[0,2],see:3,select:4,self:4,separ:4,serial:[0,2],serialize_to_fil:0,set:[0,1,3],set_mirror_map:0,setup:4,ship:4,should:[0,1],side:3,simantov:2,singl:4,skeleton:[3,4],so:1,solver:[0,1,3],solver_express:3,solver_nam:0,solver_syntax:3,someth:3,specif:0,specifi:[0,3,4],start:1,startup:1,state:0,str:0,straight:0,succe:3,success:5,suffix:3,support:[2,3,4],sure:1,t:3,targeten:4,team:4,theme:4,theodosiu:2,thi:[0,1,3,4,5],thing:3,through:3,time:4,toggl:4,tool:[2,4,5],top:5,transform:[0,3],transform_express:3,transform_syntax:3,type:[0,1],ue4rbfsolvernod:[2,5],ue5:2,ueposeblendernod:0,uerbfapi:[0,1],uerbfsolv:3,uerbfsolvernod:[0,2,5],ui:[0,1,2],ui_context:0,under:1,unexpect:4,unmut:0,up:[1,3,5],updat:0,update_pos:0,upgrad:2,upon:0,us:[3,4],user:4,util:1,v2:[1,2],valu:3,versa:3,version:[0,4,5],via:[0,1,2,4,5],vice:3,view:[0,1],want:3,warn:4,we:3,when:[1,3,5],where:4,which:4,widget:1,window:5,wip:2,wish:1,without:[1,4],work:4,workflow:[2,4],worth:4,wrangler:[0,1,3,4,5],write:4,you:[1,3,4,5],your:[1,4,5],z0:3,za:3},titles:["API","Extensions","Pose Wrangler Documentation","Mirror Mapping","2.0.0 Overview","Upgrading to 2.0.0"],titleterms:{"0":[2,4,5],"1":[2,4],"2":[2,4,5],"3":4,"4":4,"5":4,"6":4,"7":4,api:0,blendshap:4,content:2,contributor:2,document:2,driven:4,driver:4,extens:[1,4],indic:2,log:4,main:0,map:3,menu:4,mirror:3,output:4,overview:[2,4],pose:[2,4],pose_wrangl:0,s:[],set:4,solver:4,tabl:2,titl:3,top:4,transform:4,ui:4,upgrad:5,v2:0,version:2,view:4,welcom:[],wrangler:2}}) \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/docs/site/html/upgrading.html b/Scripts/Animation/epic_pose_wrangler/docs/site/html/upgrading.html new file mode 100644 index 0000000..82226f0 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/docs/site/html/upgrading.html @@ -0,0 +1,117 @@ + + + + + + + Upgrading to 2.0.0 — Pose Wrangler 2.0.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Upgrading to 2.0.0

+

When you load up Pose Wrangler with a scene that contains UE4RBFSolverNode you will be greeted with the new legacy +version of PoseWrangler. This version is almost identical with the version previously available via GitHub, with the +addition of the log window and the upgrade button.

+

If you have the new version of the plugin available you will be presented with an option to upgrade your scene at the +top of the Pose Wrangler window. Pressing this button will execute an upgrade script that will replace all the +UE4RBFSolverNode with UERBFSolverNode and clean up old/redundant nodes left lingering in the scene.

+UI Overview +

If the upgrade is successful you will be presented with the new tool window.

+UI Overview +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/log.py b/Scripts/Animation/epic_pose_wrangler/log.py new file mode 100644 index 0000000..67a7d7b --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/log.py @@ -0,0 +1,7 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +import logging + +# Grab the pose wrangler log +LOG = logging.getLogger("EpicGames.PoseWrangler") +# Set the logging level +LOG.setLevel(logging.DEBUG) diff --git a/Scripts/Animation/epic_pose_wrangler/main.py b/Scripts/Animation/epic_pose_wrangler/main.py new file mode 100644 index 0000000..3c9be6c --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/main.py @@ -0,0 +1,31 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +import os + +from epic_pose_wrangler.log import LOG +from epic_pose_wrangler.model.plugin_manager import PluginManager + + +class PoseWrangler(object): + """ + Main entrypoint for interacting with PoseWrangler. Will handle loading the correct version of the tool based on the + available version of the plugin + """ + + def __init__(self, view=True): + # Get the current version of the tool + self._api = PluginManager.get_pose_wrangler(view=view, parent=self) + + @property + def api(self): + return self._api + + def upgrade(self, file_path=None, delete_file=False): + """ + Restart pose_wrangler with the specified serialized solver data file + :param file_path :type str: path to serialized json data + :param delete_file :type bool: should the file be deleted once the upgrade has completed? + """ + LOG.info("Rebooting PoseWrangler") + self._api = PluginManager.get_pose_wrangler(view=self._api.view, parent=self, file_path=file_path) + if delete_file: + os.remove(file_path) diff --git a/Scripts/Animation/epic_pose_wrangler/model/__init__.py b/Scripts/Animation/epic_pose_wrangler/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Scripts/Animation/epic_pose_wrangler/model/api.py b/Scripts/Animation/epic_pose_wrangler/model/api.py new file mode 100644 index 0000000..b0644e9 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/model/api.py @@ -0,0 +1,13 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +class RBFAPI(object): + """ + Base class for creating RBF API classes + """ + UPGRADE_AVAILABLE = False + VERSION = "0.0.0" + + def __init__(self, view=False, parent=None, file_path=None): + super(RBFAPI, self).__init__() + self._view = view + self._parent = parent + self._file_path = file_path diff --git a/Scripts/Animation/epic_pose_wrangler/model/exceptions.py b/Scripts/Animation/epic_pose_wrangler/model/exceptions.py new file mode 100644 index 0000000..e0efdc8 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/model/exceptions.py @@ -0,0 +1,43 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +from epic_pose_wrangler.log import LOG + + +class PoseWranglerException(Exception): + """ + Base Exception for PoseWrangler related errors + """ + + def __init__(self, message): + super(PoseWranglerException, self).__init__(message) + # Log the message as an error + LOG.error(message) + + +class InvalidPoseWranglerPlugin(PoseWranglerException, RuntimeError): + """ + Exception raised when no valid plugins could be loaded + """ + + +class PoseWranglerSettingsError(PoseWranglerException): + """ + Raised when a setting is invalid + """ + + +class InvalidMirrorMapping(PoseWranglerSettingsError): + """ + Raised when the mirror mapping is incorrect + """ + + +class PoseWranglerIOError(PoseWranglerException): + """ + Raised when issues with serialization/deserialization arise + """ + + +class PoseWranglerFunctionalityNotImplemented(PoseWranglerException): + """ + Raised when pose wrangler functionality hasn't been implemented yet + """ diff --git a/Scripts/Animation/epic_pose_wrangler/model/mirror_mapping.py b/Scripts/Animation/epic_pose_wrangler/model/mirror_mapping.py new file mode 100644 index 0000000..431bbc1 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/model/mirror_mapping.py @@ -0,0 +1,126 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +""" +Example Mapping (MetaHuman) +{ + # Regular expression to validate that the solver follows the correct naming convention for mirroring + "solver_expression": "(?P[a-zA-Z0-9]+)?(?P_[lr]{1}_)(?P[a-zA-Z0-9]+)", + # Regular expression to validate that the joint follows the correct naming convention for mirroring + "transform_expression": "(?P[a-zA-Z0-9]+)?(?P_[lr]{1}_)(?P[a-zA-Z0-9]+)", + "left": { + # Left side syntax for the solver + "solver_syntax": "_l_", + # Left side syntax for the joint + "transform_syntax": "_l_" + }, + "right": { + # Right side syntax for the solver + "solver_syntax": "_r_", + # Right side syntax for the joint + "transform_syntax": "_r_" + } +} +""" +import json +import os + +from epic_pose_wrangler.log import LOG + + +class MirrorMapping(object): + """ + Class for managing mirror settings + """ + LEFT = "left" + RIGHT = "right" + + def __init__(self, file_path=None, source_side="left"): + # Make a list of valid mappings that should exist in the mirror mapping file + self._valid_mappings = [MirrorMapping.LEFT, MirrorMapping.RIGHT] + # If no file path is specified, use the MetaHuman config as the fallback + if file_path is None: + LOG.debug("No mirror mapping specified, using default MetaHuman conventions") + file_path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + 'resources', + 'mirror_mappings', + 'metahuman.json' + ) + self._file_path = file_path + # Load the json mapping data + with open(file_path, 'r') as f: + self._mapping_data = json.loads(f.read()) + + # Set the solver expression from the file + self._solver_expression = self._mapping_data['solver_expression'] + # Set the transform expression from the file + self._transform_expression = self._mapping_data['transform_expression'] + + # Set the source side and create defaults + self._source_side = source_side + self._source_mapping_data = {} + self._source_solver_syntax = "" + self._source_transform_syntax = "" + + self._target_mapping_data = {} + self._target_solver_syntax = "" + self._target_transform_syntax = "" + # Set the source side property to trigger the default values to be updated + self.source_side = source_side + + @property + def file_path(self): + return self._file_path + + @property + def solver_expression(self): + return self._solver_expression + + @property + def transform_expression(self): + return self._transform_expression + + @property + def source_side(self): + return self._source_side + + @source_side.setter + def source_side(self, side): + """ + Sets the source side and updates the source/target values accordingly + :param side: MirrorMapping.LEFT or MirrorMapping.RIGHT + """ + if side not in self._valid_mappings: + raise ValueError("Invalid side specified, options are: {}".format(", ".join(self._valid_mappings))) + self._source_side = side + self._source_mapping_data = self._mapping_data[self._source_side] + self._source_solver_syntax = self._source_mapping_data['solver_syntax'] + self._source_transform_syntax = self._source_mapping_data['transform_syntax'] + + self._target_mapping_data = self._mapping_data[ + MirrorMapping.RIGHT if self._source_side == MirrorMapping.LEFT else MirrorMapping.LEFT] + self._target_solver_syntax = self._target_mapping_data['solver_syntax'] + self._target_transform_syntax = self._target_mapping_data['transform_syntax'] + + @property + def source_solver_syntax(self): + return self._source_solver_syntax + + @property + def source_transform_syntax(self): + return self._source_transform_syntax + + @property + def target_solver_syntax(self): + return self._target_solver_syntax + + @property + def target_transform_syntax(self): + return self._target_transform_syntax + + def swap_sides(self): + """ + Swap the source side to the opposite of the current side. + """ + new_target = MirrorMapping.LEFT if self.source_side == MirrorMapping.RIGHT else MirrorMapping.RIGHT + self.source_side = new_target diff --git a/Scripts/Animation/epic_pose_wrangler/model/plugin_manager.py b/Scripts/Animation/epic_pose_wrangler/model/plugin_manager.py new file mode 100644 index 0000000..542e8c8 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/model/plugin_manager.py @@ -0,0 +1,111 @@ +# Copyright Epic Games, Inc. All Rights Reserved. +from collections import OrderedDict + +from maya import cmds + +from epic_pose_wrangler.log import LOG +from epic_pose_wrangler.model import exceptions + + +class PluginManager: + """ + Class for loading latest available plugin and managing pose_wrangler versions + """ + # The name of the recommended solver + RECOMMENDED_SOLVER = "UERBFSolverNode" + # Empty list to keep track of the loaded solver nodes + LOADED_NODES = [] + # Generate an ordered manifest of known plugin name variants with the newest plugins + __PLUGIN_VERSIONS = OrderedDict( + { + "MayaUERBFPlugin_{}".format(cmds.about(version=True)): "UERBFSolverNode", + "MayaUERBFPlugin{}".format(cmds.about(version=True)): "UERBFSolverNode", + "MayaUERBFPlugin": "UERBFSolverNode", + "MayaUE4RBFPlugin_{}".format(cmds.about(version=True)): "UE4RBFSolverNode", + "MayaUE4RBFPlugin{}".format(cmds.about(version=True)): "UE4RBFSolverNode", + "MayaUE4RBFPlugin": "UE4RBFSolverNode"} + ) + + @staticmethod + def load_plugin(): + """ + Load any valid RBF plugins + :return :type list: node names loaded + """ + + PluginManager.LOADED_NODES = [] + # Iterate through all of the valid plugin versions and attempt to load + for plugin_name, solver_name in PluginManager.__PLUGIN_VERSIONS.items(): + # If the plugin is already loaded, add the solver name to the list of loaded nodes + if cmds.pluginInfo(plugin_name, q=True, loaded=True) and solver_name not in PluginManager.LOADED_NODES: + PluginManager.LOADED_NODES.append(solver_name) + else: + try: + # Attempt to load the plugin + cmds.loadPlugin(plugin_name) + # If the solver name is not already in the list, add it + if solver_name not in PluginManager.LOADED_NODES: + PluginManager.LOADED_NODES.append(solver_name) + # Ignore errors + except RuntimeError as e: + pass + # If we have no loaded nodes no plugin loaded correctly + if not PluginManager.LOADED_NODES: + raise exceptions.InvalidPoseWranglerPlugin("Unable to load valid RBF plugin version.") + + return PluginManager.LOADED_NODES + + @staticmethod + def is_scene_using_recommended_solver(): + """ + Scan the current scene to find which version of the solver is being used + :return :type bool: is the recommended solver being used for all RBF nodes + """ + solvers = [] + # Get a list of the solver names + for solver_node_name in list(PluginManager.__PLUGIN_VERSIONS.values()): + if solver_node_name not in solvers: + solvers.append(solver_node_name) + # Iterate through the solver names + for solver_node_name in solvers: + # Check if any solvers exist in the scene of the specified type and check if the solver name is the + # recommended name. If we have old solvers in the scene, we aren't using the latest version. + if cmds.ls(type=solver_node_name) and solver_node_name != PluginManager.RECOMMENDED_SOLVER: + return False + return True + + @staticmethod + def get_pose_wrangler(view=True, parent=None, file_path=None): + """ + Get the correct version of the pose wrangler tool depending on the available plugins and nodes in the scene + :param view :type bool: Should we be displaying a UI to the user? + :param parent :type main.PoseWrangler: reference to the main entry point for the tool, used for + restarting/upgrading the tool + :param file_path :type str: (optional) path to a json file containing serialized solver data + :return :type object: reference to the currently loaded version of pose wrangler + """ + # Load the RBF plugin + loaded_nodes = PluginManager.load_plugin() + # If the recommended solver is not loaded, fall back to the original pose wrangler implementation + if PluginManager.RECOMMENDED_SOLVER not in loaded_nodes: + LOG.warning("You are currently using an outdated plugin. Certain functionality may be limited.") + from epic_pose_wrangler.v1 import main + return main.UE4RBFAPI(view=view, parent=parent) + # Bool to keep track of importing the newest api version + import_failed = False + # Check if the scene uses the latest solver + if PluginManager.is_scene_using_recommended_solver(): + # Try and import the latest tool version + try: + from epic_pose_wrangler.v2 import main + return main.UERBFAPI(view=view, parent=parent, file_path=file_path) + except ImportError as e: + LOG.error("Unable to import API v2, falling back to API v1 - {exception}".format(exception=e)) + import_failed = True + # Fall back to API v1 + from epic_pose_wrangler.v1 import main + # If the recommended solver is available but finds old nodes in the scene and imports correctly, provide + # the option to upgrade to the latest version + if not import_failed: + main.UE4RBFAPI.UPGRADE_AVAILABLE = True + return main.UE4RBFAPI(view=view, parent=parent) diff --git a/Scripts/Animation/epic_pose_wrangler/model/settings.py b/Scripts/Animation/epic_pose_wrangler/model/settings.py new file mode 100644 index 0000000..2c5a7a9 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/model/settings.py @@ -0,0 +1,48 @@ +import os + +from PySide2 import QtCore + +from epic_pose_wrangler.log import LOG +from epic_pose_wrangler.model import exceptions + + +class SettingsManager(object): + """ + Settings Manager for reading/writing to PoseWrangler settings ini file + """ + QSETTINGS = None + + def __init__(self): + # Initialize the QSettings + QtCore.QSettings.setPath(QtCore.QSettings.IniFormat, QtCore.QSettings.UserScope, os.environ['LOCALAPPDATA']) + # Store the QSettings + self.__class__.QSETTINGS = QtCore.QSettings( + QtCore.QSettings.IniFormat, + QtCore.QSettings.UserScope, + "Epic Games", + "PoseWrangler" + ) + self.__class__.QSETTINGS.setFallbacksEnabled(False) + LOG.debug("Successfully initialized SettingsManager") + + @classmethod + def get_setting(cls, name): + """ + Get the setting with the specified name + :param name :type str: setting name + :return :type str or None: setting value + """ + # If the settings haven't been initialized, raise exception + if cls.QSETTINGS is None: + raise exceptions.PoseWranglerSettingsError("Unable to load settings, " + "{cls} must be initialized first".format(cls=cls)) + return cls.QSETTINGS.value(name, None) + + @classmethod + def set_setting(cls, name, value): + """ + Add/Overwrite the setting with the specified name and value + :param name :type str: setting name + :param value :type any: setting value + """ + cls.QSETTINGS.setValue(name, value) diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFPlugin2018.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFPlugin2018.mll new file mode 100644 index 0000000..68f25d3 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFPlugin2018.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFUI.py b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFUI.py new file mode 100644 index 0000000..59c3122 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUE4RBFUI.py @@ -0,0 +1,201 @@ +import sys +import inspect + +import maya +import maya.OpenMaya as OpenMaya +import maya.OpenMayaMPx as OpenMayaMPx +import maya.cmds as cmds +from pymel.core.windows import Callback, CallbackWithArgs + +StreamTypesPerSubjectType = { + "Prop": ["Root Only", "Full Hierarchy"], + "Character": ["Root Only", "Full Hierarchy"], + "Camera": ["Root Only", "Full Hierarchy", "Camera"], + "Light": ["Root Only", "Full Hierarchy", "Light"], + } + +def OnRemoveSubject(SubjectPath): + cmds.LiveLinkRemoveSubject(SubjectPath) + RefreshSubjects() + +def CreateSubjectTable(): + cmds.rowColumnLayout("SubjectLayout", numberOfColumns=5, columnWidth=[(1, 20), (2,80), (3, 100), (4, 180), (5, 120)], columnOffset=[(1, 'right', 5), (2, 'right', 10), (4, 'left', 10)], parent="SubjectWrapperLayout") + cmds.text(label="") + cmds.text(label="Subject Type", font="boldLabelFont", align="left") + cmds.text(label="Subject Name", font="boldLabelFont", align="left") + cmds.text(label="DAG Path", font="boldLabelFont", align="left") + cmds.text(label="Stream Type", font="boldLabelFont", align="left") + cmds.rowColumnLayout("SubjectLayout", edit=True, rowOffset=(1, "bottom", 10)) + +#Populate subjects list from c++ +def PopulateSubjects(): + SubjectNames = cmds.LiveLinkSubjectNames() + SubjectPaths = cmds.LiveLinkSubjectPaths() + SubjectTypes = cmds.LiveLinkSubjectTypes() + SubjectRoles = cmds.LiveLinkSubjectRoles() + if SubjectPaths is not None: + RowCounter = 0 + for (SubjectName, SubjectPath, SubjectType, SubjectRole) in zip(SubjectNames, SubjectPaths, SubjectTypes, SubjectRoles): + cmds.button(label="-", height=21, command=Callback(OnRemoveSubject, SubjectPath), parent="SubjectLayout") + cmds.text(label=SubjectType, height=21, align="left", parent="SubjectLayout") + cmds.textField(text=SubjectName, height=21, changeCommand=CallbackWithArgs(cmds.LiveLinkChangeSubjectName, SubjectPath), parent="SubjectLayout") + cmds.text(label=SubjectPath, align="left", height=21, parent="SubjectLayout") + + LayoutName = "ColumnLayoutRow_" + str(RowCounter) # adding a trailing index makes the name unique which is required by the api + + cmds.columnLayout(LayoutName, parent="SubjectLayout") + cmds.optionMenu("SubjectTypeMenu", parent=LayoutName, height=21, changeCommand=CallbackWithArgs(cmds.LiveLinkChangeSubjectStreamType, SubjectPath)) + + for StreamType in StreamTypesPerSubjectType[SubjectType]: + cmds.menuItem(label=StreamType) + + StreamTypeIndex = StreamTypesPerSubjectType[SubjectType].index(SubjectRole) + 1 # menu items are 1-indexed + cmds.optionMenu("SubjectTypeMenu", edit=True, select=StreamTypeIndex) + + RowCounter += 1 + +def ClearSubjects(): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + cmds.deleteUI("SubjectLayout") + +#Refresh subjects list +def RefreshSubjects(): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + cmds.deleteUI("SubjectLayout") + CreateSubjectTable() + PopulateSubjects() + +#Connection UI Colours +ConnectionActiveColour = [0.71, 0.9, 0.1] +ConnectionInactiveColour = [1.0, 0.4, 0.4] +ConnectionColourMap = { + True : ConnectionActiveColour, + False: ConnectionInactiveColour +} + +#Base class for command (common creator method + allows for automatic register/unregister) +class LiveLinkCommand(OpenMayaMPx.MPxCommand): + def __init__(self): + OpenMayaMPx.MPxCommand.__init__(self) + + @classmethod + def Creator(Cls): + return OpenMayaMPx.asMPxPtr( Cls() ) + +# Is supplied object a live link command +def IsLiveLinkCommand(InCls): + return inspect.isclass(InCls) and issubclass(InCls, LiveLinkCommand) and InCls != LiveLinkCommand + +# Given a list of strings of names return all the live link commands listed +def GetLiveLinkCommandsFromModule(ModuleItems): + EvalItems = (eval(Item) for Item in ModuleItems) + return [Command for Command in EvalItems if IsLiveLinkCommand(Command) ] + +# Command to create the Live Link UI +class MayaLiveLinkUI(LiveLinkCommand): + WindowName = "MayaLiveLinkUI" + Title = "Maya Live Link UI" + WindowSize = (500, 300) + + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + if (cmds.window(self.WindowName , exists=True)): + cmds.deleteUI(self.WindowName) + window = cmds.window( self.WindowName, title=self.Title, widthHeight=(self.WindowSize[0], self.WindowSize[1]) ) + + #Get current connection status + ConnectionText, ConnectedState = cmds.LiveLinkConnectionStatus() + + cmds.columnLayout( "mainColumn", adjustableColumn=True ) + cmds.rowLayout("HeaderRow", numberOfColumns=3, adjustableColumn=1, parent = "mainColumn") + cmds.text(label="Unreal Engine Live Link", align="left") + cmds.text(label="Connection:") + cmds.text("ConnectionStatusUI", label=ConnectionText, align="center", backgroundColor=ConnectionColourMap[ConnectedState], width=150) + + cmds.separator(h=20, style="none", parent="mainColumn") + cmds.columnLayout("SubjectWrapperLayout", parent="mainColumn") # just used as a container that will survive refreshing, so the following controls stay in their correct place + + CreateSubjectTable() + PopulateSubjects() + + cmds.separator(h=20, style="none", parent="mainColumn") + cmds.button( label='Add Selection', parent = "mainColumn", command=self.AddSelection) + + cmds.showWindow( self.WindowName ) + + def AddSelection(self, *args): + cmds.LiveLinkAddSelection() + RefreshSubjects() + +# Command to Refresh the subject UI +class MayaLiveLinkRefreshUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + RefreshSubjects() + +class MayaLiveLinkClearUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + def doIt(self, argList): + ClearSubjects() + CreateSubjectTable() + +# Command to Refresh the connection UI +class MayaLiveLinkRefreshConnectionUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + #Get current connection status + ConnectionText, ConnectedState = cmds.LiveLinkConnectionStatus() + cmds.text("ConnectionStatusUI", edit=True, label=ConnectionText, backgroundColor=ConnectionColourMap[ConnectedState]) + +class MayaLiveLinkGetActiveCamera(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + self.clearResult() + try: + c = cmds.getPanel(wf=1) + cam = cmds.modelPanel(c, q=True, camera=True) + except: + pass + else: + self.setResult(cam) + +#Grab commands declared +Commands = GetLiveLinkCommandsFromModule(dir()) + +#Initialize the script plug-in +def initializePlugin(mobject): + mplugin = OpenMayaMPx.MFnPlugin(mobject) + + print("LiveLink:" + for Command in Commands: + try: + print("\tRegistering Command '%s'"%Command.__name__ + mplugin.registerCommand( Command.__name__, Command.Creator ) + except: + sys.stderr.write( "Failed to register command: %s\n" % Command.__name__ ) + raise + +# Uninitialize the script plug-in +def uninitializePlugin(mobject): + mplugin = OpenMayaMPx.MFnPlugin(mobject) + + for Command in Commands: + try: + mplugin.deregisterCommand( Command.__name__ ) + except: + sys.stderr.write( "Failed to unregister command: %s\n" % Command.__name__ ) \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUERBFPlugin.mll new file mode 100644 index 0000000..41e3295 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so new file mode 100644 index 0000000..c317e16 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8 b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8 new file mode 100644 index 0000000..4bdbe32 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8 differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8.0.8 b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8.0.8 new file mode 100644 index 0000000..4bdbe32 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2018/libembeddedRL4.so.8.0.8 differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFPlugin2019.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFPlugin2019.mll new file mode 100644 index 0000000..87afdbf Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFPlugin2019.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFUI.py b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFUI.py new file mode 100644 index 0000000..59c3122 --- /dev/null +++ b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUE4RBFUI.py @@ -0,0 +1,201 @@ +import sys +import inspect + +import maya +import maya.OpenMaya as OpenMaya +import maya.OpenMayaMPx as OpenMayaMPx +import maya.cmds as cmds +from pymel.core.windows import Callback, CallbackWithArgs + +StreamTypesPerSubjectType = { + "Prop": ["Root Only", "Full Hierarchy"], + "Character": ["Root Only", "Full Hierarchy"], + "Camera": ["Root Only", "Full Hierarchy", "Camera"], + "Light": ["Root Only", "Full Hierarchy", "Light"], + } + +def OnRemoveSubject(SubjectPath): + cmds.LiveLinkRemoveSubject(SubjectPath) + RefreshSubjects() + +def CreateSubjectTable(): + cmds.rowColumnLayout("SubjectLayout", numberOfColumns=5, columnWidth=[(1, 20), (2,80), (3, 100), (4, 180), (5, 120)], columnOffset=[(1, 'right', 5), (2, 'right', 10), (4, 'left', 10)], parent="SubjectWrapperLayout") + cmds.text(label="") + cmds.text(label="Subject Type", font="boldLabelFont", align="left") + cmds.text(label="Subject Name", font="boldLabelFont", align="left") + cmds.text(label="DAG Path", font="boldLabelFont", align="left") + cmds.text(label="Stream Type", font="boldLabelFont", align="left") + cmds.rowColumnLayout("SubjectLayout", edit=True, rowOffset=(1, "bottom", 10)) + +#Populate subjects list from c++ +def PopulateSubjects(): + SubjectNames = cmds.LiveLinkSubjectNames() + SubjectPaths = cmds.LiveLinkSubjectPaths() + SubjectTypes = cmds.LiveLinkSubjectTypes() + SubjectRoles = cmds.LiveLinkSubjectRoles() + if SubjectPaths is not None: + RowCounter = 0 + for (SubjectName, SubjectPath, SubjectType, SubjectRole) in zip(SubjectNames, SubjectPaths, SubjectTypes, SubjectRoles): + cmds.button(label="-", height=21, command=Callback(OnRemoveSubject, SubjectPath), parent="SubjectLayout") + cmds.text(label=SubjectType, height=21, align="left", parent="SubjectLayout") + cmds.textField(text=SubjectName, height=21, changeCommand=CallbackWithArgs(cmds.LiveLinkChangeSubjectName, SubjectPath), parent="SubjectLayout") + cmds.text(label=SubjectPath, align="left", height=21, parent="SubjectLayout") + + LayoutName = "ColumnLayoutRow_" + str(RowCounter) # adding a trailing index makes the name unique which is required by the api + + cmds.columnLayout(LayoutName, parent="SubjectLayout") + cmds.optionMenu("SubjectTypeMenu", parent=LayoutName, height=21, changeCommand=CallbackWithArgs(cmds.LiveLinkChangeSubjectStreamType, SubjectPath)) + + for StreamType in StreamTypesPerSubjectType[SubjectType]: + cmds.menuItem(label=StreamType) + + StreamTypeIndex = StreamTypesPerSubjectType[SubjectType].index(SubjectRole) + 1 # menu items are 1-indexed + cmds.optionMenu("SubjectTypeMenu", edit=True, select=StreamTypeIndex) + + RowCounter += 1 + +def ClearSubjects(): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + cmds.deleteUI("SubjectLayout") + +#Refresh subjects list +def RefreshSubjects(): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + cmds.deleteUI("SubjectLayout") + CreateSubjectTable() + PopulateSubjects() + +#Connection UI Colours +ConnectionActiveColour = [0.71, 0.9, 0.1] +ConnectionInactiveColour = [1.0, 0.4, 0.4] +ConnectionColourMap = { + True : ConnectionActiveColour, + False: ConnectionInactiveColour +} + +#Base class for command (common creator method + allows for automatic register/unregister) +class LiveLinkCommand(OpenMayaMPx.MPxCommand): + def __init__(self): + OpenMayaMPx.MPxCommand.__init__(self) + + @classmethod + def Creator(Cls): + return OpenMayaMPx.asMPxPtr( Cls() ) + +# Is supplied object a live link command +def IsLiveLinkCommand(InCls): + return inspect.isclass(InCls) and issubclass(InCls, LiveLinkCommand) and InCls != LiveLinkCommand + +# Given a list of strings of names return all the live link commands listed +def GetLiveLinkCommandsFromModule(ModuleItems): + EvalItems = (eval(Item) for Item in ModuleItems) + return [Command for Command in EvalItems if IsLiveLinkCommand(Command) ] + +# Command to create the Live Link UI +class MayaLiveLinkUI(LiveLinkCommand): + WindowName = "MayaLiveLinkUI" + Title = "Maya Live Link UI" + WindowSize = (500, 300) + + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + if (cmds.window(self.WindowName , exists=True)): + cmds.deleteUI(self.WindowName) + window = cmds.window( self.WindowName, title=self.Title, widthHeight=(self.WindowSize[0], self.WindowSize[1]) ) + + #Get current connection status + ConnectionText, ConnectedState = cmds.LiveLinkConnectionStatus() + + cmds.columnLayout( "mainColumn", adjustableColumn=True ) + cmds.rowLayout("HeaderRow", numberOfColumns=3, adjustableColumn=1, parent = "mainColumn") + cmds.text(label="Unreal Engine Live Link", align="left") + cmds.text(label="Connection:") + cmds.text("ConnectionStatusUI", label=ConnectionText, align="center", backgroundColor=ConnectionColourMap[ConnectedState], width=150) + + cmds.separator(h=20, style="none", parent="mainColumn") + cmds.columnLayout("SubjectWrapperLayout", parent="mainColumn") # just used as a container that will survive refreshing, so the following controls stay in their correct place + + CreateSubjectTable() + PopulateSubjects() + + cmds.separator(h=20, style="none", parent="mainColumn") + cmds.button( label='Add Selection', parent = "mainColumn", command=self.AddSelection) + + cmds.showWindow( self.WindowName ) + + def AddSelection(self, *args): + cmds.LiveLinkAddSelection() + RefreshSubjects() + +# Command to Refresh the subject UI +class MayaLiveLinkRefreshUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + RefreshSubjects() + +class MayaLiveLinkClearUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + def doIt(self, argList): + ClearSubjects() + CreateSubjectTable() + +# Command to Refresh the connection UI +class MayaLiveLinkRefreshConnectionUI(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + if (cmds.window(MayaLiveLinkUI.WindowName , exists=True)): + #Get current connection status + ConnectionText, ConnectedState = cmds.LiveLinkConnectionStatus() + cmds.text("ConnectionStatusUI", edit=True, label=ConnectionText, backgroundColor=ConnectionColourMap[ConnectedState]) + +class MayaLiveLinkGetActiveCamera(LiveLinkCommand): + def __init__(self): + LiveLinkCommand.__init__(self) + + # Invoked when the command is run. + def doIt(self,argList): + self.clearResult() + try: + c = cmds.getPanel(wf=1) + cam = cmds.modelPanel(c, q=True, camera=True) + except: + pass + else: + self.setResult(cam) + +#Grab commands declared +Commands = GetLiveLinkCommandsFromModule(dir()) + +#Initialize the script plug-in +def initializePlugin(mobject): + mplugin = OpenMayaMPx.MFnPlugin(mobject) + + print("LiveLink:" + for Command in Commands: + try: + print("\tRegistering Command '%s'"%Command.__name__ + mplugin.registerCommand( Command.__name__, Command.Creator ) + except: + sys.stderr.write( "Failed to register command: %s\n" % Command.__name__ ) + raise + +# Uninitialize the script plug-in +def uninitializePlugin(mobject): + mplugin = OpenMayaMPx.MFnPlugin(mobject) + + for Command in Commands: + try: + mplugin.deregisterCommand( Command.__name__ ) + except: + sys.stderr.write( "Failed to unregister command: %s\n" % Command.__name__ ) \ No newline at end of file diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUERBFPlugin.mll b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUERBFPlugin.mll new file mode 100644 index 0000000..7fd5a41 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/MayaUERBFPlugin.mll differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so new file mode 100644 index 0000000..f3b3a62 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8 b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8 new file mode 100644 index 0000000..dc18633 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8 differ diff --git a/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8.0.8 b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8.0.8 new file mode 100644 index 0000000..dc18633 Binary files /dev/null and b/Scripts/Animation/epic_pose_wrangler/plugins/Linux/2019/libembeddedRL4.so.8.0.8 differ