TA logoThe Technical Artist
Home/Houdini/VEX
article 01
houdini

VEX Programming

Workingtutorial~4 min readreviewed 2026-07-05

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.

1.1What Is VEX?

Placeholder with instructions to screenshot a Houdini network with an attribute wrangle
placeholder - a wrangle in its natural habitat

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:

  • Change attributes on thousands or millions of points.
  • Build custom deformers, solvers, or procedural effects.
  • Do math-heavy work that would make Python crawl.
  • Put reusable logic inside HDAs.
Where VEX Runs

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.

1.2VEX Vs Python In Houdini

Houdini gives you both VEX and Python. They're not rivals. They're for different jobs:

AspectVEXPython
ExecutionMulti-threaded, JIT compiledSingle-threaded, interpreted
Speed~100-1000x faster per-elementSlower for per-point work
Best forAttribute math, geometry opsUI tools, pipeline, file I/O
AccessGeometry attributes onlyFull Houdini scene, OS, network
Node typeWrangle nodesPython SOP, shelf tools, scripts
Tip

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.

1.3Data Types And Variables

Unlike Python, VEX makes you say your types up front. Here are the ones you'll use constantly:

VEX
// 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});
Warning

VEX vector literals use curly braces: {0, 1, 0}. If the values come from variables, use set(x, y, z) instead.

1.4Attribute Manipulation

Attributes are Houdini's bloodstream. VEX reads and writes them with the @ shorthand, or with explicit functions like point(), prim(), and detail().

The @ Syntax

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.

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

Point, Prim, And Detail Attributes

Use explicit functions when you need a specific point, prim, detail attribute, or another input:

VEX
// 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);
}
Tip

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.

1.5Common VEX Functions

These show up constantly. Get comfortable with them and most everyday wrangles stop feeling mysterious.

Fit() - Remap Values

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.

VEX
// 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 red

Chramp() - Channel Ramp

Reads a ramp parameter, giving artists an interactive curve to control falloffs, gradients, and profiles without touching code.

VEX
// 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);

Noise() - Procedural Noise

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.

VEX
// 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"));

1.6Working With Groups

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.

VEX
// 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});
}
Group Patterns

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.

1.7Practical Examples

Color By Normals

A classic debug trick: map normal direction to RGB. If normals are flipped or weird before export, this makes the problem visible fast.

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

Controlled Scatter With Density Attribute

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.

VEX
// 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);
Tip

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.

Procedural UV From Position

Generate UVs from position when the source geometry has no clean UVs, like boolean results, VDB meshes, or quick procedural prototypes.

VEX
// 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);
Going Further

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.