Last active
December 9, 2025 12:17
-
-
Save gregoiredehame/085c10c4129d66fb6fca887acf1cbcd2 to your computer and use it in GitHub Desktop.
playblast with facs attributes HUD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from __future__ import annotations | |
| import maya.mel as mel | |
| import maya.cmds as cmds | |
| import maya.api.OpenMaya as om2 | |
| import maya.api.OpenMayaUI as omui2 | |
| import os | |
| import time | |
| import logging | |
| log = logging.getLogger(__name__) | |
| log.setLevel(logging.INFO) | |
| class Playblast(object): | |
| def __init__(self, start_frame:float=None, end_frame:float=None, file_path:str=None, camera:str=None, height:float=None, width:float=None, source_object:str=None, source_attributes:list=None) -> str or None: | |
| """ class object that will playblast with a custom HUD based on a given object and given attributes. | |
| this has been created in order to quickly interate some playblasts while we animate a facs face rig to also review muscles and facs animation. | |
| other than that, this playblast class can be used as a default playbast tool. just leave source_object and source_attributes arguments to None to avoid HUD. | |
| Args: | |
| start_frame: (float): - float start frame for playblast if None, will use current frame range. | |
| end_frame: (float): - float end frame for playblast if None, will use current frame range. | |
| file_path: (str): - string path of the file path that will be used to save playblast on disk. | |
| camera: (str): - camera to be used for playblast. if None, will use current camera. | |
| height: (float): - height pixel for playblast outputs. If None, will use current camera settings. | |
| width: (float): - width pixel for playblast outputs. If None, will use current camera settings. | |
| source_object: (str): - source object to query attributes from. ( if None, will query first selected ). | |
| source_attributes: (list): - source attributes to attach to the HUD. ( if None, will query all custom attributes on given source object ) | |
| Returns: | |
| file_path (str): - will output the playblast path on disk. | |
| Exemple Usage: | |
| Playblast(start_frame=1001, end_frame=1100, file_path="C:/Users/Desktop/WORK/myPlayblast.mov", camera="persp", height=1080, width=1920, source_object="locator1", source_attributes=["jawOpen", "eyeClosed"]) | |
| """ | |
| self.block_size = "small" | |
| if not file_path: | |
| scene_name = cmds.file(query=True, sceneName=True) | |
| file_path = cmds.fileDialog2(fileFilter="MOV (*.mov);;AVI (*.avi)", dialogStyle=2, fileMode=0, caption='Playblast As', startingDirectory=os.path.dirname(scene_name) if scene_name else "") | |
| file_path = file_path[0] if file_path else None | |
| print(file_path) | |
| if file_path and file_path.endswith(('.mov','.avi')): | |
| start_time = time.process_time() | |
| sections, old_huds, old_huds_visibility = self.desactivate_huds() | |
| undo = cmds.undoInfo(query=True, state=True) | |
| try: | |
| cmds.undoInfo(state=False) | |
| start_frame, end_frame, camera, width, height = self.setup_playblast(start_frame=start_frame, end_frame=end_frame, camera=camera, width=width, height=height) | |
| attributes_huds = [] | |
| selection = cmds.ls(selection=True) | |
| if not source_object or not cmds.objExists(source_object): | |
| source_object = selection[0] if len(selection) > 0 else None | |
| if source_object and cmds.objExists(source_object): | |
| attributes = self.channel_box_attributes(source_object=source_object) if not source_attributes else source_attributes | |
| dictionnary = self.query_sections(total_attributes=attributes, height=(height/2)) | |
| for section in dictionnary.keys(): | |
| total_attributes = dictionnary[section] | |
| if section in [5,6,7,8,9]: total_attributes.reverse() | |
| for attribute in total_attributes: | |
| hud = self.create_hud_slider(source_object=source_object, source_attribute=attribute, total_attributes=total_attributes, section=section) | |
| attributes_huds.append(hud) | |
| cmds.select(clear=True) | |
| self.launch_playblast(start_time=start_time, start_frame=start_frame, end_frame=end_frame, file_path=file_path, camera=camera, width=width, height=height) | |
| [cmds.headsUpDisplay(hud, remove=True) for hud in attributes_huds] | |
| except Warning: | |
| self.reactivate_huds(sections=sections, huds=old_huds, huds_visibility=old_huds_visibility) | |
| cmds.undoInfo(state=undo) | |
| except Exception: | |
| self.reactivate_huds(sections=sections, huds=old_huds, huds_visibility=old_huds_visibility) | |
| cmds.undoInfo(state=undo) | |
| finally: | |
| self.reactivate_huds(sections=sections, huds=old_huds, huds_visibility=old_huds_visibility) | |
| cmds.undoInfo(state=undo) | |
| else: | |
| log.info('- playblast cancelled. no file path has been given.') | |
| def channel_box_attributes(self, source_object:str=None, exclude_list:list=['translateX', 'translateY', 'translateZ', 'rotateX', 'rotateY', 'rotateZ', 'scaleX', 'scaleY', 'scaleZ', 'visibility']) -> list: | |
| """ Function that will return a proper list of all attributes on the channel box | |
| Args: | |
| source_object: (str): - source object to query attributes from. | |
| exclude_list: (list): - list of the attributes you want to remove from returns. | |
| """ | |
| attributes = [] | |
| for attribute in cmds.listAttr(source_object, keyable=True, unlocked=True) or []: | |
| [attributes.append(attribute) if attribute not in attributes + exclude_list else None] | |
| for attribute in cmds.listAttr(source_object, keyable=True, locked=True) or []: | |
| [attributes.append(attribute) if attribute not in attributes + exclude_list else None] | |
| return attributes | |
| def query_sections(self, total_attributes:list=[], attribute_height:float=20, height:int=None) -> dict: | |
| """ Function that will return a proper python dictionnary based on all given attribute, and pixel heights on screen, so we know which sections it needs to be attached to. | |
| Args: | |
| total_attributes: (list): - total attributes list. | |
| attribute_height: (float): - float value of the text pixel heights on screen. | |
| height: (int): - integer value of the render on screen. | |
| """ | |
| if self.block_size == "small": attribute_height = 20 | |
| elif self.block_size == "medium": attribute_height = 35 | |
| elif self.block_size == "large": attribute_height = 50 | |
| all_section_in_order = [0,5,1,6,2,7,3,8,4,9] | |
| sections = {} | |
| current_height = 0 | |
| current_section = 0 | |
| for attribute in total_attributes: | |
| if current_height + attribute_height > height: | |
| current_section += 1 | |
| current_height = 0 | |
| if all_section_in_order[current_section] not in sections: | |
| sections[all_section_in_order[current_section]] = [] | |
| sections[all_section_in_order[current_section]].append(attribute) | |
| current_height += attribute_height | |
| return sections | |
| def create_hud_slider(self, source_object:str=None, source_attribute:str=None, total_attributes:list=None, section:int=None) -> list (hud, script): | |
| """ Function that will create the hud sliders based on a given attribute. | |
| some script jobs will be created so that function will return both hud and script node, to make sure we remove them post playblast. | |
| Args: | |
| source_object: (str): - source object to create HUD from. | |
| source_attribute: (str): - source attribute to create HUD from. | |
| """ | |
| index = total_attributes.index(source_attribute) | |
| if cmds.headsUpDisplay('slider_hud_%s_%s'% (source_object, source_attribute), query=True, exists=True): | |
| cmds.headsUpDisplay('slider_hud_%s_%s'% (source_object, source_attribute), remove=True) | |
| if cmds.attributeQuery(source_attribute, node=source_object, exists=True): | |
| for hud in cmds.headsUpDisplay(listHeadsUpDisplays=True) or []: | |
| if cmds.headsUpDisplay(hud, query=True, section=True) == section and cmds.headsUpDisplay(hud, query=True, block=True) == index: | |
| try: cmds.headsUpDisplay(hud, edit=True, block=100+index) | |
| except: pass | |
| cmds.headsUpDisplay('slider_hud_%s_%s'% (source_object, source_attribute), section=section, block=index, labelWidth=150, dataWidth=75, blockSize=self.block_size, label=source_attribute, labelFontSize='large', dataAlignment='left', dataFontSize='large', attachToRefresh=1, command=lambda :self.update_attribute_value(source_object=source_object, source_attribute=source_attribute)) | |
| return 'slider_hud_%s_%s'% (source_object, source_attribute) | |
| else: | |
| log.info('- skipping "%s.%s". does not exists.'% (source_object, source_attribute)) | |
| def update_attribute_value(self, source_object:str=None, source_attribute:str=None) -> float: | |
| """ Function that will return the current attribute value each time the frame will change on playblast. | |
| the function will return a dictionnary will all current block and section position, such as visiblities infos. | |
| Args: | |
| source_object: (str): - string object to query attribute from. | |
| source_attribute: (str): - string attribute to return value from. | |
| """ | |
| try: return cmds.getAttr(f'{source_object}.{source_attribute}') | |
| except: return 0.0 | |
| def desactivate_huds(self) -> (dict, list, list): | |
| """ Function that will list all the current HUD on scene. and will momentarily hide them. | |
| the function will return a dictionnary will all current block and section position, such as visiblities infos. | |
| Args: | |
| // | |
| """ | |
| old_huds = cmds.headsUpDisplay(listHeadsUpDisplays=True) or [] | |
| old_huds_visibility = [cmds.headsUpDisplay(hud, query=True, visible=True) for hud in old_huds] | |
| sections = {i: {} for i in range(10)} | |
| for hud in old_huds: | |
| section, block = cmds.headsUpDisplay(hud, query=True, section=True), cmds.headsUpDisplay(hud, query=True, block=True) | |
| sections[section][block] = hud | |
| cmds.headsUpDisplay(hud, edit=True, visible=False) | |
| return (sections, old_huds, old_huds_visibility) | |
| def reactivate_huds(self, sections:dict=None, huds:list=[], huds_visibility:list=[]) -> None: | |
| """ Function that will reactivate all given huds. | |
| Args: | |
| section: (dict): - dictionnary off all the HUD to reactivate per section and block. | |
| huds: (list): - list of the huds that need to be restore. | |
| huds_visibility: (list): - list of the huds visibilities status. | |
| """ | |
| for hud, visibility in zip(huds, huds_visibility): | |
| try: cmds.headsUpDisplay(hud, edit=True, visible=visibility) | |
| except: pass | |
| for section, blocks in sections.items(): | |
| for block, hud in sorted(blocks.items()): | |
| try: cmds.headsUpDisplay(hud, edit=True, section=section, block=block) | |
| except: pass | |
| def setup_playblast(self, start_frame:int=None, end_frame:int=None, camera:str=None, width:int=None, height:int=None) -> (int, int, str, int, int): | |
| """ Function that will setup playblast settings and make sure we run this using mayaHardware2. | |
| Args: | |
| start_frame: (int): - start frame for playblast. | |
| end_frame: (int): - end frame for playblast. | |
| camera: (string): - string name of the camera to look thru | |
| width: (int): - width float pixel for playblast. | |
| height: (int): - height float pixel for playblast. | |
| """ | |
| viewport = omui2.M3dView.active3dView() | |
| width = viewport.portWidth() if not width else width | |
| height = viewport.portHeight() if not height else height | |
| start_frame = cmds.playbackOptions(query=True, minTime=True) if not start_frame else start_frame | |
| end_frame = cmds.playbackOptions(query=True, maxTime=True) if not end_frame else end_frame | |
| mel.eval('setCurrentRenderer mayaHardware2') | |
| cmds.setAttr('hardwareRenderingGlobals.renderMode', 2) | |
| cmds.setAttr('hardwareRenderingGlobals.multiSampleEnable', 1) | |
| current_res = cmds.listConnections('defaultRenderGlobals.resolution')[0] | |
| cmds.setAttr(f"{current_res}.deviceAspectRatio", float(width)/float(height)) | |
| cmds.setAttr(f"{current_res}.height", height) | |
| cmds.setAttr(f"{current_res}.width", width) | |
| cmds.setAttr(f"{current_res}.pixelAspect", 1.0) | |
| cmds.setAttr('defaultRenderGlobals.startFrame', start_frame) | |
| cmds.setAttr('defaultRenderGlobals.endFrame', end_frame) | |
| cmds.setAttr('defaultRenderGlobals.imageFormat', 0) | |
| cameras = [] | |
| for cam in cmds.ls(type='camera'): | |
| try: | |
| cmds.setAttr(f"{cam}.renderable", lock=False) | |
| cmds.setAttr(f"{cam}.renderable", 0) | |
| except: pass | |
| try: cameras.append(cmds.listRelatives(cam, parent=True)[0]) | |
| except: pass | |
| if not camera or camera not in cameras: | |
| camera = om2.MFnDagNode(om2.MDagPath(omui2.M3dView.active3dView().getCamera()).transform()).name() | |
| cmds.lookThru(camera) | |
| for shape in cmds.listRelatives(camera, shapes=True): | |
| try: | |
| cmds.setAttr(f"{cam}.renderable", lock=False) | |
| cmds.setAttr(f"{shape}.renderable", 1) | |
| except: pass | |
| return (start_frame, end_frame, camera, width, height) | |
| def launch_playblast(self, start_time:float=None, start_frame:int=None, end_frame:int=None, file_path:str=None, camera:str=None, width:int=None, height:int=None) -> str: | |
| """ Function that will run the playblast and write to disk is using mayaHardware2. | |
| Args: | |
| start_time: (float): - start time for the entire function. | |
| start_frame: (int): - int start frame | |
| end_frame: (int): - int end frame | |
| file_path: (string): - string name to save the playblast. | |
| camera: (string): - string name of the camera that will be used. | |
| """ | |
| try: | |
| playback_slider = mel.eval('$tmpVar=$gPlayBackSlider') | |
| sound_file = cmds.timeControl(playback_slider, query=True, sound=True) | |
| if file_path.endswith('.mov'): | |
| cmds.playblast(format='qt', startTime=start_frame, endTime=end_frame, percent=100, filename=file_path, showOrnaments=True, forceOverwrite=True, clearCache=True, offScreen=True, viewer=False, compression="H.264", quality=100, widthHeight=[width, height], sound=sound_file) | |
| return file_path | |
| elif file_path.endswith('.avi'): | |
| cmds.playblast(format='avi', startTime=start_frame, endTime=end_frame, percent=100, filename=file_path, showOrnaments=True, forceOverwrite=True, clearCache=True, offScreen=True, viewer=False, compression="MS-CRAM", quality=100, widthHeight=[width, height], sound=sound_file) | |
| return file_path | |
| else: | |
| log.warning('- unknown playblast format. only .mov and .avi are available.') | |
| except Exception as e: | |
| log.exception(f'- skipping playblast. "{e}".') | |
| return None | |
| if __name__ == "__main__": | |
| Playblast(start_frame=0, end_frame=100, file_path=None, camera="persp", height=1080, width=1920) |
Author
gregoiredehame
commented
Dec 4, 2023

Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment