Skip to content

Python

Introduction

People look at python in Houdini for 3 broad reasons:

  1. They used python in Maya, and assume it makes more sense to jump into python in Houdini rather than Vex or to learn more advanced sops.
  2. They need to do pipeline or UI customisation
  3. They need to import or export esoteric file formats, do fiddly string formatting

2 and 3 are valid reasons. 1 is NOT a valid reason. If that is you, turn right around and go to the Houdini page or the JoyOfVex page, but don't jump into Houdini Python just yet.

I rant at length about this in various places, but the core idea is that you use python in maya to get around a lot of its shortcomings; workflows are often brittle and need to destroyed and recreated on demand, assets are 'baked' into the scene and need to be reimported, stuff can't easily be changed after the fact.

Houdini is designed to work with full construction history enabled and live at all times, or to be reading files from disk 'live'. Because of this, most of the reasons you'd use python in Maya aren't required in Houdini. If you jump straight into houdini python, and end up driving Houdini like its Maya, you end up making slow, over engineered, code heavy setups. Don't do this! Only jump into Houdini Python when you feel you understand proceduralism, the Houdini way of things.

For the rest of you, 2 and 3 broadly filter down into these categories:

  1. Run once events:
    1. Python console
    2. Shelf
    3. A button, HDA
  2. 'Reactive' code in a network flow:
    1. A python sop
    2. A tops node
    3. Rops callbacks

The first category is the more common use case. You can open the python console and use it pretty interactively, query all sorts of things about houdini, your hip file, the UI. Once you start creating reusable little snippets, the shelf is the easiest way to store those things. When you get into making HDAs python can live there too, and final python mastery comes from extending the pythonlibs paths, having python on disk, but that's more for pipeline td's.

For the second, a good place to start is the python sop.

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.

python
# 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])
# 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.

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

General Houdini Python

The following are more tips and tricks that you'd run in the python console, in shelf buttons, in hda python code.

A little note on style; I know one kickflip trick on my python skateboard, and that trick is a list comprehension. I think I write about it elsewhere on the wiki, but its a way to create a new array from an array, modifying the elements of the array, and filtering the elements, all in one smug semi-indecipherable format. Google 'python list comprehension' if you haven't come across it before, its handy.

Get selected nodes

python
hou.selectedNodes()
hou.selectedNodes()

Get all upstream nodes

python
n = hou.node('/obj/grid1/null1')
n.inputAncestors()
n = hou.node('/obj/grid1/null1')
n.inputAncestors()

To filter that down to all ancestor nodes that are alembic nodes, for example:

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() ]

Bypass a node

Look through all the hou.node definitions, you won't find it. It's hiding in hou.sopnode rather than hou.node, I guess because a high level node like in /obj can't be bypassed, while lower level sop/dop/cop nodes can. Anyway:

python
n = hou.node('/obj/geo1/font1')
n.bypass(1)   # node is now bypassed
n.bypass(0)   # node is now active
n = hou.node('/obj/geo1/font1')
n.bypass(1)   # node is now bypassed
n.bypass(0)   # node is now active

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:

python
hou.nodeType('Sop/null').instances()
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:

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

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

python
hou.nodeType('Sop/copytopoints::2.0').instances()
hou.nodeType('Sop/copytopoints::2.0').instances()

Change all nodes of type to another type

Granted, not a common use case, but handy to know.

Redshift material networks usually have a final node of type redshift_material. For redshift to work in Lops, materials need to end in a usd_redshift_material node, which looks identical apart from the usd prefix. You can manually change a node type by right clicking on it and choosing r.click actions->change type but what if you need to do this on lots of nodes? This list comprehension shows how. The important command here is node.changeNodetype():

python
[n.changeNodeType('redshift_usd_material') for n in hou.nodeType('Vop/redshift_material').instances()]
[n.changeNodeType('redshift_usd_material') for n in hou.nodeType('Vop/redshift_material').instances()]

