Difference between revisions of "Houdini UI Customization"
| Line 126: | Line 126: | ||
import nodegraphutils as utils | import nodegraphutils as utils | ||
from time import gmtime, strftime | from time import gmtime, strftime | ||
from os.path import expanduser | |||
home = expanduser("~") | |||
from pathlib import Path | from pathlib import Path | ||
exe = home+"\Downloads\exe\MiniCap.exe" | |||
widthRatio = 4 # change to make screenshot bigger or smaller, this is ~x4 node width | widthRatio = 4 # change to make screenshot bigger or smaller, this is ~x4 node width | ||
def takeScreenShot(savePath): | def takeScreenShot(savePath): | ||
'''change to your preferred capture app /!\ no error checking for now ''' | '''change to your preferred capture app /!\ no error checking for now ''' | ||
subprocess.check_call([exe,"-captureregselect","-save",savePath,"-exit"]) | |||
#following is older code for linux i'll update it if I linux again | |||
''' | |||
Path(os.path.dirname(Path(savePath))).mkdir(parents=True, exist_ok=True) | Path(os.path.dirname(Path(savePath))).mkdir(parents=True, exist_ok=True) | ||
if platform == "linux" or platform == "linux2": | if platform == "linux" or platform == "linux2": | ||
| Line 147: | Line 153: | ||
elif platform == "win32": | elif platform == "win32": | ||
subprocess.check_call([r"C:\Users\bernie\Documents\houdini18.0\config\exe\MiniCap.exe","-captureregselect","-save",savePath,"-exit"]) | subprocess.check_call([r"C:\Users\bernie\Documents\houdini18.0\config\exe\MiniCap.exe","-captureregselect","-save",savePath,"-exit"]) | ||
''' | |||
def removeBackgroundImage(**kwargs): | def removeBackgroundImage(**kwargs): | ||
| Line 183: | Line 190: | ||
hippath = str(hou.getenv('HIP')) + '/screenshots' | hippath = str(hou.getenv('HIP')) + '/screenshots' | ||
screenshotName = hipname + '.' + timestamp + '.png' | screenshotName = hipname + '.' + timestamp + '.png' | ||
systempath = hippath + ' | pythonpath = hippath+'/'+screenshotName | ||
systempath = hippath + '\\' + screenshotName | |||
systempath = Path(systempath) | |||
print(systempath) | |||
houdinipath = '$HIP/screenshots/'+screenshotName | houdinipath = '$HIP/screenshots/'+screenshotName | ||
#take screenshot with capture region | #take screenshot with capture region | ||
takeScreenShot(systempath) | takeScreenShot(systempath) | ||
| Line 214: | Line 226: | ||
nullNode.move([lastSel.size()[0]*2,-lastSel.size()[1]*2]) | nullNode.move([lastSel.size()[0]*2,-lastSel.size()[1]*2]) | ||
rez = hou.imageResolution( | rez = hou.imageResolution(pythonpath) | ||
ratio = 1.0*rez[1]/rez[0] | ratio = 1.0*rez[1]/rez[0] | ||
rect = hou.BoundingRect(0,-lastSel.size()[1]*1.1,widthRatio,-widthRatio*ratio-lastSel.size()[1]*1.1) | rect = hou.BoundingRect(0,-lastSel.size()[1]*1.1,widthRatio,-widthRatio*ratio-lastSel.size()[1]*1.1) | ||
Revision as of 18:05, 8 July 2025
Various things I do to my Houdini
Shelf Tools
I find it odd that Sidefx makes you go through shelf items for various UI functions that have nothing to do with the shelf, but that's the way the cookie crumbles. Also maybe I'm nostalgic but despite it's flaws the maya shelves were kinda easier to understand. As I understand it single shelf items in houdini can be stored in different files, which means if you fumble around you can easily have two items with the same name but saved in two different places (I think. I'm confused).
Toggle Viewport Background Color
Bound it to F6
import sys
import toolutils
bg = None
try:
# cycle next bg
if kwargs['ctrlclick']: raise
bgs = hou.session.bg[:]
bgs = bgs[1:]+bgs[:1]
if kwargs['shiftclick']: bgs = ['bw', 'light', 'wb']
bg = bgs[0]
hou.session.bg = bgs
except:
# set up default bg vars
hou.session.bg = ['wb', 'bw', 'light']
bg = hou.session.bg[0]
bgs = { 'wb':'dark', 'bw':'grey', 'light':'light' }
hou.hscript("viewdisplay -B %s *" % bg)
hou.ui.setStatusMessage("Cycled background to %s" % bgs[bg].upper() )
Toggle Auto / Manual scene update
Bound to F11, I use it every day.
import hou
mode = hou.updateModeSetting().name()
if mode == 'AutoUpdate':
hou.setUpdateMode(hou.updateMode.Manual)
if mode == 'Manual':
hou.setUpdateMode(hou.updateMode.AutoUpdate)
Auto create a wrangle node
I'm pretty sure this exists already but why not reinvent the wheel. Creates an attr wrangle and keeps inputs/outputs, with the code box already in focus. I only use the 'P' parameter viewer (the one that pops up in your network view) so other desktops might not work.
bound to ctrl-shift-w
# TODO fix some weird inputs/outputs configurations
import hou
sn = hou.selectedNodes()
if sn:
w = sn[-1].parent().createNode('attribwrangle')
outputs = sn[-1].outputs()
w.setInput(0,sn[-1])
w.setCurrent(True, clear_all_selected=True)
w.setSelected(True,clear_all_selected=True, show_asset_if_selected=True)
w.setDisplayFlag(True)
sn[-1].setDisplayFlag(False)
if outputs:
for output in outputs:
output.setInput(0,w)
else:
#should not error out if it's launched in a shelf above a network plane
w = hou.ui.curDesktop().paneTabUnderCursor().pwd().createNode('attribwrangle')
w.moveToGoodPosition(relative_to_inputs=True, move_inputs=False, move_outputs=True, move_unconnected=False)
networkview = hou.ui.curDesktop().paneTabUnderCursor()
networkview.setCurrentNode(w,True)
networkview.parmMoveFocusTo('snippet')
Copy auto Paste-Merge
Copy from https://berniebernie.fr/wiki/Houdini_Python#Paste_clipboard_nodes_to_object_merges
Allow to copy nodes elsewhere with an object merge. Bound it to alt-v (so ctrl-c, alt-v in another place)
# this snippet will paste nodes in clipboard to object merges
# use it with a shortcut (I overrode 'alt-v' in network pane context)
import hou
network = hou.ui.curDesktop().paneTabUnderCursor()
networkpath = network.pwd().path()
pos = network.cursorPosition()
clipboard = hou.ui.getTextFromClipboard()
n = 0
if clipboard:
list = clipboard.split()
for item in list:
if hou.node(item) != None:
merge = hou.node(networkpath).createNode('object_merge','merge_'+item.split('/')[-1])
merge.parm('objpath1').set(str(item))
merge.setPosition(pos)
merge.move([n*2,0])
if n == 0:
merge.setSelected(True,True)
else:
merge.setSelected(True,False)
n = n + 1
Screenshot to background image
Customize your capture command line, no error checking 'cause I'm not a professional programmer, aka it works on my machine.
Should work with windows (hardcoded path to minicap ) and linux centos with xfce-screenshoter
#python 3.5+
import hou
import os
import subprocess
import nodegraphutils as utils
from time import gmtime, strftime
from os.path import expanduser
home = expanduser("~")
from pathlib import Path
exe = home+"\Downloads\exe\MiniCap.exe"
widthRatio = 4 # change to make screenshot bigger or smaller, this is ~x4 node width
def takeScreenShot(savePath):
'''change to your preferred capture app /!\ no error checking for now '''
subprocess.check_call([exe,"-captureregselect","-save",savePath,"-exit"])
#following is older code for linux i'll update it if I linux again
'''
Path(os.path.dirname(Path(savePath))).mkdir(parents=True, exist_ok=True)
if platform == "linux" or platform == "linux2":
savepath = os.path.dirname(Path(savePath)) #screenshot dir
screenshotPNG = subprocess.check_output('mv "$(xfce4-screenshooter -ro ls)" -v '+savepath,shell=True)
screenshotPNG = screenshotPNG.decode().strip().split('\n')[0].split(' -> ')[1].replace("'","") #helluva nasty oneliner
os.chdir(savepath)
os.rename(os.path.basename(screenshotPNG),os.path.basename(savePath))
# linux
elif platform == "darwin":
exit
elif platform == "win32":
subprocess.check_call([r"C:\Users\bernie\Documents\houdini18.0\config\exe\MiniCap.exe","-captureregselect","-save",savePath,"-exit"])
'''
def removeBackgroundImage(**kwargs):
''' erases bg image from tuples of backgroundImages() if it can find it, updates bg '''
deletingNode = [x[1] for x in kwargs.items()][0]
image = deletingNode.parm('houdinipath').eval()
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
backgroundImagesDic = editor.backgroundImages()
backgroundImagesDic = tuple(x for x in backgroundImagesDic if x.path() != image)
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
def changeBackgroundImageBrightness(event_type,**kwargs):
''' changes brightness/visibility if template or bypass flags are checked -- its poorly written/thought but i was tired'''
nullNode = [x[1] for x in kwargs.items()][0]
image = nullNode.parm('houdinipath').eval()
brightness = 1.0
if nullNode.isBypassed():
brightness = 0.0
elif nullNode.isTemplateFlagSet():
brightness = 0.5
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
backgroundImagesDic = editor.backgroundImages()
i = 0
for item in backgroundImagesDic:
if item.path() == image:
backgroundImagesDic[i].setBrightness(brightness)
break
i = i + 1
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
#generate unique(ish) path for screenshot
timestamp = strftime('%Y%m%d_%H%M%S', gmtime())
hipname = str(hou.getenv('HIPNAME'))
hippath = str(hou.getenv('HIP')) + '/screenshots'
screenshotName = hipname + '.' + timestamp + '.png'
pythonpath = hippath+'/'+screenshotName
systempath = hippath + '\\' + screenshotName
systempath = Path(systempath)
print(systempath)
houdinipath = '$HIP/screenshots/'+screenshotName
#take screenshot with capture region
takeScreenShot(systempath)
#set up background image plane
editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
image = hou.NetworkImage()
image.setPath(houdinipath)
sel = hou.selectedNodes()
nullNode = ''
if sel:
lastSel = sel[-1]
nullNode = lastSel.parent().createNode('null','screenshot')
if lastSel.outputConnections():
nullNode.setInput(0,lastSel)
else:
nullNode = editor.pwd().createNode('null','screenshot')
nullNode.moveToGoodPosition()
lastSel = nullNode
#configure image plane placement
nullNode.setUserData('nodeshape','task')
nullNode.setPosition(lastSel.position())
nullNode.setColor(hou.Color(.3,.3,.3))
nullNode.move([lastSel.size()[0]*2,-lastSel.size()[1]*2])
rez = hou.imageResolution(pythonpath)
ratio = 1.0*rez[1]/rez[0]
rect = hou.BoundingRect(0,-lastSel.size()[1]*1.1,widthRatio,-widthRatio*ratio-lastSel.size()[1]*1.1)
image.setRelativeToPath(nullNode.path())
image.setRect(rect)
#following is adding a spare parm with image path to be able to know which node corresponds to which background image
#could have used a user attribute or relativeToPath() and smarter logic but it works and it helps me visualize filepath
hou_parm_template_group = hou.ParmTemplateGroup()
hou_parm_template = hou.LabelParmTemplate("houdinipath", "Label", column_labels=(['\\'+houdinipath]))
hou_parm_template.hideLabel(True)
hou_parm_template_group.append(hou_parm_template)
nullNode.setParmTemplateGroup(hou_parm_template_group)
#attach a function that deletes the background image plane if the corresponding node is deleted (faster than doing it by hand)
nullNode.addEventCallback((hou.nodeEventType.BeingDeleted,), removeBackgroundImage)
#attach a function to change visibility or opacity if corresponding node flags are changed
nullNode.addEventCallback((hou.nodeEventType.FlagChanged,), changeBackgroundImageBrightness)
#add image to network background
backgroundImagesDic = editor.backgroundImages()
backgroundImagesDic = backgroundImagesDic + (image,)
editor.setBackgroundImages(backgroundImagesDic)
utils.saveBackgroundImages(editor.pwd(), backgroundImagesDic)
Other UI
Previews
Detach parameter window à la maya
Auto add frame offset parm
Userdocs XMLs
This is for right click context menus. Use the hscript 'menurefresh' when developping so as not to relaunch H each time (like a reload(module) in python).
<?xml version="1.0" encoding="UTF-8"?> <menuDocument> <menu> <subMenu id="bernie_rclick"> <label>Bernie's</label> <modifyItem><insertAfter>motion_effects_menu</insertAfter></modifyItem> <scriptItem id="bernie_set_frameoffset_parm"> <label>Add a frame offset parm to $F#</label> <context> <expression> import bernie_tools return bernie_tools.validate_sequence_parm(kwargs) </expression> </context> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.add_sequence_offset_spareparm(kwargs)]]></scriptCode> </scriptItem> <scriptItem id="bernie_set_frameoffset_parm"> <label>Browse to...</label> <context> <expression> import bernie_tools reload(bernie_tools) return bernie_tools.validate_uri(kwargs) </expression> </context> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.browse_to(kwargs)]]></scriptCode> </scriptItem> <scriptItem id="bernie_open_parm_spreadsheet"> <label>Open/Add in Parameter Spreadsheet</label> <scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.open_parm_spreadsheet(kwargs)]]></scriptCode> </scriptItem> </subMenu> </menu> </menuDocument>
ParmGearMenu.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
(long sidefx text)
-->
<menuDocument>
<!-- menuDocument can only contain 1 menu element, whose id is
implicitly "root_menu"
-->
<menu>
<scriptItem id="detach_parameter_window">
<label>Detach Parameter Window...</label>
<scriptCode><![CDATA[import bernie_tools;reload(bernie_tools);bernie_tools.detach_parameter_window(kwargs)]]></scriptCode>
</scriptItem>
</menu>
</menuDocument>
bernie_tools.py
py3
Some updates
import hou
import traceback
import re
import os
def return_first_parm(allKwargs):
'''given a right click context with kwargs, return what we want, the parameter -- which can be locked or not'''
parm = lockedparm = normalparm = False
try:
lockedparm = allKwargs['locked_parms'][0]
except:
pass
try:
normalparm = allKwargs['parms'][0]
except:
pass
if lockedparm:
parm = lockedparm
elif normalparm:
parm = normalparm
else:
print('bug')
return parm
def validate_sequence_parm(kwargs):
'''Checks if the first parm contains a value with $F'''
parm = return_first_parm(kwargs)
returnvalue = False
try:
returnvalue = '$F' in parm.rawValue()
except:
print("ERROR: %s" % traceback.format_exc())
return returnvalue
def validate_uri(kwargs):
'''Checks if the given argument, or its parent or grandparent is either a file or folder'''
parm = return_first_parm(kwargs)
parm = parm.eval()
parm = str(parm)
returnvalue = False
if os.path.isdir(parm) or os.path.isdir(os.path.dirname(parm)) or os.path.isdir(os.path.dirname(os.path.dirname(parm))):
returnvalue = True
return returnvalue
def browse_to(kwargs):
'''opens a browser to this given path (if a file), or its parent if it doesn't exist'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
dir = ''
if os.path.isdir(path):
dir = path
elif os.path.isdir(os.path.dirname(path)):
dir = os.path.dirname(path)
else:
print('No directory found')
if dir != '':
os.startfile(os.path.realpath(dir))
def add_sequence_offset_spareparm(kwargs):
'''Adds an int spare parm + file parm below a parm that has a file sequence expression of type '$F#' using hscript's padzero'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
parmval = parm.rawValue()
matchObj = re.match( r'^(.*)\$F(\d*)(.*)', parmval, re.M|re.I)
if matchObj:
n = parm.node()
parmGrp = n.parmTemplateGroup()
existing_parm = parmGrp.find(parm.name())
label = existing_parm.label()
name = existing_parm.name()
#create an offset parm with a similar name. No error checking!
fileParmTemplate = hou.StringParmTemplate(name+'_file',label+'_file', 1, default_value=([parmval]),string_type=hou.stringParmType.FileReference)
offsetParmTemplate = hou.IntParmTemplate(name+'_offset',label+'_offset', 1, default_expression=(["$F + 0"]))
parmGrp.insertAfter(existing_parm, fileParmTemplate)
parmGrp.insertAfter(fileParmTemplate, offsetParmTemplate)
n.setParmTemplateGroup(parmGrp)
#display the expression as it's most like we want to mmb through the offset
n.parm(name+'_offset').showExpression(1)
expression = "path=parm('"+name+"_file').rawValue().split('$F')\nframe=parm('"+name+"_offset').eval()\npad = int(path[1][0]) if path[1][1] is '.' else 0\nreturn path[0]+str(frame).zfill(pad)+'.'+path[1].split('.')[-1];"
hou_keyframe = hou.StringKeyframe()
hou_keyframe.setTime(1) #arbitrary keyframe needed to enable expression i think
hou_keyframe.setExpression(expression, hou.exprLanguage.Python)
parm.setKeyframe(hou_keyframe)
def detach_parameter_window(kwargs):
'''Open a floating parameter pane for a particular node.'''
node = kwargs['node']
pane_tab = hou.ui.curDesktop().createFloatingPaneTab(hou.paneTabType.Parm)
pane_tab.setCurrentNode(node)
pane_tab.setPin(True)
return pane_tab
def open_parm_spreadsheet(kwargs):
'''Opens the parameter spreadsheet with the current node selection and the right-clicked parm, appends the parm if it is already opened'''
# todo: keep existing parm mask to append to it. Can't retrieve current value AFAIK.
#import pprint
#pprint.pprint(kwargs)
#selectedParms = return_first_parm(kwargs)
parms = []
for parm in kwargs['parms']:
parms.append(parm)
for parm in kwargs['locked_parms']:
parms.append(parm)
selectedParm = parms[0]
parms = [x.name() for x in parms]
nodepaths = " ".join([node.path() for node in hou.selectedNodes()])
if not hou.selectedNodes():
nodepaths = selectedParm.node().path()
parmsheetP = selectedParm.name()
parmsheetPaths = nodepaths
if kwargs['ctrlclick']:
rmi = hou.ui.readMultiInput('Edit the node path and parms.', ['Node(s)','Parm(s)'], buttons=('OK','Cancel'), severity=hou.severityType.Message, default_choice=0, close_choice=1, help='Node path can have wildcards', title='Parm Chooser', initial_contents=(nodepaths.split(' ', 1)[0] ,selectedParm.name()))
if rmi[0]==0:
parmsheetPaths = rmi[1][0]
parmsheetP = rmi[1][1]
ps = hou.ui.findPaneTab('parmsheet')
if not ps:
desktop = hou.ui.curDesktop()
ps = desktop.createFloatingPaneTab(hou.paneTabType.ParmSpreadsheet)
cmd = 'parmsheet -w 0 -p "'+parmsheetP+'" -o "'+parmsheetPaths+'" '+ps.name()
hou.hscript(cmd)
def test(var):
print(var)
py2
Need to update to py3 for H19
In my userdocs\houdini18.5\python2.7libs folder
#march2022 update: fuck putin and improved the add offset to sequence so that you can still choose a new file and keep the offset without having to delete the spare parms (for imageplane frame offset for instance)
import hou
import traceback
import re
import os
def return_first_parm(allKwargs):
'''given a right click context with kwargs, return what we want, the parameter -- which can be locked or not'''
parm = lockedparm = normalparm = False
try:
lockedparm = allKwargs['locked_parms'][0]
except:
pass
try:
normalparm = allKwargs['parms'][0]
except:
pass
if lockedparm:
parm = lockedparm
elif normalparm:
parm = normalparm
else:
print('bug')
return parm
def validate_sequence_parm(kwargs):
'''Checks if the first parm contains a value with $F'''
parm = return_first_parm(kwargs)
returnvalue = False
try:
returnvalue = '$F' in parm.rawValue()
except:
print("ERROR: %s" % traceback.format_exc())
return returnvalue
def validate_uri(kwargs):
'''Checks if the given argument, or its parent or grandparent is either a file or folder'''
parm = return_first_parm(kwargs)
parm = parm.eval()
parm = str(parm)
returnvalue = False
if os.path.isdir(parm) or os.path.isdir(os.path.dirname(parm)) or os.path.isdir(os.path.dirname(os.path.dirname(parm))):
returnvalue = True
return returnvalue
def browse_to(kwargs):
'''opens a browser to this given path (if a file), or its parent if it doesn't exist'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
dir = ''
if os.path.isdir(path):
dir = path
elif os.path.isdir(os.path.dirname(path)):
dir = os.path.dirname(path)
else:
print('No directory found')
if dir != '':
os.startfile(os.path.realpath(dir))
def add_sequence_offset_spareparm(kwargs):
'''Adds an int spare parm + file parm below a parm that has a file sequence expression of type '$F#' using hscript's padzero'''
parm = return_first_parm(kwargs)
path = os.path.dirname(parm.eval())
parmval = parm.rawValue()
matchObj = re.match( r'^(.*)\$F(\d*)(.*)', parmval, re.M|re.I)
if matchObj:
n = parm.node()
parmGrp = n.parmTemplateGroup()
existing_parm = parmGrp.find(parm.name())
label = existing_parm.label()
#create an offset parm with a similar name. No error checking!
fileParmTemplate = hou.StringParmTemplate(label+'_file',label+'_file', 1, default_value=([parmval]),string_type=hou.stringParmType.FileReference)
offsetParmTemplate = hou.IntParmTemplate(label+'_offset',label+'_offset', 1, default_expression=(["$F + 0"]))
parmGrp.insertAfter(existing_parm, fileParmTemplate)
parmGrp.insertAfter(fileParmTemplate, offsetParmTemplate)
n.setParmTemplateGroup(parmGrp)
#display the expression as it's most like we want to mmb through the offset
n.parm(label+'_offset').showExpression(1)
expression = "path=parm('"+label+"_file').rawValue().split('$F')\nframe=parm('"+label+"_offset').eval()\npad = int(path[1][0]) if path[1][1] is '.' else 0\nreturn path[0]+str(frame).zfill(pad)+'.'+path[1].split('.')[-1];"
hou_keyframe = hou.StringKeyframe()
hou_keyframe.setTime(1) #arbitrary keyframe needed to enable expression i think
hou_keyframe.setExpression(expression, hou.exprLanguage.Python)
parm.setKeyframe(hou_keyframe)
def detach_parameter_window(kwargs):
'''Open a floating parameter pane for a particular node.'''
node = kwargs['node']
pane_tab = hou.ui.curDesktop().createFloatingPaneTab(hou.paneTabType.Parm)
pane_tab.setCurrentNode(node)
pane_tab.setPin(True)
return pane_tab
def open_parm_spreadsheet(kwargs):
'''Opens the parameter spreadsheet with the current node selection and the right-clicked parm, appends the parm if it is already opened'''
#import pprint
#pprint.pprint(kwargs)
selectedParm = return_first_parm(kwargs)
nodepaths = " ".join([node.path() for node in hou.selectedNodes()])
if not hou.selectedNodes():
nodepaths = selectedParm.node().path()
#print(nodepaths)
ps = hou.ui.findPaneTab('parmsheet')
if not ps:
desktop = hou.ui.curDesktop()
ps = desktop.createFloatingPaneTab(hou.paneTabType.ParmSpreadsheet)
cmd = 'parmsheet -w 1 -p "'+selectedParm.name()+'" -o "'+nodepaths+'" '+ps.name()
hou.hscript(cmd)
def test(var):
print(var)