Difference between revisions of "HoudiniPython"

From cgwiki
(14 intermediate revisions by the same user not shown)
Line 8: Line 8:
  
 
<source lang="python">
 
<source lang="python">
# add an attribute
+
# Add a point
# don't do this in a geo loop!
+
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.
 
# 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)
 
geo.addAttrib(hou.attribType.Point, "id", 0.0)
  
# get a detail attribute
+
# Get a detail attribute
 
start = geo.attribValue('tmin')
 
start = geo.attribValue('tmin')
  
# get @ptnum, @numpt
+
# Get @ptnum, @numpt
 
for point in geo.points():
 
for point in geo.points():
 
   print point.number()
 
   print point.number()
Line 25: Line 27:
 
   print point.attribValue("Cd")
 
   print point.attribValue("Cd")
  
# set an attribute
+
# Set an attribute
 
for point in geo.points():
 
for point in geo.points():
 
   point.setAttribValue("Cd", [1,0.5,0])
 
   point.setAttribValue("Cd", [1,0.5,0])
Line 53: Line 55:
 
   point.setAttribValue("s", seconds)
 
   point.setAttribValue("s", seconds)
 
</source>
 
</source>
 
=== Load $HIP/hda on hip load via hou.session ===
 
 
Download hip: [[:File:hda_from_hip.zip]]
 
 
Hacky. ''But I like hacky.''
 
 
My current job has a couple of interesting contraints. Hip files need to be run In The Cloud, and ideally run as standalone as possible. That means minimal environment variables, minimal fixed locations for libraries or assets, just the hip and what the hip might need in subfolders.
 
 
We also need multiple people working on a single file at once, using github to manage our files. So that means having a single monolithic hip is an issue, because really only one person can work on it at once (we've tried using the diff tools, but it falls apart on big files, especially if you have things like stash nodes and frozen nodes).
 
 
Splitting work up into HDA's would be ideal, but the assumtption is you load all your hda's at houdini startup, and use environment variables to say where those HDAs will be found, which breaks our first requirement.
 
 
Enter this trick.
 
 
The amazing Chris Gardner shared a python script he wrote to hot load hda's. Give it a folder, it'll scan the folder and load any hda's it finds. That script lives here:
 
 
https://gist.github.com/chris-gardner/c9daf34a668c5dddda94b9f6276d8cb8
 
 
But how can we call that from a hip if we're trying to no load any external scripts or dependencies? Hou.session, that's how!
 
 
Hou.session is a python module that is saved as part of a hip, and is loaded (and therefore executed) on hip load. It can be accessed via Window -> Python Source Editor.
 
 
As such I copied Chris's script into hou.session, and I run
 
 
loadHdaLibrary(hou.getenv('HIP')+'/hda')
 
 
At the bottom of the script. Now I can make a hda folder next to the hip, save hda's in there, and they'll be loaded when the hip is loaded.
 
 
Obviously you'd want to move stuff to a more solid pipeline infrastructure over time, but this'll do in the short term.
 
  
 
=== Make a general python input window ===
 
=== Make a general python input window ===
Line 91: Line 63:
  
 
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 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.
 +
 +
If it's python code just for you, making a shelf button is probably the easiest way. Make a shelf, r.click on it, new tool, python away. But what if you want your python code to live in a hip?
  
 
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).
 
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).
Line 124: Line 98:
 
<source lang="python">
 
<source lang="python">
 
[ x for x in n.inputAncestors() if 'alembic' in x.type().name() ]
 
[ x for x in n.inputAncestors() if 'alembic' in x.type().name() ]
 +
</source>
 +
 +
=== 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.
 +
 +
Read this, https://www.sidefx.com/docs/houdini/hom/hou/nodeType_.html
 +
 +
Do this:
 +
 +
<source lang="python">
 +
hou.nodeType('Sop/null').instances()
 +
</source>
 +
 +
If you have a namespace, follow the style outlined here: https://www.sidefx.com/docs/houdini/assets/namespaces.html#the-parts-of-an-asset-name.  Ie, namespace::Category/nodename:
 +
 +
<source lang="python">
 +
hou.nodeType('3Delight::Vop/dlTexture').instances()
 +
</source>
 +
 +
If nodes have versions (like polyextrude 2.0, copytopoints2.0), you need to include that:
 +
 +
<source lang="python">
 +
hou.nodeType('Sop/copytopoints::2.0').instances()
 
</source>
 
</source>
  
Line 200: Line 198:
 
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...
 
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...
  
<source lang="javascript" >
+
<source lang="python" >
 
import os
 
import os
  
Line 218: Line 216:
 
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:
 
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:
  