Get point attributes from a node

From the node get its geometry, then its point attributes, then the short names of those attributes.

python
[ x.name() for x in hou.node('/obj/mygeo/mysop').geometry().pointAttribs() ]
[ 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:

python
print '*',' '.join([ '^'+x.name() for x in hou.node('/obj/geo1/mysop').geometry().pointAttribs()])
print '*',' '.join([ '^'+x.name() for x in hou.node('/obj/geo1/mysop').geometry().pointAttribs()])

will return

vex
* ^id ^Cd ^Alpha ^center ^orient ^P ^uniqueId ^materialId
* ^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!

python
open('/tmp/error.txt','w').write(hou.node('/path/to/node').errors())
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.

python
n = hou.node('/obj/subnet1/null2')
n.parent()
# <hou.SopNode of type subnet at /obj/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).

python
n = hou.node('/obj/mountain1')
n.inputs()
# (<hou.SopNode of type box at /obj/box1>,)
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!

python
# 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')
# 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...

python
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)
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:

python
n = hou.node('/obj/obj1/my_fbx_rop/rop_fbx2')
n.parm("execute").pressButton()
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:

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

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()
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:

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

Create sticky notes with random colours

Put this into a shelf, assign a hotkey, eg alt-s:

python
import toolutils
import numpy
scene_view = toolutils.sceneViewer()
context = scene_view.pwd()
net_editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

note = context.createStickyNote()
note.setColor(hou.Color(numpy.random.rand(3)))
note.setPosition(net_editor.cursorPosition())
import toolutils
import numpy
scene_view = toolutils.sceneViewer()
context = scene_view.pwd()
net_editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)

note = context.createStickyNote()
note.setColor(hou.Color(numpy.random.rand(3)))
note.setPosition(net_editor.cursorPosition())

Create command line for selected tops node

The help gives some examples of how to run tops from a command line, it'd be handy to create that command interactively. Drop this on a shelf button, select a top node, run the button, it creates a command line you can run in a shell:

python
import hou

prefix = 'hython $HHP/pdgjob/topcook.py --hip '
hipfile = hou.hipFile.path()
topnode = hou.selectedNodes()[0].path()
returnstring = 'prefix+hipfile+' --toppath '+topnode
hou.ui.displayMessage('command:',details_expanded=True,details=returnstring)
import hou

prefix = 'hython $HHP/pdgjob/topcook.py --hip '
hipfile = hou.hipFile.path()
topnode = hou.selectedNodes()[0].path()
returnstring = 'prefix+hipfile+' --toppath '+topnode
hou.ui.displayMessage('command:',details_expanded=True,details=returnstring)

Get absolute node path from relative parameter path

Handy tip from Alessandro Pepe.

You have a node with the path "/obj/geo/mynode") that has a parameter on it with the operator path "../ropnet1/OUT"

Hou.node() is pretty flexible here. Give hou.node a path, then immediately call node() again with a relative path, and it will return the full absolute path:

python
hou.node("/obj/geo/mynode").node("../ropnet1/OUT").   # returns /obj/ropnet1/OUT
hou.node("/obj/geo/mynode").node("../ropnet1/OUT").   # returns /obj/ropnet1/OUT

Thanks Alessandro!

Read a csv as points with attributes

The table sop can do this, but expects you to tag the csv columns into attribute names BY HAND. What am I, a farmer?

No, lets do some very unsafe, full of assumptions work here. Just dump the csv into geometry, one point per row, read the first row as attribute names, and jam it all into points. Clean up can happen later.

Oh ok, a tiny bit of tidying. Attribute names can only be alphanumeric and underscores, this will strip column names back to be only alphanumeric first before pushing them into attribute names.

python
import csv
node = hou.pwd()
geo = node.geometry()

