The sidefx cookbook: https://www.sidefx.com/docs/houdini/hom/cb/pythonsop.html
Remember a python sop is like a wrangle, only obtuse. Generally speaking just use vex. A python sop is handy for esoteric things like reading in odd file formats and converting directly to geometry, or processing odd string and time formats into something more vex friendly for later on.
There's no implicit parallel processing here, so you usually have to setup a loop of some kind to read all the points or write all the points.
# Add a point point = geo.createPoint() # Add an attribute. This is like a 'attribute create sop', so only do this once, not while looping over geometry! # the default value sets the type, so 1.0 is a float, but 1 is an int. geo.addAttrib(hou.attribType.Point, "id", 0.0) # Get a detail attribute start = geo.attribValue('tmin') # Get @ptnum, @numpt for point in geo.points(): print point.number() print len(geo.iterPoints()) # get a point attribute for point in geo.points(): print point.attribValue("Cd") # Set an attribute for point in geo.points(): point.setAttribValue("Cd", [1,0.5,0])
Here's an example of why you might need this. You import data that has time values stored as a string like '2019-12-08T19:04:06.900', and you want to convert that to regular seconds. That's gonna be some awkward string manipulation in vex, but its something python can handle pretty easily with its datetime module. I've used an attrib promote to get the min and max values stored as detail attributes.
from datetime import datetime as dt fmt = '%Y-%m-%dT%H:%M:%S.%f' geo.addAttrib(hou.attribType.Point, "s", 0.0) start = geo.attribValue('tmin') start = dt.strptime(start, fmt) end = geo.attribValue('tmax') end = dt.strptime(end, fmt) duration = (end-start).total_seconds() for point in geo.points(): t = point.attribValue("time") t = dt.strptime(t, fmt) seconds = (t - start).total_seconds() seconds/=duration point.setAttribValue("s", seconds)
Make a general python input window
Download scene: File:python_code_window.hip
The python console in Houdini is a live command line, which is great for some things, but doesn't allow you to enter multi-line code easily. Maya's script editor allows this, I wanted a similar thing in Houdini.
The python SOP looks like its the answer, but don't be fooled. Its the python equivalent of a point wrangle, its designed to process geometry, not do general node or UI fiddling (which is really what you'd want python for).
With the help of Luke Gravett, here's a way to make something akin to maya's python script editor. This is version 2 of such a thing, it's just a null with a text editor and a button to execute the code. An earlier version of this used an OTL/HDA, which was more fiddly than necessary.
- Make a null sop, name it 'my_python_code'
- parameter pane, gear menu, 'Edit Parameter Interface...':
- add a string parameter, label 'Code', enable 'multi-line string', language 'Python', set name to 'Code'
- add a button, label 'Run', change the callback method to python ('little dropdown at the end of the line), python callback is
- Hit Accept
Now you can type in code, click the button, make magic.
Get selected nodes
Get all upstream nodes
n = hou.node('/obj/grid1/null1') n.inputAncestors()
To filter that down to all ancestor nodes that are alembic nodes, for example:
[ x for x in n.inputAncestors() if 'alembic' in x.type().name() ]
Get all nodes of type
A trick I remember doing a lot in Maya, can do in Houdini too. Nodetype is specific thing in houdini's python implementation, you can then query all instances of it.
ctype = hou.nodeType('Sop/copytopoints::2.0') print ctype.instances()
This looks pretty straightforward according to the docs; specify 'Category/NodeName', which you can get by querying the type of a node:
mergetype = hou.nodeType('Sop/merge') print mergetype.instances()
But try it on this, and it'll likely return no nodes:
ctype = hou.nodeType('Sop/copytopoints') print ctype.instances()
Read the docs a bit closer, they say the node type is displayed at the top of the type properties, which reveals the issue here:
There's a version suffix in there, the previous code is defaulting to finding the old copytopoints, no good. Lets fix that:
ctype = hou.nodeType('Sop/copytopoints::2.0') print ctype.instances()
Better. What about looking up all 3delight texture nodes? I know its a vop node, so lets call it like we did the other nodes:
textype = hou.nodeType('Vop/dlTexture') print textype.instances()
Hmm, doesn't return anything. If I drag an existing dlTexture node into the python editor and query its type, I get this:
>>> hou.node('/mat/dlTexture1').type() <hou.NodeType for Vop 3Delight::dlTexture>
Ok, lets use that full name then with the namespace:
textype = hou.nodeType('Vop/3Delight::dlTexture') print textype.instances()
Still nothing. Weird. Turns out it doesn't seem to like the namespace combined with the 'Vop/' prefix, instead you need to use the full format that asks for a specific object of nodetypecategories. Annoying, but there it is:
textype = hou.nodeType(hou.vopNodeTypeCategory(), '3Delight::dlTexture') print textype.instances()
Get point attributes from a node
From the node get its geometry, then its point attributes, then the short names of those attributes.
[ x.name() for x in hou.node('/obj/mygeo/mysop').geometry().pointAttribs() ]
To go one further and make a nice list to feed to an attribute delete node, use a join() with a single space, prepend with ^'s, and stick an asterisk on the front:
print '*',' '.join([ '^'+x.name() for x in hou.node('/obj/geo1/mysop').geometry().pointAttribs()])
* ^id ^Cd ^Alpha ^center ^orient ^P ^uniqueId ^materialId
Drag most things into the python window
Do this, and what you drag will be converted into the python text equivalent. This works for nodes, parameters, shelf buttons, most parts of the UI.
Write out mmb error text to file
Get parent vs get input
Parent in houdini means the container; ie if you have a subnet1, and inside is box1, if you ask box1 for its parent, its subnet1.
n = hou.node('/obj/subnet1/null2') n.parent() # <hou.SopNode of type subnet at /obj/subnet1>
If you have box1 connected to mountain1, and ask mountain1 for its inputs, you'll get box1 (as a list).
n = hou.node('/obj/mountain1') n.inputs() # (<hou.SopNode of type box at /obj/box1>,)
get attrib from point listed in group field of sop
So many layers of indirection!
# get list of nodes, here i've dragged a bunch of nodes into a node list parm i added to a python script node I created above nodes = kwargs['node'].parent().parm('nodes').eval() # for each node in the list: for n in nodes.split(' '): n = hou.node(n) # read the group field, here they're all in a format like '@myattrib=57-58' group = n.parm('group').eval() # use the handy globPoints function to convert that group syntax to a list of points for p in n.geometry().globPoints(group): # get the attrib we're after! print p.attribValue('awesomeattrib')
Create a cop file node for every subdir of a directory
Assumes a lot; that the image sequence within each folder is of the same name, and the parent folder only contains subfolders with images, no error checking is done, may contain traces of peanut etc...
import os dir = '/path/to/parent/folder' seq = 'render.$F4.exr' subdirs = [x for x in os.listdir(dir) if os.path.isdir(os.path.join(dir,x))] for d in subdirs: imgpath = os.path.join(dir,d)+'/'+seq filenode = hou.node('/img').createNode('file') filenode.parm('filename1').set(imgpath)
press 'save to disk' on a rop
Most rops (both for rendering and for saving geometry) have the main 'save to disk' button named 'execute', and to press a button you call pressbutton. Hence you can do this:
n = hou.node('/obj/obj1/my_fbx_rop/rop_fbx2') n.parm("execute").pressButton()
python expression for image path
Contrived example, more for the workflow than anything. Say you want to use python to generate the image path for a mantra rop.
- Alt-lmb click on the parm title to set an expression
- r.click, Expression -> Change language to python. The field will go purple to confirm this.
- R.click, Expression -> Edit expression. This brings up the multi line editor.
- Write your expression, the final output needs to be a return statement
- lmb on the parm title to see the evaluated expression.
Here's an expression to set the path from $HIP, but then go 1 folder up and across, and use pythons handy file path manipulations to see the full path rather than ../ stuff:
import os hip = hou.expandString('$HIP') dir = os.path.join(hip,'../elsewhere/renders') dir = os.path.abspath(dir) file = 'render.$F4.exr' return os.path.join(dir,file)