Update: Imre Tuske from Weta pointed out that there’s a simpler method… it turns out that you can force expressions to evaluate inside a Wrangle by wrapping them up in backticks. So if you have a channel called “do_vexpressions” to enable or disable VEXpressions, and another channel called “vexpression” that contains the user code, you could insert this line where you’d want the inserted code to appear:

`ifs(ch("do_vexpression"), chs("vexpression"), "")`

There’s always an easier way!

Back to the original post…

A lot of nodes in Houdini, POP nodes especially, support a thing called “VEXpressions,” which is a way of using a somewhat simplified VEX syntax to make adjustments to an effect. For example, in a POP Wind DOP, you could write this expression:

amp *= @nage;

amp in this case refers to the amplitude of the wind effect. It’s multiplied by the normalized age of each particle, so older particles are affected more (as their normalized age approaches 1.0). This flexibility is great, since it allows the artist to make very specific changes to an otherwise generalized effect in order to get exactly the desired result.

When developing MOPs, it was very important to keep the simplicity of the framework as consistent as possible: a single scalar attribute, the Falloff, controls the “amplitude” of the effect per-primitive. This is great conceptually, but in practice a more experienced Houdini user is going to want to be able to affect things as specifically as possible, and that’s where VEXpressions come in. So how are these VEXpressions accomplished?

I jumped inside the POP Wind DOP to find out:

What’s the emoji for crying and puking at the same time?

Ew. VOPs.

Taking a closer look, we have a ton of parameters on the left, each of them with the prefix “parm”. These all go into an “If-Then Block” controlled by the parameter “parm_uselocalwind”, which is driven by the “Use VEXpressions” toggle on the POP Wind DOP’s parameter interface. Jumping inside the If-Then Block reveals this:

Inside the If-Then, all of these parameters are just going into a Snippet VOP… which is a Wrangle!

There’s a few things to take from this. First of all, the Snippet VOP in the middle there is essentially an Attribute Wrangle. If you were to look inside an Attribute Wrangle, it’s mostly just a VOPnet with a Snippet VOP inside. Second, check out those input/output names… all of the “parm” prefixed attributes are being bound to the actual attribute names we see on the Parameter Interface, so “parm_amp” becomes “amp”. Third, check out this expression:

ifs(ch("../../parm_uselocalwind"), chs("../../../localwindexpression"), "")

This expression is saying, “if Use VEXpressions is enabled, return the user’s VEXpression here as a string.” That would be the code from the beginning of this example: amp *= @nage;. Then the results are kicked out of the subnetwork, where they continue through the rest of the VOPnet.

The reason this all works is because in VOPs, we are defining a big list of parameters like “amp” and “scale” via the Parameter VOPs at the very beginning, and then allowing the user to modify the values of those parameters through the Snippet. In code terms, the Parameter VOP that defines “amp” (or “parm_amp”) is just like saying float amp = ch("amp");. This is why in the VEXpression, the user can just type amp *= @nage;, because amp has already been defined as a local variable!

If we were to break this down into code, this VEXpression code insertion process looks like this:

vector wind = chv("wind");
float amp = ch("amp");
vector freq = chv("freq");
// blah blah blah, define lots of variables

amp *= nage;

// now run the actual wind code, the rest of the VOPnet

Our VEXpression, then, needs to be defined after all of the potential parameters the user can modify are defined, but before the part of the code that’s actually affecting everything.

Okay, on to implementation.

I first attempted to implement this VEXpression stuff in the MOPs Transform Modifier, and it’s still using this setup as of the latest build. Here’s the network:

The MOPs Transform Modifier.

This isn’t quite the behemoth of nodes that the POP Wind DOP is, but still… all of the possible inputs need to be created as Parameter VOPs, which then pipe into the If-Then loop, have to be bound to specifically-named export variables, and then they get modified by the Snippet internally before being exported back out. This is annoying, and makes developing slower.

It turns out there’s a better way!

Since all we’re doing is injecting some user code into an existing string of code, we can do this with some Python instead. It’s still a bit awkward, but at least it means all of the development can be done in a plain old Wrangle.

Check out the guts of the MOPs Move Along Spline modifier:

A portion of the MOPs Move Along Spline network.

See that highlighted bit of code? That’s a marker of sorts used to tell Python where to inject any defined VEXpression. The two Attribute Wrangles here are more or less identical, except that one just acts as a placeholder to store the raw code, and the other actually has any connected parameters and such, and builds the final expression including the VEXpression.

On the purple node, you can see that the entire code snippet there is a Python expression (hence the purple coloration of the text background). If you right-click the snippet and edit the expression, you’ll see this:

snippet = hou.node('../attach_to_curve_simple_CODE').evalParm('snippet').split('/* SNIPPET */')
vexpression = hou.pwd().evalParm('vexpression')
do_vexpression = hou.pwd().evalParm('do_vexpression')
if not do_vexpression:
vexpression = ""

return "\n".join([snippet[0], vexpression, snippet[1]])

This expression is grabbing the code from the original wrangle (the one with the _CODE suffix), then finding that “/* SNIPPET */” comment string and splitting the expression into two parts: before the SNIPPET, and after the SNIPPET (snippet[0] and snippet[1]). If the “do_vexpression” parameter is enabled, the variable “vexpression” is set to the value of the user-supplied VEXpression string parameter. If not, “vexpression” is just set to an empty string. Then the “vexpression” string is sandwiched between the original expression parts, and the whole combined string is returned as the final value of the snippet.

That’s all there is to it! You could easily combine all of this onto a single wrangle by adding some spare parameters, but I found this mentally a little easier to deal with (at first anyways) as two separate wrangles. Remember that if you want to use a Python expression for a parameter, you first want to set a keyframe or some other expression, then right-click and use the options popup to switch the expression language to Python.

Leave a Reply

Your email address will not be published. Required fields are marked *