with open('$HIP/data.csv') as csvfile:
    csv_points = list(csv.reader(csvfile))

    headers = csv_points[0]  # first row is headers
    headers = [ ''.join(x for x in h if x.isalpha()) for h in headers] # tidy up names
    for h in headers:
        geo.addAttrib(hou.attribType.Point, h, 0.0)  # assume data is float

    csv_points.pop(0)   # remove the header row

    # iterate over the rest of the rows, creating points with data
    for p in csv_points:
        pt = geo.createPoint()
        for i,h in enumerate(headers):
            pt.setAttribValue(h,float(p[i]))
import csv
node = hou.pwd()
geo = node.geometry()

with open('$HIP/data.csv') as csvfile:
    csv_points = list(csv.reader(csvfile))

    headers = csv_points[0]  # first row is headers
    headers = [ ''.join(x for x in h if x.isalpha()) for h in headers] # tidy up names
    for h in headers:
        geo.addAttrib(hou.attribType.Point, h, 0.0)  # assume data is float

    csv_points.pop(0)   # remove the header row

    # iterate over the rest of the rows, creating points with data
    for p in csv_points:
        pt = geo.createPoint()
        for i,h in enumerate(headers):
            pt.setAttribValue(h,float(p[i]))

Set keyframes with python from right click menu

I mention parts of this tip over on the HoudiniUserInterfaceTips page, but my instinct was to look for it here on the python page too.

To set keyframes using python is a bit roundabout. You create a keyframe object, set its values, then pass that to parameter.setKeyframe(). Many thanks to Ryoji Fujita for writing up clues to this on his site.

So lets say you're doing this the dumb way from the python console:

python
parm = hou.node('/obj/geo1/characterblendshapechannels1').parm('blend2')
mykey = hou.Keyframe()
mykey.setValue(0.5)
mykey.setFrame(15)
parm.setKeyframe(mykey)
parm = hou.node('/obj/geo1/characterblendshapechannels1').parm('blend2')
mykey = hou.Keyframe()
mykey.setValue(0.5)
mykey.setFrame(15)
parm.setKeyframe(mykey)

Hooray! But now you want to go next level, and incorporate this with some right click magic. Matt Ebb shared a tip a while ago about customising the context menu on his blog, I had to go relearn it.

Houdini's right click menu is controlled by PARMmenu.xml. You can have an identically named file in your prefs folder (as in literally where you find a bunch of blah.pref files), and if you're careful, you can append your own custom commands.

Debugging is a pain, a handy command to run in a texport is 'menurefresh', which will rescan all the PARMmenu.xml files it can find, and rebuild the menus.

In my case I'd been keyframing blendshapes where I wanted them to just be active for 1 frame. Going a frame back and setting the value to 0, then a frame forward and setting the value to 0 was getting annoying, so I made a little r.click thing. Now I set the keyframe, r.click, choose "Pin keyframe to zero at +- 1 frame", and the work is done.

My PARMmenu.xml looks like this. You can see the python block in the middle is basically the same as before, except I dynamically get the parm via kwargs, I get the frame from hou.frame(), and I just go plus/minus 1 on other side of the current frame.

python
<menuDocument>
    <!-- menuDocument can only contain 1 menu element, whose id is
         implicitly "root_menu"
      -->
    <menu>
    <scriptItem id="bs_pin_keyframes">
        <label>Pin keyframe to zero at +- 1 frame</label>
        <parent>root_menu</parent>
        <insertAfter />
        <scriptCode><![CDATA[
parm = kwargs['parms'][0]

mykey = hou.Keyframe()
mykey.setValue(0)
mykey.setFrame(hou.frame()-1)
parm.setKeyframe(mykey)
mykey.setFrame(hou.frame()+1)
parm.setKeyframe(mykey)
            ]]>
        </scriptCode>
    </scriptItem>
    </menu>
