Houdini VEX: Creating an Attraction Force with Smooth Arrival & Deceleration
09 Sep 26 (1d ago)
Whether designing cyclical push/pull shockwaves (where particles burst outward and get reeled back into the origin) or pulling instanced geometry into a tight formation, attraction setups are a staple of procedural animation in SideFX Houdini.
However, a standard distance-based pull introduces two classic simulation bugs:
- Orbital Overshoot: Points accelerate toward the target with so much kinetic momentum that they slingshot past the origin, oscillating back and forth indefinitely like an unstable planetary orbit.
- Instant Snapping / Micro-Jitter: Abruptly zeroing velocity (
@v = 0) when crossing an arrival radius causes points to freeze unnaturally or jitter violently across the target threshold.
Implementing a classic "Arrival" steering behavior—where desired velocity ramps down proportionally to target distance within a designated braking radius—allows points to glide smoothly to a target and settle cleanly.
1. SOPs vs. POPs: Architectural Distinction
Before writing code, consider where your points live in Houdini's evaluation pipeline:
- SOPs / SOP Solver (Kinematic Animation): If you are updating positions deterministically frame-over-frame (e.g., inside a SOP Solver or manually trailing
@P += @v * @TimeInc), setting@vdirectly gives you predictable, frame-rate-independent control. - POP Networks (Dynamic Simulation): In dynamic DOP simulations, directly overriding
@vevery frame fights the POP solver’s internal sub-step integrator, bypasses particle mass, cancels gravity/drag, and breaks physical collision responses. For POP networks, calculate a Steering Force (@force) instead.
2. The SOP Approach: Direct Velocity Steering
When animating points procedurally in SOPs or writing a cyclical rewind step, calculate a normalized trajectory vector, scale it by an arrival deceleration ramp, and assign it directly to @v:
// Run over: Points (Point Wrangle inside SOPs or SOP Solver)
// 1. Define attraction target position
vector target = chv("target_pos"); // Default: {0, 0, 0}
// 2. Compute relative trajectory and distance
vector dir = target - @P;
float dist = length(dir);
// Guard against division-by-zero if a point is already at the target
vector dir_norm = (dist > 0.0001) ? normalize(dir) : set(0, 0, 0);
// 3. User Parameters
float max_speed = chf("max_speed"); // Cruising velocity (e.g., 6.0)
float slow_radius = chf("slow_radius"); // Braking zone radius (e.g., 2.0)
float stop_dist = chf("stop_dist"); // Dead-zone radius (e.g., 0.05)
// 4. Evaluate Arrival Ramp
if (dist > stop_dist) {
// Ramp speed down linearly from max_speed to 0 within the slow_radius
float ramp = clamp(dist / slow_radius, 0.0, 1.0);
float desired_speed = max_speed * ramp;
@v = dir_norm * desired_speed;
} else {
// Lock the point down completely inside the dead zone
@v = set(0, 0, 0);
}
Speed
▲
Max ─────┐ (Cruising speed)
│ \
│ \ Braking zone (ramp = dist / slow_radius)
│ \
0 └──────────┴───────► Distance to Target
stop_dist slow_radius
Note on SOP Trailing: Assigning
@vin a standard Point Wrangle sets the velocity attribute, but it does not move the geometry unless you feed it into a SOP Solver, append a Point Replicate / Trail SOP, or manually advance position in the wrangle:@P += @v * @TimeInc;
3. The POP Approach: Dynamic Steering Force (@force)
Inside a POP Network, steering behavior follows Craig Reynolds' classic formula:
$$\text{Steering Force} = \text{Desired Velocity} - \text{Current Velocity}$$
Applying this delta to @force allows particles to slow down smoothly while preserving physical properties like inertia, mass, and obstacle bounce responses:
// Run over: Points (POP Wrangle)
vector target = chv("target_pos");
vector to_target = target - @P;
float dist = length(to_target);
float max_speed = chf("max_speed"); // Maximum approach speed (e.g., 6.0)
float slow_radius = chf("slow_radius"); // Distance to start braking (e.g., 2.5)
float stop_dist = chf("stop_dist"); // Dead zone threshold (e.g., 0.08)
float max_force = chf("max_force"); // Clamps max acceleration (e.g., 20.0)
if (dist > stop_dist) {
// 1. Calculate desired velocity vector with arrival ramp
float ramp = clamp(dist / slow_radius, 0.0, 1.0);
vector desired_v = normalize(to_target) * (max_speed * ramp);
// 2. Compute steering delta
vector steer = desired_v - @v;
// 3. Clamp steering acceleration to prevent unnatural instant turns
if (length(steer) > max_force) {
steer = normalize(steer) * max_force;
}
// 4. Apply force scaled by particle mass (F = m * a)
@force += steer * f@mass;
} else {
// Damp residual momentum inside the arrival zone rather than hard-locking
@v *= 0.8;
}
Parameter Reference
| Parameter | Recommended Range | Purpose |
|---|---|---|
max_speed | 4.0 – 8.0 | Top speed points reach while cruising outside the braking zone. |
slow_radius | 1.5 – 3.5 | The distance from the target where deceleration begins. |
stop_dist | 0.05 – 0.1 | The dead-zone radius that prevents micro-jittering around the exact origin. |
max_force (POPs) | 10.0 – 25.0 | Limits steering authority so particles don't snap unnaturally on a dime. |
By separating the braking radius (slow_radius) from the dead zone (stop_dist), points decelerate naturally into formation instead of jerking to a sudden halt.