Light Shaders

Light shaders are called from other shaders by sampling a light using the mi_sample_light or mi_trace_light functions, which perform some calculations and then call the given light shader, or directly if a ray hits a source. mi_sample_light may also request that it is called more than once if an area light source is to be sampled. For an example for using mi_sample_light, see the section on material shaders above. mi_trace_light performs less exact shading for area lights, and is provided for backwards compatibility only.

The light shader computes the amount of light contributed by the light source to a previous intersection point, stored in state→point. The calculation may be based on the direction state→dir to that point, and the distance state→dist from the light source to that ray. There may also be shader parameters that specify directional and distance attenuation. Directional lights have no location; state→dist is undefined in this case.

Light shaders are also responsible for shadow casting. Shadows are computed by finding all objects that are in the path of the light from the light source to the illuminated intersection point. This is done in the light shader by casting "shadow rays" after the standard light color computation including attenuation is finished. Shadow rays are cast from the light source back towards the illuminated point (or vice versa if shadow segment mode is enabled), in the same direction of the light ray. Every time an occluding object is found, that object's shadow shader is called, if it has one, which reduces the amount of light based on the object's transparency and color. If an occluding object is found that has no shadow shader, it is assumed to be opaque, so no light from the light source can reach the illuminated point. For details on shadow shaders, see the next section.

Here is an example for a simple point light that supports no attenuation, but casts shadows:

struct mypoint {
    miColor         color;
};

miBoolean mypoint(
    register miColor        *result,
    register miState        *state,
    register struct mypoint *paras)
{
    *result = *mi_eval_color(&paras->color);
    return(mi_trace_shadow(result, state));
}

The shader parameters are assumed to contain the light color. The shadows are calculated simply by giving the shadow shaders of all occluding objects the chance to reduce the light from the light source by calling mi_trace_shadow. The shader returns miTRUE if some light reaches the illuminated point.

There is a useful trick that can improve performance significantly: the light shader should trace shadow rays only if the contribution from the light is greater than some threshold, for example because distance or angle attenuation has left so little of the light color (less than 1/256, for example) that it does not matter whether this contribution is counted or not. If this is the case, the shader might skip shadow calculation (significantly increasing speed in a complex scene) and return (miBoolean)2 to indicate that if this is a small area light source, there is no point in continuing to sample it because all the other samples are not going to make a contribution either. Returning 2 does not work well for large area light sources because some parts of the light may be closer than others and may exceed the threshold for returning early. Consider the following alternate shader body:

{
   *result = *mi_eval_color(&paras->color);
   apply_distance_attenuation(result);
   apply_angle_attenuation(result);
   if (result->r < .005 && result->g < .005
                        && result->b < .005)
       return((miBoolean)2);
   else
       return(mi_trace_shadow(result, state));
}

The point light can be turned into a spot light by adding directional attenuation parameters for the inner and outer cones and a spot direction parameter to the shader parameters, and change the shader to reduce the light intensity if the illuminated point falls between the inner and outer cones, and turns the light off if it does not fall into the outer cone at all:

struct mib_light_spot {
    miColor         color;          /* color of light source */
    miBoolean       shadow;         /* light casts shadows */
    miScalar        factor;         /* makes opaque objects transparent */
    miBoolean       atten;          /* distance attenuation */
    miScalar        start, stop;    /* if atten, distance range */
    miScalar        cone;           /* inner solid cone */
};

DLLEXPORT miBoolean mib_light_spot(
    register miColor        *result,
    register miState        *state,
    register struct mib_light_spot *paras)
{
    register miScalar       d, t, start, stop, cone;
    miScalar                spread;
    miVector                ldir, dir;
    miTag                   ltag;

    *result = *mi_eval_color(&paras->color);
    if (state->type != miRAY_LIGHT)                 /* visible area light*/
            return(miTRUE);
                                                            /*angle atten*/
    ltag = ((miInstance *)mi_db_access(state->light_instance))->item;
    mi_db_unpin(state->light_instance);
    mi_query(miQ_LIGHT_DIRECTION, state, ltag, &ldir);
    mi_vector_to_light(state, &dir, &state->dir);
    mi_vector_normalize(&dir);
    d = mi_vector_dot(&dir, &ldir);
    if (d <= 0)
            return(miFALSE);
    mi_query(miQ_LIGHT_SPREAD, state, ltag, &spread);
    if (d < spread)
            return(miFALSE);
    cone = *mi_eval_scalar(&paras->cone);
    if (d < cone) {
            t = 1 - (d - cone) / (spread - cone);
            result->r *= t;
            result->g *= t;
            result->b *= t;
    }
    if (*mi_eval_scalar(&paras->atten)) {                   /* dist atten*/
            stop = *mi_eval_scalar(&paras->stop);
            if (state->dist >= stop)
                    return(miFALSE);

            start = *mi_eval_scalar(&paras->start);
            if (state->dist > start && fabs(stop - start) > EPS) {
                    t = 1 - (state->dist - start) / (stop - start);
                    result->r *= t;
                    result->g *= t;
                    result->b *= t;
            }
    }
    if (*mi_eval_boolean(&paras->shadow)) {                 /* shadows: */
            d = *mi_eval_scalar(&paras->factor);
            if (d < 1) {
                    miColor filter;
                    filter.r = filter.g = filter.b = filter.a = 1;
                                                            /* opaque */
                    if (!mi_trace_shadow(&filter,state) || BLACK(filter)) {
                            result->r *= d;
                            result->g *= d;
                            result->b *= d;
                            if (d == 0)
                                    return(miFALSE);
                    } else {                                /* transparent*/
                            double omf = 1 - d;
                            result->r *= d + omf * filter.r;
                            result->g *= d + omf * filter.g;
                            result->b *= d + omf * filter.b;
                    }
            }
    }
    return(miTRUE);
}