</menuDocument>
<menuDocument>
    <!-- menuDocument can only contain 1 menu element, whose id is
         implicitly "root_menu"
      -->
    <menu>
    <scriptItem id="bs_pin_keyframes">
        <label>Pin keyframe to zero at +- 1 frame</label>
        <parent>root_menu</parent>
        <insertAfter />
        <scriptCode><![CDATA[
parm = kwargs['parms'][0]

mykey = hou.Keyframe()
mykey.setValue(0)
mykey.setFrame(hou.frame()-1)
parm.setKeyframe(mykey)
mykey.setFrame(hou.frame()+1)
parm.setKeyframe(mykey)
            ]]>
        </scriptCode>
    </scriptItem>
    </menu>
</menuDocument>

Also every time I write PARMmenu I think of a classic aussie pub parmi, and I get hungry. Time to tackle Mount Parmarama again...

Font sop and python

Download hip: font_per_frame.hip

Edit: Michael Frederickson offered a simpler method, see further down...

The font sop is pretty strange, especially in how it populates its list of fonts. Henry Foster found it relies on a opmenu script to generate the fonts list. A few times I've wanted to browse the fonts, but the interface is annoyingly clicky. When someone else asked a similar question, I thought I'd take a closer look.

I figured if I could iterate through each font per frame, that'd be a reasonable compromise. The only way I could think to do this was an ugly hodgepodge of python, hscript, vex. I hope there's a better way...

The hscript call is opmenu -l -a /obj/geo1/font1 file. You can run an hscript command from python with hou.hscript():

python
fonts = hou.hscript("opmenu -l -a /obj/geo1/font1 file")
fonts = hou.hscript("opmenu -l -a /obj/geo1/font1 file")

The return value is a big ugly string with newline symbols and double quotes, quite the mess. Enter one of my standard awful list comprehensions to try and split it up on newlines, remove the double quotes, strip any errant spaces:

python
fonts = [x.strip().strip('"') for x in fonts[0].split('\n')]
fonts = [x.strip().strip('"') for x in fonts[0].split('\n')]

This will have duplicate entries, so can cast to a dictionary and back to clean that up:

python
fonts = list(dict.fromkeys(fonts))
fonts = list(dict.fromkeys(fonts))

And finally we'll store that as a string array attribute at the detail level. The full python sop looks like this:

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

geo.addAttrib(hou.attribType.Global, "fonts", [''])

fonts = hou.hscript("opmenu -l -a /obj/geo1/font1 file")
fonts = [x.strip().strip('"') for x in fonts[0].split('\n')]
fonts = list(dict.fromkeys(fonts))

geo.setGlobalAttribValue('fonts',fonts)
node = hou.pwd()
geo = node.geometry()

geo.addAttrib(hou.attribType.Global, "fonts", [''])

fonts = hou.hscript("opmenu -l -a /obj/geo1/font1 file")
fonts = [x.strip().strip('"') for x in fonts[0].split('\n')]
fonts = list(dict.fromkeys(fonts))

geo.setGlobalAttribValue('fonts',fonts)

Now to push that to a font sop. I use a wrangle in detail mode so I can extract an element from that array per frame, and store it as another string attribute:

vex
string fonts[] = detail(0,'fonts');
s@font = fonts[int(@Frame)];
string fonts[] = detail(0,'fonts');
s@font = fonts[int(@Frame)];

And then I link that to a font sop via a spare attribute, and use an expression on the font parm:

vex
`points(-1,0,'font')`
`points(-1,0,'font')`

Font sop and python 2

As I'd hoped, I put the call out to the angels for an easier method, the angels sent me Michael Frederickson.

  1. Create a vanilla font sop
  2. Create an expression on the font parm (select it, alt-e/cmd-e to bring up the editor)
  3. Clear the field, set the expression language to python
  4. Set the expression to this:

hou.pwd().parm("file").menuItems()[int(hou.frame())]

</syntaxhighlight>
</syntaxhighlight>
  1. Confirm

Way more straightforward than my silly method.