Houdini UI Customization

From bernie's
Jump to navigation Jump to search

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)

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)

M7N0Stq.gif

# 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

kuc1PnO.gif

Customize your capture command line, no/little error checking 'cause I'm not a professional programmer.

19.5 edit: looks like the update broke some things ? Will look into it when I have time but it should be an easy fix. TODO: make it work under linux/xfce: 'mv "$(xfce4-screenshooter -ro ls)" -v ~/Downloads'

import hou
import os
import subprocess
import nodegraphutils as utils
from time import gmtime, strftime

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([r"C:\Users\me\Downloads\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'
systempath = hippath + '\\' + screenshotName
houdinipath = '$HIP/screenshots/'+screenshotName
try: 
    os.makedirs(os.path.dirname(systempath))
except OSError:
    if not os.path.isdir(os.path.dirname(systempath)):
        raise


#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(systempath)
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

3txLohO.gif

Auto add frame offset parm

ZXNrwvC.gif

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).

PARMmenu.xml

<?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)