├── plug-ins ├── virtuCameraMaya │ ├── virtuCameraIcon_32pt.png │ ├── __init__.py │ ├── virtuCameraMayaConfig.py │ └── virtuCameraMaya.py └── virtuCameraMayaPlugin.py ├── README.md ├── .gitignore └── LICENSE /plug-ins/virtuCameraMaya/virtuCameraIcon_32pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theweirdbyte/VirtuCameraMaya/HEAD/plug-ins/virtuCameraMaya/virtuCameraIcon_32pt.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirtuCameraMaya 2 | ### Maya® plug-in to use with VirtuCamera iOS App for realtime camera motion capture. 3 | 4 | Follow the installation instructions from https://virtucamera.com/installation-in-maya/ 5 | -------------------------------------------------------------------------------- /plug-ins/virtuCameraMaya/__init__.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraMaya 2 | # Copyright (c) 2025 The Weird Byte. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | from .virtuCameraMaya import VirtuCameraMaya 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | 108 | # General 109 | .DS_Store 110 | .AppleDouble 111 | .LSOverride 112 | 113 | # Icon must end with two \r 114 | Icon 115 | 116 | # Thumbnails 117 | ._* 118 | 119 | # Files that might appear in the root of a volume 120 | .DocumentRevisions-V100 121 | .fseventsd 122 | .Spotlight-V100 123 | .TemporaryItems 124 | .Trashes 125 | .VolumeIcon.icns 126 | .com.apple.timemachine.donotpresent 127 | 128 | # Directories potentially created on remote AFP share 129 | .AppleDB 130 | .AppleDesktop 131 | Network Trash Folder 132 | Temporary Items 133 | .apdisk 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | VirtuCameraMaya: 2 | 3 | Copyright (c) 2025 The Weird Byte. 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 BY THE COPYRIGHT HOLDERS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 24 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | ---- 28 | VirtuCameraMaya includes the PyVirtuCamera software module, licensed under the terms described below. 29 | 30 | PyVirtuCamera: 31 | 32 | Copyright (c) 2025 The Weird Byte. 33 | 34 | Redistribution and use of the software module "PyVirtuCamera" (the “Software”) 35 | is permitted, free of charge, provided that the following conditions are met: 36 | * Redistributions must reproduce the above copyright notice, 37 | this list of conditions and the following disclaimer in the 38 | documentation and/or other materials provided with the distribution. 39 | * You may not decompile, disassemble, reverse engineer or modify 40 | any portion of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 43 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 44 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 45 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 46 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 47 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 48 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 49 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 50 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 51 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 52 | 53 | 54 | - - - - - Acknowledgements - - - - - 55 | 56 | PyVirtuCamera is distributed along with the following software libraries: 57 | 58 | * FFmpeg (https://www.ffmpeg.org) 59 | License: LGPL 60 | 61 | * PyAV (https://github.com/PyAV-Org/PyAV) 62 | License: BSD 63 | 64 | * PyPNG (https://github.com/drj11/pypng) 65 | License: MIT 66 | 67 | * python-qrcode (https://github.com/lincolnloop/python-qrcode) 68 | License: MIT 69 | 70 | * ifaddr (https://github.com/pydron/ifaddr) 71 | License: MIT 72 | 73 | * python-mss (https://github.com/BoboTiG/python-mss) 74 | License: MIT 75 | 76 | * six (https://github.com/benjaminp/six) 77 | License: MIT 78 | -------------------------------------------------------------------------------- /plug-ins/virtuCameraMayaPlugin.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraMaya 2 | # Copyright (c) 2025 The Weird Byte. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import sys, os 26 | import maya.api.OpenMaya as OpenMaya 27 | import maya.cmds as cmds 28 | import virtuCameraMaya 29 | 30 | ########################################################## 31 | # Plug-in 32 | ########################################################## 33 | class VirtuCameraMayaPlugin( OpenMaya.MPxCommand ): 34 | kPluginCmdName = 'virtuCamera' 35 | 36 | def __init__(self): 37 | ''' Constructor. ''' 38 | OpenMaya.MPxCommand.__init__(self) 39 | 40 | @staticmethod 41 | def cmdCreator(): 42 | ''' Create an instance of our command. ''' 43 | return VirtuCameraMayaPlugin() 44 | 45 | def doIt(self, args): 46 | ''' Command execution. ''' 47 | import virtuCameraMaya 48 | #virtuCameraMaya = reload(virtuCameraMaya.virtuCameraMaya) 49 | virtuCameraMaya.VirtuCameraMaya() 50 | 51 | ########################################################## 52 | # Plug-in initialization. 53 | ########################################################## 54 | def maya_useNewAPI(): 55 | """ 56 | The presence of this function tells Maya that the plugin produces, and 57 | expects to be passed, objects created using the Maya Python API 2.0. 58 | """ 59 | pass 60 | 61 | def configPlugin(): 62 | pluginPath = os.path.dirname(os.path.abspath(virtuCameraMaya.__file__)) 63 | shelfName = 'VirtuCamera' 64 | buttonName = 'VirtuCamera' 65 | iconName = 'virtuCameraIcon_32pt.png' 66 | iconPath = os.path.join(pluginPath, iconName) 67 | buttonExists = False 68 | if not cmds.shelfLayout(shelfName, ex=True): 69 | cmds.shelfLayout(shelfName, p='ShelfLayout') 70 | else: 71 | buttons = cmds.shelfLayout(shelfName, q=True, ca=True) 72 | if buttons: 73 | for button in buttons: 74 | if cmds.shelfButton(button, q=True, l=True) == buttonName: 75 | buttonExists = True 76 | break 77 | if not buttonExists: 78 | cmds.shelfButton(w=35, h=35, i=iconPath, l=buttonName, c='from maya import cmds; cmds.'+VirtuCameraMayaPlugin.kPluginCmdName+'()', p=shelfName) 79 | 80 | def initializePlugin( mobject ): 81 | ''' Initialize the plug-in when Maya loads it. ''' 82 | configPlugin() 83 | mplugin = OpenMaya.MFnPlugin( mobject ) 84 | try: 85 | mplugin.registerCommand( VirtuCameraMayaPlugin.kPluginCmdName, VirtuCameraMayaPlugin.cmdCreator ) 86 | except: 87 | sys.stderr.write( 'Failed to register command: ' + VirtuCameraMayaPlugin.kPluginCmdName ) 88 | 89 | def uninitializePlugin( mobject ): 90 | ''' Uninitialize the plug-in when Maya un-loads it. ''' 91 | mplugin = OpenMaya.MFnPlugin( mobject ) 92 | try: 93 | mplugin.deregisterCommand( VirtuCameraMayaPlugin.kPluginCmdName ) 94 | except: 95 | sys.stderr.write( 'Failed to unregister command: ' + VirtuCameraMayaPlugin.kPluginCmdName ) 96 | -------------------------------------------------------------------------------- /plug-ins/virtuCameraMaya/virtuCameraMayaConfig.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraMaya 2 | # Copyright (c) 2025 The Weird Byte. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import maya.cmds as cmds 26 | import xml.etree.ElementTree as et 27 | import os 28 | 29 | class VirtuCameraMayaConfig(object): 30 | # Constants 31 | _WINDOW_SIZE = (800,600) 32 | _SAMPLE_PY = '# SAMPLE CODE\n# Duplicates the camera selected in VirtuCamera\n# Tip: %SELCAM% will be replaced by the path to the camera transform\n\nimport maya.cmds as cmds\n\ncam_transform = %SELCAM%\ncmds.duplicate(cam_transform)' 33 | _SAMPLE_MEL = '// SAMPLE CODE\n// Duplicates the camera selected in VirtuCamera\n// Tip: %SELCAM% will be replaced by the path to the camera transform\n\n$cam_transform = %SELCAM%;\nduplicate $cam_transform;\n' 34 | LANG_PY = 1 35 | LANG_MEL = 2 36 | CAPMODE_BUFFER_POINTER = 'Viewport Buffer' 37 | CAPMODE_SCREENSHOT = 'Screenshot' 38 | DEFAULT_CAPMODE = CAPMODE_BUFFER_POINTER # Default capture mode 39 | DEFAULT_SRVPORT = 23354 # TCP port used by default 40 | 41 | 42 | def __init__(self, config_file_path, saved_callback): 43 | self.config_file_path = config_file_path 44 | self._saved_callback = saved_callback 45 | self.read_config() 46 | 47 | def _init_vars(self): 48 | self.server_port = self.DEFAULT_SRVPORT 49 | self.capture_mode = self.DEFAULT_CAPMODE 50 | self.script_count = 0 51 | self.script_codes = [] 52 | self.script_langs = [] 53 | self.script_labels = [] 54 | 55 | def read_config(self): 56 | self._init_vars() 57 | 58 | if not os.path.isfile(self.config_file_path): 59 | return 60 | tree = et.ElementTree() 61 | try: 62 | with open(self.config_file_path,'r') as file: 63 | tree.parse(file) 64 | except: 65 | print('VirtuCamera: Error reading config file') 66 | return 67 | 68 | config = tree.getroot() 69 | for item in config: 70 | # Read script config 71 | if item.tag == 'scripts': 72 | self.script_count = len(item) 73 | if self.script_count > 0: 74 | for script in item: 75 | self.script_labels.append(script.get('label')) 76 | self.script_langs.append(int(script.get('lang'))) 77 | script_code = script.text 78 | if script_code == None: 79 | script_code = '' 80 | self.script_codes.append(script_code) 81 | # Read general config 82 | elif item.tag == 'general': 83 | srvport = item.get('srvport') 84 | if srvport != None: 85 | self.server_port = int(srvport) 86 | capmode = item.get('capmode') 87 | if capmode != None: 88 | self.capture_mode = capmode 89 | 90 | def _init_vars_ui(self): 91 | self._self_updating_label = False 92 | self._last_script_num = 0 93 | self._last_server_port = self.server_port 94 | self._last_capture_mode = self.capture_mode 95 | self._last_label = '' 96 | self._script_code_cache = [] 97 | self._script_lang_cache = [] 98 | self._script_label_cache = [] 99 | 100 | def show_window(self): 101 | window = 'VirtuCameraMayaConfigWindow' 102 | if cmds.window(window, q=True, exists=True): 103 | cmds.showWindow(window) # Show existing UI if exists 104 | else: 105 | self._init_vars_ui() 106 | self._start_ui() 107 | self.update_window() 108 | 109 | def update_window(self): 110 | self._load_ui() 111 | self._update_ui_from_cache() 112 | self._update_enable_state_ui() 113 | 114 | def _load_ui(self): 115 | self._script_code_cache = list(self.script_codes) 116 | self._script_lang_cache = list(self.script_langs) 117 | self._script_label_cache = list(self.script_labels) 118 | self._script_count_ui = self.script_count 119 | if self._script_count_ui > 0: 120 | self._set_script_num_ui(1, 1, self._script_count_ui) 121 | self._set_port_num_ui(self.server_port) 122 | self._set_cap_mode_ui(self.capture_mode) 123 | 124 | def _save_config(self): 125 | config = et.Element('virtuCameraConfig') 126 | scripts = et.SubElement(config, 'scripts') 127 | for i in range(len(self._script_code_cache)): 128 | script = et.SubElement(scripts, 'script'+str(i)) 129 | script.set('label', self._script_label_cache[i]) 130 | script.set('lang', str(self._script_lang_cache[i])) 131 | script.text = self._script_code_cache[i] 132 | general = et.SubElement(config, 'general') 133 | general.set('srvport', str(self._get_port_num_ui())) 134 | general.set('capmode', self._get_cap_mode_ui()) 135 | tree = et.ElementTree(config) 136 | try: 137 | with open(self.config_file_path,'wb') as savefile: 138 | tree.write(savefile) 139 | return True 140 | except: 141 | cmds.confirmDialog(title="Error", message='Error saving config file, make sure you have write permission in the plug-in folder', button='Ok', defaultButton='Ok') 142 | return False 143 | 144 | def _cache_pos(self): 145 | return self._get_script_num_ui() - 1 146 | 147 | def _add_cache_entry(self): 148 | cache_pos = self._cache_pos() 149 | self._script_code_cache.insert(cache_pos, self._SAMPLE_PY) 150 | self._script_label_cache.insert(cache_pos, '') 151 | self._script_lang_cache.insert(cache_pos, self.LANG_PY) 152 | 153 | def _remove_cache_entry(self): 154 | cache_pos = self._cache_pos() 155 | self._script_code_cache.pop(cache_pos) 156 | self._script_label_cache.pop(cache_pos) 157 | self._script_lang_cache.pop(cache_pos) 158 | 159 | def _update_cache(self): 160 | cache_pos = self._last_script_num - 1 161 | if cache_pos < 0: 162 | return 163 | code = self._get_script_code_ui() 164 | label = self._get_label_ui() 165 | lang = self._get_lang_ui() 166 | self._script_code_cache[cache_pos] = code 167 | self._script_label_cache[cache_pos] = label 168 | self._script_lang_cache[cache_pos] = lang 169 | 170 | def _update_ui_from_cache(self): 171 | cache_pos = self._cache_pos() 172 | code = '' 173 | label = '' 174 | lang = self.LANG_PY 175 | if cache_pos >= 0: 176 | code = self._script_code_cache[cache_pos] 177 | label = self._script_label_cache[cache_pos] 178 | lang = self._script_lang_cache[cache_pos] 179 | self._set_script_code_ui(code) 180 | self._set_label_ui(label) 181 | self._set_lang_ui(lang) 182 | 183 | def _enable_control(self, control): 184 | cmds.control(control, edit=True, enable=True) 185 | 186 | def _disable_control(self, control): 187 | cmds.control(control, edit=True, enable=False) 188 | 189 | def _zero_script_count_ui(self): 190 | self._disable_control(self._script_num_ui) 191 | self._enable_control(self._new_bt_ui) 192 | self._disable_control(self._rem_bt_ut) 193 | self._disable_control(self._label_ui) 194 | self._disable_control(self._lang_ui) 195 | self._disable_control(self._code_lb_ui) 196 | self._disable_control(self._ui_sfield) 197 | 198 | def _one_script_count_ui(self): 199 | self._disable_control(self._script_num_ui) 200 | self._enable_control(self._new_bt_ui) 201 | self._enable_control(self._rem_bt_ut) 202 | self._enable_control(self._label_ui) 203 | self._enable_control(self._lang_ui) 204 | self._enable_control(self._code_lb_ui) 205 | self._enable_control(self._ui_sfield) 206 | 207 | def _mid_script_count_ui(self): 208 | self._enable_control(self._script_num_ui) 209 | self._enable_control(self._new_bt_ui) 210 | self._enable_control(self._rem_bt_ut) 211 | self._enable_control(self._label_ui) 212 | self._enable_control(self._lang_ui) 213 | self._enable_control(self._code_lb_ui) 214 | self._enable_control(self._ui_sfield) 215 | 216 | def _full_script_count_ui(self): 217 | self._enable_control(self._script_num_ui) 218 | self._disable_control(self._new_bt_ui) 219 | self._enable_control(self._rem_bt_ut) 220 | self._enable_control(self._label_ui) 221 | self._enable_control(self._lang_ui) 222 | self._enable_control(self._code_lb_ui) 223 | self._enable_control(self._ui_sfield) 224 | 225 | def _update_enable_state_ui(self): 226 | if self._script_count_ui == 0: 227 | self._zero_script_count_ui() 228 | elif self._script_count_ui == 1: 229 | self._one_script_count_ui() 230 | elif self._script_count_ui == 99: 231 | self._full_script_count_ui() 232 | else: 233 | self._mid_script_count_ui() 234 | 235 | def _get_port_num_ui(self): 236 | return cmds.intField(self._port_num_ui, query=True, value=True) 237 | 238 | def _set_port_num_ui(self, val): 239 | cmds.intField(self._port_num_ui, edit=True, value=val) 240 | 241 | def _get_cap_mode_ui(self): 242 | return cmds.optionMenuGrp(self._cap_mode_ui, query=True, value=True) 243 | 244 | def _set_cap_mode_ui(self, val): 245 | cmds.optionMenuGrp(self._cap_mode_ui, edit=True, value=val) 246 | 247 | def _get_script_code_ui(self): 248 | return cmds.scrollField(self._ui_sfield, query=True, text=True) 249 | 250 | def _set_script_code_ui(self, text): 251 | cmds.scrollField(self._ui_sfield, edit=True, text=text) 252 | 253 | def _get_script_num_ui(self): 254 | return cmds.intSliderGrp(self._script_num_ui, query=True, value=True) 255 | 256 | def _set_script_num_ui(self, num, min_num, max_num): 257 | cmds.intSliderGrp(self._script_num_ui, edit=True, minValue=min_num, maxValue=max_num, fieldMinValue=min_num, fieldMaxValue=max_num, value=num) 258 | self._last_script_num = num 259 | 260 | def _get_lang_ui(self): 261 | return cmds.radioButtonGrp(self._lang_ui, query=True, select=True) 262 | 263 | def _set_lang_ui(self, lang): 264 | cmds.radioButtonGrp(self._lang_ui, edit=True, select=lang) 265 | 266 | def _get_label_ui(self): 267 | return cmds.textFieldGrp(self._label_ui, query=True, text=True) 268 | 269 | def _set_label_ui(self, text): 270 | self._self_updating_label = True 271 | cmds.textFieldGrp(self._label_ui, edit=True, text=text) 272 | self._self_updating_label = False 273 | self._last_label = text 274 | 275 | def _increase_script_count(self): 276 | self._script_count_ui += 1 277 | script_num = self._get_script_num_ui() 278 | script_num += 1 279 | self._set_script_num_ui(script_num, 1, self._script_count_ui) 280 | 281 | def _decrease_script_count(self): 282 | self._script_count_ui -= 1 283 | if self._script_count_ui == 0: 284 | min_val = 0 285 | else: 286 | min_val = 1 287 | script_num = self._get_script_num_ui() 288 | script_num -= 1 289 | self._set_script_num_ui(script_num, min_val, self._script_count_ui) 290 | 291 | def _new_script_ui(self, caller=None): 292 | self._update_cache() 293 | self._increase_script_count() 294 | self._add_cache_entry() 295 | self._update_ui_from_cache() 296 | self._update_enable_state_ui() 297 | self._enable_control(self._ui_save) 298 | 299 | 300 | def _remove_script_ui(self, caller=None): 301 | self._remove_cache_entry() 302 | self._decrease_script_count() 303 | self._update_ui_from_cache() 304 | self._update_enable_state_ui() 305 | self._enable_control(self._ui_save) 306 | 307 | def _label_changed_ui(self, caller=None): 308 | if self._self_updating_label: 309 | return 310 | label = self._get_label_ui() 311 | prev_label = self._last_label 312 | if len(label) > 9: 313 | label = label[:9] 314 | label = label.replace('%', '') 315 | self._set_label_ui(label) 316 | if label != prev_label: 317 | self._enable_control(self._ui_save) 318 | 319 | def _script_number_changed_ui(self, caller=None): 320 | self._update_cache() 321 | self._last_script_num = self._get_script_num_ui() 322 | self._update_ui_from_cache() 323 | 324 | def _languaje_changed_ui(self, caller=None): 325 | lang = self._get_lang_ui() 326 | script_code = self._get_script_code_ui() 327 | 328 | if lang == self.LANG_PY and script_code == self._SAMPLE_MEL: 329 | self._set_script_code_ui(self._SAMPLE_PY) 330 | elif lang == self.LANG_MEL and script_code == self._SAMPLE_PY: 331 | self._set_script_code_ui(self._SAMPLE_MEL) 332 | self._enable_control(self._ui_save) 333 | 334 | def _code_changed_ui(self, caller=None): 335 | self._enable_control(self._ui_save) 336 | 337 | def _port_num_changed_ui(self, caller=None): 338 | self._last_server_port = self._get_port_num_ui() 339 | self._enable_control(self._ui_save) 340 | 341 | def _cap_mode_changed_ui(self, caller=None): 342 | self._last_capture_mode = self._get_cap_mode_ui() 343 | self._enable_control(self._ui_save) 344 | 345 | def _save_ui(self, caller=None): 346 | self._disable_control(self._ui_save) 347 | self._update_cache() 348 | if self._save_config(): 349 | self.read_config() # read config back to update class state 350 | self._saved_callback() 351 | else: 352 | self._enable_control(self._ui_save) 353 | 354 | def _revert_ui(self): 355 | self._set_script_num_ui(self._last_script_num, 1, self._script_count_ui) 356 | self._set_port_num_ui(self._last_server_port) 357 | self._set_cap_mode_ui(self._last_capture_mode) 358 | self._enable_control(self._ui_save) 359 | 360 | def _close_ui(self, caller=None): 361 | not_saved = cmds.control(self._ui_save, query=True, enable=True) 362 | if not_saved: 363 | result = cmds.confirmDialog( title='Warning', message='Config not saved', button=['Save', "Don't Save", 'Cancel'], defaultButton='Save', cancelButton='Cancel', dismissString='Cancel' ) 364 | if result == 'Save': 365 | self._save_ui() 366 | elif result == 'Cancel': 367 | self._update_cache() 368 | cmds.evalDeferred(self._start_ui) 369 | cmds.evalDeferred(self._revert_ui) 370 | cmds.evalDeferred(self._update_ui_from_cache) 371 | cmds.evalDeferred(self._update_enable_state_ui) 372 | 373 | def _start_ui(self): 374 | # Remove size preference to force the window calculate its size 375 | windowName = 'VirtuCameraMayaConfigWindow' 376 | if cmds.windowPref(windowName, exists=True): 377 | cmds.windowPref(windowName, remove=True) 378 | self._ui_window = cmds.window(windowName, width=self._WINDOW_SIZE[0], height=self._WINDOW_SIZE[1], menuBarVisible=False, titleBar=True, visible=True, sizeable=True, closeCommand=self._close_ui, title='VirtuCamera Configuration') 379 | form_lay = cmds.formLayout(width=550, height=400) 380 | col_lay = cmds.columnLayout(adjustableColumn=True, columnAttach=('both', 0), width=465) 381 | 382 | cmds.text(label='General Settings', align='left') 383 | cmds.separator(height=15, style='none') 384 | cmds.rowLayout(numberOfColumns=3, columnWidth3=(59, 80, 45), columnAttach=[(1, 'both', 0), (2, 'both', 0), (3, 'both', 0)]) 385 | cmds.separator(style='none') 386 | cmds.text(label='Server Port', align='right') 387 | self._port_num_ui = cmds.intField(width=45, value=self.DEFAULT_SRVPORT, minValue=0, maxValue=65535, changeCommand=self._port_num_changed_ui) 388 | cmds.setParent('..') 389 | cmds.separator(height=5, style='none') 390 | self._cap_mode_ui = cmds.optionMenuGrp(label='Capture Mode', changeCommand=self._cap_mode_changed_ui) 391 | cmds.menuItem(label=self.CAPMODE_BUFFER_POINTER) 392 | cmds.menuItem(label=self.CAPMODE_SCREENSHOT) 393 | cmds.text(label=" 'Viewport Buffer' is faster. Use 'Screenshot' if you are having\n problems visualizing the viewport on the App.", align='left') 394 | cmds.separator(height=25, style='none') 395 | cmds.separator() 396 | cmds.separator(height=15, style='none') 397 | cmds.text(label='Custom Scripts', align='left') 398 | cmds.separator(height=15, style='none') 399 | cmds.rowLayout(numberOfColumns=3, columnWidth3=(600, 80, 80), adjustableColumn=1, columnAttach=[(1, 'both', 0), (2, 'both', 0), (3, 'both', 0)]) 400 | self._script_num_ui = cmds.intSliderGrp(field=True, label='Script Number', minValue=0, maxValue=1, fieldMinValue=0, fieldMaxValue=1, value=0, dragCommand=self._script_number_changed_ui, enable=False) 401 | self._new_bt_ui = cmds.button(label='New', command=self._new_script_ui) 402 | self._rem_bt_ut = cmds.button(label='Remove', command=self._remove_script_ui, enable=False) 403 | cmds.setParent('..') 404 | self._label_ui = cmds.textFieldGrp(label='Button Label', textChangedCommand=self._label_changed_ui, enable=False) 405 | self._lang_ui = cmds.radioButtonGrp(label='Language', labelArray2=['Python', 'MEL'], numberOfRadioButtons=2, select=self.LANG_PY, changeCommand=self._languaje_changed_ui, enable=False) 406 | self._code_lb_ui = cmds.text(label='Script Code', align='left', enable=False) 407 | cmds.setParent('..') 408 | self._ui_sfield = cmds.scrollField(editable=True, wordWrap=False, keyPressCommand=self._code_changed_ui, enable=False) 409 | col_lay2 = cmds.columnLayout(adjustableColumn=True, columnAttach=('both', 0), width=465) 410 | cmds.rowLayout(numberOfColumns=2, columnWidth2=(680, 80), adjustableColumn=1, columnAttach=[(1, 'both', 0), (2, 'both', 0)]) 411 | cmds.separator(style='none') 412 | self._ui_save = cmds.button(label='Save', width=80, command=self._save_ui, enable=False) 413 | cmds.setParent('..') 414 | cmds.setParent('..') 415 | cmds.formLayout(form_lay, edit=True, attachForm=[(col_lay, 'top', 20), (col_lay, 'left', 20), (col_lay, 'right', 20), (self._ui_sfield, 'left', 20), (self._ui_sfield, 'right', 20), (col_lay2, 'bottom', 20), (col_lay2, 'left', 20), (col_lay2, 'right', 20)], attachControl=[(self._ui_sfield, 'top', 0, col_lay), (self._ui_sfield, 'bottom', 15, col_lay2)]) 416 | cmds.setParent('..') 417 | cmds.showWindow(self._ui_window) 418 | -------------------------------------------------------------------------------- /plug-ins/virtuCameraMaya/virtuCameraMaya.py: -------------------------------------------------------------------------------- 1 | # VirtuCameraMaya 2 | # Copyright (c) 2025 The Weird Byte. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, 18 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 19 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | # Python modules 26 | import os, sys, traceback 27 | 28 | # Maya modules 29 | import maya.api.OpenMaya as api 30 | import maya.api.OpenMayaUI as apiUI 31 | from maya import OpenMayaUI as v1apiUI 32 | import maya.cmds as cmds 33 | import maya.mel as mel 34 | import maya.utils as utils 35 | 36 | # QT 37 | from PySide2 import QtWidgets 38 | from shiboken2 import wrapInstance 39 | 40 | # Config handling lib 41 | from . import virtuCameraMayaConfig 42 | 43 | # PyVirtuCamera core lib 44 | from .virtucamera import VCBase, VCServer 45 | 46 | class VirtuCameraMaya(VCBase): 47 | # Constants 48 | PLUGIN_VERSION = (2,0,2) 49 | WINDOW_WIDTH = 160 50 | WINDOW_HEIGHT = 180 51 | CONFIG_FILE = 'configuration.xml' # Configuration file name 52 | VC_TO_ZUP_MAT = api.MMatrix((1, 0, 0, 0, 0, 0, 1, 0, 0,-1, 0, 0, 0, 0, 0, 1)) 53 | ZUP_TO_VC_MAT = api.MMatrix((1, 0, 0, 0, 0, 0,-1, 0, 0, 1, 0, 0, 0, 0, 0, 1)) 54 | CAMERA_KEY_ATTRS = ('.focalLength','.tx','.ty','.tz','.rx','.ry','.rz') 55 | MAYA_FPS_PRESETS = { 56 | 'game': 15.0, 57 | 'film': 24.0, 58 | 'pal': 25.0, 59 | 'ntsc': 30.0, 60 | 'show': 48.0, 61 | 'palf': 50.0, 62 | 'ntscf': 60.0, 63 | 'millisec': 1000.0, 64 | 'sec': 1.0, 65 | 'min': 1.0/60.0, 66 | 'hour': 1.0/3600.0 67 | } 68 | 69 | # Show existing UI if exists 70 | def __new__(cls, *args, **kwargs): 71 | window = 'VirtuCameraMayaWindow' 72 | if cmds.window(window, q=True, exists=True): 73 | cmds.showWindow(window) 74 | return None 75 | else: 76 | return super(VirtuCameraMaya, cls).__new__(cls, *args, **kwargs) 77 | 78 | def __init__(self): 79 | mayapy = None 80 | if os.name == 'nt': 81 | # On windows, get path to python executable, 82 | # needed for the viewport video feed to work 83 | mayapy = os.path.join(os.path.dirname(sys.executable), "mayapy.exe") 84 | 85 | # Init virtucamera.VCServer 86 | self.vcserver = VCServer( 87 | platform = "Maya", 88 | plugin_version = self.PLUGIN_VERSION, 89 | event_mode = VCServer.EVENTMODE_PUSH, 90 | vcbase = self, 91 | main_thread_func = utils.executeInMainThreadWithResult, 92 | python_executable = mayapy 93 | ) 94 | 95 | # Load plug-in configuration 96 | config_file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), self.CONFIG_FILE) 97 | self.config = virtuCameraMayaConfig.VirtuCameraMayaConfig(config_file_path, self.vcserver.update_script_labels) 98 | 99 | self.is_closing_ui = False 100 | self.hidden_views = [] 101 | self.start_ui() 102 | 103 | 104 | # -- User Interface ------------------------------------ 105 | 106 | def update_ui_layout(self): 107 | cmds.formLayout(self.ui_layout, edit=True, 108 | attachForm=[(self.ui_bt_serve, 'left', 5), (self.ui_bt_serve, 'top', 5), (self.ui_tx_help, 'left', 5), (self.ui_tx_help, 'right', 5), (self.ui_bt_conf, 'top', 5), (self.ui_bt_conf, 'right', 5), (self.ui_view, 'left', 5), (self.ui_view, 'right', 5), (self.ui_view, 'bottom', 5)], 109 | attachControl=[(self.ui_bt_serve, 'right', 5, self.ui_bt_conf), (self.ui_tx_help, 'top', 5, self.ui_bt_serve), (self.ui_view, 'top', 5, self.ui_tx_help)]) 110 | 111 | def start_serving(self, caller=None): 112 | if self.vcserver.start_serving(self.config.server_port): 113 | self.serving_ui() 114 | 115 | def stop_serving(self, caller=None): 116 | self.vcserver.stop_serving() 117 | 118 | def close_ui(self, caller=None): 119 | self.is_closing_ui = True 120 | self.vcserver.stop_serving() 121 | 122 | def open_config_window(self, caller=None): 123 | self.config.show_window() 124 | 125 | def start_ui(self): 126 | # Remove size preference to force the window calculate its size 127 | windowName = 'VirtuCameraMayaWindow' 128 | if cmds.windowPref(windowName, exists=True): 129 | cmds.windowPref(windowName, remove=True) 130 | 131 | self.ui_window = cmds.window(windowName, 132 | width=self.WINDOW_WIDTH, 133 | height=self.WINDOW_HEIGHT, 134 | menuBarVisible=False, 135 | titleBar=True, 136 | visible=True, 137 | sizeable=True, 138 | closeCommand=self.close_ui, 139 | title='VC %s.%s.%s'%self.PLUGIN_VERSION) 140 | self.ui_layout = cmds.formLayout(numberOfDivisions=100) 141 | self.ui_bt_serve = cmds.button(label='Start Serving', 142 | command=self.start_serving) 143 | self.ui_tx_help = cmds.text(label='', width=self.WINDOW_WIDTH-10) 144 | self.ui_bt_conf = cmds.button(label='Config', 145 | command=self.open_config_window) 146 | self.ui_view = cmds.text(label='Click on Start Serving and\nconnect through the App', 147 | backgroundColor=[0.2,0.2,0.2], 148 | width=self.WINDOW_WIDTH-10, 149 | height=self.WINDOW_HEIGHT-10) 150 | self.update_ui_layout() 151 | 152 | def serving_ui(self): 153 | if self.is_closing_ui: 154 | return 155 | cmds.button(self.ui_bt_serve, e=True, enable=True, label='Stop Serving', command=self.stop_serving) 156 | cmds.text(self.ui_tx_help, e=True, label='Scan QR Code with VirtuCamera App') 157 | qw = v1apiUI.MQtUtil.findControl(self.ui_view) 158 | widget = wrapInstance(int(qw), QtWidgets.QWidget) 159 | widget.setPixmap(self.vcserver.get_qr_image_qt(3)) 160 | 161 | def stopped_ui(self): 162 | if self.is_closing_ui: 163 | return 164 | cmds.button(self.ui_bt_serve, e=True, enable=True, label='Start Serving', command=self.start_serving) 165 | cmds.text(self.ui_tx_help, e=True, label='') 166 | cmds.text(self.ui_view, e=True, label='Click on Start Serving and\nconnect through the App') 167 | 168 | def connected_ui(self): 169 | cmds.text(self.ui_tx_help, e=True, label='') 170 | cmds.text(self.ui_view, e=True, label='Client App connected') 171 | 172 | def start_capturing_ui(self, hide_inactive_views): 173 | cmds.text(self.ui_view, edit=True, label='Client App connected\nStreaming viewport') 174 | if hide_inactive_views: 175 | # Workaround to make M3dView.readColorBuffer() work when multiple viewports are visible 176 | self.hide_inactive_views() 177 | 178 | def stop_capturing_ui(self): 179 | if self.is_closing_ui: 180 | return 181 | self.unhide_views() 182 | cmds.text(self.ui_view, edit=True, label='Client App connected') 183 | 184 | def hide_inactive_views(self): 185 | model_panels = cmds.getPanel(type="modelPanel") 186 | for pan in model_panels: 187 | if not cmds.modelEditor(pan, q=True, activeView=True): 188 | view_control = cmds.modelPanel(pan, q=True, control=True) 189 | if view_control: 190 | cmds.control(view_control, edit=True, manage=False) 191 | self.hidden_views.append(view_control) 192 | 193 | def unhide_views(self): 194 | for view in self.hidden_views: 195 | if cmds.control(view, q=True, exists=True): 196 | cmds.control(view, edit=True, manage=True) 197 | self.hidden_views = [] 198 | 199 | def get_active_view(self): 200 | model_panels = cmds.getPanel(type="modelPanel") 201 | for pan in model_panels: 202 | if cmds.modelEditor(pan, q=True, activeView=True): 203 | return pan 204 | 205 | 206 | # -- Utility Functions ------------------------------------ 207 | 208 | # Rotate matrix up axis from VirtuCamera (Y+) to Maya 209 | def vc_to_maya_up_axis(self, tr_matrix): 210 | if self.is_z_up: 211 | mat = api.MMatrix(tr_matrix) 212 | mat *= self.VC_TO_ZUP_MAT 213 | return tuple(mat) 214 | return tr_matrix 215 | 216 | # Rotate matrix up axis from Maya to VirtuCamera (Y+) 217 | def maya_to_vc_up_axis(self, tr_matrix): 218 | if self.is_z_up: 219 | mat = api.MMatrix(tr_matrix) 220 | mat *= self.ZUP_TO_VC_MAT 221 | return tuple(mat) 222 | return tr_matrix 223 | 224 | 225 | # SCENE STATE RELATED METHODS: 226 | # --------------------------- 227 | 228 | def get_playback_state(self, vcserver): 229 | """ Must Return the playback state of the scene as a tuple or list 230 | in the following order: (current_frame, range_start, range_end) 231 | * current_frame (float) - The current frame number. 232 | * range_start (float) - Animation range start frame number. 233 | * range_end (float) - Animation range end frame number. 234 | 235 | Parameters 236 | ---------- 237 | vcserver : virtucamera.VCServer object 238 | Instance of virtucamera.VCServer calling this method. 239 | 240 | Returns 241 | ------- 242 | tuple or list of 3 floats 243 | playback state as (current_frame, range_start, range_end) 244 | """ 245 | 246 | range_start = cmds.playbackOptions(q=True, min=True) 247 | range_end = cmds.playbackOptions(q=True, max=True) 248 | current_frame = cmds.currentTime(q=True) 249 | return (current_frame, range_start, range_end) 250 | 251 | 252 | def get_playback_fps(self, vcserver): 253 | """ Must return a float value with the scene playback rate 254 | in Frames Per Second. 255 | 256 | Parameters 257 | ---------- 258 | vcserver : virtucamera.VCServer object 259 | Instance of virtucamera.VCServer calling this method. 260 | 261 | Returns 262 | ------- 263 | float 264 | scene playback rate in FPS. 265 | """ 266 | 267 | maya_fps = cmds.currentUnit(query=True, time=True) 268 | if maya_fps[-3:] == 'fps': 269 | play_fps = float(maya_fps[:-3]) 270 | elif maya_fps[-2:] == 'df': 271 | play_fps = float(maya_fps[:-2]) 272 | else: 273 | play_fps = self.MAYA_FPS_PRESETS[maya_fps] 274 | return play_fps 275 | 276 | 277 | def set_frame(self, vcserver, frame): 278 | """ Must set the current frame number on the scene 279 | 280 | Parameters 281 | ---------- 282 | vcserver : virtucamera.VCServer object 283 | Instance of virtucamera.VCServer calling this method. 284 | frame : float 285 | The current frame number. 286 | """ 287 | 288 | # if maya is playing, stop it 289 | if cmds.play(q=True, state=True): 290 | cmds.play(state=False) 291 | cmds.currentTime(frame, update=True) 292 | 293 | 294 | def set_playback_range(self, vcserver, start, end): 295 | """ Must set the animation frame range on the scene 296 | 297 | Parameters 298 | ---------- 299 | vcserver : virtucamera.VCServer object 300 | Instance of virtucamera.VCServer calling this method. 301 | start : float 302 | Animation range start frame number. 303 | end : float 304 | Animation range end frame number. 305 | """ 306 | 307 | cmds.playbackOptions(min=start, max=end) 308 | 309 | 310 | def start_playback(self, vcserver, forward): 311 | """ This method must start the playback of animation in the scene. 312 | Not used at the moment, but must be implemented just in case 313 | the app starts using it in the future. At the moment 314 | VCBase.set_frame() is called instead. 315 | 316 | Parameters 317 | ---------- 318 | vcserver : virtucamera.VCServer object 319 | Instance of virtucamera.VCServer calling this method. 320 | forward : bool 321 | if True, play the animation forward, if False, play it backwards. 322 | """ 323 | 324 | cmds.play(forward=forward) 325 | 326 | 327 | def stop_playback(self, vcserver): 328 | """ This method must stop the playback of animation in the scene. 329 | Not used at the moment, but must be implemented just in case 330 | the app starts using it in the future. 331 | 332 | Parameters 333 | ---------- 334 | vcserver : virtucamera.VCServer object 335 | Instance of virtucamera.VCServer calling this method. 336 | """ 337 | 338 | cmds.play(state=False) 339 | 340 | 341 | # CAMERA RELATED METHODS: 342 | # ----------------------- 343 | 344 | 345 | def get_scene_cameras(self, vcserver): 346 | """ Must Return a list or tuple with the names of all the scene cameras. 347 | 348 | Parameters 349 | ---------- 350 | vcserver : virtucamera.VCServer object 351 | Instance of virtucamera.VCServer calling this method. 352 | 353 | Returns 354 | ------- 355 | tuple or list 356 | names of all the scene cameras. 357 | """ 358 | 359 | cameras = cmds.listCameras(perspective=True) 360 | # replace shapes with transforms (maya returns shapes when other objects are parented under a camera) 361 | cam_shapes = cmds.ls(cameras, shapes=True) 362 | cameras = list(set(cmds.ls(cameras, type="transform") + cmds.ls(cmds.listRelatives(cam_shapes, parent=True, fullPath=True), type="transform"))) 363 | cameras.sort() 364 | return cameras 365 | 366 | 367 | def get_camera_exists(self, vcserver, camera_name): 368 | """ Must Return True if the specified camera exists in the scene, 369 | False otherwise. 370 | 371 | Parameters 372 | ---------- 373 | vcserver : virtucamera.VCServer object 374 | Instance of virtucamera.VCServer calling this method. 375 | camera_name : str 376 | Name of the camera to check for. 377 | 378 | Returns 379 | ------- 380 | bool 381 | 'True' if the camera 'camera_name' exists, 'False' otherwise. 382 | """ 383 | 384 | return cmds.objExists(camera_name) 385 | 386 | 387 | def get_camera_has_keys(self, vcserver, camera_name): 388 | """ Must Return whether the specified camera has animation keyframes 389 | in the transform or flocal length parameters, as a tuple or list, 390 | in the following order: (transform_has_keys, focal_length_has_keys) 391 | * transform_has_keys (bool) - True if the transform has keyframes. 392 | * focal_length_has_keys (bool) - True if the flen has keyframes. 393 | 394 | Parameters 395 | ---------- 396 | vcserver : virtucamera.VCServer object 397 | Instance of virtucamera.VCServer calling this method. 398 | camera_name : str 399 | Name of the camera to check for. 400 | 401 | Returns 402 | ------- 403 | tuple or list of 2 bool 404 | whether the camera 'camera_name' has keys or not as 405 | (transform_has_keys, focal_length_has_keys) 406 | """ 407 | 408 | transform_has_keys = False 409 | focal_length_has_keys = False 410 | for attr in self.CAMERA_KEY_ATTRS: 411 | if cmds.connectionInfo(camera_name+attr, isDestination=True): 412 | if attr == '.focalLength': 413 | focal_length_has_keys = True 414 | else: 415 | transform_has_keys = True 416 | break 417 | return (transform_has_keys, focal_length_has_keys) 418 | 419 | 420 | def get_camera_focal_length(self, vcserver, camera_name): 421 | """ Must Return the focal length value of the specified camera. 422 | 423 | Parameters 424 | ---------- 425 | vcserver : virtucamera.VCServer object 426 | Instance of virtucamera.VCServer calling this method. 427 | camera_name : str 428 | Name of the camera to get the data from. 429 | 430 | Returns 431 | ------- 432 | float 433 | focal length value of the camera 'camera_name'. 434 | """ 435 | 436 | focal_len = cmds.getAttr(camera_name+'.focalLength') 437 | return focal_len 438 | 439 | 440 | def get_camera_transform(self, vcserver, camera_name): 441 | """ Must return a tuple or list of 16 floats with the 4x4 442 | transform matrix of the specified camera. 443 | 444 | * The up axis must be Y+ 445 | * The order must be: 446 | (rxx, rxy, rxz, 0, 447 | ryx, ryy, ryz, 0, 448 | rzx, rzy, rzz, 0, 449 | tx, ty, tz, 1) 450 | Being 'r' rotation and 't' translation, 451 | 452 | Is your responsability to rotate or transpose the matrix if needed, 453 | most 3D softwares offer fast APIs to do so. 454 | 455 | Parameters 456 | ---------- 457 | vcserver : virtucamera.VCServer object 458 | Instance of virtucamera.VCServer calling this method. 459 | camera_name : str 460 | Name of the camera to get the data from. 461 | 462 | Returns 463 | ------- 464 | tuple or list of 16 float 465 | 4x4 transform matrix as 466 | (rxx, rxy, rxz, 0, ryx, ryy, ryz, 0, rzx, rzy, rzz, 0 , tx, ty, tz, 1) 467 | """ 468 | 469 | tr_matrix = cmds.xform(camera_name, q=True, matrix=True) 470 | return self.maya_to_vc_up_axis(tr_matrix) 471 | 472 | 473 | def set_camera_focal_length(self, vcserver, camera_name, focal_length): 474 | """ Must set the focal length of the specified camera. 475 | 476 | Parameters 477 | ---------- 478 | vcserver : virtucamera.VCServer object 479 | Instance of virtucamera.VCServer calling this method. 480 | camera_name : str 481 | Name of the camera to set the focal length to. 482 | focal_length : float 483 | focal length value to be set on the camera 'camera_name' 484 | """ 485 | 486 | cmds.setAttr(camera_name+'.focalLength', focal_length) 487 | 488 | 489 | def set_camera_transform(self, vcserver, camera_name, transform_matrix): 490 | """ Must set the transform of the specified camera. 491 | The transform matrix is provided as a tuple of 16 floats 492 | with a 4x4 transform matrix. 493 | 494 | * The up axis is Y+ 495 | * The order is: 496 | (rxx, rxy, rxz, 0, 497 | ryx, ryy, ryz, 0, 498 | rzx, rzy, rzz, 0, 499 | tx, ty, tz, 1) 500 | Being 'r' rotation and 't' translation, 501 | 502 | Is your responsability to rotate or transpose the matrix if needed, 503 | most 3D softwares offer fast APIs to do so. 504 | 505 | Parameters 506 | ---------- 507 | vcserver : virtucamera.VCServer object 508 | Instance of virtucamera.VCServer calling this method. 509 | camera_name : str 510 | Name of the camera to set the transform to. 511 | transform_matrix : tuple of 16 floats 512 | transformation matrix to be set on the camera 'camera_name' 513 | """ 514 | 515 | cmds.xform(camera_name, matrix = self.vc_to_maya_up_axis(transform_matrix)) 516 | 517 | 518 | def set_camera_flen_keys(self, vcserver, camera_name, keyframes, focal_length_values): 519 | """ Must set keyframes on the focal length of the specified camera. 520 | The frame numbers are provided as a tuple of floats and 521 | the focal length values are provided as a tuple of floats 522 | with a focal length value for every keyframe. 523 | 524 | The first element of the 'keyframes' tuple corresponds to the first 525 | element of the 'focal_length_values' tuple, the second to the second, 526 | and so on. 527 | 528 | Parameters 529 | ---------- 530 | vcserver : virtucamera.VCServer object 531 | Instance of virtucamera.VCServer calling this method. 532 | camera_name : str 533 | Name of the camera to set the keyframes to. 534 | keyframes : tuple of floats 535 | Frame numbers to create the keyframes on. 536 | focal_length_values : tuple of floats 537 | focal length values to be set as keyframes on the camera 'camera_name' 538 | """ 539 | 540 | for keyframe, focal_length in zip(keyframes, focal_length_values): 541 | self.set_camera_focal_length(vcserver, camera_name, focal_length) 542 | cmds.setKeyframe(camera_name, attribute='focalLength', t=keyframe) 543 | 544 | 545 | def set_camera_transform_keys(self, vcserver, camera_name, keyframes, transform_matrix_values): 546 | """ Must set keyframes on the transform of the specified camera. 547 | The frame numbers are provided as a tuple of floats and 548 | the transform matrixes are provided as a tuple of tuples of 16 floats 549 | with 4x4 transform matrixes, with a matrix for every keyframe. 550 | 551 | The first element of the 'keyframes' tuple corresponds to the first 552 | element of the 'transform_matrix_values' tuple, the second to the second, 553 | and so on. 554 | 555 | * The up axis is Y+ 556 | * The order is: 557 | (rxx, rxy, rxz, 0, 558 | ryx, ryy, ryz, 0, 559 | rzx, rzy, rzz, 0, 560 | tx, ty, tz, 1) 561 | Being 'r' rotation and 't' translation, 562 | 563 | Is your responsability to rotate or transpose the matrixes if needed, 564 | most 3D softwares offer fast APIs to do so. 565 | 566 | Parameters 567 | ---------- 568 | vcserver : virtucamera.VCServer object 569 | Instance of virtucamera.VCServer calling this method. 570 | camera_name : str 571 | Name of the camera to set the keyframes to. 572 | keyframes : tuple of floats 573 | Frame numbers to create the keyframes on. 574 | transform_matrix_values : tuple of tuples of 16 floats 575 | transformation matrixes to be set as keyframes on the camera 'camera_name' 576 | """ 577 | 578 | for keyframe, matrix in zip(keyframes, transform_matrix_values): 579 | self.set_camera_transform(vcserver, camera_name, matrix) 580 | cmds.setKeyframe(camera_name, attribute=['t','r'], t=keyframe) 581 | anim_curves = cmds.listConnections((camera_name+'.rotateX', camera_name+'.rotateY', camera_name+'.rotateZ'), type='animCurve', skipConversionNodes=True) 582 | cmds.filterCurve(anim_curves) 583 | 584 | 585 | def remove_camera_keys(self, vcserver, camera_name): 586 | """ This method must remove all transform 587 | and focal length keyframes in the specified camera. 588 | 589 | Parameters 590 | ---------- 591 | vcserver : virtucamera.VCServer object 592 | Instance of virtucamera.VCServer calling this method. 593 | camera_name : str 594 | Name of the camera to remove the keyframes from. 595 | """ 596 | 597 | for attr in self.CAMERA_KEY_ATTRS: 598 | attr_path = camera_name + attr 599 | if cmds.connectionInfo(attr_path, isDestination=True): 600 | source_attr = cmds.connectionInfo(attr_path, sourceFromDestination=True) 601 | source = source_attr.split('.')[0] 602 | cmds.delete(source) 603 | 604 | 605 | def create_new_camera(self, vcserver): 606 | """ This method must create a new camera in the scene 607 | and return its name. 608 | 609 | Parameters 610 | ---------- 611 | vcserver : virtucamera.VCServer object 612 | Instance of virtucamera.VCServer calling this method. 613 | 614 | Returns 615 | ------- 616 | str 617 | Newly created camera name. 618 | """ 619 | 620 | new_cam = cmds.camera()[0] 621 | if cmds.objExists(vcserver.current_camera): 622 | for attr in self.CAMERA_KEY_ATTRS: 623 | old_val = cmds.getAttr(vcserver.current_camera+attr) 624 | cmds.setAttr(new_cam+attr, old_val) 625 | return new_cam 626 | 627 | 628 | # VIEWPORT CAPTURE RELATED METHODS: 629 | # --------------------------------- 630 | 631 | 632 | def capture_will_start(self, vcserver): 633 | """ This method is called whenever a client app requests a video 634 | feed from the viewport. Usefull to init a pixel buffer 635 | or other objects you may need to capture the viewport 636 | 637 | IMPORTANT! Calling vcserver.set_capture_resolution() and 638 | vcserver.set_capture_mode() here is a must. Please check 639 | the documentation for those methods. 640 | 641 | You can also call vcserver.set_vertical_flip() here optionally, 642 | if you need to flip your pixel buffer. Disabled by default. 643 | 644 | Parameters 645 | ---------- 646 | vcserver : virtucamera.VCServer object 647 | Instance of virtucamera.VCServer calling this method. 648 | """ 649 | 650 | view = apiUI.M3dView.active3dView() 651 | width = view.portWidth() 652 | height = view.portHeight() 653 | self.img = api.MImage() 654 | vcserver.set_capture_resolution(width, height) 655 | vcserver.set_vertical_flip(True) 656 | if self.config.capture_mode == self.config.CAPMODE_SCREENSHOT: 657 | vcserver.set_capture_mode(vcserver.CAPMODE_SCREENSHOT) 658 | self.start_capturing_ui(hide_inactive_views=False) 659 | else: 660 | vcserver.set_capture_mode(vcserver.CAPMODE_BUFFER_POINTER, vcserver.CAPFORMAT_UBYTE_BGRA) 661 | self.start_capturing_ui(hide_inactive_views=True) 662 | 663 | 664 | def capture_did_end(self, vcserver): 665 | """ Optional, this method is called whenever a client app 666 | stops the viewport video feed. Usefull to destroy a pixel buffer 667 | or other objects you may have created to capture the viewport. 668 | 669 | Parameters 670 | ---------- 671 | vcserver : virtucamera.VCServer object 672 | Instance of virtucamera.VCServer calling this method. 673 | """ 674 | 675 | if vcserver.is_connected: 676 | self.stop_capturing_ui() 677 | 678 | def get_capture_coords(self, vcserver, camera_name): 679 | """ If vcserver.capture_mode == vcserver.CAPMODE_SCREENSHOT, it must 680 | return a tuple or list with the left-top coordinates (x,y) 681 | of the screen region to be captured, being 'x' the horizontal axis 682 | and 'y' the vertical axis. If you don't use CAPMODE_SCREENSHOT, 683 | you don't need to overload this method. 684 | 685 | If the screen region has changed in size from the previous call to 686 | this method, and therefore the capture resolution is different, 687 | vcserver.set_capture_resolution() must be called here before returning. 688 | You can use vcserver.capture_width and vcserver.capture_height 689 | to check the previous resolution. 690 | 691 | The name of the camera selected in the app is provided, 692 | as can be usefull to set-up the viewport render in some cases. 693 | 694 | Parameters 695 | ---------- 696 | vcserver : virtucamera.VCServer object 697 | Instance of virtucamera.VCServer calling this method. 698 | camera_name : str 699 | Name of the camera that is currently selected in the App. 700 | 701 | Returns 702 | ------- 703 | tuple or list of 2 float 704 | left-top screen coordinates of the capture region as (x,y). 705 | """ 706 | 707 | view = apiUI.M3dView.active3dView() 708 | width = view.portWidth() 709 | height = view.portHeight() 710 | coords = view.getScreenPosition() 711 | if width != vcserver.capture_width or height != vcserver.capture_height: 712 | vcserver.set_capture_resolution(width, height) 713 | return coords 714 | 715 | 716 | def get_capture_pointer(self, vcserver, camera_name): 717 | """ If vcserver.capture_mode == vcserver.CAPMODE_BUFFER_POINTER, 718 | it must return an int representing a memory address to the first 719 | element of a contiguous buffer containing raw pixels of the 720 | viewport image. The buffer must be kept allocated untill the next 721 | call to this function, is your responsability to do so. 722 | If you don't use CAPMODE_BUFFER_POINTER 723 | you don't need to overload this method. 724 | 725 | If the capture resolution has changed in size from the previous call to 726 | this method, vcserver.set_capture_resolution() must be called here 727 | before returning. You can use vcserver.capture_width and 728 | vcserver.capture_height to check the previous resolution. 729 | 730 | The name of the camera selected in the app is provided, 731 | as can be usefull to set-up the viewport render in some cases. 732 | 733 | Parameters 734 | ---------- 735 | vcserver : virtucamera.VCServer object 736 | Instance of virtucamera.VCServer calling this method. 737 | camera_name : str 738 | Name of the camera that is currently selected in the App. 739 | 740 | Returns 741 | ------- 742 | int 743 | value of the memory address to the first element of the buffer. 744 | """ 745 | 746 | view = apiUI.M3dView.active3dView() 747 | width = view.portWidth() 748 | height = view.portHeight() 749 | if width != vcserver.capture_width or height != vcserver.capture_height: 750 | vcserver.set_capture_resolution(width, height) 751 | view.readColorBuffer(self.img) 752 | img_ptr = self.img.pixels() 753 | return img_ptr 754 | 755 | 756 | def look_through_camera(self, vcserver, camera_name): 757 | """ This method must set the viewport to look through 758 | the specified camera. 759 | 760 | Parameters 761 | ---------- 762 | vcserver : virtucamera.VCServer object 763 | Instance of virtucamera.VCServer calling this method. 764 | camera_name : str 765 | Name of the camera to look through 766 | """ 767 | 768 | cmds.modelPanel(self.get_active_view(), e=True, camera=camera_name) 769 | 770 | 771 | # APP/SERVER FEEDBACK METHODS: 772 | # ---------------------------- 773 | 774 | def client_connected(self, vcserver, client_ip, client_port): 775 | """ Optional, this method is called whenever a client app 776 | connects to the server. Usefull to give the user 777 | feedback about a successfull connection. 778 | 779 | Parameters 780 | ---------- 781 | vcserver : virtucamera.VCServer object 782 | Instance of virtucamera.VCServer calling this method. 783 | client_ip : str 784 | ip address of the remote client 785 | client_port : int 786 | port number of the remote client 787 | """ 788 | 789 | self.connected_ui() 790 | # Store Maya Z Up axis, will be used for matrix conversion 791 | self.is_z_up = cmds.upAxis( q=True, axis=True ) == 'z' 792 | 793 | 794 | def client_disconnected(self, vcserver): 795 | """ Optional, this method is called whenever a client app 796 | disconnects from the server, even if it's disconnected by calling 797 | stop_serving() with the virtucamera.VCServer API. Usefull to give 798 | the user feedback about the disconnection. 799 | 800 | Parameters 801 | ---------- 802 | vcserver : virtucamera.VCServer object 803 | Instance of virtucamera.VCServer calling this method. 804 | """ 805 | 806 | if vcserver.is_serving: 807 | self.serving_ui() 808 | 809 | 810 | def server_did_stop(self, vcserver): 811 | """ Optional, calling stop_serving() on virtucamera.VCServer 812 | doesn't instantly stop the server, it is done in the background 813 | due to the asyncronous nature of some of its processes. 814 | This method is called when all services have been completely 815 | stopped. 816 | 817 | Parameters 818 | ---------- 819 | vcserver : virtucamera.VCServer object 820 | Instance of virtucamera.VCServer calling this method. 821 | """ 822 | 823 | self.stopped_ui() 824 | 825 | 826 | # CUSTOM SCRIPT METHODS: 827 | # ---------------------- 828 | 829 | def get_script_labels(self, vcserver): 830 | """ Optionally Return a list or tuple of str with the labels of 831 | custom scripts to be called from VirtuCamera App. Each label is 832 | a string that identifies the script that will be showed 833 | as a button in the App. 834 | 835 | The order of the labels is important. Later if the App asks 836 | to execute a script, an index based on this order will be provided 837 | to VCBase.execute_script(), so that method must also be implemented. 838 | 839 | Parameters 840 | ---------- 841 | vcserver : virtucamera.VCServer object 842 | Instance of virtucamera.VCServer calling this method. 843 | 844 | Returns 845 | ------- 846 | tuple or list of str 847 | custom script labels. 848 | """ 849 | 850 | return self.config.script_labels 851 | 852 | 853 | def execute_script(self, vcserver, script_index, current_camera): 854 | """ Only required if VCBase.get_script_labels() 855 | has been implemented. This method is called whenever the user 856 | taps on a custom script button in the app. 857 | 858 | Each of the labels returned from VCBase.get_script_labels() 859 | identify a custom script that is showed as a button in the app. 860 | The order of the labels is important and 'script_index' is a 0-based 861 | index representing what script to execute from that list/tuple. 862 | 863 | This function must return True if the script executed correctly, 864 | False if there where errors. It's recommended to print any errors, 865 | so that the user has some feedback about what went wrong. 866 | 867 | You may want to provide a way for the user to refer to the currently 868 | selected camera in their scripts, so that they can act over it. 869 | 'current_camera' is provided for this situation. 870 | 871 | Parameters 872 | ---------- 873 | vcserver : virtucamera.VCServer object 874 | Instance of virtucamera.VCServer calling this method. 875 | script_index : int 876 | Script number to be executed. 877 | current_camera : str 878 | Name of the currently selected camera 879 | """ 880 | 881 | if script_index >= self.config.script_count: 882 | print("Can't execute script "+str(script_index+1)+". Reason: Script doesn't exist") 883 | return False 884 | 885 | script_code = self.config.script_codes[script_index] 886 | if script_code == '': 887 | print("Can't execute script "+str(script_index+1)+". Reason: Empty script") 888 | return False 889 | 890 | script_code = script_code.replace('%SELCAM%', '"'+current_camera+'"') 891 | script_lang = self.config.script_langs[script_index] 892 | # use try to prevent any possible errors in the script from stopping plug-in execution 893 | try: 894 | if script_lang == self.config.LANG_PY: 895 | exec(script_code) 896 | elif script_lang == self.config.LANG_MEL: 897 | mel.eval(script_code) 898 | return True 899 | except: 900 | # Print traceback to inform the user 901 | traceback.print_exc() 902 | return False 903 | --------------------------------------------------------------------------------