Houdini 18.5 Solaris

Creating a lens shader for Karma

On this page

Overview

Karma is Houdini’s new renderer, designed to supersede Mantra.

A lens shader defines a cvex function run by the renderer to generate the origin and direction of rays sent out by the ray tracer.

In Karma, a lens shader not only allows you to define the direction and origin of the ray, but the color (tint), the ray’s placement in (shutter) time, and the ray’s clipping range.

Houdini includes a pre-made Physical Lens lens shader VOP which has parameters for many lens effects, including but not limited to rolling shutter, chromatic aberrations, and tilt/shift. Advanced users can optionally write a completely custom shader function in VEX.

How to

To...Do this

Set up a Physical Lens node

  1. In a LOP network (such as /stage), set up the camera you want to render from.

  2. Next to the camera, create a Material Network node.

  3. Dive inside the Material Network and create a Physical Lens node.

    Use the parameters on the Physical Lens node to set up effects such as rolling shutter, chromatic aberrations, and tilt/shift.

  4. Go back up and select the camera node. Click the Karma tab.

    • Set the drop-down menu next to Use lens shader to "Set or create", then turn on Use lens shader.

    • Set the drop-down menu next to Lens shader VOP to "Set or create", then click the chooser icon next to the text field and use the chooser window to find and select the Physical Lens node you created.

  5. If you switch the scene viewer to look through the camera using Karma, it should render the scene using the Physical Lens shader you configured.

Create a lens shader node from VEX source code

  1. Write the source code for a cvex shader into a .vfl file (for example, my_lens_shader.vfl). See the examples below.

  2. You can use VEX pragmas in the source code to influence the node interface (such as the node’s human-readable label and parameter looks and ranges) will be generated from the code .

  3. Compile the VEX code into a VOP asset using vcc:

    vcc -O vop -l my_lens_shader.hda my_lens_shader.vfl
    
  4. Put the generated .hda file somewhere in HOUDINIPATH/otls, for example $HOUDINI_USER_PREF_DIR/otls/my_lens_shader.hda, so Houdini will load it at startup.

  5. In Houdini, create an instance of your compiled VOP asset.

  6. In the camera, turn on Use lens shader and set Lens shader VOP to the node path of your lens shader node.

Create a lens shader node from a VOP network

  1. In a Material Network, create a CVEX Shader Builder network.

  2. Inside the CVEX Shader Builder, create a VOP network that defines a lens shader.

    Use Parameter VOPs and/or Bind VOPs to define the input and exported parameters.

  3. Create a digital asset from the CVEX Shader Builder (right-click the CVEX Shader Builder node and choose Create digital asset). Then, in the new asset’s type properties, click the Save tab and turn on Save cached code.

    (It is necessary to put the lens shader in an .hda file, because USD currently does not provide a way to pass a lens shader directly to the renderer, so instead we pass it an encoded string containing a reference to the external .hda file and the argument values.)

  4. In the camera, turn on Use lens shader and set Lens shader VOP to the node path of your lens shader node.

Lens shader arguments

Many of the arguments simply pass the values of various camera parameters to your shader.

int ix

The pixel coordinate on the x axis.

int iy

The pixel coordinate on the y axis.

float focal

The focal length of the camera.

float focus

The focus distance of the camera.

float fstop

The f-stop of the camera (the N in f/N).

float aperture

The aperture width of the camera.

float orthowidth

The orthographic width of the camera.

vector4 viewport

The camera window, as {xmin, xmax, ymin, ymax} in NDC (normalized device coordinates).

int seed

A seed value which is specific to the current pixel.

int sampleindex

The number of times this pixel has been processed (sometimes known as pass index).

vector4 datawindow

The data window as a rectangle (in pixel coordinates, not NDC).

float aspect

The aspect ratio of display window (width divided by height).

int xres

The pixel width of the display window.

int yres

The pixel height of the display window.

int isRHS

Whether you should interpret the values passed to your function given in RHS (right-hand space). If your shader does not take this argument, Houdini always passes values in left-hand space.

This argument is for better compatibility with Karma, which works in RHS, and better portability.

float &Time

Unit time (from 0.0 to 1.0) specifying what point in the shutter time the ray was sent.

Outputting a different value than what was passed in is how you achieve rolling shutter and other kinds of motion blur effect.

vector2 &clippingrange

