Difference between revisions of "Houdini UI Customization"
m (→Shelf Tools) |
m (→Shelf Tools) |
||
| Line 1: | Line 1: | ||
Various things I do to my Houdini | Various things I do to my Houdini | ||
Quick reminder to myself. When testing out things, call '''menurefresh''' in Hscript textport to refresh different menus (notably PARMmenu.xml) | |||
== Shelf Tools == | == 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. | 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. | ||
Revision as of 15:44, 21 October 2025
Various things I do to my Houdini
Quick reminder to myself. When testing out things, call menurefresh in Hscript textport to refresh different menus (notably PARMmenu.xml)
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). Just make sure you keep an eyeball on where your shelf tool is stored (often in Default.shelf or some random production folder if you use Rez like us)
Selection to bounding box to delete
Very often I manually select prims to delete a few local faces, then change something upstream, then my blast is all mangled. This will create a bounding box of the currently selected geo, group them with it and then blast the group. It's never perfect because of how bounding boxes operate (points/prims/othogonal to world axes), but it's very useful in my daily work.
Also Gemini did 90% of the coding and actually does error catching. It's a lot better than me :0
import hou, toolutils
def create_group_from_bbox():
"""
Creates a 'group' node and sets its bounding region parameters
based on the bounding box of the current geo selection
"""
try:
selected_nodes = hou.selectedNodes()
geo = toolutils.sceneViewer().selectGeometry()
# Check if exactly one node is selected.
if len(selected_nodes) != 1:
print("Please select a single geometry node to get the bounding box from.")
return
selected_node = selected_nodes[0]
# Check if the selected node is a geometry node (SOP).
if selected_node.type().category() != hou.sopNodeTypeCategory():
print("The selected node is not a geometry node (SOP). Please select a valid node.")
return
hou.clearAllSelected()
bbox = geo.boundingBox()
parent_node = selected_node.parent()
selected_node.setSelected(0)
group_node = parent_node.createNode("groupcreate", "bbox_group")
pos = [selected_node.position()[0],selected_node.position()[1]-1]
group_node.setPosition(pos)
group_node.setInput(0, selected_node)
group_node.parm("groupbounding").set(1)
group_node.parmTuple("size").set(bbox.sizevec())
group_node.parmTuple("t").set(bbox.center())
blast = parent_node.createNode("blast", "bbox_blast")
blast.setInput(0, group_node)
pos[1] = pos[1] - 1
blast.setPosition(pos)
blast.parm('group').set(group_node.parm("groupname").eval())
group_node.setSelected(1)
blast.setDisplayFlag(1)
blast.setSelected(1)
blast.setRenderFlag(1)
group_node.setCurrent(True, clear_all_selected=True)
except hou.Error as e:
print(f"An error occurred: {e}")
create_group_from_bbox()
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)