VEX from zero: the types, attributes, functions, and tiny wrangle patterns you need before the big Houdini graphs start making sense.
Everything you need to start writing wrangles and to read the 950-snippet library without squinting.

VEX looks like C had a baby with a spreadsheet, and that's a compliment. It's Houdini's own language, built for one job: running the same little program on every point (or prim, or vertex, or voxel) at once, very fast. VEX code runs inside Wrangle nodes (Attribute Wrangle, Volume Wrangle, etc.) and executes once per element simultaneously across all available CPU threads.
Use VEX when you need to:
You'll usually meet VEX in an Attribute Wrangle, running over points by default. It also shows up in Volume Wrangles, POP Wrangles, and other Houdini contexts once you start branching out.
Houdini gives you both VEX and Python. They're not rivals. They're for different jobs:
| Aspect | VEX | Python |
|---|---|---|
| Execution | Multi-threaded, JIT compiled | Single-threaded, interpreted |
| Speed | ~100-1000x faster per-element | Slower for per-point work |
| Best for | Attribute math, geometry ops | UI tools, pipeline, file I/O |
| Access | Geometry attributes only | Full Houdini scene, OS, network |
| Node type | Wrangle nodes | Python SOP, shelf tools, scripts |
Rule of thumb: if the work happens per point, prim, vertex, or voxel, reach for VEX. If the work touches nodes, files, UI, folders, exports, or the outside world, reach for Python.
Unlike Python, VEX makes you say your types up front. Here are the ones you'll use constantly:
// Scalar types
int count = 10;
float radius = 1.5;
// Vector types
vector pos = {0, 1, 0}; // 3-float vector (x, y, z)
vector2 uv = {0.5, 0.5}; // 2-float vector
vector4 orient = {0, 0, 0, 1}; // quaternion
// Matrix types
matrix3 rot = ident(); // 3x3 identity matrix
matrix xform = ident(); // 4x4 identity matrix
// String type
string name = "piece_01";
// Arrays
int ids[] = {1, 2, 3, 4};
float weights[] = array(0.1, 0.5, 0.3, 0.1);
vector colors[] = {};
append(colors, {1, 0, 0});
append(colors, {0, 1, 0});VEX vector literals use curly braces: {0, 1, 0}. If the values come from variables, use set(x, y, z) instead.
Attributes are Houdini's bloodstream. VEX reads and writes them with the @ shorthand, or with explicit functions like point(), prim(), and detail().
The @ prefix binds straight to geometry attributes. Houdini already knows common ones like @P, @N, @Cd, and @v are vectors. For your own attributes, prefix the type so VEX doesn't guess wrong.
// Reading and writing built-in attributes
@P.y += sin(@P.x * 2.0); // Displace Y by sine of X
@N = normalize(@N); // Re-normalize normals
@Cd = {1, 0, 0}; // Set color to red
@v = {0, 1, 0}; // Set velocity
// Custom attributes - prefix with type
f@weight = 0.5; // float
i@id = @ptnum; // integer
s@name = "default"; // string
v@rest = @P; // vector
p@orient = {0, 0, 0, 1}; // vector4 (quaternion)Use explicit functions when you need a specific point, prim, detail attribute, or another input:
// Read point attribute from second input (input 1)
vector target_pos = point(1, "P", @ptnum);
// Read a primitive attribute
string mat = prim(0, "shop_materialpath", @primnum);
// Read a detail (global) attribute
int total = detail(0, "total_count");
// Write detail attribute (run over "Detail (only once)")
setdetailattrib(0, "bbox_size", getbbox_size(0));
// Set a prim string attribute
setprimattrib(0, "name", @primnum, "wall_segment");
// Read from neighboring point
int nbs[] = neighbours(0, @ptnum);
foreach(int nb; nbs) {
vector nb_pos = point(0, "P", nb);
float dist = distance(@P, nb_pos);
}The first argument in point(), prim(), and detail() is the input index. Input 0 is the first input of the Wrangle node, input 1 is the second, and so on.
These show up constantly. Get comfortable with them and most everyday wrangles stop feeling mysterious.
Moves a value from one range into another. You'll use it for height masks, color ramps, density, random values, and almost everything that needs to become 0-1.
// Remap height (Y position) from world range to 0-1
float height = fit(@P.y, -1.0, 5.0, 0.0, 1.0);
// Clamp version - values outside source range are clamped
float clamped = fit01(clamp(height, 0, 1), 0.2, 0.8);
// Use fit to drive color by Y position
float t = fit(@P.y, 0, 10, 0, 1);
@Cd = lerp({0, 0, 1}, {1, 0, 0}, t); // Blue to redReads a ramp parameter, giving artists an interactive curve to control falloffs, gradients, and profiles without touching code.
// Create a ramp-driven falloff based on distance from center
float dist = length(set(@P.x, 0, @P.z));
float max_dist = chf("max_radius"); // Float parameter
float t = clamp(dist / max_dist, 0, 1);
float falloff = chramp("falloff_profile", t);
// Apply falloff to deformation
@P.y += falloff * chf("amplitude");
// Ramp-based color gradient
float height_norm = fit(@P.y, 0, chf("height"), 0, 1);
@Cd = chramp("color_ramp", height_norm);VEX has several noise flavors. They're the backbone of organic variation: rocks, terrain, masks, particles, breakup, and anything that shouldn't look machine-stamped.
// Basic Perlin noise (returns 0-1 range)
float n = noise(@P * chf("frequency"));
// Signed noise (returns -1 to 1) - better for displacement
float sn = snoise(@P * chf("freq") + chf("offset"));
// Fractal noise with octaves (turbulence)
float turb = 0;
float amp = 1.0;
float freq = chf("base_freq");
int octaves = chi("octaves");
for(int i = 0; i < octaves; i++) {
turb += abs(snoise(@P * freq)) * amp;
freq *= 2.0;
amp *= 0.5;
}
@P += @N * turb * chf("displacement_scale");
// Curl noise for divergence-free motion (great for particles)
@v = curlnoise(@P * chf("curl_freq") + @Time * chf("speed"));Groups are named subsets of geometry. VEX can ask whether something is in a group, add it, or remove it, which makes targeted edits much easier.
// Test if current point is in a group
if(inpointgroup(0, "selected", @ptnum)) {
@Cd = {1, 1, 0}; // Highlight selected points
}
// Add points to a group based on condition
if(@P.y > chf("threshold")) {
setpointgroup(0, "above_threshold", @ptnum, 1);
}
// Remove from group
setpointgroup(0, "temp_group", @ptnum, 0);
// Primitive groups
if(inprimgroup(0, "walls", @primnum)) {
setprimattrib(0, "shop_materialpath", @primnum, "/mat/brick");
}
// Create groups from attribute values
string piece = s@name;
setpointgroup(0, piece, @ptnum, 1);
// Expand group - get all points in a named group
int pts[] = expandpointgroup(0, "selected");
foreach(int pt; pts) {
setpointattrib(0, "Cd", pt, {1, 0, 0});
}Many SOPs accept patterns like @P.y>0.5 or @name=piece* directly in the Group field. Use VEX groups when the logic gets more complex, or when several downstream nodes need the same selection.
A classic debug trick: map normal direction to RGB. If normals are flipped or weird before export, this makes the problem visible fast.
// Map normals (-1 to 1) to color (0 to 1)
vector n = normalize(@N);
@Cd = n * 0.5 + 0.5;
// Variant: visualize just the vertical component
float up_dot = dot(@N, {0, 1, 0});
float t = fit(up_dot, -1, 1, 0, 1);
@Cd = lerp({0.2, 0.3, 1.0}, {0.1, 1.0, 0.3}, t); // Blue (down) to green (up)Use VEX to build a density attribute, then let a Scatter SOP do the point placement. This gives you art-directable distribution instead of random dots everywhere.
// Run on terrain mesh before Scatter SOP
// Creates density attribute based on slope and noise
vector up = {0, 1, 0};
float slope = 1.0 - dot(@N, up); // 0 = flat, 1 = vertical
// Only scatter on relatively flat areas
float slope_mask = fit(slope, 0.0, 0.4, 1.0, 0.0);
slope_mask = clamp(slope_mask, 0, 1);
// Add noise variation for natural look
float n = noise(@P * chf("noise_freq") + 42);
n = fit(n, 0.3, 0.7, 0, 1);
// Height-based falloff - less vegetation at extremes
float h = fit(@P.y, chf("min_height"), chf("max_height"), 0, 1);
float height_mask = chramp("height_falloff", h);
// Combine masks
f@density = slope_mask * n * height_mask;
// Use a ramp for final artistic control
f@density = chramp("density_profile", f@density);In the Scatter SOP, set Density Attribute to density. Areas with higher values get more scattered points. This pattern is used across the industry for vegetation placement, rock scattering, and debris distribution.
Generate UVs from position when the source geometry has no clean UVs, like boolean results, VDB meshes, or quick procedural prototypes.
// Triplanar UV projection based on normal direction
vector n = abs(normalize(@N));
vector pos = @P * chf("uv_scale");
// Determine dominant axis
vector2 uv;
if(n.x >= n.y && n.x >= n.z) {
uv = set(pos.z, pos.y); // Project from X axis
} else if(n.y >= n.x && n.y >= n.z) {
uv = set(pos.x, pos.z); // Project from Y axis
} else {
uv = set(pos.x, pos.y); // Project from Z axis
}
v@uv = set(uv.x, uv.y, 0);
// Optional: blend between projections for smooth transitions
// Weights based on normal direction (triplanar blending)
vector weights = pow(n, chf("blend_sharpness"));
weights /= (weights.x + weights.y + weights.z);
vector2 uv_x = set(pos.z, pos.y);
vector2 uv_y = set(pos.x, pos.z);
vector2 uv_z = set(pos.x, pos.y);
vector2 blended = uv_x * weights.x + uv_y * weights.y + uv_z * weights.z;
v@uv = set(blended.x, blended.y, 0);This is just the front door. VEX also handles volumes, ray casts with intersect(), matrix transforms, and solver math. From here, jump to Workflows for production patterns, or Python in Houdini for tools, exports, and pipeline glue.