The values passed to your function represent the near and far clipping distances (taken from the camera to which the lens shader is assigned).

Your function can change these values, to change the near/far limits for the ray (the minimum and maximum distances the ray can travel).

With a perspective transform, you should adjust the clipping distances by the Z of the ray direction to ensure consistent distances. For other projections, the interpretation of the clipping range may be different.

vector &P

Set this to the origin of the sent ray in 3D.

vector &I

Set this to the direction of the ray sent ray as a vector.

vector &tint

Set this to the color/contribution of the sent ray.

vector2 &jitter

Set this to any relative jitter (horizontal and vertical) you applied to the pixel. You must output this variable if generating your own x and y (see below).

Backwards compatibility arguments

The following arguments are supported for backwards compatibility with existing Mantra lens shader functions. If you are writing a lens shader from scratch for Karma, you should avoid using these arguments and only use the arguments documented above instead.

In Karma, the possible area of dofx and dofy, the area of the "aperture" (really, the circle of confusion) is based on the camera’s focal length and f-stop.

(The Houdini camera has aperture height and aperture width parameters but this is not a physical camera’s aperture. In many ways it represents the actual sensor dimensions, or you can think of it as a screen of the given dimensions, that you are firing the pixels through, at the focal length away.)

float x

The x coordinate of the current pixel (jittered), fit between the NDC values of the camera’s viewport.

float y

The y coordinate of the current pixel (jittered), fit between the NDC values of the camera’s viewport.

float dofx

The x coordinate of the DOF offset, sampled from a disk the size of the aperture (centered at 0, 0).

float dofy

The y coordinate of the DOF offset, sampled from a disk the size of the aperture (centered at 0, 0).

int &valid

After the function is run, set this to 1 or 0 to indicate whether the ray was valid or not. Setting this to 0 is the same as setting the contribution to {0, 0, 0}

Examples

Simple projection example

A simple lens shader that does a projection mapping with depth of field.

This example uses are Mantra-style arguments. In a Mantra shader, zoom and focus are arguments passed by the user to the shader. In Karma, if focus is not specified, Karma will pass the focus distance from the camera.

cvex simplelens (
    // Inputs
    float x = 0;
    float y = 0;
    float Time = 0;
    float dofx = 0;
    float dofy = 0;
    float aspect = 1;

    // Outputs
    export vector P = 0;
    export vector I = 0;

    // Shader arguments
    float zoom = 1;
    float focus = 1;
) {
    // Set direction vector
    I = set(x * aspect / zoom, y / zoom, 1.0);
    // Set position
    P = set(dofx, dofy, 0);
    // Focus direction to focus distance away
    // no matter where P is, a pixel's ray will hit
    // the focus plane in the same place everytime
    I *= focus;
    I -= P;
}

This example does not use focal length and aperture width to compute the field of view and zoom, instead it uses an explicit zoom parameter.

Standard projection

Since we can have the camera’s information passed to the shader, we can build a projection that matches Karma’s standard projection.

cvex simplelens (
    // Inputs
    float x = 0;
    float y = 0;
    float Time = 0;
    float dofx = 0;
    float dofy = 0;
    float aspect = 1;
    // Camera
    float focus = 1;
    float focal = 1;
    float fstop = 0;
    float aperture = 1; // note this matches with the horizontalAperture (in meters)

    // Outputs
    export vector P = 0;
    export vector I = 0;
) {
    // Set direction vector using the camera's properties
    I = set( x * aperture * 0.5 * aspect / focal,
         y * aperture * 0.5 / focal,
         1.0 );
    // Set position
    P = set(dofx, dofy, 0);
    // Focus direction to focus distance away
    // no matter where P is, a pixel's ray will hit
    // the focus plane in the same place everytime
    I *= focus;
    I -= P;
}

This lens shader should match the regular perspective camera perfectly.

Square bokeh

If you don’t like boring old circular bokeh, you might want a square shaped bokeh.

However, the dofx and dofy aren’t easily mapped to a square, so we will generate them ourselves. We have Karma pass us the pixel’s seed and sampleindex, then use random_brj to get a uniformly distributed sequence based on them.

