What Is VEX?
VEX (Vector Expression Language) is Houdini's native shading and geometry manipulation language. It looks similar to C but is purpose-built for parallel execution over geometry elements - points, primitives, vertices, and voxels. 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:
- Manipulate attributes on thousands or millions of points
- Implement custom deformers, solvers, or procedural effects
- Perform math-heavy operations where Python would be too slow
- Create reusable logic inside HDAs
VEX executes in Attribute Wrangle (SOPs), Volume Wrangle (SOPs), POP Wrangle (DOPs), and various other contexts. The most common is the Attribute Wrangle, which defaults to running over points.
VEX vs Python in Houdini
Houdini supports both VEX and Python for scripting, but they serve very different purposes:
| Aspect | VEX | Python |
|---|---|---|
| Execution | Multi-threaded, JIT compiled | Single-threaded, interpreted |
| Speed | ~100-1000× 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 operation touches every point or primitive, use VEX. If it manipulates nodes, parameters, or external systems, use Python.
Data Types and Variables
VEX is statically typed. You must declare variable types explicitly. Here are the core types 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 vectors use curly braces {0, 1, 0} for literal initialization, not parentheses. Using set(0, 1, 0) also works and is required when initializing from variables: vector v = set(x, y, z);
Attribute Manipulation
Attributes are the lifeblood of Houdini. VEX reads and writes attributes using the @ shorthand or the explicit point(), prim(), detail() functions.
The @ Syntax
The @ prefix binds directly to geometry attributes. VEX auto-detects common attributes like @P, @N, @Cd, and @v as vectors. Custom attributes default to float unless you prefix the type.
// 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 to read attributes from specific elements or other inputs:
// 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.
Common VEX Functions
These functions appear in almost every Houdini project. Master them and you'll handle 90% of procedural tasks.
fit() - Remap Values
Remaps a value from one range to another. Essential for normalizing attribute values.
// 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.
// 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
Houdini's VEX includes multiple noise types. They're the backbone of organic procedural effects.
// 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"));
Working with Groups
Groups let you isolate subsets of geometry for targeted operations. VEX can both test group membership and assign elements to groups.
// 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 SOP nodes accept group patterns like @P.y>0.5 or @name=piece* directly in their Group field. Use VEX groups when you need more complex logic or when the same group is referenced by multiple downstream nodes.
Practical Examples
Color by Normals
A classic shader-debug trick: map the normal direction to RGB color. Useful for validating normals before export.
// 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 create a density attribute that drives a Scatter SOP, giving art-directable distribution.
// 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.
Procedural UV from Position
Generate UVs procedurally - useful when dealing with geometry that doesn't have clean UVs, like boolean results or VDB meshes.
// 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);
These examples scratch the surface. VEX also supports volumetric operations, ray casting with intersect(), matrix transforms, and even linear algebra for custom solvers. Check the Workflows page for how to integrate VEX into production setups, or explore Python in Houdini for combining VEX with Python-driven pipelines.