arrow_forward_ios

HouPy Wiki

Support

This might seem a bit niche but I discovered this while helping someone with their HDA and thought it might be interesting to look at.

Say you have this sm How would you bake that transform so you just have a camera with a bunch of keyframes instead ?

My scene, as shown earlier, is simple : a camera ('/obj/cam1') and a null ('/obj/null1').

The rotation along the y axis of the null is animated with an expression so the camera orbits around the null, very basic stuff.

The final code is fairly awkward to try and run in a shell, so I prototyped in the source editor instead.

First step, grab the original camera, copy it, then offset it along the x axis in the network so it isn't sitting right on top of our original camera, then disconnect it from the null. I've hardcoded the original camera here but depending on what you want to do you might want to use hou.selectedNodes() or something else.

import hou

origcam = hou.node('/obj/cam1')

cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0]
cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0)))
cam2.setInput(0,None)

In Houdini, object nodes have local and world transforms that you can access with Python, refer to this docs page for more information.

If I get my original camera's world transform and set that to my new camera , they will be at the same position, using objNode.worldTransform() and objNode.setWorldTransform(). The limitation here is that you can access a worldTransform matrix at a certain point in time(objNode.worldTransformAtTime()) but can't set it at a certain point in time (objNode.setWorldTransformAtTime()). Bummer, right ? It does make sense though, because otherwise you would have something animated but no indication that it actually is.

The obvious workaround is to set keyframes. Luckily, objNodes come with a setParmTransform() method that sets that world transform using the parameters, which we can set keyframes on.

Even more luckily, SideFx shows us how the method is implemented, so we can tweak it to fit our needs.

This is SideFx's code, which you can find at the linked posted above.

def setParmTransform(self, matrix):
parm_values = matrix.explode(
    transform_order=self.parm('xOrd').evalAsString(),    rotate_order=self.parm('rOrd').evalAsString(),    pivot=hou.Vector3(self.evalParmTuple('p')))
for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'):
    self.parmTuple(parm_name).set(parm_values[key])

The main issue is that we can't call parmTuple.setKeyframe(). We have to call parm.setKeyframe(). We'll be adding a nested for loop to iterate through the tuple.

for index,parm in enumerate(node.parmTuple(parm_name)):
	val = parm_values[key]
	val = val[index]
	parm.setKeyframe(hou.Keyframe(val,time))

This way we're iterating through every parameter of the tuple and setting a keyframe at the given time.

One thing to know is that methods that ask for a time argument require a time argument, which means it can't be frames. SideFx provides a hou.frameToTime() function and again the way it's implemented which is gonna be useful for us.

What we'll do is iterate through the timeline by getting the start and end frame, convert that to a time with the formula mentioned in the docs and then we can run our function.

Let's convert setParmTransform() to be a function instead of a method, integrate our nested loop, then run the function in a loop that runs for every frame of the timeline.

Here's our final snippet :

import hou

origcam = hou.node('/obj/cam1')

def customSetParmTransform(node, matrix,time=0):

    parm_values = matrix.explode(
        transform_order=node.parm('xOrd').evalAsString(),
        rotate_order=node.parm('rOrd').evalAsString(),
        pivot=hou.Vector3(node.evalParmTuple('p')))

    for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'):

        for index,parm in enumerate(node.parmTuple(parm_name)):

            val = parm_values[key]
            val = val[index]
            parm.setKeyframe(hou.Keyframe(val,time))


cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0]
cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0)))
cam2.setInput(0,None)

frange = hou.playbar.frameRange()
start = int(frange[0])
end = int(frange[1])


for i in range(start,end+1):

    ftt = (float(i)-1.0) / hou.fps()

    customSetParmTransform(cam2,origcam.worldTransformAtTime(ftt),ftt)

This runs a bit slowly admittedly. SideFx recommends using parm.setKeyframes((keyframeTuple)) for faster performance but I couldn't think of a clever way to integrate this, so it's brute force for now. Let us know if you find a better way.