<source lang="javascript" >
+
<source lang="python" >
 
n = hou.node('/obj/obj1/my_fbx_rop/rop_fbx2')
 
n = hou.node('/obj/obj1/my_fbx_rop/rop_fbx2')
 
n.parm("execute").pressButton()
 
n.parm("execute").pressButton()
Line 237: Line 235:
 
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:
 
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:
  
<source lang="javascript" >
+
<source lang="python" >
 
import os
 
import os
 
hip =  hou.expandString('$HIP')
 
hip =  hou.expandString('$HIP')
Line 244: Line 242:
 
file = 'render.$F4.exr'
 
file = 'render.$F4.exr'
 
return os.path.join(dir,file)
 
return os.path.join(dir,file)
 +
</source>
 +
 +
=== Write some detail attribs out to a json file ===
 +
 +
Thanks to Oliver Hotz (the guy behind the amazing [https://origamidigital.com/cart/index.php?route=product/product&path=59_63&product_id=64 OdTools] ) for the pointers here.
 +
 +
<source lang="python">
 +
import json
 +
 +
node = hou.pwd()
 +
geo = node.geometry()
 +
 +
minP = geo.attribValue('minP')
 +
maxP = geo.attribValue('maxP')
 +
res =  geo.attribValue('res')
 +
jsondata =  json.dumps({'minP':minP, 'maxP':maxP, 'res':res} ,sort_keys=True, indent=4)
 +
 +
f = open("$HIP/export/`@filename`/stuff.json", "w")
 +
f.write(jsondata)
 +
f.close()
 +
</source>
 +
 +
and to read that back in:
 +
 +
<source lang="python">
 +
import json
 +
 +
node = hou.pwd()
 +
geo = node.geometry()
 +
 +
with open("$HIP/export/`@filename`/stuff.json") as f:
 +
    jsondata = f.read()
 +
 +
results = json.loads(jsondata)
 +
 +
for k,v in results.items():
 +
    geo.addAttrib(hou.attribType.Global, k, v)
 
</source>
 
</source>

Revision as of 06:04, 20 May 2021

Python sop

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

Text editor demo.gif

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.

If it's python code just for you, making a shelf button is probably the easiest way. Make a shelf, r.click on it, new tool, python away. But what if you want your python code to live in a hip?

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.

  1. Make a null sop, name it 'my_python_code'
  2. parameter pane, gear menu, 'Edit Parameter Interface...':
  3. add a string parameter, label 'Code', enable 'multi-line string', language 'Python', set name to 'Code'
  4. add a button, label 'Run', change the callback method to python ('little dropdown at the end of the line), python callback is
exec(kwargs['node'].parm('Code').eval())
  1. Hit Accept

Now you can type in code, click the button, make magic.

Get selected nodes

hou.selectedNodes()

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.

Read this, https://www.sidefx.com/docs/houdini/hom/hou/nodeType_.html

Do this:

hou.nodeType('Sop/null').instances()

If you have a namespace, follow the style outlined here: https://www.sidefx.com/docs/houdini/assets/namespaces.html#the-parts-of-an-asset-name. Ie, namespace::Category/nodename:

hou.nodeType('3Delight::Vop/dlTexture').instances()

If nodes have versions (like polyextrude 2.0, copytopoints2.0), you need to include that:

hou.nodeType('Sop/copytopoints::2.0').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()])


will return

* ^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

Handy!

open('/tmp/error.txt','w').write(hou.node('/path/to/node').errors())

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.

  1. Alt-lmb click on the parm title to set an expression
  2. r.click, Expression -> Change language to python. The field will go purple to confirm this.
  3. R.click, Expression -> Edit expression. This brings up the multi line editor.
  4. Write your expression, the final output needs to be a return statement
  5. Apply/Accept
  6. 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)

Write some detail attribs out to a json file

Thanks to Oliver Hotz (the guy behind the amazing OdTools ) for the pointers here.

import json

node = hou.pwd()
geo = node.geometry()

minP = geo.attribValue('minP')
maxP = geo.attribValue('maxP')
res =  geo.attribValue('res')
jsondata =  json.dumps({'minP':minP, 'maxP':maxP, 'res':res} ,sort_keys=True, indent=4)

f = open("$HIP/export/`@filename`/stuff.json", "w")
f.write(jsondata)
f.close()

and to read that back in:

import json

node = hou.pwd()
geo = node.geometry()

with open("$HIP/export/`@filename`/stuff.json") as f:
    jsondata = f.read()

results = json.loads(jsondata)

for k,v in results.items():
    geo.addAttrib(hou.attribType.Global, k, v)