Houdini-Fun

Houdini Normalized Device Coordinates

NDC is a screen space coordinate system, great for perspective illusions and raycasting tricks.

X and Y represent the 2D screen coordinates, while Z represents the distance to the camera.

The X and Y coordinates are normalized between 0 and 1, with 0.5 in the middle.

The Z coordinates are 0 at the camera, negative in front and positive behind the camera. Why negative? No idea!

Converting NDC

You can convert a world space coordinate to NDC using toNDC():

vector ndcPos = toNDC("/obj/cam1", v@P);

Then convert it back to world space using fromNDC():

vector worldPos = fromNDC("/obj/cam1", ndcPos);

Here’s some NDC tricks you can play with. Download the HIP file!

Get the camera position

The origin of NDC space is the camera, so just convert {0, 0, 0} to world space.

// Run this in a detail wrangle
string cam = "/obj/cam1";
vector camPos = fromNDC(cam, {0, 0, 0});
addpoint(0, camPos);

{0.5, 0.5, 0} is technically more correct, but gives the same result.


Draw a ray from the camera

The Z axis aligns with the camera direction, so move along it to draw a ray.

// Run this in a detail wrangle
string cam = chs("cam");
float offset = chf("raylength");

// Sample two positions along the Z axis in NDC space to draw a ray
vector camPos = fromNDC(cam, {0.5, 0.5, 0});
vector camPos2 = fromNDC(cam, set(0.5, 0.5, -offset));

int a = addpoint(0, camPos);
int b = addpoint(0, camPos2);

addprim(0, "polyline", a, b);


Flatten to the XY plane

Using NDC coordinates directly in world space flattens the geometry to how it looks on screen, like a printed photo.

string cam = chs("cam");

// Flatten by setting Z to a constant value
v@P = toNDC(cam, v@P);
v@P.z = 0;


Another trick is turning this into an outline, much like Labs Extract Silouette.

  1. Add a Triangulate2D node. Set “Silhouette” to * and enable outside removal. This triangulates the mesh.
  1. Add a Divide node set to “Remove Shared Edges”. This wipes the interior triangles and produces a clean outline.

Flatten to the camera plane

Using NDC coordinates in camera space lets you flatten geometry but keep it identical from the camera perspective.

string cam = chs("cam");
float offset = ch("distance");

// Flatten to camera by setting Z to a constant value
vector p = toNDC(cam, v@P);
p.z = -offset;

v@P = fromNDC(cam, p);


Move along the Z axis

Same as above, except subtracting the Z coordinate. Again the geometry is identical from the camera perspective.

string cam = chs("cam");
float offset = ch("distance");

// Distort relative to camera by adding or multiplying the Z value
vector p = toNDC(cam, v@P);
p.z -= offset;

v@P = fromNDC(cam, p);


Perspective illusion

Same as above, except using another camera as reference. The Z coordinates are flipped as the cameras cross paths.

string cam = chs("cam");
float mult = ch("multiply");
float add = ch("add");

// Distort relative to camera by adding or multiplying the Z value
vector p = toNDC(cam, v@P);
p.z = p.z * mult + add;

v@P = fromNDC(cam, p);


Frustum box

The VEX equivalent of Camera Frustum qL.

  1. Add a box. The X and Y coordinates range from 0 to 1. The Z coordinates are negative from 0 to the depth you want.
  1. Convert from NDC to world coordinates.

string cam = chs("cam");

// Optionally animate it along the Z axis
v@P.z -= chf("distance");

v@P = fromNDC(cam, v@P);


Frustum plane

The VEX equivalent of Camera Plane qL.

  1. Same as above, but add a grid instead. The X and Y coordinates range from 0 to 1.
  1. Convert from NDC to world coordinates.

string cam = chs("cam");

// Optionally animate it along the Z axis
v@P.z = -chf("distance");

v@P = fromNDC(cam, v@P);


Project onto geometry

This technique is great for holograms. I first saw Entagma use it for a raytracer.

  1. Take the frustum plane above and subdivide it a bunch.
  2. Find the projection direction per point.

Since the plane is flattened on Z in NDC space, this is easy. Just subtract the camera position from the current position.

string cam = chs("cam");
vector camPos = fromNDC(cam, {0, 0, 0});

// Projection direction
v@N = normalize(v@P - camPos);

  1. Ray onto the target geometry.

For arbitrary geometry, the same idea applies. Convert to NDC space, flatten Z and convert back to world space.

string cam = chs("cam");
vector camPos = fromNDC(cam, {0, 0, 0});

// Flatten Z axis in NDC space
vector ndcPos = toNDC(cam, v@P);
ndcPos.z = -1;
vector worldPos = fromNDC(cam, ndcPos);

// Projection direction
v@N = normalize(worldPos - camPos);

Cull offscreen geometry

Perhaps the most common use of NDC space is removing offscreen geometry.

So what is offscreen? For X and Y it’s anything outside the 0 to 1 range, and for Z it’s anything positive.

string cam = chs("cam");
vector ndcPos = toNDC(cam, v@P);

if (ndcPos.x < 0 || ndcPos.x > 1 // Remove outside 0-1 on X
 || ndcPos.y < 0 || ndcPos.y > 1 // Remove outside 0-1 on Y
 || ndcPos.z > 0) { // Remove behind camera (positive Z)
    removepoint(0, i@ptnum);
}


It helps to add some wiggle room near the edges to avoid issues like glitchy motion blur or flickering shadows.

float padding = chf("padding");
string cam = chs("cam");
vector ndcPos = toNDC(cam, v@P);

if (ndcPos.x < -padding || ndcPos.x > 1 + padding // Remove outside 0-1 on X (with padding)
 || ndcPos.y < -padding || ndcPos.y > 1 + padding // Remove outside 0-1 on Y (with padding)
 || ndcPos.z > 0) { // Remove behind camera (positive Z)
    removepoint(0, i@ptnum);
}