Right, so I got frustrated and decided to rewrite it using parm.setKeyframes(). After some wrangling, this is what I ended up with

import hou

origcam = hou.node('/obj/cam1')

def customSetParmTransform(node, matrix,time,it,kflist):

    parm_values = matrix.explode(
        transform_order=node.parm('xOrd').evalAsString(),
        rotate_order=node.parm('rOrd').evalAsString(),
        pivot=hou.Vector3(node.evalParmTuple('p')))


    for parm_name, key in ('t', 'translate'), ('r', 'rotate'), ('s', 'scale'):

        letters = ('x','y','z')

        for index,parm in enumerate(node.parmTuple(parm_name)):

            val = parm_values[key]
            val = val[index]
            keyf = hou.Keyframe(val,time)

            klist = keydict[parm_name+letters[index]]
            klist[it] = keyf

cam2 = hou.copyNodesTo((origcam,),origcam.parent())[0]
cam2.setPosition(cam2.position() + hou.Vector2((2.0,0.0)))
cam2.setInput(0,None)

parms = ('tx','ty','tz','rx','ry','rz','sx','sy','sz')

frange = hou.playbar.frameRange()
start = int(frange[0])
end = int(frange[1])

total = end-(start-1)

keydict = dict()

for parm in parms:

    keydict[parm] = [0]*total


nindex = 0

for i in range(start,end+1):

    ftt = (float(i)-1.0) / hou.fps()

    customSetParmTransform(cam2,origcam.worldTransformAtTime(ftt),ftt,nindex,keydict)

    nindex+=1



for index, parm in enumerate(parms):

    kftuple = tuple(keydict[parm])

    cam2.parm(parm).setKeyframes(kftuple)

This runs so much quicker.

Contributed by:
Picture of the author

Verbs

flag

If you're not familiar with verbs, make sure you watch this Jeff Lait Masterclass: https://vimeo.com/222881605

You can skip to around 50:00 for the verb part, but I recommend you watch the whole thing, otherwise verbs won't really make sense. If you're already familiar with compiling/compiled blocks then feel free to skip to the verbs part.

Here's a couple example of using verbs:

def runVerb(geo,name,input=[], parms=None):

    verb = hou.sopNodeTypeCategory().nodeVerb(name)
    if parms:
        verb.setParms(parms)

    verb.execute(geo,input)

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

runVerb(geo,"box")

runVerb(geo,"vdbfrompolygons",[geo])

runVerb(geo,"vdbsmoothsdf",[geo])

parms = {"conversion" : 2}

runVerb(geo,"convertvdb",[geo],parms)

node.geometry().clear()
node.geometry().merge(geo)

runVerb() is a wrapper to help execute a verb. When executing a verb you need what you would need if you were using a node instead:

  • a name : "box", "vdbfrompolygons",...
  • a geo to execute the verb on. If you're generating a box then you can start from an empty hou.Geometry() object but if you want to run a smooth verb on a box for example, you'll need that box's geometry object.
  • an input / multiple inputs : if you try to execute a ray verb or a boolean verb you'll need multiple inputs. The input argument is a list of hou.Geometry() objects.
  • parameters : Parms is a dictionnary parameter. If left blank, values will be set to default. Any parameter included in the dictionary will override the defaults.

Remember that SideFx provides a couple examples here

Here's another snippet that generates boxes on boxes on boxes:

def box(geo,height):

    verb = hou.sopNodeTypeCategory().nodeVerb('box')
    parms = dict()
    parms["t"] = (0.0,height,0.0)
    verb.setParms(parms)
    verb.execute(geo,[])


node = hou.pwd()

startheight = 0.5

geo = hou.Geometry()

numboxes = 15  # node.parm('numboxes').evalAsInt()

for i in range(numboxes):

    box(geo,startheight+i)
    node.geometry().merge(geo)

Contributed by:
Picture of the author