Ben Traje
← Back to modelling

Mesh Proximity & Inside/Outside Detection in Houdini VEX (xyzdist + primuv)

09 Sep 26 (1d ago)

Whether you are building collision masks, procedural rigging falloffs, or transfer weights, testing geometry against another mesh's surface is a cornerstone VEX workflow.

In Houdini, the power duo for this is xyzdist() (which queries distance and surface coordinates on arbitrary polygonal surfaces) and primuv() (which samples any attribute at that exact coordinate).

Here is a breakdown of two standard setups: Proximity Detection (surface distance threshold) and Signed Surface Testing (detecting whether points are inside or outside a target volume using dot products).

Method 1: Proximity & Contact Mask (xyzdist)

This is ideal for contact shadows, soft landing impacts, or proximity-based color masking. It queries the shortest Euclidean distance from every point on Input 0 to the nearest polygonal primitive on Input 1.

// Run over: Points
// Input 0: Geometry to evaluate
// Input 1: Target collision/reference mesh

int hit_prim;
vector hit_uv;

// Query shortest distance and surface parameterization on Input 1
float dist = xyzdist(1, @P, hit_prim, hit_uv);

float threshold = chf("threshold");

// Base color: White
@Cd = set(1.0, 1.0, 1.0);

// Mark points within proximity range: Red
if (dist <= threshold) {
    @Cd = set(1.0, 0.0, 0.0);
}

Performance Tip: If you only care about points within a specific search bubble, pass threshold directly as the maximum search radius to xyzdist:

float dist = xyzdist(1, @P, hit_prim, hit_uv, threshold);

This prevents Houdini from evaluating distant points across the entire spatial tree, making the node substantially faster on heavy geometry.

Method 2: Inside vs. Outside Detection (dot + primuv)

Distance alone cannot tell you whether a point is floating in open air or buried inside a mesh volume.

To determine if a point is inside a closed target mesh, sample the surface normal (N) at the nearest point on Input 1. If the vector pointing from the surface to your point points opposite to the surface normal, the point is inside.

// Run over: Points
// Input 0: Points/Geometry to test
// Input 1: Watertight mesh (must have correct outward-facing normals)

int hit_prim;
vector hit_uv;
float dist = xyzdist(1, @P, hit_prim, hit_uv);

// Sample position and normal at the exact surface coordinate
vector hit_pos = primuv(1, "P", hit_prim, hit_uv);
vector hit_N   = primuv(1, "N", hit_prim, hit_uv);

// Default color: White (Outside)
@Cd = set(1.0, 1.0, 1.0);

// Avoid normalization errors when point sits directly on the surface
if (dist > 0.0001) {
    vector dir = normalize(@P - hit_pos);

    // Negative dot product means angle > 90° relative to outward normal (Inside)
    if (dot(dir, hit_N) < 0.0) {
        @Cd = set(1.0, 0.0, 0.0); // Red (Inside)
    }
}

3 Critical Traps to Avoid

1. Ensure Normals Exist on Input 1

primuv(1, "N", ...) looks for an explicit @N attribute. If your target geometry does not have explicit vertex or primitive normals cached, primuv will return {0, 0, 0}, breaking the dot product calculation.

  • Fix: Drop a Normal SOP before Input 1 (configured to generate primitive or point normals).

2. High Curvature and Concave Pockets

Checking dot(dir, hit_N) < 0 assumes a clean, outward-facing surface normal. On sharp concave folds or complex self-intersecting meshes, the closest primitive might orient in unexpected ways.

  • If you need bulletproof inside/outside testing for non-convex or messy shapes without worrying about normals, you can either:
  • Convert Input 1 into a VDB (VDB from Polygons) and sample signed distance using volumesample().
  • Use the built-in intersect() function to cast rays and count odd/even boundary crossings.

3. Normalize Your Target Normal

If the target mesh has scaled transforms, its normals may not be unit length. While the sign of the dot product remains valid regardless of vector length, running normalize(hit_N) ensures consistency if you later blend weights based on angles.