This shader performs both distance attenuation (it illuminates only between the start and stop parameters if defined) and angle attenuation (full energy up to cone, then a linear falloff to the spread of the light source definition, accessed with mi_query). Outside spread the shader will not even get called (this is why spread is a light definition parameter, not a shader parameter - mental ray needs it reject light sources during sampling). The shader can also handle visible light sources: it returns its light color if the ray type is not equal to miRAY_LIGHT. Finally, the shader casts shadow rays if they are enabled to reduce the returned light if there are occluding objects.

Note that none of these light shaders takes the normal at the illuminated point into account; the light shader is merely responsible for calculating the amount of light that reaches that point. The material shader (or other shader) that sampled the light must use the dot_nd value returned by mi_sample_light, and its own shader parameters such as the diffuse color, to calculate the actual fraction of light reflected by the material. Note also that state→instance is not the instance of the light but of the illuminated point; use state→light_instance instead.

mental ray 3.1 also supports geometric area light sources, which behave like the fixed four area light source types (rectangle, disc, sphere, and cylinder) except that the light shape is defined by an object instance. All points on the surface of the object are emitting light uniformly. For every light sample, mental ray constructs an intersection point with the object, and then calls the light shader. The light shader now has two sets of parameters:

If the light object is uniformly emitting light from all points on its surface, no modifications to the light shader are required to work with geometric area light sources. Note that geometric area light source are supported only for point lights, but not spot or infinite lights.

mental ray supports an additional user-defined area light source, which leaves sample point selection completely to the light shader. The shader is expected to select points on its surface, cast shadow rays at these points if required, and return the illumination from that point. mental ray treats such lights as zero-dimension point lights, without any further information on origin and direction. Instead, the light shader modifies the values state→org and state→dir as necessary, including any dependent values like state→dist and state→dot_nd. The shader is called repeatedly until it returns (miBoolean)2. A call counter is provided in state->count, which is 0 for the first call, and counts up to 65536 (normally a few or a few tens of calls are considered sufficient).

mental ray also supports light profiles, such as IES or Eulumdat light profiles, that are supplied by physical lamp vendors. A light profile can be attached to light objects in the scene as a property, or may be passed directly to light shaders which attenuate the light color (which is not provided by the profile) by the profile's directional attenuation specification. This is done by the mi_lightprofile_sample shader interface function, which requires a profile argument that is taken from a shader parameter:

typedef struct {
    miColor   color;      /* color of light source */
    miTag     profile;    /* tag of ies profile to use */
} photometric_light_t;

DLLEXPORT int photometric_light_version(void) {return(1);}

DLLEXPORT miBoolean photometric_light(
    miColor             *result,
    miState             *state,
    photometric_light_t *paras)   
{
    miTag    lp_tag = *mi_eval_tag(&paras->profile);
    miScalar factor = mi_lightprofile_sample(state, lp_tag, miTRUE);
    factor *= 1.0 / (state->dist * state->dist);

    *result    = *mi_eval_color(&paras->color);
    result->r *= factor;
    result->g *= factor;
    result->b *= factor;

    return(mi_trace_shadow(result, state));
}

This shader performs physically correct inverse-square distance falloff, and also takes care of shadows. Here is a scene example that uses this shader:

declare shader
    color "photometric_light" (
        color        "color",
        lightprofile "profile")
    version 1
end declare

lightprofile "myprof"
    format  ies
    hermite 1
    file    "/usr/local/ies/profile.ies"
end lightprofile

light "mylight"
    "photometric_light" (
        "color"   100 100 100,
        "profile" "myprof")
    ...
end light

The light color is chosen large because of the inverse-square falloff; it depends on the distance from the light to the illuminated point. This scene fragment assumes that an IES light profile file, as provided by the lamp vendor, exists in /usr/local/ies/profile.ies.

Copyright © 1986, 2015 NVIDIA ARC GmbH. All rights reserved.