Rect Light#

A RectLight emits from one side of a rectangle along its local -Z axis. Use it for panel lights, windows, and softboxes. It also uses the common light attributes.

A test scene lit by a rectangular area light

A rect light produces illumination from a rectangular emitting surface.#

USD declaration

Description

float inputs:width = 1

Rectangle width along the local X axis, in scene units.

float inputs:height = 1

Rectangle height along the local Y axis, in scene units.

asset inputs:texture:file

Optional color texture that modulates emission across the rectangle.

The following examples create a 1.2-by-0.6 scene-unit panel light:

def RectLight "Panel"
{
    float inputs:height = 0.6
    float inputs:width = 1.2
}
light = UsdLux.RectLight.Define(stage, "/World/Panel")
light.GetHeightAttr().Set(0.6)
light.GetWidthAttr().Set(1.2)

Projector Lights#

A projector light turns a RectLight into a pinhole emitter. Instead of emitting from the rectangular surface, it projects inputs:texture:file through the light center. Use this for slide projectors, patterned spotlights, and calibrated projection systems.

USD declaration

Description

bool isProjector = false

Enables pinhole projector behavior. This control is supported only on RectLight.

asset inputs:texture:file

Color image to project. Without a texture, the projector emits its light color.

asset projector:directionTexture:file

Optional float lookup texture that maps outgoing directions to UV coordinates in the projected color image. Use it to reshape or calibrate the projection.

The rect light’s width and height define the default pinhole projection. Both panels below use the same transform, dimensions, and texture. The area-light intensity is increased to compensate for its much lower output at this small size since its energy is emitted uniformly over all directions rather than being focused:

Chelsea texture on an ordinary rect area light and projected through a rect projector light

Projector mode emits the image from a pinhole instead of illuminating the scene as an area light. The area-light intensity is increased for visibility. Projected image: Chelsea the cat, photographed by Stefan van der Walt and dedicated to the public domain under CC0 1.0 Universal.#

Authoring a Projector Light#

The following examples enable a rect projector, assign Chelsea as its color texture, and add a direction lookup:

def RectLight "Projector"
{
    bool isProjector = true
    float inputs:width = 1.5
    float inputs:height = 1
    float inputs:intensity = 10000

    # This is the color image being projected.
    asset inputs:texture:file = @../../data/chelsea.png@

    # This optional direction lookup reshapes the projection.
    asset projector:directionTexture:file = @../../data/projector-fisheye-direction.exr@
}
projector = UsdLux.RectLight.Define(stage, "/World/Projector")
projector.GetPrim().CreateAttribute("isProjector", Sdf.ValueTypeNames.Bool).Set(
    True
)
projector.GetWidthAttr().Set(1.5)
projector.GetHeightAttr().Set(1.0)
projector.LightAPI().GetIntensityAttr().Set(10000.0)

# This is the color image being projected.
projector.CreateTextureFileAttr("../../data/chelsea.png")

# This optional direction lookup reshapes the projection.
projector.GetPrim().CreateAttribute(
    "projector:directionTexture:file", Sdf.ValueTypeNames.Asset
).Set("../../data/projector-fisheye-direction.exr")

Projection Direction Texture#

Without projector:directionTexture:file, the projector uses a rectangular pinhole mapping based on inputs:width and inputs:height. A direction texture replaces that mapping.

The direction texture is not a color image or a field of XYZ directions. It is a floating-point lookup indexed by octahedrally encoded outgoing direction. Its red and green channels store the UV coordinates used to sample inputs:texture:file. Only values strictly between 0 and 1 emit light; boundary values and values outside that range mask emission.

The following comparison uses the same Chelsea color texture. The second panel uses a direction texture generated from an example OpenCV pinhole calibration:

Chelsea projected with the default pinhole mapping and an OpenCV-calibrated direction mapping

The example-generated direction texture applies the OpenCV calibration without changing the projected color image.#

Generating A Direction Texture From OpenCV Calibration#

Start with the calibrated image resolution, camera matrix, and distortion coefficients. The matrix uses pixel units. The distortion vector follows OpenCV’s ordering for radial, tangential, thin-prism, and sensor-tilt terms:

IMAGE_SIZE = (3840, 2160)
CAMERA_MATRIX = np.array(
    [
        [2400.0, 0.0, 1900.0],
        [0.0, 2421.0, 1200.0],
        [0.0, 0.0, 1.0],
    ],
    dtype=np.float64,
)
# OpenCV order: k1, k2, p1, p2, k3, k4, k5, k6,
# s1, s2, s3, s4, tau_x, tau_y.
DISTORTION = np.array(
    [
        -0.32734,
        0.07468,
        -0.00046,
        0.00106,
        0.0,
        0.0,
        0.0,
        0.0,
        -0.0008216463,
        0.0,
        -0.01727441,
        0.0,
        -0.03459962,
        0.001508283,
    ],
    dtype=np.float64,
)

The direction texture is indexed by an octahedral encoding of outgoing directions. Generate one normalized direction at each texel center:

texel_centers = (np.arange(texture_size, dtype=np.float64) + 0.5) / texture_size
oct_x, oct_y = np.meshgrid(
    texel_centers * 2.0 - 1.0,
    texel_centers * 2.0 - 1.0,
)
direction_z = 1.0 - np.abs(oct_x) - np.abs(oct_y)

folded_x = (1.0 - np.abs(oct_y)) * np.where(oct_x >= 0.0, 1.0, -1.0)
folded_y = (1.0 - np.abs(oct_x)) * np.where(oct_y >= 0.0, 1.0, -1.0)
direction_x = np.where(direction_z <= 0.0, folded_x, oct_x)
direction_y = np.where(direction_z <= 0.0, folded_y, oct_y)

directions = np.stack((direction_x, direction_y, direction_z), axis=-1)
directions /= np.linalg.norm(directions, axis=-1, keepdims=True)

Project the front-facing directions with cv2.projectPoints() and normalize the resulting pixel coordinates to UV space. Store -1 for directions that OpenCV projects outside the image so the projector masks them:

flat_directions = directions.reshape(-1, 3)
uv = np.full((flat_directions.shape[0], 2), -1.0, dtype=np.float32)

# The renderer flips local -Z before indexing the lookup, so valid decoded
# directions face +Z in the coordinate system passed to OpenCV.
front_facing = flat_directions[:, 2] > 1.0e-8
image_points, _ = cv2.projectPoints(
    flat_directions[front_facing],
    np.zeros(3),
    np.zeros(3),
    camera_matrix,
    distortion,
)
projected_uv = image_points[:, 0, :] / np.asarray(image_size)

# Projector lights emit only for UVs strictly inside (0, 1). Mark every
# direction that projects outside the calibrated image with -1.
inside_image = np.all((projected_uv > 0.0) & (projected_uv < 1.0), axis=1)
front_indices = np.flatnonzero(front_facing)
uv[front_indices[inside_image]] = projected_uv[inside_image]
uv = uv.reshape(directions.shape[:2] + (2,))

Finally, write the UV coordinates as float32 R and G channels in a square OpenEXR image:

height, width, channels = uv.shape
if width != height or channels != 2:
    raise ValueError("direction textures must be square float RG images")

path.parent.mkdir(parents=True, exist_ok=True)
float_channel = Imath.Channel(Imath.PixelType(Imath.PixelType.FLOAT))
header = OpenEXR.Header(width, height)
header["channels"] = {"R": float_channel, "G": float_channel}
header["compression"] = Imath.Compression(Imath.Compression.ZIP_COMPRESSION)

output = OpenEXR.OutputFile(str(path), header)
output.writePixels(
    {
        "R": np.ascontiguousarray(uv[:, :, 0], dtype=np.float32).tobytes(),
        "G": np.ascontiguousarray(uv[:, :, 1], dtype=np.float32).tobytes(),
    }
)
output.close()

Note

The renderer samples the lookup with linear filtering. Values can blend across the boundary between valid UVs and the -1 mask. Use enough lookup resolution for a smooth edge, and add an invalid guard band when you need a hard calibrated aperture boundary.

See Python: Projector Light Calibration for the complete runnable example.