Ben Traje
← Back to modelling

Houdini VEX: How to Calculate a Midpoint Point and Safely Delete Input Geometry

10 Sep 26 (Today)

Creating a point precisely at the geometric midpoint between two reference vertices—complete with interpolated normals and coordinate vectors—is a classic procedural operation. It is commonly used to generate pivot anchors, place joint locators, or generate raycast origins.

However, writing this in VEX introduces a classic trap: order of operations in topological changes.

If you delete the geometry before querying positions, or if you loop forward through point indices using removepoint(), your point numbers shift and your lookups will return {0, 0, 0}.

Here is why that happens, how to structure the logic correctly, and the cleanest ways to execute this in Houdini.

The Traps in the Original Snippet

1. Sampling Data After Deletion

// BUG: If you delete the points first...
for(int i = 0; i < @numpt; i++) {
    removepoint(0, i);
}

// ...pt1 and pt2 no longer exist to query their P and N!
vector pos1 = point(0, "P", pt1);

Topology changes in VEX must always happen after you have extracted all necessary transform data and vector attributes from the input geometry.

2. Forward-Loop Point Index Shifting

When you call removepoint(0, 0) on a geometry with 100 points, point 1 immediately shifts down to become index 0. A forward loop (i++) will skip every other point and trigger out-of-bounds errors. If you ever need to iterate and delete points manually in VEX, you must count backwards:

for (int i = @numpt - 1; i >= 0; i--) {
    removepoint(0, i);
}

The Clean Solution: Detail Wrangle

Because you are reducing an entire geometry down to a single midpoint entity, this should be executed in a Detail Wrangle (Run Over: Detail (only once)). Running this in a Point Wrangle would run the script repeatedly for every point in the input.

// Run over: Detail (only once)

int pt1 = chi("point_a"); // e.g. 123
int pt2 = chi("point_b"); // e.g. 124

// 1. QUERY ATTRIBUTES FIRST (While geometry is intact)
vector pos1 = point(0, "P", pt1);
vector pos2 = point(0, "P", pt2);
vector mid_pos = (pos1 + pos2) * 0.5;

vector n1 = point(0, "N", pt1);
vector n2 = point(0, "N", pt2);

// Guard against zero-length vectors when normals oppose each other
vector mid_n = n1 + n2;
mid_n = (length(mid_n) > 0.0001) ? normalize(mid_n) : set(0, 1, 0);

// 2. CREATE THE NEW MIDPOINT
int new_pt = addpoint(0, mid_pos);

// Ensure attributes exist on geometry before writing
if (haspointattrib(0, "N") == 0) addpointattrib(0, "N", set(0, 0, 0));
if (haspointattrib(0, "up") == 0) addpointattrib(0, "up", set(0, 1, 0));

setpointattrib(0, "N", new_pt, mid_n);
setpointattrib(0, "up", new_pt, set(0, 1, 0));

// 3. CLEAN UP ORIGINAL GEOMETRY LAST
// Remove all points EXCEPT the newly created midpoint
for (int i = @numpt - 1; i >= 0; i--) {
    if (i != new_pt) {
        removepoint(0, i);
    }
}

An Even Cleaner Alternative: Non-Destructive 2-Input Setup

Rather than populating and wiping geometry inside the same stream, standard Houdini practice separates reading from writing using inputs:

  1. Wire your base mesh into Input 0 of a Null SOP (for reference).
  2. Wire an empty Add SOP (generating a blank stream) into Input 0 of a Detail Wrangle, and your base mesh into Input 1.
[ Base Mesh ] 
      │
      ├───► (Input 1: Reference Data)
      │                                   ► [ Detail Wrangle ] ──► (Isolated Midpoint)
[ Add SOP (Empty) ] ──► (Input 0: Destination)

Now you don't have to delete anything at all:

// Run over: Detail (Input 0 is empty, Input 1 is source geometry)

int pt1 = chi("point_a");
int pt2 = chi("point_b");

// Query directly from Input 1
vector pos1 = point(1, "P", pt1);
vector pos2 = point(1, "P", pt2);
vector mid_pos = (pos1 + pos2) * 0.5;

vector n1 = point(1, "N", pt1);
vector n2 = point(1, "N", pt2);
vector mid_n = normalize(n1 + n2);

// Generate point directly in the clean output stream
int new_pt = addpoint(0, mid_pos);
setpointattrib(0, "N", new_pt, mid_n, "set");
setpointattrib(0, "up", new_pt, set(0, 1, 0), "set");

This keeps your node graph non-destructive, avoids loop-based deletion overhead, and guarantees the output contains solely the generated midpoint.