Skip to content

Instantly share code, notes, and snippets.

@gregoiredehame
Last active December 9, 2025 12:17
Show Gist options
  • Select an option

  • Save gregoiredehame/085c10c4129d66fb6fca887acf1cbcd2 to your computer and use it in GitHub Desktop.

Select an option

Save gregoiredehame/085c10c4129d66fb6fca887acf1cbcd2 to your computer and use it in GitHub Desktop.
playblast with facs attributes HUD
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)
@gregoiredehame
Copy link
Author

capture

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