cvex simplelens (
    // Inputs
    float x = 0;
    float y = 0;
    float Time = 0;
    // Camera
    float focus = 1;
    float focal = 1;
    float fstop = 0;
    float aperture = 1; // note this matches with the horizontalAperture (in meters)
    float aspect = 1;
    // Random Sequencing
    int seed = 0;
    int sampleindex = 0;

    // Outputs
    export vector P = 0;
    export vector I = 0;
) {
    // Set direction vector using the camera's properties
    I = set( x * aperture * 0.5 * aspect / focal,
         y * aperture * 0.5 / focal,
         1.0 );

    // Get a random dof point in a 0-1 unit box
    vector2 dof = random_brj(seed, sampleindex);
    float diameter = focal / fstop; // get aperture / CoC diameter
    dof = (dof - 0.5) * diameter; // center the unit box and set diameter
    P = set(dof.x, dof.y, 0);

    // Focus direction to focus distance away
    // no matter where P is, a pixel's ray will hit
    // the focus plane in the same place everytime
    I *= focus;
    I -= P;
}

This is an example of using random_brj to generate uniformly distributed sequences in your lens shader, however there is one thing to remember: if you have multiple random numbers to generate, you don’t want to use the same seed for every random number, but you do want to use a pixel-unique seed as your basis for the sequence. In the example above we simply XOR the pixel seed with a constant seed for each random number we want to generate.

Generating X and Y

Below is an example where we generate the x and y completely in the lens shader using /solaris/random_brj.

// simple random seeds for generating jitter and dof
#define JITTER_SEED     0x98A208B1
#define DOF_SEED        0xA8B2440D

cvex simplelens (
    // Inputs
    int ix = 0;
    int iy = 0;
    // Camera
    float focus = 1;
    float focal = 1;
    float fstop = 0;
    float aperture = 1;
    float aspect = 1;
    // Random Sequencing
    int seed = 0;
    int sampleindex = 0;
    // Screen Info
    vector4 datawindow = 0;
    vector4 viewport = { -1, 1, -1, 1 };
    int xres = 0;
    int yres = 0;
    int isRHS = 0;

    // Outputs
    export vector P = 0;
    export vector I = 0;
    export vector2 jitter = 0; // note this must be returned if generating x and y
) {
    // Get pixel in reference to the data window
    int rx = ix + int(datawindow[0]);
    int ry = iy + int(datawindow[2]);
    // Must return jitter
    jitter = random_brj(seed ^ JITTER_SEED, sampleindex);
    float x = float(rx) + jitter.x;
    float y = float(ry) + jitter.y;

    // Map to NDC
    x = efit(x, 0, float(xres), viewport[0], viewport[1]);
    y = efit(y, 0, float(yres), viewport[2], viewport[3]);

    // Set direction vector using the camera's properties
    I = set( x * aperture * 0.5 * aspect / focal,
         y * aperture * 0.5 / focal,
         -1.0 ); // note RHS is assumed

    // Get a random dof point in a 0-1 unit box
    vector2 dof = random_brj(seed, sampleindex);
    float diameter = focal / fstop; // get aperture / CoC diameter
    dof = (dof - 0.5) * diameter; // center the unit box and set diameter
    P = set(dof.x, dof.y, 0);

    // Focus direction to focus distance away
    // no matter where P is, a pixel's ray will hit
    // the focus plane in the same place everytime
    I *= focus;
    I -= P;

    if (!isRHS)
    {
        P.z = -P.z;
        I.z = -I.z;
    }
}
  • This example uses the new Karma arguments, such as the data window, viewport, X and Y resolution, and isRHS.

  • These arguments let you map a pixel position to the NDC X and Y.

  • In this example we assume right-hand space (RHS) and negate the Z coordinates if isRHS is false.

  • You must output jitter if you do not use the x and y parameters. Not outputting jitter in that case will result in undefined behavior.

Tinting

Unlike Mantra’s lens shaders, Karma lens shaders can tint the rays, allowing you to create effects such as chromatic aberrations or anaglyphic 3D .

This example makes the camera behave like an anaglyph camera. Typically you would do this by creating two cameras, separated about an eye’s span apart (approximagtely 65mm), color one red, and the other cyan, and overlay their images.

This lens shader can achieve this in a single camera. It sends half the rays out from the left eye position with a tint of cyan, and the other half from the right eye position with a tint of red. (There is a trick to getting this to work, which is the shader needs to focus the rays to actually get the 3D depth of field.)

You can find demo files for this example in $HFS/houdini/help/files/anaglyph.hip.gz and $HFS/houdini/help/files/anaglyphlens.hda.

