<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>
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>
fromNDC() converts NDC coordinates into 3D positions using a camera.toNDC() converts 3D positions into normalized device coordinates relative to a camera.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>
fromNDC() converts a position expressed in Normalized Device Coordinates (NDC) into a 3D position relative to a specified camera.
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.
// 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.
// 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.
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.
| 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. |