[SESI logo]

Houdini Development Toolkit - Version 6.5

Side Effects Software Inc. 2004

Mantra

Adding a Light Shader

To add a custom light shader, there are two steps to perform:

  1. Write a class that implements an light shader algorithm.

  2. Modify the Houdini interface to include the new shader.


Writing the light shader class

Houdini's rendering library is called the RAY library. Most of the functions and classes will contain this prefix. In the RAY library, all light shaders are sub-classed from the RAY_LightShader class. Each light shader instance has a pointer to a RAY_Light light. This light contains all sorts of information, like the position of the light in space, the color of the light, various maps and other information.

The easiest way to write a new light shader is to take an existing shader and modify it to implement a different algorithm. An example is provided here that can be copied and modified as necessary. The example source computes illumination through a virtual window. The edges of the window are "blurred".

The sample shader takes as parameters the width and height of the window panes, as well as a border width and a fuzz width. of which are optional. The color is specified with the -c red green blue option while the density is given with the -a alpha option.

When compiled, the source file will produce a DSO which should be accessible by Mantra.

The header file RAY_LightCustom.h:

#ifndef __RAY_LightCustom__ #define __RAY_LightCustom__ #include <RAY/RAY_LightShader.h> class RAY_LightWindow : public RAY_LightShader { public: RAY_LightWindow(); virtual ~RAY_LightWindow(); virtual int getIllumination(RAY_ShaderFunc &func, UT_Vector3 &base_light, UT_Vector3 &light_color, UT_Vector3 &light_direction); virtual int hasRayTracedShadows() const; static RAY_LightShader *construct(CMD_Args &args); private: int myUseBlur; float height; // Height of a pane float width; // Width of a pane float border; // Border between panes float fuzz; // Blurry amount around edge }; #endif All fog shaders are subclassed from the RAY_LightShader class. This class provides the services that interface the shader with the rest of the RAY library. Most of the code here is necessary and fills in functions that are required by the RAY_LightShader class.

Note that there is some data in the private part of the class. This is data that is specific to the shader. The window shader has four floats which represent the parameters of the window. Any shader specific data should be placed here.

The key method which needs to be filled out is getIllumination. The method is passed in a shader function which can be used to query the state of the surface being illuminated. There are also some convenience methods which compute shadows for you:

  • getFastShadow() - computes shadows as if every surface was opaque.
  • getTransShadow() - computes shadows using the shadow channel of all intervening surfaces.
  • getFilterShadow() - computes shadows by evaluating the texture on all intervening surfaces.
  • The sample code contains an ifdeffed section of code which performs the fastShadow computation.

    As well the getIllumination method is passed three UT_Vector3 classes. These must be filled out by the shader. The first vector represents the base color of the light (before any shadows have been computed). This may be used by some surface shaders. The second vector represents the illumination which reaches the surface being shaded. The third vector represents the unit vector of the light to the point on the surface being shaded.

    The source file RAY_LightCustom.C:

    void RAYaddLightShader() { RAY_LightEntry *entry; entry = new RAY_LightEntry("window", "Window Shadow", RAY_LightWindow::construct); entry->insertIntoList(); } Mantra recognizes each light shader is stored in a class called RAY_LightEntry. The DSO function RAYaddLightShader() should create a RAY_LightEntry class with the correct name for your shader and then tell the entry to install itself. This lets mantra know your shader exists.

    int RAY_LightWindow::getIllumination(RAY_ShaderFunc &func, UT_Vector3 &base_light, UT_Vector3 &light_color, UT_Vector3 &light_direction) { float distance; float alpha; RAY_Octree *shadow_tree; float blur; float x, y, tmp; float dx, dy; blur = (myUseBlur) ? light->getSpread() / 100 : 0; // Here we call the base class to compute base light. This includes: // - Cone lights // - Attenuation // - Projection maps // - Z-Depth shadows distance = getBaseLight(func, base_light, light_direction); light_color = base_light; // We're actually totally occluded here (i.e. outside a cone), so return if (distance < 0) return 0; // // Now, we test to see the illumination through the window. // // First, we compute our position in X & Y. tmp = dot(light_direction, light->getZAxis()); if (tmp >= 0) return 0; // Behind the window... tmp = -light->getZoom() / tmp; x = fabsf(dot(light->getXAxis(), light_direction)) * tmp; y = fabsf(dot(light->getYAxis(), light_direction)) * tmp; // Now, compute whether we're in our "window"... if (x > width || x < border) return 0; if (y > height || y < border) return 0; if (x > width - fuzz) dx = x - (width - fuzz); else if (x < fuzz+border) dx = border+fuzz - x; else dx = 0; if (y > height - fuzz) dy = y - (height - fuzz); else if (y < fuzz+border) dy = border+fuzz - y; else dy = 0; light_color *= (cosf(dx/fuzz*M_PI)+1) * 0.5F; light_color *= (cosf(dy/fuzz*M_PI)+1) * 0.25; #if 0 // If we want, we could also cast ray traced shadows... // Here, we're inside our panes, so just cast shadows... if (shadow_tree = light->getShadowOctree()) { alpha = func.getFastShadow(shadow_tree, light_direction, distance, blur); light_color *= alpha; if (alpha < 0.001F) return 0; // No illumination } #endif return 1; // We're illuminating } The getIllumination() method is where the actual shading takes place. This function can be broken down into three distinct sections:
    1. Computing the base light. This is done by calling the base class method getBaseLight. The base class method will compute all cone lights, attenuation, and projection maps that the RAY_Light has assigned to it. This is an optional step. However, this method does compute the light direction and illumination for you automatically. In some cases (i.e. where you have your own cone or attenuation function), it may be desirable to skip this stage and initialize the light_direction vector yourself. If the distance returned by getBaseLight is less than 0, it means that the light has no illumination.
    2. Compute masking of illumination due to the window pane algorithm. This is explained below.
    3. The third stage is ifdeffed out in this example. The most expensive computation is usually computing shadows. Since we want our shader to be fast, we don't compute ray traced shadows. If we wanted, we could use the base class method getDepthShadow() which uses the z-depth map attached to the light source to compute soft shadows without ray-tracing.
    If the light contributes no illumination, the return code should be 0 otherwise, the return code should be 1.

    The Window Pane Algorithm

    The window pane computation is really quite simple.

    1. Compute the X/Y position relative to the light. These four lines of code simply project the direction to the surface onto the lights axes. The zoom factor of the light is taken into account, meaning that if the user changes the focal length of the light, the projected image will be altered.
    2. Determine whether we are inside the pane. Since the window is symmetric around the origin, we simply take the absolute value of the X & Y components and determine whether they are in a single quadrant of the window. If they aren't we return 0, indicating that the light has no illumination at this point.
    3. As a final step, we compute the blur on regions near the edge of the window. We do this by computing a delta and then using a half cosine curve to blend smoothly between the black outside and the bright inside. The cosine curves for X & Y are multiplied together. This code could be written much more efficiently than is illustrated here.

    Modifying the Houdini interface

    Light shaders are chosen from within Houdini using a user dialog which is built from a script. The new custom light shader must now be added to this dialog script. The dialog script containing the list of light shaders is found in hfs/houdini/config/Scripts/MANlight.ds. To add the new shader, just copy a simple entry like that for the fastShadow shader, and modify it as required. It may be a good idea to backup the original file before making changes to it.

    The command name should match the shader name used within the RAY_LightEntry defining the shader.

    Here is what the entry for the window shader might look like:

    command { name window label "Window Pane" default "window -w 1 -h 1.5 -b 0.02 -z 0.2" parm { name width label "Pane Width" option -w type float size 1 default { 1 } } parm { name height label "Pane Height" option -h type float default { 1.5 } } parm { name border label "Window Border" option -b type float default { 0.02 } } parm { name fuzz label "Blur Size" option -z type float default { 0.2 } } }
    Table of Contents
    Operators | Surface Operations | Particle Operations | Composite Operators | Channel Operators
    Material & Texture | Objects | Command and Expression | Render Output |
    Mantra Shaders | Utility Classes | Geometry Library | Image Library | Clip Library
    Customizing UI | Questions & Answers

    Copyright © 2004 Side Effects Software Inc.
    477 Richmond Street West, Toronto, Ontario, Canada M5V 3E7