Simstim

Houdini

Sliders are uniforms: reading parameters in Houdini VEX, explained for programmers

Houdini's VEX runs one small function over every point, like a GPU kernel, and ch() reads UI sliders the way a shader reads uniforms. A parametric wave, plus two traps: time is in seconds, and new sliders start at zero.

· 6 min read · houdini · vex · tutorial · vex fundamentals

The post's VEX wrangle code beside a Karma render of the wave it makes on a grid, with the amplitude, frequency, tint and bump-shaped profile ramp values that drive it.

TL;DR Houdini's VEX language runs one small function over every point of a 3D model, like a GPU kernel over an array. ch() and its typed siblings (chf, chv, chramp…) let that kernel read values from UI sliders, colors and curves, much like uniforms in a shader. This post builds a tiny parametric wave and covers two common traps: time is measured in seconds, not frames, and new sliders start at zero.

Jacking in

This is Day 1 of VEX Fundamentals, a Simstim series for people who write code but have never opened a 3D package. Gibson's simstim let you ride someone else's senses. This is the tamer version: you ride along as we work through VEX, the language that sits under most procedural work in Houdini.

Our syllabus for the series is Juraj Tomori's open VEX tutorial, alongside the SideFX VEX docs and Matt Estela's CGWiki. Everything below is our own explanation and code.

Houdini in 60 seconds

  • SOPs (Surface Operators) are nodes that take geometry in and give geometry out. A Houdini scene is a dataflow graph of these nodes, a bit like a build graph.
  • Geometry is made of points (positions), primitives (polygons, curves, volumes), vertices (a primitive's references to points) and one detail (the whole object).
  • Attributes are named, typed arrays attached to one of those four levels. P is point position (a vector), Cd is color. In code you write them as v@P or f@height: the letter gives the type and @ means "this element's attribute".
  • A wrangle is a SOP where you type VEX. Its Run Over setting picks the iteration domain. Set it to Points and your snippet runs once per point, in parallel, with no order guaranteed. If you've written a CUDA kernel or a NumPy ufunc, you already have the right mental model.

The core idea: parameters are uniforms

A kernel needs inputs that don't change from element to element, such as an amplitude, a color or a frequency. In GLSL those are uniforms. In VEX they're node parameters, and you read them with the ch family:

  • chf("name") returns a float, chi an int, chv a vector and chs a string.
  • chramp("name", pos) samples a ramp parameter, which is a curve the user draws in the UI. Treat it as a 1D texture lookup with pos in [0, 1].

The part that makes this feel like magic: once you've typed the calls, the Create spare parameters button to the right of the code editor reads your snippet and adds a matching UI widget for every ch call it finds. Your function signature becomes a UI.

Here's a parametric wave that uses four parameter types.

// Attribute Wrangle, Run Over: Points
// Input: a Grid SOP (e.g. 10x10 size, 100x100 rows/cols)
float TAU  = 6.2831853;

float amp  = chf("amplitude");     // float slider
float freq = chf("frequency");     // float slider
int   mode = chi("mode");          // 0 = wave * ramp, 1 = ramp only
vector tint = chv("tint");         // vector parm; set its type to Color in the UI

// normalized 0..1 coordinate across the grid in X
float u = relbbox(0, v@P).x;

// artist-drawn falloff curve, sampled like a 1D texture
float shape = chramp("profile", u);

float h = shape;
if (mode == 0)
    h *= sin(u * freq * TAU);

// same slider, evaluated half a second in the past (time is in SECONDS)
float amp_lag = chf("amplitude", @Time - 0.5);

v@P.y += amp * h;
v@Cd   = lerp(tint * 0.15, tint, shape);   // dark-to-tint along the profile
f@lag  = amp - amp_lag;                    // inspect in the Geometry Spreadsheet

To run it: drop a Grid SOP, wire it into an Attribute Wrangle, leave Run Over on Points, paste the code and click the create-parameters button. Set amplitude to about 1 and frequency to about 3, and draw a bump in the profile ramp. The new sliders only run from 0 to 1, so type the 3 into the field.

For comparison, here's the same idea in NumPy. The "kernel" is vectorized array math and the "parameters" are plain function arguments:

import numpy as np

def wave(P, amp=1.0, freq=3.0, ramp_x=(0, .5, 1), ramp_y=(0, 1, 0)):
    x = P[:, 0]
    u = (x - x.min()) / np.ptp(x)             # relbbox(...).x
    shape = np.interp(u, ramp_x, ramp_y)      # chramp()
    P = P.copy()
    P[:, 1] += amp * shape * np.sin(u * freq * 2 * np.pi)
    return P

The difference is where the arguments come from. In Houdini they live on the node, so they can be animated with keyframes, driven by expressions, or pointed at another node's parameter. You can read across nodes too: chf("../controls/height") evaluates a parameter on a sibling node. If that parameter contains an HScript or Python expression, ch returns the evaluated result, not the expression text.

What trips you up, and what clicks

  1. Time is in seconds. Tomori's tutorial says the optional time argument lets you evaluate a channel "at different frame". The current SideFX page for chf names the argument time_in_sec, and the page for ch spells it out: the time is in seconds, not frames. So chf("amplitude", 12) means 12 seconds, which is frame 289 at 24 fps (Houdini's frame 1 sits at time 0), not frame 12. Pass a frame number by mistake and you sample a moment far past your animation, where the value has usually stopped changing, so nothing seems to move. Pair it with @Time, which is also in seconds.
  2. New sliders start at zero. Click the button and the grid goes completely flat. That isn't a bug: the button makes each float parameter with a default of 0 and a 0 to 1 slider, so amplitude and frequency come in at 0 and everything multiplies to zero. Get into the habit of setting defaults straight after creating parameters, before you blame your math.
  3. chramp is overloaded on return type. The docs list both a float and a vector version with the same arguments. VEX chooses one from the type you assign to, which feels odd if you come from C++ or Java, where overloads are resolved on argument types. float s = chramp(...) samples a float ramp and vector c = chramp(...) samples a color ramp, so keep an explicit type on the left-hand side. One catch: the create-parameters button reads only the call, not your left-hand side. It makes a color ramp only when you write the cast, vector(chramp("name", u)), and a float ramp otherwise.
  4. It's read-only, and that's the point. Every point reads the same parameter value (unless you pass a time), so it behaves like a uniform. Per-element variation comes from attributes, the topic of the next post in this series.

Try it yourself

  1. Install Houdini Apprentice, the free, non-commercial edition from SideFX.
  2. Build the Grid → Attribute Wrangle graph above.
  3. Exercise: add chi("axis") so the wave can run along X or Z (the grid is flat in Y). Hint: relbbox returns a vector, and you can index it with [].
  4. Stretch goal: keyframe amplitude and color the grid by f@lag. Every point gets the same value, so the whole grid's color tracks how fast amplitude is changing: a crude velocity readout built from a parameter's own history.

Not ready to install Houdini? Run the NumPy version above on a np.meshgrid and plot it with matplotlib. The idea carries over directly.

Functions used: chf, chi, chv, chramp, relbbox, sin, lerp. Each one links to its entry in the Simstim VEX reference.

Sources