<aside>

$$ \text{World Space: Where a point exists in 3D space}\quad \longleftrightarrow \quad\text{NDC: Where it appears in the camera's image, including depth} $$

</aside>

<aside>

Companion HDA: HDA NDC EXPLORER (Normalized Device Coordinates) 🔴

</aside>

1. What problem does NDC solve?

Imagine you have a camera in Houdini. You want to describe where a point appears in the camera's image. Instead of describing the point using its 3D world position, you describe its position relative to the image.

<aside>

Two main functions

2. What does NDC look like?

Imagine you're looking through a camera in Houdini. The camera produces an image.

NDC gives you a way to describe positions relative to that image, using normalized coordinates.

For example, imagine the image coordinates are:

Y works similarly, describing the vertical position.

NDC also includes a Z coordinate, which represents depth according to the projection convention.

<aside>

NDC describes a normalized (0 to 1) point's position relative to the camera's projected image, rather than its ordinary 3D world position.

</aside>

3. fromNDC()

fromNDC() converts a position expressed in Normalized Device Coordinates (NDC) into a 3D position relative to a specified camera.

Example 1 — Position a Point at the Image Center

First, set the point's position to the center of the NDC image:

// Set the position to the center of the NDC image.
// Z specifies the depth in NDC space.
@P = {0.5, 0.5, 0};

Then convert that position into 3D space using the camera:

// Get the camera path.
string cam = chs("camImport");

// Convert the NDC position into 3D space using the camera.
@P = fromNDC(cam, @P);

Result: The point is positioned at the center of the camera's projected image, at the depth corresponding to NDC Z = 0.

Example 2: Move Geometry Through Camera Depth

// Get the camera path.
string cam = chs("camImport");

// Use the point's position as NDC coordinates.
vector ndc = @P;

// Control the projected depth with a parameter.
ndc.z = -chf("ndc_depth");

// Convert the NDC position back into 3D space.
@P = fromNDC(cam, ndc);

How it works

The input geometry's point positions are interpreted as NDC coordinates. The ndc_depth parameter controls their Z value, allowing you to explore how changing projected depth affects the reconstructed 3D geometry.

Example 3: Create a Camera Frustum

// Get the camera path.
string cam = chs("camImport");

// Read the point position in NDC space.
vector ndc = @P;

// Extend the horizontal and vertical margins.
ndc.x = 0.5 + (ndc.x - 0.5) * chf("margin_x");
ndc.y = 0.5 + (ndc.y - 0.5) * chf("margin_y");

// Control the starting and ending depth.
float depth_start = -chf("depth_start");
float depth_end = -chf("depth_end") - -chf("depth_start");

ndc.z = fit(@P.z, -0.5, 0.5, depth_start, depth_end);

// Convert NDC coordinates into 3D space.
@P = fromNDC(cam, ndc);

How it works

The input box must be positioned specifically in NDC space: its center should be at (0.5, 0.5, 0), with its Z values extending from 0 into negative values. These coordinates define the starting shape that will be reconstructed relative to the camera.

The margin_x and margin_y parameters extend or reduce the horizontal and vertical boundaries, while depth_start and depth_end control the resulting depth range.

fromNDC() converts the adjusted coordinates into 3D space, creating a procedural camera frustum.

Important: Keep the input box's Z values at zero or negative. Positive NDC depth values can place points outside the intended camera depth range and cause clipping or unexpected results. Add a Reverse SOP after the wrangle to correct the inverted polygon winding.

4. Why does a Box become a pyramid-like shape?

When you apply fromNDC() to a Box, Houdini converts each point from NDC into 3D space using the specified camera.

In this setup, the points on the Box's front face have Z = 0. After conversion, they collapse toward the camera position, regardless of their original X and Y values.

The other points, which have different Z values, map to different positions in 3D space. This creates the pyramid-like shape.

The key idea: fromNDC() converts normalized coordinates into camera-dependent 3D positions. The Z value affects the depth of the result, while X and Y determine the projected position. In this particular setup, Z = 0 produces the observed convergence toward the camera.

What Can You Do With It?

Application How fromNDC() helps Example
Camera Frustum Construction Converts NDC positions into 3D positions relative to the camera. Build a camera frustum by converting image-boundary coordinates at chosen depths into 3D points.
Screen-Space Placement Places geometry at specified positions in the camera image, given an appropriate depth. Position points at chosen locations in the frame for camera-dependent setups.