Houdini Python
Getting the topmost input of a node
Let's say you want to get the first node in a tree (like a file sop) that's connected to a given SOP. This goes up inputs (the first one) until it won't find one anymore. Simple but useful.
import hou
myNode = hou.node('/obj/geo1/null1')
while myNode.input(0):
myNode = myNode.input(0)
print(myNode.path())
Delete children of a node that have their display flags turned off
import hou
obj = hou.selectedNodes()[0]
for curNode in obj.children():
if(curNode.type().name() == 'geo' and curNode.isDisplayFlagSet() == False):
#print(curNode)
curNode.destroy()
Move all keyframes to the beginning of the timeline
Story is I have some tracked camera+plate that are TC'ed at range at frame 24878933 for instance which is a pain in the ass, and no shotgun/grid tool to put that to an artist-friendly frame start like 101 or 1001
This will take a node selection (or a node as argument), figure out the range of all the keyframes on the node and subnodes (optionally), and offset the range of these keyframes to match the start of your current timeline.
import hou
def offsetParmKeyframes( parmTup, offset ):
'''given a parm, move its keyframes in time by the offset. Deletes an recreates the keyframes'''
keyframes = parmTup.keyframes()
if keyframes:
parmTup.deleteAllKeyframes()
for kf in keyframes:
kf.setFrame( kf.frame() + offset )
parmTup.setKeyframe( kf )
def keyframesToPlaybar(node=None,recursive=False):
'''given a node or a selection, go through keyframes and move all keys to the timeline'''
#gather all parms and subitem parms
nodelist = []
if node is None:
nodelist = hou.selectedNodes()
if not nodelist:
hou.ui.displayMessage('Select a node')
return
if recursive:
newnodelist = ()
for node in nodelist:
newnodelist = newnodelist + node.allSubChildren()
nodelist = nodelist + newnodelist
else:
nodelist.append(node)
#keep only those that have >1 keyframes, find out the min and max framerange of all the keyframes found
parms = []
frameRangeMinMax = [999999999,-999999999]
for node in nodelist:
for parm in node.parms():
#only keep parms that have more than one keyframe, otherwise it's an expression or single keyframe >= doesn't need to be moved
if len(parm.keyframes())>1:
parms.append(parm)
frameRangeMinMax[0] = min([parm.keyframes()[0].frame(),frameRangeMinMax[0]])
frameRangeMinMax[1] = max([parm.keyframes()[-1].frame(),frameRangeMinMax[1]])
#print(frameRangeMinMax)
#print(parms)
offset = hou.playbar.playbackRange()[0] - frameRangeMinMax[0]
for parm in parms:
offsetParmKeyframes( parm, offset )
return offset
keyframesToPlaybar(None,1)
After Effects camera to Houdini camera
Pretty crappy/hacky code but works enough for me to post here. This assumes it's a baked, every-frame-is-keyframed camera, should work with linear keyframes too.
Howto:
- Put this code in a shelf
- in AE select the position and rotation properties (that have keyframes. I need to generalize the code to add more properties)
- Ctrl-c
- In Houdini press your newly created shelf button. Should be good!
import hou, re
def clipboardToCamera():
''' takes a clipboard containing AE keyframe data and creates and returns camera '''
#todo: generalize to other items, add aperture/zoom
#baked constants
matchFps = False # match scene fps from comp size
matchRez = True # match camera rez from comp size
translateResize = -0.01 # scene scale change, this one worked for me
AEclipboard = hou.ui.getTextFromClipboard()
#check if we have AE data
if 'Keyframe Data' not in AEclipboard:
hou.ui.displayMessage('No keyframe data from AE found')
return
#match FPS
if matchFps:
try:
AEfps = float(AEclipboard.split('Units Per Second')[1].splitlines()[0])
hou.setFps(AEfps)
hou.ui.displayMessage('Changing FPS to '+string(AEfps))
except:
print('Units Per Second not found in keyframe data')
#check for transform keyword, if so, create camera and add keys
if 'Transform' in AEclipboard:
camera = hou.node('/obj').createNode('cam')
#I found I needed to change rotation order to match AE<>Houdini camera rotation
camera.parm('rOrd').set(5)
camera.parm('iconscale').set(50)
#match Resolution
if matchRez:
try:
w = float(AEclipboard.split('Source Width')[1].splitlines()[0])
h = float(AEclipboard.split('Source Height')[1].splitlines()[0])
camera.parm('resx').set(w)
camera.parm('resy').set(h)
except:
print('Source Width and/or Source Height not found in keyframe data')
#aeNames = ['Camera Options Aperture','Transform Position','Transform Orientation']
aeNames = ['Transform Position','Transform Orientation']
houdiniParms = ['t','r']
itemN = -1
for index, item in enumerate( aeNames ):
aeName = item.replace(' ', '\\t*\\s*')
# print('split @ '+aeName)
# the followin is nasty but should give us tuples [[frame#1,x,y,z][frame#2,x,y,z]]
clipboardSplit = re.split(aeName, AEclipboard)
clipboarSplitKeyframes = clipboardSplit[1].split('\n\n')[0].splitlines()
kf = [x.split('\t')[1:-1] for x in clipboarSplitKeyframes[2:]]
#print(kf)
# make a keyframe for each line
for line in kf:
itemN = itemN + 1
#if itemN < 5:
# print(line)
for index2, subparm in enumerate(['x','y','z']):
curParm = camera.path()+'/'+houdiniParms[index]+subparm
if houdiniParms[index] == 't':
scale = translateResize
else:
scale = 1
frameOffset = 0
#if itemN < 5:
# print(curParm+' '+str(int(line[0])+frameOffset)+' '+str(float(line[index2+1])*scale))
setKeyFrame(curParm,int(line[0])+frameOffset,float(line[index2+1])*scale)
def setKeyFrame(parm,time,value):
hou_keyframe = hou.Keyframe()
hou_keyframe.setFrame(time)
hou_keyframe.setValue(value)
hou_keyframe.setSlope(0)
hou_keyframe.setInSlope(0)
hou_keyframe.useSlope(False)
hou_keyframe.setAccel(0)
hou_keyframe.useAccel(False)
hou_keyframe.interpretAccelAsRatio(False)
hou_keyframe.setExpression("constant()", hou.exprLanguage.Hscript)
hou.parm(parm).setKeyframe(hou_keyframe)
clipboardToCamera()
Python node cook timer
Poor man's Performance Monitor. Probably better versions out there but it worked for my HDA.
Uses Python3's timeit and hou.session 'global' var to to a time delta by calling two python scripts sandwiching your heavy Sops.
First script node
import timeit;hou.session.t0=timeit.default_timer()
(python sleep 3 seconds in my example: import time;time.sleep(3) )
And the timer stop. Should be cleaned up as there's probably node cooking/caching shenanigans going on.
import timeit
t1=timeit.default_timer()
tdelta = t1-hou.session.t0
hou.pwd().setComment(str(f"{tdelta:.03f} secs."))
hou.pwd().setGenericFlag(hou.nodeFlag.DisplayComment,True)
Get start and end file number from sequence
import re, hou, glob
f = hou.node('/obj/geo1/file2').parm('file')
def outputSequenceStartEnd(fileparm):
'''given a file parm return the beginning and end of sequence'''
fileparm = f.rawValue()
matchObj = re.match( r'^(.*)\$F(\d*)(.*)', parmval, re.M|re.I) #expects $F* in the filename
before = matchObj.groups()[0]
extension = matchObj.groups()[2]
fileGlob = before + '*' + extension
files = glob.glob(fileGlob)
files = sorted(files)
#eww
startframe = files[0].split(before[-6:])[-1].split(extension)[0] #dirty hack to get the file number because reusing the regex clashes with the glob return
endframe = files[-1].split(before[-6:])[-1].split(extension)[0]
return [startframe,endframe]
print(outputSequenceStartEnd(f))
#returns ['200000', '204805']
Because I'm tired of having to navigate if there is more than one Octane ROP in my file. As a bonus saves last used IPR as default
import hou
def chooseIPRdialog(buttons):
try:
c = hou.session.choice
except:
hou.session.choice = 0
buttons.append('Cancel')
dialog = hou.ui.displayMessage('IPR choice', buttons=buttons,default_choice=hou.session.choice)
hou.session.choice = dialog
if dialog == len(buttons)-1:
dialog = False
return dialog
#grab iprs
iprs = []
for node in hou.node('/').allSubChildren():
if node.type().name() == 'Octane_ROP':
iprs.append(node)
#launch dialog, return ipr, launch ipr if not cancelled
names = [x.name() for x in iprs]
if len(names) == 1:
iprs[0].parm('HO_IPR').pressButton()
else:
chosenIPR = chooseIPRdialog(names)
if chosenIPR is not False:
iprs[chosenIPR].parm('HO_IPR').pressButton()
Log for loop progress to console
Kinda dirty but you can just use a print argument in a python sop it should log out and be visible in your renderfarm logs. Here I showed progress every 500 items.
#i have two spare parameters fetching current iteration and total number of iterations
#using detail(1,'iteration',0) expression or -1 if you use a spare
iterFloat = float(`chs("iter")`)
maxFloat = float(`chs("numiter")`)
if(iterFloat%500==0):
print(str(round(iterFloat/maxFloat*100,1))+ " % "+str(int(iterFloat))+"/"+str(int(maxFloat)))
Auto-add frame offset parameter
I've written padzero(...) too many times in production so I decided to waste a few hours writing a way to add it to the right-click menu available for parms
For this to work I need two things:
An XML file called PARMmenu.xml (doc), in the python path or more generally the base folder of your preferences.
I loosely based myself on code from qLib and other ressources. But the important thing to note is that there are two 'important' sections:
- A 'context' tag that is a python code that parses the parameter you right click and returns True or False -- which will tell Houdini if you need to show the menu (it is generated on the fly). In my case it looks for '$F#' in the parameter value
- A 'scriptCode' tag that is the actual code that is run
To make things cleaner I call code from my personal python library (see below) instead of putting the full code here.
/!\ Pain in the Butt Houdini19 update: since it's Python3+ I can't use 'reload(module)' anymore. Code below work for Python2.7
<?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> </subMenu> </menu> </menuDocument>
This is the actual code, that I place in a pythonpath (here it is in $HOUDINI_USER_PREF/python2.7libs/bernie_tools.py)
import hou
import traceback
import re
def validate_sequence_parm(kwargs):
'''Checks if the first parm contains a value with $F'''
returnvalue = False
try:
returnvalue = '$F' in kwargs['parms'][0].rawValue()
except:
print("ERROR: %s" % traceback.format_exc())
return returnvalue
def add_sequence_offset_spareparm(kwargs):
'''Adds an int spare parm below a parm that has a file sequence expression of type '$F#' using hscript padzero'''
parm = kwargs['parms'][0]
parmval = parm.rawValue()
#check if there is a $F(plus digit)
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())
#create an offset parm with a similar name. No error checking!
plabel = existing_parm.label() + " Offset"
pname = parm.name()+"_offset"
offsetParmTemplate = hou.IntParmTemplate( pname, plabel, 1, default_expression=(["$F + 0"]))
parmGrp.insertAfter(existing_parm, offsetParmTemplate)
n.setParmTemplateGroup(parmGrp)
#display the expression as it's most like we want to mmb through the offset
n.parm(pname).showExpression(1)
#figure out if there is any padding in the original parm using the regex result, otherwise padding at 1 (no padding)
padding = 1
if matchObj.groups()[1]:
padding = matchObj.groups()[1]
expression = matchObj.groups()[0]+"`padzero("+str(padding)+",ch('" + pname + "'))`"+matchObj.groups()[2]
parm.set(expression)
Set path parameter true for all alembic nodes
Don't know why it's not on by default, thanks maya ? Will probably add it to a more general right click menu.
import hou
sel = hou.selectedNodes()
for item in sel:
for node in item.allSubChildren():
if node.type().name() == 'alembic':
node.parm('addpath').set(1)
Houdini set random layercolorid for Octane
Piece of shit integration. Sorry Juanjo
from random import randint, uniform
from colorsys import hsv_to_rgb
s = 1.0
v = 1.0
for o in hou.selectedItems():
h = uniform(0.0, 1.0)
s = uniform(0.5, 1.0)
v = uniform(0.5, 1.0)
r, g, b = hsv_to_rgb(h, s, v)
o.parmTuple('octane_objprop_color').set([r,g,b])
Selecting other nodes of similar type
- Will select all nodes that correspond to the type of the currently selected nodes, in the same hierachy.
- Todo: allow to select items that are visible only in the region of the viewpane we are in. is it useful ?
import hou
selection = hou.selectedNodes()
nodetypes = []
for node in selection:
nodetype = node.type()
if nodetype not in nodetypes:
nodetypes.append(node.type())
selectednodes = []
for node in hou.selectedNodes()[0].parent().allSubChildren():
if node.type() in nodetypes:
selectednodes.append(node)
hou.clearAllSelected()
for node in selectednodes:
node.setSelected(1,0)
selection[-1].setSelected(1,0)
hou.ui.setStatusMessage(str(len(selectednodes)) + " nodes selected")
get complementary color
MMhh . This seems overly complicated for something so simple in other languages. Will modify it if I find a better solution
r = parm('octane_gradient3_1cr').eval()
g = parm('octane_gradient3_1cg').eval()
b = parm('octane_gradient3_1cb').eval()
#create color object
col = hou.Color((r,g,b))
#create hsv object from rgb color
hsv = hou.Color.hsv(col)
#get complementary color hue (accross the color wheel)
h = (hsv[0]+180)%360
#create new color
c = hou.Color()
#set color from hsv values
c.setHSV([h,hsv[1],hsv[2]])
#return red
return c.rgb()[0]
screenshot to background image
work in progress, interactively take a screenshot and puts it in the network view
- saves image in a screenshot subfolder next to .hip file
- is attached to a null that you can move/delete/template
- uses Minicap as my capture software on Windows
- is probably buggy
- For future reference for myself, this is where houdini stores (some of?) its UI python functions C:\Program Files\Side Effects Software\Houdini ####\houdini\python2.7libs, maybe it's overloadable to edit the interface ? Also good nographutils hidden doc from Juraj Tomori :https://jtomori.github.io/houdini_additional_python_docs/nodegraphutils.html
- Similar code has been embedded to Prism pipeline: https://prism-pipeline.com/
TBD:
- make it work in all contexts and levels (right now it breaks at / level and haven't tried in CHOPs/ROPs
- embed images in HDAs ?
- make it OS agnostic and no dependencies (screenshot via Python?)>>> linux version on Odforce: https://forums.odforce.net/topic/32581-networkimage/?tab=comments#comment-203986
Code moved to https://berniebernie.fr/wiki/Houdini_UI_Customization#Screenshot_to_background_image
load all objs in list by frame
Generate obj list:
import os
dir_path = os.path.dirname(os.path.realpath(__file__))
listfilename = 'objs_list.txt'
objlist = []
for root, dirs, files in os.walk(dir_path):
for file in files:
if file.lower().endswith('.obj'):
objlist.append(os.path.join(root, file))
print('Writing '+str(len(objlist))+'to '+dir_path+'/'+listfilename)
with open(dir_path+'/'+listfilename, 'w') as file_handler:
for item in objlist:
file_handler.write("{}\n".format(item))
In read file expression (with an added 'file' parm pointing to the above filelist.txt, called filelist) :
import os, linecache
fl = hou.parm('filelist').eval()
path = linecache.getline(fl, int(hou.frame())).rstrip()
return path.replace(os.sep,'/')
ROP output driver to animated gif
To place in post render script. Linux for now, imagemagick is finicky. Matt Estela has a great page as usual https://www.tokeru.com/cgwiki/index.php?title=GeneralUtilties#Gifs
import distutils.spawn, subprocess
convert = distutils.spawn.find_executable('convert')
if convert:
convertCmd = 'convert -loop 0 -delay 4 {}{} -layers OptimizePlus -colorspace sRGB +map {}'
splitpath = hou.pwd().parm('picture').eval().split('.')
gifPath = '.'.join(splitpath[:-2])+'.gif'
wildcardPath = '.'.join(splitpath[:-2])+'.*.'+splitpath[-1]
convertCmd = convertCmd.format(wildcardPath,'[600x600]',gifPath)
convertCmd = convertCmd + '&& xdg-open '+gifPath
subprocess.Popen(convertCmd,shell=True)
else:
print('imgmagick convert to gif not found')
Selected node as python code
(linux/windows may vary)
import hou
import os
import subprocess
sel = hou.selectedNodes()
strCode = sel[0].asCode(1,1)
fp = hou.getenv('tmp')+"/houdini_node_output.txt"
with open(fp, "w") as text_file:
text_file.write(strCode)
#os.system('xdg-open \"%s\"' % fp)
os.startfile(fp)
#Hey look at me, I'm a big idiot and I wrote the script from scratch because I had forgotten I had already coded it. How well, here's another version
import hou, tempfile, os
new_file, filename = tempfile.mkstemp(suffix='.txt')
text = ''
for node in hou.selectedNodes():
text += node.asCode()
os.write(new_file, str.encode(text))
os.close(new_file)
os.startfile(filename)
gives:
Paste clipboard nodes to object merges
# 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
Toggle off/on separate AOVs Arnold ROPs
Just saving it in case I need it
for child in hou.node('/out').children():
#print child.type().name()
if 'ppArnold' in child.type().name():
#if child.name() == 'osacks_electric_brain_january_visual_cortex':
for p in child.parms():
if 'ar_aov_separate' in str(p) and 'file' not in str(p):
p.set(0)
print(child.name()+"."+str(p)+" "+str(p.eval()))
Set expresions linking light visible parameter to display flag
idk why the candidate light didn't work on my machine so i used this
for child in hou.node('/obj').children():
if 'light' in child.type().name():
child.parm('light_enable').setExpression('hou.pwd().isDisplayFlagSet()', language=hou.exprLanguage.Python)
Skip if frame is in list
Works if there's a parm called skip on the same (switch) node
#true only on frames specified. Dirty but works
if str(int(hou.frame())) in hou.pwd().parm('skip').eval().split(' '):
return 1
else:
return 0
and on a button if you want to easily add current frame to the list, make sure it's set to python
hou.pwd().parm('skip').set(hou.pwd().parm('skip').eval()+' '+str(int(hou.frame())))
Import neuron swc data
wip
# reads SWC and creates geo from it
# http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html
#
# 0 ¦ 1 ¦ 2 3 4 ¦ 5 ¦ 6
# sample number ¦ structure identifier ¦ x y z ¦ radius ¦ parent sample (connectivity)
import os
from itertools import *
node = hou.pwd()
geo = node.geometry()
# procedures
def isBadLine(line):
return line[0]=='#'
#def createPoint(geo,data):
curDir = hou.node('/obj/swc_to_poly/directory_to_read').parm('readin').eval()
print("processing: "+curDir)
#read swc files in dir
files = []
for name in os.listdir(curDir):
if name.endswith('.swc'):
files.append(os.path.join(curDir, name))
files.sort()
files = files[-1:]
for file in files:
print(" --- processing file: "+file)
with open(file) as f:
for line in dropwhile(isBadLine, f): # only read lines that don't have hashtag
data = line.lstrip().rstrip().split(" ")
data = map(float,data)
point = geo.createPoint() # create one point per line and fill attributes
point.setPosition(hou.Vector3(*data[2:5]))
point.setAttribValue('id', int(data[0]))
point.setAttribValue('structid', int(data[1]))
point.setAttribValue('radius', data[5])
point.setAttribValue('parent_id', int(data[6]))
#print file
Create nodes according to primitive groups
node = hou.pwd()
#custom undo stack
with hou.undos.group("Poly Reduce Groups"):
#uses parent, otherwise recursion happens with python sop node
node = node.inputs()[0]
geo = node.geometry()
groups = geo.primGroups()
count = 0
mainReduce = ''
merge = node.parent().createNode('merge')
for o in groups:
#keep a single prim group
blast = node.parent().createNode('blast')
blast.setInput(0,node)
blast.moveToGoodPosition()
blast.parm('negate').set(True)
blast.parm('group').set(o.name())
#use polyreduce with keep points on with an expression
reduce = node.parent().createNode('polyreduce')
reduce.setInput(0,blast)
reduce.moveToGoodPosition()
reduce.parm('originalpoints').set(True)
reduce.parm('referframe').set(True)
reduce.parm('framereference').setExpression('round((\$F+'+str(count)+')/ch("step"))*ch("step")')
#create the 'step by' channel on this node
parmTemplateGrp = reduce.parmTemplateGroup()
stepByParm = hou.FloatParmTemplate('step', 'StepBy', 1,default_value=([10]))
parmTemplateGrp.append(stepByParm)
reduce.setParmTemplateGroup(parmTemplateGrp)
if count == 0:
reduce.setColor(hou.Color([0.6,1,0.6]))
mainReduce = reduce
reduce.parm('percentage').set(10)
else:
params = ['percentage','optimizationbias','borderweight','attribweight','topologicalweight','step']
for element in params:
reduce.parm(element).set(mainReduce.parm(element))
merge.setInput(count,reduce,0)
count = count + 1
merge.moveToGoodPosition()
merge.setDisplayFlag(True)
mainReduce.setSelected(True, clear_all_selected=True)
Load a folder of objs and merge them
import hou
import glob
obj = hou.node("/obj")
s = obj.createNode('geo', 'loader', run_init_scripts=False)
m = s.createNode('merge')
path = '/users/me/Downloads/body/*.obj'
i = -1
for file in glob.glob(path):
i += 1
f = s.createNode('file')
f.parm('file').set(file)
f.moveToGoodPosition()
m.setInput(i,f,0)
m.moveToGoodPosition()
Best fitting bbox from eigenvectors
thanks to petz @ https://forums.odforce.net/topic/14879-bounding-box/
# This code is called when instances of this SOP cook.
geo = hou.pwd().geometry()
prims = geo.prims()
points = geo.points()
# parms
output = 0
maxIter = 10
threshold = 0.001
centroid = sum([point.position() for point in points], hou.Vector3()) * (1.0 / len(points))
# build covariance matrix
val11 = 0; val12 = 0; val13 = 0
val21 = 0; val22 = 0; val23 = 0
val31 = 0; val32 = 0; val33 = 0
for point in points:
pos = point.position()
val11 += (pos[0] - centroid[0]) * (pos[0] - centroid[0])
val12 += (pos[0] - centroid[0]) * (pos[1] - centroid[1])
val13 += (pos[0] - centroid[0]) * (pos[2] - centroid[2])
val21 += (pos[1] - centroid[1]) * (pos[0] - centroid[0])
val22 += (pos[1] - centroid[1]) * (pos[1] - centroid[1])
val23 += (pos[1] - centroid[1]) * (pos[2] - centroid[2])
val31 += (pos[2] - centroid[2]) * (pos[0] - centroid[0])
val32 += (pos[2] - centroid[2]) * (pos[1] - centroid[1])
val33 += (pos[2] - centroid[2]) * (pos[2] - centroid[2])
mat = hou.Matrix3(((val11, val12, val13), (val21, val22, val23), (val31, val32, val33)))
mat = hou.Matrix4(mat.inverted())
# search for eigenvector with lowest eigenvalue
vec1 = hou.Vector3(1.0, 1.0, 1.0)
vecTemp = vec1 * mat
vec2 = vecTemp * (1.0 / vecTemp.length())
i = 0
while not vec1.isAlmostEqual(vec2) and i < 100:
vec1 = vec2
vecTemp = vec1 * mat
vec2 = vecTemp * (1.0 / vecTemp.length())
i += 1
minAxis = vec2.normalized()
# build matrix to transform geometry to initial position and orientation
up = hou.Vector3(0.0, 1.0, 0.0)
matUp = hou.hmath.buildTranslate(-centroid)
matUp *= minAxis.matrixToRotateTo(up)
geo.transform(matUp)
# initialize attributes
initRot = 9
angleSum = 0
ratio = 1
i = 0
# adaptively rotate geometry until best orientation for smallest bounding box is found
while i < maxIter and ratio > threshold:
angle = 0
angleHold = 0
angleRotBest = 0
bboxSize = geo.boundingBox().sizevec()
vol = bboxSize[0] * bboxSize[1] * bboxSize[2]
volMin = vol
volMinHold = vol
for n in range(10):
angle += initRot
matRot = hou.hmath.buildRotateAboutAxis(up, initRot)
geo.transform(matRot)
bboxSize = geo.boundingBox().sizevec()
vol = bboxSize[0] * bboxSize[1] * bboxSize[2]
if vol < volMin:
volMin = vol
angleRotBest = angle
# check ratio of vol and vol of previous bounding box
ratio = abs(volMinHold - volMin)
volMinHold = volMin
if ratio == 0.0 and i == 0:
ratio = 1.0
angleRot = angle - angleRotBest + initRot
angleSum += angleRotBest - initRot
matRot = hou.hmath.buildRotateAboutAxis(up, -angleRot)
geo.transform(matRot)
initRot *= 0.2
i += 1
# bounding box
bbox = geo.boundingBox()
size = bbox.sizevec()
vol = size[0] * size[1] * size[2]
# build matrix to transform geometry back to oiginal position and orientation
mat = hou.hmath.buildRotateAboutAxis(up, -angleSum) * matUp.inverted()
matAttrib = geo.findGlobalAttrib("mat")
if not matAttrib:
matAttrib = geo.addAttrib(hou.attribType.Global, "mat", ( 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0))
geo.setGlobalAttribValue(matAttrib, mat.asTuple())
#create objects from voronoi
from math import cos, sin, pi
obj = hou.selectedNodes()[0]
node = obj.displayNode()
prims = node.geometry().prims()
lastprim = prims[ len(prims)-1 ]
lastprimName = lastprim.attribValue("name")
lastprimName = lastprimName.partition("piece")
lastprimName = int( lastprimName[ len(lastprimName)-1 ] )
print lastprimName
subnet = hou.node("obj").createNode("subnet")
for i in range(0,lastprimName+1):
geo = subnet.createNode("geo")
geo.setPosition([cos( (1.0*i/lastprimName+1 ) * 2.0 * pi )*4.0, sin( (1.0*i/lastprimName+1 ) * 2.0 * pi )*4.0 ]) # lol
objectMerge = geo.createNode("object_merge")
delete = objectMerge.createOutputNode("delete")
objectMerge.parm("objpath1").set(node.path())
delete.parm("negate").set(1)
delete.parm("group").set("@name=piece"+str(i))
delete.setDisplayFlag(1)
Get functions in an OTL
#in button callback
hou.pwd().hdaModule().hello()
#in script window
def hello:
....
Preroll output to stdout (good for HQUEUE), put in a python node that gets fetched each step (source?). Also add f1 f2 parms to the python node with $RFSTART and $RFEND
import sys
start = hou.node(".").parm("f1").eval()
end = hou.node(".").parm("f2").eval()
current = hou.frame()
progress = str(int(hou.hmath.fit(current, start, end, 0.0, 100.0))).zfill(3)
print("\rFrame {0} done".format(current))
print("\rALF_PROGRESS {0}%".format(progress))
sys.stdout.flush()