// random seeds to be XOR'd with the given seed
#define JITTER_SEED         0x98A208B1
#define COINTOSS_SEED       0xA8B2440D

cvex
anaglyphlens(
    // Inputs
    int ix = 0;
    int iy = 0;
    // Camera
    float focus = 1;
    float focal = 1;
    float fstop = 0;
    float aperture = 1;
    float aspect = 1;
    // Random Sequencing
    int seed = 0;
    int sampleindex = 0;
    // Screen Info
    vector4 datawindow = 0;
    vector4 viewport = { -1, 1, -1, 1 };
    int xres = 0;
    int yres = 0;
    int isRHS = 0;

    // Outputs
    export vector P = 0;
    export vector I = 0;
    export vector tint = 1;
    export vector2 jitter = 0; // note this must be returned if generating x and y

    // Shader arguments
    float dist = 0.065;
    vector rcolor = { 1, 0, 0 };
    int enablecustomleft = 0;
    vector lcolor = { 0, 1, 1 };
    )
{
    // Get pixel in reference to the data window
    int rx = ix + int(datawindow[0]);
    int ry = iy + int(datawindow[2]);
    // Must return jitter
    jitter = random_brj(seed ^ JITTER_SEED, sampleindex);
    float x = float(rx) + jitter.x;
    float y = float(ry) + jitter.y;

    // Map to NDC
    x = efit(x, 0, float(xres), viewport[0], viewport[1]);
    y = efit(y, 0, float(yres), viewport[2], viewport[3]);

    // Set direction vector using the camera's properties
    I = set( x * aperture * 0.5 * aspect / focal,
         y * aperture * 0.5 / focal,
         -1.0 ); // note RHS is assumed

    vector use_lcolor = lcolor;
    if (!enablecustomleft)
        use_lcolor = 1 - rcolor; // get compliment of right eye color

    float cointoss = random_brj(seed ^ COINTOSS_SEED, sampleindex);
    cointoss = fit01(cointoss, 0, 2);
    if (cointoss < 1)
    {
        tint = rcolor;
        P.x = 0.5 * dist;
    }
    else if (cointoss < 2)
    {
        tint = use_lcolor;
        P.x = -0.5 * dist;
    }

    // Same focusing as before...
    I *= focus;
    I -= P;

    if (!isRHS)
    {
        P.z = -P.z;
        I.z = -I.z;
    }
}

Rolling shutter

cvex
rollingshutter(
    // Inputs
    int ix = 0;
    int iy = 0;
    // Camera
    float focus = 1;
    float focal = 1;
    float fstop = 0;
    float aperture = 1;
    float aspect = 1;
    // Random Sequencing
    int seed = 0;
    int sampleindex = 0;
    // Screen Info
    vector4 datawindow = 0;
    vector4 viewport = { -1, 1, -1, 1 };
    int xres = 0;
    int yres = 0;
    int isRHS = 0;

    // Exported Input
    export float Time = 0;

    // Outputs
    export vector P = 0;
    export vector I = 0;
    export vector2 jitter = 0; // note this must be returned if generating x and y

    // Shader arguments
    float blurriness = 0.05; // amount of blurriness (0-1)
) {
    // Get pixel in reference to the data window
    int rx = ix + int(datawindow[0]);
    int ry = iy + int(datawindow[1]);
    // Must return jitter
    jitter = random_brj(seed ^ JITTER_SEED, sampleindex);
    float x = float(rx) + jitter.x;
    float y = float(ry) + jitter.y;

    // Map to NDC
    x = efit(x, 0, float(xres), viewport[0], viewport[1]);
    y = efit(y, 0, float(yres), viewport[2], viewport[3]);

    // Set direction vector using the camera's properties
    I = set( x * aperture * 0.5 * aspect / focal,
         y * aperture * 0.5 / focal,
         -1.0 ); // note RHS is assumed

    // Implement rolling shutter:
    // Map the ray's time position based on the ray's position in the y axis
    float time_offset = fit(y, viewport[2], viewport[3], 0, 1);
    // Time is random number between 0 and 1, by limiting it we limit the blurriness
    // to a smaller portion of the shutter length
    Time = Time * blurriness + time_offset;

    // Same focusing as before...
    I *= focus;
    I -= P;

    if (!isRHS)
    {
        P.z = -P.z;
        I.z = -I.z;
    }
}

Solaris

USD

Tutorials

Karma renderer